zh_commentsAPP注释 #26

Merged
pfcmahxf5 merged 1 commits from zh_branch into master 3 months ago

@ -0,0 +1,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (Program Files)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (Program Files)" project-jdk-type="Python SDK" />
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/src.iml" filepath="$PROJECT_DIR$/.idea/src.iml" />
</modules>
</component>
</project>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.12 (Program Files)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

@ -0,0 +1,2 @@
#zh:
#codingutf-8

@ -0,0 +1,66 @@
#zh:
#codingutf-8
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# 注册模型的地方
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
"""自定义用户创建表单"""
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) # 密码字段
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) # 确认密码字段
class Meta:
model = BlogUser # 指定关联的模型
fields = ('email',) # 表单中包含的字段只包含email
def clean_password2(self):
"""验证两次输入的密码是否一致"""
password1 = self.cleaned_data.get("password1") # 获取第一次输入的密码
password2 = self.cleaned_data.get("password2") # 获取第二次输入的密码
if password1 and password2 and password1 != password2: # 如果两次密码不一致
raise forms.ValidationError(_("passwords do not match")) # 抛出验证错误
return password2 # 返回确认的密码
def save(self, commit=True):
"""保存用户,对密码进行哈希处理"""
user = super().save(commit=False) # 先不提交到数据库
user.set_password(self.cleaned_data["password1"]) # 设置哈希后的密码
if commit: # 如果需要提交
user.source = 'adminsite' # 设置用户来源为管理员站点
user.save() # 保存用户到数据库
return user # 返回用户对象
class BlogUserChangeForm(UserChangeForm):
"""自定义用户信息修改表单"""
class Meta:
model = BlogUser # 指定关联的模型
fields = '__all__' # 包含所有字段
field_classes = {'username': UsernameField} # 指定用户名字段类型
def __init__(self, *args, **kwargs):
"""初始化表单"""
super().__init__(*args, **kwargs) # 调用父类初始化方法
class BlogUserAdmin(UserAdmin):
"""自定义用户管理后台配置"""
form = BlogUserChangeForm # 使用自定义的用户修改表单
add_form = BlogUserCreationForm # 使用自定义的用户创建表单
list_display = (
'id', # 显示ID
'nickname', # 显示昵称
'username', # 显示用户名
'email', # 显示邮箱
'last_login', # 显示最后登录时间
'date_joined', # 显示注册日期
'source' # 显示用户来源
)
list_display_links = ('id', 'username') # 设置可点击的字段链接
ordering = ('-id',) # 按ID倒序排列

@ -0,0 +1,18 @@
#zh:
#codingutf-8
from django.apps import AppConfig # 导入Django应用配置基类
class AccountsConfig(AppConfig):
"""账户应用的配置类"""
# 指定应用的Python路径Django 3.x及以下版本使用
# 在Django 4.x中name字段被替换为使用应用标签
name = 'accounts'
# 在Django 4.x中可以添加以下字段
# default_auto_field = 'django.db.models.BigAutoField' # 默认主键类型
# verbose_name = '用户账户' # 人类可读的应用名称(中文)
# def ready(self):
# # 导入信号处理器等初始化代码
# import accounts.signals

@ -0,0 +1,141 @@
#zh:
#codingutf-8
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser
class LoginForm(AuthenticationForm):
"""用户登录表单"""
def __init__(self, *args, **kwargs):
"""初始化表单设置字段的widget属性"""
super(LoginForm, self).__init__(*args, **kwargs)
# 设置用户名字段的输入框样式和占位符
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 设置密码字段的输入框样式和占位符
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
"""用户注册表单"""
def __init__(self, *args, **kwargs):
"""初始化表单设置所有字段的widget属性"""
super(RegisterForm, self).__init__(*args, **kwargs)
# 设置用户名字段的输入框样式和占位符
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 设置邮箱字段的输入框样式和占位符
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
# 设置密码字段的输入框样式和占位符
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# 设置确认密码字段的输入框样式和占位符
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
"""验证邮箱是否已存在"""
email = self.cleaned_data['email']
# 检查邮箱是否已经被注册
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
"""表单元数据配置"""
model = get_user_model() # 获取当前用户模型
fields = ("username", "email") # 表单包含的字段
class ForgetPasswordForm(forms.Form):
"""忘记密码重置表单"""
new_password1 = forms.CharField(
label=_("New password"), # 新密码标签
widget=forms.PasswordInput( # 密码输入框
attrs={
"class": "form-control", # CSS类名
'placeholder': _("New password") # 占位符文本
}
),
)
new_password2 = forms.CharField(
label="确认密码", # 确认密码标签
widget=forms.PasswordInput( # 密码输入框
attrs={
"class": "form-control", # CSS类名
'placeholder': _("Confirm password") # 占位符文本
}
),
)
email = forms.EmailField(
label='邮箱', # 邮箱标签
widget=forms.TextInput( # 文本输入框
attrs={
'class': 'form-control', # CSS类名
'placeholder': _("Email") # 占位符文本
}
),
)
code = forms.CharField(
label=_('Code'), # 验证码标签
widget=forms.TextInput( # 文本输入框
attrs={
'class': 'form-control', # CSS类名
'placeholder': _("Code") # 占位符文本
}
),
)
def clean_new_password2(self):
"""验证两次输入的新密码是否一致"""
password1 = self.data.get("new_password1") # 获取第一次输入的密码
password2 = self.data.get("new_password2") # 获取第二次输入的密码
if password1 and password2 and password1 != password2: # 检查密码是否一致
raise ValidationError(_("passwords do not match")) # 密码不一致报错
password_validation.validate_password(password2) # 验证密码强度
return password2 # 返回确认的密码
def clean_email(self):
"""验证邮箱是否存在"""
user_email = self.cleaned_data.get("email") # 获取邮箱
# 检查邮箱是否在系统中注册
if not BlogUser.objects.filter(email=user_email).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist")) # 邮箱不存在报错
return user_email # 返回邮箱
def clean_code(self):
"""验证验证码是否正确"""
code = self.cleaned_data.get("code") # 获取验证码
# 调用utils模块验证验证码
error = utils.verify(
email=self.cleaned_data.get("email"), # 传入邮箱
code=code, # 传入验证码
)
if error: # 如果有错误信息
raise ValidationError(error) # 抛出验证错误
return code # 返回验证码
class ForgetPasswordCodeForm(forms.Form):
"""获取忘记密码验证码表单"""
email = forms.EmailField(
label=_('Email'), # 邮箱标签
)

@ -0,0 +1,50 @@
#zh:
#codingutf-8
import django.contrib.auth.models # 导入Django认证系统的模型类用于用户管理
import django.contrib.auth.validators # 导入Django认证系统的验证器用于用户输入验证
from django.db import migrations, models # 从Django数据库模块导入迁移工具和模型基类
import django.utils.timezone # 导入Django的时区工具用于处理时间相关操作
class Migration(migrations.Migration): # 定义迁移类继承自Django的迁移基类用于数据库结构变更
initial = True # 标记当前迁移为初始迁移,即该应用的第一个数据库迁移文件
dependencies = [ # 定义迁移依赖关系,确保迁移执行顺序正确
('auth', '0012_alter_user_first_name_max_length'), # 依赖于auth应用的特定迁移版本
]
operations = [ # 定义当前迁移需要执行的数据库操作列表
migrations.CreateModel( # 创建新数据模型的迁移操作
name='BlogUser', # 要创建的模型名称为BlogUser
fields=[ # 定义模型包含的字段列表
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # 自增主键字段,自动创建,作为唯一标识
('password', models.CharField(max_length=128, verbose_name='password')), # 密码字段最大长度128字符
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), # 最后登录时间字段,可为空,允许空白
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), # 是否为超级用户默认False拥有所有权限
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), # 用户名字段唯一最长150字符使用Unicode验证器
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), # 名可为空最长150字符
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), # 姓可为空最长150字符
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), # 邮箱字段,自动验证格式,可为空
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), # 是否为管理员默认False决定能否登录admin后台
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), # 账号是否激活默认True用于软删除
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), # 注册时间,默认当前时区时间
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), # 自定义昵称字段可为空最长100字符
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # 记录用户创建时间,默认当前时区时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # 记录用户信息最后修改时间,默认当前时区时间
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), # 记录用户账号的创建来源可为空最长100字符
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), # 与用户组的多对多关系关联auth应用的Group模型
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), # 与权限的多对多关系关联auth应用的Permission模型
],
options={ # 模型的元数据配置
'verbose_name': '用户', # 模型的单数显示名称
'verbose_name_plural': '用户', # 模型的复数显示名称
'ordering': ['-id'], # 默认排序方式按id降序排列最新用户在前
'get_latest_by': 'id', # 指定通过id字段获取最新记录
},
managers=[ # 模型的管理器配置
('objects', django.contrib.auth.models.UserManager()), # 使用Django内置的UserManager作为模型管理器提供用户管理功能
],
),
]

@ -0,0 +1,46 @@
#zh:
#codingutf-8
from django.db import migrations, models # 导入Django数据库迁移工具和模型字段类
import django.utils.timezone # 导入Django时区工具用于处理时间相关操作
class Migration(migrations.Migration): # 定义迁移类,处理数据库结构变更
dependencies = [ # 定义当前迁移依赖的其他迁移文件
('accounts', '0001_initial'), # 依赖于accounts应用的0001_initial迁移
]
operations = [ # 定义当前迁移需要执行的数据库操作列表
migrations.AlterModelOptions( # 修改模型的元选项配置
name='bloguser', # 要修改的模型名称为BlogUser
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, # 更新元选项,将显示名称改为英文
),
migrations.RemoveField( # 移除模型中的字段
model_name='bloguser', # 目标模型为BlogUser
name='created_time', # 要移除的字段名为created_time
),
migrations.RemoveField( # 移除模型中的另一个字段
model_name='bloguser', # 目标模型为BlogUser
name='last_mod_time', # 要移除的字段名为last_mod_time
),
migrations.AddField( # 向模型添加新字段
model_name='bloguser', # 目标模型为BlogUser
name='creation_time', # 新字段名称为creation_time
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # 字段类型为DateTimeField默认值为当前时间显示名为"creation time"
),
migrations.AddField( # 向模型添加另一个新字段
model_name='bloguser', # 目标模型为BlogUser
name='last_modify_time', # 新字段名称为last_modify_time
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # 字段类型为DateTimeField默认值为当前时间显示名为"last modify time"
),
migrations.AlterField( # 修改模型中已有字段的配置
model_name='bloguser', # 目标模型为BlogUser
name='nickname', # 要修改的字段名为nickname
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), # 更新字段显示名为"nick name",其他属性保持不变
),
migrations.AlterField( # 修改模型中另一个已有字段的配置
model_name='bloguser', # 目标模型为BlogUser
name='source', # 要修改的字段名为source
field=models.CharField(blank=True, max_length=100, verbose_name='create source'), # 更新字段显示名为"create source",其他属性保持不变
),
]

@ -0,0 +1,2 @@
#zh:
#codingutf-8

@ -0,0 +1,50 @@
#zh:
#codingutf-8
from django.contrib.auth.models import AbstractUser # 导入Django内置的抽象用户基类
from django.db import models # 导入Django的模型模块
from django.urls import reverse # 用于生成URL反向解析
from django.utils.timezone import now # 获取当前时间
from django.utils.translation import gettext_lazy as _ # 国际化翻译
from django.utils import get_current_site # 获取当前站点信息
# 在此处创建模型
class BlogUser(AbstractUser):
"""自定义博客用户模型继承自Django的AbstractUser"""
# 昵称字段最大长度100字符允许为空
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# 创建时间字段,默认值为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间字段,默认值为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 用户来源字段,记录用户创建来源(如网站注册、管理员创建等)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
"""获取用户的绝对URL用于生成作者详情页链接"""
return reverse(
'blog:author_detail', kwargs={ # 反向解析URL
'author_name': self.username}) # 使用用户名作为URL参数
def __str__(self):
"""对象的字符串表示,返回邮箱地址"""
return self.email
def get_full_url(self):
"""获取完整的用户URL包含域名"""
site = get_current_site().domain # 获取当前站点的域名
url = "https://{site}{path}".format(site=site, # 格式化完整URL
path=self.get_absolute_url())
return url
class Meta:
"""模型的元数据配置"""
ordering = ['-id'] # 默认按ID降序排列
verbose_name = _('user') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称(与单数相同)
get_latest_by = 'id' # 指定获取最新记录的字段

@ -0,0 +1,2 @@
#zh:
#codingutf-8

@ -0,0 +1,245 @@
#zh:
#codingutf-8
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# 在此处创建测试
class AccountTest(TestCase):
"""账户功能测试类"""
def setUp(self):
"""测试初始化方法,在每个测试方法执行前运行"""
self.client = Client() # 创建测试客户端
self.factory = RequestFactory() # 创建请求工厂
# 创建测试用户
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--=" # 新密码用于测试
def test_validate_account(self):
"""测试账户验证功能"""
site = get_current_site().domain # 获取当前站点域名
# 创建超级用户
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')
self.assertEqual(loginresult, True) # 断言登录成功
# 测试访问管理员页面
response = self.client.get('/admin/')
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' # 文章类型
article.status = 'p' # 发布状态
article.save()
# 测试访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200) # 断言可以访问文章管理页面
def test_validate_register(self):
"""测试用户注册功能"""
# 验证注册前用户不存在
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 发送注册请求
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',
})
# 验证注册后用户存在
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)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
# 测试验证页面访问
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# 测试用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 提升用户权限
user.is_superuser = True
user.is_staff = True
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()
# 测试文章管理页面访问
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 测试用户登出
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200]) # 登出后重定向
# 测试登出后访问文章管理页面(应该被重定向)
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])
# 测试错误登录后访问文章管理页面
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) # 发送验证邮件
# 测试正确验证码验证
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None) # 断言验证成功,无错误
# 测试错误邮箱验证
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) # 断言返回错误信息
def test_forget_password_email_code_success(self):
"""测试成功发送忘记密码验证码"""
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200) # 断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") # 断言返回成功信息
def test_forget_password_email_code_fail(self):
"""测试发送忘记密码验证码失败情况"""
# 测试空邮箱
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试无效邮箱格式
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
)
self.assertEqual(resp.status_code, 302) # 断言重定向(成功)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None) # 断言用户存在
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
)
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
)
self.assertEqual(resp.status_code, 200) # 断言停留在当前页面(失败)

@ -0,0 +1,42 @@
#zh:
#codingutf-8
from django.urls import path # 导入路径路由
from django.urls import re_path # 导入正则表达式路由
from . import views # 导入当前应用的视图模块
from .forms import LoginForm # 导入自定义登录表单
app_name = "accounts" # 定义应用命名空间用于URL反向解析
urlpatterns = [
# 用户登录路由
re_path(r'^login/$', # 匹配以/login/结尾的URL
views.LoginView.as_view(success_url='/'), # 使用类视图,登录成功后跳转到首页
name='login', # URL名称用于反向解析
kwargs={'authentication_form': LoginForm}), # 传递自定义登录表单
# 用户注册路由
re_path(r'^register/$', # 匹配以/register/结尾的URL
views.RegisterView.as_view(success_url="/"), # 使用类视图,注册成功后跳转到首页
name='register'), # URL名称用于反向解析
# 用户登出路由
re_path(r'^logout/$', # 匹配以/logout/结尾的URL
views.LogoutView.as_view(), # 使用类视图处理登出
name='logout'), # URL名称用于反向解析
# 账户操作结果页面路由
path(r'account/result.html', # 精确匹配/account/result.html路径
views.account_result, # 使用函数视图
name='result'), # URL名称用于反向解析
# 忘记密码页面路由(表单提交)
re_path(r'^forget_password/$', # 匹配以/forget_password/结尾的URL
views.ForgetPasswordView.as_view(), # 使用类视图处理忘记密码逻辑
name='forget_password'), # URL名称用于反向解析
# 获取忘记密码验证码路由
re_path(r'^forget_password_code/$', # 匹配以/forget_password_code/结尾的URL
views.ForgetPasswordEmailCode.as_view(), # 使用类视图发送验证码
name='forget_password_code'), # URL名称用于反向解析
]

@ -0,0 +1,59 @@
#zh:
#codingutf-8
from django.contrib.auth import get_user_model # 导入获取用户模型的方法
from django.contrib.auth.backends import ModelBackend # 导入Django认证后端基类
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义认证后端允许使用用户名或邮箱登录
扩展了Django的默认认证系统支持更灵活的登录方式
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
用户认证方法
重写父类方法支持用户名和邮箱两种登录方式
Args:
request: HTTP请求对象
username: 用户输入的用户名或邮箱
password: 用户输入的密码
**kwargs: 其他参数
Returns:
User: 认证成功的用户对象
None: 认证失败
"""
# 判断输入的是邮箱还是用户名
if '@' in username:
kwargs = {'email': username} # 如果包含@符号,按邮箱处理
else:
kwargs = {'username': username} # 否则按用户名处理
try:
# 根据用户名或邮箱查找用户
user = get_user_model().objects.get(**kwargs)
# 验证密码是否正确
if user.check_password(password):
return user # 认证成功,返回用户对象
except get_user_model().DoesNotExist:
# 用户不存在返回None表示认证失败
return None
def get_user(self, user_id):
"""
根据用户ID获取用户对象
用于会话管理保持用户登录状态
Args:
user_id: 用户ID
Returns:
User: 用户对象
None: 用户不存在
"""
try:
return get_user_model().objects.get(pk=user_id) # 根据主键查找用户
except get_user_model().DoesNotExist:
return None # 用户不存在

@ -0,0 +1,76 @@
#zh:
#codingutf-8
import typing # 导入类型提示模块
from datetime import timedelta # 导入时间间隔模块
from django.core.cache import cache # 导入Django缓存框架
from django.utils.translation import gettext # 导入翻译函数
from django.utils.translation import gettext_lazy as _ # 导入惰性翻译
from 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"
"""
# 构建邮件HTML内容包含验证码信息
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
# 调用邮件发送函数发送邮件
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""
验证邮箱验证码是否正确
Args:
email: 邮箱地址
code: 用户输入的验证码
Returns:
str: 如果验证失败返回错误信息验证成功返回None
Note:
这里的错误处理不太合理应该采用raise抛出异常
否则调用方也需要对error进行处理
"""
cache_code = get_code(email) # 从缓存中获取该邮箱对应的验证码
if cache_code != code: # 比较缓存中的验证码和用户输入的验证码
return gettext("Verification code error") # 验证码错误,返回错误信息
# 验证成功返回None
def set_code(email: str, code: str):
"""
将验证码存储到缓存中
Args:
email: 邮箱地址作为缓存的key
code: 验证码作为缓存的value
"""
# 使用邮箱作为key验证码作为value设置过期时间为5分钟
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""
从缓存中获取验证码
Args:
email: 邮箱地址缓存的key
Returns:
str: 如果存在返回验证码不存在返回None
"""
return cache.get(email) # 从缓存中获取指定邮箱的验证码

@ -0,0 +1,249 @@
#zh:
#codingutf-8
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
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
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
# 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
# 在此处创建视图
class RegisterView(FormView):
"""用户注册视图"""
form_class = RegisterForm # 使用自定义注册表单
template_name = 'account/registration_form.html' # 注册模板路径
@method_decorator(csrf_protect) # CSRF保护装饰器
def dispatch(self, *args, **kwargs):
"""请求分发方法"""
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
"""表单验证通过后的处理"""
if form.is_valid():
# 保存用户但不提交到数据库
user = form.save(False)
user.is_active = False # 设置用户为未激活状态
user.source = 'Register' # 记录用户来源
user.save(True) # 保存用户到数据库
# 获取当前站点信息
site = get_current_site().domain
# 生成邮箱验证签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 调试模式下使用本地地址
if settings.DEBUG:
site = '127.0.0.1:8000'
# 构建验证URL
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# 构建邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# 发送验证邮件
send_email(
emailto=[user.email],
title='验证您的电子邮箱',
content=content)
# 重定向到结果页面
url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
# 表单无效,重新渲染表单页
return self.render_to_response({'form': form})
class LogoutView(RedirectView):
"""用户登出视图"""
url = '/login/' # 登出后重定向的URL
@method_decorator(never_cache) # 禁止缓存装饰器
def dispatch(self, request, *args, **kwargs):
"""请求分发方法"""
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""处理GET请求"""
logout(request) # 执行登出操作
delete_sidebar_cache() # 删除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
"""用户登录视图"""
form_class = LoginForm # 使用自定义登录表单
template_name = 'account/login.html' # 登录模板路径
success_url = '/' # 登录成功默认重定向URL
redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名
login_ttl = 2626560 # 会话有效期:一个月(以秒为单位)
# 方法装饰器保护敏感数据、CSRF防护、禁止缓存
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""请求分发方法"""
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""获取模板上下文数据"""
# 获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/' # 默认重定向到首页
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
"""表单验证通过后的处理"""
# 使用Django内置的认证表单
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
# 清除侧边栏缓存
delete_sidebar_cache()
logger.info(self.redirect_field_name)
# 执行登录操作
auth.login(self.request, form.get_user())
# 处理"记住我"功能
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl) # 设置会话有效期
return super(LoginView, self).form_valid(form)
else:
# 表单无效,重新渲染表单页
return self.render_to_response({'form': form})
def get_success_url(self):
"""获取登录成功后的重定向URL"""
redirect_to = self.request.POST.get(self.redirect_field_name)
# 验证URL安全性
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[self.request.get_host()]):
redirect_to = self.success_url # 不安全的URL使用默认URL
return redirect_to
def account_result(request):
"""账户操作结果页面视图函数"""
type = request.GET.get('type') # 操作类型
id = request.GET.get('id') # 用户ID
# 获取用户对象不存在则返回404
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
# 如果用户已激活,重定向到首页
if user.is_active:
return HttpResponseRedirect('/')
# 处理注册和验证两种类型
if type and type in ['register', 'validation']:
if type == 'register':
# 注册成功页面内容
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# 邮箱验证处理
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
# 验证签名
if sign != c_sign:
return HttpResponseForbidden() # 签名不匹配,拒绝访问
# 激活用户
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
# 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
# 无效类型,重定向到首页
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
"""忘记密码重置视图"""
form_class = ForgetPasswordForm # 使用忘记密码表单
template_name = 'account/forget_password.html' # 模板路径
def form_valid(self, form):
"""表单验证通过后的处理"""
if form.is_valid():
# 获取用户并重置密码
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# 对密码进行哈希处理
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save() # 保存用户
return HttpResponseRedirect('/login/') # 重定向到登录页
else:
# 表单无效,重新渲染表单页
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
"""忘记密码验证码发送视图"""
def post(self, request: HttpRequest):
"""处理POST请求发送验证码"""
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱") # 表单验证失败
to_email = form.cleaned_data["email"]
# 生成并发送验证码
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok") # 返回成功响应
Loading…
Cancel
Save