Compare commits

..

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

@ -0,0 +1,7 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": []
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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,154 +1,27 @@
<<<<<<< HEAD
#django核心组件导入
from django import forms# Django 表单处理模块
from django.contrib.auth.admin import UserAdmin # Django 默认用户管理后台类
from django.contrib.auth.forms import UserChangeForm # 用户信息修改表单基类
from django.contrib.auth.forms import UsernameField# 用户名专用表单字段
from django.utils.translation import gettext_lazy as _ # 国际化翻译函数
=======
from django import forms# 导入Django表单模块用于创建自定义表单
from django.contrib.auth.admin import UserAdmin# 导入Django自带的用户管理类用于继承扩展
from django.contrib.auth.forms import UserChangeForm# 导入用户修改表单和用户名字段类
from django.contrib.auth.forms import UsernameField # 导入国际化翻译工具,用于字段标签的多语言支持
from django.utils.translation import gettext_lazy as _
>>>>>>> LXY_branch
<<<<<<< HEAD
<<<<<<< HEAD
# 本地应用导入
# Register your models here.
<<<<<<< HEAD
from .models import BlogUser # 导入自定义用户模型
=======
# 导入自定义用户模型
=======
# jyn:导入自定义用户模型
>>>>>>> JYN_branch
from .models import BlogUser
>>>>>>> JYN_branch
class BlogUserCreationForm(forms.ModelForm):
"""
<<<<<<< HEAD
自定义用户创建表单(用于管理后台添加新用户)
继承自 ModelForm专门处理 BlogUser 模型的创建
"""
# 定义密码输入字段(需要输入两次以确保一致)
password1 = forms.CharField(label=_('password'), # 字段标签(支持国际化)
widget=forms.PasswordInput) # 密码输入控件
password2 = forms.CharField(label=_('Enter password again'), # 确认密码标签
widget=forms.PasswordInput) # 密码输入控件
class Meta:
model = BlogUser# 关联的模型类
fields = ('email',)# 创建用户时显示的字段这里只显示email字段
=======
自定义用户创建表单用于在管理员界面添加新用户
继承自ModelForm提供密码验证功能
"""
<<<<<<< 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):
"""
验证两次输入的密码是否一致
<<<<<<< HEAD
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
password1 = self.cleaned_data.get("password1")# 获取第一次输入的密码
password2 = self.cleaned_data.get("password2")# 获取第二次输入的密码
# 如果两次密码不一致,抛出验证错误
=======
这是Django表单验证机制的一部分方法名以clean_开头
"""
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
<<<<<<< HEAD
# 检查密码是否存在且不一致
>>>>>>> JYN_branch
=======
# jyn:检查密码是否存在且不一致
>>>>>>> JYN_branch
if password1 and password2 and password1 != password2:
<<<<<<< HEAD
<<<<<<< HEAD
raise forms.ValidationError(_("passwords do not match"))# 错误信息(支持国际化)
return password2# 返回验证后的值
def save(self, commit=True):
"""
<<<<<<< HEAD
重写保存方法在保存用户前处理密码哈希
"""
# Save the provided password in hashed format
user = super().save(commit=False) # 调用父类保存方法但不提交到数据库
user.set_password(self.cleaned_data["password1"]) # 对密码进行哈希加密
if commit:
user.source = 'adminsite' # 设置用户来源标记(表示通过管理后台创建)
user.save()# 保存到数据库
return user# 返回用户对象
=======
重写保存方法确保密码以哈希形式存储
而不是明文存储
"""
# 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保存用户时对密码进行哈希处理后存储
@ -156,120 +29,24 @@ class BlogUserCreationForm(forms.ModelForm): # lxy定义两个密码字段使
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):
"""
<<<<<<< HEAD
自定义用户信息修改表单用于管理后台编辑用户
继承自 Django 自带的 UserChangeForm
"""
class Meta:
<<<<<<< HEAD
<<<<<<< HEAD
model = BlogUser # 关联的模型类
fields = '__all__'# 显示所有字段
field_classes = {'username': UsernameField}# 指定用户名使用专用字段类型
def __init__(self, *args, **kwargs):
"""
表单初始化方法
可以在这里对表单字段进行自定义设置
"""
super().__init__(*args, **kwargs) # 调用父类初始化方法
# 可以在这里添加自定义逻辑,如修改字段属性等
class BlogUserAdmin(UserAdmin):
"""
自定义用户管理后台配置
继承自 Django 自带的 UserAdmin
"""
# 指定使用的表单类
form = BlogUserChangeForm # 编辑用户时使用的表单
add_form = BlogUserCreationForm# 添加用户时使用的表单
# 管理后台列表页显示配置
list_display = (
'id', # 用户ID
'nickname', # 用户昵称
'username',# 用户名
'email', # 电子邮箱
'last_login', # 最后登录时间
'date_joined', # 注册时间
'source')# 用户来源标记
# 设置哪些字段可以点击跳转到编辑页
list_display_links = ('id', 'username')
# 默认排序规则按ID降序排列
ordering = ('-id',)
=======
自定义用户修改表单用于在管理员界面编辑用户信息
继承自Django内置的UserChangeForm
"""
class Meta:
# 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',
@ -277,26 +54,6 @@ class BlogUserAdmin(UserAdmin):
'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,5 @@
<<<<<<< HEAD
<<<<<<< HEAD
from django.apps import AppConfig
class AccountsConfig(AppConfig):
"""
<<<<<<< HEAD
Accounts 应用的配置类
功能
1. 定义应用名称 Django 内部识别
2. 可在此处覆盖 ready() 方法以注册信号等
"""
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,107 +1,26 @@
<<<<<<< HEAD
from django import forms #导入 Django 表单模块,用于创建自定义表单类。
from django.contrib.auth import get_user_model, password_validation #get_user_model 用于获取项目中自定义的用户模型(遵循 Django 推荐的用户模型扩展方式)。
<<<<<<< HEAD
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm #UserCreationForm导入 Django 内置的认证表单AuthenticationForm 用于登录和用户创建表单UserCreationForm 用于注册),作为自定义表单的基类。
from django.core.exceptions import ValidationError #导入 Django 的验证异常类,用于在表单验证时抛出自定义错误。
from django.forms import widgets #导入 Django 表单的小部件模块,用于自定义表单字段的渲染样式(如输入框类型、样式类等)。
from django.utils.translation import gettext_lazy as _ #导入 Django 的延迟翻译函数,用于表单字段标签、错误提示的国际化翻译。
from . import utils #导入当前应用下的 utils 模块(假设包含工具类或函数,此处代码未展示具体使用)。
from .models import BlogUser #导入当前应用下定义的 BlogUser 模型(自定义用户模型)。
class LoginForm(AuthenticationForm): #继承 Django 内置的 AuthenticationForm自定义登录表单的样式和逻辑。
def __init__(self, *args, **kwargs): #重写构造方法,用于自定义表单字段的小部件属性。
super(LoginForm, self).__init__(*args, **kwargs) #调用父类构造方法,确保基础功能正常。
=======
# 登录表单继承自Django内置的AuthenticationForm
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
>>>>>>> 5d714be542986b7f935eb0ec4df9b0918e75eeca
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"}) #为用户名字段设置文本输入小部件,定义占位符和 Bootstrap 样式类form-control
=======
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils # jyn:导入自定义工具模块,可能用于验证码验证等功能
from .models import BlogUser # jyn:导入自定义用户模型
from . import utils
from .models import BlogUser
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(
<<<<<<< HEAD
attrs={'placeholder': "password", "class": "form-control"}) #为密码字段设置密码输入小部件,同样定义占位符和样式类。
<<<<<<< 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自定义注册表单的字段、样式和验证逻辑。
=======
<<<<<<< HEAD
# 注册表单继承自Django内置的UserCreationForm
class RegisterForm(UserCreationForm):
>>>>>>> 5d714be542986b7f935eb0ec4df9b0918e75eeca
=======
class RegisterForm(UserCreationForm):
"""
自定义注册表单继承自Django内置的UserCreationForm
用于处理用户注册逻辑包含用户名邮箱和密码验证
"""
>>>>>>> 8b27cdad9a9ccc84febce3bcf1d211ed109f96f2
>>>>>>> JYN_branch
def __init__(self, *args, **kwargs):
# jyn:调用父类构造方法初始化表单
super(RegisterForm, self).__init__(*args, **kwargs)
<<<<<<< HEAD
# 自定义用户名、邮箱和密码字段的HTML属性
=======
<<<<<<< HEAD
# 自定义各字段的输入控件,添加样式和占位符
>>>>>>> JYN_branch
=======
# jyn:自定义各字段的输入控件,添加样式和占位符
>>>>>>> JYN_branch
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
@ -110,58 +29,19 @@ class RegisterForm(UserCreationForm):
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
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):
"""
邮箱验证方法检查邮箱是否已被注册
表单验证机制中以clean_为前缀的方法会自动被调用
"""
email = self.cleaned_data['email']
# jyn:检查该邮箱是否已存在于数据库中
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists")) #jyn: 抛出验证错误
raise ValidationError(_("email already exists"))
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:
model = get_user_model()
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):
"""
忘记密码表单用于用户重置密码的流程
包含新密码确认密码邮箱和验证码字段
"""
# jyn:新密码字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -171,16 +51,7 @@ class ForgetPasswordForm(forms.Form):
}
),
)
<<<<<<< HEAD
# 新密码字段2用于确认
=======
<<<<<<< HEAD
# 确认新密码字段
>>>>>>> JYN_branch
=======
# jyn:确认新密码字段
>>>>>>> JYN_branch
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
@ -190,16 +61,7 @@ class ForgetPasswordForm(forms.Form):
}
),
)
<<<<<<< HEAD
# 邮箱字段
=======
<<<<<<< HEAD
# 邮箱字段(用于验证用户身份)
>>>>>>> JYN_branch
=======
# jyn:邮箱字段(用于验证用户身份)
>>>>>>> JYN_branch
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
@ -209,16 +71,7 @@ class ForgetPasswordForm(forms.Form):
}
),
)
<<<<<<< HEAD
# 验证码字段
=======
<<<<<<< HEAD
# 验证码字段(用于身份验证)
>>>>>>> JYN_branch
=======
# jyn:验证码字段(用于身份验证)
>>>>>>> JYN_branch
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
@ -228,104 +81,39 @@ 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):
"""验证两次输入的密码是否一致,并验证密码强度"""
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
# jyn:检查两次密码是否一致
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
<<<<<<< HEAD
password_validation.validate_password(password2)# 使用Django的密码验证器
=======
# jyn:使用Django内置的密码验证器验证密码强度
password_validation.validate_password(password2)
>>>>>>> JYN_branch
return password2
<<<<<<< HEAD
<<<<<<< HEAD
# 验证邮箱是否已注册
=======
# clean_new_password2方法验证两次新密码是否一致并对密码进行有效性校验
>>>>>>> LXY_branch
=======
#lxyclean_new_password2方法验证两次新密码是否一致并对密码进行有效性校验
>>>>>>> LXY_branch
def clean_email(self):
"""验证邮箱是否已注册"""
user_email = self.cleaned_data.get("email")
# jyn:检查该邮箱是否存在于系统中
if not BlogUser.objects.filter(email=user_email).exists():
# jyn:提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露)
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
<<<<<<< HEAD
# 验证用户输入的验证码是否正确
=======
<<<<<<< HEAD
# clean_email方法验证邮箱是否已注册基于BlogUser模型未注册则抛出“邮箱不存在”的验证错误
>>>>>>> LXY_branch
=======
#lxyclean_email方法验证邮箱是否已注册基于BlogUser模型未注册则抛出“邮箱不存在”的验证错误
>>>>>>> LXY_branch
def clean_code(self):
"""验证验证码是否有效"""
code = self.cleaned_data.get("code")
<<<<<<< HEAD
<<<<<<< HEAD
error = utils.verify(# 调用工具函数验证验证码
=======
# 调用工具函数验证邮箱和验证码是否匹配
=======
# jyn:调用工具函数验证邮箱和验证码是否匹配
>>>>>>> JYN_branch
error = utils.verify(
>>>>>>> JYN_branch
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error) # jyn:验证码无效时抛出错误
raise ValidationError(error)
return code
<<<<<<< HEAD
<<<<<<< HEAD
# 忘记密码功能中的验证码发送表单(仅需邮箱字段)
=======
#clean_code方法调用工具方法utils.verify验证验证码有效性无效则抛出错误
=======
#lxy clean_code方法调用工具方法utils.verify验证验证码有效性无效则抛出错误
>>>>>>> LXY_branch
>>>>>>> LXY_branch
class ForgetPasswordCodeForm(forms.Form):
"""
发送密码重置验证码的表单
仅包含邮箱字段用于提交需要重置密码的邮箱
"""
email = forms.EmailField(
label=_('Email'),
<<<<<<< HEAD
)
<<<<<<< HEAD
=======
)
#仅包含email字段邮箱输入用于忘记密码流程中验证邮箱的步骤
>>>>>>> LXY_branch
=======
#lxy仅包含email字段邮箱输入用于忘记密码流程中验证邮箱的步骤
>>>>>>> LXY_branch
#lxy仅包含email字段邮箱输入用于忘记密码流程中验证邮箱的步骤

@ -3,41 +3,10 @@ from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site # jyn:导入获取当前站点信息的工具函数
from djangoblog.utils import get_current_site
# Create your models here.
<<<<<<< HEAD
# 自定义用户模型继承Django内置的AbstractUser
class BlogUser(AbstractUser):
<<<<<<< HEAD
# 用户昵称(可选)
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# 账号创建时间(默认当前时间)
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间(默认当前时间)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 账号创建来源(如:网站注册/第三方登录等,可选)
=======
"""
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)
# 获取用户详情页的绝对URL用于模板中的{% url %}反向解析)
=======
class BlogUser(AbstractUser):#lxy自定义用户模型BlogUser继承自Django内置的AbstractUser可扩展的用户抽象类
nickname = models.CharField(_('nick name'), max_length=100, blank=True)#lxy定义nickname字段字符类型支持国际化翻译最大长度100可为空。
@ -46,96 +15,18 @@ class BlogUser(AbstractUser):#lxy自定义用户模型BlogUser继承自Django
source = models.CharField(_('create source'), max_length=100, blank=True)#lxy定义source字段字符类型记录用户创建来源最大长度100可为空。
>>>>>>> LXY_branch
def get_absolute_url(self):
"""
jyn:返回用户详情页的URL
Django推荐为模型定义此方法用于获取对象的标准URL
"""
return reverse(
<<<<<<< HEAD
'blog:author_detail', kwargs={
<<<<<<< HEAD
<<<<<<< HEAD
'author_name': self.username})
# 定义对象的字符串表示Admin后台和shell中显示
=======
'blog:author_detail', # 对应的URL名称
kwargs={'author_name': self.username} # 传递的参数
)
=======
'author_name': self.username})#定义获取用户详情页绝对URL的方法通过reverse反向解析路由blog:author_detail传递username参数。
>>>>>>> LXY_branch
=======
'author_name': self.username})#lxy定义获取用户详情页绝对URL的方法通过reverse反向解析路由blog:author_detail传递username参数。
>>>>>>> LXY_branch
>>>>>>> JYN_branch
def __str__(self):
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
"""模型的字符串表示,这里返回用户的邮箱"""
=======
"""jyn:模型的字符串表示,这里返回用户的邮箱"""
>>>>>>> JYN_branch
return self.email
# 获取用户详情页的完整URL包含域名用于分享链接
=======
return self.email#定义对象的字符串表示方法返回用户的email
=======
return self.email#lxy定义对象的字符串表示方法返回用户的email
>>>>>>> LXY_branch
>>>>>>> LXY_branch
def get_full_url(self):
<<<<<<< HEAD
<<<<<<< HEAD
site = get_current_site().domain# 获取当前站点域名
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
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
# 元数据配置(模型级别的选项)
class Meta:
<<<<<<< HEAD
<<<<<<< HEAD
ordering = ['-id'] # 默认按ID降序排列
verbose_name = _('user') # 单数形式名称(后台显示)
verbose_name_plural = verbose_name# 复数形式名称(后台显示)
get_latest_by = 'id'# 指定最新记录的排序字段
=======
"""模型的元数据配置"""
ordering = ['-id'] # 默认排序方式按id降序最新创建的用户在前
verbose_name = _('user') # 模型的单数显示名称(支持国际化)
verbose_name_plural = verbose_name # 模型的复数显示名称(与单数相同)
get_latest_by = 'id' # 指定使用id字段获取最新对象用于Model.objects.latest()
>>>>>>> JYN_branch
=======
return url#定义获取带域名的完整URL方法结合当前站点域名和get_absolute_url生成完整链接
class Meta:
ordering = ['-id']#查询结果按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:
@ -143,11 +34,3 @@ class BlogUser(AbstractUser):#lxy自定义用户模型BlogUser继承自Django
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,539 +3,182 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser # jyn:导入自定义用户模型
from blog.models import Article, Category # jyn:导入博客相关模型
from djangoblog.utils import * # jyn:导入项目工具函数
from . import utils # jyn:导入当前应用的工具函数
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# Create your tests here.
# 创建测试类继承Django的TestCase
class AccountTest(TestCase):
<<<<<<< HEAD
# 测试初始化方法(每个测试方法运行前都会执行)
def setUp(self):
# 初始化测试客户端(模拟浏览器请求)
self.client = Client()
# 初始化请求工厂(用于生成请求对象)
self.factory = RequestFactory()
# 创建一个普通测试用户
=======
"""
账号相关功能的测试类继承自Django的TestCase
包含用户登录注册密码重置等功能的测试用例
"""
def setUp(self):
"""
测试前的初始化方法会在每个测试方法执行前运行
用于创建测试所需的基础数据
"""
<<<<<<< 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(
username="test",
email="admin@admin.com",
password="12345678"
)
<<<<<<< HEAD
<<<<<<< HEAD
# 测试用的随机字符串
self.new_test = "xxx123--="
# 测试用户账号验证功能
def test_validate_account(self):
# 获取当前站点域名
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
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
<<<<<<< HEAD
<<<<<<< HEAD
# 从数据库获取刚创建的超级用户(验证是否创建成功)
testuser = BlogUser.objects.get(username='liangliangyy1')
=======
# 获取创建的用户
testuser = BlogUser.objects.get(username='liangliangyy1')
>>>>>>> JYN_branch
# 测试登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
<<<<<<< HEAD
self.assertEqual(loginresult, True)# 验证登录成功
# 测试访问管理员后台
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)# 验证返回200状态码
=======
self.assertEqual(loginresult, True) # 断言登录成功
=======
# jyn:获取创建的用户
testuser = BlogUser.objects.get(username='liangliangyy1')
self.assertEqual(response.status_code, 200)
# 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.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
<<<<<<< HEAD
=======
<<<<<<< HEAD
>>>>>>> JYN_branch
# 创建测试文章
=======
# jyn:创建测试文章
>>>>>>> JYN_branch
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
<<<<<<< HEAD
article.author = user# 关联超级用户
article.category = category# 关联上面创建的分类
article.type = 'a' # 文章类型
article.status = 'p' # 发布状态
article.save()
# 测试访问文章的管理URL
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200) # 验证返回200状态码
=======
article.author = user
article.category = category
article.type = 'a' # jyn:假设'a'表示文章类型
article.status = 'p' # jyn:假设'p'表示已发布
article.type = 'a'
article.status = '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):
<<<<<<< HEAD
# 验证测试邮箱初始不存在
=======
"""测试用户注册功能,包括注册流程、邮箱验证和权限控制"""
<<<<<<< HEAD
# 初始状态下,该邮箱应不存在
>>>>>>> JYN_branch
=======
# jyn:初始状态下,该邮箱应不存在
>>>>>>> JYN_branch
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
<<<<<<< HEAD
# 模拟注册请求
=======
<<<<<<< HEAD
# 模拟用户注册提交
>>>>>>> JYN_branch
=======
# jyn:模拟用户注册提交
>>>>>>> JYN_branch
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
<<<<<<< HEAD
# 验证用户已创建(通过邮箱查询)
=======
<<<<<<< HEAD
# 注册后,该邮箱应存在
>>>>>>> JYN_branch
=======
#jyn: 注册后,该邮箱应存在
>>>>>>> JYN_branch
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
<<<<<<< HEAD
# 获取刚注册的用户
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)))
# 构造验证URL
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
<<<<<<< HEAD
# 测试访问验证URL
response = self.client.get(url)
self.assertEqual(response.status_code, 200) # 验证返回200状态码
# 使用测试客户端登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
# 获取指定邮箱的用户并设置为超级用户和工作人员
=======
# jyn:访问验证链接
response = self.client.get(url)
self.assertEqual(response.status_code, 200) # jyn:断言验证页面访问成功
self.assertEqual(response.status_code, 200)
# 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.is_superuser = True
user.is_staff = True
user.save()
<<<<<<< HEAD
# 删除侧边栏缓存
delete_sidebar_cache()
# 创建分类
=======
# jyn:清除缓存
delete_sidebar_cache()
<<<<<<< HEAD
# 创建测试分类
>>>>>>> JYN_branch
=======
# jyn:创建测试分类
>>>>>>> JYN_branch
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
<<<<<<< HEAD
# 创建文章
=======
<<<<<<< HEAD
# 创建测试文章
>>>>>>> JYN_branch
=======
# jyn:创建测试文章
>>>>>>> JYN_branch
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
<<<<<<< HEAD
# 测试已登录用户访问文章管理URL
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 测试注销功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
# 测试注销后访问文章管理URL应重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# 测试使用错误密码登录
=======
# 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]) # 登出通常是重定向
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'), {
'username': 'user1233',
'password': 'password123'# 注意这里密码与登录时使用的不同
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
<<<<<<< HEAD
# 测试使用错误密码登录后访问文章管理URL应重定向
=======
<<<<<<< HEAD
# 错误登录后访问管理页面
>>>>>>> JYN_branch
=======
# jyn:错误登录后访问管理页面
>>>>>>> JYN_branch
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])#lxy测试用户注册流程验证注册前后用户数量变化邮箱验证链接的有效性以及注册后用户权限、文章发布等功能
>>>>>>> LXY_branch
>>>>>>> LXY_branch
def test_verify_email_code(self):
"""测试邮箱验证码验证功能"""
to_email = "admin@admin.com"
<<<<<<< HEAD
<<<<<<< HEAD
code = generate_code() # 生成验证码
utils.set_code(to_email, code)# 存储验证码
utils.send_verify_email(to_email, code) # 发送验证邮件(实际测试中可能不会真的发送)
# 测试正确验证码
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
# 测试错误邮箱
err = utils.verify("admin@123.com", code)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(type(err), str)# 应返回错误信息字符串
# 测试忘记密码发送验证码功能 - 成功情况
=======
code = generate_code() # 生成验证码
utils.set_code(to_email, code) # 存储验证码
utils.send_verify_email(to_email, code) # 发送验证邮件
=======
self.assertEqual(type(err), str)#测试邮箱验证码功能:验证有效邮箱和无效邮箱的验证码校验结果
>>>>>>> LXY_branch
=======
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:应返回错误信息
self.assertEqual(type(err), str)#lxy测试邮箱验证码功能验证有效邮箱和无效邮箱的验证码校验结果
>>>>>>> JYN_branch
def test_forget_password_email_code_success(self):
"""测试发送密码重置验证码成功的情况"""
resp = self.client.post(
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)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.content.decode("utf-8"), "ok")# 验证返回成功消息
# 测试忘记密码发送验证码功能 - 失败情况
def test_forget_password_email_code_fail(self):
# 测试不提供邮箱
=======
data=dict(email="admin@admin.com") # 使用已存在的邮箱
)
self.assertEqual(resp.status_code, 200) # 断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") # 断言返回成功信息
=======
self.assertEqual(resp.content.decode("utf-8"), "ok")#测试忘记密码的邮箱验证码发送:分别验证成功和失败场景(如邮箱错误)的接口响应
>>>>>>> LXY_branch
=======
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(
path=reverse("account:forget_password_code"),
data=dict()
)
<<<<<<< HEAD
<<<<<<< HEAD
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(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试忘记密码重置功能 - 成功情况
def test_forget_password_email_success(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)# 为测试用户设置验证码
# 准备重置密码数据
=======
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
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test,# 确认密码
email=self.blog_user.email,# 用户邮箱
code=code, # 验证码
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
<<<<<<< HEAD
<<<<<<< HEAD
# 提交重置密码请求
=======
# 提交密码重置请求
>>>>>>> JYN_branch
=======
# jyn:提交密码重置请求
>>>>>>> JYN_branch
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.status_code, 302) # 应重定向
=======
self.assertEqual(resp.status_code, 302) # 成功重置后通常重定向
>>>>>>> JYN_branch
=======
self.assertEqual(resp.status_code, 302) # jyn:成功重置后通常重定向
>>>>>>> JYN_branch
# jyn:验证密码是否已更新
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # jyn:type: BlogUser
self.assertNotEqual(blog_user, None) # jyn:断言用户存在
# jyn:断言密码修改成功
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
# 测试忘记密码重置功能 - 用户不存在情况
def test_forget_password_email_not_user(self):
"""测试使用不存在的邮箱重置密码的情况"""
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
<<<<<<< HEAD
<<<<<<< HEAD
email="123@123.com",# 不存在的邮箱
=======
email="123@123.com", # 不存在的邮箱
>>>>>>> JYN_branch
=======
email="123@123.com", # jyn:不存在的邮箱
>>>>>>> JYN_branch
email="123@123.com",
code="123456",
)
resp = self.client.post(
@ -543,68 +186,22 @@ class AccountTest(TestCase):
data=data
)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.status_code, 200) # 应返回错误页面而非重定向
=======
self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码
>>>>>>> JYN_branch
=======
self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码
>>>>>>> JYN_branch
# 测试忘记密码重置功能 - 验证码错误情况
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
<<<<<<< HEAD
code = generate_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
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
<<<<<<< HEAD
<<<<<<< HEAD
code="111111",# 错误验证码
=======
code="111111", # 错误的验证码
>>>>>>> JYN_branch
=======
code="111111", # jyn:错误的验证码
>>>>>>> JYN_branch
code="111111",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
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,99 +1,11 @@
<<<<<<< HEAD
# 导入 Django 的 URL 路由工具
from django.urls import path# 用于简单路径匹配(如 'account/result.html'
from django.urls import re_path# 用于正则表达式路径匹配(如 '^login/$'
# 导入当前应用的视图和表单
from . import views# 导入 views.py 中的所有视图类/函数
from .forms import LoginForm# 导入自定义登录表单(用于覆盖默认表单)
# 定义应用的命名空间(用于反向解析 URL 时避免冲突)
app_name = "accounts"
# 定义 URL 路由列表
urlpatterns = [
# 登录视图(使用正则路径)
re_path(r'^login/$', # 正则匹配路径:以 login 结尾(斜杠必需)
views.LoginView.as_view(success_url='/'), # 使用 Django 内置 LoginView登录成功后跳转到首页
name='login',# URL 名称(用于模板或代码中反向解析)
kwargs={'authentication_form': LoginForm}# 传递额外参数:覆盖默认登录表单
),
# 注册视图
re_path(r'^register/$', # 正则匹配路径:以 register 结尾
views.RegisterView.as_view(success_url="/"),# 自定义注册视图,注册成功后跳转首页
name='register'), # URL 名称
# 登出视图
re_path(r'^logout/$', # 正则匹配路径:以 logout 结尾
views.LogoutView.as_view(),# 使用 Django 内置 LogoutView
name='logout'),# URL 名称
# 账户结果页面(使用 path非正则
path(r'account/result.html',# 简单路径匹配(自动添加开头结尾的 ^$
views.account_result,# 指向普通视图函数(非类视图)
name='result'),# URL 名称
# 忘记密码视图
re_path(r'^forget_password/$', # 正则匹配路径:以 forget_password 结尾
views.ForgetPasswordView.as_view(),# 自定义忘记密码视图
name='forget_password'),# URL 名称
# 忘记密码验证码处理视图
re_path(r'^forget_password_code/$',# 正则匹配路径:以 forget_password_code 结尾
views.ForgetPasswordEmailCode.as_view(), # 自定义验证码处理视图
name='forget_password_code'), # URL 名称
]
=======
from django.urls import path
from django.urls import re_path # jyn:用于正则表达式匹配URL
from django.urls import re_path
from . import views # jyn:导入当前应用的视图函数/类
from .forms import LoginForm # jyn:导入自定义的登录表单
from . import views
from .forms import LoginForm
# 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',
@ -115,8 +27,3 @@ urlpatterns = [re_path(r'^login/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),#忘记密码验证码路由对应ForgetPasswordEmailCode
]
>>>>>>> LXY_branch
=======
name='forget_password_code'), # jyn:URL名称
]
>>>>>>> JYN_branch

@ -1,138 +1,27 @@
<<<<<<< HEAD
<<<<<<< HEAD
# 导入 Django 认证系统所需的模块
from django.contrib.auth import get_user_model# 动态获取当前项目的 User 模型
from django.contrib.auth.backends import ModelBackend# Django 默认的认证后端基类
=======
from django.contrib.auth import get_user_model # 获取项目配置的用户模型(支持自定义模型)
from django.contrib.auth.backends import ModelBackend # 导入Django内置的模型认证后端
>>>>>>> JYN_branch
=======
from django.contrib.auth import get_user_model # jyn:获取项目配置的用户模型(支持自定义模型)
from django.contrib.auth.backends import ModelBackend # jyn:导入Django内置的模型认证后端
>>>>>>> JYN_branch
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):#lxy自定义Django认证后端支持用户名或邮箱两种方式登录。
"""
<<<<<<< HEAD
允许使用用户名或邮箱登录
承自 Django ModelBackend重写 authenticate get_user 方法
=======
自定义认证后端继承自Django的ModelBackend
扩展功能允许用户使用用户名邮箱两种方式登录而非仅支持用户名
>>>>>>> JYN_branch
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
<<<<<<< HEAD
重写认证方法支持用户名或邮箱登录
参数:
request: HttpRequest 对象可能包含额外的认证上下文
username: 用户输入的用户名或邮箱前端传递的字段名固定为 username
password: 用户输入的密码
**kwargs: 其他可能的参数如通过信号传递的额外参数
返回:
如果认证成功返回 User 对象否则返回 None
"""
# 判断输入是邮箱还是用户名(通过 '@' 符号区分)
if '@' in username:
# 如果是邮箱,设置查询条件为 email 字段
kwargs = {'email': username}
else:
# 否则按用户名查询
=======
认证核心方法验证用户输入的凭证用户名/邮箱 + 密码是否有效
参数说明
- request当前请求对象
- username前端传入的用户名参数实际可能是用户名或邮箱
- password前端传入的密码明文
返回验证成功返回用户对象失败返回None
"""
# jyn:判断输入的「username」是否包含@符号,以此区分邮箱和用户名
if '@' in username:
# jyn:若包含@,则按邮箱字段查询用户
kwargs = {'email': username}
else:
<<<<<<< HEAD
# 若不包含@,则按用户名字段查询用户
>>>>>>> JYN_branch
kwargs = {'username': username}
try:
<<<<<<< HEAD
# 尝试从数据库获取用户(使用当前项目的自定义 User 模型)
user = get_user_model().objects.get(**kwargs)
# 验证密码是否正确(使用 Django 的密码哈希校验)
if user.check_password(password):
return user# 认证成功返回用户对象
return user
except get_user_model().DoesNotExist:
# 用户不存在时返回 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
#lxy核心认证逻辑判断输入是否为邮箱含@分别用邮箱或用户名查询用户验证密码后返回用户对象若用户不存在则返回None。
def get_user(self, username):
"""
<<<<<<< HEAD
根据用户 ID 获取用户对象用于 Session 认证等场景
参数:
username: 实际是用户的 primary key通常由 Session 存储
返回:
找到用户返回 User 对象否则返回 None
"""
try:
# 通过主键查询用户Django 默认行为)
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
# 用户不存在时返回 None
return None
<<<<<<< HEAD
<<<<<<< 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
#lxy根据用户ID主键查询用户不存在则返回None用于Django认证系统的用户查询环节

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

@ -1,189 +1,59 @@
# 导入日志模块,用于记录运行时的信息和错误
import logging
# Django 国际化工具,`gettext_lazy` 用于延迟翻译(适合模块级字符串)
from django.utils.translation import gettext_lazy as _
# Django 配置项,用于访问 settings.py 中的设置
from django.conf import settings
# Django 认证系统核心模块
from django.contrib import auth
<<<<<<< HEAD
<<<<<<< HEAD
# 认证相关常量(如重定向字段名)
from django.contrib.auth import REDIRECT_FIELD_NAME
# 获取当前用户模型的快捷方式
from django.contrib.auth import get_user_model
# 用户登出功能
from django.contrib.auth import logout
# Django 内置的认证表单(如登录表单)
from django.contrib.auth.forms import AuthenticationForm
# 密码哈希工具
from django.contrib.auth.hashers import make_password
# HTTP 响应类(重定向、禁止访问等)
from django.http import HttpResponseRedirect, HttpResponseForbidden
# HTTP 请求和响应的类型提示(可选,用于类型检查)
from django.http.request import HttpRequest
from django.http.response import HttpResponse
# 快捷函数(如获取对象或返回 404
from django.shortcuts import get_object_or_404
# 渲染模板的快捷方式
from django.shortcuts import render
# URL 反转工具(通过名称生成 URL
from django.urls import reverse
# 视图装饰器工具
from django.utils.decorators import method_decorator
# URL 安全验证工具(防止重定向攻击)
from django.utils.http import url_has_allowed_host_and_scheme
# 基础视图类
from django.views import View
# 缓存控制装饰器(禁用缓存)
from django.views.decorators.cache import never_cache
# CSRF 防护装饰器
from django.views.decorators.csrf import csrf_protect
# 敏感参数标记(如密码字段)
from django.views.decorators.debug import sensitive_post_parameters
# 通用视图类(表单视图、重定向视图)
from django.views.generic import FormView, RedirectView
# 项目自定义工具
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
# 当前应用的工具模块
from . import utils
# 当前应用的表单(注册、登录、忘记密码等)
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
# 当前应用的模型(博客用户)
from .models import BlogUser
# 初始化日志记录器__name__ 表示当前模块名)
logger = logging.getLogger(__name__)
=======
from django.contrib.auth import REDIRECT_FIELD_NAME # 登录后重定向字段名常量
from django.contrib.auth import get_user_model # 获取项目配置的用户模型
from django.contrib.auth import logout # 登出功能函数
from django.contrib.auth.forms import AuthenticationForm # Django内置登录表单
from django.contrib.auth.hashers import make_password # 密码哈希处理函数
from django.http import HttpResponseRedirect, HttpResponseForbidden # HTTP响应类
=======
from django.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.
# 注册视图类(继承自 FormView处理表单提交
=======
logger = logging.getLogger(__name__) # jyn:初始化日志记录器
# jyn:Create your views here.
>>>>>>> JYN_branch
class RegisterView(FormView):
<<<<<<< HEAD
# 指定使用的表单类
form_class = RegisterForm
<<<<<<< HEAD
<<<<<<< HEAD
template_name = 'account/registration_form.html'
# 使用装饰器确保视图禁用缓存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)
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)
# 表单验证通过后的处理逻辑
def form_valid(self, form):
<<<<<<< HEAD
# 再次检查表单有效性(冗余,因为 FormView 已验证)
if form.is_valid():
# 保存用户对象(但暂不激活 is_active=False
user = form.save(False)
user.is_active = False# 用户需验证邮箱后才能登录
user.source = 'Register' # 标记用户来源为注册
user.save(True)# 实际保存到数据库
# 获取当前站点域名(用于生成验证链接)
site = get_current_site().domain
# 生成签名(双重 SHA256 哈希,用于验证链接安全性)
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 开发环境使用本地地址(避免因域名未配置导致链接失效)
if settings.DEBUG:
site = '127.0.0.1:8000'
# 生成验证结果的 URL如 /account/result/?type=validation&id=1&sign=abc123
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# 构造邮件内容(包含验证链接)
=======
"""表单验证通过后执行的逻辑(注册核心流程)"""
def form_valid(self, form):
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生成签名确保链接安全性
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
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 = """
<p>请点击下面链接验证您的邮箱</p>
@ -194,577 +64,140 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# 发送验证邮件
send_email(
<<<<<<< HEAD
<<<<<<< HEAD
emailto=[
user.email, # 收件人列表
user.email,
],
title='验证您的电子邮箱', # 邮件标题
content=content)# 邮件正文
# 重定向到注册结果页面(附带用户 ID
title='验证您的电子邮箱',
content=content)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
# 表单无效时重新渲染表单(显示错误信息)
return self.render_to_response({
'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重定向到登录页面
class LogoutView(RedirectView):
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
# 登出后重定向的URL
url = '/login/'
# 使用never_cache装饰器确保视图不会被缓存
=======
url = '/login/'#处理用户登出,登出后重定向到/login/
=======
url = '/login/'#lxy处理用户登出登出后重定向到/login/
>>>>>>> LXY_branch
>>>>>>> LXY_branch
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
# 调用父类的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)
# 处理GET请求
def get(self, request, *args, **kwargs):
<<<<<<< HEAD
# 执行登出操作
logout(request)
# 删除侧边栏缓存
delete_sidebar_cache()
<<<<<<< HEAD
<<<<<<< HEAD
# 调用父类的get方法完成重定向
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
class LoginView(FormView):
<<<<<<< HEAD
# 使用的表单类
form_class = LoginForm
# 模板文件路径
template_name = 'account/login.html'
# 登录成功后跳转的URL
success_url = '/'
# 重定向字段名
redirect_field_name = REDIRECT_FIELD_NAME
<<<<<<< HEAD
# 登录会话有效期(一个月的时间,单位:秒)
login_ttl = 2626560 # 一个月的时间
# 使用多个装饰器装饰dispatch方法
@method_decorator(sensitive_post_parameters('password'))# 标记密码参数为敏感信息
@method_decorator(csrf_protect) # 启用CSRF保护
@method_decorator(never_cache)# 禁止缓存
def dispatch(self, request, *args, **kwargs):
# 调用父类的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(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
>>>>>>> JYN_branch
return super(LoginView, self).dispatch(request, *args, **kwargs)
# 获取模板上下文数据
def get_context_data(self, **kwargs):
<<<<<<< HEAD
# 从GET参数中获取重定向URL
=======
"""添加额外上下文数据(重定向地址)到模板"""
<<<<<<< HEAD
# 获取URL中的重定向参数如登录前访问的受保护页面
>>>>>>> JYN_branch
=======
# jyn:获取URL中的重定向参数如登录前访问的受保护页面
>>>>>>> JYN_branch
redirect_to = self.request.GET.get(self.redirect_field_name)
# 如果不存在则设置为根路径
if redirect_to is None:
<<<<<<< HEAD
<<<<<<< HEAD
redirect_to = '/'
# 将重定向URL添加到上下文
kwargs['redirect_to'] = redirect_to
# 调用父类方法获取其他上下文数据
<<<<<<< HEAD
<<<<<<< HEAD
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):
# 重新创建认证表单这里可能有逻辑问题因为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)
# 再次验证表单
if form.is_valid():
<<<<<<< HEAD
<<<<<<< HEAD
# 删除侧边栏缓存
delete_sidebar_cache()
# 记录日志
logger.info(self.redirect_field_name)
# 登录用户
auth.login(self.request, form.get_user())
# 如果用户选择了"记住我"
=======
delete_sidebar_cache() # 删除侧边栏缓存(更新用户登录状态)
logger.info(self.redirect_field_name) # 日志记录重定向字段名
=======
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"):
# 设置较长的会话过期时间
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)
<<<<<<< HEAD
=======
#lxyreturn HttpResponseRedirect('/')
>>>>>>> LXY_branch
else:
<<<<<<< HEAD
<<<<<<< HEAD
# 表单无效,重新渲染表单并显示错误
return self.render_to_response({
'form': form
<<<<<<< HEAD
<<<<<<< HEAD
})
# 获取成功后的跳转URL
=======
})#form_valid方法中验证表单登录用户并根据“记住我”选项设置会话过期时间
=======
})#lxyform_valid方法中验证表单登录用户并根据“记住我”选项设置会话过期时间
>>>>>>> LXY_branch
>>>>>>> LXY_branch
def get_success_url(self):
# 从POST参数中获取重定向URL
redirect_to = self.request.POST.get(self.redirect_field_name)
# 检查URL是否安全
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
# 如果不安全则使用默认成功URL
redirect_to = self.success_url
<<<<<<< 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#get_success_url方法处理登录后的重定向地址确保其安全性
>>>>>>> LXY_branch
=======
return redirect_to#lxyget_success_url方法处理登录后的重定向地址确保其安全性
>>>>>>> LXY_branch
# 账户操作结果页面(如注册成功、邮箱验证等)
def account_result(request):
<<<<<<< HEAD
# 从GET参数中获取类型和用户ID
type = request.GET.get('type')
id = request.GET.get('id')
# 获取用户对象如果不存在返回404
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
# 如果用户已激活,直接重定向到首页
if user.is_active:
return HttpResponseRedirect('/')
# 检查类型参数是否有效
=======
"""
账号操作结果视图函数函数视图
处理注册成功提示邮箱验证逻辑并展示结果页面
"""
# 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 == 'register':
# jyn:注册成功:提示用户查收验证邮件
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
<<<<<<< HEAD
<<<<<<< HEAD
# 生成验证签名
=======
# 邮箱验证:验证签名是否正确,正确则激活用户
# 重新计算签名与URL中的签名对比防止链接篡改
>>>>>>> JYN_branch
=======
# jyn:邮箱验证:验证签名是否正确,正确则激活用户
# jyn:重新计算签名与URL中的签名对比防止链接篡改
>>>>>>> JYN_branch
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 获取请求中的签名
sign = request.GET.get('sign')
# 验证签名是否匹配
if sign != c_sign:
<<<<<<< HEAD
<<<<<<< HEAD
return HttpResponseForbidden()
# 激活用户账户
user.is_active = True
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 = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
<<<<<<< HEAD
<<<<<<< HEAD
# 渲染结果页面
=======
# 渲染结果页面,传递标题和内容
>>>>>>> JYN_branch
=======
# jyn:渲染结果页面,传递标题和内容
>>>>>>> JYN_branch
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
<<<<<<< HEAD
<<<<<<< HEAD
# 无效类型重定向到首页
=======
# 操作类型不合法,重定向到首页
>>>>>>> JYN_branch
=======
# jyn:操作类型不合法,重定向到首页
>>>>>>> JYN_branch
return HttpResponseRedirect('/')
# 忘记密码视图继承自FormView
class ForgetPasswordView(FormView):
<<<<<<< HEAD
# 使用的表单类
form_class = ForgetPasswordForm
<<<<<<< HEAD
<<<<<<< HEAD
# 模板文件路径
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):
"""表单验证通过后执行的逻辑(密码重置核心流程)"""
if form.is_valid():
<<<<<<< HEAD
<<<<<<< HEAD
# 根据邮箱获取用户对象
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# 设置新密码(加密)
=======
# 1. 获取表单中的邮箱,查询对应的用户
blog_user = BlogUser.objects.filter(
email=form.cleaned_data.get("email")
).get()
# 2. 对新密码进行哈希处理,并更新用户密码
>>>>>>> JYN_branch
=======
# 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.save()
<<<<<<< HEAD
<<<<<<< HEAD
# 重定向到登录页面
return HttpResponseRedirect('/login/')
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})
# 忘记密码验证码发送视图继承自View
class ForgetPasswordEmailCode(View):
<<<<<<< HEAD
# 处理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):
# 验证表单
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
# 表单无效返回错误
return HttpResponse("错误的邮箱")
# 获取邮箱地址
to_email = form.cleaned_data["email"]
# 生成验证码
code = generate_code()
# 发送验证邮件
utils.send_verify_email(to_email, code)
# 存储验证码(通常有有效期)
utils.set_code(to_email, code)
<<<<<<< HEAD
# 返回成功响应
return HttpResponse("ok")
=======
"""
发送密码重置验证码类视图继承自基础View
负责验证邮箱合法性生成验证码发送验证邮件并返回结果
"""
def post(self, request: HttpRequest):
"""处理POST请求发送验证码核心逻辑"""
#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,38 +1,14 @@
from django import forms
#ymq导入Django的forms模块用于创建表单
from django.contrib import admin
#ymq导入Django的admin模块用于后台管理配置
from django.contrib.auth import get_user_model
#ymq导入获取用户模型的函数便于灵活引用用户模型
from django.urls import reverse
#ymq导入reverse函数用于生成URL反向解析
from django.utils.html import format_html
#ymq导入format_html函数用于安全生成HTML内容
from django.utils.translation import gettext_lazy as _
#ymq导入国际化翻译函数将文本标记为可翻译
# Register your models here.
from .models import Article
#ymq从当前应用的models模块导入Article模型
<<<<<<< HEAD
class ArticleForm(forms.ModelForm):
#ymq定义Article模型对应的表单类继承自ModelForm
# body = forms.CharField(widget=AdminPagedownWidget())
#ymq注释掉的代码原本计划为body字段使用AdminPagedownWidget编辑器
class Meta:
#ymqMeta类用于配置表单元数据
model = Article
#ymq指定表单关联的模型为Article
fields = '__all__'
#ymq指定表单包含模型的所有字段
def makr_article_publish(modeladmin, request, queryset):
#ymq定义批量发布文章的动作函数
=======
class ArticleForm(forms.ModelForm):#lxy 文章表单类
# body = forms.CharField(widget=AdminPagedownWidget())#lxy 富文本组件
@ -42,68 +18,32 @@ class ArticleForm(forms.ModelForm):#lxy 文章表单类
def makr_article_publish(modeladmin, request, queryset):#lxy 批量设为已发布
>>>>>>> LXY_branch
queryset.update(status='p')
#ymq将选中的文章状态更新为'p'(发布状态)
<<<<<<< HEAD
def draft_article(modeladmin, request, queryset):
#ymq定义批量设为草稿的动作函数
=======
def draft_article(modeladmin, request, queryset):#lxy 批量设为草稿
>>>>>>> LXY_branch
queryset.update(status='d')
#ymq将选中的文章状态更新为'd'(草稿状态)
<<<<<<< HEAD
def close_article_commentstatus(modeladmin, request, queryset):
#ymq定义批量关闭评论的动作函数
=======
def close_article_commentstatus(modeladmin, request, queryset):#lxy 关闭评论
>>>>>>> LXY_branch
queryset.update(comment_status='c')
#ymq将选中的文章评论状态更新为'c'(关闭状态)
<<<<<<< HEAD
def open_article_commentstatus(modeladmin, request, queryset):
#ymq定义批量开启评论的动作函数
=======
def open_article_commentstatus(modeladmin, request, queryset):#lxy 开启评论
>>>>>>> LXY_branch
queryset.update(comment_status='o')
#ymq将选中的文章评论状态更新为'o'(开启状态)
#lxy 操作描述
makr_article_publish.short_description = _('Publish selected articles')
#ymq设置发布动作在admin中的显示名称支持国际化
draft_article.short_description = _('Draft selected articles')
#ymq设置草稿动作在admin中的显示名称支持国际化
close_article_commentstatus.short_description = _('Close article comments')
#ymq设置关闭评论动作在admin中的显示名称支持国际化
open_article_commentstatus.short_description = _('Open article comments')
#ymq设置开启评论动作在admin中的显示名称支持国际化
<<<<<<< HEAD
class ArticlelAdmin(admin.ModelAdmin):
#ymq定义Article模型的admin管理类继承自ModelAdmin
list_per_page = 20
#ymq设置每页显示20条记录
search_fields = ('body', 'title')
#ymq设置可搜索的字段为body和title
form = ArticleForm
#ymq指定使用自定义的ArticleForm表单
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',
'title',
'author',
@ -113,124 +53,43 @@ class ArticlelAdmin(admin.ModelAdmin):#lxy 文章Admin配置
'status',
'type',
'article_order')
<<<<<<< HEAD
#ymq设置列表页显示的字段
list_display_links = ('id', 'title')
#ymq设置列表页中可点击跳转编辑页的字段
list_filter = ('status', 'type', 'category')
#ymq设置可用于筛选的字段
filter_horizontal = ('tags',)
#ymq设置多对多字段的水平筛选器tags字段
exclude = ('creation_time', 'last_modify_time')
#ymq设置编辑页中排除的字段不显示
view_on_site = True
#ymq启用"在站点上查看"功能
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,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
#ymq注册批量操作动作
<<<<<<< HEAD
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)
#ymq获取分类模型的应用标签和模型名称
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))
#ymq返回HTML链接点击可跳转到分类编辑页
<<<<<<< HEAD
link_to_category.short_description = _('category')
#ymq设置自定义字段在列表页的显示名称支持国际化
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)
>>>>>>> LXY_branch
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
#ymq限制作者字段只能选择超级用户
return form
#ymq返回修改后的表单
<<<<<<< HEAD
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)
#ymq调用父类的保存方法完成默认保存
<<<<<<< HEAD
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:
#ymq如果有具体对象返回对象的完整URL
url = obj.get_full_url()
return url
else:
#ymq如果无对象返回当前站点域名
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
<<<<<<< HEAD
class TagAdmin(admin.ModelAdmin):
#ymq定义Tag模型的admin管理类
exclude = ('slug', 'last_mod_time', 'creation_time')
#ymq编辑页排除slug、最后修改时间和创建时间字段
class CategoryAdmin(admin.ModelAdmin):
#ymq定义Category模型的admin管理类
list_display = ('name', 'parent_category', 'index')
#ymq列表页显示名称、父分类和排序索引字段
exclude = ('slug', 'last_mod_time', 'creation_time')
#ymq编辑页排除slug、最后修改时间和创建时间字段
class LinksAdmin(admin.ModelAdmin):
#ymq定义Links模型的admin管理类
exclude = ('last_mod_time', 'creation_time')
#ymq编辑页排除最后修改时间和创建时间字段
class SideBarAdmin(admin.ModelAdmin):
#ymq定义SideBar模型的admin管理类
list_display = ('name', 'content', 'is_enable', 'sequence')
#ymq列表页显示名称、内容、是否启用和排序序号字段
exclude = ('last_mod_time', 'creation_time')
#ymq编辑页排除最后修改时间和创建时间字段
class BlogSettingsAdmin(admin.ModelAdmin):
#ymq定义BlogSettings模型的admin管理类
=======
class TagAdmin(admin.ModelAdmin):#lxy 标签Admin配置
exclude = ('slug', 'last_mod_time', 'creation_time')#lxy 隐藏字段
@ -250,6 +109,4 @@ class SideBarAdmin(admin.ModelAdmin): #lxy 侧边栏Admin配置
class BlogSettingsAdmin(admin.ModelAdmin):#lxy 博客设置Admin配置
>>>>>>> LXY_branch
pass
#ymq暂未设置特殊配置使用默认admin行为

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

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

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

@ -1,41 +1,8 @@
<<<<<<< HEAD
import logging #导入 Python 标准库的 logging 模块,用于日志记录,方便追踪程序运行过程中的关键信息。
=======
import logging
#ymq导入logging模块用于记录搜索相关日志
>>>>>>> c6856732b39cce6b1aab30e6649dcdb806b75b9f
from django import forms
#ymq导入Django的forms模块用于创建自定义表单
from haystack.forms import SearchForm
#ymq导入Haystack的SearchForm基类扩展实现博客搜索表单
<<<<<<< HEAD
logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
class BlogSearchForm(SearchForm):
#ymq定义博客搜索表单类继承自Haystack的SearchForm
querydata = forms.CharField(required=True)
#ymq定义搜索关键词字段required=True表示该字段为必填项
def search(self):
#ymq重写父类的search方法自定义搜索逻辑
datas = super(BlogSearchForm, self).search()
#ymq调用父类search方法获取基础搜索结果
if not self.is_valid():
#ymq如果表单数据验证不通过返回无结果响应
return self.no_query_found()
if self.cleaned_data['querydata']:
#ymq如果存在合法的搜索关键词记录关键词日志
logger.info(self.cleaned_data['querydata'])
return datas
#ymq返回最终的搜索结果集
=======
logger = logging.getLogger(__name__)#lxy 获取当前模块的日志记录器
@ -50,4 +17,3 @@ class BlogSearchForm(SearchForm): #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
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
#ymq从blog.documents导入Elasticsearch相关的文档类和管理器以及启用状态常量
# TODO 参数化
class Command(BaseCommand):
#ymq定义自定义管理命令类继承自BaseCommand
help = 'build search index'
#ymq定义命令的帮助信息使用python manage.py help build_index时显示
def handle(self, *args, **options):
#ymq命令的核心处理方法执行实际的索引构建逻辑
if ELASTICSEARCH_ENABLED:
#ymq仅当Elasticsearch启用时执行以下操作
ElaspedTimeDocumentManager.build_index()
#ymq调用性能耗时文档管理器构建索引若不存在
manager = ElapsedTimeDocument()
manager.init()
#ymq初始化ElapsedTimeDocument对应的索引结构
manager = ArticleDocumentManager()
manager.delete_index()
#ymq删除已存在的文章索引重建前清理
manager.rebuild()
#ymq重建文章索引将数据库中的文章数据同步到Elasticsearch

@ -1,20 +1,13 @@
from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from blog.models import Tag, Category
#ymq从blog应用导入Tag标签和Category分类模型
# TODO 参数化
class Command(BaseCommand):
#ymq定义自定义管理命令类继承自BaseCommand
help = 'build search words'
#ymq命令的帮助信息说明该命令用于生成搜索词
def handle(self, *args, **options):
#ymq命令的核心处理方法执行生成搜索词的逻辑
# 从标签和分类中提取名称使用set去重
datas = set([t.name for t in Tag.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
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from djangoblog.utils import cache
#ymq从项目工具模块导入缓存工具
class Command(BaseCommand):
#ymq定义清除缓存的自定义命令类继承自BaseCommand
help = 'clear the whole cache'
#ymq命令的帮助信息说明该命令用于清除所有缓存
def handle(self, *args, **options):
#ymq命令的核心处理方法执行清除缓存操作
cache.clear() # 调用缓存工具的clear方法清除所有缓存数据
cache.clear()
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
#ymq向标准输出写入成功信息使用Django的SUCCESS样式通常为绿色

@ -1,62 +1,40 @@
from django.contrib.auth import get_user_model
#ymq导入获取用户模型的函数便于灵活引用用户模型
from django.contrib.auth.hashers import make_password
#ymq导入密码加密函数用于安全存储密码
from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from blog.models import Article, Tag, Category
#ymq从blog应用导入文章、标签、分类模型
class Command(BaseCommand):
#ymq定义创建测试数据的自定义命令类继承自BaseCommand
help = 'create test datas'
#ymq命令的帮助信息说明该命令用于创建测试数据
def handle(self, *args, **options):
#ymq命令的核心处理方法执行创建测试数据的逻辑
# 创建或获取测试用户(邮箱、用户名、密码加密存储)
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# 创建或获取父分类
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
# 创建或获取子分类(关联父分类)
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
category.save() # 保存子分类
# 创建基础标签
category.save()
basetag = Tag()
basetag.name = "标签"
basetag.save()
# 批量创建20篇测试文章
for i in range(1, 20):
# 创建或获取文章(关联分类、作者)
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i), # 文章标题带序号
body='nice content ' + str(i), # 文章内容带序号
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
# 创建带序号的标签
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
# 给文章添加标签(包含基础标签和序号标签)
article.tags.add(tag)
article.tags.add(basetag)
article.save() # 保存文章
article.save()
# 清除缓存,确保测试数据立即生效
from djangoblog.utils import cache
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
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from djangoblog.spider_notify import SpiderNotify
#ymq导入蜘蛛通知工具类用于向搜索引擎提交URL
from djangoblog.utils import get_current_site
#ymq导入获取当前站点信息的工具函数
from blog.models import Article, Tag, Category
#ymq从blog应用导入文章、标签、分类模型
site = get_current_site().domain
#ymq获取当前站点的域名用于构建完整URL
class Command(BaseCommand):
#ymq定义百度URL提交命令类继承自BaseCommand
help = 'notify baidu url'
#ymq命令的帮助信息说明该命令用于向百度提交URL
def add_arguments(self, parser):
#ymq定义命令参数指定提交的数据类型
parser.add_argument(
'data_type',
type=str,
@ -28,46 +20,31 @@ class Command(BaseCommand):
'tag',
'category'],
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):
#ymq构建包含域名的完整URL
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
#ymq命令核心处理方法执行URL收集和提交
type = options['data_type'] # 获取用户指定的数据类型
self.stdout.write('start get %s' % type) # 输出开始收集信息的提示
type = options['data_type']
self.stdout.write('start get %s' % type)
urls = [] # 存储待提交的URL列表
# 根据数据类型收集对应的URL
urls = []
if type == 'article' or type == 'all':
# 收集已发布文章的URL
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
if type == 'tag' or type == 'all':
# 收集所有标签页的URL
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
if type == 'category' or type == 'all':
# 收集所有分类页的URL
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
# 输出待提交的URL数量
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
# 调用工具类向百度提交URL
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
#ymq导入requests库用于发送HTTP请求测试图片URL有效性
from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from django.templatetags.static import static
#ymq导入static标签用于获取静态文件URL
from djangoblog.utils import save_user_avatar
#ymq导入保存用户头像的工具函数
from oauth.models import OAuthUser
#ymq从oauth应用导入OAuthUser模型存储第三方用户信息
from oauth.oauthmanager import get_manager_by_type
#ymq导入获取对应第三方登录管理器的函数
class Command(BaseCommand):
#ymq定义同步用户头像的自定义命令类继承自BaseCommand
help = 'sync user avatar'
#ymq命令的帮助信息说明该命令用于同步用户头像
def test_picture(self, url):
#ymq测试图片URL是否有效状态码200
try:
if requests.get(url, timeout=2).status_code == 200:
return True # URL有效返回True
return True
except:
pass # 异常或状态码非200返回None
pass
def handle(self, *args, **options):
#ymq命令核心处理方法执行用户头像同步逻辑
static_url = static("../") # 获取静态文件基础URL
users = OAuthUser.objects.all() # 获取所有第三方用户
self.stdout.write(f'开始同步{len(users)}个用户头像') # 输出待同步用户数量
static_url = static("../")
users = OAuthUser.objects.all()
self.stdout.write(f'开始同步{len(users)}个用户头像')
for u in users:
#ymq遍历每个用户进行头像同步
self.stdout.write(f'开始同步:{u.nickname}') # 输出当前同步的用户名
url = u.picture # 获取用户当前头像URL
self.stdout.write(f'开始同步:{u.nickname}')
url = u.picture
if url:
# 处理已有头像URL的情况
if url.startswith(static_url):
# 头像URL是本地静态文件
if self.test_picture(url):
# 图片有效,跳过同步
continue
else:
# 图片无效,重新获取
if u.metadata:
# 有元数据,通过第三方管理器获取头像
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url) # 保存头像并获取本地URL
url = save_user_avatar(url)
else:
# 无元数据,使用默认头像
url = static('blog/img/avatar.png')
else:
# 头像URL是外部链接保存到本地
url = save_user_avatar(url)
else:
# 无头像URL使用默认头像
url = static('blog/img/avatar.png')
if url:
# 保存更新后的头像URL
self.stdout.write(f'结束同步:{u.nickname}.url:{url}')
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
u.picture = url
u.save()
self.stdout.write('结束同步') # 输出同步完成提示
self.stdout.write('结束同步')

@ -1,59 +1,14 @@
import logging
import time
#ymq导入logging用于日志记录time用于计算页面加载时间
from ipware import get_client_ip
#ymq导入get_client_ip工具用于获取客户端IP地址
from user_agents import parse
#ymq导入parse函数用于解析用户代理字符串
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
#ymq从博客文档模块导入Elasticsearch启用状态和性能日志管理器
logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
<<<<<<< HEAD
class OnlineMiddleware(object):
#ymq定义在线中间件类用于记录页面加载性能和访问信息
def __init__(self, get_response=None):
#ymq初始化中间件接收Django的响应处理器
self.get_response = get_response
super().__init__()
def __call__(self, request):
#ymq中间件核心方法处理请求并返回响应
''' page render time '''
#ymq记录页面渲染时间的逻辑
start_time = time.time() # 记录请求处理开始时间
response = self.get_response(request) # 调用后续中间件或视图处理请求
#ymq获取用户代理和IP地址
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:
try:
cast_time = time.time() - start_time # 计算页面加载耗时(秒)
#ymq如果启用了Elasticsearch记录性能数据
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2) #ymq: 转换为毫秒并保留两位小数
url = request.path # 获取请求的URL路径
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
@ -80,18 +35,9 @@ class OnlineMiddleware(object):#lxy 在线统计中间件
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip) # 替换页面中的加载时间标记
>>>>>>> LXY_branch
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
<<<<<<< HEAD
#ymq捕获并记录处理过程中的异常
logger.error("Error OnlineMiddleware: %s" % e)
return response #ymq: 返回处理后的响应
=======
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
#ymq该迁移文件由Django 4.1.7自动生成生成时间为2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
#ymq导入Django迁移相关模块、时间工具和markdown编辑器字段
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
initial = True
#ymq标记为初始迁移第一次创建模型时生成
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
#ymq依赖于用户模型确保用户表先创建
]
operations = [
#ymq定义数据库操作列表按顺序执行创建模型的操作
migrations.CreateModel(
#ymq创建BlogSettings模型网站配置
name='BlogSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#ymq自增主键字段
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
('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='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
#ymq以上为网站配置的各个字段包含网站基本信息、显示设置、备案信息等
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
#ymq模型的显示名称
},
),
migrations.CreateModel(
#ymq创建Links模型友情链接
name='Links',
fields=[
('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='显示类型')),
('created_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={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'],
#ymq按排序号升序排列
},
),
migrations.CreateModel(
#ymq创建SideBar模型侧边栏
name='SideBar',
fields=[
('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='是否启用')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#ymq侧边栏字段包含标题、内容、排序等
],
options={
'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏',
'ordering': ['sequence'],
#ymq按排序号升序排列
},
),
migrations.CreateModel(
#ymq创建Tag模型标签
name='Tag',
fields=[
('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='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
#ymq标签字段包含名称、URL友好标识slug
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
#ymq按标签名升序排列
},
),
migrations.CreateModel(
#ymq创建Category模型分类
name='Category',
fields=[
('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)),
('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='父级分类')),
#ymq分类字段支持多级分类自关联外键、权重排序等
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['-index'],
#ymq按权重降序排列权重越大越靠前
},
),
migrations.CreateModel(
#ymq创建Article模型文章
name='Article',
fields=[
('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='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
#ymq使用markdown编辑器字段存储文章正文
('pub_time', models.DateTimeField(default=django.utils.timezone.now, 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='评论状态')),
@ -154,19 +124,14 @@ class Migration(migrations.Migration):
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('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='作者')),
#ymq关联用户模型外键级联删除
('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='标签集合')),
#ymq多对多关联标签模型
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'],
#ymq先按排序号降序再按发布时间降序
'get_latest_by': 'id',
#ymq按id获取最新记录
},
),
]
]

@ -1,34 +1,23 @@
# 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
#ymq导入Django迁移相关模块
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [
('blog', '0001_initial'),
#ymq依赖于blog应用的0001_initial迁移文件确保先执行初始迁移
]
operations = [
#ymq定义数据库操作列表添加新字段
migrations.AddField(
#ymq向BlogSettings模型添加global_footer字段
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
#ymq字段类型为文本字段允许为空默认值为空字符串verbose_name为"公共尾部"
),
migrations.AddField(
#ymq向BlogSettings模型添加global_header字段
model_name='blogsettings',
name='global_header',
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
#ymq该迁移文件由Django 4.2.1自动生成生成时间为2023-05-09 07:45
from django.db import migrations, models
#ymq导入Django迁移相关模块
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
class Migration(migrations.Migration):
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
#ymq依赖于blog应用的0002号迁移文件确保先执行该迁移
]
operations = [
#ymq定义数据库操作此处为添加字段
migrations.AddField(
#ymq向BlogSettings模型添加comment_need_review字段
model_name='blogsettings', # 目标模型名称
name='comment_need_review', # 新字段名称
model_name='blogsettings',
name='comment_need_review',
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
#ymq该迁移文件由Django 4.2.1自动生成生成时间为2023-05-09 07:51
from django.db import migrations
#ymq导入Django迁移相关模块
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
#ymq依赖于blog应用的0003号迁移文件确保先执行该迁移
]
operations = [
#ymq定义数据库操作列表主要是重命名字段
migrations.RenameField(
#ymq重命名BlogSettings模型的analyticscode字段
model_name='blogsettings', # 目标模型名称
old_name='analyticscode', # 旧字段名
new_name='analytics_code', # 新字段名(改为下划线命名规范)
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
migrations.RenameField(
#ymq重命名BlogSettings模型的beiancode字段
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code', # 改为下划线命名规范
new_name='beian_code',
),
migrations.RenameField(
#ymq重命名BlogSettings模型的sitename字段
model_name='blogsettings',
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
#ymq该迁移文件由Django 4.2.5自动生成生成时间为2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
#ymq导入Django迁移相关模块、时间工具和markdown编辑器字段
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
#ymq依赖于用户模型和blog应用的0004号迁移文件
]
operations = [
#ymq定义数据库操作列表包含模型选项修改、字段删除、添加和修改
# 修改模型的元数据选项主要是verbose_name的国际化调整
migrations.AlterModelOptions(
name='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',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# 删除旧的时间字段(命名方式调整)
migrations.RemoveField(
model_name='article',
name='created_time',
@ -76,8 +67,6 @@ class Migration(migrations.Migration):
model_name='tag',
name='last_mod_time',
),
# 添加新的时间字段统一命名为creation_time和last_modify_time
migrations.AddField(
model_name='article',
name='creation_time',
@ -118,8 +107,6 @@ class Migration(migrations.Migration):
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 修改Article模型的字段属性主要是verbose_name国际化
migrations.AlterField(
model_name='article',
name='article_order',
@ -180,8 +167,6 @@ class Migration(migrations.Migration):
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
# 修改BlogSettings模型的字段属性verbose_name国际化
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
@ -237,8 +222,6 @@ class Migration(migrations.Migration):
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
# 修改Category模型的字段属性
migrations.AlterField(
model_name='category',
name='index',
@ -254,8 +237,6 @@ class Migration(migrations.Migration):
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(
model_name='links',
name='is_enable',
@ -286,8 +267,6 @@ class Migration(migrations.Migration):
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(
model_name='sidebar',
name='content',
@ -313,11 +292,9 @@ class Migration(migrations.Migration):
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
# 修改Tag模型的字段属性
migrations.AlterField(
model_name='tag',
name='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
#ymq该迁移文件由Django 4.2.7自动生成生成时间为2024年1月26日02:41
from django.db import migrations
#ymq导入Django迁移相关模块
class Migration(migrations.Migration):
#ymq定义迁移类继承自migrations.Migration
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
#ymq依赖于blog应用的0005号迁移文件确保先执行该迁移
]
operations = [
#ymq定义数据库操作此处为修改模型选项
migrations.AlterModelOptions(
name='blogsettings',
#ymq修改BlogSettings模型的显示名称改为英文"Website configuration"
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]
]

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

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

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

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

@ -1,7 +1,6 @@
import logging
import os
import uuid
#ymq导入日志、文件操作、UUID生成相关模块
from django.conf import settings
from django.core.paginator import Paginator
@ -15,45 +14,16 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
#ymq导入Django核心组件、视图类、HTTP响应类等
from blog.models import Article, Category, LinkShowType, Links, Tag
#ymq从blog应用导入模型类
from comments.forms import CommentForm
#ymq从comments应用导入评论表单
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
#ymq导入插件钩子相关模块用于扩展文章功能
from djangoblog.utils import cache, get_blog_setting, get_sha256
#ymq导入工具函数用于缓存、获取博客设置和加密
logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
<<<<<<< HEAD
class ArticleListView(ListView):
#ymq文章列表基础视图类继承自Django的ListView
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY # 分页大小,从配置中获取
page_kwarg = 'page' # 页码参数名
link_type = LinkShowType.L # 链接展示类型
def get_view_cache_key(self):
#ymq获取视图缓存键未实际使用预留方法
return self.request.get['pages']
@property
def page_number(self):
#ymq获取当前页码从URL参数或默认值
=======
class ArticleListView(ListView): #lxy 文章列表视图基类
template_name = 'blog/article_index.html' #lxy 渲染模板
context_object_name = 'article_list' #lxy 模板中数据变量名
@ -67,7 +37,6 @@ class ArticleListView(ListView): #lxy 文章列表视图基类
@property
def page_number(self):#lxy 获取当前页码
>>>>>>> LXY_branch
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
@ -77,13 +46,13 @@ class ArticleListView(ListView): #lxy 文章列表视图基类
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError() # 强制子类实现该方法
raise NotImplementedError()
def get_queryset_data(self):#lxy 子类需重写:获取查询集数据
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError() # 强制子类实现该方法
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):#lxy 从缓存取查询集
'''
@ -96,13 +65,8 @@ class ArticleListView(ListView): #lxy 文章列表视图基类
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
<<<<<<< HEAD
article_list = self.get_queryset_data() # 调用子类实现的方法获取数据
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))
return article_list
@ -111,64 +75,46 @@ class ArticleListView(ListView): #lxy 文章列表视图基类
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key() # 获取缓存键
value = self.get_queryset_from_cache(key) # 从缓存获取数据
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
<<<<<<< HEAD
def get_context_data(self, **kwargs):
#ymq扩展上下文数据添加链接类型
=======
def get_context_data(self, **kwargs):#lxy 补充上下文数据
>>>>>>> LXY_branch
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 首页视图
'''
首页视图
首页
'''
<<<<<<< HEAD
# 友情链接类型:首页展示
link_type = LinkShowType.I
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')
return article_list
def get_queryset_cache_key(self):
#ymq生成首页缓存键包含页码
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面视图
文章详情页面
'''
template_name = 'blog/article_detail.html' # 详情页模板
model = Article # 关联模型
pk_url_kwarg = 'article_id' # URL中主键参数名
context_object_name = "article" # 模板中上下文变量名
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
def get_context_data(self, **kwargs):
#ymq扩展文章详情页的上下文数据
comment_form = CommentForm() # 初始化评论表单
comment_form = CommentForm()
# 获取文章评论列表
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None) # 过滤顶级评论
blog_setting = get_blog_setting() # 获取博客设置
# 评论分页处理
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
@ -184,37 +130,26 @@ class ArticleDetailView(DetailView):
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
# 生成评论分页链接
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 向上下文添加数据
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
<<<<<<< HEAD
# 上一篇/下一篇文章
kwargs['next_article'] = self.object.next_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)
article = self.object
# 触发插件钩子:文章详情已获取
# Action Hook, 通知插件"文章详情已获取"
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,
request=self.request)
@ -223,35 +158,23 @@ class ArticleDetailView(DetailView):
class CategoryDetailView(ArticleListView):#lxy 分类详情视图
'''
分类目录列表视图
分类目录列表
'''
<<<<<<< HEAD
page_type = "分类目录归档" # 页面类型标识
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']
category = get_object_or_404(Category, slug=slug)
>>>>>>> LXY_branch
categoryname = category.name
self.categoryname = categoryname
# 获取所有子分类名称
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
# 查询属于当前分类及子分类的已发布文章
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
#ymq生成分类列表缓存键
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
@ -261,69 +184,59 @@ class CategoryDetailView(ArticleListView):#lxy 分类详情视图
return cache_key
def get_context_data(self, **kwargs):
#ymq扩展分类页上下文数据
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1] # 处理多级分类名称
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(** kwargs)
return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView):
'''
作者详情页视图
作者详情页
'''
page_type = '作者文章归档' # 页面类型标识
page_type = '作者文章归档'
def get_queryset_cache_key(self):
#ymq生成作者文章列表缓存键
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(
author_name=author_name, page=self.page_number)
return cache_key
def get_queryset_data(self):
#ymq获取指定作者的文章列表
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p') # 过滤已发布的文章
author__username=author_name, type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
#ymq扩展作者页上下文数据
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(** kwargs)
return super(AuthorDetailView, self).get_context_data(**kwargs)
class TagDetailView(ArticleListView):
'''
标签列表页面视图
标签列表页面
'''
page_type = '分类标签归档' # 页面类型标识
page_type = '分类标签归档'
def get_queryset_data(self):
#ymq获取指定标签的文章列表
slug = self.kwargs['tag_name'] # 从URL获取标签别名
tag = get_object_or_404(Tag, slug=slug) # 获取标签对象
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
# 查询包含当前标签的已发布文章
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
<<<<<<< HEAD
def get_queryset_cache_key(self):
#ymq生成标签文章列表缓存键
=======
def get_queryset_cache_key(self):#lxy 标签缓存键(含标签名、页码)
>>>>>>> LXY_branch
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
@ -333,56 +246,30 @@ class TagDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
#ymq扩展标签页上下文数据
# tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
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 文章归档视图
'''
文章归档页面视图
文章归档页面
'''
page_type = '文章归档' # 页面类型标识
paginate_by = None # 不分页
page_kwarg = None # 无页码参数
template_name = 'blog/article_archives.html' # 归档页模板
<<<<<<< HEAD
def get_queryset_data(self):
#ymq获取所有已发布文章用于归档
return Article.objects.filter(status='p').all()
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
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'
return cache_key
<<<<<<< HEAD
class LinkListView(ListView):
#ymq友情链接列表视图
model = Links # 关联模型
template_name = 'blog/links_list.html' # 链接列表模板
def get_queryset(self):
#ymq只获取启用的友情链接
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
#ymqElasticsearch搜索视图继承自Haystack的SearchView
def get_context(self):
#ymq构建搜索结果页面的上下文数据
paginator, page = self.build_page() # 处理分页
=======
class LinkListView(ListView):#lxy 友链列表视图
model = Links
template_name = 'blog/links_list.html' #lxy 渲染模板
@ -394,78 +281,66 @@ class LinkListView(ListView):#lxy 友链列表视图
class EsSearchView(SearchView):#lxy ES搜索视图
def get_context(self):#lxy 补充搜索上下文
paginator, page = self.build_page()
>>>>>>> LXY_branch
context = {
"query": self.query, # 搜索关键词
"form": self.form, # 搜索表单
"page": page, # 当前页数据
"paginator": paginator, # 分页器
"suggestion": None, # 搜索建议(默认无)
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
# 如果启用拼写建议,添加建议内容
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context()) # 添加额外上下文
context.update(self.extra_context())
return context
@csrf_exempt # 禁用CSRF保护用于外部调用
@csrf_exempt
def fileupload(request):
"""
图片/文件上传接口需验证签名
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None) # 获取签名参数
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden() # 无签名则拒绝
# 验证签名双重SHA256加密对比
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden() # 签名错误则拒绝
response = [] # 存储上传后的文件URL
return HttpResponseForbidden()
response = []
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d') # 按日期组织文件
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # 图片扩展名
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
# 判断是否为图片
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)
if not os.path.exists(base_dir):
os.makedirs(base_dir) # 目录不存在则创建
# 生成唯一文件名UUID+原扩展名)
os.makedirs(base_dir)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 安全校验:防止路径遍历攻击
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
# 保存文件
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# 图片压缩处理
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True) # 压缩质量为20
# 生成文件访问URL
image.save(savepath, quality=20, optimize=True)
url = static(savepath)
response.append(url)
return HttpResponse(response) # 返回所有上传文件的URL
return HttpResponse(response)
else:
return HttpResponse("only for post") # 只允许POST方法
return HttpResponse("only for post")
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
#ymq404错误处理视图
if exception:
logger.error(exception) # 记录错误日志
logger.error(exception)
url = request.get_full_path()
return render(request,
template_name,
@ -475,7 +350,6 @@ def page_not_found_view(
def server_error_view(request, template_name='blog/error_page.html'):
#ymq500错误处理视图
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
@ -487,9 +361,8 @@ def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
#ymq403错误处理视图
if exception:
logger.error(exception) # 记录错误日志
logger.error(exception)
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
@ -497,6 +370,5 @@ def permission_denied_view(
def clean_cache_view(request):
#ymq清理缓存的视图
cache.clear() # 清除所有缓存
return HttpResponse('ok') # 返回成功响应
cache.clear()
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.urls import reverse # jyn: 用于反向解析 admin 后台的模型编辑页面 URL
from django.utils.html import format_html # jyn: 用于安全生成 HTML 标签,避免 XSS 风险
from django.utils.translation import gettext_lazy as _ # jyn: 用于国际化翻译,支持多语言显示
>>>>>>> JYN_branch
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
def disable_commentstatus(modeladmin, request, queryset):
"""
<<<<<<< HEAD
自定义Admin批量操作批量禁用选中的评论
参数说明
- modeladmin当前关联的Admin模型类实例
- request当前请求对象
- queryset用户在Admin中选中的评论数据集合
"""
# 批量更新选中评论的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):
"""
<<<<<<< HEAD
自定义Admin批量操作批量启用选中的评论
参数与disable_commentstatus一致功能相反
"""
# 批量更新选中评论的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')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
"""jyn: 评论模型Comment的 Admin 配置类,控制后台展示和操作逻辑"""
list_per_page = 20 # jyn: 列表页每页显示 20 条评论记录
list_per_page = 20
list_display = (
'id', # jyn: 评论 ID
'body', # jyn: 评论内容
'link_to_userinfo', # jyn: 自定义字段,显示评论作者跳转链接
'link_to_article', # jyn: 自定义字段,显示关联文章跳转链接
'is_enable', # jyn: 评论启用状态True/False
'creation_time' # jyn: 评论创建时间
)
list_display_links = ('id', 'body', 'is_enable') # jyn: 列表页中可点击跳转编辑页的字段
list_filter = ('is_enable',) # jyn: 右侧筛选器,按启用状态筛选评论
exclude = ('creation_time', 'last_modify_time') # jyn: 编辑页隐藏的字段(自动生成,无需手动修改)
actions = [disable_commentstatus, enable_commentstatus] # jyn: 注册批量操作函数
>>>>>>> JYN_branch
'id',
'body',
'link_to_userinfo',
'link_to_article',
'is_enable',
'creation_time')
list_display_links = ('id', 'body', 'is_enable')
list_filter = ('is_enable',)
exclude = ('creation_time', 'last_modify_time')
actions = [disable_commentstatus, enable_commentstatus]
# 2. 自定义列表页字段:生成“评论作者”的跳转链接
def link_to_userinfo(self, obj):
"""
<<<<<<< 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)
# jyn: 反向解析作者模型的编辑页 URL传入作者 ID 作为参数
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# jyn: 生成 HTML 链接,优先显示昵称,无昵称则显示邮箱
return format_html(
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):
"""
<<<<<<< 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)
# jyn: 反向解析文章模型的编辑页 URL传入文章 ID 作为参数
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# jyn: 生成 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_article.short_description = _('Article')
>>>>>>> JYN_branch

@ -1,29 +1,5 @@
<<<<<<< HEAD
# 导入Django的App配置基类所有应用的配置类都需继承此类
from django.apps import AppConfig
class CommentsConfig(AppConfig):
"""
comments应用的配置类
作用定义应用的核心标识初始化行为等是Django识别和管理该应用的入口
"""
# 应用的唯一名称必须与应用目录名一致Django通过该值定位应用
name = 'comments'
<<<<<<< HEAD
# 可选扩展配置(当前代码未实现,可根据需求添加):
# - verbose_name应用的人性化名称如 verbose_name = "评论管理"用于Admin后台显示
# - default_auto_field指定模型默认的主键类型如 default_auto_field = "django.db.models.BigAutoField"
=======
>>>>>>> ZYY_branch
=======
# 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
# 导入Django表单核心模块
from django import forms # Django表单基础模块提供表单字段、验证等功能
from django.forms import ModelForm # 模型表单类,可快速将模型转换为表单(减少重复代码)
from django import forms
from django.forms import ModelForm
# 导入当前应用下的Comment模型评论模型表单需与该模型关联
from .models import Comment
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(
widget=forms.HiddenInput, # 表单控件:隐藏输入框(前端不显示,仅用于传递数据)
required=False # 是否必填False表示允许为空普通评论无父评论回复评论时才传值
)
widget=forms.HiddenInput, required=False)
# 2. Meta类模型表单的核心配置关联模型、指定字段等
class Meta:
<<<<<<< HEAD
model = Comment # 关联的模型当前表单与Comment模型绑定
fields = ['body'] # 表单需显示/处理的模型字段仅包含评论内容body字段
# 注Comment模型中其他字段如author、article、creation_time等
# 通常由后端自动填充如从登录态获取author无需前端用户输入
=======
model = Comment # jyn: 关联的核心模型,表单数据同步至该模型
fields = ['body'] # jyn: 前端需提交的字段(评论内容),自动生成对应表单控件
>>>>>>> JYN_branch
model = Comment
fields = ['body']

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

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

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

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

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

@ -1,117 +1,38 @@
<<<<<<< HEAD
# 导入日志模块和Django国际化工具以及项目自定义工具
import logging # Python内置日志模块用于记录邮件发送过程中的错误信息
import logging
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 send_email # 项目自定义工具封装邮件发送逻辑底层调用Django邮件功能
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
# 初始化日志记录器按当前模块名称创建logger用于记录该模块的运行日志如邮件发送失败
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):
"""
<<<<<<< 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
# jyn: 邮件主题(支持国际化)
subject = _('Thanks for your comment')
# jyn: 拼接文章的完整访问链接HTTPS协议
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>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {
'article_url': article_url,
'article_title': comment.article.title
}
tomail = comment.author.email # jyn: 获取当前评论者的邮箱地址
send_email([tomail], subject, html_content) # jyn: 调用工具函数发送邮件
# 2. 若当前评论是回复(有父评论),向父评论者发送「收到回复」通知邮件
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
tomail = comment.author.email
send_email([tomail], subject, html_content)
try:
if comment.parent_comment: # jyn: 判断当前评论是否为对其他评论的回复
# jyn: 构建父评论者邮件的HTML内容告知其评论被回复支持国际化
if comment.parent_comment:
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
>>>>>>> JYN_branch
<br/>
go check it out!
<br/>
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': article_url,
'article_title': comment.article.title,
'comment_body': comment.parent_comment.body # jyn: 包含父评论的正文内容
}
tomail = comment.parent_comment.author.email # jyn: 获取父评论者的邮箱地址
send_email([tomail], subject, html_content) # jyn: 发送回复通知邮件
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
tomail = comment.parent_comment.author.email
send_email([tomail], subject, html_content)
except Exception as e:
logger.error(e) # jyn: 捕获发送过程中的异常,记录错误日志(不中断程序执行)
>>>>>>> JYN_branch
logger.error(e)

@ -1,191 +1,63 @@
<<<<<<< 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.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django.views.generic.edit import FormView
>>>>>>> ZYY_branch
# 导入项目内关联模型和表单:用户、文章、评论表单、评论模型
from accounts.models import BlogUser
from blog.models import Article
from .forms import CommentForm # 评论功能的表单类之前定义的CommentForm
from .forms import CommentForm
from .models import Comment
class CommentPostView(FormView):
"""
评论提交的类视图继承FormView处理评论表单的展示验证和数据保存
核心功能接收用户提交的评论数据验证合法性后保存到数据库支持评论回复
"""
# 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: 基于类的表单处理视图基类,简化表单验证逻辑
form_class = CommentForm
template_name = 'blog/article_detail.html'
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)
def dispatch(self, *args, **kwargs):
<<<<<<< HEAD
"""
类视图的请求入口方法所有请求都会先经过此方法
作用调用父类的dispatch逻辑同时应用CSRF保护
"""
=======
"""jyn: 重写dispatch方法添加CSRF防护所有请求先经过该方法分发"""
>>>>>>> JYN_branch
return super(CommentPostView, self).dispatch(*args, **kwargs)
# 3. 处理GET请求当用户以GET方式访问该视图时触发
def get(self, request, *args, **kwargs):
<<<<<<< HEAD
"""
GET请求逻辑不处理表单提交直接重定向到对应的文章详情页的评论区
避免用户直接通过URL以GET方式访问该视图时出现异常
"""
# 从URL路径参数中获取文章IDkwargs对应URL中的<int:article_id>
article_id = self.kwargs['article_id']
# 查询对应的文章找不到则返回404
article = get_object_or_404(Article, pk=article_id)
# 获取文章详情页的绝对URL并拼接评论区锚点#comments跳转到页面评论区域
url = article.get_absolute_url()
# 重定向到文章详情页的评论区
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):
<<<<<<< HEAD
"""
表单数据验证失败如评论内容为空格式错误时的处理
作用重新渲染文章详情页带上错误的表单对象前端显示错误提示
"""
# 获取URL中的文章ID查询对应的文章
article_id = self.kwargs['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({
'form': form, # 带有错误信息的表单
'article': article # 当前文章对象(用于渲染文章详情)
'form': form,
'article': article
})
# 5. 处理表单验证成功的逻辑当form.is_valid()为True时触发核心业务逻辑
def form_valid(self, form):
<<<<<<< HEAD
"""提交的数据验证合法后的逻辑:保存评论数据到数据库,处理评论状态和回复关联"""
# 1. 获取当前登录用户(评论作者)
user = self.request.user # 从请求对象中获取登录用户
author = BlogUser.objects.get(pk=user.pk) # 通过用户ID查询完整的BlogUser对象
# 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
"""提交的数据验证合法后的逻辑"""
user = self.request.user
author = BlogUser.objects.get(pk=user.pk)
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
# jyn: 校验文章评论状态和发布状态,禁止对关闭评论/草稿文章提交评论
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.") # jyn: 抛出验证错误,终止评论提交
# jyn: 不立即保存评论commit=False先补充关联字段
raise ValidationError("该文章评论已关闭.")
comment = form.save(False)
comment.article = article # jyn: 关联评论所属文章
# jyn: 根据博客设置决定评论是否需要审核(无需审核则直接启用)
comment.article = article
from djangoblog.utils import get_blog_setting
settings = get_blog_setting() # jyn: 获取博客全局设置
settings = get_blog_setting()
if not settings.comment_need_review:
comment.is_enable = True # jyn: 无需审核时,评论直接启用
comment.author = author # jyn: 关联评论作者
comment.is_enable = True
comment.author = author
# jyn: 处理回复功能若存在父评论ID则关联父评论
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']) # jyn: 查询父评论
comment.parent_comment = parent_comment # jyn: 关联父评论
comment.save(True) # jyn: 最终保存评论数据到数据库
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
# jyn: 重定向到文章详情页的当前评论锚点(精准定位到新提交的评论)
comment.save(True)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))
>>>>>>> JYN_branch

@ -1,25 +1,22 @@
# ZYY 导入 Django 内置的 AdminSite 和 LogEntry 模型
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry # ZYY操作日志模型
from django.contrib.sites.admin import SiteAdmin # ZYYDjango 内置站点管理
from django.contrib.sites.models import Site # ZYY多站点支持模型
# ZYY 导入自定义应用的 admin 和 models
from accounts.admin import * #ZYY 用户账户管理
from blog.admin import *# ZYY博客核心管理
from blog.models import * # ZYY博客数据模型
from comments.admin import *#ZYY 评论管理
from comments.models import * # ZYY评论数据模型
# ZYY 导入自定义的 LogEntryAdmin
from djangoblog.logentryadmin import LogEntryAdmin # ZYY自定义日志管理
from oauth.admin import * # ZYY第三方登录管理
from oauth.models import * # ZYY第三方登录模型
from owntracks.admin import * #ZYY 位置跟踪管理
from owntracks.models import *# ZYY位置跟踪模型
from servermanager.admin import * #ZYY 服务器管理
from servermanager.models import *# ZYY服务器模型
# ZYY 自定义 AdminSite 类
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
@ -30,7 +27,6 @@ class DjangoBlogAdminSite(AdminSite):
def has_permission(self, request):
return request.user.is_superuser
# ZYY 自定义 URL 的示例(已注释)
# def get_urls(self):
# urls = super().get_urls()
# from django.urls import path
@ -41,37 +37,28 @@ class DjangoBlogAdminSite(AdminSite):
# ]
# return urls + my_urls
# ZYY 实例化自定义 AdminSite
admin_site = DjangoBlogAdminSite(name='admin')
# ZYY 注册 blog 应用的模型和管理类
admin_site.register(Article, ArticlelAdmin)# ZYY文章管理
admin_site.register(Category, CategoryAdmin) # ZYY分类管理
admin_site.register(Tag, TagAdmin) #ZYY 标签管理
admin_site.register(Links, LinksAdmin) # ZYY友情链接
admin_site.register(SideBar, SideBarAdmin)# ZYY侧边栏配置
admin_site.register(BlogSettings, BlogSettingsAdmin)# ZYY博客全局设置
admin_site = DjangoBlogAdminSite(name='admin')
#ZYY 注册 servermanager 应用的模型和管理类
admin_site.register(commands, CommandsAdmin) #ZYY 命令记录
admin_site.register(EmailSendLog, EmailSendLogAdmin)# ZYY邮件日志
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
# ZYY 注册 accounts 应用的模型和管理类
admin_site.register(BlogUser, BlogUserAdmin) # ZYY博客用户
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
# ZYY 注册 comments 应用的模型和管理类
admin_site.register(Comment, CommentAdmin)#ZYY 评论内容
admin_site.register(BlogUser, BlogUserAdmin)
admin_site.register(Comment, CommentAdmin)
# ZYY 注册 oauth 应用的模型和管理类
admin_site.register(OAuthUser, OAuthUserAdmin) #ZYY绑定账号
admin_site.register(OAuthConfig, OAuthConfigAdmin) #ZYY 平台配置
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
# ZYY 注册 owntracks 应用的模型和管理类
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # ZYY移动轨迹
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
# ZYY 注册 Django 内置的 Site 模型和管理类
admin_site.register(Site, SiteAdmin) # ZYY多站点配置
admin_site.register(Site, SiteAdmin)
# ZYY 注册 Django 内置的 LogEntry 模型和自定义 LogEntryAdmin
admin_site.register(LogEntry, LogEntryAdmin) # ZYY管理操作日志
admin_site.register(LogEntry, LogEntryAdmin)

@ -1,27 +1,11 @@
# ZYYDjango 应用配置类
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
# ZYY: 指定默认主键字段类型为BigAutoField64位自增ID
# ZYY替代旧版AutoField32位适合数据量大的应用
default_auto_field = 'django.db.models.BigAutoField'
# ZYY: 应用唯一标识,需与项目目录名一致
# 用于Django内部识别应用如管理后台、迁移等
name = 'djangoblog'
def ready(self):
"""ZYY: 应用启动时的初始化钩子
- Django在完成应用注册后会自动调用
- 适合执行启动时加载的任务如插件系统信号注册等
- 注意此方法可能被多次调用特别是在开发服务器热重载时
"""
super().ready() # 确保父类初始化逻辑执行
# ZYY: 插件系统加载入口
# ZYY设计说明
# ZYY1. 延迟导入避免循环依赖AppConfig初始化阶段不宜大量导入
# ZYY2. 插件系统应实现幂等性应对ready()多次调用)
# ZYY3. 建议添加异常处理防止插件加载失败影响应用启动
super().ready()
# Import and load plugins here
from .plugin_manage.loader import load_plugins
load_plugins()
load_plugins()

@ -1,41 +1,34 @@
# ZYY信号处理与系统通知模块
import _thread # ZYY: 使用底层线程处理耗时操作(如邮件发送),避免阻塞主请求
import _thread
import logging
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry # ZYY: 排除管理后台操作日志的缓存清理
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
from comments.models import Comment
from comments.utils import send_comment_email # ZYY: 异步发送评论通知邮件
from djangoblog.spider_notify import SpiderNotify # ZYY: 搜索引擎推送接口
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
logger = logging.getLogger(__name__)
# ZYY: 自定义信号定义
oauth_user_login_signal = django.dispatch.Signal(['id']) # ZYY: OAuth用户登录后处理信号
send_email_signal = django.dispatch.Signal(['emailto', 'title', 'content']) # ZYY: 邮件发送信号
oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
"""ZYY: 邮件发送信号处理器
- 使用信号机制解耦邮件发送逻辑
- 自动记录发送日志到数据库
- 捕获异常避免影响主流程
"""
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
# ZYY: 构造多部分邮件支持HTML内容
msg = EmailMultiAlternatives(
title,
content,
@ -43,7 +36,6 @@ def send_email_signal_handler(sender, **kwargs):
to=emailto)
msg.content_subtype = "html"
# ZYY: 记录邮件发送日志
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
@ -52,7 +44,7 @@ def send_email_signal_handler(sender, **kwargs):
try:
result = msg.send()
log.send_result = result > 0 # ZYY: 根据返回值判断是否发送成功
log.send_result = result > 0
except Exception as e:
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
@ -61,78 +53,62 @@ def send_email_signal_handler(sender, **kwargs):
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
"""ZYY: OAuth用户登录后处理
- 自动处理头像域名适配
- 清理侧边栏缓存
"""
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
# ZYY: 处理头像URL域名适配避免混合内容警告
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
delete_sidebar_cache() # ZYY: 用户信息变更后清理相关缓存
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(sender, instance, created, raw, using, update_fields, **kwargs):
"""ZYY: 模型保存后通用处理器
- 处理内容更新后的缓存清理
- 搜索引擎URL提交
- 评论通知的异步处理
"""
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
clearcache = False
# ZYY: 排除管理后台日志对象
if isinstance(instance, LogEntry):
return
# ZYY: 处理支持URL获取的模型如文章、页面等
if 'get_full_url' in dir(instance):
is_update_views = update_fields == {'views'} # ZYY: 仅浏览量更新时不触发完整处理
# ZYY: 非测试环境且非浏览量更新时推送搜索引擎
is_update_views = update_fields == {'views'}
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url]) # ZYY: 百度站长推送
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
if not is_update_views:
clearcache = True # ZYY: 标记需要清理缓存
clearcache = True
# ZYY: 评论处理特别逻辑
if isinstance(instance, Comment):
if instance.is_enable: # ZYY: 仅处理已启用的评论
if instance.is_enable:
path = instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0: # ZYY: 去除端口号
if site.find(':') > 0:
site = site[0:site.find(':')]
# ZYY: 清理多级缓存文章详情页、SEO数据、评论列表等
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
if cache.get('seo_processor'):
cache.delete('seo_processor')
comment_cache_key = 'article_comments_{id}'.format(id=instance.article.id)
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)])
# ZYY: 使用独立线程发送评论通知邮件
_thread.start_new_thread(send_comment_email, (instance,))
# ZYY: 通用缓存清理(当内容更新时)
if clearcache:
cache.clear()
@ -140,11 +116,7 @@ def model_post_save_callback(sender, instance, created, raw, using, update_field
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
"""ZYY: 用户认证状态变化处理
- 记录用户登录日志
- 清理用户相关缓存
"""
if user and user.username:
logger.info(user) # ZYY: 记录用户认证行为
delete_sidebar_cache() # ZYY: 用户状态变化后清理缓存
# cache.clear() # ZYY: 注释掉的激进缓存清理策略
logger.info(user)
delete_sidebar_cache()
# cache.clear()

@ -13,83 +13,49 @@ logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
# ZYY 初始化Elasticsearch搜索后端
# ZYY参数:
# ZYY connection_alias: 连接别名
# ZYY connection_options: 连接选项
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager()# ZYY 文章文档管理器实例
self.include_spelling = True # ZYY 是否包含拼写建议
self.manager = ArticleDocumentManager()
self.include_spelling = True
def _get_models(self, iterable):
# ZYY 获取模型列表并转换为文档格式
# ZYY参数:
# ZYY iterable: 可迭代对象,包含要处理的模型实例
# ZYY返回:
# ZYY 转换后的文档列表
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
# ZYY 创建索引并重建文档
# 参数:
# models: 要处理的模型列表
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
def _delete(self, models):
# ZYY 删除模型对应的文档
# 参数:
# models: 要删除的模型列表
for m in models:
m.delete()
return True
def _rebuild(self, models):
# ZYY 重建索引
# 参数:
# models: 要重建的模型列表,为空则处理所有文章
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
def update(self, index, iterable, commit=True):
# ZYY 更新索引
# 参数:
# index: 索引名称
# iterable: 可迭代对象,包含要更新的模型实例
# commit: 是否立即提交
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
# ZYY 从索引中移除对象
# 参数:
# obj_or_string: 要移除的对象或字符串
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
# ZYY 清空索引
# 参数:
# models: 要清空的模型列表(未使用,始终清空所有)
# commit: 是否立即提交
self.remove(None)
@staticmethod
def get_suggestion(query: str) -> str:
"""ZYY获取推荐词, 如果没有找到添加原搜索词
参数:
query: 原始查询字符串
返回:
推荐的搜索词字符串"""
"""获取推荐词, 如果没有找到添加原搜索词"""
search = ArticleDocument.search() \
.query("match", body=query) \
@ -107,27 +73,17 @@ class ElasticSearchBackend(BaseSearchBackend):
@log_query
def search(self, query_string, **kwargs):
# ZYY 执行搜索
# 参数:
# query_string: 查询字符串
# **kwargs: 其他搜索参数,包括:
# start_offset: 起始偏移量
# end_offset: 结束偏移量
logger.info('search query_string:' + query_string)
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# ZYY推荐词搜索
# 推荐词搜索
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
# ZYY构建复合查询:
# ZYY1. 应该匹配body或title字段
# ZYY2. 最小匹配度为70%
# ZYY3. 过滤状态为'p'(已发布)和类型为'a'(文章)
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
@ -139,16 +95,15 @@ class ElasticSearchBackend(BaseSearchBackend):
.source(False)[start_offset: end_offset]
results = search.execute()
hits = results['hits'].total # ZYY 总命中数
hits = results['hits'].total
raw_results = []
for raw_result in results['hits']['hits']:
app_label = 'blog'# ZYY 应用标签
model_name = 'Article'# ZYY 模型名称
additional_fields = {}# ZYY 额外字段(当前未使用)
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
result_class = SearchResult
# ZYY: 构建搜索结果对象并收集到列表中
result = result_class(
app_label,
model_name,
@ -156,32 +111,25 @@ class ElasticSearchBackend(BaseSearchBackend):
raw_result['_score'],
**additional_fields)
raw_results.append(result)
# ZYY: 初始化分面数据和拼写建议
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion
# ZYY: 返回标准化的搜索结果结构
return {
'results': raw_results, # ZYY: 实际搜索结果对象列表
'hits': hits,# ZYY: 总命中数
'facets': facets,# ZYY: 分面统计数据(当前为空)
'spelling_suggestion': spelling_suggestion,# ZYY: 拼写纠正建议
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
class ElasticSearchQuery(BaseSearchQuery):
# ZYY: 将datetime对象转换为ES兼容的字符串格式
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S')) # ZYY: 包含时间的完整格式
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000')) # ZYY: 仅日期的格式
return force_str(date.strftime('%Y%m%d000000'))
# ZYY: 清理用户输入的查询片段
def clean(self, query_fragment):
"""
ZYY: 提供在将用户输入呈现给后端之前进行净化的机制
Whoosh 1.X在此有所不同,不能再使用反斜杠转义保留字符而是应该引用整个单词
"""
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
@ -194,11 +142,9 @@ class ElasticSearchQuery(BaseSearchQuery):
cleaned_words = []
for word in words:
# ZYY: 处理保留字(转为小写)
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
# ZYY: 处理保留字符(用单引号包裹整个词)
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
@ -207,35 +153,31 @@ class ElasticSearchQuery(BaseSearchQuery):
cleaned_words.append(word)
return ' '.join(cleaned_words)
# ZYY: 构建查询片段(这里直接返回原始查询字符串)
def build_query_fragment(self, field, filter_type, value):
return value.query_string
# ZYY: 获取结果总数
def get_count(self):
results = self.get_results()
return len(results) if results else 0
# ZYY: 获取拼写建议
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
# ZYY: 构建查询参数
def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
# ZYY: 扩展ModelSearchForm以支持Elasticsearch特定功能
class ElasticSearchModelSearchForm(ModelSearchForm):
def search(self):
# 是否建议搜索
# ZYY: 根据请求参数设置是否启用搜索建议
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()# ZYY: 调用父类搜索方法
sqs = super().search()
return sqs
# ZYY: Elasticsearch搜索引擎实现
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend # ZYY: 指定使用的后端
query = ElasticSearchQuery # ZYY: 指定使用的查询类
backend = ElasticSearchBackend
query = ElasticSearchQuery

@ -6,60 +6,35 @@ from django.utils.feedgenerator import Rss201rev2Feed
from blog.models import Article
from djangoblog.utils import CommonMarkdown
# ZYY: Django内置Feed类用于生成RSS/Atom订阅源
class DjangoBlogFeed(Feed):
# ZYY: 指定使用RSS 2.0规范(支持命名空间扩展)
feed_type = Rss201rev2Feed
# ZYY: ================ 订阅源元数据配置 ================
description = '大巧无工,重剑无锋.'# ZYY: 订阅源副标题/描述
title = "且听风吟 大巧无工,重剑无锋. "# ZYY: 订阅源主标题
link = "/feed/" # ZYY: 订阅源自引用URL实际应为网站根URL
description = '大巧无工,重剑无锋.'
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
# ZYY: ================ 作者信息方法 ================
# ZYY: 注意这些方法在每次生成feed时都会查询数据库
def author_name(self):
# ZYY: 获取站点作者昵称(潜在问题:未处理无用户情况)
return get_user_model().objects.first().nickname
def author_link(self):
# ZYY: 获取作者个人页面URL假设用户模型有get_absolute_url方法
return get_user_model().objects.first().get_absolute_url()
# ZYY: ================ 订阅内容核心方法 ================
def items(self):
# ZYY: 筛选条件:
# ZYYtype='a' - 只包含文章类型(可能区分文章/页面等)
# ZYYstatus='p' - 只包含已发布状态(避免草稿泄露)
# ZYYorder_by('-pub_time') - 按发布时间降序
# ZYY[:5] - 限制最近5篇
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
# ZYY: ================ 单个条目字段映射 ================
def item_title(self, item):
# ZYY: 直接使用文章标题作为条目标题
return item.title
def item_description(self, item):
# ZYY: 将文章内容通过Markdown渲染后作为描述
# 注意可能产生XSS风险需确保CommonMarkdown有净化处理
return CommonMarkdown.get_markdown(item.body)
# ZYY: ================ 订阅源附加信息 ================
def feed_copyright(self):
# ZYY: 动态生成版权声明(自动更新年份)
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
# ZYY: 使用文章自身的绝对URL作为条目链接
return item.get_absolute_url()
# ZYY: ================ 条目唯一标识 ================
def item_guid(self, item):
# ZYY: 原代码不完整,通常应实现为:
# ZYYreturn item.get_absolute_url() # 使用URL作为唯一标识
#ZYY 或 return str(item.id) # 使用数据库ID
# ZYY当前实现缺失会导致某些阅读器无法识别条目更新
return # ZYY: 注意这里缺少返回值,实际使用会报错
return

@ -9,107 +9,83 @@ from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
# ZYY: 列表过滤器配置 - 允许按内容类型筛选日志
list_filter = [
'content_type'
]
# ZYY: 搜索字段配置 - 允许按对象表示和变更消息搜索
search_fields = [
'object_repr',
'change_message'
]
# ZYY: 可点击的列表显示字段 - 指定哪些字段可以点击进入详情页
list_display_links = [
'action_time',
'get_change_message',
]
# ZYY: 列表显示字段配置 - 定义在日志列表中显示的字段
list_display = [
'action_time', #ZYY 操作时间
'user_link', # ZYY用户链接自定义方法
'content_type', # ZYY内容类型
'object_link', # ZYY对象链接自定义方法
'get_change_message', #ZYY 变更消息
'action_time',
'user_link',
'content_type',
'object_link',
'get_change_message',
]
# ZYY: 权限控制 - 禁止通过admin添加日志条目
def has_add_permission(self, request):
return False
# ZYY: 权限控制 - 允许超级用户或有特定权限的用户修改日志但禁止POST请求
def has_change_permission(self, request, obj=None):
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
# ZYY: 权限控制 - 禁止通过admin删除日志条目
def has_delete_permission(self, request, obj=None):
return False
# ZYY: 自定义方法 - 生成对象链接(如果是删除操作则显示纯文本)
def object_link(self, obj):
# ZYY: 初始化为转义后的对象表示字符串防XSS
object_link = escape(obj.object_repr)
content_type = obj.content_type
# ZYY: 如果不是删除操作且有内容类型,尝试生成可点击链接
if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
try:
# ZYY: 构建admin修改页面的URL
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
)
# ZYY: 生成HTML链接后续会标记为安全
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
# ZYY: 如果URL反转失败保持纯文本显示
pass
# ZYY: 标记字符串为安全HTML因为我们已经正确转义和构建
return mark_safe(object_link)
# ZYY: 配置列表排序和描述信息
object_link.admin_order_field = 'object_repr' # 按object_repr字段排序
object_link.short_description = _('object') # 列标题显示为"object"
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
# ZYY: 自定义方法 - 生成用户链接
def user_link(self, obj):
# ZYY: 获取用户模型的内容类型
content_type = ContentType.objects.get_for_model(type(obj.user))
# ZYY: 初始化为转义后的用户字符串表示
user_link = escape(force_str(obj.user))
try:
# ZYY: 尝试构建用户admin修改页面的URL
# try returning an actual link instead of object repr string
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
)
# ZYY: 生成HTML链接
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
# ZYY: URL反转失败时保持纯文本
pass
# ZYY: 标记为安全HTML
return mark_safe(user_link)
# ZYY: 配置列表排序和描述信息
user_link.admin_order_field = 'user' # ZYY按user字段排序
user_link.short_description = _('user') # ZYY列标题显示为"user"
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
# ZYY: 优化查询集 - 预取关联的内容类型减少查询次数
def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
# ZYY: 操作控制 - 移除默认的批量删除操作
def get_actions(self, request):
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
return actions

@ -1,60 +1,41 @@
# ZYY: 导入Python标准库的logging模块用于记录日志
import logging
# ZYY: 获取当前模块的日志记录器
# ZYY: __name__表示当前模块名logging会根据模块名自动组织日志层次
logger = logging.getLogger(__name__)
# ZYY: 定义插件基类,所有插件应继承此类
class BasePlugin:
# ZYY: 插件元数据字段声明(类属性)
# ZYY: PLUGIN_NAME: 插件名称(必须)
# 插件元数据
PLUGIN_NAME = None
# ZYY: PLUGIN_DESCRIPTION: 插件描述(必须)
PLUGIN_DESCRIPTION = None
# ZYY: PLUGIN_VERSION: 插件版本(必须)
PLUGIN_VERSION = None
# ZYY: 构造函数,在实例化插件时自动调用
def __init__(self):
# ZYY: 检查必需的元数据是否已定义
# ZYY: 如果任何元数据为None则抛出ValueError异常
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
# ZYY: 调用插件初始化方法
self.init_plugin()
# ZYY: 调用钩子注册方法
self.register_hooks()
# ZYY: 插件初始化方法(可被子类重写)
def init_plugin(self):
"""
ZYY: 插件初始化逻辑
ZYY: 子类可以重写此方法来实现特定的初始化操作
插件初始化逻辑
子类可以重写此方法来实现特定的初始化操作
"""
# ZYY: 记录插件初始化日志
logger.info(f'{self.PLUGIN_NAME} initialized.')
# ZYY: 插件钩子注册方法(可被子类重写)
def register_hooks(self):
"""
ZYY: 注册插件钩子
ZYY: 子类可以重写此方法来注册特定的钩子
注册插件钩子
子类可以重写此方法来注册特定的钩子
"""
# ZYY: 基类不实现具体钩子注册逻辑
pass
# ZYY: 获取插件信息的方法
def get_plugin_info(self):
"""
ZYY: 获取插件信息
获取插件信息
:return: 包含插件元数据的字典
"""
# ZYY: 返回包含插件名称、描述和版本的字典
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
}
}

@ -1,20 +1,7 @@
# ZYY: 定义文章详情页加载的钩子名称常量
# ZYY: 用于标识文章详情页加载事件,插件可监听此事件执行自定义逻辑
ARTICLE_DETAIL_LOAD = 'article_detail_load'
# ZYY: 定义文章创建的钩子名称常量
# ZYY: 用于标识文章创建事件,通常在文章保存前触发
ARTICLE_CREATE = 'article_create'
# ZYY: 定义文章更新的钩子名称常量
# ZYY: 用于标识文章更新事件,通常在文章修改后触发
ARTICLE_UPDATE = 'article_update'
# ZYY: 定义文章删除的钩子名称常量
# ZYY: 用于标识文章删除事件,通常在文章被删除前触发
ARTICLE_DELETE = 'article_delete'
# ZYY: 定义文章内容处理的钩子名称常量
# ZYY: 参考WordPress的the_content过滤器概念用于在文章内容渲染时进行处理
# ZYY: 插件可以通过此钩子修改文章最终显示的内容
ARTICLE_CONTENT_HOOK_NAME = "the_content"
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -1,77 +1,44 @@
# ZYY: 导入Python标准库logging模块用于记录钩子系统的运行日志
import logging
# ZYY: 获取当前模块的日志记录器,用于记录调试和错误信息
logger = logging.getLogger(__name__)
# ZYY: 定义全局钩子存储字典,键为钩子名称,值为回调函数列表
_hooks = {}
# ZYY: 钩子注册函数,用于将回调函数注册到指定钩子
def register(hook_name: str, callback: callable):
"""
ZYY: 注册一个钩子回调函数到指定钩子名称
ZYY: 如果钩子不存在则创建新列表然后将回调添加到对应钩子的回调列表中
ZYY: @param hook_name: 要注册的钩子名称
ZYY: @param callback: 要注册的回调函数必须是可调用对象
注册一个钩子回调
"""
# ZYY: 检查钩子是否已存在,不存在则初始化空列表
if hook_name not in _hooks:
_hooks[hook_name] = []
# ZYY: 将回调函数添加到对应钩子的回调列表
_hooks[hook_name].append(callback)
# ZYY: 记录调试日志,显示成功注册的钩子和回调函数名
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
# ZYY: Action钩子执行函数用于触发指定名称的所有Action钩子回调
def run_action(hook_name: str, *args, **kwargs):
"""
ZYY: 执行一个Action Hook无返回值的钩子
ZYY: 会按注册顺序依次执行所有回调函数不处理返回值
ZYY: @param hook_name: 要触发的钩子名称
ZYY: @param args: 传递给回调函数的位置参数
ZYY: @param kwargs: 传递给回调函数的关键字参数
执行一个 Action Hook
它会按顺序执行所有注册到该钩子上的回调函数
"""
# ZYY: 检查是否有回调注册到该钩子
if hook_name in _hooks:
logger.debug(f"Running action hook '{hook_name}'")
# ZYY: 遍历并执行所有注册的回调函数
for callback in _hooks[hook_name]:
try:
# ZYY: 执行回调函数,可能抛出异常
callback(*args, **kwargs)
except Exception as e:
# ZYY: 捕获并记录回调执行中的异常,避免影响其他回调
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
# ZYY: Filter钩子执行函数用于触发指定名称的所有Filter钩子回调并处理返回值
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
ZYY: 执行一个Filter Hook有返回值的钩子
ZYY: 会将初始值依次传递给所有回调函数处理最终返回处理后的值
ZYY: @param hook_name: 要触发的钩子名称
ZYY: @param value: 要处理的初始值
ZYY: @param args: 传递给回调函数的位置参数
ZYY: @param kwargs: 传递给回调函数的关键字参数
ZYY: @return: 经过所有回调处理后的最终值
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
"""
# ZYY: 检查是否有回调注册到该钩子
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
# ZYY: 遍历并执行所有注册的回调函数每次用返回值更新value
for callback in _hooks[hook_name]:
try:
value = callback(value, *args, **kwargs)
except Exception as e:
# ZYY: 捕获并记录回调执行中的异常,避免影响其他回调
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
# ZYY: 返回最终处理后的值
return value
return value

@ -1,32 +1,19 @@
# ZYY: 导入Python标准库os模块用于处理文件和目录路径
import os
# ZYY: 导入Python标准库logging模块用于记录插件加载过程中的日志信息
import logging
# ZYY: 导入Django配置模块用于访问Django项目的设置参数
from django.conf import settings
# ZYY: 获取当前模块的日志记录器,用于记录插件加载的调试和错误信息
logger = logging.getLogger(__name__)
# ZYY: 定义插件加载函数用于动态加载和初始化Django插件
def load_plugins():
"""
ZYY: 动态加载并初始化位于'plugins'目录中的插件
ZYY: 该函数应在Django应用注册表就绪后调用以确保所有依赖项可用
ZYY: 加载过程会读取settings.ACTIVE_PLUGINS配置并尝试加载每个指定的插件
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
# ZYY: 遍历settings.ACTIVE_PLUGINS中配置的所有活跃插件名称
for plugin_name in settings.ACTIVE_PLUGINS:
# ZYY: 构建插件的完整路径(目录路径)
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
# ZYY: 检查插件目录是否存在且包含plugin.py文件插件主模块
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# ZYY: 动态导入插件模块格式plugins.<plugin_name>.plugin
__import__(f'plugins.{plugin_name}.plugin')
# ZYY: 记录成功加载插件的日志信息
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
# ZYY: 捕获并记录插件导入失败的错误信息(包含异常堆栈)
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -12,344 +12,332 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
import os
import sys
from pathlib import Path
from django.utils.translation import gettext_lazy as _
# ZYY 将环境变量字符串转换为布尔值
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# ZYY 在项目内部构建路径例如BASE_DIR / 'subdir'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# ZYY 快速启动开发设置 - 不适用于生产环境
# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# ZYY 安全警告: 请保密生产环境中使用的密钥!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@ (mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# ZYY 安全警告: 不要在生产环境中开启调试模式!
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False
# ZYY 检测是否为测试环境
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ZYY 允许的主机列表
# ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# ZYY Django 4.0新增配置: CSRF信任来源
# django 4.0新增配置
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
# ZYY 应用定义
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth',
'django. contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.sitemaps',
'mdeditor',
'haystack',
'blog',
'accounts',
'comments',
'oauth',
' servermanager',
'owntracks',
'compressor',
'djangoblog'
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.sitemaps',
'mdeditor',
'haystack',
'blog',
'accounts',
'comments',
'oauth',
'servermanager',
'owntracks',
'compressor',
'djangoblog'
]
# ZYY 中间件配置
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.gzip.GZipMiddleware',
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib. messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'blog.middleware.OnlineMiddleware'
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.gzip.GZipMiddleware',
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'blog.middleware.OnlineMiddleware'
]
# ZYY 根URL配置
ROOT_URLCONF = 'djangoblog.urls'
# ZYY 模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR , 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor'
],
},
},
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor'
],
},
},
]
# ZYY WSGI应用配置
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# ZYY 数据库配置
# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/ref/settings/#databases
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': ' django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': {
'charset': 'utf8mb4'},
}}
# ZYY 密码验证配置
# ZYY 参考链接: https://docs.djang oproject.com/en/1.10/ref/settings/#auth-password-validators
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': {
'charset': 'utf8mb4'},
}}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# ZYY 语言配置
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
os.path.join(BASE_DIR, 'locale'),
)
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# ZYY 静态文件配置(CSS, JavaScript, Images)
# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/howto /static-files/
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
# ZYY 自动更新搜索索引
# Automatically update searching index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# ZYY 允许用户使用用户名和密码登录
# Allow user login with username and password
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
'accounts.user_login_backend.EmailOrUsernameModelBackend']
# ZYY 静态文件根目录
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
# ZYY 自定义用户模型
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# ZYY Bootstrap颜色样式
# bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# ZYY 分页配置
# paginate
PAGINATE_BY = 10
# ZYY HTTP缓存超时时间
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# ZYY 缓存配置
# cache setting
CACHES = {
'default': {
'BACKEND': 'django.core .cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake',
}
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake',
}
}
# ZYY 使用redis作为缓存
# 使用redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
SITE_ID = 1
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# ZYY 百度推送URL配置
BAIDU_NOTIFY_URL = os.environ .get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# ZYY 邮件配置
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com '
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER
# ZYY 设置debug=false时不会处理异常邮件通知
# Setting debug=false did NOT handle except email notifications
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# ZYY 微信管理员密码(两次md5 加密)
# WX ADMIN password(Two times md5)
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
# ZYY 日志目录配置
LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
os.makedirs(LOG_PATH, exist_ok=True)
# ZYY 日志配置
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'root': {
'level': 'INFO',
'handlers': ['console', 'log_file'],
},
'formatters': {
'verbose': {
'format': '[%( asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'log_file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D',
'formatter': 'verbose',
'interval': 1,
'delay': True ,
'backupCount': 5,
'encoding': 'utf-8'
},
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': {
'class': 'logging.NullHandler',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
}
}
'version': 1,
'disable_existing_loggers': False,
'root': {
'level': 'INFO',
'handlers': ['console', 'log_file'],
},
'formatters': {
'verbose': {
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'log_file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D',
'formatter': 'verbose',
'interval': 1,
'delay': True,
'backupCount': 5,
'encoding': 'utf-8'
},
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': {
'class': 'logging.NullHandler',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
}
}
}
# ZYY 静态文件查找器配置
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other
'compressor.finders.CompressorFinder',
)
# ZYY 压缩配置
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
COMPRESS_CSS_FILTERS = [
# 创建绝对URL
'compressor.filters.css_default.CssAbsoluteFilter',
# CSS压缩
'compressor.filters.cssmin.CSSMinFilter'
# creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter',
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
]
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
'compressor.filters.jsmin.JSMinFilter'
]
# ZYY 媒体文件配置
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
# ZYY X-Frame-Options配置
X_FRAME_OPTIONS = 'SAMEORIGIN'
# ZYY 默认自增字段类型
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# ZYY Elasticsearch配置
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_ HOST')
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# ZYY 插件系统配置
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright',
'reading_time',
' external_links',
'view_count',
'seo_optimizer'
'article_copyright',
'reading_time',
'external_links',
'view_count',
'seo_optimizer'
]

@ -1,73 +1,59 @@
# ZYY 导入必要的模块和类
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from blog.models import Article, Category, Tag
# ZYY 定义静态视图站点地图类,用于生成博客首页等静态页面的站点地图
class StaticViewSitemap(Sitemap):
priority = 0.5 # ZYY 优先级设置为0.5
changefreq = 'daily' # ZYY 更新频率设置为每天
priority = 0.5
changefreq = 'daily'
def items(self):
# ZYY 返回需要生成站点地图的URL名称列表这里只有博客首页
return ['blog:index', ]
def location(self, item):
# ZYY 根据URL名称生成具体的URL地址
return reverse(item)
# ZYY 定义文章站点地图类,用于生成文章页面的站点地图
class ArticleSiteMap(Sitemap):
changefreq = "monthly" # ZYY 更新频率设置为每月
priority = "0.6" # ZYY 优先级设置为0.6
changefreq = "monthly"
priority = "0.6"
def items(self):
# ZYY 返回所有状态为'p'(已发布)的文章对象
return Article.objects.filter(status='p')
def lastmod(self, obj):
# ZYY 返回文章的最后修改时间
return obj.last_modify_time
# ZYY 定义分类站点地图类,用于生成分类页面的站点地图
class CategorySiteMap(Sitemap):
changefreq = "Weekly" # ZYY 更新频率设置为每周
priority = "0.6" # ZYY 优先级设置为0.6
changefreq = "Weekly"
priority = "0.6"
def items(self):
# ZYY 返回所有的分类对象
return Category.objects.all()
def lastmod(self, obj):
# ZYY 返回分类的最后修改时间
return obj.last_modify_time
# ZYY 定义标签站点地图类,用于生成标签页面的站点地图
class TagSiteMap(Sitemap):
changefreq = "Weekly" # ZYY 更新频率设置为每周
priority = "0.3" # ZYY 优先级设置为0.3
changefreq = "Weekly"
priority = "0.3"
def items(self):
# ZYY 返回所有的标签对象
return Tag.objects.all()
def lastmod(self, obj):
# ZYY 返回标签的最后修改时间
return obj.last_modify_time
# ZYY 定义用户站点地图类,用于生成用户页面的站点地图
class UserSiteMap(Sitemap):
changefreq = "Weekly" # ZYY 更新频率设置为每周
priority = "0.3" # ZYY 优先级设置为0.3
changefreq = "Weekly"
priority = "0.3"
def items(self):
# ZYY 返回所有文章的作者集合(去重)
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
# ZYY 返回用户的注册时间作为最后修改时间
return obj.date_joined
return obj.date_joined

@ -1,32 +1,21 @@
# ZYY 导入日志模块,用于记录程序运行状态
import logging
# ZYY 导入requests库用于发送HTTP请求
import requests
# ZYY 导入Django配置用于获取项目设置
from django.conf import settings
# ZYY 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
# ZYY 定义蜘蛛通知类用于向搜索引擎提交URL通知
class SpiderNotify():
@staticmethod
# ZYY 静态方法用于通知百度蜘蛛抓取指定URL列表
def baidu_notify(urls):
try:
# ZYY 将URL列表拼接成字符串每个URL占一行
data = '\n'.join(urls)
# ZYY 向百度推送接口发送POST请求提交URL数据
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
# ZYY 记录百度接口返回的结果(成功响应)
logger.info(result.text)
except Exception as e:
# ZYY 捕获并记录所有异常(如网络错误、接口错误等)
logger.error(e)
@staticmethod
# ZYY 静态方法,提供统一的通知接口(目前仅调用百度通知)
def notify(url):
# ZYY 调用百度通知方法注意这里参数名是url但实际传入的是单个URL可能存在命名歧义
SpiderNotify.baidu_notify(url)
SpiderNotify.baidu_notify(url)

@ -5,18 +5,11 @@ from djangoblog.utils import *
class DjangoBlogTest(TestCase):
def setUp(self):
# ZYY: 测试用例初始化方法,当前为空实现
pass
def test_utils(self):
# ZYY: 测试utils模块中的工具函数
# ZYY: 测试get_sha256函数验证字符串'test'的sha256哈希值不为空
md5 = get_sha256('test')
self.assertIsNotNone(md5)
# ZYY: 测试CommonMarkdown类的get_markdown方法
# ZYY: 输入包含标题、代码块和链接的Markdown文本验证输出不为空
c = CommonMarkdown.get_markdown('''
# Title1
@ -31,12 +24,9 @@ class DjangoBlogTest(TestCase):
''')
self.assertIsNotNone(c)
# ZYY: 测试parse_dict_to_url函数
# ZYY: 创建测试字典验证字典转URL参数字符串的功能
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)# ZYY: 验证转换结果不为空
self.assertIsNotNone(data)

@ -13,92 +13,52 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
# ZYY: 导入Django基础配置模块
from django.conf import settings
# ZYY: 导入国际化URL模式工具
from django.conf.urls.i18n import i18n_patterns
# ZYY: 导入静态文件URL处理工具
from django.conf.urls.static import static
# ZYY: 导入站点地图视图
from django.contrib.sitemaps.views import sitemap
# ZYY: 导入URL路径处理函数
from django.urls import path, include
# ZYY: 导入正则表达式URL模式
from django.urls import re_path
# ZYY: 导入Haystack搜索视图工厂
from haystack.views import search_view_factory
# ZYY: 导入自定义视图和模块
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# ZYY: 定义站点地图配置,包含各类内容的站点地图
sitemaps = {
'blog': ArticleSiteMap, # ZYY: 文章站点地图
'Category': CategorySiteMap, # ZYY: 分类站点地图
'Tag': TagSiteMap, # ZYY: 标签站点地图
'User': UserSiteMap, # ZYY: 用户站点地图
'static': StaticViewSitemap # ZYY: 静态页面站点地图
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
}
# ZYY: 定义自定义错误处理视图
handler404 = 'blog.views.page_not_found_view' # ZYY: 404错误处理
handler500 = 'blog.views.server_error_view' # ZYY: 500错误处理
handle403 = 'blog.views.permission_denied_view' # ZYY: 403错误处理
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
# ZYY: 基础URL模式配置
urlpatterns = [
# ZYY: 国际化URL前缀处理
path('i18n/', include('django.conf.urls.i18n')),
]
# ZYY: 添加国际化URL模式
urlpatterns += i18n_patterns(
# ZYY: 管理后台URL使用自定义admin_site
re_path(r'admin/', admin_site.urls),
# ZYY: 博客应用URL命名空间为blog
re_path(r'^admin/', admin_site.urls),
re_path(r'', include('blog.urls', namespace='blog')),
# ZYY: Markdown编辑器URL
re_path(r'mdeditor/', include('mdeditor.urls')),
# ZYY: 评论系统URL命名空间为comment
re_path(r'', include('comments.urls', namespace='comment')),
# ZYY: 账户系统URL命名空间为account
re_path(r'', include('accounts.urls', namespace='account')),
# ZYY: 第三方登录URL命名空间为oauth
re_path(r'', include('oauth.urls', namespace='oauth')),
# ZYY: 站点地图URL生成sitemap.xml
re_path(r'sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
# ZYY: RSS订阅URL两种访问路径
re_path(r'feed/$', DjangoBlogFeed()),
re_path(r'rss/$', DjangoBlogFeed()),
# ZYY: 搜索功能URL使用Elasticsearch搜索视图
re_path('search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
# ZYY: 服务器管理URL命名空间为servermanager
re_path(r'', include('servermanager.urls', namespace='servermanager')),
# ZYY: OwnTracks应用URL命名空间为owntracks
re_path(r'', include('owntracks.urls', namespace='owntracks')),
# ZYY: 禁用默认语言前缀prefix_default_language=False
prefix_default_language=False
) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # ZYY: 添加静态文件URL处理
# ZYY: 开发环境专用配置DEBUG模式下
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG:
# ZYY: 添加媒体文件URL处理仅开发环境
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
document_root=settings.MEDIA_ROOT)

@ -8,6 +8,7 @@ import random
import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
@ -15,20 +16,21 @@ from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
logger = logging.getLogger(__name__)
# ZYY: 获取最新的文章ID和评论ID
def get_max_articleid_commentid():
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
# ZYY: 获取字符串的SHA256哈希值
def get_sha256(str):
m = sha256(str.encode('utf-8'))
return m.hexdigest()
# ZYY: 缓存装饰器 ,用于缓存函数结果
def cache_decorator(expiration=3 * 60):
def wrapper(func):
def news(*args, **kwargs):
@ -39,6 +41,7 @@ def cache_decorator(expiration=3 * 60):
key = None
if not key:
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
value = cache.get(key)
@ -50,7 +53,7 @@ def cache_decorator(expiration=3 * 60):
return value
else:
logger.debug(
'cache_decorator set cache:%s key:%s' %
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
if value is None:
@ -58,40 +61,44 @@ def cache_decorator(expiration=3 * 60):
else:
cache.set(key, value, expiration)
return value
return news
return wrapper
# ZYY: 刷新视图缓存
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
: param key_prefix:前缀
:param key_prefix:前缀
:return:是否成功
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
request = HttpRequest()
request.META = {'SERVER_NAME': servername , 'SERVER_PORT': serverport}
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key):
cache.delete(key)
return True
return True
return False
# ZYY: 使用缓存装饰器获取当前站点信息
@cache_decorator()
def get_current_site():
site = Site.objects.get_current ()
site = Site.objects.get_current()
return site
class CommonMarkdown:
# ZYY: 将Markdown文本转换为HTML并生成目录
@staticmethod
def _convert_markdown(value):
md = markdown.Markdown(
@ -106,19 +113,17 @@ class CommonMarkdown:
toc = md.toc
return body, toc
# ZYY: 获取带有目录的Markdown转换结果
@staticmethod
def get_markdown_with_toc(value):
body, toc = CommonMarkdown._convert_markdown(value)
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
# ZYY: 获取Markdown转换结果不带目录
@staticmethod
def get_markdown(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body
# ZYY: 发送邮件
def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
@ -127,19 +132,19 @@ def send_email(emailto, title, content):
title=title,
content=content)
# ZYY: 生成随机数验证码
def generate_code() -> str:
"""生成随机数验证码"""
return ''.join(random .sample(string.digits, 6))
return ''.join(random.sample(string.digits, 6))
# ZYY: 将字典解析为URL字符串
def parse_dict_to_url(dict):
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
# ZYY: 获取博客设置,如果缓存中没有则从数据库加载并设置缓存
def get_blog_setting():
value = cache.get('get_blog_setting')
if value:
@ -158,7 +163,7 @@ def get_blog_setting():
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
@ -167,7 +172,7 @@ def get_blog_setting():
cache.set('get_blog_setting', value)
return value
# ZYY: 保存用户头像到本地
def save_user_avatar(url):
'''
保存用户头像
@ -175,12 +180,14 @@ def save_user_avatar(url):
:return: 本地路径
'''
logger.info(url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
@ -188,12 +195,12 @@ def save_user_avatar(url):
logger.info('保存用户头像:' + basedir + save_filename)
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
return static('avatar /' + save_filename)
return static('avatar/' + save_filename)
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png')
# ZYY: 删除侧边栏缓存
def delete_sidebar_cache():
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
@ -201,13 +208,13 @@ def delete_sidebar_cache():
logger.info('delete sidebar key:' + k)
cache.delete(k)
# ZYY: 删除视图缓存
def delete_view_cache(prefix, keys):
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
# ZYY: 获取静态资源URL
def get_resource_url():
if settings.STATIC_URL:
return settings.STATIC_URL
@ -215,11 +222,11 @@ def get_resource_url():
site = get_current_site()
return 'http://' + site.domain + '/static/'
# ZYY: 允许的HTML标签和属性
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
# ZYY: 清理HTML只保留允许的标签和属性
def sanitize_html(html):
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

@ -1,7 +1,7 @@
# encoding: utf-8
from __future__ import absolute_import, division, print_function, unicode_literals
# ZYY: 导入必要的模块和函数
import json
import os
import re
@ -34,14 +34,12 @@ from whoosh.qparser import QueryParser
from whoosh.searching import ResultsPage
from whoosh.writing import AsyncWriter
# ZYY: 尝试导入whoosh模块如果失败则抛出异常
try:
import whoosh
except ImportError:
raise MissingDependency(
"The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
# ZYY: 检查whoosh版本是否满足要求
# Handle minimum requirement.
if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
raise MissingDependency(
@ -54,20 +52,16 @@ DATETIME_REGEX = re.compile(
LOCALS = threading.local()
LOCALS.RAM_STORE = None
# ZYY: 自定义的HtmlFormatter类用于在whoosh中统一结果格式
class WhooshHtmlFormatter(HtmlFormatter):
"""
This is a HtmlFormatter simpler than the whoosh.HtmlFormatter.
We use it to have consistent results across backends. Specifically,
Solr, Xapian and Elasticsearch are using this formatting.
"""
"""
ZYY: 这是一个比whoosh.HtmlFormatter更简单的HtmlFormatter
我们使用它来确保不同后端如Solr, Xapian和Elasticsearch之间的结果一致性
"""
template = '<%(tag)s>%(t)s</%(tag)s>'
# ZYY: Whoosh搜索后端类
class WhooshSearchBackend(BaseSearchBackend):
# Word reserved by Whoosh for special use.
RESERVED_WORDS = (
@ -76,7 +70,7 @@ class WhooshSearchBackend(BaseSearchBackend):
'OR',
'TO',
)
# ZYY: Whoosh保留的特殊用途字符
# Characters reserved by Whoosh for special use.
# The '\\' must come first, so as not to overwrite the other slash
# replacements.
@ -86,7 +80,6 @@ class WhooshSearchBackend(BaseSearchBackend):
)
def __init__(self, connection_alias, **connection_options):
# ZYY: 初始化Whoosh搜索后端
super(
WhooshSearchBackend,
self).__init__(
@ -114,13 +107,9 @@ class WhooshSearchBackend(BaseSearchBackend):
"""
Defers loading until needed.
"""
"""
ZYY: 延迟加载直到需要时才加载
"""
from haystack import connections
new_index = False
# ZYY: 确保索引存在
# Make sure the index is there.
if self.use_file_storage and not os.path.exists(self.path):
os.makedirs(self.path)
@ -155,14 +144,12 @@ class WhooshSearchBackend(BaseSearchBackend):
self.setup_complete = True
# ZYY: 构建索引模式
def build_schema(self, fields):
schema_fields = {
ID: WHOOSH_ID(stored=True, unique=True),
DJANGO_CT: WHOOSH_ID(stored=True),
DJANGO_ID: WHOOSH_ID(stored=True),
}
# ZYY: 获取Haystack中硬编码的键数量用于后续的错误处理
# Grab the number of keys that are hard-coded into Haystack.
# We'll use this to (possibly) fail slightly more gracefully later.
initial_key_count = len(schema_fields)
@ -186,7 +173,6 @@ class WhooshSearchBackend(BaseSearchBackend):
schema_fields[field_class.index_fieldname] = NUMERIC(
stored=field_class.stored, numtype=float, field_boost=field_class.boost)
elif field_class.field_type == 'boolean':
# ZYY: BOOLEAN字段不支持field_boost
# Field boost isn't supported on BOOLEAN as of 1.8.2.
schema_fields[field_class.index_fieldname] = BOOLEAN(
stored=field_class.stored)
@ -198,14 +184,13 @@ class WhooshSearchBackend(BaseSearchBackend):
stored=field_class.stored,
field_boost=field_class.boost)
else:
# ZYY: 使用中文分析器
# schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True)
schema_fields[field_class.index_fieldname] = TEXT(
stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
if field_class.document is True:
content_field_name = field_class.index_fieldname
schema_fields[field_class.index_fieldname].spelling = True
# ZYY: 如果没有找到任何字段,则抛出错误
# Fail more gracefully than relying on the backend to die if no fields
# are found.
if len(schema_fields) <= initial_key_count:
@ -213,7 +198,7 @@ class WhooshSearchBackend(BaseSearchBackend):
"No fields were found in any search_indexes. Please correct this before attempting to search.")
return (content_field_name, Schema(**schema_fields))
# ZYY: 更新索引
def update(self, index, iterable, commit=True):
if not self.setup_complete:
self.setup()
@ -227,13 +212,11 @@ class WhooshSearchBackend(BaseSearchBackend):
except SkipDocument:
self.log.debug(u"Indexing for object `%s` skipped", obj)
else:
# ZYY: 确保所有数据都是unicode格式
# Really make sure it's unicode, because Whoosh won't have it any
# other way.
for key in doc:
doc[key] = self._from_python(doc[key])
# ZYY: Whoosh 2.5.0+不支持文档boost
# Document boosts aren't supported in Whoosh 2.5.0+.
if 'boost' in doc:
del doc['boost']
@ -244,7 +227,6 @@ class WhooshSearchBackend(BaseSearchBackend):
if not self.silently_fail:
raise
# ZYY: 记录错误信息
# We'll log the object identifier but won't include the actual object
# to avoid the possibility of that generating encoding errors while
# processing the log message:
@ -258,12 +240,10 @@ class WhooshSearchBackend(BaseSearchBackend):
"object": get_identifier(obj)}})
if len(iterable) > 0:
# ZYY: 无论如何都提交更改,以避免锁定问题
# For now, commit no matter what, as we run into locking issues
# otherwise.
writer.commit()
# ZYY: 从索引中删除文档
def remove(self, obj_or_string, commit=True):
if not self.setup_complete:
self.setup()
@ -286,7 +266,6 @@ class WhooshSearchBackend(BaseSearchBackend):
e,
exc_info=True)
# ZYY: 清空索引
def clear(self, models=None, commit=True):
if not self.setup_complete:
self.setup()
@ -323,9 +302,8 @@ class WhooshSearchBackend(BaseSearchBackend):
else:
self.log.error(
"Failed to clear Whoosh index: %s", e, exc_info=True)
# ZYY: 删除索引文件
def delete_index(self):
# ZYY: 根据Whoosh邮件列表的建议如果要从索引中删除所有内容更高效的方法是直接删除索引文件
# Per the Whoosh mailing list, if wiping out everything from the index,
# it's much more efficient to simply delete the index files.
if self.use_file_storage and os.path.exists(self.path):
@ -333,11 +311,9 @@ class WhooshSearchBackend(BaseSearchBackend):
elif not self.use_file_storage:
self.storage.clean()
# ZYY: 重新创建索引
# Recreate everything.
self.setup()
# ZYY: 优化索引
def optimize(self):
if not self.setup_complete:
self.setup()
@ -345,15 +321,12 @@ class WhooshSearchBackend(BaseSearchBackend):
self.index = self.index.refresh()
self.index.optimize()
# ZYY: 计算分页
def calculate_page(self, start_offset=0, end_offset=None):
# Prevent against Whoosh throwing an error. Requires an end_offset
# greater than 0.
# ZYY: 防止Whoosh抛出错误要求end_offset大于0
if end_offset is not None and end_offset <= 0:
end_offset = 1
# ZYY: 确定页码
# Determine the page.
page_num = 0
@ -368,7 +341,6 @@ class WhooshSearchBackend(BaseSearchBackend):
if page_length and page_length > 0:
page_num = int(start_offset / page_length)
# ZYY: Whoosh使用1-based的页码
# Increment because Whoosh uses 1-based page numbers.
page_num += 1
return page_num, page_length
@ -396,8 +368,7 @@ class WhooshSearchBackend(BaseSearchBackend):
**kwargs):
if not self.setup_complete:
self.setup()
# ZYY: 空查询应该返回无结果
# A zero length query should return no results.
if len(query_string) == 0:
return {
@ -406,7 +377,7 @@ class WhooshSearchBackend(BaseSearchBackend):
}
query_string = force_str(query_string)
# ZYY: 单字符查询(非通配符)会被停用词过滤器过滤掉,应该返回无结果
# A one-character query (non-wildcard) gets nabbed by a stopwords
# filter and should yield zero results.
if len(query_string) <= 1 and query_string != u'*':
@ -418,7 +389,6 @@ class WhooshSearchBackend(BaseSearchBackend):
reverse = False
if sort_by is not None:
# ZYY: 确定是否需要反转结果以及Whoosh是否能处理排序要求
# Determine if we need to reverse the results and if Whoosh can
# handle what it's being asked to sort by. Reversing is an
# all-or-nothing action, unfortunately.
@ -475,7 +445,6 @@ class WhooshSearchBackend(BaseSearchBackend):
if models and len(models):
model_choices = sorted(get_model_ct(model) for model in models)
elif limit_to_registered_models:
# ZYY: 使用窄查询限制结果到当前路由器处理的模型
# Using narrow queries, limit the results to only models handled
# with the current routers.
model_choices = self.build_models_list()
@ -492,7 +461,6 @@ class WhooshSearchBackend(BaseSearchBackend):
narrow_searcher = None
if narrow_queries is not None:
# ZYY: 潜在的高开销操作
# Potentially expensive? I don't see another way to do it in
# Whoosh...
narrow_searcher = self.index.searcher()
@ -518,7 +486,6 @@ class WhooshSearchBackend(BaseSearchBackend):
searcher = self.index.searcher()
parsed_query = self.parser.parse(query_string)
# ZYY: 处理无效/停用词查询,优雅恢复
# In the event of an invalid/stopworded query, recover gracefully.
if parsed_query is None:
return {
@ -535,7 +502,6 @@ class WhooshSearchBackend(BaseSearchBackend):
'reverse': reverse,
}
# ZYY: 处理已缩小范围的查询结果
# Handle the case where the results have been narrowed.
if narrowed_results is not None:
search_kwargs['filter'] = narrowed_results
@ -555,7 +521,7 @@ class WhooshSearchBackend(BaseSearchBackend):
'hits': 0,
'spelling_suggestion': None,
}
# ZYY: Whoosh 2.5.1如果请求的页码过高,会返回错误的页码
# Because as of Whoosh 2.5.1, it will return the wrong page of
# results if you request something too high. :(
if raw_page.pagenum < page_num:
@ -593,7 +559,7 @@ class WhooshSearchBackend(BaseSearchBackend):
'hits': 0,
'spelling_suggestion': spelling_suggestion,
}
# ZYY: 查找类似文档
def more_like_this(
self,
model_instance,
@ -606,7 +572,7 @@ class WhooshSearchBackend(BaseSearchBackend):
**kwargs):
if not self.setup_complete:
self.setup()
# ZYY: 延迟模型将具有不同的类名("RealClass_Deferred_fieldname"),该类名不会在我们的注册表中
# Deferred models will have a different class ("RealClass_Deferred_fieldname")
# which won't be in our registry:
model_klass = model_instance._meta.concrete_model
@ -623,7 +589,6 @@ class WhooshSearchBackend(BaseSearchBackend):
if models and len(models):
model_choices = sorted(get_model_ct(model) for model in models)
elif limit_to_registered_models:
# ZYY: 使用窄查询限制结果到当前路由器处理的模型
# Using narrow queries, limit the results to only models handled
# with the current routers.
model_choices = self.build_models_list()
@ -643,7 +608,6 @@ class WhooshSearchBackend(BaseSearchBackend):
narrow_searcher = None
if narrow_queries is not None:
# ZYY: 潜在的高开销操作
# Potentially expensive? I don't see another way to do it in
# Whoosh...
narrow_searcher = self.index.searcher()
@ -678,11 +642,10 @@ class WhooshSearchBackend(BaseSearchBackend):
raw_results = results[0].more_like_this(
field_name, top=end_offset)
# ZYY: 处理已缩小范围的查询结果
# Handle the case where the results have been narrowed.
if narrowed_results is not None and hasattr(raw_results, 'filter'):
raw_results.filter(narrowed_results)
# ZYY: Whoosh 2.5.1如果请求的页码过高,会返回错误的页码
try:
raw_page = ResultsPage(raw_results, page_num, page_length)
except ValueError:
@ -711,7 +674,7 @@ class WhooshSearchBackend(BaseSearchBackend):
narrow_searcher.close()
return results
# ZYY: 处理查询结果
def _process_results(
self,
raw_page,
@ -721,153 +684,174 @@ class WhooshSearchBackend(BaseSearchBackend):
result_class=None):
from haystack import connections
results = []
# ZYY: 获取命中数
# It's important to grab the hits first before slicing. Otherwise, this
# can cause pagination failures.
hits = len(raw_page)
# ZYY: 如果result_class为None则将其设置为SearchResult
if result_class is None:
result_class = SearchResult
facets = {}
spelling_suggestion = None
# ZYY: 获取统一索引
unified_index = connections[self.connection_alias].get_unified_index()
# ZYY: 获取已索引的模型列表
indexed_models = unified_index.get_indexed_models()
for doc_offset, raw_result in enumerate(raw_page):
# ZYY: 获取文档得分如果没有则为0
score = raw_page.score(doc_offset) or 0
app_label, model_name = raw_result[DJANGO_CT].split('.')
additional_fields = {}
# ZYY: 根据app_label和model_name获取模型
model = haystack_get_model(app_label, model_name)
if model and model in indexed_models:
for key, value in raw_result.items():
index = unified_index.get_index(model)
string_key = str(key)
# ZYY : 检查字段是否在索引中且具有convert方法特殊处理KEYWORD字段
if string_key in index.fields and hasattr(
index.fields[string_key], 'convert'):
# ZYY: 处理多值字段
if index.fields[string_key].is_multivalued:
if value is None or len(value) == 0:
additional_fields[string_key] = []
if result_class is None:
result_class = SearchResult
facets = {}
spelling_suggestion = None
unified_index = connections[self.connection_alias].get_unified_index()
indexed_models = unified_index.get_indexed_models()
for doc_offset, raw_result in enumerate(raw_page):
score = raw_page.score(doc_offset) or 0
app_label, model_name = raw_result[DJANGO_CT].split('.')
additional_fields = {}
model = haystack_get_model(app_label, model_name)
if model and model in indexed_models:
for key, value in raw_result.items():
index = unified_index.get_index(model)
string_key = str(key)
if string_key in index.fields and hasattr(
index.fields[string_key], 'convert'):
# Special-cased due to the nature of KEYWORD fields.
if index.fields[string_key].is_multivalued:
if value is None or len(value) == 0:
additional_fields[string_key] = []
else:
additional_fields[string_key] = value.split(
',')
else:
additional_fields[string_key] = index.fields[string_key].convert(
value)
else:
additional_fields[string_key] = value.split(
',')
else:
additional_fields[string_key] = index.fields[string_key].convert(
value)
additional_fields[string_key] = self._to_python(value)
del (additional_fields[DJANGO_CT])
del (additional_fields[DJANGO_ID])
if highlight:
sa = StemmingAnalyzer()
formatter = WhooshHtmlFormatter('em')
terms = [token.text for token in sa(query_string)]
whoosh_result = whoosh_highlight(
additional_fields.get(self.content_field_name),
terms,
sa,
ContextFragmenter(),
formatter
)
additional_fields['highlighted'] = {
self.content_field_name: [whoosh_result],
}
result = result_class(
app_label,
model_name,
raw_result[DJANGO_ID],
score,
**additional_fields)
results.append(result)
else:
additional_fields[string_key] = self._to_python(value)
del (additional_fields[DJANGO_CT])
del (additional_fields[DJANGO_ID])
if highlight:
sa = StemmingAnalyzer()
formatter = WhooshHtmlFormatter('em')
terms = [token .text for token in sa(query_string)]
# ZYY: 使用Whoosh进行高亮显示
whoosh_result = whoosh_highlight(
additional_fields.get(self.content_field_name),
terms,
sa,
ContextFragmenter(),
formatter
)
additional_fields['highlighted'] = {
self.content_field_name: [whoosh_result],
}
result = result_class(
app_label,
model_name,
raw_result[DJANGO_ID],
score,
**additional_fields)
results.append(result)
else:
hits -= 1
if self.include_spelling:
if spelling_query:
# ZYY: 根据spelling_query创建拼写建议
spelling_suggestion = self.create_spelling_suggestion(
spelling_query)
else:
# ZYY: 根据query_string创建拼写建议
spelling_suggestion = self.create_spelling_suggestion(
query_string)
return {
'results': results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
# ZYY: 创建拼写建议的方法
def create_spelling_suggestion(self, query_string):
spelling_suggestion = None
reader = self.index.reader()
corrector = reader.corrector(self.content_field_name)
cleaned_query = force_str(query_string)
if not query_string:
hits -= 1
if self.include_spelling:
if spelling_query:
spelling_suggestion = self.create_spelling_suggestion(
spelling_query)
else:
spelling_suggestion = self.create_spelling_suggestion(
query_string)
return {
'results': results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
def create_spelling_suggestion(self, query_string):
spelling_suggestion = None
reader = self.index.reader()
corrector = reader.corrector(self.content_field_name)
cleaned_query = force_str(query_string)
if not query_string:
return spelling_suggestion
# Clean the string.
for rev_word in self.RESERVED_WORDS:
cleaned_query = cleaned_query.replace(rev_word, '')
for rev_char in self.RESERVED_CHARACTERS:
cleaned_query = cleaned_query.replace(rev_char, '')
# Break it down.
query_words = cleaned_query.split()
suggested_words = []
for word in query_words:
suggestions = corrector.suggest(word, limit=1)
if len(suggestions) > 0:
suggested_words.append(suggestions[0])
spelling_suggestion = ' '.join(suggested_words)
return spelling_suggestion
# ZYY: 清理字符串,移除保留字和保留字符
for rev_word in self.RESERVED_WORDS:
cleaned_query = cleaned_query.replace(rev_word, '')
for rev_char in self.RESERVED_CHARACTERS:
cleaned_query = cleaned_query.replace(rev_char, '')
# ZYY: 分解查询词
query_words = cleaned_query.split()
suggested_words = []
for word in query_words:
suggestions = corrector.suggest(word, limit=1)
if len(suggestions) > 0:
suggested_words.append(suggestions[0])
spelling_suggestion = ' '.join(suggested_words)
return spelling_suggestion
# ZYY: 将Python值转换为Whoosh使用的字符串
def _from_python(self, value):
if hasattr(value, 'strftime'):
if not hasattr(value, 'hour'):
value = datetime(value.year, value.month, value.day, 0, 0, 0)
elif isinstance(value, bool):
if value:
value = ' true'
def _from_python(self, value):
"""
Converts Python values to a string for Whoosh.
Code courtesy of pysolr.
"""
if hasattr(value, 'strftime'):
if not hasattr(value, 'hour'):
value = datetime(value.year, value.month, value.day, 0, 0, 0)
elif isinstance(value, bool):
if value:
value = 'true'
else:
value = 'false'
elif isinstance(value, (list, tuple)):
value = u','.join([force_str(v) for v in value])
elif isinstance(value, (six.integer_types, float)):
# Leave it alone.
pass
else:
value = 'false'
elif isinstance(value, (list, tuple)):
value = u','.join([force_str(v) for v in value])
elif isinstance(value, (six.integer_types, float)):
# ZYY: 整数和浮点数保持不变
pass
else:
value = force_str(value)
return value
# ZYY: 将Whoosh的值转换为Python原生值
def _to_python(self, value):
if value == 'true ':
return True
elif value == 'false':
return False
if value and isinstance(value, six.string_types):
possible_datetime = DATETIME_REGEX.search(value)
if possible_datetime:
date_values = possible_datetime.groupdict()
for dk, dv in date_values.items():
date_values[dk] = int(dv)
return datetime(
date_values['year'],
date_values['month'],
date_values ['day'],
date_values['hour'],
date_values['minute'],
date_values['second'])
value = force_str(value)
return value
def _to_python(self, value):
"""
Converts values from Whoosh to native Python values.
A port of the same method in pysolr, as they deal with data the same way.
"""
if value == 'true':
return True
elif value == 'false':
return False
if value and isinstance(value, six.string_types):
possible_datetime = DATETIME_REGEX.search(value)
if possible_datetime:
date_values = possible_datetime.groupdict()
for dk, dv in date_values.items():
date_values[dk] = int(dv)
return datetime(
date_values['year'],
date_values['month'],
date_values['day'],
date_values['hour'],
date_values['minute'],
date_values['second'])
try:
# ZYY: 尝试使用json加载值
# Attempt to use json to load the values.
converted_value = json.loads(value)
# ZYY: 尝试处理大多数内置类型
# Try to handle most built-in types.
if isinstance(
converted_value,
(list,
@ -879,139 +863,182 @@ def _to_python(self, value):
complex)):
return converted_value
except BaseException:
# ZYY: 如果失败或不信任,则继续
pass
return value
# If it fails (SyntaxError or its ilk) or we don't trust it,
# continue on.
pass
return value
class WhooshSearchQuery(BaseSearchQuery):
# ZYY: 将datetime对象转换为Whoosh使用的字符串
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
return force_str(date.strftime('%Y%m%d000000'))
# ZYY: 清理查询片段,处理保留字和保留字符
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
Whoosh 1.X differs here in that you can no longer use a backslash
to escape reserved characters. Instead, the whole word should be
quoted.
"""
words = query_fragment.split()
cleaned_words = []
for word in words:
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
break
cleaned_words. append(word)
cleaned_words.append(word)
return ' '.join(cleaned_words)
# ZYY: 构建查询片段
def build_query_fragment(self, field, filter_type, value):
from haystack import connections
query_frag = ''
is_datetime = False
if not hasattr(value, 'input_type_name'):
# ZYY: 处理ValuesListQuerySet的情况
# Handle when we've got a ``ValuesListQuerySet``...
if hasattr(value, 'values_list'):
value = list(value)
if hasattr(value, ' strftime'):
if hasattr(value, 'strftime'):
is_datetime = True
if isinstance(value, six.string_types) and value != ' ':
# ZYY: 假设为Clean类型
# It's not an ``InputType``. Assume ``Clean``.
value = Clean(value)
else:
value = PythonData(value)
# ZYY: 使用InputType准备查询
prepared_value = value.prepare(self)
if not isinstance(prepared_value, (set, list, tuple )):
# ZYY: 转换为pysolr需要的格式
prepared_value = self.backend._from_python(prepared_value)
# ZYY: 'content'是特殊保留字,表示无特殊字段
if field == 'content':
index_fieldname = ''
else:
index_fieldname = u'%s:' % connections[self._using].get_unified_index(
).get_index_fieldname(field)
filter_types = {
'content': '%s',
'contains': '*%s*',
' endswith': "*%s",
'startswith': "%s*",
'exact': '%s',
'gt': "{%s to}",
'gte': "[%s to]",
'lt': "{to %s}",
'lte': "[to %s]",
'fuzzy': u'%s~',
}
if value.post_process is False:
query_frag = prepared_value
else:
if filter_type in [
'content',
'contains',
'startswith ',
'endswith',
'fuzzy']:
if value.input_type_name == 'exact':
query_frag = prepared_value
# Prepare the query using the InputType.
prepared_value = value.prepare(self)
if not isinstance(prepared_value, (set, list, tuple)):
# Then convert whatever we get back to what pysolr wants if needed.
prepared_value = self.backend._from_python(prepared_value)
# 'content' is a special reserved word, much like 'pk' in
# Django's ORM layer. It indicates 'no special field'.
if field == 'content':
index_fieldname = ''
else:
index_fieldname = u'%s:' % connections[self._using].get_unified_index(
).get_index_fieldname(field)
filter_types = {
'content': '%s',
'contains': '*%s*',
'endswith': "*%s",
'startswith': "%s*",
'exact': '%s',
'gt': "{%s to}",
'gte': "[%s to]",
'lt': "{to %s}",
'lte': "[to %s]",
'fuzzy': u'%s~',
}
if value.post_process is False:
query_frag = prepared_value
else:
if filter_type in [
'content',
'contains',
'startswith',
'endswith',
'fuzzy']:
if value.input_type_name == 'exact':
query_frag = prepared_value
else:
# Iterate over terms & incorportate the converted form of
# each into the query.
terms = []
if isinstance(prepared_value, six.string_types):
possible_values = prepared_value.split(' ')
else:
# ZYY: 遍历词项并合并转换后的形式
terms = []
if isinstance(prepared_value, six.string_types):
possible_values = prepared_value.split(' ')
else:
if is_datetime is True:
prepared_value = self._convert_datetime(
prepared_value)
possible_values = [prepared_value]
for possible_value in possible_values:
terms.append(
filter_types[filter_type] %
self.backend._from_python(possible_value))
if len(terms) == 1:
query_frag = terms[0]
else:
query_frag = u"(%s)" % " AND ".join(terms)
elif filter_type == 'in':
in_options = []
for possible_value in prepared_value:
is_datetime = False
if hasattr(possible_value, 'strftime'):
is_datetime = True
pv = self.backend._from_python (possible_value)
if is_datetime is True:
pv = self._convert_datetime(pv)
if isinstance(pv, six.string_types) and not is_datetime:
in_options.append('"%s"' % pv)
else:
in_options.append('%s' % pv)
query_frag = "(%s)" % " OR ".join(in_options)
elif filter_type == 'range':
start = self.backend._from_python(prepared_value[0])
end = self.backend._from_python(prepared_value[1])
if hasattr(prepared_value [0], 'strftime'):
start = self._convert_datetime(start)
if hasattr(prepared_value[1], 'strftime'):
end = self._convert_datetime(end)
query_frag = u"[%s to %s]" % (start, end)
elif filter_type == 'exact':
if value.input_type_name == 'exact':
query_frag = prepared_value
prepared_value = self._convert_datetime(
prepared_value)
possible_values = [prepared_value]
for possible_value in possible_values:
terms.append(
filter_types[filter_type] %
self.backend._from_python(possible_value))
if len(terms) == 1:
query_frag = terms[0]
else:
prepared_value = Exact(prepared_value).prepare(self)
query_frag = filter_types[filter_type] % prepared_value
else:
query_frag = u"(%s)" % " AND ".join(terms)
elif filter_type == 'in':
in_options = []
for possible_value in prepared_value:
is_datetime = False
if hasattr(possible_value, 'strftime'):
is_datetime = True
pv = self.backend._from_python(possible_value)
if is_datetime is True:
prepared_value = self._convert_datetime(prepared_value)
pv = self._convert_datetime(pv)
if isinstance(pv, six.string_types) and not is_datetime:
in_options.append('"%s"' % pv)
else:
in_options.append('%s' % pv)
query_frag = "(%s)" % " OR ".join(in_options)
elif filter_type == 'range':
start = self.backend._from_python(prepared_value[0])
end = self.backend._from_python(prepared_value[1])
if hasattr(prepared_value[0], 'strftime'):
start = self._convert_datetime(start)
if hasattr(prepared_value[1], 'strftime'):
end = self._convert_datetime(end)
query_frag = u"[%s to %s]" % (start, end)
elif filter_type == 'exact':
if value.input_type_name == 'exact':
query_frag = prepared_value
else:
prepared_value = Exact(prepared_value).prepare(self)
query_frag = filter_types[filter_type] % prepared_value
if len(query_frag) and not isinstance(value, Raw):
if not query_frag.startswith('(') and not query_frag.endswith(')'):
query_frag = "(%s)" % query_frag
else:
if is_datetime is True:
prepared_value = self._convert_datetime(prepared_value)
query_frag = filter_types[filter_type] % prepared_value
if len(query_frag) and not isinstance(value, Raw):
if not query_frag.startswith('(') and not query_frag.endswith(')'):
query_frag = "(%s)" % query_frag
return u"%s%s" % (index_fieldname, query_frag)
# if not filter_type in ('in', 'range'):
# # 'in' is a bit of a special case, as we don't want to
# # convert a valid list/tuple to string. Defer handling it
# # until later...
# value = self.backend._from_python(value)
class WhooshEngine(BaseEngine):
backend = WhooshSearchBackend
query = WhooshSearchQuery
query = WhooshSearchQuery

@ -6,19 +6,11 @@ It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
# ZYY: 导入操作系统模块,用于与操作系统交互(如环境变量设置)
import os
# ZYY: 从Django核心模块导入WSGI应用获取函数
# ZYY: WSGI (Web Server Gateway Interface) 是Python web应用与服务器之间的标准接口
from django.core.wsgi import get_wsgi_application
# ZYY: 设置默认环境变量DJANGO_SETTINGS_MODULE
# ZYY: 该变量指定Django项目的设置模块路径格式为"项目名.settings"
# ZYY: os.environ.setdefault()表示如果环境变量已存在则不覆盖
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
# ZYY: 获取WSGI应用实例并赋值给application变量
# ZYY: 这是WSGI服务器的入口点服务器将通过这个变量调用Django应用
# ZYY: get_wsgi_application()会加载Django设置并初始化应用
application = get_wsgi_application()

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

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

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

@ -9,78 +9,78 @@ import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # zy: 关键依赖 - 依赖用户模型
('oauth', '0001_initial'), # zy: 重要依赖 - 依赖初始迁移文件
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('oauth', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
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(
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(
model_name='oauthconfig',
name='created_time', # zy: 删除旧字段 - 原创建时间字段
name='created_time',
),
migrations.RemoveField(
model_name='oauthconfig',
name='last_mod_time', # zy: 删除旧字段 - 原修改时间字段
name='last_mod_time',
),
migrations.RemoveField(
model_name='oauthuser',
name='created_time', # zy: 删除旧字段 - 原创建时间字段
name='created_time',
),
migrations.RemoveField(
model_name='oauthuser',
name='last_mod_time', # zy: 删除旧字段 - 原修改时间字段
name='last_mod_time',
),
migrations.AddField(
model_name='oauthconfig',
name='creation_time', # zy: 新增字段 - 标准化的创建时间字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # zy: 使用国际化字段名称
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='oauthconfig',
name='last_modify_time', # zy: 新增字段 - 标准化的最后修改时间字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # zy: 使用国际化字段名称
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AddField(
model_name='oauthuser',
name='creation_time', # zy: 新增字段 - 标准化的创建时间字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # zy: 使用国际化字段名称
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='oauthuser',
name='last_modify_time', # zy: 新增字段 - 标准化的最后修改时间字段
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # zy: 使用国际化字段名称
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AlterField(
model_name='oauthconfig',
name='callback_url', # zy: 修改字段 - 回调地址字段
field=models.CharField(default='', max_length=200, verbose_name='callback url'), # zy: 重要变更 - 默认值改为空字符串,使用国际化字段名称
name='callback_url',
field=models.CharField(default='', max_length=200, verbose_name='callback url'),
),
migrations.AlterField(
model_name='oauthconfig',
name='is_enable', # zy: 修改字段 - 启用状态字段
field=models.BooleanField(default=True, verbose_name='is enable'), # zy: 使用国际化字段名称
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
migrations.AlterField(
model_name='oauthconfig',
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'), # 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'),
),
migrations.AlterField(
model_name='oauthuser',
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'), # 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'),
),
migrations.AlterField(
model_name='oauthuser',
name='nickname', # zy: 修改字段 - 昵称字段
field=models.CharField(max_length=50, verbose_name='nickname'), # zy: 使用国际化字段名称
name='nickname',
field=models.CharField(max_length=50, verbose_name='nickname'),
),
]
]

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

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

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

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

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

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

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

Loading…
Cancel
Save