Compare commits

..

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,138 +1,43 @@
<<<<<<< HEAD from django import forms
#django核心组件导入 from django.contrib.auth.admin import UserAdmin
from django import forms# Django 表单处理模块 from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.admin import UserAdmin # Django 默认用户管理后台类 from django.contrib.auth.forms import UsernameField
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 _ from django.utils.translation import gettext_lazy as _
>>>>>>> LXY_branch
<<<<<<< HEAD
<<<<<<< HEAD
# 本地应用导入
# Register your models here.
<<<<<<< HEAD
from .models import BlogUser # 导入自定义用户模型
=======
# 导入自定义用户模型
=======
# jyn:导入自定义用户模型 # jyn:导入自定义用户模型
>>>>>>> JYN_branch
from .models import BlogUser from .models import BlogUser
>>>>>>> JYN_branch
class BlogUserCreationForm(forms.ModelForm): 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提供密码验证功能 继承自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:密码字段使用PasswordInput小部件确保输入不可见
>>>>>>> JYN_branch
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
# jyn:确认密码字段,用于验证两次输入的密码是否一致 # jyn:确认密码字段,用于验证两次输入的密码是否一致
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta: class Meta:
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
# 指定关联的模型
=======
# jyn:指定关联的模型 # jyn:指定关联的模型
>>>>>>> JYN_branch
model = BlogUser model = BlogUser
# jyn:表单中包含的字段,这里只显示邮箱 # jyn:表单中包含的字段,这里只显示邮箱
fields = ('email',) fields = ('email',)
>>>>>>> JYN_branch
def clean_password2(self): 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_开头 这是Django表单验证机制的一部分方法名以clean_开头
""" """
password1 = self.cleaned_data.get("password1") password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2") password2 = self.cleaned_data.get("password2")
<<<<<<< HEAD
# 检查密码是否存在且不一致
>>>>>>> JYN_branch
=======
# jyn:检查密码是否存在且不一致 # jyn:检查密码是否存在且不一致
>>>>>>> JYN_branch
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
<<<<<<< HEAD raise forms.ValidationError(_("passwords do not match"))
<<<<<<< HEAD return password2
raise forms.ValidationError(_("passwords do not match"))# 错误信息(支持国际化)
return password2# 返回验证后的值
def save(self, commit=True): 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# 返回用户对象
=======
重写保存方法确保密码以哈希形式存储 重写保存方法确保密码以哈希形式存储
而不是明文存储 而不是明文存储
""" """
@ -144,79 +49,11 @@ class BlogUserCreationForm(forms.ModelForm): # lxy定义两个密码字段使
# jyn:标记用户来源为管理员站点 # jyn:标记用户来源为管理员站点
user.source = 'adminsite' user.source = 'adminsite'
user.save() user.save()
=======
raise forms.ValidationError(_("passwords do not match"))# 密码不一致时抛出错误
=======
raise forms.ValidationError(_("passwords do not match"))# lxy密码不一致时抛出错误
>>>>>>> LXY_branch
return password2
def save(self, commit=True):# lxy保存用户时对密码进行哈希处理后存储
# Save the provided password in hashed format
user = super().save(commit=False)# lxy先不提交到数据库
user.set_password(self.cleaned_data["password1"]) # lxy哈希处理密码
if commit:
<<<<<<< HEAD
user.source = 'adminsite'# 标记用户来源为“后台管理”
user.save()# 提交到数据库
>>>>>>> LXY_branch
=======
user.source = 'adminsite'# lxy标记用户来源为“后台管理”
user.save()# lxy提交到数据库
>>>>>>> LXY_branch
return user return user
>>>>>>> JYN_branch
class BlogUserChangeForm(UserChangeForm): 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 继承自Django内置的UserChangeForm
""" """
@ -242,34 +79,7 @@ class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm form = BlogUserChangeForm
# jyn:指定添加用户时使用的表单 # jyn:指定添加用户时使用的表单
add_form = BlogUserCreationForm 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:列表页面显示的字段
>>>>>>> JYN_branch
list_display = ( list_display = (
'id', 'id',
'nickname', 'nickname',
@ -277,26 +87,9 @@ class BlogUserAdmin(UserAdmin):
'email', 'email',
'last_login', 'last_login',
'date_joined', 'date_joined',
<<<<<<< HEAD
'source' 'source'
) )
# jyn:列表页面中可点击跳转的字段 # jyn:列表页面中可点击跳转的字段
list_display_links = ('id', 'username') 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降序排列最新的用户在前 # jyn:排序方式按id降序排列最新的用户在前
ordering = ('-id',) ordering = ('-id',)
>>>>>>> JYN_branch

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

@ -1,34 +1,3 @@
<<<<<<< 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 import forms
from django.contrib.auth import get_user_model, password_validation from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
@ -52,56 +21,21 @@ class LoginForm(AuthenticationForm):
self.fields['username'].widget = widgets.TextInput( self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"}) attrs={'placeholder': "username", "class": "form-control"})
<<<<<<< HEAD
# 自定义密码输入框添加占位符和CSS类
>>>>>>> 8b27cdad9a9ccc84febce3bcf1d211ed109f96f2
=======
# jyn:自定义密码输入框添加占位符和CSS类 # jyn:自定义密码输入框添加占位符和CSS类
>>>>>>> ef493f43496bf1ae99bf4389327ac0ae80bb9ae1
self.fields['password'].widget = widgets.PasswordInput( self.fields['password'].widget = widgets.PasswordInput(
<<<<<<< HEAD
attrs={'placeholder': "password", "class": "form-control"}) #为密码字段设置密码输入小部件,同样定义占位符和样式类。
<<<<<<< HEAD
=======
attrs={'placeholder': "password", "class": "form-control"}) 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): class RegisterForm(UserCreationForm):
""" """
自定义注册表单继承自Django内置的UserCreationForm 自定义注册表单继承自Django内置的UserCreationForm
用于处理用户注册逻辑包含用户名邮箱和密码验证 用于处理用户注册逻辑包含用户名邮箱和密码验证
""" """
>>>>>>> 8b27cdad9a9ccc84febce3bcf1d211ed109f96f2
>>>>>>> JYN_branch
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# jyn:调用父类构造方法初始化表单 # jyn:调用父类构造方法初始化表单
super(RegisterForm, self).__init__(*args, **kwargs) super(RegisterForm, self).__init__(*args, **kwargs)
<<<<<<< HEAD
# 自定义用户名、邮箱和密码字段的HTML属性
=======
<<<<<<< HEAD
# 自定义各字段的输入控件,添加样式和占位符
>>>>>>> JYN_branch
=======
# jyn:自定义各字段的输入控件,添加样式和占位符 # jyn:自定义各字段的输入控件,添加样式和占位符
>>>>>>> JYN_branch
self.fields['username'].widget = widgets.TextInput( self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"}) attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput( self.fields['email'].widget = widgets.EmailInput(
@ -110,15 +44,7 @@ class RegisterForm(UserCreationForm):
attrs={'placeholder': "password", "class": "form-control"}) attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput( self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"}) attrs={'placeholder': "repeat password", "class": "form-control"})
<<<<<<< HEAD
<<<<<<< HEAD
# 验证邮箱唯一性
=======
#__init__方法中设置username文本输入、email邮箱输入、password1和password2密码输入字段的占位符与form-control样式
>>>>>>> LXY_branch
=======
#lxy__init__方法中设置username文本输入、email邮箱输入、password1和password2密码输入字段的占位符与form-control样式
>>>>>>> LXY_branch
def clean_email(self): def clean_email(self):
""" """
邮箱验证方法检查邮箱是否已被注册 邮箱验证方法检查邮箱是否已被注册
@ -129,33 +55,12 @@ class RegisterForm(UserCreationForm):
if get_user_model().objects.filter(email=email).exists(): if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists")) #jyn: 抛出验证错误 raise ValidationError(_("email already exists")) #jyn: 抛出验证错误
return email return email
<<<<<<< HEAD
<<<<<<< HEAD
# 指定关联的用户模型和表单字段
class Meta:
<<<<<<< HEAD
model = get_user_model() # 使用项目配置的用户模型可能是自定义的BlogUser
fields = ("username", "email") # 注册表单中显示的字段
=======
#clean_email方法验证邮箱是否已被注册若存在则抛出“邮箱已存在”的验证错误
class Meta:
model = get_user_model()
fields = ("username", "email")
#Meta类指定关联模型为自定义用户模型表单字段包含username和email
>>>>>>> LXY_branch
=======
#lxyclean_email方法验证邮箱是否已被注册若存在则抛出“邮箱已存在”的验证错误
class Meta: class Meta:
model = get_user_model()
fields = ("username", "email")
#lxy Meta类指定关联模型为自定义用户模型表单字段包含username和email
>>>>>>> LXY_branch
=======
model = get_user_model() # jyn:使用项目配置的用户模型可能是自定义的BlogUser model = get_user_model() # jyn:使用项目配置的用户模型可能是自定义的BlogUser
fields = ("username", "email") # jyn:注册表单中显示的字段 fields = ("username", "email") # jyn:注册表单中显示的字段
>>>>>>> JYN_branch
# 忘记密码表单(验证邮箱和验证码)
class ForgetPasswordForm(forms.Form): class ForgetPasswordForm(forms.Form):
""" """
忘记密码表单用于用户重置密码的流程 忘记密码表单用于用户重置密码的流程
@ -171,16 +76,8 @@ class ForgetPasswordForm(forms.Form):
} }
), ),
) )
<<<<<<< HEAD
# 新密码字段2用于确认
=======
<<<<<<< HEAD
# 确认新密码字段
>>>>>>> JYN_branch
=======
# jyn:确认新密码字段 # jyn:确认新密码字段
>>>>>>> JYN_branch
new_password2 = forms.CharField( new_password2 = forms.CharField(
label="确认密码", label="确认密码",
widget=forms.PasswordInput( widget=forms.PasswordInput(
@ -190,16 +87,8 @@ class ForgetPasswordForm(forms.Form):
} }
), ),
) )
<<<<<<< HEAD
# 邮箱字段
=======
<<<<<<< HEAD
# 邮箱字段(用于验证用户身份)
>>>>>>> JYN_branch
=======
# jyn:邮箱字段(用于验证用户身份) # jyn:邮箱字段(用于验证用户身份)
>>>>>>> JYN_branch
email = forms.EmailField( email = forms.EmailField(
label='邮箱', label='邮箱',
widget=forms.TextInput( widget=forms.TextInput(
@ -209,16 +98,8 @@ class ForgetPasswordForm(forms.Form):
} }
), ),
) )
<<<<<<< HEAD
# 验证码字段
=======
<<<<<<< HEAD
# 验证码字段(用于身份验证)
>>>>>>> JYN_branch
=======
# jyn:验证码字段(用于身份验证) # jyn:验证码字段(用于身份验证)
>>>>>>> JYN_branch
code = forms.CharField( code = forms.CharField(
label=_('Code'), label=_('Code'),
widget=forms.TextInput( widget=forms.TextInput(
@ -228,15 +109,7 @@ class ForgetPasswordForm(forms.Form):
} }
), ),
) )
<<<<<<< HEAD
<<<<<<< HEAD
# 验证两次输入的密码是否一致,并检查密码强度
=======
#定义new_password1新密码密码输入、new_password2确认密码密码输入、email邮箱文本输入、code验证码文本输入字段均设置form-control样式和占位符。
>>>>>>> LXY_branch
=======
#lxy定义new_password1新密码密码输入、new_password2确认密码密码输入、email邮箱文本输入、code验证码文本输入字段均设置form-control样式和占位符。
>>>>>>> LXY_branch
def clean_new_password2(self): def clean_new_password2(self):
"""验证两次输入的密码是否一致,并验证密码强度""" """验证两次输入的密码是否一致,并验证密码强度"""
password1 = self.data.get("new_password1") password1 = self.data.get("new_password1")
@ -245,25 +118,12 @@ class ForgetPasswordForm(forms.Form):
# jyn:检查两次密码是否一致 # jyn:检查两次密码是否一致
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match")) raise ValidationError(_("passwords do not match"))
<<<<<<< HEAD
password_validation.validate_password(password2)# 使用Django的密码验证器
=======
# jyn:使用Django内置的密码验证器验证密码强度 # jyn:使用Django内置的密码验证器验证密码强度
password_validation.validate_password(password2) password_validation.validate_password(password2)
>>>>>>> JYN_branch
return password2 return password2
<<<<<<< HEAD
<<<<<<< HEAD
# 验证邮箱是否已注册
=======
# clean_new_password2方法验证两次新密码是否一致并对密码进行有效性校验
>>>>>>> LXY_branch
=======
#lxyclean_new_password2方法验证两次新密码是否一致并对密码进行有效性校验
>>>>>>> LXY_branch
def clean_email(self): def clean_email(self):
"""验证邮箱是否已注册""" """验证邮箱是否已注册"""
user_email = self.cleaned_data.get("email") user_email = self.cleaned_data.get("email")
@ -272,46 +132,20 @@ class ForgetPasswordForm(forms.Form):
# jyn:提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露) # jyn:提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露)
raise ValidationError(_("email does not exist")) raise ValidationError(_("email does not exist"))
return user_email return user_email
<<<<<<< HEAD
# 验证用户输入的验证码是否正确
=======
<<<<<<< HEAD
# clean_email方法验证邮箱是否已注册基于BlogUser模型未注册则抛出“邮箱不存在”的验证错误
>>>>>>> LXY_branch
=======
#lxyclean_email方法验证邮箱是否已注册基于BlogUser模型未注册则抛出“邮箱不存在”的验证错误
>>>>>>> LXY_branch
def clean_code(self): def clean_code(self):
"""验证验证码是否有效""" """验证验证码是否有效"""
code = self.cleaned_data.get("code") code = self.cleaned_data.get("code")
<<<<<<< HEAD
<<<<<<< HEAD
error = utils.verify(# 调用工具函数验证验证码
=======
# 调用工具函数验证邮箱和验证码是否匹配
=======
# jyn:调用工具函数验证邮箱和验证码是否匹配 # jyn:调用工具函数验证邮箱和验证码是否匹配
>>>>>>> JYN_branch
error = utils.verify( error = utils.verify(
>>>>>>> JYN_branch
email=self.cleaned_data.get("email"), email=self.cleaned_data.get("email"),
code=code, code=code,
) )
if error: if error:
raise ValidationError(error) # jyn:验证码无效时抛出错误 raise ValidationError(error) # jyn:验证码无效时抛出错误
return code return code
<<<<<<< HEAD
<<<<<<< HEAD
# 忘记密码功能中的验证码发送表单(仅需邮箱字段)
=======
#clean_code方法调用工具方法utils.verify验证验证码有效性无效则抛出错误
=======
#lxy clean_code方法调用工具方法utils.verify验证验证码有效性无效则抛出错误
>>>>>>> LXY_branch
>>>>>>> LXY_branch
class ForgetPasswordCodeForm(forms.Form): class ForgetPasswordCodeForm(forms.Form):
""" """
发送密码重置验证码的表单 发送密码重置验证码的表单
@ -319,13 +153,4 @@ class ForgetPasswordCodeForm(forms.Form):
""" """
email = forms.EmailField( email = forms.EmailField(
label=_('Email'), label=_('Email'),
<<<<<<< HEAD )
)
<<<<<<< HEAD
=======
)
#仅包含email字段邮箱输入用于忘记密码流程中验证邮箱的步骤
>>>>>>> LXY_branch
=======
#lxy仅包含email字段邮箱输入用于忘记密码流程中验证邮箱的步骤
>>>>>>> LXY_branch

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

@ -10,17 +10,8 @@ from . import utils # jyn:导入当前应用的工具函数
# Create your tests here. # Create your tests here.
# 创建测试类继承Django的TestCase
class AccountTest(TestCase): class AccountTest(TestCase):
<<<<<<< HEAD
# 测试初始化方法(每个测试方法运行前都会执行)
def setUp(self):
# 初始化测试客户端(模拟浏览器请求)
self.client = Client()
# 初始化请求工厂(用于生成请求对象)
self.factory = RequestFactory()
# 创建一个普通测试用户
=======
""" """
账号相关功能的测试类继承自Django的TestCase 账号相关功能的测试类继承自Django的TestCase
包含用户登录注册密码重置等功能的测试用例 包含用户登录注册密码重置等功能的测试用例
@ -30,71 +21,25 @@ class AccountTest(TestCase):
测试前的初始化方法会在每个测试方法执行前运行 测试前的初始化方法会在每个测试方法执行前运行
用于创建测试所需的基础数据 用于创建测试所需的基础数据
""" """
<<<<<<< HEAD
self.client = Client() # 创建测试客户端,用于模拟用户请求
self.factory = RequestFactory() # 创建请求工厂,用于构建请求对象
# 创建一个测试用户
>>>>>>> JYN_branch
=======
self.client = Client() # jyn:创建测试客户端,用于模拟用户请求 self.client = Client() # jyn:创建测试客户端,用于模拟用户请求
self.factory = RequestFactory() # jyn:创建请求工厂,用于构建请求对象 self.factory = RequestFactory() # jyn:创建请求工厂,用于构建请求对象
# jyn:创建一个测试用户 # jyn:创建一个测试用户
>>>>>>> JYN_branch
self.blog_user = BlogUser.objects.create_user( self.blog_user = BlogUser.objects.create_user(
username="test", username="test",
email="admin@admin.com", email="admin@admin.com",
password="12345678" password="12345678"
) )
<<<<<<< HEAD
<<<<<<< HEAD
# 测试用的随机字符串
self.new_test = "xxx123--="
# 测试用户账号验证功能
def test_validate_account(self):
# 获取当前站点域名
site = get_current_site().domain
# 创建一个超级用户(用于测试管理员权限)
=======
self.new_test = "xxx123--=" # 测试用的新密码
=======
self.new_test = "xxx123--=" #jyn: 测试用的新密码 self.new_test = "xxx123--=" #jyn: 测试用的新密码
>>>>>>> JYN_branch
def test_validate_account(self): def test_validate_account(self):
"""测试用户账号验证相关功能,包括登录和管理员权限""" """测试用户账号验证相关功能,包括登录和管理员权限"""
site = get_current_site().domain # jyn:获取当前站点域名 site = get_current_site().domain # jyn:获取当前站点域名
<<<<<<< HEAD
# 创建一个超级用户
>>>>>>> JYN_branch
=======
# jyn:创建一个超级用户 # jyn:创建一个超级用户
>>>>>>> JYN_branch
user = BlogUser.objects.create_superuser( user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", email="liangliangyy1@gmail.com",
username="liangliangyy1", username="liangliangyy1",
password="qwer!@#$ggg") password="qwer!@#$ggg")
<<<<<<< HEAD
<<<<<<< HEAD
# 从数据库获取刚创建的超级用户(验证是否创建成功)
testuser = BlogUser.objects.get(username='liangliangyy1')
=======
# 获取创建的用户
testuser = BlogUser.objects.get(username='liangliangyy1')
>>>>>>> JYN_branch
# 测试登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
<<<<<<< HEAD
self.assertEqual(loginresult, True)# 验证登录成功
# 测试访问管理员后台
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)# 验证返回200状态码
=======
self.assertEqual(loginresult, True) # 断言登录成功
=======
# jyn:获取创建的用户 # jyn:获取创建的用户
testuser = BlogUser.objects.get(username='liangliangyy1') testuser = BlogUser.objects.get(username='liangliangyy1')
@ -103,45 +48,22 @@ class AccountTest(TestCase):
username='liangliangyy1', username='liangliangyy1',
password='qwer!@#$ggg') password='qwer!@#$ggg')
self.assertEqual(loginresult, True) #jyn: 断言登录成功 self.assertEqual(loginresult, True) #jyn: 断言登录成功
>>>>>>> JYN_branch
# jyn:测试访问管理员页面 # jyn:测试访问管理员页面
response = self.client.get('/admin/') response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功 self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功
<<<<<<< HEAD
>>>>>>> JYN_branch
# 创建测试分类
=======
# jyn:创建测试分类 # jyn:创建测试分类
>>>>>>> JYN_branch
category = Category() category = Category()
category.name = "categoryaaa" category.name = "categoryaaa"
category.creation_time = timezone.now() category.creation_time = timezone.now()
category.last_modify_time = timezone.now() category.last_modify_time = timezone.now()
category.save() category.save()
<<<<<<< HEAD
=======
<<<<<<< HEAD
>>>>>>> JYN_branch
# 创建测试文章
=======
# jyn:创建测试文章 # jyn:创建测试文章
>>>>>>> JYN_branch
article = Article() article = Article()
article.title = "nicetitleaaa" article.title = "nicetitleaaa"
article.body = "nicecontentaaa" 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.author = user
article.category = category article.category = category
article.type = 'a' # jyn:假设'a'表示文章类型 article.type = 'a' # jyn:假设'a'表示文章类型
@ -150,94 +72,37 @@ class AccountTest(TestCase):
# jyn:测试访问文章管理页面 # jyn:测试访问文章管理页面
response = self.client.get(article.get_admin_url()) 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:断言页面访问成功 self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功
>>>>>>> JYN_branch
# 测试用户注册功能
def test_validate_register(self): def test_validate_register(self):
<<<<<<< HEAD
# 验证测试邮箱初始不存在
=======
"""测试用户注册功能,包括注册流程、邮箱验证和权限控制""" """测试用户注册功能,包括注册流程、邮箱验证和权限控制"""
<<<<<<< HEAD
# 初始状态下,该邮箱应不存在
>>>>>>> JYN_branch
=======
# jyn:初始状态下,该邮箱应不存在 # jyn:初始状态下,该邮箱应不存在
>>>>>>> JYN_branch
self.assertEquals( self.assertEquals(
0, len( 0, len(
BlogUser.objects.filter( BlogUser.objects.filter(
email='user123@user.com'))) email='user123@user.com')))
<<<<<<< HEAD
# 模拟注册请求
=======
<<<<<<< HEAD
# 模拟用户注册提交
>>>>>>> JYN_branch
=======
# jyn:模拟用户注册提交 # jyn:模拟用户注册提交
>>>>>>> JYN_branch
response = self.client.post(reverse('account:register'), { response = self.client.post(reverse('account:register'), {
'username': 'user1233', 'username': 'user1233',
'email': 'user123@user.com', 'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T', 'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T', 'password2': 'password123!q@wE#R$T',
}) })
<<<<<<< HEAD
# 验证用户已创建(通过邮箱查询)
=======
<<<<<<< HEAD
# 注册后,该邮箱应存在
>>>>>>> JYN_branch
=======
#jyn: 注册后,该邮箱应存在 #jyn: 注册后,该邮箱应存在
>>>>>>> JYN_branch
self.assertEquals( self.assertEquals(
1, len( 1, len(
BlogUser.objects.filter( BlogUser.objects.filter(
email='user123@user.com'))) email='user123@user.com')))
<<<<<<< HEAD
# 获取刚注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 生成验证签名(用于邮箱验证等场景)
=======
#jyn: 获取刚注册的用户 #jyn: 获取刚注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0] user = BlogUser.objects.filter(email='user123@user.com')[0]
<<<<<<< HEAD
# 生成验证链接(模拟邮箱验证流程)
>>>>>>> JYN_branch
=======
# jyn:生成验证链接(模拟邮箱验证流程) # jyn:生成验证链接(模拟邮箱验证流程)
>>>>>>> JYN_branch
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 构造验证URL
path = reverse('accounts:result') path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format( url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign) path=path, id=user.id, sign=sign)
<<<<<<< HEAD
# 测试访问验证URL
response = self.client.get(url)
self.assertEqual(response.status_code, 200) # 验证返回200状态码
# 使用测试客户端登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
# 获取指定邮箱的用户并设置为超级用户和工作人员
=======
# jyn:访问验证链接 # jyn:访问验证链接
response = self.client.get(url) response = self.client.get(url)
@ -245,46 +110,23 @@ class AccountTest(TestCase):
# jyn:登录新注册用户 # jyn:登录新注册用户
self.client.login(username='user1233', password='password123!q@wE#R$T') self.client.login(username='user1233', password='password123!q@wE#R$T')
<<<<<<< HEAD
# 提升用户权限
>>>>>>> JYN_branch
=======
# jyn:提升用户权限 # jyn:提升用户权限
>>>>>>> JYN_branch
user = BlogUser.objects.filter(email='user123@user.com')[0] user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True user.is_superuser = True
user.is_staff = True user.is_staff = True
user.save() user.save()
<<<<<<< HEAD
# 删除侧边栏缓存
delete_sidebar_cache()
# 创建分类
=======
# jyn:清除缓存 # jyn:清除缓存
delete_sidebar_cache() delete_sidebar_cache()
<<<<<<< HEAD
# 创建测试分类
>>>>>>> JYN_branch
=======
# jyn:创建测试分类 # jyn:创建测试分类
>>>>>>> JYN_branch
category = Category() category = Category()
category.name = "categoryaaa" category.name = "categoryaaa"
category.creation_time = timezone.now() category.creation_time = timezone.now()
category.last_modify_time = timezone.now() category.last_modify_time = timezone.now()
category.save() category.save()
<<<<<<< HEAD
# 创建文章
=======
<<<<<<< HEAD
# 创建测试文章
>>>>>>> JYN_branch
=======
# jyn:创建测试文章 # jyn:创建测试文章
>>>>>>> JYN_branch
article = Article() article = Article()
article.category = category article.category = category
article.title = "nicetitle333" article.title = "nicetitle333"
@ -293,18 +135,6 @@ class AccountTest(TestCase):
article.type = 'a' article.type = 'a'
article.status = 'p' article.status = 'p'
article.save() 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:测试访问文章管理页面 # jyn:测试访问文章管理页面
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url())
@ -318,71 +148,23 @@ class AccountTest(TestCase):
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200]) self.assertIn(response.status_code, [301, 302, 200])
<<<<<<< HEAD
# 使用错误密码登录
>>>>>>> JYN_branch
=======
# jyn:使用错误密码登录 # jyn:使用错误密码登录
>>>>>>> JYN_branch
response = self.client.post(reverse('account:login'), { response = self.client.post(reverse('account:login'), {
'username': 'user1233', 'username': 'user1233',
'password': 'password123'# 注意这里密码与登录时使用的不同 'password': 'password123'
}) })
self.assertIn(response.status_code, [301, 302, 200]) self.assertIn(response.status_code, [301, 302, 200])
<<<<<<< HEAD
# 测试使用错误密码登录后访问文章管理URL应重定向
=======
<<<<<<< HEAD
# 错误登录后访问管理页面
>>>>>>> JYN_branch
=======
# jyn:错误登录后访问管理页面 # jyn:错误登录后访问管理页面
>>>>>>> JYN_branch
response = self.client.get(article.get_admin_url()) response = self.client.get(article.get_admin_url())
<<<<<<< HEAD
<<<<<<< HEAD
self.assertIn(response.status_code, [301, 302, 200]) self.assertIn(response.status_code, [301, 302, 200])
# 测试邮箱验证码验证
=======
self.assertIn(response.status_code, [301, 302, 200])#测试用户注册流程:验证注册前后用户数量变化,邮箱验证链接的有效性,以及注册后用户权限、文章发布等功能
=======
self.assertIn(response.status_code, [301, 302, 200])#lxy测试用户注册流程验证注册前后用户数量变化邮箱验证链接的有效性以及注册后用户权限、文章发布等功能
>>>>>>> LXY_branch
>>>>>>> LXY_branch
def test_verify_email_code(self): def test_verify_email_code(self):
"""测试邮箱验证码验证功能""" """测试邮箱验证码验证功能"""
to_email = "admin@admin.com" to_email = "admin@admin.com"
<<<<<<< HEAD
<<<<<<< HEAD
code = generate_code() # 生成验证码
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:生成验证码 code = generate_code() # jyn:生成验证码
utils.set_code(to_email, code) # jyn:存储验证码 utils.set_code(to_email, code) # jyn:存储验证码
utils.send_verify_email(to_email, code) # jyn:发送验证邮件 utils.send_verify_email(to_email, code) # jyn:发送验证邮件
>>>>>>> JYN_branch
# jyn:验证正确的邮箱和验证码 # jyn:验证正确的邮箱和验证码
err = utils.verify("admin@admin.com", code) err = utils.verify("admin@admin.com", code)
@ -392,41 +174,10 @@ class AccountTest(TestCase):
err = utils.verify("admin@123.com", code) err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) # jyn:应返回错误信息 self.assertEqual(type(err), str) # jyn:应返回错误信息
>>>>>>> JYN_branch
def test_forget_password_email_code_success(self): def test_forget_password_email_code_success(self):
"""测试发送密码重置验证码成功的情况""" """测试发送密码重置验证码成功的情况"""
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password_code"), path=reverse("account:forget_password_code"),
<<<<<<< HEAD
<<<<<<< HEAD
data=dict(email="admin@admin.com") # 使用正确邮箱格式
)
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:使用已存在的邮箱 data=dict(email="admin@admin.com") # jyn:使用已存在的邮箱
) )
@ -436,47 +187,17 @@ class AccountTest(TestCase):
def test_forget_password_email_code_fail(self): def test_forget_password_email_code_fail(self):
"""测试发送密码重置验证码失败的情况""" """测试发送密码重置验证码失败的情况"""
# jyn:不提供邮箱 # jyn:不提供邮箱
>>>>>>> JYN_branch
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password_code"), path=reverse("account:forget_password_code"),
data=dict() 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:断言返回错误信息 self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # jyn:断言返回错误信息
# jyn:提供无效格式的邮箱 # jyn:提供无效格式的邮箱
>>>>>>> JYN_branch
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password_code"), path=reverse("account:forget_password_code"),
data=dict(email="admin@com") data=dict(email="admin@com")
) )
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试忘记密码重置功能 - 成功情况
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: 断言返回错误信息 self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #jyn: 断言返回错误信息
def test_forget_password_email_success(self): def test_forget_password_email_success(self):
@ -484,35 +205,18 @@ class AccountTest(TestCase):
code = generate_code() # jyn:生成验证码 code = generate_code() # jyn:生成验证码
utils.set_code(self.blog_user.email, code) #jyn: 存储验证码 utils.set_code(self.blog_user.email, code) #jyn: 存储验证码
# jyn:准备重置密码的数据 # jyn:准备重置密码的数据
>>>>>>> JYN_branch
data = dict( data = dict(
new_password1=self.new_test, # 新密码 new_password1=self.new_test,
new_password2=self.new_test,# 确认密码 new_password2=self.new_test,
email=self.blog_user.email,# 用户邮箱 email=self.blog_user.email,
code=code, # 验证码 code=code,
) )
<<<<<<< HEAD
<<<<<<< HEAD
# 提交重置密码请求
=======
# 提交密码重置请求
>>>>>>> JYN_branch
=======
# jyn:提交密码重置请求 # jyn:提交密码重置请求
>>>>>>> JYN_branch
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password"), path=reverse("account:forget_password"),
data=data data=data
) )
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.status_code, 302) # 应重定向
=======
self.assertEqual(resp.status_code, 302) # 成功重置后通常重定向
>>>>>>> JYN_branch
=======
self.assertEqual(resp.status_code, 302) # jyn:成功重置后通常重定向 self.assertEqual(resp.status_code, 302) # jyn:成功重置后通常重定向
>>>>>>> JYN_branch
# jyn:验证密码是否已更新 # jyn:验证密码是否已更新
blog_user = BlogUser.objects.filter( blog_user = BlogUser.objects.filter(
@ -521,21 +225,13 @@ class AccountTest(TestCase):
self.assertNotEqual(blog_user, None) # jyn:断言用户存在 self.assertNotEqual(blog_user, None) # jyn:断言用户存在
# jyn:断言密码修改成功 # jyn:断言密码修改成功
self.assertEqual(blog_user.check_password(data["new_password1"]), True) self.assertEqual(blog_user.check_password(data["new_password1"]), True)
# 测试忘记密码重置功能 - 用户不存在情况
def test_forget_password_email_not_user(self): def test_forget_password_email_not_user(self):
"""测试使用不存在的邮箱重置密码的情况""" """测试使用不存在的邮箱重置密码的情况"""
data = dict( data = dict(
new_password1=self.new_test, new_password1=self.new_test,
new_password2=self.new_test, new_password2=self.new_test,
<<<<<<< HEAD
<<<<<<< HEAD
email="123@123.com",# 不存在的邮箱
=======
email="123@123.com", # 不存在的邮箱
>>>>>>> JYN_branch
=======
email="123@123.com", # jyn:不存在的邮箱 email="123@123.com", # jyn:不存在的邮箱
>>>>>>> JYN_branch
code="123456", code="123456",
) )
resp = self.client.post( resp = self.client.post(
@ -543,68 +239,22 @@ class AccountTest(TestCase):
data=data 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:应返回页面但不重置密码 self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码
>>>>>>> JYN_branch
# 测试忘记密码重置功能 - 验证码错误情况
def test_forget_password_email_code_error(self): def test_forget_password_email_code_error(self):
<<<<<<< HEAD
code = generate_code()
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:生成正确验证码 code = generate_code() # jyn:生成正确验证码
utils.set_code(self.blog_user.email, code) #jyn: 存储验证码 utils.set_code(self.blog_user.email, code) #jyn: 存储验证码
# jyn:使用错误的验证码 # jyn:使用错误的验证码
>>>>>>> JYN_branch
data = dict( data = dict(
new_password1=self.new_test, new_password1=self.new_test,
new_password2=self.new_test, new_password2=self.new_test,
email=self.blog_user.email, email=self.blog_user.email,
<<<<<<< HEAD
<<<<<<< HEAD
code="111111",# 错误验证码
=======
code="111111", # 错误的验证码
>>>>>>> JYN_branch
=======
code="111111", # jyn:错误的验证码 code="111111", # jyn:错误的验证码
>>>>>>> JYN_branch
) )
resp = self.client.post( resp = self.client.post(
path=reverse("account:forget_password"), path=reverse("account:forget_password"),
data=data data=data
) )
<<<<<<< HEAD self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码
<<<<<<< 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,42 +1,3 @@
<<<<<<< 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 path
from django.urls import re_path # jyn:用于正则表达式匹配URL from django.urls import re_path # jyn:用于正则表达式匹配URL
@ -46,12 +7,7 @@ from .forms import LoginForm # jyn:导入自定义的登录表单
# jyn:定义应用命名空间避免URL名称冲突 # jyn:定义应用命名空间避免URL名称冲突
app_name = "accounts" app_name = "accounts"
<<<<<<< HEAD
<<<<<<< HEAD
# URL路由配置列表映射URL路径到对应的视图
=======
# jyn:URL路由配置列表映射URL路径到对应的视图 # jyn:URL路由配置列表映射URL路径到对应的视图
>>>>>>> JYN_branch
urlpatterns = [ urlpatterns = [
# jyn:登录页面路由使用正则匹配以login/结尾的路径 # jyn:登录页面路由使用正则匹配以login/结尾的路径
re_path(r'^login/$', re_path(r'^login/$',
@ -89,34 +45,5 @@ urlpatterns = [
re_path(r'^forget_password_code/$', re_path(r'^forget_password_code/$',
# jyn:调用ForgetPasswordEmailCode类视图处理发送验证码逻辑 # jyn:调用ForgetPasswordEmailCode类视图处理发送验证码逻辑
views.ForgetPasswordEmailCode.as_view(), views.ForgetPasswordEmailCode.as_view(),
<<<<<<< HEAD
name='forget_password_code'), # URL名称
]
>>>>>>> JYN_branch
=======
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),#登录路由对应LoginView用LoginForm表单成功重定向/
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),#注册路由对应RegisterView成功重定向/。
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),#登出路由对应LogoutView。
path(r'account/result.html',
views.account_result,
name='result'),#结果页路由对应account_result视图
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),#忘记密码路由对应ForgetPasswordView
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),#忘记密码验证码路由对应ForgetPasswordEmailCode
]
>>>>>>> LXY_branch
=======
name='forget_password_code'), # jyn:URL名称 name='forget_password_code'), # jyn:URL名称
] ]
>>>>>>> JYN_branch

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

@ -1,29 +1,6 @@
# 导入标准库模块 import typing
import typing # 用于类型注解 from datetime import timedelta
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# 假设是项目封装的邮件发送函数
# 验证码有效期5分钟
=======
from django.core.cache import cache # 导入Django缓存模块用于存储验证码
from django.utils.translation import gettext # 用于获取即时翻译文本
from django.utils.translation import gettext_lazy as _ # 用于延迟翻译文本(支持国际化)
<<<<<<< HEAD
<<<<<<< HEAD
from djangoblog.utils import send_email # 导入项目自定义的发送邮件工具函数
# 验证码有效期5分钟全局变量统一控制时效
>>>>>>> JYN_branch
=======
from django.core.cache import cache # jyn:导入Django缓存模块用于存储验证码 from django.core.cache import cache # jyn:导入Django缓存模块用于存储验证码
from django.utils.translation import gettext # jyn:用于获取即时翻译文本 from django.utils.translation import gettext # jyn:用于获取即时翻译文本
from django.utils.translation import gettext_lazy as _ # jyn:用于延迟翻译文本(支持国际化) from django.utils.translation import gettext_lazy as _ # jyn:用于延迟翻译文本(支持国际化)
@ -31,25 +8,11 @@ from django.utils.translation import gettext_lazy as _ # jyn:用于延迟翻译
from djangoblog.utils import send_email #jyn: 导入项目自定义的发送邮件工具函数 from djangoblog.utils import send_email #jyn: 导入项目自定义的发送邮件工具函数
# jyn:验证码有效期5分钟全局变量统一控制时效 # jyn:验证码有效期5分钟全局变量统一控制时效
>>>>>>> JYN_branch
_code_ttl = timedelta(minutes=5) _code_ttl = timedelta(minutes=5)
=======
_code_ttl = timedelta(minutes=5)#验证码有效期设置为5分钟。
>>>>>>> LXY_branch
=======
_code_ttl = timedelta(minutes=5)#lxy验证码有效期设置为5分钟。
>>>>>>> LXY_branch
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
""" """
<<<<<<< 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: Args:
to_mail: 接收验证码的目标邮箱地址 to_mail: 接收验证码的目标邮箱地址
@ -60,25 +23,12 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
html_content = _( html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it " "You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code} "properly") % {'code': code}
<<<<<<< HEAD
# 调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容
>>>>>>> JYN_branch
=======
# jyn:调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容 # jyn:调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容
>>>>>>> JYN_branch
send_email([to_mail], subject, html_content) send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]: def verify(email: str, code: str) -> typing.Optional[str]:
""" """
<<<<<<< HEAD
# 从缓存获取该邮箱对应的正确验证码
cache_code = get_code(email)
# 比对用户输入的验证码和缓存中的验证码
if cache_code != code:
return gettext("Verification code error") # 返回翻译后的错误信息
# 隐含逻辑:验证成功时返回 None调用方需检查返回值是否为 None
=======
验证用户输入的验证码是否有效 验证用户输入的验证码是否有效
Args: Args:
email: 用户提交的邮箱用于匹配缓存中的验证码 email: 用户提交的邮箱用于匹配缓存中的验证码
@ -95,74 +45,26 @@ def verify(email: str, code: str) -> typing.Optional[str]:
if cache_code != code: if cache_code != code:
#jyn: 验证码不匹配时,返回国际化的错误提示 #jyn: 验证码不匹配时,返回国际化的错误提示
return gettext("Verification code error") return gettext("Verification code error")
>>>>>>> JYN_branch
def set_code(email: str, code: str): def set_code(email: str, code: str):
""" """
<<<<<<< HEAD
将验证码存入缓存
Args:
email (str): 用户邮箱作为缓存键
code (str): 要存储的验证码
Note:
验证码有效期由全局变量 _code_ttl 控制5分钟
"""
# 使用 Django 缓存设置键值对,并指定过期时间(转换为秒)
"""设置code"""
=======
将验证码存入缓存以邮箱为key设置有效期 将验证码存入缓存以邮箱为key设置有效期
Args: Args:
email: 作为缓存key的邮箱地址确保一个邮箱对应一个验证码 email: 作为缓存key的邮箱地址确保一个邮箱对应一个验证码
code: 需要存入缓存的验证码 code: 需要存入缓存的验证码
""" """
<<<<<<< HEAD
# 调用Django缓存的set方法key=邮箱value=验证码timeout=有效期(秒)
>>>>>>> JYN_branch
=======
# jyn:调用Django缓存的set方法key=邮箱value=验证码timeout=有效期(秒) # jyn:调用Django缓存的set方法key=邮箱value=验证码timeout=有效期(秒)
>>>>>>> JYN_branch
cache.set(email, code, _code_ttl.seconds) cache.set(email, code, _code_ttl.seconds)
<<<<<<< HEAD
<<<<<<< HEAD
def get_code(email: str) -> typing.Optional[str]: def get_code(email: str) -> typing.Optional[str]:
""" """
<<<<<<< HEAD
从缓存获取验证码
Args:
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: Args:
email: 用于查询的邮箱地址缓存key email: 用于查询的邮箱地址缓存key
Return: Return:
缓存中存在该邮箱对应的验证码时返回字符串不存在时返回None 缓存中存在该邮箱对应的验证码时返回字符串不存在时返回None
""" """
<<<<<<< HEAD
# 调用Django缓存的get方法根据邮箱key获取验证码
return cache.get(email)
>>>>>>> JYN_branch
=======
# jyn:调用Django缓存的get方法根据邮箱key获取验证码 # jyn:调用Django缓存的get方法根据邮箱key获取验证码
return cache.get(email) return cache.get(email)
>>>>>>> JYN_branch

@ -1,73 +1,13 @@
# 导入日志模块,用于记录运行时的信息和错误
import logging import logging
# Django 国际化工具,`gettext_lazy` 用于延迟翻译(适合模块级字符串)
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# Django 配置项,用于访问 settings.py 中的设置
from django.conf import settings from django.conf import settings
# Django 认证系统核心模块
from django.contrib import auth from django.contrib import auth
<<<<<<< HEAD
<<<<<<< HEAD
# 认证相关常量(如重定向字段名)
from django.contrib.auth import REDIRECT_FIELD_NAME
# 获取当前用户模型的快捷方式
from django.contrib.auth import 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 REDIRECT_FIELD_NAME # jyn:登录后重定向字段名常量
from django.contrib.auth import get_user_model # jyn:获取项目配置的用户模型 from django.contrib.auth import get_user_model # jyn:获取项目配置的用户模型
from django.contrib.auth import logout # jyn:登出功能函数 from django.contrib.auth import logout # jyn:登出功能函数
from django.contrib.auth.forms import AuthenticationForm # jyn:Django内置登录表单 from django.contrib.auth.forms import AuthenticationForm # jyn:Django内置登录表单
from django.contrib.auth.hashers import make_password # jyn:密码哈希处理函数 from django.contrib.auth.hashers import make_password # jyn:密码哈希处理函数
from django.http import HttpResponseRedirect, HttpResponseForbidden # jyn:HTTP响应类 from django.http import HttpResponseRedirect, HttpResponseForbidden # jyn:HTTP响应类
>>>>>>> JYN_branch
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404, render # jyn:快捷函数 from django.shortcuts import get_object_or_404, render # jyn:快捷函数
@ -87,39 +27,12 @@ from .forms import (RegisterForm, LoginForm, ForgetPasswordForm,
ForgetPasswordCodeForm) # jyn:当前应用表单类 ForgetPasswordCodeForm) # jyn:当前应用表单类
from .models import BlogUser # jyn:自定义用户模型 from .models import BlogUser # jyn:自定义用户模型
<<<<<<< HEAD
logger = logging.getLogger(__name__) # 初始化日志记录器
>>>>>>> JYN_branch
# Create your views here.
# 注册视图类(继承自 FormView处理表单提交
=======
logger = logging.getLogger(__name__) # jyn:初始化日志记录器 logger = logging.getLogger(__name__) # jyn:初始化日志记录器
# jyn:Create your views here. # jyn:Create your views here.
>>>>>>> JYN_branch
class RegisterView(FormView): 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处理表单提交的通用视图 用户注册类视图继承自FormView处理表单提交的通用视图
负责用户注册表单展示数据验证发送验证邮件及注册结果跳转 负责用户注册表单展示数据验证发送验证邮件及注册结果跳转
@ -130,33 +43,9 @@ class RegisterView(FormView):
@method_decorator(csrf_protect) # jyn:为视图添加CSRF保护 @method_decorator(csrf_protect) # jyn:为视图添加CSRF保护
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
"""重写分发方法,添加装饰器后调用父类逻辑""" """重写分发方法,添加装饰器后调用父类逻辑"""
>>>>>>> JYN_branch
return super(RegisterView, self).dispatch(*args, **kwargs) return super(RegisterView, self).dispatch(*args, **kwargs)
# 表单验证通过后的处理逻辑
def form_valid(self, form):
<<<<<<< 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 def form_valid(self, form):
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# 构造邮件内容(包含验证链接)
=======
"""表单验证通过后执行的逻辑(注册核心流程)""" """表单验证通过后执行的逻辑(注册核心流程)"""
if form.is_valid(): if form.is_valid():
# jyn:1. 暂存用户数据不立即保存is_active设为False需邮箱验证后激活 # jyn:1. 暂存用户数据不立即保存is_active设为False需邮箱验证后激活
@ -178,12 +67,7 @@ class RegisterView(FormView):
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign) site=site, path=path, id=user.id, sign=sign)
<<<<<<< HEAD
# 3. 构造验证邮件内容并发送
>>>>>>> JYN_branch
=======
# jyn:3. 构造验证邮件内容并发送 # jyn:3. 构造验证邮件内容并发送
>>>>>>> JYN_branch
content = """ content = """
<p>请点击下面链接验证您的邮箱</p> <p>请点击下面链接验证您的邮箱</p>
@ -194,42 +78,11 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器 如果上面链接无法打开请将此链接复制至浏览器
{url} {url}
""".format(url=url) """.format(url=url)
# 发送验证邮件
send_email( send_email(
<<<<<<< HEAD
<<<<<<< HEAD
emailto=[
user.email, # 收件人列表
],
title='验证您的电子邮箱', # 邮件标题
content=content)# 邮件正文
# 重定向到注册结果页面(附带用户 ID
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
# 表单无效时重新渲染表单(显示错误信息)
return self.render_to_response({
'form': form
<<<<<<< HEAD
<<<<<<< HEAD
})
=======
emailto=[user.email], # 收件人邮箱(新注册用户的邮箱)
title='验证您的电子邮箱', # 邮件标题
content=content # 邮件HTML内容
=======
emailto=[user.email], # jyn:收件人邮箱(新注册用户的邮箱) emailto=[user.email], # jyn:收件人邮箱(新注册用户的邮箱)
title='验证您的电子邮箱', #jyn: 邮件标题 title='验证您的电子邮箱', #jyn: 邮件标题
content=content #jyn: 邮件HTML内容 content=content #jyn: 邮件HTML内容
>>>>>>> JYN_branch
) )
=======
})#form_valid方法中保存用户并设置为非活跃状态生成邮箱验证链接并发送验证邮件最后重定向到结果页。
>>>>>>> LXY_branch
=======
})#lxyform_valid方法中保存用户并设置为非活跃状态生成邮箱验证链接并发送验证邮件最后重定向到结果页。
>>>>>>> LXY_branch
# jyn:4. 跳转到注册结果页(提示用户查收验证邮件) # jyn:4. 跳转到注册结果页(提示用户查收验证邮件)
url = reverse('accounts:result') + f'?type=register&id={str(user.id)}' url = reverse('accounts:result') + f'?type=register&id={str(user.id)}'
@ -237,27 +90,9 @@ class RegisterView(FormView):
else: else:
# jyn:表单验证失败,重新渲染表单并显示错误 # jyn:表单验证失败,重新渲染表单并显示错误
return self.render_to_response({'form': form}) return self.render_to_response({'form': form})
>>>>>>> JYN_branch
# 登出视图继承自RedirectView重定向到登录页面
class LogoutView(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处理重定向的通用视图 用户登出类视图继承自RedirectView处理重定向的通用视图
负责清除用户会话缓存并重定向到登录页 负责清除用户会话缓存并重定向到登录页
@ -267,219 +102,60 @@ class LogoutView(RedirectView):
@method_decorator(never_cache) # jyn:禁止缓存登出页面,避免浏览器缓存导致的问题 @method_decorator(never_cache) # jyn:禁止缓存登出页面,避免浏览器缓存导致的问题
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""重写分发方法,添加装饰器后调用父类逻辑""" """重写分发方法,添加装饰器后调用父类逻辑"""
>>>>>>> JYN_branch
return super(LogoutView, self).dispatch(request, *args, **kwargs) return super(LogoutView, self).dispatch(request, *args, **kwargs)
# 处理GET请求
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
<<<<<<< HEAD
# 执行登出操作
logout(request)
# 删除侧边栏缓存
delete_sidebar_cache()
<<<<<<< HEAD
<<<<<<< HEAD
# 调用父类的get方法完成重定向
return super(LogoutView, self).get(request, *args, **kwargs)
=======
"""处理GET请求登出核心逻辑""" """处理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:清除用户会话,实现登出 logout(request) # jyn:清除用户会话,实现登出
delete_sidebar_cache() # jyn:删除侧边栏缓存(可能存储了用户相关信息) delete_sidebar_cache() # jyn:删除侧边栏缓存(可能存储了用户相关信息)
return super(LogoutView, self).get(request, *args, **kwargs) # jyn:执行重定向 return super(LogoutView, self).get(request, *args, **kwargs) # jyn:执行重定向
>>>>>>> JYN_branch
# 登录视图继承自FormView
class LoginView(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 用户登录类视图继承自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: 指定使用的自定义登录表单 form_class = LoginForm #jyn: 指定使用的自定义登录表单
template_name = 'account/login.html' # jyn:登录页面模板路径 template_name = 'account/login.html' # jyn:登录页面模板路径
success_url = '/' # jyn:登录成功默认重定向地址(首页) success_url = '/' # jyn:登录成功默认重定向地址(首页)
redirect_field_name = REDIRECT_FIELD_NAME # jyn:重定向字段名默认next redirect_field_name = REDIRECT_FIELD_NAME # jyn:重定向字段名默认next
login_ttl = 2626560 #jyn: “记住登录”状态的有效期约等于1个月 login_ttl = 2626560 #jyn: “记住登录”状态的有效期约等于1个月
>>>>>>> JYN_branch
# jyn:为视图添加多重装饰器敏感参数保护、CSRF保护、禁止缓存 # jyn:为视图添加多重装饰器敏感参数保护、CSRF保护、禁止缓存
@method_decorator(sensitive_post_parameters('password')) # jyn:保护密码参数,避免日志泄露 @method_decorator(sensitive_post_parameters('password')) # jyn:保护密码参数,避免日志泄露
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
@method_decorator(never_cache) @method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
>>>>>>> JYN_branch
return super(LoginView, self).dispatch(request, *args, **kwargs) return super(LoginView, self).dispatch(request, *args, **kwargs)
# 获取模板上下文数据
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
<<<<<<< HEAD
# 从GET参数中获取重定向URL
=======
"""添加额外上下文数据(重定向地址)到模板""" """添加额外上下文数据(重定向地址)到模板"""
<<<<<<< HEAD
# 获取URL中的重定向参数如登录前访问的受保护页面
>>>>>>> JYN_branch
=======
# jyn:获取URL中的重定向参数如登录前访问的受保护页面 # jyn:获取URL中的重定向参数如登录前访问的受保护页面
>>>>>>> JYN_branch
redirect_to = self.request.GET.get(self.redirect_field_name) redirect_to = self.request.GET.get(self.redirect_field_name)
# 如果不存在则设置为根路径
if redirect_to is None: if redirect_to is None:
<<<<<<< HEAD
<<<<<<< HEAD
redirect_to = '/'
# 将重定向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:默认重定向到首页 redirect_to = '/' # jyn:默认重定向到首页
kwargs['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)
=======
return super(LoginView, self).get_context_data(**kwargs)#处理用户登录逻辑指定表单类LoginForm、模板account / login.html和成功后重定向地址 /
>>>>>>> LXY_branch
def form_valid(self, form): def form_valid(self, form):
"""表单验证通过后执行的逻辑(登录核心流程)""" """表单验证通过后执行的逻辑(登录核心流程)"""
<<<<<<< HEAD
# 用Django内置AuthenticationForm重新验证确保认证逻辑符合默认规范
>>>>>>> JYN_branch
=======
# jyn:用Django内置AuthenticationForm重新验证确保认证逻辑符合默认规范 # jyn:用Django内置AuthenticationForm重新验证确保认证逻辑符合默认规范
>>>>>>> JYN_branch
form = AuthenticationForm(data=self.request.POST, request=self.request) form = AuthenticationForm(data=self.request.POST, request=self.request)
# 再次验证表单
if form.is_valid(): if form.is_valid():
<<<<<<< HEAD
<<<<<<< HEAD
# 删除侧边栏缓存
delete_sidebar_cache()
# 记录日志
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:删除侧边栏缓存(更新用户登录状态) delete_sidebar_cache() # jyn:删除侧边栏缓存(更新用户登录状态)
logger.info(self.redirect_field_name) # jyn:日志记录重定向字段名 logger.info(self.redirect_field_name) # jyn:日志记录重定向字段名
>>>>>>> JYN_branch
# jyn:执行登录:将用户信息存入会话 # jyn:执行登录:将用户信息存入会话
auth.login(self.request, form.get_user()) auth.login(self.request, form.get_user())
<<<<<<< HEAD
# 处理“记住我”功能若勾选设置会话有效期为1个月
>>>>>>> JYN_branch
=======
# jyn:处理“记住我”功能若勾选设置会话有效期为1个月 # jyn:处理“记住我”功能若勾选设置会话有效期为1个月
>>>>>>> JYN_branch
if self.request.POST.get("remember"): if self.request.POST.get("remember"):
# 设置较长的会话过期时间
self.request.session.set_expiry(self.login_ttl) self.request.session.set_expiry(self.login_ttl)
<<<<<<< HEAD
<<<<<<< HEAD
# 调用父类方法处理成功跳转
=======
# 调用父类form_valid执行重定向
>>>>>>> JYN_branch
=======
# jyn:调用父类form_valid执行重定向 # jyn:调用父类form_valid执行重定向
>>>>>>> JYN_branch
return super(LoginView, self).form_valid(form) return super(LoginView, self).form_valid(form)
<<<<<<< HEAD
=======
#lxyreturn HttpResponseRedirect('/')
>>>>>>> LXY_branch
else: 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:表单验证失败(如密码错误),重新渲染表单并显示错误
>>>>>>> JYN_branch
return self.render_to_response({'form': form}) return self.render_to_response({'form': form})
def get_success_url(self): def get_success_url(self):
@ -489,34 +165,11 @@ class LoginView(FormView):
# jyn:验证重定向地址的安全性:避免跳转到外部恶意网站 # jyn:验证重定向地址的安全性:避免跳转到外部恶意网站
if not url_has_allowed_host_and_scheme( if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[self.request.get_host()]): url=redirect_to, allowed_hosts=[self.request.get_host()]):
<<<<<<< HEAD
redirect_to = self.success_url # 不安全则使用默认重定向地址
>>>>>>> JYN_branch
=======
redirect_to = self.success_url # jyn:不安全则使用默认重定向地址 redirect_to = self.success_url # jyn:不安全则使用默认重定向地址
>>>>>>> JYN_branch
return redirect_to return redirect_to
=======
return redirect_to#get_success_url方法处理登录后的重定向地址确保其安全性
>>>>>>> LXY_branch
=======
return redirect_to#lxyget_success_url方法处理登录后的重定向地址确保其安全性
>>>>>>> LXY_branch
# 账户操作结果页面(如注册成功、邮箱验证等)
def account_result(request): def account_result(request):
<<<<<<< HEAD
# 从GET参数中获取类型和用户ID
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('/')
# 检查类型参数是否有效
=======
""" """
账号操作结果视图函数函数视图 账号操作结果视图函数函数视图
处理注册成功提示邮箱验证逻辑并展示结果页面 处理注册成功提示邮箱验证逻辑并展示结果页面
@ -527,30 +180,13 @@ def account_result(request):
# jyn:获取对应的用户若不存在则返回404 # jyn:获取对应的用户若不存在则返回404
user = get_object_or_404(get_user_model(), id=id) user = get_object_or_404(get_user_model(), id=id)
<<<<<<< HEAD
<<<<<<< HEAD
logger.info(type) # 日志记录操作类型 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:若用户已激活,直接重定向到首页(避免重复验证) # jyn:若用户已激活,直接重定向到首页(避免重复验证)
if user.is_active: if user.is_active:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
# jyn:处理合法的操作类型(注册成功提示/邮箱验证) # jyn:处理合法的操作类型(注册成功提示/邮箱验证)
>>>>>>> JYN_branch
if type and type in ['register', 'validation']: if type and type in ['register', 'validation']:
if type == 'register': if type == 'register':
# jyn:注册成功:提示用户查收验证邮件 # jyn:注册成功:提示用户查收验证邮件
@ -559,182 +195,56 @@ def account_result(request):
''' '''
title = '注册成功' title = '注册成功'
else: else:
<<<<<<< HEAD
<<<<<<< HEAD
# 生成验证签名
=======
# 邮箱验证:验证签名是否正确,正确则激活用户
# 重新计算签名与URL中的签名对比防止链接篡改
>>>>>>> JYN_branch
=======
# jyn:邮箱验证:验证签名是否正确,正确则激活用户 # jyn:邮箱验证:验证签名是否正确,正确则激活用户
# jyn:重新计算签名与URL中的签名对比防止链接篡改 # jyn:重新计算签名与URL中的签名对比防止链接篡改
>>>>>>> JYN_branch
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 获取请求中的签名
sign = request.GET.get('sign') sign = request.GET.get('sign')
# 验证签名是否匹配
if sign != c_sign: if sign != c_sign:
<<<<<<< HEAD
<<<<<<< HEAD
return HttpResponseForbidden()
# 激活用户账户
user.is_active = True
user.save()
# 验证成功提示内容
=======
return HttpResponseForbidden() # 签名不匹配返回403禁止访问
# 激活用户将is_active设为True
user.is_active = True
user.save()
# 验证成功:提示用户可登录
>>>>>>> JYN_branch
=======
return HttpResponseForbidden() # jyn:签名不匹配返回403禁止访问 return HttpResponseForbidden() # jyn:签名不匹配返回403禁止访问
# jyn:激活用户将is_active设为True # jyn:激活用户将is_active设为True
user.is_active = True user.is_active = True
user.save() user.save()
# jyn:验证成功:提示用户可登录 # jyn:验证成功:提示用户可登录
>>>>>>> JYN_branch
content = ''' content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站 恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
''' '''
title = '验证成功' title = '验证成功'
<<<<<<< HEAD
<<<<<<< HEAD
# 渲染结果页面
=======
# 渲染结果页面,传递标题和内容
>>>>>>> JYN_branch
=======
# jyn:渲染结果页面,传递标题和内容 # jyn:渲染结果页面,传递标题和内容
>>>>>>> JYN_branch
return render(request, 'account/result.html', { return render(request, 'account/result.html', {
'title': title, 'title': title,
'content': content 'content': content
}) })
else: else:
<<<<<<< HEAD
<<<<<<< HEAD
# 无效类型重定向到首页
=======
# 操作类型不合法,重定向到首页
>>>>>>> JYN_branch
=======
# jyn:操作类型不合法,重定向到首页 # jyn:操作类型不合法,重定向到首页
>>>>>>> JYN_branch
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
# 忘记密码视图继承自FormView
class ForgetPasswordView(FormView): class ForgetPasswordView(FormView):
<<<<<<< HEAD
# 使用的表单类
form_class = ForgetPasswordForm
<<<<<<< HEAD
<<<<<<< HEAD
# 模板文件路径
template_name = 'account/forget_password.html'
=======
""" """
忘记密码类视图继承自FormView 忘记密码类视图继承自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:指定使用的密码重置表单 form_class = ForgetPasswordForm # jyn:指定使用的密码重置表单
template_name = 'account/forget_password.html' # jyn:密码重置页面模板路径 template_name = 'account/forget_password.html' # jyn:密码重置页面模板路径
>>>>>>> JYN_branch
# 表单验证通过后的处理
def form_valid(self, form): def form_valid(self, form):
"""表单验证通过后执行的逻辑(密码重置核心流程)""" """表单验证通过后执行的逻辑(密码重置核心流程)"""
if form.is_valid(): if form.is_valid():
<<<<<<< HEAD
<<<<<<< HEAD
# 根据邮箱获取用户对象
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# 设置新密码(加密)
=======
# 1. 获取表单中的邮箱,查询对应的用户
blog_user = BlogUser.objects.filter(
email=form.cleaned_data.get("email")
).get()
# 2. 对新密码进行哈希处理,并更新用户密码
>>>>>>> JYN_branch
=======
# jyn:1. 获取表单中的邮箱,查询对应的用户 # jyn:1. 获取表单中的邮箱,查询对应的用户
blog_user = BlogUser.objects.filter( blog_user = BlogUser.objects.filter(
email=form.cleaned_data.get("email") email=form.cleaned_data.get("email")
).get() ).get()
# jyn:2. 对新密码进行哈希处理,并更新用户密码 # jyn:2. 对新密码进行哈希处理,并更新用户密码
>>>>>>> JYN_branch
blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.password = make_password(form.cleaned_data["new_password2"])
# 保存用户对象
blog_user.save() blog_user.save()
<<<<<<< HEAD
<<<<<<< HEAD
# 重定向到登录页面
return HttpResponseRedirect('/login/')
else:
<<<<<<< HEAD
<<<<<<< HEAD
# 表单无效,重新渲染表单并显示错误
=======
# 3. 密码重置成功,重定向到登录页
return HttpResponseRedirect('/login/')
else:
# 表单验证失败(如验证码错误、密码不一致),重新渲染表单
>>>>>>> JYN_branch
=======
# jyn:3. 密码重置成功,重定向到登录页 # jyn:3. 密码重置成功,重定向到登录页
return HttpResponseRedirect('/login/') return HttpResponseRedirect('/login/')
else: else:
# jyn:表单验证失败(如验证码错误、密码不一致),重新渲染表单 # jyn:表单验证失败(如验证码错误、密码不一致),重新渲染表单
>>>>>>> JYN_branch
return self.render_to_response({'form': form}) 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处理忘记密码的邮箱验证码发送逻辑 class ForgetPasswordEmailCode(View):
>>>>>>> LXY_branch
def post(self, request: HttpRequest):
# 验证表单
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
# 表单无效返回错误
return HttpResponse("错误的邮箱")
# 获取邮箱地址
to_email = form.cleaned_data["email"]
# 生成验证码
code = generate_code()
# 发送验证邮件
utils.send_verify_email(to_email, code)
# 存储验证码(通常有有效期)
utils.set_code(to_email, code)
<<<<<<< HEAD
# 返回成功响应
return HttpResponse("ok")
=======
""" """
发送密码重置验证码类视图继承自基础View 发送密码重置验证码类视图继承自基础View
负责验证邮箱合法性生成验证码发送验证邮件并返回结果 负责验证邮箱合法性生成验证码发送验证邮件并返回结果
@ -752,19 +262,5 @@ class ForgetPasswordEmailCode(View):#lxy处理忘记密码的邮箱验证码发
utils.send_verify_email(to_email, code) # jyn:发送验证码邮件 utils.send_verify_email(to_email, code) # jyn:发送验证码邮件
utils.set_code(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”提示 # jyn:3. 操作成功返回“ok”提示
return HttpResponse("ok") return HttpResponse("ok")
>>>>>>> JYN_branch

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,75 +1,23 @@
<<<<<<< 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: 该模块用于配置 Django 管理后台的评论Comment模型管理界面
# jyn: 功能包括:自定义评论启用/禁用批量操作、列表页展示字段配置、关联对象跳转链接、界面显示优化等 # jyn: 功能包括:自定义评论启用/禁用批量操作、列表页展示字段配置、关联对象跳转链接、界面显示优化等
from django.contrib import admin from django.contrib import admin
from django.urls import reverse # jyn: 用于反向解析 admin 后台的模型编辑页面 URL from django.urls import reverse # jyn: 用于反向解析 admin 后台的模型编辑页面 URL
from django.utils.html import format_html # jyn: 用于安全生成 HTML 标签,避免 XSS 风险 from django.utils.html import format_html # jyn: 用于安全生成 HTML 标签,避免 XSS 风险
from django.utils.translation import gettext_lazy as _ # jyn: 用于国际化翻译,支持多语言显示 from django.utils.translation import gettext_lazy as _ # jyn: 用于国际化翻译,支持多语言显示
>>>>>>> JYN_branch
def disable_commentstatus(modeladmin, request, queryset): def disable_commentstatus(modeladmin, request, queryset):
""" """
<<<<<<< HEAD
自定义Admin批量操作批量禁用选中的评论
参数说明
- modeladmin当前关联的Admin模型类实例
- request当前请求对象
- queryset用户在Admin中选中的评论数据集合
"""
# 批量更新选中评论的is_enable字段为False禁用状态
queryset.update(is_enable=False)
=======
jyn: 批量禁用评论的自定义操作函数 jyn: 批量禁用评论的自定义操作函数
:param modeladmin: 关联的 ModelAdmin 实例 :param modeladmin: 关联的 ModelAdmin 实例
:param request: 当前请求对象 :param request: 当前请求对象
:param queryset: 选中的评论记录集合 :param queryset: 选中的评论记录集合
""" """
queryset.update(is_enable=False) # jyn: 将选中评论的 is_enable 字段设为 False实现禁用 queryset.update(is_enable=False) # jyn: 将选中评论的 is_enable 字段设为 False实现禁用
>>>>>>> JYN_branch
def enable_commentstatus(modeladmin, request, queryset): def enable_commentstatus(modeladmin, request, queryset):
""" """
<<<<<<< HEAD
自定义Admin批量操作批量启用选中的评论
参数与disable_commentstatus一致功能相反
"""
# 批量更新选中评论的is_enable字段为True启用状态
queryset.update(is_enable=True)
# 为批量操作函数设置在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: 批量启用评论的自定义操作函数 jyn: 批量启用评论的自定义操作函数
:param modeladmin: 关联的 ModelAdmin 实例 :param modeladmin: 关联的 ModelAdmin 实例
:param request: 当前请求对象 :param request: 当前请求对象
@ -98,24 +46,9 @@ class CommentAdmin(admin.ModelAdmin):
list_filter = ('is_enable',) # jyn: 右侧筛选器,按启用状态筛选评论 list_filter = ('is_enable',) # jyn: 右侧筛选器,按启用状态筛选评论
exclude = ('creation_time', 'last_modify_time') # jyn: 编辑页隐藏的字段(自动生成,无需手动修改) exclude = ('creation_time', 'last_modify_time') # jyn: 编辑页隐藏的字段(自动生成,无需手动修改)
actions = [disable_commentstatus, enable_commentstatus] # jyn: 注册批量操作函数 actions = [disable_commentstatus, enable_commentstatus] # jyn: 注册批量操作函数
>>>>>>> JYN_branch
# 2. 自定义列表页字段:生成“评论作者”的跳转链接
def link_to_userinfo(self, obj): def link_to_userinfo(self, obj):
""" """
<<<<<<< HEAD
obj当前循环的评论对象每条评论对应一个obj
返回值带有HTML链接的作者名称点击跳转到作者的Admin编辑页
"""
# 获取评论作者模型如User模型的元数据app名称和模型名称
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 反转生成作者Admin编辑页的URL格式为“admin:app名_模型名_change”参数为作者ID
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 生成安全的HTML链接优先显示作者昵称没有昵称则显示邮箱
# 注原代码中HTML标签内href属性缺失值应改为href="%s"),此处按正确逻辑补充
return format_html(
u'%s' %
=======
jyn: 自定义列表字段生成评论作者的 admin 编辑页跳转链接 jyn: 自定义列表字段生成评论作者的 admin 编辑页跳转链接
:param obj: 当前 Comment 模型实例 :param obj: 当前 Comment 模型实例
:return: 包含跳转链接的 HTML 字符串 :return: 包含跳转链接的 HTML 字符串
@ -127,29 +60,11 @@ class CommentAdmin(admin.ModelAdmin):
# jyn: 生成 HTML 链接,优先显示昵称,无昵称则显示邮箱 # jyn: 生成 HTML 链接,优先显示昵称,无昵称则显示邮箱
return format_html( return format_html(
u'<a href="%s">%s</a>' % u'<a href="%s">%s</a>' %
>>>>>>> JYN_branch
(link, obj.author.nickname if obj.author.nickname else obj.author.email) (link, obj.author.nickname if obj.author.nickname else obj.author.email)
) )
# 3. 自定义列表页字段:生成“评论所属文章”的跳转链接
def link_to_article(self, obj): def link_to_article(self, obj):
""" """
<<<<<<< HEAD
逻辑与link_to_userinfo类似生成文章的Admin编辑页跳转链接
"""
# 获取评论所属文章模型的元数据
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 反转生成文章Admin编辑页的URL参数为文章ID
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# 生成HTML链接显示文章标题点击跳转到文章编辑页
return format_html(
u'%s' % (link, obj.article.title)
)
# 4. 为自定义字段设置在Admin界面显示的名称支持国际化
link_to_userinfo.short_description = _('User') # 自定义字段“link_to_userinfo”显示为“用户”
link_to_article.short_description = _('Article') # 自定义字段“link_to_article”显示为“文章”
=======
jyn: 自定义列表字段生成关联文章的 admin 编辑页跳转链接 jyn: 自定义列表字段生成关联文章的 admin 编辑页跳转链接
:param obj: 当前 Comment 模型实例 :param obj: 当前 Comment 模型实例
:return: 包含跳转链接的 HTML 字符串 :return: 包含跳转链接的 HTML 字符串
@ -166,4 +81,3 @@ class CommentAdmin(admin.ModelAdmin):
# jyn: 为自定义列表字段设置后台显示名称(支持国际化) # jyn: 为自定义列表字段设置后台显示名称(支持国际化)
link_to_userinfo.short_description = _('User') link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article') link_to_article.short_description = _('Article')
>>>>>>> JYN_branch

@ -1,23 +1,3 @@
<<<<<<< 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的配置类用于定义应用元数据和初始化行为 # jyn: 评论应用comments的配置类用于定义应用元数据和初始化行为
from django.apps import AppConfig # jyn: Django 内置的应用配置基类,所有应用配置需继承此类 from django.apps import AppConfig # jyn: Django 内置的应用配置基类,所有应用配置需继承此类
@ -26,4 +6,3 @@ class CommentsConfig(AppConfig):
"""jyn: comments 应用的核心配置类,指定应用名称等关键信息""" """jyn: comments 应用的核心配置类,指定应用名称等关键信息"""
name = 'comments' # jyn: 应用唯一标识名称与项目目录名一致Django 通过该名称识别应用 name = 'comments' # jyn: 应用唯一标识名称与项目目录名一致Django 通过该名称识别应用
>>>>>>> JYN_branch

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

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

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

@ -1,62 +1,3 @@
<<<<<<< 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 _ # 国际化翻译,支持多语言显示
# 导入关联模型从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: 评论功能核心数据模型模块,定义评论的数据库结构、关联关系及基础配置 # jyn: 评论功能核心数据模型模块,定义评论的数据库结构、关联关系及基础配置
from django.conf import settings # jyn: 导入 Django 项目配置,用于获取自定义用户模型 from django.conf import settings # jyn: 导入 Django 项目配置,用于获取自定义用户模型
from django.db import models # jyn: Django 数据库模型基类,所有数据模型需继承此类 from django.db import models # jyn: Django 数据库模型基类,所有数据模型需继承此类
@ -99,38 +40,15 @@ class Comment(models.Model):
default=False, default=False,
blank=False, blank=False,
null=False null=False
>>>>>>> JYN_branch
) )
# 模型元数据:控制模型的整体行为(排序、显示名等)
class Meta: 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 界面行为""" """jyn: 模型元数据配置,控制数据库表结构和 Admin 界面行为"""
ordering = ['-id'] # jyn: 数据查询默认排序:按评论 ID 倒序(最新评论在前) ordering = ['-id'] # jyn: 数据查询默认排序:按评论 ID 倒序(最新评论在前)
verbose_name = _('comment') # jyn: 模型单数显示名称(支持国际化) verbose_name = _('comment') # jyn: 模型单数显示名称(支持国际化)
verbose_name_plural = verbose_name # jyn: 模型复数显示名称(与单数一致) verbose_name_plural = verbose_name # jyn: 模型复数显示名称(与单数一致)
get_latest_by = 'id' # jyn: 使用 latest() 方法时,按 ID 字段获取最新记录 get_latest_by = 'id' # jyn: 使用 latest() 方法时,按 ID 字段获取最新记录
>>>>>>> JYN_branch
# 模型实例的字符串表示打印评论对象时显示正文便于调试和Admin显示
def __str__(self): def __str__(self):
<<<<<<< HEAD
<<<<<<< HEAD
=======
"""jyn: 模型实例的字符串表示Admin 界面及打印时显示评论正文""" """jyn: 模型实例的字符串表示Admin 界面及打印时显示评论正文"""
>>>>>>> JYN_branch
return self.body
=======
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,41 +1,3 @@
<<<<<<< HEAD
# 导入Django测试核心模块、URL工具及项目内模型/工具
from django.test import Client, RequestFactory, TransactionTestCase # Django测试类Client模拟HTTP请求RequestFactory构造请求对象TransactionTestCase支持事务回滚
from django.urls import reverse # 生成URL通过URL名称反向解析避免硬编码
# 导入项目内关联模型:用户、分类、文章、评论模型
from accounts.models import BlogUser
from blog.models import Category, Article
from comments.models import Comment
# 导入评论相关自定义模板标签和通用工具函数
from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid
# Create your tests here.
class CommentsTest(TransactionTestCase):
"""
评论功能测试类继承TransactionTestCase用于测试评论的提交状态更新等核心逻辑
支持数据库事务回滚确保测试用例间数据隔离
"""
def setUp(self):
"""
测试前置初始化方法每个测试用例执行前自动调用
作用创建测试所需的基础数据客户端用户系统配置等
"""
# 1. 初始化测试工具Client模拟浏览器请求RequestFactory构造原始请求对象
self.client = Client()
self.factory = RequestFactory()
# 2. 初始化博客系统配置:设置“评论需审核”(模拟真实场景中评论需管理员审核才能显示)
from blog.models import BlogSettings # 局部导入避免循环引用
value = BlogSettings() # 创建配置对象
value.comment_need_review = True # 开启评论审核开关
value.save() # 保存到测试数据库
# 3. 创建测试超级用户:用于模拟登录状态下提交评论
=======
# jyn: 评论功能测试模块,涵盖评论提交、审核状态、回复功能、模板标签及工具函数的完整性测试 # jyn: 评论功能测试模块,涵盖评论提交、审核状态、回复功能、模板标签及工具函数的完整性测试
from django.test import Client, RequestFactory, TransactionTestCase # jyn: Django 测试核心类Client模拟请求RequestFactory构造请求TransactionTestCase支持事务回滚 from django.test import Client, RequestFactory, TransactionTestCase # jyn: Django 测试核心类Client模拟请求RequestFactory构造请求TransactionTestCase支持事务回滚
from django.urls import reverse # jyn: 反向解析URL用于生成测试请求地址 from django.urls import reverse # jyn: 反向解析URL用于生成测试请求地址
@ -62,87 +24,32 @@ class CommentsTest(TransactionTestCase):
value.save() value.save()
# jyn: 创建超级用户,用于测试登录状态下的评论提交 # jyn: 创建超级用户,用于测试登录状态下的评论提交
>>>>>>> JYN_branch
self.user = BlogUser.objects.create_superuser( self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", # 测试邮箱 email="liangliangyy1@gmail.com",
username="liangliangyy1", # 测试用户名 username="liangliangyy1",
password="liangliangyy1" # 测试密码明文Django会自动加密存储 password="liangliangyy1")
)
def update_article_comment_status(self, article): def update_article_comment_status(self, article):
""" """
<<<<<<< HEAD
辅助方法批量更新某篇文章下所有评论的启用状态设为启用
模拟管理员审核通过评论的操作用于测试审核后评论的显示逻辑
参数
- article目标文章对象需更新其下所有评论
"""
# 获取该文章下所有评论
comments = article.comment_set.all()
# 遍历评论将“是否启用”字段设为True并保存
=======
jyn: 辅助方法批量更新指定文章下所有评论的启用状态模拟后台审核通过 jyn: 辅助方法批量更新指定文章下所有评论的启用状态模拟后台审核通过
:param article: 目标文章实例 :param article: 目标文章实例
""" """
comments = article.comment_set.all() # jyn: 获取文章关联的所有评论 comments = article.comment_set.all() # jyn: 获取文章关联的所有评论
>>>>>>> JYN_branch
for comment in comments: for comment in comments:
comment.is_enable = True # jyn: 设为启用状态 comment.is_enable = True # jyn: 设为启用状态
comment.save() # jyn: 保存修改 comment.save() # jyn: 保存修改
def test_validate_comment(self): def test_validate_comment(self):
<<<<<<< HEAD
"""
核心测试用例验证评论提交流程登录创建文章提交评论验证状态
覆盖场景登录用户提交评论评论未审核时不显示审核后正常显示
"""
# 1. 模拟用户登录:使用之前创建的测试超级用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 2. 创建测试分类:文章需关联分类,先创建分类数据
=======
"""jyn: 核心测试用例,验证评论提交、审核、回复、模板标签及工具函数的正确性""" """jyn: 核心测试用例,验证评论提交、审核、回复、模板标签及工具函数的正确性"""
# 1. 模拟用户登录 # 1. 模拟用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1') self.client.login(username='liangliangyy1', password='liangliangyy1')
# 2. 创建测试分类和文章(评论的关联对象) # 2. 创建测试分类和文章(评论的关联对象)
>>>>>>> JYN_branch
category = Category() category = Category()
category.name = "categoryccc" # 分类名称 category.name = "categoryccc"
category.save() # 保存到测试数据库 category.save()
# 3. 创建测试文章:评论需关联文章,创建一篇已发布的文章
article = Article() article = Article()
<<<<<<< HEAD
article.title = "nicetitleccc" # 文章标题
article.body = "nicecontentccc" # 文章内容
article.author = self.user # 关联作者(测试用户)
article.category = category # 关联分类(刚创建的测试分类)
article.type = 'a' # 文章类型(假设'a'代表普通文章)
article.status = 'p' # 文章状态(假设'p'代表已发布)
article.save() # 保存到测试数据库
# 4. 构造评论提交URL通过URL名称“comments:postcomment”反向解析传入文章ID
comment_url = reverse(
'comments:postcomment',
kwargs={'article_id': article.id} # URL参数文章ID指定评论所属文章
)
# 5. 模拟POST请求提交评论向评论URL发送包含评论内容的请求
response = self.client.post(
comment_url,
{'body': '123ffffffffff'} # 请求参数:评论正文
)
# 6. 验证评论提交结果检查响应状态码是否为302重定向通常提交后跳回文章页
self.assertEqual(response.status_code, 302)
# 7. 验证“未审核评论不显示”重新获取文章检查其评论列表长度是否为0因评论需审核
article = Article.objects.get(pk=article.pk) # 从数据库重新查询(避免缓存)
self.assertEqual(len(article.comment_list()), 0) # comment_list()应为自定义方法,返回启用的评论
# 8. 模拟审核通过:调用辅助方法,将该文章下所有评论设为“启用”
=======
article.title = "nicetitleccc" article.title = "nicetitleccc"
article.body = "nicecontentccc" article.body = "nicecontentccc"
article.author = self.user article.author = self.user
@ -168,14 +75,9 @@ class CommentsTest(TransactionTestCase):
self.assertEqual(len(article.comment_list()), 0) # jyn: 假设comment_list()返回启用的评论 self.assertEqual(len(article.comment_list()), 0) # jyn: 假设comment_list()返回启用的评论
# 模拟审核通过,再次验证评论数量 # 模拟审核通过,再次验证评论数量
>>>>>>> JYN_branch
self.update_article_comment_status(article) self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1) # jyn: 审核后应显示1条评论 self.assertEqual(len(article.comment_list()), 1) # jyn: 审核后应显示1条评论
<<<<<<< HEAD
# 9. 验证“审核后评论显示”再次检查评论列表长度是否为1审核通过后应显示
self.assertEqual(len(article.comment_list()), 1)
=======
# 5. 测试再次提交相同内容评论(验证重复提交允许性) # 5. 测试再次提交相同内容评论(验证重复提交允许性)
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
@ -219,5 +121,4 @@ class CommentsTest(TransactionTestCase):
self.assertIsNotNone(s) # jyn: 验证返回结果非空 self.assertIsNotNone(s) # jyn: 验证返回结果非空
# 10. 测试评论邮件通知(验证邮件发送功能) # 10. 测试评论邮件通知(验证邮件发送功能)
from comments.utils import send_comment_email from comments.utils import send_comment_email
send_comment_email(comment) # jyn: 调用工具函数发送评论通知邮件(无返回值,验证无报错) send_comment_email(comment) # jyn: 调用工具函数发送评论通知邮件(无返回值,验证无报错)
>>>>>>> JYN_branch

@ -1,33 +1,3 @@
<<<<<<< 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()方法(类视图需转为视图函数)
# 该视图负责处理评论提交的业务逻辑(如数据验证、保存评论等)
views.CommentPostView.as_view(),
# URL名称用于反向解析如在模板或代码中通过name='postcomment'生成URL
name='postcomment'
),
]
=======
# jyn: 评论应用comments的URL配置模块定义评论相关接口的路由映射 # jyn: 评论应用comments的URL配置模块定义评论相关接口的路由映射
from django.urls import path # jyn: Django URL路径匹配核心函数用于定义路由规则 from django.urls import path # jyn: Django URL路径匹配核心函数用于定义路由规则
from . import views # jyn: 导入当前应用的视图模块,关联路由与视图逻辑 from . import views # jyn: 导入当前应用的视图模块,关联路由与视图逻辑
@ -42,4 +12,3 @@ urlpatterns = [
name='postcomment' # jyn: 路由名称用于reverse反向解析URL name='postcomment' # jyn: 路由名称用于reverse反向解析URL
), ),
] ]
>>>>>>> JYN_branch

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

@ -1,38 +1,3 @@
<<<<<<< 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 .models import Comment
class CommentPostView(FormView):
"""
评论提交的类视图继承FormView处理评论表单的展示验证和数据保存
核心功能接收用户提交的评论数据验证合法性后保存到数据库支持评论回复
"""
# 1. 类视图基础配置
form_class = CommentForm # 指定关联的表单类使用CommentForm处理提交数据
template_name = 'blog/article_detail.html' # 指定表单渲染的模板:评论在文章详情页提交,故用文章详情模板
=======
# jyn: 评论功能核心视图模块,处理评论提交、数据验证、权限判断及页面跳转逻辑 # jyn: 评论功能核心视图模块,处理评论提交、数据验证、权限判断及页面跳转逻辑
from django.core.exceptions import ValidationError # jyn: Django 数据验证异常类,用于抛出评论相关错误 from django.core.exceptions import ValidationError # jyn: Django 数据验证异常类,用于抛出评论相关错误
from django.http import HttpResponseRedirect # jyn: 重定向响应类,评论提交后跳转至指定页面 from django.http import HttpResponseRedirect # jyn: 重定向响应类,评论提交后跳转至指定页面
@ -51,109 +16,30 @@ class CommentPostView(FormView):
"""jyn: 评论提交处理视图类继承FormView实现表单验证、数据保存及页面跳转""" """jyn: 评论提交处理视图类继承FormView实现表单验证、数据保存及页面跳转"""
form_class = CommentForm # jyn: 指定关联的表单类,用于前端提交数据的验证 form_class = CommentForm # jyn: 指定关联的表单类,用于前端提交数据的验证
template_name = 'blog/article_detail.html' # jyn: 表单验证失败时渲染的模板(文章详情页) template_name = 'blog/article_detail.html' # jyn: 表单验证失败时渲染的模板(文章详情页)
>>>>>>> JYN_branch
# 2. 给dispatch方法添加CSRF保护所有请求GET/POST都经过CSRF验证
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
<<<<<<< HEAD
"""
类视图的请求入口方法所有请求都会先经过此方法
作用调用父类的dispatch逻辑同时应用CSRF保护
"""
=======
"""jyn: 重写dispatch方法添加CSRF防护所有请求先经过该方法分发""" """jyn: 重写dispatch方法添加CSRF防护所有请求先经过该方法分发"""
>>>>>>> JYN_branch
return super(CommentPostView, self).dispatch(*args, **kwargs) return super(CommentPostView, self).dispatch(*args, **kwargs)
# 3. 处理GET请求当用户以GET方式访问该视图时触发
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
<<<<<<< HEAD
"""
GET请求逻辑不处理表单提交直接重定向到对应的文章详情页的评论区
避免用户直接通过URL以GET方式访问该视图时出现异常
"""
# 从URL路径参数中获取文章IDkwargs对应URL中的<int:article_id>
article_id = self.kwargs['article_id']
# 查询对应的文章找不到则返回404
article = get_object_or_404(Article, pk=article_id)
# 获取文章详情页的绝对URL并拼接评论区锚点#comments跳转到页面评论区域
url = article.get_absolute_url()
# 重定向到文章详情页的评论区
return HttpResponseRedirect(url + "#comments")
=======
"""jyn: 处理GET请求直接重定向到文章详情页的评论区""" """jyn: 处理GET请求直接重定向到文章详情页的评论区"""
article_id = self.kwargs['article_id'] # jyn: 从URL参数中获取文章ID article_id = self.kwargs['article_id'] # jyn: 从URL参数中获取文章ID
article = get_object_or_404(Article, pk=article_id) # jyn: 查询文章不存在则返回404 article = get_object_or_404(Article, pk=article_id) # jyn: 查询文章不存在则返回404
url = article.get_absolute_url() # jyn: 获取文章的绝对URL url = article.get_absolute_url() # jyn: 获取文章的绝对URL
return HttpResponseRedirect(url + "#comments") # jyn: 重定向到文章评论区锚点 return HttpResponseRedirect(url + "#comments") # jyn: 重定向到文章评论区锚点
>>>>>>> JYN_branch
# 4. 处理表单验证失败的逻辑当form.is_valid()为False时触发
def form_invalid(self, form): def form_invalid(self, form):
<<<<<<< HEAD
"""
表单数据验证失败如评论内容为空格式错误时的处理
作用重新渲染文章详情页带上错误的表单对象前端显示错误提示
"""
# 获取URL中的文章ID查询对应的文章
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
# 渲染模板传递错误的表单对象form和文章对象article前端可显示错误信息
=======
"""jyn: 表单数据验证失败时的处理逻辑,返回文章详情页并携带错误表单""" """jyn: 表单数据验证失败时的处理逻辑,返回文章详情页并携带错误表单"""
article_id = self.kwargs['article_id'] # jyn: 从URL参数获取文章ID article_id = self.kwargs['article_id'] # jyn: 从URL参数获取文章ID
article = get_object_or_404(Article, pk=article_id) # jyn: 查询目标文章 article = get_object_or_404(Article, pk=article_id) # jyn: 查询目标文章
# jyn: 渲染文章详情页,传递错误表单和文章实例(前端显示错误信息) # jyn: 渲染文章详情页,传递错误表单和文章实例(前端显示错误信息)
>>>>>>> JYN_branch
return self.render_to_response({ return self.render_to_response({
'form': form, # 带有错误信息的表单 'form': form,
'article': article # 当前文章对象(用于渲染文章详情) 'article': article
}) })
# 5. 处理表单验证成功的逻辑当form.is_valid()为True时触发核心业务逻辑
def form_valid(self, form): def form_valid(self, form):
<<<<<<< HEAD
"""提交的数据验证合法后的逻辑:保存评论数据到数据库,处理评论状态和回复关联"""
# 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: 表单数据验证合法后的核心逻辑,保存评论数据并跳转""" """jyn: 表单数据验证合法后的核心逻辑,保存评论数据并跳转"""
user = self.request.user # jyn: 获取当前登录用户 user = self.request.user # jyn: 获取当前登录用户
author = BlogUser.objects.get(pk=user.pk) # jyn: 通过用户ID查询BlogUser实例评论作者 author = BlogUser.objects.get(pk=user.pk) # jyn: 通过用户ID查询BlogUser实例评论作者
@ -188,4 +74,3 @@ class CommentPostView(FormView):
return HttpResponseRedirect( return HttpResponseRedirect(
"%s#div-comment-%d" % "%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk)) (article.get_absolute_url(), comment.pk))
>>>>>>> JYN_branch

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

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

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

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

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

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

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

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

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

@ -1,32 +1,19 @@
# ZYY: 导入Python标准库os模块用于处理文件和目录路径
import os import os
# ZYY: 导入Python标准库logging模块用于记录插件加载过程中的日志信息
import logging import logging
# ZYY: 导入Django配置模块用于访问Django项目的设置参数
from django.conf import settings from django.conf import settings
# ZYY: 获取当前模块的日志记录器,用于记录插件加载的调试和错误信息
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ZYY: 定义插件加载函数用于动态加载和初始化Django插件
def load_plugins(): def load_plugins():
""" """
ZYY: 动态加载并初始化位于'plugins'目录中的插件 Dynamically loads and initializes plugins from the 'plugins' directory.
ZYY: 该函数应在Django应用注册表就绪后调用以确保所有依赖项可用 This function is intended to be called when the Django app registry is ready.
ZYY: 加载过程会读取settings.ACTIVE_PLUGINS配置并尝试加载每个指定的插件
""" """
# ZYY: 遍历settings.ACTIVE_PLUGINS中配置的所有活跃插件名称
for plugin_name in settings.ACTIVE_PLUGINS: for plugin_name in settings.ACTIVE_PLUGINS:
# ZYY: 构建插件的完整路径(目录路径)
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) 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')): if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try: try:
# ZYY: 动态导入插件模块格式plugins.<plugin_name>.plugin
__import__(f'plugins.{plugin_name}.plugin') __import__(f'plugins.{plugin_name}.plugin')
# ZYY: 记录成功加载插件的日志信息
logger.info(f"Successfully loaded plugin: {plugin_name}") logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e: 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 os
import sys import sys
from pathlib import Path from pathlib import Path
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# ZYY 将环境变量字符串转换为布尔值
def env_to_bool(env, default): def env_to_bool(env, default):
str_val = os.environ.get(env) str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True' 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 BASE_DIR = Path(__file__).resolve().parent.parent
# ZYY 快速启动开发设置 - 不适用于生产环境 # Quick-start development settings - unsuitable for production
# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # See 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'
# 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 = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False # DEBUG = False
# ZYY 检测是否为测试环境
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ZYY 允许的主机列表
# ALLOWED_HOSTS = [] # ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# django 4.0新增配置
# ZYY Django 4.0新增配置: CSRF信任来源
CSRF_TRUSTED_ORIGINS = ['http://example.com'] CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
# ZYY 应用定义
INSTALLED_APPS = [ INSTALLED_APPS = [
# 'django.contrib.admin', # 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig', 'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth', 'django.contrib.auth',
'django. contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.sitemaps', 'django.contrib.sitemaps',
'mdeditor', 'mdeditor',
'haystack', 'haystack',
'blog', 'blog',
'accounts', 'accounts',
'comments', 'comments',
'oauth', 'oauth',
' servermanager', 'servermanager',
'owntracks', 'owntracks',
'compressor', 'compressor',
'djangoblog' 'djangoblog'
] ]
# ZYY 中间件配置
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.gzip.GZipMiddleware', 'django.middleware.locale.LocaleMiddleware',
# 'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.gzip.GZipMiddleware',
'django.middleware.common.CommonMiddleware', # 'django.middleware.cache.UpdateCacheMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # 'django.middleware.cache.FetchFromCacheMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib. messages.middleware.MessageMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.http.ConditionalGetMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'blog.middleware.OnlineMiddleware' 'django.middleware.http.ConditionalGetMiddleware',
'blog.middleware.OnlineMiddleware'
] ]
# ZYY 根URL配置
ROOT_URLCONF = 'djangoblog.urls' ROOT_URLCONF = 'djangoblog.urls'
# ZYY 模板配置
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR , 'templates')], 'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
'django.template.context_processors.debug', 'django.template.context_processors.debug',
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor' 'blog.context_processors.seo_processor'
], ],
}, },
}, },
] ]
# ZYY WSGI应用配置
WSGI_APPLICATION = 'djangoblog.wsgi.application' WSGI_APPLICATION = 'djangoblog.wsgi.application'
# ZYY 数据库配置 # Database
# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/ref/settings/#databases # https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': ' django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int( 'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306), os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': { 'OPTIONS': {
'charset': 'utf8mb4'}, 'charset': 'utf8mb4'},
}} }}
# ZYY 密码验证配置 # Password validation
# ZYY 参考链接: https://docs.djang oproject.com/en/1.10/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
}, },
] ]
# ZYY 语言配置
LANGUAGES = ( LANGUAGES = (
('en', _('English')), ('en', _('English')),
('zh-hans', _('Simplified Chinese')), ('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')), ('zh-hant', _('Traditional Chinese')),
) )
LOCALE_PATHS = ( LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'), os.path.join(BASE_DIR, 'locale'),
) )
LANGUAGE_CODE = 'zh-hans' LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai' TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = False USE_TZ = False
# ZYY 静态文件配置(CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/howto /static-files/ # https://docs.djangoproject.com/en/1.10/howto/static-files/
HAYSTACK_CONNECTIONS = { HAYSTACK_CONNECTIONS = {
'default': { 'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
}, },
} }
# Automatically update searching index
# ZYY 自动更新搜索索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password
# ZYY 允许用户使用用户名和密码登录
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend'] 'accounts.user_login_backend.EmailOrUsernameModelBackend']
# ZYY 静态文件根目录
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static') STATICFILES = os.path.join(BASE_DIR, 'static')
# ZYY 自定义用户模型
AUTH_USER_MODEL = 'accounts.BlogUser' AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/' LOGIN_URL = '/login/'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S' TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d' DATE_TIME_FORMAT = '%Y-%m-%d'
# ZYY Bootstrap颜色样式 # bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [ BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger' 'default', 'primary', 'success', 'info', 'warning', 'danger'
] ]
# ZYY 分页配置 # paginate
PAGINATE_BY = 10 PAGINATE_BY = 10
# http cache timeout
# ZYY HTTP缓存超时时间
CACHE_CONTROL_MAX_AGE = 2592000 CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
# ZYY 缓存配置
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core .cache.backends.locmem.LocMemCache', 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800, 'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake', 'LOCATION': 'unique-snowflake',
} }
} }
# 使用redis作为缓存
# ZYY 使用redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"): if os.environ.get("DJANGO_REDIS_URL"):
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
} }
} }
SITE_ID = 1 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配置 # Email:
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_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) 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_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER SERVER_EMAIL = EMAIL_HOST_USER
# Setting debug=false did NOT handle except email notifications
# ZYY 设置debug=false时不会处理异常邮件通知
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# WX ADMIN password(Two times md5)
# ZYY 微信管理员密码(两次md5 加密)
WXADMIN = os.environ.get( WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
# ZYY 日志目录配置
LOG_PATH = os.path.join(BASE_DIR, 'logs') LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH): if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True) os.makedirs(LOG_PATH, exist_ok=True)
# ZYY 日志配置
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
'root': { 'root': {
'level': 'INFO', 'level': 'INFO',
'handlers': ['console', 'log_file'], 'handlers': ['console', 'log_file'],
}, },
'formatters': { 'formatters': {
'verbose': { 'verbose': {
'format': '[%( asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
} }
}, },
'filters': { 'filters': {
'require_debug_false': { 'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse', '()': 'django.utils.log.RequireDebugFalse',
}, },
'require_debug_true': { 'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue', '()': 'django.utils.log.RequireDebugTrue',
}, },
}, },
'handlers': { 'handlers': {
'log_file': { 'log_file': {
'level': 'INFO', 'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler', 'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'djangoblog.log'), 'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D', 'when': 'D',
'formatter': 'verbose', 'formatter': 'verbose',
'interval': 1, 'interval': 1,
'delay': True , 'delay': True,
'backupCount': 5, 'backupCount': 5,
'encoding': 'utf-8' 'encoding': 'utf-8'
}, },
'console': { 'console': {
'level': 'DEBUG', 'level': 'DEBUG',
'filters': ['require_debug_true'], 'filters': ['require_debug_true'],
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'verbose' 'formatter': 'verbose'
}, },
'null': { 'null': {
'class': 'logging.NullHandler', 'class': 'logging.NullHandler',
}, },
'mail_admins': { 'mail_admins': {
'level': 'ERROR', 'level': 'ERROR',
'filters': ['require_debug_false'], 'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler' 'class': 'django.utils.log.AdminEmailHandler'
} }
}, },
'loggers': { 'loggers': {
'djangoblog': { 'djangoblog': {
'handlers': ['log_file', 'console'], 'handlers': ['log_file', 'console'],
'level': 'INFO', 'level': 'INFO',
'propagate': True, 'propagate': True,
}, },
'django.request': { 'django.request': {
'handlers': ['mail_admins'], 'handlers': ['mail_admins'],
'level': 'ERROR', 'level': 'ERROR',
'propagate': False, 'propagate': False,
} }
} }
} }
# ZYY 静态文件查找器配置
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other # other
'compressor.finders.CompressorFinder', 'compressor.finders.CompressorFinder',
) )
# ZYY 压缩配置
COMPRESS_ENABLED = True COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True # COMPRESS_OFFLINE = True
COMPRESS_CSS_FILTERS = [ COMPRESS_CSS_FILTERS = [
# 创建绝对URL # creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter', 'compressor.filters.css_default.CssAbsoluteFilter',
# CSS压缩 # css minimizer
'compressor.filters.cssmin.CSSMinFilter' 'compressor.filters.cssmin.CSSMinFilter'
] ]
COMPRESS_JS_FILTERS = [ COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter' 'compressor.filters.jsmin.JSMinFilter'
] ]
# ZYY 媒体文件配置
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
# ZYY X-Frame-Options配置
X_FRAME_OPTIONS = 'SAMEORIGIN' X_FRAME_OPTIONS = 'SAMEORIGIN'
# ZYY 默认自增字段类型
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# ZYY Elasticsearch配置
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = { ELASTICSEARCH_DSL = {
'default': { 'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_ HOST') 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
}, },
} }
HAYSTACK_CONNECTIONS = { HAYSTACK_CONNECTIONS = {
'default': { 'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
}, },
} }
# ZYY 插件系统配置 # Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins' PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [ ACTIVE_PLUGINS = [
'article_copyright', 'article_copyright',
'reading_time', 'reading_time',
' external_links', 'external_links',
'view_count', 'view_count',
'seo_optimizer' 'seo_optimizer'
] ]

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

@ -1,32 +1,21 @@
# ZYY 导入日志模块,用于记录程序运行状态
import logging import logging
# ZYY 导入requests库用于发送HTTP请求
import requests import requests
# ZYY 导入Django配置用于获取项目设置
from django.conf import settings from django.conf import settings
# ZYY 获取当前模块的日志记录器
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ZYY 定义蜘蛛通知类用于向搜索引擎提交URL通知
class SpiderNotify(): class SpiderNotify():
@staticmethod @staticmethod
# ZYY 静态方法用于通知百度蜘蛛抓取指定URL列表
def baidu_notify(urls): def baidu_notify(urls):
try: try:
# ZYY 将URL列表拼接成字符串每个URL占一行
data = '\n'.join(urls) data = '\n'.join(urls)
# ZYY 向百度推送接口发送POST请求提交URL数据
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
# ZYY 记录百度接口返回的结果(成功响应)
logger.info(result.text) logger.info(result.text)
except Exception as e: except Exception as e:
# ZYY 捕获并记录所有异常(如网络错误、接口错误等)
logger.error(e) logger.error(e)
@staticmethod @staticmethod
# ZYY 静态方法,提供统一的通知接口(目前仅调用百度通知)
def notify(url): 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): class DjangoBlogTest(TestCase):
def setUp(self): def setUp(self):
# ZYY: 测试用例初始化方法,当前为空实现
pass pass
def test_utils(self): def test_utils(self):
# ZYY: 测试utils模块中的工具函数
# ZYY: 测试get_sha256函数验证字符串'test'的sha256哈希值不为空
md5 = get_sha256('test') md5 = get_sha256('test')
self.assertIsNotNone(md5) self.assertIsNotNone(md5)
# ZYY: 测试CommonMarkdown类的get_markdown方法
# ZYY: 输入包含标题、代码块和链接的Markdown文本验证输出不为空
c = CommonMarkdown.get_markdown(''' c = CommonMarkdown.get_markdown('''
# Title1 # Title1
@ -31,12 +24,9 @@ class DjangoBlogTest(TestCase):
''') ''')
self.assertIsNotNone(c) self.assertIsNotNone(c)
# ZYY: 测试parse_dict_to_url函数
# ZYY: 创建测试字典验证字典转URL参数字符串的功能
d = { d = {
'd': 'key1', 'd': 'key1',
'd2': 'key2' 'd2': 'key2'
} }
data = parse_dict_to_url(d) 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 1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
""" """
# ZYY: 导入Django基础配置模块
from django.conf import settings from django.conf import settings
# ZYY: 导入国际化URL模式工具
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
# ZYY: 导入静态文件URL处理工具
from django.conf.urls.static import static from django.conf.urls.static import static
# ZYY: 导入站点地图视图
from django.contrib.sitemaps.views import sitemap from django.contrib.sitemaps.views import sitemap
# ZYY: 导入URL路径处理函数
from django.urls import path, include from django.urls import path, include
# ZYY: 导入正则表达式URL模式
from django.urls import re_path from django.urls import re_path
# ZYY: 导入Haystack搜索视图工厂
from haystack.views import search_view_factory from haystack.views import search_view_factory
# ZYY: 导入自定义视图和模块
from blog.views import EsSearchView from blog.views import EsSearchView
from djangoblog.admin_site import admin_site from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# ZYY: 定义站点地图配置,包含各类内容的站点地图
sitemaps = { sitemaps = {
'blog': ArticleSiteMap, # ZYY: 文章站点地图
'Category': CategorySiteMap, # ZYY: 分类站点地图 'blog': ArticleSiteMap,
'Tag': TagSiteMap, # ZYY: 标签站点地图 'Category': CategorySiteMap,
'User': UserSiteMap, # ZYY: 用户站点地图 'Tag': TagSiteMap,
'static': StaticViewSitemap # ZYY: 静态页面站点地图 'User': UserSiteMap,
'static': StaticViewSitemap
} }
# ZYY: 定义自定义错误处理视图 handler404 = 'blog.views.page_not_found_view'
handler404 = 'blog.views.page_not_found_view' # ZYY: 404错误处理 handler500 = 'blog.views.server_error_view'
handler500 = 'blog.views.server_error_view' # ZYY: 500错误处理 handle403 = 'blog.views.permission_denied_view'
handle403 = 'blog.views.permission_denied_view' # ZYY: 403错误处理
# ZYY: 基础URL模式配置
urlpatterns = [ urlpatterns = [
# ZYY: 国际化URL前缀处理
path('i18n/', include('django.conf.urls.i18n')), path('i18n/', include('django.conf.urls.i18n')),
] ]
# ZYY: 添加国际化URL模式
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
# ZYY: 管理后台URL使用自定义admin_site re_path(r'^admin/', admin_site.urls),
re_path(r'admin/', admin_site.urls),
# ZYY: 博客应用URL命名空间为blog
re_path(r'', include('blog.urls', namespace='blog')), re_path(r'', include('blog.urls', namespace='blog')),
# ZYY: Markdown编辑器URL
re_path(r'mdeditor/', include('mdeditor.urls')), re_path(r'mdeditor/', include('mdeditor.urls')),
# ZYY: 评论系统URL命名空间为comment
re_path(r'', include('comments.urls', namespace='comment')), re_path(r'', include('comments.urls', namespace='comment')),
# ZYY: 账户系统URL命名空间为account
re_path(r'', include('accounts.urls', namespace='account')), re_path(r'', include('accounts.urls', namespace='account')),
# ZYY: 第三方登录URL命名空间为oauth
re_path(r'', include('oauth.urls', namespace='oauth')), re_path(r'', include('oauth.urls', namespace='oauth')),
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
# ZYY: 站点地图URL生成sitemap.xml
re_path(r'sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'), name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()),
# ZYY: RSS订阅URL两种访问路径 re_path(r'^rss/$', DjangoBlogFeed()),
re_path(r'feed/$', DjangoBlogFeed()), re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
re_path(r'rss/$', DjangoBlogFeed()),
# ZYY: 搜索功能URL使用Elasticsearch搜索视图
re_path('search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'), name='search'),
# ZYY: 服务器管理URL命名空间为servermanager
re_path(r'', include('servermanager.urls', namespace='servermanager')), re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks'))
# ZYY: OwnTracks应用URL命名空间为owntracks , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
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模式下
if settings.DEBUG: if settings.DEBUG:
# ZYY: 添加媒体文件URL处理仅开发环境
urlpatterns += static(settings.MEDIA_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 string
import uuid import uuid
from hashlib import sha256 from hashlib import sha256
import bleach import bleach
import markdown import markdown
import requests import requests
@ -15,20 +16,21 @@ from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.cache import cache from django.core.cache import cache
from django.templatetags.static import static from django.templatetags.static import static
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ZYY: 获取最新的文章ID和评论ID
def get_max_articleid_commentid(): def get_max_articleid_commentid():
from blog.models import Article from blog.models import Article
from comments.models import Comment from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk) return (Article.objects.latest().pk, Comment.objects.latest().pk)
# ZYY: 获取字符串的SHA256哈希值
def get_sha256(str): def get_sha256(str):
m = sha256(str.encode('utf-8')) m = sha256(str.encode('utf-8'))
return m.hexdigest() return m.hexdigest()
# ZYY: 缓存装饰器 ,用于缓存函数结果
def cache_decorator(expiration=3 * 60): def cache_decorator(expiration=3 * 60):
def wrapper(func): def wrapper(func):
def news(*args, **kwargs): def news(*args, **kwargs):
@ -39,6 +41,7 @@ def cache_decorator(expiration=3 * 60):
key = None key = None
if not key: if not key:
unique_str = repr((func, args, kwargs)) unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8')) m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest() key = m.hexdigest()
value = cache.get(key) value = cache.get(key)
@ -50,7 +53,7 @@ def cache_decorator(expiration=3 * 60):
return value return value
else: else:
logger.debug( logger.debug(
'cache_decorator set cache:%s key:%s' % 'cache_decorator set cache:%s key:%s' %
(func.__name__, key)) (func.__name__, key))
value = func(*args, **kwargs) value = func(*args, **kwargs)
if value is None: if value is None:
@ -58,40 +61,44 @@ def cache_decorator(expiration=3 * 60):
else: else:
cache.set(key, value, expiration) cache.set(key, value, expiration)
return value return value
return news return news
return wrapper return wrapper
# ZYY: 刷新视图缓存
def expire_view_cache(path, servername, serverport, key_prefix=None): def expire_view_cache(path, servername, serverport, key_prefix=None):
''' '''
刷新视图缓存 刷新视图缓存
:param path:url路径 :param path:url路径
:param servername:host :param servername:host
:param serverport:端口 :param serverport:端口
: param key_prefix:前缀 :param key_prefix:前缀
:return:是否成功 :return:是否成功
''' '''
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.cache import get_cache_key from django.utils.cache import get_cache_key
request = HttpRequest() request = HttpRequest()
request.META = {'SERVER_NAME': servername , 'SERVER_PORT': serverport} request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path request.path = path
key = get_cache_key(request, key_prefix=key_prefix, cache=cache) key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key: if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path)) logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key): if cache.get(key):
cache.delete(key) cache.delete(key)
return True return True
return False return False
# ZYY: 使用缓存装饰器获取当前站点信息
@cache_decorator() @cache_decorator()
def get_current_site(): def get_current_site():
site = Site.objects.get_current () site = Site.objects.get_current()
return site return site
class CommonMarkdown: class CommonMarkdown:
# ZYY: 将Markdown文本转换为HTML并生成目录
@staticmethod @staticmethod
def _convert_markdown(value): def _convert_markdown(value):
md = markdown.Markdown( md = markdown.Markdown(
@ -106,19 +113,17 @@ class CommonMarkdown:
toc = md.toc toc = md.toc
return body, toc return body, toc
# ZYY: 获取带有目录的Markdown转换结果
@staticmethod @staticmethod
def get_markdown_with_toc(value): def get_markdown_with_toc(value):
body, toc = CommonMarkdown._convert_markdown(value) body, toc = CommonMarkdown._convert_markdown(value)
return body, toc return body, toc
# ZYY: 获取Markdown转换结果不带目录
@staticmethod @staticmethod
def get_markdown(value): def get_markdown(value):
body, toc = CommonMarkdown._convert_markdown(value) body, toc = CommonMarkdown._convert_markdown(value)
return body return body
# ZYY: 发送邮件
def send_email(emailto, title, content): def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal from djangoblog.blog_signals import send_email_signal
send_email_signal.send( send_email_signal.send(
@ -127,19 +132,19 @@ def send_email(emailto, title, content):
title=title, title=title,
content=content) content=content)
# ZYY: 生成随机数验证码
def generate_code() -> str: 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): def parse_dict_to_url(dict):
from urllib.parse import quote from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()]) for k, v in dict.items()])
return url return url
# ZYY: 获取博客设置,如果缓存中没有则从数据库加载并设置缓存
def get_blog_setting(): def get_blog_setting():
value = cache.get('get_blog_setting') value = cache.get('get_blog_setting')
if value: if value:
@ -158,7 +163,7 @@ def get_blog_setting():
setting.show_google_adsense = False setting.show_google_adsense = False
setting.open_site_comment = True setting.open_site_comment = True
setting.analytics_code = '' setting.analytics_code = ''
setting.beian_code = '' setting.beian_code = ''
setting.show_gongan_code = False setting.show_gongan_code = False
setting.comment_need_review = False setting.comment_need_review = False
setting.save() setting.save()
@ -167,7 +172,7 @@ def get_blog_setting():
cache.set('get_blog_setting', value) cache.set('get_blog_setting', value)
return value return value
# ZYY: 保存用户头像到本地
def save_user_avatar(url): def save_user_avatar(url):
''' '''
保存用户头像 保存用户头像
@ -175,12 +180,14 @@ def save_user_avatar(url):
:return: 本地路径 :return: 本地路径
''' '''
logger.info(url) logger.info(url)
try: try:
basedir = os.path.join(settings.STATICFILES, 'avatar') basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2) rsp = requests.get(url, timeout=2)
if rsp.status_code == 200: if rsp.status_code == 200:
if not os.path.exists(basedir): if not os.path.exists(basedir):
os.makedirs(basedir) os.makedirs(basedir)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif'] image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0 isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg' ext = os.path.splitext(url)[1] if isimage else '.jpg'
@ -188,12 +195,12 @@ def save_user_avatar(url):
logger.info('保存用户头像:' + basedir + save_filename) logger.info('保存用户头像:' + basedir + save_filename)
with open(os.path.join(basedir, save_filename), 'wb+') as file: with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content) file.write(rsp.content)
return static('avatar /' + save_filename) return static('avatar/' + save_filename)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
return static('blog/img/avatar.png') return static('blog/img/avatar.png')
# ZYY: 删除侧边栏缓存
def delete_sidebar_cache(): def delete_sidebar_cache():
from blog.models import LinkShowType from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values] keys = ["sidebar" + x for x in LinkShowType.values]
@ -201,13 +208,13 @@ def delete_sidebar_cache():
logger.info('delete sidebar key:' + k) logger.info('delete sidebar key:' + k)
cache.delete(k) cache.delete(k)
# ZYY: 删除视图缓存
def delete_view_cache(prefix, keys): def delete_view_cache(prefix, keys):
from django.core.cache.utils import make_template_fragment_key from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys) key = make_template_fragment_key(prefix, keys)
cache.delete(key) cache.delete(key)
# ZYY: 获取静态资源URL
def get_resource_url(): def get_resource_url():
if settings.STATIC_URL: if settings.STATIC_URL:
return settings.STATIC_URL return settings.STATIC_URL
@ -215,11 +222,11 @@ def get_resource_url():
site = get_current_site() site = get_current_site()
return 'http://' + site.domain + '/static/' return 'http://' + site.domain + '/static/'
# ZYY: 允许的HTML标签和属性
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p'] 'h2', 'p']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
# ZYY: 清理HTML只保留允许的标签和属性
def sanitize_html(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 # encoding: utf-8
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
# ZYY: 导入必要的模块和函数
import json import json
import os import os
import re import re
@ -34,14 +34,12 @@ from whoosh.qparser import QueryParser
from whoosh.searching import ResultsPage from whoosh.searching import ResultsPage
from whoosh.writing import AsyncWriter from whoosh.writing import AsyncWriter
# ZYY: 尝试导入whoosh模块如果失败则抛出异常
try: try:
import whoosh import whoosh
except ImportError: except ImportError:
raise MissingDependency( raise MissingDependency(
"The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
# ZYY: 检查whoosh版本是否满足要求
# Handle minimum requirement. # Handle minimum requirement.
if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
raise MissingDependency( raise MissingDependency(
@ -54,20 +52,16 @@ DATETIME_REGEX = re.compile(
LOCALS = threading.local() LOCALS = threading.local()
LOCALS.RAM_STORE = None LOCALS.RAM_STORE = None
# ZYY: 自定义的HtmlFormatter类用于在whoosh中统一结果格式
class WhooshHtmlFormatter(HtmlFormatter): class WhooshHtmlFormatter(HtmlFormatter):
""" """
This is a HtmlFormatter simpler than the whoosh.HtmlFormatter. This is a HtmlFormatter simpler than the whoosh.HtmlFormatter.
We use it to have consistent results across backends. Specifically, We use it to have consistent results across backends. Specifically,
Solr, Xapian and Elasticsearch are using this formatting. Solr, Xapian and Elasticsearch are using this formatting.
""" """
"""
ZYY: 这是一个比whoosh.HtmlFormatter更简单的HtmlFormatter
我们使用它来确保不同后端如Solr, Xapian和Elasticsearch之间的结果一致性
"""
template = '<%(tag)s>%(t)s</%(tag)s>' template = '<%(tag)s>%(t)s</%(tag)s>'
# ZYY: Whoosh搜索后端类
class WhooshSearchBackend(BaseSearchBackend): class WhooshSearchBackend(BaseSearchBackend):
# Word reserved by Whoosh for special use. # Word reserved by Whoosh for special use.
RESERVED_WORDS = ( RESERVED_WORDS = (
@ -76,7 +70,7 @@ class WhooshSearchBackend(BaseSearchBackend):
'OR', 'OR',
'TO', 'TO',
) )
# ZYY: Whoosh保留的特殊用途字符
# Characters reserved by Whoosh for special use. # Characters reserved by Whoosh for special use.
# The '\\' must come first, so as not to overwrite the other slash # The '\\' must come first, so as not to overwrite the other slash
# replacements. # replacements.
@ -86,7 +80,6 @@ class WhooshSearchBackend(BaseSearchBackend):
) )
def __init__(self, connection_alias, **connection_options): def __init__(self, connection_alias, **connection_options):
# ZYY: 初始化Whoosh搜索后端
super( super(
WhooshSearchBackend, WhooshSearchBackend,
self).__init__( self).__init__(
@ -114,13 +107,9 @@ class WhooshSearchBackend(BaseSearchBackend):
""" """
Defers loading until needed. Defers loading until needed.
""" """
"""
ZYY: 延迟加载直到需要时才加载
"""
from haystack import connections from haystack import connections
new_index = False new_index = False
# ZYY: 确保索引存在
# Make sure the index is there. # Make sure the index is there.
if self.use_file_storage and not os.path.exists(self.path): if self.use_file_storage and not os.path.exists(self.path):
os.makedirs(self.path) os.makedirs(self.path)
@ -155,14 +144,12 @@ class WhooshSearchBackend(BaseSearchBackend):
self.setup_complete = True self.setup_complete = True
# ZYY: 构建索引模式
def build_schema(self, fields): def build_schema(self, fields):
schema_fields = { schema_fields = {
ID: WHOOSH_ID(stored=True, unique=True), ID: WHOOSH_ID(stored=True, unique=True),
DJANGO_CT: WHOOSH_ID(stored=True), DJANGO_CT: WHOOSH_ID(stored=True),
DJANGO_ID: WHOOSH_ID(stored=True), DJANGO_ID: WHOOSH_ID(stored=True),
} }
# ZYY: 获取Haystack中硬编码的键数量用于后续的错误处理
# Grab the number of keys that are hard-coded into Haystack. # Grab the number of keys that are hard-coded into Haystack.
# We'll use this to (possibly) fail slightly more gracefully later. # We'll use this to (possibly) fail slightly more gracefully later.
initial_key_count = len(schema_fields) initial_key_count = len(schema_fields)
@ -186,7 +173,6 @@ class WhooshSearchBackend(BaseSearchBackend):
schema_fields[field_class.index_fieldname] = NUMERIC( schema_fields[field_class.index_fieldname] = NUMERIC(
stored=field_class.stored, numtype=float, field_boost=field_class.boost) stored=field_class.stored, numtype=float, field_boost=field_class.boost)
elif field_class.field_type == 'boolean': elif field_class.field_type == 'boolean':
# ZYY: BOOLEAN字段不支持field_boost
# Field boost isn't supported on BOOLEAN as of 1.8.2. # Field boost isn't supported on BOOLEAN as of 1.8.2.
schema_fields[field_class.index_fieldname] = BOOLEAN( schema_fields[field_class.index_fieldname] = BOOLEAN(
stored=field_class.stored) stored=field_class.stored)
@ -198,14 +184,13 @@ class WhooshSearchBackend(BaseSearchBackend):
stored=field_class.stored, stored=field_class.stored,
field_boost=field_class.boost) field_boost=field_class.boost)
else: 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=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True)
schema_fields[field_class.index_fieldname] = TEXT( schema_fields[field_class.index_fieldname] = TEXT(
stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True) stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
if field_class.document is True: if field_class.document is True:
content_field_name = field_class.index_fieldname content_field_name = field_class.index_fieldname
schema_fields[field_class.index_fieldname].spelling = True schema_fields[field_class.index_fieldname].spelling = True
# ZYY: 如果没有找到任何字段,则抛出错误
# Fail more gracefully than relying on the backend to die if no fields # Fail more gracefully than relying on the backend to die if no fields
# are found. # are found.
if len(schema_fields) <= initial_key_count: 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.") "No fields were found in any search_indexes. Please correct this before attempting to search.")
return (content_field_name, Schema(**schema_fields)) return (content_field_name, Schema(**schema_fields))
# ZYY: 更新索引
def update(self, index, iterable, commit=True): def update(self, index, iterable, commit=True):
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
@ -227,13 +212,11 @@ class WhooshSearchBackend(BaseSearchBackend):
except SkipDocument: except SkipDocument:
self.log.debug(u"Indexing for object `%s` skipped", obj) self.log.debug(u"Indexing for object `%s` skipped", obj)
else: else:
# ZYY: 确保所有数据都是unicode格式
# Really make sure it's unicode, because Whoosh won't have it any # Really make sure it's unicode, because Whoosh won't have it any
# other way. # other way.
for key in doc: for key in doc:
doc[key] = self._from_python(doc[key]) doc[key] = self._from_python(doc[key])
# ZYY: Whoosh 2.5.0+不支持文档boost
# Document boosts aren't supported in Whoosh 2.5.0+. # Document boosts aren't supported in Whoosh 2.5.0+.
if 'boost' in doc: if 'boost' in doc:
del doc['boost'] del doc['boost']
@ -244,7 +227,6 @@ class WhooshSearchBackend(BaseSearchBackend):
if not self.silently_fail: if not self.silently_fail:
raise raise
# ZYY: 记录错误信息
# We'll log the object identifier but won't include the actual object # We'll log the object identifier but won't include the actual object
# to avoid the possibility of that generating encoding errors while # to avoid the possibility of that generating encoding errors while
# processing the log message: # processing the log message:
@ -258,12 +240,10 @@ class WhooshSearchBackend(BaseSearchBackend):
"object": get_identifier(obj)}}) "object": get_identifier(obj)}})
if len(iterable) > 0: if len(iterable) > 0:
# ZYY: 无论如何都提交更改,以避免锁定问题
# For now, commit no matter what, as we run into locking issues # For now, commit no matter what, as we run into locking issues
# otherwise. # otherwise.
writer.commit() writer.commit()
# ZYY: 从索引中删除文档
def remove(self, obj_or_string, commit=True): def remove(self, obj_or_string, commit=True):
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
@ -286,7 +266,6 @@ class WhooshSearchBackend(BaseSearchBackend):
e, e,
exc_info=True) exc_info=True)
# ZYY: 清空索引
def clear(self, models=None, commit=True): def clear(self, models=None, commit=True):
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
@ -323,9 +302,8 @@ class WhooshSearchBackend(BaseSearchBackend):
else: else:
self.log.error( self.log.error(
"Failed to clear Whoosh index: %s", e, exc_info=True) "Failed to clear Whoosh index: %s", e, exc_info=True)
# ZYY: 删除索引文件
def delete_index(self): def delete_index(self):
# ZYY: 根据Whoosh邮件列表的建议如果要从索引中删除所有内容更高效的方法是直接删除索引文件
# Per the Whoosh mailing list, if wiping out everything from the index, # Per the Whoosh mailing list, if wiping out everything from the index,
# it's much more efficient to simply delete the index files. # it's much more efficient to simply delete the index files.
if self.use_file_storage and os.path.exists(self.path): if self.use_file_storage and os.path.exists(self.path):
@ -333,11 +311,9 @@ class WhooshSearchBackend(BaseSearchBackend):
elif not self.use_file_storage: elif not self.use_file_storage:
self.storage.clean() self.storage.clean()
# ZYY: 重新创建索引
# Recreate everything. # Recreate everything.
self.setup() self.setup()
# ZYY: 优化索引
def optimize(self): def optimize(self):
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
@ -345,15 +321,12 @@ class WhooshSearchBackend(BaseSearchBackend):
self.index = self.index.refresh() self.index = self.index.refresh()
self.index.optimize() self.index.optimize()
# ZYY: 计算分页
def calculate_page(self, start_offset=0, end_offset=None): def calculate_page(self, start_offset=0, end_offset=None):
# Prevent against Whoosh throwing an error. Requires an end_offset # Prevent against Whoosh throwing an error. Requires an end_offset
# greater than 0. # greater than 0.
# ZYY: 防止Whoosh抛出错误要求end_offset大于0
if end_offset is not None and end_offset <= 0: if end_offset is not None and end_offset <= 0:
end_offset = 1 end_offset = 1
# ZYY: 确定页码
# Determine the page. # Determine the page.
page_num = 0 page_num = 0
@ -368,7 +341,6 @@ class WhooshSearchBackend(BaseSearchBackend):
if page_length and page_length > 0: if page_length and page_length > 0:
page_num = int(start_offset / page_length) page_num = int(start_offset / page_length)
# ZYY: Whoosh使用1-based的页码
# Increment because Whoosh uses 1-based page numbers. # Increment because Whoosh uses 1-based page numbers.
page_num += 1 page_num += 1
return page_num, page_length return page_num, page_length
@ -396,8 +368,7 @@ class WhooshSearchBackend(BaseSearchBackend):
**kwargs): **kwargs):
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
# ZYY: 空查询应该返回无结果
# A zero length query should return no results. # A zero length query should return no results.
if len(query_string) == 0: if len(query_string) == 0:
return { return {
@ -406,7 +377,7 @@ class WhooshSearchBackend(BaseSearchBackend):
} }
query_string = force_str(query_string) query_string = force_str(query_string)
# ZYY: 单字符查询(非通配符)会被停用词过滤器过滤掉,应该返回无结果
# A one-character query (non-wildcard) gets nabbed by a stopwords # A one-character query (non-wildcard) gets nabbed by a stopwords
# filter and should yield zero results. # filter and should yield zero results.
if len(query_string) <= 1 and query_string != u'*': if len(query_string) <= 1 and query_string != u'*':
@ -418,7 +389,6 @@ class WhooshSearchBackend(BaseSearchBackend):
reverse = False reverse = False
if sort_by is not None: if sort_by is not None:
# ZYY: 确定是否需要反转结果以及Whoosh是否能处理排序要求
# Determine if we need to reverse the results and if Whoosh can # Determine if we need to reverse the results and if Whoosh can
# handle what it's being asked to sort by. Reversing is an # handle what it's being asked to sort by. Reversing is an
# all-or-nothing action, unfortunately. # all-or-nothing action, unfortunately.
@ -475,7 +445,6 @@ class WhooshSearchBackend(BaseSearchBackend):
if models and len(models): if models and len(models):
model_choices = sorted(get_model_ct(model) for model in models) model_choices = sorted(get_model_ct(model) for model in models)
elif limit_to_registered_models: elif limit_to_registered_models:
# ZYY: 使用窄查询限制结果到当前路由器处理的模型
# Using narrow queries, limit the results to only models handled # Using narrow queries, limit the results to only models handled
# with the current routers. # with the current routers.
model_choices = self.build_models_list() model_choices = self.build_models_list()
@ -492,7 +461,6 @@ class WhooshSearchBackend(BaseSearchBackend):
narrow_searcher = None narrow_searcher = None
if narrow_queries is not None: if narrow_queries is not None:
# ZYY: 潜在的高开销操作
# Potentially expensive? I don't see another way to do it in # Potentially expensive? I don't see another way to do it in
# Whoosh... # Whoosh...
narrow_searcher = self.index.searcher() narrow_searcher = self.index.searcher()
@ -518,7 +486,6 @@ class WhooshSearchBackend(BaseSearchBackend):
searcher = self.index.searcher() searcher = self.index.searcher()
parsed_query = self.parser.parse(query_string) parsed_query = self.parser.parse(query_string)
# ZYY: 处理无效/停用词查询,优雅恢复
# In the event of an invalid/stopworded query, recover gracefully. # In the event of an invalid/stopworded query, recover gracefully.
if parsed_query is None: if parsed_query is None:
return { return {
@ -535,7 +502,6 @@ class WhooshSearchBackend(BaseSearchBackend):
'reverse': reverse, 'reverse': reverse,
} }
# ZYY: 处理已缩小范围的查询结果
# Handle the case where the results have been narrowed. # Handle the case where the results have been narrowed.
if narrowed_results is not None: if narrowed_results is not None:
search_kwargs['filter'] = narrowed_results search_kwargs['filter'] = narrowed_results
@ -555,7 +521,7 @@ class WhooshSearchBackend(BaseSearchBackend):
'hits': 0, 'hits': 0,
'spelling_suggestion': None, 'spelling_suggestion': None,
} }
# ZYY: Whoosh 2.5.1如果请求的页码过高,会返回错误的页码
# Because as of Whoosh 2.5.1, it will return the wrong page of # Because as of Whoosh 2.5.1, it will return the wrong page of
# results if you request something too high. :( # results if you request something too high. :(
if raw_page.pagenum < page_num: if raw_page.pagenum < page_num:
@ -593,7 +559,7 @@ class WhooshSearchBackend(BaseSearchBackend):
'hits': 0, 'hits': 0,
'spelling_suggestion': spelling_suggestion, 'spelling_suggestion': spelling_suggestion,
} }
# ZYY: 查找类似文档
def more_like_this( def more_like_this(
self, self,
model_instance, model_instance,
@ -606,7 +572,7 @@ class WhooshSearchBackend(BaseSearchBackend):
**kwargs): **kwargs):
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
# ZYY: 延迟模型将具有不同的类名("RealClass_Deferred_fieldname"),该类名不会在我们的注册表中
# Deferred models will have a different class ("RealClass_Deferred_fieldname") # Deferred models will have a different class ("RealClass_Deferred_fieldname")
# which won't be in our registry: # which won't be in our registry:
model_klass = model_instance._meta.concrete_model model_klass = model_instance._meta.concrete_model
@ -623,7 +589,6 @@ class WhooshSearchBackend(BaseSearchBackend):
if models and len(models): if models and len(models):
model_choices = sorted(get_model_ct(model) for model in models) model_choices = sorted(get_model_ct(model) for model in models)
elif limit_to_registered_models: elif limit_to_registered_models:
# ZYY: 使用窄查询限制结果到当前路由器处理的模型
# Using narrow queries, limit the results to only models handled # Using narrow queries, limit the results to only models handled
# with the current routers. # with the current routers.
model_choices = self.build_models_list() model_choices = self.build_models_list()
@ -643,7 +608,6 @@ class WhooshSearchBackend(BaseSearchBackend):
narrow_searcher = None narrow_searcher = None
if narrow_queries is not None: if narrow_queries is not None:
# ZYY: 潜在的高开销操作
# Potentially expensive? I don't see another way to do it in # Potentially expensive? I don't see another way to do it in
# Whoosh... # Whoosh...
narrow_searcher = self.index.searcher() narrow_searcher = self.index.searcher()
@ -678,11 +642,10 @@ class WhooshSearchBackend(BaseSearchBackend):
raw_results = results[0].more_like_this( raw_results = results[0].more_like_this(
field_name, top=end_offset) field_name, top=end_offset)
# ZYY: 处理已缩小范围的查询结果
# Handle the case where the results have been narrowed. # Handle the case where the results have been narrowed.
if narrowed_results is not None and hasattr(raw_results, 'filter'): if narrowed_results is not None and hasattr(raw_results, 'filter'):
raw_results.filter(narrowed_results) raw_results.filter(narrowed_results)
# ZYY: Whoosh 2.5.1如果请求的页码过高,会返回错误的页码
try: try:
raw_page = ResultsPage(raw_results, page_num, page_length) raw_page = ResultsPage(raw_results, page_num, page_length)
except ValueError: except ValueError:
@ -711,7 +674,7 @@ class WhooshSearchBackend(BaseSearchBackend):
narrow_searcher.close() narrow_searcher.close()
return results return results
# ZYY: 处理查询结果
def _process_results( def _process_results(
self, self,
raw_page, raw_page,
@ -721,153 +684,174 @@ class WhooshSearchBackend(BaseSearchBackend):
result_class=None): result_class=None):
from haystack import connections from haystack import connections
results = [] results = []
# ZYY: 获取命中数
# It's important to grab the hits first before slicing. Otherwise, this # It's important to grab the hits first before slicing. Otherwise, this
# can cause pagination failures. # can cause pagination failures.
hits = len(raw_page) hits = len(raw_page)
# ZYY: 如果result_class为None则将其设置为SearchResult if result_class is None:
if result_class is None: result_class = SearchResult
result_class = SearchResult
facets = {} facets = {}
spelling_suggestion = None spelling_suggestion = None
# ZYY: 获取统一索引 unified_index = connections[self.connection_alias].get_unified_index()
unified_index = connections[self.connection_alias].get_unified_index() indexed_models = unified_index.get_indexed_models()
# ZYY: 获取已索引的模型列表
indexed_models = unified_index.get_indexed_models() for doc_offset, raw_result in enumerate(raw_page):
for doc_offset, raw_result in enumerate(raw_page): score = raw_page.score(doc_offset) or 0
# ZYY: 获取文档得分如果没有则为0 app_label, model_name = raw_result[DJANGO_CT].split('.')
score = raw_page.score(doc_offset) or 0 additional_fields = {}
app_label, model_name = raw_result[DJANGO_CT].split('.') model = haystack_get_model(app_label, model_name)
additional_fields = {}
# ZYY: 根据app_label和model_name获取模型 if model and model in indexed_models:
model = haystack_get_model(app_label, model_name) for key, value in raw_result.items():
if model and model in indexed_models: index = unified_index.get_index(model)
for key, value in raw_result.items(): string_key = str(key)
index = unified_index.get_index(model)
string_key = str(key) if string_key in index.fields and hasattr(
# ZYY : 检查字段是否在索引中且具有convert方法特殊处理KEYWORD字段 index.fields[string_key], 'convert'):
if string_key in index.fields and hasattr( # Special-cased due to the nature of KEYWORD fields.
index.fields[string_key], 'convert'): if index.fields[string_key].is_multivalued:
# ZYY: 处理多值字段 if value is None or len(value) == 0:
if index.fields[string_key].is_multivalued: additional_fields[string_key] = []
if value is None or len(value) == 0: else:
additional_fields[string_key] = [] additional_fields[string_key] = value.split(
',')
else:
additional_fields[string_key] = index.fields[string_key].convert(
value)
else: else:
additional_fields[string_key] = value.split( additional_fields[string_key] = self._to_python(value)
',')
else: del (additional_fields[DJANGO_CT])
additional_fields[string_key] = index.fields[string_key].convert( del (additional_fields[DJANGO_ID])
value)
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: else:
additional_fields[string_key] = self._to_python(value) hits -= 1
del (additional_fields[DJANGO_CT])
del (additional_fields[DJANGO_ID]) if self.include_spelling:
if highlight: if spelling_query:
sa = StemmingAnalyzer() spelling_suggestion = self.create_spelling_suggestion(
formatter = WhooshHtmlFormatter('em') spelling_query)
terms = [token .text for token in sa(query_string)] else:
# ZYY: 使用Whoosh进行高亮显示 spelling_suggestion = self.create_spelling_suggestion(
whoosh_result = whoosh_highlight( query_string)
additional_fields.get(self.content_field_name),
terms, return {
sa, 'results': results,
ContextFragmenter(), 'hits': hits,
formatter 'facets': facets,
) 'spelling_suggestion': spelling_suggestion,
additional_fields['highlighted'] = { }
self.content_field_name: [whoosh_result],
} def create_spelling_suggestion(self, query_string):
result = result_class( spelling_suggestion = None
app_label, reader = self.index.reader()
model_name, corrector = reader.corrector(self.content_field_name)
raw_result[DJANGO_ID], cleaned_query = force_str(query_string)
score,
**additional_fields) if not query_string:
results.append(result) return spelling_suggestion
else:
hits -= 1 # Clean the string.
if self.include_spelling: for rev_word in self.RESERVED_WORDS:
if spelling_query: cleaned_query = cleaned_query.replace(rev_word, '')
# ZYY: 根据spelling_query创建拼写建议
spelling_suggestion = self.create_spelling_suggestion( for rev_char in self.RESERVED_CHARACTERS:
spelling_query) cleaned_query = cleaned_query.replace(rev_char, '')
else:
# ZYY: 根据query_string创建拼写建议 # Break it down.
spelling_suggestion = self.create_spelling_suggestion( query_words = cleaned_query.split()
query_string) suggested_words = []
return {
'results': results, for word in query_words:
'hits': hits, suggestions = corrector.suggest(word, limit=1)
'facets': facets,
'spelling_suggestion': spelling_suggestion, if len(suggestions) > 0:
} suggested_words.append(suggestions[0])
# ZYY: 创建拼写建议的方法 spelling_suggestion = ' '.join(suggested_words)
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 return spelling_suggestion
# ZYY: 清理字符串,移除保留字和保留字符
for rev_word in self.RESERVED_WORDS: def _from_python(self, value):
cleaned_query = cleaned_query.replace(rev_word, '') """
for rev_char in self.RESERVED_CHARACTERS: Converts Python values to a string for Whoosh.
cleaned_query = cleaned_query.replace(rev_char, '')
# ZYY: 分解查询词 Code courtesy of pysolr.
query_words = cleaned_query.split() """
suggested_words = [] if hasattr(value, 'strftime'):
for word in query_words: if not hasattr(value, 'hour'):
suggestions = corrector.suggest(word, limit=1) value = datetime(value.year, value.month, value.day, 0, 0, 0)
if len(suggestions) > 0: elif isinstance(value, bool):
suggested_words.append(suggestions[0]) if value:
spelling_suggestion = ' '.join(suggested_words) value = 'true'
return spelling_suggestion else:
value = 'false'
# ZYY: 将Python值转换为Whoosh使用的字符串 elif isinstance(value, (list, tuple)):
def _from_python(self, value): value = u','.join([force_str(v) for v in value])
if hasattr(value, 'strftime'): elif isinstance(value, (six.integer_types, float)):
if not hasattr(value, 'hour'): # Leave it alone.
value = datetime(value.year, value.month, value.day, 0, 0, 0) pass
elif isinstance(value, bool):
if value:
value = ' true'
else: else:
value = 'false' value = force_str(value)
elif isinstance(value, (list, tuple)): return value
value = u','.join([force_str(v) for v in value])
elif isinstance(value, (six.integer_types, float)): def _to_python(self, value):
# ZYY: 整数和浮点数保持不变 """
pass Converts values from Whoosh to native Python values.
else:
value = force_str(value) A port of the same method in pysolr, as they deal with data the same way.
return value """
if value == 'true':
# ZYY: 将Whoosh的值转换为Python原生值 return True
def _to_python(self, value): elif value == 'false':
if value == 'true ': return False
return True
elif value == 'false': if value and isinstance(value, six.string_types):
return False possible_datetime = DATETIME_REGEX.search(value)
if value and isinstance(value, six.string_types):
possible_datetime = DATETIME_REGEX.search(value) if possible_datetime:
if possible_datetime: date_values = possible_datetime.groupdict()
date_values = possible_datetime.groupdict()
for dk, dv in date_values.items(): for dk, dv in date_values.items():
date_values[dk] = int(dv) date_values[dk] = int(dv)
return datetime(
date_values['year'], return datetime(
date_values['month'], date_values['year'],
date_values ['day'], date_values['month'],
date_values['hour'], date_values['day'],
date_values['minute'], date_values['hour'],
date_values['second']) date_values['minute'],
date_values['second'])
try: try:
# ZYY: 尝试使用json加载值 # Attempt to use json to load the values.
converted_value = json.loads(value) converted_value = json.loads(value)
# ZYY: 尝试处理大多数内置类型
# Try to handle most built-in types.
if isinstance( if isinstance(
converted_value, converted_value,
(list, (list,
@ -879,139 +863,182 @@ def _to_python(self, value):
complex)): complex)):
return converted_value return converted_value
except BaseException: except BaseException:
# ZYY: 如果失败或不信任,则继续 # If it fails (SyntaxError or its ilk) or we don't trust it,
pass # continue on.
return value pass
return value
class WhooshSearchQuery(BaseSearchQuery): class WhooshSearchQuery(BaseSearchQuery):
# ZYY: 将datetime对象转换为Whoosh使用的字符串
def _convert_datetime(self, date): def _convert_datetime(self, date):
if hasattr(date, 'hour'): if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S')) return force_str(date.strftime('%Y%m%d%H%M%S'))
else: else:
return force_str(date.strftime('%Y%m%d000000')) return force_str(date.strftime('%Y%m%d000000'))
# ZYY: 清理查询片段,处理保留字和保留字符
def clean(self, query_fragment): 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() words = query_fragment.split()
cleaned_words = [] cleaned_words = []
for word in words: for word in words:
if word in self.backend.RESERVED_WORDS: if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower()) word = word.replace(word, word.lower())
for char in self.backend.RESERVED_CHARACTERS: for char in self.backend.RESERVED_CHARACTERS:
if char in word: if char in word:
word = "'%s'" % word word = "'%s'" % word
break break
cleaned_words. append(word)
cleaned_words.append(word)
return ' '.join(cleaned_words) return ' '.join(cleaned_words)
# ZYY: 构建查询片段
def build_query_fragment(self, field, filter_type, value): def build_query_fragment(self, field, filter_type, value):
from haystack import connections from haystack import connections
query_frag = '' query_frag = ''
is_datetime = False is_datetime = False
if not hasattr(value, 'input_type_name'): if not hasattr(value, 'input_type_name'):
# ZYY: 处理ValuesListQuerySet的情况 # Handle when we've got a ``ValuesListQuerySet``...
if hasattr(value, 'values_list'): if hasattr(value, 'values_list'):
value = list(value) value = list(value)
if hasattr(value, ' strftime'):
if hasattr(value, 'strftime'):
is_datetime = True is_datetime = True
if isinstance(value, six.string_types) and value != ' ': if isinstance(value, six.string_types) and value != ' ':
# ZYY: 假设为Clean类型 # It's not an ``InputType``. Assume ``Clean``.
value = Clean(value) value = Clean(value)
else: else:
value = PythonData(value) value = PythonData(value)
# ZYY: 使用InputType准备查询
prepared_value = value.prepare(self) # Prepare the query using the InputType.
if not isinstance(prepared_value, (set, list, tuple )): prepared_value = value.prepare(self)
# ZYY: 转换为pysolr需要的格式
prepared_value = self.backend._from_python(prepared_value) if not isinstance(prepared_value, (set, list, tuple)):
# ZYY: 'content'是特殊保留字,表示无特殊字段 # Then convert whatever we get back to what pysolr wants if needed.
if field == 'content': prepared_value = self.backend._from_python(prepared_value)
index_fieldname = ''
else: # 'content' is a special reserved word, much like 'pk' in
index_fieldname = u'%s:' % connections[self._using].get_unified_index( # Django's ORM layer. It indicates 'no special field'.
).get_index_fieldname(field) if field == 'content':
filter_types = { index_fieldname = ''
'content': '%s', else:
'contains': '*%s*', index_fieldname = u'%s:' % connections[self._using].get_unified_index(
' endswith': "*%s", ).get_index_fieldname(field)
'startswith': "%s*",
'exact': '%s', filter_types = {
'gt': "{%s to}", 'content': '%s',
'gte': "[%s to]", 'contains': '*%s*',
'lt': "{to %s}", 'endswith': "*%s",
'lte': "[to %s]", 'startswith': "%s*",
'fuzzy': u'%s~', 'exact': '%s',
} 'gt': "{%s to}",
if value.post_process is False: 'gte': "[%s to]",
query_frag = prepared_value 'lt': "{to %s}",
else: 'lte': "[to %s]",
if filter_type in [ 'fuzzy': u'%s~',
'content', }
'contains',
'startswith ', if value.post_process is False:
'endswith', query_frag = prepared_value
'fuzzy']: else:
if value.input_type_name == 'exact': if filter_type in [
query_frag = prepared_value '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: 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: if is_datetime is True:
pv = self._convert_datetime(pv) prepared_value = self._convert_datetime(
if isinstance(pv, six.string_types) and not is_datetime: prepared_value)
in_options.append('"%s"' % pv)
else: possible_values = [prepared_value]
in_options.append('%s' % pv)
query_frag = "(%s)" % " OR ".join(in_options) for possible_value in possible_values:
elif filter_type == 'range': terms.append(
start = self.backend._from_python(prepared_value[0]) filter_types[filter_type] %
end = self.backend._from_python(prepared_value[1]) self.backend._from_python(possible_value))
if hasattr(prepared_value [0], 'strftime'):
start = self._convert_datetime(start) if len(terms) == 1:
if hasattr(prepared_value[1], 'strftime'): query_frag = terms[0]
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: else:
prepared_value = Exact(prepared_value).prepare(self) query_frag = u"(%s)" % " AND ".join(terms)
query_frag = filter_types[filter_type] % prepared_value elif filter_type == 'in':
else: 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: 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 query_frag = filter_types[filter_type] % prepared_value
if len(query_frag) and not isinstance(value, Raw): else:
if not query_frag.startswith('(') and not query_frag.endswith(')'): if is_datetime is True:
query_frag = "(%s)" % query_frag 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) 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): class WhooshEngine(BaseEngine):
backend = WhooshSearchBackend 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 For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
""" """
# ZYY: 导入操作系统模块,用于与操作系统交互(如环境变量设置)
import os import os
# ZYY: 从Django核心模块导入WSGI应用获取函数
# ZYY: WSGI (Web Server Gateway Interface) 是Python web应用与服务器之间的标准接口
from django.core.wsgi import get_wsgi_application 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") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
# ZYY: 获取WSGI应用实例并赋值给application变量
# ZYY: 这是WSGI服务器的入口点服务器将通过这个变量调用Django应用
# ZYY: get_wsgi_application()会加载Django设置并初始化应用
application = get_wsgi_application() application = get_wsgi_application()

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save