From f2ba999735faef4b01694a78dfb479bcec5f4937 Mon Sep 17 00:00:00 2001 From: zyy <1339568841@qq.com> Date: Sun, 12 Oct 2025 17:29:41 +0800 Subject: [PATCH 1/5] admin.py --- src/django-master/accounts/admin.py | 107 ++++++++++++++++++--------- src/django-master/accounts/apps.py | 10 ++- src/django-master/accounts/forms.py | 30 ++++---- src/django-master/comments/models.py | 60 +++++++-------- 4 files changed, 127 insertions(+), 80 deletions(-) diff --git a/src/django-master/accounts/admin.py b/src/django-master/accounts/admin.py index 32e483c..d3889c0 100644 --- a/src/django-master/accounts/admin.py +++ b/src/django-master/accounts/admin.py @@ -1,59 +1,100 @@ -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 _ +#django核心组件导入 +from django import forms# Django 表单处理模块 +from django.contrib.auth.admin import UserAdmin # Django 默认用户管理后台类 +from django.contrib.auth.forms import UserChangeForm # 用户信息修改表单基类 +from django.contrib.auth.forms import UsernameField# 用户名专用表单字段 +from django.utils.translation import gettext_lazy as _ # 国际化翻译函数 +# 本地应用导入 # Register your models here. -from .models import BlogUser +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) + """ + 自定义用户创建表单(用于管理后台添加新用户) + 继承自 ModelForm专门处理 BlogUser 模型的创建 + """ + + # 定义密码输入字段(需要输入两次以确保一致) + password1 = forms.CharField(label=_('password'), # 字段标签(支持国际化) + widget=forms.PasswordInput) # 密码输入控件 + password2 = forms.CharField(label=_('Enter password again'), # 确认密码标签 + widget=forms.PasswordInput) # 密码输入控件 class Meta: - model = BlogUser - fields = ('email',) + model = BlogUser# 关联的模型类 + fields = ('email',)# 创建用户时显示的字段(这里只显示email字段) def clean_password2(self): + """ + 验证两次输入的密码是否一致 + Django 表单验证方法,方法名必须以 clean_ 开头 + """ # Check that the two password entries match - password1 = self.cleaned_data.get("password1") - password2 = self.cleaned_data.get("password2") + 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 + raise forms.ValidationError(_("passwords do not match"))# 错误信息(支持国际化) + return password2# 返回验证后的值 + def save(self, commit=True): + """ + 重写保存方法,在保存用户前处理密码哈希 + """ # Save the provided password in hashed format - user = super().save(commit=False) - user.set_password(self.cleaned_data["password1"]) + user = super().save(commit=False) # 调用父类保存方法但不提交到数据库 + user.set_password(self.cleaned_data["password1"]) # 对密码进行哈希加密 if commit: - user.source = 'adminsite' - user.save() - return user + user.source = 'adminsite' # 设置用户来源标记(表示通过管理后台创建) + user.save()# 保存到数据库 + return user# 返回用户对象 class BlogUserChangeForm(UserChangeForm): + """ + 自定义用户信息修改表单(用于管理后台编辑用户) + 继承自 Django 自带的 UserChangeForm + """ + class Meta: - model = BlogUser - fields = '__all__' - field_classes = {'username': UsernameField} + model = BlogUser # 关联的模型类 + fields = '__all__'# 显示所有字段 + field_classes = {'username': UsernameField}# 指定用户名使用专用字段类型 def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - + """ + 表单初始化方法 + 可以在这里对表单字段进行自定义设置 + """ + super().__init__(*args, **kwargs) # 调用父类初始化方法 + # 可以在这里添加自定义逻辑,如修改字段属性等 class BlogUserAdmin(UserAdmin): - form = BlogUserChangeForm - add_form = BlogUserCreationForm + """ + 自定义用户管理后台配置 + 继承自 Django 自带的 UserAdmin + """ + + # 指定使用的表单类 + form = BlogUserChangeForm # 编辑用户时使用的表单 + add_form = BlogUserCreationForm# 添加用户时使用的表单 + + # 管理后台列表页显示配置 list_display = ( - 'id', - 'nickname', - 'username', - 'email', - 'last_login', - 'date_joined', - 'source') + 'id', # 用户ID + 'nickname', # 用户昵称 + 'username',# 用户名 + 'email', # 电子邮箱 + 'last_login', # 最后登录时间 + 'date_joined', # 注册时间 + 'source')# 用户来源标记 + + # 设置哪些字段可以点击跳转到编辑页 list_display_links = ('id', 'username') + + # 默认排序规则(按ID降序排列) ordering = ('-id',) + diff --git a/src/django-master/accounts/apps.py b/src/django-master/accounts/apps.py index 9b3fc5a..bf4ecc8 100644 --- a/src/django-master/accounts/apps.py +++ b/src/django-master/accounts/apps.py @@ -1,5 +1,11 @@ from django.apps import AppConfig - + class AccountsConfig(AppConfig): - name = 'accounts' + """ + Accounts 应用的配置类。 + 功能: + 1. 定义应用名称(供 Django 内部识别)。 + 2. 可在此处覆盖 ready() 方法以注册信号等。 + """ + name = 'accounts'# 必须与项目中的应用目录名完全一致 \ No newline at end of file diff --git a/src/django-master/accounts/forms.py b/src/django-master/accounts/forms.py index fce4137..a0b5769 100644 --- a/src/django-master/accounts/forms.py +++ b/src/django-master/accounts/forms.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from . import utils from .models import BlogUser - +# 登录表单,继承自Django内置的AuthenticationForm class LoginForm(AuthenticationForm): def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) @@ -16,11 +16,11 @@ class LoginForm(AuthenticationForm): self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) - +# 注册表单,继承自Django内置的UserCreationForm class RegisterForm(UserCreationForm): def __init__(self, *args, **kwargs): super(RegisterForm, self).__init__(*args, **kwargs) - + # 自定义用户名、邮箱和密码字段的HTML属性 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) self.fields['email'].widget = widgets.EmailInput( @@ -29,18 +29,18 @@ class RegisterForm(UserCreationForm): 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"), @@ -51,7 +51,7 @@ class ForgetPasswordForm(forms.Form): } ), ) - + # 新密码字段2(用于确认) new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -61,7 +61,7 @@ class ForgetPasswordForm(forms.Form): } ), ) - + # 邮箱字段 email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -71,7 +71,7 @@ class ForgetPasswordForm(forms.Form): } ), ) - + # 验证码字段 code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -81,16 +81,16 @@ class ForgetPasswordForm(forms.Form): } ), ) - + # 验证两次输入的密码是否一致,并检查密码强度 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) + password_validation.validate_password(password2)# 使用Django的密码验证器 return password2 - + # 验证邮箱是否已注册 def clean_email(self): user_email = self.cleaned_data.get("email") if not BlogUser.objects.filter( @@ -99,10 +99,10 @@ class ForgetPasswordForm(forms.Form): # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 raise ValidationError(_("email does not exist")) return user_email - + # 验证用户输入的验证码是否正确 def clean_code(self): code = self.cleaned_data.get("code") - error = utils.verify( + error = utils.verify(# 调用工具函数验证验证码 email=self.cleaned_data.get("email"), code=code, ) @@ -110,7 +110,7 @@ class ForgetPasswordForm(forms.Form): raise ValidationError(error) return code - +# 忘记密码功能中的验证码发送表单(仅需邮箱字段) class ForgetPasswordCodeForm(forms.Form): email = forms.EmailField( label=_('Email'), diff --git a/src/django-master/comments/models.py b/src/django-master/comments/models.py index 7c3bbc8..0ac8c4d 100644 --- a/src/django-master/comments/models.py +++ b/src/django-master/comments/models.py @@ -1,39 +1,39 @@ -from django.conf import settings +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 blog.models import Article +from djangoblog.utils import get_current_site # Create your models here. - -class Comment(models.Model): - body = models.TextField('正文', max_length=300) +# 自定义用户模型,继承Django内置的AbstractUser +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) - author = models.ForeignKey( - settings.AUTH_USER_MODEL, - verbose_name=_('author'), - on_delete=models.CASCADE) - article = models.ForeignKey( - Article, - verbose_name=_('article'), - on_delete=models.CASCADE) - parent_comment = models.ForeignKey( - 'self', - verbose_name=_('parent comment'), - blank=True, - null=True, - on_delete=models.CASCADE) - is_enable = models.BooleanField(_('enable'), - default=False, blank=False, null=False) - - class Meta: - ordering = ['-id'] - verbose_name = _('comment') - verbose_name_plural = verbose_name - get_latest_by = 'id' - + # 账号创建来源(如:网站注册/第三方登录等,可选) + source = models.CharField(_('create source'), max_length=100, blank=True) + # 获取用户详情页的绝对URL(用于模板中的{% url %}反向解析) + def get_absolute_url(self): + return reverse( + 'blog:author_detail', kwargs={ + 'author_name': self.username}) + # 定义对象的字符串表示(Admin后台和shell中显示) def __str__(self): - return self.body + return self.email + # 获取用户详情页的完整URL(包含域名,用于分享链接) + def get_full_url(self): + site = get_current_site().domain# 获取当前站点域名 + url = "https://{site}{path}".format(site=site, + path=self.get_absolute_url()) + return url + # 元数据配置(模型级别的选项) + class Meta: + ordering = ['-id'] # 默认按ID降序排列 + verbose_name = _('user') # 单数形式名称(后台显示) + verbose_name_plural = verbose_name# 复数形式名称(后台显示) + get_latest_by = 'id'# 指定最新记录的排序字段 From 281ca887afe32a1b5e5b4d5990ef60a877185e40 Mon Sep 17 00:00:00 2001 From: zyy <1339568841@qq.com> Date: Sun, 12 Oct 2025 17:53:18 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/comments/admin.py | 129 ++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 38 deletions(-) diff --git a/src/django-master/comments/admin.py b/src/django-master/comments/admin.py index a814f3f..d3889c0 100644 --- a/src/django-master/comments/admin.py +++ b/src/django-master/comments/admin.py @@ -1,47 +1,100 @@ -from django.contrib import admin -from django.urls import reverse -from django.utils.html import format_html -from django.utils.translation import gettext_lazy as _ +#django核心组件导入 +from django import forms# Django 表单处理模块 +from django.contrib.auth.admin import UserAdmin # Django 默认用户管理后台类 +from django.contrib.auth.forms import UserChangeForm # 用户信息修改表单基类 +from django.contrib.auth.forms import UsernameField# 用户名专用表单字段 +from django.utils.translation import gettext_lazy as _ # 国际化翻译函数 +# 本地应用导入 +# Register your models here. +from .models import BlogUser # 导入自定义用户模型 -def disable_commentstatus(modeladmin, request, queryset): - queryset.update(is_enable=False) +class BlogUserCreationForm(forms.ModelForm): + """ + 自定义用户创建表单(用于管理后台添加新用户) + 继承自 ModelForm专门处理 BlogUser 模型的创建 + """ -def enable_commentstatus(modeladmin, request, queryset): - queryset.update(is_enable=True) + # 定义密码输入字段(需要输入两次以确保一致) + 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字段) -disable_commentstatus.short_description = _('Disable comments') -enable_commentstatus.short_description = _('Enable comments') + def clean_password2(self): + """ + 验证两次输入的密码是否一致 + Django 表单验证方法,方法名必须以 clean_ 开头 + """ + # Check that the two password entries match + 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# 返回验证后的值 -class CommentAdmin(admin.ModelAdmin): - list_per_page = 20 + def save(self, commit=True): + """ + 重写保存方法,在保存用户前处理密码哈希 + """ + # Save the provided password in hashed format + user = super().save(commit=False) # 调用父类保存方法但不提交到数据库 + user.set_password(self.cleaned_data["password1"]) # 对密码进行哈希加密 + if commit: + user.source = 'adminsite' # 设置用户来源标记(表示通过管理后台创建) + user.save()# 保存到数据库 + return user# 返回用户对象 + + +class BlogUserChangeForm(UserChangeForm): + """ + 自定义用户信息修改表单(用于管理后台编辑用户) + 继承自 Django 自带的 UserChangeForm + """ + + class Meta: + model = BlogUser # 关联的模型类 + fields = '__all__'# 显示所有字段 + field_classes = {'username': UsernameField}# 指定用户名使用专用字段类型 + + def __init__(self, *args, **kwargs): + """ + 表单初始化方法 + 可以在这里对表单字段进行自定义设置 + """ + super().__init__(*args, **kwargs) # 调用父类初始化方法 + # 可以在这里添加自定义逻辑,如修改字段属性等 + +class BlogUserAdmin(UserAdmin): + """ + 自定义用户管理后台配置 + 继承自 Django 自带的 UserAdmin + """ + + # 指定使用的表单类 + form = BlogUserChangeForm # 编辑用户时使用的表单 + add_form = BlogUserCreationForm# 添加用户时使用的表单 + + # 管理后台列表页显示配置 list_display = ( - 'id', - 'body', - 'link_to_userinfo', - 'link_to_article', - 'is_enable', - 'creation_time') - list_display_links = ('id', 'body', 'is_enable') - list_filter = ('is_enable',) - exclude = ('creation_time', 'last_modify_time') - actions = [disable_commentstatus, enable_commentstatus] - - def link_to_userinfo(self, obj): - info = (obj.author._meta.app_label, obj.author._meta.model_name) - link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) - return format_html( - u'%s' % - (link, obj.author.nickname if obj.author.nickname else obj.author.email)) - - def link_to_article(self, obj): - info = (obj.article._meta.app_label, obj.article._meta.model_name) - link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) - return format_html( - u'%s' % (link, obj.article.title)) - - link_to_userinfo.short_description = _('User') - link_to_article.short_description = _('Article') + 'id', # 用户ID + 'nickname', # 用户昵称 + 'username',# 用户名 + 'email', # 电子邮箱 + 'last_login', # 最后登录时间 + 'date_joined', # 注册时间 + 'source')# 用户来源标记 + + # 设置哪些字段可以点击跳转到编辑页 + list_display_links = ('id', 'username') + + # 默认排序规则(按ID降序排列) + ordering = ('-id',) + From 12d9b8b1c3fa9ce233a8f0e87b424fd184974e85 Mon Sep 17 00:00:00 2001 From: zyy <1339568841@qq.com> Date: Sun, 12 Oct 2025 20:53:24 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/accounts/models.py | 24 +- src/django-master/accounts/tests.py | 120 ++++--- src/django-master/accounts/urls.py | 61 ++-- .../accounts/user_login_backend.py | 37 +- src/django-master/accounts/utils.py | 47 ++- src/django-master/accounts/views.py | 153 ++++++-- src/django-master/comments/apps.py | 12 +- src/django-master/comments/forms.py | 120 ++++++- src/django-master/comments/tests.py | 302 +++++++++++----- src/django-master/comments/views.py | 328 +++++++++++++++--- 10 files changed, 923 insertions(+), 281 deletions(-) diff --git a/src/django-master/accounts/models.py b/src/django-master/accounts/models.py index 3baddbb..0ac8c4d 100644 --- a/src/django-master/accounts/models.py +++ b/src/django-master/accounts/models.py @@ -7,29 +7,33 @@ from djangoblog.utils import get_current_site # Create your models here. - +# 自定义用户模型,继承Django内置的AbstractUser 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) - + # 获取用户详情页的绝对URL(用于模板中的{% url %}反向解析) def get_absolute_url(self): return reverse( 'blog:author_detail', kwargs={ 'author_name': self.username}) - + # 定义对象的字符串表示(Admin后台和shell中显示) def __str__(self): return self.email - + # 获取用户详情页的完整URL(包含域名,用于分享链接) def get_full_url(self): - site = get_current_site().domain + 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' + ordering = ['-id'] # 默认按ID降序排列 + verbose_name = _('user') # 单数形式名称(后台显示) + verbose_name_plural = verbose_name# 复数形式名称(后台显示) + get_latest_by = 'id'# 指定最新记录的排序字段 diff --git a/src/django-master/accounts/tests.py b/src/django-master/accounts/tests.py index 6893411..503396e 100644 --- a/src/django-master/accounts/tests.py +++ b/src/django-master/accounts/tests.py @@ -10,86 +10,106 @@ from . import utils # Create your tests here. - +# 创建测试类(继承Django的TestCase) 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) + self.assertEqual(loginresult, True)# 验证登录成功 + # 测试访问管理员后台 response = self.client.get('/admin/') - self.assertEqual(response.status_code, 200) - + self.assertEqual(response.status_code, 200)# 验证返回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.author = user# 关联超级用户 + article.category = category# 关联上面创建的分类 + article.type = 'a' # 文章类型 + article.status = 'p' # 发布状态 article.save() - + # 测试访问文章的管理URL response = self.client.get(article.get_admin_url()) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) # 验证返回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))) + # 构造验证URL path = reverse('accounts:result') url = '{path}?type=validation&id={id}&sign={sign}'.format( path=path, id=user.id, sign=sign) + # 测试访问验证URL response = self.client.get(url) - self.assertEqual(response.status_code, 200) - + self.assertEqual(response.status_code, 200) # 验证返回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" @@ -99,73 +119,76 @@ class AccountTest(TestCase): article.type = 'a' article.status = 'p' article.save() - + # 测试已登录用户访问文章管理URL 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]) - + # 测试注销后访问文章管理URL(应重定向) 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' + 'password': 'password123'# 注意这里密码与登录时使用的不同 }) self.assertIn(response.status_code, [301, 302, 200]) - + # 测试使用错误密码登录后访问文章管理URL(应重定向) response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) - + # 测试邮箱验证码验证 def test_verify_email_code(self): to_email = "admin@admin.com" - code = generate_code() - utils.set_code(to_email, code) - utils.send_verify_email(to_email, code) - + code = generate_code() # 生成验证码 + 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) - + 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") + data=dict(email="admin@admin.com") # 使用正确邮箱格式 ) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode("utf-8"), "ok") - + 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) + 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, + 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) + self.assertEqual(resp.status_code, 302) # 应重定向 # 验证用户密码是否修改成功 blog_user = BlogUser.objects.filter( @@ -173,12 +196,12 @@ class AccountTest(TestCase): ).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", + email="123@123.com",# 不存在的邮箱 code="123456", ) resp = self.client.post( @@ -186,22 +209,23 @@ class AccountTest(TestCase): data=data ) - self.assertEqual(resp.status_code, 200) - + self.assertEqual(resp.status_code, 200) # 应返回错误页面而非重定向 + # 测试忘记密码重置功能 - 验证码错误情况 def test_forget_password_email_code_error(self): code = generate_code() - utils.set_code(self.blog_user.email, code) + 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", + code="111111",# 错误验证码 ) resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.status_code, 200)# 应返回错误页面而非重定向 diff --git a/src/django-master/accounts/urls.py b/src/django-master/accounts/urls.py index 107a801..4ab17e2 100644 --- a/src/django-master/accounts/urls.py +++ b/src/django-master/accounts/urls.py @@ -1,28 +1,37 @@ -from django.urls import path -from django.urls import re_path - -from . import views -from .forms import LoginForm - +# 导入 Django 的 URL 路由工具 +from django.urls import path# 用于简单路径匹配(如 'account/result.html') +from django.urls import re_path# 用于正则表达式路径匹配(如 '^login/$') +# 导入当前应用的视图和表单 +from . import views# 导入 views.py 中的所有视图类/函数 +from .forms import LoginForm# 导入自定义登录表单(用于覆盖默认表单) +# 定义应用的命名空间(用于反向解析 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'), +# 定义 URL 路由列表 +urlpatterns = [ + # 登录视图(使用正则路径) + re_path(r'^login/$', # 正则匹配路径:以 login 结尾(斜杠必需) + views.LoginView.as_view(success_url='/'), # 使用 Django 内置 LoginView,登录成功后跳转到首页 + name='login',# URL 名称(用于模板或代码中反向解析) + kwargs={'authentication_form': LoginForm}# 传递额外参数:覆盖默认登录表单 + ), + # 注册视图 + re_path(r'^register/$', # 正则匹配路径:以 register 结尾 + views.RegisterView.as_view(success_url="/"),# 自定义注册视图,注册成功后跳转首页 + name='register'), # URL 名称 + # 登出视图 + re_path(r'^logout/$', # 正则匹配路径:以 logout 结尾 + views.LogoutView.as_view(),# 使用 Django 内置 LogoutView + name='logout'),# URL 名称 + # 账户结果页面(使用 path,非正则) + path(r'account/result.html',# 简单路径匹配(自动添加开头结尾的 ^$) + views.account_result,# 指向普通视图函数(非类视图) + name='result'),# URL 名称 + # 忘记密码视图 + re_path(r'^forget_password/$', # 正则匹配路径:以 forget_password 结尾 + views.ForgetPasswordView.as_view(),# 自定义忘记密码视图 + name='forget_password'),# URL 名称 + # 忘记密码验证码处理视图 + re_path(r'^forget_password_code/$',# 正则匹配路径:以 forget_password_code 结尾 + views.ForgetPasswordEmailCode.as_view(), # 自定义验证码处理视图 + name='forget_password_code'), # URL 名称 ] diff --git a/src/django-master/accounts/user_login_backend.py b/src/django-master/accounts/user_login_backend.py index 73cdca1..603e009 100644 --- a/src/django-master/accounts/user_login_backend.py +++ b/src/django-master/accounts/user_login_backend.py @@ -1,26 +1,57 @@ -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend +# 导入 Django 认证系统所需的模块 +from django.contrib.auth import get_user_model# 动态获取当前项目的 User 模型 +from django.contrib.auth.backends import ModelBackend# Django 默认的认证后端基类 class EmailOrUsernameModelBackend(ModelBackend): """ 允许使用用户名或邮箱登录 + 承自 Django 的 ModelBackend,重写 authenticate 和 get_user 方法。 """ def authenticate(self, request, username=None, password=None, **kwargs): + """ + 重写认证方法,支持用户名或邮箱登录。 + + 参数: + request: HttpRequest 对象(可能包含额外的认证上下文) + username: 用户输入的用户名或邮箱(前端传递的字段名固定为 username) + password: 用户输入的密码 + **kwargs: 其他可能的参数(如通过信号传递的额外参数) + + 返回: + 如果认证成功返回 User 对象,否则返回 None + """ + # 判断输入是邮箱还是用户名(通过 '@' 符号区分) if '@' in username: + # 如果是邮箱,设置查询条件为 email 字段 kwargs = {'email': username} else: + # 否则按用户名查询 kwargs = {'username': username} try: + # 尝试从数据库获取用户(使用当前项目的自定义 User 模型) user = get_user_model().objects.get(**kwargs) + # 验证密码是否正确(使用 Django 的密码哈希校验) if user.check_password(password): - return user + return user# 认证成功返回用户对象 except get_user_model().DoesNotExist: + # 用户不存在时返回 None(Django 会继续尝试其他认证后端) return None def get_user(self, username): + """ + 根据用户 ID 获取用户对象(用于 Session 认证等场景)。 + + 参数: + username: 实际是用户的 primary key(通常由 Session 存储) + + 返回: + 找到用户返回 User 对象,否则返回 None + """ try: + # 通过主键查询用户(Django 默认行为) return get_user_model().objects.get(pk=username) except get_user_model().DoesNotExist: + # 用户不存在时返回 None return None diff --git a/src/django-master/accounts/utils.py b/src/django-master/accounts/utils.py index 4b94bdf..811766a 100644 --- a/src/django-master/accounts/utils.py +++ b/src/django-master/accounts/utils.py @@ -1,12 +1,15 @@ -import typing -from datetime import timedelta +# 导入标准库模块 +import typing # 用于类型注解 +from datetime import timedelta # 用于处理时间间隔 -from django.core.cache import cache -from django.utils.translation import gettext -from django.utils.translation import gettext_lazy as _ - -from djangoblog.utils import send_email +# 导入 Django 核心组件 +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) @@ -17,9 +20,11 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")) subject: 邮件主题 code: 验证码 """ + # 构造邮件正文(使用国际化字符串,并插入动态验证码) 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) @@ -34,16 +39,42 @@ def verify(email: str, code: str) -> typing.Optional[str]: 这里的错误处理不太合理,应该采用raise抛出 否测调用方也需要对error进行处理 """ + # 从缓存获取该邮箱对应的正确验证码 cache_code = get_code(email) + # 比对用户输入的验证码和缓存中的验证码 if cache_code != code: - return gettext("Verification code error") + return gettext("Verification code error") # 返回翻译后的错误信息 + # 隐含逻辑:验证成功时返回 None(调用方需检查返回值是否为 None) def set_code(email: str, code: str): + """ + 将验证码存入缓存 + + Args: + email (str): 用户邮箱(作为缓存键) + code (str): 要存储的验证码 + + Note: + 验证码有效期由全局变量 _code_ttl 控制(5分钟) + """ + # 使用 Django 缓存设置键值对,并指定过期时间(转换为秒) """设置code""" cache.set(email, code, _code_ttl.seconds) def get_code(email: str) -> typing.Optional[str]: + """ + 从缓存获取验证码 + + Args: + email (str): 用户邮箱(缓存键) + + Returns: + typing.Optional[str]: + - 存在则返回验证码字符串 + - 不存在或过期返回 None + """ + # 直接调用 Django 缓存的 get 方法 """获取code""" return cache.get(email) diff --git a/src/django-master/accounts/views.py b/src/django-master/accounts/views.py index ae67aec..b168f68 100644 --- a/src/django-master/accounts/views.py +++ b/src/django-master/accounts/views.py @@ -1,59 +1,92 @@ +# 导入日志模块,用于记录运行时的信息和错误 import logging +# Django 国际化工具,`gettext_lazy` 用于延迟翻译(适合模块级字符串) from django.utils.translation import gettext_lazy as _ +# Django 配置项,用于访问 settings.py 中的设置 from django.conf import settings +# Django 认证系统核心模块 from django.contrib import auth +# 认证相关常量(如重定向字段名) from django.contrib.auth import REDIRECT_FIELD_NAME +# 获取当前用户模型的快捷方式 from django.contrib.auth import get_user_model +# 用户登出功能 from django.contrib.auth import logout +# Django 内置的认证表单(如登录表单) from django.contrib.auth.forms import AuthenticationForm +# 密码哈希工具 from django.contrib.auth.hashers import make_password +# HTTP 响应类(重定向、禁止访问等) from django.http import HttpResponseRedirect, HttpResponseForbidden +# HTTP 请求和响应的类型提示(可选,用于类型检查) from django.http.request import HttpRequest from django.http.response import HttpResponse +# 快捷函数(如获取对象或返回 404) from django.shortcuts import get_object_or_404 +# 渲染模板的快捷方式 from django.shortcuts import render +# URL 反转工具(通过名称生成 URL) from django.urls import reverse +# 视图装饰器工具 from django.utils.decorators import method_decorator +# URL 安全验证工具(防止重定向攻击) from django.utils.http import url_has_allowed_host_and_scheme +# 基础视图类 from django.views import View +# 缓存控制装饰器(禁用缓存) from django.views.decorators.cache import never_cache +# CSRF 防护装饰器 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 - +# 初始化日志记录器(__name__ 表示当前模块名) logger = logging.getLogger(__name__) # Create your views here. - +# 注册视图类(继承自 FormView,处理表单提交) class RegisterView(FormView): + # 指定使用的表单类 form_class = RegisterForm template_name = 'account/registration_form.html' - + # 使用装饰器确保视图禁用缓存(never_cache)并启用 CSRF 防护 @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): + # 调用父类方法处理请求 return super(RegisterView, self).dispatch(*args, **kwargs) - + # 表单验证通过后的处理逻辑 def form_valid(self, form): + # 再次检查表单有效性(冗余,因为 FormView 已验证) if form.is_valid(): - user = form.save(False) - user.is_active = False - user.source = 'Register' - user.save(True) + # 保存用户对象(但暂不激活 is_active=False) + user = form.save(False) + user.is_active = False# 用户需验证邮箱后才能登录 + user.source = 'Register' # 标记用户来源为注册 + user.save(True)# 实际保存到数据库 + + # 获取当前站点域名(用于生成验证链接) site = get_current_site().domain + # 生成签名(双重 SHA256 哈希,用于验证链接安全性) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) - + # 开发环境使用本地地址(避免因域名未配置导致链接失效) if settings.DEBUG: site = '127.0.0.1:8000' + + # 生成验证结果的 URL(如 /account/result/?type=validation&id=1&sign=abc123) 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 = """
请点击下面链接验证您的邮箱
@@ -64,92 +97,123 @@ class RegisterView(FormView): 如果上面链接无法打开,请将此链接复制至浏览器。 {url} """.format(url=url) + # 发送验证邮件 send_email( emailto=[ - user.email, + user.email, # 收件人列表 ], - title='验证您的电子邮箱', - content=content) - + title='验证您的电子邮箱', # 邮件标题 + content=content)# 邮件正文 + # 重定向到注册结果页面(附带用户 ID) url = reverse('accounts:result') + \ '?type=register&id=' + str(user.id) return HttpResponseRedirect(url) else: + # 表单无效时重新渲染表单(显示错误信息) return self.render_to_response({ 'form': form }) - +# 登出视图,继承自RedirectView,重定向到登录页面 class LogoutView(RedirectView): + # 登出后重定向的URL url = '/login/' - + # 使用never_cache装饰器确保视图不会被缓存 @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): + # 调用父类的dispatch方法处理请求 return super(LogoutView, self).dispatch(request, *args, **kwargs) - + # 处理GET请求 def get(self, request, *args, **kwargs): + # 执行登出操作 logout(request) + # 删除侧边栏缓存 delete_sidebar_cache() + # 调用父类的get方法完成重定向 return super(LogoutView, self).get(request, *args, **kwargs) - +# 登录视图,继承自FormView class LoginView(FormView): + # 使用的表单类 form_class = LoginForm + # 模板文件路径 template_name = 'account/login.html' + # 登录成功后跳转的URL 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) + # 使用多个装饰器装饰dispatch方法 + @method_decorator(sensitive_post_parameters('password'))# 标记密码参数为敏感信息 + @method_decorator(csrf_protect) # 启用CSRF保护 + @method_decorator(never_cache)# 禁止缓存 def dispatch(self, request, *args, **kwargs): - + # 调用父类的dispatch方法处理请求 return super(LoginView, self).dispatch(request, *args, **kwargs) + # 获取模板上下文数据 def get_context_data(self, **kwargs): + # 从GET参数中获取重定向URL redirect_to = self.request.GET.get(self.redirect_field_name) + # 如果不存在则设置为根路径 if redirect_to is None: redirect_to = '/' + # 将重定向URL添加到上下文 kwargs['redirect_to'] = redirect_to + # 调用父类方法获取其他上下文数据 return super(LoginView, self).get_context_data(**kwargs) - + # 表单验证通过后的处理 def form_valid(self, form): + # 重新创建认证表单(这里可能有逻辑问题,因为form已经传入) 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) # return HttpResponseRedirect('/') else: + # 表单无效,重新渲染表单并显示错误 return self.render_to_response({ 'form': form }) - + # 获取成功后的跳转URL def get_success_url(self): + # 从POST参数中获取重定向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()]): + # 如果不安全则使用默认成功URL redirect_to = self.success_url return redirect_to - +# 账户操作结果页面(如注册成功、邮箱验证等) def account_result(request): + # 从GET参数中获取类型和用户ID type = request.GET.get('type') id = request.GET.get('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 = ''' @@ -157,48 +221,69 @@ def account_result(request): ''' 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('/') - +# 忘记密码视图,继承自FormView 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}) - +# 忘记密码验证码发送视图,继承自View class ForgetPasswordEmailCode(View): - + # 处理POST请求 def post(self, request: HttpRequest): + # 验证表单 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") diff --git a/src/django-master/comments/apps.py b/src/django-master/comments/apps.py index ff01b77..32029ed 100644 --- a/src/django-master/comments/apps.py +++ b/src/django-master/comments/apps.py @@ -1,5 +1,11 @@ from django.apps import AppConfig + - -class CommentsConfig(AppConfig): - name = 'comments' +class AccountsConfig(AppConfig): + """ + Accounts 应用的配置类。 + 功能: + 1. 定义应用名称(供 Django 内部识别)。 + 2. 可在此处覆盖 ready() 方法以注册信号等。 + """ + name = 'accounts'# 必须与项目中的应用目录名完全一致 diff --git a/src/django-master/comments/forms.py b/src/django-master/comments/forms.py index e83737d..a0b5769 100644 --- a/src/django-master/comments/forms.py +++ b/src/django-master/comments/forms.py @@ -1,13 +1,117 @@ from django import forms -from django.forms import ModelForm +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 -from .models import Comment +# 登录表单,继承自Django内置的AuthenticationForm +class LoginForm(AuthenticationForm): + def __init__(self, *args, **kwargs): + 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"}) +# 注册表单,继承自Django内置的UserCreationForm +class RegisterForm(UserCreationForm): + def __init__(self, *args, **kwargs): + super(RegisterForm, self).__init__(*args, **kwargs) + # 自定义用户名、邮箱和密码字段的HTML属性 + 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 CommentForm(ModelForm): - parent_comment_id = forms.IntegerField( - widget=forms.HiddenInput, required=False) +# 忘记密码表单(验证邮箱和验证码) +class ForgetPasswordForm(forms.Form): + new_password1 = forms.CharField( + label=_("New password"), + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("New password") + } + ), + ) + # 新密码字段2(用于确认) + new_password2 = forms.CharField( + label="确认密码", + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("Confirm password") + } + ), + ) + # 邮箱字段 + email = forms.EmailField( + label='邮箱', + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Email") + } + ), + ) + # 验证码字段 + code = forms.CharField( + label=_('Code'), + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Code") + } + ), + ) + # 验证两次输入的密码是否一致,并检查密码强度 + def clean_new_password2(self): + password1 = self.data.get("new_password1") + password2 = self.data.get("new_password2") + if password1 and password2 and password1 != password2: + raise ValidationError(_("passwords do not match")) + password_validation.validate_password(password2)# 使用Django的密码验证器 - class Meta: - model = Comment - fields = ['body'] + 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") + 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'), + ) diff --git a/src/django-master/comments/tests.py b/src/django-master/comments/tests.py index 2a7f55f..503396e 100644 --- a/src/django-master/comments/tests.py +++ b/src/django-master/comments/tests.py @@ -1,109 +1,231 @@ -from django.test import Client, RequestFactory, TransactionTestCase +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 Category, Article -from comments.models import Comment -from comments.templatetags.comments_tags import * -from djangoblog.utils import get_max_articleid_commentid +from blog.models import Article, Category +from djangoblog.utils import * +from . import utils # Create your tests here. - -class CommentsTest(TransactionTestCase): +# 创建测试类(继承Django的TestCase) +class AccountTest(TestCase): + # 测试初始化方法(每个测试方法运行前都会执行) def setUp(self): + # 初始化测试客户端(模拟浏览器请求) self.client = Client() + # 初始化请求工厂(用于生成请求对象) self.factory = RequestFactory() - from blog.models import BlogSettings - value = BlogSettings() - value.comment_need_review = True - value.save() - - self.user = BlogUser.objects.create_superuser( + # 创建一个普通测试用户 + 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="liangliangyy1") - - def update_article_comment_status(self, article): - comments = article.comment_set.all() - for comment in comments: - comment.is_enable = True - comment.save() - - def test_validate_comment(self): - self.client.login(username='liangliangyy1', password='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)# 验证返回200状态码 + # 创建测试分类 category = Category() - category.name = "categoryccc" + 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() + # 测试访问文章的管理URL + response = self.client.get(article.get_admin_url()) + self.assertEqual(response.status_code, 200) # 验证返回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))) + # 构造验证URL + path = reverse('accounts:result') + url = '{path}?type=validation&id={id}&sign={sign}'.format( + path=path, id=user.id, sign=sign) + # 测试访问验证URL + response = self.client.get(url) + self.assertEqual(response.status_code, 200) # 验证返回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.title = "nicetitleccc" - article.body = "nicecontentccc" - article.author = self.user article.category = category + article.title = "nicetitle333" + article.body = "nicecontentttt" + article.author = user + article.type = 'a' article.status = 'p' article.save() + # 测试已登录用户访问文章管理URL + 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]) + # 测试注销后访问文章管理URL(应重定向) + 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]) + # 测试使用错误密码登录后访问文章管理URL(应重定向) + 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)# 应返回错误页面而非重定向 - comment_url = reverse( - 'comments:postcomment', kwargs={ - 'article_id': article.id}) - - response = self.client.post(comment_url, - { - 'body': '123ffffffffff' - }) - - self.assertEqual(response.status_code, 302) - - article = Article.objects.get(pk=article.pk) - self.assertEqual(len(article.comment_list()), 0) - self.update_article_comment_status(article) - - self.assertEqual(len(article.comment_list()), 1) - - response = self.client.post(comment_url, - { - 'body': '123ffffffffff', - }) - - self.assertEqual(response.status_code, 302) - - article = Article.objects.get(pk=article.pk) - self.update_article_comment_status(article) - self.assertEqual(len(article.comment_list()), 2) - parent_comment_id = article.comment_list()[0].id - - response = self.client.post(comment_url, - { - 'body': ''' - # Title1 - - ```python - import os - ``` - - [url](https://www.lylinux.net/) - - [ddd](http://www.baidu.com) - - - ''', - 'parent_comment_id': parent_comment_id - }) - - self.assertEqual(response.status_code, 302) - self.update_article_comment_status(article) - article = Article.objects.get(pk=article.pk) - self.assertEqual(len(article.comment_list()), 3) - comment = Comment.objects.get(id=parent_comment_id) - tree = parse_commenttree(article.comment_list(), comment) - self.assertEqual(len(tree), 1) - data = show_comment_item(comment, True) - self.assertIsNotNone(data) - s = get_max_articleid_commentid() - self.assertIsNotNone(s) - - from comments.utils import send_comment_email - send_comment_email(comment) diff --git a/src/django-master/comments/views.py b/src/django-master/comments/views.py index ad9b2b9..b168f68 100644 --- a/src/django-master/comments/views.py +++ b/src/django-master/comments/views.py @@ -1,63 +1,289 @@ -# Create your views here. -from django.core.exceptions import ValidationError -from django.http import HttpResponseRedirect +# 导入日志模块,用于记录运行时的信息和错误 +import logging +# Django 国际化工具,`gettext_lazy` 用于延迟翻译(适合模块级字符串) +from django.utils.translation import gettext_lazy as _ +# Django 配置项,用于访问 settings.py 中的设置 +from django.conf import settings +# Django 认证系统核心模块 +from django.contrib import auth +# 认证相关常量(如重定向字段名) +from django.contrib.auth import REDIRECT_FIELD_NAME +# 获取当前用户模型的快捷方式 +from django.contrib.auth import get_user_model +# 用户登出功能 +from django.contrib.auth import logout +# Django 内置的认证表单(如登录表单) +from django.contrib.auth.forms import AuthenticationForm +# 密码哈希工具 +from django.contrib.auth.hashers import make_password +# HTTP 响应类(重定向、禁止访问等) +from django.http import HttpResponseRedirect, HttpResponseForbidden +# HTTP 请求和响应的类型提示(可选,用于类型检查) +from django.http.request import HttpRequest +from django.http.response import HttpResponse +# 快捷函数(如获取对象或返回 404) from django.shortcuts import get_object_or_404 +# 渲染模板的快捷方式 +from django.shortcuts import render +# URL 反转工具(通过名称生成 URL) +from django.urls import reverse +# 视图装饰器工具 from django.utils.decorators import method_decorator +# URL 安全验证工具(防止重定向攻击) +from django.utils.http import url_has_allowed_host_and_scheme +# 基础视图类 +from django.views import View +# 缓存控制装饰器(禁用缓存) +from django.views.decorators.cache import never_cache +# CSRF 防护装饰器 from django.views.decorators.csrf import csrf_protect -from django.views.generic.edit import FormView - -from accounts.models import BlogUser -from blog.models import Article -from .forms import CommentForm -from .models import Comment - +# 敏感参数标记(如密码字段) +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 +# 初始化日志记录器(__name__ 表示当前模块名) +logger = logging.getLogger(__name__) -class CommentPostView(FormView): - form_class = CommentForm - template_name = 'blog/article_detail.html' +# Create your views here. +# 注册视图类(继承自 FormView,处理表单提交) +class RegisterView(FormView): + # 指定使用的表单类 + form_class = RegisterForm + template_name = 'account/registration_form.html' + # 使用装饰器确保视图禁用缓存(never_cache)并启用 CSRF 防护 @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): - return super(CommentPostView, self).dispatch(*args, **kwargs) + # 调用父类方法处理请求 + return super(RegisterView, self).dispatch(*args, **kwargs) + # 表单验证通过后的处理逻辑 + def form_valid(self, form): + # 再次检查表单有效性(冗余,因为 FormView 已验证) + if form.is_valid(): + # 保存用户对象(但暂不激活 is_active=False) + user = form.save(False) + user.is_active = False# 用户需验证邮箱后才能登录 + user.source = 'Register' # 标记用户来源为注册 + user.save(True)# 实际保存到数据库 + # 获取当前站点域名(用于生成验证链接) + site = get_current_site().domain + # 生成签名(双重 SHA256 哈希,用于验证链接安全性) + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + # 开发环境使用本地地址(避免因域名未配置导致链接失效) + if settings.DEBUG: + site = '127.0.0.1:8000' + + # 生成验证结果的 URL(如 /account/result/?type=validation&id=1&sign=abc123) + 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 = """ +请点击下面链接验证您的邮箱
+ + {url} + + 再次感谢您! +请点击下面链接验证您的邮箱
- {url} +class CommentPostView(FormView): + form_class = CommentForm + template_name = 'blog/article_detail.html' - 再次感谢您! -^~NCdZ=6?u^CrTh2)
zvh&=7Ojult#e2Gch0itZAk(7wlBY_rv%#3=n(hF6f=9;V0X+EO3(;Q=6J0ow8J>I1
zg;})E PsMlGn4
zik~0PxtPw+c!>=RZmwh;mFKx_LV)g0?&GD8D|%+gm<4e&j(itc^z;k?(PG^XA7g;x
zuoE-9#2M~`R2key*Vs!+FpvFdK%f$2PaLqr5dtmF{J~_~8RYK|)<;C0Uta-uj}+Qq
zhZ$)=9wpV`Md8ls&%x%!j=07PwmEf4B9GQ06Rf)K*$zva;MdcxyvWnoCn0;y(g0-K
zf&6+IB7>Xp*3T{#_nF@EuTsOvy?h0V`BVdzWKn;lG&c{-rQYr`MxQAi6wM@c76;lf
zr5~%PmISl( $y=<#cmAje66qjQ-kxv0~
zp^R;hF{4|OE&Rl>^88ea+)ApC7IA%k!KtVuARWn1XrdLl{zvO7w64_7_hiH+|
z9N$6DHQ`p6=#Pl}WFVpNwDrx8dCrmE_LqBMv0jD1Qa&x7$vf!17(=iiOzL6lp9Gc}
zYm;Zqc;Ts@UCJ;OXe(c)+Rj<98!IOhlZ)*-@tQiZT!;&pFMU!dUP0bMRt^g%e~~oBP3K3ui)Ua$xYDF)vr$4N}?jS=~z6f@5hx-P9x%
zF(@kdA!*O%HMGJkmp!a1PZc0cEw0NQu^;b6_>&3=5&6kHwE_+8=9#2k%n{VJq{baL
zyOJ2d?3MbXkcH|KZ(Le`vkHNtj*U6y;B2uy;Z50FeTxO~eTew%fri=oQ~{zIx4v6X
z9df|2Zx>!%IPnf8aY#-T*v ~GT84fo@-=vJWA<~HK(Ms-kEdL(bku$Y%|u8w>INd*Ju@V`~|->
zMa?A&?nUA2rG(KdnC}qBlQJa(9KugB286w?A>z!;R~bZPuU8|cA_HJWZL!_
C-G&eU71mq6>