Compare commits

..

No commits in common. 'master' and 'ZYY_branch' have entirely different histories.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,36 +1,17 @@
<<<<<<< HEAD
#django核心组件导入 #django核心组件导入
from django import forms# Django 表单处理模块 from django import forms# Django 表单处理模块
from django.contrib.auth.admin import UserAdmin # Django 默认用户管理后台类 from django.contrib.auth.admin import UserAdmin # Django 默认用户管理后台类
from django.contrib.auth.forms import UserChangeForm # 用户信息修改表单基类 from django.contrib.auth.forms import UserChangeForm # 用户信息修改表单基类
from django.contrib.auth.forms import UsernameField# 用户名专用表单字段 from django.contrib.auth.forms import UsernameField# 用户名专用表单字段
from django.utils.translation import gettext_lazy as _ # 国际化翻译函数 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
<<<<<<< HEAD
# 本地应用导入 # 本地应用导入
# Register your models here. # Register your models here.
<<<<<<< HEAD
from .models import BlogUser # 导入自定义用户模型 from .models import BlogUser # 导入自定义用户模型
=======
# 导入自定义用户模型
=======
# jyn:导入自定义用户模型
>>>>>>> JYN_branch
from .models import BlogUser
>>>>>>> JYN_branch
class BlogUserCreationForm(forms.ModelForm): class BlogUserCreationForm(forms.ModelForm):
""" """
<<<<<<< HEAD
自定义用户创建表单(用于管理后台添加新用户) 自定义用户创建表单(用于管理后台添加新用户)
继承自 ModelForm专门处理 BlogUser 模型的创建 继承自 ModelForm专门处理 BlogUser 模型的创建
""" """
@ -44,85 +25,23 @@ class BlogUserCreationForm(forms.ModelForm):
class Meta: class Meta:
model = BlogUser# 关联的模型类 model = BlogUser# 关联的模型类
fields = ('email',)# 创建用户时显示的字段这里只显示email字段 fields = ('email',)# 创建用户时显示的字段这里只显示email字段
=======
自定义用户创建表单用于在管理员界面添加新用户
继承自ModelForm提供密码验证功能
"""
<<<<<<< HEAD
# 密码字段使用PasswordInput小部件确保输入不可见
=======
from .models import BlogUser# 导入当前应用下的BlogUser模型自定义用户模型
<<<<<<< HEAD
class BlogUserCreationForm(forms.ModelForm): # 定义两个密码字段使用PasswordInput小部件隐藏输入
>>>>>>> LXY_branch
=======
class BlogUserCreationForm(forms.ModelForm): # lxy定义两个密码字段使用PasswordInput小部件隐藏输入
>>>>>>> LXY_branch
=======
# jyn:密码字段使用PasswordInput小部件确保输入不可见
>>>>>>> JYN_branch
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
# jyn:确认密码字段,用于验证两次输入的密码是否一致
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
# 指定关联的模型
=======
# jyn:指定关联的模型
>>>>>>> JYN_branch
model = BlogUser
# jyn:表单中包含的字段,这里只显示邮箱
fields = ('email',)
>>>>>>> JYN_branch
def clean_password2(self): def clean_password2(self):
""" """
验证两次输入的密码是否一致 验证两次输入的密码是否一致
<<<<<<< HEAD
Django 表单验证方法方法名必须以 clean_ 开头 Django 表单验证方法方法名必须以 clean_ 开头
""" """
=======
model = BlogUser# 关联的模型是BlogUser
fields = ('email',) # 表单中显示的字段(仅邮箱,密码单独定义)
def clean_password2(self):# 验证两个密码是否一致
>>>>>>> LXY_branch
=======
model = BlogUser# lxy关联的模型是BlogUser
fields = ('email',) # lxy表单中显示的字段仅邮箱密码单独定义
def clean_password2(self):# lxy验证两个密码是否一致
>>>>>>> LXY_branch
# Check that the two password entries match # Check that the two password entries match
password1 = self.cleaned_data.get("password1")# 获取第一次输入的密码 password1 = self.cleaned_data.get("password1")# 获取第一次输入的密码
password2 = self.cleaned_data.get("password2")# 获取第二次输入的密码 password2 = self.cleaned_data.get("password2")# 获取第二次输入的密码
# 如果两次密码不一致,抛出验证错误 # 如果两次密码不一致,抛出验证错误
=======
这是Django表单验证机制的一部分方法名以clean_开头
"""
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
<<<<<<< HEAD
# 检查密码是否存在且不一致
>>>>>>> JYN_branch
=======
# jyn:检查密码是否存在且不一致
>>>>>>> JYN_branch
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
<<<<<<< HEAD
<<<<<<< HEAD
raise forms.ValidationError(_("passwords do not match"))# 错误信息(支持国际化) raise forms.ValidationError(_("passwords do not match"))# 错误信息(支持国际化)
return password2# 返回验证后的值 return password2# 返回验证后的值
def save(self, commit=True): def save(self, commit=True):
""" """
<<<<<<< HEAD
重写保存方法在保存用户前处理密码哈希 重写保存方法在保存用户前处理密码哈希
""" """
# Save the provided password in hashed format # Save the provided password in hashed format
@ -132,52 +51,15 @@ class BlogUserCreationForm(forms.ModelForm): # lxy定义两个密码字段使
user.source = 'adminsite' # 设置用户来源标记(表示通过管理后台创建) user.source = 'adminsite' # 设置用户来源标记(表示通过管理后台创建)
user.save()# 保存到数据库 user.save()# 保存到数据库
return user# 返回用户对象 return user# 返回用户对象
=======
重写保存方法确保密码以哈希形式存储
而不是明文存储
"""
# jyn:先调用父类方法获取用户对象,但不立即保存到数据库
user = super().save(commit=False)
# jyn:使用set_password方法对密码进行哈希处理
user.set_password(self.cleaned_data["password1"])
if commit:
# jyn:标记用户来源为管理员站点
user.source = 'adminsite'
user.save()
=======
raise forms.ValidationError(_("passwords do not match"))# 密码不一致时抛出错误
=======
raise forms.ValidationError(_("passwords do not match"))# lxy密码不一致时抛出错误
>>>>>>> LXY_branch
return password2
def save(self, commit=True):# lxy保存用户时对密码进行哈希处理后存储
# Save the provided password in hashed format
user = super().save(commit=False)# lxy先不提交到数据库
user.set_password(self.cleaned_data["password1"]) # lxy哈希处理密码
if commit:
<<<<<<< HEAD
user.source = 'adminsite'# 标记用户来源为“后台管理”
user.save()# 提交到数据库
>>>>>>> LXY_branch
=======
user.source = 'adminsite'# lxy标记用户来源为“后台管理”
user.save()# lxy提交到数据库
>>>>>>> LXY_branch
return user
>>>>>>> JYN_branch
class BlogUserChangeForm(UserChangeForm): class BlogUserChangeForm(UserChangeForm):
""" """
<<<<<<< HEAD
自定义用户信息修改表单用于管理后台编辑用户 自定义用户信息修改表单用于管理后台编辑用户
继承自 Django 自带的 UserChangeForm 继承自 Django 自带的 UserChangeForm
""" """
class Meta: class Meta:
<<<<<<< HEAD
<<<<<<< HEAD
model = BlogUser # 关联的模型类 model = BlogUser # 关联的模型类
fields = '__all__'# 显示所有字段 fields = '__all__'# 显示所有字段
field_classes = {'username': UsernameField}# 指定用户名使用专用字段类型 field_classes = {'username': UsernameField}# 指定用户名使用专用字段类型
@ -216,87 +98,3 @@ class BlogUserAdmin(UserAdmin):
# 默认排序规则按ID降序排列 # 默认排序规则按ID降序排列
ordering = ('-id',) ordering = ('-id',)
=======
自定义用户修改表单用于在管理员界面编辑用户信息
继承自Django内置的UserChangeForm
"""
class Meta:
# jyn:指定关联的模型
model = BlogUser
# jyn:显示所有字段
fields = '__all__'
# jyn:指定用户名字段的处理类
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
"""初始化方法,调用父类的初始化方法"""
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
"""
自定义用户管理员类用于在Django管理后台配置用户模型的显示和操作
继承自Django内置的UserAdmin
"""
# jyn:指定编辑用户时使用的表单
form = BlogUserChangeForm
# jyn:指定添加用户时使用的表单
add_form = BlogUserCreationForm
<<<<<<< HEAD
# 列表页面显示的字段
=======
model = BlogUser # 关联的模型是BlogUser
fields = '__all__'# 显示模型的所有字段
field_classes = {'username': UsernameField}# 为用户名字段指定类保持Django原生逻辑
=======
model = BlogUser # lxy关联的模型是BlogUser
fields = '__all__'# lxy显示模型的所有字段
field_classes = {'username': UsernameField}# lxy为用户名字段指定类保持Django原生逻辑
>>>>>>> LXY_branch
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)#lxy调用父类的初始化方法
class BlogUserAdmin(UserAdmin):
<<<<<<< HEAD
form = BlogUserChangeForm# 指定修改用户时使用的表单
add_form = BlogUserCreationForm# 指定创建用户时使用的表单
>>>>>>> LXY_branch
=======
form = BlogUserChangeForm#lxy指定修改用户时使用的表单
add_form = BlogUserCreationForm# lxy指定创建用户时使用的表单
>>>>>>> LXY_branch
=======
# jyn:列表页面显示的字段
>>>>>>> JYN_branch
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
<<<<<<< HEAD
'source'
)
# jyn:列表页面中可点击跳转的字段
list_display_links = ('id', 'username')
<<<<<<< HEAD
# 排序方式按id降序排列最新的用户在前
ordering = ('-id',)
>>>>>>> JYN_branch
=======
'source')
<<<<<<< HEAD
list_display_links = ('id', 'username')# 列表页中可点击跳转的字段
ordering = ('-id',)# 列表页的排序方式按ID倒序
>>>>>>> LXY_branch
=======
list_display_links = ('id', 'username')#lxy列表页中可点击跳转的字段
ordering = ('-id',)#lxy列表页的排序方式按ID倒序
>>>>>>> LXY_branch
=======
# jyn:排序方式按id降序排列最新的用户在前
ordering = ('-id',)
>>>>>>> JYN_branch

@ -1,44 +1,11 @@
<<<<<<< HEAD
<<<<<<< HEAD
from django.apps import AppConfig from django.apps import AppConfig
class AccountsConfig(AppConfig): class AccountsConfig(AppConfig):
""" """
<<<<<<< HEAD
Accounts 应用的配置类 Accounts 应用的配置类
功能 功能
1. 定义应用名称 Django 内部识别 1. 定义应用名称 Django 内部识别
2. 可在此处覆盖 ready() 方法以注册信号等 2. 可在此处覆盖 ready() 方法以注册信号等
""" """
name = 'accounts'# 必须与项目中的应用目录名完全一致 name = 'accounts'# 必须与项目中的应用目录名完全一致
=======
Django应用配置类用于定义'accounts'应用的元数据
每个Django应用都需要一个配置类用于设置应用的各种属性和行为
通常放在应用目录下的apps.py文件中
"""
<<<<<<< HEAD
# 应用的名称,必须与应用目录名一致
# 这个名称会被Django用来识别和管理应用
name = 'accounts'
>>>>>>> JYN_branch
=======
from django.apps import AppConfig#导入 Django 框架中用于应用配置的 AppConfig 类,这是 Django 应用配置的核心类
class AccountsConfig(AppConfig):#定义 AccountsConfig 类,继承自 AppConfig用于对 accounts 应用进行自定义配置
name = 'accounts'#指定该应用的名称为 accountsDjango 会通过这个名称来识别和管理该应用
>>>>>>> LXY_branch
=======
from django.apps import AppConfig#lxy导入 Django 框架中用于应用配置的 AppConfig 类,这是 Django 应用配置的核心类
class AccountsConfig(AppConfig):#lxy定义 AccountsConfig 类,继承自 AppConfig用于对 accounts 应用进行自定义配置
name = 'accounts'#lxy指定该应用的名称为 accountsDjango 会通过这个名称来识别和管理该应用
>>>>>>> LXY_branch
=======
# jyn:应用的名称,必须与应用目录名一致
# jyn:这个名称会被Django用来识别和管理应用
name = 'accounts'
>>>>>>> JYN_branch

@ -1,4 +1,3 @@
<<<<<<< HEAD
from django import forms #导入 Django 表单模块,用于创建自定义表单类。 from django import forms #导入 Django 表单模块,用于创建自定义表单类。
from django.contrib.auth import get_user_model, password_validation #get_user_model 用于获取项目中自定义的用户模型(遵循 Django 推荐的用户模型扩展方式)。 from django.contrib.auth import get_user_model, password_validation #get_user_model 用于获取项目中自定义的用户模型(遵循 Django 推荐的用户模型扩展方式)。
@ -28,80 +27,21 @@ class LoginForm(AuthenticationForm):
>>>>>>> 5d714be542986b7f935eb0ec4df9b0918e75eeca >>>>>>> 5d714be542986b7f935eb0ec4df9b0918e75eeca
self.fields['username'].widget = widgets.TextInput( self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"}) #为用户名字段设置文本输入小部件,定义占位符和 Bootstrap 样式类form-control 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 # jyn:导入自定义工具模块,可能用于验证码验证等功能
from .models import BlogUser # jyn:导入自定义用户模型
class LoginForm(AuthenticationForm):
"""
自定义登录表单继承自Django内置的AuthenticationForm
用于处理用户登录验证主要扩展了表单字段的样式
"""
def __init__(self, *args, **kwargs):
# jyn:调用父类构造方法初始化表单
super(LoginForm, self).__init__(*args, **kwargs)
# jyn:自定义用户名输入框添加占位符和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
<<<<<<< HEAD
# 自定义密码输入框添加占位符和CSS类
>>>>>>> 8b27cdad9a9ccc84febce3bcf1d211ed109f96f2
=======
# jyn:自定义密码输入框添加占位符和CSS类
>>>>>>> ef493f43496bf1ae99bf4389327ac0ae80bb9ae1
self.fields['password'].widget = widgets.PasswordInput( self.fields['password'].widget = widgets.PasswordInput(
<<<<<<< HEAD
attrs={'placeholder': "password", "class": "form-control"}) #为密码字段设置密码输入小部件,同样定义占位符和样式类。 attrs={'placeholder': "password", "class": "form-control"}) #为密码字段设置密码输入小部件,同样定义占位符和样式类。
<<<<<<< HEAD <<<<<<< HEAD
=======
attrs={'placeholder': "password", "class": "form-control"})
<<<<<<< HEAD
#自定义登录表单在__init__方法中设置username文本输入占位符、form-control样式和password密码输入占位符、form-control样式字段的前端显示样式。
>>>>>>> LXY_branch
=======
#lxy自定义登录表单在__init__方法中设置username文本输入占位符、form-control样式和password密码输入占位符、form-control样式字段的前端显示样式。
>>>>>>> LXY_branch
<<<<<<< HEAD
class RegisterForm(UserCreationForm): #继承 Django 内置的 UserCreationForm自定义注册表单的字段、样式和验证逻辑。 class RegisterForm(UserCreationForm): #继承 Django 内置的 UserCreationForm自定义注册表单的字段、样式和验证逻辑。
======= =======
<<<<<<< HEAD
# 注册表单继承自Django内置的UserCreationForm # 注册表单继承自Django内置的UserCreationForm
class RegisterForm(UserCreationForm): class RegisterForm(UserCreationForm):
>>>>>>> 5d714be542986b7f935eb0ec4df9b0918e75eeca >>>>>>> 5d714be542986b7f935eb0ec4df9b0918e75eeca
=======
class RegisterForm(UserCreationForm):
"""
自定义注册表单继承自Django内置的UserCreationForm
用于处理用户注册逻辑包含用户名邮箱和密码验证
"""
>>>>>>> 8b27cdad9a9ccc84febce3bcf1d211ed109f96f2
>>>>>>> JYN_branch
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# jyn:调用父类构造方法初始化表单
super(RegisterForm, self).__init__(*args, **kwargs) super(RegisterForm, self).__init__(*args, **kwargs)
<<<<<<< HEAD
# 自定义用户名、邮箱和密码字段的HTML属性 # 自定义用户名、邮箱和密码字段的HTML属性
=======
<<<<<<< HEAD
# 自定义各字段的输入控件,添加样式和占位符
>>>>>>> JYN_branch
=======
# jyn:自定义各字段的输入控件,添加样式和占位符
>>>>>>> JYN_branch
self.fields['username'].widget = widgets.TextInput( self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"}) attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput( self.fields['email'].widget = widgets.EmailInput(
@ -110,58 +50,19 @@ class RegisterForm(UserCreationForm):
attrs={'placeholder': "password", "class": "form-control"}) attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput( self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"}) attrs={'placeholder': "repeat password", "class": "form-control"})
<<<<<<< HEAD
<<<<<<< HEAD
# 验证邮箱唯一性 # 验证邮箱唯一性
=======
#__init__方法中设置username文本输入、email邮箱输入、password1和password2密码输入字段的占位符与form-control样式
>>>>>>> LXY_branch
=======
#lxy__init__方法中设置username文本输入、email邮箱输入、password1和password2密码输入字段的占位符与form-control样式
>>>>>>> LXY_branch
def clean_email(self): def clean_email(self):
"""
邮箱验证方法检查邮箱是否已被注册
表单验证机制中以clean_为前缀的方法会自动被调用
"""
email = self.cleaned_data['email'] email = self.cleaned_data['email']
# jyn:检查该邮箱是否已存在于数据库中
if get_user_model().objects.filter(email=email).exists(): if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists")) #jyn: 抛出验证错误 raise ValidationError(_("email already exists"))
return email return email
<<<<<<< HEAD
<<<<<<< HEAD
# 指定关联的用户模型和表单字段 # 指定关联的用户模型和表单字段
class Meta:
<<<<<<< HEAD
model = get_user_model() # 使用项目配置的用户模型可能是自定义的BlogUser
fields = ("username", "email") # 注册表单中显示的字段
=======
#clean_email方法验证邮箱是否已被注册若存在则抛出“邮箱已存在”的验证错误
class Meta:
model = get_user_model()
fields = ("username", "email")
#Meta类指定关联模型为自定义用户模型表单字段包含username和email
>>>>>>> LXY_branch
=======
#lxyclean_email方法验证邮箱是否已被注册若存在则抛出“邮箱已存在”的验证错误
class Meta: class Meta:
model = get_user_model() model = get_user_model()
fields = ("username", "email") fields = ("username", "email")
#lxy Meta类指定关联模型为自定义用户模型表单字段包含username和email
>>>>>>> LXY_branch
=======
model = get_user_model() # jyn:使用项目配置的用户模型可能是自定义的BlogUser
fields = ("username", "email") # jyn:注册表单中显示的字段
>>>>>>> JYN_branch
# 忘记密码表单(验证邮箱和验证码) # 忘记密码表单(验证邮箱和验证码)
class ForgetPasswordForm(forms.Form): class ForgetPasswordForm(forms.Form):
"""
忘记密码表单用于用户重置密码的流程
包含新密码确认密码邮箱和验证码字段
"""
# jyn:新密码字段
new_password1 = forms.CharField( new_password1 = forms.CharField(
label=_("New password"), label=_("New password"),
widget=forms.PasswordInput( widget=forms.PasswordInput(
@ -171,16 +72,7 @@ class ForgetPasswordForm(forms.Form):
} }
), ),
) )
<<<<<<< HEAD
# 新密码字段2用于确认 # 新密码字段2用于确认
=======
<<<<<<< HEAD
# 确认新密码字段
>>>>>>> JYN_branch
=======
# jyn:确认新密码字段
>>>>>>> JYN_branch
new_password2 = forms.CharField( new_password2 = forms.CharField(
label="确认密码", label="确认密码",
widget=forms.PasswordInput( widget=forms.PasswordInput(
@ -190,16 +82,7 @@ class ForgetPasswordForm(forms.Form):
} }
), ),
) )
<<<<<<< HEAD
# 邮箱字段 # 邮箱字段
=======
<<<<<<< HEAD
# 邮箱字段(用于验证用户身份)
>>>>>>> JYN_branch
=======
# jyn:邮箱字段(用于验证用户身份)
>>>>>>> JYN_branch
email = forms.EmailField( email = forms.EmailField(
label='邮箱', label='邮箱',
widget=forms.TextInput( widget=forms.TextInput(
@ -209,16 +92,7 @@ class ForgetPasswordForm(forms.Form):
} }
), ),
) )
<<<<<<< HEAD
# 验证码字段 # 验证码字段
=======
<<<<<<< HEAD
# 验证码字段(用于身份验证)
>>>>>>> JYN_branch
=======
# jyn:验证码字段(用于身份验证)
>>>>>>> JYN_branch
code = forms.CharField( code = forms.CharField(
label=_('Code'), label=_('Code'),
widget=forms.TextInput( widget=forms.TextInput(
@ -228,104 +102,37 @@ class ForgetPasswordForm(forms.Form):
} }
), ),
) )
<<<<<<< HEAD
<<<<<<< HEAD
# 验证两次输入的密码是否一致,并检查密码强度 # 验证两次输入的密码是否一致,并检查密码强度
=======
#定义new_password1新密码密码输入、new_password2确认密码密码输入、email邮箱文本输入、code验证码文本输入字段均设置form-control样式和占位符。
>>>>>>> LXY_branch
=======
#lxy定义new_password1新密码密码输入、new_password2确认密码密码输入、email邮箱文本输入、code验证码文本输入字段均设置form-control样式和占位符。
>>>>>>> LXY_branch
def clean_new_password2(self): def clean_new_password2(self):
"""验证两次输入的密码是否一致,并验证密码强度"""
password1 = self.data.get("new_password1") password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2") password2 = self.data.get("new_password2")
# jyn:检查两次密码是否一致
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match")) raise ValidationError(_("passwords do not match"))
<<<<<<< HEAD
password_validation.validate_password(password2)# 使用Django的密码验证器 password_validation.validate_password(password2)# 使用Django的密码验证器
=======
# jyn:使用Django内置的密码验证器验证密码强度
password_validation.validate_password(password2)
>>>>>>> JYN_branch
return password2 return password2
<<<<<<< HEAD
<<<<<<< HEAD
# 验证邮箱是否已注册 # 验证邮箱是否已注册
=======
# clean_new_password2方法验证两次新密码是否一致并对密码进行有效性校验
>>>>>>> LXY_branch
=======
#lxyclean_new_password2方法验证两次新密码是否一致并对密码进行有效性校验
>>>>>>> LXY_branch
def clean_email(self): def clean_email(self):
"""验证邮箱是否已注册"""
user_email = self.cleaned_data.get("email") user_email = self.cleaned_data.get("email")
# jyn:检查该邮箱是否存在于系统中 if not BlogUser.objects.filter(
if not BlogUser.objects.filter(email=user_email).exists(): email=user_email
# jyn:提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露) ).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist")) raise ValidationError(_("email does not exist"))
return user_email return user_email
<<<<<<< HEAD
# 验证用户输入的验证码是否正确 # 验证用户输入的验证码是否正确
=======
<<<<<<< HEAD
# clean_email方法验证邮箱是否已注册基于BlogUser模型未注册则抛出“邮箱不存在”的验证错误
>>>>>>> LXY_branch
=======
#lxyclean_email方法验证邮箱是否已注册基于BlogUser模型未注册则抛出“邮箱不存在”的验证错误
>>>>>>> LXY_branch
def clean_code(self): def clean_code(self):
"""验证验证码是否有效"""
code = self.cleaned_data.get("code") code = self.cleaned_data.get("code")
<<<<<<< HEAD
<<<<<<< HEAD
error = utils.verify(# 调用工具函数验证验证码 error = utils.verify(# 调用工具函数验证验证码
=======
# 调用工具函数验证邮箱和验证码是否匹配
=======
# jyn:调用工具函数验证邮箱和验证码是否匹配
>>>>>>> JYN_branch
error = utils.verify(
>>>>>>> JYN_branch
email=self.cleaned_data.get("email"), email=self.cleaned_data.get("email"),
code=code, code=code,
) )
if error: if error:
raise ValidationError(error) # jyn:验证码无效时抛出错误 raise ValidationError(error)
return code return code
<<<<<<< HEAD
<<<<<<< HEAD
# 忘记密码功能中的验证码发送表单(仅需邮箱字段) # 忘记密码功能中的验证码发送表单(仅需邮箱字段)
=======
#clean_code方法调用工具方法utils.verify验证验证码有效性无效则抛出错误
=======
#lxy clean_code方法调用工具方法utils.verify验证验证码有效性无效则抛出错误
>>>>>>> LXY_branch
>>>>>>> LXY_branch
class ForgetPasswordCodeForm(forms.Form): class ForgetPasswordCodeForm(forms.Form):
"""
发送密码重置验证码的表单
仅包含邮箱字段用于提交需要重置密码的邮箱
"""
email = forms.EmailField( email = forms.EmailField(
label=_('Email'), label=_('Email'),
<<<<<<< HEAD
) )
<<<<<<< HEAD
=======
)
#仅包含email字段邮箱输入用于忘记密码流程中验证邮箱的步骤
>>>>>>> LXY_branch
=======
#lxy仅包含email字段邮箱输入用于忘记密码流程中验证邮箱的步骤
>>>>>>> LXY_branch

@ -3,14 +3,12 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site # jyn:导入获取当前站点信息的工具函数 from djangoblog.utils import get_current_site
# Create your models here. # Create your models here.
<<<<<<< HEAD
# 自定义用户模型继承Django内置的AbstractUser # 自定义用户模型继承Django内置的AbstractUser
class BlogUser(AbstractUser): class BlogUser(AbstractUser):
<<<<<<< HEAD
# 用户昵称(可选) # 用户昵称(可选)
nickname = models.CharField(_('nick name'), max_length=100, blank=True) nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# 账号创建时间(默认当前时间) # 账号创建时间(默认当前时间)
@ -18,136 +16,24 @@ class BlogUser(AbstractUser):
# 最后修改时间(默认当前时间) # 最后修改时间(默认当前时间)
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 账号创建来源(如:网站注册/第三方登录等,可选) # 账号创建来源(如:网站注册/第三方登录等,可选)
=======
"""
jyn:自定义用户模型继承自Django内置的AbstractUser
扩展了默认用户模型的字段以满足博客系统的特定需求
"""
# jyn:昵称字段,允许为空
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# jyn:账号创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# jyn:最后修改时间,默认为当前时间(通常需要在保存时更新)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
<<<<<<< HEAD
# 账号创建来源(如管理员添加、前台注册等),用于追踪用户注册渠道
>>>>>>> JYN_branch
=======
# jyn:账号创建来源(如管理员添加、前台注册等),用于追踪用户注册渠道
>>>>>>> JYN_branch
source = models.CharField(_('create source'), max_length=100, blank=True) source = models.CharField(_('create source'), max_length=100, blank=True)
# 获取用户详情页的绝对URL用于模板中的{% url %}反向解析) # 获取用户详情页的绝对URL用于模板中的{% url %}反向解析)
=======
class BlogUser(AbstractUser):#lxy自定义用户模型BlogUser继承自Django内置的AbstractUser可扩展的用户抽象类
nickname = models.CharField(_('nick name'), max_length=100, blank=True)#lxy定义nickname字段字符类型支持国际化翻译最大长度100可为空。
creation_time = models.DateTimeField(_('creation time'), default=now)#lxy定义creation_time字段日期时间类型默认值为当前时间now方法
last_modify_time = models.DateTimeField(_('last modify time'), default=now)#lxy定义last_modify_time字段日期时间类型默认值为当前时间。
source = models.CharField(_('create source'), max_length=100, blank=True)#lxy定义source字段字符类型记录用户创建来源最大长度100可为空。
>>>>>>> LXY_branch
def get_absolute_url(self): def get_absolute_url(self):
"""
jyn:返回用户详情页的URL
Django推荐为模型定义此方法用于获取对象的标准URL
"""
return reverse( return reverse(
<<<<<<< HEAD
'blog:author_detail', kwargs={ 'blog:author_detail', kwargs={
<<<<<<< HEAD
<<<<<<< HEAD
'author_name': self.username}) 'author_name': self.username})
# 定义对象的字符串表示Admin后台和shell中显示 # 定义对象的字符串表示Admin后台和shell中显示
=======
'blog:author_detail', # 对应的URL名称
kwargs={'author_name': self.username} # 传递的参数
)
=======
'author_name': self.username})#定义获取用户详情页绝对URL的方法通过reverse反向解析路由blog:author_detail传递username参数。
>>>>>>> LXY_branch
=======
'author_name': self.username})#lxy定义获取用户详情页绝对URL的方法通过reverse反向解析路由blog:author_detail传递username参数。
>>>>>>> LXY_branch
>>>>>>> JYN_branch
def __str__(self): def __str__(self):
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
"""模型的字符串表示,这里返回用户的邮箱"""
=======
"""jyn:模型的字符串表示,这里返回用户的邮箱"""
>>>>>>> JYN_branch
return self.email return self.email
# 获取用户详情页的完整URL包含域名用于分享链接 # 获取用户详情页的完整URL包含域名用于分享链接
=======
return self.email#定义对象的字符串表示方法返回用户的email
=======
return self.email#lxy定义对象的字符串表示方法返回用户的email
>>>>>>> LXY_branch
>>>>>>> LXY_branch
def get_full_url(self): def get_full_url(self):
<<<<<<< HEAD
<<<<<<< HEAD
site = get_current_site().domain# 获取当前站点域名 site = get_current_site().domain# 获取当前站点域名
url = "https://{site}{path}".format(site=site, url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url()) path=self.get_absolute_url())
<<<<<<< HEAD
<<<<<<< HEAD
=======
"""获取用户详情页的完整URL包含域名"""
# 获取当前站点的域名
=======
"""jyn:获取用户详情页的完整URL包含域名"""
# jyn:获取当前站点的域名
>>>>>>> JYN_branch
site = get_current_site().domain
# jyn:拼接完整URL协议+域名+路径)
url = "https://{site}{path}".format(
site=site,
path=self.get_absolute_url() # jyn:调用get_absolute_url获取相对路径
)
>>>>>>> JYN_branch
return url return url
# 元数据配置(模型级别的选项) # 元数据配置(模型级别的选项)
class Meta: class Meta:
<<<<<<< HEAD
<<<<<<< HEAD
ordering = ['-id'] # 默认按ID降序排列 ordering = ['-id'] # 默认按ID降序排列
verbose_name = _('user') # 单数形式名称(后台显示) verbose_name = _('user') # 单数形式名称(后台显示)
verbose_name_plural = verbose_name# 复数形式名称(后台显示) verbose_name_plural = verbose_name# 复数形式名称(后台显示)
get_latest_by = 'id'# 指定最新记录的排序字段 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']#查询结果按id倒序排列
verbose_name = _('user')#模型的单数显示名称(支持国际化)
verbose_name_plural = verbose_name#模型的复数显示名称与单数一致。
get_latest_by = 'id'#指定按id获取最新记录
>>>>>>> LXY_branch
=======
return url#lxy定义获取带域名的完整URL方法结合当前站点域名和get_absolute_url生成完整链接
class Meta:
ordering = ['-id']#lxy查询结果按id倒序排列
verbose_name = _('user')#lxy模型的单数显示名称支持国际化
verbose_name_plural = verbose_name#lxy模型的复数显示名称与单数一致。
get_latest_by = 'id'#lxy指定按id获取最新记录
>>>>>>> LXY_branch
=======
"""jyn:模型的元数据配置"""
ordering = ['-id'] # jyn:默认排序方式按id降序最新创建的用户在前
verbose_name = _('user') # jyn:模型的单数显示名称(支持国际化)
verbose_name_plural = verbose_name # jyn:模型的复数显示名称(与单数相同)
get_latest_by = 'id' # jyn:指定使用id字段获取最新对象用于Model.objects.latest()
>>>>>>> JYN_branch

@ -3,16 +3,15 @@ from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser # jyn:导入自定义用户模型 from accounts.models import BlogUser
from blog.models import Article, Category # jyn:导入博客相关模型 from blog.models import Article, Category
from djangoblog.utils import * # jyn:导入项目工具函数 from djangoblog.utils import *
from . import utils # jyn:导入当前应用的工具函数 from . import utils
# Create your tests here. # Create your tests here.
# 创建测试类继承Django的TestCase # 创建测试类继承Django的TestCase
class AccountTest(TestCase): class AccountTest(TestCase):
<<<<<<< HEAD
# 测试初始化方法(每个测试方法运行前都会执行) # 测试初始化方法(每个测试方法运行前都会执行)
def setUp(self): def setUp(self):
# 初始化测试客户端(模拟浏览器请求) # 初始化测试客户端(模拟浏览器请求)
@ -20,33 +19,11 @@ class AccountTest(TestCase):
# 初始化请求工厂(用于生成请求对象) # 初始化请求工厂(用于生成请求对象)
self.factory = RequestFactory() self.factory = RequestFactory()
# 创建一个普通测试用户 # 创建一个普通测试用户
=======
"""
账号相关功能的测试类继承自Django的TestCase
包含用户登录注册密码重置等功能的测试用例
"""
def setUp(self):
"""
测试前的初始化方法会在每个测试方法执行前运行
用于创建测试所需的基础数据
"""
<<<<<<< HEAD
self.client = Client() # 创建测试客户端,用于模拟用户请求
self.factory = RequestFactory() # 创建请求工厂,用于构建请求对象
# 创建一个测试用户
>>>>>>> JYN_branch
=======
self.client = Client() # jyn:创建测试客户端,用于模拟用户请求
self.factory = RequestFactory() # jyn:创建请求工厂,用于构建请求对象
# jyn:创建一个测试用户
>>>>>>> JYN_branch
self.blog_user = BlogUser.objects.create_user( self.blog_user = BlogUser.objects.create_user(
username="test", username="test",
email="admin@admin.com", email="admin@admin.com",
password="12345678" password="12345678"
) )
<<<<<<< HEAD
<<<<<<< HEAD
# 测试用的随机字符串 # 测试用的随机字符串
self.new_test = "xxx123--=" self.new_test = "xxx123--="
# 测试用户账号验证功能 # 测试用户账号验证功能
@ -54,85 +31,30 @@ class AccountTest(TestCase):
# 获取当前站点域名 # 获取当前站点域名
site = get_current_site().domain site = get_current_site().domain
# 创建一个超级用户(用于测试管理员权限) # 创建一个超级用户(用于测试管理员权限)
=======
self.new_test = "xxx123--=" # 测试用的新密码
=======
self.new_test = "xxx123--=" #jyn: 测试用的新密码
>>>>>>> JYN_branch
def test_validate_account(self):
"""测试用户账号验证相关功能,包括登录和管理员权限"""
site = get_current_site().domain # jyn:获取当前站点域名
<<<<<<< HEAD
# 创建一个超级用户
>>>>>>> JYN_branch
=======
# jyn:创建一个超级用户
>>>>>>> JYN_branch
user = BlogUser.objects.create_superuser( user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", email="liangliangyy1@gmail.com",
username="liangliangyy1", username="liangliangyy1",
password="qwer!@#$ggg") password="qwer!@#$ggg")
<<<<<<< HEAD
<<<<<<< HEAD
# 从数据库获取刚创建的超级用户(验证是否创建成功) # 从数据库获取刚创建的超级用户(验证是否创建成功)
testuser = BlogUser.objects.get(username='liangliangyy1') testuser = BlogUser.objects.get(username='liangliangyy1')
=======
# 获取创建的用户
testuser = BlogUser.objects.get(username='liangliangyy1')
>>>>>>> JYN_branch
# 测试登录功能 # 测试登录功能
loginresult = self.client.login( loginresult = self.client.login(
username='liangliangyy1', username='liangliangyy1',
password='qwer!@#$ggg') password='qwer!@#$ggg')
<<<<<<< HEAD
self.assertEqual(loginresult, True)# 验证登录成功 self.assertEqual(loginresult, True)# 验证登录成功
# 测试访问管理员后台 # 测试访问管理员后台
response = self.client.get('/admin/') response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)# 验证返回200状态码 self.assertEqual(response.status_code, 200)# 验证返回200状态码
=======
self.assertEqual(loginresult, True) # 断言登录成功
=======
# jyn:获取创建的用户
testuser = BlogUser.objects.get(username='liangliangyy1')
# jyn:测试登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True) #jyn: 断言登录成功
>>>>>>> JYN_branch
# jyn:测试访问管理员页面
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功
<<<<<<< HEAD
>>>>>>> JYN_branch
# 创建测试分类 # 创建测试分类
=======
# jyn:创建测试分类
>>>>>>> JYN_branch
category = Category() category = Category()
category.name = "categoryaaa" category.name = "categoryaaa"
category.creation_time = timezone.now() category.creation_time = timezone.now()
category.last_modify_time = timezone.now() category.last_modify_time = timezone.now()
category.save() category.save()
<<<<<<< HEAD
=======
<<<<<<< HEAD
>>>>>>> JYN_branch
# 创建测试文章 # 创建测试文章
=======
# jyn:创建测试文章
>>>>>>> JYN_branch
article = Article() article = Article()
article.title = "nicetitleaaa" article.title = "nicetitleaaa"
article.body = "nicecontentaaa" article.body = "nicecontentaaa"
<<<<<<< HEAD
article.author = user# 关联超级用户 article.author = user# 关联超级用户
article.category = category# 关联上面创建的分类 article.category = category# 关联上面创建的分类
article.type = 'a' # 文章类型 article.type = 'a' # 文章类型
@ -141,159 +63,62 @@ class AccountTest(TestCase):
# 测试访问文章的管理URL # 测试访问文章的管理URL
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200) # 验证返回200状态码 self.assertEqual(response.status_code, 200) # 验证返回200状态码
=======
article.author = user
article.category = category
article.type = 'a' # jyn:假设'a'表示文章类型
article.status = 'p' # jyn:假设'p'表示已发布
article.save()
# jyn:测试访问文章管理页面
response = self.client.get(article.get_admin_url())
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(response.status_code, 200) # 断言页面访问成功
>>>>>>> JYN_branch
=======
self.assertEqual(response.status_code, 200)#测试管理员账号登录后台功能:创建超级用户,验证登录状态和后台页面访问状态
>>>>>>> LXY_branch
=======
self.assertEqual(response.status_code, 200)#lxy测试管理员账号登录后台功能创建超级用户验证登录状态和后台页面访问状态
>>>>>>> LXY_branch
=======
self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功
>>>>>>> JYN_branch
# 测试用户注册功能 # 测试用户注册功能
def test_validate_register(self): def test_validate_register(self):
<<<<<<< HEAD
# 验证测试邮箱初始不存在 # 验证测试邮箱初始不存在
=======
"""测试用户注册功能,包括注册流程、邮箱验证和权限控制"""
<<<<<<< HEAD
# 初始状态下,该邮箱应不存在
>>>>>>> JYN_branch
=======
# jyn:初始状态下,该邮箱应不存在
>>>>>>> JYN_branch
self.assertEquals( self.assertEquals(
0, len( 0, len(
BlogUser.objects.filter( BlogUser.objects.filter(
email='user123@user.com'))) email='user123@user.com')))
<<<<<<< HEAD
# 模拟注册请求 # 模拟注册请求
=======
<<<<<<< HEAD
# 模拟用户注册提交
>>>>>>> JYN_branch
=======
# jyn:模拟用户注册提交
>>>>>>> JYN_branch
response = self.client.post(reverse('account:register'), { response = self.client.post(reverse('account:register'), {
'username': 'user1233', 'username': 'user1233',
'email': 'user123@user.com', 'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T', 'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T', 'password2': 'password123!q@wE#R$T',
}) })
<<<<<<< HEAD
# 验证用户已创建(通过邮箱查询) # 验证用户已创建(通过邮箱查询)
=======
<<<<<<< HEAD
# 注册后,该邮箱应存在
>>>>>>> JYN_branch
=======
#jyn: 注册后,该邮箱应存在
>>>>>>> JYN_branch
self.assertEquals( self.assertEquals(
1, len( 1, len(
BlogUser.objects.filter( BlogUser.objects.filter(
email='user123@user.com'))) email='user123@user.com')))
<<<<<<< HEAD
# 获取刚注册的用户 # 获取刚注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0] user = BlogUser.objects.filter(email='user123@user.com')[0]
# 生成验证签名(用于邮箱验证等场景) # 生成验证签名(用于邮箱验证等场景)
=======
#jyn: 获取刚注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
<<<<<<< HEAD
# 生成验证链接(模拟邮箱验证流程)
>>>>>>> JYN_branch
=======
# jyn:生成验证链接(模拟邮箱验证流程)
>>>>>>> JYN_branch
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 构造验证URL # 构造验证URL
path = reverse('accounts:result') path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format( url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign) path=path, id=user.id, sign=sign)
<<<<<<< HEAD
# 测试访问验证URL # 测试访问验证URL
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) # 验证返回200状态码 self.assertEqual(response.status_code, 200) # 验证返回200状态码
# 使用测试客户端登录 # 使用测试客户端登录
self.client.login(username='user1233', password='password123!q@wE#R$T') self.client.login(username='user1233', password='password123!q@wE#R$T')
# 获取指定邮箱的用户并设置为超级用户和工作人员 # 获取指定邮箱的用户并设置为超级用户和工作人员
=======
# jyn:访问验证链接
response = self.client.get(url)
self.assertEqual(response.status_code, 200) # jyn:断言验证页面访问成功
# jyn:登录新注册用户
self.client.login(username='user1233', password='password123!q@wE#R$T')
<<<<<<< HEAD
# 提升用户权限
>>>>>>> JYN_branch
=======
# jyn:提升用户权限
>>>>>>> JYN_branch
user = BlogUser.objects.filter(email='user123@user.com')[0] user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True user.is_superuser = True
user.is_staff = True user.is_staff = True
user.save() user.save()
<<<<<<< HEAD
# 删除侧边栏缓存 # 删除侧边栏缓存
delete_sidebar_cache() delete_sidebar_cache()
# 创建分类 # 创建分类
=======
# jyn:清除缓存
delete_sidebar_cache()
<<<<<<< HEAD
# 创建测试分类
>>>>>>> JYN_branch
=======
# jyn:创建测试分类
>>>>>>> JYN_branch
category = Category() category = Category()
category.name = "categoryaaa" category.name = "categoryaaa"
category.creation_time = timezone.now() category.creation_time = timezone.now()
category.last_modify_time = timezone.now() category.last_modify_time = timezone.now()
category.save() category.save()
<<<<<<< HEAD
# 创建文章 # 创建文章
=======
<<<<<<< HEAD
# 创建测试文章
>>>>>>> JYN_branch
=======
# jyn:创建测试文章
>>>>>>> JYN_branch
article = Article() article = Article()
article.category = category article.category = category
article.title = "nicetitle333" article.title = "nicetitle333"
article.body = "nicecontentttt" article.body = "nicecontentttt"
article.author = user article.author = user
article.type = 'a' article.type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()
<<<<<<< HEAD
# 测试已登录用户访问文章管理URL # 测试已登录用户访问文章管理URL
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -304,58 +129,17 @@ class AccountTest(TestCase):
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200]) self.assertIn(response.status_code, [301, 302, 200])
# 测试使用错误密码登录 # 测试使用错误密码登录
=======
# jyn:测试访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# jyn:测试登出功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200]) # 登出通常是重定向
# jyn:登出后访问管理页面(应被拒绝或重定向)
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
<<<<<<< HEAD
# 使用错误密码登录
>>>>>>> JYN_branch
=======
# jyn:使用错误密码登录
>>>>>>> JYN_branch
response = self.client.post(reverse('account:login'), { response = self.client.post(reverse('account:login'), {
'username': 'user1233', 'username': 'user1233',
'password': 'password123'# 注意这里密码与登录时使用的不同 'password': 'password123'# 注意这里密码与登录时使用的不同
}) })
self.assertIn(response.status_code, [301, 302, 200]) self.assertIn(response.status_code, [301, 302, 200])
<<<<<<< HEAD
# 测试使用错误密码登录后访问文章管理URL应重定向 # 测试使用错误密码登录后访问文章管理URL应重定向
=======
<<<<<<< HEAD
# 错误登录后访问管理页面
>>>>>>> JYN_branch
=======
# jyn:错误登录后访问管理页面
>>>>>>> JYN_branch
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url())
<<<<<<< HEAD
<<<<<<< HEAD
self.assertIn(response.status_code, [301, 302, 200]) self.assertIn(response.status_code, [301, 302, 200])
# 测试邮箱验证码验证 # 测试邮箱验证码验证
=======
self.assertIn(response.status_code, [301, 302, 200])#测试用户注册流程:验证注册前后用户数量变化,邮箱验证链接的有效性,以及注册后用户权限、文章发布等功能
=======
self.assertIn(response.status_code, [301, 302, 200])#lxy测试用户注册流程验证注册前后用户数量变化邮箱验证链接的有效性以及注册后用户权限、文章发布等功能
>>>>>>> LXY_branch
>>>>>>> LXY_branch
def test_verify_email_code(self): def test_verify_email_code(self):
"""测试邮箱验证码验证功能"""
to_email = "admin@admin.com" to_email = "admin@admin.com"
<<<<<<< HEAD
<<<<<<< HEAD
code = generate_code() # 生成验证码 code = generate_code() # 生成验证码
utils.set_code(to_email, code)# 存储验证码 utils.set_code(to_email, code)# 存储验证码
utils.send_verify_email(to_email, code) # 发送验证邮件(实际测试中可能不会真的发送) utils.send_verify_email(to_email, code) # 发送验证邮件(实际测试中可能不会真的发送)
@ -364,178 +148,60 @@ class AccountTest(TestCase):
self.assertEqual(err, None) self.assertEqual(err, None)
# 测试错误邮箱 # 测试错误邮箱
err = utils.verify("admin@123.com", code) err = utils.verify("admin@123.com", code)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(type(err), str)# 应返回错误信息字符串 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
=======
self.assertEqual(type(err), str)#lxy测试邮箱验证码功能验证有效邮箱和无效邮箱的验证码校验结果
>>>>>>> LXY_branch
=======
code = generate_code() # jyn:生成验证码
utils.set_code(to_email, code) # jyn:存储验证码
utils.send_verify_email(to_email, code) # jyn:发送验证邮件
>>>>>>> JYN_branch
# jyn:验证正确的邮箱和验证码
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None) # jyn:应无错误
# jyn:验证错误的邮箱和正确的验证码
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) # jyn:应返回错误信息
>>>>>>> JYN_branch
def test_forget_password_email_code_success(self): def test_forget_password_email_code_success(self):
"""测试发送密码重置验证码成功的情况"""
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password_code"), path=reverse("account:forget_password_code"),
<<<<<<< HEAD
<<<<<<< HEAD
data=dict(email="admin@admin.com") # 使用正确邮箱格式 data=dict(email="admin@admin.com") # 使用正确邮箱格式
) )
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.content.decode("utf-8"), "ok")# 验证返回成功消息 self.assertEqual(resp.content.decode("utf-8"), "ok")# 验证返回成功消息
# 测试忘记密码发送验证码功能 - 失败情况 # 测试忘记密码发送验证码功能 - 失败情况
def test_forget_password_email_code_fail(self): 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
=======
self.assertEqual(resp.content.decode("utf-8"), "ok")#lxy测试忘记密码的邮箱验证码发送分别验证成功和失败场景如邮箱错误的接口响应
>>>>>>> LXY_branch
def test_forget_password_email_code_fail(self):
"""测试发送密码重置验证码失败的情况"""
# 不提供邮箱
>>>>>>> JYN_branch
=======
data=dict(email="admin@admin.com") # jyn:使用已存在的邮箱
)
self.assertEqual(resp.status_code, 200) # jyn:断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") # jyn:断言返回成功信息
def test_forget_password_email_code_fail(self):
"""测试发送密码重置验证码失败的情况"""
# jyn:不提供邮箱
>>>>>>> JYN_branch
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password_code"), path=reverse("account:forget_password_code"),
data=dict() data=dict()
) )
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试提供错误格式邮箱 # 测试提供错误格式邮箱
=======
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误信息
# 提供无效格式的邮箱
>>>>>>> JYN_branch
=======
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # jyn:断言返回错误信息
# jyn:提供无效格式的邮箱
>>>>>>> JYN_branch
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password_code"), path=reverse("account:forget_password_code"),
data=dict(email="admin@com") data=dict(email="admin@com")
) )
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试忘记密码重置功能 - 成功情况 # 测试忘记密码重置功能 - 成功情况
def test_forget_password_email_success(self): def test_forget_password_email_success(self):
code = generate_code() 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
=======
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #jyn: 断言返回错误信息
def test_forget_password_email_success(self):
"""测试密码重置成功的情况"""
code = generate_code() # jyn:生成验证码
utils.set_code(self.blog_user.email, code) #jyn: 存储验证码
# jyn:准备重置密码的数据
>>>>>>> JYN_branch
data = dict( data = dict(
new_password1=self.new_test, # 新密码 new_password1=self.new_test, # 新密码
new_password2=self.new_test,# 确认密码 new_password2=self.new_test,# 确认密码
email=self.blog_user.email,# 用户邮箱 email=self.blog_user.email,# 用户邮箱
code=code, # 验证码 code=code, # 验证码
) )
<<<<<<< HEAD
<<<<<<< HEAD
# 提交重置密码请求 # 提交重置密码请求
=======
# 提交密码重置请求
>>>>>>> JYN_branch
=======
# jyn:提交密码重置请求
>>>>>>> JYN_branch
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password"), path=reverse("account:forget_password"),
data=data data=data
) )
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.status_code, 302) # 应重定向 self.assertEqual(resp.status_code, 302) # 应重定向
=======
self.assertEqual(resp.status_code, 302) # 成功重置后通常重定向
>>>>>>> JYN_branch
=======
self.assertEqual(resp.status_code, 302) # jyn:成功重置后通常重定向
>>>>>>> JYN_branch
# jyn:验证密码是否已更新 # 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter( blog_user = BlogUser.objects.filter(
email=self.blog_user.email, email=self.blog_user.email,
).first() # jyn:type: BlogUser ).first() # type: BlogUser
self.assertNotEqual(blog_user, None) # jyn:断言用户存在 self.assertNotEqual(blog_user, None)
# jyn:断言密码修改成功
self.assertEqual(blog_user.check_password(data["new_password1"]), True) self.assertEqual(blog_user.check_password(data["new_password1"]), True)
# 测试忘记密码重置功能 - 用户不存在情况 # 测试忘记密码重置功能 - 用户不存在情况
def test_forget_password_email_not_user(self): def test_forget_password_email_not_user(self):
"""测试使用不存在的邮箱重置密码的情况"""
data = dict( data = dict(
new_password1=self.new_test, new_password1=self.new_test,
new_password2=self.new_test, new_password2=self.new_test,
<<<<<<< HEAD
<<<<<<< HEAD
email="123@123.com",# 不存在的邮箱 email="123@123.com",# 不存在的邮箱
=======
email="123@123.com", # 不存在的邮箱
>>>>>>> JYN_branch
=======
email="123@123.com", # jyn:不存在的邮箱
>>>>>>> JYN_branch
code="123456", code="123456",
) )
resp = self.client.post( resp = self.client.post(
@ -543,68 +209,23 @@ class AccountTest(TestCase):
data=data data=data
) )
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.status_code, 200) # 应返回错误页面而非重定向 self.assertEqual(resp.status_code, 200) # 应返回错误页面而非重定向
=======
self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码
>>>>>>> JYN_branch
=======
self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码
>>>>>>> JYN_branch
# 测试忘记密码重置功能 - 验证码错误情况 # 测试忘记密码重置功能 - 验证码错误情况
def test_forget_password_email_code_error(self): def test_forget_password_email_code_error(self):
<<<<<<< HEAD
code = generate_code() code = generate_code()
utils.set_code(self.blog_user.email, code)# 设置正确验证码 utils.set_code(self.blog_user.email, code)# 设置正确验证码
# 使用错误验证码提交 # 使用错误验证码提交
=======
"""测试使用错误的验证码重置密码的情况"""
<<<<<<< HEAD
code = generate_code() # 生成正确验证码
utils.set_code(self.blog_user.email, code) # 存储验证码
# 使用错误的验证码
>>>>>>> JYN_branch
=======
code = generate_code() # jyn:生成正确验证码
utils.set_code(self.blog_user.email, code) #jyn: 存储验证码
# jyn:使用错误的验证码
>>>>>>> JYN_branch
data = dict( data = dict(
new_password1=self.new_test, new_password1=self.new_test,
new_password2=self.new_test, new_password2=self.new_test,
email=self.blog_user.email, email=self.blog_user.email,
<<<<<<< HEAD
<<<<<<< HEAD
code="111111",# 错误验证码 code="111111",# 错误验证码
=======
code="111111", # 错误的验证码
>>>>>>> JYN_branch
=======
code="111111", # jyn:错误的验证码
>>>>>>> JYN_branch
) )
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password"), path=reverse("account:forget_password"),
data=data data=data
) )
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.status_code, 200)# 应返回错误页面而非重定向 self.assertEqual(resp.status_code, 200)# 应返回错误页面而非重定向
=======
self.assertEqual(resp.status_code, 200)#测试忘记密码流程:成功场景:验证密码修改后是否生效;失败场景:验证不存在用户、验证码错误时的接口响应
>>>>>>> LXY_branch
=======
self.assertEqual(resp.status_code, 200)#lxy测试忘记密码流程成功场景验证密码修改后是否生效失败场景验证不存在用户、验证码错误时的接口响应
>>>>>>> LXY_branch
=======
self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码
>>>>>>> JYN_branch
=======
self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码
>>>>>>> JYN_branch

@ -1,4 +1,3 @@
<<<<<<< HEAD
# 导入 Django 的 URL 路由工具 # 导入 Django 的 URL 路由工具
from django.urls import path# 用于简单路径匹配(如 'account/result.html' from django.urls import path# 用于简单路径匹配(如 'account/result.html'
from django.urls import re_path# 用于正则表达式路径匹配(如 '^login/$' from django.urls import re_path# 用于正则表达式路径匹配(如 '^login/$'
@ -36,87 +35,3 @@ urlpatterns = [
views.ForgetPasswordEmailCode.as_view(), # 自定义验证码处理视图 views.ForgetPasswordEmailCode.as_view(), # 自定义验证码处理视图
name='forget_password_code'), # URL 名称 name='forget_password_code'), # URL 名称
] ]
=======
from django.urls import path
from django.urls import re_path # jyn:用于正则表达式匹配URL
from . import views # jyn:导入当前应用的视图函数/类
from .forms import LoginForm # jyn:导入自定义的登录表单
# jyn:定义应用命名空间避免URL名称冲突
app_name = "accounts"
<<<<<<< HEAD
<<<<<<< HEAD
# URL路由配置列表映射URL路径到对应的视图
=======
# jyn:URL路由配置列表映射URL路径到对应的视图
>>>>>>> JYN_branch
urlpatterns = [
# jyn:登录页面路由使用正则匹配以login/结尾的路径
re_path(r'^login/$',
# jyn:调用LoginView类视图指定登录成功后重定向到首页/
views.LoginView.as_view(success_url='/'),
name='login', # jyn:URL的名称用于反向解析
# jyn:向视图传递额外参数指定登录表单为自定义的LoginForm
kwargs={'authentication_form': LoginForm}),
# jyn:注册页面路由匹配以register/结尾的路径
re_path(r'^register/$',
# jyn:调用RegisterView类视图注册成功后重定向到首页
views.RegisterView.as_view(success_url="/"),
name='register'), # jyn:URL名称用于反向解析
# jyn:登出功能路由匹配以logout/结尾的路径
re_path(r'^logout/$',
# jyn:调用LogoutView类视图Django内置或自定义
views.LogoutView.as_view(),
name='logout'), #jyn: URL名称
# jyn:账号操作结果页面路由精确匹配account/result.html路径
path(r'account/result.html',
# jyn:调用函数视图account_result
views.account_result,
name='result'), #jyn: URL名称用于展示注册/验证等结果
# jyn:忘记密码页面路由匹配以forget_password/结尾的路径
re_path(r'^forget_password/$',
# jyn:调用ForgetPasswordView类视图
views.ForgetPasswordView.as_view(),
name='forget_password'), #jyn: URL名称
# jyn:发送密码重置验证码页面路由匹配以forget_password_code/结尾的路径
re_path(r'^forget_password_code/$',
# jyn:调用ForgetPasswordEmailCode类视图处理发送验证码逻辑
views.ForgetPasswordEmailCode.as_view(),
<<<<<<< HEAD
name='forget_password_code'), # URL名称
]
>>>>>>> JYN_branch
=======
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),#登录路由对应LoginView用LoginForm表单成功重定向/
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),#注册路由对应RegisterView成功重定向/。
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),#登出路由对应LogoutView。
path(r'account/result.html',
views.account_result,
name='result'),#结果页路由对应account_result视图
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),#忘记密码路由对应ForgetPasswordView
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),#忘记密码验证码路由对应ForgetPasswordEmailCode
]
>>>>>>> LXY_branch
=======
name='forget_password_code'), # jyn:URL名称
]
>>>>>>> JYN_branch

@ -1,32 +1,16 @@
<<<<<<< HEAD
<<<<<<< HEAD
# 导入 Django 认证系统所需的模块 # 导入 Django 认证系统所需的模块
from django.contrib.auth import get_user_model# 动态获取当前项目的 User 模型 from django.contrib.auth import get_user_model# 动态获取当前项目的 User 模型
from django.contrib.auth.backends import ModelBackend# Django 默认的认证后端基类 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
=======
from django.contrib.auth import get_user_model # jyn:获取项目配置的用户模型(支持自定义模型)
from django.contrib.auth.backends import ModelBackend # jyn:导入Django内置的模型认证后端
>>>>>>> JYN_branch
class EmailOrUsernameModelBackend(ModelBackend):#lxy自定义Django认证后端支持用户名或邮箱两种方式登录。 class EmailOrUsernameModelBackend(ModelBackend):
""" """
<<<<<<< HEAD
允许使用用户名或邮箱登录 允许使用用户名或邮箱登录
承自 Django ModelBackend重写 authenticate get_user 方法 承自 Django ModelBackend重写 authenticate get_user 方法
=======
自定义认证后端继承自Django的ModelBackend
扩展功能允许用户使用用户名邮箱两种方式登录而非仅支持用户名
>>>>>>> JYN_branch
""" """
def authenticate(self, request, username=None, password=None, **kwargs): def authenticate(self, request, username=None, password=None, **kwargs):
""" """
<<<<<<< HEAD
重写认证方法支持用户名或邮箱登录 重写认证方法支持用户名或邮箱登录
参数: 参数:
@ -44,26 +28,8 @@ class EmailOrUsernameModelBackend(ModelBackend):#lxy自定义Django认证后端
kwargs = {'email': username} kwargs = {'email': username}
else: else:
# 否则按用户名查询 # 否则按用户名查询
=======
认证核心方法验证用户输入的凭证用户名/邮箱 + 密码是否有效
参数说明
- request当前请求对象
- username前端传入的用户名参数实际可能是用户名或邮箱
- password前端传入的密码明文
返回验证成功返回用户对象失败返回None
"""
# jyn:判断输入的「username」是否包含@符号,以此区分邮箱和用户名
if '@' in username:
# jyn:若包含@,则按邮箱字段查询用户
kwargs = {'email': username}
else:
<<<<<<< HEAD
# 若不包含@,则按用户名字段查询用户
>>>>>>> JYN_branch
kwargs = {'username': username} kwargs = {'username': username}
try: try:
<<<<<<< HEAD
# 尝试从数据库获取用户(使用当前项目的自定义 User 模型) # 尝试从数据库获取用户(使用当前项目的自定义 User 模型)
user = get_user_model().objects.get(**kwargs) user = get_user_model().objects.get(**kwargs)
# 验证密码是否正确(使用 Django 的密码哈希校验) # 验证密码是否正确(使用 Django 的密码哈希校验)
@ -71,31 +37,10 @@ class EmailOrUsernameModelBackend(ModelBackend):#lxy自定义Django认证后端
return user# 认证成功返回用户对象 return user# 认证成功返回用户对象
except get_user_model().DoesNotExist: except get_user_model().DoesNotExist:
# 用户不存在时返回 NoneDjango 会继续尝试其他认证后端) # 用户不存在时返回 NoneDjango 会继续尝试其他认证后端)
=======
# 根据上述条件从数据库查询唯一用户
=======
# jyn:若不包含@,则按用户名字段查询用户
kwargs = {'username': username}
try:
# jyn:根据上述条件从数据库查询唯一用户
>>>>>>> JYN_branch
user = get_user_model().objects.get(**kwargs)
# jyn:验证密码check_password会自动将明文密码与数据库中存储的哈希密码比对
if user.check_password(password):
return user # jyn:密码正确,返回用户对象(认证成功)
except get_user_model().DoesNotExist:
<<<<<<< HEAD
# 若查询不到用户(用户名/邮箱不存在返回None认证失败
>>>>>>> JYN_branch
=======
# jyn:若查询不到用户(用户名/邮箱不存在返回None认证失败
>>>>>>> JYN_branch
return None return None
#lxy核心认证逻辑判断输入是否为邮箱含@分别用邮箱或用户名查询用户验证密码后返回用户对象若用户不存在则返回None。
def get_user(self, username): def get_user(self, username):
""" """
<<<<<<< HEAD
根据用户 ID 获取用户对象用于 Session 认证等场景 根据用户 ID 获取用户对象用于 Session 认证等场景
参数: 参数:
@ -110,29 +55,3 @@ class EmailOrUsernameModelBackend(ModelBackend):#lxy自定义Django认证后端
except get_user_model().DoesNotExist: except get_user_model().DoesNotExist:
# 用户不存在时返回 None # 用户不存在时返回 None
return None return None
<<<<<<< HEAD
<<<<<<< HEAD
=======
根据用户ID获取用户对象Django认证系统必须实现的方法
作用认证成功后系统通过此方法获取用户完整信息
参数username实际为用户IDDjango默认用ID作为唯一标识
返回存在则返回用户对象不存在返回None
"""
try:
# jyn:通过主键ID查询用户
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
<<<<<<< HEAD
# 若用户不存在返回None
return None
>>>>>>> JYN_branch
=======
#根据用户ID主键查询用户不存在则返回None用于Django认证系统的用户查询环节
>>>>>>> LXY_branch
=======
#lxy根据用户ID主键查询用户不存在则返回None用于Django认证系统的用户查询环节
>>>>>>> LXY_branch
=======
# jyn:若用户不存在返回None
return None
>>>>>>> JYN_branch

@ -2,8 +2,6 @@
import typing # 用于类型注解 import typing # 用于类型注解
from datetime import timedelta # 用于处理时间间隔 from datetime import timedelta # 用于处理时间间隔
<<<<<<< HEAD
<<<<<<< HEAD
# 导入 Django 核心组件 # 导入 Django 核心组件
from django.core.cache import cache # Django 缓存系统 from django.core.cache import cache # Django 缓存系统
from django.utils.translation import gettext# 实时翻译 from django.utils.translation import gettext# 实时翻译
@ -12,95 +10,45 @@ from django.utils.translation import gettext_lazy as _# 惰性翻译(用于字
from djangoblog.utils import send_email# 假设是项目封装的邮件发送函数 from djangoblog.utils import send_email# 假设是项目封装的邮件发送函数
# 验证码有效期5分钟 # 验证码有效期5分钟
=======
from django.core.cache import cache # 导入Django缓存模块用于存储验证码
from django.utils.translation import gettext # 用于获取即时翻译文本
from django.utils.translation import gettext_lazy as _ # 用于延迟翻译文本(支持国际化)
<<<<<<< HEAD
<<<<<<< HEAD
from djangoblog.utils import send_email # 导入项目自定义的发送邮件工具函数
# 验证码有效期5分钟全局变量统一控制时效
>>>>>>> JYN_branch
=======
from django.core.cache import cache # jyn:导入Django缓存模块用于存储验证码
from django.utils.translation import gettext # jyn:用于获取即时翻译文本
from django.utils.translation import gettext_lazy as _ # jyn:用于延迟翻译文本(支持国际化)
from djangoblog.utils import send_email #jyn: 导入项目自定义的发送邮件工具函数
# jyn:验证码有效期5分钟全局变量统一控制时效
>>>>>>> JYN_branch
_code_ttl = timedelta(minutes=5) _code_ttl = timedelta(minutes=5)
=======
_code_ttl = timedelta(minutes=5)#验证码有效期设置为5分钟。
>>>>>>> LXY_branch
=======
_code_ttl = timedelta(minutes=5)#lxy验证码有效期设置为5分钟。
>>>>>>> LXY_branch
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
Args:
to_mail: 接受邮箱
subject: 邮件主题
code: 验证码
""" """
<<<<<<< HEAD
# 构造邮件正文(使用国际化字符串,并插入动态验证码) # 构造邮件正文(使用国际化字符串,并插入动态验证码)
html_content = _( html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it " "You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code} "properly") % {'code': code}
# 调用项目封装的邮件发送函数 # 调用项目封装的邮件发送函数
=======
发送邮箱验证码邮件主要用于密码重置场景
Args:
to_mail: 接收验证码的目标邮箱地址
code: 生成的随机验证码
subject: 邮件主题默认值为Verify Email支持国际化
"""
# jyn:构造邮件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}
<<<<<<< HEAD
# 调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容
>>>>>>> JYN_branch
=======
# jyn:调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容
>>>>>>> JYN_branch
send_email([to_mail], subject, html_content) send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]: def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
Args:
email: 请求邮箱
code: 验证码
Return:
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
""" """
<<<<<<< HEAD
# 从缓存获取该邮箱对应的正确验证码 # 从缓存获取该邮箱对应的正确验证码
cache_code = get_code(email) cache_code = get_code(email)
# 比对用户输入的验证码和缓存中的验证码 # 比对用户输入的验证码和缓存中的验证码
if cache_code != code: if cache_code != code:
return gettext("Verification code error") # 返回翻译后的错误信息 return gettext("Verification code error") # 返回翻译后的错误信息
# 隐含逻辑:验证成功时返回 None调用方需检查返回值是否为 None # 隐含逻辑:验证成功时返回 None调用方需检查返回值是否为 None
=======
验证用户输入的验证码是否有效
Args:
email: 用户提交的邮箱用于匹配缓存中的验证码
code: 用户输入的验证码
Return:
验证失败时返回错误提示字符串验证成功时返回None
Note:
代码注释中指出当前错误处理逻辑不合理应使用raise抛出异常而非返回错误字符串
若返回错误字符串调用方需额外判断返回值是否为错误增加了代码耦合度
"""
# jyn:从缓存中获取该邮箱对应的验证码
cache_code = get_code(email)
# jyn:对比用户输入的验证码与缓存中的验证码
if cache_code != code:
#jyn: 验证码不匹配时,返回国际化的错误提示
return gettext("Verification code error")
>>>>>>> JYN_branch
def set_code(email: str, code: str): def set_code(email: str, code: str):
""" """
<<<<<<< HEAD
将验证码存入缓存 将验证码存入缓存
Args: Args:
@ -112,26 +60,11 @@ def set_code(email: str, code: str):
""" """
# 使用 Django 缓存设置键值对,并指定过期时间(转换为秒) # 使用 Django 缓存设置键值对,并指定过期时间(转换为秒)
"""设置code""" """设置code"""
=======
将验证码存入缓存以邮箱为key设置有效期
Args:
email: 作为缓存key的邮箱地址确保一个邮箱对应一个验证码
code: 需要存入缓存的验证码
"""
<<<<<<< HEAD
# 调用Django缓存的set方法key=邮箱value=验证码timeout=有效期(秒)
>>>>>>> JYN_branch
=======
# jyn:调用Django缓存的set方法key=邮箱value=验证码timeout=有效期(秒)
>>>>>>> JYN_branch
cache.set(email, code, _code_ttl.seconds) cache.set(email, code, _code_ttl.seconds)
<<<<<<< HEAD
<<<<<<< HEAD
def get_code(email: str) -> typing.Optional[str]: def get_code(email: str) -> typing.Optional[str]:
""" """
<<<<<<< HEAD
从缓存获取验证码 从缓存获取验证码
Args: Args:
@ -143,26 +76,5 @@ def get_code(email: str) -> typing.Optional[str]:
- 不存在或过期返回 None - 不存在或过期返回 None
""" """
# 直接调用 Django 缓存的 get 方法 # 直接调用 Django 缓存的 get 方法
=======
def get_code(email: str) -> typing.Optional[str]:#从缓存中获取指定邮箱对应的验证码
>>>>>>> LXY_branch
=======
def get_code(email: str) -> typing.Optional[str]:#lxy从缓存中获取指定邮箱对应的验证码
>>>>>>> LXY_branch
"""获取code""" """获取code"""
return cache.get(email) return cache.get(email)
=======
从缓存中获取指定邮箱对应的验证码
Args:
email: 用于查询的邮箱地址缓存key
Return:
缓存中存在该邮箱对应的验证码时返回字符串不存在时返回None
"""
<<<<<<< HEAD
# 调用Django缓存的get方法根据邮箱key获取验证码
return cache.get(email)
>>>>>>> JYN_branch
=======
# jyn:调用Django缓存的get方法根据邮箱key获取验证码
return cache.get(email)
>>>>>>> JYN_branch

@ -6,8 +6,6 @@ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
# Django 认证系统核心模块 # Django 认证系统核心模块
from django.contrib import auth from django.contrib import auth
<<<<<<< HEAD
<<<<<<< HEAD
# 认证相关常量(如重定向字段名) # 认证相关常量(如重定向字段名)
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
# 获取当前用户模型的快捷方式 # 获取当前用户模型的快捷方式
@ -53,88 +51,21 @@ from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCo
from .models import BlogUser from .models import BlogUser
# 初始化日志记录器__name__ 表示当前模块名) # 初始化日志记录器__name__ 表示当前模块名)
logger = logging.getLogger(__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.contrib.auth import REDIRECT_FIELD_NAME # jyn:登录后重定向字段名常量
from django.contrib.auth import get_user_model # jyn:获取项目配置的用户模型
from django.contrib.auth import logout # jyn:登出功能函数
from django.contrib.auth.forms import AuthenticationForm # jyn:Django内置登录表单
from django.contrib.auth.hashers import make_password # jyn:密码哈希处理函数
from django.http import HttpResponseRedirect, HttpResponseForbidden # jyn:HTTP响应类
>>>>>>> JYN_branch
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404, render # jyn:快捷函数
from django.urls import reverse # jyn:URL反向解析
from django.utils.decorators import method_decorator # jyn:类视图装饰器工具
from django.utils.http import url_has_allowed_host_and_scheme # jyn:验证重定向URL安全性
from django.views import View # jyn:基础视图类
from django.views.decorators.cache import never_cache # jyn:禁止缓存装饰器
from django.views.decorators.csrf import csrf_protect #jyn: CSRF保护装饰器
from django.views.decorators.debug import sensitive_post_parameters # jyn:敏感参数保护装饰器
from django.views.generic import FormView, RedirectView # jyn:通用类视图
from djangoblog.utils import (send_email, get_sha256, get_current_site,
generate_code, delete_sidebar_cache) #jyn: 项目工具函数
from . import utils # jyn:当前应用工具函数(验证码相关)
from .forms import (RegisterForm, LoginForm, ForgetPasswordForm,
ForgetPasswordCodeForm) # jyn:当前应用表单类
from .models import BlogUser # jyn:自定义用户模型
<<<<<<< HEAD
logger = logging.getLogger(__name__) # 初始化日志记录器
>>>>>>> JYN_branch
# Create your views here. # Create your views here.
# 注册视图类(继承自 FormView处理表单提交 # 注册视图类(继承自 FormView处理表单提交
=======
logger = logging.getLogger(__name__) # jyn:初始化日志记录器
# jyn:Create your views here.
>>>>>>> JYN_branch
class RegisterView(FormView): class RegisterView(FormView):
<<<<<<< HEAD
# 指定使用的表单类 # 指定使用的表单类
form_class = RegisterForm form_class = RegisterForm
<<<<<<< HEAD
<<<<<<< HEAD
template_name = 'account/registration_form.html' template_name = 'account/registration_form.html'
# 使用装饰器确保视图禁用缓存never_cache并启用 CSRF 防护 # 使用装饰器确保视图禁用缓存never_cache并启用 CSRF 防护
=======
template_name = 'account/registration_form.html'#处理用户注册逻辑指定表单类RegisterForm和模板account/registration_form.html。
=======
template_name = 'account/registration_form.html'#lxy处理用户注册逻辑指定表单类RegisterForm和模板account/registration_form.html。
>>>>>>> LXY_branch
>>>>>>> LXY_branch
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
# 调用父类方法处理请求 # 调用父类方法处理请求
=======
"""
用户注册类视图继承自FormView处理表单提交的通用视图
负责用户注册表单展示数据验证发送验证邮件及注册结果跳转
"""
form_class = RegisterForm # jyn:指定使用的注册表单
template_name = 'account/registration_form.html' # jyn:注册页面模板路径
@method_decorator(csrf_protect) # jyn:为视图添加CSRF保护
def dispatch(self, *args, **kwargs):
"""重写分发方法,添加装饰器后调用父类逻辑"""
>>>>>>> JYN_branch
return super(RegisterView, self).dispatch(*args, **kwargs) return super(RegisterView, self).dispatch(*args, **kwargs)
# 表单验证通过后的处理逻辑 # 表单验证通过后的处理逻辑
def form_valid(self, form): def form_valid(self, form):
<<<<<<< HEAD
# 再次检查表单有效性(冗余,因为 FormView 已验证) # 再次检查表单有效性(冗余,因为 FormView 已验证)
if form.is_valid(): if form.is_valid():
# 保存用户对象(但暂不激活 is_active=False # 保存用户对象(但暂不激活 is_active=False
@ -156,34 +87,6 @@ class RegisterView(FormView):
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign) site=site, path=path, id=user.id, sign=sign)
# 构造邮件内容(包含验证链接) # 构造邮件内容(包含验证链接)
=======
"""表单验证通过后执行的逻辑(注册核心流程)"""
if form.is_valid():
# jyn:1. 暂存用户数据不立即保存is_active设为False需邮箱验证后激活
user = form.save(False)
user.is_active = False # jyn:初始状态:未激活(需邮箱验证)
user.source = 'Register' # jyn:标记注册来源为“前台注册”
user.save(True) # jyn:保存用户到数据库
# jyn:2. 生成邮箱验证链接(包含签名,防止篡改)
site = get_current_site().domain # jyn:获取当前站点域名
# jyn:双重SHA256加密用SECRET_KEY+用户ID生成签名确保链接安全性
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# jyn:开发环境下替换域名(适配本地测试)
if settings.DEBUG:
site = '127.0.0.1:8000'
# jyn:反向解析结果页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)
<<<<<<< HEAD
# 3. 构造验证邮件内容并发送
>>>>>>> JYN_branch
=======
# jyn:3. 构造验证邮件内容并发送
>>>>>>> JYN_branch
content = """ content = """
<p>请点击下面链接验证您的邮箱</p> <p>请点击下面链接验证您的邮箱</p>
@ -196,8 +99,6 @@ class RegisterView(FormView):
""".format(url=url) """.format(url=url)
# 发送验证邮件 # 发送验证邮件
send_email( send_email(
<<<<<<< HEAD
<<<<<<< HEAD
emailto=[ emailto=[
user.email, # 收件人列表 user.email, # 收件人列表
], ],
@ -211,97 +112,28 @@ class RegisterView(FormView):
# 表单无效时重新渲染表单(显示错误信息) # 表单无效时重新渲染表单(显示错误信息)
return self.render_to_response({ return self.render_to_response({
'form': form 'form': form
<<<<<<< HEAD
<<<<<<< HEAD
}) })
=======
emailto=[user.email], # 收件人邮箱(新注册用户的邮箱)
title='验证您的电子邮箱', # 邮件标题
content=content # 邮件HTML内容
=======
emailto=[user.email], # jyn:收件人邮箱(新注册用户的邮箱)
title='验证您的电子邮箱', #jyn: 邮件标题
content=content #jyn: 邮件HTML内容
>>>>>>> JYN_branch
)
=======
})#form_valid方法中保存用户并设置为非活跃状态生成邮箱验证链接并发送验证邮件最后重定向到结果页。
>>>>>>> LXY_branch
=======
})#lxyform_valid方法中保存用户并设置为非活跃状态生成邮箱验证链接并发送验证邮件最后重定向到结果页。
>>>>>>> LXY_branch
# jyn:4. 跳转到注册结果页(提示用户查收验证邮件)
url = reverse('accounts:result') + f'?type=register&id={str(user.id)}'
return HttpResponseRedirect(url)
else:
# jyn:表单验证失败,重新渲染表单并显示错误
return self.render_to_response({'form': form})
>>>>>>> JYN_branch
# 登出视图继承自RedirectView重定向到登录页面 # 登出视图继承自RedirectView重定向到登录页面
class LogoutView(RedirectView): class LogoutView(RedirectView):
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
# 登出后重定向的URL # 登出后重定向的URL
url = '/login/' url = '/login/'
# 使用never_cache装饰器确保视图不会被缓存 # 使用never_cache装饰器确保视图不会被缓存
=======
url = '/login/'#处理用户登出,登出后重定向到/login/
=======
url = '/login/'#lxy处理用户登出登出后重定向到/login/
>>>>>>> LXY_branch
>>>>>>> LXY_branch
@method_decorator(never_cache) @method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
# 调用父类的dispatch方法处理请求 # 调用父类的dispatch方法处理请求
=======
"""
用户登出类视图继承自RedirectView处理重定向的通用视图
负责清除用户会话缓存并重定向到登录页
"""
url = '/login/' # jyn:登出后默认重定向地址(登录页)
@method_decorator(never_cache) # jyn:禁止缓存登出页面,避免浏览器缓存导致的问题
def dispatch(self, request, *args, **kwargs):
"""重写分发方法,添加装饰器后调用父类逻辑"""
>>>>>>> JYN_branch
return super(LogoutView, self).dispatch(request, *args, **kwargs) return super(LogoutView, self).dispatch(request, *args, **kwargs)
# 处理GET请求 # 处理GET请求
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
<<<<<<< HEAD
# 执行登出操作 # 执行登出操作
logout(request) logout(request)
# 删除侧边栏缓存 # 删除侧边栏缓存
delete_sidebar_cache() delete_sidebar_cache()
<<<<<<< HEAD
<<<<<<< HEAD
# 调用父类的get方法完成重定向 # 调用父类的get方法完成重定向
return super(LogoutView, self).get(request, *args, **kwargs) return super(LogoutView, self).get(request, *args, **kwargs)
=======
"""处理GET请求登出核心逻辑"""
<<<<<<< HEAD
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
=======
return super(LogoutView, self).get(request, *args, **kwargs)#lxyget方法中调用logout登出用户删除侧边栏缓存后完成重定向
>>>>>>> LXY_branch
=======
logout(request) # jyn:清除用户会话,实现登出
delete_sidebar_cache() # jyn:删除侧边栏缓存(可能存储了用户相关信息)
return super(LogoutView, self).get(request, *args, **kwargs) # jyn:执行重定向
>>>>>>> JYN_branch
# 登录视图继承自FormView # 登录视图继承自FormView
class LoginView(FormView): class LoginView(FormView):
<<<<<<< HEAD
# 使用的表单类 # 使用的表单类
form_class = LoginForm form_class = LoginForm
# 模板文件路径 # 模板文件路径
@ -310,7 +142,6 @@ class LoginView(FormView):
success_url = '/' success_url = '/'
# 重定向字段名 # 重定向字段名
redirect_field_name = REDIRECT_FIELD_NAME redirect_field_name = REDIRECT_FIELD_NAME
<<<<<<< HEAD
# 登录会话有效期(一个月的时间,单位:秒) # 登录会话有效期(一个月的时间,单位:秒)
login_ttl = 2626560 # 一个月的时间 login_ttl = 2626560 # 一个月的时间
# 使用多个装饰器装饰dispatch方法 # 使用多个装饰器装饰dispatch方法
@ -319,92 +150,26 @@ class LoginView(FormView):
@method_decorator(never_cache)# 禁止缓存 @method_decorator(never_cache)# 禁止缓存
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
# 调用父类的dispatch方法处理请求 # 调用父类的dispatch方法处理请求
=======
"""
用户登录类视图继承自FormView
负责登录表单展示数据验证用户认证记住登录状态及重定向
"""
<<<<<<< HEAD
form_class = LoginForm # 指定使用的自定义登录表单
template_name = 'account/login.html' # 登录页面模板路径
success_url = '/' # 登录成功默认重定向地址(首页)
redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名默认next
login_ttl = 2626560 # “记住登录”状态的有效期约等于1个月
=======
login_ttl = 2626560 #lxy 一个月的时间
>>>>>>> LXY_branch
=======
form_class = LoginForm #jyn: 指定使用的自定义登录表单
template_name = 'account/login.html' # jyn:登录页面模板路径
success_url = '/' # jyn:登录成功默认重定向地址(首页)
redirect_field_name = REDIRECT_FIELD_NAME # jyn:重定向字段名默认next
login_ttl = 2626560 #jyn: “记住登录”状态的有效期约等于1个月
>>>>>>> JYN_branch
# jyn:为视图添加多重装饰器敏感参数保护、CSRF保护、禁止缓存
@method_decorator(sensitive_post_parameters('password')) # jyn:保护密码参数,避免日志泄露
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
>>>>>>> JYN_branch
return super(LoginView, self).dispatch(request, *args, **kwargs) return super(LoginView, self).dispatch(request, *args, **kwargs)
# 获取模板上下文数据 # 获取模板上下文数据
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
<<<<<<< HEAD
# 从GET参数中获取重定向URL # 从GET参数中获取重定向URL
=======
"""添加额外上下文数据(重定向地址)到模板"""
<<<<<<< HEAD
# 获取URL中的重定向参数如登录前访问的受保护页面
>>>>>>> JYN_branch
=======
# jyn:获取URL中的重定向参数如登录前访问的受保护页面
>>>>>>> JYN_branch
redirect_to = self.request.GET.get(self.redirect_field_name) redirect_to = self.request.GET.get(self.redirect_field_name)
# 如果不存在则设置为根路径 # 如果不存在则设置为根路径
if redirect_to is None: if redirect_to is None:
<<<<<<< HEAD
<<<<<<< HEAD
redirect_to = '/' redirect_to = '/'
# 将重定向URL添加到上下文 # 将重定向URL添加到上下文
kwargs['redirect_to'] = redirect_to kwargs['redirect_to'] = redirect_to
# 调用父类方法获取其他上下文数据 # 调用父类方法获取其他上下文数据
<<<<<<< HEAD
<<<<<<< HEAD
return super(LoginView, self).get_context_data(**kwargs) return super(LoginView, self).get_context_data(**kwargs)
# 表单验证通过后的处理 # 表单验证通过后的处理
=======
return super(LoginView, self).get_context_data(**kwargs)#lxy处理用户登录逻辑指定表单类LoginForm、模板account / login.html和成功后重定向地址 /
>>>>>>> LXY_branch
def form_valid(self, form): def form_valid(self, form):
# 重新创建认证表单这里可能有逻辑问题因为form已经传入 # 重新创建认证表单这里可能有逻辑问题因为form已经传入
=======
redirect_to = '/' # 默认重定向到首页
kwargs['redirect_to'] = redirect_to # 将重定向地址传入模板上下文
=======
redirect_to = '/' # jyn:默认重定向到首页
kwargs['redirect_to'] = redirect_to # jyn:将重定向地址传入模板上下文
>>>>>>> JYN_branch
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):
"""表单验证通过后执行的逻辑(登录核心流程)"""
<<<<<<< HEAD
# 用Django内置AuthenticationForm重新验证确保认证逻辑符合默认规范
>>>>>>> JYN_branch
=======
# jyn:用Django内置AuthenticationForm重新验证确保认证逻辑符合默认规范
>>>>>>> JYN_branch
form = AuthenticationForm(data=self.request.POST, request=self.request) form = AuthenticationForm(data=self.request.POST, request=self.request)
# 再次验证表单 # 再次验证表单
if form.is_valid(): if form.is_valid():
<<<<<<< HEAD
<<<<<<< HEAD
# 删除侧边栏缓存 # 删除侧边栏缓存
delete_sidebar_cache() delete_sidebar_cache()
# 记录日志 # 记录日志
@ -413,56 +178,18 @@ class LoginView(FormView):
# 登录用户 # 登录用户
auth.login(self.request, form.get_user()) auth.login(self.request, form.get_user())
# 如果用户选择了"记住我" # 如果用户选择了"记住我"
=======
delete_sidebar_cache() # 删除侧边栏缓存(更新用户登录状态)
logger.info(self.redirect_field_name) # 日志记录重定向字段名
=======
delete_sidebar_cache() # jyn:删除侧边栏缓存(更新用户登录状态)
logger.info(self.redirect_field_name) # jyn:日志记录重定向字段名
>>>>>>> JYN_branch
# jyn:执行登录:将用户信息存入会话
auth.login(self.request, form.get_user())
<<<<<<< HEAD
# 处理“记住我”功能若勾选设置会话有效期为1个月
>>>>>>> JYN_branch
=======
# jyn:处理“记住我”功能若勾选设置会话有效期为1个月
>>>>>>> JYN_branch
if self.request.POST.get("remember"): if self.request.POST.get("remember"):
# 设置较长的会话过期时间 # 设置较长的会话过期时间
self.request.session.set_expiry(self.login_ttl) self.request.session.set_expiry(self.login_ttl)
<<<<<<< HEAD
<<<<<<< HEAD
# 调用父类方法处理成功跳转 # 调用父类方法处理成功跳转
=======
# 调用父类form_valid执行重定向
>>>>>>> JYN_branch
=======
# jyn:调用父类form_valid执行重定向
>>>>>>> JYN_branch
return super(LoginView, self).form_valid(form) return super(LoginView, self).form_valid(form)
<<<<<<< HEAD # return HttpResponseRedirect('/')
=======
#lxyreturn HttpResponseRedirect('/')
>>>>>>> LXY_branch
else: else:
<<<<<<< HEAD
<<<<<<< HEAD
# 表单无效,重新渲染表单并显示错误 # 表单无效,重新渲染表单并显示错误
return self.render_to_response({ return self.render_to_response({
'form': form 'form': form
<<<<<<< HEAD
<<<<<<< HEAD
}) })
# 获取成功后的跳转URL # 获取成功后的跳转URL
=======
})#form_valid方法中验证表单登录用户并根据“记住我”选项设置会话过期时间
=======
})#lxyform_valid方法中验证表单登录用户并根据“记住我”选项设置会话过期时间
>>>>>>> LXY_branch
>>>>>>> LXY_branch
def get_success_url(self): def get_success_url(self):
# 从POST参数中获取重定向URL # 从POST参数中获取重定向URL
@ -473,39 +200,10 @@ class LoginView(FormView):
self.request.get_host()]): self.request.get_host()]):
# 如果不安全则使用默认成功URL # 如果不安全则使用默认成功URL
redirect_to = self.success_url redirect_to = self.success_url
<<<<<<< HEAD
<<<<<<< HEAD
=======
# 表单验证失败(如密码错误),重新渲染表单并显示错误
=======
# jyn:表单验证失败(如密码错误),重新渲染表单并显示错误
>>>>>>> JYN_branch
return self.render_to_response({'form': form})
def get_success_url(self):
"""自定义登录成功后的重定向地址优先使用URL中的next参数"""
# jyn:获取POST请求中的重定向地址用户登录前尝试访问的页面
redirect_to = self.request.POST.get(self.redirect_field_name)
# jyn:验证重定向地址的安全性:避免跳转到外部恶意网站
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[self.request.get_host()]):
<<<<<<< HEAD
redirect_to = self.success_url # 不安全则使用默认重定向地址
>>>>>>> JYN_branch
=======
redirect_to = self.success_url # jyn:不安全则使用默认重定向地址
>>>>>>> JYN_branch
return redirect_to return redirect_to
=======
return redirect_to#get_success_url方法处理登录后的重定向地址确保其安全性
>>>>>>> LXY_branch
=======
return redirect_to#lxyget_success_url方法处理登录后的重定向地址确保其安全性
>>>>>>> LXY_branch
# 账户操作结果页面(如注册成功、邮箱验证等) # 账户操作结果页面(如注册成功、邮箱验证等)
def account_result(request): def account_result(request):
<<<<<<< HEAD
# 从GET参数中获取类型和用户ID # 从GET参数中获取类型和用户ID
type = request.GET.get('type') type = request.GET.get('type')
id = request.GET.get('id') id = request.GET.get('id')
@ -516,207 +214,63 @@ def account_result(request):
if user.is_active: if user.is_active:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
# 检查类型参数是否有效 # 检查类型参数是否有效
=======
"""
账号操作结果视图函数函数视图
处理注册成功提示邮箱验证逻辑并展示结果页面
"""
# jyn:从URL参数中获取操作类型register/validation和用户ID
type = request.GET.get('type')
id = request.GET.get('id')
# jyn:获取对应的用户若不存在则返回404
user = get_object_or_404(get_user_model(), id=id)
<<<<<<< HEAD
<<<<<<< HEAD
logger.info(type) # 日志记录操作类型
<<<<<<< HEAD
# 若用户已激活,直接重定向到首页(避免重复验证)
=======
logger.info(type)#处理注册和邮箱验证的结果逻辑根据type参数区分场景
>>>>>>> LXY_branch
=======
logger.info(type)#lxy处理注册和邮箱验证的结果逻辑根据type参数区分场景
>>>>>>> LXY_branch
if user.is_active:
return HttpResponseRedirect('/')
# 处理合法的操作类型(注册成功提示/邮箱验证)
>>>>>>> JYN_branch
=======
# jyn:若用户已激活,直接重定向到首页(避免重复验证)
if user.is_active:
return HttpResponseRedirect('/')
# jyn:处理合法的操作类型(注册成功提示/邮箱验证)
>>>>>>> JYN_branch
if type and type in ['register', 'validation']: if type and type in ['register', 'validation']:
if type == 'register': if type == 'register':
# jyn:注册成功:提示用户查收验证邮件
content = ''' content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站 恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
''' '''
title = '注册成功' title = '注册成功'
else: else:
<<<<<<< HEAD
<<<<<<< HEAD
# 生成验证签名 # 生成验证签名
=======
# 邮箱验证:验证签名是否正确,正确则激活用户
# 重新计算签名与URL中的签名对比防止链接篡改
>>>>>>> JYN_branch
=======
# jyn:邮箱验证:验证签名是否正确,正确则激活用户
# jyn:重新计算签名与URL中的签名对比防止链接篡改
>>>>>>> JYN_branch
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 获取请求中的签名 # 获取请求中的签名
sign = request.GET.get('sign') sign = request.GET.get('sign')
# 验证签名是否匹配 # 验证签名是否匹配
if sign != c_sign: if sign != c_sign:
<<<<<<< HEAD
<<<<<<< HEAD
return HttpResponseForbidden() return HttpResponseForbidden()
# 激活用户账户 # 激活用户账户
user.is_active = True user.is_active = True
user.save() user.save()
# 验证成功提示内容 # 验证成功提示内容
=======
return HttpResponseForbidden() # 签名不匹配返回403禁止访问
# 激活用户将is_active设为True
user.is_active = True
user.save()
# 验证成功:提示用户可登录
>>>>>>> JYN_branch
=======
return HttpResponseForbidden() # jyn:签名不匹配返回403禁止访问
# jyn:激活用户将is_active设为True
user.is_active = True
user.save()
# jyn:验证成功:提示用户可登录
>>>>>>> JYN_branch
content = ''' content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站 恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
''' '''
title = '验证成功' title = '验证成功'
<<<<<<< HEAD
<<<<<<< HEAD
# 渲染结果页面 # 渲染结果页面
=======
# 渲染结果页面,传递标题和内容
>>>>>>> JYN_branch
=======
# jyn:渲染结果页面,传递标题和内容
>>>>>>> JYN_branch
return render(request, 'account/result.html', { return render(request, 'account/result.html', {
'title': title, 'title': title,
'content': content 'content': content
}) })
else: else:
<<<<<<< HEAD
<<<<<<< HEAD
# 无效类型重定向到首页 # 无效类型重定向到首页
=======
# 操作类型不合法,重定向到首页
>>>>>>> JYN_branch
=======
# jyn:操作类型不合法,重定向到首页
>>>>>>> JYN_branch
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
# 忘记密码视图继承自FormView # 忘记密码视图继承自FormView
class ForgetPasswordView(FormView): class ForgetPasswordView(FormView):
<<<<<<< HEAD
# 使用的表单类 # 使用的表单类
form_class = ForgetPasswordForm form_class = ForgetPasswordForm
<<<<<<< HEAD
<<<<<<< HEAD
# 模板文件路径 # 模板文件路径
template_name = 'account/forget_password.html' template_name = 'account/forget_password.html'
=======
"""
忘记密码类视图继承自FormView
负责密码重置表单展示数据验证及更新用户密码
"""
<<<<<<< HEAD
form_class = ForgetPasswordForm # 指定使用的密码重置表单
template_name = 'account/forget_password.html' # 密码重置页面模板路径
>>>>>>> JYN_branch
=======
template_name = 'account/forget_password.html'#处理忘记密码逻辑指定表单类ForgetPasswordForm和模板account/forget_password.html
>>>>>>> LXY_branch
=======
template_name = 'account/forget_password.html'#lxy处理忘记密码逻辑指定表单类ForgetPasswordForm和模板account/forget_password.html
>>>>>>> LXY_branch
=======
form_class = ForgetPasswordForm # jyn:指定使用的密码重置表单
template_name = 'account/forget_password.html' # jyn:密码重置页面模板路径
>>>>>>> JYN_branch
# 表单验证通过后的处理 # 表单验证通过后的处理
def form_valid(self, form): def form_valid(self, form):
"""表单验证通过后执行的逻辑(密码重置核心流程)"""
if form.is_valid(): if form.is_valid():
<<<<<<< HEAD
<<<<<<< HEAD
# 根据邮箱获取用户对象 # 根据邮箱获取用户对象
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() 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
=======
# jyn:1. 获取表单中的邮箱,查询对应的用户
blog_user = BlogUser.objects.filter(
email=form.cleaned_data.get("email")
).get()
# jyn:2. 对新密码进行哈希处理,并更新用户密码
>>>>>>> JYN_branch
blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.password = make_password(form.cleaned_data["new_password2"])
# 保存用户对象 # 保存用户对象
blog_user.save() blog_user.save()
<<<<<<< HEAD
<<<<<<< HEAD
# 重定向到登录页面 # 重定向到登录页面
return HttpResponseRedirect('/login/') return HttpResponseRedirect('/login/')
else: else:
<<<<<<< HEAD
<<<<<<< HEAD
# 表单无效,重新渲染表单并显示错误 # 表单无效,重新渲染表单并显示错误
=======
# 3. 密码重置成功,重定向到登录页
return HttpResponseRedirect('/login/')
else:
# 表单验证失败(如验证码错误、密码不一致),重新渲染表单
>>>>>>> JYN_branch
=======
# jyn:3. 密码重置成功,重定向到登录页
return HttpResponseRedirect('/login/')
else:
# jyn:表单验证失败(如验证码错误、密码不一致),重新渲染表单
>>>>>>> JYN_branch
return self.render_to_response({'form': form}) return self.render_to_response({'form': form})
# 忘记密码验证码发送视图继承自View # 忘记密码验证码发送视图继承自View
class ForgetPasswordEmailCode(View): class ForgetPasswordEmailCode(View):
<<<<<<< HEAD
# 处理POST请求 # 处理POST请求
=======
return self.render_to_response({'form': form})#form_valid方法中验证表单后重置用户密码并重定向到登录页
=======
return self.render_to_response({'form': form})#lxyform_valid方法中验证表单后重置用户密码并重定向到登录页
>>>>>>> LXY_branch
class ForgetPasswordEmailCode(View):#lxy处理忘记密码的邮箱验证码发送逻辑
>>>>>>> LXY_branch
def post(self, request: HttpRequest): def post(self, request: HttpRequest):
# 验证表单 # 验证表单
form = ForgetPasswordCodeForm(request.POST) form = ForgetPasswordCodeForm(request.POST)
@ -731,40 +285,5 @@ class ForgetPasswordEmailCode(View):#lxy处理忘记密码的邮箱验证码发
utils.send_verify_email(to_email, code) utils.send_verify_email(to_email, code)
# 存储验证码(通常有有效期) # 存储验证码(通常有有效期)
utils.set_code(to_email, code) utils.set_code(to_email, code)
<<<<<<< HEAD
# 返回成功响应 # 返回成功响应
return HttpResponse("ok") return HttpResponse("ok")
=======
"""
发送密码重置验证码类视图继承自基础View
负责验证邮箱合法性生成验证码发送验证邮件并返回结果
"""
def post(self, request: HttpRequest):
"""处理POST请求发送验证码核心逻辑"""
#jyn: 1. 验证邮箱表单数据
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱") # jyn:邮箱格式不合法,返回错误提示
# jyn:2. 生成验证码并发送邮件
to_email = form.cleaned_data["email"] # jyn:获取合法的邮箱地址
code = generate_code() # jyn:生成随机验证码
utils.send_verify_email(to_email, code) # jyn:发送验证码邮件
utils.set_code(to_email, code) # jyn:将验证码存入缓存(设置有效期)
<<<<<<< HEAD
# 3. 操作成功返回“ok”提示
return HttpResponse("ok")
>>>>>>> JYN_branch
=======
<<<<<<< HEAD
return HttpResponse("ok")# post方法中验证邮箱表单生成并发送验证码将验证码存入缓存后返回成功标识
>>>>>>> LXY_branch
=======
return HttpResponse("ok")#lxypost方法中验证邮箱表单生成并发送验证码将验证码存入缓存后返回成功标识
>>>>>>> LXY_branch
=======
# jyn:3. 操作成功返回“ok”提示
return HttpResponse("ok")
>>>>>>> JYN_branch

@ -1,109 +1,49 @@
from django import forms from django import forms
#ymq导入Django的forms模块用于创建表单
from django.contrib import admin from django.contrib import admin
#ymq导入Django的admin模块用于后台管理配置
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
#ymq导入获取用户模型的函数便于灵活引用用户模型
from django.urls import reverse from django.urls import reverse
#ymq导入reverse函数用于生成URL反向解析
from django.utils.html import format_html from django.utils.html import format_html
#ymq导入format_html函数用于安全生成HTML内容
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
#ymq导入国际化翻译函数将文本标记为可翻译
# Register your models here. # Register your models here.
from .models import Article from .models import Article
#ymq从当前应用的models模块导入Article模型
<<<<<<< HEAD
class ArticleForm(forms.ModelForm): class ArticleForm(forms.ModelForm):
#ymq定义Article模型对应的表单类继承自ModelForm
# body = forms.CharField(widget=AdminPagedownWidget()) # body = forms.CharField(widget=AdminPagedownWidget())
#ymq注释掉的代码原本计划为body字段使用AdminPagedownWidget编辑器
class Meta: class Meta:
#ymqMeta类用于配置表单元数据
model = Article model = Article
#ymq指定表单关联的模型为Article
fields = '__all__' fields = '__all__'
#ymq指定表单包含模型的所有字段
def makr_article_publish(modeladmin, request, queryset): def makr_article_publish(modeladmin, request, queryset):
#ymq定义批量发布文章的动作函数
=======
class ArticleForm(forms.ModelForm):#lxy 文章表单类
# body = forms.CharField(widget=AdminPagedownWidget())#lxy 富文本组件
class Meta:
model = Article#lxy 关联Article模型
fields = '__all__'#lxy 包含所有字段
def makr_article_publish(modeladmin, request, queryset):#lxy 批量设为已发布
>>>>>>> LXY_branch
queryset.update(status='p') queryset.update(status='p')
#ymq将选中的文章状态更新为'p'(发布状态)
<<<<<<< HEAD
def draft_article(modeladmin, request, queryset): def draft_article(modeladmin, request, queryset):
#ymq定义批量设为草稿的动作函数
=======
def draft_article(modeladmin, request, queryset):#lxy 批量设为草稿
>>>>>>> LXY_branch
queryset.update(status='d') queryset.update(status='d')
#ymq将选中的文章状态更新为'd'(草稿状态)
<<<<<<< HEAD
def close_article_commentstatus(modeladmin, request, queryset): def close_article_commentstatus(modeladmin, request, queryset):
#ymq定义批量关闭评论的动作函数
=======
def close_article_commentstatus(modeladmin, request, queryset):#lxy 关闭评论
>>>>>>> LXY_branch
queryset.update(comment_status='c') queryset.update(comment_status='c')
#ymq将选中的文章评论状态更新为'c'(关闭状态)
<<<<<<< HEAD
def open_article_commentstatus(modeladmin, request, queryset): def open_article_commentstatus(modeladmin, request, queryset):
#ymq定义批量开启评论的动作函数
=======
def open_article_commentstatus(modeladmin, request, queryset):#lxy 开启评论
>>>>>>> LXY_branch
queryset.update(comment_status='o') queryset.update(comment_status='o')
#ymq将选中的文章评论状态更新为'o'(开启状态)
#lxy 操作描述
makr_article_publish.short_description = _('Publish selected articles') makr_article_publish.short_description = _('Publish selected articles')
#ymq设置发布动作在admin中的显示名称支持国际化
draft_article.short_description = _('Draft selected articles') draft_article.short_description = _('Draft selected articles')
#ymq设置草稿动作在admin中的显示名称支持国际化
close_article_commentstatus.short_description = _('Close article comments') close_article_commentstatus.short_description = _('Close article comments')
#ymq设置关闭评论动作在admin中的显示名称支持国际化
open_article_commentstatus.short_description = _('Open article comments') open_article_commentstatus.short_description = _('Open article comments')
#ymq设置开启评论动作在admin中的显示名称支持国际化
<<<<<<< HEAD
class ArticlelAdmin(admin.ModelAdmin): class ArticlelAdmin(admin.ModelAdmin):
#ymq定义Article模型的admin管理类继承自ModelAdmin
list_per_page = 20 list_per_page = 20
#ymq设置每页显示20条记录
search_fields = ('body', 'title') search_fields = ('body', 'title')
#ymq设置可搜索的字段为body和title
form = ArticleForm form = ArticleForm
#ymq指定使用自定义的ArticleForm表单
list_display = ( list_display = (
=======
class ArticlelAdmin(admin.ModelAdmin):#lxy 文章Admin配置
list_per_page = 20#lxy 每页显示20条
search_fields = ('body', 'title')#lxy 搜索字段
form = ArticleForm#lxy 关联表单
list_display = (#lxy 列表显示字段
>>>>>>> LXY_branch
'id', 'id',
'title', 'title',
'author', 'author',
@ -113,143 +53,60 @@ class ArticlelAdmin(admin.ModelAdmin):#lxy 文章Admin配置
'status', 'status',
'type', 'type',
'article_order') 'article_order')
<<<<<<< HEAD
#ymq设置列表页显示的字段
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
#ymq设置列表页中可点击跳转编辑页的字段
list_filter = ('status', 'type', 'category') list_filter = ('status', 'type', 'category')
#ymq设置可用于筛选的字段
filter_horizontal = ('tags',) filter_horizontal = ('tags',)
#ymq设置多对多字段的水平筛选器tags字段
exclude = ('creation_time', 'last_modify_time') exclude = ('creation_time', 'last_modify_time')
#ymq设置编辑页中排除的字段不显示
view_on_site = True view_on_site = True
#ymq启用"在站点上查看"功能
actions = [ actions = [
=======
list_display_links = ('id', 'title') #lxy 排序字段
list_filter = ('status', 'type', 'category') #lxy 可点击字段
filter_horizontal = ('tags',)#lxy 标签选择器
exclude = ('creation_time', 'last_modify_time')#lxy 隐藏字段
view_on_site = True#lxy 允许查看站点
actions = [#lxy 自定义操作
>>>>>>> LXY_branch
makr_article_publish, makr_article_publish,
draft_article, draft_article,
close_article_commentstatus, close_article_commentstatus,
open_article_commentstatus] open_article_commentstatus]
#ymq注册批量操作动作
<<<<<<< HEAD
def link_to_category(self, obj): def link_to_category(self, obj):
#ymq自定义列表页中分类字段的显示方式转为链接
=======
def link_to_category(self, obj):#lxy 分类链接
>>>>>>> LXY_branch
info = (obj.category._meta.app_label, obj.category._meta.model_name) info = (obj.category._meta.app_label, obj.category._meta.model_name)
#ymq获取分类模型的应用标签和模型名称
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
#ymq生成分类的编辑页URL
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name)) return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
#ymq返回HTML链接点击可跳转到分类编辑页
<<<<<<< HEAD
link_to_category.short_description = _('category') link_to_category.short_description = _('category')
#ymq设置自定义字段在列表页的显示名称支持国际化
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
#ymq重写获取表单的方法自定义表单字段
form = super(ArticlelAdmin, self).get_form(request, obj,** kwargs)
#ymq调用父类方法获取表单
=======
link_to_category.short_description = _('category') #lxy 字段名称
def get_form(self, request, obj=None, **kwargs):#lxy 重写表单
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
>>>>>>> LXY_branch
form.base_fields['author'].queryset = get_user_model( form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True) ).objects.filter(is_superuser=True)
#ymq限制作者字段只能选择超级用户
return form return form
#ymq返回修改后的表单
<<<<<<< HEAD
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
#ymq重写保存模型的方法可在此添加自定义保存逻辑
=======
def save_model(self, request, obj, form, change):#lxy 重写保存
>>>>>>> LXY_branch
super(ArticlelAdmin, self).save_model(request, obj, form, change) super(ArticlelAdmin, self).save_model(request, obj, form, change)
#ymq调用父类的保存方法完成默认保存
<<<<<<< HEAD
def get_view_on_site_url(self, obj=None): def get_view_on_site_url(self, obj=None):
#ymq重写"在站点上查看"的URL生成方法
=======
def get_view_on_site_url(self, obj=None):#lxy 查看站点URL
>>>>>>> LXY_branch
if obj: if obj:
#ymq如果有具体对象返回对象的完整URL
url = obj.get_full_url() url = obj.get_full_url()
return url return url
else: else:
#ymq如果无对象返回当前站点域名
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
site = get_current_site().domain site = get_current_site().domain
return site return site
<<<<<<< HEAD
class TagAdmin(admin.ModelAdmin): class TagAdmin(admin.ModelAdmin):
#ymq定义Tag模型的admin管理类
exclude = ('slug', 'last_mod_time', 'creation_time') exclude = ('slug', 'last_mod_time', 'creation_time')
#ymq编辑页排除slug、最后修改时间和创建时间字段
class CategoryAdmin(admin.ModelAdmin): class CategoryAdmin(admin.ModelAdmin):
#ymq定义Category模型的admin管理类
list_display = ('name', 'parent_category', 'index') list_display = ('name', 'parent_category', 'index')
#ymq列表页显示名称、父分类和排序索引字段
exclude = ('slug', 'last_mod_time', 'creation_time') exclude = ('slug', 'last_mod_time', 'creation_time')
#ymq编辑页排除slug、最后修改时间和创建时间字段
class LinksAdmin(admin.ModelAdmin): class LinksAdmin(admin.ModelAdmin):
#ymq定义Links模型的admin管理类
exclude = ('last_mod_time', 'creation_time') exclude = ('last_mod_time', 'creation_time')
#ymq编辑页排除最后修改时间和创建时间字段
class SideBarAdmin(admin.ModelAdmin): class SideBarAdmin(admin.ModelAdmin):
#ymq定义SideBar模型的admin管理类
list_display = ('name', 'content', 'is_enable', 'sequence') list_display = ('name', 'content', 'is_enable', 'sequence')
#ymq列表页显示名称、内容、是否启用和排序序号字段
exclude = ('last_mod_time', 'creation_time') exclude = ('last_mod_time', 'creation_time')
#ymq编辑页排除最后修改时间和创建时间字段
class BlogSettingsAdmin(admin.ModelAdmin): class BlogSettingsAdmin(admin.ModelAdmin):
#ymq定义BlogSettings模型的admin管理类
=======
class TagAdmin(admin.ModelAdmin):#lxy 标签Admin配置
exclude = ('slug', 'last_mod_time', 'creation_time')#lxy 隐藏字段
class CategoryAdmin(admin.ModelAdmin):#lxy 分类Admin配置
list_display = ('name', 'parent_category', 'index') #lxy 列表显示
exclude = ('slug', 'last_mod_time', 'creation_time')#lxy 隐藏字段
class LinksAdmin(admin.ModelAdmin): #lxy 链接Admin配置
exclude = ('last_mod_time', 'creation_time') #lxy 隐藏字段
class SideBarAdmin(admin.ModelAdmin): #lxy 侧边栏Admin配置
list_display = ('name', 'content', 'is_enable', 'sequence')#lxy 列表显示
exclude = ('last_mod_time', 'creation_time') #lxy 隐藏字段
class BlogSettingsAdmin(admin.ModelAdmin):#lxy 博客设置Admin配置
>>>>>>> LXY_branch
pass pass
#ymq暂未设置特殊配置使用默认admin行为

@ -1,15 +1,5 @@
<<<<<<< HEAD
from django.apps import AppConfig from django.apps import AppConfig
#ymq导入Django的AppConfig类用于定义应用的配置信息
class BlogConfig(AppConfig): class BlogConfig(AppConfig):
#ymq定义博客应用的配置类继承自AppConfig
name = 'blog' name = 'blog'
#ymq指定应用的名称为'blog'Django通过该名称识别此应用
=======
from django.apps import AppConfig#lxy 导入Django应用配置类
class BlogConfig(AppConfig):#lxy 博客应用的配置类
name = 'blog'#lxy 应用名称对应项目中的blog模块
>>>>>>> LXY_branch

@ -1,101 +1,43 @@
import logging import logging
#ymq导入logging模块用于日志记录
from django.utils import timezone from django.utils import timezone
#ymq导入Django的timezone模块用于处理时间相关操作
from djangoblog.utils import cache, get_blog_setting from djangoblog.utils import cache, get_blog_setting
#ymq从项目工具模块导入缓存工具和获取博客设置的函数
from .models import Category, Article from .models import Category, Article
#ymq从当前应用的models模块导入分类和文章模型
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
<<<<<<< HEAD
def seo_processor(requests): def seo_processor(requests):
#ymq定义SEO上下文处理器用于向模板全局注入通用数据
key = 'seo_processor' key = 'seo_processor'
#ymq缓存键名用于标识当前处理器的缓存数据
value = cache.get(key) value = cache.get(key)
#ymq尝试从缓存中获取数据
if value: if value:
#ymq如果缓存存在直接返回缓存数据
return value return value
else: else:
#ymq如果缓存不存在重新生成数据
logger.info('set processor cache.') logger.info('set processor cache.')
#ymq记录日志提示正在设置缓存
setting = get_blog_setting() setting = get_blog_setting()
#ymq获取博客的全局设置信息
#ymq构建需要传递给模板的上下文数据字典
value = { value = {
'SITE_NAME': setting.site_name, # 网站名称 'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示谷歌广告 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # 谷歌广告代码 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 网站SEO描述 'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description, # 网站描述 'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords, # 网站关键词 'SITE_KEYWORDS': setting.site_keywords,
# 网站基础URL协议+域名)
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度 'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(), # 导航分类列表 'nav_category_list': Category.objects.all(),
# 导航页面列表(类型为页面且状态为已发布)
'nav_pages': Article.objects.filter( 'nav_pages': Article.objects.filter(
type='p', type='p',
status='p'), status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启网站评论 'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code, # 网站备案号 'BEIAN_CODE': setting.beian_code,
'ANALYTICS_CODE': setting.analytics_code, # 统计分析代码 'ANALYTICS_CODE': setting.analytics_code,
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, # 公安备案号 "BEIAN_CODE_GONGAN": setting.gongan_beiancode,
"SHOW_GONGAN_CODE": setting.show_gongan_code, # 是否显示公安备案号 "SHOW_GONGAN_CODE": setting.show_gongan_code,
"CURRENT_YEAR": timezone.now().year, # 当前年份(用于页脚等位置) "CURRENT_YEAR": timezone.now().year,
"GLOBAL_HEADER": setting.global_header, # 全局头部代码 "GLOBAL_HEADER": setting.global_header,
"GLOBAL_FOOTER": setting.global_footer, # 全局底部代码 "GLOBAL_FOOTER": setting.global_footer,
"COMMENT_NEED_REVIEW": setting.comment_need_review, # 评论是否需要审核 "COMMENT_NEED_REVIEW": setting.comment_need_review,
} }
cache.set(key, value, 60 * 60 * 10) cache.set(key, value, 60 * 60 * 10)
#ymq将生成的上下文数据存入缓存有效期10小时60秒*60分*10小时
return value return value
#ymq返回构建好的上下文数据字典
=======
def seo_processor(requests):#lxy SEO相关上下文处理器
key = 'seo_processor'#lxy 缓存键名
value = cache.get(key)#lxy 从缓存取数据
if value:
return value#lxy 有缓存则直接返回
else:
logger.info('set processor cache.')#lxy 记录缓存设置日志
setting = get_blog_setting()#lxy 获取博客配置
value = {
'SITE_NAME': setting.site_name,#lxy 站点名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,#lxy 是否显示谷歌广告
#lxy 谷歌广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description,#lxy 站点SEO描述
'SITE_DESCRIPTION': setting.site_description,#lxy 站点描述
'SITE_KEYWORDS': setting.site_keywords,#lxy 站点关键词
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',#lxy 站点基础URL
'ARTICLE_SUB_LENGTH': setting.article_sub_length,#lxy 文章摘要长度
'nav_category_list': Category.objects.all(),#lxy 导航分类列表
'nav_pages': Article.objects.filter(#lxy 导航页面
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment,#lxy 是否开启站点评论
'BEIAN_CODE': setting.beian_code,#lxy 备案号
'ANALYTICS_CODE': setting.analytics_code,#lxy 统计代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,#lxy 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, #lxy 是否显示公安备案
"CURRENT_YEAR": timezone.now().year,#lxy 当前年份
"GLOBAL_HEADER": setting.global_header,#lxy 全局头部内容
"GLOBAL_FOOTER": setting.global_footer,#lxy 全局底部内容
"COMMENT_NEED_REVIEW": setting.comment_need_review,#lxy 评论是否需要审核
}
cache.set(key, value, 60 * 60 * 10)#lxy 设置缓存有效期10小时)
return value#lxy 返回上下文数据
>>>>>>> LXY_branch

@ -1,51 +1,26 @@
import time import time
#ymq导入time模块用于处理时间相关操作如生成唯一ID
import elasticsearch.client import elasticsearch.client
#ymq导入elasticsearch客户端模块用于操作Elasticsearch的Ingest API
from django.conf import settings from django.conf import settings
#ymq导入Django的settings模块用于获取项目配置
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
#ymq导入elasticsearch-dsl相关类用于定义Elasticsearch文档结构和字段类型
from elasticsearch_dsl.connections import connections from elasticsearch_dsl.connections import connections
#ymq导入elasticsearch-dsl的连接管理工具用于创建与Elasticsearch的连接
from blog.models import Article from blog.models import Article
#ymq从blog应用导入Article模型用于同步数据到Elasticsearch
#ymq判断是否启用Elasticsearch检查settings中是否配置了ELASTICSEARCH_DSL
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
#lxy 判断是否启用ES
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
<<<<<<< HEAD
#ymq如果启用Elasticsearch创建连接
connections.create_connection( connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
#ymq创建Elasticsearch客户端实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient from elasticsearch.client import IngestClient
#ymq创建Ingest客户端用于管理数据处理管道
=======
connections.create_connection(#lxy 创建ES连接
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch#lxy 导入ES客户端
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])#lxy 初始化ES客户端 c = IngestClient(es)
from elasticsearch.client import IngestClient#lxy 导入Ingest客户端
>>>>>>> LXY_branch
c = IngestClient(es) #lxy 初始化Ingest客户端
try: try:
<<<<<<< HEAD
#ymq尝试获取名为'geoip'的管道,检查是否已存在
c.get_pipeline('geoip') c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError: except elasticsearch.exceptions.NotFoundError:
#ymq如果管道不存在则创建它用于解析IP地址的地理位置信息
=======
c.get_pipeline('geoip')#lxy 检查geoip管道是否存在
except elasticsearch.exceptions.NotFoundError:#lxy 创建geoip管道解析IP地理信息
>>>>>>> LXY_branch
c.put_pipeline('geoip', body='''{ c.put_pipeline('geoip', body='''{
"description" : "Add geoip info", "description" : "Add geoip info",
"processors" : [ "processors" : [
@ -58,338 +33,181 @@ if ELASTICSEARCH_ENABLED:
}''') }''')
<<<<<<< HEAD
class GeoIp(InnerDoc): class GeoIp(InnerDoc):
#ymq定义地理位置信息的内部文档嵌套结构 continent_name = Keyword()
continent_name = Keyword() # 大陆名称(关键字类型,不分词) country_iso_code = Keyword()
country_iso_code = Keyword() # 国家ISO代码关键字类型 country_name = Keyword()
country_name = Keyword() # 国家名称(关键字类型) location = GeoPoint()
location = GeoPoint() # 地理位置坐标(经纬度)
class UserAgentBrowser(InnerDoc): class UserAgentBrowser(InnerDoc):
#ymq定义用户代理中浏览器信息的内部文档 Family = Keyword()
Family = Keyword() # 浏览器家族如Chrome、Firefox Version = Keyword()
Version = Keyword() # 浏览器版本
class UserAgentOS(UserAgentBrowser): class UserAgentOS(UserAgentBrowser):
#ymq定义用户代理中操作系统信息的内部文档继承浏览器结构
pass pass
=======
class GeoIp(InnerDoc): #lxy IP地理信息嵌套文档
continent_name = Keyword() #lxy 大洲名称
country_iso_code = Keyword() #lxy 国家ISO代码
country_name = Keyword() #lxy 国家名称
location = GeoPoint() #lxy 地理位置坐标
class UserAgentBrowser(InnerDoc): #lxy 浏览器信息嵌套文档
Family = Keyword() #lxy 浏览器类型
Version = Keyword() #lxy 浏览器版本
class UserAgentOS(UserAgentBrowser): #lxy 操作系统信息嵌套文档 class UserAgentDevice(InnerDoc):
pass #lxy 继承浏览器文档结构 Family = Keyword()
>>>>>>> LXY_branch Brand = Keyword()
Model = Keyword()
class UserAgentDevice(InnerDoc): #lxy 设备信息嵌套文档
Family = Keyword() #lxy 设备类型
Brand = Keyword() #lxy 设备品牌
Model = Keyword() #lxy 设备型号
<<<<<<< HEAD
class UserAgentDevice(InnerDoc):
#ymq定义用户代理中设备信息的内部文档
Family = Keyword() # 设备家族
Brand = Keyword() # 设备品牌
Model = Keyword() # 设备型号
=======
class UserAgent(InnerDoc): #lxy 用户代理信息嵌套文档
browser = Object(UserAgentBrowser, required=False) #lxy 浏览器信息
os = Object(UserAgentOS, required=False) #lxy 操作系统信息
device = Object(UserAgentDevice, required=False) #lxy 设备信息
string = Text() #lxy 用户代理原始字符串
is_bot = Boolean() #lxy 是否为爬虫
>>>>>>> LXY_branch
class ElapsedTimeDocument(Document): # lxy 耗时统计ES文档类
url = Keyword() # lxy 请求URL
time_taken = Long() # lxy 耗时(毫秒)
log_datetime = Date() # lxy 日志时间
ip = Keyword() # lxy 请求IP
geoip = Object(GeoIp, required=False) # lxy IP地理信息
useragent = Object(UserAgent, required=False) # lxy 用户代理信息
<<<<<<< HEAD
class UserAgent(InnerDoc): class UserAgent(InnerDoc):
#ymq定义用户代理完整信息的内部文档嵌套结构 browser = Object(UserAgentBrowser, required=False)
browser = Object(UserAgentBrowser, required=False) # 浏览器信息 os = Object(UserAgentOS, required=False)
os = Object(UserAgentOS, required=False) # 操作系统信息 device = Object(UserAgentDevice, required=False)
device = Object(UserAgentDevice, required=False) # 设备信息 string = Text()
string = Text() # 原始用户代理字符串 is_bot = Boolean()
is_bot = Boolean() # 是否为爬虫
class ElapsedTimeDocument(Document): class ElapsedTimeDocument(Document):
#ymq定义用于记录性能耗时的Elasticsearch文档 url = Keyword()
url = Keyword() # 访问的URL关键字类型 time_taken = Long()
time_taken = Long() # 耗时(毫秒) log_datetime = Date()
log_datetime = Date() # 日志记录时间 ip = Keyword()
ip = Keyword() # 访问IP地址 geoip = Object(GeoIp, required=False)
geoip = Object(GeoIp, required=False) # 地理位置信息(嵌套) useragent = Object(UserAgent, required=False)
useragent = Object(UserAgent, required=False) # 用户代理信息(嵌套)
class Index: class Index:
#ymq定义索引配置 name = 'performance'
name = 'performance' # 索引名称
settings = {
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
=======
class Index: #lxy ES索引配置
name = 'performance' #lxy 索引名称
settings = { settings = {
"number_of_shards": 1, #lxy 分片数 "number_of_shards": 1,
"number_of_replicas": 0 #lxy 副本数 "number_of_replicas": 0
>>>>>>> LXY_branch
} }
class Meta: #lxy 文档元信息
doc_type = 'ElapsedTime' #lxy 文档类型
<<<<<<< HEAD
class Meta: class Meta:
doc_type = 'ElapsedTime' # 文档类型Elasticsearch 7.x后逐渐废弃 doc_type = 'ElapsedTime'
class ElaspedTimeDocumentManager: class ElaspedTimeDocumentManager:
#ymq性能耗时文档的管理类用于索引的创建、删除和数据插入
@staticmethod @staticmethod
def build_index(): def build_index():
#ymq创建索引如果不存在
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance") # 检查索引是否存在 res = client.indices.exists(index="performance")
if not res: if not res:
ElapsedTimeDocument.init() # 初始化索引 ElapsedTimeDocument.init()
@staticmethod @staticmethod
def delete_index(): def delete_index():
#ymq删除performance索引
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404]) # 忽略不存在的情况 es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod @staticmethod
def create(url, time_taken, log_datetime, useragent, ip): def create(url, time_taken, log_datetime, useragent, ip):
#ymq创建一条性能耗时记录 ElaspedTimeDocumentManager.build_index()
ElaspedTimeDocumentManager.build_index() # 确保索引存在
#ymq构建用户代理信息对象
ua = UserAgent() ua = UserAgent()
=======
class ElapsedTimeDocumentManager: #lxy 耗时文档管理类
@staticmethod
def build_index(): #lxy 创建ES索引
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance") #lxy 检查索引是否存在
if not res:
ElapsedTimeDocument.init() #lxy 初始化索引
@staticmethod
def delete_index(): #lxy 删除ES索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404]) #lxy 忽略不存在错误
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip): #lxy 创建耗时文档
ElapsedTimeDocumentManager.build_index()
ua = UserAgent() #lxy 初始化用户代理对象
>>>>>>> LXY_branch
ua.browser = UserAgentBrowser() ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family ua.browser.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string ua.browser.Version = useragent.browser.version_string
ua.os = UserAgentOS() ua.os = UserAgentOS()
ua.os.Family = useragent.os.family ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string ua.os.Version = useragent.os.version_string
ua.device = UserAgentDevice() ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model ua.device.Model = useragent.device.model
ua.string = useragent.ua_string ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot ua.is_bot = useragent.is_bot
<<<<<<< HEAD
#ymq创建文档实例使用时间戳作为唯一ID
doc = ElapsedTimeDocument( doc = ElapsedTimeDocument(
meta={ meta={
'id': int( 'id': int(
round( round(
time.time() * time.time() *
1000)) # 毫秒级时间戳作为ID 1000))
=======
doc = ElapsedTimeDocument( #lxy 构造耗时文档
meta={
'id': int(round(time.time() * 10000)) #lxy 生成唯一ID
>>>>>>> LXY_branch
}, },
url=url, time_taken=time_taken, log_datetime=log_datetime, url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip) useragent=ua, ip=ip)
<<<<<<< HEAD
#ymq保存文档时应用geoip管道解析IP地址
doc.save(pipeline="geoip") doc.save(pipeline="geoip")
class ArticleDocument(Document): class ArticleDocument(Document):
#ymq定义文章信息的Elasticsearch文档用于搜索
#ymqbody和title使用IK分词器max_word分词更细smart更简洁
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
#ymq嵌套作者信息
author = Object(properties={ author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer() 'id': Integer()
}) })
#ymq嵌套分类信息
category = Object(properties={ category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer() 'id': Integer()
}) })
#ymq嵌套标签信息数组
tags = Object(properties={ tags = Object(properties={
=======
doc.save(pipeline="geoip") #lxy 保存文档用geoip管道解析IP
class ArticleDocument(Document): #lxy 文章ES文档类
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') #lxy 文章内容(中文分词)
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') #lxy 文章标题(中文分词)
author = Object(properties={ #lxy 作者信息
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
category = Object(properties={ #lxy 分类信息
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer() 'id': Integer()
}) })
tags = Object(properties={ #lxy 标签信息
>>>>>>> LXY_branch pub_time = Date()
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), status = Text()
'id': Integer() comment_status = Text()
}) type = Text()
pub_time = Date() #lxy 发布时间 views = Integer()
status = Text() #lxy 文章状态 article_order = Integer()
comment_status = Text() #lxy 评论状态
type = Text() #lxy 文章类型
views = Integer() #lxy 阅读量
article_order = Integer() #lxy 文章排序
<<<<<<< HEAD
pub_time = Date() # 发布时间
status = Text() # 状态(发布/草稿)
comment_status = Text() # 评论状态(开启/关闭)
type = Text() # 类型(文章/页面)
views = Integer() # 浏览量
article_order = Integer() # 排序序号
class Index: class Index:
name = 'blog' # 索引名称 name = 'blog'
=======
class Index: #lxy 文章索引配置
name = 'blog' #lxy 索引名称
>>>>>>> LXY_branch
settings = { settings = {
"number_of_shards": 1, #lxy 分片数 "number_of_shards": 1,
"number_of_replicas": 0 #lxy 副本数 "number_of_replicas": 0
} }
class Meta: #lxy 文档元信息
doc_type = 'Article' #lxy 文档类型
<<<<<<< HEAD
class Meta: class Meta:
doc_type = 'Article' # 文档类型 doc_type = 'Article'
class ArticleDocumentManager(): class ArticleDocumentManager():
#ymq文章文档的管理类用于索引操作和数据同步
def __init__(self): def __init__(self):
#ymq初始化时创建索引
self.create_index() self.create_index()
def create_index(self): def create_index(self):
#ymq初始化文章索引
ArticleDocument.init() ArticleDocument.init()
def delete_index(self): def delete_index(self):
#ymq删除blog索引
=======
class ArticleDocumentManager():#lxy 文章文档管理类
def __init__(self): #lxy 初始化方法
self.create_index()
def create_index(self):#lxy 创建文章索引
ArticleDocument.init()
def delete_index(self): #lxy 删除文章索引
>>>>>>> LXY_branch
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404]) es.indices.delete(index='blog', ignore=[400, 404])
<<<<<<< HEAD
def convert_to_doc(self, articles): def convert_to_doc(self, articles):
#ymq将Django模型对象转换为Elasticsearch文档对象
=======
def convert_to_doc(self, articles):#lxy 文章对象转ES文档
>>>>>>> LXY_branch
return [ return [
ArticleDocument( ArticleDocument(
meta={'id': article.id}, # 使用文章ID作为文档ID meta={
'id': article.id},
body=article.body, body=article.body,
title=article.title, title=article.title,
author={ author={
'nickname': article.author.username, 'nickname': article.author.username,
'id': article.author.id 'id': article.author.id},
},
category={ category={
'name': article.category.name, 'name': article.category.name,
'id': article.category.id 'id': article.category.id},
}, tags=[
tags=[{'name': t.name, 'id': t.id} for t in article.tags.all()], # 转换多对多标签 {
'name': t.name,
'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time, pub_time=article.pub_time,
status=article.status, status=article.status,
comment_status=article.comment_status, comment_status=article.comment_status,
type=article.type, type=article.type,
views=article.views, views=article.views,
article_order=article.article_order article_order=article.article_order) for article in articles]
) for article in articles
]
<<<<<<< HEAD
def rebuild(self, articles=None): def rebuild(self, articles=None):
#ymq重建索引默认同步所有文章可指定文章列表
ArticleDocument.init() ArticleDocument.init()
articles = articles if articles else Article.objects.all() # 获取文章数据 articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles) # 转换为文档对象 docs = self.convert_to_doc(articles)
for doc in docs: for doc in docs:
doc.save() # 保存到Elasticsearch doc.save()
def update_docs(self, docs): def update_docs(self, docs):
#ymq批量更新文档
for doc in docs: for doc in docs:
doc.save() doc.save()
=======
def rebuild(self, articles=None):#lxy 重建文章索引
ArticleDocument.init()
articles = articles if articles else Article.objects.all()#lxy 获取所有文章
docs = self.convert_to_doc(articles)
for doc in docs:
doc.save()#lxy 保存到ES
def update_docs(self, docs):#lxy 更新文章文档
for doc in docs:
doc.save() #lxy 保存更新
>>>>>>> LXY_branch

@ -1,53 +1,19 @@
<<<<<<< HEAD
import logging #导入 Python 标准库的 logging 模块,用于日志记录,方便追踪程序运行过程中的关键信息。 import logging #导入 Python 标准库的 logging 模块,用于日志记录,方便追踪程序运行过程中的关键信息。
=======
import logging
#ymq导入logging模块用于记录搜索相关日志
>>>>>>> c6856732b39cce6b1aab30e6649dcdb806b75b9f
from django import forms from django import forms
#ymq导入Django的forms模块用于创建自定义表单
from haystack.forms import SearchForm from haystack.forms import SearchForm
#ymq导入Haystack的SearchForm基类扩展实现博客搜索表单
<<<<<<< HEAD
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
class BlogSearchForm(SearchForm): class BlogSearchForm(SearchForm):
#ymq定义博客搜索表单类继承自Haystack的SearchForm
querydata = forms.CharField(required=True) querydata = forms.CharField(required=True)
#ymq定义搜索关键词字段required=True表示该字段为必填项
def search(self): def search(self):
#ymq重写父类的search方法自定义搜索逻辑
datas = super(BlogSearchForm, self).search() datas = super(BlogSearchForm, self).search()
#ymq调用父类search方法获取基础搜索结果
if not self.is_valid(): if not self.is_valid():
#ymq如果表单数据验证不通过返回无结果响应
return self.no_query_found() return self.no_query_found()
if self.cleaned_data['querydata']: if self.cleaned_data['querydata']:
#ymq如果存在合法的搜索关键词记录关键词日志
logger.info(self.cleaned_data['querydata']) logger.info(self.cleaned_data['querydata'])
return datas return datas
#ymq返回最终的搜索结果集
=======
logger = logging.getLogger(__name__)#lxy 获取当前模块的日志记录器
class BlogSearchForm(SearchForm): #lxy 博客搜索表单类
querydata = forms.CharField(required=True)#lxy 搜索关键词字段(必填)
def search(self):#lxy 搜索方法
datas = super(BlogSearchForm, self).search()#lxy 调用父类搜索方法
if not self.is_valid():#lxy 校验表单是否合法
return self.no_query_found()#lxy 不合法则返回无结果
if self.cleaned_data['querydata']:#lxy 若有搜索关键词
logger.info(self.cleaned_data['querydata'])#lxy 记录搜索关键词日志
return datas#lxy 返回搜索结果
>>>>>>> LXY_branch

@ -1,31 +1,18 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \ from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED ELASTICSEARCH_ENABLED
#ymq从blog.documents导入Elasticsearch相关的文档类和管理器以及启用状态常量
# TODO 参数化 # TODO 参数化
class Command(BaseCommand): class Command(BaseCommand):
#ymq定义自定义管理命令类继承自BaseCommand
help = 'build search index' help = 'build search index'
#ymq定义命令的帮助信息使用python manage.py help build_index时显示
def handle(self, *args, **options): def handle(self, *args, **options):
#ymq命令的核心处理方法执行实际的索引构建逻辑
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
#ymq仅当Elasticsearch启用时执行以下操作
ElaspedTimeDocumentManager.build_index() ElaspedTimeDocumentManager.build_index()
#ymq调用性能耗时文档管理器构建索引若不存在
manager = ElapsedTimeDocument() manager = ElapsedTimeDocument()
manager.init() manager.init()
#ymq初始化ElapsedTimeDocument对应的索引结构
manager = ArticleDocumentManager() manager = ArticleDocumentManager()
manager.delete_index() manager.delete_index()
#ymq删除已存在的文章索引重建前清理
manager.rebuild() manager.rebuild()
#ymq重建文章索引将数据库中的文章数据同步到Elasticsearch

@ -1,20 +1,13 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from blog.models import Tag, Category from blog.models import Tag, Category
#ymq从blog应用导入Tag标签和Category分类模型
# TODO 参数化 # TODO 参数化
class Command(BaseCommand): class Command(BaseCommand):
#ymq定义自定义管理命令类继承自BaseCommand
help = 'build search words' help = 'build search words'
#ymq命令的帮助信息说明该命令用于生成搜索词
def handle(self, *args, **options): def handle(self, *args, **options):
#ymq命令的核心处理方法执行生成搜索词的逻辑
# 从标签和分类中提取名称使用set去重
datas = set([t.name for t in Tag.objects.all()] + datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()]) [t.name for t in Category.objects.all()])
# 按行打印所有去重后的名称(作为搜索词) print('\n'.join(datas))
print('\n'.join(datas))

@ -1,17 +1,11 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from djangoblog.utils import cache from djangoblog.utils import cache
#ymq从项目工具模块导入缓存工具
class Command(BaseCommand): class Command(BaseCommand):
#ymq定义清除缓存的自定义命令类继承自BaseCommand
help = 'clear the whole cache' help = 'clear the whole cache'
#ymq命令的帮助信息说明该命令用于清除所有缓存
def handle(self, *args, **options): def handle(self, *args, **options):
#ymq命令的核心处理方法执行清除缓存操作 cache.clear()
cache.clear() # 调用缓存工具的clear方法清除所有缓存数据
self.stdout.write(self.style.SUCCESS('Cleared cache\n')) self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
#ymq向标准输出写入成功信息使用Django的SUCCESS样式通常为绿色

@ -1,62 +1,40 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
#ymq导入获取用户模型的函数便于灵活引用用户模型
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
#ymq导入密码加密函数用于安全存储密码
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from blog.models import Article, Tag, Category from blog.models import Article, Tag, Category
#ymq从blog应用导入文章、标签、分类模型
class Command(BaseCommand): class Command(BaseCommand):
#ymq定义创建测试数据的自定义命令类继承自BaseCommand
help = 'create test datas' help = 'create test datas'
#ymq命令的帮助信息说明该命令用于创建测试数据
def handle(self, *args, **options): def handle(self, *args, **options):
#ymq命令的核心处理方法执行创建测试数据的逻辑
# 创建或获取测试用户(邮箱、用户名、密码加密存储)
user = get_user_model().objects.get_or_create( user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0] email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# 创建或获取父分类
pcategory = Category.objects.get_or_create( pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0] name='我是父类目', parent_category=None)[0]
# 创建或获取子分类(关联父分类)
category = Category.objects.get_or_create( category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0] name='子类目', parent_category=pcategory)[0]
category.save() # 保存子分类 category.save()
# 创建基础标签
basetag = Tag() basetag = Tag()
basetag.name = "标签" basetag.name = "标签"
basetag.save() basetag.save()
# 批量创建20篇测试文章
for i in range(1, 20): for i in range(1, 20):
# 创建或获取文章(关联分类、作者)
article = Article.objects.get_or_create( article = Article.objects.get_or_create(
category=category, category=category,
title='nice title ' + str(i), # 文章标题带序号 title='nice title ' + str(i),
body='nice content ' + str(i), # 文章内容带序号 body='nice content ' + str(i),
author=user)[0] author=user)[0]
# 创建带序号的标签
tag = Tag() tag = Tag()
tag.name = "标签" + str(i) tag.name = "标签" + str(i)
tag.save() tag.save()
# 给文章添加标签(包含基础标签和序号标签)
article.tags.add(tag) article.tags.add(tag)
article.tags.add(basetag) article.tags.add(basetag)
article.save() # 保存文章 article.save()
# 清除缓存,确保测试数据立即生效
from djangoblog.utils import cache from djangoblog.utils import cache
cache.clear() cache.clear()
self.stdout.write(self.style.SUCCESS('created test datas \n'))
# 输出成功信息
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -1,24 +1,16 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from djangoblog.spider_notify import SpiderNotify from djangoblog.spider_notify import SpiderNotify
#ymq导入蜘蛛通知工具类用于向搜索引擎提交URL
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
#ymq导入获取当前站点信息的工具函数
from blog.models import Article, Tag, Category from blog.models import Article, Tag, Category
#ymq从blog应用导入文章、标签、分类模型
site = get_current_site().domain site = get_current_site().domain
#ymq获取当前站点的域名用于构建完整URL
class Command(BaseCommand): class Command(BaseCommand):
#ymq定义百度URL提交命令类继承自BaseCommand
help = 'notify baidu url' help = 'notify baidu url'
#ymq命令的帮助信息说明该命令用于向百度提交URL
def add_arguments(self, parser): def add_arguments(self, parser):
#ymq定义命令参数指定提交的数据类型
parser.add_argument( parser.add_argument(
'data_type', 'data_type',
type=str, type=str,
@ -28,46 +20,31 @@ class Command(BaseCommand):
'tag', 'tag',
'category'], 'category'],
help='article : all article,tag : all tag,category: all category,all: All of these') help='article : all article,tag : all tag,category: all category,all: All of these')
#ymq参数说明article-所有文章tag-所有标签category-所有分类all-全部
def get_full_url(self, path): def get_full_url(self, path):
#ymq构建包含域名的完整URL
url = "https://{site}{path}".format(site=site, path=path) url = "https://{site}{path}".format(site=site, path=path)
return url return url
def handle(self, *args, **options): def handle(self, *args, **options):
#ymq命令核心处理方法执行URL收集和提交 type = options['data_type']
type = options['data_type'] # 获取用户指定的数据类型 self.stdout.write('start get %s' % type)
self.stdout.write('start get %s' % type) # 输出开始收集信息的提示
urls = [] # 存储待提交的URL列表 urls = []
# 根据数据类型收集对应的URL
if type == 'article' or type == 'all': if type == 'article' or type == 'all':
# 收集已发布文章的URL
for article in Article.objects.filter(status='p'): for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url()) urls.append(article.get_full_url())
if type == 'tag' or type == 'all': if type == 'tag' or type == 'all':
# 收集所有标签页的URL
for tag in Tag.objects.all(): for tag in Tag.objects.all():
url = tag.get_absolute_url() url = tag.get_absolute_url()
urls.append(self.get_full_url(url)) urls.append(self.get_full_url(url))
if type == 'category' or type == 'all': if type == 'category' or type == 'all':
# 收集所有分类页的URL
for category in Category.objects.all(): for category in Category.objects.all():
url = category.get_absolute_url() url = category.get_absolute_url()
urls.append(self.get_full_url(url)) urls.append(self.get_full_url(url))
# 输出待提交的URL数量
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
'start notify %d urls' % 'start notify %d urls' %
len(urls))) len(urls)))
# 调用工具类向百度提交URL
SpiderNotify.baidu_notify(urls) SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))
# 输出提交完成的提示
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -1,70 +1,47 @@
import requests import requests
#ymq导入requests库用于发送HTTP请求测试图片URL有效性
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from django.templatetags.static import static from django.templatetags.static import static
#ymq导入static标签用于获取静态文件URL
from djangoblog.utils import save_user_avatar from djangoblog.utils import save_user_avatar
#ymq导入保存用户头像的工具函数
from oauth.models import OAuthUser from oauth.models import OAuthUser
#ymq从oauth应用导入OAuthUser模型存储第三方用户信息
from oauth.oauthmanager import get_manager_by_type from oauth.oauthmanager import get_manager_by_type
#ymq导入获取对应第三方登录管理器的函数
class Command(BaseCommand): class Command(BaseCommand):
#ymq定义同步用户头像的自定义命令类继承自BaseCommand
help = 'sync user avatar' help = 'sync user avatar'
#ymq命令的帮助信息说明该命令用于同步用户头像
def test_picture(self, url): def test_picture(self, url):
#ymq测试图片URL是否有效状态码200
try: try:
if requests.get(url, timeout=2).status_code == 200: if requests.get(url, timeout=2).status_code == 200:
return True # URL有效返回True return True
except: except:
pass # 异常或状态码非200返回None pass
def handle(self, *args, **options): def handle(self, *args, **options):
#ymq命令核心处理方法执行用户头像同步逻辑 static_url = static("../")
static_url = static("../") # 获取静态文件基础URL users = OAuthUser.objects.all()
users = OAuthUser.objects.all() # 获取所有第三方用户 self.stdout.write(f'开始同步{len(users)}个用户头像')
self.stdout.write(f'开始同步{len(users)}个用户头像') # 输出待同步用户数量
for u in users: for u in users:
#ymq遍历每个用户进行头像同步 self.stdout.write(f'开始同步:{u.nickname}')
self.stdout.write(f'开始同步:{u.nickname}') # 输出当前同步的用户名 url = u.picture
url = u.picture # 获取用户当前头像URL
if url: if url:
# 处理已有头像URL的情况
if url.startswith(static_url): if url.startswith(static_url):
# 头像URL是本地静态文件
if self.test_picture(url): if self.test_picture(url):
# 图片有效,跳过同步
continue continue
else: else:
# 图片无效,重新获取
if u.metadata: if u.metadata:
# 有元数据,通过第三方管理器获取头像
manage = get_manager_by_type(u.type) manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata) url = manage.get_picture(u.metadata)
url = save_user_avatar(url) # 保存头像并获取本地URL url = save_user_avatar(url)
else: else:
# 无元数据,使用默认头像
url = static('blog/img/avatar.png') url = static('blog/img/avatar.png')
else: else:
# 头像URL是外部链接保存到本地
url = save_user_avatar(url) url = save_user_avatar(url)
else: else:
# 无头像URL使用默认头像
url = static('blog/img/avatar.png') url = static('blog/img/avatar.png')
if url: if url:
# 保存更新后的头像URL self.stdout.write(
self.stdout.write(f'结束同步:{u.nickname}.url:{url}') f'结束同步:{u.nickname}.url:{url}')
u.picture = url u.picture = url
u.save() u.save()
self.stdout.write('结束同步')
self.stdout.write('结束同步') # 输出同步完成提示

@ -1,97 +1,42 @@
import logging import logging
import time import time
#ymq导入logging用于日志记录time用于计算页面加载时间
from ipware import get_client_ip from ipware import get_client_ip
#ymq导入get_client_ip工具用于获取客户端IP地址
from user_agents import parse from user_agents import parse
#ymq导入parse函数用于解析用户代理字符串
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
#ymq从博客文档模块导入Elasticsearch启用状态和性能日志管理器
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
<<<<<<< HEAD
class OnlineMiddleware(object): class OnlineMiddleware(object):
#ymq定义在线中间件类用于记录页面加载性能和访问信息
def __init__(self, get_response=None): def __init__(self, get_response=None):
#ymq初始化中间件接收Django的响应处理器
self.get_response = get_response self.get_response = get_response
super().__init__() super().__init__()
def __call__(self, request): def __call__(self, request):
#ymq中间件核心方法处理请求并返回响应
''' page render time ''' ''' page render time '''
#ymq记录页面渲染时间的逻辑 start_time = time.time()
start_time = time.time() # 记录请求处理开始时间 response = self.get_response(request)
response = self.get_response(request) # 调用后续中间件或视图处理请求 http_user_agent = request.META.get('HTTP_USER_AGENT', '')
ip, _ = get_client_ip(request)
#ymq获取用户代理和IP地址 user_agent = parse(http_user_agent)
http_user_agent = request.META.get('HTTP_USER_AGENT', '') # 获取用户代理字符串
ip, _ = get_client_ip(request) # 获取客户端IP地址
user_agent = parse(http_user_agent) # 解析用户代理信息(浏览器、设备等)
#ymq非流式响应才处理流式响应无法修改内容
if not response.streaming: if not response.streaming:
try: try:
cast_time = time.time() - start_time # 计算页面加载耗时(秒) cast_time = time.time() - start_time
#ymq如果启用了Elasticsearch记录性能数据
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2) #ymq: 转换为毫秒并保留两位小数 time_taken = round((cast_time) * 1000, 2)
url = request.path # 获取请求的URL路径 url = request.path
from django.utils import timezone from django.utils import timezone
#ymq调用管理器创建性能日志记录
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(), #ymq: 记录当前时间
useragent=user_agent, #ymq: 已解析的用户代理信息
ip=ip) #ymq: 客户端IP
#ymq替换响应内容中的<!!LOAD_TIMES!!>标记为实际加载时间保留前5位字符
=======
class OnlineMiddleware(object):#lxy 在线统计中间件
def __init__(self, get_response=None):#lxy 初始化方法
self.get_response = get_response
super().__init__()
def __call__(self, request):#lxy 中间件核心方法(处理请求)
''' page render time '''
start_time = time.time()
response = self.get_response(request)
http_user_agent = request.META.get('HTTP_USER_AGENT', '')#lxy 获取用户代理
ip, _ = get_client_ip(request)#lxy 获取客户端IP
user_agent = parse(http_user_agent) #lxy 解析用户代理
if not response.streaming: #lxy 非流式响应时执行
try:
cast_time = time.time() - start_time#lxy 计算耗时
if ELASTICSEARCH_ENABLED:#lxy 若启用ES
time_taken = round((cast_time) * 1000, 2)#lxy 耗时转毫秒
url = request.path#lxy 请求路径
from django.utils import timezone # 记录耗时到ES
ElaspedTimeDocumentManager.create( ElaspedTimeDocumentManager.create(
url=url, url=url,
time_taken=time_taken, time_taken=time_taken,
log_datetime=timezone.now(), log_datetime=timezone.now(),
useragent=user_agent, useragent=user_agent,
ip=ip) # 替换页面中的加载时间标记 ip=ip)
>>>>>>> LXY_branch
response.content = response.content.replace( response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5])) b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e: except Exception as e:
<<<<<<< HEAD
#ymq捕获并记录处理过程中的异常
logger.error("Error OnlineMiddleware: %s" % e) logger.error("Error OnlineMiddleware: %s" % e)
return response #ymq: 返回处理后的响应 return response
=======
logger.error("Error OnlineMiddleware: %s" % e)#lxy 捕获异常并日志
return response#lxy 返回响应
>>>>>>> LXY_branch

@ -1,34 +1,25 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14 # Generated by Django 4.1.7 on 2023-03-02 07:14
#ymq该迁移文件由Django 4.1.7自动生成生成时间为2023-03-02 07:14
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
import mdeditor.fields import mdeditor.fields
#ymq导入Django迁移相关模块、时间工具和markdown编辑器字段
class Migration(migrations.Migration): class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
initial = True initial = True
#ymq标记为初始迁移第一次创建模型时生成
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
#ymq依赖于用户模型确保用户表先创建
] ]
operations = [ operations = [
#ymq定义数据库操作列表按顺序执行创建模型的操作
migrations.CreateModel( migrations.CreateModel(
#ymq创建BlogSettings模型网站配置
name='BlogSettings', name='BlogSettings',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#ymq自增主键字段
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')), ('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')), ('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')), ('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
@ -44,17 +35,13 @@ class Migration(migrations.Migration):
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')), ('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')), ('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')), ('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
#ymq以上为网站配置的各个字段包含网站基本信息、显示设置、备案信息等
], ],
options={ options={
'verbose_name': '网站配置', 'verbose_name': '网站配置',
'verbose_name_plural': '网站配置', 'verbose_name_plural': '网站配置',
#ymq模型的显示名称
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
#ymq创建Links模型友情链接
name='Links', name='Links',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -65,18 +52,14 @@ class Migration(migrations.Migration):
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')), ('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, 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='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#ymq友情链接字段包含名称、URL、排序、显示位置等
], ],
options={ options={
'verbose_name': '友情链接', 'verbose_name': '友情链接',
'verbose_name_plural': '友情链接', 'verbose_name_plural': '友情链接',
'ordering': ['sequence'], 'ordering': ['sequence'],
#ymq按排序号升序排列
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
#ymq创建SideBar模型侧边栏
name='SideBar', name='SideBar',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -86,18 +69,14 @@ class Migration(migrations.Migration):
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')), ('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, 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='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#ymq侧边栏字段包含标题、内容、排序等
], ],
options={ options={
'verbose_name': '侧边栏', 'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏', 'verbose_name_plural': '侧边栏',
'ordering': ['sequence'], 'ordering': ['sequence'],
#ymq按排序号升序排列
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
#ymq创建Tag模型标签
name='Tag', name='Tag',
fields=[ fields=[
('id', models.AutoField(primary_key=True, serialize=False)), ('id', models.AutoField(primary_key=True, serialize=False)),
@ -105,18 +84,14 @@ class Migration(migrations.Migration):
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')), ('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
#ymq标签字段包含名称、URL友好标识slug
], ],
options={ options={
'verbose_name': '标签', 'verbose_name': '标签',
'verbose_name_plural': '标签', 'verbose_name_plural': '标签',
'ordering': ['name'], 'ordering': ['name'],
#ymq按标签名升序排列
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
#ymq创建Category模型分类
name='Category', name='Category',
fields=[ fields=[
('id', models.AutoField(primary_key=True, serialize=False)), ('id', models.AutoField(primary_key=True, serialize=False)),
@ -126,18 +101,14 @@ class Migration(migrations.Migration):
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')), ('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')), ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
#ymq分类字段支持多级分类自关联外键、权重排序等
], ],
options={ options={
'verbose_name': '分类', 'verbose_name': '分类',
'verbose_name_plural': '分类', 'verbose_name_plural': '分类',
'ordering': ['-index'], 'ordering': ['-index'],
#ymq按权重降序排列权重越大越靠前
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
#ymq创建Article模型文章
name='Article', name='Article',
fields=[ fields=[
('id', models.AutoField(primary_key=True, serialize=False)), ('id', models.AutoField(primary_key=True, serialize=False)),
@ -145,7 +116,6 @@ class Migration(migrations.Migration):
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')), ('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')), ('body', mdeditor.fields.MDTextField(verbose_name='正文')),
#ymq使用markdown编辑器字段存储文章正文
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')), ('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')), ('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')), ('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
@ -154,19 +124,14 @@ class Migration(migrations.Migration):
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')), ('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')), ('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
#ymq关联用户模型外键级联删除
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')), ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
#ymq关联分类模型外键级联删除
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')), ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
#ymq多对多关联标签模型
], ],
options={ options={
'verbose_name': '文章', 'verbose_name': '文章',
'verbose_name_plural': '文章', 'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'], 'ordering': ['-article_order', '-pub_time'],
#ymq先按排序号降序再按发布时间降序
'get_latest_by': 'id', 'get_latest_by': 'id',
#ymq按id获取最新记录
}, },
), ),
] ]

@ -1,34 +1,23 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08 # Generated by Django 4.1.7 on 2023-03-29 06:08
#ymq该迁移文件由Django 4.1.7自动生成生成时间为2023-03-29 06:08
from django.db import migrations, models from django.db import migrations, models
#ymq导入Django迁移相关模块
class Migration(migrations.Migration): class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [ dependencies = [
('blog', '0001_initial'), ('blog', '0001_initial'),
#ymq依赖于blog应用的0001_initial迁移文件确保先执行初始迁移
] ]
operations = [ operations = [
#ymq定义数据库操作列表添加新字段
migrations.AddField( migrations.AddField(
#ymq向BlogSettings模型添加global_footer字段
model_name='blogsettings', model_name='blogsettings',
name='global_footer', name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
#ymq字段类型为文本字段允许为空默认值为空字符串verbose_name为"公共尾部"
), ),
migrations.AddField( migrations.AddField(
#ymq向BlogSettings模型添加global_header字段
model_name='blogsettings', model_name='blogsettings',
name='global_header', name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
#ymq字段类型为文本字段允许为空默认值为空字符串verbose_name为"公共头部"
), ),
] ]

@ -1,25 +1,17 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45 # Generated by Django 4.2.1 on 2023-05-09 07:45
#ymq该迁移文件由Django 4.2.1自动生成生成时间为2023-05-09 07:45
from django.db import migrations, models from django.db import migrations, models
#ymq导入Django迁移相关模块
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
class Migration(migrations.Migration):
dependencies = [ dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'), ('blog', '0002_blogsettings_global_footer_and_more'),
#ymq依赖于blog应用的0002号迁移文件确保先执行该迁移
] ]
operations = [ operations = [
#ymq定义数据库操作此处为添加字段
migrations.AddField( migrations.AddField(
#ymq向BlogSettings模型添加comment_need_review字段 model_name='blogsettings',
model_name='blogsettings', # 目标模型名称 name='comment_need_review',
name='comment_need_review', # 新字段名称
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
#ymq字段类型为布尔值默认值为False不需要审核后台显示名称为"评论是否需要审核"
), ),
] ]

@ -1,39 +1,27 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51 # Generated by Django 4.2.1 on 2023-05-09 07:51
#ymq该迁移文件由Django 4.2.1自动生成生成时间为2023-05-09 07:51
from django.db import migrations from django.db import migrations
#ymq导入Django迁移相关模块
class Migration(migrations.Migration): class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [ dependencies = [
('blog', '0003_blogsettings_comment_need_review'), ('blog', '0003_blogsettings_comment_need_review'),
#ymq依赖于blog应用的0003号迁移文件确保先执行该迁移
] ]
operations = [ operations = [
#ymq定义数据库操作列表主要是重命名字段
migrations.RenameField( migrations.RenameField(
#ymq重命名BlogSettings模型的analyticscode字段 model_name='blogsettings',
model_name='blogsettings', # 目标模型名称 old_name='analyticscode',
old_name='analyticscode', # 旧字段名 new_name='analytics_code',
new_name='analytics_code', # 新字段名(改为下划线命名规范)
), ),
migrations.RenameField( migrations.RenameField(
#ymq重命名BlogSettings模型的beiancode字段
model_name='blogsettings', model_name='blogsettings',
old_name='beiancode', old_name='beiancode',
new_name='beian_code', # 改为下划线命名规范 new_name='beian_code',
), ),
migrations.RenameField( migrations.RenameField(
#ymq重命名BlogSettings模型的sitename字段
model_name='blogsettings', model_name='blogsettings',
old_name='sitename', old_name='sitename',
new_name='site_name', # 改为下划线命名规范 new_name='site_name',
), ),
] ]

@ -1,27 +1,20 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13 # Generated by Django 4.2.5 on 2023-09-06 13:13
#ymq该迁移文件由Django 4.2.5自动生成生成时间为2023-09-06 13:13
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
import mdeditor.fields import mdeditor.fields
#ymq导入Django迁移相关模块、时间工具和markdown编辑器字段
class Migration(migrations.Migration): class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'), ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
#ymq依赖于用户模型和blog应用的0004号迁移文件
] ]
operations = [ operations = [
#ymq定义数据库操作列表包含模型选项修改、字段删除、添加和修改
# 修改模型的元数据选项主要是verbose_name的国际化调整
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='article', name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'}, options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
@ -42,8 +35,6 @@ class Migration(migrations.Migration):
name='tag', name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'}, options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
), ),
# 删除旧的时间字段(命名方式调整)
migrations.RemoveField( migrations.RemoveField(
model_name='article', model_name='article',
name='created_time', name='created_time',
@ -76,8 +67,6 @@ class Migration(migrations.Migration):
model_name='tag', model_name='tag',
name='last_mod_time', name='last_mod_time',
), ),
# 添加新的时间字段统一命名为creation_time和last_modify_time
migrations.AddField( migrations.AddField(
model_name='article', model_name='article',
name='creation_time', name='creation_time',
@ -118,8 +107,6 @@ class Migration(migrations.Migration):
name='last_modify_time', name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
), ),
# 修改Article模型的字段属性主要是verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='article', model_name='article',
name='article_order', name='article_order',
@ -180,8 +167,6 @@ class Migration(migrations.Migration):
name='views', name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'), field=models.PositiveIntegerField(default=0, verbose_name='views'),
), ),
# 修改BlogSettings模型的字段属性verbose_name国际化
migrations.AlterField( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='article_comment_count', name='article_comment_count',
@ -237,8 +222,6 @@ class Migration(migrations.Migration):
name='site_seo_description', name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'), field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
), ),
# 修改Category模型的字段属性
migrations.AlterField( migrations.AlterField(
model_name='category', model_name='category',
name='index', name='index',
@ -254,8 +237,6 @@ class Migration(migrations.Migration):
name='parent_category', name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
), ),
# 修改Links模型的字段属性
migrations.AlterField( migrations.AlterField(
model_name='links', model_name='links',
name='is_enable', name='is_enable',
@ -286,8 +267,6 @@ class Migration(migrations.Migration):
name='show_type', name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'), field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
), ),
# 修改SideBar模型的字段属性
migrations.AlterField( migrations.AlterField(
model_name='sidebar', model_name='sidebar',
name='content', name='content',
@ -313,11 +292,9 @@ class Migration(migrations.Migration):
name='sequence', name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'), field=models.IntegerField(unique=True, verbose_name='order'),
), ),
# 修改Tag模型的字段属性
migrations.AlterField( migrations.AlterField(
model_name='tag', model_name='tag',
name='name', name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'), field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
), ),
] ]

@ -1,23 +1,17 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41 # Generated by Django 4.2.7 on 2024-01-26 02:41
#ymq该迁移文件由Django 4.2.7自动生成生成时间为2024年1月26日02:41
from django.db import migrations from django.db import migrations
#ymq导入Django迁移相关模块
class Migration(migrations.Migration): class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [ dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'), ('blog', '0005_alter_article_options_alter_category_options_and_more'),
#ymq依赖于blog应用的0005号迁移文件确保先执行该迁移
] ]
operations = [ operations = [
#ymq定义数据库操作此处为修改模型选项
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='blogsettings', name='blogsettings',
#ymq修改BlogSettings模型的显示名称改为英文"Website configuration"
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
), ),
] ]

@ -1,7 +1,6 @@
import logging import logging
import re import re
from abc import abstractmethod from abc import abstractmethod
#ymq导入logging用于日志记录re用于正则表达式操作abstractmethod用于定义抽象方法
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -9,349 +8,206 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField # 导入markdown编辑器字段 from mdeditor.fields import MDTextField
from uuslug import slugify # 导入slug生成工具 from uuslug import slugify
from djangoblog.utils import cache_decorator, cache # 导入缓存相关工具 from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具 from djangoblog.utils import get_current_site
<<<<<<< HEAD logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
=======
logger = logging.getLogger(__name__) #lxy 初始化日志记录器
>>>>>>> LXY_branch
class LinkShowType(models.TextChoices): #lxy 链接展示类型枚举
I = ('i', _('index')) #lxy 首页展示
L = ('l', _('list')) #lxy 列表页展示
P = ('p', _('post')) #lxy 文章页展示
A = ('a', _('all')) #lxy 所有页面展示
S = ('s', _('side')) #lxy 侧边栏展示
<<<<<<< HEAD
class LinkShowType(models.TextChoices): class LinkShowType(models.TextChoices):
#ymq定义链接展示位置的枚举类 I = ('i', _('index'))
I = ('i', _('index')) # 首页展示 L = ('l', _('list'))
L = ('l', _('list')) # 列表页展示 P = ('p', _('post'))
P = ('p', _('post')) # 文章页展示 A = ('a', _('all'))
A = ('a', _('all')) # 所有页面展示 S = ('s', _('slide'))
S = ('s', _('slide')) # 幻灯片展示
class BaseModel(models.Model): class BaseModel(models.Model):
#ymq定义模型基类封装公共字段和方法抽象类 id = models.AutoField(primary_key=True)
id = models.AutoField(primary_key=True) # 自增主键 creation_time = models.DateTimeField(_('creation time'), default=now)
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 last_modify_time = models.DateTimeField(_('modify time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
#ymq重写保存方法处理slug生成和特殊更新逻辑
# 判断是否是更新文章浏览量的操作
is_update_views = isinstance( is_update_views = isinstance(
self, self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views: if is_update_views:
# 单独处理浏览量更新,提高性能
Article.objects.filter(pk=self.pk).update(views=self.views) Article.objects.filter(pk=self.pk).update(views=self.views)
else: else:
# 自动生成slug用于URL友好化
if 'slug' in self.__dict__: if 'slug' in self.__dict__:
# 根据title或name字段生成slug
slug = getattr( slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr( self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name') self, 'name')
=======
class BaseModel(models.Model): #lxy 模型基类(公共字段)
id = models.AutoField(primary_key=True) #lxy 主键ID
creation_time = models.DateTimeField(_('creation time'), default=now) #lxy 创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) #lxy 修改时间
def save(self, *args, **kwargs): #lxy 重写保存方法
# 判断是否是更新文章阅读量
is_update_views = isinstance(self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
Article.objects.filter(pk=self.pk).update(views=self.views) #lxy 单独更新阅读量
else:
if 'slug' in self.__dict__: #lxy 若有slug字段自动生成
slug = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name')
>>>>>>> LXY_branch
setattr(self, 'slug', slugify(slug)) setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs) #lxy 调用父类保存 super().save(*args, **kwargs)
<<<<<<< HEAD
def get_full_url(self): def get_full_url(self):
#ymq生成包含域名的完整URL
=======
def get_full_url(self): #lxy 获取完整URL
>>>>>>> LXY_branch
site = get_current_site().domain site = get_current_site().domain
url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url return url
class Meta: class Meta:
<<<<<<< HEAD abstract = True
abstract = True # 声明为抽象模型,不生成数据库表
@abstractmethod @abstractmethod
def get_absolute_url(self): def get_absolute_url(self):
#ymq抽象方法子类必须实现用于生成模型实例的URL
pass pass
class Article(BaseModel): class Article(BaseModel):
"""文章模型""" """文章"""
# 状态选项:草稿/已发布
=======
abstract = True #lxy 抽象基类(不生成表)
@abstractmethod
def get_absolute_url(self): #lxy 抽象方法获取对象URL
pass
class Article(BaseModel): #lxy 文章模型
# 文章状态枚举
>>>>>>> LXY_branch
STATUS_CHOICES = ( STATUS_CHOICES = (
('d', _('Draft')), #lxy 草稿 ('d', _('Draft')),
('p', _('Published')), #lxy 已发布 ('p', _('Published')),
) )
<<<<<<< HEAD
# 评论状态选项:开启/关闭
=======
# 评论状态枚举
>>>>>>> LXY_branch
COMMENT_STATUS = ( COMMENT_STATUS = (
('o', _('open')), #lxy 开放评论 ('o', _('Open')),
('c', _('close')), #lxy 关闭评论 ('c', _('Close')),
) )
<<<<<<< HEAD
# 类型选项:文章/页面
=======
# 文章类型枚举
>>>>>>> LXY_branch
TYPE = ( TYPE = (
('a', _('Article')), #lxy 文章 ('a', _('Article')),
('p', _('Page')), #lxy 页面 ('p', _('Page')),
) )
<<<<<<< HEAD title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题
body = MDTextField(_('body')) # 文章内容使用markdown编辑器
pub_time = models.DateTimeField( pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now) # 发布时间 _('publish time'), blank=False, null=False, default=now)
status = models.CharField( status = models.CharField(
_('status'), _('status'),
max_length=1, max_length=1,
choices=STATUS_CHOICES, choices=STATUS_CHOICES,
default='p') # 发布状态 default='p')
comment_status = models.CharField( comment_status = models.CharField(
_('comment status'), _('comment status'),
max_length=1, max_length=1,
choices=COMMENT_STATUS, choices=COMMENT_STATUS,
default='o') # 评论状态 default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 内容类型 type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0) # 浏览量 views = models.PositiveIntegerField(_('views'), default=0)
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
blank=False, blank=False,
null=False, null=False,
on_delete=models.CASCADE) # 关联作者(外键) on_delete=models.CASCADE)
article_order = models.IntegerField( article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0) # 排序序号 _('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录 show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
category = models.ForeignKey( category = models.ForeignKey(
'Category', 'Category',
verbose_name=_('category'), verbose_name=_('category'),
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, blank=False,
null=False) # 关联分类(外键) null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 关联标签(多对多) tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
def body_to_string(self): def body_to_string(self):
#ymq返回文章内容字符串
return self.body return self.body
def __str__(self): def __str__(self):
#ymq模型实例的字符串表示文章标题
return self.title
class Meta:
ordering = ['-article_order', '-pub_time'] # 默认排序:先按排序号降序,再按发布时间降序
=======
title = models.CharField(_('title'), max_length=200, unique=True) #lxy 标题
body = MDTextField(_('body')) #lxy 正文Markdown
pub_time = models.DateTimeField(_('publish time'), blank=False, null=False, default=now) #lxy 发布时间
status = models.CharField(_('status'), max_length=1, choices=STATUS_CHOICES, default='p') #lxy 文章状态
comment_status = models.CharField(_('comment status'), max_length=1, choices=COMMENT_STATUS, default='o') #lxy 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') #lxy 文章类型
views = models.PositiveIntegerField(_('views'), default=0) #lxy 阅读量
author = models.ForeignKey( #lxy 关联作者
settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=False, null=False, on_delete=models.CASCADE
)
article_order = models.IntegerField(_('order'), blank=False, null=False, default=0) #lxy 排序序号
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) #lxy 是否显示目录
category = models.ForeignKey( #lxy 关联分类
'Category', verbose_name=_('category'), on_delete=models.CASCADE, blank=False, null=False
)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) #lxy 关联标签(多对多)
def body_to_string(self): #lxy 正文转字符串
return self.body
def __str__(self): #lxy 实例字符串表示
return self.title return self.title
class Meta: class Meta:
ordering = ['-article_order', '-pub_time'] #lxy 默认排序(倒序) ordering = ['-article_order', '-pub_time']
>>>>>>> LXY_branch
verbose_name = _('article') verbose_name = _('article')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
get_latest_by = 'id' # 按id获取最新记录 get_latest_by = 'id'
<<<<<<< HEAD
def get_absolute_url(self): def get_absolute_url(self):
#ymq生成文章详情页的URL
return reverse('blog:detailbyid', kwargs={ return reverse('blog:detailbyid', kwargs={
=======
def get_absolute_url(self): #lxy 文章详情页URL
return reverse('blog:detail', kwargs={
>>>>>>> LXY_branch
'article_id': self.id, 'article_id': self.id,
'year': self.creation_time.year, 'year': self.creation_time.year,
'month': self.creation_time.month, 'month': self.creation_time.month,
'day': self.creation_time.day 'day': self.creation_time.day
}) })
<<<<<<< HEAD @cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self): def get_category_tree(self):
#ymq获取当前文章所属分类的层级结构含父级分类
=======
@cache_decorator(60 * 60 * 10) #lxy 缓存10小时
def get_category_tree(self): #lxy 获取分类层级
>>>>>>> LXY_branch
tree = self.category.get_category_tree() tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names return names
<<<<<<< HEAD
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
#ymq重写保存方法可扩展自定义逻辑
super().save(*args, **kwargs) super().save(*args, **kwargs)
def viewed(self): def viewed(self):
#ymq增加浏览量并保存
self.views += 1 self.views += 1
self.save(update_fields=['views']) # 只更新views字段提高性能 self.save(update_fields=['views'])
def comment_list(self): def comment_list(self):
#ymq获取文章的评论列表带缓存
=======
def save(self, *args, **kwargs): #lxy 重写保存
super().save(*args, **kwargs)
def viewed(self): #lxy 阅读量+1
self.views += 1
self.save(update_fields=['views']) #lxy 仅更新阅读量
def comment_list(self): #lxy 获取文章评论
>>>>>>> LXY_branch
cache_key = 'article_comments_{id}'.format(id=self.id) cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key) value = cache.get(cache_key)
if value: if value:
logger.info('get article comments:{id}'.format(id=self.id)) logger.info('get article comments:{id}'.format(id=self.id))
return value return value
else: else:
<<<<<<< HEAD
comments = self.comment_set.filter(is_enable=True).order_by('-id') comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100) # 缓存100分钟 cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id)) logger.info('set article comments:{id}'.format(id=self.id))
return comments return comments
def get_admin_url(self): def get_admin_url(self):
#ymq生成文章在admin后台的编辑URL
info = (self._meta.app_label, self._meta.model_name) info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,)) return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100) # 缓存100分钟 @cache_decorator(expiration=60 * 100)
def next_article(self): def next_article(self):
#ymq获取下一篇文章ID更大的已发布文章 # 下一篇
return Article.objects.filter( return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first() id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100) # 缓存100分钟 @cache_decorator(expiration=60 * 100)
def prev_article(self): def prev_article(self):
#ymq获取上一篇文章ID更小的已发布文章 # 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first() return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self): def get_first_image_url(self):
"""从文章内容中提取第一张图片的URL""" """
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) # 匹配markdown图片语法 Get the first image url from article.body.
======= :return:
comments = self.comment_set.filter(is_enable=True).order_by('-id') #lxy 过滤启用的评论 """
cache.set(cache_key, comments, 60 * 100) #lxy 缓存评论 match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self): #lxy 后台管理URL
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100) #lxy 缓存
def next_article(self): #lxy 获取下一篇文章
return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100) #lxy 缓存
def prev_article(self): #lxy 获取上一篇文章
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self): #lxy 获取正文第一张图URL
match = re.search(pattern=r'!\[.*?]\((.*?)\)', self.body)
>>>>>>> LXY_branch
if match: if match:
return match.group(1) return match.group(1)
return "" return ""
class Category(BaseModel): # lxy 分类模型
name = models.CharField(_('category name'), max_length=30, unique=True) # lxy 分类名称
parent_category = models.ForeignKey( # lxy 父分类(自关联)
'self', verbose_name=_('parent category'), blank=True, null=True, on_delete=models.CASCADE
)
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # lxy 别名
index = models.IntegerField(default=0, verbose_name=_('index')) # lxy 排序索引
<<<<<<< HEAD
class Category(BaseModel): class Category(BaseModel):
"""文章分类模型""" """文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称 name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey( parent_category = models.ForeignKey(
'self', 'self',
verbose_name=_('parent category'), verbose_name=_('parent category'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) # 父分类(自关联,支持多级分类) on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好化标识 slug = models.SlugField(default='no-slug', max_length=60, blank=True)
index = models.IntegerField(default=0, verbose_name=_('index')) # 排序索引 index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta: class Meta:
ordering = ['-index'] # 按索引降序排列 ordering = ['-index']
verbose_name = _('category') verbose_name = _('category')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
def get_absolute_url(self): def get_absolute_url(self):
#ymq生成分类详情页的URL
return reverse( return reverse(
'blog:category_detail', kwargs={ 'blog:category_detail', kwargs={
'category_name': self.slug}) 'category_name': self.slug})
def __str__(self): def __str__(self):
#ymq模型实例的字符串表示分类名称
return self.name return self.name
@cache_decorator(60 * 60 * 10) # 缓存10小时 @cache_decorator(60 * 60 * 10)
def get_category_tree(self): def get_category_tree(self):
"""递归获取当前分类的所有父级分类,形成层级结构""" """
递归获得分类目录的父级
:return:
"""
categorys = [] categorys = []
def parse(category): def parse(category):
@ -362,9 +218,12 @@ class Category(BaseModel):
parse(self) parse(self)
return categorys return categorys
@cache_decorator(60 * 60 * 10) # 缓存10小时 @cache_decorator(60 * 60 * 10)
def get_sub_categorys(self): def get_sub_categorys(self):
"""获取当前分类的所有子分类(含多级子分类)""" """
获得当前分类目录所有子集
:return:
"""
categorys = [] categorys = []
all_categorys = Category.objects.all() all_categorys = Category.objects.all()
@ -382,296 +241,136 @@ class Category(BaseModel):
class Tag(BaseModel): class Tag(BaseModel):
"""文章标签模型""" """文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称 name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好化标识 slug = models.SlugField(default='no-slug', max_length=60, blank=True)
def __str__(self): def __str__(self):
#ymq模型实例的字符串表示标签名称
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
#ymq生成标签详情页的URL
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10) # 缓存10小时 @cache_decorator(60 * 60 * 10)
def get_article_count(self): def get_article_count(self):
#ymq获取该标签关联的文章数量
return Article.objects.filter(tags__name=self.name).distinct().count() return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta: class Meta:
ordering = ['name'] # 按名称排序 ordering = ['name']
verbose_name = _('tag') verbose_name = _('tag')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
class Links(models.Model): class Links(models.Model):
"""友情链接模型""" """友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称
link = models.URLField(_('link')) # 链接URL name = models.CharField(_('link name'), max_length=30, unique=True)
sequence = models.IntegerField(_('order'), unique=True) # 排序序号 link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField( is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False) # 是否显示 _('is show'), default=True, blank=False, null=False)
show_type = models.CharField( show_type = models.CharField(
_('show type'), _('show type'),
max_length=1, max_length=1,
choices=LinkShowType.choices, choices=LinkShowType.choices,
default=LinkShowType.I) # 展示位置 default=LinkShowType.I)
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta: class Meta:
ordering = ['sequence'] # 按排序序号排列 ordering = ['sequence']
verbose_name = _('link') verbose_name = _('link')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
def __str__(self): def __str__(self):
#ymq模型实例的字符串表示链接名称
return self.name return self.name
class SideBar(models.Model): class SideBar(models.Model):
"""侧边栏模型可展示自定义HTML内容""" """侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100) # 侧边栏标题 name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content')) # 侧边栏内容HTML content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True) # 排序序号 sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用 is_enable = models.BooleanField(_('is enable'), default=True)
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta: class Meta:
ordering = ['sequence'] # 按排序序号排列 ordering = ['sequence']
verbose_name = _('sidebar') verbose_name = _('sidebar')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
def __str__(self): def __str__(self):
#ymq模型实例的字符串表示侧边栏标题
=======
class Meta:
ordering = ['-index'] # lxy 按索引倒序
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self): # lxy 分类详情页URL
return reverse('blog:category_detail', kwargs={'category_name': self.slug})
def __str__(self): # lxy 实例字符串表示
return self.name
def get_category_tree(self): # lxy 获取分类层级链
categories = []
def parse(category):
categories.append(category)
if category.parent_category:
parse(category.parent_category)
parse(self)
return categories
@cache_decorator(60 * 60 * 10) # lxy 缓存
def get_sub_categories(self): # lxy 获取所有子分类
categories = []
all_categories = Category.objects.all()
def parse(category):
if category not in categories:
categories.append(category)
children = all_categories.filter(parent_category=category)
for child in children:
if category not in categories:
categories.append(child)
parse(child)
parse(self)
return categories
class Tag(BaseModel): #lxy 标签模型
name = models.CharField(_('tag name'), max_length=30, unique=True) #lxy 标签名称
slug = models.SlugField(default='no-slug', max_length=60, blank=True) #lxy 别名
def __str__(self): #lxy 实例字符串表示
return self.name return self.name
def get_absolute_url(self): #lxy 标签详情页URL
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10) #lxy 缓存
def get_article_count(self): #lxy 获取标签关联文章数
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name'] #lxy 按名称排序
verbose_name = _('tag')
verbose_name_plural = verbose_name
class Links(models.Model): #lxy 友链模型
name = models.CharField(_('link name'), max_length=30, unique=True) #lxy 友链名称
link = models.URLField(_('link')) #lxy 友链地址
sequence = models.IntegerField(_('order'), unique=True) #lxy 排序序号
is_enable = models.BooleanField(_('is show'), default=True, blank=False, null=False) #lxy 是否启用
show_type = models.CharField( #lxy 展示类型
_('show type'), max_length=1, choices=LinkShowType.choices, default=LinkShowType.I
)
creation_time = models.DateTimeField(_('creation time'), default=now) #lxy 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) #lxy 修改时间
class Meta:
ordering = ['sequence'] #lxy 按序号排序
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self): #lxy 实例字符串表示
return self.name
class SideBar(models.Model): #lxy 侧边栏模型
name = models.CharField(_('title'), max_length=100) #lxy 侧边栏标题
content = models.TextField(_('content')) #lxy 侧边栏内容HTML
sequence = models.IntegerField(_('order'), unique=True) #lxy 排序序号
is_enable = models.BooleanField(_('is enable'), default=True) #lxy 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) #lxy 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) #lxy 修改时间
class Meta:
ordering = ['sequence'] #lxy 按序号排序
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self): #lxy 实例字符串表示
>>>>>>> LXY_branch
return self.name
class BlogSettings(models.Model): # lxy 博客配置模型
site_name = models.CharField(_('site name'), blank=False, default='') # lxy 站点名称
site_description = models.TextField( # lxy 站点描述
_('site description'), max_length=1000, null=False, blank=False, default=''
)
site_seo_description = models.TextField( # lxy 站点SEO描述
_('site seo description'), max_length=1000, null=False, blank=False, default=''
)
site_keywords = models.TextField( # lxy 站点关键词
_('site keywords'), max_length=1000, null=False, blank=False, default=''
)
article_sub_length = models.IntegerField(_('article sub length'), default=300) # lxy 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # lxy 侧边栏文章数
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # lxy 侧边栏评论数
show_google_adsense = models.BooleanField(_('show adsense'), default=False) # lxy 是否显示谷歌广告
google_adsense_codes = models.TextField( # lxy 谷歌广告代码
_('adsense code'), max_length=2000, null=True, blank=True, default=''
)
open_site_comment = models.BooleanField(_('open site comment'), default=True) # lxy 是否开放站点评论
global_header = models.TextField("公共头部", null=True, blank=True, default='') # lxy 公共头部HTML
global_footer = models.TextField("公共底部", null=True, blank=True, default='') # lxy 公共底部HTML
beian_code = models.CharField( # lxy 备案号
'备案号', max_length=2000, null=True, blank=True, default=''
)
<<<<<<< HEAD
class BlogSettings(models.Model): class BlogSettings(models.Model):
"""博客全局配置模型""" """blog的配置"""
site_name = models.CharField( site_name = models.CharField(
_('site name'), _('site name'),
max_length=200, max_length=200,
null=False, null=False,
blank=False, blank=False,
default='') # 网站名称 default='')
site_description = models.TextField( site_description = models.TextField(
_('site description'), _('site description'),
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') # 网站描述 default='')
site_seo_description = models.TextField( site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='') # SEO描述 _('site seo description'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField( site_keywords = models.TextField(
_('site keywords'), _('site keywords'),
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') # 网站关键词 default='')
article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度 article_sub_length = models.IntegerField(_('article sub length'), default=300)
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量 sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量 sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页评论数量 article_comment_count = models.IntegerField(_('article comment count'), default=5)
show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示谷歌广告 show_google_adsense = models.BooleanField(_('show adsense'), default=False)
google_adsense_codes = models.TextField( google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='') # 谷歌广告代码 _('adsense code'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启网站评论 open_site_comment = models.BooleanField(_('open site comment'), default=True)
global_header = models.TextField("公共头部", null=True, blank=True, default='') # 全局头部代码 global_header = models.TextField("公共头部", null=True, blank=True, default='')
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 全局尾部代码 global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
beian_code = models.CharField( beian_code = models.CharField(
'备案号', '备案号',
max_length=2000, max_length=2000,
null=True, null=True,
blank=True, blank=True,
default='') # 网站备案号 default='')
analytics_code = models.TextField( analytics_code = models.TextField(
=======
analytics_code = models.TextField( # lxy 网站统计代码
>>>>>>> LXY_branch
"网站统计代码", "网站统计代码",
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
<<<<<<< HEAD default='')
default='') # 统计分析代码
show_gongan_code = models.BooleanField( show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False) # 是否显示公安备案号 '是否显示公安备案号', default=False, null=False)
gongan_beiancode = models.TextField( gongan_beiancode = models.TextField(
'公安备案号', '公安备案号',
max_length=2000, max_length=2000,
null=True, null=True,
blank=True, blank=True,
default='') # 公安备案号 default='')
comment_need_review = models.BooleanField( comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False) # 评论是否需要审核 '评论是否需要审核', default=False, null=False)
=======
default=""
)
show_gongan_code = models.BooleanField( # lxy 是否显示公安备案号
"是否显示公安备案号", default=False, null=False
)
gongan_beiancode = models.TextField( # lxy 公安备案号
"公安备案号",
max_length=2000,
null=True,
blank=True,
default=""
)
comment_need_review = models.BooleanField( # lxy 评论是否需要审核
"评论是否需要审核", default=False, null=False
)
>>>>>>> LXY_branch
class Meta: # lxy 模型元信息 class Meta:
verbose_name = _('Website configuration') # lxy 单数名称 verbose_name = _('Website configuration')
verbose_name_plural = verbose_name # lxy 复数名称 verbose_name_plural = verbose_name
<<<<<<< HEAD
def __str__(self): def __str__(self):
#ymq模型实例的字符串表示网站名称
return self.site_name return self.site_name
def clean(self): def clean(self):
#ymq数据验证确保全局配置只能有一条记录
=======
def __str__(self): # lxy 实例字符串表示
return self.site_name
def clean(self): # lxy 数据校验(确保仅存在一个配置)
>>>>>>> LXY_branch
if BlogSettings.objects.exclude(id=self.id).count(): if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration')) # lxy 抛出唯一配置异常 raise ValidationError(_('There can only be one configuration'))
<<<<<<< HEAD
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
#ymq保存配置后清除缓存确保配置立即生效
super().save(*args, **kwargs) super().save(*args, **kwargs)
from djangoblog.utils import cache from djangoblog.utils import cache
cache.clear() cache.clear()
=======
def save(self, *args, **kwargs): # lxy 重写保存方法
super().save(*args, **kwargs) # lxy 调用父类保存
from djangoblog.utils import cache
cache.clear() # lxy 保存后清空缓存
>>>>>>> LXY_branch

@ -1,33 +1,13 @@
from haystack import indexes from haystack import indexes
#ymq导入Haystack的indexes模块用于定义搜索索引
from blog.models import Article from blog.models import Article
#ymq从blog应用导入Article模型为其创建搜索索引
<<<<<<< HEAD
class ArticleIndex(indexes.SearchIndex, indexes.Indexable): class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
#ymq定义文章搜索索引类继承自SearchIndex和Indexable
#ymq: document=True表示该字段是主要搜索字段use_template=True表示使用模板定义字段内容
text = indexes.CharField(document=True, use_template=True) text = indexes.CharField(document=True, use_template=True)
def get_model(self): def get_model(self):
#ymq指定该索引对应的模型
return Article return Article
def index_queryset(self, using=None): def index_queryset(self, using=None):
#ymq定义需要被索引的数据集
#ymq: 只索引状态为'p'(已发布)的文章
return self.get_model().objects.filter(status='p') return self.get_model().objects.filter(status='p')
=======
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):#lxy 文章搜索索引类
text = indexes.CharField(document=True, use_template=True)#lxy 搜索字段(关联模板)
def get_model(self):#lxy 指定关联的模型
return Article#lxy 关联Article模型
def index_queryset(self, using=None):#lxy 指定要索引的数据集
# 仅索引状态为“已发布p”的文章
return self.get_model().objects.filter(status='p')#lxy 过滤已发布文章
>>>>>>> LXY_branch

@ -23,18 +23,15 @@ from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
register = template.Library() register = template.Library()
#ymq注册模板标签库用于在Django模板中使用自定义标签和过滤器
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def head_meta(context): def head_meta(context):
#ymq自定义简单标签用于生成页面头部元信息通过插件钩子处理
return mark_safe(hooks.apply_filters('head_meta', '', context)) return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag @register.simple_tag
def timeformat(data): def timeformat(data):
#ymq格式化时间仅时间部分使用settings中定义的TIME_FORMAT
try: try:
return data.strftime(settings.TIME_FORMAT) return data.strftime(settings.TIME_FORMAT)
except Exception as e: except Exception as e:
@ -44,7 +41,6 @@ def timeformat(data):
@register.simple_tag @register.simple_tag
def datetimeformat(data): def datetimeformat(data):
#ymq格式化日期时间使用settings中定义的DATE_TIME_FORMAT
try: try:
return data.strftime(settings.DATE_TIME_FORMAT) return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e: except Exception as e:
@ -55,13 +51,11 @@ def datetimeformat(data):
@register.filter() @register.filter()
@stringfilter @stringfilter
def custom_markdown(content): def custom_markdown(content):
#ymq将内容转换为Markdown格式并标记为安全HTML用于文章内容
return mark_safe(CommonMarkdown.get_markdown(content)) return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag @register.simple_tag
def get_markdown_toc(content): def get_markdown_toc(content):
#ymq获取Markdown内容的目录TOC并标记为安全HTML
from djangoblog.utils import CommonMarkdown from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content) body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc) return mark_safe(toc)
@ -70,7 +64,6 @@ def get_markdown_toc(content):
@register.filter() @register.filter()
@stringfilter @stringfilter
def comment_markdown(content): def comment_markdown(content):
#ymq处理评论内容的Markdown转换并过滤不安全HTML标签
content = CommonMarkdown.get_markdown(content) content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content)) return mark_safe(sanitize_html(content))
@ -83,7 +76,6 @@ def truncatechars_content(content):
:param content: :param content:
:return: :return:
""" """
#ymq按网站设置的长度截断文章内容保留HTML标签
from django.template.defaultfilters import truncatechars_html from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting() blogsetting = get_blog_setting()
@ -93,8 +85,8 @@ def truncatechars_content(content):
@register.filter(is_safe=True) @register.filter(is_safe=True)
@stringfilter @stringfilter
def truncate(content): def truncate(content):
#ymq截断内容为150字符并去除HTML标签用于生成纯文本摘要
from django.utils.html import strip_tags from django.utils.html import strip_tags
return strip_tags(content)[:150] return strip_tags(content)[:150]
@ -105,13 +97,12 @@ def load_breadcrumb(article):
:param article: :param article:
:return: :return:
""" """
#ymq生成文章面包屑导航数据包含分类层级和网站名称
names = article.get_category_tree() names = article.get_category_tree()
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting() blogsetting = get_blog_setting()
site = get_current_site().domain site = get_current_site().domain
names.append((blogsetting.site_name, '/')) names.append((blogsetting.site_name, '/'))
names = names[::-1] # 反转列表,使层级从网站到当前分类 names = names[::-1]
return { return {
'names': names, 'names': names,
@ -127,7 +118,6 @@ def load_articletags(article):
:param article: :param article:
:return: :return:
""" """
#ymq获取文章关联的标签列表包含标签URL、文章数和随机样式
tags = article.tags.all() tags = article.tags.all()
tags_list = [] tags_list = []
for tag in tags: for tag in tags:
@ -147,7 +137,6 @@ def load_sidebar(user, linktype):
加载侧边栏 加载侧边栏
:return: :return:
""" """
#ymq加载侧边栏数据带缓存包含文章列表、分类、标签等
value = cache.get("sidebar" + linktype) value = cache.get("sidebar" + linktype)
if value: if value:
value['user'] = user value['user'] = user
@ -156,7 +145,6 @@ def load_sidebar(user, linktype):
logger.info('load sidebar') logger.info('load sidebar')
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting() blogsetting = get_blog_setting()
# 获取最近文章、分类、热门文章等数据
recent_articles = Article.objects.filter( recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count] status='p')[:blogsetting.sidebar_article_count]
sidebar_categorys = Category.objects.all() sidebar_categorys = Category.objects.all()
@ -169,8 +157,8 @@ def load_sidebar(user, linktype):
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A)) Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
commment_list = Comment.objects.filter(is_enable=True).order_by( commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count] '-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 处理标签云(按文章数计算字体大小) # 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 5 increment = 5
tags = Tag.objects.all() tags = Tag.objects.all()
sidebar_tags = None sidebar_tags = None
@ -178,6 +166,7 @@ def load_sidebar(user, linktype):
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]] s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
count = sum([t[1] for t in s]) count = sum([t[1] for t in s])
dd = 1 if (count == 0 or not len(tags)) else count / len(tags) dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
import random
sidebar_tags = list( sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
random.shuffle(sidebar_tags) random.shuffle(sidebar_tags)
@ -196,7 +185,6 @@ def load_sidebar(user, linktype):
'sidebar_tags': sidebar_tags, 'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars 'extra_sidebars': extra_sidebars
} }
# 缓存侧边栏数据3小时
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3) cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype)) logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user value['user'] = user
@ -210,7 +198,6 @@ def load_article_metas(article, user):
:param article: :param article:
:return: :return:
""" """
#ymq加载文章元信息作者、发布时间等供模板使用
return { return {
'article': article, 'article': article,
'user': user 'user': user
@ -219,11 +206,9 @@ def load_article_metas(article, user):
@register.inclusion_tag('blog/tags/article_pagination.html') @register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name): def load_pagination_info(page_obj, page_type, tag_name):
#ymq生成分页导航链接支持首页、标签、作者、分类等不同页面类型
previous_url = '' previous_url = ''
next_url = '' next_url = ''
if page_type == '': if page_type == '':
# 首页分页
if page_obj.has_next(): if page_obj.has_next():
next_number = page_obj.next_page_number() next_number = page_obj.next_page_number()
next_url = reverse('blog:index_page', kwargs={'page': next_number}) next_url = reverse('blog:index_page', kwargs={'page': next_number})
@ -233,7 +218,6 @@ def load_pagination_info(page_obj, page_type, tag_name):
'blog:index_page', kwargs={ 'blog:index_page', kwargs={
'page': previous_number}) 'page': previous_number})
if page_type == '分类标签归档': if page_type == '分类标签归档':
# 标签页分页
tag = get_object_or_404(Tag, name=tag_name) tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next(): if page_obj.has_next():
next_number = page_obj.next_page_number() next_number = page_obj.next_page_number()
@ -250,7 +234,6 @@ def load_pagination_info(page_obj, page_type, tag_name):
'page': previous_number, 'page': previous_number,
'tag_name': tag.slug}) 'tag_name': tag.slug})
if page_type == '作者文章归档': if page_type == '作者文章归档':
# 作者页分页
if page_obj.has_next(): if page_obj.has_next():
next_number = page_obj.next_page_number() next_number = page_obj.next_page_number()
next_url = reverse( next_url = reverse(
@ -267,7 +250,6 @@ def load_pagination_info(page_obj, page_type, tag_name):
'author_name': tag_name}) 'author_name': tag_name})
if page_type == '分类目录归档': if page_type == '分类目录归档':
# 分类页分页
category = get_object_or_404(Category, name=tag_name) category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next(): if page_obj.has_next():
next_number = page_obj.next_page_number() next_number = page_obj.next_page_number()
@ -299,7 +281,6 @@ def load_article_detail(article, isindex, user):
:param isindex:是否列表页若是列表页只显示摘要 :param isindex:是否列表页若是列表页只显示摘要
:return: :return:
""" """
#ymq加载文章详情数据区分列表页显示摘要和详情页显示全文
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting() blogsetting = get_blog_setting()
@ -311,35 +292,35 @@ def load_article_detail(article, isindex, user):
} }
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter @register.filter
def gravatar_url(email, size=40): def gravatar_url(email, size=40):
"""获得gravatar头像URL""" """获得gravatar头像"""
#ymq获取用户头像URL优先使用第三方登录头像否则使用Gravatar
cachekey = 'gravatat/' + email cachekey = 'gravatat/' + email
url = cache.get(cachekey) url = cache.get(cachekey)
if url: if url:
return url return url
else: else:
# 检查是否有第三方登录用户的头像
usermodels = OAuthUser.objects.filter(email=email) usermodels = OAuthUser.objects.filter(email=email)
if usermodels: if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels)) o = list(filter(lambda x: x.picture is not None, usermodels))
if o: if o:
return o[0].picture return o[0].picture
# 生成Gravatar头像URL
email = email.encode('utf-8') email = email.encode('utf-8')
default = static('blog/img/avatar.png') # 默认头像
default = static('blog/img/avatar.png')
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5( url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)})) email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
cache.set(cachekey, url, 60 * 60 * 10) # 缓存头像URL 10小时 cache.set(cachekey, url, 60 * 60 * 10)
logger.info('set gravatar cache.key:{key}'.format(key=cachekey)) logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url return url
@register.filter @register.filter
def gravatar(email, size=40): def gravatar(email, size=40):
"""获得gravatar头像img标签""" """获得gravatar头像"""
#ymq生成头像img标签调用gravatar_url获取URL
url = gravatar_url(email, size) url = gravatar_url(email, size)
return mark_safe( return mark_safe(
'<img src="%s" height="%d" width="%d">' % '<img src="%s" height="%d" width="%d">' %
@ -354,12 +335,10 @@ def query(qs, **kwargs):
... ...
{% endfor %} {% endfor %}
""" """
#ymq模板中过滤查询集的标签支持动态传参过滤 return qs.filter(**kwargs)
return qs.filter(** kwargs)
@register.filter @register.filter
def addstr(arg1, arg2): def addstr(arg1, arg2):
"""concatenate arg1 & arg2""" """concatenate arg1 & arg2"""
#ymq字符串拼接过滤器将两个参数转换为字符串并拼接 return str(arg1) + str(arg2)
return str(arg1) + str(arg2)

@ -1,132 +1,73 @@
import os import os
#ymq导入os模块用于文件路径和文件操作
from django.conf import settings from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
#ymq导入文件上传相关类用于模拟文件上传测试
from django.core.management import call_command from django.core.management import call_command
#ymq导入call_command用于调用Django管理命令
from django.core.paginator import Paginator from django.core.paginator import Paginator
#ymq导入分页类用于测试分页功能
from django.templatetags.static import static from django.templatetags.static import static
#ymq导入static标签用于获取静态文件路径
from django.test import Client, RequestFactory, TestCase from django.test import Client, RequestFactory, TestCase
#ymq导入测试相关类Client用于模拟HTTP请求TestCase提供测试框架
from django.urls import reverse from django.urls import reverse
#ymq导入reverse用于反向解析URL
from django.utils import timezone from django.utils import timezone
#ymq导入timezone用于处理时间相关测试数据
from accounts.models import BlogUser from accounts.models import BlogUser
#ymq从accounts应用导入用户模型
from blog.forms import BlogSearchForm from blog.forms import BlogSearchForm
#ymq从blog应用导入搜索表单
from blog.models import Article, Category, Tag, SideBar, Links from blog.models import Article, Category, Tag, SideBar, Links
#ymq从blog应用导入模型类用于测试数据创建和查询
from blog.templatetags.blog_tags import load_pagination_info, load_articletags from blog.templatetags.blog_tags import load_pagination_info, load_articletags
#ymq导入自定义模板标签函数用于测试模板标签功能
from djangoblog.utils import get_current_site, get_sha256 from djangoblog.utils import get_current_site, get_sha256
#ymq导入工具函数用于获取站点信息和加密
from oauth.models import OAuthUser, OAuthConfig from oauth.models import OAuthUser, OAuthConfig
#ymq从oauth应用导入模型用于测试第三方登录相关功能
# Create your tests here. # Create your tests here.
<<<<<<< HEAD
class ArticleTest(TestCase): class ArticleTest(TestCase):
#ymq定义文章相关的测试类继承自TestCase
def setUp(self): def setUp(self):
#ymq测试前置方法在每个测试方法执行前运行初始化测试客户端和工厂 self.client = Client()
self.client = Client() # 创建测试客户端用于模拟HTTP请求 self.factory = RequestFactory()
self.factory = RequestFactory() # 创建请求工厂,用于构造请求对象
def test_validate_article(self): def test_validate_article(self):
#ymq测试文章相关功能的完整性包括创建、查询、页面访问等 site = get_current_site().domain
site = get_current_site().domain # 获取当前站点域名
# 创建或获取测试用户
user = BlogUser.objects.get_or_create( user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com", email="liangliangyy@gmail.com",
username="liangliangyy")[0] username="liangliangyy")[0]
user.set_password("liangliangyy") # 设置用户密码
user.is_staff = True # 设为 staff允许访问admin
user.is_superuser = True # 设为超级用户
user.save() # 保存用户
# 测试用户个人页面访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200) # 断言页面正常响应
# 测试admin相关页面访问未登录状态
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
# 创建侧边栏测试数据
=======
class ArticleTest(TestCase): # lxy 文章相关测试类
def setUp(self): # lxy 测试初始化
self.client = Client() # lxy 测试客户端
self.factory = RequestFactory() # lxy 请求工厂
def test_validate_article(self): # lxy 文章功能验证
site = get_current_site().domain # lxy 获取站点域名
# 创建测试用户
user = BlogUser.objects.get_or_create(email="liangliangyy@gmail.com", username="liangliangyy")[0]
user.set_password("liangliangyy") user.set_password("liangliangyy")
user.is_staff = True user.is_staff = True
user.is_superuser = True user.is_superuser = True
user.save() user.save()
# 测试用户页面访问
response = self.client.get(user.get_absolute_url()) response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200) # lxy 断言状态码200 self.assertEqual(response.status_code, 200)
# 访问后台页面(无权限,仅请求) response = self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/servermanager/emailsendlog/') response = self.client.get('admin/admin/logentry/')
self.client.get('admin/admin/logentry/')
# 创建测试侧边栏
>>>>>>> LXY_branch
s = SideBar() s = SideBar()
s.sequence = 1 s.sequence = 1
s.name = 'test' s.name = 'test'
s.content = 'test content' s.content = 'test content'
s.is_enable = True s.is_enable = True
s.save() s.save()
<<<<<<< HEAD
# 创建分类测试数据
=======
# 创建测试分类
>>>>>>> LXY_branch
category = Category() category = Category()
category.name = "category" category.name = "category"
category.creation_time = timezone.now() category.creation_time = timezone.now()
category.last_mod_time = timezone.now() category.last_mod_time = timezone.now()
category.save() category.save()
# 创建标签测试数据
tag = Tag() tag = Tag()
tag.name = "nicetag" tag.name = "nicetag"
tag.save() tag.save()
# 创建文章测试数据
article = Article() article = Article()
article.title = "nicetitle" article.title = "nicetitle"
article.body = "nicecontent" article.body = "nicecontent"
article.author = user article.author = user
article.category = category article.category = category
article.type = 'a' # 类型为文章 article.type = 'a'
article.status = 'p' # 状态为已发布 article.status = 'p'
article.save() article.save()
self.assertEqual(0, article.tags.count())
# 测试文章标签关联 article.tags.add(tag)
self.assertEqual(0, article.tags.count()) # 初始无标签
article.tags.add(tag) # 添加标签
article.save() article.save()
self.assertEqual(1, article.tags.count()) # 断言标签已添加 self.assertEqual(1, article.tags.count())
# 批量创建文章(用于测试分页)
for i in range(20): for i in range(20):
article = Article() article = Article()
article.title = "nicetitle" + str(i) article.title = "nicetitle" + str(i)
@ -138,73 +79,56 @@ class ArticleTest(TestCase): # lxy 文章相关测试类
article.save() article.save()
article.tags.add(tag) article.tags.add(tag)
article.save() article.save()
# 测试Elasticsearch搜索功能如果启用
from blog.documents import ELASTICSEARCH_ENABLED from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
call_command("build_index") # 调用命令构建索引 call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'}) # 模拟搜索请求 response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200) # 断言搜索页面正常响应 self.assertEqual(response.status_code, 200)
# 测试文章详情页访问
response = self.client.get(article.get_absolute_url()) response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# 测试蜘蛛通知功能
from djangoblog.spider_notify import SpiderNotify from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url()) # 通知搜索引擎 SpiderNotify.notify(article.get_absolute_url())
# 测试标签页访问
response = self.client.get(tag.get_absolute_url()) response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# 测试分类页访问
response = self.client.get(category.get_absolute_url()) response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# 测试搜索功能
response = self.client.get('/search', {'q': 'django'}) response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# 测试文章模板标签
s = load_articletags(article) s = load_articletags(article)
self.assertIsNotNone(s) # 断言标签返回非空 self.assertIsNotNone(s)
# 用户登录
self.client.login(username='liangliangyy', password='liangliangyy') self.client.login(username='liangliangyy', password='liangliangyy')
# 测试归档页面访问
response = self.client.get(reverse('blog:archives')) response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# 测试不同类型的分页功能
p = Paginator(Article.objects.all(), settings.PAGINATE_BY) p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '') # 全部文章分页 self.check_pagination(p, '', '')
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug) # 标签文章分页 self.check_pagination(p, '分类标签归档', tag.slug)
p = Paginator( p = Paginator(
Article.objects.filter( Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY) author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy') # 作者文章分页 self.check_pagination(p, '作者文章归档', 'liangliangyy')
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug) # 分类文章分页 self.check_pagination(p, '分类目录归档', category.slug)
# 测试搜索表单
f = BlogSearchForm() f = BlogSearchForm()
f.search() # 调用搜索方法 f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
# 测试百度蜘蛛通知 from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()]) SpiderNotify.baidu_notify([article.get_full_url()])
# 测试头像相关模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com') # 获取头像URL u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com') # 生成头像HTML u = gravatar('liangliangyy@gmail.com')
# 测试友情链接页面
link = Links( link = Links(
sequence=1, sequence=1,
name="lylinux", name="lylinux",
@ -212,83 +136,57 @@ class ArticleTest(TestCase): # lxy 文章相关测试类
link.save() link.save()
response = self.client.get('/links.html') response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# 测试RSS订阅和站点地图
response = self.client.get('/feed/') response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get('/sitemap.xml') response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# 测试admin操作删除文章、访问日志
self.client.get("/admin/blog/article/1/delete/") self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/') self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/') self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/') self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value): def check_pagination(self, p, type, value):
#ymq测试分页功能的辅助方法
for page in range(1, p.num_pages + 1): for page in range(1, p.num_pages + 1):
# 调用分页模板标签获取分页信息
s = load_pagination_info(p.page(page), type, value) s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s) # 断言分页信息非空 self.assertIsNotNone(s)
# 测试上一页链接
if s['previous_url']: if s['previous_url']:
response = self.client.get(s['previous_url']) response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# 测试下一页链接
if s['next_url']: if s['next_url']:
response = self.client.get(s['next_url']) response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_image(self): def test_image(self):
#ymq测试图片上传功能
import requests import requests
# 下载测试图片
rsp = requests.get( rsp = requests.get(
'https://www.python.org/static/img/python-logo.png') 'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png') # 保存路径 imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file: with open(imagepath, 'wb') as file:
file.write(rsp.content) # 保存图片 file.write(rsp.content)
# 测试未授权上传
rsp = self.client.post('/upload') rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403) # 断言被拒绝 self.assertEqual(rsp.status_code, 403)
# 生成上传签名(模拟授权)
sign = get_sha256(get_sha256(settings.SECRET_KEY)) sign = get_sha256(get_sha256(settings.SECRET_KEY))
# 模拟带签名的上传请求
with open(imagepath, 'rb') as file: with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile( imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg') 'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile} form_data = {'python.png': imgfile}
rsp = self.client.post( rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True) '/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200) # 断言上传成功 self.assertEqual(rsp.status_code, 200)
os.remove(imagepath)
os.remove(imagepath) # 清理测试文件
# 测试用户头像保存和邮件发送工具函数
from djangoblog.utils import save_user_avatar, send_email from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent') # 测试发送邮件 send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar( save_user_avatar(
'https://www.python.org/static/img/python-logo.png') # 测试保存头像 'https://www.python.org/static/img/python-logo.png')
<<<<<<< HEAD
def test_errorpage(self): def test_errorpage(self):
#ymq测试错误页面404 rsp = self.client.get('/eee')
rsp = self.client.get('/eee') # 访问不存在的URL self.assertEqual(rsp.status_code, 404)
self.assertEqual(rsp.status_code, 404) # 断言返回404
def test_commands(self): def test_commands(self):
#ymq测试Django管理命令
# 创建测试用户
=======
def test_errorpage(self): # lxy 错误页面测试
rsp = self.client.get('/eee') # lxy 访问不存在的路径
self.assertEqual(rsp.status_code, 404) # lxy 断言404状态码
def test_commands(self):#lxy 命令行指令测试
>>>>>>> LXY_branch
user = BlogUser.objects.get_or_create( user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com", email="liangliangyy@gmail.com",
username="liangliangyy")[0] username="liangliangyy")[0]
@ -296,15 +194,13 @@ class ArticleTest(TestCase): # lxy 文章相关测试类
user.is_staff = True user.is_staff = True
user.is_superuser = True user.is_superuser = True
user.save() user.save()
# 创建OAuth配置
c = OAuthConfig() c = OAuthConfig()
c.type = 'qq' c.type = 'qq'
c.appkey = 'appkey' c.appkey = 'appkey'
c.appsecret = 'appsecret' c.appsecret = 'appsecret'
c.save() c.save()
# 创建OAuth用户关联
u = OAuthUser() u = OAuthUser()
u.type = 'qq' u.type = 'qq'
u.openid = 'openid' u.openid = 'openid'
@ -315,8 +211,7 @@ class ArticleTest(TestCase): # lxy 文章相关测试类
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}''' }'''
u.save() u.save()
# 创建另一个OAuth用户
u = OAuthUser() u = OAuthUser()
u.type = 'qq' u.type = 'qq'
u.openid = 'openid1' u.openid = 'openid1'
@ -326,26 +221,12 @@ class ArticleTest(TestCase): # lxy 文章相关测试类
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}''' }'''
u.save() u.save()
# 测试Elasticsearch索引构建命令如果启用
from blog.documents import ELASTICSEARCH_ENABLED from blog.documents import ELASTICSEARCH_ENABLED
<<<<<<< HEAD
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
call_command("build_index") call_command("build_index")
call_command("ping_baidu", "all")
# 测试其他管理命令 call_command("create_testdata")
call_command("ping_baidu", "all") # 百度ping通知 call_command("clear_cache")
call_command("create_testdata") # 创建测试数据 call_command("sync_user_avatar")
call_command("clear_cache") # 清理缓存 call_command("build_search_words")
call_command("sync_user_avatar") # 同步用户头像
call_command("build_search_words") # 构建搜索词
=======
if ELASTICSEARCH_ENABLED:
call_command("build_index") # lxy 构建ES索引
call_command("ping_baidu", "all") # lxy ping百度
call_command("create_testdata") # lxy 创建测试数据
call_command("clear_cache") # lxy 清理缓存
call_command("sync_user_avatar") # lxy 同步用户头像
call_command("build_search_words") # lxy 构建搜索词
>>>>>>> LXY_branch

@ -1,153 +1,62 @@
from django.urls import path from django.urls import path
#ymq导入Django的path函数用于定义URL路由
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
#ymq导入缓存装饰器用于对视图进行缓存
from . import views from . import views
#ymq从当前应用导入views模块引用视图函数/类
<<<<<<< HEAD
app_name = "blog" app_name = "blog"
#ymq定义应用命名空间避免URL名称冲突
=======
app_name = "blog" #lxy 应用命名空间
>>>>>>> LXY_branch
urlpatterns = [ urlpatterns = [
path( path(
r'', r'',
views.IndexView.as_view(), views.IndexView.as_view(),
<<<<<<< HEAD
name='index'), name='index'),
#ymq首页URL映射到IndexView视图类名称为'index'
path( path(
r'page/<int:page>/', r'page/<int:page>/',
views.IndexView.as_view(), views.IndexView.as_view(),
name='index_page'), name='index_page'),
#ymq分页首页URL接收整数类型的page参数名称为'index_page'
path( path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html', r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(), views.ArticleDetailView.as_view(),
name='detailbyid'), name='detailbyid'),
#ymq文章详情页URL接收年、月、日、文章ID参数名称为'detailbyid'
path( path(
r'category/<slug:category_name>.html', r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(),
name='category_detail'), name='category_detail'),
#ymq分类详情页URL接收slug类型的分类名称参数名称为'category_detail'
path( path(
r'category/<slug:category_name>/<int:page>.html', r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(),
name='category_detail_page'), name='category_detail_page'),
#ymq分类分页详情页URL接收分类名称和页码参数名称为'category_detail_page'
path( path(
r'author/<author_name>.html', r'author/<author_name>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(),
name='author_detail'), name='author_detail'),
#ymq作者详情页URL接收作者名称参数名称为'author_detail'
path( path(
r'author/<author_name>/<int:page>.html', r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(),
name='author_detail_page'), name='author_detail_page'),
#ymq作者分页详情页URL接收作者名称和页码参数名称为'author_detail_page'
path( path(
r'tag/<slug:tag_name>.html', r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(),
name='tag_detail'), name='tag_detail'),
#ymq标签详情页URL接收slug类型的标签名称参数名称为'tag_detail'
path( path(
r'tag/<slug:tag_name>/<int:page>.html', r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(),
name='tag_detail_page'), name='tag_detail_page'),
#ymq标签分页详情页URL接收标签名称和页码参数名称为'tag_detail_page'
=======
name='index'),#lxy 路由名称:首页
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),#lxy 路由名称:首页分页
# 文章详情路由(按日期+ID
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),#lxy 路由名称:文章详情
# 分类详情路由
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'), #lxy 路由名称:分类详情
# 分类详情分页路由
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),#lxy 路由名称:分类详情分页
# 作者详情路由
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),#lxy 路由名称:作者详情
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),#lxy 路由名称:作者详情分页
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),#lxy 路由名称:标签详情
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),#lxy 路由名称:标签详情分页
>>>>>>> LXY_branch
path( path(
'archives.html', 'archives.html',
cache_page( cache_page(
60 * 60)( 60 * 60)(
views.ArchivesView.as_view()), views.ArchivesView.as_view()),
<<<<<<< HEAD
name='archives'), name='archives'),
#ymq归档页面URL使用cache_page装饰器缓存1小时60*60秒名称为'archives'
path( path(
'links.html', 'links.html',
views.LinkListView.as_view(), views.LinkListView.as_view(),
name='links'), name='links'),
#ymq友情链接页面URL映射到LinkListView视图类名称为'links'
path( path(
r'upload', r'upload',
views.fileupload, views.fileupload,
name='upload'), name='upload'),
#ymq文件上传URL映射到fileupload视图函数名称为'upload'
path( path(
r'clean', r'clean',
views.clean_cache_view, views.clean_cache_view,
name='clean'), name='clean'),
#ymq清理缓存URL映射到clean_cache_view视图函数名称为'clean'
]
=======
name='archives'),#lxy 路由名称:归档页
path(
'links.html',
views.LinkListView.as_view(),
name='links'),#lxy 路由名称:友链页
path(
r'upload',
views.fileupload,
name='upload'),#lxy 路由名称:文件上传
path(
r'clean',
views.clean_cache_view,
name='clean'),#lxy 路由名称:缓存清理
] ]
>>>>>>> LXY_branch

@ -1,7 +1,6 @@
import logging import logging
import os import os
import uuid import uuid
#ymq导入日志、文件操作、UUID生成相关模块
from django.conf import settings from django.conf import settings
from django.core.paginator import Paginator from django.core.paginator import Paginator
@ -15,25 +14,17 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from haystack.views import SearchView from haystack.views import SearchView
#ymq导入Django核心组件、视图类、HTTP响应类等
from blog.models import Article, Category, LinkShowType, Links, Tag from blog.models import Article, Category, LinkShowType, Links, Tag
#ymq从blog应用导入模型类
from comments.forms import CommentForm from comments.forms import CommentForm
#ymq从comments应用导入评论表单
from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
#ymq导入插件钩子相关模块用于扩展文章功能
from djangoblog.utils import cache, get_blog_setting, get_sha256 from djangoblog.utils import cache, get_blog_setting, get_sha256
#ymq导入工具函数用于缓存、获取博客设置和加密
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
<<<<<<< HEAD
class ArticleListView(ListView): class ArticleListView(ListView):
#ymq文章列表基础视图类继承自Django的ListView
# template_name属性用于指定使用哪个模板进行渲染 # template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html' template_name = 'blog/article_index.html'
@ -42,50 +33,33 @@ class ArticleListView(ListView):
# 页面类型,分类目录或标签列表等 # 页面类型,分类目录或标签列表等
page_type = '' page_type = ''
paginate_by = settings.PAGINATE_BY # 分页大小,从配置中获取 paginate_by = settings.PAGINATE_BY
page_kwarg = 'page' # 页码参数名 page_kwarg = 'page'
link_type = LinkShowType.L # 链接展示类型 link_type = LinkShowType.L
def get_view_cache_key(self): def get_view_cache_key(self):
#ymq获取视图缓存键未实际使用预留方法
return self.request.get['pages'] return self.request.get['pages']
@property @property
def page_number(self): def page_number(self):
#ymq获取当前页码从URL参数或默认值
=======
class ArticleListView(ListView): #lxy 文章列表视图基类
template_name = 'blog/article_index.html' #lxy 渲染模板
context_object_name = 'article_list' #lxy 模板中数据变量名
page_type = '' #lxy 页面类型标识
paginate_by = settings.PAGINATE_BY #lxy 每页数量
page_kwarg = 'page' #lxy 分页参数名
link_type = LinkShowType.L #lxy 友链展示类型
def get_view_cache_key(self): #lxy 获取视图缓存键
return self.request.get['pages']
@property
def page_number(self):#lxy 获取当前页码
>>>>>>> LXY_branch
page_kwarg = self.page_kwarg page_kwarg = self.page_kwarg
page = self.kwargs.get( page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1 page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page return page
def get_queryset_cache_key(self):#lxy 子类需重写:获取查询集缓存键 def get_queryset_cache_key(self):
""" """
子类重写.获得queryset的缓存key 子类重写.获得queryset的缓存key
""" """
raise NotImplementedError() # 强制子类实现该方法 raise NotImplementedError()
def get_queryset_data(self):#lxy 子类需重写:获取查询集数据 def get_queryset_data(self):
""" """
子类重写.获取queryset的数据 子类重写.获取queryset的数据
""" """
raise NotImplementedError() # 强制子类实现该方法 raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):#lxy 从缓存取查询集 def get_queryset_from_cache(self, cache_key):
''' '''
缓存页面数据 缓存页面数据
:param cache_key: 缓存key :param cache_key: 缓存key
@ -96,79 +70,56 @@ class ArticleListView(ListView): #lxy 文章列表视图基类
logger.info('get view cache.key:{key}'.format(key=cache_key)) logger.info('get view cache.key:{key}'.format(key=cache_key))
return value return value
else: else:
<<<<<<< HEAD article_list = self.get_queryset_data()
article_list = self.get_queryset_data() # 调用子类实现的方法获取数据 cache.set(cache_key, article_list)
cache.set(cache_key, article_list) # 存入缓存
=======
article_list = self.get_queryset_data()#lxy 从数据库取数据
cache.set(cache_key, article_list)#lxy 写入缓存
>>>>>>> LXY_branch
logger.info('set view cache.key:{key}'.format(key=cache_key)) logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list return article_list
def get_queryset(self):#lxy 获取查询集(优先缓存) def get_queryset(self):
''' '''
重写默认从缓存获取数据 重写默认从缓存获取数据
:return: :return:
''' '''
key = self.get_queryset_cache_key() # 获取缓存键 key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key) # 从缓存获取数据 value = self.get_queryset_from_cache(key)
return value return value
<<<<<<< HEAD
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
#ymq扩展上下文数据添加链接类型
=======
def get_context_data(self, **kwargs):#lxy 补充上下文数据
>>>>>>> LXY_branch
kwargs['linktype'] = self.link_type kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(** kwargs) return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):#lxy 首页视图 class IndexView(ArticleListView):
''' '''
首页视图 首页
''' '''
<<<<<<< HEAD # 友情链接类型
# 友情链接类型:首页展示
link_type = LinkShowType.I link_type = LinkShowType.I
def get_queryset_data(self): def get_queryset_data(self):
#ymq获取首页文章列表已发布的文章
=======
# 友情链接类型
link_type = LinkShowType.I#lxy 首页友链展示类型
def get_queryset_data(self): #lxy 获取首页文章(已发布)
>>>>>>> LXY_branch
article_list = Article.objects.filter(type='a', status='p') article_list = Article.objects.filter(type='a', status='p')
return article_list return article_list
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
#ymq生成首页缓存键包含页码
cache_key = 'index_{page}'.format(page=self.page_number) cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key return cache_key
class ArticleDetailView(DetailView): class ArticleDetailView(DetailView):
''' '''
文章详情页面视图 文章详情页面
''' '''
template_name = 'blog/article_detail.html' # 详情页模板 template_name = 'blog/article_detail.html'
model = Article # 关联模型 model = Article
pk_url_kwarg = 'article_id' # URL中主键参数名 pk_url_kwarg = 'article_id'
context_object_name = "article" # 模板中上下文变量名 context_object_name = "article"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
#ymq扩展文章详情页的上下文数据 comment_form = CommentForm()
comment_form = CommentForm() # 初始化评论表单
# 获取文章评论列表
article_comments = self.object.comment_list() article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None) # 过滤顶级评论 parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting() # 获取博客设置 blog_setting = get_blog_setting()
# 评论分页处理
paginator = Paginator(parent_comments, blog_setting.article_comment_count) paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1') page = self.request.GET.get('comment_page', '1')
if not page.isnumeric(): if not page.isnumeric():
@ -184,74 +135,51 @@ class ArticleDetailView(DetailView):
next_page = p_comments.next_page_number() if p_comments.has_next() else None next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
# 生成评论分页链接
if next_page: if next_page:
kwargs[ kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container' 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page: if prev_page:
kwargs[ kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container' 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 向上下文添加数据
kwargs['form'] = comment_form kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len( kwargs['comment_count'] = len(
article_comments) if article_comments else 0 article_comments) if article_comments else 0
<<<<<<< HEAD
# 上一篇/下一篇文章
kwargs['next_article'] = self.object.next_article kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article kwargs['prev_article'] = self.object.prev_article
=======
kwargs['next_article'] = self.object.next_article # lxy 下一篇文章
kwargs['prev_article'] = self.object.prev_article # lxy 上一篇文章
>>>>>>> LXY_branch
# 调用父类方法获取基础上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs) context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object article = self.object
# Action Hook, 通知插件"文章详情已获取"
# 触发插件钩子:文章详情已获取
hooks.run_action('after_article_body_get', article=article, request=self.request) hooks.run_action('after_article_body_get', article=article, request=self.request)
# 应用插件过滤器:修改文章正文 # # Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article, article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request) request=self.request)
return context return context
class CategoryDetailView(ArticleListView):#lxy 分类详情视图 class CategoryDetailView(ArticleListView):
''' '''
分类目录列表视图 分类目录列表
''' '''
<<<<<<< HEAD page_type = "分类目录归档"
page_type = "分类目录归档" # 页面类型标识
def get_queryset_data(self): def get_queryset_data(self):
#ymq获取指定分类下的文章列表
slug = self.kwargs['category_name'] # 从URL获取分类别名
category = get_object_or_404(Category, slug=slug) # 获取分类对象
=======
page_type = "分类目录归档"#lxy 页面类型
def get_queryset_data(self):#lxy 获取分类下文章
slug = self.kwargs['category_name'] slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug) category = get_object_or_404(Category, slug=slug)
>>>>>>> LXY_branch
categoryname = category.name categoryname = category.name
self.categoryname = categoryname self.categoryname = categoryname
# 获取所有子分类名称
categorynames = list( categorynames = list(
map(lambda c: c.name, category.get_sub_categorys())) map(lambda c: c.name, category.get_sub_categorys()))
# 查询属于当前分类及子分类的已发布文章
article_list = Article.objects.filter( article_list = Article.objects.filter(
category__name__in=categorynames, status='p') category__name__in=categorynames, status='p')
return article_list return article_list
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
#ymq生成分类列表缓存键
slug = self.kwargs['category_name'] slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug) category = get_object_or_404(Category, slug=slug)
categoryname = category.name categoryname = category.name
@ -261,69 +189,59 @@ class CategoryDetailView(ArticleListView):#lxy 分类详情视图
return cache_key return cache_key
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
#ymq扩展分类页上下文数据
categoryname = self.categoryname categoryname = self.categoryname
try: try:
categoryname = categoryname.split('/')[-1] # 处理多级分类名称 categoryname = categoryname.split('/')[-1]
except BaseException: except BaseException:
pass pass
kwargs['page_type'] = CategoryDetailView.page_type kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(** kwargs) return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView): class AuthorDetailView(ArticleListView):
''' '''
作者详情页视图 作者详情页
''' '''
page_type = '作者文章归档' # 页面类型标识 page_type = '作者文章归档'
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
#ymq生成作者文章列表缓存键
from uuslug import slugify from uuslug import slugify
author_name = slugify(self.kwargs['author_name']) # 作者名转slug author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format( cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number) author_name=author_name, page=self.page_number)
return cache_key return cache_key
def get_queryset_data(self): def get_queryset_data(self):
#ymq获取指定作者的文章列表
author_name = self.kwargs['author_name'] author_name = self.kwargs['author_name']
article_list = Article.objects.filter( article_list = Article.objects.filter(
author__username=author_name, type='a', status='p') # 过滤已发布的文章 author__username=author_name, type='a', status='p')
return article_list return article_list
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
#ymq扩展作者页上下文数据
author_name = self.kwargs['author_name'] author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(** kwargs) return super(AuthorDetailView, self).get_context_data(**kwargs)
class TagDetailView(ArticleListView): class TagDetailView(ArticleListView):
''' '''
标签列表页面视图 标签列表页面
''' '''
page_type = '分类标签归档' # 页面类型标识 page_type = '分类标签归档'
def get_queryset_data(self): def get_queryset_data(self):
#ymq获取指定标签的文章列表 slug = self.kwargs['tag_name']
slug = self.kwargs['tag_name'] # 从URL获取标签别名 tag = get_object_or_404(Tag, slug=slug)
tag = get_object_or_404(Tag, slug=slug) # 获取标签对象
tag_name = tag.name tag_name = tag.name
self.name = tag_name self.name = tag_name
# 查询包含当前标签的已发布文章
article_list = Article.objects.filter( article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p') tags__name=tag_name, type='a', status='p')
return article_list return article_list
<<<<<<< HEAD
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
#ymq生成标签文章列表缓存键
=======
def get_queryset_cache_key(self):#lxy 标签缓存键(含标签名、页码)
>>>>>>> LXY_branch
slug = self.kwargs['tag_name'] slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug) tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name tag_name = tag.name
@ -333,139 +251,101 @@ class TagDetailView(ArticleListView):
return cache_key return cache_key
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
#ymq扩展标签页上下文数据 # tag_name = self.kwargs['tag_name']
tag_name = self.name tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(** kwargs) return super(TagDetailView, self).get_context_data(**kwargs)
class ArchivesView(ArticleListView):#lxy 文章归档视图 class ArchivesView(ArticleListView):
''' '''
文章归档页面视图 文章归档页面
''' '''
page_type = '文章归档' # 页面类型标识 page_type = '文章归档'
paginate_by = None # 不分页 paginate_by = None
page_kwarg = None # 无页码参数 page_kwarg = None
template_name = 'blog/article_archives.html' # 归档页模板 template_name = 'blog/article_archives.html'
<<<<<<< HEAD
def get_queryset_data(self): def get_queryset_data(self):
#ymq获取所有已发布文章用于归档
return Article.objects.filter(status='p').all() return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
#ymq生成归档页缓存键
=======
def get_queryset_data(self):#lxy 获取所有已发布文章
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):#lxy 归档缓存键
>>>>>>> LXY_branch
cache_key = 'archives' cache_key = 'archives'
return cache_key return cache_key
<<<<<<< HEAD
class LinkListView(ListView): class LinkListView(ListView):
#ymq友情链接列表视图 model = Links
model = Links # 关联模型 template_name = 'blog/links_list.html'
template_name = 'blog/links_list.html' # 链接列表模板
def get_queryset(self): def get_queryset(self):
#ymq只获取启用的友情链接
return Links.objects.filter(is_enable=True) return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView): class EsSearchView(SearchView):
#ymqElasticsearch搜索视图继承自Haystack的SearchView
def get_context(self): def get_context(self):
#ymq构建搜索结果页面的上下文数据
paginator, page = self.build_page() # 处理分页
=======
class LinkListView(ListView):#lxy 友链列表视图
model = Links
template_name = 'blog/links_list.html' #lxy 渲染模板
def get_queryset(self):#lxy 获取启用的友链
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):#lxy ES搜索视图
def get_context(self):#lxy 补充搜索上下文
paginator, page = self.build_page() paginator, page = self.build_page()
>>>>>>> LXY_branch
context = { context = {
"query": self.query, # 搜索关键词 "query": self.query,
"form": self.form, # 搜索表单 "form": self.form,
"page": page, # 当前页数据 "page": page,
"paginator": paginator, # 分页器 "paginator": paginator,
"suggestion": None, # 搜索建议(默认无) "suggestion": None,
} }
# 如果启用拼写建议,添加建议内容
if hasattr(self.results, "query") and self.results.query.backend.include_spelling: if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion() context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context()) # 添加额外上下文 context.update(self.extra_context())
return context return context
@csrf_exempt # 禁用CSRF保护用于外部调用 @csrf_exempt
def fileupload(request): def fileupload(request):
""" """
图片/文件上传接口需验证签名 该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request: :param request:
:return: :return:
""" """
if request.method == 'POST': if request.method == 'POST':
sign = request.GET.get('sign', None) # 获取签名参数 sign = request.GET.get('sign', None)
if not sign: if not sign:
return HttpResponseForbidden() # 无签名则拒绝 return HttpResponseForbidden()
# 验证签名双重SHA256加密对比
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden() # 签名错误则拒绝 return HttpResponseForbidden()
response = []
response = [] # 存储上传后的文件URL
for filename in request.FILES: for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d') # 按日期组织文件 timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # 图片扩展名 imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename)) fname = u''.join(str(filename))
# 判断是否为图片
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
# 确定存储目录(图片/文件分开存储)
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr) base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir): if not os.path.exists(base_dir):
os.makedirs(base_dir) # 目录不存在则创建 os.makedirs(base_dir)
# 生成唯一文件名UUID+原扩展名)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}")) savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 安全校验:防止路径遍历攻击
if not savepath.startswith(base_dir): if not savepath.startswith(base_dir):
return HttpResponse("only for post") return HttpResponse("only for post")
# 保存文件
with open(savepath, 'wb+') as wfile: with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks(): for chunk in request.FILES[filename].chunks():
wfile.write(chunk) wfile.write(chunk)
# 图片压缩处理
if isimage: if isimage:
from PIL import Image from PIL import Image
image = Image.open(savepath) image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True) # 压缩质量为20 image.save(savepath, quality=20, optimize=True)
# 生成文件访问URL
url = static(savepath) url = static(savepath)
response.append(url) response.append(url)
return HttpResponse(response) # 返回所有上传文件的URL return HttpResponse(response)
else: else:
return HttpResponse("only for post") # 只允许POST方法 return HttpResponse("only for post")
def page_not_found_view( def page_not_found_view(
request, request,
exception, exception,
template_name='blog/error_page.html'): template_name='blog/error_page.html'):
#ymq404错误处理视图
if exception: if exception:
logger.error(exception) # 记录错误日志 logger.error(exception)
url = request.get_full_path() url = request.get_full_path()
return render(request, return render(request,
template_name, template_name,
@ -475,7 +355,6 @@ def page_not_found_view(
def server_error_view(request, template_name='blog/error_page.html'): def server_error_view(request, template_name='blog/error_page.html'):
#ymq500错误处理视图
return render(request, return render(request,
template_name, template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'), {'message': _('Sorry, the server is busy, please click the home page to see other?'),
@ -487,9 +366,8 @@ def permission_denied_view(
request, request,
exception, exception,
template_name='blog/error_page.html'): template_name='blog/error_page.html'):
#ymq403错误处理视图
if exception: if exception:
logger.error(exception) # 记录错误日志 logger.error(exception)
return render( return render(
request, template_name, { request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'), 'message': _('Sorry, you do not have permission to access this page?'),
@ -497,6 +375,5 @@ def permission_denied_view(
def clean_cache_view(request): def clean_cache_view(request):
#ymq清理缓存的视图 cache.clear()
cache.clear() # 清除所有缓存 return HttpResponse('ok')
return HttpResponse('ok') # 返回成功响应

@ -1,169 +1,47 @@
<<<<<<< HEAD
# 导入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 _ # 用于国际化翻译(支持多语言)
=======
# jyn: 该模块用于配置 Django 管理后台的评论Comment模型管理界面
# jyn: 功能包括:自定义评论启用/禁用批量操作、列表页展示字段配置、关联对象跳转链接、界面显示优化等
from django.contrib import admin from django.contrib import admin
from django.urls import reverse # jyn: 用于反向解析 admin 后台的模型编辑页面 URL from django.urls import reverse
from django.utils.html import format_html # jyn: 用于安全生成 HTML 标签,避免 XSS 风险 from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ # jyn: 用于国际化翻译,支持多语言显示 from django.utils.translation import gettext_lazy as _
>>>>>>> JYN_branch
def disable_commentstatus(modeladmin, request, queryset): def disable_commentstatus(modeladmin, request, queryset):
"""
<<<<<<< HEAD
自定义Admin批量操作批量禁用选中的评论
参数说明
- modeladmin当前关联的Admin模型类实例
- request当前请求对象
- queryset用户在Admin中选中的评论数据集合
"""
# 批量更新选中评论的is_enable字段为False禁用状态
queryset.update(is_enable=False) queryset.update(is_enable=False)
=======
jyn: 批量禁用评论的自定义操作函数
:param modeladmin: 关联的 ModelAdmin 实例
:param request: 当前请求对象
:param queryset: 选中的评论记录集合
"""
queryset.update(is_enable=False) # jyn: 将选中评论的 is_enable 字段设为 False实现禁用
>>>>>>> JYN_branch
def enable_commentstatus(modeladmin, request, queryset): def enable_commentstatus(modeladmin, request, queryset):
"""
<<<<<<< HEAD
自定义Admin批量操作批量启用选中的评论
参数与disable_commentstatus一致功能相反
"""
# 批量更新选中评论的is_enable字段为True启用状态
queryset.update(is_enable=True) queryset.update(is_enable=True)
# 为批量操作函数设置在Admin界面显示的名称支持国际化
disable_commentstatus.short_description = _('Disable comments') # 显示为“禁用评论”
enable_commentstatus.short_description = _('Enable comments') # 显示为“启用评论”
class CommentAdmin(admin.ModelAdmin):
"""
评论模型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] # 列表页支持的批量操作(绑定上面定义的两个函数)
=======
jyn: 批量启用评论的自定义操作函数
:param modeladmin: 关联的 ModelAdmin 实例
:param request: 当前请求对象
:param queryset: 选中的评论记录集合
"""
queryset.update(is_enable=True) # jyn: 将选中评论的 is_enable 字段设为 True实现启用
# jyn: 为批量操作函数设置后台显示名称(支持国际化)
disable_commentstatus.short_description = _('Disable comments') disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments') enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin): class CommentAdmin(admin.ModelAdmin):
"""jyn: 评论模型Comment的 Admin 配置类,控制后台展示和操作逻辑""" list_per_page = 20
list_per_page = 20 # jyn: 列表页每页显示 20 条评论记录
list_display = ( list_display = (
'id', # jyn: 评论 ID 'id',
'body', # jyn: 评论内容 'body',
'link_to_userinfo', # jyn: 自定义字段,显示评论作者跳转链接 'link_to_userinfo',
'link_to_article', # jyn: 自定义字段,显示关联文章跳转链接 'link_to_article',
'is_enable', # jyn: 评论启用状态True/False 'is_enable',
'creation_time' # jyn: 评论创建时间 'creation_time')
) list_display_links = ('id', 'body', 'is_enable')
list_display_links = ('id', 'body', 'is_enable') # jyn: 列表页中可点击跳转编辑页的字段 list_filter = ('is_enable',)
list_filter = ('is_enable',) # jyn: 右侧筛选器,按启用状态筛选评论 exclude = ('creation_time', 'last_modify_time')
exclude = ('creation_time', 'last_modify_time') # jyn: 编辑页隐藏的字段(自动生成,无需手动修改) actions = [disable_commentstatus, enable_commentstatus]
actions = [disable_commentstatus, enable_commentstatus] # jyn: 注册批量操作函数
>>>>>>> JYN_branch
# 2. 自定义列表页字段:生成“评论作者”的跳转链接
def link_to_userinfo(self, obj): def link_to_userinfo(self, obj):
"""
<<<<<<< HEAD
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' %
=======
jyn: 自定义列表字段生成评论作者的 admin 编辑页跳转链接
:param obj: 当前 Comment 模型实例
:return: 包含跳转链接的 HTML 字符串
"""
# jyn: 获取作者模型UserInfo的 app 名称和模型名称,用于反向解析 URL
info = (obj.author._meta.app_label, obj.author._meta.model_name) info = (obj.author._meta.app_label, obj.author._meta.model_name)
# jyn: 反向解析作者模型的编辑页 URL传入作者 ID 作为参数
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# jyn: 生成 HTML 链接,优先显示昵称,无昵称则显示邮箱
return format_html( return format_html(
u'<a href="%s">%s</a>' % u'<a href="%s">%s</a>' %
>>>>>>> JYN_branch (link, obj.author.nickname if obj.author.nickname else obj.author.email))
(link, obj.author.nickname if obj.author.nickname else obj.author.email)
)
# 3. 自定义列表页字段:生成“评论所属文章”的跳转链接
def link_to_article(self, obj): def link_to_article(self, obj):
"""
<<<<<<< HEAD
逻辑与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)
)
# 4. 为自定义字段设置在Admin界面显示的名称支持国际化
link_to_userinfo.short_description = _('User') # 自定义字段“link_to_userinfo”显示为“用户”
link_to_article.short_description = _('Article') # 自定义字段“link_to_article”显示为“文章”
=======
jyn: 自定义列表字段生成关联文章的 admin 编辑页跳转链接
:param obj: 当前 Comment 模型实例
:return: 包含跳转链接的 HTML 字符串
"""
# jyn: 获取文章模型Article的 app 名称和模型名称
info = (obj.article._meta.app_label, obj.article._meta.model_name) info = (obj.article._meta.app_label, obj.article._meta.model_name)
# jyn: 反向解析文章模型的编辑页 URL传入文章 ID 作为参数
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# jyn: 生成 HTML 链接,显示文章标题
return format_html( return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title) u'<a href="%s">%s</a>' % (link, obj.article.title))
)
# jyn: 为自定义列表字段设置后台显示名称(支持国际化)
link_to_userinfo.short_description = _('User') link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article') link_to_article.short_description = _('Article')
>>>>>>> JYN_branch

@ -1,29 +1,6 @@
<<<<<<< HEAD
# 导入Django的App配置基类所有应用的配置类都需继承此类
from django.apps import AppConfig from django.apps import AppConfig
class CommentsConfig(AppConfig): class CommentsConfig(AppConfig):
"""
comments应用的配置类
作用定义应用的核心标识初始化行为等是Django识别和管理该应用的入口
"""
# 应用的唯一名称必须与应用目录名一致Django通过该值定位应用
name = 'comments' name = 'comments'
<<<<<<< HEAD
# 可选扩展配置(当前代码未实现,可根据需求添加):
# - verbose_name应用的人性化名称如 verbose_name = "评论管理"用于Admin后台显示
# - default_auto_field指定模型默认的主键类型如 default_auto_field = "django.db.models.BigAutoField"
=======
>>>>>>> ZYY_branch
=======
# jyn: 评论应用comments的配置类用于定义应用元数据和初始化行为
from django.apps import AppConfig # jyn: Django 内置的应用配置基类,所有应用配置需继承此类
class CommentsConfig(AppConfig):
"""jyn: comments 应用的核心配置类,指定应用名称等关键信息"""
name = 'comments' # jyn: 应用唯一标识名称与项目目录名一致Django 通过该名称识别应用
>>>>>>> JYN_branch

@ -1,42 +1,13 @@
<<<<<<< HEAD from django import forms
# 导入Django表单核心模块 from django.forms import ModelForm
from django import forms # Django表单基础模块提供表单字段、验证等功能
from django.forms import ModelForm # 模型表单类,可快速将模型转换为表单(减少重复代码)
# 导入当前应用下的Comment模型评论模型表单需与该模型关联
from .models import Comment from .models import Comment
class CommentForm(ModelForm): class CommentForm(ModelForm):
"""
评论模型对应的模型表单类继承ModelForm
核心作用生成前端评论提交表单并关联Comment模型处理数据存储
"""
# 1. 自定义额外字段父评论ID用于实现评论回复功能
=======
# jyn: 评论功能的表单配置模块,定义前端提交评论的表单结构和字段规则
from django import forms
from django.forms import ModelForm # jyn: Django 内置模型表单基类,自动关联模型字段生成表单
from .models import Comment # jyn: 导入评论核心模型,表单与模型数据联动
class CommentForm(ModelForm):
"""jyn: 评论提交表单类,继承 ModelForm 实现模型与表单的自动映射"""
# jyn: 父评论 ID 字段,隐藏输入(用于回复功能),非必填(首次评论无父评论)
>>>>>>> JYN_branch
parent_comment_id = forms.IntegerField( parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, # 表单控件:隐藏输入框(前端不显示,仅用于传递数据) widget=forms.HiddenInput, required=False)
required=False # 是否必填False表示允许为空普通评论无父评论回复评论时才传值
)
# 2. Meta类模型表单的核心配置关联模型、指定字段等
class Meta: class Meta:
<<<<<<< HEAD model = Comment
model = Comment # 关联的模型当前表单与Comment模型绑定 fields = ['body']
fields = ['body'] # 表单需显示/处理的模型字段仅包含评论内容body字段
# 注Comment模型中其他字段如author、article、creation_time等
# 通常由后端自动填充如从登录态获取author无需前端用户输入
=======
model = Comment # jyn: 关联的核心模型,表单数据同步至该模型
fields = ['body'] # jyn: 前端需提交的字段(评论内容),自动生成对应表单控件
>>>>>>> JYN_branch

@ -1,91 +1,38 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14 # 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.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion # 用于定义外键删除策略如CASCADE import django.db.models.deletion
import django.utils.timezone # 用于时间字段的默认值 import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
"""
Comment模型的初始迁移类负责在数据库中创建`comments_comment`
所有迁移类都必须继承migrations.Migration
"""
# 标识该迁移是模型的「初始迁移」第一次为Comment模型创建表
initial = True initial = True
# 迁移依赖:执行当前迁移前,必须先执行依赖的迁移
dependencies = [ dependencies = [
('blog', '0001_initial'), # 依赖blog应用的0001_initial迁移因Comment关联blog的Article模型 ('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型的可交换迁移(适配自定义用户模型) migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
# 迁移操作当前迁移要执行的具体数据库操作此处为「创建Comment表」
operations = [ operations = [
# 1. 创建Comment模型对应的数据库表
migrations.CreateModel( migrations.CreateModel(
name='Comment', # 模型名称与代码中定义的Comment类一致 name='Comment',
# 2. 定义表的字段(对应模型中的字段,映射到数据库表的列)
fields=[ fields=[
# 主键字段BigAutoField自增bigint类型Django默认主键无需在模型中手动定义 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('id', models.BigAutoField( ('body', models.TextField(max_length=300, verbose_name='正文')),
auto_created=True, # 自动创建 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
primary_key=True, # 设为主键 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
serialize=False, # 不序列化(主键无需序列化) ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
verbose_name='ID' # 字段显示名Admin后台中显示 ('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='作者')),
# 评论正文字段TextField长文本类型对应模型中的body字段 ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
('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={ options={
'verbose_name': '评论', # 模型单数显示名Admin中显示 'verbose_name': '评论',
'verbose_name_plural': '评论', # 模型复数显示名与单数一致避免“评论s” 'verbose_name_plural': '评论',
'ordering': ['-id'], # 表数据默认排序按id倒序最新评论在前 'ordering': ['-id'],
'get_latest_by': 'id', # 用Model.objects.latest()时按id取最新数据 'get_latest_by': 'id',
# 注Django会自动根据模型名生成表名app名_模型名 → comments_comment
}, },
), ),
] ]

@ -5,14 +5,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('comments', '0001_initial'), ('comments', '0001_initial'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='comment', model_name='comment',
name='is_enable', name='is_enable',
field=models.BooleanField(default=False, verbose_name='是否显示'), field=models.BooleanField(default=False, verbose_name='是否显示'),
), ),
] ]

@ -1,131 +1,41 @@
<<<<<<< HEAD from django.conf import settings
# 导入Django核心模块配置、数据库模型、时间工具、国际化 from django.db import models
from django.conf import settings # 导入项目配置(用于获取自定义用户模型) from django.utils.timezone import now
from django.db import models # Django数据库模型基类所有模型需继承models.Model from django.utils.translation import gettext_lazy as _
from django.utils.timezone import now # 获取当前时区时间,用于时间字段默认值
from django.utils.translation import gettext_lazy as _ # 国际化翻译,支持多语言显示
# 导入关联模型从blog应用导入Article模型评论需关联到具体文章
from blog.models import Article from blog.models import Article
from django.utils import timezone from django.utils import timezone
# Create your models here. # Create your models here.
class Comment(models.Model):
"""
评论模型存储用户对文章的评论数据支持评论回复父子评论
与User用户Article文章为多对一关系与自身为自关联实现回复
"""
# 1. 评论正文长文本字段限制最大300字符
body = models.TextField('正文', max_length=300)
<<<<<<< HEAD
# 2. 时间字段:创建时间和最后修改时间,默认值为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now) # 评论创建时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 评论最后修改时间
# 3. 关联用户:多对一(多个评论属于一个用户)
=======
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=timezone.now)
>>>>>>> ZYY_branch
author = models.ForeignKey(
settings.AUTH_USER_MODEL, # 关联项目配置的用户模型而非固定User更灵活
verbose_name=_('author'), # 字段在Admin后台显示的名称支持国际化
on_delete=models.CASCADE # 级联删除:若用户被删除,其所有评论也会被删除
)
# 4. 关联文章:多对一(多个评论属于一篇文章)
article = models.ForeignKey(
Article, # 关联blog应用的Article模型
verbose_name=_('article'), # Admin显示名
on_delete=models.CASCADE # 级联删除:文章删除,关联评论也删除
)
# 5. 父评论:自关联(实现评论回复,多个子评论对应一个父评论)
parent_comment = models.ForeignKey(
'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 # 数据库中不允许为空
=======
# jyn: 评论功能核心数据模型模块,定义评论的数据库结构、关联关系及基础配置
from django.conf import settings # jyn: 导入 Django 项目配置,用于获取自定义用户模型
from django.db import models # jyn: Django 数据库模型基类,所有数据模型需继承此类
from django.utils.timezone import now # jyn: 获取当前时区时间,用于时间字段默认值
from django.utils.translation import gettext_lazy as _ # jyn: 国际化翻译支持
from blog.models import Article # jyn: 导入博客文章模型,建立评论与文章的关联
class Comment(models.Model): class Comment(models.Model):
"""jyn: 评论数据模型,存储评论核心信息,关联用户、文章及父评论(支持回复功能)"""
# jyn: 评论正文,文本字段,最大长度限制 300 字符
body = models.TextField('正文', max_length=300) body = models.TextField('正文', max_length=300)
# jyn: 评论创建时间,默认值为当前时间,支持国际化显示
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
# jyn: 评论最后修改时间,默认值为当前时间,支持国际化显示 last_modify_time = models.DateTimeField(_('last modify time'), default=timezone.now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# jyn: 关联评论作者(用户模型),级联删除(用户删除时评论同步删除),支持国际化
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
on_delete=models.CASCADE on_delete=models.CASCADE)
)
# jyn: 关联所属文章,级联删除(文章删除时评论同步删除),支持国际化
article = models.ForeignKey( article = models.ForeignKey(
Article, Article,
verbose_name=_('article'), verbose_name=_('article'),
on_delete=models.CASCADE on_delete=models.CASCADE)
)
# jyn: 关联父评论(自关联),支持回复功能,允许为空,级联删除,支持国际化
parent_comment = models.ForeignKey( parent_comment = models.ForeignKey(
'self', 'self',
verbose_name=_('parent comment'), verbose_name=_('parent comment'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE on_delete=models.CASCADE)
) is_enable = models.BooleanField(_('enable'),
# jyn: 评论启用状态,布尔值,默认禁用(需后台审核),非空约束 default=False, blank=False, null=False)
is_enable = models.BooleanField(
_('enable'),
default=False,
blank=False,
null=False
>>>>>>> JYN_branch
)
# 模型元数据:控制模型的整体行为(排序、显示名等)
class Meta: class Meta:
<<<<<<< HEAD ordering = ['-id']
ordering = ['-id'] # 数据查询时按ID倒序排列最新评论在前 verbose_name = _('comment')
verbose_name = _('comment') # 模型单数显示名Admin中“评论” verbose_name_plural = verbose_name
verbose_name_plural = verbose_name # 模型复数显示名与单数一致避免“评论s” get_latest_by = 'id'
get_latest_by = 'id' # 使用Model.objects.latest()时按id字段取最新数据
=======
"""jyn: 模型元数据配置,控制数据库表结构和 Admin 界面行为"""
ordering = ['-id'] # jyn: 数据查询默认排序:按评论 ID 倒序(最新评论在前)
verbose_name = _('comment') # jyn: 模型单数显示名称(支持国际化)
verbose_name_plural = verbose_name # jyn: 模型复数显示名称(与单数一致)
get_latest_by = 'id' # jyn: 使用 latest() 方法时,按 ID 字段获取最新记录
>>>>>>> JYN_branch
# 模型实例的字符串表示打印评论对象时显示正文便于调试和Admin显示
def __str__(self): def __str__(self):
<<<<<<< HEAD
<<<<<<< HEAD
=======
"""jyn: 模型实例的字符串表示Admin 界面及打印时显示评论正文"""
>>>>>>> JYN_branch
return self.body
=======
return self.body return self.body
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -133,4 +43,3 @@ class Comment(models.Model):
self.creation_time = timezone.now() self.creation_time = timezone.now()
self.last_modify_time = timezone.now() self.last_modify_time = timezone.now()
super(Comment, self).save(*args, **kwargs) super(Comment, self).save(*args, **kwargs)
>>>>>>> ZYY_branch

@ -1,192 +1,80 @@
<<<<<<< HEAD from django.test import Client, RequestFactory, TransactionTestCase
# 导入Django测试核心模块、URL工具及项目内模型/工具 from django.urls import reverse
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 accounts.models import BlogUser
from blog.models import Category, Article from blog.models import Category, Article
from comments.models import Comment from comments.models import Comment
# 导入评论相关自定义模板标签和通用工具函数
from comments.templatetags.comments_tags import * from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid from djangoblog.utils import get_max_articleid_commentid
# Create your tests here. # Create your tests here.
class CommentsTest(TransactionTestCase):
"""
评论功能测试类继承TransactionTestCase用于测试评论的提交状态更新等核心逻辑
支持数据库事务回滚确保测试用例间数据隔离
"""
class CommentsTest(TransactionTestCase):
def setUp(self): def setUp(self):
"""
测试前置初始化方法每个测试用例执行前自动调用
作用创建测试所需的基础数据客户端用户系统配置等
"""
# 1. 初始化测试工具Client模拟浏览器请求RequestFactory构造原始请求对象
self.client = Client() self.client = Client()
self.factory = RequestFactory() self.factory = RequestFactory()
# 2. 初始化博客系统配置:设置“评论需审核”(模拟真实场景中评论需管理员审核才能显示)
from blog.models import BlogSettings # 局部导入避免循环引用
value = BlogSettings() # 创建配置对象
value.comment_need_review = True # 开启评论审核开关
value.save() # 保存到测试数据库
# 3. 创建测试超级用户:用于模拟登录状态下提交评论
=======
# jyn: 评论功能测试模块,涵盖评论提交、审核状态、回复功能、模板标签及工具函数的完整性测试
from django.test import Client, RequestFactory, TransactionTestCase # jyn: Django 测试核心类Client模拟请求RequestFactory构造请求TransactionTestCase支持事务回滚
from django.urls import reverse # jyn: 反向解析URL用于生成测试请求地址
from accounts.models import BlogUser # jyn: 导入用户模型,用于创建测试用户
from blog.models import Category, Article # jyn: 导入博客分类、文章模型,用于创建测试文章
from comments.models import Comment # jyn: 导入评论模型,用于验证评论数据
from comments.templatetags.comments_tags import * # jyn: 导入评论相关模板标签,测试模板渲染逻辑
from djangoblog.utils import get_max_articleid_commentid # jyn: 导入工具函数测试ID获取功能
class CommentsTest(TransactionTestCase):
"""jyn: 评论功能集成测试类,覆盖评论提交、状态更新、回复、模板标签及邮件通知等核心流程"""
def setUp(self):
"""jyn: 测试前置初始化方法,每次测试用例执行前自动调用,创建测试依赖数据"""
self.client = Client() # jyn: 初始化测试客户端用于模拟前端HTTP请求
self.factory = RequestFactory() # jyn: 初始化请求工厂,用于构造自定义请求对象
# jyn: 创建博客设置,开启评论需审核功能(模拟真实环境的评论审核机制)
from blog.models import BlogSettings from blog.models import BlogSettings
value = BlogSettings() value = BlogSettings()
value.comment_need_review = True # jyn: 评论需后台审核才能显示 value.comment_need_review = True
value.save() value.save()
# jyn: 创建超级用户,用于测试登录状态下的评论提交
>>>>>>> JYN_branch
self.user = BlogUser.objects.create_superuser( self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", # 测试邮箱 email="liangliangyy1@gmail.com",
username="liangliangyy1", # 测试用户名 username="liangliangyy1",
password="liangliangyy1" # 测试密码明文Django会自动加密存储 password="liangliangyy1")
)
def update_article_comment_status(self, article): def update_article_comment_status(self, article):
"""
<<<<<<< HEAD
辅助方法批量更新某篇文章下所有评论的启用状态设为启用
模拟管理员审核通过评论的操作用于测试审核后评论的显示逻辑
参数
- article目标文章对象需更新其下所有评论
"""
# 获取该文章下所有评论
comments = article.comment_set.all() comments = article.comment_set.all()
# 遍历评论将“是否启用”字段设为True并保存
=======
jyn: 辅助方法批量更新指定文章下所有评论的启用状态模拟后台审核通过
:param article: 目标文章实例
"""
comments = article.comment_set.all() # jyn: 获取文章关联的所有评论
>>>>>>> JYN_branch
for comment in comments: for comment in comments:
comment.is_enable = True # jyn: 设为启用状态 comment.is_enable = True
comment.save() # jyn: 保存修改 comment.save()
def test_validate_comment(self): def test_validate_comment(self):
<<<<<<< HEAD
"""
核心测试用例验证评论提交流程登录创建文章提交评论验证状态
覆盖场景登录用户提交评论评论未审核时不显示审核后正常显示
"""
# 1. 模拟用户登录:使用之前创建的测试超级用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 2. 创建测试分类:文章需关联分类,先创建分类数据
=======
"""jyn: 核心测试用例,验证评论提交、审核、回复、模板标签及工具函数的正确性"""
# 1. 模拟用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1') self.client.login(username='liangliangyy1', password='liangliangyy1')
# 2. 创建测试分类和文章(评论的关联对象)
>>>>>>> JYN_branch
category = Category() category = Category()
category.name = "categoryccc" # 分类名称 category.name = "categoryccc"
category.save() # 保存到测试数据库 category.save()
# 3. 创建测试文章:评论需关联文章,创建一篇已发布的文章
article = Article() article = Article()
<<<<<<< HEAD
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} # URL参数文章ID指定评论所属文章
)
# 5. 模拟POST请求提交评论向评论URL发送包含评论内容的请求
response = self.client.post(
comment_url,
{'body': '123ffffffffff'} # 请求参数:评论正文
)
# 6. 验证评论提交结果检查响应状态码是否为302重定向通常提交后跳回文章页
self.assertEqual(response.status_code, 302)
# 7. 验证“未审核评论不显示”重新获取文章检查其评论列表长度是否为0因评论需审核
article = Article.objects.get(pk=article.pk) # 从数据库重新查询(避免缓存)
self.assertEqual(len(article.comment_list()), 0) # comment_list()应为自定义方法,返回启用的评论
# 8. 模拟审核通过:调用辅助方法,将该文章下所有评论设为“启用”
=======
article.title = "nicetitleccc" article.title = "nicetitleccc"
article.body = "nicecontentccc" article.body = "nicecontentccc"
article.author = self.user article.author = self.user
article.category = category article.category = category
article.type = 'a' # jyn: 文章类型(假设'a'代表普通文章) article.type = 'a'
article.status = 'p' # jyn: 文章状态(假设'p'代表已发布) article.status = 'p'
article.save() article.save()
# 3. 生成评论提交接口的URL
comment_url = reverse( comment_url = reverse(
'comments:postcomment', kwargs={ 'comments:postcomment', kwargs={
'article_id': article.id}) # jyn: 反向解析"提交评论"接口传入文章ID 'article_id': article.id})
# 4. 测试首次提交普通评论(未审核状态)
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
'body': '123ffffffffff' # jyn: 评论正文 'body': '123ffffffffff'
}) })
self.assertEqual(response.status_code, 302) # jyn: 验证提交成功后重定向状态码302
# 未审核时,文章评论列表应为空 self.assertEqual(response.status_code, 302)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0) # jyn: 假设comment_list()返回启用的评论
# 模拟审核通过,再次验证评论数量 article = Article.objects.get(pk=article.pk)
>>>>>>> JYN_branch self.assertEqual(len(article.comment_list()), 0)
self.update_article_comment_status(article) self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1) # jyn: 审核后应显示1条评论
<<<<<<< HEAD
# 9. 验证“审核后评论显示”再次检查评论列表长度是否为1审核通过后应显示
self.assertEqual(len(article.comment_list()), 1) self.assertEqual(len(article.comment_list()), 1)
=======
# 5. 测试再次提交相同内容评论(验证重复提交允许性)
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
'body': '123ffffffffff', 'body': '123ffffffffff',
}) })
self.assertEqual(response.status_code, 302) # jyn: 验证提交成功
self.assertEqual(response.status_code, 302)
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article) self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2) # jyn: 评论总数应为2条 self.assertEqual(len(article.comment_list()), 2)
parent_comment_id = article.comment_list()[0].id
# 6. 测试回复功能提交带父评论ID的评论
parent_comment_id = article.comment_list()[0].id # jyn: 获取第一条评论ID作为父评论
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
'body': ''' 'body': '''
@ -199,25 +87,23 @@ class CommentsTest(TransactionTestCase):
[url](https://www.lylinux.net/) [url](https://www.lylinux.net/)
[ddd](http://www.baidu.com) [ddd](http://www.baidu.com)
''', # jyn: 带Markdown格式、链接、代码块的评论正文
'parent_comment_id': parent_comment_id # jyn: 指定父评论ID实现回复
''',
'parent_comment_id': parent_comment_id
}) })
self.assertEqual(response.status_code, 302) # jyn: 验证回复提交成功
self.assertEqual(response.status_code, 302)
self.update_article_comment_status(article) self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk) article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3) # jyn: 评论总数应为3条含1条回复 self.assertEqual(len(article.comment_list()), 3)
# 7. 测试评论树解析(验证回复层级关系)
comment = Comment.objects.get(id=parent_comment_id) comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment) # jyn: 调用模板标签解析评论树 tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1) # jyn: 验证父评论下有1条子回复 self.assertEqual(len(tree), 1)
# 8. 测试评论项渲染模板标签(验证模板标签功能正常) data = show_comment_item(comment, True)
data = show_comment_item(comment, True) # jyn: 调用模板标签生成评论HTML片段 self.assertIsNotNone(data)
self.assertIsNotNone(data) # jyn: 验证渲染结果非空 s = get_max_articleid_commentid()
# 9. 测试工具函数验证最大ID获取功能 self.assertIsNotNone(s)
s = get_max_articleid_commentid() # jyn: 调用工具函数获取最大文章ID和评论ID
self.assertIsNotNone(s) # jyn: 验证返回结果非空
# 10. 测试评论邮件通知(验证邮件发送功能)
from comments.utils import send_comment_email from comments.utils import send_comment_email
send_comment_email(comment) # jyn: 调用工具函数发送评论通知邮件(无返回值,验证无报错) send_comment_email(comment)
>>>>>>> JYN_branch

@ -1,45 +1,11 @@
<<<<<<< HEAD
# 导入Django的URL路径配置模块
from django.urls import path from django.urls import path
# 导入当前应用comments的视图模块views.py用于关联URL和视图逻辑
from . import views from . import views
# 定义应用命名空间在模板或反向解析URL时需通过「app_name:URL名称」的格式定位如comments:postcomment
# 作用避免不同应用间URL名称冲突
app_name = "comments" app_name = "comments"
# URL路由列表配置URL路径与视图的映射关系
urlpatterns = [ urlpatterns = [
# 评论提交URL处理用户对特定文章的评论提交请求
path( path(
# URL路径规则 'article/<int:article_id>/postcomment',
# - 'article/':固定路径前缀,标识与文章相关的操作
# - '<<int:article_id>/'动态路径参数接收整数类型的文章ID用于指定评论所属文章
# - 'postcomment':固定路径后缀,标识“提交评论”的操作
'article/<<int:article_id>/postcomment',
# 关联的视图调用views.py中的CommentPostView类视图的as_view()方法(类视图需转为视图函数)
# 该视图负责处理评论提交的业务逻辑(如数据验证、保存评论等)
views.CommentPostView.as_view(), views.CommentPostView.as_view(),
name='postcomment'),
# URL名称用于反向解析如在模板或代码中通过name='postcomment'生成URL
name='postcomment'
),
]
=======
# jyn: 评论应用comments的URL配置模块定义评论相关接口的路由映射
from django.urls import path # jyn: Django URL路径匹配核心函数用于定义路由规则
from . import views # jyn: 导入当前应用的视图模块,关联路由与视图逻辑
app_name = "comments" # jyn: 路由命名空间,避免不同应用间路由名称冲突
urlpatterns = [
# jyn: 提交评论接口路由接收文章ID参数映射到CommentPostView视图类
path(
'article/<<int:article_id>/postcomment', # jyn: 路由路径,<<int:article_id>接收整数型文章ID参数
views.CommentPostView.as_view(), # jyn: 将请求分发到基于类的视图CommentPostView需调用as_view()转换)
name='postcomment' # jyn: 路由名称用于reverse反向解析URL
),
] ]
>>>>>>> JYN_branch

@ -1,117 +1,38 @@
<<<<<<< HEAD import logging
# 导入日志模块和Django国际化工具以及项目自定义工具
import logging # Python内置日志模块用于记录邮件发送过程中的错误信息
from django.utils.translation import gettext_lazy as _ # Django国际化工具支持多语言邮件内容 from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site # 项目自定义工具获取当前网站的域名如example.com from djangoblog.utils import get_current_site
from djangoblog.utils import send_email # 项目自定义工具封装邮件发送逻辑底层调用Django邮件功能 from djangoblog.utils import send_email
# 初始化日志记录器按当前模块名称创建logger用于记录该模块的运行日志如邮件发送失败
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
=======
# jyn: 评论功能邮件通知工具模块,用于评论提交后向评论者及被回复者发送邮件通知
import logging # jyn: 日志模块,记录邮件发送过程中的异常信息
from django.utils.translation import gettext_lazy as _ # jyn: 国际化翻译支持,邮件内容适配多语言
from djangoblog.utils import get_current_site # jyn: 工具函数,获取当前网站域名
from djangoblog.utils import send_email # jyn: 工具函数,封装邮件发送逻辑
logger = logging.getLogger(__name__) # jyn: 创建日志实例,用于记录模块内的日志信息
>>>>>>> JYN_branch
def send_comment_email(comment): def send_comment_email(comment):
"""
<<<<<<< HEAD
评论相关邮件发送函数触发场景为用户提交评论后
1. 向评论作者发送评论提交成功的感谢邮件
2. 若该评论是回复有父评论向父评论作者发送评论被回复的通知邮件
参数
- comment已保存到数据库的Comment模型对象包含评论作者所属文章父评论等信息
"""
# 1. 基础数据准备:获取当前网站域名,用于拼接文章访问链接
site = get_current_site().domain # 如从配置中获取域名“blog.example.com”
# 2. 构建邮件基础信息(通用主题、文章访问链接)
subject = _('Thanks for your comment') # 邮件主题(支持国际化,多语言环境下自动切换)
# 拼接文章的完整访问URLhttps://域名 + 文章的相对路径通过模型get_absolute_url()获取)
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# 3. 向「当前评论作者」发送感谢邮件
# 构建HTML格式的邮件内容支持超链接%s占位符通过字典传值替换
html_content = _("""<p>Thank you very much for your comments on this site</p >
You can visit %s
to review your comments,
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
%s""") % (article_url, comment.article.title, article_url)
tomail = comment.author.email # 收件人邮箱:当前评论作者的邮箱
send_email([tomail], subject, html_content) # 调用自定义工具发送邮件收件人列表、主题、HTML内容
# 4. 若当前评论是「回复评论」(有父评论),向「父评论作者」发送回复通知邮件
try:
if comment.parent_comment: # 判断当前评论是否有父评论(即是否是回复)
# 构建回复通知的HTML邮件内容告知父评论作者“你的评论被回复了”
html_content = _("""Your comment on %s<br/> has
received a reply. <br/> %s
=======
jyn: 发送评论相关邮件通知的核心函数
:param comment: 已保存的 Comment 模型实例新提交的评论或回复
:return: 无返回值内部处理邮件发送逻辑及异常捕获
"""
# jyn: 获取当前网站域名,用于拼接文章访问链接
site = get_current_site().domain site = get_current_site().domain
# jyn: 邮件主题(支持国际化)
subject = _('Thanks for your comment') subject = _('Thanks for your comment')
# jyn: 拼接文章的完整访问链接HTTPS协议
article_url = f"https://{site}{comment.article.get_absolute_url()}" article_url = f"https://{site}{comment.article.get_absolute_url()}"
# 1. 向当前评论者发送「评论提交成功」通知邮件
# jyn: 构建评论者邮件的HTML内容包含文章链接和感谢语支持国际化
html_content = _("""<p>Thank you very much for your comments on this site</p> html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a> You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments, to review your comments,
Thank you again! Thank you again!
<br /> <br />
If the link above cannot be opened, please copy this link to your browser. If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % { %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
'article_url': article_url, tomail = comment.author.email
'article_title': comment.article.title send_email([tomail], subject, html_content)
}
tomail = comment.author.email # jyn: 获取当前评论者的邮箱地址
send_email([tomail], subject, html_content) # jyn: 调用工具函数发送邮件
# 2. 若当前评论是回复(有父评论),向父评论者发送「收到回复」通知邮件
try: try:
if comment.parent_comment: # jyn: 判断当前评论是否为对其他评论的回复 if comment.parent_comment:
# jyn: 构建父评论者邮件的HTML内容告知其评论被回复支持国际化
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s received a reply. <br/> %(comment_body)s
>>>>>>> JYN_branch
<br/> <br/>
go check it out! go check it out!
<br/> <br/>
If the link above cannot be opened, please copy this link to your browser. If the link above cannot be opened, please copy this link to your browser.
<<<<<<< HEAD
%s
""") % (article_url, comment.article.title, comment.parent_comment.body, article_url)
tomail = comment.parent_comment.author.email # 收件人邮箱:父评论作者的邮箱
send_email([tomail], subject, html_content) # 发送回复通知邮件
# 捕获邮件发送过程中的所有异常(如邮箱格式错误、邮件服务器故障等)
except Exception as e:
logger.error(e) # 将错误信息记录到日志(便于后续排查问题,不中断程序运行)
=======
%(article_url)s %(article_url)s
""") % { """) % {'article_url': article_url, 'article_title': comment.article.title,
'article_url': article_url, 'comment_body': comment.parent_comment.body}
'article_title': comment.article.title, tomail = comment.parent_comment.author.email
'comment_body': comment.parent_comment.body # jyn: 包含父评论的正文内容 send_email([tomail], subject, html_content)
}
tomail = comment.parent_comment.author.email # jyn: 获取父评论者的邮箱地址
send_email([tomail], subject, html_content) # jyn: 发送回复通知邮件
except Exception as e: except Exception as e:
logger.error(e) # jyn: 捕获发送过程中的异常,记录错误日志(不中断程序执行) logger.error(e)
>>>>>>> JYN_branch

@ -1,191 +1,62 @@
<<<<<<< HEAD
<<<<<<< HEAD
# Create your views here.
# 导入Django核心模块、异常类、视图工具及项目内模型/表单
from django.core.exceptions import ValidationError # Django内置验证异常类用于抛出自定义验证错误
from django.http import HttpResponseRedirect # 用于重定向HTTP响应如提交后跳回文章页
from django.shortcuts import get_object_or_404 # 快捷查询找到数据返回对象找不到返回404页面
from django.utils.decorators import method_decorator # 用于给类视图的方法添加装饰器
from django.views.decorators.csrf import csrf_protect # CSRF保护装饰器防止跨站请求伪造攻击
from django.views.generic.edit import FormView # 通用表单视图类:简化表单提交、验证、处理的逻辑
=======
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
>>>>>>> ZYY_branch
# 导入项目内关联模型和表单:用户、文章、评论表单、评论模型
from accounts.models import BlogUser from accounts.models import BlogUser
from blog.models import Article from blog.models import Article
from .forms import CommentForm # 评论功能的表单类之前定义的CommentForm from .forms import CommentForm
from .models import Comment from .models import Comment
class CommentPostView(FormView): class CommentPostView(FormView):
""" form_class = CommentForm
评论提交的类视图继承FormView处理评论表单的展示验证和数据保存 template_name = 'blog/article_detail.html'
核心功能接收用户提交的评论数据验证合法性后保存到数据库支持评论回复
"""
# 1. 类视图基础配置
form_class = CommentForm # 指定关联的表单类使用CommentForm处理提交数据
template_name = 'blog/article_detail.html' # 指定表单渲染的模板:评论在文章详情页提交,故用文章详情模板
=======
# jyn: 评论功能核心视图模块,处理评论提交、数据验证、权限判断及页面跳转逻辑
from django.core.exceptions import ValidationError # jyn: Django 数据验证异常类,用于抛出评论相关错误
from django.http import HttpResponseRedirect # jyn: 重定向响应类,评论提交后跳转至指定页面
from django.shortcuts import get_object_or_404 # jyn: 快捷查询函数查询不到数据时返回404
from django.utils.decorators import method_decorator # jyn: 方法装饰器工具,用于给类视图方法添加装饰器
from django.views.decorators.csrf import csrf_protect # jyn: CSRF防护装饰器防止跨站请求伪造
from django.views.generic.edit import FormView # jyn: 基于类的表单处理视图基类,简化表单验证逻辑
from accounts.models import BlogUser # jyn: 导入用户模型,关联评论作者
from blog.models import Article # jyn: 导入文章模型,关联评论所属文章
from .forms import CommentForm # jyn: 导入评论表单类,用于前端数据验证
from .models import Comment # jyn: 导入评论模型,用于数据存储
class CommentPostView(FormView):
"""jyn: 评论提交处理视图类继承FormView实现表单验证、数据保存及页面跳转"""
form_class = CommentForm # jyn: 指定关联的表单类,用于前端提交数据的验证
template_name = 'blog/article_detail.html' # jyn: 表单验证失败时渲染的模板(文章详情页)
>>>>>>> JYN_branch
# 2. 给dispatch方法添加CSRF保护所有请求GET/POST都经过CSRF验证
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
<<<<<<< HEAD
"""
类视图的请求入口方法所有请求都会先经过此方法
作用调用父类的dispatch逻辑同时应用CSRF保护
"""
=======
"""jyn: 重写dispatch方法添加CSRF防护所有请求先经过该方法分发"""
>>>>>>> JYN_branch
return super(CommentPostView, self).dispatch(*args, **kwargs) return super(CommentPostView, self).dispatch(*args, **kwargs)
# 3. 处理GET请求当用户以GET方式访问该视图时触发
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
<<<<<<< HEAD
"""
GET请求逻辑不处理表单提交直接重定向到对应的文章详情页的评论区
避免用户直接通过URL以GET方式访问该视图时出现异常
"""
# 从URL路径参数中获取文章IDkwargs对应URL中的<int:article_id>
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
# 查询对应的文章找不到则返回404
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
# 获取文章详情页的绝对URL并拼接评论区锚点#comments跳转到页面评论区域
url = article.get_absolute_url() url = article.get_absolute_url()
# 重定向到文章详情页的评论区
return HttpResponseRedirect(url + "#comments") return HttpResponseRedirect(url + "#comments")
=======
"""jyn: 处理GET请求直接重定向到文章详情页的评论区"""
article_id = self.kwargs['article_id'] # jyn: 从URL参数中获取文章ID
article = get_object_or_404(Article, pk=article_id) # jyn: 查询文章不存在则返回404
url = article.get_absolute_url() # jyn: 获取文章的绝对URL
return HttpResponseRedirect(url + "#comments") # jyn: 重定向到文章评论区锚点
>>>>>>> JYN_branch
# 4. 处理表单验证失败的逻辑当form.is_valid()为False时触发
def form_invalid(self, form): def form_invalid(self, form):
<<<<<<< HEAD
"""
表单数据验证失败如评论内容为空格式错误时的处理
作用重新渲染文章详情页带上错误的表单对象前端显示错误提示
"""
# 获取URL中的文章ID查询对应的文章
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
# 渲染模板传递错误的表单对象form和文章对象article前端可显示错误信息
=======
"""jyn: 表单数据验证失败时的处理逻辑,返回文章详情页并携带错误表单"""
article_id = self.kwargs['article_id'] # jyn: 从URL参数获取文章ID
article = get_object_or_404(Article, pk=article_id) # jyn: 查询目标文章
# jyn: 渲染文章详情页,传递错误表单和文章实例(前端显示错误信息)
>>>>>>> JYN_branch
return self.render_to_response({ return self.render_to_response({
'form': form, # 带有错误信息的表单 'form': form,
'article': article # 当前文章对象(用于渲染文章详情) 'article': article
}) })
# 5. 处理表单验证成功的逻辑当form.is_valid()为True时触发核心业务逻辑
def form_valid(self, form): def form_valid(self, form):
<<<<<<< HEAD """提交的数据验证合法后的逻辑"""
"""提交的数据验证合法后的逻辑:保存评论数据到数据库,处理评论状态和回复关联""" user = self.request.user
# 1. 获取当前登录用户(评论作者) author = BlogUser.objects.get(pk=user.pk)
user = self.request.user # 从请求对象中获取登录用户 article_id = self.kwargs['article_id']
author = BlogUser.objects.get(pk=user.pk) # 通过用户ID查询完整的BlogUser对象 article = get_object_or_404(Article, pk=article_id)
# 2. 获取当前评论对应的文章
article_id = self.kwargs['article_id'] # 从URL参数获取文章ID
article = get_object_or_404(Article, pk=article_id) # 查询文章不存在则404
# 3. 验证文章评论状态:若文章关闭评论或处于草稿状态,抛出验证错误
# 假设'article.comment_status == 'c''表示关闭评论,'article.status == 'c''表示文章草稿
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.") # 抛出异常,前端可捕获并显示
# 4. 保存评论先不提交到数据库False表示暂存内存后续补充字段
comment = form.save(False) # form.save(False)返回评论对象但不执行数据库INSERT
comment.article = article # 给评论关联文章补充form中未包含的article字段
# 5. 根据系统配置决定评论是否需要审核(直接启用或待审核)
from djangoblog.utils import get_blog_setting # 局部导入:避免循环引用
settings = get_blog_setting() # 获取博客系统全局配置如comment_need_review
if not settings.comment_need_review: # 若系统配置“评论无需审核”
comment.is_enable = True # 评论直接设为“启用”状态,前端可显示
comment.author = author # 给评论关联作者补充form中未包含的author字段
# 6. 处理评论回复若表单中包含父评论ID给当前评论关联父评论
if form.cleaned_data['parent_comment_id']: # 检查表单清理后的数据中是否有父评论ID
# 通过父评论ID查询对应的父评论对象
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']
)
# 注原代码此处不完整缺少赋值语句正确逻辑应为“comment.parent_comment = parent_comment”
# 补充后才会将当前评论与父评论关联,实现回复功能
comment.parent_comment = parent_comment
# 原代码缺失最终需调用comment.save()将评论数据提交到数据库,否则评论不会保存)
# comment.save()
=======
"""jyn: 表单数据验证合法后的核心逻辑,保存评论数据并跳转"""
user = self.request.user # jyn: 获取当前登录用户
author = BlogUser.objects.get(pk=user.pk) # jyn: 通过用户ID查询BlogUser实例评论作者
article_id = self.kwargs['article_id'] # jyn: 从URL参数获取文章ID
article = get_object_or_404(Article, pk=article_id) # jyn: 查询目标文章不存在返回404
# jyn: 校验文章评论状态和发布状态,禁止对关闭评论/草稿文章提交评论
if article.comment_status == 'c' or article.status == 'c': if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.") # jyn: 抛出验证错误,终止评论提交 raise ValidationError("该文章评论已关闭.")
# jyn: 不立即保存评论commit=False先补充关联字段
comment = form.save(False) comment = form.save(False)
comment.article = article # jyn: 关联评论所属文章 comment.article = article
# jyn: 根据博客设置决定评论是否需要审核(无需审核则直接启用)
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
settings = get_blog_setting() # jyn: 获取博客全局设置 settings = get_blog_setting()
if not settings.comment_need_review: if not settings.comment_need_review:
comment.is_enable = True # jyn: 无需审核时,评论直接启用 comment.is_enable = True
comment.author = author
comment.author = author # jyn: 关联评论作者
# jyn: 处理回复功能若存在父评论ID则关联父评论
if form.cleaned_data['parent_comment_id']: if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get( parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']) # jyn: 查询父评论 pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment # jyn: 关联父评论 comment.parent_comment = parent_comment
comment.save(True) # jyn: 最终保存评论数据到数据库
# jyn: 重定向到文章详情页的当前评论锚点(精准定位到新提交的评论) comment.save(True)
return HttpResponseRedirect( return HttpResponseRedirect(
"%s#div-comment-%d" % "%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk)) (article.get_absolute_url(), comment.pk))
>>>>>>> JYN_branch

@ -1,6 +1,7 @@
import logging import logging
from django.contrib import admin from django.contrib import admin
# Register your models here.
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
@ -8,30 +9,29 @@ logger = logging.getLogger(__name__)
class OAuthUserAdmin(admin.ModelAdmin): class OAuthUserAdmin(admin.ModelAdmin):
search_fields = ('nickname', 'email') # zy: 管理员搜索字段配置 search_fields = ('nickname', 'email')
list_per_page = 20 # zy: 分页设置每页20条记录 list_per_page = 20
list_display = ( list_display = (
'id', 'id',
'nickname', 'nickname',
'link_to_usermodel', # zy: 自定义字段-关联用户链接 'link_to_usermodel',
'show_user_image', # zy: 自定义字段-显示用户头像 'show_user_image',
'type', 'type',
'email', 'email',
) )
list_display_links = ('id', 'nickname') # zy: 可点击进入编辑页的字段 list_display_links = ('id', 'nickname')
list_filter = ('author', 'type',) # zy: 右侧筛选器字段 list_filter = ('author', 'type',)
readonly_fields = []
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
# zy: 重要将所有字段设为只读防止管理员修改OAuth用户数据
return list(self.readonly_fields) + \ return list(self.readonly_fields) + \
[field.name for field in obj._meta.fields] + \ [field.name for field in obj._meta.fields] + \
[field.name for field in obj._meta.many_to_many] [field.name for field in obj._meta.many_to_many]
def has_add_permission(self, request): def has_add_permission(self, request):
return False # zy: 关键禁止在admin中添加OAuth用户只能通过OAuth流程创建 return False
def link_to_usermodel(self, obj): def link_to_usermodel(self, obj):
# zy: 核心功能:生成关联用户的可点击链接
if obj.author: if obj.author:
info = (obj.author._meta.app_label, obj.author._meta.model_name) info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
@ -40,16 +40,15 @@ class OAuthUserAdmin(admin.ModelAdmin):
(link, obj.author.nickname if obj.author.nickname else obj.author.email)) (link, obj.author.nickname if obj.author.nickname else obj.author.email))
def show_user_image(self, obj): def show_user_image(self, obj):
# zy: 核心功能在admin列表中显示用户头像图片
img = obj.picture img = obj.picture
return format_html( return format_html(
u'<img src="%s" style="width:50px;height:50px"></img>' % u'<img src="%s" style="width:50px;height:50px"></img>' %
(img)) (img))
link_to_usermodel.short_description = '用户' # zy: 自定义列显示名称 link_to_usermodel.short_description = '用户'
show_user_image.short_description = '用户头像' show_user_image.short_description = '用户头像'
class OAuthConfigAdmin(admin.ModelAdmin): class OAuthConfigAdmin(admin.ModelAdmin):
list_display = ('type', 'appkey', 'appsecret', 'is_enable') # zy: OAuth配置列表显示字段 list_display = ('type', 'appkey', 'appsecret', 'is_enable')
list_filter = ('type',) # zy: 按OAuth类型筛选配置 list_filter = ('type',)

@ -2,4 +2,4 @@ from django.apps import AppConfig
class OauthConfig(AppConfig): class OauthConfig(AppConfig):
name = 'oauth' # zy定义Django应用的名称必须与文件夹名称一致 name = 'oauth'

@ -3,10 +3,10 @@ from django.forms import widgets
class RequireEmailForm(forms.Form): class RequireEmailForm(forms.Form):
email = forms.EmailField(label='电子邮箱', required=True) # zy必填邮箱字段用于OAuth登录时补充邮箱信息 email = forms.EmailField(label='电子邮箱', required=True)
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False) # zy隐藏字段用于传递OAuth用户ID oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(RequireEmailForm, self).__init__(*args, **kwargs) super(RequireEmailForm, self).__init__(*args, **kwargs)
self.fields['email'].widget = widgets.EmailInput( self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"}) # zy自定义邮箱输入框样式添加Bootstrap类 attrs={'placeholder': "email", "class": "form-control"})

@ -8,50 +8,50 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True # zy: 重要 - 标记为初始迁移文件 initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # zy: 关键依赖 - 依赖用户模型 migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='OAuthConfig', name='OAuthConfig',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # zy: 主键字段 - 自增BigAutoField ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')), # zy: 关键字段 - OAuth类型选择包含五种服务商 ('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),
('appkey', models.CharField(max_length=200, verbose_name='AppKey')), # zy: 重要字段 - 应用密钥ID ('appkey', models.CharField(max_length=200, verbose_name='AppKey')),
('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')), # zy: 重要字段 - 应用密钥,需安全存储 ('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')),
('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')), # zy: 关键字段 - OAuth回调地址默认值为百度 ('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), # zy: 控制字段 - 是否启用该OAuth配置 ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # zy: 时间字段 - 记录创建时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # zy: 时间字段 - 记录最后修改时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
], ],
options={ options={
'verbose_name': 'oauth配置', # zy: 单数显示名称 'verbose_name': 'oauth配置',
'verbose_name_plural': 'oauth配置', # zy: 复数显示名称 'verbose_name_plural': 'oauth配置',
'ordering': ['-created_time'], # zy: 默认排序 - 按创建时间倒序 'ordering': ['-created_time'],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='OAuthUser', name='OAuthUser',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # zy: 主键字段 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('openid', models.CharField(max_length=50)), # zy: 关键字段 - OAuth服务商提供的用户唯一标识 ('openid', models.CharField(max_length=50)),
('nickname', models.CharField(max_length=50, verbose_name='昵称')), # zy: 用户昵称字段 ('nickname', models.CharField(max_length=50, verbose_name='昵称')),
('token', models.CharField(blank=True, max_length=150, null=True)), # zy: 令牌字段 - 存储访问令牌,可为空 ('token', models.CharField(blank=True, max_length=150, null=True)),
('picture', models.CharField(blank=True, max_length=350, null=True)), # zy: 头像字段 - 存储头像URL地址 ('picture', models.CharField(blank=True, max_length=350, null=True)),
('type', models.CharField(max_length=50)), # zy: 关键字段 - OAuth服务商类型 ('type', models.CharField(max_length=50)),
('email', models.CharField(blank=True, max_length=50, null=True)), # zy: 邮箱字段 - 用户邮箱,可为空 ('email', models.CharField(blank=True, max_length=50, null=True)),
('metadata', models.TextField(blank=True, null=True)), # zy: 元数据字段 - 存储完整的OAuth用户信息 ('metadata', models.TextField(blank=True, null=True)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # zy: 创建时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # zy: 修改时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), # zy: 关键关联 - 关联系统用户,可为空(未绑定状态),级联删除 ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
], ],
options={ options={
'verbose_name': 'oauth用户', # zy: 单数显示名称 'verbose_name': 'oauth用户',
'verbose_name_plural': 'oauth用户', # zy: 复数显示名称 'verbose_name_plural': 'oauth用户',
'ordering': ['-created_time'], # zy: 默认排序 - 按创建时间倒序 'ordering': ['-created_time'],
}, },
), ),
] ]

@ -9,78 +9,78 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # zy: 关键依赖 - 依赖用户模型 migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('oauth', '0001_initial'), # zy: 重要依赖 - 依赖初始迁移文件 ('oauth', '0001_initial'),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='oauthconfig', name='oauthconfig',
options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'}, # zy: 修改排序字段为creation_time保持中文显示名称 options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='oauthuser', name='oauthuser',
options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'}, # zy: 修改排序字段为creation_time改为英文显示名称 options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'},
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='oauthconfig', model_name='oauthconfig',
name='created_time', # zy: 删除旧字段 - 原创建时间字段 name='created_time',
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='oauthconfig', model_name='oauthconfig',
name='last_mod_time', # zy: 删除旧字段 - 原修改时间字段 name='last_mod_time',
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='oauthuser', model_name='oauthuser',
name='created_time', # zy: 删除旧字段 - 原创建时间字段 name='created_time',
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='oauthuser', model_name='oauthuser',
name='last_mod_time', # zy: 删除旧字段 - 原修改时间字段 name='last_mod_time',
), ),
migrations.AddField( migrations.AddField(
model_name='oauthconfig', model_name='oauthconfig',
name='creation_time', # zy: 新增字段 - 标准化的创建时间字段 name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # zy: 使用国际化字段名称 field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
migrations.AddField( migrations.AddField(
model_name='oauthconfig', model_name='oauthconfig',
name='last_modify_time', # zy: 新增字段 - 标准化的最后修改时间字段 name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # zy: 使用国际化字段名称 field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
), ),
migrations.AddField( migrations.AddField(
model_name='oauthuser', model_name='oauthuser',
name='creation_time', # zy: 新增字段 - 标准化的创建时间字段 name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # zy: 使用国际化字段名称 field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
migrations.AddField( migrations.AddField(
model_name='oauthuser', model_name='oauthuser',
name='last_modify_time', # zy: 新增字段 - 标准化的最后修改时间字段 name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # zy: 使用国际化字段名称 field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='oauthconfig', model_name='oauthconfig',
name='callback_url', # zy: 修改字段 - 回调地址字段 name='callback_url',
field=models.CharField(default='', max_length=200, verbose_name='callback url'), # zy: 重要变更 - 默认值改为空字符串,使用国际化字段名称 field=models.CharField(default='', max_length=200, verbose_name='callback url'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='oauthconfig', model_name='oauthconfig',
name='is_enable', # zy: 修改字段 - 启用状态字段 name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'), # zy: 使用国际化字段名称 field=models.BooleanField(default=True, verbose_name='is enable'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='oauthconfig', model_name='oauthconfig',
name='type', # zy: 修改字段 - 类型选择字段 name='type',
field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'), # zy: 重要变更 - 选项值改为英文,使用国际化字段名称 field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='oauthuser', model_name='oauthuser',
name='author', # zy: 修改字段 - 关联用户字段 name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), # zy: 使用国际化字段名称 field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='oauthuser', model_name='oauthuser',
name='nickname', # zy: 修改字段 - 昵称字段 name='nickname',
field=models.CharField(max_length=50, verbose_name='nickname'), # zy: 使用国际化字段名称 field=models.CharField(max_length=50, verbose_name='nickname'),
), ),
] ]

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'), # zy: 依赖 - 依赖之前的第二次迁移文件 ('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='oauthuser', model_name='oauthuser',
name='nickname', # zy: 修改字段 - 用户昵称字段 name='nickname',
field=models.CharField(max_length=50, verbose_name='nick name'), # zy: 细微调整 - 将字段显示名称从'nickname'改为'nick name'(添加空格) field=models.CharField(max_length=50, verbose_name='nick name'),
), ),
] ]

@ -12,24 +12,24 @@ class OAuthUser(models.Model):
verbose_name=_('author'), verbose_name=_('author'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) # zy: 核心关联 - 关联系统用户,可为空(未绑定状态) on_delete=models.CASCADE)
openid = models.CharField(max_length=50) # zy: 关键字段 - OAuth服务商提供的用户唯一标识 openid = models.CharField(max_length=50)
nickname = models.CharField(max_length=50, verbose_name=_('nick name')) # zy: 用户昵称 nickname = models.CharField(max_length=50, verbose_name=_('nick name'))
token = models.CharField(max_length=150, null=True, blank=True) # zy: 重要 - 访问令牌用于API调用 token = models.CharField(max_length=150, null=True, blank=True)
picture = models.CharField(max_length=350, blank=True, null=True) # zy: 用户头像URL picture = models.CharField(max_length=350, blank=True, null=True)
type = models.CharField(blank=False, null=False, max_length=50) # zy: 关键 - OAuth类型weibo、github等 type = models.CharField(blank=False, null=False, max_length=50)
email = models.CharField(max_length=50, null=True, blank=True) # zy: 用户邮箱 email = models.CharField(max_length=50, null=True, blank=True)
metadata = models.TextField(null=True, blank=True) # zy: 重要 - 存储完整的OAuth用户信息JSON metadata = models.TextField(null=True, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now) # zy: 记录创建时间 creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # zy: 最后修改时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now)
def __str__(self): def __str__(self):
return self.nickname # zy: 对象显示为昵称 return self.nickname
class Meta: class Meta:
verbose_name = _('oauth user') # zy: 单数显示名称 verbose_name = _('oauth user')
verbose_name_plural = verbose_name # zy: 复数显示名称 verbose_name_plural = verbose_name
ordering = ['-creation_time'] # zy: 默认按创建时间倒序排列 ordering = ['-creation_time']
class OAuthConfig(models.Model): class OAuthConfig(models.Model):
@ -39,30 +39,29 @@ class OAuthConfig(models.Model):
('github', 'GitHub'), ('github', 'GitHub'),
('facebook', 'FaceBook'), ('facebook', 'FaceBook'),
('qq', 'QQ'), ('qq', 'QQ'),
) # zy: 核心配置 - 支持的OAuth类型选项 )
type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') # zy: 关键 - OAuth服务商类型 type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
appkey = models.CharField(max_length=200, verbose_name='AppKey') # zy: 重要 - 应用密钥ID appkey = models.CharField(max_length=200, verbose_name='AppKey')
appsecret = models.CharField(max_length=200, verbose_name='AppSecret') # zy: 重要 - 应用密钥,需加密存储 appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
callback_url = models.CharField( callback_url = models.CharField(
max_length=200, max_length=200,
verbose_name=_('callback url'), verbose_name=_('callback url'),
blank=False, blank=False,
default='') # zy: 关键 - OAuth回调URL default='')
is_enable = models.BooleanField( is_enable = models.BooleanField(
_('is enable'), default=True, blank=False, null=False) # zy: 重要 - 控制该配置是否启用 _('is enable'), default=True, blank=False, null=False)
creation_time = models.DateTimeField(_('creation time'), default=now) # zy: 配置创建时间 creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # zy: 配置最后修改时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now)
def clean(self): def clean(self):
# zy: 核心验证 - 确保同类型OAuth配置唯一防止重复配置
if OAuthConfig.objects.filter( if OAuthConfig.objects.filter(
type=self.type).exclude(id=self.id).count(): type=self.type).exclude(id=self.id).count():
raise ValidationError(_(self.type + _('already exists'))) raise ValidationError(_(self.type + _('already exists')))
def __str__(self): def __str__(self):
return self.type # zy: 对象显示为类型名称 return self.type
class Meta: class Meta:
verbose_name = 'oauth配置' # zy: 中文显示名称 verbose_name = 'oauth配置'
verbose_name_plural = verbose_name # zy: 复数显示名称 verbose_name_plural = verbose_name
ordering = ['-creation_time'] # zy: 按创建时间倒序排列 ordering = ['-creation_time']

@ -15,73 +15,73 @@ logger = logging.getLogger(__name__)
class OAuthAccessTokenException(Exception): class OAuthAccessTokenException(Exception):
''' '''
oauth授权失败异常 oauth授权失败异常
''' # zy: 核心异常类 - 专门处理OAuth授权过程中的令牌获取失败 '''
class BaseOauthManager(metaclass=ABCMeta): class BaseOauthManager(metaclass=ABCMeta):
"""获取用户授权""" """获取用户授权"""
AUTH_URL = None # zy: 关键URL - OAuth授权页面地址子类必须实现 AUTH_URL = None
"""获取token""" """获取token"""
TOKEN_URL = None # zy: 关键URL - 获取访问令牌的API地址子类必须实现 TOKEN_URL = None
"""获取用户信息""" """获取用户信息"""
API_URL = None # zy: 关键URL - 获取用户信息的API地址子类必须实现 API_URL = None
'''icon图标名''' '''icon图标名'''
ICON_NAME = None # zy: 标识符 - OAuth服务商类型标识用于匹配配置 ICON_NAME = None
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
self.access_token = access_token # zy: 核心属性 - OAuth访问令牌 self.access_token = access_token
self.openid = openid # zy: 核心属性 - 用户在OAuth服务商的唯一ID self.openid = openid
@property @property
def is_access_token_set(self): def is_access_token_set(self):
return self.access_token is not None # zy: 重要检查 - 判断访问令牌是否已设置 return self.access_token is not None
@property @property
def is_authorized(self): def is_authorized(self):
return self.is_access_token_set and self.access_token is not None and self.openid is not None # zy: 核心检查 - 判断是否已完成授权 return self.is_access_token_set and self.access_token is not None and self.openid is not None
@abstractmethod @abstractmethod
def get_authorization_url(self, nexturl='/'): def get_authorization_url(self, nexturl='/'):
pass # zy: 抽象方法 - 生成授权URL子类必须实现 pass
@abstractmethod @abstractmethod
def get_access_token_by_code(self, code): def get_access_token_by_code(self, code):
pass # zy: 抽象方法 - 通过授权码获取访问令牌,子类必须实现 pass
@abstractmethod @abstractmethod
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
pass # zy: 抽象方法 - 获取用户信息,子类必须实现 pass
@abstractmethod @abstractmethod
def get_picture(self, metadata): def get_picture(self, metadata):
pass # zy: 抽象方法 - 从元数据中提取头像,子类必须实现 pass
def do_get(self, url, params, headers=None): def do_get(self, url, params, headers=None):
rsp = requests.get(url=url, params=params, headers=headers) # zy: 核心方法 - 执行GET请求 rsp = requests.get(url=url, params=params, headers=headers)
logger.info(rsp.text) # zy: 重要日志 - 记录API响应 logger.info(rsp.text)
return rsp.text return rsp.text
def do_post(self, url, params, headers=None): def do_post(self, url, params, headers=None):
rsp = requests.post(url, params, headers=headers) # zy: 核心方法 - 执行POST请求 rsp = requests.post(url, params, headers=headers)
logger.info(rsp.text) # zy: 重要日志 - 记录API响应 logger.info(rsp.text)
return rsp.text return rsp.text
def get_config(self): def get_config(self):
value = OAuthConfig.objects.filter(type=self.ICON_NAME) # zy: 关键查询 - 获取对应类型的OAuth配置 value = OAuthConfig.objects.filter(type=self.ICON_NAME)
return value[0] if value else None # zy: 返回第一个配置或None return value[0] if value else None
class WBOauthManager(BaseOauthManager): class WBOauthManager(BaseOauthManager):
AUTH_URL = 'https://api.weibo.com/oauth2/authorize' # zy: 微博授权URL AUTH_URL = 'https://api.weibo.com/oauth2/authorize'
TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' # zy: 微博令牌获取URL TOKEN_URL = 'https://api.weibo.com/oauth2/access_token'
API_URL = 'https://api.weibo.com/2/users/show.json' # zy: 微博用户信息API API_URL = 'https://api.weibo.com/2/users/show.json'
ICON_NAME = 'weibo' # zy: 微博标识 ICON_NAME = 'weibo'
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
config = self.get_config() config = self.get_config()
self.client_id = config.appkey if config else '' # zy: 关键配置 - 应用Key self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else '' # zy: 关键配置 - 应用Secret self.client_secret = config.appsecret if config else ''
self.callback_url = config.callback_url if config else '' # zy: 关键配置 - 回调地址 self.callback_url = config.callback_url if config else ''
super( super(
WBOauthManager, WBOauthManager,
self).__init__( self).__init__(
@ -92,12 +92,13 @@ class WBOauthManager(BaseOauthManager):
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'response_type': 'code', 'response_type': 'code',
'redirect_uri': self.callback_url + '&next_url=' + nexturl # zy: 重要参数 - 携带next_url用于跳转 'redirect_uri': self.callback_url + '&next_url=' + nexturl
} }
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) # zy: 生成完整的授权URL url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url return url
def get_access_token_by_code(self, code): def get_access_token_by_code(self, code):
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.client_secret, 'client_secret': self.client_secret,
@ -105,19 +106,19 @@ class WBOauthManager(BaseOauthManager):
'code': code, 'code': code,
'redirect_uri': self.callback_url 'redirect_uri': self.callback_url
} }
rsp = self.do_post(self.TOKEN_URL, params) # zy: 关键调用 - 获取访问令牌 rsp = self.do_post(self.TOKEN_URL, params)
obj = json.loads(rsp) obj = json.loads(rsp)
if 'access_token' in obj: if 'access_token' in obj:
self.access_token = str(obj['access_token']) # zy: 保存访问令牌 self.access_token = str(obj['access_token'])
self.openid = str(obj['uid']) # zy: 保存用户ID self.openid = str(obj['uid'])
return self.get_oauth_userinfo() # zy: 重要流程 - 获取令牌后立即获取用户信息 return self.get_oauth_userinfo()
else: else:
raise OAuthAccessTokenException(rsp) # zy: 令牌获取失败抛出异常 raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
if not self.is_authorized: if not self.is_authorized:
return None # zy: 安全检查 - 确保已授权 return None
params = { params = {
'uid': self.openid, 'uid': self.openid,
'access_token': self.access_token 'access_token': self.access_token
@ -125,24 +126,24 @@ class WBOauthManager(BaseOauthManager):
rsp = self.do_get(self.API_URL, params) rsp = self.do_get(self.API_URL, params)
try: try:
datas = json.loads(rsp) datas = json.loads(rsp)
user = OAuthUser() # zy: 创建OAuth用户对象 user = OAuthUser()
user.metadata = rsp # zy: 保存原始响应数据 user.metadata = rsp
user.picture = datas['avatar_large'] # zy: 设置用户头像 user.picture = datas['avatar_large']
user.nickname = datas['screen_name'] # zy: 设置用户昵称 user.nickname = datas['screen_name']
user.openid = datas['id'] # zy: 设置开放ID user.openid = datas['id']
user.type = 'weibo' # zy: 设置类型 user.type = 'weibo'
user.token = self.access_token # zy: 设置访问令牌 user.token = self.access_token
if 'email' in datas and datas['email']: if 'email' in datas and datas['email']:
user.email = datas['email'] # zy: 设置邮箱(如果有) user.email = datas['email']
return user return user
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
logger.error('weibo oauth error.rsp:' + rsp) # zy: 重要错误日志 logger.error('weibo oauth error.rsp:' + rsp)
return None return None
def get_picture(self, metadata): def get_picture(self, metadata):
datas = json.loads(metadata) datas = json.loads(metadata)
return datas['avatar_large'] # zy: 从元数据中提取头像URL return datas['avatar_large']
class ProxyManagerMixin: class ProxyManagerMixin:
@ -150,27 +151,27 @@ class ProxyManagerMixin:
if os.environ.get("HTTP_PROXY"): if os.environ.get("HTTP_PROXY"):
self.proxies = { self.proxies = {
"http": os.environ.get("HTTP_PROXY"), "http": os.environ.get("HTTP_PROXY"),
"https": os.environ.get("HTTP_PROXY") # zy: 重要配置 - 设置HTTP代理 "https": os.environ.get("HTTP_PROXY")
} }
else: else:
self.proxies = None self.proxies = None
def do_get(self, url, params, headers=None): def do_get(self, url, params, headers=None):
rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies) # zy: 带代理的GET请求 rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies)
logger.info(rsp.text) logger.info(rsp.text)
return rsp.text return rsp.text
def do_post(self, url, params, headers=None): def do_post(self, url, params, headers=None):
rsp = requests.post(url, params, headers=headers, proxies=self.proxies) # zy: 带代理的POST请求 rsp = requests.post(url, params, headers=headers, proxies=self.proxies)
logger.info(rsp.text) logger.info(rsp.text)
return rsp.text return rsp.text
class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth' # zy: Google授权URL AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' # zy: Google令牌URL TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo' # zy: Google用户信息API API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
ICON_NAME = 'google' # zy: Google标识 ICON_NAME = 'google'
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
config = self.get_config() config = self.get_config()
@ -188,7 +189,7 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
'client_id': self.client_id, 'client_id': self.client_id,
'response_type': 'code', 'response_type': 'code',
'redirect_uri': self.callback_url, 'redirect_uri': self.callback_url,
'scope': 'openid email', # zy: 重要参数 - 请求openid和email权限 'scope': 'openid email',
} }
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url return url
@ -199,6 +200,7 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
'client_secret': self.client_secret, 'client_secret': self.client_secret,
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'code': code, 'code': code,
'redirect_uri': self.callback_url 'redirect_uri': self.callback_url
} }
rsp = self.do_post(self.TOKEN_URL, params) rsp = self.do_post(self.TOKEN_URL, params)
@ -207,9 +209,9 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
if 'access_token' in obj: if 'access_token' in obj:
self.access_token = str(obj['access_token']) self.access_token = str(obj['access_token'])
self.openid = str(obj['id_token']) # zy: Google使用id_token作为openid self.openid = str(obj['id_token'])
logger.info(self.ICON_NAME + ' oauth ' + rsp) logger.info(self.ICON_NAME + ' oauth ' + rsp)
return self.access_token # zy: 返回访问令牌 return self.access_token
else: else:
raise OAuthAccessTokenException(rsp) raise OAuthAccessTokenException(rsp)
@ -221,12 +223,13 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
} }
rsp = self.do_get(self.API_URL, params) rsp = self.do_get(self.API_URL, params)
try: try:
datas = json.loads(rsp) datas = json.loads(rsp)
user = OAuthUser() user = OAuthUser()
user.metadata = rsp user.metadata = rsp
user.picture = datas['picture'] # zy: Google头像字段 user.picture = datas['picture']
user.nickname = datas['name'] user.nickname = datas['name']
user.openid = datas['sub'] # zy: Google用户ID字段 user.openid = datas['sub']
user.token = self.access_token user.token = self.access_token
user.type = 'google' user.type = 'google'
if datas['email']: if datas['email']:
@ -243,10 +246,10 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
AUTH_URL = 'https://github.com/login/oauth/authorize' # zy: GitHub授权URL AUTH_URL = 'https://github.com/login/oauth/authorize'
TOKEN_URL = 'https://github.com/login/oauth/access_token' # zy: GitHub令牌URL TOKEN_URL = 'https://github.com/login/oauth/access_token'
API_URL = 'https://api.github.com/user' # zy: GitHub用户信息API API_URL = 'https://api.github.com/user'
ICON_NAME = 'github' # zy: GitHub标识 ICON_NAME = 'github'
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
config = self.get_config() config = self.get_config()
@ -263,8 +266,8 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'response_type': 'code', 'response_type': 'code',
'redirect_uri': f'{self.callback_url}&next_url={next_url}', # zy: 使用f-string格式化URL 'redirect_uri': f'{self.callback_url}&next_url={next_url}',
'scope': 'user' # zy: 请求user权限范围 'scope': 'user'
} }
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url return url
@ -275,12 +278,13 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
'client_secret': self.client_secret, 'client_secret': self.client_secret,
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'code': code, 'code': code,
'redirect_uri': self.callback_url 'redirect_uri': self.callback_url
} }
rsp = self.do_post(self.TOKEN_URL, params) rsp = self.do_post(self.TOKEN_URL, params)
from urllib import parse from urllib import parse
r = parse.parse_qs(rsp) # zy: 重要 - GitHub返回的是查询字符串格式 r = parse.parse_qs(rsp)
if 'access_token' in r: if 'access_token' in r:
self.access_token = (r['access_token'][0]) self.access_token = (r['access_token'][0])
return self.access_token return self.access_token
@ -288,13 +292,14 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
raise OAuthAccessTokenException(rsp) raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
rsp = self.do_get(self.API_URL, params={}, headers={ rsp = self.do_get(self.API_URL, params={}, headers={
"Authorization": "token " + self.access_token # zy: 关键 - GitHub需要在header中传递token "Authorization": "token " + self.access_token
}) })
try: try:
datas = json.loads(rsp) datas = json.loads(rsp)
user = OAuthUser() user = OAuthUser()
user.picture = datas['avatar_url'] # zy: GitHub头像字段 user.picture = datas['avatar_url']
user.nickname = datas['name'] user.nickname = datas['name']
user.openid = datas['id'] user.openid = datas['id']
user.type = 'github' user.type = 'github'
@ -314,10 +319,10 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth' # zy: Facebook授权URL指定API版本 AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth'
TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token' # zy: Facebook令牌URL TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token'
API_URL = 'https://graph.facebook.com/me' # zy: Facebook用户信息API API_URL = 'https://graph.facebook.com/me'
ICON_NAME = 'facebook' # zy: Facebook标识 ICON_NAME = 'facebook'
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
config = self.get_config() config = self.get_config()
@ -335,7 +340,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
'client_id': self.client_id, 'client_id': self.client_id,
'response_type': 'code', 'response_type': 'code',
'redirect_uri': self.callback_url, 'redirect_uri': self.callback_url,
'scope': 'email,public_profile' # zy: 请求邮箱和公开资料权限 'scope': 'email,public_profile'
} }
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url return url
@ -344,7 +349,9 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.client_secret, 'client_secret': self.client_secret,
# 'grant_type': 'authorization_code',
'code': code, 'code': code,
'redirect_uri': self.callback_url 'redirect_uri': self.callback_url
} }
rsp = self.do_post(self.TOKEN_URL, params) rsp = self.do_post(self.TOKEN_URL, params)
@ -360,7 +367,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
params = { params = {
'access_token': self.access_token, 'access_token': self.access_token,
'fields': 'id,name,picture,email' # zy: 重要 - 指定需要返回的字段 'fields': 'id,name,picture,email'
} }
try: try:
rsp = self.do_get(self.API_URL, params) rsp = self.do_get(self.API_URL, params)
@ -374,7 +381,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
if 'email' in datas and datas['email']: if 'email' in datas and datas['email']:
user.email = datas['email'] user.email = datas['email']
if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']: if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']:
user.picture = str(datas['picture']['data']['url']) # zy: Facebook头像嵌套在data对象中 user.picture = str(datas['picture']['data']['url'])
return user return user
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
@ -386,11 +393,11 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
class QQOauthManager(BaseOauthManager): class QQOauthManager(BaseOauthManager):
AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize' # zy: QQ授权URL AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize'
TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' # zy: QQ令牌URL TOKEN_URL = 'https://graph.qq.com/oauth2.0/token'
API_URL = 'https://graph.qq.com/user/get_user_info' # zy: QQ用户信息API API_URL = 'https://graph.qq.com/user/get_user_info'
OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' # zy: 关键 - QQ需要单独获取openid OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me'
ICON_NAME = 'qq' # zy: QQ标识 ICON_NAME = 'qq'
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
config = self.get_config() config = self.get_config()
@ -422,7 +429,7 @@ class QQOauthManager(BaseOauthManager):
} }
rsp = self.do_get(self.TOKEN_URL, params) rsp = self.do_get(self.TOKEN_URL, params)
if rsp: if rsp:
d = urllib.parse.parse_qs(rsp) # zy: 重要 - QQ返回查询字符串格式 d = urllib.parse.parse_qs(rsp)
if 'access_token' in d: if 'access_token' in d:
token = d['access_token'] token = d['access_token']
self.access_token = token[0] self.access_token = token[0]
@ -440,18 +447,18 @@ class QQOauthManager(BaseOauthManager):
rsp = rsp.replace( rsp = rsp.replace(
'callback(', '').replace( 'callback(', '').replace(
')', '').replace( ')', '').replace(
';', '') # zy: 关键处理 - 清理JSONP响应格式 ';', '')
obj = json.loads(rsp) obj = json.loads(rsp)
openid = str(obj['openid']) openid = str(obj['openid'])
self.openid = openid self.openid = openid
return openid return openid
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
openid = self.get_open_id() # zy: 重要 - 先获取openid openid = self.get_open_id()
if openid: if openid:
params = { params = {
'access_token': self.access_token, 'access_token': self.access_token,
'oauth_consumer_key': self.client_id, # zy: QQ需要传递client_id作为oauth_consumer_key 'oauth_consumer_key': self.client_id,
'openid': self.openid 'openid': self.openid
} }
rsp = self.do_get(self.API_URL, params) rsp = self.do_get(self.API_URL, params)
@ -466,7 +473,7 @@ class QQOauthManager(BaseOauthManager):
if 'email' in obj: if 'email' in obj:
user.email = obj['email'] user.email = obj['email']
if 'figureurl' in obj: if 'figureurl' in obj:
user.picture = str(obj['figureurl']) # zy: QQ头像字段 user.picture = str(obj['figureurl'])
return user return user
def get_picture(self, metadata): def get_picture(self, metadata):
@ -476,22 +483,22 @@ class QQOauthManager(BaseOauthManager):
@cache_decorator(expiration=100 * 60) @cache_decorator(expiration=100 * 60)
def get_oauth_apps(): def get_oauth_apps():
configs = OAuthConfig.objects.filter(is_enable=True).all() # zy: 关键查询 - 只获取启用的OAuth配置 configs = OAuthConfig.objects.filter(is_enable=True).all()
if not configs: if not configs:
return [] return []
configtypes = [x.type for x in configs] # zy: 提取配置类型列表 configtypes = [x.type for x in configs]
applications = BaseOauthManager.__subclasses__() # zy: 重要 - 获取所有子类OAuth管理器 applications = BaseOauthManager.__subclasses__()
apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes] # zy: 创建对应的管理器实例 apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes]
return apps return apps
def get_manager_by_type(type): def get_manager_by_type(type):
applications = get_oauth_apps() # zy: 获取所有可用的OAuth应用 applications = get_oauth_apps()
if applications: if applications:
finds = list( finds = list(
filter( filter(
lambda x: x.ICON_NAME.lower() == type.lower(), # zy: 按类型过滤 lambda x: x.ICON_NAME.lower() == type.lower(),
applications)) applications))
if finds: if finds:
return finds[0] # zy: 返回第一个匹配的管理器 return finds[0]
return None # zy: 未找到返回None return None

@ -3,20 +3,20 @@ from django.urls import reverse
from oauth.oauthmanager import get_oauth_apps from oauth.oauthmanager import get_oauth_apps
register = template.Library() # zy: 重要 - 创建Django模板标签库实例 register = template.Library()
@register.inclusion_tag('oauth/oauth_applications.html') # zy: 核心装饰器 - 注册包含标签,指定渲染模板 @register.inclusion_tag('oauth/oauth_applications.html')
def load_oauth_applications(request): # zy: 关键函数 - 加载OAuth应用列表的模板标签 def load_oauth_applications(request):
applications = get_oauth_apps() # zy: 重要调用 - 获取所有启用的OAuth应用管理器 applications = get_oauth_apps()
if applications: # zy: 安全检查 - 确保有可用的OAuth应用 if applications:
baseurl = reverse('oauth:oauthlogin') # zy: 关键调用 - 生成OAuth登录入口URL baseurl = reverse('oauth:oauthlogin')
path = request.get_full_path() # zy: 获取当前完整路径,用于登录后跳转 path = request.get_full_path()
apps = list(map(lambda x: (x.ICON_NAME, '{baseurl}?type={type}&next_url={next}'.format( apps = list(map(lambda x: (x.ICON_NAME, '{baseurl}?type={type}&next_url={next}'.format(
baseurl=baseurl, type=x.ICON_NAME, next=path)), applications)) # zy: 核心逻辑 - 生成每个OAuth应用的登录URL baseurl=baseurl, type=x.ICON_NAME, next=path)), applications))
else: else:
apps = [] # zy: 默认值 - 无可用应用时返回空列表 apps = []
return { return {
'apps': apps # zy: 返回字典 - 包含应用列表供模板使用 'apps': apps
} }

@ -14,64 +14,64 @@ from oauth.oauthmanager import BaseOauthManager
# Create your tests here. # Create your tests here.
class OAuthConfigTest(TestCase): class OAuthConfigTest(TestCase):
def setUp(self): def setUp(self):
self.client = Client() # zy: 测试客户端 - 用于模拟HTTP请求 self.client = Client()
self.factory = RequestFactory() # zy: 请求工厂 - 用于创建请求对象 self.factory = RequestFactory()
def test_oauth_login_test(self): def test_oauth_login_test(self):
c = OAuthConfig() # zy: 创建OAuth配置对象 c = OAuthConfig()
c.type = 'weibo' # zy: 设置OAuth类型为微博 c.type = 'weibo'
c.appkey = 'appkey' # zy: 设置应用Key c.appkey = 'appkey'
c.appsecret = 'appsecret' # zy: 设置应用Secret c.appsecret = 'appsecret'
c.save() # zy: 保存配置到数据库 c.save()
response = self.client.get('/oauth/oauthlogin?type=weibo') # zy: 模拟OAuth登录请求 response = self.client.get('/oauth/oauthlogin?type=weibo')
self.assertEqual(response.status_code, 302) # zy: 断言重定向状态码 self.assertEqual(response.status_code, 302)
self.assertTrue("api.weibo.com" in response.url) # zy: 断言跳转到微博授权页面 self.assertTrue("api.weibo.com" in response.url)
response = self.client.get('/oauth/authorize?type=weibo&code=code') # zy: 模拟授权回调请求 response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302) # zy: 断言重定向状态码 self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/') # zy: 断言跳转到首页 self.assertEqual(response.url, '/')
class OauthLoginTest(TestCase): class OauthLoginTest(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.client = Client() self.client = Client()
self.factory = RequestFactory() self.factory = RequestFactory()
self.apps = self.init_apps() # zy: 初始化所有OAuth应用 self.apps = self.init_apps()
def init_apps(self): def init_apps(self):
applications = [p() for p in BaseOauthManager.__subclasses__()] # zy: 重要 - 获取所有OAuth管理器子类的实例 applications = [p() for p in BaseOauthManager.__subclasses__()]
for application in applications: for application in applications:
c = OAuthConfig() c = OAuthConfig()
c.type = application.ICON_NAME.lower() # zy: 设置配置类型 c.type = application.ICON_NAME.lower()
c.appkey = 'appkey' # zy: 模拟应用Key c.appkey = 'appkey'
c.appsecret = 'appsecret' # zy: 模拟应用Secret c.appsecret = 'appsecret'
c.save() # zy: 保存每个OAuth配置 c.save()
return applications return applications
def get_app_by_type(self, type): def get_app_by_type(self, type):
for app in self.apps: for app in self.apps:
if app.ICON_NAME.lower() == type: # zy: 按类型查找对应的OAuth管理器 if app.ICON_NAME.lower() == type:
return app return app
@patch("oauth.oauthmanager.WBOauthManager.do_post") # zy: 关键 - 模拟微博POST请求 @patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get") # zy: 关键 - 模拟微博GET请求 @patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_login(self, mock_do_get, mock_do_post): def test_weibo_login(self, mock_do_get, mock_do_post):
weibo_app = self.get_app_by_type('weibo') # zy: 获取微博OAuth管理器 weibo_app = self.get_app_by_type('weibo')
assert weibo_app assert weibo_app
url = weibo_app.get_authorization_url() # zy: 获取授权URL url = weibo_app.get_authorization_url()
mock_do_post.return_value = json.dumps({"access_token": "access_token", mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid" "uid": "uid"
}) # zy: 模拟令牌接口返回 })
mock_do_get.return_value = json.dumps({ mock_do_get.return_value = json.dumps({
"avatar_large": "avatar_large", "avatar_large": "avatar_large",
"screen_name": "screen_name", "screen_name": "screen_name",
"id": "id", "id": "id",
"email": "email", "email": "email",
}) # zy: 模拟用户信息接口返回 })
userinfo = weibo_app.get_access_token_by_code('code') # zy: 关键调用 - 通过授权码获取用户信息 userinfo = weibo_app.get_access_token_by_code('code')
self.assertEqual(userinfo.token, 'access_token') # zy: 断言令牌正确 self.assertEqual(userinfo.token, 'access_token')
self.assertEqual(userinfo.openid, 'id') # zy: 断言用户ID正确 self.assertEqual(userinfo.openid, 'id')
@patch("oauth.oauthmanager.GoogleOauthManager.do_post") @patch("oauth.oauthmanager.GoogleOauthManager.do_post")
@patch("oauth.oauthmanager.GoogleOauthManager.do_get") @patch("oauth.oauthmanager.GoogleOauthManager.do_get")
@ -81,18 +81,18 @@ class OauthLoginTest(TestCase):
url = google_app.get_authorization_url() url = google_app.get_authorization_url()
mock_do_post.return_value = json.dumps({ mock_do_post.return_value = json.dumps({
"access_token": "access_token", "access_token": "access_token",
"id_token": "id_token", # zy: Google特有字段 - ID令牌 "id_token": "id_token",
}) })
mock_do_get.return_value = json.dumps({ mock_do_get.return_value = json.dumps({
"picture": "picture", "picture": "picture",
"name": "name", "name": "name",
"sub": "sub", # zy: Google用户ID字段 "sub": "sub",
"email": "email", "email": "email",
}) })
token = google_app.get_access_token_by_code('code') token = google_app.get_access_token_by_code('code')
userinfo = google_app.get_oauth_userinfo() # zy: 重要 - 分开获取用户信息 userinfo = google_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token') self.assertEqual(userinfo.token, 'access_token')
self.assertEqual(userinfo.openid, 'sub') # zy: 断言Google用户ID self.assertEqual(userinfo.openid, 'sub')
@patch("oauth.oauthmanager.GitHubOauthManager.do_post") @patch("oauth.oauthmanager.GitHubOauthManager.do_post")
@patch("oauth.oauthmanager.GitHubOauthManager.do_get") @patch("oauth.oauthmanager.GitHubOauthManager.do_get")
@ -100,18 +100,18 @@ class OauthLoginTest(TestCase):
github_app = self.get_app_by_type('github') github_app = self.get_app_by_type('github')
assert github_app assert github_app
url = github_app.get_authorization_url() url = github_app.get_authorization_url()
self.assertTrue("github.com" in url) # zy: 断言GitHub授权URL self.assertTrue("github.com" in url)
self.assertTrue("client_id" in url) # zy: 断言包含client_id参数 self.assertTrue("client_id" in url)
mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer" # zy: 重要 - GitHub返回查询字符串格式 mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer"
mock_do_get.return_value = json.dumps({ mock_do_get.return_value = json.dumps({
"avatar_url": "avatar_url", # zy: GitHub头像字段 "avatar_url": "avatar_url",
"name": "name", "name": "name",
"id": "id", "id": "id",
"email": "email", "email": "email",
}) })
token = github_app.get_access_token_by_code('code') token = github_app.get_access_token_by_code('code')
userinfo = github_app.get_oauth_userinfo() userinfo = github_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') # zy: 断言GitHub令牌格式 self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a')
self.assertEqual(userinfo.openid, 'id') self.assertEqual(userinfo.openid, 'id')
@patch("oauth.oauthmanager.FaceBookOauthManager.do_post") @patch("oauth.oauthmanager.FaceBookOauthManager.do_post")
@ -120,7 +120,7 @@ class OauthLoginTest(TestCase):
facebook_app = self.get_app_by_type('facebook') facebook_app = self.get_app_by_type('facebook')
assert facebook_app assert facebook_app
url = facebook_app.get_authorization_url() url = facebook_app.get_authorization_url()
self.assertTrue("facebook.com" in url) # zy: 断言Facebook授权URL self.assertTrue("facebook.com" in url)
mock_do_post.return_value = json.dumps({ mock_do_post.return_value = json.dumps({
"access_token": "access_token", "access_token": "access_token",
}) })
@ -130,7 +130,7 @@ class OauthLoginTest(TestCase):
"email": "email", "email": "email",
"picture": { "picture": {
"data": { "data": {
"url": "url" # zy: Facebook头像嵌套结构 "url": "url"
} }
} }
}) })
@ -139,20 +139,20 @@ class OauthLoginTest(TestCase):
self.assertEqual(userinfo.token, 'access_token') self.assertEqual(userinfo.token, 'access_token')
@patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[ @patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[
'access_token=access_token&expires_in=3600', # zy: 第一次调用 - 获取令牌 'access_token=access_token&expires_in=3600',
'callback({"client_id":"appid","openid":"openid"} );', # zy: 第二次调用 - 获取openidJSONP格式 'callback({"client_id":"appid","openid":"openid"} );',
json.dumps({ json.dumps({
"nickname": "nickname", "nickname": "nickname",
"email": "email", "email": "email",
"figureurl": "figureurl", # zy: QQ头像字段 "figureurl": "figureurl",
"openid": "openid", "openid": "openid",
}) # zy: 第三次调用 - 获取用户信息 })
]) ])
def test_qq_login(self, mock_do_get): def test_qq_login(self, mock_do_get):
qq_app = self.get_app_by_type('qq') qq_app = self.get_app_by_type('qq')
assert qq_app assert qq_app
url = qq_app.get_authorization_url() url = qq_app.get_authorization_url()
self.assertTrue("qq.com" in url) # zy: 断言QQ授权URL self.assertTrue("qq.com" in url)
token = qq_app.get_access_token_by_code('code') token = qq_app.get_access_token_by_code('code')
userinfo = qq_app.get_oauth_userinfo() userinfo = qq_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token') self.assertEqual(userinfo.token, 'access_token')
@ -160,7 +160,7 @@ class OauthLoginTest(TestCase):
@patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get") @patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post): def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post):
# zy: 重要测试 - 测试带邮箱的微博登录完整流程
mock_do_post.return_value = json.dumps({"access_token": "access_token", mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid" "uid": "uid"
}) })
@ -168,7 +168,7 @@ class OauthLoginTest(TestCase):
"avatar_large": "avatar_large", "avatar_large": "avatar_large",
"screen_name": "screen_name1", "screen_name": "screen_name1",
"id": "id", "id": "id",
"email": "email", # zy: 包含邮箱信息 "email": "email",
} }
mock_do_get.return_value = json.dumps(mock_user_info) mock_do_get.return_value = json.dumps(mock_user_info)
@ -176,18 +176,17 @@ class OauthLoginTest(TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue("api.weibo.com" in response.url) self.assertTrue("api.weibo.com" in response.url)
response = self.client.get('/oauth/authorize?type=weibo&code=code') # zy: 模拟授权回调 response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/') # zy: 直接跳转首页(有邮箱) self.assertEqual(response.url, '/')
user = auth.get_user(self.client) # zy: 获取当前登录用户 user = auth.get_user(self.client)
assert user.is_authenticated # zy: 断言用户已认证 assert user.is_authenticated
self.assertTrue(user.is_authenticated) self.assertTrue(user.is_authenticated)
self.assertEqual(user.username, mock_user_info['screen_name']) # zy: 断言用户名 self.assertEqual(user.username, mock_user_info['screen_name'])
self.assertEqual(user.email, mock_user_info['email']) # zy: 断言邮箱 self.assertEqual(user.email, mock_user_info['email'])
self.client.logout() # zy: 注销用户 self.client.logout()
# zy: 重复登录测试
response = self.client.get('/oauth/authorize?type=weibo&code=code') response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/') self.assertEqual(response.url, '/')
@ -201,7 +200,7 @@ class OauthLoginTest(TestCase):
@patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get") @patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post): def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post):
# zy: 重要测试 - 测试不带邮箱的微博登录流程(需要补充邮箱)
mock_do_post.return_value = json.dumps({"access_token": "access_token", mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid" "uid": "uid"
}) })
@ -209,7 +208,6 @@ class OauthLoginTest(TestCase):
"avatar_large": "avatar_large", "avatar_large": "avatar_large",
"screen_name": "screen_name1", "screen_name": "screen_name1",
"id": "id", "id": "id",
# zy: 故意不包含邮箱字段
} }
mock_do_get.return_value = json.dumps(mock_user_info) mock_do_get.return_value = json.dumps(mock_user_info)
@ -221,31 +219,31 @@ class OauthLoginTest(TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
oauth_user_id = int(response.url.split('/')[-1].split('.')[0]) # zy: 从URL中提取OAuth用户ID oauth_user_id = int(response.url.split('/')[-1].split('.')[0])
self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html') # zy: 断言跳转到邮箱补充页面 self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html')
response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id}) # zy: 提交邮箱表单 response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
sign = get_sha256(settings.SECRET_KEY + sign = get_sha256(settings.SECRET_KEY +
str(oauth_user_id) + settings.SECRET_KEY) # zy: 重要 - 生成邮箱确认签名 str(oauth_user_id) + settings.SECRET_KEY)
url = reverse('oauth:bindsuccess', kwargs={ url = reverse('oauth:bindsuccess', kwargs={
'oauthid': oauth_user_id, 'oauthid': oauth_user_id,
}) })
self.assertEqual(response.url, f'{url}?type=email') # zy: 断言跳转到绑定成功页面 self.assertEqual(response.url, f'{url}?type=email')
path = reverse('oauth:email_confirm', kwargs={ path = reverse('oauth:email_confirm', kwargs={
'id': oauth_user_id, 'id': oauth_user_id,
'sign': sign # zy: 使用签名进行邮箱确认 'sign': sign
}) })
response = self.client.get(path) # zy: 模拟邮箱确认链接点击 response = self.client.get(path)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success') # zy: 最终成功页面 self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success')
user = auth.get_user(self.client) user = auth.get_user(self.client)
from oauth.models import OAuthUser from oauth.models import OAuthUser
oauth_user = OAuthUser.objects.get(author=user) # zy: 查询关联的OAuth用户 oauth_user = OAuthUser.objects.get(author=user)
self.assertTrue(user.is_authenticated) # zy: 断言用户已登录 self.assertTrue(user.is_authenticated)
self.assertEqual(user.username, mock_user_info['screen_name']) # zy: 断言用户名 self.assertEqual(user.username, mock_user_info['screen_name'])
self.assertEqual(user.email, 'test@gmail.com') # zy: 断言补充的邮箱 self.assertEqual(user.email, 'test@gmail.com')
self.assertEqual(oauth_user.pk, oauth_user_id) # zy: 断言OAuth用户ID匹配 self.assertEqual(oauth_user.pk, oauth_user_id)

@ -2,24 +2,24 @@ from django.urls import path
from . import views from . import views
app_name = "oauth" # zy: 重要 - 定义应用命名空间用于URL反向解析 app_name = "oauth"
urlpatterns = [ urlpatterns = [
path( path(
r'oauth/authorize', # zy: 核心路径 - OAuth授权回调处理 r'oauth/authorize',
views.authorize), # zy: 关键视图 - 处理OAuth服务商回调获取用户信息 views.authorize),
path( path(
r'oauth/requireemail/<int:oauthid>.html', # zy: 动态路径 - 需要补充邮箱页面带OAuth用户ID参数 r'oauth/requireemail/<int:oauthid>.html',
views.RequireEmailView.as_view(), # zy: 类视图 - 处理邮箱补充表单 views.RequireEmailView.as_view(),
name='require_email'), # zy: URL名称 - 用于反向解析 name='require_email'),
path( path(
r'oauth/emailconfirm/<int:id>/<sign>.html', # zy: 关键路径 - 邮箱确认链接带ID和签名参数 r'oauth/emailconfirm/<int:id>/<sign>.html',
views.emailconfirm, # zy: 重要视图 - 验证邮箱确认签名并完成绑定 views.emailconfirm,
name='email_confirm'), # zy: URL名称 - 邮箱确认功能 name='email_confirm'),
path( path(
r'oauth/bindsuccess/<int:oauthid>.html', # zy: 成功页面 - OAuth绑定成功提示 r'oauth/bindsuccess/<int:oauthid>.html',
views.bindsuccess, # zy: 视图函数 - 显示绑定成功信息 views.bindsuccess,
name='bindsuccess'), # zy: URL名称 - 绑定成功页面 name='bindsuccess'),
path( path(
r'oauth/oauthlogin', # zy: 入口路径 - OAuth登录入口 r'oauth/oauthlogin',
views.oauthlogin, # zy: 入口视图 - 跳转到OAuth服务商授权页面 views.oauthlogin,
name='oauthlogin')] # zy: URL名称 - OAuth登录功能 name='oauthlogin')]

@ -27,29 +27,29 @@ logger = logging.getLogger(__name__)
def get_redirecturl(request): def get_redirecturl(request):
nexturl = request.GET.get('next_url', None) # zy: 获取重定向URL参数 nexturl = request.GET.get('next_url', None)
if not nexturl or nexturl == '/login/' or nexturl == '/login': if not nexturl or nexturl == '/login/' or nexturl == '/login':
nexturl = '/' # zy: 默认重定向到首页 nexturl = '/'
return nexturl return nexturl
p = urlparse(nexturl) # zy: 解析URL防止开放重定向攻击 p = urlparse(nexturl)
if p.netloc: if p.netloc:
site = get_current_site().domain site = get_current_site().domain
if not p.netloc.replace('www.', '') == site.replace('www.', ''): if not p.netloc.replace('www.', '') == site.replace('www.', ''):
logger.info('非法url:' + nexturl) # zy: 安全记录 - 记录非法URL logger.info('非法url:' + nexturl)
return "/" return "/"
return nexturl return nexturl
def oauthlogin(request): def oauthlogin(request):
type = request.GET.get('type', None) # zy: 获取OAuth类型参数 type = request.GET.get('type', None)
if not type: if not type:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
manager = get_manager_by_type(type) # zy: 关键调用 - 获取对应类型的OAuth管理器 manager = get_manager_by_type(type)
if not manager: if not manager:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
nexturl = get_redirecturl(request) nexturl = get_redirecturl(request)
authorizeurl = manager.get_authorization_url(nexturl) # zy: 核心功能 - 生成授权URL authorizeurl = manager.get_authorization_url(nexturl)
return HttpResponseRedirect(authorizeurl) # zy: 重定向到OAuth服务商授权页面 return HttpResponseRedirect(authorizeurl)
def authorize(request): def authorize(request):
@ -59,96 +59,96 @@ def authorize(request):
manager = get_manager_by_type(type) manager = get_manager_by_type(type)
if not manager: if not manager:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
code = request.GET.get('code', None) # zy: 关键参数 - OAuth服务商返回的授权码 code = request.GET.get('code', None)
try: try:
rsp = manager.get_access_token_by_code(code) # zy: 核心调用 - 使用授权码获取访问令牌 rsp = manager.get_access_token_by_code(code)
except OAuthAccessTokenException as e: except OAuthAccessTokenException as e:
logger.warning("OAuthAccessTokenException:" + str(e)) # zy: 重要日志 - 令牌获取异常 logger.warning("OAuthAccessTokenException:" + str(e))
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
rsp = None rsp = None
nexturl = get_redirecturl(request) nexturl = get_redirecturl(request)
if not rsp: if not rsp:
return HttpResponseRedirect(manager.get_authorization_url(nexturl)) # zy: 失败时重新授权 return HttpResponseRedirect(manager.get_authorization_url(nexturl))
user = manager.get_oauth_userinfo() # zy: 关键调用 - 获取用户信息 user = manager.get_oauth_userinfo()
if user: if user:
if not user.nickname or not user.nickname.strip(): if not user.nickname or not user.nickname.strip():
user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') # zy: 生成默认昵称 user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
try: try:
temp = OAuthUser.objects.get(type=type, openid=user.openid) # zy: 检查是否已存在该OAuth用户 temp = OAuthUser.objects.get(type=type, openid=user.openid)
temp.picture = user.picture temp.picture = user.picture
temp.metadata = user.metadata temp.metadata = user.metadata
temp.nickname = user.nickname temp.nickname = user.nickname
user = temp # zy: 使用已存在的用户记录 user = temp
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
# facebook的token过长 # facebook的token过长
if type == 'facebook': if type == 'facebook':
user.token = '' # zy: 特殊处理 - Facebook令牌过长清空存储 user.token = ''
if user.email: # zy: 关键判断 - 用户有邮箱直接登录 if user.email:
with transaction.atomic(): # zy: 重要 - 数据库事务保证数据一致性 with transaction.atomic():
author = None author = None
try: try:
author = get_user_model().objects.get(id=user.author_id) # zy: 查找已关联的用户 author = get_user_model().objects.get(id=user.author_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
if not author: if not author:
result = get_user_model().objects.get_or_create(email=user.email) # zy: 根据邮箱获取或创建用户 result = get_user_model().objects.get_or_create(email=user.email)
author = result[0] author = result[0]
if result[1]: # zy: 判断是否为新创建的用户 if result[1]:
try: try:
get_user_model().objects.get(username=user.nickname) get_user_model().objects.get(username=user.nickname)
except ObjectDoesNotExist: except ObjectDoesNotExist:
author.username = user.nickname # zy: 使用OAuth昵称作为用户名 author.username = user.nickname
else: else:
author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') # zy: 昵称冲突时生成唯一用户名 author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
author.source = 'authorize' # zy: 标记用户来源 author.source = 'authorize'
author.save() author.save()
user.author = author # zy: 关联OAuth用户到系统用户 user.author = author
user.save() user.save()
oauth_user_login_signal.send( oauth_user_login_signal.send(
sender=authorize.__class__, id=user.id) # zy: 重要 - 发送登录信号 sender=authorize.__class__, id=user.id)
login(request, author) # zy: 核心功能 - 登录用户 login(request, author)
return HttpResponseRedirect(nexturl) return HttpResponseRedirect(nexturl)
else: # zy: 用户没有邮箱,需要补充 else:
user.save() user.save()
url = reverse('oauth:require_email', kwargs={ url = reverse('oauth:require_email', kwargs={
'oauthid': user.id 'oauthid': user.id
}) # zy: 生成邮箱补充页面URL })
return HttpResponseRedirect(url) # zy: 重定向到邮箱补充页面 return HttpResponseRedirect(url)
else: else:
return HttpResponseRedirect(nexturl) return HttpResponseRedirect(nexturl)
def emailconfirm(request, id, sign): def emailconfirm(request, id, sign):
if not sign: if not sign:
return HttpResponseForbidden() # zy: 安全拒绝 - 无签名参数 return HttpResponseForbidden()
if not get_sha256(settings.SECRET_KEY + if not get_sha256(settings.SECRET_KEY +
str(id) + str(id) +
settings.SECRET_KEY).upper() == sign.upper(): # zy: 重要 - 验证签名防止篡改 settings.SECRET_KEY).upper() == sign.upper():
return HttpResponseForbidden() return HttpResponseForbidden()
oauthuser = get_object_or_404(OAuthUser, pk=id) oauthuser = get_object_or_404(OAuthUser, pk=id)
with transaction.atomic(): with transaction.atomic():
if oauthuser.author: if oauthuser.author:
author = get_user_model().objects.get(pk=oauthuser.author_id) author = get_user_model().objects.get(pk=oauthuser.author_id)
else: else:
result = get_user_model().objects.get_or_create(email=oauthuser.email) # zy: 创建系统用户 result = get_user_model().objects.get_or_create(email=oauthuser.email)
author = result[0] author = result[0]
if result[1]: if result[1]:
author.source = 'emailconfirm' # zy: 标记来源为邮箱确认 author.source = 'emailconfirm'
author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip( author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip(
) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') # zy: 设置用户名 ) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
author.save() author.save()
oauthuser.author = author # zy: 完成关联 oauthuser.author = author
oauthuser.save() oauthuser.save()
oauth_user_login_signal.send( oauth_user_login_signal.send(
sender=emailconfirm.__class__, sender=emailconfirm.__class__,
id=oauthuser.id) # zy: 发送登录信号 id=oauthuser.id)
login(request, author) # zy: 登录用户 login(request, author)
site = 'http://' + get_current_site().domain site = 'http://' + get_current_site().domain
content = _(''' content = _('''
@ -162,22 +162,22 @@ def emailconfirm(request, id, sign):
%(site)s %(site)s
''') % {'oauthuser_type': oauthuser.type, 'site': site} ''') % {'oauthuser_type': oauthuser.type, 'site': site}
send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content) # zy: 发送绑定成功邮件 send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content)
url = reverse('oauth:bindsuccess', kwargs={ url = reverse('oauth:bindsuccess', kwargs={
'oauthid': id 'oauthid': id
}) })
url = url + '?type=success' url = url + '?type=success'
return HttpResponseRedirect(url) # zy: 重定向到成功页面 return HttpResponseRedirect(url)
class RequireEmailView(FormView): class RequireEmailView(FormView):
form_class = RequireEmailForm # zy: 使用邮箱表单类 form_class = RequireEmailForm
template_name = 'oauth/require_email.html' # zy: 模板路径 template_name = 'oauth/require_email.html'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
oauthid = self.kwargs['oauthid'] # zy: 获取URL参数中的OAuth用户ID oauthid = self.kwargs['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid) oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
if oauthuser.email: # zy: 安全检查 - 如果已有邮箱直接跳过 if oauthuser.email:
pass pass
# return HttpResponseRedirect('/') # return HttpResponseRedirect('/')
@ -187,32 +187,32 @@ class RequireEmailView(FormView):
oauthid = self.kwargs['oauthid'] oauthid = self.kwargs['oauthid']
return { return {
'email': '', 'email': '',
'oauthid': oauthid # zy: 初始化表单数据 'oauthid': oauthid
} }
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
oauthid = self.kwargs['oauthid'] oauthid = self.kwargs['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid) oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
if oauthuser.picture: if oauthuser.picture:
kwargs['picture'] = oauthuser.picture # zy: 传递用户头像到模板 kwargs['picture'] = oauthuser.picture
return super(RequireEmailView, self).get_context_data(**kwargs) return super(RequireEmailView, self).get_context_data(**kwargs)
def form_valid(self, form): def form_valid(self, form):
email = form.cleaned_data['email'] # zy: 获取验证后的邮箱 email = form.cleaned_data['email']
oauthid = form.cleaned_data['oauthid'] oauthid = form.cleaned_data['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid) oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
oauthuser.email = email # zy: 保存邮箱到OAuth用户 oauthuser.email = email
oauthuser.save() oauthuser.save()
sign = get_sha256(settings.SECRET_KEY + sign = get_sha256(settings.SECRET_KEY +
str(oauthuser.id) + settings.SECRET_KEY) # zy: 生成邮箱确认签名 str(oauthuser.id) + settings.SECRET_KEY)
site = get_current_site().domain site = get_current_site().domain
if settings.DEBUG: if settings.DEBUG:
site = '127.0.0.1:8000' # zy: 开发环境域名 site = '127.0.0.1:8000'
path = reverse('oauth:email_confirm', kwargs={ path = reverse('oauth:email_confirm', kwargs={
'id': oauthid, 'id': oauthid,
'sign': sign 'sign': sign
}) })
url = "http://{site}{path}".format(site=site, path=path) # zy: 生成完整的确认链接 url = "http://{site}{path}".format(site=site, path=path)
content = _(""" content = _("""
<p>Please click the link below to bind your email</p> <p>Please click the link below to bind your email</p>
@ -225,29 +225,29 @@ class RequireEmailView(FormView):
<br /> <br />
%(url)s %(url)s
""") % {'url': url} """) % {'url': url}
send_email(emailto=[email, ], title=_('Bind your email'), content=content) # zy: 发送邮箱确认邮件 send_email(emailto=[email, ], title=_('Bind your email'), content=content)
url = reverse('oauth:bindsuccess', kwargs={ url = reverse('oauth:bindsuccess', kwargs={
'oauthid': oauthid 'oauthid': oauthid
}) })
url = url + '?type=email' # zy: 添加类型参数 url = url + '?type=email'
return HttpResponseRedirect(url) # zy: 重定向到绑定成功页面 return HttpResponseRedirect(url)
def bindsuccess(request, oauthid): def bindsuccess(request, oauthid):
type = request.GET.get('type', None) # zy: 获取成功类型 type = request.GET.get('type', None)
oauthuser = get_object_or_404(OAuthUser, pk=oauthid) oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
if type == 'email': if type == 'email':
title = _('Bind your email') title = _('Bind your email')
content = _( content = _(
'Congratulations, the binding is just one step away. ' 'Congratulations, the binding is just one step away. '
'Please log in to your email to check the email to complete the binding. Thank you.') # zy: 等待邮箱确认提示 'Please log in to your email to check the email to complete the binding. Thank you.')
else: else:
title = _('Binding successful') title = _('Binding successful')
content = _( content = _(
"Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s" "Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s"
" to directly log in to this website without a password. You are welcome to continue to follow this site." % { " to directly log in to this website without a password. You are welcome to continue to follow this site." % {
'oauthuser_type': oauthuser.type}) # zy: 绑定成功提示 'oauthuser_type': oauthuser.type})
return render(request, 'oauth/bindsuccess.html', { return render(request, 'oauth/bindsuccess.html', {
'title': title, 'title': title,
'content': content # zy: 渲染成功页面 'content': content
}) })

Loading…
Cancel
Save