diff --git a/doc/E-R图.jpg b/doc/E-R图.jpg new file mode 100644 index 00000000..6208bbb8 Binary files /dev/null and b/doc/E-R图.jpg differ diff --git a/doc/熊欢2315304115软件界面设计说明书.docx b/doc/熊欢2315304115软件界面设计说明书.docx new file mode 100644 index 00000000..c316192d Binary files /dev/null and b/doc/熊欢2315304115软件界面设计说明书.docx differ diff --git a/doc/第六周 熊欢.docx b/doc/第六周 熊欢.docx new file mode 100644 index 00000000..96135e7b Binary files /dev/null and b/doc/第六周 熊欢.docx differ diff --git a/src/DjangoBlog/accounts/admin.py b/src/DjangoBlog/accounts/admin.py index 61e9d2e6..f81d3674 100644 --- a/src/DjangoBlog/accounts/admin.py +++ b/src/DjangoBlog/accounts/admin.py @@ -1,6 +1,7 @@ <<<<<<< HEAD <<<<<<< HEAD from django import forms +<<<<<<< HEAD from django.contrib.auth.admin import UserAdmin # 导入Django默认的用户管理Admin类(基础模板) from django.contrib.auth.forms import UserChangeForm # 导入默认的用户编辑表单(用于继承修改) from django.contrib.auth.forms import UsernameField# 用户名字段的专用类(自带验证逻辑) @@ -15,6 +16,16 @@ from django.utils.translation import gettext_lazy as _# 导入国际化翻译工 # Register your models here. from .models import BlogUser # 导入自定义的用户模型(替代Django默认User) +======= +from django.contrib.auth.admin import UserAdmin # xh:导入Django默认的用户管理Admin类(基础模板) +from django.contrib.auth.forms import UserChangeForm # xh:导入默认的用户编辑表单(用于继承修改) +from django.contrib.auth.forms import UsernameField # xh:用户名字段的专用类(自带验证逻辑) +from django.utils.translation import gettext_lazy as _ # xh:国际化支持,文本可翻译 + +# Register your models here. +from .models import BlogUser # xh:导入自定义的用户模型(替代Django默认User) + +>>>>>>> xh_branch # 自定义用户创建表单(用于在后台添加新用户) class BlogUserCreationForm(forms.ModelForm): @@ -22,12 +33,21 @@ class BlogUserCreationForm(forms.ModelForm): password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)# 确认密码字段(再次输入密码) class Meta: +<<<<<<< HEAD model = BlogUser # 绑定自定义的BlogUser模型 fields = ('email',) # 新增用户时,默认显示的核心字段(仅邮箱,其他字段可后续编辑) # 密码验证逻辑:检查两次输入的密码是否一致 def clean_password2(self): # Check that the two password entries match password1 = self.cleaned_data.get("password1")# 获取第一个密码框的清洗后数据 +======= + model = BlogUser # xh:绑定自定义的BlogUser模型 + fields = ('email',) # xh:新增用户时,默认显示的核心字段(仅邮箱,其他字段可后续编辑) + + def clean_password2(self): + # xh:密码验证逻辑:检查两次输入的密码是否一致 + password1 = self.cleaned_data.get("password1")# xh:获取第一个密码框的清洗后数据 +>>>>>>> xh_branch password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: # 如果密码不一致,抛出验证错误 @@ -40,6 +60,7 @@ from django.contrib.auth.forms import UserChangeForm# 从 django 内置的 auth from django.contrib.auth.forms import UsernameField# 从 django 内置的 auth 表单模块导入 UsernameField,用于用户名字段的处理 from django.utils.translation import gettext_lazy as _# 从 django 翻译工具模块导入 gettext_lazy 并别名成 _,用于实现国际化翻译(延迟翻译) +<<<<<<< HEAD # Register your models here. from .models import BlogUser# 从当前目录的 models 模块导入自定义的 BlogUser 模型 @@ -64,6 +85,10 @@ class BlogUserCreationForm(forms.ModelForm):# 定义 BlogUserCreationForm 类, def save(self, commit=True): # 定义 save 方法,用于保存用户数据,commit 参数控制是否立即提交到数据库 # Save the provided password in hashed format <<<<<<< HEAD +======= + def save(self, commit=True): + #xh:将提供的密码以哈希格式保存 +>>>>>>> xh_branch user = super().save(commit=False) # 使用 Django 内置方法加密密码 user.set_password(self.cleaned_data["password1"]) diff --git a/src/DjangoBlog/accounts/apps.py b/src/DjangoBlog/accounts/apps.py index 9b3fc5a4..066c2f91 100644 --- a/src/DjangoBlog/accounts/apps.py +++ b/src/DjangoBlog/accounts/apps.py @@ -1,5 +1,8 @@ -from django.apps import AppConfig +from django.apps import AppConfig # xh:导入Django的应用配置基类 class AccountsConfig(AppConfig): + #xh:Accounts应用的配置类,继承自Django的AppConfig基类,用于配置accounts应用的各种设置 + # xh:应用名称,Django使用这个名称来识别应用 + # xh:必须与应用的目录名保持一致 name = 'accounts' diff --git a/src/DjangoBlog/accounts/forms.py b/src/DjangoBlog/accounts/forms.py index fce4137e..6ff67050 100644 --- a/src/DjangoBlog/accounts/forms.py +++ b/src/DjangoBlog/accounts/forms.py @@ -9,62 +9,91 @@ from .models import BlogUser class LoginForm(AuthenticationForm): + #xh:用户登录表单,继承自Django内置的AuthenticationForm,提供用户认证功能 + def __init__(self, *args, **kwargs): + #xh:初始化方法,自定义表单字段的widget属性,设置输入框的placeholder和CSS类 super(LoginForm, self).__init__(*args, **kwargs) - self.fields['username'].widget = widgets.TextInput( - attrs={'placeholder': "username", "class": "form-control"}) + # xh:设置用户名字段的输入框属性 + self.fields['username'].widget =widgets.TextInput( + attrs={ + 'placeholder': "username", # xh:占位符文本 + "class": "form-control" # xh:Bootstrap样式类 + }) + # 设置密码字段的输入框属性 self.fields['password'].widget = widgets.PasswordInput( - attrs={'placeholder': "password", "class": "form-control"}) + attrs={ + 'placeholder': "password", # xh:占位符文本 + "class": "form-control" # xh:Bootstrap样式类 + }) class RegisterForm(UserCreationForm): + #xh:用户注册表单,继承自Django内置的UserCreationForm,提供用户注册功能,自动包含密码验证逻辑(密码强度、两次密码一致性等) + def __init__(self, *args, **kwargs): + #xh:初始化方法,自定义所有表单字段的widget属性 super(RegisterForm, self).__init__(*args, **kwargs) + # xh:设置用户名字段的输入框属性 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) + + # xh:设置邮箱字段的输入框属性 self.fields['email'].widget = widgets.EmailInput( attrs={'placeholder': "email", "class": "form-control"}) + + # xh:设置密码字段的输入框属性 self.fields['password1'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) + + # xh:设置确认密码字段的输入框属性 self.fields['password2'].widget = widgets.PasswordInput( attrs={'placeholder': "repeat password", "class": "form-control"}) def clean_email(self): + #xh:邮箱字段验证方法,确保邮箱地址在系统中唯一 email = self.cleaned_data['email'] + # xh:检查邮箱是否已存在 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") + #xh:表单元数据配置""" + model = get_user_model() # xh:使用项目中配置的用户模型 + fields = ("username", "email") # xh:表单包含的字段 class ForgetPasswordForm(forms.Form): + #xh:忘记密码表单(用于验证码和密码重置),包含新密码设置、邮箱验证和验证码验证 + + # xh:新密码字段 new_password1 = forms.CharField( - label=_("New password"), + label=_("New password"), # xh:字段标签(支持国际化) widget=forms.PasswordInput( attrs={ - "class": "form-control", - 'placeholder': _("New password") + "class": "form-control", # xh:Bootstrap样式 + 'placeholder': _("New password") # xh:占位符文本 } ), ) + # xh:确认密码字段 new_password2 = forms.CharField( - label="确认密码", + label="确认密码", # xh:中文标签 widget=forms.PasswordInput( attrs={ "class": "form-control", - 'placeholder': _("Confirm password") + 'placeholder': _("Confirm password") # xh:英文占位符 } ), ) + # xh:邮箱字段(用于验证用户身份) email = forms.EmailField( - label='邮箱', - widget=forms.TextInput( + label='邮箱', # xh:中文标签 + widget=forms.TextInput( # xh:使用TextInput而不是EmailInput以便更好地控制样式 attrs={ 'class': 'form-control', 'placeholder': _("Email") @@ -72,8 +101,9 @@ class ForgetPasswordForm(forms.Form): ), ) + # xh:验证码字段(用于二次验证) code = forms.CharField( - label=_('Code'), + label=_('Code'), # xh:验证码标签 widget=forms.TextInput( attrs={ 'class': 'form-control', @@ -83,35 +113,44 @@ class ForgetPasswordForm(forms.Form): ) def clean_new_password2(self): + #xh:确认密码验证方法,验证两次输入的密码是否一致,并检查密码强度 password1 = self.data.get("new_password1") password2 = self.data.get("new_password2") + + # xh:检查两次密码是否一致 if password1 and password2 and password1 != password2: raise ValidationError(_("passwords do not match")) + + # xh:使用Django内置的密码验证器验证密码强度 password_validation.validate_password(password2) return password2 def clean_email(self): + #xh:邮箱验证方法,验证邮箱是否在系统中已注册 user_email = self.cleaned_data.get("email") - if not BlogUser.objects.filter( - email=user_email - ).exists(): - # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + # xh:检查邮箱是否存在(是否已注册) + if not BlogUser.objects.filter(email=user_email).exists(): + # TODO: 这里的报错提示可能会暴露邮箱是否注册,根据安全需求可以修改 raise ValidationError(_("email does not exist")) return user_email def clean_code(self): + #xh:验证码验证方法,调用utils模块的验证函数检查验证码是否正确 code = self.cleaned_data.get("code") + # xh:调用验证工具函数检查验证码 error = utils.verify( - email=self.cleaned_data.get("email"), - code=code, + email=self.cleaned_data.get("email"), # xh:传入邮箱 + code=code, # xh:传入验证码 ) if error: - raise ValidationError(error) + raise ValidationError(error) # xh:验证失败,抛出错误 return code class ForgetPasswordCodeForm(forms.Form): + #xh:忘记密码验证码请求表单,简化版表单,仅用于请求发送密码重置验证码 + email = forms.EmailField( - label=_('Email'), - ) + label=_('Email'), # xh:只需要邮箱字段来请求发送验证码 + ) \ No newline at end of file diff --git a/src/DjangoBlog/accounts/migrations/0001_initial.py b/src/DjangoBlog/accounts/migrations/0001_initial.py index d2fbcab5..1882ceac 100644 --- a/src/DjangoBlog/accounts/migrations/0001_initial.py +++ b/src/DjangoBlog/accounts/migrations/0001_initial.py @@ -1,4 +1,5 @@ # Generated by Django 4.1.7 on 2023-03-02 07:14 +# xh:自动生成的Django迁移文件,记录数据库结构变更 import django.contrib.auth.models import django.contrib.auth.validators @@ -7,43 +8,102 @@ import django.utils.timezone class Migration(migrations.Migration): + #xh:数据库迁移类,用于创建BlogUser用户模型 - initial = True + initial = True # xh:标记为初始迁移 + # xh:依赖的迁移文件 dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ('auth', '0012_alter_user_first_name_max_length'), # xh:依赖Django的auth应用 ] operations = [ migrations.CreateModel( - name='BlogUser', + name='BlogUser', # xh:创建BlogUser模型表 fields=[ + # xh:主键ID,自增BigAutoField ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + + # xh:Django认证系统必需的密码字段 ('password', models.CharField(max_length=128, verbose_name='password')), + + # xh:最后登录时间,可为空 ('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')), - ('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')), + + # xh:超级用户标志 + ('is_superuser', models.BooleanField(default=False, + help_text='Designates that this user has all permissions without explicitly assigning them.', + verbose_name='superuser status')), + + # xh:用户名字段,具有唯一性验证和字符限制 + ('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')), + + # xh:名字字段,可为空 ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + + # xh:姓氏字段,可为空 ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + + # xh:邮箱字段,可为空 ('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')), - ('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')), + + # xh:员工状态,决定是否可以登录admin后台 + ('is_staff', models.BooleanField(default=False, + help_text='Designates whether the user can log into this admin site.', + verbose_name='staff status')), + + # xh:活跃状态,用于软删除 + ('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')), + + # xh:账户创建时间 ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + + # xh:自定义字段:昵称 ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), + + # 自定义字段:记录创建时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + + # xh:自定义字段:最后修改时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + + # xh:自定义字段:用户创建来源(如:网站注册、第三方登录等) ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), - ('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')), - ('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')), + + # xh:用户组多对多关系 + ('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')), + + # xh:用户权限多对多关系 + ('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')), ], + # xh:模型元数据配置 options={ - 'verbose_name': '用户', - 'verbose_name_plural': '用户', - 'ordering': ['-id'], - 'get_latest_by': 'id', + 'verbose_name': '用户', # xh:单数显示名称 + 'verbose_name_plural': '用户', # xh:复数显示名称 + 'ordering': ['-id'], # xh:默认按ID降序排列 + 'get_latest_by': 'id', # xh:指定获取最新记录的字段 }, + # xh:指定模型管理器 managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ('objects', django.contrib.auth.models.UserManager()), # xh:使用Django默认的用户管理器 ], ), - ] + ] \ No newline at end of file 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 1a9f5095..1931e87c 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,81 @@ # Generated by Django 4.2.5 on 2023-09-06 13:13 +# xh:自动生成的Django迁移文件,用于修改BlogUser模型结构 from django.db import migrations, models import django.utils.timezone class Migration(migrations.Migration): + #xh:数据库迁移类,用于修改BlogUser模型的字段和配置 + # xh:依赖之前的初始迁移文件 dependencies = [ - ('accounts', '0001_initial'), + ('accounts', '0001_initial'), # xh:依赖accounts应用的初始迁移 ] operations = [ + # xh:修改模型的元数据选项 migrations.AlterModelOptions( name='bloguser', - options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, + options={ + 'get_latest_by': 'id', # xh:指定按id字段获取最新记录 + 'ordering': ['-id'], # xh:按id降序排列 + 'verbose_name': 'user', # xh:修改单数显示名称为英文'user' + 'verbose_name_plural': 'user', # xh:修改复数显示名称为英文'user' + }, ), + + # xh:删除旧的创建时间字段 migrations.RemoveField( model_name='bloguser', - name='created_time', + name='created_time', # xh:移除原有的created_time字段 ), + + # xh:删除旧的修改时间字段 migrations.RemoveField( model_name='bloguser', - name='last_mod_time', + name='last_mod_time', # xh:移除原有的last_mod_time字段 ), + + # xh:添加新的创建时间字段(重命名) migrations.AddField( model_name='bloguser', name='creation_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + field=models.DateTimeField( + default=django.utils.timezone.now, # xh:默认值为当前时间 + verbose_name='creation time' # xh:字段显示名称为英文 + ), ), + + # xh:添加新的最后修改时间字段(重命名) migrations.AddField( model_name='bloguser', name='last_modify_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + field=models.DateTimeField( + default=django.utils.timezone.now, # xh:默认值为当前时间 + verbose_name='last modify time' # xh:字段显示名称为英文 + ), ), + + # xh:修改昵称字段的显示名称 migrations.AlterField( model_name='bloguser', name='nickname', - field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), + field=models.CharField( + blank=True, + max_length=100, + verbose_name='nick name' # xh:从中文'昵称'改为英文'nick name' + ), ), + + # xh:修改来源字段的显示名称 migrations.AlterField( model_name='bloguser', name='source', - field=models.CharField(blank=True, max_length=100, verbose_name='create source'), + field=models.CharField( + blank=True, + max_length=100, + verbose_name='create source' # xh:从中文'创建来源'改为英文'create source' + ), ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog/accounts/models.py b/src/DjangoBlog/accounts/models.py index 3baddbb2..469f80ae 100644 --- a/src/DjangoBlog/accounts/models.py +++ b/src/DjangoBlog/accounts/models.py @@ -1,35 +1,64 @@ -from django.contrib.auth.models import AbstractUser -from django.db import models -from django.urls import reverse -from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _ -from djangoblog.utils import get_current_site +# xh:导入必要的模块 +from django.contrib.auth.models import AbstractUser # xh:Django抽象用户基类 +from django.db import models # xh:Django模型字段 +from django.urls import reverse # xh:URL反向解析 +from django.utils.timezone import now # xh:获取当前时间 +from django.utils.translation import gettext_lazy as _ # xh:国际化翻译 +from djangoblog.utils import get_current_site # xh:自定义工具函数,获取当前站点 -# Create your models here. - class BlogUser(AbstractUser): - 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) + #xh:自定义用户模型,继承自Django的AbstractUser,扩展了博客系统的用户功能 + + # xh:昵称字段 - 用户的显示名称 + nickname = models.CharField( + _('nick name'), # xh:字段显示名称(支持国际化) + max_length=100, # xh:最大长度100字符 + blank=True # xh:允许为空 + ) + + # xh:创建时间 - 记录用户注册时间 + creation_time = models.DateTimeField( + _('creation time'), # xh:字段显示名称 + default=now # xh:默认值为当前时间 + ) + + # xh:最后修改时间 - 记录用户信息最后修改时间 + last_modify_time = models.DateTimeField( + _('last modify time'), # xh:字段显示名称 + default=now # xh:默认值为当前时间 + ) + + # xh:创建来源 - 记录用户的注册渠道 + source = models.CharField( + _('create source'), # xh:字段显示名称 + max_length=100, # xh:最大长度100字符 + blank=True # xh:允许为空 + ) def get_absolute_url(self): + #xh:获取用户的绝对URL(用于后台管理等场景),返回用户详情页的URL return reverse( - 'blog:author_detail', kwargs={ - 'author_name': self.username}) + 'blog:author_detail', # xh:URL配置的名称 + kwargs={'author_name': self.username} # xh:传递用户名参数 + ) def __str__(self): - return self.email + #xh:对象的字符串表示方法,在Django admin和管理界面中显示 + return self.email # xh:使用邮箱作为对象的显示标识 def get_full_url(self): - site = get_current_site().domain - url = "https://{site}{path}".format(site=site, - path=self.get_absolute_url()) + #xh:获取用户的完整URL(包含域名),用于生成可在站外访问的完整用户链接 + site = get_current_site().domain # xh:获取当前站点的域名 + url = "https://{site}{path}".format( + site=site, # xh:域名 + path=self.get_absolute_url() # xh:相对路径 + ) return url class Meta: - ordering = ['-id'] - verbose_name = _('user') - verbose_name_plural = verbose_name - get_latest_by = 'id' + #xh:模型的元数据配置,定义模型的各种行为和显示选项 + ordering = ['-id'] # xh:默认按ID降序排列(最新的在前) + verbose_name = _('user') # xh:单数显示名称 + verbose_name_plural = verbose_name # xh:复数显示名称(与单数相同) + get_latest_by = 'id' # xh:指定按id字段获取最新记录 \ No newline at end of file diff --git a/src/DjangoBlog/accounts/tests.py b/src/DjangoBlog/accounts/tests.py index 6893411c..ac7db461 100644 --- a/src/DjangoBlog/accounts/tests.py +++ b/src/DjangoBlog/accounts/tests.py @@ -9,81 +9,104 @@ from djangoblog.utils import * from . import utils -# Create your tests here. - class AccountTest(TestCase): + #xh:用户账户功能测试类,测试用户注册、登录、密码重置等核心功能 + def setUp(self): - self.client = Client() - self.factory = RequestFactory() + #xh:测试前置设置方法,在每个测试方法执行前运行,用于创建测试数据 + self.client = Client() # xh:Django测试客户端,用于模拟HTTP请求 + self.factory = RequestFactory() # xh:用于创建请求对象 + # xh:创建测试用户 self.blog_user = BlogUser.objects.create_user( username="test", email="admin@admin.com", password="12345678" ) - self.new_test = "xxx123--=" + self.new_test = "xxx123--=" # xh:测试用的新密码 def test_validate_account(self): + #xh:测试账户验证功能,验证超级用户创建、登录、文章管理等功能 site = get_current_site().domain + # xh:创建超级用户 user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="qwer!@#$ggg") testuser = BlogUser.objects.get(username='liangliangyy1') + # xh:测试用户登录 loginresult = self.client.login( username='liangliangyy1', password='qwer!@#$ggg') - self.assertEqual(loginresult, True) + self.assertEqual(loginresult, True) # xh:断言登录成功 + + # xh:测试访问管理员页面 response = self.client.get('/admin/') - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) # xh:断言可以访问admin + # xh:创建测试分类 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() category.save() + # xh:创建测试文章 article = Article() article.title = "nicetitleaaa" article.body = "nicecontentaaa" article.author = user article.category = category - article.type = 'a' - article.status = 'p' + article.type = 'a' # xh:文章类型 + article.status = 'p' # xh:发布状态 article.save() + # xh:测试访问文章管理页面 response = self.client.get(article.get_admin_url()) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) # xh:断言可以访问文章管理页 def test_validate_register(self): + #xh:测试用户注册流程,验证用户注册、邮箱验证、登录、权限管理等完整流程 + # xh:验证注册前用户不存在 self.assertEquals( - 0, len( - BlogUser.objects.filter( - email='user123@user.com'))) + 0, len(BlogUser.objects.filter(email='user123@user.com'))) + + # xh:提交注册表单 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', + 'password1': 'password123!q@wE#R$T', # xh:密码1 + 'password2': 'password123!q@wE#R$T', # xh:确认密码 }) + + # xh:验证用户创建成功 self.assertEquals( - 1, len( - BlogUser.objects.filter( - email='user123@user.com'))) + 1, len(BlogUser.objects.filter(email='user123@user.com'))) + user = BlogUser.objects.filter(email='user123@user.com')[0] + + # xh:生成邮箱验证签名(模拟验证流程) 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) + + # xh:访问验证结果页面 response = self.client.get(url) self.assertEqual(response.status_code, 200) + # xh:测试用户登录 self.client.login(username='user1233', password='password123!q@wE#R$T') + + # xh:提升用户权限为超级用户 user = BlogUser.objects.filter(email='user123@user.com')[0] user.is_superuser = True user.is_staff = True user.save() - delete_sidebar_cache() + + delete_sidebar_cache() # xh:清理侧边栏缓存 + + # xh:创建测试数据 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() @@ -95,90 +118,110 @@ class AccountTest(TestCase): article.title = "nicetitle333" article.body = "nicecontentttt" article.author = user - article.type = 'a' article.status = 'p' article.save() + # xh:测试文章管理页面访问 response = self.client.get(article.get_admin_url()) self.assertEqual(response.status_code, 200) + # xh:测试用户登出 response = self.client.get(reverse('account:logout')) - self.assertIn(response.status_code, [301, 302, 200]) + self.assertIn(response.status_code, [301, 302, 200]) # xh:重定向或成功 + # xh:登出后访问管理页面应该被重定向 response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) + # xh:测试错误密码登录 response = self.client.post(reverse('account:login'), { 'username': 'user1233', - 'password': 'password123' + 'password': 'password123' # xh:错误密码 }) self.assertIn(response.status_code, [301, 302, 200]) + # xh:错误密码登录后仍无法访问管理页面 response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) def test_verify_email_code(self): + #xh:测试邮箱验证码功能,验证验证码的生成、发送和验证流程 to_email = "admin@admin.com" - code = generate_code() + code = generate_code() # xh:生成验证码 + + # xh:保存验证码到缓存/数据库 utils.set_code(to_email, code) + # xh:发送验证邮件(测试环境可能不会实际发送) utils.send_verify_email(to_email, code) + # xh:测试正确验证码验证 err = utils.verify("admin@admin.com", code) - self.assertEqual(err, None) + self.assertEqual(err, None) # xh:断言验证成功(无错误) + # xh:测试错误邮箱验证 err = utils.verify("admin@123.com", code) - self.assertEqual(type(err), str) + self.assertEqual(type(err), str) # xh:断言返回错误信息 def test_forget_password_email_code_success(self): + #xh:测试忘记密码验证码请求成功情况 resp = self.client.post( path=reverse("account:forget_password_code"), - data=dict(email="admin@admin.com") + data=dict(email="admin@admin.com") # xh:正确邮箱格式 ) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode("utf-8"), "ok") + self.assertEqual(resp.status_code, 200) # xh:断言请求成功 + self.assertEqual(resp.content.decode("utf-8"), "ok") # xh:断言返回成功消息 def test_forget_password_email_code_fail(self): + #xh:测试忘记密码验证码请求失败情况,验证空数据和错误邮箱格式的处理 + # xh:测试空数据提交 resp = self.client.post( path=reverse("account:forget_password_code"), - data=dict() + data=dict() # xh:空数据 ) self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + # xh:测试错误邮箱格式 resp = self.client.post( path=reverse("account:forget_password_code"), - data=dict(email="admin@com") + data=dict(email="admin@com") # xh:无效邮箱格式 ) self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") def test_forget_password_email_success(self): + #xh:测试密码重置成功流程,验证完整的密码重置功能 code = generate_code() - utils.set_code(self.blog_user.email, code) + utils.set_code(self.blog_user.email, code) # xh:设置验证码 + + # xh:构造密码重置数据 data = dict( - new_password1=self.new_test, - new_password2=self.new_test, - email=self.blog_user.email, - code=code, + new_password1=self.new_test, # xh:新密码 + new_password2=self.new_test, # xh:确认密码 + email=self.blog_user.email, # xh:用户邮箱 + code=code, # xh:正确验证码 ) + + # xh:提交密码重置请求 resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 302) + self.assertEqual(resp.status_code, 302) # xh:断言重定向(成功) - # 验证用户密码是否修改成功 + # xh:验证用户密码是否修改成功 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) + self.assertNotEqual(blog_user, None) # xh:断言用户存在 + self.assertEqual(blog_user.check_password(data["new_password1"]), True) # xh:断言密码修改成功 def test_forget_password_email_not_user(self): + #xh:测试不存在的用户密码重置,验证系统对不存在用户的处理 data = dict( new_password1=self.new_test, new_password2=self.new_test, - email="123@123.com", + email="123@123.com", # xh:不存在的邮箱 code="123456", ) resp = self.client.post( @@ -186,22 +229,22 @@ class AccountTest(TestCase): data=data ) - self.assertEqual(resp.status_code, 200) - + self.assertEqual(resp.status_code, 200) # xh:断言返回错误页面(非重定向) def test_forget_password_email_code_error(self): + #xh:测试验证码错误情况,验证错误验证码的处理 code = generate_code() - utils.set_code(self.blog_user.email, code) + utils.set_code(self.blog_user.email, code) # xh:设置正确验证码 + data = dict( new_password1=self.new_test, new_password2=self.new_test, email=self.blog_user.email, - code="111111", + code="111111", # xh:错误验证码 ) resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 200) - + self.assertEqual(resp.status_code, 200) # xh:断言验证失败(非重定向) \ No newline at end of file diff --git a/src/DjangoBlog/accounts/urls.py b/src/DjangoBlog/accounts/urls.py index 107a801d..9e446f22 100644 --- a/src/DjangoBlog/accounts/urls.py +++ b/src/DjangoBlog/accounts/urls.py @@ -1,28 +1,54 @@ -from django.urls import path -from django.urls import re_path +# xh:导入URL路由相关的模块 +from django.urls import path # xh:用于简单路径匹配 +from django.urls import re_path # xh:用于正则表达式路径匹配 -from . import views -from .forms import LoginForm +# xh:导入当前应用的视图和表单 +from . import views # xh:导入当前目录下的views模块 +from .forms import LoginForm # xh:导入自定义登录表单 +# xh:应用命名空间,用于URL反向解析时区分不同应用的相同URL名称 app_name = "accounts" -urlpatterns = [re_path(r'^login/$', - views.LoginView.as_view(success_url='/'), - name='login', - kwargs={'authentication_form': LoginForm}), - re_path(r'^register/$', - views.RegisterView.as_view(success_url="/"), - name='register'), - re_path(r'^logout/$', - views.LogoutView.as_view(), - name='logout'), - path(r'account/result.html', - views.account_result, - name='result'), - re_path(r'^forget_password/$', - views.ForgetPasswordView.as_view(), - name='forget_password'), - re_path(r'^forget_password_code/$', - views.ForgetPasswordEmailCode.as_view(), - name='forget_password_code'), - ] +# xh:URL模式配置列表,定义URL路径与视图的映射关系 +urlpatterns = [ + # xh:用户登录路由 - 使用正则表达式匹配 + re_path(r'^login/$', # xh:匹配以/login/结尾的URL + views.LoginView.as_view( # xh:使用类视图,转换为视图函数 + success_url='/' # xh:登录成功后的重定向URL(首页) + ), + name='login', # xh:URL名称,用于反向解析 + kwargs={'authentication_form': LoginForm} # xh:额外参数:指定登录表单 + ), + + # xh:用户注册路由 + re_path(r'^register/$', # xh:匹配以/register/结尾的URL + views.RegisterView.as_view( + success_url="/" # xh:注册成功后的重定向URL(首页) + ), + name='register' # xh:URL名称:register + ), + + # xh:用户登出路由 + re_path(r'^logout/$', # xh:匹配以/logout/结尾的URL + views.LogoutView.as_view(), # xh:登出视图,无额外参数 + name='logout' # xh:URL名称:logout + ), + + # xh:账户操作结果页面路由 - 使用path(更简洁的路径语法) + path(r'account/result.html', # xh:匹配固定路径/account/result.html + views.account_result, # xh:使用函数视图 + name='result' # xh:URL名称:result + ), + + # xh:忘记密码页面路由(提交密码重置表单) + re_path(r'^forget_password/$', # xh:匹配以/forget_password/结尾的URL + views.ForgetPasswordView.as_view(), # xh:密码重置视图 + name='forget_password' # xh:URL名称:forget_password + ), + + # xh:忘记密码验证码请求路由(获取验证码) + re_path(r'^forget_password_code/$', # xh:匹配以/forget_password_code/结尾的URL + views.ForgetPasswordEmailCode.as_view(), # xh:验证码请求视图 + name='forget_password_code' # xh:URL名称:forget_password_code + ), +] \ No newline at end of file diff --git a/src/DjangoBlog/accounts/user_login_backend.py b/src/DjangoBlog/accounts/user_login_backend.py index 73cdca1b..817ce398 100644 --- a/src/DjangoBlog/accounts/user_login_backend.py +++ b/src/DjangoBlog/accounts/user_login_backend.py @@ -1,26 +1,39 @@ -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend +# xh:导入必要的模块 +from django.contrib.auth import get_user_model # xh:获取项目中使用的用户模型 +from django.contrib.auth.backends import ModelBackend # xh:Django认证后端基类 class EmailOrUsernameModelBackend(ModelBackend): - """ - 允许使用用户名或邮箱登录 - """ + #xh:自定义认证后端,允许用户使用用户名或邮箱地址进行登录,继承自Django的ModelBackend,扩展了认证方式 def authenticate(self, request, username=None, password=None, **kwargs): + #xh:用户认证方法,根据用户名或邮箱验证用户身份 + # xh:判断输入的是邮箱还是用户名 if '@' in username: + # xh:如果包含@符号,按邮箱处理 kwargs = {'email': username} else: + # xh:否则按用户名处理 kwargs = {'username': username} + try: + # xh:根据用户名或邮箱查找用户 user = get_user_model().objects.get(**kwargs) + + # xh:验证密码是否正确(自动处理哈希比较) if user.check_password(password): - return user + return user # xh:认证成功,返回用户对象 + except get_user_model().DoesNotExist: + # xh:用户不存在,返回None表示认证失败 return None - def get_user(self, username): + def get_user(self, user_id): + #xh:根据用户ID获取用户对象,用于会话认证,在请求处理期间获取用户信息 + try: - return get_user_model().objects.get(pk=username) + # xh:根据主键ID查找用户 + return get_user_model().objects.get(pk=user_id) except get_user_model().DoesNotExist: + # xh:用户不存在,返回None return None diff --git a/src/DjangoBlog/accounts/views.py b/src/DjangoBlog/accounts/views.py index ae67aec4..72572e39 100644 --- a/src/DjangoBlog/accounts/views.py +++ b/src/DjangoBlog/accounts/views.py @@ -26,179 +26,222 @@ from . import utils from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm from .models import BlogUser +# xh:配置日志记录器 logger = logging.getLogger(__name__) -# Create your views here. - class RegisterView(FormView): - form_class = RegisterForm - template_name = 'account/registration_form.html' + #xh:用户注册视图,处理新用户的注册流程,包括表单验证、用户创建和邮箱验证 + form_class = RegisterForm # xh:使用的表单类 + template_name = 'account/registration_form.html' # xh:模板路径 - @method_decorator(csrf_protect) + @method_decorator(csrf_protect) # xh:CSRF保护,防止跨站请求伪造 def dispatch(self, *args, **kwargs): return super(RegisterView, self).dispatch(*args, **kwargs) def form_valid(self, form): + #xh:表单验证通过后的处理逻辑 if form.is_valid(): + # xh:保存用户但不提交到数据库(commit=False) user = form.save(False) - user.is_active = False - user.source = 'Register' - user.save(True) + user.is_active = False # xh:设置用户为未激活状态,需要邮箱验证 + user.source = 'Register' # xh:记录用户来源 + user.save(True) # xh:保存到数据库 + + # xh:获取当前站点信息 site = get_current_site().domain + + # xh:生成邮箱验证签名(使用双重SHA256加密) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + # xh:调试模式下使用本地地址 if settings.DEBUG: site = '127.0.0.1:8000' + + # xh:构建验证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) + # xh:邮件内容模板 content = """ -

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

- - {url} - - 再次感谢您! -
- 如果上面链接无法打开,请将此链接复制至浏览器。 - {url} - """.format(url=url) +

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

+ {url} + 再次感谢您! +
+ 如果上面链接无法打开,请将此链接复制至浏览器。 + {url} + """.format(url=url) + + # xh:发送验证邮件 send_email( - emailto=[ - user.email, - ], + emailto=[user.email], title='验证您的电子邮箱', content=content) - url = reverse('accounts:result') + \ - '?type=register&id=' + str(user.id) + # xh:重定向到结果页面 + url = reverse('accounts:result') + '?type=register&id=' + str(user.id) return HttpResponseRedirect(url) else: - return self.render_to_response({ - 'form': form - }) + # xh:表单无效,重新渲染表单页 + return self.render_to_response({'form': form}) class LogoutView(RedirectView): - url = '/login/' + #xh:用户登出视图,处理用户登出逻辑,清理会话和缓存 + url = '/login/' # xh:登出后重定向的URL - @method_decorator(never_cache) + @method_decorator(never_cache) # xh:禁止缓存,确保每次都是最新状态 def dispatch(self, request, *args, **kwargs): return super(LogoutView, self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): - logout(request) - delete_sidebar_cache() + #xh:GET请求处理:执行登出操作 + logout(request) # xh:Django内置登出函数,清理会话 + delete_sidebar_cache() # xh:清理侧边栏缓存 return super(LogoutView, self).get(request, *args, **kwargs) class LoginView(FormView): - form_class = LoginForm - template_name = 'account/login.html' - success_url = '/' - redirect_field_name = REDIRECT_FIELD_NAME - login_ttl = 2626560 # 一个月的时间 - - @method_decorator(sensitive_post_parameters('password')) - @method_decorator(csrf_protect) - @method_decorator(never_cache) + #xh:用户登录视图,处理用户认证和登录逻辑 + form_class = LoginForm # xh:登录表单 + template_name = 'account/login.html' # xh:登录模板 + success_url = '/' # xh:登录成功默认重定向URL + redirect_field_name = REDIRECT_FIELD_NAME # xh:重定向字段名(默认:'next') + login_ttl = 2626560 # xh:会话过期时间:一个月(秒) + + # xh:方法装饰器:增强安全性 + @method_decorator(sensitive_post_parameters('password')) # xh:敏感参数保护 + @method_decorator(csrf_protect) #xh:CSRF保护 + @method_decorator(never_cache) # xh:禁止缓存 def dispatch(self, request, *args, **kwargs): - return super(LoginView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): + #xh:添加上下文数据:重定向URL redirect_to = self.request.GET.get(self.redirect_field_name) if redirect_to is None: - redirect_to = '/' + redirect_to = '/' # xh:默认重定向到首页 kwargs['redirect_to'] = redirect_to - return super(LoginView, self).get_context_data(**kwargs) def form_valid(self, form): + #xh:表单验证通过后的登录处理 + # xh:使用Django内置的认证表单进行验证 form = AuthenticationForm(data=self.request.POST, request=self.request) if form.is_valid(): + # xh:清理侧边栏缓存 delete_sidebar_cache() logger.info(self.redirect_field_name) + # xh:执行用户登录(创建会话) auth.login(self.request, form.get_user()) + + # xh:处理"记住我"功能 if self.request.POST.get("remember"): + # xh:设置会话过期时间为一个月 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 - }) + # xh:认证失败,重新显示表单 + return self.render_to_response({'form': form}) def get_success_url(self): - + #xh:获取登录成功后的重定向URL,进行安全验证,防止开放重定向攻击 redirect_to = self.request.POST.get(self.redirect_field_name) + + # xh:验证重定向URL的安全性 if not url_has_allowed_host_and_scheme( - url=redirect_to, allowed_hosts=[ - self.request.get_host()]): - redirect_to = self.success_url + url=redirect_to, + allowed_hosts=[self.request.get_host()]): + redirect_to = self.success_url # xh:不安全则使用默认URL + return redirect_to def account_result(request): - type = request.GET.get('type') - id = request.GET.get('id') + #xh:账户操作结果页面,显示注册结果或处理邮箱验证 + type = request.GET.get('type') # xh:操作类型:register 或 validation + id = request.GET.get('id') # xh:用户ID + # xh:获取用户对象,不存在则返回404 user = get_object_or_404(get_user_model(), id=id) logger.info(type) + + # xh:如果用户已激活,直接重定向到首页 if user.is_active: return HttpResponseRedirect('/') + + # xh:处理注册结果或邮箱验证 if type and type in ['register', 'validation']: if type == 'register': - content = ''' - 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 - ''' + # xh:注册成功页面 + content = '恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。' title = '注册成功' else: + # xh:邮箱验证处理 c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = request.GET.get('sign') + + # xh:验证签名,防止篡改 if sign != c_sign: - return HttpResponseForbidden() + return HttpResponseForbidden() # 签名不匹配,拒绝访问 + + # xh:激活用户账户 user.is_active = True user.save() - content = ''' - 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 - ''' + content = '恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。' title = '验证成功' + + # xh:渲染结果页面 return render(request, 'account/result.html', { 'title': title, 'content': content }) else: + # xh:无效类型,重定向到首页 return HttpResponseRedirect('/') class ForgetPasswordView(FormView): + #xh:忘记密码视图,处理密码重置请求 form_class = ForgetPasswordForm template_name = 'account/forget_password.html' def form_valid(self, form): + #xh:表单验证通过后的密码重置处理 if form.is_valid(): + # xh:根据邮箱查找用户 blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() + + # xh:对新密码进行哈希处理并保存 blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.save() + + # xh:重定向到登录页面 return HttpResponseRedirect('/login/') else: + # xh:表单无效,重新显示 return self.render_to_response({'form': form}) class ForgetPasswordEmailCode(View): + #xh:忘记密码验证码发送视图,处理密码重置验证码的发送 def post(self, request: HttpRequest): + #xh:处理POST请求:发送密码重置验证码 form = ForgetPasswordCodeForm(request.POST) + if not form.is_valid(): - return HttpResponse("错误的邮箱") - to_email = form.cleaned_data["email"] + return HttpResponse("错误的邮箱") # xh:表单验证失败 + + to_email = form.cleaned_data["email"] # xh:获取邮箱地址 - code = generate_code() - utils.send_verify_email(to_email, code) - utils.set_code(to_email, code) + # xh:生成并发送验证码 + code = generate_code() # xh:生成6位随机验证码 + utils.send_verify_email(to_email, code) # xh:发送验证邮件 + utils.set_code(to_email, code) # xh:保存验证码到缓存/数据库 - return HttpResponse("ok") + return HttpResponse("ok") # xh:返回成功响应 \ No newline at end of file