Merge branch 'develop'

master
ymq 3 months ago
commit 3955e1eaf0

@ -13,6 +13,7 @@ from django.contrib.auth.forms import UsernameField # 导入国际化翻译工
from django.utils.translation import gettext_lazy as _
>>>>>>> LXY_branch
<<<<<<< HEAD
<<<<<<< HEAD
# 本地应用导入
# Register your models here.
@ -20,6 +21,9 @@ from django.utils.translation import gettext_lazy as _
from .models import BlogUser # 导入自定义用户模型
=======
# 导入自定义用户模型
=======
# jyn:导入自定义用户模型
>>>>>>> JYN_branch
from .models import BlogUser
>>>>>>> JYN_branch
@ -44,22 +48,35 @@ class BlogUserCreationForm(forms.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_branch
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
# 确认密码字段,用于验证两次输入的密码是否一致
# jyn:确认密码字段,用于验证两次输入的密码是否一致
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
# 指定关联的模型
=======
# jyn:指定关联的模型
>>>>>>> JYN_branch
model = BlogUser
# 表单中包含的字段,这里只显示邮箱
# jyn:表单中包含的字段,这里只显示邮箱
fields = ('email',)
>>>>>>> JYN_branch
@ -74,6 +91,12 @@ class BlogUserCreationForm(forms.ModelForm): # 定义两个密码字段,使用
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")# 获取第一次输入的密码
@ -84,9 +107,14 @@ class BlogUserCreationForm(forms.ModelForm): # 定义两个密码字段,使用
"""
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
<<<<<<< HEAD
# 检查密码是否存在且不一致
>>>>>>> JYN_branch
=======
# jyn:检查密码是否存在且不一致
>>>>>>> JYN_branch
if password1 and password2 and password1 != password2:
<<<<<<< HEAD
<<<<<<< HEAD
raise forms.ValidationError(_("passwords do not match"))# 错误信息(支持国际化)
return password2# 返回验证后的值
@ -108,25 +136,33 @@ class BlogUserCreationForm(forms.ModelForm): # 定义两个密码字段,使用
重写保存方法确保密码以哈希形式存储
而不是明文存储
"""
# 先调用父类方法获取用户对象,但不立即保存到数据库
# jyn:先调用父类方法获取用户对象,但不立即保存到数据库
user = super().save(commit=False)
# 使用set_password方法对密码进行哈希处理
# jyn:使用set_password方法对密码进行哈希处理
user.set_password(self.cleaned_data["password1"])
if commit:
# 标记用户来源为管理员站点
# jyn:标记用户来源为管理员站点
user.source = 'adminsite'
user.save()
=======
raise forms.ValidationError(_("passwords do not match"))# 密码不一致时抛出错误
=======
raise forms.ValidationError(_("passwords do not match"))# lxy密码不一致时抛出错误
>>>>>>> LXY_branch
return password2
def save(self, commit=True):# 保存用户时,对密码进行哈希处理后存储
def save(self, commit=True):# lxy保存用户时,对密码进行哈希处理后存储
# Save the provided password in hashed format
user = super().save(commit=False)# 先不提交到数据库
user.set_password(self.cleaned_data["password1"]) # 哈希处理密码
user = super().save(commit=False)# lxy先不提交到数据库
user.set_password(self.cleaned_data["password1"]) # lxy哈希处理密码
if commit:
<<<<<<< HEAD
user.source = 'adminsite'# 标记用户来源为“后台管理”
user.save()# 提交到数据库
>>>>>>> LXY_branch
=======
user.source = 'adminsite'# lxy标记用户来源为“后台管理”
user.save()# lxy提交到数据库
>>>>>>> LXY_branch
return user
>>>>>>> JYN_branch
@ -140,6 +176,7 @@ class BlogUserChangeForm(UserChangeForm):
"""
class Meta:
<<<<<<< HEAD
<<<<<<< HEAD
model = BlogUser # 关联的模型类
fields = '__all__'# 显示所有字段
@ -184,11 +221,11 @@ class BlogUserAdmin(UserAdmin):
继承自Django内置的UserChangeForm
"""
class Meta:
# 指定关联的模型
# jyn:指定关联的模型
model = BlogUser
# 显示所有字段
# jyn:显示所有字段
fields = '__all__'
# 指定用户名字段的处理类
# jyn:指定用户名字段的处理类
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
@ -201,24 +238,38 @@ class BlogUserAdmin(UserAdmin):
自定义用户管理员类用于在Django管理后台配置用户模型的显示和操作
继承自Django内置的UserAdmin
"""
# 指定编辑用户时使用的表单
# jyn:指定编辑用户时使用的表单
form = BlogUserChangeForm
# 指定添加用户时使用的表单
# jyn:指定添加用户时使用的表单
add_form = BlogUserCreationForm
<<<<<<< HEAD
# 列表页面显示的字段
=======
model = BlogUser # 关联的模型是BlogUser
fields = '__all__'# 显示模型的所有字段
field_classes = {'username': UsernameField}# 为用户名字段指定类保持Django原生逻辑
=======
model = BlogUser # lxy关联的模型是BlogUser
fields = '__all__'# lxy显示模型的所有字段
field_classes = {'username': UsernameField}# lxy为用户名字段指定类保持Django原生逻辑
>>>>>>> LXY_branch
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)# 调用父类的初始化方法
super().__init__(*args, **kwargs)#lxy调用父类的初始化方法
class BlogUserAdmin(UserAdmin):
<<<<<<< HEAD
form = BlogUserChangeForm# 指定修改用户时使用的表单
add_form = BlogUserCreationForm# 指定创建用户时使用的表单
>>>>>>> LXY_branch
=======
form = BlogUserChangeForm#lxy指定修改用户时使用的表单
add_form = BlogUserCreationForm# lxy指定创建用户时使用的表单
>>>>>>> LXY_branch
=======
# jyn:列表页面显示的字段
>>>>>>> JYN_branch
list_display = (
'id',
'nickname',
@ -229,13 +280,23 @@ class BlogUserAdmin(UserAdmin):
<<<<<<< HEAD
'source'
)
# 列表页面中可点击跳转的字段
# jyn:列表页面中可点击跳转的字段
list_display_links = ('id', 'username')
<<<<<<< HEAD
# 排序方式按id降序排列最新的用户在前
ordering = ('-id',)
>>>>>>> JYN_branch
=======
'source')
<<<<<<< HEAD
list_display_links = ('id', 'username')# 列表页中可点击跳转的字段
ordering = ('-id',)# 列表页的排序方式按ID倒序
>>>>>>> LXY_branch
=======
list_display_links = ('id', 'username')#lxy列表页中可点击跳转的字段
ordering = ('-id',)#lxy列表页的排序方式按ID倒序
>>>>>>> LXY_branch
=======
# jyn:排序方式按id降序排列最新的用户在前
ordering = ('-id',)
>>>>>>> JYN_branch

@ -1,4 +1,5 @@
<<<<<<< HEAD
<<<<<<< HEAD
from django.apps import AppConfig
@ -17,6 +18,7 @@ class AccountsConfig(AppConfig):
每个Django应用都需要一个配置类用于设置应用的各种属性和行为
通常放在应用目录下的apps.py文件中
"""
<<<<<<< HEAD
# 应用的名称,必须与应用目录名一致
# 这个名称会被Django用来识别和管理应用
name = 'accounts'
@ -28,3 +30,15 @@ from django.apps import AppConfig#导入 Django 框架中用于应用配置的 A
class AccountsConfig(AppConfig):#定义 AccountsConfig 类,继承自 AppConfig用于对 accounts 应用进行自定义配置
name = 'accounts'#指定该应用的名称为 accountsDjango 会通过这个名称来识别和管理该应用
>>>>>>> LXY_branch
=======
from django.apps import AppConfig#lxy导入 Django 框架中用于应用配置的 AppConfig 类,这是 Django 应用配置的核心类
class AccountsConfig(AppConfig):#lxy定义 AccountsConfig 类,继承自 AppConfig用于对 accounts 应用进行自定义配置
name = 'accounts'#lxy指定该应用的名称为 accountsDjango 会通过这个名称来识别和管理该应用
>>>>>>> LXY_branch
=======
# jyn:应用的名称,必须与应用目录名一致
# jyn:这个名称会被Django用来识别和管理应用
name = 'accounts'
>>>>>>> JYN_branch

@ -35,8 +35,8 @@ from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils # 导入自定义工具模块,可能用于验证码验证等功能
from .models import BlogUser # 导入自定义用户模型
from . import utils # jyn:导入自定义工具模块,可能用于验证码验证等功能
from .models import BlogUser # jyn:导入自定义用户模型
class LoginForm(AuthenticationForm):
@ -45,15 +45,19 @@ class LoginForm(AuthenticationForm):
用于处理用户登录验证主要扩展了表单字段的样式
"""
def __init__(self, *args, **kwargs):
# 调用父类构造方法初始化表单
# jyn:调用父类构造方法初始化表单
super(LoginForm, self).__init__(*args, **kwargs)
# 自定义用户名输入框添加占位符和CSS类
# jyn:自定义用户名输入框添加占位符和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
<<<<<<< HEAD
# 自定义密码输入框添加占位符和CSS类
>>>>>>> 8b27cdad9a9ccc84febce3bcf1d211ed109f96f2
=======
# jyn:自定义密码输入框添加占位符和CSS类
>>>>>>> ef493f43496bf1ae99bf4389327ac0ae80bb9ae1
self.fields['password'].widget = widgets.PasswordInput(
<<<<<<< HEAD
attrs={'placeholder': "password", "class": "form-control"}) #为密码字段设置密码输入小部件,同样定义占位符和样式类。
@ -61,8 +65,12 @@ class LoginForm(AuthenticationForm):
<<<<<<< HEAD
=======
attrs={'placeholder': "password", "class": "form-control"})
<<<<<<< HEAD
#自定义登录表单在__init__方法中设置username文本输入占位符、form-control样式和password密码输入占位符、form-control样式字段的前端显示样式。
>>>>>>> LXY_branch
=======
#lxy自定义登录表单在__init__方法中设置username文本输入占位符、form-control样式和password密码输入占位符、form-control样式字段的前端显示样式。
>>>>>>> LXY_branch
<<<<<<< HEAD
@ -82,13 +90,17 @@ class RegisterForm(UserCreationForm):
>>>>>>> 8b27cdad9a9ccc84febce3bcf1d211ed109f96f2
>>>>>>> JYN_branch
def __init__(self, *args, **kwargs):
# 调用父类构造方法初始化表单
# jyn:调用父类构造方法初始化表单
super(RegisterForm, self).__init__(*args, **kwargs)
<<<<<<< HEAD
# 自定义用户名、邮箱和密码字段的HTML属性
=======
<<<<<<< HEAD
# 自定义各字段的输入控件,添加样式和占位符
>>>>>>> JYN_branch
=======
# jyn:自定义各字段的输入控件,添加样式和占位符
>>>>>>> JYN_branch
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
@ -98,10 +110,14 @@ class RegisterForm(UserCreationForm):
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
<<<<<<< HEAD
<<<<<<< HEAD
# 验证邮箱唯一性
=======
#__init__方法中设置username文本输入、email邮箱输入、password1和password2密码输入字段的占位符与form-control样式
>>>>>>> LXY_branch
=======
#lxy__init__方法中设置username文本输入、email邮箱输入、password1和password2密码输入字段的占位符与form-control样式
>>>>>>> LXY_branch
def clean_email(self):
"""
@ -109,13 +125,15 @@ class RegisterForm(UserCreationForm):
表单验证机制中以clean_为前缀的方法会自动被调用
"""
email = self.cleaned_data['email']
# 检查该邮箱是否已存在于数据库中
# jyn:检查该邮箱是否已存在于数据库中
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists")) # 抛出验证错误
raise ValidationError(_("email already exists")) #jyn: 抛出验证错误
return email
<<<<<<< HEAD
<<<<<<< HEAD
# 指定关联的用户模型和表单字段
class Meta:
<<<<<<< HEAD
model = get_user_model() # 使用项目配置的用户模型可能是自定义的BlogUser
fields = ("username", "email") # 注册表单中显示的字段
=======
@ -125,6 +143,17 @@ class RegisterForm(UserCreationForm):
fields = ("username", "email")
#Meta类指定关联模型为自定义用户模型表单字段包含username和email
>>>>>>> LXY_branch
=======
#lxyclean_email方法验证邮箱是否已被注册若存在则抛出“邮箱已存在”的验证错误
class Meta:
model = get_user_model()
fields = ("username", "email")
#lxy Meta类指定关联模型为自定义用户模型表单字段包含username和email
>>>>>>> LXY_branch
=======
model = get_user_model() # jyn:使用项目配置的用户模型可能是自定义的BlogUser
fields = ("username", "email") # jyn:注册表单中显示的字段
>>>>>>> JYN_branch
# 忘记密码表单(验证邮箱和验证码)
class ForgetPasswordForm(forms.Form):
@ -132,7 +161,7 @@ class ForgetPasswordForm(forms.Form):
忘记密码表单用于用户重置密码的流程
包含新密码确认密码邮箱和验证码字段
"""
# 新密码字段
# jyn:新密码字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -146,7 +175,11 @@ class ForgetPasswordForm(forms.Form):
# 新密码字段2用于确认
=======
<<<<<<< HEAD
# 确认新密码字段
>>>>>>> JYN_branch
=======
# jyn:确认新密码字段
>>>>>>> JYN_branch
new_password2 = forms.CharField(
label="确认密码",
@ -161,7 +194,11 @@ class ForgetPasswordForm(forms.Form):
# 邮箱字段
=======
<<<<<<< HEAD
# 邮箱字段(用于验证用户身份)
>>>>>>> JYN_branch
=======
# jyn:邮箱字段(用于验证用户身份)
>>>>>>> JYN_branch
email = forms.EmailField(
label='邮箱',
@ -176,7 +213,11 @@ class ForgetPasswordForm(forms.Form):
# 验证码字段
=======
<<<<<<< HEAD
# 验证码字段(用于身份验证)
>>>>>>> JYN_branch
=======
# jyn:验证码字段(用于身份验证)
>>>>>>> JYN_branch
code = forms.CharField(
label=_('Code'),
@ -187,17 +228,21 @@ class ForgetPasswordForm(forms.Form):
}
),
)
<<<<<<< HEAD
<<<<<<< HEAD
# 验证两次输入的密码是否一致,并检查密码强度
=======
#定义new_password1新密码密码输入、new_password2确认密码密码输入、email邮箱文本输入、code验证码文本输入字段均设置form-control样式和占位符。
>>>>>>> LXY_branch
=======
#lxy定义new_password1新密码密码输入、new_password2确认密码密码输入、email邮箱文本输入、code验证码文本输入字段均设置form-control样式和占位符。
>>>>>>> LXY_branch
def clean_new_password2(self):
"""验证两次输入的密码是否一致,并验证密码强度"""
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
# 检查两次密码是否一致
# jyn:检查两次密码是否一致
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
<<<<<<< HEAD
@ -205,50 +250,66 @@ class ForgetPasswordForm(forms.Form):
=======
# 使用Django内置的密码验证器验证密码强度
# jyn:使用Django内置的密码验证器验证密码强度
password_validation.validate_password(password2)
>>>>>>> JYN_branch
return password2
<<<<<<< HEAD
<<<<<<< HEAD
# 验证邮箱是否已注册
=======
# clean_new_password2方法验证两次新密码是否一致并对密码进行有效性校验
>>>>>>> LXY_branch
=======
#lxyclean_new_password2方法验证两次新密码是否一致并对密码进行有效性校验
>>>>>>> LXY_branch
def clean_email(self):
"""验证邮箱是否已注册"""
user_email = self.cleaned_data.get("email")
# 检查该邮箱是否存在于系统中
# jyn:检查该邮箱是否存在于系统中
if not BlogUser.objects.filter(email=user_email).exists():
# 提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露)
# jyn:提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露)
raise ValidationError(_("email does not exist"))
return user_email
<<<<<<< HEAD
# 验证用户输入的验证码是否正确
=======
<<<<<<< HEAD
# clean_email方法验证邮箱是否已注册基于BlogUser模型未注册则抛出“邮箱不存在”的验证错误
>>>>>>> LXY_branch
=======
#lxyclean_email方法验证邮箱是否已注册基于BlogUser模型未注册则抛出“邮箱不存在”的验证错误
>>>>>>> LXY_branch
def clean_code(self):
"""验证验证码是否有效"""
code = self.cleaned_data.get("code")
<<<<<<< HEAD
<<<<<<< HEAD
error = utils.verify(# 调用工具函数验证验证码
=======
# 调用工具函数验证邮箱和验证码是否匹配
=======
# jyn:调用工具函数验证邮箱和验证码是否匹配
>>>>>>> JYN_branch
error = utils.verify(
>>>>>>> JYN_branch
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error) # 验证码无效时抛出错误
raise ValidationError(error) # jyn:验证码无效时抛出错误
return code
<<<<<<< HEAD
<<<<<<< HEAD
# 忘记密码功能中的验证码发送表单(仅需邮箱字段)
=======
#clean_code方法调用工具方法utils.verify验证验证码有效性无效则抛出错误
=======
#lxy clean_code方法调用工具方法utils.verify验证验证码有效性无效则抛出错误
>>>>>>> LXY_branch
>>>>>>> LXY_branch
class ForgetPasswordCodeForm(forms.Form):
@ -260,7 +321,11 @@ class ForgetPasswordCodeForm(forms.Form):
label=_('Email'),
<<<<<<< HEAD
)
<<<<<<< HEAD
=======
)
#仅包含email字段邮箱输入用于忘记密码流程中验证邮箱的步骤
>>>>>>> LXY_branch
=======
#lxy仅包含email字段邮箱输入用于忘记密码流程中验证邮箱的步骤
>>>>>>> LXY_branch

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

@ -3,10 +3,10 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser # 导入自定义用户模型
from blog.models import Article, Category # 导入博客相关模型
from djangoblog.utils import * # 导入项目工具函数
from . import utils # 导入当前应用的工具函数
from accounts.models import BlogUser # jyn:导入自定义用户模型
from blog.models import Article, Category # jyn:导入博客相关模型
from djangoblog.utils import * # jyn:导入项目工具函数
from . import utils # jyn:导入当前应用的工具函数
# Create your tests here.
@ -30,15 +30,22 @@ class AccountTest(TestCase):
测试前的初始化方法会在每个测试方法执行前运行
用于创建测试所需的基础数据
"""
<<<<<<< HEAD
self.client = Client() # 创建测试客户端,用于模拟用户请求
self.factory = RequestFactory() # 创建请求工厂,用于构建请求对象
# 创建一个测试用户
>>>>>>> JYN_branch
=======
self.client = Client() # jyn:创建测试客户端,用于模拟用户请求
self.factory = RequestFactory() # jyn:创建请求工厂,用于构建请求对象
# jyn:创建一个测试用户
>>>>>>> JYN_branch
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
<<<<<<< HEAD
<<<<<<< HEAD
# 测试用的随机字符串
self.new_test = "xxx123--="
@ -49,17 +56,25 @@ class AccountTest(TestCase):
# 创建一个超级用户(用于测试管理员权限)
=======
self.new_test = "xxx123--=" # 测试用的新密码
=======
self.new_test = "xxx123--=" #jyn: 测试用的新密码
>>>>>>> JYN_branch
def test_validate_account(self):
"""测试用户账号验证相关功能,包括登录和管理员权限"""
site = get_current_site().domain # 获取当前站点域名
site = get_current_site().domain # jyn:获取当前站点域名
<<<<<<< HEAD
# 创建一个超级用户
>>>>>>> JYN_branch
=======
# jyn:创建一个超级用户
>>>>>>> JYN_branch
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
<<<<<<< HEAD
<<<<<<< HEAD
# 从数据库获取刚创建的超级用户(验证是否创建成功)
testuser = BlogUser.objects.get(username='liangliangyy1')
@ -79,13 +94,27 @@ class AccountTest(TestCase):
self.assertEqual(response.status_code, 200)# 验证返回200状态码
=======
self.assertEqual(loginresult, True) # 断言登录成功
=======
# jyn:获取创建的用户
testuser = BlogUser.objects.get(username='liangliangyy1')
# jyn:测试登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True) #jyn: 断言登录成功
>>>>>>> JYN_branch
# 测试访问管理员页面
# jyn:测试访问管理员页面
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200) # 断言页面访问成功
self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功
<<<<<<< HEAD
>>>>>>> JYN_branch
# 创建测试分类
=======
# jyn:创建测试分类
>>>>>>> JYN_branch
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
@ -94,8 +123,12 @@ class AccountTest(TestCase):
<<<<<<< HEAD
=======
<<<<<<< HEAD
>>>>>>> JYN_branch
# 创建测试文章
=======
# jyn:创建测试文章
>>>>>>> JYN_branch
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
@ -111,18 +144,26 @@ class AccountTest(TestCase):
=======
article.author = user
article.category = category
article.type = 'a' # 假设'a'表示文章类型
article.status = 'p' # 假设'p'表示已发布
article.type = 'a' # jyn:假设'a'表示文章类型
article.status = 'p' # jyn:假设'p'表示已发布
article.save()
# 测试访问文章管理页面
# jyn:测试访问文章管理页面
response = self.client.get(article.get_admin_url())
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(response.status_code, 200) # 断言页面访问成功
>>>>>>> JYN_branch
=======
self.assertEqual(response.status_code, 200)#测试管理员账号登录后台功能:创建超级用户,验证登录状态和后台页面访问状态
>>>>>>> LXY_branch
=======
self.assertEqual(response.status_code, 200)#lxy测试管理员账号登录后台功能创建超级用户验证登录状态和后台页面访问状态
>>>>>>> LXY_branch
=======
self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功
>>>>>>> JYN_branch
# 测试用户注册功能
def test_validate_register(self):
@ -130,7 +171,11 @@ class AccountTest(TestCase):
# 验证测试邮箱初始不存在
=======
"""测试用户注册功能,包括注册流程、邮箱验证和权限控制"""
<<<<<<< HEAD
# 初始状态下,该邮箱应不存在
>>>>>>> JYN_branch
=======
# jyn:初始状态下,该邮箱应不存在
>>>>>>> JYN_branch
self.assertEquals(
0, len(
@ -140,7 +185,11 @@ class AccountTest(TestCase):
# 模拟注册请求
=======
<<<<<<< HEAD
# 模拟用户注册提交
>>>>>>> JYN_branch
=======
# jyn:模拟用户注册提交
>>>>>>> JYN_branch
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
@ -152,7 +201,11 @@ class AccountTest(TestCase):
# 验证用户已创建(通过邮箱查询)
=======
<<<<<<< HEAD
# 注册后,该邮箱应存在
>>>>>>> JYN_branch
=======
#jyn: 注册后,该邮箱应存在
>>>>>>> JYN_branch
self.assertEquals(
1, len(
@ -164,9 +217,13 @@ class AccountTest(TestCase):
# 生成验证签名(用于邮箱验证等场景)
=======
# 获取刚注册的用户
#jyn: 获取刚注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
<<<<<<< HEAD
# 生成验证链接(模拟邮箱验证流程)
>>>>>>> JYN_branch
=======
# jyn:生成验证链接(模拟邮箱验证流程)
>>>>>>> JYN_branch
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 构造验证URL
@ -182,13 +239,17 @@ class AccountTest(TestCase):
# 获取指定邮箱的用户并设置为超级用户和工作人员
=======
# 访问验证链接
# jyn:访问验证链接
response = self.client.get(url)
self.assertEqual(response.status_code, 200) # 断言验证页面访问成功
self.assertEqual(response.status_code, 200) # jyn:断言验证页面访问成功
# 登录新注册用户
# jyn:登录新注册用户
self.client.login(username='user1233', password='password123!q@wE#R$T')
<<<<<<< HEAD
# 提升用户权限
>>>>>>> JYN_branch
=======
# jyn:提升用户权限
>>>>>>> JYN_branch
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
@ -200,10 +261,14 @@ class AccountTest(TestCase):
# 创建分类
=======
# 清除缓存
# jyn:清除缓存
delete_sidebar_cache()
<<<<<<< HEAD
# 创建测试分类
>>>>>>> JYN_branch
=======
# jyn:创建测试分类
>>>>>>> JYN_branch
category = Category()
category.name = "categoryaaa"
@ -214,7 +279,11 @@ class AccountTest(TestCase):
# 创建文章
=======
<<<<<<< HEAD
# 创建测试文章
>>>>>>> JYN_branch
=======
# jyn:创建测试文章
>>>>>>> JYN_branch
article = Article()
article.category = category
@ -237,19 +306,23 @@ class AccountTest(TestCase):
# 测试使用错误密码登录
=======
# 测试访问文章管理页面
# jyn:测试访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 测试登出功能
# jyn:测试登出功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200]) # 登出通常是重定向
# 登出后访问管理页面(应被拒绝或重定向)
# jyn:登出后访问管理页面(应被拒绝或重定向)
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
<<<<<<< HEAD
# 使用错误密码登录
>>>>>>> JYN_branch
=======
# jyn:使用错误密码登录
>>>>>>> JYN_branch
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
@ -260,19 +333,28 @@ class AccountTest(TestCase):
# 测试使用错误密码登录后访问文章管理URL应重定向
=======
<<<<<<< HEAD
# 错误登录后访问管理页面
>>>>>>> JYN_branch
=======
# jyn:错误登录后访问管理页面
>>>>>>> JYN_branch
response = self.client.get(article.get_admin_url())
<<<<<<< HEAD
<<<<<<< HEAD
self.assertIn(response.status_code, [301, 302, 200])
# 测试邮箱验证码验证
=======
self.assertIn(response.status_code, [301, 302, 200])#测试用户注册流程:验证注册前后用户数量变化,邮箱验证链接的有效性,以及注册后用户权限、文章发布等功能
=======
self.assertIn(response.status_code, [301, 302, 200])#lxy测试用户注册流程验证注册前后用户数量变化邮箱验证链接的有效性以及注册后用户权限、文章发布等功能
>>>>>>> LXY_branch
>>>>>>> LXY_branch
def test_verify_email_code(self):
"""测试邮箱验证码验证功能"""
to_email = "admin@admin.com"
<<<<<<< HEAD
<<<<<<< HEAD
code = generate_code() # 生成验证码
utils.set_code(to_email, code)# 存储验证码
@ -282,6 +364,7 @@ class AccountTest(TestCase):
self.assertEqual(err, None)
# 测试错误邮箱
err = utils.verify("admin@123.com", code)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(type(err), str)# 应返回错误信息字符串
# 测试忘记密码发送验证码功能 - 成功情况
@ -292,25 +375,35 @@ class AccountTest(TestCase):
=======
self.assertEqual(type(err), str)#测试邮箱验证码功能:验证有效邮箱和无效邮箱的验证码校验结果
>>>>>>> LXY_branch
=======
self.assertEqual(type(err), str)#lxy测试邮箱验证码功能验证有效邮箱和无效邮箱的验证码校验结果
>>>>>>> LXY_branch
=======
code = generate_code() # jyn:生成验证码
utils.set_code(to_email, code) # jyn:存储验证码
utils.send_verify_email(to_email, code) # jyn:发送验证邮件
>>>>>>> JYN_branch
# 验证正确的邮箱和验证码
# jyn:验证正确的邮箱和验证码
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None) # 应无错误
self.assertEqual(err, None) # jyn:应无错误
# 验证错误的邮箱和正确的验证码
# jyn:验证错误的邮箱和正确的验证码
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) # 应返回错误信息
self.assertEqual(type(err), str) # jyn:应返回错误信息
>>>>>>> JYN_branch
def test_forget_password_email_code_success(self):
"""测试发送密码重置验证码成功的情况"""
resp = self.client.post(
path=reverse("account:forget_password_code"),
<<<<<<< HEAD
<<<<<<< HEAD
data=dict(email="admin@admin.com") # 使用正确邮箱格式
)
self.assertEqual(resp.status_code, 200)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.content.decode("utf-8"), "ok")# 验证返回成功消息
# 测试忘记密码发送验证码功能 - 失败情况
@ -325,15 +418,30 @@ class AccountTest(TestCase):
=======
self.assertEqual(resp.content.decode("utf-8"), "ok")#测试忘记密码的邮箱验证码发送:分别验证成功和失败场景(如邮箱错误)的接口响应
>>>>>>> LXY_branch
=======
self.assertEqual(resp.content.decode("utf-8"), "ok")#lxy测试忘记密码的邮箱验证码发送分别验证成功和失败场景如邮箱错误的接口响应
>>>>>>> LXY_branch
def test_forget_password_email_code_fail(self):
"""测试发送密码重置验证码失败的情况"""
# 不提供邮箱
>>>>>>> JYN_branch
=======
data=dict(email="admin@admin.com") # jyn:使用已存在的邮箱
)
self.assertEqual(resp.status_code, 200) # jyn:断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") # jyn:断言返回成功信息
def test_forget_password_email_code_fail(self):
"""测试发送密码重置验证码失败的情况"""
# jyn:不提供邮箱
>>>>>>> JYN_branch
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试提供错误格式邮箱
@ -341,11 +449,17 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误信息
# 提供无效格式的邮箱
>>>>>>> JYN_branch
=======
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # jyn:断言返回错误信息
# jyn:提供无效格式的邮箱
>>>>>>> JYN_branch
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试忘记密码重置功能 - 成功情况
@ -361,6 +475,15 @@ class AccountTest(TestCase):
code = generate_code() # 生成验证码
utils.set_code(self.blog_user.email, code) # 存储验证码
# 准备重置密码的数据
>>>>>>> JYN_branch
=======
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #jyn: 断言返回错误信息
def test_forget_password_email_success(self):
"""测试密码重置成功的情况"""
code = generate_code() # jyn:生成验证码
utils.set_code(self.blog_user.email, code) #jyn: 存储验证码
# jyn:准备重置密码的数据
>>>>>>> JYN_branch
data = dict(
new_password1=self.new_test, # 新密码
@ -368,27 +491,35 @@ class AccountTest(TestCase):
email=self.blog_user.email,# 用户邮箱
code=code, # 验证码
)
<<<<<<< HEAD
<<<<<<< HEAD
# 提交重置密码请求
=======
# 提交密码重置请求
>>>>>>> JYN_branch
=======
# jyn:提交密码重置请求
>>>>>>> JYN_branch
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.status_code, 302) # 应重定向
=======
self.assertEqual(resp.status_code, 302) # 成功重置后通常重定向
>>>>>>> JYN_branch
=======
self.assertEqual(resp.status_code, 302) # jyn:成功重置后通常重定向
>>>>>>> JYN_branch
# 验证密码是否已更新
# jyn:验证密码是否已更新
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None) # 断言用户存在
# 断言密码修改成功
).first() # jyn:type: BlogUser
self.assertNotEqual(blog_user, None) # jyn:断言用户存在
# jyn:断言密码修改成功
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
# 测试忘记密码重置功能 - 用户不存在情况
def test_forget_password_email_not_user(self):
@ -396,10 +527,14 @@ class AccountTest(TestCase):
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
<<<<<<< HEAD
<<<<<<< HEAD
email="123@123.com",# 不存在的邮箱
=======
email="123@123.com", # 不存在的邮箱
>>>>>>> JYN_branch
=======
email="123@123.com", # jyn:不存在的邮箱
>>>>>>> JYN_branch
code="123456",
)
@ -408,11 +543,15 @@ class AccountTest(TestCase):
data=data
)
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.status_code, 200) # 应返回错误页面而非重定向
=======
self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码
>>>>>>> JYN_branch
=======
self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码
>>>>>>> JYN_branch
# 测试忘记密码重置功能 - 验证码错误情况
def test_forget_password_email_code_error(self):
@ -422,18 +561,28 @@ class AccountTest(TestCase):
# 使用错误验证码提交
=======
"""测试使用错误的验证码重置密码的情况"""
<<<<<<< HEAD
code = generate_code() # 生成正确验证码
utils.set_code(self.blog_user.email, code) # 存储验证码
# 使用错误的验证码
>>>>>>> JYN_branch
=======
code = generate_code() # jyn:生成正确验证码
utils.set_code(self.blog_user.email, code) #jyn: 存储验证码
# jyn:使用错误的验证码
>>>>>>> JYN_branch
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
<<<<<<< HEAD
<<<<<<< HEAD
code="111111",# 错误验证码
=======
code="111111", # 错误的验证码
>>>>>>> JYN_branch
=======
code="111111", # jyn:错误的验证码
>>>>>>> JYN_branch
)
resp = self.client.post(
@ -441,13 +590,21 @@ class AccountTest(TestCase):
data=data
)
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
self.assertEqual(resp.status_code, 200)# 应返回错误页面而非重定向
=======
self.assertEqual(resp.status_code, 200)#测试忘记密码流程:成功场景:验证密码修改后是否生效;失败场景:验证不存在用户、验证码错误时的接口响应
>>>>>>> LXY_branch
=======
self.assertEqual(resp.status_code, 200)#lxy测试忘记密码流程成功场景验证密码修改后是否生效失败场景验证不存在用户、验证码错误时的接口响应
>>>>>>> LXY_branch
=======
self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码
>>>>>>> JYN_branch
=======
self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码
>>>>>>> JYN_branch

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

@ -1,4 +1,5 @@
<<<<<<< HEAD
<<<<<<< HEAD
# 导入 Django 认证系统所需的模块
from django.contrib.auth import get_user_model# 动态获取当前项目的 User 模型
from django.contrib.auth.backends import ModelBackend# Django 默认的认证后端基类
@ -6,9 +7,13 @@ from django.contrib.auth.backends import ModelBackend# Django 默认的认证后
from django.contrib.auth import get_user_model # 获取项目配置的用户模型(支持自定义模型)
from django.contrib.auth.backends import ModelBackend # 导入Django内置的模型认证后端
>>>>>>> JYN_branch
=======
from django.contrib.auth import get_user_model # jyn:获取项目配置的用户模型(支持自定义模型)
from django.contrib.auth.backends import ModelBackend # jyn:导入Django内置的模型认证后端
>>>>>>> JYN_branch
class EmailOrUsernameModelBackend(ModelBackend):#自定义Django认证后端支持用户名或邮箱两种方式登录。
class EmailOrUsernameModelBackend(ModelBackend):#lxy自定义Django认证后端支持用户名或邮箱两种方式登录。
"""
<<<<<<< HEAD
允许使用用户名或邮箱登录
@ -47,11 +52,12 @@ class EmailOrUsernameModelBackend(ModelBackend):#自定义Django认证后端
- password前端传入的密码明文
返回验证成功返回用户对象失败返回None
"""
# 判断输入的「username」是否包含@符号,以此区分邮箱和用户名
# jyn:判断输入的「username」是否包含@符号,以此区分邮箱和用户名
if '@' in username:
# 若包含@,则按邮箱字段查询用户
# jyn:若包含@,则按邮箱字段查询用户
kwargs = {'email': username}
else:
<<<<<<< HEAD
# 若不包含@,则按用户名字段查询用户
>>>>>>> JYN_branch
kwargs = {'username': username}
@ -67,15 +73,26 @@ class EmailOrUsernameModelBackend(ModelBackend):#自定义Django认证后端
# 用户不存在时返回 NoneDjango 会继续尝试其他认证后端)
=======
# 根据上述条件从数据库查询唯一用户
=======
# jyn:若不包含@,则按用户名字段查询用户
kwargs = {'username': username}
try:
# jyn:根据上述条件从数据库查询唯一用户
>>>>>>> JYN_branch
user = get_user_model().objects.get(**kwargs)
# 验证密码check_password会自动将明文密码与数据库中存储的哈希密码比对
# jyn:验证密码check_password会自动将明文密码与数据库中存储的哈希密码比对
if user.check_password(password):
return user # 密码正确,返回用户对象(认证成功)
return user # jyn:密码正确,返回用户对象(认证成功)
except get_user_model().DoesNotExist:
<<<<<<< HEAD
# 若查询不到用户(用户名/邮箱不存在返回None认证失败
>>>>>>> JYN_branch
=======
# jyn:若查询不到用户(用户名/邮箱不存在返回None认证失败
>>>>>>> JYN_branch
return None
#核心认证逻辑:判断输入是否为邮箱(含@分别用邮箱或用户名查询用户验证密码后返回用户对象若用户不存在则返回None。
#lxy核心认证逻辑:判断输入是否为邮箱(含@分别用邮箱或用户名查询用户验证密码后返回用户对象若用户不存在则返回None。
def get_user(self, username):
"""
<<<<<<< HEAD
@ -94,6 +111,7 @@ class EmailOrUsernameModelBackend(ModelBackend):#自定义Django认证后端
# 用户不存在时返回 None
return None
<<<<<<< HEAD
<<<<<<< HEAD
=======
根据用户ID获取用户对象Django认证系统必须实现的方法
作用认证成功后系统通过此方法获取用户完整信息
@ -101,12 +119,20 @@ class EmailOrUsernameModelBackend(ModelBackend):#自定义Django认证后端
返回存在则返回用户对象不存在返回None
"""
try:
# 通过主键ID查询用户
# jyn:通过主键ID查询用户
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
<<<<<<< HEAD
# 若用户不存在返回None
return None
>>>>>>> JYN_branch
=======
#根据用户ID主键查询用户不存在则返回None用于Django认证系统的用户查询环节
>>>>>>> LXY_branch
=======
#lxy根据用户ID主键查询用户不存在则返回None用于Django认证系统的用户查询环节
>>>>>>> LXY_branch
=======
# jyn:若用户不存在返回None
return None
>>>>>>> JYN_branch

@ -2,6 +2,7 @@
import typing # 用于类型注解
from datetime import timedelta # 用于处理时间间隔
<<<<<<< HEAD
<<<<<<< HEAD
# 导入 Django 核心组件
from django.core.cache import cache # Django 缓存系统
@ -16,15 +17,28 @@ from django.core.cache import cache # 导入Django缓存模块用于存储
from django.utils.translation import gettext # 用于获取即时翻译文本
from django.utils.translation import gettext_lazy as _ # 用于延迟翻译文本(支持国际化)
<<<<<<< HEAD
<<<<<<< HEAD
from djangoblog.utils import send_email # 导入项目自定义的发送邮件工具函数
# 验证码有效期5分钟全局变量统一控制时效
>>>>>>> JYN_branch
=======
from django.core.cache import cache # jyn:导入Django缓存模块用于存储验证码
from django.utils.translation import gettext # jyn:用于获取即时翻译文本
from django.utils.translation import gettext_lazy as _ # jyn:用于延迟翻译文本(支持国际化)
from djangoblog.utils import send_email #jyn: 导入项目自定义的发送邮件工具函数
# jyn:验证码有效期5分钟全局变量统一控制时效
>>>>>>> JYN_branch
_code_ttl = timedelta(minutes=5)
=======
_code_ttl = timedelta(minutes=5)#验证码有效期设置为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")):
@ -42,11 +56,15 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
code: 生成的随机验证码
subject: 邮件主题默认值为Verify Email支持国际化
"""
# 构造邮件HTML内容包含验证码和有效期提示使用%(code)s占位符注入验证码
# jyn:构造邮件HTML内容包含验证码和有效期提示使用%(code)s占位符注入验证码
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
<<<<<<< HEAD
# 调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容
>>>>>>> JYN_branch
=======
# jyn:调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容
>>>>>>> JYN_branch
send_email([to_mail], subject, html_content)
@ -71,11 +89,11 @@ def verify(email: str, code: str) -> typing.Optional[str]:
代码注释中指出当前错误处理逻辑不合理应使用raise抛出异常而非返回错误字符串
若返回错误字符串调用方需额外判断返回值是否为错误增加了代码耦合度
"""
# 从缓存中获取该邮箱对应的验证码
# jyn:从缓存中获取该邮箱对应的验证码
cache_code = get_code(email)
# 对比用户输入的验证码与缓存中的验证码
# jyn:对比用户输入的验证码与缓存中的验证码
if cache_code != code:
# 验证码不匹配时,返回国际化的错误提示
#jyn: 验证码不匹配时,返回国际化的错误提示
return gettext("Verification code error")
>>>>>>> JYN_branch
@ -100,11 +118,16 @@ def set_code(email: str, code: str):
email: 作为缓存key的邮箱地址确保一个邮箱对应一个验证码
code: 需要存入缓存的验证码
"""
<<<<<<< HEAD
# 调用Django缓存的set方法key=邮箱value=验证码timeout=有效期(秒)
>>>>>>> JYN_branch
=======
# jyn:调用Django缓存的set方法key=邮箱value=验证码timeout=有效期(秒)
>>>>>>> JYN_branch
cache.set(email, code, _code_ttl.seconds)
<<<<<<< HEAD
<<<<<<< HEAD
def get_code(email: str) -> typing.Optional[str]:
"""
@ -122,6 +145,9 @@ def get_code(email: str) -> typing.Optional[str]:
# 直接调用 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)
@ -132,6 +158,11 @@ def get_code(email: str) -> typing.Optional[str]:#从缓存中获取指定邮箱
Return:
缓存中存在该邮箱对应的验证码时返回字符串不存在时返回None
"""
<<<<<<< HEAD
# 调用Django缓存的get方法根据邮箱key获取验证码
return cache.get(email)
>>>>>>> JYN_branch
=======
# jyn:调用Django缓存的get方法根据邮箱key获取验证码
return cache.get(email)
>>>>>>> JYN_branch

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

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

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

@ -1,43 +1,101 @@
import logging
#ymq导入logging模块用于日志记录
from django.utils import timezone
#ymq导入Django的timezone模块用于处理时间相关操作
from djangoblog.utils import cache, get_blog_setting
#ymq从项目工具模块导入缓存工具和获取博客设置的函数
from .models import Category, Article
#ymq从当前应用的models模块导入分类和文章模型
logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
<<<<<<< HEAD
def seo_processor(requests):
#ymq定义SEO上下文处理器用于向模板全局注入通用数据
key = 'seo_processor'
#ymq缓存键名用于标识当前处理器的缓存数据
value = cache.get(key)
#ymq尝试从缓存中获取数据
if value:
#ymq如果缓存存在直接返回缓存数据
return value
else:
#ymq如果缓存不存在重新生成数据
logger.info('set processor cache.')
#ymq记录日志提示正在设置缓存
setting = get_blog_setting()
#ymq获取博客的全局设置信息
#ymq构建需要传递给模板的上下文数据字典
value = {
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_NAME': setting.site_name, # 网站名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示谷歌广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # 谷歌广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 网站SEO描述
'SITE_DESCRIPTION': setting.site_description, # 网站描述
'SITE_KEYWORDS': setting.site_keywords, # 网站关键词
# 网站基础URL协议+域名)
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度
'nav_category_list': Category.objects.all(), # 导航分类列表
# 导航页面列表(类型为页面且状态为已发布)
'nav_pages': Article.objects.filter(
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,
'ANALYTICS_CODE': setting.analytics_code,
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
"SHOW_GONGAN_CODE": setting.show_gongan_code,
"CURRENT_YEAR": timezone.now().year,
"GLOBAL_HEADER": setting.global_header,
"GLOBAL_FOOTER": setting.global_footer,
"COMMENT_NEED_REVIEW": setting.comment_need_review,
'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启网站评论
'BEIAN_CODE': setting.beian_code, # 网站备案号
'ANALYTICS_CODE': setting.analytics_code, # 统计分析代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, # 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, # 是否显示公安备案号
"CURRENT_YEAR": timezone.now().year, # 当前年份(用于页脚等位置)
"GLOBAL_HEADER": setting.global_header, # 全局头部代码
"GLOBAL_FOOTER": setting.global_footer, # 全局底部代码
"COMMENT_NEED_REVIEW": setting.comment_need_review, # 评论是否需要审核
}
cache.set(key, value, 60 * 60 * 10)
#ymq将生成的上下文数据存入缓存有效期10小时60秒*60分*10小时
return value
#ymq返回构建好的上下文数据字典
=======
def seo_processor(requests):#lxy SEO相关上下文处理器
key = 'seo_processor'#lxy 缓存键名
value = cache.get(key)#lxy 从缓存取数据
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,26 +1,51 @@
import time
#ymq导入time模块用于处理时间相关操作如生成唯一ID
import elasticsearch.client
#ymq导入elasticsearch客户端模块用于操作Elasticsearch的Ingest API
from django.conf import settings
#ymq导入Django的settings模块用于获取项目配置
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
#ymq导入elasticsearch-dsl相关类用于定义Elasticsearch文档结构和字段类型
from elasticsearch_dsl.connections import connections
#ymq导入elasticsearch-dsl的连接管理工具用于创建与Elasticsearch的连接
from blog.models import Article
#ymq从blog应用导入Article模型用于同步数据到Elasticsearch
#ymq判断是否启用Elasticsearch检查settings中是否配置了ELASTICSEARCH_DSL
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
#lxy 判断是否启用ES
if ELASTICSEARCH_ENABLED:
<<<<<<< HEAD
#ymq如果启用Elasticsearch创建连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
#ymq创建Elasticsearch客户端实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
#ymq创建Ingest客户端用于管理数据处理管道
=======
connections.create_connection(#lxy 创建ES连接
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch#lxy 导入ES客户端
c = IngestClient(es)
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])#lxy 初始化ES客户端
from elasticsearch.client import IngestClient#lxy 导入Ingest客户端
>>>>>>> LXY_branch
c = IngestClient(es) #lxy 初始化Ingest客户端
try:
<<<<<<< HEAD
#ymq尝试获取名为'geoip'的管道,检查是否已存在
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
#ymq如果管道不存在则创建它用于解析IP地址的地理位置信息
=======
c.get_pipeline('geoip')#lxy 检查geoip管道是否存在
except elasticsearch.exceptions.NotFoundError:#lxy 创建geoip管道解析IP地理信息
>>>>>>> LXY_branch
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -33,181 +58,338 @@ if ELASTICSEARCH_ENABLED:
}''')
<<<<<<< HEAD
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
#ymq定义地理位置信息的内部文档嵌套结构
continent_name = Keyword() # 大陆名称(关键字类型,不分词)
country_iso_code = Keyword() # 国家ISO代码关键字类型
country_name = Keyword() # 国家名称(关键字类型)
location = GeoPoint() # 地理位置坐标(经纬度)
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
#ymq定义用户代理中浏览器信息的内部文档
Family = Keyword() # 浏览器家族如Chrome、Firefox
Version = Keyword() # 浏览器版本
class UserAgentOS(UserAgentBrowser):
#ymq定义用户代理中操作系统信息的内部文档继承浏览器结构
pass
=======
class GeoIp(InnerDoc): #lxy IP地理信息嵌套文档
continent_name = Keyword() #lxy 大洲名称
country_iso_code = Keyword() #lxy 国家ISO代码
country_name = Keyword() #lxy 国家名称
location = GeoPoint() #lxy 地理位置坐标
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgentBrowser(InnerDoc): #lxy 浏览器信息嵌套文档
Family = Keyword() #lxy 浏览器类型
Version = Keyword() #lxy 浏览器版本
class UserAgentOS(UserAgentBrowser): #lxy 操作系统信息嵌套文档
pass #lxy 继承浏览器文档结构
>>>>>>> LXY_branch
class UserAgentDevice(InnerDoc): #lxy 设备信息嵌套文档
Family = Keyword() #lxy 设备类型
Brand = Keyword() #lxy 设备品牌
Model = Keyword() #lxy 设备型号
<<<<<<< HEAD
class UserAgentDevice(InnerDoc):
#ymq定义用户代理中设备信息的内部文档
Family = Keyword() # 设备家族
Brand = Keyword() # 设备品牌
Model = Keyword() # 设备型号
=======
class UserAgent(InnerDoc): #lxy 用户代理信息嵌套文档
browser = Object(UserAgentBrowser, required=False) #lxy 浏览器信息
os = Object(UserAgentOS, required=False) #lxy 操作系统信息
device = Object(UserAgentDevice, required=False) #lxy 设备信息
string = Text() #lxy 用户代理原始字符串
is_bot = Boolean() #lxy 是否为爬虫
>>>>>>> LXY_branch
class ElapsedTimeDocument(Document): # lxy 耗时统计ES文档类
url = Keyword() # lxy 请求URL
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):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
#ymq定义用户代理完整信息的内部文档嵌套结构
browser = Object(UserAgentBrowser, required=False) # 浏览器信息
os = Object(UserAgentOS, required=False) # 操作系统信息
device = Object(UserAgentDevice, required=False) # 设备信息
string = Text() # 原始用户代理字符串
is_bot = Boolean() # 是否为爬虫
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
#ymq定义用于记录性能耗时的Elasticsearch文档
url = Keyword() # 访问的URL关键字类型
time_taken = Long() # 耗时(毫秒)
log_datetime = Date() # 日志记录时间
ip = Keyword() # 访问IP地址
geoip = Object(GeoIp, required=False) # 地理位置信息(嵌套)
useragent = Object(UserAgent, required=False) # 用户代理信息(嵌套)
class Index:
name = 'performance'
#ymq定义索引配置
name = 'performance' # 索引名称
settings = {
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
=======
class Index: #lxy ES索引配置
name = 'performance' #lxy 索引名称
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
"number_of_shards": 1, #lxy 分片数
"number_of_replicas": 0 #lxy 副本数
>>>>>>> LXY_branch
}
class Meta: #lxy 文档元信息
doc_type = 'ElapsedTime' #lxy 文档类型
<<<<<<< HEAD
class Meta:
doc_type = 'ElapsedTime'
doc_type = 'ElapsedTime' # 文档类型Elasticsearch 7.x后逐渐废弃
class ElaspedTimeDocumentManager:
#ymq性能耗时文档的管理类用于索引的创建、删除和数据插入
@staticmethod
def build_index():
#ymq创建索引如果不存在
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
res = client.indices.exists(index="performance") # 检查索引是否存在
if not res:
ElapsedTimeDocument.init()
ElapsedTimeDocument.init() # 初始化索引
@staticmethod
def delete_index():
#ymq删除performance索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
es.indices.delete(index='performance', ignore=[400, 404]) # 忽略不存在的情况
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
ElaspedTimeDocumentManager.build_index()
#ymq创建一条性能耗时记录
ElaspedTimeDocumentManager.build_index() # 确保索引存在
#ymq构建用户代理信息对象
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.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
<<<<<<< HEAD
#ymq创建文档实例使用时间戳作为唯一ID
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
1000))
1000)) # 毫秒级时间戳作为ID
=======
doc = ElapsedTimeDocument( #lxy 构造耗时文档
meta={
'id': int(round(time.time() * 10000)) #lxy 生成唯一ID
>>>>>>> LXY_branch
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
url=url, time_taken=time_taken, log_datetime=log_datetime,
useragent=ua, ip=ip)
<<<<<<< HEAD
#ymq保存文档时应用geoip管道解析IP地址
doc.save(pipeline="geoip")
class ArticleDocument(Document):
#ymq定义文章信息的Elasticsearch文档用于搜索
#ymqbody和title使用IK分词器max_word分词更细smart更简洁
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
#ymq嵌套作者信息
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
#ymq嵌套分类信息
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
#ymq嵌套标签信息数组
tags = Object(properties={
=======
doc.save(pipeline="geoip") #lxy 保存文档用geoip管道解析IP
class ArticleDocument(Document): #lxy 文章ES文档类
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'),
'id': Integer()
})
pub_time = Date()
status = Text()
comment_status = Text()
type = Text()
views = Integer()
article_order = Integer()
tags = Object(properties={ #lxy 标签信息
>>>>>>> LXY_branch
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
pub_time = Date() #lxy 发布时间
status = Text() #lxy 文章状态
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:
name = 'blog'
name = 'blog' # 索引名称
=======
class Index: #lxy 文章索引配置
name = 'blog' #lxy 索引名称
>>>>>>> LXY_branch
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
"number_of_shards": 1, #lxy 分片数
"number_of_replicas": 0 #lxy 副本数
}
class Meta: #lxy 文档元信息
doc_type = 'Article' #lxy 文档类型
<<<<<<< HEAD
class Meta:
doc_type = 'Article'
doc_type = 'Article' # 文档类型
class ArticleDocumentManager():
#ymq文章文档的管理类用于索引操作和数据同步
def __init__(self):
#ymq初始化时创建索引
self.create_index()
def create_index(self):
#ymq初始化文章索引
ArticleDocument.init()
def delete_index(self):
#ymq删除blog索引
=======
class ArticleDocumentManager():#lxy 文章文档管理类
def __init__(self): #lxy 初始化方法
self.create_index()
def create_index(self):#lxy 创建文章索引
ArticleDocument.init()
def delete_index(self): #lxy 删除文章索引
>>>>>>> LXY_branch
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
<<<<<<< HEAD
def convert_to_doc(self, articles):
#ymq将Django模型对象转换为Elasticsearch文档对象
=======
def convert_to_doc(self, articles):#lxy 文章对象转ES文档
>>>>>>> LXY_branch
return [
ArticleDocument(
meta={
'id': article.id},
meta={'id': article.id}, # 使用文章ID作为文档ID
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
'id': article.author.id
},
category={
'name': article.category.name,
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
'id': article.category.id
},
tags=[{'name': t.name, 'id': t.id} for t in article.tags.all()], # 转换多对多标签
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
article_order=article.article_order
) for article in articles
]
<<<<<<< HEAD
def rebuild(self, articles=None):
#ymq重建索引默认同步所有文章可指定文章列表
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
articles = articles if articles else Article.objects.all() # 获取文章数据
docs = self.convert_to_doc(articles) # 转换为文档对象
for doc in docs:
doc.save()
doc.save() # 保存到Elasticsearch
def update_docs(self, docs):
#ymq批量更新文档
for doc in docs:
doc.save()
=======
def rebuild(self, articles=None):#lxy 重建文章索引
ArticleDocument.init()
articles = articles if articles else Article.objects.all()#lxy 获取所有文章
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,19 +1,53 @@
<<<<<<< HEAD
import logging #导入 Python 标准库的 logging 模块,用于日志记录,方便追踪程序运行过程中的关键信息。
=======
import logging
#ymq导入logging模块用于记录搜索相关日志
>>>>>>> c6856732b39cce6b1aab30e6649dcdb806b75b9f
from django import forms
#ymq导入Django的forms模块用于创建自定义表单
from haystack.forms import SearchForm
#ymq导入Haystack的SearchForm基类扩展实现博客搜索表单
<<<<<<< HEAD
logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
class BlogSearchForm(SearchForm):
#ymq定义博客搜索表单类继承自Haystack的SearchForm
querydata = forms.CharField(required=True)
#ymq定义搜索关键词字段required=True表示该字段为必填项
def search(self):
#ymq重写父类的search方法自定义搜索逻辑
datas = super(BlogSearchForm, self).search()
#ymq调用父类search方法获取基础搜索结果
if not self.is_valid():
#ymq如果表单数据验证不通过返回无结果响应
return self.no_query_found()
if self.cleaned_data['querydata']:
#ymq如果存在合法的搜索关键词记录关键词日志
logger.info(self.cleaned_data['querydata'])
return datas
#ymq返回最终的搜索结果集
=======
logger = logging.getLogger(__name__)#lxy 获取当前模块的日志记录器
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,18 +1,31 @@
from django.core.management.base import BaseCommand
#ymq导入Django的BaseCommand类用于创建自定义管理命令
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
#ymq从blog.documents导入Elasticsearch相关的文档类和管理器以及启用状态常量
# TODO 参数化
class Command(BaseCommand):
#ymq定义自定义管理命令类继承自BaseCommand
help = 'build search index'
#ymq定义命令的帮助信息使用python manage.py help build_index时显示
def handle(self, *args, **options):
#ymq命令的核心处理方法执行实际的索引构建逻辑
if ELASTICSEARCH_ENABLED:
#ymq仅当Elasticsearch启用时执行以下操作
ElaspedTimeDocumentManager.build_index()
#ymq调用性能耗时文档管理器构建索引若不存在
manager = ElapsedTimeDocument()
manager.init()
#ymq初始化ElapsedTimeDocument对应的索引结构
manager = ArticleDocumentManager()
manager.delete_index()
#ymq删除已存在的文章索引重建前清理
manager.rebuild()
#ymq重建文章索引将数据库中的文章数据同步到Elasticsearch

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,73 +1,132 @@
import os
#ymq导入os模块用于文件路径和文件操作
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
#ymq导入文件上传相关类用于模拟文件上传测试
from django.core.management import call_command
#ymq导入call_command用于调用Django管理命令
from django.core.paginator import Paginator
#ymq导入分页类用于测试分页功能
from django.templatetags.static import static
#ymq导入static标签用于获取静态文件路径
from django.test import Client, RequestFactory, TestCase
#ymq导入测试相关类Client用于模拟HTTP请求TestCase提供测试框架
from django.urls import reverse
#ymq导入reverse用于反向解析URL
from django.utils import timezone
#ymq导入timezone用于处理时间相关测试数据
from accounts.models import BlogUser
#ymq从accounts应用导入用户模型
from blog.forms import BlogSearchForm
#ymq从blog应用导入搜索表单
from blog.models import Article, Category, Tag, SideBar, Links
#ymq从blog应用导入模型类用于测试数据创建和查询
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
#ymq导入自定义模板标签函数用于测试模板标签功能
from djangoblog.utils import get_current_site, get_sha256
#ymq导入工具函数用于获取站点信息和加密
from oauth.models import OAuthUser, OAuthConfig
#ymq从oauth应用导入模型用于测试第三方登录相关功能
# Create your tests here.
<<<<<<< HEAD
class ArticleTest(TestCase):
#ymq定义文章相关的测试类继承自TestCase
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
#ymq测试前置方法在每个测试方法执行前运行初始化测试客户端和工厂
self.client = Client() # 创建测试客户端用于模拟HTTP请求
self.factory = RequestFactory() # 创建请求工厂,用于构造请求对象
def test_validate_article(self):
site = get_current_site().domain
#ymq测试文章相关功能的完整性包括创建、查询、页面访问等
site = get_current_site().domain # 获取当前站点域名
# 创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy") # 设置用户密码
user.is_staff = True # 设为 staff允许访问admin
user.is_superuser = True # 设为超级用户
user.save() # 保存用户
# 测试用户个人页面访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200) # 断言页面正常响应
# 测试admin相关页面访问未登录状态
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
# 创建侧边栏测试数据
=======
class ArticleTest(TestCase): # lxy 文章相关测试类
def setUp(self): # lxy 测试初始化
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.is_staff = True
user.is_superuser = True
user.save()
# 测试用户页面访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
self.assertEqual(response.status_code, 200) # lxy 断言状态码200
# 访问后台页面(无权限,仅请求)
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('admin/admin/logentry/')
# 创建测试侧边栏
>>>>>>> LXY_branch
s = SideBar()
s.sequence = 1
s.name = 'test'
s.content = 'test content'
s.is_enable = True
s.save()
<<<<<<< HEAD
# 创建分类测试数据
=======
# 创建测试分类
>>>>>>> LXY_branch
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
# 创建标签测试数据
tag = Tag()
tag.name = "nicetag"
tag.save()
# 创建文章测试数据
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.type = 'a' # 类型为文章
article.status = 'p' # 状态为已发布
article.save()
self.assertEqual(0, article.tags.count())
article.tags.add(tag)
# 测试文章标签关联
self.assertEqual(0, article.tags.count()) # 初始无标签
article.tags.add(tag) # 添加标签
article.save()
self.assertEqual(1, article.tags.count())
self.assertEqual(1, article.tags.count()) # 断言标签已添加
# 批量创建文章(用于测试分页)
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@ -79,56 +138,73 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
# 测试Elasticsearch搜索功能如果启用
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
call_command("build_index") # 调用命令构建索引
response = self.client.get('/search', {'q': 'nicetitle'}) # 模拟搜索请求
self.assertEqual(response.status_code, 200) # 断言搜索页面正常响应
# 测试文章详情页访问
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试蜘蛛通知功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
SpiderNotify.notify(article.get_absolute_url()) # 通知搜索引擎
# 测试标签页访问
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试分类页访问
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试搜索功能
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
# 测试文章模板标签
s = load_articletags(article)
self.assertIsNotNone(s)
self.assertIsNotNone(s) # 断言标签返回非空
# 用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
# 测试归档页面访问
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# 测试不同类型的分页功能
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
self.check_pagination(p, '', '') # 全部文章分页
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
self.check_pagination(p, '分类标签归档', tag.slug) # 标签文章分页
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
self.check_pagination(p, '作者文章归档', 'liangliangyy') # 作者文章分页
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
self.check_pagination(p, '分类目录归档', category.slug) # 分类文章分页
# 测试搜索表单
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
from djangoblog.spider_notify import SpiderNotify
f.search() # 调用搜索方法
# 测试百度蜘蛛通知
SpiderNotify.baidu_notify([article.get_full_url()])
# 测试头像相关模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
u = gravatar_url('liangliangyy@gmail.com') # 获取头像URL
u = gravatar('liangliangyy@gmail.com') # 生成头像HTML
# 测试友情链接页面
link = Links(
sequence=1,
name="lylinux",
@ -136,57 +212,83 @@ class ArticleTest(TestCase):
link.save()
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# 测试RSS订阅和站点地图
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# 测试admin操作删除文章、访问日志
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value):
#ymq测试分页功能的辅助方法
for page in range(1, p.num_pages + 1):
# 调用分页模板标签获取分页信息
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
self.assertIsNotNone(s) # 断言分页信息非空
# 测试上一页链接
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
# 测试下一页链接
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
#ymq测试图片上传功能
import requests
# 下载测试图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png') # 保存路径
with open(imagepath, 'wb') as file:
file.write(rsp.content)
file.write(rsp.content) # 保存图片
# 测试未授权上传
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
self.assertEqual(rsp.status_code, 403) # 断言被拒绝
# 生成上传签名(模拟授权)
sign = get_sha256(get_sha256(settings.SECRET_KEY))
# 模拟带签名的上传请求
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile}
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
os.remove(imagepath)
self.assertEqual(rsp.status_code, 200) # 断言上传成功
os.remove(imagepath) # 清理测试文件
# 测试用户头像保存和邮件发送工具函数
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
send_email(['qq@qq.com'], 'testTitle', 'testContent') # 测试发送邮件
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
'https://www.python.org/static/img/python-logo.png') # 测试保存头像
<<<<<<< HEAD
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
#ymq测试错误页面404
rsp = self.client.get('/eee') # 访问不存在的URL
self.assertEqual(rsp.status_code, 404) # 断言返回404
def test_commands(self):
#ymq测试Django管理命令
# 创建测试用户
=======
def test_errorpage(self): # lxy 错误页面测试
rsp = self.client.get('/eee') # lxy 访问不存在的路径
self.assertEqual(rsp.status_code, 404) # lxy 断言404状态码
def test_commands(self):#lxy 命令行指令测试
>>>>>>> LXY_branch
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -194,13 +296,15 @@ class ArticleTest(TestCase):
user.is_staff = True
user.is_superuser = True
user.save()
# 创建OAuth配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
# 创建OAuth用户关联
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
@ -211,7 +315,8 @@ class ArticleTest(TestCase):
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
# 创建另一个OAuth用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -221,12 +326,26 @@ class ArticleTest(TestCase):
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
# 测试Elasticsearch索引构建命令如果启用
from blog.documents import ELASTICSEARCH_ENABLED
<<<<<<< HEAD
if ELASTICSEARCH_ENABLED:
call_command("build_index")
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")
# 测试其他管理命令
call_command("ping_baidu", "all") # 百度ping通知
call_command("create_testdata") # 创建测试数据
call_command("clear_cache") # 清理缓存
call_command("sync_user_avatar") # 同步用户头像
call_command("build_search_words") # 构建搜索词
=======
if ELASTICSEARCH_ENABLED:
call_command("build_index") # lxy 构建ES索引
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,62 +1,153 @@
from django.urls import path
#ymq导入Django的path函数用于定义URL路由
from django.views.decorators.cache import cache_page
#ymq导入缓存装饰器用于对视图进行缓存
from . import views
#ymq从当前应用导入views模块引用视图函数/类
<<<<<<< HEAD
app_name = "blog"
#ymq定义应用命名空间避免URL名称冲突
=======
app_name = "blog" #lxy 应用命名空间
>>>>>>> LXY_branch
urlpatterns = [
path(
r'',
views.IndexView.as_view(),
<<<<<<< HEAD
name='index'),
#ymq首页URL映射到IndexView视图类名称为'index'
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
#ymq分页首页URL接收整数类型的page参数名称为'index_page'
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
#ymq文章详情页URL接收年、月、日、文章ID参数名称为'detailbyid'
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
#ymq分类详情页URL接收slug类型的分类名称参数名称为'category_detail'
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
#ymq分类分页详情页URL接收分类名称和页码参数名称为'category_detail_page'
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
#ymq作者详情页URL接收作者名称参数名称为'author_detail'
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
#ymq作者分页详情页URL接收作者名称和页码参数名称为'author_detail_page'
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
#ymq标签详情页URL接收slug类型的标签名称参数名称为'tag_detail'
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
#ymq标签分页详情页URL接收标签名称和页码参数名称为'tag_detail_page'
=======
name='index'),#lxy 路由名称:首页
path(
r'page/<int:page>/',
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(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
<<<<<<< HEAD
name='archives'),
#ymq归档页面URL使用cache_page装饰器缓存1小时60*60秒名称为'archives'
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
#ymq友情链接页面URL映射到LinkListView视图类名称为'links'
path(
r'upload',
views.fileupload,
name='upload'),
#ymq文件上传URL映射到fileupload视图函数名称为'upload'
path(
r'clean',
views.clean_cache_view,
name='clean'),
#ymq清理缓存URL映射到clean_cache_view视图函数名称为'clean'
]
=======
name='archives'),#lxy 路由名称:归档页
path(
'links.html',
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,6 +1,7 @@
import logging
import os
import uuid
#ymq导入日志、文件操作、UUID生成相关模块
from django.conf import settings
from django.core.paginator import Paginator
@ -14,17 +15,25 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
#ymq导入Django核心组件、视图类、HTTP响应类等
from blog.models import Article, Category, LinkShowType, Links, Tag
#ymq从blog应用导入模型类
from comments.forms import CommentForm
#ymq从comments应用导入评论表单
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
#ymq导入插件钩子相关模块用于扩展文章功能
from djangoblog.utils import cache, get_blog_setting, get_sha256
#ymq导入工具函数用于缓存、获取博客设置和加密
logger = logging.getLogger(__name__)
#ymq创建当前模块的日志记录器实例
<<<<<<< HEAD
class ArticleListView(ListView):
#ymq文章列表基础视图类继承自Django的ListView
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
@ -33,33 +42,50 @@ class ArticleListView(ListView):
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
paginate_by = settings.PAGINATE_BY # 分页大小,从配置中获取
page_kwarg = 'page' # 页码参数名
link_type = LinkShowType.L # 链接展示类型
def get_view_cache_key(self):
#ymq获取视图缓存键未实际使用预留方法
return self.request.get['pages']
@property
def page_number(self):
#ymq获取当前页码从URL参数或默认值
=======
class ArticleListView(ListView): #lxy 文章列表视图基类
template_name = 'blog/article_index.html' #lxy 渲染模板
context_object_name = 'article_list' #lxy 模板中数据变量名
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 = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
def get_queryset_cache_key(self):
def get_queryset_cache_key(self):#lxy 子类需重写:获取查询集缓存键
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
raise NotImplementedError() # 强制子类实现该方法
def get_queryset_data(self):
def get_queryset_data(self):#lxy 子类需重写:获取查询集数据
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
raise NotImplementedError() # 强制子类实现该方法
def get_queryset_from_cache(self, cache_key):
def get_queryset_from_cache(self, cache_key):#lxy 从缓存取查询集
'''
缓存页面数据
:param cache_key: 缓存key
@ -70,56 +96,79 @@ class ArticleListView(ListView):
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
<<<<<<< HEAD
article_list = self.get_queryset_data() # 调用子类实现的方法获取数据
cache.set(cache_key, article_list) # 存入缓存
=======
article_list = self.get_queryset_data()#lxy 从数据库取数据
cache.set(cache_key, article_list)#lxy 写入缓存
>>>>>>> LXY_branch
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
def get_queryset(self):
def get_queryset(self):#lxy 获取查询集(优先缓存)
'''
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
key = self.get_queryset_cache_key() # 获取缓存键
value = self.get_queryset_from_cache(key) # 从缓存获取数据
return value
<<<<<<< HEAD
def get_context_data(self, **kwargs):
#ymq扩展上下文数据添加链接类型
=======
def get_context_data(self, **kwargs):#lxy 补充上下文数据
>>>>>>> LXY_branch
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
return super(ArticleListView, self).get_context_data(** kwargs)
class IndexView(ArticleListView):
class IndexView(ArticleListView):#lxy 首页视图
'''
首页
首页视图
'''
# 友情链接类型
<<<<<<< HEAD
# 友情链接类型:首页展示
link_type = LinkShowType.I
def get_queryset_data(self):
#ymq获取首页文章列表已发布的文章
=======
# 友情链接类型
link_type = LinkShowType.I#lxy 首页友链展示类型
def get_queryset_data(self): #lxy 获取首页文章(已发布)
>>>>>>> LXY_branch
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
#ymq生成首页缓存键包含页码
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面
文章详情页面视图
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
template_name = 'blog/article_detail.html' # 详情页模板
model = Article # 关联模型
pk_url_kwarg = 'article_id' # URL中主键参数名
context_object_name = "article" # 模板中上下文变量名
def get_context_data(self, **kwargs):
comment_form = CommentForm()
#ymq扩展文章详情页的上下文数据
comment_form = CommentForm() # 初始化评论表单
# 获取文章评论列表
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
parent_comments = article_comments.filter(parent_comment=None) # 过滤顶级评论
blog_setting = get_blog_setting() # 获取博客设置
# 评论分页处理
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
@ -135,51 +184,74 @@ class ArticleDetailView(DetailView):
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
# 生成评论分页链接
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 向上下文添加数据
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
<<<<<<< HEAD
# 上一篇/下一篇文章
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
=======
kwargs['next_article'] = self.object.next_article # lxy 下一篇文章
kwargs['prev_article'] = self.object.prev_article # lxy 上一篇文章
>>>>>>> LXY_branch
# 调用父类方法获取基础上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
# 触发插件钩子:文章详情已获取
hooks.run_action('after_article_body_get', article=article, request=self.request)
# # Filter Hook, 允许插件修改文章正文
# 应用插件过滤器:修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
return context
class CategoryDetailView(ArticleListView):
class CategoryDetailView(ArticleListView):#lxy 分类详情视图
'''
分类目录列表
分类目录列表视图
'''
page_type = "分类目录归档"
<<<<<<< HEAD
page_type = "分类目录归档" # 页面类型标识
def get_queryset_data(self):
#ymq获取指定分类下的文章列表
slug = self.kwargs['category_name'] # 从URL获取分类别名
category = get_object_or_404(Category, slug=slug) # 获取分类对象
=======
page_type = "分类目录归档"#lxy 页面类型
def get_queryset_data(self):#lxy 获取分类下文章
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
>>>>>>> LXY_branch
categoryname = category.name
self.categoryname = categoryname
# 获取所有子分类名称
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
# 查询属于当前分类及子分类的已发布文章
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
#ymq生成分类列表缓存键
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
@ -189,59 +261,69 @@ class CategoryDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
#ymq扩展分类页上下文数据
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1]
categoryname = categoryname.split('/')[-1] # 处理多级分类名称
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
return super(CategoryDetailView, self).get_context_data(** kwargs)
class AuthorDetailView(ArticleListView):
'''
作者详情页
作者详情页视图
'''
page_type = '作者文章归档'
page_type = '作者文章归档' # 页面类型标识
def get_queryset_cache_key(self):
#ymq生成作者文章列表缓存键
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
author_name = slugify(self.kwargs['author_name']) # 作者名转slug
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
def get_queryset_data(self):
#ymq获取指定作者的文章列表
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
author__username=author_name, type='a', status='p') # 过滤已发布的文章
return article_list
def get_context_data(self, **kwargs):
#ymq扩展作者页上下文数据
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
return super(AuthorDetailView, self).get_context_data(** kwargs)
class TagDetailView(ArticleListView):
'''
标签列表页面
标签列表页面视图
'''
page_type = '分类标签归档'
page_type = '分类标签归档' # 页面类型标识
def get_queryset_data(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
#ymq获取指定标签的文章列表
slug = self.kwargs['tag_name'] # 从URL获取标签别名
tag = get_object_or_404(Tag, slug=slug) # 获取标签对象
tag_name = tag.name
self.name = tag_name
# 查询包含当前标签的已发布文章
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
<<<<<<< HEAD
def get_queryset_cache_key(self):
#ymq生成标签文章列表缓存键
=======
def get_queryset_cache_key(self):#lxy 标签缓存键(含标签名、页码)
>>>>>>> LXY_branch
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
@ -251,101 +333,139 @@ class TagDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
#ymq扩展标签页上下文数据
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
return super(TagDetailView, self).get_context_data(** kwargs)
class ArchivesView(ArticleListView):
class ArchivesView(ArticleListView):#lxy 文章归档视图
'''
文章归档页面
文章归档页面视图
'''
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
page_type = '文章归档' # 页面类型标识
paginate_by = None # 不分页
page_kwarg = None # 无页码参数
template_name = 'blog/article_archives.html' # 归档页模板
<<<<<<< HEAD
def get_queryset_data(self):
#ymq获取所有已发布文章用于归档
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
#ymq生成归档页缓存键
=======
def get_queryset_data(self):#lxy 获取所有已发布文章
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):#lxy 归档缓存键
>>>>>>> LXY_branch
cache_key = 'archives'
return cache_key
<<<<<<< HEAD
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
#ymq友情链接列表视图
model = Links # 关联模型
template_name = 'blog/links_list.html' # 链接列表模板
def get_queryset(self):
#ymq只获取启用的友情链接
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
#ymqElasticsearch搜索视图继承自Haystack的SearchView
def get_context(self):
#ymq构建搜索结果页面的上下文数据
paginator, page = self.build_page() # 处理分页
=======
class LinkListView(ListView):#lxy 友链列表视图
model = Links
template_name = 'blog/links_list.html' #lxy 渲染模板
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()
>>>>>>> LXY_branch
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
"query": self.query, # 搜索关键词
"form": self.form, # 搜索表单
"page": page, # 当前页数据
"paginator": paginator, # 分页器
"suggestion": None, # 搜索建议(默认无)
}
# 如果启用拼写建议,添加建议内容
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
context.update(self.extra_context()) # 添加额外上下文
return context
@csrf_exempt
@csrf_exempt # 禁用CSRF保护用于外部调用
def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
图片/文件上传接口需验证签名
:param request:
:return:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
sign = request.GET.get('sign', None) # 获取签名参数
if not sign:
return HttpResponseForbidden()
return HttpResponseForbidden() # 无签名则拒绝
# 验证签名双重SHA256加密对比
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
return HttpResponseForbidden() # 签名错误则拒绝
response = [] # 存储上传后的文件URL
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
timestr = timezone.now().strftime('%Y/%m/%d') # 按日期组织文件
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # 图片扩展名
fname = u''.join(str(filename))
# 判断是否为图片
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
# 确定存储目录(图片/文件分开存储)
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
os.makedirs(base_dir) # 目录不存在则创建
# 生成唯一文件名UUID+原扩展名)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 安全校验:防止路径遍历攻击
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
# 保存文件
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# 图片压缩处理
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
image.save(savepath, quality=20, optimize=True) # 压缩质量为20
# 生成文件访问URL
url = static(savepath)
response.append(url)
return HttpResponse(response)
return HttpResponse(response) # 返回所有上传文件的URL
else:
return HttpResponse("only for post")
return HttpResponse("only for post") # 只允许POST方法
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
#ymq404错误处理视图
if exception:
logger.error(exception)
logger.error(exception) # 记录错误日志
url = request.get_full_path()
return render(request,
template_name,
@ -355,6 +475,7 @@ def page_not_found_view(
def server_error_view(request, template_name='blog/error_page.html'):
#ymq500错误处理视图
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
@ -366,8 +487,9 @@ def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
#ymq403错误处理视图
if exception:
logger.error(exception)
logger.error(exception) # 记录错误日志
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
@ -375,5 +497,6 @@ def permission_denied_view(
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
#ymq清理缓存的视图
cache.clear() # 清除所有缓存
return HttpResponse('ok') # 返回成功响应

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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