From 1bd2f46548060ce6eced2ecafe25829989e04fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=BD=E5=91=A8=E6=98=93?= <2745525394@qq.com> Date: Sun, 9 Nov 2025 22:54:35 +0800 Subject: [PATCH] =?UTF-8?q?accounts=E9=83=A8=E5=88=86=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E6=BA=90=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DjangoBlog/accounts/admin.py | 17 ++- src/DjangoBlog/accounts/apps.py | 1 + src/DjangoBlog/accounts/forms.py | 23 +++- .../accounts/migrations/0001_initial.py | 31 +++++- ...s_remove_bloguser_created_time_and_more.py | 90 +++++++++------ src/DjangoBlog/accounts/models.py | 18 ++- src/DjangoBlog/accounts/tests.py | 104 +++++++++++++++--- src/DjangoBlog/accounts/urls.py | 23 ++-- src/DjangoBlog/accounts/user_login_backend.py | 8 +- src/DjangoBlog/accounts/utils.py | 38 +++---- src/DjangoBlog/accounts/views.py | 30 +++-- 11 files changed, 280 insertions(+), 103 deletions(-) diff --git a/src/DjangoBlog/accounts/admin.py b/src/DjangoBlog/accounts/admin.py index 32e483c..59e6124 100644 --- a/src/DjangoBlog/accounts/admin.py +++ b/src/DjangoBlog/accounts/admin.py @@ -9,15 +9,19 @@ from .models import BlogUser class BlogUserCreationForm(forms.ModelForm): + #lht: 创建用户表单,用于在Django管理后台创建新用户 + #lht: 密码输入字段 password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) + #lht: 确认密码输入字段 password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) class Meta: + #lht: 指定关联的模型和字段 model = BlogUser fields = ('email',) def clean_password2(self): - # Check that the two password entries match + #lht: 验证两次密码输入是否一致 password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: @@ -25,28 +29,35 @@ class BlogUserCreationForm(forms.ModelForm): return password2 def save(self, commit=True): - # Save the provided password in hashed format + #lht: 保存用户并加密密码 user = super().save(commit=False) user.set_password(self.cleaned_data["password1"]) if commit: + #lht: 设置用户来源为管理后台 user.source = 'adminsite' user.save() return user class BlogUserChangeForm(UserChangeForm): + #lht: 修改用户表单,用于在Django管理后台编辑现有用户 class Meta: + #lht: 指定关联的模型、字段和字段类 model = BlogUser fields = '__all__' field_classes = {'username': UsernameField} def __init__(self, *args, **kwargs): + #lht: 初始化表单 super().__init__(*args, **kwargs) class BlogUserAdmin(UserAdmin): + #lht: 用户管理界面配置,自定义Django管理后台的用户管理界面 + #lht: 指定修改用户和创建用户使用的表单 form = BlogUserChangeForm add_form = BlogUserCreationForm + #lht: 定义在列表页面显示的字段 list_display = ( 'id', 'nickname', @@ -55,5 +66,7 @@ class BlogUserAdmin(UserAdmin): 'last_login', 'date_joined', 'source') + #lht: 定义在列表页面中可点击跳转到编辑页面的字段 list_display_links = ('id', 'username') + #lht: 定义默认排序方式 ordering = ('-id',) diff --git a/src/DjangoBlog/accounts/apps.py b/src/DjangoBlog/accounts/apps.py index 9b3fc5a..7750a08 100644 --- a/src/DjangoBlog/accounts/apps.py +++ b/src/DjangoBlog/accounts/apps.py @@ -2,4 +2,5 @@ from django.apps import AppConfig class AccountsConfig(AppConfig): + #lht:指定应用的名称,Django会根据这个名称找到对应的应用目录 name = 'accounts' diff --git a/src/DjangoBlog/accounts/forms.py b/src/DjangoBlog/accounts/forms.py index fce4137..4900b6d 100644 --- a/src/DjangoBlog/accounts/forms.py +++ b/src/DjangoBlog/accounts/forms.py @@ -9,8 +9,11 @@ from .models import BlogUser class LoginForm(AuthenticationForm): + #lht: 登录表单,继承Django内置认证表单 def __init__(self, *args, **kwargs): + #lht: 调用父类构造函数 super(LoginForm, self).__init__(*args, **kwargs) + #lht: 自定义用户名和密码字段的显示样式 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) self.fields['password'].widget = widgets.PasswordInput( @@ -18,9 +21,11 @@ class LoginForm(AuthenticationForm): class RegisterForm(UserCreationForm): + #lht: 用户注册表单,继承Django内置用户创建表单 def __init__(self, *args, **kwargs): + #lht: 调用父类构造函数 super(RegisterForm, self).__init__(*args, **kwargs) - + #lht: 自定义各字段的显示样式 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) self.fields['email'].widget = widgets.EmailInput( @@ -31,17 +36,21 @@ class RegisterForm(UserCreationForm): attrs={'placeholder': "repeat password", "class": "form-control"}) def clean_email(self): + #lht: 验证邮箱唯一性 email = self.cleaned_data['email'] if get_user_model().objects.filter(email=email).exists(): raise ValidationError(_("email already exists")) return email class Meta: + #lht: 指定模型和字段 model = get_user_model() fields = ("username", "email") class ForgetPasswordForm(forms.Form): + #lht: 忘记密码表单 + #lht: 新密码字段 new_password1 = forms.CharField( label=_("New password"), widget=forms.PasswordInput( @@ -52,6 +61,7 @@ class ForgetPasswordForm(forms.Form): ), ) + #lht: 确认新密码字段 new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -62,6 +72,7 @@ class ForgetPasswordForm(forms.Form): ), ) + #lht: 邮箱字段 email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -72,6 +83,7 @@ class ForgetPasswordForm(forms.Form): ), ) + #lht: 验证码字段 code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -83,25 +95,30 @@ class ForgetPasswordForm(forms.Form): ) def clean_new_password2(self): + #lht: 验证两次输入的密码是否一致 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")) + #lht: 验证密码强度 password_validation.validate_password(password2) return password2 def clean_email(self): + #lht: 验证邮箱是否存在 user_email = self.cleaned_data.get("email") if not BlogUser.objects.filter( email=user_email ).exists(): - # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + #lht: todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 raise ValidationError(_("email does not exist")) return user_email def clean_code(self): + #lht: 验证验证码是否正确 code = self.cleaned_data.get("code") + #lht: 调用工具函数验证验证码 error = utils.verify( email=self.cleaned_data.get("email"), code=code, @@ -112,6 +129,8 @@ class ForgetPasswordForm(forms.Form): class ForgetPasswordCodeForm(forms.Form): + #lht: 忘记密码时获取验证码的表单 + #lht: 邮箱字段 email = forms.EmailField( label=_('Email'), ) diff --git a/src/DjangoBlog/accounts/migrations/0001_initial.py b/src/DjangoBlog/accounts/migrations/0001_initial.py index d2fbcab..8bcfcba 100644 --- a/src/DjangoBlog/accounts/migrations/0001_initial.py +++ b/src/DjangoBlog/accounts/migrations/0001_initial.py @@ -7,41 +7,62 @@ import django.utils.timezone class Migration(migrations.Migration): - + #lht: 标记这是一个初始迁移文件 initial = True + #lht: 定义依赖关系,该迁移依赖于auth应用的0012_alter_user_first_name_max_length迁移 dependencies = [ ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ + #lht: 创建BlogUser模型的操作 migrations.CreateModel( name='BlogUser', fields=[ + #lht: 主键字段,自动创建的BigAutoField ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + #lht: 密码字段,存储加密后的密码 ('password', models.CharField(max_length=128, verbose_name='password')), + #lht: 上次登录时间字段 ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + #lht: 超级用户状态字段,拥有所有权限 ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + #lht: 用户名字段,具有唯一性约束和验证器 ('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')), + #lht: 名字字段 ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + #lht: 姓氏字段 ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + #lht: 邮箱地址字段 ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + #lht: 员工状态字段,决定是否可以登录管理站点 ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + #lht: 活跃状态字段,决定用户账户是否有效 ('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')), + #lht: 加入日期字段 ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + #lht: 昵称字段,博客用户的额外信息 ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), + #lht: 创建时间字段 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + #lht: 最后修改时间字段 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + #lht: 创建来源字段,标记用户通过何种方式创建 ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), + #lht: 用户组关联字段,多对多关系 ('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')), + #lht: 用户权限字段,多对多关系 ('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')), ], + #lht: 模型选项配置 options={ - 'verbose_name': '用户', - 'verbose_name_plural': '用户', - 'ordering': ['-id'], - 'get_latest_by': 'id', + 'verbose_name': '用户', #lht: 单数名称 + 'verbose_name_plural': '用户', #lht: 复数名称 + 'ordering': ['-id'], #lht: 默认排序方式,按ID降序 + 'get_latest_by': 'id', #lht: 获取最新记录的依据字段 }, + #lht: 模型管理器 managers=[ ('objects', django.contrib.auth.models.UserManager()), ], diff --git a/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py index 1a9f509..8bcfcba 100644 --- a/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py +++ b/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py @@ -1,46 +1,70 @@ -# Generated by Django 4.2.5 on 2023-09-06 13:13 +# Generated by Django 4.1.7 on 2023-03-02 07:14 +import django.contrib.auth.models +import django.contrib.auth.validators from django.db import migrations, models import django.utils.timezone class Migration(migrations.Migration): + #lht: 标记这是一个初始迁移文件 + initial = True + #lht: 定义依赖关系,该迁移依赖于auth应用的0012_alter_user_first_name_max_length迁移 dependencies = [ - ('accounts', '0001_initial'), + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ - migrations.AlterModelOptions( - name='bloguser', - options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, - ), - migrations.RemoveField( - model_name='bloguser', - name='created_time', - ), - migrations.RemoveField( - model_name='bloguser', - name='last_mod_time', - ), - migrations.AddField( - model_name='bloguser', - name='creation_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), - ), - migrations.AddField( - model_name='bloguser', - name='last_modify_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), - ), - migrations.AlterField( - model_name='bloguser', - name='nickname', - field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), - ), - migrations.AlterField( - model_name='bloguser', - name='source', - field=models.CharField(blank=True, max_length=100, verbose_name='create source'), + #lht: 创建BlogUser模型的操作 + migrations.CreateModel( + name='BlogUser', + fields=[ + #lht: 主键字段,自动创建的BigAutoField + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + #lht: 密码字段,存储加密后的密码 + ('password', models.CharField(max_length=128, verbose_name='password')), + #lht: 上次登录时间字段 + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + #lht: 超级用户状态字段,拥有所有权限 + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + #lht: 用户名字段,具有唯一性约束和验证器 + ('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')), + #lht: 名字字段 + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + #lht: 姓氏字段 + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + #lht: 邮箱地址字段 + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + #lht: 员工状态字段,决定是否可以登录管理站点 + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + #lht: 活跃状态字段,决定用户账户是否有效 + ('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')), + #lht: 加入日期字段 + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + #lht: 昵称字段,博客用户的额外信息 + ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), + #lht: 创建时间字段 + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + #lht: 最后修改时间字段 + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + #lht: 创建来源字段,标记用户通过何种方式创建 + ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), + #lht: 用户组关联字段,多对多关系 + ('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')), + #lht: 用户权限字段,多对多关系 + ('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')), + ], + #lht: 模型选项配置 + options={ + 'verbose_name': '用户', #lht: 单数名称 + 'verbose_name_plural': '用户', #lht: 复数名称 + 'ordering': ['-id'], #lht: 默认排序方式,按ID降序 + 'get_latest_by': 'id', #lht: 获取最新记录的依据字段 + }, + #lht: 模型管理器 + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], ), ] diff --git a/src/DjangoBlog/accounts/models.py b/src/DjangoBlog/accounts/models.py index 3baddbb..00f2896 100644 --- a/src/DjangoBlog/accounts/models.py +++ b/src/DjangoBlog/accounts/models.py @@ -6,30 +6,38 @@ from django.utils.translation import gettext_lazy as _ from djangoblog.utils import get_current_site -# Create your models here. +#lht: Create your models here. class BlogUser(AbstractUser): + #lht: 用户昵称字段 nickname = models.CharField(_('nick name'), max_length=100, blank=True) + #lht: 用户创建时间 creation_time = models.DateTimeField(_('creation time'), default=now) + #lht: 用户最后修改时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now) + #lht: 用户来源标识(如通过注册、后台创建等) source = models.CharField(_('create source'), max_length=100, blank=True) def get_absolute_url(self): + #lht: 返回用户个人页面的URL return reverse( 'blog:author_detail', kwargs={ 'author_name': self.username}) def __str__(self): + #lht: 字符串表示,返回用户邮箱 return self.email def get_full_url(self): + #lht: 获取用户页面的完整URL site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url class Meta: - ordering = ['-id'] - verbose_name = _('user') - verbose_name_plural = verbose_name - get_latest_by = 'id' + #lht: 模型元数据配置 + ordering = ['-id'] #lht: 默认按ID倒序排列 + verbose_name = _('user') #lht: 单数名称 + verbose_name_plural = verbose_name #lht: 复数名称 + get_latest_by = 'id' #lht: 获取最新记录的字段 diff --git a/src/DjangoBlog/accounts/tests.py b/src/DjangoBlog/accounts/tests.py index 6893411..9d8ea97 100644 --- a/src/DjangoBlog/accounts/tests.py +++ b/src/DjangoBlog/accounts/tests.py @@ -9,34 +9,51 @@ from djangoblog.utils import * from . import utils -# Create your tests here. +#lht: Create your tests here. class AccountTest(TestCase): + #lht: """ + #lht: 账户功能测试类 + #lht: 继承Django的TestCase,用于测试账户相关的各种功能 + #lht: """ + def setUp(self): - self.client = Client() - self.factory = RequestFactory() + #lht: """ + #lht: 测试前的准备工作 + #lht: 每个测试方法执行前都会调用此方法 + #lht: """ + self.client = Client() #lht: 创建测试客户端,用于模拟HTTP请求 + self.factory = RequestFactory() #lht: 创建请求工厂,用于创建请求对象 + #lht: 创建一个测试用户,用于后续的测试 self.blog_user = BlogUser.objects.create_user( username="test", email="admin@admin.com", password="12345678" ) - self.new_test = "xxx123--=" + self.new_test = "xxx123--=" #lht: 设置测试用的新密码 def test_validate_account(self): + #lht: """ + #lht: 测试账户验证功能 + #lht: 包括超级用户创建、登录验证、管理员权限等 + #lht: """ site = get_current_site().domain + #lht: 创建超级用户用于测试 user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="qwer!@#$ggg") testuser = BlogUser.objects.get(username='liangliangyy1') + #lht: 测试用户登录功能 loginresult = self.client.login( username='liangliangyy1', password='qwer!@#$ggg') - self.assertEqual(loginresult, True) - response = self.client.get('/admin/') - self.assertEqual(response.status_code, 200) + self.assertEqual(loginresult, True) #lht: 验证登录成功 + response = self.client.get('/admin/') #lht: 访问管理后台 + self.assertEqual(response.status_code, 200) #lht: 验证访问成功 + #lht: 创建分类和文章用于测试 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() @@ -52,24 +69,36 @@ class AccountTest(TestCase): article.status = 'p' article.save() + #lht: 测试能否正常访问文章管理页面 response = self.client.get(article.get_admin_url()) self.assertEqual(response.status_code, 200) def test_validate_register(self): + #lht: """ + #lht: 测试用户注册流程 + #lht: 包括注册、邮箱验证、登录、权限设置等完整流程 + #lht: """ + #lht: 验证目标邮箱尚未注册 self.assertEquals( 0, len( BlogUser.objects.filter( email='user123@user.com'))) + + #lht: 模拟用户注册请求 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', }) + + #lht: 验证用户已成功创建 self.assertEquals( 1, len( BlogUser.objects.filter( email='user123@user.com'))) + + #lht: 获取新创建的用户并验证邮箱链接 user = BlogUser.objects.filter(email='user123@user.com')[0] sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) path = reverse('accounts:result') @@ -78,12 +107,17 @@ class AccountTest(TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) + #lht: 使用新用户登录 self.client.login(username='user1233', password='password123!q@wE#R$T') user = BlogUser.objects.filter(email='user123@user.com')[0] + + #lht: 设置用户为超级用户和员工,以便访问管理功能 user.is_superuser = True user.is_staff = True user.save() delete_sidebar_cache() + + #lht: 创建分类和文章 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() @@ -100,52 +134,71 @@ class AccountTest(TestCase): article.status = 'p' article.save() + #lht: 验证能够访问文章管理页面 response = self.client.get(article.get_admin_url()) self.assertEqual(response.status_code, 200) + #lht: 测试用户登出功能 response = self.client.get(reverse('account:logout')) self.assertIn(response.status_code, [301, 302, 200]) + #lht: 登出后应无法访问管理页面 response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) + #lht: 测试使用错误密码登录 response = self.client.post(reverse('account:login'), { 'username': 'user1233', 'password': 'password123' }) self.assertIn(response.status_code, [301, 302, 200]) + #lht: 登录失败后仍无法访问管理页面 response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) def test_verify_email_code(self): + #lht: """ + #lht: 测试邮箱验证码验证功能 + #lht: """ to_email = "admin@admin.com" - code = generate_code() - utils.set_code(to_email, code) - utils.send_verify_email(to_email, code) + code = generate_code() #lht: 生成验证码 + utils.set_code(to_email, code) #lht: 设置验证码 + utils.send_verify_email(to_email, code) #lht: 发送验证码(模拟) + #lht: 验证正确的验证码能通过验证 err = utils.verify("admin@admin.com", code) self.assertEqual(err, None) + #lht: 验证错误的验证码不能通过验证 err = utils.verify("admin@123.com", code) self.assertEqual(type(err), str) def test_forget_password_email_code_success(self): + #lht: """ + #lht: 测试忘记密码时成功获取验证码 + #lht: """ resp = self.client.post( path=reverse("account:forget_password_code"), data=dict(email="admin@admin.com") ) + #lht: 验证请求成功且返回"ok" self.assertEqual(resp.status_code, 200) self.assertEqual(resp.content.decode("utf-8"), "ok") def test_forget_password_email_code_fail(self): + #lht: """ + #lht: 测试忘记密码时获取验证码失败的情况 + #lht: """ + #lht: 测试没有提供邮箱的情况 resp = self.client.post( path=reverse("account:forget_password_code"), data=dict() ) self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + #lht: 测试提供无效邮箱的情况 resp = self.client.post( path=reverse("account:forget_password_code"), data=dict(email="admin@com") @@ -153,32 +206,42 @@ class AccountTest(TestCase): self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") def test_forget_password_email_success(self): + #lht: """ + #lht: 测试成功重置密码的完整流程 + #lht: """ code = generate_code() - utils.set_code(self.blog_user.email, code) + utils.set_code(self.blog_user.email, code) #lht: 设置验证码 + + #lht: 准备重置密码的数据 data = dict( new_password1=self.new_test, new_password2=self.new_test, email=self.blog_user.email, code=code, ) + + #lht: 发送重置密码请求 resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 302) + self.assertEqual(resp.status_code, 302) #lht: 重定向表示成功 - # 验证用户密码是否修改成功 + #lht: 验证用户密码是否修改成功 blog_user = BlogUser.objects.filter( email=self.blog_user.email, - ).first() # type: BlogUser + ).first() #lht: 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): + #lht: """ + #lht: 测试为不存在的用户重置密码的情况 + #lht: """ data = dict( new_password1=self.new_test, new_password2=self.new_test, - email="123@123.com", + email="123@123.com", #lht: 不存在的邮箱 code="123456", ) resp = self.client.post( @@ -186,22 +249,27 @@ class AccountTest(TestCase): data=data ) + #lht: 应该返回200状态码而不是重定向,因为验证失败 self.assertEqual(resp.status_code, 200) - def test_forget_password_email_code_error(self): + #lht: """ + #lht: 测试使用错误验证码重置密码的情况 + #lht: """ code = generate_code() utils.set_code(self.blog_user.email, code) + + #lht: 使用错误的验证码 data = dict( new_password1=self.new_test, new_password2=self.new_test, email=self.blog_user.email, - code="111111", + code="111111", #lht: 错误的验证码 ) resp = self.client.post( path=reverse("account:forget_password"), data=data ) + #lht: 应该返回200状态码而不是重定向,因为验证失败 self.assertEqual(resp.status_code, 200) - diff --git a/src/DjangoBlog/accounts/urls.py b/src/DjangoBlog/accounts/urls.py index 107a801..abd6127 100644 --- a/src/DjangoBlog/accounts/urls.py +++ b/src/DjangoBlog/accounts/urls.py @@ -4,25 +4,32 @@ from django.urls import re_path from . import views from .forms import LoginForm -app_name = "accounts" +app_name = "accounts" #lht: 应用命名空间 -urlpatterns = [re_path(r'^login/$', +urlpatterns = [ + #lht: 登录URL + re_path(r'^login/$', views.LoginView.as_view(success_url='/'), name='login', kwargs={'authentication_form': LoginForm}), - re_path(r'^register/$', + #lht: 注册URL + re_path(r'^register/$', views.RegisterView.as_view(success_url="/"), name='register'), - re_path(r'^logout/$', + #lht: 登出URL + re_path(r'^logout/$', views.LogoutView.as_view(), name='logout'), - path(r'account/result.html', + #lht: 账户操作结果页面 + path(r'account/result.html', views.account_result, name='result'), - re_path(r'^forget_password/$', + #lht: 忘记密码页面 + re_path(r'^forget_password/$', views.ForgetPasswordView.as_view(), name='forget_password'), - re_path(r'^forget_password_code/$', + #lht: 获取忘记密码验证码 + re_path(r'^forget_password_code/$', views.ForgetPasswordEmailCode.as_view(), name='forget_password_code'), - ] +] diff --git a/src/DjangoBlog/accounts/user_login_backend.py b/src/DjangoBlog/accounts/user_login_backend.py index 73cdca1..3656921 100644 --- a/src/DjangoBlog/accounts/user_login_backend.py +++ b/src/DjangoBlog/accounts/user_login_backend.py @@ -3,11 +3,12 @@ from django.contrib.auth.backends import ModelBackend class EmailOrUsernameModelBackend(ModelBackend): - """ - 允许使用用户名或邮箱登录 - """ + #lht: """ + #lht: 允许使用用户名或邮箱登录 + #lht: """ def authenticate(self, request, username=None, password=None, **kwargs): + #lht: 根据输入内容判断是邮箱还是用户名 if '@' in username: kwargs = {'email': username} else: @@ -20,6 +21,7 @@ class EmailOrUsernameModelBackend(ModelBackend): return None def get_user(self, username): + #lht: 根据用户名获取用户对象 try: return get_user_model().objects.get(pk=username) except get_user_model().DoesNotExist: diff --git a/src/DjangoBlog/accounts/utils.py b/src/DjangoBlog/accounts/utils.py index 4b94bdf..04fb6ba 100644 --- a/src/DjangoBlog/accounts/utils.py +++ b/src/DjangoBlog/accounts/utils.py @@ -7,16 +7,16 @@ from django.utils.translation import gettext_lazy as _ from djangoblog.utils import send_email -_code_ttl = timedelta(minutes=5) +_code_ttl = timedelta(minutes=5) #lht: 验证码有效期5分钟 def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): - """发送重设密码验证码 - Args: - to_mail: 接受邮箱 - subject: 邮件主题 - code: 验证码 - """ + #lht: """发送重设密码验证码 + #lht: Args: + #lht: to_mail: 接受邮箱 + #lht: subject: 邮件主题 + #lht: code: 验证码 + #lht: """ html_content = _( "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " "properly") % {'code': code} @@ -24,26 +24,26 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")) def verify(email: str, code: str) -> typing.Optional[str]: - """验证code是否有效 - Args: - email: 请求邮箱 - code: 验证码 - Return: - 如果有错误就返回错误str - Node: - 这里的错误处理不太合理,应该采用raise抛出 - 否测调用方也需要对error进行处理 - """ + #lht: """验证code是否有效 + #lht: Args: + #lht: email: 请求邮箱 + #lht: code: 验证码 + #lht: Return: + #lht: 如果有错误就返回错误str + #lht: Node: + #lht: 这里的错误处理不太合理,应该采用raise抛出 + #lht: 否测调用方也需要对error进行处理 + #lht: """ cache_code = get_code(email) if cache_code != code: return gettext("Verification code error") def set_code(email: str, code: str): - """设置code""" + #lht: """设置code""" cache.set(email, code, _code_ttl.seconds) def get_code(email: str) -> typing.Optional[str]: - """获取code""" + #lht: """获取code""" return cache.get(email) diff --git a/src/DjangoBlog/accounts/views.py b/src/DjangoBlog/accounts/views.py index ae67aec..0b2bf5f 100644 --- a/src/DjangoBlog/accounts/views.py +++ b/src/DjangoBlog/accounts/views.py @@ -29,21 +29,24 @@ from .models import BlogUser logger = logging.getLogger(__name__) -# Create your views here. +#lht: Create your views here. class RegisterView(FormView): + #lht: 用户注册视图 form_class = RegisterForm template_name = 'account/registration_form.html' @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): + #lht: 处理请求分发,添加CSRF保护装饰器 return super(RegisterView, self).dispatch(*args, **kwargs) def form_valid(self, form): + #lht: 表单验证成功时的处理逻辑 if form.is_valid(): user = form.save(False) - user.is_active = False - user.source = 'Register' + user.is_active = False #lht: 新注册用户默认不激活 + user.source = 'Register' #lht: 标记来源为注册 user.save(True) site = get_current_site().domain sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) @@ -54,6 +57,7 @@ class RegisterView(FormView): url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( site=site, path=path, id=user.id, sign=sign) + #lht: 构造验证邮件内容 content = """

请点击下面链接验证您的邮箱

@@ -81,33 +85,38 @@ class RegisterView(FormView): class LogoutView(RedirectView): + #lht: 用户登出视图 url = '/login/' @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): + #lht: 处理请求分发,添加不缓存装饰器 return super(LogoutView, self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): + #lht: 处理GET请求,执行登出操作 logout(request) - delete_sidebar_cache() + delete_sidebar_cache() #lht: 清除侧边栏缓存 return super(LogoutView, self).get(request, *args, **kwargs) class LoginView(FormView): + #lht: 用户登录视图 form_class = LoginForm template_name = 'account/login.html' success_url = '/' redirect_field_name = REDIRECT_FIELD_NAME - login_ttl = 2626560 # 一个月的时间 + login_ttl = 2626560 #lht: 登录会话保持时间(一个月) @method_decorator(sensitive_post_parameters('password')) @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): - + #lht: 处理请求分发,添加敏感参数保护、CSRF保护和不缓存装饰器 return super(LoginView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): + #lht: 获取重定向URL redirect_to = self.request.GET.get(self.redirect_field_name) if redirect_to is None: redirect_to = '/' @@ -116,6 +125,7 @@ class LoginView(FormView): return super(LoginView, self).get_context_data(**kwargs) def form_valid(self, form): + #lht: 表单验证成功时的处理逻辑 form = AuthenticationForm(data=self.request.POST, request=self.request) if form.is_valid(): @@ -126,14 +136,13 @@ class LoginView(FormView): if self.request.POST.get("remember"): self.request.session.set_expiry(self.login_ttl) return super(LoginView, self).form_valid(form) - # return HttpResponseRedirect('/') else: return self.render_to_response({ 'form': form }) def get_success_url(self): - + #lht: 获取登录成功后的跳转URL redirect_to = self.request.POST.get(self.redirect_field_name) if not url_has_allowed_host_and_scheme( url=redirect_to, allowed_hosts=[ @@ -143,6 +152,7 @@ class LoginView(FormView): def account_result(request): + #lht: 账户操作结果页面 type = request.GET.get('type') id = request.GET.get('id') @@ -176,10 +186,12 @@ def account_result(request): class ForgetPasswordView(FormView): + #lht: 忘记密码视图 form_class = ForgetPasswordForm template_name = 'account/forget_password.html' def form_valid(self, form): + #lht: 表单验证成功时的处理逻辑 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"]) @@ -190,8 +202,10 @@ class ForgetPasswordView(FormView): class ForgetPasswordEmailCode(View): + #lht: 发送忘记密码验证码视图 def post(self, request: HttpRequest): + #lht: 处理POST请求,发送验证码邮件 form = ForgetPasswordCodeForm(request.POST) if not form.is_valid(): return HttpResponse("错误的邮箱")