diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f980ab9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,7 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/doc/2315304410-三班-鞠泆男 软件界面设计说明书.docx b/doc/2315304410-三班-鞠泆男 软件界面设计说明书.docx new file mode 100644 index 0000000..970c291 Binary files /dev/null and b/doc/2315304410-三班-鞠泆男 软件界面设计说明书.docx differ diff --git a/doc/2315304410三班鞠泆男第五周作业-软件数据模型设计说明书.docx b/doc/2315304410三班鞠泆男第五周作业-软件数据模型设计说明书.docx new file mode 100644 index 0000000..86dbe16 Binary files /dev/null and b/doc/2315304410三班鞠泆男第五周作业-软件数据模型设计说明书.docx differ diff --git a/doc/2315304412-陆欣颖-软件数据模型设计说明书.docx b/doc/2315304412-陆欣颖-软件数据模型设计说明书.docx new file mode 100644 index 0000000..86dbe16 Binary files /dev/null and b/doc/2315304412-陆欣颖-软件数据模型设计说明书.docx differ diff --git a/doc/2315304412-陆欣颖-软件界面设计说明书.docx b/doc/2315304412-陆欣颖-软件界面设计说明书.docx new file mode 100644 index 0000000..970c291 Binary files /dev/null and b/doc/2315304412-陆欣颖-软件界面设计说明书.docx differ diff --git a/doc/2315304420-四班-张悦 软件界面设计说明书.docx b/doc/2315304420-四班-张悦 软件界面设计说明书.docx new file mode 100644 index 0000000..970c291 Binary files /dev/null and b/doc/2315304420-四班-张悦 软件界面设计说明书.docx differ diff --git a/doc/2315304422-四班-郑盈盈 软件界面设计说明书(1).docx b/doc/2315304422-四班-郑盈盈 软件界面设计说明书(1).docx new file mode 100644 index 0000000..06b5cf4 Binary files /dev/null and b/doc/2315304422-四班-郑盈盈 软件界面设计说明书(1).docx differ diff --git a/doc/个人贡献.docx b/doc/个人贡献.docx new file mode 100644 index 0000000..e8d520d Binary files /dev/null and b/doc/个人贡献.docx differ diff --git a/doc/第五周作业-软件数据模型设计说明书.docx b/doc/第五周作业-软件数据模型设计说明书.docx new file mode 100644 index 0000000..86dbe16 Binary files /dev/null and b/doc/第五周作业-软件数据模型设计说明书.docx differ diff --git a/doc/第五周小组任务个人完成部分.docx b/doc/第五周小组任务个人完成部分.docx new file mode 100644 index 0000000..2709b83 Binary files /dev/null and b/doc/第五周小组任务个人完成部分.docx differ diff --git a/src/django-master/accounts/admin.py b/src/django-master/accounts/admin.py index 32e483c..7409b10 100644 --- a/src/django-master/accounts/admin.py +++ b/src/django-master/accounts/admin.py @@ -1,52 +1,224 @@ -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 +<<<<<<< HEAD +#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 _ # 国际化翻译函数 +======= +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 _ +>>>>>>> LXY_branch +<<<<<<< HEAD +# 本地应用导入 # Register your models here. +<<<<<<< HEAD +from .models import BlogUser # 导入自定义用户模型 +======= +# 导入自定义用户模型 from .models import BlogUser +>>>>>>> JYN_branch class BlogUserCreationForm(forms.ModelForm): + """ +<<<<<<< HEAD + 自定义用户创建表单(用于管理后台添加新用户) + 继承自 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',)# 创建用户时显示的字段(这里只显示email字段) +======= + 自定义用户创建表单,用于在管理员界面添加新用户 + 继承自ModelForm,提供密码验证功能 + """ + # 密码字段,使用PasswordInput小部件确保输入不可见 +======= +from .models import BlogUser# 导入当前应用下的BlogUser模型(自定义用户模型) + + +class BlogUserCreationForm(forms.ModelForm): # 定义两个密码字段,使用PasswordInput小部件隐藏输入 +>>>>>>> LXY_branch password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) + # 确认密码字段,用于验证两次输入的密码是否一致 password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) class Meta: +<<<<<<< HEAD + # 指定关联的模型 model = BlogUser + # 表单中包含的字段,这里只显示邮箱 fields = ('email',) +>>>>>>> JYN_branch def clean_password2(self): + """ + 验证两次输入的密码是否一致 +<<<<<<< HEAD + Django 表单验证方法,方法名必须以 clean_ 开头 + """ +======= + model = BlogUser# 关联的模型是BlogUser + fields = ('email',) # 表单中显示的字段(仅邮箱,密码单独定义) + + def clean_password2(self):# 验证两个密码是否一致 +>>>>>>> LXY_branch # Check that the two password entries match + password1 = self.cleaned_data.get("password1")# 获取第一次输入的密码 + password2 = self.cleaned_data.get("password2")# 获取第二次输入的密码 + # 如果两次密码不一致,抛出验证错误 +======= + 这是Django表单验证机制的一部分,方法名以clean_开头 + """ password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") + # 检查密码是否存在且不一致 +>>>>>>> JYN_branch if password1 and password2 and password1 != password2: - raise forms.ValidationError(_("passwords do not match")) - return password2 +<<<<<<< HEAD + raise forms.ValidationError(_("passwords do not match"))# 错误信息(支持国际化) + return password2# 返回验证后的值 + def save(self, commit=True): + """ +<<<<<<< HEAD + 重写保存方法,在保存用户前处理密码哈希 + """ # 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# 返回用户对象 +======= + 重写保存方法,确保密码以哈希形式存储 + 而不是明文存储 + """ + # 先调用父类方法获取用户对象,但不立即保存到数据库 user = super().save(commit=False) + # 使用set_password方法对密码进行哈希处理 user.set_password(self.cleaned_data["password1"]) if commit: + # 标记用户来源为管理员站点 user.source = 'adminsite' user.save() +======= + 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"]) # 哈希处理密码 + if commit: + user.source = 'adminsite'# 标记用户来源为“后台管理” + user.save()# 提交到数据库 +>>>>>>> LXY_branch return user +>>>>>>> JYN_branch class BlogUserChangeForm(UserChangeForm): + """ +<<<<<<< HEAD + 自定义用户信息修改表单(用于管理后台编辑用户) + 继承自 Django 自带的 UserChangeForm + """ + + class Meta: +<<<<<<< HEAD + 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', # 用户ID + 'nickname', # 用户昵称 + 'username',# 用户名 + 'email', # 电子邮箱 + 'last_login', # 最后登录时间 + 'date_joined', # 注册时间 + 'source')# 用户来源标记 + + # 设置哪些字段可以点击跳转到编辑页 + list_display_links = ('id', 'username') + + # 默认排序规则(按ID降序排列) + ordering = ('-id',) + +======= + 自定义用户修改表单,用于在管理员界面编辑用户信息 + 继承自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管理后台配置用户模型的显示和操作 + 继承自Django内置的UserAdmin + """ + # 指定编辑用户时使用的表单 form = BlogUserChangeForm + # 指定添加用户时使用的表单 add_form = BlogUserCreationForm + # 列表页面显示的字段 +======= + model = BlogUser # 关联的模型是BlogUser + fields = '__all__'# 显示模型的所有字段 + field_classes = {'username': UsernameField}# 为用户名字段指定类(保持Django原生逻辑) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs)# 调用父类的初始化方法 + + +class BlogUserAdmin(UserAdmin): + form = BlogUserChangeForm# 指定修改用户时使用的表单 + add_form = BlogUserCreationForm# 指定创建用户时使用的表单 +>>>>>>> LXY_branch list_display = ( 'id', 'nickname', @@ -54,6 +226,16 @@ class BlogUserAdmin(UserAdmin): 'email', 'last_login', 'date_joined', - 'source') +<<<<<<< HEAD + 'source' + ) + # 列表页面中可点击跳转的字段 list_display_links = ('id', 'username') + # 排序方式,按id降序排列(最新的用户在前) ordering = ('-id',) +>>>>>>> JYN_branch +======= + 'source') + list_display_links = ('id', 'username')# 列表页中可点击跳转的字段 + ordering = ('-id',)# 列表页的排序方式(按ID倒序) +>>>>>>> LXY_branch diff --git a/src/django-master/accounts/apps.py b/src/django-master/accounts/apps.py index 9b3fc5a..5129d43 100644 --- a/src/django-master/accounts/apps.py +++ b/src/django-master/accounts/apps.py @@ -1,5 +1,30 @@ +<<<<<<< HEAD from django.apps import AppConfig - + class AccountsConfig(AppConfig): + """ +<<<<<<< HEAD + Accounts 应用的配置类。 + 功能: + 1. 定义应用名称(供 Django 内部识别)。 + 2. 可在此处覆盖 ready() 方法以注册信号等。 + """ + name = 'accounts'# 必须与项目中的应用目录名完全一致 +======= + Django应用配置类,用于定义'accounts'应用的元数据 + + 每个Django应用都需要一个配置类,用于设置应用的各种属性和行为 + 通常放在应用目录下的apps.py文件中 + """ + # 应用的名称,必须与应用目录名一致 + # 这个名称会被Django用来识别和管理应用 name = 'accounts' +>>>>>>> JYN_branch +======= +from django.apps import AppConfig#导入 Django 框架中用于应用配置的 AppConfig 类,这是 Django 应用配置的核心类 + + +class AccountsConfig(AppConfig):#定义 AccountsConfig 类,继承自 AppConfig,用于对 accounts 应用进行自定义配置 + name = 'accounts'#指定该应用的名称为 accounts,Django 会通过这个名称来识别和管理该应用 +>>>>>>> LXY_branch diff --git a/src/django-master/accounts/forms.py b/src/django-master/accounts/forms.py index fce4137..df3e0f8 100644 --- a/src/django-master/accounts/forms.py +++ b/src/django-master/accounts/forms.py @@ -1,26 +1,95 @@ +<<<<<<< HEAD +from django import forms #导入 Django 表单模块,用于创建自定义表单类。 + +from django.contrib.auth import get_user_model, password_validation #get_user_model 用于获取项目中自定义的用户模型(遵循 Django 推荐的用户模型扩展方式)。 + +<<<<<<< HEAD + +from django.contrib.auth.forms import AuthenticationForm, UserCreationForm #UserCreationForm:导入 Django 内置的认证表单(AuthenticationForm 用于登录)和用户创建表单(UserCreationForm 用于注册),作为自定义表单的基类。 +from django.core.exceptions import ValidationError #导入 Django 的验证异常类,用于在表单验证时抛出自定义错误。 +from django.forms import widgets #导入 Django 表单的小部件模块,用于自定义表单字段的渲染样式(如输入框类型、样式类等)。 + +from django.utils.translation import gettext_lazy as _ #导入 Django 的延迟翻译函数,用于表单字段标签、错误提示的国际化翻译。 + +from . import utils #导入当前应用下的 utils 模块(假设包含工具类或函数,此处代码未展示具体使用)。 +from .models import BlogUser #导入当前应用下定义的 BlogUser 模型(自定义用户模型)。 + + +class LoginForm(AuthenticationForm): #继承 Django 内置的 AuthenticationForm,自定义登录表单的样式和逻辑。 + def __init__(self, *args, **kwargs): #重写构造方法,用于自定义表单字段的小部件属性。 + + super(LoginForm, self).__init__(*args, **kwargs) #调用父类构造方法,确保基础功能正常。 + +======= +# 登录表单,继承自Django内置的AuthenticationForm +class LoginForm(AuthenticationForm): + def __init__(self, *args, **kwargs): + super(LoginForm, self).__init__(*args, **kwargs) +>>>>>>> 5d714be542986b7f935eb0ec4df9b0918e75eeca + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) #为用户名字段设置文本输入小部件,定义占位符和 Bootstrap 样式类(form-control)。 +======= from django import forms from django.contrib.auth import get_user_model, password_validation from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.core.exceptions import ValidationError from django.forms import widgets from django.utils.translation import gettext_lazy as _ -from . import utils -from .models import BlogUser +from . import utils # 导入自定义工具模块,可能用于验证码验证等功能 +from .models import BlogUser # 导入自定义用户模型 class LoginForm(AuthenticationForm): + """ + 自定义登录表单,继承自Django内置的AuthenticationForm + 用于处理用户登录验证,主要扩展了表单字段的样式 + """ def __init__(self, *args, **kwargs): + # 调用父类构造方法初始化表单 super(LoginForm, self).__init__(*args, **kwargs) + + # 自定义用户名输入框:添加占位符和CSS类 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) + + # 自定义密码输入框:添加占位符和CSS类 +>>>>>>> 8b27cdad9a9ccc84febce3bcf1d211ed109f96f2 self.fields['password'].widget = widgets.PasswordInput( +<<<<<<< HEAD + attrs={'placeholder': "password", "class": "form-control"}) #为密码字段设置密码输入小部件,同样定义占位符和样式类。 + +<<<<<<< HEAD +======= attrs={'placeholder': "password", "class": "form-control"}) +#自定义登录表单,在__init__方法中设置username(文本输入,占位符、form-control样式)和password(密码输入,占位符、form-control样式)字段的前端显示样式。 +>>>>>>> LXY_branch + +<<<<<<< HEAD +class RegisterForm(UserCreationForm): #继承 Django 内置的 UserCreationForm,自定义注册表单的字段、样式和验证逻辑。 +======= +<<<<<<< HEAD +# 注册表单,继承自Django内置的UserCreationForm +class RegisterForm(UserCreationForm): +>>>>>>> 5d714be542986b7f935eb0ec4df9b0918e75eeca +======= class RegisterForm(UserCreationForm): + """ + 自定义注册表单,继承自Django内置的UserCreationForm + 用于处理用户注册逻辑,包含用户名、邮箱和密码验证 + """ +>>>>>>> 8b27cdad9a9ccc84febce3bcf1d211ed109f96f2 +>>>>>>> JYN_branch def __init__(self, *args, **kwargs): + # 调用父类构造方法初始化表单 super(RegisterForm, self).__init__(*args, **kwargs) +<<<<<<< HEAD + # 自定义用户名、邮箱和密码字段的HTML属性 +======= + # 自定义各字段的输入控件,添加样式和占位符 +>>>>>>> JYN_branch self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) self.fields['email'].widget = widgets.EmailInput( @@ -29,19 +98,41 @@ class RegisterForm(UserCreationForm): attrs={'placeholder': "password", "class": "form-control"}) self.fields['password2'].widget = widgets.PasswordInput( attrs={'placeholder': "repeat password", "class": "form-control"}) - +<<<<<<< HEAD + # 验证邮箱唯一性 +======= +#__init__方法中设置username(文本输入)、email(邮箱输入)、password1和password2(密码输入)字段的占位符与form-control样式 +>>>>>>> LXY_branch def clean_email(self): + """ + 邮箱验证方法:检查邮箱是否已被注册 + 表单验证机制中,以clean_为前缀的方法会自动被调用 + """ email = self.cleaned_data['email'] + # 检查该邮箱是否已存在于数据库中 if get_user_model().objects.filter(email=email).exists(): - raise ValidationError(_("email already exists")) + raise ValidationError(_("email already exists")) # 抛出验证错误 return email - +<<<<<<< HEAD + # 指定关联的用户模型和表单字段 + class Meta: + model = get_user_model() # 使用项目配置的用户模型(可能是自定义的BlogUser) + fields = ("username", "email") # 注册表单中显示的字段 +======= +#clean_email方法验证邮箱是否已被注册,若存在则抛出“邮箱已存在”的验证错误 class Meta: model = get_user_model() fields = ("username", "email") +#Meta类指定关联模型为自定义用户模型,表单字段包含username和email +>>>>>>> LXY_branch - +# 忘记密码表单(验证邮箱和验证码) class ForgetPasswordForm(forms.Form): + """ + 忘记密码表单,用于用户重置密码的流程 + 包含新密码、确认密码、邮箱和验证码字段 + """ + # 新密码字段 new_password1 = forms.CharField( label=_("New password"), widget=forms.PasswordInput( @@ -51,7 +142,12 @@ class ForgetPasswordForm(forms.Form): } ), ) +<<<<<<< HEAD + # 新密码字段2(用于确认) +======= + # 确认新密码字段 +>>>>>>> JYN_branch new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -61,7 +157,12 @@ class ForgetPasswordForm(forms.Form): } ), ) +<<<<<<< HEAD + # 邮箱字段 +======= + # 邮箱字段(用于验证用户身份) +>>>>>>> JYN_branch email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -71,7 +172,12 @@ class ForgetPasswordForm(forms.Form): } ), ) +<<<<<<< HEAD + # 验证码字段 +======= + # 验证码字段(用于身份验证) +>>>>>>> JYN_branch code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -81,37 +187,80 @@ class ForgetPasswordForm(forms.Form): } ), ) - +<<<<<<< HEAD + # 验证两次输入的密码是否一致,并检查密码强度 +======= +#定义new_password1(新密码,密码输入)、new_password2(确认密码,密码输入)、email(邮箱,文本输入)、code(验证码,文本输入)字段,均设置form-control样式和占位符。 +>>>>>>> LXY_branch 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) +<<<<<<< HEAD + password_validation.validate_password(password2)# 使用Django的密码验证器 +======= + + # 使用Django内置的密码验证器验证密码强度 + password_validation.validate_password(password2) + +>>>>>>> JYN_branch return password2 - +<<<<<<< HEAD + # 验证邮箱是否已注册 +======= +# clean_new_password2方法验证两次新密码是否一致,并对密码进行有效性校验 +>>>>>>> LXY_branch def clean_email(self): + """验证邮箱是否已注册""" user_email = self.cleaned_data.get("email") - if not BlogUser.objects.filter( - email=user_email - ).exists(): - # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 + # 检查该邮箱是否存在于系统中 + if not BlogUser.objects.filter(email=user_email).exists(): + # 提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露) raise ValidationError(_("email does not exist")) return user_email +<<<<<<< HEAD + # 验证用户输入的验证码是否正确 +======= +# clean_email方法验证邮箱是否已注册(基于BlogUser模型),未注册则抛出“邮箱不存在”的验证错误 +>>>>>>> LXY_branch def clean_code(self): + """验证验证码是否有效""" code = self.cleaned_data.get("code") +<<<<<<< HEAD + error = utils.verify(# 调用工具函数验证验证码 +======= + # 调用工具函数验证邮箱和验证码是否匹配 error = utils.verify( +>>>>>>> JYN_branch email=self.cleaned_data.get("email"), code=code, ) if error: - raise ValidationError(error) + raise ValidationError(error) # 验证码无效时抛出错误 return code +<<<<<<< HEAD +# 忘记密码功能中的验证码发送表单(仅需邮箱字段) +======= +#clean_code方法调用工具方法utils.verify验证验证码有效性,无效则抛出错误 +>>>>>>> LXY_branch class ForgetPasswordCodeForm(forms.Form): + """ + 发送密码重置验证码的表单 + 仅包含邮箱字段,用于提交需要重置密码的邮箱 + """ email = forms.EmailField( label=_('Email'), +<<<<<<< HEAD + ) +======= ) +#仅包含email字段(邮箱输入),用于忘记密码流程中验证邮箱的步骤 +>>>>>>> LXY_branch diff --git a/src/django-master/accounts/models.py b/src/django-master/accounts/models.py index 3baddbb..0662c2b 100644 --- a/src/django-master/accounts/models.py +++ b/src/django-master/accounts/models.py @@ -3,33 +3,112 @@ 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 +from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具函数 # Create your models here. - +<<<<<<< HEAD +# 自定义用户模型,继承Django内置的AbstractUser class BlogUser(AbstractUser): +<<<<<<< HEAD + # 用户昵称(可选) 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) + # 账号创建来源(如:网站注册/第三方登录等,可选) +======= + """ + 自定义用户模型,继承自Django内置的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) + # 账号创建来源(如管理员添加、前台注册等),用于追踪用户注册渠道 +>>>>>>> JYN_branch source = models.CharField(_('create source'), max_length=100, blank=True) + # 获取用户详情页的绝对URL(用于模板中的{% url %}反向解析) +======= + +class BlogUser(AbstractUser):#自定义用户模型BlogUser,继承自Django内置的AbstractUser(可扩展的用户抽象类)。 + nickname = models.CharField(_('nick name'), max_length=100, blank=True)#定义nickname字段,字符类型,支持国际化翻译,最大长度100,可为空。 + creation_time = models.DateTimeField(_('creation time'), default=now)#定义creation_time字段,日期时间类型,默认值为当前时间(now方法)。 + last_modify_time = models.DateTimeField(_('last modify time'), default=now)#定义last_modify_time字段,日期时间类型,默认值为当前时间。 + source = models.CharField(_('create source'), max_length=100, blank=True)#定义source字段,字符类型,记录用户创建来源,最大长度100,可为空。 + +>>>>>>> LXY_branch def get_absolute_url(self): + """ + 返回用户详情页的URL + Django推荐为模型定义此方法,用于获取对象的标准URL + """ return reverse( +<<<<<<< HEAD 'blog:author_detail', kwargs={ +<<<<<<< HEAD 'author_name': self.username}) + # 定义对象的字符串表示(Admin后台和shell中显示) +======= + 'blog:author_detail', # 对应的URL名称 + kwargs={'author_name': self.username} # 传递的参数 + ) +======= + 'author_name': self.username})#定义获取用户详情页绝对URL的方法,通过reverse反向解析路由blog:author_detail,传递username参数。 +>>>>>>> LXY_branch +>>>>>>> JYN_branch def __str__(self): +<<<<<<< HEAD + """模型的字符串表示,这里返回用户的邮箱""" return self.email + # 获取用户详情页的完整URL(包含域名,用于分享链接) +======= + return self.email#定义对象的字符串表示方法,返回用户的email +>>>>>>> LXY_branch def get_full_url(self): - site = get_current_site().domain +<<<<<<< HEAD + site = get_current_site().domain# 获取当前站点域名 url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) +<<<<<<< HEAD +======= + """获取用户详情页的完整URL(包含域名)""" + # 获取当前站点的域名 + site = get_current_site().domain + # 拼接完整URL(协议+域名+路径) + url = "https://{site}{path}".format( + site=site, + path=self.get_absolute_url() # 调用get_absolute_url获取相对路径 + ) +>>>>>>> JYN_branch return url + # 元数据配置(模型级别的选项) + class Meta: +<<<<<<< HEAD + ordering = ['-id'] # 默认按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' # 指定使用id字段获取最新对象(用于Model.objects.latest()) +>>>>>>> JYN_branch +======= + return url#定义获取带域名的完整URL方法,结合当前站点域名和get_absolute_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'#指定按id获取最新记录 +>>>>>>> LXY_branch diff --git a/src/django-master/accounts/tests.py b/src/django-master/accounts/tests.py index 6893411..32e5973 100644 --- a/src/django-master/accounts/tests.py +++ b/src/django-master/accounts/tests.py @@ -3,182 +3,404 @@ from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from accounts.models import BlogUser -from blog.models import Article, Category -from djangoblog.utils import * -from . import utils +from accounts.models import BlogUser # 导入自定义用户模型 +from blog.models import Article, Category # 导入博客相关模型 +from djangoblog.utils import * # 导入项目工具函数 +from . import utils # 导入当前应用的工具函数 # Create your tests here. - +# 创建测试类(继承Django的TestCase) class AccountTest(TestCase): +<<<<<<< HEAD + # 测试初始化方法(每个测试方法运行前都会执行) def setUp(self): + # 初始化测试客户端(模拟浏览器请求) self.client = Client() + # 初始化请求工厂(用于生成请求对象) self.factory = RequestFactory() + # 创建一个普通测试用户 +======= + """ + 账号相关功能的测试类,继承自Django的TestCase + 包含用户登录、注册、密码重置等功能的测试用例 + """ + def setUp(self): + """ + 测试前的初始化方法,会在每个测试方法执行前运行 + 用于创建测试所需的基础数据 + """ + self.client = Client() # 创建测试客户端,用于模拟用户请求 + self.factory = RequestFactory() # 创建请求工厂,用于构建请求对象 + # 创建一个测试用户 +>>>>>>> JYN_branch self.blog_user = BlogUser.objects.create_user( username="test", email="admin@admin.com", password="12345678" ) +<<<<<<< HEAD + # 测试用的随机字符串 self.new_test = "xxx123--=" - + # 测试用户账号验证功能 def test_validate_account(self): + # 获取当前站点域名 site = get_current_site().domain + # 创建一个超级用户(用于测试管理员权限) +======= + self.new_test = "xxx123--=" # 测试用的新密码 + + def test_validate_account(self): + """测试用户账号验证相关功能,包括登录和管理员权限""" + site = get_current_site().domain # 获取当前站点域名 + + # 创建一个超级用户 +>>>>>>> JYN_branch user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="qwer!@#$ggg") +<<<<<<< HEAD + # 从数据库获取刚创建的超级用户(验证是否创建成功) + testuser = BlogUser.objects.get(username='liangliangyy1') +======= + # 获取创建的用户 testuser = BlogUser.objects.get(username='liangliangyy1') +>>>>>>> JYN_branch + # 测试登录功能 loginresult = self.client.login( username='liangliangyy1', password='qwer!@#$ggg') - self.assertEqual(loginresult, True) +<<<<<<< HEAD + self.assertEqual(loginresult, True)# 验证登录成功 + # 测试访问管理员后台 response = self.client.get('/admin/') - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200)# 验证返回200状态码 +======= + self.assertEqual(loginresult, True) # 断言登录成功 + + # 测试访问管理员页面 + response = self.client.get('/admin/') + self.assertEqual(response.status_code, 200) # 断言页面访问成功 +>>>>>>> JYN_branch + # 创建测试分类 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() category.save() +<<<<<<< HEAD +======= +>>>>>>> JYN_branch + # 创建测试文章 article = Article() article.title = "nicetitleaaa" article.body = "nicecontentaaa" +<<<<<<< HEAD + 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状态码 +======= article.author = user article.category = category - article.type = 'a' - article.status = 'p' + article.type = 'a' # 假设'a'表示文章类型 + article.status = 'p' # 假设'p'表示已发布 article.save() + # 测试访问文章管理页面 response = self.client.get(article.get_admin_url()) - self.assertEqual(response.status_code, 200) - +<<<<<<< HEAD + self.assertEqual(response.status_code, 200) # 断言页面访问成功 +>>>>>>> JYN_branch +======= + self.assertEqual(response.status_code, 200)#测试管理员账号登录后台功能:创建超级用户,验证登录状态和后台页面访问状态 +>>>>>>> LXY_branch + + # 测试用户注册功能 def test_validate_register(self): +<<<<<<< HEAD + # 验证测试邮箱初始不存在 +======= + """测试用户注册功能,包括注册流程、邮箱验证和权限控制""" + # 初始状态下,该邮箱应不存在 +>>>>>>> JYN_branch self.assertEquals( 0, len( BlogUser.objects.filter( email='user123@user.com'))) +<<<<<<< HEAD + # 模拟注册请求 +======= + + # 模拟用户注册提交 +>>>>>>> JYN_branch 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', }) +<<<<<<< HEAD + # 验证用户已创建(通过邮箱查询) +======= + + # 注册后,该邮箱应存在 +>>>>>>> JYN_branch self.assertEquals( 1, len( BlogUser.objects.filter( email='user123@user.com'))) +<<<<<<< HEAD + # 获取刚注册的用户 user = BlogUser.objects.filter(email='user123@user.com')[0] + # 生成验证签名(用于邮箱验证等场景) +======= + + # 获取刚注册的用户 + user = BlogUser.objects.filter(email='user123@user.com')[0] + # 生成验证链接(模拟邮箱验证流程) +>>>>>>> JYN_branch 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) +<<<<<<< HEAD + # 测试访问验证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') + # 获取指定邮箱的用户并设置为超级用户和工作人员 +======= + + # 访问验证链接 + response = self.client.get(url) + self.assertEqual(response.status_code, 200) # 断言验证页面访问成功 + # 登录新注册用户 self.client.login(username='user1233', password='password123!q@wE#R$T') + # 提升用户权限 +>>>>>>> JYN_branch user = BlogUser.objects.filter(email='user123@user.com')[0] user.is_superuser = True user.is_staff = True user.save() +<<<<<<< HEAD + # 删除侧边栏缓存 + delete_sidebar_cache() + # 创建分类 +======= + + # 清除缓存 delete_sidebar_cache() + + # 创建测试分类 +>>>>>>> JYN_branch category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() category.save() +<<<<<<< HEAD + # 创建文章 +======= + # 创建测试文章 +>>>>>>> JYN_branch article = Article() article.category = category article.title = "nicetitle333" article.body = "nicecontentttt" article.author = user - article.type = 'a' article.status = 'p' article.save() +<<<<<<< HEAD + # 测试已登录用户访问文章管理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.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]) + self.assertIn(response.status_code, [301, 302, 200]) # 登出通常是重定向 + # 登出后访问管理页面(应被拒绝或重定向) response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) + # 使用错误密码登录 +>>>>>>> JYN_branch response = self.client.post(reverse('account:login'), { 'username': 'user1233', - 'password': 'password123' + 'password': 'password123'# 注意这里密码与登录时使用的不同 }) self.assertIn(response.status_code, [301, 302, 200]) +<<<<<<< HEAD + # 测试使用错误密码登录后访问文章管理URL(应重定向) +======= + # 错误登录后访问管理页面 +>>>>>>> JYN_branch response = self.client.get(article.get_admin_url()) +<<<<<<< HEAD self.assertIn(response.status_code, [301, 302, 200]) + # 测试邮箱验证码验证 +======= + self.assertIn(response.status_code, [301, 302, 200])#测试用户注册流程:验证注册前后用户数量变化,邮箱验证链接的有效性,以及注册后用户权限、文章发布等功能 +>>>>>>> LXY_branch 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) - +<<<<<<< HEAD + 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) +<<<<<<< HEAD + self.assertEqual(type(err), str)# 应返回错误信息字符串 + # 测试忘记密码发送验证码功能 - 成功情况 +======= + code = generate_code() # 生成验证码 + utils.set_code(to_email, code) # 存储验证码 + utils.send_verify_email(to_email, code) # 发送验证邮件 +======= + self.assertEqual(type(err), str)#测试邮箱验证码功能:验证有效邮箱和无效邮箱的验证码校验结果 +>>>>>>> LXY_branch + + # 验证正确的邮箱和验证码 + 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) # 应返回错误信息 +>>>>>>> JYN_branch def test_forget_password_email_code_success(self): + """测试发送密码重置验证码成功的情况""" resp = self.client.post( path=reverse("account:forget_password_code"), - data=dict(email="admin@admin.com") +<<<<<<< HEAD + data=dict(email="admin@admin.com") # 使用正确邮箱格式 ) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode("utf-8"), "ok") +<<<<<<< HEAD + self.assertEqual(resp.content.decode("utf-8"), "ok")# 验证返回成功消息 + # 测试忘记密码发送验证码功能 - 失败情况 + def test_forget_password_email_code_fail(self): + # 测试不提供邮箱 +======= + 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")#测试忘记密码的邮箱验证码发送:分别验证成功和失败场景(如邮箱错误)的接口响应 +>>>>>>> LXY_branch def test_forget_password_email_code_fail(self): + """测试发送密码重置验证码失败的情况""" + # 不提供邮箱 +>>>>>>> JYN_branch resp = self.client.post( path=reverse("account:forget_password_code"), data=dict() ) +<<<<<<< HEAD self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + # 测试提供错误格式邮箱 +======= + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误信息 + # 提供无效格式的邮箱 +>>>>>>> JYN_branch resp = self.client.post( path=reverse("account:forget_password_code"), data=dict(email="admin@com") ) +<<<<<<< HEAD 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)# 为测试用户设置验证码 + # 准备重置密码数据 +======= + 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) # 存储验证码 + # 准备重置密码的数据 +>>>>>>> JYN_branch 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, # 验证码 ) +<<<<<<< HEAD + # 提交重置密码请求 +======= + # 提交密码重置请求 +>>>>>>> JYN_branch resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 302) +<<<<<<< HEAD + self.assertEqual(resp.status_code, 302) # 应重定向 +======= + self.assertEqual(resp.status_code, 302) # 成功重置后通常重定向 +>>>>>>> JYN_branch - # 验证用户密码是否修改成功 + # 验证密码是否已更新 blog_user = BlogUser.objects.filter( email=self.blog_user.email, ).first() # type: BlogUser - self.assertNotEqual(blog_user, None) + 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", +<<<<<<< HEAD + email="123@123.com",# 不存在的邮箱 +======= + email="123@123.com", # 不存在的邮箱 +>>>>>>> JYN_branch code="123456", ) resp = self.client.post( @@ -186,22 +408,46 @@ class AccountTest(TestCase): data=data ) - self.assertEqual(resp.status_code, 200) - +<<<<<<< HEAD + self.assertEqual(resp.status_code, 200) # 应返回错误页面而非重定向 +======= + self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码 +>>>>>>> JYN_branch + # 测试忘记密码重置功能 - 验证码错误情况 def test_forget_password_email_code_error(self): +<<<<<<< HEAD code = generate_code() - utils.set_code(self.blog_user.email, code) + utils.set_code(self.blog_user.email, code)# 设置正确验证码 + # 使用错误验证码提交 +======= + """测试使用错误的验证码重置密码的情况""" + code = generate_code() # 生成正确验证码 + utils.set_code(self.blog_user.email, code) # 存储验证码 + # 使用错误的验证码 +>>>>>>> JYN_branch data = dict( new_password1=self.new_test, new_password2=self.new_test, email=self.blog_user.email, - code="111111", +<<<<<<< HEAD + code="111111",# 错误验证码 +======= + code="111111", # 错误的验证码 +>>>>>>> JYN_branch ) resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 200) +<<<<<<< HEAD +<<<<<<< HEAD + self.assertEqual(resp.status_code, 200)# 应返回错误页面而非重定向 +======= + self.assertEqual(resp.status_code, 200)#测试忘记密码流程:成功场景:验证密码修改后是否生效;失败场景:验证不存在用户、验证码错误时的接口响应 +>>>>>>> LXY_branch +======= + self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码 +>>>>>>> JYN_branch diff --git a/src/django-master/accounts/urls.py b/src/django-master/accounts/urls.py index 107a801..3a0b0b3 100644 --- a/src/django-master/accounts/urls.py +++ b/src/django-master/accounts/urls.py @@ -1,28 +1,113 @@ +<<<<<<< HEAD +# 导入 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" +# 定义 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 名称 + ] +======= from django.urls import path -from django.urls import re_path +from django.urls import re_path # 用于正则表达式匹配URL -from . import views -from .forms import LoginForm +from . import views # 导入当前应用的视图函数/类 +from .forms import LoginForm # 导入自定义的登录表单 +# 定义应用命名空间,避免URL名称冲突 app_name = "accounts" +<<<<<<< HEAD +# URL路由配置列表,映射URL路径到对应的视图 +urlpatterns = [ + # 登录页面路由:使用正则匹配以login/结尾的路径 + re_path(r'^login/$', + # 调用LoginView类视图,指定登录成功后重定向到首页(/) + views.LoginView.as_view(success_url='/'), + name='login', # URL的名称,用于反向解析 + # 向视图传递额外参数:指定登录表单为自定义的LoginForm + kwargs={'authentication_form': LoginForm}), + + # 注册页面路由:匹配以register/结尾的路径 + re_path(r'^register/$', + # 调用RegisterView类视图,注册成功后重定向到首页 + views.RegisterView.as_view(success_url="/"), + name='register'), # URL名称,用于反向解析 + + # 登出功能路由:匹配以logout/结尾的路径 + re_path(r'^logout/$', + # 调用LogoutView类视图(Django内置或自定义) + views.LogoutView.as_view(), + name='logout'), # URL名称 + + # 账号操作结果页面路由:精确匹配account/result.html路径 + path(r'account/result.html', + # 调用函数视图account_result + views.account_result, + name='result'), # URL名称,用于展示注册/验证等结果 + + # 忘记密码页面路由:匹配以forget_password/结尾的路径 + re_path(r'^forget_password/$', + # 调用ForgetPasswordView类视图 + views.ForgetPasswordView.as_view(), + name='forget_password'), # URL名称 + + # 发送密码重置验证码页面路由:匹配以forget_password_code/结尾的路径 + re_path(r'^forget_password_code/$', + # 调用ForgetPasswordEmailCode类视图(处理发送验证码逻辑) + views.ForgetPasswordEmailCode.as_view(), + name='forget_password_code'), # URL名称 +] +>>>>>>> JYN_branch +======= urlpatterns = [re_path(r'^login/$', views.LoginView.as_view(success_url='/'), name='login', - kwargs={'authentication_form': LoginForm}), + kwargs={'authentication_form': LoginForm}),#登录路由,对应LoginView,用LoginForm表单,成功重定向/ re_path(r'^register/$', views.RegisterView.as_view(success_url="/"), - name='register'), + name='register'),#注册路由,对应RegisterView,成功重定向/。 + re_path(r'^logout/$', views.LogoutView.as_view(), - name='logout'), + name='logout'),#登出路由,对应LogoutView。 path(r'account/result.html', views.account_result, - name='result'), + name='result'),#结果页路由,对应account_result视图 re_path(r'^forget_password/$', views.ForgetPasswordView.as_view(), - name='forget_password'), + name='forget_password'),#忘记密码路由,对应ForgetPasswordView re_path(r'^forget_password_code/$', views.ForgetPasswordEmailCode.as_view(), - name='forget_password_code'), + name='forget_password_code'),#忘记密码验证码路由,对应ForgetPasswordEmailCode ] +>>>>>>> LXY_branch diff --git a/src/django-master/accounts/user_login_backend.py b/src/django-master/accounts/user_login_backend.py index 73cdca1..a2d8f85 100644 --- a/src/django-master/accounts/user_login_backend.py +++ b/src/django-master/accounts/user_login_backend.py @@ -1,26 +1,112 @@ -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend +<<<<<<< HEAD +# 导入 Django 认证系统所需的模块 +from django.contrib.auth import get_user_model# 动态获取当前项目的 User 模型 +from django.contrib.auth.backends import ModelBackend# Django 默认的认证后端基类 +======= +from django.contrib.auth import get_user_model # 获取项目配置的用户模型(支持自定义模型) +from django.contrib.auth.backends import ModelBackend # 导入Django内置的模型认证后端 +>>>>>>> JYN_branch -class EmailOrUsernameModelBackend(ModelBackend): +class EmailOrUsernameModelBackend(ModelBackend):#自定义Django认证后端,支持用户名或邮箱两种方式登录。 """ +<<<<<<< HEAD 允许使用用户名或邮箱登录 + 承自 Django 的 ModelBackend,重写 authenticate 和 get_user 方法。 +======= + 自定义认证后端,继承自Django的ModelBackend + 扩展功能:允许用户使用「用户名」或「邮箱」两种方式登录,而非仅支持用户名 +>>>>>>> JYN_branch """ def authenticate(self, request, username=None, password=None, **kwargs): + """ +<<<<<<< HEAD + 重写认证方法,支持用户名或邮箱登录。 + + 参数: + request: HttpRequest 对象(可能包含额外的认证上下文) + username: 用户输入的用户名或邮箱(前端传递的字段名固定为 username) + password: 用户输入的密码 + **kwargs: 其他可能的参数(如通过信号传递的额外参数) + + 返回: + 如果认证成功返回 User 对象,否则返回 None + """ + # 判断输入是邮箱还是用户名(通过 '@' 符号区分) if '@' in username: + # 如果是邮箱,设置查询条件为 email 字段 kwargs = {'email': username} else: + # 否则按用户名查询 +======= + 认证核心方法:验证用户输入的凭证(用户名/邮箱 + 密码)是否有效 + 参数说明: + - request:当前请求对象 + - username:前端传入的「用户名」参数(实际可能是用户名或邮箱) + - password:前端传入的密码(明文) + 返回:验证成功返回用户对象,失败返回None + """ + # 判断输入的「username」是否包含@符号,以此区分邮箱和用户名 + if '@' in username: + # 若包含@,则按邮箱字段查询用户 + kwargs = {'email': username} + else: + # 若不包含@,则按用户名字段查询用户 +>>>>>>> JYN_branch kwargs = {'username': username} + try: +<<<<<<< HEAD + # 尝试从数据库获取用户(使用当前项目的自定义 User 模型) + user = get_user_model().objects.get(**kwargs) + # 验证密码是否正确(使用 Django 的密码哈希校验) + if user.check_password(password): + return user# 认证成功返回用户对象 + except get_user_model().DoesNotExist: + # 用户不存在时返回 None(Django 会继续尝试其他认证后端) +======= + # 根据上述条件从数据库查询唯一用户 user = get_user_model().objects.get(**kwargs) + # 验证密码:check_password会自动将明文密码与数据库中存储的哈希密码比对 if user.check_password(password): - return user + return user # 密码正确,返回用户对象(认证成功) except get_user_model().DoesNotExist: + # 若查询不到用户(用户名/邮箱不存在),返回None(认证失败) +>>>>>>> JYN_branch return None - +#核心认证逻辑:判断输入是否为邮箱(含@),分别用邮箱或用户名查询用户,验证密码后返回用户对象;若用户不存在则返回None。 def get_user(self, username): + """ +<<<<<<< HEAD + 根据用户 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 +<<<<<<< HEAD +======= + 根据用户ID获取用户对象(Django认证系统必须实现的方法) + 作用:认证成功后,系统通过此方法获取用户完整信息 + 参数:username实际为用户ID(Django默认用ID作为唯一标识) + 返回:存在则返回用户对象,不存在返回None + """ try: + # 通过主键(ID)查询用户 return get_user_model().objects.get(pk=username) except get_user_model().DoesNotExist: + # 若用户不存在,返回None return None +>>>>>>> JYN_branch +======= +#根据用户ID(主键)查询用户,不存在则返回None,用于Django认证系统的用户查询环节 +>>>>>>> LXY_branch diff --git a/src/django-master/accounts/utils.py b/src/django-master/accounts/utils.py index 4b94bdf..2e649f0 100644 --- a/src/django-master/accounts/utils.py +++ b/src/django-master/accounts/utils.py @@ -1,49 +1,137 @@ -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 _ +<<<<<<< HEAD +# 导入 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# 假设是项目封装的邮件发送函数 -from djangoblog.utils import send_email +# 验证码有效期(5分钟) +======= +from django.core.cache import cache # 导入Django缓存模块,用于存储验证码 +from django.utils.translation import gettext # 用于获取即时翻译文本 +from django.utils.translation import gettext_lazy as _ # 用于延迟翻译文本(支持国际化) +<<<<<<< HEAD +from djangoblog.utils import send_email # 导入项目自定义的发送邮件工具函数 + +# 验证码有效期:5分钟(全局变量,统一控制时效) +>>>>>>> JYN_branch _code_ttl = timedelta(minutes=5) +======= +_code_ttl = timedelta(minutes=5)#验证码有效期,设置为5分钟。 +>>>>>>> LXY_branch def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): - """发送重设密码验证码 + """ +<<<<<<< HEAD + # 构造邮件正文(使用国际化字符串,并插入动态验证码) + html_content = _( + "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " + "properly") % {'code': code} + # 调用项目封装的邮件发送函数 +======= + 发送邮箱验证码邮件(主要用于密码重置场景) Args: - to_mail: 接受邮箱 - subject: 邮件主题 - code: 验证码 + to_mail: 接收验证码的目标邮箱地址 + code: 生成的随机验证码 + subject: 邮件主题,默认值为“Verify Email”(支持国际化) """ + # 构造邮件HTML内容,包含验证码和有效期提示,使用%(code)s占位符注入验证码 html_content = _( "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " "properly") % {'code': code} + # 调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容 +>>>>>>> JYN_branch send_email([to_mail], subject, html_content) def verify(email: str, code: str) -> typing.Optional[str]: - """验证code是否有效 + """ +<<<<<<< HEAD + # 从缓存获取该邮箱对应的正确验证码 + cache_code = get_code(email) + # 比对用户输入的验证码和缓存中的验证码 + if cache_code != code: + return gettext("Verification code error") # 返回翻译后的错误信息 + # 隐含逻辑:验证成功时返回 None(调用方需检查返回值是否为 None) +======= + 验证用户输入的验证码是否有效 Args: - email: 请求邮箱 - code: 验证码 + email: 用户提交的邮箱(用于匹配缓存中的验证码) + code: 用户输入的验证码 Return: - 如果有错误就返回错误str - Node: - 这里的错误处理不太合理,应该采用raise抛出 - 否测调用方也需要对error进行处理 + 验证失败时返回错误提示字符串,验证成功时返回None + Note: + 代码注释中指出当前错误处理逻辑不合理:应使用raise抛出异常,而非返回错误字符串 + 若返回错误字符串,调用方需额外判断返回值是否为错误,增加了代码耦合度 """ + # 从缓存中获取该邮箱对应的验证码 cache_code = get_code(email) + # 对比用户输入的验证码与缓存中的验证码 if cache_code != code: + # 验证码不匹配时,返回国际化的错误提示 return gettext("Verification code error") +>>>>>>> JYN_branch def set_code(email: str, code: str): + """ +<<<<<<< HEAD + 将验证码存入缓存 + + Args: + email (str): 用户邮箱(作为缓存键) + code (str): 要存储的验证码 + + Note: + 验证码有效期由全局变量 _code_ttl 控制(5分钟) + """ + # 使用 Django 缓存设置键值对,并指定过期时间(转换为秒) """设置code""" +======= + 将验证码存入缓存,以邮箱为key,设置有效期 + Args: + email: 作为缓存key的邮箱地址(确保一个邮箱对应一个验证码) + code: 需要存入缓存的验证码 + """ + # 调用Django缓存的set方法:key=邮箱,value=验证码,timeout=有效期(秒) +>>>>>>> JYN_branch cache.set(email, code, _code_ttl.seconds) +<<<<<<< HEAD def get_code(email: str) -> typing.Optional[str]: + """ +<<<<<<< HEAD + 从缓存获取验证码 + + Args: + email (str): 用户邮箱(缓存键) + + Returns: + typing.Optional[str]: + - 存在则返回验证码字符串 + - 不存在或过期返回 None + """ + # 直接调用 Django 缓存的 get 方法 +======= +def get_code(email: str) -> typing.Optional[str]:#从缓存中获取指定邮箱对应的验证码 +>>>>>>> LXY_branch """获取code""" return cache.get(email) +======= + 从缓存中获取指定邮箱对应的验证码 + Args: + email: 用于查询的邮箱地址(缓存key) + Return: + 缓存中存在该邮箱对应的验证码时返回字符串,不存在时返回None + """ + # 调用Django缓存的get方法,根据邮箱key获取验证码 + return cache.get(email) +>>>>>>> JYN_branch diff --git a/src/django-master/accounts/views.py b/src/django-master/accounts/views.py index ae67aec..c6f0601 100644 --- a/src/django-master/accounts/views.py +++ b/src/django-master/accounts/views.py @@ -1,59 +1,164 @@ +# 导入日志模块,用于记录运行时的信息和错误 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 +<<<<<<< HEAD +# 认证相关常量(如重定向字段名) 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__) +======= +from django.contrib.auth import REDIRECT_FIELD_NAME # 登录后重定向字段名常量 +from django.contrib.auth import get_user_model # 获取项目配置的用户模型 +from django.contrib.auth import logout # 登出功能函数 +from django.contrib.auth.forms import AuthenticationForm # Django内置登录表单 +from django.contrib.auth.hashers import make_password # 密码哈希处理函数 +from django.http import HttpResponseRedirect, HttpResponseForbidden # HTTP响应类 +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.shortcuts import get_object_or_404, render # 快捷函数 +from django.urls import reverse # URL反向解析 +from django.utils.decorators import method_decorator # 类视图装饰器工具 +from django.utils.http import url_has_allowed_host_and_scheme # 验证重定向URL安全性 +from django.views import View # 基础视图类 +from django.views.decorators.cache import never_cache # 禁止缓存装饰器 +from django.views.decorators.csrf import csrf_protect # CSRF保护装饰器 +from django.views.decorators.debug import sensitive_post_parameters # 敏感参数保护装饰器 +from django.views.generic import FormView, RedirectView # 通用类视图 + +from djangoblog.utils import (send_email, get_sha256, get_current_site, + generate_code, delete_sidebar_cache) # 项目工具函数 +from . import utils # 当前应用工具函数(验证码相关) +from .forms import (RegisterForm, LoginForm, ForgetPasswordForm, + ForgetPasswordCodeForm) # 当前应用表单类 +from .models import BlogUser # 自定义用户模型 + +logger = logging.getLogger(__name__) # 初始化日志记录器 +>>>>>>> JYN_branch # Create your views here. - +# 注册视图类(继承自 FormView,处理表单提交) class RegisterView(FormView): +<<<<<<< HEAD + # 指定使用的表单类 form_class = RegisterForm +<<<<<<< HEAD template_name = 'account/registration_form.html' + # 使用装饰器确保视图禁用缓存(never_cache)并启用 CSRF 防护 +======= + template_name = 'account/registration_form.html'#处理用户注册逻辑,指定表单类RegisterForm和模板account/registration_form.html。 +>>>>>>> LXY_branch @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): + # 调用父类方法处理请求 +======= + """ + 用户注册类视图,继承自FormView(处理表单提交的通用视图) + 负责用户注册表单展示、数据验证、发送验证邮件及注册结果跳转 + """ + form_class = RegisterForm # 指定使用的注册表单 + template_name = 'account/registration_form.html' # 注册页面模板路径 + + @method_decorator(csrf_protect) # 为视图添加CSRF保护 + def dispatch(self, *args, **kwargs): + """重写分发方法,添加装饰器后调用父类逻辑""" +>>>>>>> JYN_branch return super(RegisterView, self).dispatch(*args, **kwargs) - + # 表单验证通过后的处理逻辑 def form_valid(self, form): +<<<<<<< HEAD + # 再次检查表单有效性(冗余,因为 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) + # 构造邮件内容(包含验证链接) +======= + """表单验证通过后执行的逻辑(注册核心流程)""" + if form.is_valid(): + # 1. 暂存用户数据,不立即保存(is_active设为False,需邮箱验证后激活) + user = form.save(False) + user.is_active = False # 初始状态:未激活(需邮箱验证) + user.source = 'Register' # 标记注册来源为“前台注册” + user.save(True) # 保存用户到数据库 + + # 2. 生成邮箱验证链接(包含签名,防止篡改) + site = get_current_site().domain # 获取当前站点域名 + # 双重SHA256加密:用SECRET_KEY+用户ID生成签名,确保链接安全性 sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + # 开发环境下替换域名(适配本地测试) if settings.DEBUG: site = '127.0.0.1:8000' + # 反向解析结果页URL,拼接完整验证链接 path = reverse('account:result') url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( site=site, path=path, id=user.id, sign=sign) + # 3. 构造验证邮件内容并发送 +>>>>>>> JYN_branch content = """
请点击下面链接验证您的邮箱
@@ -64,141 +169,421 @@ class RegisterView(FormView): 如果上面链接无法打开,请将此链接复制至浏览器。 {url} """.format(url=url) + # 发送验证邮件 send_email( +<<<<<<< HEAD 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 +<<<<<<< HEAD }) +======= + emailto=[user.email], # 收件人邮箱(新注册用户的邮箱) + title='验证您的电子邮箱', # 邮件标题 + content=content # 邮件HTML内容 + ) +======= + })#form_valid方法中,保存用户并设置为非活跃状态,生成邮箱验证链接并发送验证邮件,最后重定向到结果页。 +>>>>>>> LXY_branch + + # 4. 跳转到注册结果页(提示用户查收验证邮件) + url = reverse('accounts:result') + f'?type=register&id={str(user.id)}' + return HttpResponseRedirect(url) + else: + # 表单验证失败,重新渲染表单并显示错误 + return self.render_to_response({'form': form}) +>>>>>>> JYN_branch - +# 登出视图,继承自RedirectView,重定向到登录页面 class LogoutView(RedirectView): +<<<<<<< HEAD +<<<<<<< HEAD + # 登出后重定向的URL url = '/login/' + # 使用never_cache装饰器确保视图不会被缓存 +======= + url = '/login/'#处理用户登出,登出后重定向到/login/ +>>>>>>> LXY_branch @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): + # 调用父类的dispatch方法处理请求 +======= + """ + 用户登出类视图,继承自RedirectView(处理重定向的通用视图) + 负责清除用户会话、缓存,并重定向到登录页 + """ + url = '/login/' # 登出后默认重定向地址(登录页) + + @method_decorator(never_cache) # 禁止缓存登出页面,避免浏览器缓存导致的问题 + def dispatch(self, request, *args, **kwargs): + """重写分发方法,添加装饰器后调用父类逻辑""" +>>>>>>> JYN_branch return super(LogoutView, self).dispatch(request, *args, **kwargs) - + # 处理GET请求 def get(self, request, *args, **kwargs): +<<<<<<< HEAD + # 执行登出操作 logout(request) + # 删除侧边栏缓存 delete_sidebar_cache() +<<<<<<< HEAD + # 调用父类的get方法完成重定向 return super(LogoutView, self).get(request, *args, **kwargs) - - +======= + """处理GET请求(登出核心逻辑)""" + logout(request) # 清除用户会话,实现登出 + delete_sidebar_cache() # 删除侧边栏缓存(可能存储了用户相关信息) + return super(LogoutView, self).get(request, *args, **kwargs) # 执行重定向 +>>>>>>> JYN_branch +======= + return super(LogoutView, self).get(request, *args, **kwargs)#get方法中调用logout登出用户,删除侧边栏缓存后完成重定向 +>>>>>>> LXY_branch + +# 登录视图,继承自FormView class LoginView(FormView): +<<<<<<< HEAD + # 使用的表单类 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')) + # 使用多个装饰器装饰dispatch方法 + @method_decorator(sensitive_post_parameters('password'))# 标记密码参数为敏感信息 + @method_decorator(csrf_protect) # 启用CSRF保护 + @method_decorator(never_cache)# 禁止缓存 + def dispatch(self, request, *args, **kwargs): + # 调用父类的dispatch方法处理请求 +======= + """ + 用户登录类视图,继承自FormView + 负责登录表单展示、数据验证、用户认证、记住登录状态及重定向 + """ + form_class = LoginForm # 指定使用的自定义登录表单 + template_name = 'account/login.html' # 登录页面模板路径 + success_url = '/' # 登录成功默认重定向地址(首页) + redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名(默认next) + login_ttl = 2626560 # “记住登录”状态的有效期(秒),约等于1个月 + + # 为视图添加多重装饰器:敏感参数保护、CSRF保护、禁止缓存 + @method_decorator(sensitive_post_parameters('password')) # 保护密码参数,避免日志泄露 @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): - +>>>>>>> JYN_branch return super(LoginView, self).dispatch(request, *args, **kwargs) + # 获取模板上下文数据 def get_context_data(self, **kwargs): +<<<<<<< HEAD + # 从GET参数中获取重定向URL +======= + """添加额外上下文数据(重定向地址)到模板""" + # 获取URL中的重定向参数(如登录前访问的受保护页面) +>>>>>>> JYN_branch redirect_to = self.request.GET.get(self.redirect_field_name) + # 如果不存在则设置为根路径 if redirect_to is None: +<<<<<<< HEAD redirect_to = '/' + # 将重定向URL添加到上下文 kwargs['redirect_to'] = redirect_to + # 调用父类方法获取其他上下文数据 +<<<<<<< HEAD return super(LoginView, self).get_context_data(**kwargs) - + # 表单验证通过后的处理 def form_valid(self, form): + # 重新创建认证表单(这里可能有逻辑问题,因为form已经传入) +======= + redirect_to = '/' # 默认重定向到首页 + kwargs['redirect_to'] = redirect_to # 将重定向地址传入模板上下文 + return super(LoginView, self).get_context_data(** kwargs) + +======= + return super(LoginView, self).get_context_data(**kwargs)#处理用户登录逻辑,指定表单类LoginForm、模板account / login.html和成功后重定向地址 / +>>>>>>> LXY_branch + def form_valid(self, form): + """表单验证通过后执行的逻辑(登录核心流程)""" + # 用Django内置AuthenticationForm重新验证(确保认证逻辑符合默认规范) +>>>>>>> JYN_branch form = AuthenticationForm(data=self.request.POST, request=self.request) - + # 再次验证表单 if form.is_valid(): +<<<<<<< HEAD + # 删除侧边栏缓存 delete_sidebar_cache() + # 记录日志 logger.info(self.redirect_field_name) + # 登录用户 auth.login(self.request, form.get_user()) + # 如果用户选择了"记住我" +======= + delete_sidebar_cache() # 删除侧边栏缓存(更新用户登录状态) + logger.info(self.redirect_field_name) # 日志记录重定向字段名 + + # 执行登录:将用户信息存入会话 + auth.login(self.request, form.get_user()) + # 处理“记住我”功能:若勾选,设置会话有效期为1个月 +>>>>>>> JYN_branch if self.request.POST.get("remember"): + # 设置较长的会话过期时间 self.request.session.set_expiry(self.login_ttl) +<<<<<<< HEAD + # 调用父类方法处理成功跳转 +======= + # 调用父类form_valid,执行重定向 +>>>>>>> JYN_branch return super(LoginView, self).form_valid(form) - # return HttpResponseRedirect('/') else: +<<<<<<< HEAD + # 表单无效,重新渲染表单并显示错误 return self.render_to_response({ 'form': form +<<<<<<< HEAD }) + # 获取成功后的跳转URL +======= + })#form_valid方法中验证表单,登录用户并根据“记住我”选项设置会话过期时间 +>>>>>>> LXY_branch 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 +<<<<<<< HEAD +======= + # 表单验证失败(如密码错误),重新渲染表单并显示错误 + return self.render_to_response({'form': form}) + def get_success_url(self): + """自定义登录成功后的重定向地址(优先使用URL中的next参数)""" + # 获取POST请求中的重定向地址(用户登录前尝试访问的页面) + redirect_to = self.request.POST.get(self.redirect_field_name) + # 验证重定向地址的安全性:避免跳转到外部恶意网站 + if not url_has_allowed_host_and_scheme( + url=redirect_to, allowed_hosts=[self.request.get_host()]): + redirect_to = self.success_url # 不安全则使用默认重定向地址 +>>>>>>> JYN_branch + return redirect_to +======= + return redirect_to#get_success_url方法处理登录后的重定向地址,确保其安全性 +>>>>>>> LXY_branch +# 账户操作结果页面(如注册成功、邮箱验证等) def account_result(request): +<<<<<<< HEAD + # 从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('/') + # 检查类型参数是否有效 +======= + """ + 账号操作结果视图函数(函数视图) + 处理注册成功提示、邮箱验证逻辑,并展示结果页面 + """ + # 从URL参数中获取操作类型(register/validation)和用户ID + type = request.GET.get('type') + id = request.GET.get('id') + + # 获取对应的用户,若不存在则返回404 + user = get_object_or_404(get_user_model(), id=id) +<<<<<<< HEAD + logger.info(type) # 日志记录操作类型 + + # 若用户已激活,直接重定向到首页(避免重复验证) +======= + logger.info(type)#处理注册和邮箱验证的结果逻辑,根据type参数区分场景: +>>>>>>> LXY_branch + if user.is_active: + return HttpResponseRedirect('/') + + # 处理合法的操作类型(注册成功提示/邮箱验证) +>>>>>>> JYN_branch if type and type in ['register', 'validation']: if type == 'register': + # 注册成功:提示用户查收验证邮件 content = ''' 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 ''' title = '注册成功' else: +<<<<<<< HEAD + # 生成验证签名 +======= + # 邮箱验证:验证签名是否正确,正确则激活用户 + # 重新计算签名,与URL中的签名对比(防止链接篡改) +>>>>>>> JYN_branch c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + # 获取请求中的签名 sign = request.GET.get('sign') + # 验证签名是否匹配 if sign != c_sign: +<<<<<<< HEAD return HttpResponseForbidden() + # 激活用户账户 user.is_active = True user.save() + # 验证成功提示内容 +======= + return HttpResponseForbidden() # 签名不匹配,返回403禁止访问 + # 激活用户:将is_active设为True + user.is_active = True + user.save() + # 验证成功:提示用户可登录 +>>>>>>> JYN_branch content = ''' 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 ''' title = '验证成功' +<<<<<<< HEAD + # 渲染结果页面 +======= + # 渲染结果页面,传递标题和内容 +>>>>>>> JYN_branch return render(request, 'account/result.html', { 'title': title, 'content': content }) else: +<<<<<<< HEAD + # 无效类型重定向到首页 +======= + # 操作类型不合法,重定向到首页 +>>>>>>> JYN_branch return HttpResponseRedirect('/') - +# 忘记密码视图,继承自FormView class ForgetPasswordView(FormView): +<<<<<<< HEAD + # 使用的表单类 form_class = ForgetPasswordForm +<<<<<<< HEAD + # 模板文件路径 template_name = 'account/forget_password.html' - +======= + """ + 忘记密码类视图,继承自FormView + 负责密码重置表单展示、数据验证,及更新用户密码 + """ + form_class = ForgetPasswordForm # 指定使用的密码重置表单 + template_name = 'account/forget_password.html' # 密码重置页面模板路径 +>>>>>>> JYN_branch +======= + template_name = 'account/forget_password.html'#处理忘记密码逻辑,指定表单类ForgetPasswordForm和模板account/forget_password.html +>>>>>>> LXY_branch + + + # 表单验证通过后的处理 def form_valid(self, form): + """表单验证通过后执行的逻辑(密码重置核心流程)""" if form.is_valid(): +<<<<<<< HEAD + # 根据邮箱获取用户对象 blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() + # 设置新密码(加密) +======= + # 1. 获取表单中的邮箱,查询对应的用户 + blog_user = BlogUser.objects.filter( + email=form.cleaned_data.get("email") + ).get() + # 2. 对新密码进行哈希处理,并更新用户密码 +>>>>>>> JYN_branch blog_user.password = make_password(form.cleaned_data["new_password2"]) + # 保存用户对象 blog_user.save() +<<<<<<< HEAD + # 重定向到登录页面 return HttpResponseRedirect('/login/') else: +<<<<<<< HEAD + # 表单无效,重新渲染表单并显示错误 +======= + # 3. 密码重置成功,重定向到登录页 + return HttpResponseRedirect('/login/') + else: + # 表单验证失败(如验证码错误、密码不一致),重新渲染表单 +>>>>>>> JYN_branch return self.render_to_response({'form': form}) - +# 忘记密码验证码发送视图,继承自View class ForgetPasswordEmailCode(View): +<<<<<<< HEAD + # 处理POST请求 +======= + return self.render_to_response({'form': form})#form_valid方法中验证表单后,重置用户密码并重定向到登录页 + +class ForgetPasswordEmailCode(View):# 处理忘记密码的邮箱验证码发送逻辑 + +>>>>>>> LXY_branch 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) +<<<<<<< HEAD + # 返回成功响应 + return HttpResponse("ok") +======= + """ + 发送密码重置验证码类视图,继承自基础View + 负责验证邮箱合法性、生成验证码、发送验证邮件,并返回结果 + """ + def post(self, request: HttpRequest): + """处理POST请求(发送验证码核心逻辑)""" + # 1. 验证邮箱表单数据 + form = ForgetPasswordCodeForm(request.POST) + if not form.is_valid(): + return HttpResponse("错误的邮箱") # 邮箱格式不合法,返回错误提示 + # 2. 生成验证码并发送邮件 + to_email = form.cleaned_data["email"] # 获取合法的邮箱地址 + code = generate_code() # 生成随机验证码 + utils.send_verify_email(to_email, code) # 发送验证码邮件 + utils.set_code(to_email, code) # 将验证码存入缓存(设置有效期) + + # 3. 操作成功,返回“ok”提示 return HttpResponse("ok") +>>>>>>> JYN_branch +======= + + return HttpResponse("ok")# post方法中验证邮箱表单,生成并发送验证码,将验证码存入缓存后返回成功标识 +>>>>>>> LXY_branch diff --git a/src/django-master/blog/forms.py b/src/django-master/blog/forms.py index 715be76..1082938 100644 --- a/src/django-master/blog/forms.py +++ b/src/django-master/blog/forms.py @@ -1,4 +1,4 @@ -import logging +import logging #导入 Python 标准库的 logging 模块,用于日志记录,方便追踪程序运行过程中的关键信息。 from django import forms from haystack.forms import SearchForm diff --git a/src/django-master/comments/admin.py b/src/django-master/comments/admin.py index a814f3f..6380066 100644 --- a/src/django-master/comments/admin.py +++ b/src/django-master/comments/admin.py @@ -1,47 +1,87 @@ -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 Admin核心模块和辅助工具 +from django.contrib import admin # Django Admin管理后台核心模块 +from django.urls import reverse # 用于生成Django内部URL(反转URL) +from django.utils.html import format_html # 用于生成安全的HTML代码(防止XSS攻击) +from django.utils.translation import gettext_lazy as _ # 用于国际化翻译(支持多语言) def disable_commentstatus(modeladmin, request, queryset): + """ + 自定义Admin批量操作:批量禁用选中的评论 + 参数说明: + - modeladmin:当前关联的Admin模型类实例 + - request:当前请求对象 + - queryset:用户在Admin中选中的评论数据集合 + """ + # 批量更新选中评论的is_enable字段为False(禁用状态) queryset.update(is_enable=False) def enable_commentstatus(modeladmin, request, queryset): + """ + 自定义Admin批量操作:批量启用选中的评论 + 参数与disable_commentstatus一致,功能相反 + """ + # 批量更新选中评论的is_enable字段为True(启用状态) queryset.update(is_enable=True) -disable_commentstatus.short_description = _('Disable comments') -enable_commentstatus.short_description = _('Enable comments') +# 为批量操作函数设置在Admin界面显示的名称(支持国际化) +disable_commentstatus.short_description = _('Disable comments') # 显示为“禁用评论” +enable_commentstatus.short_description = _('Enable comments') # 显示为“启用评论” class CommentAdmin(admin.ModelAdmin): - list_per_page = 20 - 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] + """ + 评论模型(Comment)在Django Admin中的配置类 + 控制评论在Admin后台的显示、操作、筛选等行为 + """ + # 1. 列表页基础配置 + list_per_page = 20 # 列表页每页显示20条评论数据 + list_display = ( # 列表页要显示的字段(自定义字段需自己实现方法) + 'id', # 评论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] # 列表页支持的批量操作(绑定上面定义的两个函数) + # 2. 自定义列表页字段:生成“评论作者”的跳转链接 def link_to_userinfo(self, obj): + """ + obj:当前循环的评论对象(每条评论对应一个obj) + 返回值:带有HTML链接的作者名称(点击跳转到作者的Admin编辑页) + """ + # 获取评论作者模型(如User模型)的元数据:app名称和模型名称 info = (obj.author._meta.app_label, obj.author._meta.model_name) + # 反转生成作者Admin编辑页的URL:格式为“admin:app名_模型名_change”,参数为作者ID link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + # 生成安全的HTML链接:优先显示作者昵称,没有昵称则显示邮箱 + # 注:原代码中HTML标签内href属性缺失值(应改为href="%s"),此处按正确逻辑补充 return format_html( - u'%s' % - (link, obj.author.nickname if obj.author.nickname else obj.author.email)) + u'%s' % + (link, obj.author.nickname if obj.author.nickname else obj.author.email) + ) + # 3. 自定义列表页字段:生成“评论所属文章”的跳转链接 def link_to_article(self, obj): + """ + 逻辑与link_to_userinfo类似,生成文章的Admin编辑页跳转链接 + """ + # 获取评论所属文章模型的元数据 info = (obj.article._meta.app_label, obj.article._meta.model_name) + # 反转生成文章Admin编辑页的URL,参数为文章ID link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) + # 生成HTML链接:显示文章标题,点击跳转到文章编辑页 return format_html( - u'%s' % (link, obj.article.title)) + u'%s' % (link, obj.article.title) + ) - link_to_userinfo.short_description = _('User') - link_to_article.short_description = _('Article') + # 4. 为自定义字段设置在Admin界面显示的名称(支持国际化) + link_to_userinfo.short_description = _('User') # 自定义字段“link_to_userinfo”显示为“用户” + link_to_article.short_description = _('Article') # 自定义字段“link_to_article”显示为“文章” \ No newline at end of file diff --git a/src/django-master/comments/apps.py b/src/django-master/comments/apps.py index ff01b77..182ec3c 100644 --- a/src/django-master/comments/apps.py +++ b/src/django-master/comments/apps.py @@ -1,5 +1,18 @@ +# 导入Django的App配置基类:所有应用的配置类都需继承此类 from django.apps import AppConfig class CommentsConfig(AppConfig): + """ + 「comments」应用的配置类 + 作用:定义应用的核心标识、初始化行为等,是Django识别和管理该应用的入口 + """ + # 应用的唯一名称(必须与应用目录名一致),Django通过该值定位应用 name = 'comments' +<<<<<<< HEAD + # 可选扩展配置(当前代码未实现,可根据需求添加): + # - verbose_name:应用的人性化名称(如 verbose_name = "评论管理"),用于Admin后台显示 + # - default_auto_field:指定模型默认的主键类型(如 default_auto_field = "django.db.models.BigAutoField") +======= + +>>>>>>> ZYY_branch diff --git a/src/django-master/comments/forms.py b/src/django-master/comments/forms.py index e83737d..db7b07c 100644 --- a/src/django-master/comments/forms.py +++ b/src/django-master/comments/forms.py @@ -1,13 +1,25 @@ -from django import forms -from django.forms import ModelForm +# 导入Django表单核心模块 +from django import forms # Django表单基础模块,提供表单字段、验证等功能 +from django.forms import ModelForm # 模型表单类,可快速将模型转换为表单(减少重复代码) +# 导入当前应用下的Comment模型(评论模型),表单需与该模型关联 from .models import Comment class CommentForm(ModelForm): + """ + 评论模型对应的模型表单类(继承ModelForm) + 核心作用:生成前端评论提交表单,并关联Comment模型处理数据存储 + """ + # 1. 自定义额外字段:父评论ID(用于实现评论回复功能) parent_comment_id = forms.IntegerField( - widget=forms.HiddenInput, required=False) + widget=forms.HiddenInput, # 表单控件:隐藏输入框(前端不显示,仅用于传递数据) + required=False # 是否必填:False表示允许为空(普通评论无父评论,回复评论时才传值) + ) + # 2. Meta类:模型表单的核心配置(关联模型、指定字段等) class Meta: - model = Comment - fields = ['body'] + model = Comment # 关联的模型:当前表单与Comment模型绑定 + fields = ['body'] # 表单需显示/处理的模型字段:仅包含评论内容(body字段) + # 注:Comment模型中其他字段(如author、article、creation_time等) + # 通常由后端自动填充(如从登录态获取author),无需前端用户输入 \ No newline at end of file diff --git a/src/django-master/comments/migrations/0001_initial.py b/src/django-master/comments/migrations/0001_initial.py index 61d1e53..c6d9eb5 100644 --- a/src/django-master/comments/migrations/0001_initial.py +++ b/src/django-master/comments/migrations/0001_initial.py @@ -1,38 +1,91 @@ # Generated by Django 4.1.7 on 2023-03-02 07:14 +# Django自动生成的数据库迁移文件:用于创建Comment(评论)模型对应的数据库表 +# 迁移文件作用:记录模型结构变化,通过`python manage.py migrate`同步到数据库 +# 导入Django迁移所需模块:配置、迁移基类、字段类型、关联逻辑、时间工具 from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone +import django.db.models.deletion # 用于定义外键删除策略(如CASCADE) +import django.utils.timezone # 用于时间字段的默认值 class Migration(migrations.Migration): + """ + Comment模型的初始迁移类:负责在数据库中创建`comments_comment`表 + 所有迁移类都必须继承migrations.Migration + """ + # 标识该迁移是模型的「初始迁移」(第一次为Comment模型创建表) initial = True + # 迁移依赖:执行当前迁移前,必须先执行依赖的迁移 dependencies = [ - ('blog', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0001_initial'), # 依赖blog应用的0001_initial迁移(因Comment关联blog的Article模型) + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型的可交换迁移(适配自定义用户模型) ] + # 迁移操作:当前迁移要执行的具体数据库操作(此处为「创建Comment表」) operations = [ + # 1. 创建Comment模型对应的数据库表 migrations.CreateModel( - name='Comment', + name='Comment', # 模型名称(与代码中定义的Comment类一致) + # 2. 定义表的字段(对应模型中的字段,映射到数据库表的列) fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('body', models.TextField(max_length=300, verbose_name='正文')), - ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), - ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), - ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), - ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), + # 主键字段:BigAutoField(自增bigint类型),Django默认主键,无需在模型中手动定义 + ('id', models.BigAutoField( + auto_created=True, # 自动创建 + primary_key=True, # 设为主键 + serialize=False, # 不序列化(主键无需序列化) + verbose_name='ID' # 字段显示名(Admin后台中显示) + )), + # 评论正文字段:TextField(长文本类型),对应模型中的body字段 + ('body', models.TextField( + max_length=300, # 最大长度300字符 + verbose_name='正文' # 显示名 + )), + # 创建时间字段:DateTimeField(日期时间类型),对应模型中的created_time + ('created_time', models.DateTimeField( + default=django.utils.timezone.now, # 默认值为当前时区时间 + verbose_name='创建时间' + )), + # 修改时间字段:DateTimeField,对应模型中的last_mod_time + ('last_mod_time', models.DateTimeField( + default=django.utils.timezone.now, + verbose_name='修改时间' + )), + # 是否显示字段:BooleanField(布尔类型),对应模型中的is_enable + ('is_enable', models.BooleanField( + default=True, # 默认值为True(创建后默认显示) + verbose_name='是否显示' + )), + # 外键:关联文章(blog应用的Article模型),对应模型中的article字段 + ('article', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, # 级联删除:文章删,评论删 + to='blog.article', # 关联目标:blog应用的Article模型 + verbose_name='文章' + )), + # 外键:关联用户(项目配置的用户模型),对应模型中的author字段 + ('author', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, # 级联删除:用户删,评论删 + to=settings.AUTH_USER_MODEL, # 关联目标:自定义用户模型(灵活适配) + verbose_name='作者' + )), + # 外键:关联父评论(自关联,Comment模型自身),对应模型中的parent_comment字段 + ('parent_comment', models.ForeignKey( + blank=True, # 表单中允许为空(普通评论无父评论) + null=True, # 数据库中允许为空(与blank=True配合) + on_delete=django.db.models.deletion.CASCADE, # 级联删除:父评论删,子评论删 + to='comments.comment', # 关联目标:comments应用的Comment模型(自关联) + verbose_name='上级评论' + )), ], + # 3. 模型的额外配置(映射到数据库表的属性和默认行为) options={ - 'verbose_name': '评论', - 'verbose_name_plural': '评论', - 'ordering': ['-id'], - 'get_latest_by': 'id', + 'verbose_name': '评论', # 模型单数显示名(Admin中显示) + 'verbose_name_plural': '评论', # 模型复数显示名(与单数一致,避免“评论s”) + 'ordering': ['-id'], # 表数据默认排序:按id倒序(最新评论在前) + 'get_latest_by': 'id', # 用Model.objects.latest()时,按id取最新数据 + # 注:Django会自动根据模型名生成表名:app名_模型名 → comments_comment }, ), - ] + ] \ No newline at end of file diff --git a/src/django-master/comments/migrations/0002_alter_comment_is_enable.py b/src/django-master/comments/migrations/0002_alter_comment_is_enable.py index 17c44db..a149ad4 100644 --- a/src/django-master/comments/migrations/0002_alter_comment_is_enable.py +++ b/src/django-master/comments/migrations/0002_alter_comment_is_enable.py @@ -5,14 +5,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('comments', '0001_initial'), - ] + dependencies = [ + ('comments', '0001_initial'), + ] - operations = [ - migrations.AlterField( - model_name='comment', - name='is_enable', - field=models.BooleanField(default=False, verbose_name='是否显示'), - ), +operations = [ + migrations.AlterField( + model_name='comment', + name='is_enable', + field=models.BooleanField(default=False, verbose_name='是否显示'), + ), ] diff --git a/src/django-master/comments/models.py b/src/django-master/comments/models.py index 7c3bbc8..f8fcd2e 100644 --- a/src/django-master/comments/models.py +++ b/src/django-master/comments/models.py @@ -1,39 +1,64 @@ -from django.conf import settings -from django.db import models -from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _ +# 导入Django核心模块:配置、数据库模型、时间工具、国际化 +from django.conf import settings # 导入项目配置(用于获取自定义用户模型) +from django.db import models # Django数据库模型基类,所有模型需继承models.Model +from django.utils.timezone import now # 获取当前时区时间,用于时间字段默认值 +from django.utils.translation import gettext_lazy as _ # 国际化翻译,支持多语言显示 +# 导入关联模型:从blog应用导入Article模型(评论需关联到具体文章) from blog.models import Article # Create your models here. - class Comment(models.Model): + """ + 评论模型:存储用户对文章的评论数据,支持评论回复(父子评论) + 与User(用户)、Article(文章)为多对一关系,与自身为自关联(实现回复) + """ + # 1. 评论正文:长文本字段,限制最大300字符 body = models.TextField('正文', max_length=300) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_modify_time = models.DateTimeField(_('last modify time'), default=now) + + # 2. 时间字段:创建时间和最后修改时间,默认值为当前时间 + creation_time = models.DateTimeField(_('creation time'), default=now) # 评论创建时间 + last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 评论最后修改时间 + + # 3. 关联用户:多对一(多个评论属于一个用户) author = models.ForeignKey( - settings.AUTH_USER_MODEL, - verbose_name=_('author'), - on_delete=models.CASCADE) + settings.AUTH_USER_MODEL, # 关联项目配置的用户模型(而非固定User,更灵活) + verbose_name=_('author'), # 字段在Admin后台显示的名称(支持国际化) + on_delete=models.CASCADE # 级联删除:若用户被删除,其所有评论也会被删除 + ) + + # 4. 关联文章:多对一(多个评论属于一篇文章) article = models.ForeignKey( - Article, - verbose_name=_('article'), - on_delete=models.CASCADE) + Article, # 关联blog应用的Article模型 + verbose_name=_('article'), # Admin显示名 + on_delete=models.CASCADE # 级联删除:文章删除,关联评论也删除 + ) + + # 5. 父评论:自关联(实现评论回复,多个子评论对应一个父评论) 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) + 'self', # 关联自身模型(表示父评论) + verbose_name=_('parent comment'), # Admin显示名 + blank=True, # 表单中允许为空(普通评论无父评论,回复评论才有) + null=True, # 数据库中允许为空(与blank=True配合使用) + on_delete=models.CASCADE # 级联删除:父评论删除,子评论也删除 + ) + + # 6. 启用状态:布尔值,控制评论是否在前端显示 + 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' + ordering = ['-id'] # 数据查询时按ID倒序排列(最新评论在前) + verbose_name = _('comment') # 模型单数显示名(Admin中“评论”) + verbose_name_plural = verbose_name # 模型复数显示名(与单数一致,避免“评论s”) + get_latest_by = 'id' # 使用Model.objects.latest()时,按id字段取最新数据 + # 模型实例的字符串表示:打印评论对象时显示正文(便于调试和Admin显示) def __str__(self): - return self.body + return self.body \ No newline at end of file diff --git a/src/django-master/comments/tests.py b/src/django-master/comments/tests.py index 2a7f55f..bcb79c6 100644 --- a/src/django-master/comments/tests.py +++ b/src/django-master/comments/tests.py @@ -1,109 +1,103 @@ -from django.test import Client, RequestFactory, TransactionTestCase -from django.urls import reverse +# 导入Django测试核心模块、URL工具及项目内模型/工具 +from django.test import Client, RequestFactory, TransactionTestCase # Django测试类:Client模拟HTTP请求,RequestFactory构造请求对象,TransactionTestCase支持事务回滚 +from django.urls import reverse # 生成URL(通过URL名称反向解析,避免硬编码) +# 导入项目内关联模型:用户、分类、文章、评论模型 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 # Create your tests here. - class CommentsTest(TransactionTestCase): + """ + 评论功能测试类:继承TransactionTestCase,用于测试评论的提交、状态更新等核心逻辑 + 支持数据库事务回滚,确保测试用例间数据隔离 + """ + def setUp(self): + """ + 测试前置初始化方法:每个测试用例执行前自动调用 + 作用:创建测试所需的基础数据(客户端、用户、系统配置等) + """ + # 1. 初始化测试工具:Client模拟浏览器请求,RequestFactory构造原始请求对象 self.client = Client() self.factory = RequestFactory() - from blog.models import BlogSettings - value = BlogSettings() - value.comment_need_review = True - value.save() + # 2. 初始化博客系统配置:设置“评论需审核”(模拟真实场景中评论需管理员审核才能显示) + from blog.models import BlogSettings # 局部导入避免循环引用 + value = BlogSettings() # 创建配置对象 + value.comment_need_review = True # 开启评论审核开关 + value.save() # 保存到测试数据库 + + # 3. 创建测试超级用户:用于模拟登录状态下提交评论 self.user = BlogUser.objects.create_superuser( - email="liangliangyy1@gmail.com", - username="liangliangyy1", - password="liangliangyy1") + email="liangliangyy1@gmail.com", # 测试邮箱 + username="liangliangyy1", # 测试用户名 + password="liangliangyy1" # 测试密码(明文,Django会自动加密存储) + ) def update_article_comment_status(self, article): + """ + 辅助方法:批量更新某篇文章下所有评论的启用状态(设为“启用”) + 模拟管理员审核通过评论的操作,用于测试审核后评论的显示逻辑 + 参数: + - article:目标文章对象(需更新其下所有评论) + """ + # 获取该文章下所有评论 comments = article.comment_set.all() + # 遍历评论,将“是否启用”字段设为True并保存 for comment in comments: comment.is_enable = True comment.save() def test_validate_comment(self): + """ + 核心测试用例:验证评论提交流程(登录→创建文章→提交评论→验证状态) + 覆盖场景:登录用户提交评论、评论未审核时不显示、审核后正常显示 + """ + # 1. 模拟用户登录:使用之前创建的测试超级用户登录 self.client.login(username='liangliangyy1', password='liangliangyy1') + # 2. 创建测试分类:文章需关联分类,先创建分类数据 category = Category() - category.name = "categoryccc" - category.save() + category.name = "categoryccc" # 分类名称 + category.save() # 保存到测试数据库 + # 3. 创建测试文章:评论需关联文章,创建一篇已发布的文章 article = Article() - article.title = "nicetitleccc" - article.body = "nicecontentccc" - article.author = self.user - article.category = category - article.type = 'a' - article.status = 'p' - article.save() - + article.title = "nicetitleccc" # 文章标题 + article.body = "nicecontentccc" # 文章内容 + article.author = self.user # 关联作者(测试用户) + article.category = category # 关联分类(刚创建的测试分类) + article.type = 'a' # 文章类型(假设'a'代表普通文章) + article.status = 'p' # 文章状态(假设'p'代表已发布) + article.save() # 保存到测试数据库 + + # 4. 构造评论提交URL:通过URL名称“comments:postcomment”反向解析,传入文章ID comment_url = reverse( - 'comments:postcomment', kwargs={ - 'article_id': article.id}) + 'comments:postcomment', + kwargs={'article_id': article.id} # URL参数:文章ID(指定评论所属文章) + ) - response = self.client.post(comment_url, - { - 'body': '123ffffffffff' - }) + # 5. 模拟POST请求提交评论:向评论URL发送包含评论内容的请求 + response = self.client.post( + comment_url, + {'body': '123ffffffffff'} # 请求参数:评论正文 + ) + # 6. 验证评论提交结果:检查响应状态码是否为302(重定向,通常提交后跳回文章页) 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) + # 7. 验证“未审核评论不显示”:重新获取文章,检查其评论列表长度是否为0(因评论需审核) + article = Article.objects.get(pk=article.pk) # 从数据库重新查询(避免缓存) + self.assertEqual(len(article.comment_list()), 0) # comment_list()应为自定义方法,返回启用的评论 - article = Article.objects.get(pk=article.pk) + # 8. 模拟审核通过:调用辅助方法,将该文章下所有评论设为“启用” 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) + # 9. 验证“审核后评论显示”:再次检查评论列表长度是否为1(审核通过后应显示) + self.assertEqual(len(article.comment_list()), 1) \ No newline at end of file diff --git a/src/django-master/comments/urls.py b/src/django-master/comments/urls.py index 7df3fab..090779f 100644 --- a/src/django-master/comments/urls.py +++ b/src/django-master/comments/urls.py @@ -1,11 +1,28 @@ +# 导入Django的URL路径配置模块 from django.urls import path +# 导入当前应用(comments)的视图模块(views.py),用于关联URL和视图逻辑 from . import views +# 定义应用命名空间:在模板或反向解析URL时,需通过「app_name:URL名称」的格式定位(如comments:postcomment) +# 作用:避免不同应用间URL名称冲突 app_name = "comments" + +# URL路由列表:配置URL路径与视图的映射关系 urlpatterns = [ + # 评论提交URL:处理用户对特定文章的评论提交请求 path( - 'article/Thank you very much for your comments on this site
- You can visit %(article_title)s - to review your comments, - Thank you again! -Thank you very much for your comments on this site
+ You can visit %s + to review your comments, + Thank you again! +