diff --git a/src/django-master/accounts/admin.py b/src/django-master/accounts/admin.py
index 7409b10..d52678e 100644
--- a/src/django-master/accounts/admin.py
+++ b/src/django-master/accounts/admin.py
@@ -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
diff --git a/src/django-master/accounts/apps.py b/src/django-master/accounts/apps.py
index 5129d43..087d079 100644
--- a/src/django-master/accounts/apps.py
+++ b/src/django-master/accounts/apps.py
@@ -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'#指定该应用的名称为 accounts,Django 会通过这个名称来识别和管理该应用
>>>>>>> LXY_branch
+=======
+from django.apps import AppConfig#lxy导入 Django 框架中用于应用配置的 AppConfig 类,这是 Django 应用配置的核心类
+
+
+class AccountsConfig(AppConfig):#lxy定义 AccountsConfig 类,继承自 AppConfig,用于对 accounts 应用进行自定义配置
+ name = 'accounts'#lxy指定该应用的名称为 accounts,Django 会通过这个名称来识别和管理该应用
+>>>>>>> LXY_branch
+=======
+ # jyn:应用的名称,必须与应用目录名一致
+ # jyn:这个名称会被Django用来识别和管理应用
+ name = 'accounts'
+>>>>>>> JYN_branch
diff --git a/src/django-master/accounts/forms.py b/src/django-master/accounts/forms.py
index df3e0f8..3ac3f11 100644
--- a/src/django-master/accounts/forms.py
+++ b/src/django-master/accounts/forms.py
@@ -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
diff --git a/src/django-master/accounts/models.py b/src/django-master/accounts/models.py
index 0662c2b..7971f1d 100644
--- a/src/django-master/accounts/models.py
+++ b/src/django-master/accounts/models.py
@@ -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
diff --git a/src/django-master/accounts/tests.py b/src/django-master/accounts/tests.py
index 32e5973..7001dc8 100644
--- a/src/django-master/accounts/tests.py
+++ b/src/django-master/accounts/tests.py
@@ -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
diff --git a/src/django-master/accounts/urls.py b/src/django-master/accounts/urls.py
index 3a0b0b3..81e18cd 100644
--- a/src/django-master/accounts/urls.py
+++ b/src/django-master/accounts/urls.py
@@ -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
diff --git a/src/django-master/accounts/user_login_backend.py b/src/django-master/accounts/user_login_backend.py
index a2d8f85..6f85886 100644
--- a/src/django-master/accounts/user_login_backend.py
+++ b/src/django-master/accounts/user_login_backend.py
@@ -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认证后端,
# 用户不存在时返回 None(Django 会继续尝试其他认证后端)
=======
# 根据上述条件从数据库查询唯一用户
+=======
+ # 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
diff --git a/src/django-master/accounts/utils.py b/src/django-master/accounts/utils.py
index 2e649f0..4428b57 100644
--- a/src/django-master/accounts/utils.py
+++ b/src/django-master/accounts/utils.py
@@ -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
diff --git a/src/django-master/accounts/views.py b/src/django-master/accounts/views.py
index c6f0601..ac71db9 100644
--- a/src/django-master/accounts/views.py
+++ b/src/django-master/accounts/views.py
@@ -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 = """
请点击下面链接验证您的邮箱
@@ -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
diff --git a/src/django-master/blog/admin.py b/src/django-master/blog/admin.py
index 46c3420..9c65461 100644
--- a/src/django-master/blog/admin.py
+++ b/src/django-master/blog/admin.py
@@ -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:
+ #ymq:Meta类用于配置表单元数据
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'%s' % (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行为
\ No newline at end of file
diff --git a/src/django-master/blog/apps.py b/src/django-master/blog/apps.py
index 7930587..ce813c5 100644
--- a/src/django-master/blog/apps.py
+++ b/src/django-master/blog/apps.py
@@ -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
diff --git a/src/django-master/blog/context_processors.py b/src/django-master/blog/context_processors.py
index 73e3088..21d681d 100644
--- a/src/django-master/blog/context_processors.py
+++ b/src/django-master/blog/context_processors.py
@@ -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
diff --git a/src/django-master/blog/documents.py b/src/django-master/blog/documents.py
index 0f1db7b..65383a6 100644
--- a/src/django-master/blog/documents.py
+++ b/src/django-master/blog/documents.py
@@ -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文档(用于搜索)
+ #ymq:body和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
diff --git a/src/django-master/blog/forms.py b/src/django-master/blog/forms.py
index 1082938..0705104 100644
--- a/src/django-master/blog/forms.py
+++ b/src/django-master/blog/forms.py
@@ -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
diff --git a/src/django-master/blog/management/commands/build_index.py b/src/django-master/blog/management/commands/build_index.py
index 3c4acd7..69a4490 100644
--- a/src/django-master/blog/management/commands/build_index.py
+++ b/src/django-master/blog/management/commands/build_index.py
@@ -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
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/build_search_words.py b/src/django-master/blog/management/commands/build_search_words.py
index cfe7e0d..35d0c6e 100644
--- a/src/django-master/blog/management/commands/build_search_words.py
+++ b/src/django-master/blog/management/commands/build_search_words.py
@@ -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))
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/clear_cache.py b/src/django-master/blog/management/commands/clear_cache.py
index 0d66172..6366680 100644
--- a/src/django-master/blog/management/commands/clear_cache.py
+++ b/src/django-master/blog/management/commands/clear_cache.py
@@ -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样式(通常为绿色)
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/create_testdata.py b/src/django-master/blog/management/commands/create_testdata.py
index 675d2ba..a3dcce8 100644
--- a/src/django-master/blog/management/commands/create_testdata.py
+++ b/src/django-master/blog/management/commands/create_testdata.py
@@ -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'))
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/ping_baidu.py b/src/django-master/blog/management/commands/ping_baidu.py
index 2c7fbdd..092ed48 100644
--- a/src/django-master/blog/management/commands/ping_baidu.py
+++ b/src/django-master/blog/management/commands/ping_baidu.py
@@ -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'))
\ No newline at end of file
diff --git a/src/django-master/blog/management/commands/sync_user_avatar.py b/src/django-master/blog/management/commands/sync_user_avatar.py
index d0f4612..f51a404 100644
--- a/src/django-master/blog/management/commands/sync_user_avatar.py
+++ b/src/django-master/blog/management/commands/sync_user_avatar.py
@@ -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('结束同步') # 输出同步完成提示
\ No newline at end of file
diff --git a/src/django-master/blog/middleware.py b/src/django-master/blog/middleware.py
index 94dd70c..909825d 100644
--- a/src/django-master/blog/middleware.py
+++ b/src/django-master/blog/middleware.py
@@ -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:替换响应内容中的标记为实际加载时间(保留前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'', 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
diff --git a/src/django-master/blog/migrations/0001_initial.py b/src/django-master/blog/migrations/0001_initial.py
index 3d391b6..a63b9e7 100644
--- a/src/django-master/blog/migrations/0001_initial.py
+++ b/src/django-master/blog/migrations/0001_initial.py
@@ -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获取最新记录
},
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py b/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py
index adbaa36..1304b8a 100644
--- a/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py
+++ b/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py
@@ -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为"公共头部"
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py b/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py
index e9f5502..908f852 100644
--- a/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py
+++ b/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py
@@ -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(不需要审核),后台显示名称为"评论是否需要审核"
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
index ceb1398..f6465d8 100644
--- a/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
+++ b/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
@@ -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', # 改为下划线命名规范
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
index d08e853..d06b10a 100644
--- a/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
+++ b/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
@@ -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'),
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/migrations/0006_alter_blogsettings_options.py b/src/django-master/blog/migrations/0006_alter_blogsettings_options.py
index e36feb4..207d123 100644
--- a/src/django-master/blog/migrations/0006_alter_blogsettings_options.py
+++ b/src/django-master/blog/migrations/0006_alter_blogsettings_options.py
@@ -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'},
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/blog/models.py b/src/django-master/blog/models.py
index 083788b..24a9195 100644
--- a/src/django-master/blog/models.py
+++ b/src/django-master/blog/models.py
@@ -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
diff --git a/src/django-master/blog/search_indexes.py b/src/django-master/blog/search_indexes.py
index 7f1dfac..63215cf 100644
--- a/src/django-master/blog/search_indexes.py
+++ b/src/django-master/blog/search_indexes.py
@@ -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
diff --git a/src/django-master/blog/templatetags/blog_tags.py b/src/django-master/blog/templatetags/blog_tags.py
index d6cd5d5..087c485 100644
--- a/src/django-master/blog/templatetags/blog_tags.py
+++ b/src/django-master/blog/templatetags/blog_tags.py
@@ -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(
'
' %
@@ -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)
\ No newline at end of file
diff --git a/src/django-master/blog/tests.py b/src/django-master/blog/tests.py
index ee13505..adc8e05 100644
--- a/src/django-master/blog/tests.py
+++ b/src/django-master/blog/tests.py
@@ -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
diff --git a/src/django-master/blog/urls.py b/src/django-master/blog/urls.py
index adf2703..b5d70e4 100644
--- a/src/django-master/blog/urls.py
+++ b/src/django-master/blog/urls.py
@@ -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//',
views.IndexView.as_view(),
name='index_page'),
+ #ymq:分页首页URL,接收整数类型的page参数,名称为'index_page'
+
path(
r'article////.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
+ #ymq:文章详情页URL,接收年、月、日、文章ID参数,名称为'detailbyid'
+
path(
r'category/.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
+ #ymq:分类详情页URL,接收slug类型的分类名称参数,名称为'category_detail'
+
path(
r'category//.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
+ #ymq:分类分页详情页URL,接收分类名称和页码参数,名称为'category_detail_page'
+
path(
r'author/.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
+ #ymq:作者详情页URL,接收作者名称参数,名称为'author_detail'
+
path(
r'author//.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
+ #ymq:作者分页详情页URL,接收作者名称和页码参数,名称为'author_detail_page'
+
path(
r'tag/.html',
views.TagDetailView.as_view(),
name='tag_detail'),
+ #ymq:标签详情页URL,接收slug类型的标签名称参数,名称为'tag_detail'
+
path(
r'tag//.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
+ #ymq:标签分页详情页URL,接收标签名称和页码参数,名称为'tag_detail_page'
+
+=======
+ name='index'),#lxy 路由名称:首页
+ path(
+ r'page//',
+ views.IndexView.as_view(),
+ name='index_page'),#lxy 路由名称:首页分页
+ # 文章详情路由(按日期+ID)
+ path(
+ r'article////.html',
+ views.ArticleDetailView.as_view(),
+ name='detailbyid'),#lxy 路由名称:文章详情
+ # 分类详情路由
+ path(
+ r'category/.html',
+ views.CategoryDetailView.as_view(),
+ name='category_detail'), #lxy 路由名称:分类详情
+ # 分类详情分页路由
+ path(
+ r'category//.html',
+ views.CategoryDetailView.as_view(),
+ name='category_detail_page'),#lxy 路由名称:分类详情分页
+ # 作者详情路由
+ path(
+ r'author/.html',
+ views.AuthorDetailView.as_view(),
+ name='author_detail'),#lxy 路由名称:作者详情
+ path(
+ r'author//.html',
+ views.AuthorDetailView.as_view(),
+ name='author_detail_page'),#lxy 路由名称:作者详情分页
+ path(
+ r'tag/.html',
+ views.TagDetailView.as_view(),
+ name='tag_detail'),#lxy 路由名称:标签详情
+ path(
+ r'tag//.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
diff --git a/src/django-master/blog/views.py b/src/django-master/blog/views.py
index d5dc7ec..a465094 100644
--- a/src/django-master/blog/views.py
+++ b/src/django-master/blog/views.py
@@ -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):
+ #ymq:Elasticsearch搜索视图,继承自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'):
+ #ymq:404错误处理视图
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'):
+ #ymq:500错误处理视图
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'):
+ #ymq:403错误处理视图
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') # 返回成功响应
\ No newline at end of file
diff --git a/src/django-master/djangoblog/admin_site.py b/src/django-master/djangoblog/admin_site.py
index f120405..9a3bbbb 100644
--- a/src/django-master/djangoblog/admin_site.py
+++ b/src/django-master/djangoblog/admin_site.py
@@ -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管理操作日志
diff --git a/src/django-master/djangoblog/apps.py b/src/django-master/djangoblog/apps.py
index d29e318..51c7f9f 100644
--- a/src/django-master/djangoblog/apps.py
+++ b/src/django-master/djangoblog/apps.py
@@ -1,11 +1,27 @@
+# ZYYDjango 应用配置类
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
+ # ZYY: 指定默认主键字段类型为BigAutoField(64位自增ID)
+ # ZYY替代旧版AutoField(32位),适合数据量大的应用
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()
\ No newline at end of file
+ load_plugins()
\ No newline at end of file
diff --git a/src/django-master/djangoblog/blog_signals.py b/src/django-master/djangoblog/blog_signals.py
index 393f441..ab597cb 100644
--- a/src/django-master/djangoblog/blog_signals.py
+++ b/src/django-master/djangoblog/blog_signals.py
@@ -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: 注释掉的激进缓存清理策略
\ No newline at end of file
diff --git a/src/django-master/djangoblog/elasticsearch_backend.py b/src/django-master/djangoblog/elasticsearch_backend.py
index 4afe498..052b035 100644
--- a/src/django-master/djangoblog/elasticsearch_backend.py
+++ b/src/django-master/djangoblog/elasticsearch_backend.py
@@ -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: 指定使用的查询类
diff --git a/src/django-master/djangoblog/feeds.py b/src/django-master/djangoblog/feeds.py
index 8c4e851..62907c1 100644
--- a/src/django-master/djangoblog/feeds.py
+++ b/src/django-master/djangoblog/feeds.py
@@ -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: 注意这里缺少返回值,实际使用会报错
diff --git a/src/django-master/djangoblog/logentryadmin.py b/src/django-master/djangoblog/logentryadmin.py
index 2f6a535..109b7d7 100644
--- a/src/django-master/djangoblog/logentryadmin.py
+++ b/src/django-master/djangoblog/logentryadmin.py
@@ -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 = '{}'.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 = '{}'.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
\ No newline at end of file
diff --git a/src/django-master/djangoblog/plugin_manage/base_plugin.py b/src/django-master/djangoblog/plugin_manage/base_plugin.py
index 2b4be5c..571e392 100644
--- a/src/django-master/djangoblog/plugin_manage/base_plugin.py
+++ b/src/django-master/djangoblog/plugin_manage/base_plugin.py
@@ -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
- }
+ }
\ No newline at end of file
diff --git a/src/django-master/djangoblog/plugin_manage/hook_constants.py b/src/django-master/djangoblog/plugin_manage/hook_constants.py
index 6685b7c..789fe82 100644
--- a/src/django-master/djangoblog/plugin_manage/hook_constants.py
+++ b/src/django-master/djangoblog/plugin_manage/hook_constants.py
@@ -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"
\ No newline at end of file
diff --git a/src/django-master/djangoblog/plugin_manage/hooks.py b/src/django-master/djangoblog/plugin_manage/hooks.py
index d712540..1108eea 100644
--- a/src/django-master/djangoblog/plugin_manage/hooks.py
+++ b/src/django-master/djangoblog/plugin_manage/hooks.py
@@ -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
\ No newline at end of file
diff --git a/src/django-master/djangoblog/plugin_manage/loader.py b/src/django-master/djangoblog/plugin_manage/loader.py
index 12e824b..8def52d 100644
--- a/src/django-master/djangoblog/plugin_manage/loader.py
+++ b/src/django-master/djangoblog/plugin_manage/loader.py
@@ -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)
__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)
\ No newline at end of file
+ # ZYY: 捕获并记录插件导入失败的错误信息(包含异常堆栈)
+ logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
\ No newline at end of file
diff --git a/src/django-master/djangoblog/settings.py b/src/django-master/djangoblog/settings.py
index d076bb6..a226a6f 100644
--- a/src/django-master/djangoblog/settings.py
+++ b/src/django-master/djangoblog/settings.py
@@ -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'
]
\ No newline at end of file
diff --git a/src/django-master/djangoblog/sitemap.py b/src/django-master/djangoblog/sitemap.py
index 8b7d446..5c0e70b 100644
--- a/src/django-master/djangoblog/sitemap.py
+++ b/src/django-master/djangoblog/sitemap.py
@@ -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
\ No newline at end of file
diff --git a/src/django-master/djangoblog/spider_notify.py b/src/django-master/djangoblog/spider_notify.py
index 7b909e9..12723a1 100644
--- a/src/django-master/djangoblog/spider_notify.py
+++ b/src/django-master/djangoblog/spider_notify.py
@@ -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)
\ No newline at end of file
diff --git a/src/django-master/djangoblog/tests.py b/src/django-master/djangoblog/tests.py
index 01237d9..4efac59 100644
--- a/src/django-master/djangoblog/tests.py
+++ b/src/django-master/djangoblog/tests.py
@@ -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: 验证转换结果不为空
diff --git a/src/django-master/djangoblog/urls.py b/src/django-master/djangoblog/urls.py
index 4aae58a..523478e 100644
--- a/src/django-master/djangoblog/urls.py
+++ b/src/django-master/djangoblog/urls.py
@@ -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)
\ No newline at end of file
diff --git a/src/django-master/djangoblog/utils.py b/src/django-master/djangoblog/utils.py
index 57f63dc..7749382 100644
--- a/src/django-master/djangoblog/utils.py
+++ b/src/django-master/djangoblog/utils.py
@@ -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)
\ No newline at end of file
diff --git a/src/django-master/djangoblog/whoosh_cn_backend.py b/src/django-master/djangoblog/whoosh_cn_backend.py
index 04e3f7f..09ec789 100644
--- a/src/django-master/djangoblog/whoosh_cn_backend.py
+++ b/src/django-master/djangoblog/whoosh_cn_backend.py
@@ -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
\ No newline at end of file
diff --git a/src/django-master/djangoblog/wsgi.py b/src/django-master/djangoblog/wsgi.py
index 2295efd..8ee9ba4 100644
--- a/src/django-master/djangoblog/wsgi.py
+++ b/src/django-master/djangoblog/wsgi.py
@@ -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()
diff --git a/src/django-master/oauth/admin.py b/src/django-master/oauth/admin.py
index 57eab5f..4fcc25c 100644
--- a/src/django-master/oauth/admin.py
+++ b/src/django-master/oauth/admin.py
@@ -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))
- 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类型筛选配置
\ No newline at end of file
diff --git a/src/django-master/oauth/apps.py b/src/django-master/oauth/apps.py
index 17fcea2..0fdf8a1 100644
--- a/src/django-master/oauth/apps.py
+++ b/src/django-master/oauth/apps.py
@@ -2,4 +2,4 @@ from django.apps import AppConfig
class OauthConfig(AppConfig):
- name = 'oauth'
+ name = 'oauth' # zy:定义Django应用的名称,必须与文件夹名称一致
\ No newline at end of file
diff --git a/src/django-master/oauth/forms.py b/src/django-master/oauth/forms.py
index 0e4ede3..e839af7 100644
--- a/src/django-master/oauth/forms.py
+++ b/src/django-master/oauth/forms.py
@@ -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类
diff --git a/src/django-master/oauth/migrations/0001_initial.py b/src/django-master/oauth/migrations/0001_initial.py
index 3aa3e03..7db6ef6 100644
--- a/src/django-master/oauth/migrations/0001_initial.py
+++ b/src/django-master/oauth/migrations/0001_initial.py
@@ -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: 默认排序 - 按创建时间倒序
},
),
]
diff --git a/src/django-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py b/src/django-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
index d5cc70e..c1cf762 100644
--- a/src/django-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
+++ b/src/django-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
@@ -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: 使用国际化字段名称
),
- ]
+ ]
\ No newline at end of file
diff --git a/src/django-master/oauth/migrations/0003_alter_oauthuser_nickname.py b/src/django-master/oauth/migrations/0003_alter_oauthuser_nickname.py
index 6af08eb..973b9c0 100644
--- a/src/django-master/oauth/migrations/0003_alter_oauthuser_nickname.py
+++ b/src/django-master/oauth/migrations/0003_alter_oauthuser_nickname.py
@@ -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'(添加空格)
),
]
diff --git a/src/django-master/oauth/models.py b/src/django-master/oauth/models.py
index be838ed..3190d89 100644
--- a/src/django-master/oauth/models.py
+++ b/src/django-master/oauth/models.py
@@ -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: 按创建时间倒序排列
\ No newline at end of file
diff --git a/src/django-master/oauth/oauthmanager.py b/src/django-master/oauth/oauthmanager.py
index 2e7ceef..1d95317 100644
--- a/src/django-master/oauth/oauthmanager.py
+++ b/src/django-master/oauth/oauthmanager.py
@@ -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
\ No newline at end of file
diff --git a/src/django-master/oauth/templatetags/oauth_tags.py b/src/django-master/oauth/templatetags/oauth_tags.py
index 7b687d5..19c8cea 100644
--- a/src/django-master/oauth/templatetags/oauth_tags.py
+++ b/src/django-master/oauth/templatetags/oauth_tags.py
@@ -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: 返回字典 - 包含应用列表供模板使用
+ }
\ No newline at end of file
diff --git a/src/django-master/oauth/tests.py b/src/django-master/oauth/tests.py
index bb23b9b..7abe687 100644
--- a/src/django-master/oauth/tests.py
+++ b/src/django-master/oauth/tests.py
@@ -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: 第二次调用 - 获取openid(JSONP格式)
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匹配
\ No newline at end of file
diff --git a/src/django-master/oauth/urls.py b/src/django-master/oauth/urls.py
index c4a12a0..58db0df 100644
--- a/src/django-master/oauth/urls.py
+++ b/src/django-master/oauth/urls.py
@@ -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/.html',
- views.RequireEmailView.as_view(),
- name='require_email'),
+ r'oauth/requireemail/.html', # zy: 动态路径 - 需要补充邮箱页面(带OAuth用户ID参数)
+ views.RequireEmailView.as_view(), # zy: 类视图 - 处理邮箱补充表单
+ name='require_email'), # zy: URL名称 - 用于反向解析
path(
- r'oauth/emailconfirm//.html',
- views.emailconfirm,
- name='email_confirm'),
+ r'oauth/emailconfirm//.html', # zy: 关键路径 - 邮箱确认链接(带ID和签名参数)
+ views.emailconfirm, # zy: 重要视图 - 验证邮箱确认签名并完成绑定
+ name='email_confirm'), # zy: URL名称 - 邮箱确认功能
path(
- r'oauth/bindsuccess/.html',
- views.bindsuccess,
- name='bindsuccess'),
+ r'oauth/bindsuccess/.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登录功能
diff --git a/src/django-master/oauth/views.py b/src/django-master/oauth/views.py
index 12e3a6e..22889a6 100644
--- a/src/django-master/oauth/views.py
+++ b/src/django-master/oauth/views.py
@@ -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 = _("""
Please click the link below to bind your email
@@ -225,29 +225,29 @@ class RequireEmailView(FormView):
%(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: 渲染成功页面
+ })
\ No newline at end of file