Compare commits

...

55 Commits

Author SHA1 Message Date
dynastxu 263d370607 Merge branch 'zy_branch' into develop
2 months ago
xdqw166 756a12fe29 11周作业表格终章
2 months ago
xdqw166 e209e1b2d4 11周作业表格
2 months ago
dynastxu 1de159624a docs: 完成介绍视频
2 months ago
dynastxu bc085a06cc docs: 完成开源软件泛读、标注和维护报告文档.docx
2 months ago
dynastxu d8c637ba15 chore(gitignore): 更新.gitignore
3 months ago
dynastxu 8dda6211ec update subtree
3 months ago
dynastxu 78ead977fa Squashed 'src/DjangoBlog/' changes from cf3b252..be4c76b
3 months ago
dynastxu f8bd1d0438 Merge branch 'develop' into xjj_branch
3 months ago
dynastxu d106cb2792 Squashed 'src/DjangoBlog/' changes from cafdade..cf3b252
3 months ago
dynastxu eee1b8c98e update subtree
3 months ago
dynastxu 265045fe65 Merge remote-tracking branch 'origin/zy_branch' into develop
3 months ago
xdqw166 f30135dfd8 修改了base html的报错
3 months ago
xdqw166 90f48adaee 功能增加变换背景颜色
3 months ago
dynastxu 7316b4c8b6 Squashed 'src/DjangoBlog/' changes from b99778c..cafdade
3 months ago
dynastxu 46862ad679 update subtree
3 months ago
dynastxu 8fa2a1d76f chore: 更新.gitignore文件忽略规则
3 months ago
dynastxu 654d036823 update subtree
3 months ago
dynastxu 60561750c0 Squashed 'src/DjangoBlog/' changes from 0bb6193..b99778c
3 months ago
dynastxu fedfe15b64 feat(script): 添加 Git 清理脚本以自动化清理未跟踪文件夹
3 months ago
dynastxu d71b49f1ca docs(readme): 更新 README 文件内容
3 months ago
dynastxu e33542c8a0 chore: 添加 MIT 许可证文件
3 months ago
dynastxu 43ea653c11 update subtree
3 months ago
dynastxu 72fddfe377 Squashed 'src/DjangoBlog/' changes from 13ebbc8..0bb6193
3 months ago
dynastxu af79acffbc chore(gitignore): 更新.gitignore文件
3 months ago
dynastxu 8a37d8cc06 Merge branch 'lrj_branch' into develop
3 months ago
ZUOFEikabuto b48a67b203 feat: [lrj] 完成OAuth模块代码质量分析和注释
3 months ago
ZUOFEikabuto cd96dfe561 feat: 完成OAuth模块代码质量分析和注释
3 months ago
ZUOFEikabuto 26efdcb6f4 在更新后的代码基础上添加oauth注释
3 months ago
dynastxu 151535a74e 完善文档
3 months ago
dynastxu ef8f3f3d19 完成 编码规范.docx
3 months ago
dynastxu dcc31a2bdf 完成 开源软件的质量分析报告文档.docx 大部分内容
3 months ago
dynastxu afaacc22cf 删除无用文件
3 months ago
dynastxu 741cac2e1f Merge branch 'xjj_branch' into develop
3 months ago
dynastxu 854e8e28c7 feat(script): 添加推送子树的批处理脚本
3 months ago
dynastxu 400192beb7 Merge branch 'zy_branch' into develop
3 months ago
dynastxu aadcbfbfc3 Merge remote-tracking branch 'origin/shw_branch' into develop
3 months ago
dynastxu f285750263 Merge remote-tracking branch 'origin/bjy_branch' into develop
3 months ago
dynastxu a5637dad09 chore(subtree): 更新子树脚本逻辑
3 months ago
dynastxu eac243818d Squashed 'src/DjangoBlog/' changes from 408d19c..13ebbc8
3 months ago
dynastxu 3240542cb9 Merge commit 'eac243818d651281e841188481859d3e6e251cc8' into xjj_branch
3 months ago
dynastxu b85f85125b chore(scripts): 添加更新子树的批处理脚本
3 months ago
bu661 01f9792c1c blog注释
3 months ago
dynastxu 001bc85a81 Squashed 'src/DjangoBlog/' changes from 1f969cc..408d19c
3 months ago
dynastxu cf73d21b06 Merge commit '001bc85a8157f3742d79d513e5300bcf3b975edb' into xjj_branch
3 months ago
xdqw166 7fc1b25b73 代码注释
3 months ago
xdqw166 e8698c02a4 代码注释
3 months ago
xdqw166 5ef0f8fb0f 代码注释
3 months ago
xdqw166 0eb7232c46 Merge remote-tracking branch 'origin/develop' into zy_branch
3 months ago
xdqw166 78de182afd 删除错误的文档
3 months ago
xdqw166 ae476bbab2 Merge branch 'zy_branch_fix' into zy_branch
3 months ago
xdqw166 5614ee69bc 交了软件界面设计大作业
3 months ago
xdqw166 6be53f48bb 交了软件界面设计大作业
3 months ago
xdqw166 f91c8887e6 Merge remote-tracking branch 'origin/zy_branch' into zy_branch
3 months ago
xdqw166 249e858738 交了软件界面设计大作业
3 months ago

9
.gitignore vendored

@ -1 +1,8 @@
.idea /.idea/
# DjangoBlog
.env
/logs/
/collectedstatic/
/djangoblog/
/static/

3
.gitmodules vendored

@ -1,3 +0,0 @@
[submodule "src/DjangoBlog"]
path = src/DjangoBlog
url = https://github.com/ETOofficial/DjangoBlog.git

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,2 +1,4 @@
# 阅读和分析开源软件 # 阅读和分析开源软件
- 软件名:[**DjangoBlog**](https://github.com/liangliangyy/DjangoBlog)
- 协议:[**MIT License**](src/DjangoBlog/LICENSE)

Binary file not shown.

Binary file not shown.

@ -0,0 +1,22 @@
@echo off
chcp 65001 >nul
echo 正在检查将要清理的文件和文件夹...
git clean -fd -n
set /p confirm=是否确认清理这些文件?(y/N):
if /i "%confirm%" neq "y" (
echo 操作已取消
pause
exit /b
)
echo 正在执行清理操作...
git clean -fd
if %errorlevel% equ 0 (
echo 清理完成!
) else (
echo 清理过程中出现错误,错误代码: %errorlevel%
)
pause

@ -0,0 +1,9 @@
@echo off
echo Pushing Git subtree...
git subtree push --prefix=src/DjangoBlog DjangoBlog g3f-CodeEdit
if %errorlevel% equ 0 (
echo Subtree push successful!
) else (
echo Subtree push failed!
)
pause

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

@ -77,3 +77,8 @@ uploads/
settings_production.py settings_production.py
werobot_session.db werobot_session.db
bin/datas/ bin/datas/
.env
# 目前似乎仅有测试代码会涉及到修改此文件夹,所以暂不进行版本管理
static/avatar/

@ -14,8 +14,8 @@ from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm): class BlogUserCreationForm(forms.ModelForm):
#shw 自定义用户创建表单用于在Django Admin后台添加新用户。 #shw 自定义用户创建表单用于在Django Admin后台添加新用户。
#shw 它继承自 ModelForm并增加了密码输入和确认的逻辑。 #shw 它继承自 ModelForm并增加了密码输入和确认的逻辑。
#shw 定义第一个密码字段使用PasswordInput控件隐藏输入内容 #shw 定义第一个密码字段使用PasswordInput控件隐藏输入内容
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
@ -23,12 +23,12 @@ class BlogUserCreationForm(forms.ModelForm):
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta: class Meta:
#shw Meta类用于配置表单与模型的关联 #shw Meta类用于配置表单与模型的关联
model = BlogUser #shw 指定该表单对应的模型是 BlogUser model = BlogUser #shw 指定该表单对应的模型是 BlogUser
fields = ('email',) #shw 在创建用户时,除了密码外,只显示邮箱字段 fields = ('email',) #shw 在创建用户时,除了密码外,只显示邮箱字段
def clean_password2(self): def clean_password2(self):
#shw 自定义验证方法,用于检查两次输入的密码是否一致 #shw 自定义验证方法,用于检查两次输入的密码是否一致
#shw Check that the two password entries match #shw Check that the two password entries match
password1 = self.cleaned_data.get("password1") #shw 从已清洗的数据中获取第一个密码 password1 = self.cleaned_data.get("password1") #shw 从已清洗的数据中获取第一个密码
password2 = self.cleaned_data.get("password2") #shw 从已清洗的数据中获取第二个密码 password2 = self.cleaned_data.get("password2") #shw 从已清洗的数据中获取第二个密码
@ -38,7 +38,7 @@ class BlogUserCreationForm(forms.ModelForm):
return password2 #shw 返回第二个密码作为清洗后的数据 return password2 #shw 返回第二个密码作为清洗后的数据
def save(self, commit=True): def save(self, commit=True):
#shw 重写save方法以便在保存用户时处理密码哈希 #shw 重写save方法以便在保存用户时处理密码哈希
#shw Save the provided password in hashed format #shw Save the provided password in hashed format
user = super().save(commit=False) #shw 调用父类的save方法但先不提交到数据库commit=False user = super().save(commit=False) #shw 调用父类的save方法但先不提交到数据库commit=False
user.set_password(self.cleaned_data["password1"]) #shw 使用Django的set_password方法将明文密码加密后存储 user.set_password(self.cleaned_data["password1"]) #shw 使用Django的set_password方法将明文密码加密后存储
@ -49,24 +49,24 @@ class BlogUserCreationForm(forms.ModelForm):
class BlogUserChangeForm(UserChangeForm): class BlogUserChangeForm(UserChangeForm):
#shw 自定义用户修改表单用于在Django Admin后台编辑现有用户信息。 #shw 自定义用户修改表单用于在Django Admin后台编辑现有用户信息。
#shw 它继承自Django的UserChangeForm以复用大部分功能。 #shw 它继承自Django的UserChangeForm以复用大部分功能。
class Meta: class Meta:
#shw Meta类用于配置表单与模型的关联 #shw Meta类用于配置表单与模型的关联
model = BlogUser #shw 指定该表单对应的模型是 BlogUser model = BlogUser #shw 指定该表单对应的模型是 BlogUser
fields = '__all__' #shw 在修改用户时,显示模型中的所有字段 fields = '__all__' #shw 在修改用户时,显示模型中的所有字段
#shw 指定 'username' 字段使用的字段类为 UsernameField #shw 指定 'username' 字段使用的字段类为 UsernameField
field_classes = {'username': UsernameField} field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
#shw 重写初始化方法,可以在这里添加自定义的初始化逻辑 #shw 重写初始化方法,可以在这里添加自定义的初始化逻辑
super().__init__(*args, **kwargs) #shw 调用父类的初始化方法 super().__init__(*args, **kwargs) #shw 调用父类的初始化方法
class BlogUserAdmin(UserAdmin): class BlogUserAdmin(UserAdmin):
#shw 自定义用户管理类用于在Django Admin后台定制 BlogUser 模型的显示和操作方式。 #shw 自定义用户管理类用于在Django Admin后台定制 BlogUser 模型的显示和操作方式。
#shw 它继承自Django的UserAdmin以复用用户管理的标准功能。 #shw 它继承自Django的UserAdmin以复用用户管理的标准功能。
form = BlogUserChangeForm #shw 指定修改用户时使用的表单 form = BlogUserChangeForm #shw 指定修改用户时使用的表单
add_form = BlogUserCreationForm #shw 指定添加用户时使用的表单 add_form = BlogUserCreationForm #shw 指定添加用户时使用的表单

@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError
from django.forms import widgets from django.forms import widgets
#shw 导入Django的国际化和翻译工具 #shw 导入Django的国际化和翻译工具
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
#shw 导入本地的工具模块
from . import utils from . import utils
#shw 导入本地的BlogUser模型 #shw 导入本地的BlogUser模型
from .models import BlogUser from .models import BlogUser

@ -1,91 +1,47 @@
# shw Generated by Django 4.1.7 on 2023-03-02 07:14 # Generated by Django 4.1.7 on 2023-03-02 07:14
# shw 这是Django自动生成的迁移文件用于记录数据库模型的变更。
# shw 生成时间2023年3月2日 07:14
# shw Django版本4.1.7
# shw 导入Django内置的认证模型和验证器因为我们的BlogUser模型继承了它们
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
# shw 导入Django数据库迁移和模型的核心模块
from django.db import migrations, models from django.db import migrations, models
# shw 导入Django的时区工具用于设置默认的当前时间
import django.utils.timezone import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
# shw 这是accounts应用的初始迁移类。
# shw 它负责在数据库中创建 BlogUser 数据表。
# shw 标记这是该应用的第一次迁移
initial = True initial = True
# shw 声明此迁移的依赖关系。
# shw 它依赖于Django内置的 'auth' 应用的 '0012_alter_user_first_name_max_length' 迁移。
# shw 这确保了在执行我们的迁移之前Django内置的用户表结构已经更新到了指定版本。
dependencies = [ dependencies = [
('auth', '0012_alter_user_first_name_max_length'), ('auth', '0012_alter_user_first_name_max_length'),
] ]
# shw 定义此迁移要执行的具体操作列表
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
# shw 操作名称:创建一个名为 'BlogUser' 的新模型(对应数据库中的表)
name='BlogUser', name='BlogUser',
# shw 定义 'BlogUser' 模型的所有字段
fields=[ fields=[
# shw 以下是继承自 AbstractUser 的标准字段
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')), ('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
help_text='Designates that this user has all permissions without explicitly assigning them.', ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'},
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
max_length=150, unique=True,
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
help_text='Designates whether the user can log into this admin site.', ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
verbose_name='staff status')),
('is_active', models.BooleanField(default=True,
help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.',
verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
# shw --- 以下是 BlogUser 模型自定义的扩展字段 ---
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), # shw 用户的昵称,可为空
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# shw 记录用户创建的时间戳
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# shw 记录用户信息最后修改的时间戳
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), ('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
# shw 记录用户的注册来源(如:'website', 'wechat' ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
# shw 以下是多对多关系字段,用于权限和分组
('groups', models.ManyToManyField(blank=True,
help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
related_name='user_set', related_query_name='user', to='auth.group',
verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.',
related_name='user_set', related_query_name='user',
to='auth.permission', verbose_name='user permissions')),
], ],
# shw 定义模型的元数据选项
options={ options={
'verbose_name': '用户', # shw 在Django Admin中显示的单数名称 'verbose_name': '用户',
'verbose_name_plural': '用户', # shw 在Django Admin中显示的复数名称 'verbose_name_plural': '用户',
'ordering': ['-id'], # shw 默认的排序方式按ID降序排列最新的在前 'ordering': ['-id'],
'get_latest_by': 'id', # shw 当使用 .latest() 方法时,默认按 'id' 字段查找 'get_latest_by': 'id',
}, },
# shw 为模型指定自定义的管理器
# shw 这里使用了Django默认的 UserManager因为它提供了创建用户和管理用户的标准方法
managers=[ managers=[
('objects', django.contrib.auth.models.UserManager()), ('objects', django.contrib.auth.models.UserManager()),
], ],

@ -1,82 +1,46 @@
#shw Generated by Django 4.2.5 on 2023-09-06 13:13 # Generated by Django 4.2.5 on 2023-09-06 13:13
#shw 这是Django自动生成的迁移文件用于记录对现有模型的修改。
#shw 生成时间2023年9月6日 13:13
#shw Django版本4.2.5
#shw 导入Django数据库迁移和模型的核心模块
from django.db import migrations, models from django.db import migrations, models
#shw 导入Django的时区工具用于设置默认的当前时间
import django.utils.timezone import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
#shw 这是accounts应用的第二次迁移类。
#shw 它修改了 BlogUser 模型的元数据、字段和字段属性。
#shw 声明此迁移的依赖关系。
#shw 它依赖于该应用的第一次迁移 '0001_initial'。
#shw 这确保了在执行此迁移之前BlogUser 表已经被创建。
dependencies = [ dependencies = [
('accounts', '0001_initial'), ('accounts', '0001_initial'),
] ]
#shw 定义此迁移要执行的具体操作列表
operations = [ operations = [
#shw 操作1: 修改模型的元数据选项
migrations.AlterModelOptions( migrations.AlterModelOptions(
#shw 指定要修改的模型名
name='bloguser', name='bloguser',
#shw 更新 Meta 类中的选项
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
), ),
#shw 操作2: 删除一个字段
migrations.RemoveField( migrations.RemoveField(
#shw 指定模型
model_name='bloguser', model_name='bloguser',
#shw 指定要删除的字段名
name='created_time', name='created_time',
), ),
#shw 操作3: 删除另一个字段
migrations.RemoveField( migrations.RemoveField(
#shw 指定模型
model_name='bloguser', model_name='bloguser',
#shw 指定要删除的字段名
name='last_mod_time', name='last_mod_time',
), ),
#shw 操作4: 添加一个新字段
migrations.AddField( migrations.AddField(
#shw 指定模型
model_name='bloguser', model_name='bloguser',
#shw 新字段名
name='creation_time', name='creation_time',
#shw 定义新字段的类型和属性
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
#shw 操作5: 添加另一个新字段
migrations.AddField( migrations.AddField(
#shw 指定模型
model_name='bloguser', model_name='bloguser',
#shw 新字段名
name='last_modify_time', name='last_modify_time',
#shw 定义新字段的类型和属性
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
), ),
#shw 操作6: 修改现有字段的属性
migrations.AlterField( migrations.AlterField(
#shw 指定模型
model_name='bloguser', model_name='bloguser',
#shw 指定要修改的字段
name='nickname', name='nickname',
#shw 重新定义该字段的属性(这里只修改了 verbose_name
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
), ),
#shw 操作7: 修改另一个现有字段的属性
migrations.AlterField( migrations.AlterField(
#shw 指定模型
model_name='bloguser', model_name='bloguser',
#shw 指定要修改的字段
name='source', name='source',
#shw 重新定义该字段的属性(这里只修改了 verbose_name
field=models.CharField(blank=True, max_length=100, verbose_name='create source'), field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
), ),
] ]

@ -8,7 +8,6 @@ from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
#shw 导入Django的国际化和翻译工具 #shw 导入Django的国际化和翻译工具
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
#shw 从项目工具模块中导入获取当前站点的函数
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site

@ -4,7 +4,6 @@ from django.test import Client, RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
#shw 导入Django的时区工具 #shw 导入Django的时区工具
from django.utils import timezone from django.utils import timezone
#shw 导入Django的国际化和翻译工具
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
#shw 导入本地的BlogUser模型 #shw 导入本地的BlogUser模型
@ -37,9 +36,7 @@ class AccountTest(TestCase):
self.new_test = "xxx123--=" #shw 定义一个测试用的新密码 self.new_test = "xxx123--=" #shw 定义一个测试用的新密码
def test_validate_account(self): def test_validate_account(self):
#shw 测试超级用户的创建、登录和后台访问权限。 site = get_current_site().domain
site = get_current_site().domain #shw 获取当前站点域名
#shw 创建一个超级用户
user = BlogUser.objects.create_superuser( user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", email="liangliangyy1@gmail.com",
username="liangliangyy1", username="liangliangyy1",
@ -82,7 +79,6 @@ class AccountTest(TestCase):
0, len( 0, len(
BlogUser.objects.filter( BlogUser.objects.filter(
email='user123@user.com'))) email='user123@user.com')))
#shw 模拟发送POST请求到注册页面
response = self.client.post(reverse('account:register'), { response = self.client.post(reverse('account:register'), {
'username': 'user1233', 'username': 'user1233',
'email': 'user123@user.com', 'email': 'user123@user.com',

@ -1,8 +1,6 @@
#shw 导入日志模块 #shw 导入日志模块
import logging import logging
#shw 导入Django的延迟翻译函数
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
#shw 导入Django的项目设置
from django.conf import settings from django.conf import settings
#shw 导入Django的认证模块 #shw 导入Django的认证模块
from django.contrib import auth from django.contrib import auth
@ -240,9 +238,11 @@ class ForgetPasswordView(FormView):
class ForgetPasswordEmailCode(View): class ForgetPasswordEmailCode(View):
#shw 发送忘记密码验证码的视图继承自基础的View。
def post(self, request: HttpRequest): #shw 发送忘记密码验证码的视图继承自基础的View。
#shw 只处理POST请求 @staticmethod
def post(request: HttpRequest):
#shw 只处理POST请求
form = ForgetPasswordCodeForm(request.POST) #shw 用POST数据实例化表单 form = ForgetPasswordCodeForm(request.POST) #shw 用POST数据实例化表单
if not form.is_valid(): #shw 验证表单(主要是验证邮箱格式) if not form.is_valid(): #shw 验证表单(主要是验证邮箱格式)
return HttpResponse("错误的邮箱") #shw 如果无效,返回错误信息 return HttpResponse("错误的邮箱") #shw 如果无效,返回错误信息

@ -1,3 +1,4 @@
# bjy: 从Django中导入所需的模块和类
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -9,106 +10,157 @@ from django.utils.translation import gettext_lazy as _
from .models import Article, Category, Tag, Links, SideBar, BlogSettings from .models import Article, Category, Tag, Links, SideBar, BlogSettings
# bjy: 为Article模型创建一个自定义的ModelForm
class ArticleForm(forms.ModelForm): class ArticleForm(forms.ModelForm):
# bjy: 示例如果使用Pagedown编辑器可以取消下面这行的注释
# body = forms.CharField(widget=AdminPagedownWidget()) # body = forms.CharField(widget=AdminPagedownWidget())
class Meta: class Meta:
# bjy: 指定这个表单对应的模型是Article
model = Article model = Article
# bjy: 表示在表单中包含模型的所有字段
fields = '__all__' fields = '__all__'
# bjy: 定义一个admin动作用于将选中的文章发布
def makr_article_publish(modeladmin, request, queryset): def makr_article_publish(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的状态为'p'(已发布)
queryset.update(status='p') queryset.update(status='p')
# bjy: 定义一个admin动作用于将选中的文章设为草稿
def draft_article(modeladmin, request, queryset): def draft_article(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的状态为'd'(草稿)
queryset.update(status='d') queryset.update(status='d')
# bjy: 定义一个admin动作用于关闭选中文章的评论功能
def close_article_commentstatus(modeladmin, request, queryset): def close_article_commentstatus(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的评论状态为'c'(关闭)
queryset.update(comment_status='c') queryset.update(comment_status='c')
# bjy: 定义一个admin动作用于开启选中文章的评论功能
def open_article_commentstatus(modeladmin, request, queryset): def open_article_commentstatus(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的评论状态为'o'(开启)
queryset.update(comment_status='o') queryset.update(comment_status='o')
# bjy: 为admin动作设置在后台显示的描述文本
makr_article_publish.short_description = _('Publish selected articles') makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles') draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments') close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments') open_article_commentstatus.short_description = _('Open article comments')
# bjy: 为Article模型自定义Admin管理界面
class ArticlelAdmin(admin.ModelAdmin): class ArticlelAdmin(admin.ModelAdmin):
# bjy: 设置每页显示20条记录
list_per_page = 20 list_per_page = 20
# bjy: 启用搜索功能搜索范围包括文章内容body和标题title
search_fields = ('body', 'title') search_fields = ('body', 'title')
# bjy: 指定使用的自定义表单
form = ArticleForm form = ArticleForm
# bjy: 在列表视图中显示的字段
list_display = ( list_display = (
'id', 'id',
'title', 'title',
'author', 'author',
'link_to_category', 'link_to_category', # bjy: 自定义方法,显示指向分类的链接
'creation_time', 'creation_time',
'views', 'views',
'status', 'status',
'type', 'type',
'article_order') 'article_order')
# bjy: 设置列表视图中可点击进入编辑页面的链接字段
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
# bjy: 启用右侧筛选栏,可按状态、类型、分类进行筛选
list_filter = ('status', 'type', 'category') list_filter = ('status', 'type', 'category')
# bjy: 启用日期层次导航,按创建时间进行分层
date_hierarchy = 'creation_time' date_hierarchy = 'creation_time'
# bjy: 为多对多字段tags提供一个水平筛选的界面
filter_horizontal = ('tags',) filter_horizontal = ('tags',)
# bjy: 在编辑页面中排除的字段,这些字段将自动处理
exclude = ('creation_time', 'last_modify_time') exclude = ('creation_time', 'last_modify_time')
# bjy: 在列表页面显示“在站点上查看”的按钮
view_on_site = True view_on_site = True
# bjy: 将自定义的admin动作添加到动作下拉列表中
actions = [ actions = [
makr_article_publish, makr_article_publish,
draft_article, draft_article,
close_article_commentstatus, close_article_commentstatus,
open_article_commentstatus] open_article_commentstatus]
# bjy: 对于外键字段author, category显示为一个输入框用于输入ID而不是下拉列表
raw_id_fields = ('author', 'category',) raw_id_fields = ('author', 'category',)
# bjy: 自定义方法,用于在列表页面显示一个指向文章分类的链接
def link_to_category(self, obj): def link_to_category(self, obj):
# bjy: 获取分类模型的app_label和model_name用于构建admin URL
info = (obj.category._meta.app_label, obj.category._meta.model_name) info = (obj.category._meta.app_label, obj.category._meta.model_name)
# bjy: 生成指向该分类编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
# bjy: 使用format_html安全地生成HTML链接
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name)) return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
# bjy: 设置该方法在列表页面列标题的显示文本
link_to_category.short_description = _('category') link_to_category.short_description = _('category')
# bjy: 重写get_form方法用于动态修改表单
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
# bjy: 获取父类的表单
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
# bjy: 修改author字段的查询集只显示超级用户
form.base_fields['author'].queryset = get_user_model( form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True) ).objects.filter(is_superuser=True)
return form return form
# bjy: 重写save_model方法在保存模型时执行额外操作
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
# bjy: 调用父类的save_model方法执行默认保存操作
super(ArticlelAdmin, self).save_model(request, obj, form, change) super(ArticlelAdmin, self).save_model(request, obj, form, change)
# bjy: 重写get_view_on_site_url方法自定义“在站点上查看”的URL
def get_view_on_site_url(self, obj=None): def get_view_on_site_url(self, obj=None):
if obj: if obj:
# bjy: 如果对象存在则调用模型的get_full_url方法获取URL
url = obj.get_full_url() url = obj.get_full_url()
return url return url
else: else:
# bjy: 如果对象不存在例如在添加新对象时则返回网站首页URL
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
site = get_current_site().domain site = get_current_site().domain
return site return site
# bjy: 为Tag模型自定义Admin管理界面
class TagAdmin(admin.ModelAdmin): class TagAdmin(admin.ModelAdmin):
# bjy: 在编辑页面中排除的字段
exclude = ('slug', 'last_mod_time', 'creation_time') exclude = ('slug', 'last_mod_time', 'creation_time')
# bjy: 为Category模型自定义Admin管理界面
class CategoryAdmin(admin.ModelAdmin): class CategoryAdmin(admin.ModelAdmin):
# bjy: 在列表视图中显示的字段
list_display = ('name', 'parent_category', 'index') list_display = ('name', 'parent_category', 'index')
# bjy: 在编辑页面中排除的字段
exclude = ('slug', 'last_mod_time', 'creation_time') exclude = ('slug', 'last_mod_time', 'creation_time')
# bjy: 为Links模型自定义Admin管理界面
class LinksAdmin(admin.ModelAdmin): class LinksAdmin(admin.ModelAdmin):
# bjy: 在编辑页面中排除的字段
exclude = ('last_mod_time', 'creation_time') exclude = ('last_mod_time', 'creation_time')
# bjy: 为SideBar模型自定义Admin管理界面
class SideBarAdmin(admin.ModelAdmin): class SideBarAdmin(admin.ModelAdmin):
# bjy: 在列表视图中显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence') list_display = ('name', 'content', 'is_enable', 'sequence')
# bjy: 在编辑页面中排除的字段
exclude = ('last_mod_time', 'creation_time') exclude = ('last_mod_time', 'creation_time')
# bjy: 为BlogSettings模型自定义Admin管理界面
class BlogSettingsAdmin(admin.ModelAdmin): class BlogSettingsAdmin(admin.ModelAdmin):
# bjy: 使用默认配置,无需自定义
pass pass

@ -1,5 +1,8 @@
# bjy: 从Django中导入AppConfig基类用于配置应用程序
from django.apps import AppConfig from django.apps import AppConfig
# bjy: 定义一个名为BlogConfig的配置类它继承自AppConfig
class BlogConfig(AppConfig): class BlogConfig(AppConfig):
# bjy: 指定这个配置类对应的应用程序名称通常是Python包的路径
name = 'blog' name = 'blog'

@ -1,43 +1,76 @@
# bjy: 导入日志模块
import logging import logging
# bjy: 从Django中导入时区工具
from django.utils import timezone from django.utils import timezone
# bjy: 从项目工具模块中导入缓存和获取博客设置的函数
from djangoblog.utils import cache, get_blog_setting from djangoblog.utils import cache, get_blog_setting
# bjy: 从当前应用的models中导入Category和Article模型
from .models import Category, Article from .models import Category, Article
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# bjy: 定义一个上下文处理器,用于在所有模板中注入全局变量
def seo_processor(requests): def seo_processor(requests):
# bjy: 定义一个缓存键名
key = 'seo_processor' key = 'seo_processor'
# bjy: 尝试从缓存中获取数据
value = cache.get(key) value = cache.get(key)
# bjy: 如果缓存中存在数据,则直接返回
if value: if value:
return value return value
else: else:
# bjy: 如果缓存中没有数据,则记录一条日志
logger.info('set processor cache.') logger.info('set processor cache.')
# bjy: 获取博客的设置对象
setting = get_blog_setting() setting = get_blog_setting()
# bjy: 构建一个包含所有SEO和全局设置的字典
value = { value = {
# bjy: 网站名称
'SITE_NAME': setting.site_name, 'SITE_NAME': setting.site_name,
# bjy: 是否显示Google AdSense广告
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
# bjy: Google AdSense的广告代码
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
# bjy: 网站的SEO描述
'SITE_SEO_DESCRIPTION': setting.site_seo_description, 'SITE_SEO_DESCRIPTION': setting.site_seo_description,
# bjy: 网站的普通描述
'SITE_DESCRIPTION': setting.site_description, 'SITE_DESCRIPTION': setting.site_description,
# bjy: 网站的关键词
'SITE_KEYWORDS': setting.site_keywords, 'SITE_KEYWORDS': setting.site_keywords,
# bjy: 网站的完整基础URL协议+域名)
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
# bjy: 文章列表页的摘要长度
'ARTICLE_SUB_LENGTH': setting.article_sub_length, 'ARTICLE_SUB_LENGTH': setting.article_sub_length,
# bjy: 用于导航栏的所有分类列表
'nav_category_list': Category.objects.all(), 'nav_category_list': Category.objects.all(),
# bjy: 用于导航栏的所有已发布的“页面”类型的文章
'nav_pages': Article.objects.filter( 'nav_pages': Article.objects.filter(
type='p', type='p', # bjy: 类型为'p'page
status='p'), status='p'), # bjy: 状态为'p'published
# bjy: 是否开启全站评论功能
'OPEN_SITE_COMMENT': setting.open_site_comment, 'OPEN_SITE_COMMENT': setting.open_site_comment,
# bjy: 网站的ICP备案号
'BEIAN_CODE': setting.beian_code, 'BEIAN_CODE': setting.beian_code,
# bjy: 网站统计代码如Google Analytics
'ANALYTICS_CODE': setting.analytics_code, 'ANALYTICS_CODE': setting.analytics_code,
# bjy: 公安备案号
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, "BEIAN_CODE_GONGAN": setting.gongan_beiancode,
# bjy: 是否显示公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, "SHOW_GONGAN_CODE": setting.show_gongan_code,
# bjy: 当前年份,用于页脚版权信息
"CURRENT_YEAR": timezone.now().year, "CURRENT_YEAR": timezone.now().year,
# bjy: 全局页头HTML代码
"GLOBAL_HEADER": setting.global_header, "GLOBAL_HEADER": setting.global_header,
# bjy: 全局页脚HTML代码
"GLOBAL_FOOTER": setting.global_footer, "GLOBAL_FOOTER": setting.global_footer,
# bjy: 评论是否需要审核
"COMMENT_NEED_REVIEW": setting.comment_need_review, "COMMENT_NEED_REVIEW": setting.comment_need_review,
} }
# bjy: 将构建好的字典存入缓存缓存时间为10小时60*60*10秒
cache.set(key, value, 60 * 60 * 10) cache.set(key, value, 60 * 60 * 10)
# bjy: 返回这个字典,它将被注入到所有模板的上下文中
return value return value

@ -1,26 +1,40 @@
# bjy: 导入时间模块
import time import time
# bjy: 导入Elasticsearch的客户端模块和异常类
import elasticsearch.client import elasticsearch.client
import elasticsearch.exceptions
# bjy: 导入Django的设置
from django.conf import settings from django.conf import settings
# bjy: 从elasticsearch_dsl中导入文档、内部文档、字段类型和连接管理器
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections from elasticsearch_dsl.connections import connections
# bjy: 从blog应用中导入Article模型
from blog.models import Article from blog.models import Article
# bjy: 检查Django设置中是否配置了ELASTICSEARCH_DSL以决定是否启用Elasticsearch功能
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
# bjy: 如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
# bjy: 根据Django设置创建到Elasticsearch的连接
connections.create_connection( connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
# bjy: 导入并实例化Elasticsearch客户端
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# bjy: 导入并实例化Ingest客户端用于管理管道
from elasticsearch.client import IngestClient from elasticsearch.client import IngestClient
c = IngestClient(es) c = IngestClient(es)
# bjy: 尝试获取名为'geoip'的管道
try: try:
c.get_pipeline('geoip') c.get_pipeline('geoip')
# bjy: 如果管道不存在,则创建它
except elasticsearch.exceptions.NotFoundError: except elasticsearch.exceptions.NotFoundError:
# bjy: 创建一个geoip管道用于根据IP地址添加地理位置信息
c.put_pipeline('geoip', body='''{ c.put_pipeline('geoip', body='''{
"description" : "Add geoip info", "description" : "Add geoip info",
"processors" : [ "processors" : [
@ -33,58 +47,90 @@ if ELASTICSEARCH_ENABLED:
}''') }''')
# bjy: 定义一个内部文档InnerDoc结构用于存储IP地理位置信息
class GeoIp(InnerDoc): class GeoIp(InnerDoc):
# bjy: 大洲名称
continent_name = Keyword() continent_name = Keyword()
# bjy: 国家ISO代码
country_iso_code = Keyword() country_iso_code = Keyword()
# bjy: 国家名称
country_name = Keyword() country_name = Keyword()
# bjy: 地理坐标(经纬度)
location = GeoPoint() location = GeoPoint()
# bjy: 定义内部文档用于存储用户代理User-Agent中的浏览器信息
class UserAgentBrowser(InnerDoc): class UserAgentBrowser(InnerDoc):
# bjy: 浏览器家族如Chrome, Firefox
Family = Keyword() Family = Keyword()
# bjy: 浏览器版本
Version = Keyword() Version = Keyword()
# bjy: 定义内部文档,用于存储用户代理中的操作系统信息
class UserAgentOS(UserAgentBrowser): class UserAgentOS(UserAgentBrowser):
# bjy: 继承自UserAgentBrowser结构相同
pass pass
# bjy: 定义内部文档,用于存储用户代理中的设备信息
class UserAgentDevice(InnerDoc): class UserAgentDevice(InnerDoc):
# bjy: 设备家族如iPhone, Android
Family = Keyword() Family = Keyword()
# bjy: 设备品牌如Apple, Samsung
Brand = Keyword() Brand = Keyword()
# bjy: 设备型号如iPhone 12
Model = Keyword() Model = Keyword()
# bjy: 定义内部文档,用于存储完整的用户代理信息
class UserAgent(InnerDoc): class UserAgent(InnerDoc):
# bjy: 嵌套浏览器信息
browser = Object(UserAgentBrowser, required=False) browser = Object(UserAgentBrowser, required=False)
# bjy: 嵌套操作系统信息
os = Object(UserAgentOS, required=False) os = Object(UserAgentOS, required=False)
# bjy: 嵌套设备信息
device = Object(UserAgentDevice, required=False) device = Object(UserAgentDevice, required=False)
# bjy: 原始User-Agent字符串
string = Text() string = Text()
# bjy: 是否为爬虫或机器人
is_bot = Boolean() is_bot = Boolean()
# bjy: 定义一个Elasticsearch文档用于存储页面性能数据如响应时间
class ElapsedTimeDocument(Document): class ElapsedTimeDocument(Document):
# bjy: 请求的URL
url = Keyword() url = Keyword()
# bjy: 请求耗时(毫秒)
time_taken = Long() time_taken = Long()
# bjy: 日志记录时间
log_datetime = Date() log_datetime = Date()
# bjy: 客户端IP地址
ip = Keyword() ip = Keyword()
# bjy: 嵌套的IP地理位置信息
geoip = Object(GeoIp, required=False) geoip = Object(GeoIp, required=False)
# bjy: 嵌套的用户代理信息
useragent = Object(UserAgent, required=False) useragent = Object(UserAgent, required=False)
class Index: class Index:
# bjy: 指定索引名称为'performance'
name = 'performance' name = 'performance'
# bjy: 设置索引的分片和副本数
settings = { settings = {
"number_of_shards": 1, "number_of_shards": 1,
"number_of_replicas": 0 "number_of_replicas": 0
} }
class Meta: class Meta:
# bjy: 指定文档类型
doc_type = 'ElapsedTime' doc_type = 'ElapsedTime'
# bjy: 定义一个管理类用于操作ElapsedTimeDocument索引
class ElaspedTimeDocumentManager: class ElaspedTimeDocumentManager:
@staticmethod @staticmethod
def build_index(): def build_index():
# bjy: 如果索引不存在,则创建它
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance") res = client.indices.exists(index="performance")
@ -93,13 +139,16 @@ class ElaspedTimeDocumentManager:
@staticmethod @staticmethod
def delete_index(): def delete_index():
# bjy: 删除'performance'索引
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404]) es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod @staticmethod
def create(url, time_taken, log_datetime, useragent, ip): def create(url, time_taken, log_datetime, useragent, ip):
# bjy: 确保索引存在
ElaspedTimeDocumentManager.build_index() ElaspedTimeDocumentManager.build_index()
# bjy: 构建UserAgent内部文档对象
ua = UserAgent() ua = UserAgent()
ua.browser = UserAgentBrowser() ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family ua.browser.Family = useragent.browser.family
@ -116,8 +165,10 @@ class ElaspedTimeDocumentManager:
ua.string = useragent.ua_string ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot ua.is_bot = useragent.is_bot
# bjy: 创建ElapsedTimeDocument文档实例
doc = ElapsedTimeDocument( doc = ElapsedTimeDocument(
meta={ meta={
# bjy: 使用当前时间的毫秒数作为文档ID
'id': int( 'id': int(
round( round(
time.time() * time.time() *
@ -127,57 +178,78 @@ class ElaspedTimeDocumentManager:
time_taken=time_taken, time_taken=time_taken,
log_datetime=log_datetime, log_datetime=log_datetime,
useragent=ua, ip=ip) useragent=ua, ip=ip)
# bjy: 保存文档,并使用'geoip'管道处理IP地址
doc.save(pipeline="geoip") doc.save(pipeline="geoip")
# bjy: 定义一个Elasticsearch文档用于存储博客文章数据以支持全文搜索
class ArticleDocument(Document): class ArticleDocument(Document):
# bjy: 文章内容使用ik分词器进行索引和搜索
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# bjy: 文章标题使用ik分词器
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# bjy: 作者信息,为一个对象类型
author = Object(properties={ author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer() 'id': Integer()
}) })
# bjy: 分类信息,为一个对象类型
category = Object(properties={ category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer() 'id': Integer()
}) })
# bjy: 标签信息,为一个对象类型
tags = Object(properties={ tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer() 'id': Integer()
}) })
# bjy: 发布时间
pub_time = Date() pub_time = Date()
# bjy: 文章状态
status = Text() status = Text()
# bjy: 评论状态
comment_status = Text() comment_status = Text()
# bjy: 文章类型
type = Text() type = Text()
# bjy: 浏览量
views = Integer() views = Integer()
# bjy: 文章排序权重
article_order = Integer() article_order = Integer()
class Index: class Index:
# bjy: 指定索引名称为'blog'
name = 'blog' name = 'blog'
# bjy: 设置索引的分片和副本数
settings = { settings = {
"number_of_shards": 1, "number_of_shards": 1,
"number_of_replicas": 0 "number_of_replicas": 0
} }
class Meta: class Meta:
# bjy: 指定文档类型
doc_type = 'Article' doc_type = 'Article'
# bjy: 定义一个管理类用于操作ArticleDocument索引
class ArticleDocumentManager(): class ArticleDocumentManager():
def __init__(self): def __init__(self):
# bjy: 初始化时创建索引
self.create_index() self.create_index()
def create_index(self): def create_index(self):
# bjy: 创建'blog'索引
ArticleDocument.init() ArticleDocument.init()
def delete_index(self): def delete_index(self):
# bjy: 删除'blog'索引
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404]) es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles): def convert_to_doc(self, articles):
# bjy: 将Django的Article查询集转换为ArticleDocument对象列表
return [ return [
ArticleDocument( ArticleDocument(
meta={ meta={
@ -202,12 +274,15 @@ class ArticleDocumentManager():
article_order=article.article_order) for article in articles] article_order=article.article_order) for article in articles]
def rebuild(self, articles=None): def rebuild(self, articles=None):
# bjy: 重建索引。如果未提供articles则使用所有文章
ArticleDocument.init() ArticleDocument.init()
articles = articles if articles else Article.objects.all() articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles) docs = self.convert_to_doc(articles)
# bjy: 遍历并保存每个文档
for doc in docs: for doc in docs:
doc.save() doc.save()
def update_docs(self, docs): def update_docs(self, docs):
# bjy: 更新一组文档
for doc in docs: for doc in docs:
doc.save() doc.save()

@ -1,19 +1,32 @@
# bjy: 导入日志模块
import logging import logging
# bjy: 从Django中导入表单模块
from django import forms from django import forms
# bjy: 从haystack一个Django搜索框架中导入基础搜索表单
from haystack.forms import SearchForm from haystack.forms import SearchForm
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# bjy: 定义一个自定义的博客搜索表单继承自Haystack的SearchForm
class BlogSearchForm(SearchForm): class BlogSearchForm(SearchForm):
# bjy: 定义一个名为querydata的字符字段用于接收用户输入的搜索关键词并设置为必填
querydata = forms.CharField(required=True) querydata = forms.CharField(required=True)
# bjy: 重写search方法用于执行搜索逻辑
def search(self): def search(self):
# bjy: 调用父类的search方法执行默认的搜索并返回结果集
datas = super(BlogSearchForm, self).search() datas = super(BlogSearchForm, self).search()
# bjy: 检查表单数据是否有效
if not self.is_valid(): if not self.is_valid():
# bjy: 如果表单无效则调用no_query_found方法通常返回一个空的结果集
return self.no_query_found() return self.no_query_found()
# bjy: 如果用户在querydata字段中输入了内容
if self.cleaned_data['querydata']: if self.cleaned_data['querydata']:
# bjy: 将用户输入的搜索关键词记录到日志中
logger.info(self.cleaned_data['querydata']) logger.info(self.cleaned_data['querydata'])
# bjy: 返回搜索结果
return datas return datas

@ -1,18 +1,23 @@
from django.core.management.base import BaseCommand
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化 # TODO 参数化
# bjy: 定义一个继承自BaseCommand的命令类用于执行构建搜索索引的任务
class Command(BaseCommand): class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"构建搜索索引"
help = 'build search index' help = 'build search index'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options): def handle(self, *args, **options):
# bjy: 检查Elasticsearch功能是否已启用确保在启用状态下才执行索引操作
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
# bjy: 调用ElapsedTimeDocumentManager的类方法构建用于记录耗时的文档索引
ElaspedTimeDocumentManager.build_index() ElaspedTimeDocumentManager.build_index()
# bjy: 创建ElapsedTimeDocument的实例并调用其init方法进行初始化可能是数据同步或设置
manager = ElapsedTimeDocument() manager = ElapsedTimeDocument()
manager.init() manager.init()
# bjy: 创建ArticleDocumentManager的实例用于管理文章的搜索索引
manager = ArticleDocumentManager() manager = ArticleDocumentManager()
# bjy: 删除现有的文章索引,为重建做准备,防止旧数据冲突
manager.delete_index() manager.delete_index()
# bjy: 重新构建文章索引将数据库中的最新文章数据同步到Elasticsearch
manager.rebuild() manager.rebuild()

@ -1,13 +1,20 @@
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
# bjy: 从当前应用的models模块导入Tag和Category模型用于获取数据
from blog.models import Tag, Category from blog.models import Tag, Category
# TODO 参数化 # TODO 参数化
# bjy: 定义一个继承自BaseCommand的命令类用于执行构建搜索词的任务
class Command(BaseCommand): class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"构建搜索词"
help = 'build search words' help = 'build search words'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options): def handle(self, *args, **options):
# bjy: 使用集合推导式获取所有Tag和Category的name字段并自动去重
datas = set([t.name for t in Tag.objects.all()] + datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()]) [t.name for t in Category.objects.all()])
# bjy: 将去重后的搜索词集合中的每个元素用换行符连接,并打印到标准输出
print('\n'.join(datas)) print('\n'.join(datas))

@ -1,11 +1,18 @@
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
# bjy: 从项目工具模块导入cache实例用于操作缓存
from djangoblog.utils import cache from djangoblog.utils import cache
# bjy: 定义一个继承自BaseCommand的命令类用于执行清空缓存的任务
class Command(BaseCommand): class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"清空所有缓存"
help = 'clear the whole cache' help = 'clear the whole cache'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options): def handle(self, *args, **options):
# bjy: 调用cache实例的clear方法清空所有缓存
cache.clear() cache.clear()
# bjy: 使用成功样式向标准输出写入操作成功的信息,并附带换行符
self.stdout.write(self.style.SUCCESS('Cleared cache\n')) self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -1,40 +1,65 @@
# bjy: 从Django的auth模块导入get_user_model函数用于动态获取当前项目激活的用户模型
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
# bjy: 从Django的auth模块导入make_password函数用于创建加密后的密码哈希
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
# bjy: 从当前应用的models模块导入Article, Tag, Category模型用于创建测试数据
from blog.models import Article, Tag, Category from blog.models import Article, Tag, Category
# bjy: 定义一个继承自BaseCommand的命令类用于执行创建测试数据的任务
class Command(BaseCommand): class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"创建测试数据"
help = 'create test datas' help = 'create test datas'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options): def handle(self, *args, **options):
# bjy: 获取或创建一个测试用户,如果不存在则创建,密码已加密
user = get_user_model().objects.get_or_create( user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0] email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# bjy: 获取或创建一个父级分类parent_category为None表示它是顶级分类
pcategory = Category.objects.get_or_create( pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0] name='我是父类目', parent_category=None)[0]
# bjy: 获取或创建一个子分类,并设置其父分类为上面创建的父级分类
category = Category.objects.get_or_create( category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0] name='子类目', parent_category=pcategory)[0]
# bjy: 显式保存子分类实例确保数据已写入数据库虽然get_or_create通常会保存
category.save() category.save()
# bjy: 创建一个基础标签,所有文章都将共用此标签
basetag = Tag() basetag = Tag()
basetag.name = "标签" basetag.name = "标签"
basetag.save() basetag.save()
# bjy: 循环19次创建19篇测试文章和对应的标签
for i in range(1, 20): for i in range(1, 20):
# bjy: 获取或创建一篇文章,关联到上面创建的分类、用户,并设置标题和内容
article = Article.objects.get_or_create( article = Article.objects.get_or_create(
category=category, category=category,
title='nice title ' + str(i), title='nice title ' + str(i),
body='nice content ' + str(i), body='nice content ' + str(i),
author=user)[0] author=user)[0]
# bjy: 为每篇文章创建一个专属标签
tag = Tag() tag = Tag()
tag.name = "标签" + str(i) tag.name = "标签" + str(i)
tag.save() tag.save()
# bjy: 将专属标签和基础标签都添加到当前文章的标签集合中
article.tags.add(tag) article.tags.add(tag)
article.tags.add(basetag) article.tags.add(basetag)
# bjy: 保存文章,使标签关联生效
article.save() article.save()
# bjy: 导入项目的cache工具用于清理缓存
from djangoblog.utils import cache from djangoblog.utils import cache
# bjy: 清空所有缓存,以确保新创建的数据能被正确加载
cache.clear() cache.clear()
# bjy: 使用成功样式向标准输出写入操作完成的信息
self.stdout.write(self.style.SUCCESS('created test datas \n')) self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -1,16 +1,23 @@
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from djangoblog.spider_notify import SpiderNotify from djangoblog.spider_notify import SpiderNotify
# bjy: 从项目工具模块导入get_current_site函数用于获取当前站点域名等信息
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category from blog.models import Article, Tag, Category
# bjy: 获取当前站点的域名用于拼接完整URL
site = get_current_site().domain site = get_current_site().domain
# bjy: 定义一个继承自BaseCommand的命令类用于执行通知百度抓取URL的任务
class Command(BaseCommand): class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"通知百度URL"
help = 'notify baidu url' help = 'notify baidu url'
# bjy: 为命令添加参数,允许用户指定通知的数据类型
def add_arguments(self, parser): def add_arguments(self, parser):
# bjy: 添加一个名为data_type的位置参数类型为字符串且只能从给定的选项中选择
parser.add_argument( parser.add_argument(
'data_type', 'data_type',
type=str, type=str,
@ -21,30 +28,43 @@ class Command(BaseCommand):
'category'], 'category'],
help='article : all article,tag : all tag,category: all category,all: All of these') help='article : all article,tag : all tag,category: all category,all: All of these')
def get_full_url(self, path): # bjy: 定义一个辅助方法用于根据路径拼接完整的URL
@staticmethod
def get_full_url(path):
# bjy: 使用https协议和当前站点域名拼接完整URL
url = "https://{site}{path}".format(site=site, path=path) url = "https://{site}{path}".format(site=site, path=path)
return url return url
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options): def handle(self, *args, **options):
# bjy: 获取用户指定的data_type参数决定通知哪些类型的URL
type = options['data_type'] type = options['data_type']
# bjy: 输出开始获取指定类型URL的信息
self.stdout.write('start get %s' % type) self.stdout.write('start get %s' % type)
# bjy: 初始化一个空列表用于收集所有待通知的URL
urls = [] urls = []
# bjy: 如果类型为article或all则收集所有已发布文章的完整URL
if type == 'article' or type == 'all': if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'): for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url()) urls.append(article.get_full_url())
# bjy: 如果类型为tag或all则收集所有标签的完整URL
if type == 'tag' or type == 'all': if type == 'tag' or type == 'all':
for tag in Tag.objects.all(): for tag in Tag.objects.all():
url = tag.get_absolute_url() url = tag.get_absolute_url()
urls.append(self.get_full_url(url)) urls.append(self.get_full_url(url))
# bjy: 如果类型为category或all则收集所有分类的完整URL
if type == 'category' or type == 'all': if type == 'category' or type == 'all':
for category in Category.objects.all(): for category in Category.objects.all():
url = category.get_absolute_url() url = category.get_absolute_url()
urls.append(self.get_full_url(url)) urls.append(self.get_full_url(url))
# bjy: 输出开始通知URL的数量信息使用成功样式
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
'start notify %d urls' % 'start notify %d urls' %
len(urls))) len(urls)))
# bjy: 调用SpiderNotify的百度通知方法将收集到的URL发送给百度
SpiderNotify.baidu_notify(urls) SpiderNotify.baidu_notify(urls)
# bjy: 输出通知完成的信息,使用成功样式
self.stdout.write(self.style.SUCCESS('finish notify')) self.stdout.write(self.style.SUCCESS('finish notify'))

@ -1,47 +1,76 @@
# bjy: 导入requests库用于发起HTTP请求检测头像URL是否可访问
import requests import requests
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
# bjy: 从Django模板标签模块导入static函数用于生成静态文件的URL
from django.templatetags.static import static from django.templatetags.static import static
# bjy: 从项目工具模块导入save_user_avatar函数用于保存用户头像到本地
from djangoblog.utils import save_user_avatar from djangoblog.utils import save_user_avatar
# bjy: 从oauth应用导入OAuthUser模型用于获取所有OAuth用户数据
from oauth.models import OAuthUser from oauth.models import OAuthUser
# bjy: 从oauth应用导入get_manager_by_type函数用于根据OAuth类型获取对应的管理器
from oauth.oauthmanager import get_manager_by_type from oauth.oauthmanager import get_manager_by_type
# bjy: 定义一个继承自BaseCommand的命令类用于执行同步用户头像的任务
class Command(BaseCommand): class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"同步用户头像"
help = 'sync user avatar' help = 'sync user avatar'
def test_picture(self, url): # bjy: 定义一个辅助方法用于测试给定的URL是否可访问返回200状态码
@staticmethod
def test_picture(url):
try: try:
# bjy: 尝试GET请求设置2秒超时如果状态码为200则返回True
if requests.get(url, timeout=2).status_code == 200: if requests.get(url, timeout=2).status_code == 200:
return True return True
except: except:
# bjy: 任何异常都视为不可访问,静默忽略
pass pass
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options): def handle(self, *args, **options):
# bjy: 获取项目静态文件的基础URL用于判断头像是否为本地静态文件
static_url = static("../") static_url = static("../")
# bjy: 获取所有OAuth用户
users = OAuthUser.objects.all() users = OAuthUser.objects.all()
# bjy: 输出开始同步用户头像的总数信息
self.stdout.write(f'开始同步{len(users)}个用户头像') self.stdout.write(f'开始同步{len(users)}个用户头像')
# bjy: 遍历每个用户,进行头像同步
for u in users: for u in users:
# bjy: 输出当前正在同步的用户昵称
self.stdout.write(f'开始同步:{u.nickname}') self.stdout.write(f'开始同步:{u.nickname}')
# bjy: 获取用户当前的头像URL
url = u.picture url = u.picture
# bjy: 如果头像URL不为空则执行同步逻辑
if url: if url:
# bjy: 如果当前头像URL是本地静态文件路径
if url.startswith(static_url): if url.startswith(static_url):
# bjy: 测试该静态文件是否可访问,若可访问则跳过此用户
if self.test_picture(url): if self.test_picture(url):
continue continue
else: else:
# bjy: 如果不可访问且用户有metadata信息则尝试通过OAuth管理器重新获取头像URL并保存
if u.metadata: if u.metadata:
manage = get_manager_by_type(u.type) manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata) url = manage.get_picture(u.metadata)
url = save_user_avatar(url) url = save_user_avatar(url)
else: else:
# bjy: 如果没有metadata则使用默认头像
url = static('blog/img/avatar.png') url = static('blog/img/avatar.png')
else: else:
# bjy: 如果头像URL不是本地静态文件则直接保存到本地
url = save_user_avatar(url) url = save_user_avatar(url)
else: else:
# bjy: 如果头像URL为空则使用默认头像
url = static('blog/img/avatar.png') url = static('blog/img/avatar.png')
# bjy: 如果最终得到的URL不为空则更新用户头像并保存
if url: if url:
# bjy: 输出同步完成后的用户昵称和头像URL
self.stdout.write( self.stdout.write(
f'结束同步:{u.nickname}.url:{url}') f'结束同步:{u.nickname}.url:{url}')
u.picture = url u.picture = url
u.save() u.save()
# bjy: 输出同步全部结束的信息
self.stdout.write('结束同步') self.stdout.write('结束同步')

@ -1,42 +1,68 @@
# bjy: 导入日志模块
import logging import logging
# bjy: 导入时间模块,用于计算页面渲染时间
import time import time
# bjy: 从ipware库导入get_client_ip函数用于获取客户端真实IP
from ipware import get_client_ip from ipware import get_client_ip
# bjy: 从user_agents库导入parse函数用于解析User-Agent字符串
from user_agents import parse from user_agents import parse
# bjy: 从blog应用的documents模块中导入Elasticsearch是否启用的标志和性能文档管理器
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# bjy: 定义一个中间件类,用于记录页面性能和在线用户信息
class OnlineMiddleware(object): class OnlineMiddleware(object):
# bjy: Django 1.10+ 兼容的初始化方法
def __init__(self, get_response=None): def __init__(self, get_response=None):
# bjy: 保存get_response可调用对象它是Django请求-响应链中的下一个处理器
self.get_response = get_response self.get_response = get_response
# bjy: 调用父类的初始化方法
super().__init__() super().__init__()
# bjy: 中间件的核心调用方法,每个请求都会经过这里
def __call__(self, request): def __call__(self, request):
''' page render time ''' """ page render time """
# bjy: 记录页面渲染开始时间
start_time = time.time() start_time = time.time()
# bjy: 调用下一个中间件或视图,获取响应对象
response = self.get_response(request) response = self.get_response(request)
# bjy: 从请求头中获取User-Agent字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '') http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# bjy: 使用ipware库获取客户端的IP地址
ip, _ = get_client_ip(request) ip, _ = get_client_ip(request)
# bjy: 解析User-Agent字符串得到结构化的用户代理信息
user_agent = parse(http_user_agent) user_agent = parse(http_user_agent)
# bjy: 检查响应是否为流式响应(如文件下载),如果不是,则进行处理
if not response.streaming: if not response.streaming:
try: try:
# bjy: 计算页面渲染耗时(秒)
cast_time = time.time() - start_time cast_time = time.time() - start_time
# bjy: 如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
# bjy: 将耗时转换为毫秒并四舍五入
time_taken = round((cast_time) * 1000, 2) time_taken = round((cast_time) * 1000, 2)
# bjy: 获取请求的URL路径
url = request.path url = request.path
# bjy: 导入Django的时区工具
from django.utils import timezone from django.utils import timezone
# bjy: 调用文档管理器将性能数据保存到Elasticsearch
ElaspedTimeDocumentManager.create( ElaspedTimeDocumentManager.create(
url=url, url=url,
time_taken=time_taken, time_taken=time_taken,
log_datetime=timezone.now(), log_datetime=timezone.now(),
useragent=user_agent, useragent=user_agent,
ip=ip) ip=ip)
# bjy: 将页面渲染耗时替换到响应内容的特定占位符<!!LOAD_TIMES!!>中
response.content = response.content.replace( response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5])) b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
# bjy: 捕获并记录处理过程中可能发生的任何异常
except Exception as e: except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e) logger.error("Error OnlineMiddleware: %s" % e)
# bjy: 返回最终的响应对象
return response return response

@ -1,23 +1,35 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08 # bjy: 此文件由Django 4.1.7于2023-03-29 06:08自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations和models用于定义迁移操作和模型字段
from django.db import migrations, models from django.db import migrations, models
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration): class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0001_initial迁移确保基础表已存在
dependencies = [ dependencies = [
('blog', '0001_initial'), ('blog', '0001_initial'),
] ]
# bjy: 定义此迁移要执行的一系列操作
operations = [ operations = [
# bjy: 操作1为BlogSettings模型添加一个名为'global_footer'的字段
migrations.AddField( migrations.AddField(
# bjy: 指定要操作的模型名称
model_name='blogsettings', model_name='blogsettings',
# bjy: 指定新字段的名称
name='global_footer', name='global_footer',
# bjy: 定义新字段的类型和属性文本类型可为空默认空字符串并设置verbose_name
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
), ),
# bjy: 操作2为BlogSettings模型添加一个名为'global_header'的字段
migrations.AddField( migrations.AddField(
# bjy: 指定要操作的模型名称
model_name='blogsettings', model_name='blogsettings',
# bjy: 指定新字段的名称
name='global_header', name='global_header',
# bjy: 定义新字段的类型和属性文本类型可为空默认空字符串并设置verbose_name
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
), ),
] ]

@ -1,17 +1,25 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45 # bjy: 此文件由Django 4.2.1于2023-05-09 07:45自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations和models用于定义迁移操作和模型字段
from django.db import migrations, models from django.db import migrations, models
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration): class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0002_blogsettings_global_footer_and_more迁移
dependencies = [ dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'), ('blog', '0002_blogsettings_global_footer_and_more'),
] ]
# bjy: 定义此迁移要执行的一系列操作
operations = [ operations = [
# bjy: 操作为BlogSettings模型添加一个新字段
migrations.AddField( migrations.AddField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings', model_name='blogsettings',
# bjy: 指定新字段的名称为'comment_need_review'
name='comment_need_review', name='comment_need_review',
# bjy: 定义新字段的类型和属性布尔类型默认值为False并设置verbose_name
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
), ),
] ]

@ -1,27 +1,43 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51 # bjy: 此文件由Django 4.2.1于2023-05-09 07:51自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations用于定义迁移操作
from django.db import migrations from django.db import migrations
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration): class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0003_blogsettings_comment_need_review迁移
dependencies = [ dependencies = [
('blog', '0003_blogsettings_comment_need_review'), ('blog', '0003_blogsettings_comment_need_review'),
] ]
# bjy: 定义此迁移要执行的一系列操作
operations = [ operations = [
# bjy: 操作1重命名BlogSettings模型中的一个字段
migrations.RenameField( migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings', model_name='blogsettings',
# bjy: 指定字段的原始名称为'analyticscode'
old_name='analyticscode', old_name='analyticscode',
# bjy: 指定字段的新名称为'analytics_code'
new_name='analytics_code', new_name='analytics_code',
), ),
# bjy: 操作2重命名BlogSettings模型中的另一个字段
migrations.RenameField( migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings', model_name='blogsettings',
# bjy: 指定字段的原始名称为'beiancode'
old_name='beiancode', old_name='beiancode',
# bjy: 指定字段的新名称为'beian_code'
new_name='beian_code', new_name='beian_code',
), ),
# bjy: 操作3重命名BlogSettings模型中的第三个字段
migrations.RenameField( migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings', model_name='blogsettings',
# bjy: 指定字段的原始名称为'sitename'
old_name='sitename', old_name='sitename',
# bjy: 指定字段的新名称为'site_name'
new_name='site_name', new_name='site_name',
), ),
] ]

@ -1,17 +1,24 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41 # bjy: 此文件由Django 4.2.7于2024-01-26 02:41自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations用于定义迁移操作
from django.db import migrations from django.db import migrations
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration): class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0005迁移
dependencies = [ dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'), ('blog', '0005_alter_article_options_alter_category_options_and_more'),
] ]
# bjy: 定义此迁移要执行的一系列操作
operations = [ operations = [
# bjy: 操作修改BlogSettings模型的Meta选项更新verbose_name为英文
migrations.AlterModelOptions( migrations.AlterModelOptions(
# bjy: 指定要操作的模型名称为'blogsettings'
name='blogsettings', name='blogsettings',
# bjy: 更新模型的verbose_name和verbose_name_plural为'Website configuration'
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
), ),
] ]

@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-11-13 13:53
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='article',
name='users_like',
field=models.ManyToManyField(blank=True, related_name='articles_liked', to=settings.AUTH_USER_MODEL, verbose_name='点赞用户'),
),
]

@ -1,49 +1,74 @@
# bjy: 导入日志模块
import logging import logging
# bjy: 导入正则表达式模块
import re import re
# bjy: 导入抽象基类模块,用于定义抽象方法
from abc import abstractmethod from abc import abstractmethod
# bjy: 从Django中导入设置、异常、模型、URL反向解析、时区和国际化工具
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# bjy: 从Django MDEditor中导入Markdown文本字段
from mdeditor.fields import MDTextField from mdeditor.fields import MDTextField
# bjy: 从uuslug中导入slugify函数用于生成URL友好的slug
from uuslug import slugify from uuslug import slugify
# bjy: 从项目工具模块中导入缓存装饰器和缓存对象
from djangoblog.utils import cache_decorator, cache from djangoblog.utils import cache_decorator, cache
# bjy: 从项目工具模块中导入获取当前站点的函数
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# bjy: 定义一个文本选择类,用于链接显示类型
class LinkShowType(models.TextChoices): class LinkShowType(models.TextChoices):
# bjy: 首页
I = ('i', _('index')) I = ('i', _('index'))
# bjy: 列表页
L = ('l', _('list')) L = ('l', _('list'))
# bjy: 文章页
P = ('p', _('post')) P = ('p', _('post'))
# bjy: 所有页面
A = ('a', _('all')) A = ('a', _('all'))
# bjy: 幻灯片
S = ('s', _('slide')) S = ('s', _('slide'))
# bjy: 定义一个基础模型类,作为其他模型的父类
class BaseModel(models.Model): class BaseModel(models.Model):
# bjy: 自增主键
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
# bjy: 创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) last_modify_time = models.DateTimeField(_('modify time'), default=now)
# bjy: 重写save方法以实现自定义逻辑
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# bjy: 检查是否是更新文章浏览量的操作
is_update_views = isinstance( is_update_views = isinstance(
self, self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views: if is_update_views:
# bjy: 如果是则直接更新数据库中的views字段避免触发其他save逻辑
Article.objects.filter(pk=self.pk).update(views=self.views) Article.objects.filter(pk=self.pk).update(views=self.views)
else: else:
# bjy: 如果模型有slug字段则根据title或name自动生成slug
if 'slug' in self.__dict__: if 'slug' in self.__dict__:
slug = getattr( slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr( self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name') self, 'name')
setattr(self, 'slug', slugify(slug)) setattr(self, 'slug', slugify(slug))
# bjy: 调用父类的save方法
super().save(*args, **kwargs) super().save(*args, **kwargs)
# bjy: 获取模型的完整URL包括域名
def get_full_url(self): def get_full_url(self):
site = get_current_site().domain site = get_current_site().domain
url = "https://{site}{path}".format(site=site, url = "https://{site}{path}".format(site=site,
@ -51,72 +76,102 @@ class BaseModel(models.Model):
return url return url
class Meta: class Meta:
# bjy: 设置为抽象模型,不会在数据库中创建表
abstract = True abstract = True
# bjy: 定义一个抽象方法,要求子类必须实现
@abstractmethod @abstractmethod
def get_absolute_url(self): def get_absolute_url(self):
pass pass
# bjy: 定义文章模型
class Article(BaseModel): class Article(BaseModel):
"""文章""" """文章"""
# bjy: 文章状态选择
STATUS_CHOICES = ( STATUS_CHOICES = (
('d', _('Draft')), ('d', _('Draft')),
('p', _('Published')), ('p', _('Published')),
) )
# bjy: 评论状态选择
COMMENT_STATUS = ( COMMENT_STATUS = (
('o', _('Open')), ('o', _('Open')),
('c', _('Close')), ('c', _('Close')),
) )
# bjy: 文章类型选择
TYPE = ( TYPE = (
('a', _('Article')), ('a', _('Article')),
('p', _('Page')), ('p', _('Page')),
) )
# bjy: 文章标题,唯一
title = models.CharField(_('title'), max_length=200, unique=True) title = models.CharField(_('title'), max_length=200, unique=True)
# bjy: 文章正文使用Markdown编辑器
body = MDTextField(_('body')) body = MDTextField(_('body'))
# bjy: 发布时间
pub_time = models.DateTimeField( pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now) _('publish time'), blank=False, null=False, default=now)
# bjy: 文章状态
status = models.CharField( status = models.CharField(
_('status'), _('status'),
max_length=1, max_length=1,
choices=STATUS_CHOICES, choices=STATUS_CHOICES,
default='p') default='p')
# bjy: 评论状态
comment_status = models.CharField( comment_status = models.CharField(
_('comment status'), _('comment status'),
max_length=1, max_length=1,
choices=COMMENT_STATUS, choices=COMMENT_STATUS,
default='o') default='o')
# bjy: 文章类型
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
# bjy: 浏览量
views = models.PositiveIntegerField(_('views'), default=0) views = models.PositiveIntegerField(_('views'), default=0)
# bjy: 作者,外键关联到用户模型
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
blank=False, blank=False,
null=False, null=False,
on_delete=models.CASCADE) on_delete=models.CASCADE)
# bjy: 文章排序权重
article_order = models.IntegerField( article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0) _('order'), blank=False, null=False, default=0)
# bjy: 是否显示目录
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
# bjy: 分类外键关联到Category模型
category = models.ForeignKey( category = models.ForeignKey(
'Category', 'Category',
verbose_name=_('category'), verbose_name=_('category'),
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, blank=False,
null=False) null=False)
# bjy: 标签多对多关联到Tag模型
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
users_like = models.ManyToManyField(
settings.AUTH_USER_MODEL, # 关联到用户模型
related_name='articles_liked', # 反向关系名称user.articles_liked.all()可获取用户点赞的所有文章
blank=True, # 允许文章没有被任何用户点赞
verbose_name='点赞用户' # 在Admin后台显示的字段名称
)
# bjy: 将body字段转换为字符串
def body_to_string(self): def body_to_string(self):
return self.body return self.body
# bjy: 定义文章的字符串表示
def __str__(self): def __str__(self):
return self.title return self.title
class Meta: class Meta:
# bjy: 默认排序方式
ordering = ['-article_order', '-pub_time'] ordering = ['-article_order', '-pub_time']
# bjy: 模型的单数和复数名称
verbose_name = _('article') verbose_name = _('article')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
# bjy: 指定按哪个字段获取最新对象
get_latest_by = 'id' get_latest_by = 'id'
# bjy: 获取文章的绝对URL
def get_absolute_url(self): def get_absolute_url(self):
return reverse('blog:detailbyid', kwargs={ return reverse('blog:detailbyid', kwargs={
'article_id': self.id, 'article_id': self.id,
@ -125,6 +180,7 @@ class Article(BaseModel):
'day': self.creation_time.day 'day': self.creation_time.day
}) })
# bjy: 获取文章的分类树路径,带缓存
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10)
def get_category_tree(self): def get_category_tree(self):
tree = self.category.get_category_tree() tree = self.category.get_category_tree()
@ -132,13 +188,16 @@ class Article(BaseModel):
return names return names
# bjy: 重写save方法
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
# bjy: 增加浏览量
def viewed(self): def viewed(self):
self.views += 1 self.views += 1
self.save(update_fields=['views']) self.save(update_fields=['views'])
# bjy: 获取文章的评论列表,带缓存
def comment_list(self): def comment_list(self):
cache_key = 'article_comments_{id}'.format(id=self.id) cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key) value = cache.get(cache_key)
@ -151,21 +210,25 @@ class Article(BaseModel):
logger.info('set article comments:{id}'.format(id=self.id)) logger.info('set article comments:{id}'.format(id=self.id))
return comments return comments
# bjy: 获取文章在Admin后台的编辑URL
def get_admin_url(self): def get_admin_url(self):
info = (self._meta.app_label, self._meta.model_name) info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,)) return reverse('admin:%s_%s_change' % info, args=(self.pk,))
# bjy: 获取下一篇文章,带缓存
@cache_decorator(expiration=60 * 100) @cache_decorator(expiration=60 * 100)
def next_article(self): def next_article(self):
# 下一篇 # 下一篇
return Article.objects.filter( return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first() id__gt=self.id, status='p').order_by('id').first()
# bjy: 获取上一篇文章,带缓存
@cache_decorator(expiration=60 * 100) @cache_decorator(expiration=60 * 100)
def prev_article(self): def prev_article(self):
# 前一篇 # 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first() return Article.objects.filter(id__lt=self.id, status='p').first()
# bjy: 从文章正文中提取第一张图片的URL
def get_first_image_url(self): def get_first_image_url(self):
""" """
Get the first image url from article.body. Get the first image url from article.body.
@ -177,31 +240,40 @@ class Article(BaseModel):
return "" return ""
# bjy: 定义分类模型
class Category(BaseModel): class Category(BaseModel):
"""文章分类""" """文章分类"""
# bjy: 分类名称,唯一
name = models.CharField(_('category name'), max_length=30, unique=True) name = models.CharField(_('category name'), max_length=30, unique=True)
# bjy: 父分类,自关联外键
parent_category = models.ForeignKey( parent_category = models.ForeignKey(
'self', 'self',
verbose_name=_('parent category'), verbose_name=_('parent category'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE)
# bjy: URL友好的别名
slug = models.SlugField(default='no-slug', max_length=60, blank=True) slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# bjy: 排序索引
index = models.IntegerField(default=0, verbose_name=_('index')) index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta: class Meta:
# bjy: 按索引降序排列
ordering = ['-index'] ordering = ['-index']
verbose_name = _('category') verbose_name = _('category')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
# bjy: 获取分类的绝对URL
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
'blog:category_detail', kwargs={ 'blog:category_detail', kwargs={
'category_name': self.slug}) 'category_name': self.slug})
# bjy: 定义分类的字符串表示
def __str__(self): def __str__(self):
return self.name return self.name
# bjy: 递归获取分类的所有父级分类,带缓存
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10)
def get_category_tree(self): def get_category_tree(self):
""" """
@ -218,6 +290,7 @@ class Category(BaseModel):
parse(self) parse(self)
return categorys return categorys
# bjy: 获取当前分类的所有子分类,带缓存
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10)
def get_sub_categorys(self): def get_sub_categorys(self):
""" """
@ -240,136 +313,186 @@ class Category(BaseModel):
return categorys return categorys
# bjy: 定义标签模型
class Tag(BaseModel): class Tag(BaseModel):
"""文章标签""" """文章标签"""
# bjy: 标签名称,唯一
name = models.CharField(_('tag name'), max_length=30, unique=True) name = models.CharField(_('tag name'), max_length=30, unique=True)
# bjy: URL友好的别名
slug = models.SlugField(default='no-slug', max_length=60, blank=True) slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# bjy: 定义标签的字符串表示
def __str__(self): def __str__(self):
return self.name return self.name
# bjy: 获取标签的绝对URL
def get_absolute_url(self): def get_absolute_url(self):
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
# bjy: 获取使用该标签的文章数量,带缓存
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10)
def get_article_count(self): def get_article_count(self):
return Article.objects.filter(tags__name=self.name).distinct().count() return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta: class Meta:
# bjy: 按名称排序
ordering = ['name'] ordering = ['name']
verbose_name = _('tag') verbose_name = _('tag')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
# bjy: 定义友情链接模型
class Links(models.Model): class Links(models.Model):
"""友情链接""" """友情链接"""
# bjy: 链接名称,唯一
name = models.CharField(_('link name'), max_length=30, unique=True) name = models.CharField(_('link name'), max_length=30, unique=True)
# bjy: 链接URL
link = models.URLField(_('link')) link = models.URLField(_('link'))
# bjy: 排序权重,唯一
sequence = models.IntegerField(_('order'), unique=True) sequence = models.IntegerField(_('order'), unique=True)
# bjy: 是否启用
is_enable = models.BooleanField( is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False) _('is show'), default=True, blank=False, null=False)
# bjy: 显示类型
show_type = models.CharField( show_type = models.CharField(
_('show type'), _('show type'),
max_length=1, max_length=1,
choices=LinkShowType.choices, choices=LinkShowType.choices,
default=LinkShowType.I) default=LinkShowType.I)
# bjy: 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta: class Meta:
# bjy: 按排序权重升序排列
ordering = ['sequence'] ordering = ['sequence']
verbose_name = _('link') verbose_name = _('link')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
# bjy: 定义链接的字符串表示
def __str__(self): def __str__(self):
return self.name return self.name
# bjy: 定义侧边栏模型
class SideBar(models.Model): class SideBar(models.Model):
"""侧边栏,可以展示一些html内容""" """侧边栏,可以展示一些html内容"""
# bjy: 侧边栏标题
name = models.CharField(_('title'), max_length=100) name = models.CharField(_('title'), max_length=100)
# bjy: 侧边栏内容HTML
content = models.TextField(_('content')) content = models.TextField(_('content'))
# bjy: 排序权重,唯一
sequence = models.IntegerField(_('order'), unique=True) sequence = models.IntegerField(_('order'), unique=True)
# bjy: 是否启用
is_enable = models.BooleanField(_('is enable'), default=True) is_enable = models.BooleanField(_('is enable'), default=True)
# bjy: 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta: class Meta:
# bjy: 按排序权重升序排列
ordering = ['sequence'] ordering = ['sequence']
verbose_name = _('sidebar') verbose_name = _('sidebar')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
# bjy: 定义侧边栏的字符串表示
def __str__(self): def __str__(self):
return self.name return self.name
# bjy: 定义博客设置模型
class BlogSettings(models.Model): class BlogSettings(models.Model):
"""blog的配置""" """blog的配置"""
# bjy: 网站名称
site_name = models.CharField( site_name = models.CharField(
_('site name'), _('site name'),
max_length=200, max_length=200,
null=False, null=False,
blank=False, blank=False,
default='') default='')
# bjy: 网站描述
site_description = models.TextField( site_description = models.TextField(
_('site description'), _('site description'),
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='')
# bjy: SEO描述
site_seo_description = models.TextField( 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='')
# bjy: 网站关键词
site_keywords = models.TextField( site_keywords = models.TextField(
_('site keywords'), _('site keywords'),
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='')
# bjy: 文章摘要长度
article_sub_length = models.IntegerField(_('article sub length'), default=300) article_sub_length = models.IntegerField(_('article sub length'), default=300)
# bjy: 侧边栏文章数量
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
# bjy: 侧边栏评论数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
# bjy: 文章页评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) article_comment_count = models.IntegerField(_('article comment count'), default=5)
# bjy: 是否显示Google AdSense
show_google_adsense = models.BooleanField(_('show adsense'), default=False) show_google_adsense = models.BooleanField(_('show adsense'), default=False)
# bjy: Google AdSense代码
google_adsense_codes = models.TextField( google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='') _('adsense code'), max_length=2000, null=True, blank=True, default='')
# bjy: 是否开启全站评论
open_site_comment = models.BooleanField(_('open site comment'), default=True) open_site_comment = models.BooleanField(_('open site comment'), default=True)
# bjy: 公共头部HTML代码
global_header = models.TextField("公共头部", null=True, blank=True, default='') global_header = models.TextField("公共头部", null=True, blank=True, default='')
# bjy: 公共尾部HTML代码
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
# bjy: ICP备案号
beian_code = models.CharField( beian_code = models.CharField(
'备案号', '备案号',
max_length=2000, max_length=2000,
null=True, null=True,
blank=True, blank=True,
default='') default='')
# bjy: 网站统计代码
analytics_code = models.TextField( analytics_code = models.TextField(
"网站统计代码", "网站统计代码",
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='')
# bjy: 是否显示公安备案号
show_gongan_code = models.BooleanField( show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False) '是否显示公安备案号', default=False, null=False)
# bjy: 公安备案号
gongan_beiancode = models.TextField( gongan_beiancode = models.TextField(
'公安备案号', '公安备案号',
max_length=2000, max_length=2000,
null=True, null=True,
blank=True, blank=True,
default='') default='')
# bjy: 评论是否需要审核
comment_need_review = models.BooleanField( comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False) '评论是否需要审核', default=False, null=False)
class Meta: class Meta:
# bjy: 模型的单数和复数名称
verbose_name = _('Website configuration') verbose_name = _('Website configuration')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
# bjy: 定义设置的字符串表示
def __str__(self): def __str__(self):
return self.site_name return self.site_name
# bjy: 重写clean方法用于模型验证
def clean(self): def clean(self):
# bjy: 确保数据库中只能有一条配置记录
if BlogSettings.objects.exclude(id=self.id).count(): if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration')) raise ValidationError(_('There can only be one configuration'))
# bjy: 重写save方法保存后清除缓存
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
from djangoblog.utils import cache from djangoblog.utils import cache

@ -1,13 +1,21 @@
# bjy: 从haystack框架中导入indexes模块用于创建搜索索引
from haystack import indexes from haystack import indexes
# bjy: 从blog应用中导入Article模型
from blog.models import Article from blog.models import Article
# bjy: 为Article模型定义一个搜索索引类
class ArticleIndex(indexes.SearchIndex, indexes.Indexable): class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
# bjy: 定义一个主文本字段,`document=True`表示这是搜索的主要字段
# bjy: `use_template=True`表示该字段的内容将由一个模板来生成
text = indexes.CharField(document=True, use_template=True) text = indexes.CharField(document=True, use_template=True)
# bjy: `get_model`方法必须实现,用于返回此索引对应的模型类
def get_model(self): def get_model(self):
return Article return Article
# bjy: `index_queryset`方法定义了哪些模型实例应该被建立索引
def index_queryset(self, using=None): def index_queryset(self, using=None):
# bjy: 这里只返回状态为'p'(已发布)的文章
return self.get_model().objects.filter(status='p') return self.get_model().objects.filter(status='p')

@ -1,9 +1,17 @@
/* bjy: 定义一个名为.button的CSS类用于设置按钮的通用样式 */
.button { .button {
/* bjy: 移除按钮的默认边框 */
border: none; border: none;
/* bjy: 设置按钮的内边距上下4像素左右80像素 */
padding: 4px 80px; padding: 4px 80px;
/* bjy: 设置按钮内部文本的水平居中对齐 */
text-align: center; text-align: center;
/* bjy: 移除文本装饰(如下划线),通常用于链接样式的按钮 */
text-decoration: none; text-decoration: none;
/* bjy: 将按钮设置为行内块级元素,使其可以设置宽高并与其他元素在同一行显示 */
display: inline-block; display: inline-block;
/* bjy: 设置按钮内部文本的字体大小为16像素 */
font-size: 16px; font-size: 16px;
/* bjy: 设置按钮的外边距上下4像素左右2像素用于控制按钮之间的间距 */
margin: 4px 2px; margin: 4px 2px;
} }

@ -1,45 +1,76 @@
// bjy: 声明一个全局变量wait用于倒计时初始值为60秒
let wait = 60; let wait = 60;
// bjy: 定义一个名为time的函数用于处理按钮的倒计时效果
// bjy: 参数o代表触发倒计时的按钮元素
function time(o) { function time(o) {
// bjy: 如果倒计时结束wait为0
if (wait == 0) { if (wait == 0) {
// bjy: 移除按钮的disabled属性使其重新可点击
o.removeAttribute("disabled"); o.removeAttribute("disabled");
// bjy: 将按钮的显示文本恢复为“获取验证码”
o.value = "获取验证码"; o.value = "获取验证码";
// bjy: 重置倒计时变量为60以便下次使用
wait = 60 wait = 60
// bjy: 结束函数执行
return false return false
} else { } else {
// bjy: 如果倒计时未结束,禁用按钮,防止重复点击
o.setAttribute("disabled", true); o.setAttribute("disabled", true);
// bjy: 更新按钮的显示文本,显示剩余的倒计时秒数
o.value = "重新发送(" + wait + ")"; o.value = "重新发送(" + wait + ")";
// bjy: 倒计时秒数减一
wait--; wait--;
// bjy: 设置一个1秒1000毫秒后执行的定时器
setTimeout(function () { setTimeout(function () {
// bjy: 定时器回调函数中递归调用time函数实现每秒更新一次倒计时
time(o) time(o)
}, },
1000) 1000)
} }
} }
// bjy: 为ID为"btn"的元素绑定点击事件处理函数
document.getElementById("btn").onclick = function () { document.getElementById("btn").onclick = function () {
// bjy: 使用jQuery选择器获取邮箱输入框元素
let id_email = $("#id_email") let id_email = $("#id_email")
// bjy: 使用jQuery选择器获取CSRF令牌的值用于Django的POST请求安全验证
let token = $("*[name='csrfmiddlewaretoken']").val() let token = $("*[name='csrfmiddlewaretoken']").val()
// bjy: 将this即被点击的按钮的引用保存到ts变量中以便在AJAX回调中使用
let ts = this let ts = this
// bjy: 使用jQuery选择器获取用于显示错误信息的元素
let myErr = $("#myErr") let myErr = $("#myErr")
// bjy: 使用jQuery发起一个AJAX请求
$.ajax( $.ajax(
{ {
// bjy: 请求的URL地址
url: "/forget_password_code/", url: "/forget_password_code/",
// bjy: 请求的类型为POST
type: "POST", type: "POST",
// bjy: 发送到服务器的数据包含邮箱和CSRF令牌
data: { data: {
"email": id_email.val(), "email": id_email.val(),
"csrfmiddlewaretoken": token "csrfmiddlewaretoken": token
}, },
// bjy: 定义请求成功时的回调函数result是服务器返回的数据
success: function (result) { success: function (result) {
// bjy: 如果服务器返回的结果不是"ok"(表示发送失败或有错误)
if (result != "ok") { if (result != "ok") {
// bjy: 移除页面上可能存在的旧错误提示
myErr.remove() myErr.remove()
// bjy: 在邮箱输入框后面动态添加一个错误提示列表,显示服务器返回的错误信息
id_email.after("<ul className='errorlist' id='myErr'><li>" + result + "</li></ul>") id_email.after("<ul className='errorlist' id='myErr'><li>" + result + "</li></ul>")
// bjy: 结束函数执行
return return
} }
// bjy: 如果发送成功,移除页面上可能存在的旧错误提示
myErr.remove() myErr.remove()
// bjy: 调用time函数开始按钮的倒计时效果
time(ts) time(ts)
}, },
// bjy: 定义请求失败时的回调函数e是错误对象
error: function (e) { error: function (e) {
// bjy: 弹出一个警告框,提示用户发送失败
alert("发送失败,请重试") alert("发送失败,请重试")
} }
} }

@ -8,44 +8,64 @@
* details, see https://creativecommons.org/licenses/by/3.0/. * details, see https://creativecommons.org/licenses/by/3.0/.
*/ */
// Intended to prevent false-positive bug reports about Bootstrap not working properly in old versions of IE due to folks testing using IE's unreliable emulation modes. // Intended to prevent false-positive bug reports about Bootstrap not working properly in old versions of IE due to folks testing using IE's unreliable emulation modes.
// bjy: 使用一个立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
(function () { (function () {
// bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作
'use strict'; 'use strict';
// bjy: 定义一个函数用于从用户代理字符串中获取IE的模拟版本号
function emulatedIEMajorVersion() { function emulatedIEMajorVersion() {
// bjy: 使用正则表达式匹配用户代理字符串中的 "MSIE x.x" 部分
var groups = /MSIE ([0-9.]+)/.exec(window.navigator.userAgent) var groups = /MSIE ([0-9.]+)/.exec(window.navigator.userAgent)
// bjy: 如果匹配不到说明不是IE或版本号无法识别返回null
if (groups === null) { if (groups === null) {
return null return null
} }
// bjy: 将匹配到的版本号字符串(如 "10.0")转换为整数
var ieVersionNum = parseInt(groups[1], 10) var ieVersionNum = parseInt(groups[1], 10)
// bjy: 取整数部分作为主版本号
var ieMajorVersion = Math.floor(ieVersionNum) var ieMajorVersion = Math.floor(ieVersionNum)
// bjy: 返回模拟的IE主版本号
return ieMajorVersion return ieMajorVersion
} }
// bjy: 定义一个函数用于检测当前浏览器实际运行的IE版本即使它处于旧版IE的模拟模式下
function actualNonEmulatedIEMajorVersion() { function actualNonEmulatedIEMajorVersion() {
// Detects the actual version of IE in use, even if it's in an older-IE emulation mode. // bjy: 此函数通过IE特有的JScript条件编译来检测真实版本
// IE JavaScript conditional compilation docs: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx // bjy: IE JavaScript条件编译文档: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx
// @cc_on docs: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx // bjy: @cc_on 文档: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx
// bjy: 创建一个新的Function其内容是IE的条件编译语句用于获取JScript版本
var jscriptVersion = new Function('/*@cc_on return @_jscript_version; @*/')() // jshint ignore:line var jscriptVersion = new Function('/*@cc_on return @_jscript_version; @*/')() // jshint ignore:line
// bjy: 如果jscriptVersion未定义说明是IE11或更高版本且不在模拟模式下
if (jscriptVersion === undefined) { if (jscriptVersion === undefined) {
return 11 // IE11+ not in emulation mode return 11 // IE11+ not in emulation mode
} }
// bjy: 如果JScript版本小于9则判断为IE8或更低版本
if (jscriptVersion < 9) { if (jscriptVersion < 9) {
return 8 // IE8 (or lower; haven't tested on IE<8) return 8 // IE8 (or lower; haven't tested on IE<8)
} }
// bjy: 否则返回JScript版本这对应于IE9或IE10在任何模式下或IE11在非IE11模式下
return jscriptVersion // IE9 or IE10 in any mode, or IE11 in non-IE11 mode return jscriptVersion // IE9 or IE10 in any mode, or IE11 in non-IE11 mode
} }
// bjy: 获取当前浏览器的用户代理字符串
var ua = window.navigator.userAgent var ua = window.navigator.userAgent
// bjy: 检查用户代理中是否包含'Opera'或'Presto'Opera的旧版渲染引擎
if (ua.indexOf('Opera') > -1 || ua.indexOf('Presto') > -1) { if (ua.indexOf('Opera') > -1 || ua.indexOf('Presto') > -1) {
return // Opera, which might pretend to be IE return // Opera, which might pretend to be IE
} }
// bjy: 调用函数获取模拟的IE版本号
var emulated = emulatedIEMajorVersion() var emulated = emulatedIEMajorVersion()
// bjy: 如果模拟版本为null说明不是IE浏览器直接返回
if (emulated === null) { if (emulated === null) {
return // Not IE return // Not IE
} }
// bjy: 调用函数获取实际的IE版本号
var nonEmulated = actualNonEmulatedIEMajorVersion() var nonEmulated = actualNonEmulatedIEMajorVersion()
// bjy: 比较模拟版本和实际版本如果不相同说明IE正处于模拟模式下
if (emulated !== nonEmulated) { if (emulated !== nonEmulated) {
// bjy: 弹出一个警告框提示用户当前正处于IE模拟模式并警告其行为可能与真实旧版IE不同
window.alert('WARNING: You appear to be using IE' + nonEmulated + ' in IE' + emulated + ' emulation mode.\nIE emulation modes can behave significantly differently from ACTUAL older versions of IE.\nPLEASE DON\'T FILE BOOTSTRAP BUGS based on testing in IE emulation modes!') window.alert('WARNING: You appear to be using IE' + nonEmulated + ' in IE' + emulated + ' emulation mode.\nIE emulation modes can behave significantly differently from ACTUAL older versions of IE.\nPLEASE DON\'T FILE BOOTSTRAP BUGS based on testing in IE emulation modes!')
} }
})(); })();

@ -7,17 +7,27 @@
// See the Getting Started docs for more information: // See the Getting Started docs for more information:
// http://getbootstrap.com/getting-started/#support-ie10-width // http://getbootstrap.com/getting-started/#support-ie10-width
// bjy: 使用一个立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
(function () { (function () {
// bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作
'use strict'; 'use strict';
// bjy: 检查当前浏览器的用户代理字符串User Agent是否匹配IEMobile/10.0
// bjy: 这是为了专门识别运行在Windows Phone 8上的IE10移动版浏览器
if (navigator.userAgent.match(/IEMobile\/10\.0/)) { if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
// bjy: 如果匹配,则创建一个新的<style>元素
var msViewportStyle = document.createElement('style') var msViewportStyle = document.createElement('style')
// bjy: 向这个<style>元素中添加一个CSS规则
msViewportStyle.appendChild( msViewportStyle.appendChild(
// bjy: 创建一个包含CSS规则的文本节点
document.createTextNode( document.createTextNode(
// bjy: CSS规则@-ms-viewport是IE10的一个特有规则用于设置视口大小
// bjy: width:auto!important 覆盖了IE10错误的默认设置强制视口宽度为设备宽度
'@-ms-viewport{width:auto!important}' '@-ms-viewport{width:auto!important}'
) )
) )
// bjy: 将这个新创建的<style>元素添加到文档的<head>部分使CSS规则生效
document.querySelector('head').appendChild(msViewportStyle) document.querySelector('head').appendChild(msViewportStyle)
} }
})(); })();

@ -1,13 +1,16 @@
/* /*
Styles for older IE versions (previous to IE9). bjy: IEIE9
*/ */
/* bjy: 为body元素设置浅灰色背景 */
body { body {
background-color: #e6e6e6; background-color: #e6e6e6;
} }
/* bjy: 当body没有自定义背景时设置背景为白色 */
body.custom-background-empty { body.custom-background-empty {
background-color: #fff; background-color: #fff;
} }
/* bjy: 在没有自定义背景或背景为白色的页面上,移除网站容器的阴影、边距和内边距 */
body.custom-background-empty .site, body.custom-background-empty .site,
body.custom-background-white .site { body.custom-background-white .site {
box-shadow: none; box-shadow: none;
@ -15,14 +18,17 @@ body.custom-background-white .site {
margin-top: 0; margin-top: 0;
padding: 0; padding: 0;
} }
/* bjy: 隐藏辅助性文本和屏幕阅读器专用文本,通过裁剪使其在视觉上不可见 */
.assistive-text, .assistive-text,
.site .screen-reader-text { .site .screen-reader-text {
clip: rect(1px 1px 1px 1px); clip: rect(1px 1px 1px 1px);
} }
/* bjy: 在全宽布局下,使内容区域占满整个宽度,并取消浮动 */
.full-width .site-content { .full-width .site-content {
float: none; float: none;
width: 100%; width: 100%;
} }
/* bjy: 防止在IE8中带有height和width属性的图片被拉伸设置width为auto */
img.size-full, img.size-full,
img.size-large, img.size-large,
img.header-image, img.header-image,
@ -32,15 +38,18 @@ img[class*="wp-image-"],
img[class*="attachment-"] { img[class*="attachment-"] {
width: auto; /* Prevent stretching of full-size and large-size images with height and width attributes in IE8 */ width: auto; /* Prevent stretching of full-size and large-size images with height and width attributes in IE8 */
} }
/* bjy: 作者头像向左浮动,并设置上边距 */
.author-avatar { .author-avatar {
float: left; float: left;
margin-top: 8px; margin-top: 8px;
margin-top: 0.571428571rem; margin-top: 0.571428571rem;
} }
/* bjy: 作者描述向右浮动宽度占80% */
.author-description { .author-description {
float: right; float: right;
width: 80%; width: 80%;
} }
/* bjy: 网站主容器样式:居中、最大宽度、阴影、溢出隐藏、内边距 */
.site { .site {
box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3); box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3);
margin: 48px auto; margin: 48px auto;
@ -48,27 +57,33 @@ img[class*="attachment-"] {
overflow: hidden; overflow: hidden;
padding: 0 40px; padding: 0 40px;
} }
/* bjy: 网站内容区域向左浮动宽度约为65.1% */
.site-content { .site-content {
float: left; float: left;
width: 65.104166667%; width: 65.104166667%;
} }
/* bjy: 在首页模板、附件页面或全宽布局下内容区域宽度为100% */
body.template-front-page .site-content, body.template-front-page .site-content,
body.attachment .site-content, body.attachment .site-content,
body.full-width .site-content { body.full-width .site-content {
width: 100%; width: 100%;
} }
/* bjy: 小工具区域向右浮动宽度约为26.04% */
.widget-area { .widget-area {
float: right; float: right;
width: 26.041666667%; width: 26.041666667%;
} }
/* bjy: 网站标题和副标题左对齐 */
.site-header h1, .site-header h1,
.site-header h2 { .site-header h2 {
text-align: left; text-align: left;
} }
/* bjy: 网站标题的字体大小和行高 */
.site-header h1 { .site-header h1 {
font-size: 26px; font-size: 26px;
line-height: 1.846153846; line-height: 1.846153846;
} }
/* bjy: 主导航菜单样式:上下边框、行内块显示、左对齐、全宽 */
.main-navigation ul.nav-menu, .main-navigation ul.nav-menu,
.main-navigation div.nav-menu > ul { .main-navigation div.nav-menu > ul {
border-bottom: 1px solid #ededed; border-bottom: 1px solid #ededed;
@ -77,32 +92,39 @@ body.full-width .site-content {
text-align: left; text-align: left;
width: 100%; width: 100%;
} }
/* bjy: 重置主导航ul的默认外边距和文本缩进 */
.main-navigation ul { .main-navigation ul {
margin: 0; margin: 0;
text-indent: 0; text-indent: 0;
} }
/* bjy: 主导航的链接和列表项设置为行内块,无文本装饰 */
.main-navigation li a, .main-navigation li a,
.main-navigation li { .main-navigation li {
display: inline-block; display: inline-block;
text-decoration: none; text-decoration: none;
} }
/* bjy: 针对IE7的特殊处理将主导航的链接和列表项设置为行内 */
.ie7 .main-navigation li a, .ie7 .main-navigation li a,
.ie7 .main-navigation li { .ie7 .main-navigation li {
display: inline; display: inline;
} }
/* bjy: 主导航链接样式:无边框、颜色、行高、大写转换 */
.main-navigation li a { .main-navigation li a {
border-bottom: 0; border-bottom: 0;
color: #6a6a6a; color: #6a6a6a;
line-height: 3.692307692; line-height: 3.692307692;
text-transform: uppercase; text-transform: uppercase;
} }
/* bjy: 主导航链接悬停时颜色变黑 */
.main-navigation li a:hover { .main-navigation li a:hover {
color: #000; color: #000;
} }
/* bjy: 主导航列表项样式:右边距、相对定位 */
.main-navigation li { .main-navigation li {
margin: 0 40px 0 0; margin: 0 40px 0 0;
position: relative; position: relative;
} }
/* bjy: 主导航下拉子菜单样式:绝对定位、隐藏(通过裁剪) */
.main-navigation li ul { .main-navigation li ul {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -114,17 +136,20 @@ body.full-width .site-content {
overflow: hidden; overflow: hidden;
clip: rect(1px, 1px, 1px, 1px); clip: rect(1px, 1px, 1px, 1px);
} }
/* bjy: 针对IE7的下拉菜单特殊处理使用display:none隐藏 */
.ie7 .main-navigation li ul { .ie7 .main-navigation li ul {
clip: inherit; clip: inherit;
display: none; display: none;
left: 0; left: 0;
overflow: visible; overflow: visible;
} }
/* bjy: 二级及更深层的下拉菜单定位 */
.main-navigation li ul ul, .main-navigation li ul ul,
.ie7 .main-navigation li ul ul { .ie7 .main-navigation li ul ul {
top: 0; top: 0;
left: 100%; left: 100%;
} }
/* bjy: 当鼠标悬停或聚焦于主导航项时,显示其子菜单 */
.main-navigation ul li:hover > ul, .main-navigation ul li:hover > ul,
.main-navigation ul li:focus > ul, .main-navigation ul li:focus > ul,
.main-navigation .focus > ul { .main-navigation .focus > ul {
@ -134,10 +159,12 @@ body.full-width .site-content {
height: inherit; height: inherit;
width: inherit; width: inherit;
} }
/* bjy: 针对IE7当鼠标悬停或聚焦时使用display:block显示子菜单 */
.ie7 .main-navigation ul li:hover > ul, .ie7 .main-navigation ul li:hover > ul,
.ie7 .main-navigation ul li:focus > ul { .ie7 .main-navigation ul li:focus > ul {
display: block; display: block;
} }
/* bjy: 下拉菜单中的链接样式:背景、边框、块级显示、字体、内边距、宽度 */
.main-navigation li ul li a { .main-navigation li ul li a {
background: #efefef; background: #efefef;
border-bottom: 1px solid #ededed; border-bottom: 1px solid #ededed;
@ -147,10 +174,12 @@ body.full-width .site-content {
padding: 8px 10px; padding: 8px 10px;
width: 180px; width: 180px;
} }
/* bjy: 下拉菜单链接悬停时的背景和颜色 */
.main-navigation li ul li a:hover { .main-navigation li ul li a:hover {
background: #e3e3e3; background: #e3e3e3;
color: #444; color: #444;
} }
/* bjy: 当前菜单项或其祖先项的链接样式:加粗 */
.main-navigation .current-menu-item > a, .main-navigation .current-menu-item > a,
.main-navigation .current-menu-ancestor > a, .main-navigation .current-menu-ancestor > a,
.main-navigation .current_page_item > a, .main-navigation .current_page_item > a,
@ -158,39 +187,48 @@ body.full-width .site-content {
color: #636363; color: #636363;
font-weight: bold; font-weight: bold;
} }
/* bjy: 隐藏主导航的移动端菜单切换按钮 */
.main-navigation .menu-toggle { .main-navigation .menu-toggle {
display: none; display: none;
} }
/* bjy: 文章标题的字体大小 */
.entry-header .entry-title { .entry-header .entry-title {
font-size: 22px; font-size: 22px;
} }
/* bjy: 评论表单中文本输入框的宽度 */
#respond form input[type="text"] { #respond form input[type="text"] {
width: 46.333333333%; width: 46.333333333%;
} }
/* bjy: 评论表单中文本域的宽度 */
#respond form textarea.blog-textarea { #respond form textarea.blog-textarea {
width: 79.666666667%; width: 79.666666667%;
} }
/* bjy: 首页模板的内容区域和文章设置溢出隐藏 */
.template-front-page .site-content, .template-front-page .site-content,
.template-front-page article { .template-front-page article {
overflow: hidden; overflow: hidden;
} }
/* bjy: 首页模板中有特色图片的文章向左浮动宽度约为47.92% */
.template-front-page.has-post-thumbnail article { .template-front-page.has-post-thumbnail article {
float: left; float: left;
width: 47.916666667%; width: 47.916666667%;
} }
/* bjy: 首页模板中的特色图片向右浮动宽度约为47.92% */
.entry-page-image { .entry-page-image {
float: right; float: right;
margin-bottom: 0; margin-bottom: 0;
width: 47.916666667%; width: 47.916666667%;
} }
/* IE Front Page Template Widget fix */ /* bjy: IE首页模板小工具修复清除浮动 */
.template-front-page .widget-area { .template-front-page .widget-area {
clear: both; clear: both;
} }
/* bjy: IE首页模板小工具修复设置小工具宽度为100%,无边框 */
.template-front-page .widget { .template-front-page .widget {
width: 100% !important; width: 100% !important;
border: none; border: none;
} }
/* bjy: 首页模板小工具区域的布局:左浮动、宽度、边距 */
.template-front-page .widget-area .widget, .template-front-page .widget-area .widget,
.template-front-page .first.front-widgets, .template-front-page .first.front-widgets,
.template-front-page.two-sidebars .widget-area .front-widgets { .template-front-page.two-sidebars .widget-area .front-widgets {
@ -198,10 +236,12 @@ body.full-width .site-content {
margin-bottom: 24px; margin-bottom: 24px;
width: 51.875%; width: 51.875%;
} }
/* bjy: 首页模板特定小工具的布局:清除右浮动 */
.template-front-page .second.front-widgets, .template-front-page .second.front-widgets,
.template-front-page .widget-area .widget:nth-child(odd) { .template-front-page .widget-area .widget:nth-child(odd) {
clear: right; clear: right;
} }
/* bjy: 首页模板另一组小工具的布局:右浮动、宽度、边距 */
.template-front-page .first.front-widgets, .template-front-page .first.front-widgets,
.template-front-page .second.front-widgets, .template-front-page .second.front-widgets,
.template-front-page.two-sidebars .widget-area .front-widgets + .front-widgets { .template-front-page.two-sidebars .widget-area .front-widgets + .front-widgets {
@ -209,65 +249,71 @@ body.full-width .site-content {
margin: 0 0 24px; margin: 0 0 24px;
width: 39.0625%; width: 39.0625%;
} }
/* bjy: 双侧边栏首页模板的小工具布局:取消浮动,宽度自适应 */
.template-front-page.two-sidebars .widget, .template-front-page.two-sidebars .widget,
.template-front-page.two-sidebars .widget:nth-child(even) { .template-front-page.two-sidebars .widget:nth-child(even) {
float: none; float: none;
width: auto; width: auto;
} }
/* add input font for <IE9 Password Box to make the bullets show up */ /* bjy: 为IE9以下的密码输入框添加字体以确保密码圆点显示正常 */
input[type="password"] { input[type="password"] {
font-family: Helvetica, Arial, sans-serif; font-family: Helvetica, Arial, sans-serif;
} }
/* RTL overrides for IE7 and IE8 /* bjy: RTLIE7IE8
-------------------------------------------------------------- */ -------------------------------------------------------------- */
/* bjy: RTL布局下网站标题右对齐 */
.rtl .site-header h1, .rtl .site-header h1,
.rtl .site-header h2 { .rtl .site-header h2 {
text-align: right; text-align: right;
} }
/* bjy: RTL布局下小工具区域和作者描述向左浮动 */
.rtl .widget-area, .rtl .widget-area,
.rtl .author-description { .rtl .author-description {
float: left; float: left;
} }
/* bjy: RTL布局下作者头像和内容区域向右浮动 */
.rtl .author-avatar, .rtl .author-avatar,
.rtl .site-content { .rtl .site-content {
float: right; float: right;
} }
/* bjy: RTL布局下主导航菜单右对齐 */
.rtl .main-navigation ul.nav-menu, .rtl .main-navigation ul.nav-menu,
.rtl .main-navigation div.nav-menu > ul { .rtl .main-navigation div.nav-menu > ul {
text-align: right; text-align: right;
} }
/* bjy: RTL布局下下拉菜单项的左边距 */
.rtl .main-navigation ul li ul li, .rtl .main-navigation ul li ul li,
.rtl .main-navigation ul li ul li ul li { .rtl .main-navigation ul li ul li ul li {
margin-left: 40px; margin-left: 40px;
margin-right: auto; margin-right: auto;
} }
/* bjy: RTL布局下二级下拉菜单位于父菜单的左侧 */
.rtl .main-navigation li ul ul { .rtl .main-navigation li ul ul {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 100%; right: 100%;
z-index: 1; z-index: 1;
} }
/* bjy: IE7 RTL布局下二级下拉菜单位置调整 */
.ie7 .rtl .main-navigation li ul ul { .ie7 .rtl .main-navigation li ul ul {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 100%; right: 100%;
z-index: 1; z-index: 1;
} }
/* bjy: IE7 RTL布局下为主导航列表项设置堆叠顺序 */
.ie7 .rtl .main-navigation ul li { .ie7 .rtl .main-navigation ul li {
z-index: 99; z-index: 99;
} }
/* bjy: IE7 RTL布局下一级下拉菜单位于父菜单上方 */
.ie7 .rtl .main-navigation li ul { .ie7 .rtl .main-navigation li ul {
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
right: 0; right: 0;
z-index: 1; z-index: 1;
} }
/* bjy: IE7 RTL布局下主导航列表项的边距调整 */
.ie7 .rtl .main-navigation li { .ie7 .rtl .main-navigation li {
margin-right: auto; margin-right: auto;
margin-left: 40px; marg
}
.ie7 .rtl .main-navigation li ul ul ul {
position: relative;
z-index: 1;
}

@ -1,36 +1,43 @@
/* Make clicks pass-through */ /* bjy: 使点击事件可以穿透进度条元素,不影响下方页面的交互 */
#nprogress { #nprogress {
pointer-events: none; pointer-events: none;
} }
/* bjy: 进度条主体样式 */
#nprogress .bar { #nprogress .bar {
/* bjy: 进度条背景色为红色 */
background: red; background: red;
/* bjy: 固定定位,使进度条始终在页面顶部 */
position: fixed; position: fixed;
/* bjy: 设置较高的堆叠顺序,确保进度条在最上层 */
z-index: 1031; z-index: 1031;
top: 0; top: 0;
left: 0; left: 0;
/* bjy: 进度条宽度和高度 */
width: 100%; width: 100%;
height: 2px; height: 2px;
} }
/* Fancy blur effect */ /* bjy: 进度条右侧的“闪光”或“模糊”效果 */
#nprogress .peg { #nprogress .peg {
display: block; display: block;
position: absolute; position: absolute;
right: 0px; right: 0px;
width: 100px; width: 100px;
height: 100%; height: 100%;
/* bjy: 使用box-shadow创建一个发光的阴影效果 */
box-shadow: 0 0 10px #29d, 0 0 5px #29d; box-shadow: 0 0 10px #29d, 0 0 5px #29d;
opacity: 1.0; opacity: 1.0;
/* bjy: 对peg元素进行轻微的旋转和位移增加动感 */
-webkit-transform: rotate(3deg) translate(0px, -4px); -webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px); -ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px); transform: rotate(3deg) translate(0px, -4px);
} }
/* Remove these to get rid of the spinner */ /* bjy: 进度指示器(右上角的旋转图标)样式。注释提示:删除这些样式可以去掉旋转图标 */
#nprogress .spinner { #nprogress .spinner {
display: block; display: block;
position: fixed; position: fixed;
@ -39,36 +46,42 @@
right: 15px; right: 15px;
} }
/* bjy: 旋转图标本身的样式 */
#nprogress .spinner-icon { #nprogress .spinner-icon {
width: 18px; width: 18px;
height: 18px; height: 18px;
box-sizing: border-box; box-sizing: border-box;
/* bjy: 设置边框,顶部和左侧为红色,形成旋转动画的视觉效果 */
border: solid 2px transparent; border: solid 2px transparent;
border-top-color: red; border-top-color: red;
border-left-color: red; border-left-color: red;
border-radius: 50%; border-radius: 50%;
/* bjy: 应用旋转动画 */
-webkit-animation: nprogress-spinner 400ms linear infinite; -webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite; animation: nprogress-spinner 400ms linear infinite;
} }
/* bjy: 自定义父容器样式,用于将进度条限制在特定区域内 */
.nprogress-custom-parent { .nprogress-custom-parent {
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
/* bjy: 当进度条在自定义父容器内时将其定位方式从fixed改为absolute */
.nprogress-custom-parent #nprogress .spinner, .nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar { .nprogress-custom-parent #nprogress .bar {
position: absolute; position: absolute;
} }
/* bjy: 定义旋转动画的关键帧兼容旧版WebKit内核浏览器 */
@-webkit-keyframes nprogress-spinner { @-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); } 0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); } 100% { -webkit-transform: rotate(360deg); }
} }
/* bjy: 定义旋转动画的关键帧(标准语法) */
@keyframes nprogress-spinner { @keyframes nprogress-spinner {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }

@ -1,305 +1,378 @@
/* bjy: Google 图标样式,定义背景图位置 */
.icon-sn-google { .icon-sn-google {
background-position: 0 -28px; background-position: 0 -28px;
} }
/* bjy: Google 背景图标样式,定义背景色和背景图位置 */
.icon-sn-bg-google { .icon-sn-bg-google {
background-color: #4285f4; background-color: #4285f4;
background-position: 0 0; background-position: 0 0;
} }
/* bjy: Google 字体图标如FontAwesome样式定义颜色 */
.fa-sn-google { .fa-sn-google {
color: #4285f4; color: #4285f4;
} }
/* bjy: GitHub 图标样式 */
.icon-sn-github { .icon-sn-github {
background-position: -28px -28px; background-position: -28px -28px;
} }
/* bjy: GitHub 背景图标样式 */
.icon-sn-bg-github { .icon-sn-bg-github {
background-color: #333; background-color: #333;
background-position: -28px 0; background-position: -28px 0;
} }
/* bjy: GitHub 字体图标样式 */
.fa-sn-github { .fa-sn-github {
color: #333; color: #333;
} }
/* bjy: 微博图标样式 */
.icon-sn-weibo { .icon-sn-weibo {
background-position: -56px -28px; background-position: -56px -28px;
} }
/* bjy: 微博背景图标样式 */
.icon-sn-bg-weibo { .icon-sn-bg-weibo {
background-color: #e90d24; background-color: #e90d24;
background-position: -56px 0; background-position: -56px 0;
} }
/* bjy: 微博字体图标样式 */
.fa-sn-weibo { .fa-sn-weibo {
color: #e90d24; color: #e90d24;
} }
/* bjy: QQ图标样式 */
.icon-sn-qq { .icon-sn-qq {
background-position: -84px -28px; background-position: -84px -28px;
} }
/* bjy: QQ背景图标样式 */
.icon-sn-bg-qq { .icon-sn-bg-qq {
background-color: #0098e6; background-color: #0098e6;
background-position: -84px 0; background-position: -84px 0;
} }
/* bjy: QQ字体图标样式 */
.fa-sn-qq { .fa-sn-qq {
color: #0098e6; color: #0098e6;
} }
/* bjy: Twitter图标样式 */
.icon-sn-twitter { .icon-sn-twitter {
background-position: -112px -28px; background-position: -112px -28px;
} }
/* bjy: Twitter背景图标样式 */
.icon-sn-bg-twitter { .icon-sn-bg-twitter {
background-color: #50abf1; background-color: #50abf1;
background-position: -112px 0; background-position: -112px 0;
} }
/* bjy: Twitter字体图标样式 */
.fa-sn-twitter { .fa-sn-twitter {
color: #50abf1; color: #50abf1;
} }
/* bjy: Facebook图标样式 */
.icon-sn-facebook { .icon-sn-facebook {
background-position: -140px -28px; background-position: -140px -28px;
} }
/* bjy: Facebook背景图标样式 */
.icon-sn-bg-facebook { .icon-sn-bg-facebook {
background-color: #4862a3; background-color: #4862a3;
background-position: -140px 0; background-position: -140px 0;
} }
/* bjy: Facebook字体图标样式 */
.fa-sn-facebook { .fa-sn-facebook {
color: #4862a3; color: #4862a3;
} }
/* bjy: 人人网图标样式 */
.icon-sn-renren { .icon-sn-renren {
background-position: -168px -28px; background-position: -168px -28px;
} }
/* bjy: 人人网背景图标样式 */
.icon-sn-bg-renren { .icon-sn-bg-renren {
background-color: #197bc8; background-color: #197bc8;
background-position: -168px 0; background-position: -168px 0;
} }
/* bjy: 人人网字体图标样式 */
.fa-sn-renren { .fa-sn-renren {
color: #197bc8; color: #197bc8;
} }
/* bjy: 腾讯微博图标样式 */
.icon-sn-tqq { .icon-sn-tqq {
background-position: -196px -28px; background-position: -196px -28px;
} }
/* bjy: 腾讯微博背景图标样式 */
.icon-sn-bg-tqq { .icon-sn-bg-tqq {
background-color: #1f9ed2; background-color: #1f9ed2;
background-position: -196px 0; background-position: -196px 0;
} }
/* bjy: 腾讯微博字体图标样式 */
.fa-sn-tqq { .fa-sn-tqq {
color: #1f9ed2; color: #1f9ed2;
} }
/* bjy: 豆瓣图标样式 */
.icon-sn-douban { .icon-sn-douban {
background-position: -224px -28px; background-position: -224px -28px;
} }
/* bjy: 豆瓣背景图标样式 */
.icon-sn-bg-douban { .icon-sn-bg-douban {
background-color: #279738; background-color: #279738;
background-position: -224px 0; background-position: -224px 0;
} }
/* bjy: 豆瓣字体图标样式 */
.fa-sn-douban { .fa-sn-douban {
color: #279738; color: #279738;
} }
/* bjy: 微信图标样式 */
.icon-sn-weixin { .icon-sn-weixin {
background-position: -252px -28px; background-position: -252px -28px;
} }
/* bjy: 微信背景图标样式 */
.icon-sn-bg-weixin { .icon-sn-bg-weixin {
background-color: #00b500; background-color: #00b500;
background-position: -252px 0; background-position: -252px 0;
} }
/* bjy: 微信字体图标样式 */
.fa-sn-weixin { .fa-sn-weixin {
color: #00b500; color: #00b500;
} }
/* bjy: 省略号图标样式 */
.icon-sn-dotted { .icon-sn-dotted {
background-position: -280px -28px; background-position: -280px -28px;
} }
/* bjy: 省略号背景图标样式 */
.icon-sn-bg-dotted { .icon-sn-bg-dotted {
background-color: #eee; background-color: #eee;
background-position: -280px 0; background-position: -280px 0;
} }
/* bjy: 省略号字体图标样式 */
.fa-sn-dotted { .fa-sn-dotted {
color: #eee; color: #eee;
} }
/* bjy: 网站图标样式 */
.icon-sn-site { .icon-sn-site {
background-position: -308px -28px; background-position: -308px -28px;
} }
/* bjy: 网站背景图标样式 */
.icon-sn-bg-site { .icon-sn-bg-site {
background-color: #00b500; background-color: #00b500;
background-position: -308px 0; background-position: -308px 0;
} }
/* bjy: 网站字体图标样式 */
.fa-sn-site { .fa-sn-site {
color: #00b500; color: #00b500;
} }
/* bjy: LinkedIn图标样式 */
.icon-sn-linkedin { .icon-sn-linkedin {
background-position: -336px -28px; background-position: -336px -28px;
} }
/* bjy: LinkedIn背景图标样式 */
.icon-sn-bg-linkedin { .icon-sn-bg-linkedin {
background-color: #0077b9; background-color: #0077b9;
background-position: -336px 0; background-position: -336px 0;
} }
/* bjy: LinkedIn字体图标样式 */
.fa-sn-linkedin { .fa-sn-linkedin {
color: #0077b9; color: #0077b9;
} }
/* bjy: 所有社交网络图标的通用样式 */
[class*=icon-sn-] { [class*=icon-sn-] {
display: inline-block; display: inline-block;
/* bjy: 设置背景图片为一张包含所有图标的SVG精灵图 */
background-image: url('../img/icon-sn.svg'); background-image: url('../img/icon-sn.svg');
background-repeat: no-repeat; background-repeat: no-repeat;
width: 28px; width: 28px;
height: 28px; height: 28px;
vertical-align: middle; vertical-align: middle;
/* bjy: 设置背景图大小高度固定为56px宽度自适应 */
background-size: auto 56px; background-size: auto 56px;
} }
/* bjy: 所有社交网络图标在鼠标悬停时的效果 */
[class*=icon-sn-]:hover { [class*=icon-sn-]:hover {
opacity: .8; opacity: .8;
/* bjy: 兼容旧版IE的透明度滤镜 */
filter: alpha(opacity=80); filter: alpha(opacity=80);
} }
/* bjy: Google按钮样式 */
.btn-sn-google { .btn-sn-google {
background: #4285f4; background: #4285f4;
} }
/* bjy: Google按钮在激活、聚焦或悬停时的样式 */
.btn-sn-google:active, .btn-sn-google:focus, .btn-sn-google:hover { .btn-sn-google:active, .btn-sn-google:focus, .btn-sn-google:hover {
background: #2a75f3; background: #2a75f3;
} }
/* bjy: GitHub按钮样式 */
.btn-sn-github { .btn-sn-github {
background: #333; background: #333;
} }
/* bjy: GitHub按钮在激活、聚焦或悬停时的样式 */
.btn-sn-github:active, .btn-sn-github:focus, .btn-sn-github:hover { .btn-sn-github:active, .btn-sn-github:focus, .btn-sn-github:hover {
background: #262626; background: #262626;
} }
/* bjy: 微博按钮样式 */
.btn-sn-weibo { .btn-sn-weibo {
background: #e90d24; background: #e90d24;
} }
/* bjy: 微博按钮在激活、聚焦或悬停时的样式 */
.btn-sn-weibo:active, .btn-sn-weibo:focus, .btn-sn-weibo:hover { .btn-sn-weibo:active, .btn-sn-weibo:focus, .btn-sn-weibo:hover {
background: #d10c20; background: #d10c20;
} }
/* bjy: QQ按钮样式 */
.btn-sn-qq { .btn-sn-qq {
background: #0098e6; background: #0098e6;
} }
/* bjy: QQ按钮在激活、聚焦或悬停时的样式 */
.btn-sn-qq:active, .btn-sn-qq:focus, .btn-sn-qq:hover { .btn-sn-qq:active, .btn-sn-qq:focus, .btn-sn-qq:hover {
background: #0087cd; background: #0087cd;
} }
/* bjy: Twitter按钮样式 */
.btn-sn-twitter { .btn-sn-twitter {
background: #50abf1; background: #50abf1;
} }
/* bjy: Twitter按钮在激活、聚焦或悬停时的样式 */
.btn-sn-twitter:active, .btn-sn-twitter:focus, .btn-sn-twitter:hover { .btn-sn-twitter:active, .btn-sn-twitter:focus, .btn-sn-twitter:hover {
background: #38a0ef; background: #38a0ef;
} }
/* bjy: Facebook按钮样式 */
.btn-sn-facebook { .btn-sn-facebook {
background: #4862a3; background: #4862a3;
} }
/* bjy: Facebook按钮在激活、聚焦或悬停时的样式 */
.btn-sn-facebook:active, .btn-sn-facebook:focus, .btn-sn-facebook:hover { .btn-sn-facebook:active, .btn-sn-facebook:focus, .btn-sn-facebook:hover {
background: #405791; background: #405791;
} }
/* bjy: 人人网按钮样式 */
.btn-sn-renren { .btn-sn-renren {
background: #197bc8; background: #197bc8;
} }
/* bjy: 人人网按钮在激活、聚焦或悬停时的样式 */
.btn-sn-renren:active, .btn-sn-renren:focus, .btn-sn-renren:hover { .btn-sn-renren:active, .btn-sn-renren:focus, .btn-sn-renren:hover {
background: #166db1; background: #166db1;
} }
/* bjy: 腾讯微博按钮样式 */
.btn-sn-tqq { .btn-sn-tqq {
background: #1f9ed2; background: #1f9ed2;
} }
/* bjy: 腾讯微博按钮在激活、聚焦或悬停时的样式 */
.btn-sn-tqq:active, .btn-sn-tqq:focus, .btn-sn-tqq:hover { .btn-sn-tqq:active, .btn-sn-tqq:focus, .btn-sn-tqq:hover {
background: #1c8dbc; background: #1c8dbc;
} }
/* bjy: 豆瓣按钮样式 */
.btn-sn-douban { .btn-sn-douban {
background: #279738; background: #279738;
} }
/* bjy: 豆瓣按钮在激活、聚焦或悬停时的样式 */
.btn-sn-douban:active, .btn-sn-douban:focus, .btn-sn-douban:hover { .btn-sn-douban:active, .btn-sn-douban:focus, .btn-sn-douban:hover {
background: #228330; background: #228330;
} }
/* bjy: 微信按钮样式 */
.btn-sn-weixin { .btn-sn-weixin {
background: #00b500; background: #00b500;
} }
/* bjy: 微信按钮在激活、聚焦或悬停时的样式 */
.btn-sn-weixin:active, .btn-sn-weixin:focus, .btn-sn-weixin:hover { .btn-sn-weixin:active, .btn-sn-weixin:focus, .btn-sn-weixin:hover {
background: #009c00; background: #009c00;
} }
/* bjy: 省略号按钮样式 */
.btn-sn-dotted { .btn-sn-dotted {
background: #eee; background: #eee;
} }
/* bjy: 省略号按钮在激活、聚焦或悬停时的样式 */
.btn-sn-dotted:active, .btn-sn-dotted:focus, .btn-sn-dotted:hover { .btn-sn-dotted:active, .btn-sn-dotted:focus, .btn-sn-dotted:hover {
background: #e1e1e1; background: #e1e1e1;
} }
/* bjy: 网站按钮样式 */
.btn-sn-site { .btn-sn-site {
background: #00b500; background: #00b500;
} }
/* bjy: 网站按钮在激活、聚焦或悬停时的样式 */
.btn-sn-site:active, .btn-sn-site:focus, .btn-sn-site:hover { .btn-sn-site:active, .btn-sn-site:focus, .btn-sn-site:hover {
background: #009c00; background: #009c00;
} }
/* bjy: LinkedIn按钮样式 */
.btn-sn-linkedin { .btn-sn-linkedin {
background: #0077b9; background: #0077b9;
} }
/* bjy: LinkedIn按钮在激活、聚焦或悬停时的样式 */
.btn-sn-linkedin:active, .btn-sn-linkedin:focus, .btn-sn-linkedin:hover { .btn-sn-linkedin:active, .btn-sn-linkedin:focus, .btn-sn-linkedin:hover {
background: #0067a0; background: #0067a0;
} }
/* bjy: 所有社交网络按钮的通用样式,包括其状态 */
[class*=btn-sn-], [class*=btn-sn-]:active, [class*=btn-sn-]:focus, [class*=btn-sn-]:hover { [class*=btn-sn-], [class*=btn-sn-]:active, [class*=btn-sn-]:focus, [class*=btn-sn-]:hover {
border: none; border: none;
color: #fff; color: #fff;
} }
/* bjy: “更多”按钮的特殊样式,移除内边距 */
.btn-sn-more { .btn-sn-more {
padding: 0; padding: 0;
} }
/* bjy: “更多”按钮及其状态的特殊样式,移除阴影 */
.btn-sn-more, .btn-sn-more:active, .btn-sn-more:hover { .btn-sn-more, .btn-sn-more:active, .btn-sn-more:hover {
box-shadow: none; box-shadow: none;
} }
/* bjy: 当社交网络图标被用作按钮内部元素时,移除其背景色 */
[class*=btn-sn-] [class*=icon-sn-] { [class*=btn-sn-] [class*=icon-sn-] {
background-color: transparent; background-color: transparent;
} }

@ -0,0 +1,598 @@
/*
* DjangoBlog
* theme.css - /
* DjangoBlog
*/
:root {
/* ====== 浅色主题变量 ====== */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--bg-card: #ffffff;
--text-primary: #333333;
--text-secondary: #666666;
--text-muted: #999999;
--border-color: #dee2e6;
--link-color: #007bff;
--link-hover-color: #0056b3;
--code-bg: #f8f9fa;
--blockquote-border: #e9ecef;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
/* 导航栏 */
--nav-bg: #f8f9fa;
--nav-text: #333333;
--nav-border: #dee2e6;
/* 按钮 */
--btn-primary-bg: #007bff;
--btn-primary-text: #ffffff;
--btn-secondary-bg: #6c757d;
--btn-secondary-text: #ffffff;
/* 输入框 */
--input-bg: #ffffff;
--input-text: #333333;
--input-border: #ced4da;
--input-focus-border: #007bff;
}
[data-theme="dark"] {
/* ====== 深色主题变量 ====== */
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #404040;
--bg-card: #2d2d2d;
--text-primary: #e9ecef;
--text-secondary: #adb5bd;
--text-muted: #6c757d;
--border-color: #495057;
--link-color: #4dabf7;
--link-hover-color: #74c0fc;
--code-bg: #2d2d2d;
--blockquote-border: #495057;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
/* 导航栏 */
--nav-bg: #2d2d2d;
--nav-text: #e9ecef;
--nav-border: #495057;
/* 按钮 */
--btn-primary-bg: #4dabf7;
--btn-primary-text: #ffffff;
--btn-secondary-bg: #6c757d;
--btn-secondary-text: #ffffff;
/* 输入框 */
--input-bg: #2d2d2d;
--input-text: #e9ecef;
--input-border: #495057;
--input-focus-border: #4dabf7;
}
/* ====== 基础元素样式 ====== */
body {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: var(--transition);
min-height: 100vh;
}
/* ====== DjangoBlog 特定样式适配 ====== */
/* 站点头部 */
.site-header {
background-color: var(--nav-bg) !important;
border-bottom: 1px solid var(--nav-border);
}
.site-title a,
.site-description {
color: var(--nav-text) !important;
}
/* 主导航 */
.main-navigation {
background-color: var(--nav-bg);
border-color: var(--nav-border);
}
.main-navigation a {
color: var(--nav-text) !important;
}
.main-navigation a:hover {
color: var(--link-hover-color) !important;
}
/* 主要内容区域 */
#page {
background-color: var(--bg-primary);
}
#main {
background-color: var(--bg-primary);
}
/* 文章和卡片样式 */
.entry-content,
.entry-header,
.entry-summary,
.type-post,
.widget,
.comment-list,
.comment-body {
background-color: var(--bg-card);
color: var(--text-primary);
border-color: var(--border-color);
}
/* 文章标题 */
.entry-title,
.entry-title a {
color: var(--text-primary) !important;
}
.entry-title a:hover {
color: var(--link-hover-color) !important;
}
/* 元信息 */
.entry-meta,
.comment-metadata {
color: var(--text-muted) !important;
}
/* 小工具 */
.widget-title {
color: var(--text-primary);
border-bottom-color: var(--border-color);
}
/* 链接 */
a {
color: var(--link-color);
}
a:hover {
color: var(--link-hover-color);
}
/* 代码块 */
pre, code {
background-color: var(--code-bg) !important;
color: var(--text-primary) !important;
border-color: var(--border-color);
}
/* 引用 */
blockquote {
border-left-color: var(--blockquote-border);
background-color: var(--bg-secondary);
color: var(--text-secondary);
}
/* 表格 */
table {
background-color: var(--bg-card);
color: var(--text-primary);
border-color: var(--border-color);
}
th, td {
border-color: var(--border-color);
background-color: var(--bg-card);
color: var(--text-primary);
}
/* 表单 */
input, textarea, select {
background-color: var(--input-bg);
border-color: var(--input-border);
color: var(--input-text);
}
input:focus, textarea:focus, select:focus {
background-color: var(--input-bg);
border-color: var(--input-focus-border);
color: var(--input-text);
}
/* 按钮 */
button, .button, input[type="submit"], input[type="button"] {
background-color: var(--btn-primary-bg);
color: var(--btn-primary-text);
border-color: var(--btn-primary-bg);
}
/* 页脚 */
.site-footer {
background-color: var(--bg-secondary) !important;
border-top-color: var(--border-color);
color: var(--text-secondary);
}
/* ====== 主题切换按钮样式 ====== */
.theme-toggle-btn {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
transition: var(--transition);
color: var(--text-primary);
margin-left: 10px;
}
.theme-toggle-btn:hover {
transform: scale(1.1);
background-color: var(--bg-tertiary);
border-color: var(--link-color);
}
.theme-toggle-btn:focus {
outline: none;
box-shadow: 0 0 0 2px var(--link-color);
}
/* ====== 导航栏集成样式 ====== */
.nav-container {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.theme-toggle-container {
margin: 0 15px;
display: flex;
align-items: center;
}
.user-menu {
display: flex;
align-items: center;
}
.user-dropdown {
position: relative;
display: inline-block;
}
.username {
padding: 8px 15px;
color: var(--text-primary);
cursor: pointer;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-secondary);
transition: var(--transition);
font-size: 14px;
}
.username:hover {
background: var(--bg-tertiary);
}
.dropdown-content {
display: none;
position: absolute;
right: 0;
background-color: var(--bg-card);
min-width: 160px;
box-shadow: var(--shadow);
border: 1px solid var(--border-color);
border-radius: 4px;
z-index: 1000;
}
.dropdown-content a {
color: var(--text-primary);
padding: 12px 16px;
text-decoration: none;
display: block;
transition: var(--transition);
border: none;
}
.dropdown-content a:hover {
background-color: var(--bg-secondary);
text-decoration: none;
}
.user-dropdown:hover .dropdown-content {
display: block;
}
.dropdown-divider {
height: 1px;
background-color: var(--border-color);
margin: 5px 0;
}
.login-link {
padding: 8px 15px;
color: var(--text-primary);
text-decoration: none;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-secondary);
transition: var(--transition);
font-size: 14px;
}
.login-link:hover {
background: var(--bg-tertiary);
color: var(--link-hover-color);
text-decoration: none;
}
/* ====== 平滑过渡效果 ====== */
body,
.site-header,
.main-navigation,
.entry-content,
.entry-header,
.widget,
.comment-list,
.site-footer,
a,
button,
input,
textarea,
select,
table,
pre,
code {
transition: var(--transition);
}
/* 图片过渡 */
img {
transition: opacity 0.3s ease;
}
[data-theme="dark"] img {
opacity: 0.9;
}
[data-theme="dark"] img:hover {
opacity: 1;
}
/* ====== 响应式调整 ====== */
@media (max-width: 768px) {
.theme-toggle-btn {
width: 36px;
height: 36px;
font-size: 1.1rem;
}
.nav-container {
flex-direction: column;
gap: 10px;
}
.theme-toggle-container {
margin: 10px 0;
}
.user-menu {
width: 100%;
justify-content: center;
}
}
/* ====== 导航栏集成优化样式 ====== */
/* 导航容器布局 */
.nav-container {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.primary-nav {
flex: 1;
}
.nav-actions {
display: flex;
align-items: center;
gap: 15px;
}
/* 主题切换按钮优化 */
.theme-toggle-container {
display: flex;
align-items: center;
}
.theme-toggle-btn {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
transition: var(--transition);
color: var(--text-primary);
}
.theme-toggle-btn:hover {
transform: scale(1.1);
background-color: var(--bg-tertiary);
border-color: var(--link-color);
}
/* 用户菜单优化 */
.user-menu {
display: flex;
align-items: center;
}
.user-dropdown {
position: relative;
display: inline-block;
}
.username {
padding: 8px 15px;
color: var(--text-primary);
cursor: pointer;
border: 1px solid var(--border-color);
border-radius: 20px;
background: var(--bg-secondary);
transition: var(--transition);
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
}
.username:hover {
background: var(--bg-tertiary);
}
.user-icon {
font-size: 12px;
}
.dropdown-content {
display: none;
position: absolute;
right: 0;
top: 100%;
background-color: var(--bg-card);
min-width: 140px;
box-shadow: var(--shadow);
border: 1px solid var(--border-color);
border-radius: 8px;
z-index: 1000;
margin-top: 5px;
}
.dropdown-content a {
color: var(--text-primary);
padding: 10px 15px;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
transition: var(--transition);
border: none;
font-size: 14px;
}
.dropdown-content a:hover {
background-color: var(--bg-secondary);
text-decoration: none;
}
.user-dropdown:hover .dropdown-content {
display: block;
}
.dropdown-divider {
height: 1px;
background-color: var(--border-color);
margin: 5px 0;
}
.icon {
font-size: 12px;
}
.login-link {
padding: 8px 15px;
color: var(--text-primary);
text-decoration: none;
border: 1px solid var(--border-color);
border-radius: 20px;
background: var(--bg-secondary);
transition: var(--transition);
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
}
.login-link:hover {
background: var(--bg-tertiary);
color: var(--link-hover-color);
text-decoration: none;
}
/* 响应式设计 */
@media (max-width: 768px) {
.nav-container {
flex-direction: column;
gap: 10px;
padding: 10px;
}
.nav-actions {
width: 100%;
justify-content: center;
gap: 10px;
}
.theme-toggle-btn {
width: 36px;
height: 36px;
font-size: 1.1rem;
}
.primary-nav {
width: 100%;
text-align: center;
}
}
/* 确保原有导航样式兼容 */
.main-navigation {
background-color: var(--nav-bg);
border-bottom: 1px solid var(--nav-border);
transition: var(--transition);
padding: 10px 0;
}
.main-navigation div {
background: transparent !important;
}
.main-navigation a {
color: var(--nav-text) !important;
transition: var(--transition);
}
.main-navigation a:hover {
color: var(--link-hover-color) !important;
background: transparent !important;
}
/* 修复可能的分层问题 */
.site-header {
position: relative;
z-index: 100;
}
.main-navigation {
position: relative;
z-index: 99;
}

@ -1,40 +1,59 @@
/** /**
* Created by liangliang on 2016/11/20. * bjy: Created by liangliang on 2016/11/20.
*/ */
// bjy: 定义一个函数,用于处理评论回复功能
// bjy: parentid参数是要回复的评论的ID
function do_reply(parentid) { function do_reply(parentid) {
// bjy: 在控制台打印父评论ID用于调试
console.log(parentid); console.log(parentid);
// bjy: 将父评论ID设置到隐藏的表单字段中
$("#id_parent_comment_id").val(parentid) $("#id_parent_comment_id").val(parentid)
// bjy: 将评论表单移动到指定评论的下方
$("#commentform").appendTo($("#div-comment-" + parentid)); $("#commentform").appendTo($("#div-comment-" + parentid));
// bjy: 隐藏“发表评论”的标题
$("#reply-title").hide(); $("#reply-title").hide();
// bjy: 显示“取消回复”的链接
$("#cancel_comment").show(); $("#cancel_comment").show();
} }
// bjy: 定义一个函数,用于取消评论回复
function cancel_reply() { function cancel_reply() {
// bjy: 显示“发表评论”的标题
$("#reply-title").show(); $("#reply-title").show();
// bjy: 隐藏“取消回复”的链接
$("#cancel_comment").hide(); $("#cancel_comment").hide();
// bjy: 清空隐藏的父评论ID字段
$("#id_parent_comment_id").val('') $("#id_parent_comment_id").val('')
// bjy: 将评论表单移回原来的位置
$("#commentform").appendTo($("#respond")); $("#commentform").appendTo($("#respond"));
} }
// bjy: 页面加载时启动NProgress进度条
NProgress.start(); NProgress.start();
// bjy: 设置进度条进度为40%
NProgress.set(0.4); NProgress.set(0.4);
//Increment // bjy: 设置一个定时器每1000毫秒1秒增加一点进度
var interval = setInterval(function () { var interval = setInterval(function () {
NProgress.inc(); NProgress.inc();
}, 1000); }, 1000);
// bjy: 当文档结构加载完成时
$(document).ready(function () { $(document).ready(function () {
// bjy: 完成进度条加载
NProgress.done(); NProgress.done();
// bjy: 清除定时器
clearInterval(interval); clearInterval(interval);
}); });
/** 侧边栏回到顶部 */ /** bjy: 侧边栏回到顶部功能 */
// bjy: 获取火箭图标的jQuery对象
var rocket = $('#rocket'); var rocket = $('#rocket');
// bjy: 监听窗口的滚动事件并使用debounce函数进行防抖处理
$(window).on('scroll', debounce(slideTopSet, 300)); $(window).on('scroll', debounce(slideTopSet, 300));
// bjy: 定义一个防抖函数,用于限制函数的执行频率
function debounce(func, wait) { function debounce(func, wait) {
var timeout; var timeout;
return function () { return function () {
@ -43,49 +62,67 @@ function debounce(func, wait) {
}; };
} }
// bjy: 定义一个函数,根据滚动位置来决定是否显示“回到顶部”按钮
function slideTopSet() { function slideTopSet() {
// bjy: 获取当前文档滚动的垂直距离
var top = $(document).scrollTop(); var top = $(document).scrollTop();
// bjy: 如果滚动距离大于200像素
if (top > 200) { if (top > 200) {
// bjy: 显示火箭图标(通过添加'show'类)
rocket.addClass('show'); rocket.addClass('show');
} else { } else {
// bjy: 否则,隐藏火箭图标(通过移除'show'类)
rocket.removeClass('show'); rocket.removeClass('show');
} }
} }
// bjy: 监听文档上的点击事件,如果点击的是火箭图标
$(document).on('click', '#rocket', function (event) { $(document).on('click', '#rocket', function (event) {
// bjy: 为火箭图标添加'move'类,触发向上飞行的动画
rocket.addClass('move'); rocket.addClass('move');
// bjy: 使用animate动画将页面平滑滚动回顶部
$('body, html').animate({ $('body, html').animate({
scrollTop: 0 scrollTop: 0
}, 800); }, 800);
}); });
// bjy: 监听标准动画结束事件
$(document).on('animationEnd', function () { $(document).on('animationEnd', function () {
// bjy: 动画结束后延迟400毫秒移除'move'类,以便下次可以再次触发
setTimeout(function () { setTimeout(function () {
rocket.removeClass('move'); rocket.removeClass('move');
}, 400); }, 400);
}); });
// bjy: 监听webkit内核的动画结束事件用于兼容性
$(document).on('webkitAnimationEnd', function () { $(document).on('webkitAnimationEnd', function () {
// bjy: 动画结束后延迟400毫秒移除'move'类
setTimeout(function () { setTimeout(function () {
rocket.removeClass('move'); rocket.removeClass('move');
}, 400); }, 400);
}); });
// bjy: 当整个页面(包括图片等资源)加载完成后执行
window.onload = function () { window.onload = function () {
// bjy: 获取页面上所有“回复”链接
var replyLinks = document.querySelectorAll(".comment-reply-link"); var replyLinks = document.querySelectorAll(".comment-reply-link");
// bjy: 遍历所有“回复”链接
for (var i = 0; i < replyLinks.length; i++) { for (var i = 0; i < replyLinks.length; i++) {
// bjy: 为每个链接绑定点击事件
replyLinks[i].onclick = function () { replyLinks[i].onclick = function () {
// bjy: 从链接的data-pk属性中获取要回复的评论ID
var pk = this.getAttribute("data-pk"); var pk = this.getAttribute("data-pk");
// bjy: 调用do_reply函数来处理回复逻辑
do_reply(pk); do_reply(pk);
}; };
} }
}; };
// bjy: 以下代码被注释掉,可能用于国际化语言切换功能
// $(document).ready(function () { // $(document).ready(function () {
// var form = $('#i18n-form'); // var form = $('#i18n-form');
// var selector = $('.i18n-select'); // var selector = $('.i18n-select');
// selector.on('change', function () { // selector.on('change', function () {
// form.submit(); // form.submit();
// }); // });
// }); // });

@ -1,102 +1,130 @@
/** /**
* MathJax 智能加载器 * bjy: MathJax 智能加载器
* 检测页面是否包含数学公式如果有则动态加载和配置MathJax * bjy: 检测页面是否包含数学公式如果有则动态加载和配置MathJax
*/ */
// bjy: 使用立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
(function() { (function() {
// bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作
'use strict'; 'use strict';
/** /**
* 检测页面是否包含数学公式 * bjy: 检测页面是否包含数学公式
* @returns {boolean} 是否包含数学公式 * @returns {boolean} 是否包含数学公式
*/ */
function hasMathFormulas() { function hasMathFormulas() {
// bjy: 获取页面的全部文本内容
const content = document.body.textContent || document.body.innerText || ''; const content = document.body.textContent || document.body.innerText || '';
// 检测常见的数学公式语法 // bjy: 使用正则表达式检测常见的数学公式语法(如$...$、$$...$$、\begin{}等)
return /\$.*?\$|\$\$.*?\$\$|\\begin\{.*?\}|\\end\{.*?\}|\\[a-zA-Z]+\{/.test(content); return /\$.*?\$|\$\$.*?\$\$|\\begin\{.*?\}|\\end\{.*?\}|\\[a-zA-Z]+\{/.test(content);
} }
/** /**
* 配置MathJax * bjy: 配置MathJax
*/ */
function configureMathJax() { function configureMathJax() {
// bjy: 在全局window对象上设置MathJax配置
window.MathJax = { window.MathJax = {
// bjy: 配置TeX解析器
tex: { tex: {
// 行内公式和块级公式分隔符 // bjy: 定义行内公式的分隔符为$
inlineMath: [['$', '$']], inlineMath: [['$', '$']],
// bjy: 定义块级公式的分隔符为$$
displayMath: [['$$', '$$']], displayMath: [['$$', '$$']],
// 处理转义字符和LaTeX环境 // bjy: 启用对转义字符(如\$)的处理
processEscapes: true, processEscapes: true,
// bjy: 启用对LaTeX环境如\begin{equation})的处理
processEnvironments: true, processEnvironments: true,
// 自动换行 // bjy: 使用AMS美国数学学会的标签规则来自动编号
tags: 'ams' tags: 'ams'
}, },
// bjy: 配置处理选项
options: { options: {
// 跳过这些HTML标签避免处理代码块等 // bjy: 跳过这些HTML标签避免在代码块、脚本等非文本内容中渲染公式
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'], skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'],
// CSS类控制 // bjy: 定义包含'tex2jax_ignore'类的元素将被忽略
ignoreHtmlClass: 'tex2jax_ignore', ignoreHtmlClass: 'tex2jax_ignore',
// bjy: 定义只有包含'tex2jax_process'类的元素才会被处理
processHtmlClass: 'tex2jax_process' processHtmlClass: 'tex2jax_process'
}, },
// 启动配置 // bjy: 配置MathJax的启动行为
startup: { startup: {
// bjy: 当MathJax准备就绪时执行的回调函数
ready() { ready() {
console.log('MathJax配置完成开始初始化...'); console.log('MathJax配置完成开始初始化...');
// bjy: 执行MathJax的默认就绪流程
MathJax.startup.defaultReady(); MathJax.startup.defaultReady();
// 处理特定区域的数学公式 // bjy: 获取需要渲染数学公式的特定区域元素
const contentEl = document.getElementById('content'); const contentEl = document.getElementById('content');
const commentsEl = document.getElementById('comments'); const commentsEl = document.getElementById('comments');
// bjy: 创建一个Promise数组用于管理异步渲染任务
const promises = []; const promises = [];
// bjy: 如果内容区域存在,则将其加入渲染队列
if (contentEl) { if (contentEl) {
promises.push(MathJax.typesetPromise([contentEl])); promises.push(MathJax.typesetPromise([contentEl]));
} }
// bjy: 如果评论区存在,也将其加入渲染队列
if (commentsEl) { if (commentsEl) {
promises.push(MathJax.typesetPromise([commentsEl])); promises.push(MathJax.typesetPromise([commentsEl]));
} }
// 等待所有渲染完成 // bjy: 等待所有区域的公式都渲染完成
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
console.log('MathJax渲染完成'); console.log('MathJax渲染完成');
// 触发自定义事件通知其他脚本MathJax已就绪 // bjy: 触发一个名为'mathjaxReady'的自定义事件通知其他脚本MathJax已就绪
document.dispatchEvent(new CustomEvent('mathjaxReady')); document.dispatchEvent(new CustomEvent('mathjaxReady'));
}).catch(error => { }).catch(error => {
// bjy: 如果渲染过程中出现错误,则打印错误信息
console.error('MathJax渲染失败:', error); console.error('MathJax渲染失败:', error);
}); });
} }
}, },
// 输出配置 // bjy: 配置CHTML输出CommonHTML一种在所有现代浏览器中工作的输出格式
chtml: { chtml: {
// bjy: 设置公式缩放比例
scale: 1, scale: 1,
// bjy: 设置最小缩放比例
minScale: 0.5, minScale: 0.5,
// bjy: 不强制公式高度与周围字体匹配
matchFontHeight: false, matchFontHeight: false,
// bjy: 设置块级公式居中对齐
displayAlign: 'center', displayAlign: 'center',
// bjy: 设置块级公式缩进为0
displayIndent: '0' displayIndent: '0'
} }
}; };
} }
/** /**
* 加载MathJax库 * bjy: 加载MathJax库
*/ */
function loadMathJax() { function loadMathJax() {
console.log('检测到数学公式开始加载MathJax...'); console.log('检测到数学公式开始加载MathJax...');
// bjy: 创建一个<script>元素用于加载MathJax
const script = document.createElement('script'); const script = document.createElement('script');
// bjy: 设置MathJax的CDN地址
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'; script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
// bjy: 异步加载脚本
script.async = true; script.async = true;
// bjy: 延迟执行脚本直到HTML文档解析完毕
script.defer = true; script.defer = true;
// bjy: 定义脚本加载成功时的回调函数
script.onload = function() { script.onload = function() {
console.log('MathJax库加载成功'); console.log('MathJax库加载成功');
}; };
// bjy: 定义脚本加载失败时的回调函数
script.onerror = function() { script.onerror = function() {
console.error('MathJax库加载失败尝试备用CDN...'); console.error('MathJax库加载失败尝试备用CDN...');
// 备用CDN // bjy: 备用CDN加载方案
const fallbackScript = document.createElement('script'); const fallbackScript = document.createElement('script');
// bjy: 首先加载polyfill以确保旧版浏览器兼容性
fallbackScript.src = 'https://polyfill.io/v3/polyfill.min.js?features=es6'; fallbackScript.src = 'https://polyfill.io/v3/polyfill.min.js?features=es6';
fallbackScript.onload = function() { fallbackScript.onload = function() {
// bjy: polyfill加载成功后再加载备用CDN的MathJax
const mathJaxScript = document.createElement('script'); const mathJaxScript = document.createElement('script');
mathJaxScript.src = 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML'; mathJaxScript.src = 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML';
mathJaxScript.async = true; mathJaxScript.async = true;
@ -104,39 +132,46 @@
}; };
document.head.appendChild(fallbackScript); document.head.appendChild(fallbackScript);
}; };
// bjy: 将创建的<script>元素添加到文档的<head>中,开始加载
document.head.appendChild(script); document.head.appendChild(script);
} }
/** /**
* 初始化函数 * bjy: 初始化函数
*/ */
function init() { function init() {
// 等待DOM完全加载 // bjy: 检查DOM是否已经加载完成
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
// bjy: 如果还在加载中则等待DOMContentLoaded事件后再执行init
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', init);
return; return;
} }
// 检测是否需要加载MathJax // bjy: 检测页面内容是否需要加载MathJax
if (hasMathFormulas()) { if (hasMathFormulas()) {
// 先配置,再加载 // bjy: 如果需要,则先配置,再加载
configureMathJax(); configureMathJax();
loadMathJax(); loadMathJax();
} else { } else {
// bjy: 如果不需要,则在控制台输出信息并跳过
console.log('未检测到数学公式跳过MathJax加载'); console.log('未检测到数学公式跳过MathJax加载');
} }
} }
// 提供重新渲染的全局方法,供动态内容使用 // bjy: 提供一个全局方法,用于在动态加载内容后重新渲染数学公式
window.rerenderMathJax = function(element) { window.rerenderMathJax = function(element) {
// bjy: 检查MathJax是否已加载并且typesetPromise方法是否存在
if (window.MathJax && window.MathJax.typesetPromise) { if (window.MathJax && window.MathJax.typesetPromise) {
// bjy: 确定要渲染的目标元素如果未提供则默认为整个body
const target = element || document.body; const target = element || document.body;
// bjy: 调用MathJax的typesetPromise方法对指定元素进行渲染并返回Promise
return window.MathJax.typesetPromise([target]); return window.MathJax.typesetPromise([target]);
} }
// bjy: 如果MathJax未准备好则返回一个已解决的Promise
return Promise.resolve(); return Promise.resolve();
}; };
// 启动初始化 // bjy: 启动初始化流程
init(); init();
})(); })();

@ -1,53 +1,75 @@
/** /**
* Handles toggling the navigation menu for small screens and * bjy: Handles toggling the navigation menu for small screens and
* accessibility for submenu items. * bjy: accessibility for submenu items.
*/ */
// bjy: 使用一个立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
( function() { ( function() {
// bjy: 获取主导航元素
var nav = document.getElementById( 'site-navigation' ), button, menu; var nav = document.getElementById( 'site-navigation' ), button, menu;
// bjy: 如果主导航元素不存在,则直接退出函数
if ( ! nav ) { if ( ! nav ) {
return; return;
} }
// bjy: 获取导航内的按钮元素(通常是移动端的菜单切换按钮)
button = nav.getElementsByTagName( 'button' )[0]; button = nav.getElementsByTagName( 'button' )[0];
// bjy: 获取导航内的菜单列表ul元素
menu = nav.getElementsByTagName( 'ul' )[0]; menu = nav.getElementsByTagName( 'ul' )[0];
// bjy: 如果按钮不存在,则直接退出函数
if ( ! button ) { if ( ! button ) {
return; return;
} }
// Hide button if menu is missing or empty. // bjy: 如果菜单不存在或为空,则隐藏切换按钮
if ( ! menu || ! menu.childNodes.length ) { if ( ! menu || ! menu.childNodes.length ) {
button.style.display = 'none'; button.style.display = 'none';
return; return;
} }
// bjy: 为按钮绑定点击事件
button.onclick = function() { button.onclick = function() {
// bjy: 确保菜单列表有 'nav-menu' 这个基础类名
if ( -1 === menu.className.indexOf( 'nav-menu' ) ) { if ( -1 === menu.className.indexOf( 'nav-menu' ) ) {
menu.className = 'nav-menu'; menu.className = 'nav-menu';
} }
// bjy: 检查按钮是否已经处于激活toggled-on状态
if ( -1 !== button.className.indexOf( 'toggled-on' ) ) { if ( -1 !== button.className.indexOf( 'toggled-on' ) ) {
// bjy: 如果是,则移除按钮和菜单的 'toggled-on' 类,以关闭菜单
button.className = button.className.replace( ' toggled-on', '' ); button.className = button.className.replace( ' toggled-on', '' );
menu.className = menu.className.replace( ' toggled-on', '' ); menu.className = menu.className.replace( ' toggled-on', '' );
} else { } else {
// bjy: 如果不是,则给按钮和菜单添加 'toggled-on' 类,以打开菜单
button.className += ' toggled-on'; button.className += ' toggled-on';
menu.className += ' toggled-on'; menu.className += ' toggled-on';
} }
}; };
} )(); } )();
// Better focus for hidden submenu items for accessibility. // bjy: Better focus for hidden submenu items for accessibility.
// bjy: 使用另一个IIFE并将jQuery作为参数传入以安全地使用$
( function( $ ) { ( function( $ ) {
// bjy: 在主导航区域内为所有链接a标签绑定焦点和失焦事件
$( '.main-navigation' ).find( 'a' ).on( 'focus.twentytwelve blur.twentytwelve', function() { $( '.main-navigation' ).find( 'a' ).on( 'focus.twentytwelve blur.twentytwelve', function() {
// bjy: 当链接获得或失去焦点时切换其父级菜单项li的 'focus' 类
// bjy: 这主要用于通过键盘Tab键导航时高亮显示当前所在的菜单项
$( this ).parents( '.menu-item, .page_item' ).toggleClass( 'focus' ); $( this ).parents( '.menu-item, .page_item' ).toggleClass( 'focus' );
} ); } );
// bjy: 检测设备是否支持触摸事件,用于处理移动端的菜单交互
if ( 'ontouchstart' in window ) { if ( 'ontouchstart' in window ) {
// bjy: 在body上监听触摸开始事件但委托给有子菜单的菜单项的链接
$('body').on( 'touchstart.twentytwelve', '.menu-item-has-children > a, .page_item_has_children > a', function( e ) { $('body').on( 'touchstart.twentytwelve', '.menu-item-has-children > a, .page_item_has_children > a', function( e ) {
// bjy: 获取当前触摸的链接的父级li元素
var el = $( this ).parent( 'li' ); var el = $( this ).parent( 'li' );
// bjy: 如果该菜单项还没有 'focus' 类(即子菜单未展开)
if ( ! el.hasClass( 'focus' ) ) { if ( ! el.hasClass( 'focus' ) ) {
// bjy: 阻止链接的默认跳转行为,因为第一次点击是展开子菜单
e.preventDefault(); e.preventDefault();
// bjy: 切换当前菜单项的 'focus' 类,展开子菜单
el.toggleClass( 'focus' ); el.toggleClass( 'focus' );
// bjy: 移除其他同级菜单项的 'focus' 类,确保一次只展开一个子菜单
el.siblings( '.focus').removeClass( 'focus' ); el.siblings( '.focus').removeClass( 'focus' );
} }
} ); } );

@ -1,37 +1,56 @@
/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress /* bjy: NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
* @license MIT */ * bjy: @license MIT */
// bjy: UMDUniversal Module Definition模式包装使库能在多种模块系统AMD, CommonJS, 浏览器全局变量)下工作
;(function(root, factory) { ;(function(root, factory) {
// bjy: 如果环境支持AMD如RequireJS则使用define定义模块
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
define(factory); define(factory);
// bjy: 如果环境支持CommonJS如Node.js则将模块导出
} else if (typeof exports === 'object') { } else if (typeof exports === 'object') {
module.exports = factory(); module.exports = factory();
// bjy: 否则将库挂载到全局对象浏览器中的window
} else { } else {
root.NProgress = factory(); root.NProgress = factory();
} }
// bjy: 传入this在浏览器中为window作为root并调用工厂函数
})(this, function() { })(this, function() {
// bjy: 创建NProgress对象作为库的命名空间
var NProgress = {}; var NProgress = {};
// bjy: 定义NProgress的版本号
NProgress.version = '0.2.0'; NProgress.version = '0.2.0';
// bjy: 定义默认配置项并挂载到NProgress.settings上
var Settings = NProgress.settings = { var Settings = NProgress.settings = {
// bjy: 进度条最小值,防止进度条在开始时看起来像没动
minimum: 0.08, minimum: 0.08,
// bjy: 动画缓动函数
easing: 'linear', easing: 'linear',
// bjy: 进度条定位方式,由程序自动检测
positionUsing: '', positionUsing: '',
// bjy: 动画速度(毫秒)
speed: 200, speed: 200,
// bjy: 是否开启自动递增trickle效果
trickle: true, trickle: true,
// bjy: 自动递增的频率(毫秒)
trickleSpeed: 200, trickleSpeed: 200,
// bjy: 是否显示右上角的加载旋转图标
showSpinner: true, showSpinner: true,
// bjy: 进度条条形的选择器
barSelector: '[role="bar"]', barSelector: '[role="bar"]',
// bjy: 加载旋转图标的选择器
spinnerSelector: '[role="spinner"]', spinnerSelector: '[role="spinner"]',
// bjy: 进度条的父容器默认为body
parent: 'body', parent: 'body',
// bjy: 进度条的HTML模板
template: '<div class="bar" role="bar"><div class="peg"></div></div><div class="spinner" role="spinner"><div class="spinner-icon"></div></div>' template: '<div class="bar" role="bar"><div class="peg"></div></div><div class="spinner" role="spinner"><div class="spinner-icon"></div></div>'
}; };
/** /**
* Updates configuration. * bjy: 更新配置
* *
* NProgress.configure({ * NProgress.configure({
* minimum: 0.1 * minimum: 0.1
@ -39,130 +58,154 @@
*/ */
NProgress.configure = function(options) { NProgress.configure = function(options) {
var key, value; var key, value;
// bjy: 遍历传入的配置项
for (key in options) { for (key in options) {
value = options[key]; value = options[key];
// bjy: 将有效的配置项更新到Settings对象中
if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value; if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
} }
// bjy: 返回NProgress自身支持链式调用
return this; return this;
}; };
/** /**
* Last number. * bjy: 存储当前进度状态的变量
*/ */
NProgress.status = null; NProgress.status = null;
/** /**
* Sets the progress bar status, where `n` is a number from `0.0` to `1.0`. * bjy: 设置进度条的状态`n`是一个从`0.0``1.0`的数字
* *
* NProgress.set(0.4); * NProgress.set(0.4);
* NProgress.set(1.0); * NProgress.set(1.0);
*/ */
NProgress.set = function(n) { NProgress.set = function(n) {
// bjy: 检查进度条是否已经启动
var started = NProgress.isStarted(); var started = NProgress.isStarted();
// bjy: 将进度值n限制在minimum和1之间
n = clamp(n, Settings.minimum, 1); n = clamp(n, Settings.minimum, 1);
// bjy: 更新状态如果进度为1则将status设为null表示完成
NProgress.status = (n === 1 ? null : n); NProgress.status = (n === 1 ? null : n);
// bjy: 如果进度条未渲染,则先渲染它
var progress = NProgress.render(!started), var progress = NProgress.render(!started),
bar = progress.querySelector(Settings.barSelector), bar = progress.querySelector(Settings.barSelector),
speed = Settings.speed, speed = Settings.speed,
ease = Settings.easing; ease = Settings.easing;
// bjy: 触发重排以确保后续的CSS过渡效果能正确应用
progress.offsetWidth; /* Repaint */ progress.offsetWidth; /* Repaint */
// bjy: 使用队列来管理动画,确保动画按顺序执行
queue(function(next) { queue(function(next) {
// Set positionUsing if it hasn't already been set // bjy: 如果定位方式未设置则自动检测最佳的CSS定位方式
if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS(); if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
// Add transition // bjy: 为进度条添加CSS过渡效果更新其位置
css(bar, barPositionCSS(n, speed, ease)); css(bar, barPositionCSS(n, speed, ease));
// bjy: 如果进度达到100%
if (n === 1) { if (n === 1) {
// Fade out // bjy: 先设置一个无过渡的状态并设置opacity为1然后触发重排
css(progress, { css(progress, {
transition: 'none', transition: 'none',
opacity: 1 opacity: 1
}); });
progress.offsetWidth; /* Repaint */ progress.offsetWidth; /* Repaint */
// bjy: 延迟后,添加淡出动画
setTimeout(function() { setTimeout(function() {
css(progress, { css(progress, {
transition: 'all ' + speed + 'ms linear', transition: 'all ' + speed + 'ms linear',
opacity: 0 opacity: 0
}); });
// bjy: 在淡出动画完成后,移除进度条元素,并执行队列的下一个任务
setTimeout(function() { setTimeout(function() {
NProgress.remove(); NProgress.remove();
next(); next();
}, speed); }, speed);
}, speed); }, speed);
} else { } else {
// bjy: 如果未完成,则在动画持续时间后执行下一个任务
setTimeout(next, speed); setTimeout(next, speed);
} }
}); });
// bjy: 返回NProgress自身支持链式调用
return this; return this;
}; };
// bjy: 检查进度条是否已经启动即status是否为数字
NProgress.isStarted = function() { NProgress.isStarted = function() {
return typeof NProgress.status === 'number'; return typeof NProgress.status === 'number';
}; };
/** /**
* Shows the progress bar. * bjy: 显示进度条
* This is the same as setting the status to 0%, except that it doesn't go backwards. * bjy: 这与将状态设置为0%相同只是它不会向后退
* *
* NProgress.start(); * NProgress.start();
* *
*/ */
NProgress.start = function() { NProgress.start = function() {
// bjy: 如果进度条未启动则将其设置为0%
if (!NProgress.status) NProgress.set(0); if (!NProgress.status) NProgress.set(0);
// bjy: 定义一个递归函数用于实现trickle自动递增效果
var work = function() { var work = function() {
setTimeout(function() { setTimeout(function() {
// bjy: 如果进度条已被手动关闭,则停止递增
if (!NProgress.status) return; if (!NProgress.status) return;
// bjy: 调用trickle方法增加一点进度
NProgress.trickle(); NProgress.trickle();
// bjy: 递归调用自身
work(); work();
}, Settings.trickleSpeed); }, Settings.trickleSpeed);
}; };
// bjy: 如果配置中开启了trickle则开始执行
if (Settings.trickle) work(); if (Settings.trickle) work();
return this; return this;
}; };
/** /**
* Hides the progress bar. * bjy: 隐藏进度条
* This is the *sort of* the same as setting the status to 100%, with the * bjy: 这与将状态设置为100%类似`done()`会制造一些更逼真的动画效果
* difference being `done()` makes some placebo effect of some realistic motion.
* *
* NProgress.done(); * NProgress.done();
* *
* If `true` is passed, it will show the progress bar even if its hidden. * bjy: 如果传入`true`即使进度条是隐藏的它也会显示并完成
* *
* NProgress.done(true); * NProgress.done(true);
*/ */
NProgress.done = function(force) { NProgress.done = function(force) {
// bjy: 如果没有强制完成且进度条未启动,则直接返回
if (!force && !NProgress.status) return this; if (!force && !NProgress.status) return this;
// bjy: 先增加一点随机进度然后设置进度为100%
return NProgress.inc(0.3 + 0.5 * Math.random()).set(1); return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
}; };
/** /**
* Increments by a random amount. * bjy: 以一个随机量增加进度
*/ */
NProgress.inc = function(amount) { NProgress.inc = function(amount) {
var n = NProgress.status; var n = NProgress.status;
// bjy: 如果进度条未启动,则启动它
if (!n) { if (!n) {
return NProgress.start(); return NProgress.start();
} else if(n > 1) { } else if(n > 1) {
// bjy: 如果进度已超过100%,不做任何事
} else { } else {
// bjy: 如果没有指定增加的量,则根据当前进度计算一个合适的增量
if (typeof amount !== 'number') { if (typeof amount !== 'number') {
if (n >= 0 && n < 0.2) { amount = 0.1; } if (n >= 0 && n < 0.2) { amount = 0.1; }
else if (n >= 0.2 && n < 0.5) { amount = 0.04; } else if (n >= 0.2 && n < 0.5) { amount = 0.04; }
@ -171,18 +214,20 @@
else { amount = 0; } else { amount = 0; }
} }
// bjy: 计算新的进度值并限制在0到0.994之间防止达到100%后还有动画
n = clamp(n + amount, 0, 0.994); n = clamp(n + amount, 0, 0.994);
return NProgress.set(n); return NProgress.set(n);
} }
}; };
// bjy: trickle方法内部调用inc
NProgress.trickle = function() { NProgress.trickle = function() {
return NProgress.inc(); return NProgress.inc();
}; };
/** /**
* Waits for all supplied jQuery promises and * bjy: 等待所有提供的jQuery promise解决
* increases the progress as the promises resolve. * bjy: 并在promise解决时增加进度
* *
* @param $promise jQUery Promise * @param $promise jQUery Promise
*/ */
@ -190,23 +235,30 @@
var initial = 0, current = 0; var initial = 0, current = 0;
NProgress.promise = function($promise) { NProgress.promise = function($promise) {
// bjy: 如果promise不存在或已经解决则直接返回
if (!$promise || $promise.state() === "resolved") { if (!$promise || $promise.state() === "resolved") {
return this; return this;
} }
// bjy: 如果这是第一个promise则启动进度条
if (current === 0) { if (current === 0) {
NProgress.start(); NProgress.start();
} }
// bjy: 增加总promise计数和当前计数
initial++; initial++;
current++; current++;
// bjy: 为promise添加always回调无论成功或失败都会执行
$promise.always(function() { $promise.always(function() {
// bjy: 当前计数减一
current--; current--;
// bjy: 如果所有promise都已完成
if (current === 0) { if (current === 0) {
initial = 0; initial = 0;
NProgress.done(); NProgress.done();
} else { } else {
// bjy: 否则根据已完成的promise比例更新进度条
NProgress.set((initial - current) / initial); NProgress.set((initial - current) / initial);
} }
}); });
@ -217,55 +269,65 @@
})(); })();
/** /**
* (Internal) renders the progress bar markup based on the `template` * bjy: (内部) 根据`template`设置渲染进度条的HTML标记
* setting.
*/ */
NProgress.render = function(fromStart) { NProgress.render = function(fromStart) {
// bjy: 如果进度条已经渲染,则直接返回已存在的元素
if (NProgress.isRendered()) return document.getElementById('nprogress'); if (NProgress.isRendered()) return document.getElementById('nprogress');
// bjy: 给html元素添加'nprogress-busy'类,可用于样式控制
addClass(document.documentElement, 'nprogress-busy'); addClass(document.documentElement, 'nprogress-busy');
// bjy: 创建进度条的容器div
var progress = document.createElement('div'); var progress = document.createElement('div');
progress.id = 'nprogress'; progress.id = 'nprogress';
// bjy: 使用模板设置其innerHTML
progress.innerHTML = Settings.template; progress.innerHTML = Settings.template;
var bar = progress.querySelector(Settings.barSelector), var bar = progress.querySelector(Settings.barSelector),
// bjy: 如果是从头开始,则初始位置在-100%,否则根据当前状态计算位置
perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0), perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0),
parent = document.querySelector(Settings.parent), parent = document.querySelector(Settings.parent),
spinner; spinner;
// bjy: 设置进度条条的初始位置和过渡效果
css(bar, { css(bar, {
transition: 'all 0 linear', transition: 'all 0 linear',
transform: 'translate3d(' + perc + '%,0,0)' transform: 'translate3d(' + perc + '%,0,0)'
}); });
// bjy: 如果配置中不显示旋转图标,则将其移除
if (!Settings.showSpinner) { if (!Settings.showSpinner) {
spinner = progress.querySelector(Settings.spinnerSelector); spinner = progress.querySelector(Settings.spinnerSelector);
spinner && removeElement(spinner); spinner && removeElement(spinner);
} }
// bjy: 如果父容器不是body则给父容器添加自定义类
if (parent != document.body) { if (parent != document.body) {
addClass(parent, 'nprogress-custom-parent'); addClass(parent, 'nprogress-custom-parent');
} }
// bjy: 将进度条元素添加到父容器中
parent.appendChild(progress); parent.appendChild(progress);
return progress; return progress;
}; };
/** /**
* Removes the element. Opposite of render(). * bjy: 移除元素与render()相反
*/ */
NProgress.remove = function() { NProgress.remove = function() {
// bjy: 移除html和父容器上的辅助类
removeClass(document.documentElement, 'nprogress-busy'); removeClass(document.documentElement, 'nprogress-busy');
removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent'); removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent');
// bjy: 从DOM中移除进度条元素
var progress = document.getElementById('nprogress'); var progress = document.getElementById('nprogress');
progress && removeElement(progress); progress && removeElement(progress);
}; };
/** /**
* Checks if the progress bar is rendered. * bjy: 检查进度条是否已渲染
*/ */
NProgress.isRendered = function() { NProgress.isRendered = function() {
@ -273,35 +335,36 @@
}; };
/** /**
* Determine which positioning CSS rule to use. * bjy: 确定使用哪种定位CSS规则
*/ */
NProgress.getPositioningCSS = function() { NProgress.getPositioningCSS = function() {
// Sniff on document.body.style // bjy: 检查body的style属性以嗅探浏览器支持
var bodyStyle = document.body.style; var bodyStyle = document.body.style;
// Sniff prefixes // bjy: 嗅探浏览器支持的CSS前缀
var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' : var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
('MozTransform' in bodyStyle) ? 'Moz' : ('MozTransform' in bodyStyle) ? 'Moz' :
('msTransform' in bodyStyle) ? 'ms' : ('msTransform' in bodyStyle) ? 'ms' :
('OTransform' in bodyStyle) ? 'O' : ''; ('OTransform' in bodyStyle) ? 'O' : '';
// bjy: 如果支持3D变换则使用translate3d
if (vendorPrefix + 'Perspective' in bodyStyle) { if (vendorPrefix + 'Perspective' in bodyStyle) {
// Modern browsers with 3D support, e.g. Webkit, IE10
return 'translate3d'; return 'translate3d';
// bjy: 如果只支持2D变换则使用translate
} else if (vendorPrefix + 'Transform' in bodyStyle) { } else if (vendorPrefix + 'Transform' in bodyStyle) {
// Browsers without 3D support, e.g. IE9
return 'translate'; return 'translate';
// bjy: 否则回退到使用margin性能较差
} else { } else {
// Browsers without translate() support, e.g. IE7-8
return 'margin'; return 'margin';
} }
}; };
/** /**
* Helpers * bjy: 辅助函数
*/ */
// bjy: 将一个数值限制在最小值和最大值之间
function clamp(n, min, max) { function clamp(n, min, max) {
if (n < min) return min; if (n < min) return min;
if (n > max) return max; if (n > max) return max;
@ -309,8 +372,7 @@
} }
/** /**
* (Internal) converts a percentage (`0..1`) to a bar translateX * bjy: (内部) 将百分比 (`0..1`) 转换为进度条的translateX百分比 (`-100%..0%`)
* percentage (`-100%..0%`).
*/ */
function toBarPerc(n) { function toBarPerc(n) {
@ -319,13 +381,14 @@
/** /**
* (Internal) returns the correct CSS for changing the bar's * bjy: (内部) 返回用于改变进度条位置的CSS
* position given an n percentage, and speed and ease from Settings * bjy: 根据给定的百分比n以及Settings中的速度和缓动函数
*/ */
function barPositionCSS(n, speed, ease) { function barPositionCSS(n, speed, ease) {
var barCSS; var barCSS;
// bjy: 根据定位方式生成不同的CSS
if (Settings.positionUsing === 'translate3d') { if (Settings.positionUsing === 'translate3d') {
barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' }; barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
} else if (Settings.positionUsing === 'translate') { } else if (Settings.positionUsing === 'translate') {
@ -334,18 +397,20 @@
barCSS = { 'margin-left': toBarPerc(n)+'%' }; barCSS = { 'margin-left': toBarPerc(n)+'%' };
} }
// bjy: 添加过渡效果
barCSS.transition = 'all '+speed+'ms '+ease; barCSS.transition = 'all '+speed+'ms '+ease;
return barCSS; return barCSS;
} }
/** /**
* (Internal) Queues a function to be executed. * bjy: (内部) 将一个函数排队等待执行
*/ */
var queue = (function() { var queue = (function() {
var pending = []; var pending = [];
// bjy: 从队列中取出第一个函数并执行
function next() { function next() {
var fn = pending.shift(); var fn = pending.shift();
if (fn) { if (fn) {
@ -353,30 +418,35 @@
} }
} }
// bjy: 返回一个函数,用于向队列中添加新任务
return function(fn) { return function(fn) {
pending.push(fn); pending.push(fn);
// bjy: 如果这是队列中唯一的任务,则立即开始执行
if (pending.length == 1) next(); if (pending.length == 1) next();
}; };
})(); })();
/** /**
* (Internal) Applies css properties to an element, similar to the jQuery * bjy: (内部) 将CSS属性应用到元素上类似于jQuery的css方法
* css method.
* *
* While this helper does assist with vendor prefixed property names, it * bjy: 虽然这个辅助函数有助于处理带供应商前缀的属性名
* does not perform any manipulation of values prior to setting styles. * bjy: 但它在设置样式之前不会对值进行任何操作
*/ */
var css = (function() { var css = (function() {
// bjy: 常见的CSS前缀列表
var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ], var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
// bjy: 缓存已检测的CSS属性名
cssProps = {}; cssProps = {};
// bjy: 将连字符格式的字符串转换为驼峰格式
function camelCase(string) { function camelCase(string) {
return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) { return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
return letter.toUpperCase(); return letter.toUpperCase();
}); });
} }
// bjy: 获取带正确前缀的CSS属性名
function getVendorProp(name) { function getVendorProp(name) {
var style = document.body.style; var style = document.body.style;
if (name in style) return name; if (name in style) return name;
@ -384,6 +454,7 @@
var i = cssPrefixes.length, var i = cssPrefixes.length,
capName = name.charAt(0).toUpperCase() + name.slice(1), capName = name.charAt(0).toUpperCase() + name.slice(1),
vendorName; vendorName;
// bjy: 遍历前缀,检查哪个前缀的属性被支持
while (i--) { while (i--) {
vendorName = cssPrefixes[i] + capName; vendorName = cssPrefixes[i] + capName;
if (vendorName in style) return vendorName; if (vendorName in style) return vendorName;
@ -392,34 +463,39 @@
return name; return name;
} }
// bjy: 获取最终的样式属性名(带缓存)
function getStyleProp(name) { function getStyleProp(name) {
name = camelCase(name); name = camelCase(name);
return cssProps[name] || (cssProps[name] = getVendorProp(name)); return cssProps[name] || (cssProps[name] = getVendorProp(name));
} }
// bjy: 应用单个CSS属性到元素
function applyCss(element, prop, value) { function applyCss(element, prop, value) {
prop = getStyleProp(prop); prop = getStyleProp(prop);
element.style[prop] = value; element.style[prop] = value;
} }
// bjy: 暴露的css函数支持单个或多个属性设置
return function(element, properties) { return function(element, properties) {
var args = arguments, var args = arguments,
prop, prop,
value; value;
// bjy: 如果传入两个参数element, properties对象
if (args.length == 2) { if (args.length == 2) {
for (prop in properties) { for (prop in properties) {
value = properties[prop]; value = properties[prop];
if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value); if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
} }
} else { } else {
// bjy: 如果传入三个参数element, prop, value
applyCss(element, args[1], args[2]); applyCss(element, args[1], args[2]);
} }
} }
})(); })();
/** /**
* (Internal) Determines if an element or space separated list of class names contains a class name. * bjy: (内部) 判断一个元素或空格分隔的类名字符串是否包含某个类名
*/ */
function hasClass(element, name) { function hasClass(element, name) {
@ -428,7 +504,7 @@
} }
/** /**
* (Internal) Adds a class to an element. * bjy: (内部) 给一个元素添加类名
*/ */
function addClass(element, name) { function addClass(element, name) {
@ -437,12 +513,12 @@
if (hasClass(oldList, name)) return; if (hasClass(oldList, name)) return;
// Trim the opening space. // bjy: 去掉开头的空格
element.className = newList.substring(1); element.className = newList.substring(1);
} }
/** /**
* (Internal) Removes a class from an element. * bjy: (内部) 从一个元素移除类名
*/ */
function removeClass(element, name) { function removeClass(element, name) {
@ -451,17 +527,16 @@
if (!hasClass(element, name)) return; if (!hasClass(element, name)) return;
// Replace the class name. // bjy: 替换掉要移除的类名
newList = oldList.replace(' ' + name + ' ', ' '); newList = oldList.replace(' ' + name + ' ', ' ');
// Trim the opening and closing spaces. // bjy: 去掉开头和结尾的空格
element.className = newList.substring(1, newList.length - 1); element.className = newList.substring(1, newList.length - 1);
} }
/** /**
* (Internal) Gets a space separated list of the class names on the element. * bjy: (内部) 获取元素上所有类名的空格分隔列表
* The list is wrapped with a single space on each end to facilitate finding * bjy: 列表的首尾都包裹一个空格以便于在列表中查找匹配项
* matches within the list.
*/ */
function classList(element) { function classList(element) {
@ -469,12 +544,13 @@
} }
/** /**
* (Internal) Removes an element from the DOM. * bjy: (内部) 从DOM中移除一个元素
*/ */
function removeElement(element) { function removeElement(element) {
element && element.parentNode && element.parentNode.removeChild(element); element && element.parentNode && element.parentNode.removeChild(element);
} }
// bjy: 返回NProgress对象
return NProgress; return NProgress;
}); });

@ -0,0 +1,132 @@
// blog/static/blog/js/theme-switcher.js
(function() {
'use strict';
class ThemeSwitcher {
constructor() {
this.themeToggle = null;
this.currentTheme = this.getPreferredTheme();
this.init();
}
init() {
console.log('初始化主题切换器,当前主题:', this.currentTheme);
this.setTheme(this.currentTheme);
this.bindEvents();
this.watchSystemTheme();
}
getPreferredTheme() {
try {
const storedTheme = localStorage.getItem('theme');
if (storedTheme) {
return storedTheme;
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
} catch (error) {
console.warn('获取主题偏好失败:', error);
return 'light';
}
}
setTheme(theme) {
try {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
this.currentTheme = theme;
this.updateToggleIcon();
console.log('主题已设置为:', theme);
const event = new CustomEvent('themeChanged', {
detail: { theme: theme }
});
document.dispatchEvent(event);
} catch (error) {
console.error('设置主题失败:', error);
}
}
toggleTheme() {
const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
console.log('切换主题:', this.currentTheme, '->', newTheme);
this.setTheme(newTheme);
}
bindEvents() {
this.themeToggle = document.getElementById('theme-toggle');
if (this.themeToggle) {
this.themeToggle.addEventListener('click', () => {
this.toggleTheme();
});
this.updateToggleIcon();
} else {
console.warn('未找到主题切换按钮 #theme-toggle');
setTimeout(() => {
this.themeToggle = document.getElementById('theme-toggle');
if (this.themeToggle) {
this.themeToggle.addEventListener('click', () => {
this.toggleTheme();
});
this.updateToggleIcon();
}
}, 1000);
}
}
updateToggleIcon() {
if (!this.themeToggle) return;
const iconElement = this.themeToggle.querySelector('.theme-icon');
if (iconElement) {
const icon = this.currentTheme === 'light' ? '🌙' : '☀️';
const title = this.currentTheme === 'light' ? '切换到深色模式' : '切换到浅色模式';
iconElement.textContent = icon;
this.themeToggle.title = title;
this.themeToggle.setAttribute('aria-label', title);
}
}
watchSystemTheme() {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = (e) => {
if (!localStorage.getItem('theme')) {
const newTheme = e.matches ? 'dark' : 'light';
console.log('系统主题变化,自动切换至:', newTheme);
this.setTheme(newTheme);
}
};
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleSystemThemeChange);
} else {
mediaQuery.addListener(handleSystemThemeChange);
}
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
window.themeSwitcher = new ThemeSwitcher();
});
} else {
window.themeSwitcher = new ThemeSwitcher();
}
window.toggleTheme = function() {
if (window.themeSwitcher) {
window.themeSwitcher.toggleTheme();
}
};
})();

@ -1,5 +1,9 @@
# bjy: 导入操作系统接口模块
import json
import os import os
from unittest.mock import patch, MagicMock
# bjy: 从Django中导入设置、文件上传、命令调用、分页器、静态文件、测试工具、URL反向解析和时区工具
from django.conf import settings from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command from django.core.management import call_command
@ -9,30 +13,41 @@ from django.test import Client, RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
# bjy: 从项目中导入用户模型、博客表单、博客模型、自定义模板标签和工具函数
from accounts.models import BlogUser from accounts.models import BlogUser
from blog.forms import BlogSearchForm from blog.forms import BlogSearchForm
from blog.models import Article, Category, Tag, SideBar, Links from blog.models import Article, Category, Tag, SideBar, Links
from blog.templatetags.blog_tags import load_pagination_info, load_articletags from blog.templatetags.blog_tags import load_pagination_info, load_articletags
from blog.views import LikeArticle
from djangoblog.utils import get_current_site, get_sha256 from djangoblog.utils import get_current_site, get_sha256
# bjy: 从项目中导入OAuth相关模型
from oauth.models import OAuthUser, OAuthConfig from oauth.models import OAuthUser, OAuthConfig
# Create your tests here. # bjy: 在此处创建测试。
# bjy: 定义一个针对文章功能的测试类继承自Django的TestCase
class ArticleTest(TestCase): class ArticleTest(TestCase):
# bjy: setUp方法在每个测试方法执行前运行用于初始化测试环境
def setUp(self): def setUp(self):
# bjy: 创建一个测试客户端实例,用于模拟浏览器请求
self.client = Client() self.client = Client()
# bjy: 创建一个请求工厂实例,用于生成请求对象
self.factory = RequestFactory() self.factory = RequestFactory()
# bjy: 定义一个测试方法,用于验证文章相关的功能
def test_validate_article(self): def test_validate_article(self):
site = get_current_site().domain site = get_current_site().domain
user = BlogUser.objects.get_or_create( user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com", email="liangliangyy@gmail.com",
username="liangliangyy")[0] username="liangliangyy")[0]
# bjy: 设置用户密码
user.set_password("liangliangyy") user.set_password("liangliangyy")
# bjy: 设置用户为员工和管理员
user.is_staff = True user.is_staff = True
user.is_superuser = True user.is_superuser = True
user.save() user.save()
# bjy: 测试用户详情页是否能正常访问
response = self.client.get(user.get_absolute_url()) response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get('/admin/servermanager/emailsendlog/') response = self.client.get('/admin/servermanager/emailsendlog/')
@ -44,16 +59,19 @@ class ArticleTest(TestCase):
s.is_enable = True s.is_enable = True
s.save() s.save()
# bjy: 创建并保存一个分类实例
category = Category() category = Category()
category.name = "category" category.name = "category"
category.creation_time = timezone.now() category.creation_time = timezone.now()
category.last_mod_time = timezone.now() category.last_mod_time = timezone.now()
category.save() category.save()
# bjy: 创建并保存一个标签实例
tag = Tag() tag = Tag()
tag.name = "nicetag" tag.name = "nicetag"
tag.save() tag.save()
# bjy: 创建并保存一篇文章
article = Article() article = Article()
article.title = "nicetitle" article.title = "nicetitle"
article.body = "nicecontent" article.body = "nicecontent"
@ -63,11 +81,15 @@ class ArticleTest(TestCase):
article.status = 'p' article.status = 'p'
article.save() article.save()
# bjy: 验证文章初始标签数量为0
self.assertEqual(0, article.tags.count()) self.assertEqual(0, article.tags.count())
# bjy: 给文章添加标签并保存
article.tags.add(tag) article.tags.add(tag)
article.save() article.save()
# bjy: 验证文章标签数量变为1
self.assertEqual(1, article.tags.count()) self.assertEqual(1, article.tags.count())
# bjy: 循环创建20篇文章用于测试分页等功能
for i in range(20): for i in range(20):
article = Article() article = Article()
article.title = "nicetitle" + str(i) article.title = "nicetitle" + str(i)
@ -79,96 +101,126 @@ class ArticleTest(TestCase):
article.save() article.save()
article.tags.add(tag) article.tags.add(tag)
article.save() article.save()
# bjy: 如果启用了Elasticsearch则重建索引并测试搜索功能
from blog.documents import ELASTICSEARCH_ENABLED from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
call_command("build_index") call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'}) response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 测试文章详情页
response = self.client.get(article.get_absolute_url()) response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 测试蜘蛛通知功能
from djangoblog.spider_notify import SpiderNotify from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url()) SpiderNotify.notify(article.get_absolute_url())
# bjy: 测试标签页
response = self.client.get(tag.get_absolute_url()) response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 测试分类页
response = self.client.get(category.get_absolute_url()) response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 测试搜索页
response = self.client.get('/search', {'q': 'django'}) response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 测试加载文章标签的模板标签
s = load_articletags(article) s = load_articletags(article)
self.assertIsNotNone(s) self.assertIsNotNone(s)
# bjy: 以超级用户身份登录
self.client.login(username='liangliangyy', password='liangliangyy') self.client.login(username='liangliangyy', password='liangliangyy')
# bjy: 测试文章归档页
response = self.client.get(reverse('blog:archives')) response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 测试文章列表的分页信息
p = Paginator(Article.objects.all(), settings.PAGINATE_BY) p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '') self.check_pagination(p, '', '')
# bjy: 测试按标签筛选后的文章分页
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug) self.check_pagination(p, '分类标签归档', tag.slug)
# bjy: 测试按作者筛选后的文章分页
p = Paginator( p = Paginator(
Article.objects.filter( Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY) author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy') self.check_pagination(p, '作者文章归档', 'liangliangyy')
# bjy: 测试按分类筛选后的文章分页
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug) self.check_pagination(p, '分类目录归档', category.slug)
# bjy: 测试搜索表单
f = BlogSearchForm() f = BlogSearchForm()
f.search() f.search()
# self.client.login(username='liangliangyy', password='liangliangyy') # bjy: 测试百度通知功能
from djangoblog.spider_notify import SpiderNotify from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()]) SpiderNotify.baidu_notify([article.get_full_url()])
# bjy: 测试获取Gravatar头像的模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com') u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com') u = gravatar('liangliangyy@gmail.com')
# bjy: 创建并保存一个友情链接
link = Links( link = Links(
sequence=1, sequence=1,
name="lylinux", name="lylinux",
link='https://wwww.lylinux.net') link='https://wwww.lylinux.net')
link.save() link.save()
# bjy: 测试友情链接页面
response = self.client.get('/links.html') response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 测试RSS订阅页面
response = self.client.get('/feed/') response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 测试网站地图
response = self.client.get('/sitemap.xml') response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 测试一些Admin后台的删除和变更操作
self.client.get("/admin/blog/article/1/delete/") self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/') self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/') self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/') self.client.get('/admin/admin/logentry/1/change/')
# bjy: 辅助方法,用于检查分页导航链接是否正确
def check_pagination(self, p, type, value): def check_pagination(self, p, type, value):
# bjy: 遍历所有页码
for page in range(1, p.num_pages + 1): for page in range(1, p.num_pages + 1):
# bjy: 加载当前页的分页信息
s = load_pagination_info(p.page(page), type, value) s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s) self.assertIsNotNone(s)
# bjy: 如果存在上一页链接,则测试其可访问性
if s['previous_url']: if s['previous_url']:
response = self.client.get(s['previous_url']) response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 如果存在下一页链接,则测试其可访问性
if s['next_url']: if s['next_url']:
response = self.client.get(s['next_url']) response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# bjy: 测试图片上传功能
def test_image(self): def test_image(self):
# bjy: 下载一个网络图片到本地
import requests import requests
rsp = requests.get( rsp = requests.get(
'https://www.python.org/static/img/python-logo.png') 'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png') imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file: with open(imagepath, 'wb') as file:
file.write(rsp.content) file.write(rsp.content)
# bjy: 测试无签名上传预期返回403
rsp = self.client.post('/upload') rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403) self.assertEqual(rsp.status_code, 403)
# bjy: 生成签名
sign = get_sha256(get_sha256(settings.SECRET_KEY)) sign = get_sha256(get_sha256(settings.SECRET_KEY))
# bjy: 使用签名上传图片
with open(imagepath, 'rb') as file: with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile( imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg') 'python.png', file.read(), content_type='image/jpg')
@ -176,17 +228,23 @@ class ArticleTest(TestCase):
rsp = self.client.post( rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True) '/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200) self.assertEqual(rsp.status_code, 200)
# bjy: 删除本地临时图片
os.remove(imagepath) os.remove(imagepath)
# bjy: 测试发送邮件和保存用户头像的工具函数
from djangoblog.utils import save_user_avatar, send_email from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent') send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar( save_user_avatar(
'https://www.python.org/static/img/python-logo.png') 'https://www.python.org/static/img/python-logo.png')
# bjy: 测试404错误页面
def test_errorpage(self): def test_errorpage(self):
rsp = self.client.get('/eee') rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404) self.assertEqual(rsp.status_code, 404)
def test_commands(self): # bjy: 测试自定义的管理命令
@staticmethod
def test_commands():
# bjy: 创建一个超级用户(如果不存在)
user = BlogUser.objects.get_or_create( user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com", email="liangliangyy@gmail.com",
username="liangliangyy")[0] username="liangliangyy")[0]
@ -195,12 +253,14 @@ class ArticleTest(TestCase):
user.is_superuser = True user.is_superuser = True
user.save() user.save()
# bjy: 创建并保存一个OAuth配置
c = OAuthConfig() c = OAuthConfig()
c.type = 'qq' c.type = 'qq'
c.appkey = 'appkey' c.appkey = 'appkey'
c.appsecret = 'appsecret' c.appsecret = 'appsecret'
c.save() c.save()
# bjy: 创建并保存一个OAuth用户关联到超级用户
u = OAuthUser() u = OAuthUser()
u.type = 'qq' u.type = 'qq'
u.openid = 'openid' u.openid = 'openid'
@ -212,6 +272,7 @@ class ArticleTest(TestCase):
}''' }'''
u.save() u.save()
# bjy: 创建另一个OAuth用户用于测试
u = OAuthUser() u = OAuthUser()
u.type = 'qq' u.type = 'qq'
u.openid = 'openid1' u.openid = 'openid1'
@ -222,11 +283,294 @@ class ArticleTest(TestCase):
}''' }'''
u.save() u.save()
# bjy: 如果启用了Elasticsearch则重建索引
from blog.documents import ELASTICSEARCH_ENABLED from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
call_command("build_index") call_command("build_index")
# bjy: 调用并测试一系列自定义管理命令
call_command("ping_baidu", "all") call_command("ping_baidu", "all")
call_command("create_testdata") call_command("create_testdata")
call_command("clear_cache") call_command("clear_cache")
call_command("sync_user_avatar") call_command("sync_user_avatar")
call_command("build_search_words") call_command("build_search_words")
class TestLikeArticle(TestCase):
"""测试 LikeArticle 视图类中的 post 方法"""
def setUp(self):
"""
初始化测试所需的数据和工具
"""
self.factory = RequestFactory()
self.user = BlogUser.objects.create_user(username='testuser', password='password')
# 创建分类Article模型需要category字段
self.category = Category.objects.create(
name="Test Category",
slug="test-category"
)
self.article = Article.objects.create(
title="Test Article",
body="This is a test article.",
author=self.user,
category=self.category, # Article模型必需字段
views=0,
)
@patch('blog.models.Article.objects.get')
def test_post_like_article_successfully(self, mock_get_article):
"""
测试场景用户第一次点赞文章成功
输入
- 已登录用户
- 存在的文章 ID
- 用户尚未点赞该文章
输出
- type = 1 表示新增点赞
- like_sum 更新为 1
- state = 200 成功状态码
"""
# 设置 mock 返回值
mock_article = MagicMock()
mock_article.users_like.filter.return_value.exists.return_value = False
mock_article.users_like.count.return_value = 1
mock_get_article.return_value = mock_article
# 构造 POST 请求
request = self.factory.post('/like/', {'article_id': str(self.article.id)})
request.user = self.user
# 执行被测函数
response = LikeArticle().post(request)
# 断言调用了 add 方法表示点赞
mock_article.users_like.add.assert_called_once_with(self.user)
mock_article.users_like.remove.assert_not_called()
# 解析响应内容
content = json.loads(response.content.decode())
self.assertEqual(response.status_code, 200)
self.assertEqual(content['type'], 1)
self.assertEqual(content['like_sum'], 1)
self.assertEqual(content['state'], 200)
@patch('blog.models.Article.objects.get')
def test_post_unlike_article_successfully(self, mock_get_article):
"""
测试场景用户取消点赞文章成功
输入
- 已登录用户
- 存在的文章 ID
- 用户已经点赞了该文章
输出
- type = 0 表示取消点赞
- like_sum 更新为 0
- state = 200 成功状态码
"""
# 设置 mock 返回值
mock_article = MagicMock()
mock_article.users_like.filter.return_value.exists.return_value = True
mock_article.users_like.count.return_value = 0
mock_get_article.return_value = mock_article
# 构造 POST 请求
request = self.factory.post('/like/', {'article_id': str(self.article.id)})
request.user = self.user
# 执行被测函数
response = LikeArticle().post(request)
# 断言调用了 remove 方法表示取消点赞
mock_article.users_like.remove.assert_called_once_with(self.user)
mock_article.users_like.add.assert_not_called()
# 解析响应内容
content = json.loads(response.content.decode())
self.assertEqual(response.status_code, 200)
self.assertEqual(content['type'], 0)
self.assertEqual(content['like_sum'], 0)
self.assertEqual(content['state'], 200)
@patch('blog.models.Article.objects.get')
def test_post_article_does_not_exist(self, mock_get_article):
"""
测试场景提供的文章 ID 不存在
输入
- 任意用户
- 不存在的文章 ID
输出
- state = 400 错误状态码
- data 包含文章不存在提示
"""
# 设置 mock 抛出 DoesNotExist 异常
mock_get_article.side_effect = Article.DoesNotExist
# 构造 POST 请求
request = self.factory.post('/like/', {'article_id': '999'})
request.user = self.user
# 执行被测函数
response = LikeArticle().post(request)
# 解析响应内容
content = json.loads(response.content.decode())
self.assertEqual(response.status_code, 200)
self.assertEqual(content['state'], 400)
self.assertIn("文章不存在", content['data'])
@patch('blog.models.Article.objects.get')
def test_post_internal_server_error(self, mock_get_article):
"""
测试场景系统内部发生异常
输入
- 任意用户
- 导致异常的操作如数据库连接失败等
输出
- state = 500 错误状态码
- data 包含具体异常描述
"""
# 设置 mock 抛出通用异常
mock_get_article.side_effect = Exception("数据库连接超时")
# 构造 POST 请求
request = self.factory.post('/like/', {'article_id': str(self.article.id)})
request.user = self.user
# 执行被测函数
response = LikeArticle().post(request)
# 解析响应内容
content = json.loads(response.content.decode())
self.assertEqual(response.status_code, 200)
self.assertEqual(content['state'], 500)
self.assertIn("服务器错误", content['data'])
class LikeIntegrationTests(TestCase):
def setUp(self):
"""设置测试数据"""
self.client = Client()
self.user = BlogUser.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.other_user = BlogUser.objects.create_user(
username='otheruser',
email='other@example.com',
password='testpass123'
)
# 创建分类因为Article模型需要category字段
self.category = Category.objects.create(
name='测试分类',
slug='test-category'
)
self.article = Article.objects.create(
title='Test Article',
body='Test content',
author=self.user,
category=self.category, # 必须提供category
# 其他必填字段使用默认值
status='p', # 发布状态
comment_status='o', # 开放评论
type='a', # 文章类型
article_order=0,
show_toc=False
)
def test_like_workflow(self):
"""测试完整的点赞流程"""
# 1. 用户登录
self.client.login(username='testuser', password='testpass123')
# 2. 发送点赞请求
response = self.client.post(
reverse('blog:like_article'),
{'article_id': self.article.id},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
# 3. 验证响应
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['type'], 1) # 点赞操作
# 4. 验证数据库状态
self.assertTrue(self.article.users_like.filter(id=self.user.id).exists())
self.assertEqual(self.article.users_like.count(), 1)
# 5. 测试取消点赞
response = self.client.post(
reverse('blog:like_article'),
{'article_id': self.article.id},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
# 6. 验证取消点赞
self.assertEqual(response.json()['type'], 0) # 取消点赞操作
self.assertFalse(self.article.users_like.filter(id=self.user.id).exists())
self.assertEqual(self.article.users_like.count(), 0)
def test_multiple_users_liking(self):
"""测试多个用户点赞同一篇文章"""
# 第一个用户点赞
self.client.login(username='testuser', password='testpass123')
self.client.post(
reverse('blog:like_article'),
{'article_id': self.article.id},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
# 第二个用户点赞
self.client.login(username='otheruser', password='testpass123')
response = self.client.post(
reverse('blog:like_article'),
{'article_id': self.article.id},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
# 验证两个用户都点赞成功
self.assertEqual(self.article.users_like.count(), 2)
self.assertTrue(self.article.users_like.filter(id=self.user.id).exists())
self.assertTrue(self.article.users_like.filter(id=self.other_user.id).exists())
def test_like_nonexistent_article(self):
"""测试给不存在的文章点赞"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(
reverse('blog:like_article'),
{'article_id': 1145}, # 不存在的文章ID
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assertEqual(response.status_code, 400)
self.assertIn('文章不存在', response.json()['data'])
def test_like_without_login(self):
"""测试未登录用户点赞"""
response = self.client.post(
reverse('blog:like_article'),
{'article_id': self.article.id},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
# 应该重定向到登录页面或者返回错误
self.assertIn(response.status_code, [302, 403]) # 重定向或权限拒绝
def test_like_with_invalid_method(self):
"""测试使用错误的HTTP方法"""
self.client.login(username='testuser', password='testpass123')
response = self.client.get(reverse('blog:like_article')) # 使用GET而不是POST
self.assertEqual(response.status_code, 405) # Method Not Allowed

@ -1,62 +1,83 @@
# bjy: 从Django中导入路径函数和缓存页面装饰器
from django.urls import path from django.urls import path
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
# bjy: 从当前应用中导入视图模块
from . import views from . import views
# bjy: 定义应用的命名空间用于在模板中反向解析URL时避免冲突
app_name = "blog" app_name = "blog"
# bjy: 定义URL模式列表
urlpatterns = [ urlpatterns = [
# bjy: 首页路由指向IndexView视图
path( path(
r'', r'',
views.IndexView.as_view(), views.IndexView.as_view(),
name='index'), name='index'),
# bjy: 首页分页路由指向IndexView视图并接收页码参数
path( path(
r'page/<int:page>/', r'page/<int:page>/',
views.IndexView.as_view(), views.IndexView.as_view(),
name='index_page'), name='index_page'),
# bjy: 文章详情页路由包含年、月、日和文章ID指向ArticleDetailView视图
path( path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html', r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(), views.ArticleDetailView.as_view(),
name='detailbyid'), name='detailbyid'),
# bjy: 分类详情页路由接收分类的slug指向CategoryDetailView视图
path( path(
r'category/<slug:category_name>.html', r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(),
name='category_detail'), name='category_detail'),
# bjy: 分类详情页分页路由接收分类slug和页码指向CategoryDetailView视图
path( path(
r'category/<slug:category_name>/<int:page>.html', r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(),
name='category_detail_page'), name='category_detail_page'),
# bjy: 作者详情页路由接收作者名指向AuthorDetailView视图
path( path(
r'author/<author_name>.html', r'author/<author_name>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(),
name='author_detail'), name='author_detail'),
# bjy: 作者详情页分页路由接收作者名和页码指向AuthorDetailView视图
path( path(
r'author/<author_name>/<int:page>.html', r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(),
name='author_detail_page'), name='author_detail_page'),
# bjy: 标签详情页路由接收标签的slug指向TagDetailView视图
path( path(
r'tag/<slug:tag_name>.html', r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(),
name='tag_detail'), name='tag_detail'),
# bjy: 标签详情页分页路由接收标签slug和页码指向TagDetailView视图
path( path(
r'tag/<slug:tag_name>/<int:page>.html', r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(),
name='tag_detail_page'), name='tag_detail_page'),
# bjy: 文章归档页路由使用cache_page装饰器缓存1小时指向ArchivesView视图
path( path(
'archives.html', 'archives.html',
cache_page( cache_page(
60 * 60)( 60 * 60)(
views.ArchivesView.as_view()), views.ArchivesView.as_view()),
name='archives'), name='archives'),
# bjy: 友情链接页路由指向LinkListView视图
path( path(
'links.html', 'links.html',
views.LinkListView.as_view(), views.LinkListView.as_view(),
name='links'), name='links'),
# bjy: 文件上传路由指向fileupload视图函数
path( path(
r'upload', r'upload',
views.fileupload, views.fileupload,
name='upload'), name='upload'),
# bjy: 清除缓存路由指向clean_cache_view视图函数
path( path(
r'clean', r'clean',
views.clean_cache_view, views.clean_cache_view,
name='clean'), name='clean'),
path(
'like_article/',
views.LikeArticle.as_view(),
name='like_article'),
] ]

@ -1,64 +1,80 @@
# bjy: 导入日志、操作系统和UUID模块
import logging import logging
import os import os
import uuid import uuid
# bjy: 从Django中导入设置、分页器、HTTP响应、快捷函数、静态文件、时区、国际化、CSRF豁免和基于类的视图
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.shortcuts import render from django.shortcuts import render
from django.templatetags.static import static from django.templatetags.static import static
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.list import ListView from django.views.generic.list import ListView
# bjy: 从Haystack中导入搜索视图
from haystack.views import SearchView from haystack.views import SearchView
# bjy: 从项目中导入博客模型、评论表单、插件管理器和工具函数
from blog.models import Article, Category, LinkShowType, Links, Tag from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256 from djangoblog.utils import cache, get_blog_setting, get_sha256
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# bjy: 定义一个基于类的文章列表视图,作为其他列表视图的基类
class ArticleListView(ListView): class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染 # bjy: 指定渲染的模板文件
template_name = 'blog/article_index.html' template_name = 'blog/article_index.html'
# bjy: 指定在模板中使用的上下文变量名
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list' context_object_name = 'article_list'
# bjy: 页面类型,用于在模板中显示不同的标题
# 页面类型,分类目录或标签列表等
page_type = '' page_type = ''
# bjy: 每页显示的文章数量,从设置中获取
paginate_by = settings.PAGINATE_BY paginate_by = settings.PAGINATE_BY
# bjy: URL中分页参数的名称
page_kwarg = 'page' page_kwarg = 'page'
# bjy: 友情链接的显示类型,默认为列表页
link_type = LinkShowType.L link_type = LinkShowType.L
# bjy: 获取视图的缓存键(此方法未使用)
def get_view_cache_key(self): def get_view_cache_key(self):
return self.request.get['pages'] return self.request.get['pages']
# bjy: 属性,用于获取当前页码
@property @property
def page_number(self): def page_number(self):
page_kwarg = self.page_kwarg page_kwarg = self.page_kwarg
# bjy: 从URL参数或GET参数中获取页码默认为1
page = self.kwargs.get( page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1 page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page return page
# bjy: 抽象方法要求子类实现用于获取queryset的缓存键
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
""" """
子类重写.获得queryset的缓存key 子类重写.获得queryset的缓存key
""" """
raise NotImplementedError() raise NotImplementedError()
# bjy: 抽象方法,要求子类实现,用于获取实际的数据集
def get_queryset_data(self): def get_queryset_data(self):
""" """
子类重写.获取queryset的数据 子类重写.获取queryset的数据
""" """
raise NotImplementedError() raise NotImplementedError()
# bjy: 从缓存中获取数据集,如果缓存不存在则查询数据库并存入缓存
def get_queryset_from_cache(self, cache_key): def get_queryset_from_cache(self, cache_key):
''' '''
缓存页面数据 缓存页面数据
@ -70,11 +86,13 @@ class ArticleListView(ListView):
logger.info('get view cache.key:{key}'.format(key=cache_key)) logger.info('get view cache.key:{key}'.format(key=cache_key))
return value return value
else: else:
# bjy: 调用子类实现的get_queryset_data方法获取数据
article_list = self.get_queryset_data() article_list = self.get_queryset_data()
cache.set(cache_key, article_list) cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key)) logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list return article_list
# bjy: 重写父类方法从缓存中获取queryset
def get_queryset(self): def get_queryset(self):
''' '''
重写默认从缓存获取数据 重写默认从缓存获取数据
@ -84,11 +102,13 @@ class ArticleListView(ListView):
value = self.get_queryset_from_cache(key) value = self.get_queryset_from_cache(key)
return value return value
# bjy: 重写父类方法,向上下文中添加链接类型
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs) return super(ArticleListView, self).get_context_data(**kwargs)
# bjy: 首页视图继承自ArticleListView
class IndexView(ArticleListView): class IndexView(ArticleListView):
''' '''
首页 首页
@ -96,15 +116,18 @@ class IndexView(ArticleListView):
# 友情链接类型 # 友情链接类型
link_type = LinkShowType.I link_type = LinkShowType.I
# bjy: 实现父类的抽象方法,获取首页的文章数据
def get_queryset_data(self): def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p') article_list = Article.objects.filter(type='a', status='p')
return article_list return article_list
# bjy: 实现父类的抽象方法,生成首页的缓存键
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number) cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key return cache_key
# bjy: 文章详情页视图
class ArticleDetailView(DetailView): class ArticleDetailView(DetailView):
''' '''
文章详情页面 文章详情页面
@ -114,14 +137,21 @@ class ArticleDetailView(DetailView):
pk_url_kwarg = 'article_id' pk_url_kwarg = 'article_id'
context_object_name = "article" context_object_name = "article"
# bjy: 重写父类方法,向上下文中添加额外的数据
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# bjy: 创建评论表单实例
comment_form = CommentForm() comment_form = CommentForm()
# bjy: 获取文章的所有评论
article_comments = self.object.comment_list() article_comments = self.object.comment_list()
# bjy: 筛选出父评论(顶级评论)
parent_comments = article_comments.filter(parent_comment=None) parent_comments = article_comments.filter(parent_comment=None)
# bjy: 获取博客设置
blog_setting = get_blog_setting() blog_setting = get_blog_setting()
# bjy: 对父评论进行分页
paginator = Paginator(parent_comments, blog_setting.article_comment_count) paginator = Paginator(parent_comments, blog_setting.article_comment_count)
# bjy: 从GET参数中获取评论页码
page = self.request.GET.get('comment_page', '1') page = self.request.GET.get('comment_page', '1')
# bjy: 校验页码是否为有效数字
if not page.isnumeric(): if not page.isnumeric():
page = 1 page = 1
else: else:
@ -131,55 +161,68 @@ class ArticleDetailView(DetailView):
if page > paginator.num_pages: if page > paginator.num_pages:
page = paginator.num_pages page = paginator.num_pages
# bjy: 获取当前页的评论对象
p_comments = paginator.page(page) p_comments = paginator.page(page)
# bjy: 获取下一页和上一页的页码
next_page = p_comments.next_page_number() if p_comments.has_next() else None next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
# bjy: 如果存在下一页则构建下一页的URL
if next_page: if next_page:
kwargs[ kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container' 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
# bjy: 如果存在上一页则构建上一页的URL
if prev_page: if prev_page:
kwargs[ kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container' 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# bjy: 将评论表单和评论数据添加到上下文
kwargs['form'] = comment_form kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len( kwargs['comment_count'] = len(
article_comments) if article_comments else 0 article_comments) if article_comments else 0
# bjy: 添加上一篇和下一篇文章
kwargs['next_article'] = self.object.next_article kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article kwargs['prev_article'] = self.object.prev_article
# bjy: 调用父类方法获取基础上下文
context = super(ArticleDetailView, self).get_context_data(**kwargs) context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object article = self.object
# 触发文章详情加载钩子,让插件可以添加额外的上下文数据 # bjy: 触发文章详情加载钩子,让插件可以添加额外的上下文数据
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request) hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request)
# Action Hook, 通知插件"文章详情已获取" # bjy: Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request) hooks.run_action('after_article_body_get', article=article, request=self.request)
return context return context
# bjy: 分类详情页视图
class CategoryDetailView(ArticleListView): class CategoryDetailView(ArticleListView):
''' '''
分类目录列表 分类目录列表
''' '''
page_type = "分类目录归档" page_type = "分类目录归档"
# bjy: 实现父类的抽象方法,获取分类下的文章数据
def get_queryset_data(self): def get_queryset_data(self):
slug = self.kwargs['category_name'] slug = self.kwargs['category_name']
# bjy: 根据slug获取分类对象如果不存在则返回404
category = get_object_or_404(Category, slug=slug) category = get_object_or_404(Category, slug=slug)
categoryname = category.name categoryname = category.name
self.categoryname = categoryname self.categoryname = categoryname
# bjy: 获取该分类及其所有子分类的名称列表
categorynames = list( categorynames = list(
map(lambda c: c.name, category.get_sub_categorys())) map(lambda c: c.name, category.get_sub_categorys()))
# bjy: 筛选出属于这些分类的所有已发布文章
article_list = Article.objects.filter( article_list = Article.objects.filter(
category__name__in=categorynames, status='p') category__name__in=categorynames, status='p')
return article_list return article_list
# bjy: 实现父类的抽象方法,生成分类页的缓存键
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
slug = self.kwargs['category_name'] slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug) category = get_object_or_404(Category, slug=slug)
@ -189,10 +232,12 @@ class CategoryDetailView(ArticleListView):
categoryname=categoryname, page=self.page_number) categoryname=categoryname, page=self.page_number)
return cache_key return cache_key
# bjy: 重写父类方法,向上下文中添加分类名称
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
categoryname = self.categoryname categoryname = self.categoryname
try: try:
# bjy: 处理多级分类的情况,只取最后一部分
categoryname = categoryname.split('/')[-1] categoryname = categoryname.split('/')[-1]
except BaseException: except BaseException:
pass pass
@ -201,12 +246,14 @@ class CategoryDetailView(ArticleListView):
return super(CategoryDetailView, self).get_context_data(**kwargs) return super(CategoryDetailView, self).get_context_data(**kwargs)
# bjy: 作者详情页视图
class AuthorDetailView(ArticleListView): class AuthorDetailView(ArticleListView):
''' '''
作者详情页 作者详情页
''' '''
page_type = '作者文章归档' page_type = '作者文章归档'
# bjy: 实现父类的抽象方法,生成作者页的缓存键
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
from uuslug import slugify from uuslug import slugify
author_name = slugify(self.kwargs['author_name']) author_name = slugify(self.kwargs['author_name'])
@ -214,12 +261,14 @@ class AuthorDetailView(ArticleListView):
author_name=author_name, page=self.page_number) author_name=author_name, page=self.page_number)
return cache_key return cache_key
# bjy: 实现父类的抽象方法,获取作者的文章数据
def get_queryset_data(self): def get_queryset_data(self):
author_name = self.kwargs['author_name'] author_name = self.kwargs['author_name']
article_list = Article.objects.filter( article_list = Article.objects.filter(
author__username=author_name, type='a', status='p') author__username=author_name, type='a', status='p')
return article_list return article_list
# bjy: 重写父类方法,向上下文中添加作者名称
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name'] author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type kwargs['page_type'] = AuthorDetailView.page_type
@ -227,12 +276,14 @@ class AuthorDetailView(ArticleListView):
return super(AuthorDetailView, self).get_context_data(**kwargs) return super(AuthorDetailView, self).get_context_data(**kwargs)
# bjy: 标签详情页视图
class TagDetailView(ArticleListView): class TagDetailView(ArticleListView):
''' '''
标签列表页面 标签列表页面
''' '''
page_type = '分类标签归档' page_type = '分类标签归档'
# bjy: 实现父类的抽象方法,获取标签下的文章数据
def get_queryset_data(self): def get_queryset_data(self):
slug = self.kwargs['tag_name'] slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug) tag = get_object_or_404(Tag, slug=slug)
@ -242,6 +293,7 @@ class TagDetailView(ArticleListView):
tags__name=tag_name, type='a', status='p') tags__name=tag_name, type='a', status='p')
return article_list return article_list
# bjy: 实现父类的抽象方法,生成标签页的缓存键
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
slug = self.kwargs['tag_name'] slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug) tag = get_object_or_404(Tag, slug=slug)
@ -251,6 +303,7 @@ class TagDetailView(ArticleListView):
tag_name=tag_name, page=self.page_number) tag_name=tag_name, page=self.page_number)
return cache_key return cache_key
# bjy: 重写父类方法,向上下文中添加标签名称
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name'] # tag_name = self.kwargs['tag_name']
tag_name = self.name tag_name = self.name
@ -259,32 +312,40 @@ class TagDetailView(ArticleListView):
return super(TagDetailView, self).get_context_data(**kwargs) return super(TagDetailView, self).get_context_data(**kwargs)
# bjy: 文章归档页视图
class ArchivesView(ArticleListView): class ArchivesView(ArticleListView):
''' '''
文章归档页面 文章归档页面
''' '''
page_type = '文章归档' page_type = '文章归档'
# bjy: 归档页不分页
paginate_by = None paginate_by = None
page_kwarg = None page_kwarg = None
template_name = 'blog/article_archives.html' template_name = 'blog/article_archives.html'
# bjy: 实现父类的抽象方法,获取所有已发布文章
def get_queryset_data(self): def get_queryset_data(self):
return Article.objects.filter(status='p').all() return Article.objects.filter(status='p').all()
# bjy: 实现父类的抽象方法,生成归档页的缓存键
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
cache_key = 'archives' cache_key = 'archives'
return cache_key return cache_key
# bjy: 友情链接页视图
class LinkListView(ListView): class LinkListView(ListView):
model = Links model = Links
template_name = 'blog/links_list.html' template_name = 'blog/links_list.html'
# bjy: 重写queryset只获取已启用的链接
def get_queryset(self): def get_queryset(self):
return Links.objects.filter(is_enable=True) return Links.objects.filter(is_enable=True)
# bjy: 自定义的Elasticsearch搜索视图
class EsSearchView(SearchView): class EsSearchView(SearchView):
# bjy: 重写get_context方法自定义搜索结果的上下文
def get_context(self): def get_context(self):
paginator, page = self.build_page() paginator, page = self.build_page()
context = { context = {
@ -294,6 +355,7 @@ class EsSearchView(SearchView):
"paginator": paginator, "paginator": paginator,
"suggestion": None, "suggestion": None,
} }
# bjy: 如果启用了拼写建议,则添加到上下文
if hasattr(self.results, "query") and self.results.query.backend.include_spelling: if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion() context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context()) context.update(self.extra_context())
@ -301,6 +363,44 @@ class EsSearchView(SearchView):
return context return context
class LikeArticle(View):
"""
处理文章点赞和取消点赞
"""
@method_decorator(login_required) # 确保只有登录用户才能点赞
def post(self, request):
try:
user = request.user
article_id = request.POST.get('article_id') # 获取文章ID
article = Article.objects.get(id=article_id) # 获取文章对象
# 检查当前用户是否已经为这篇文章点过赞
if article.users_like.filter(id=user.id).exists():
# 如果点过赞,则取消点赞 (从多对多关系中移除)
article.users_like.remove(user)
action_type = 0 # 0代表取消点赞
else:
# 如果没点过赞,则添加点赞 (添加到多对多关系)
article.users_like.add(user)
action_type = 1 # 1代表点赞
# 获取更新后的点赞总数
like_count = article.users_like.count()
# 返回JSON数据给前端
return JsonResponse({
'state': 200,
'type': action_type,
'like_sum': like_count
})
except Article.DoesNotExist:
return JsonResponse({'state': 400, 'data': '文章不存在'})
except Exception as e:
return JsonResponse({'state': 500, 'data': f'服务器错误: {e}'})
# bjy: 文件上传视图使用csrf_exempt豁免CSRF验证
@csrf_exempt @csrf_exempt
def fileupload(request): def fileupload(request):
""" """
@ -309,38 +409,52 @@ def fileupload(request):
:return: :return:
""" """
if request.method == 'POST': if request.method == 'POST':
# bjy: 从GET参数中获取签名
sign = request.GET.get('sign', None) sign = request.GET.get('sign', None)
if not sign: if not sign:
return HttpResponseForbidden() return HttpResponseForbidden()
# bjy: 验证签名是否正确
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden() return HttpResponseForbidden()
response = [] response = []
# bjy: 遍历所有上传的文件
for filename in request.FILES: for filename in request.FILES:
# bjy: 按年/月/日创建目录
timestr = timezone.now().strftime('%Y/%m/%d') timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename)) fname = u''.join(str(filename))
# bjy: 判断文件是否为图片
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr) base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
# bjy: 如果目录不存在则创建
if not os.path.exists(base_dir): if not os.path.exists(base_dir):
os.makedirs(base_dir) os.makedirs(base_dir)
# bjy: 生成唯一的文件名并拼接保存路径
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}")) savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# bjy: 安全检查,防止路径遍历攻击
if not savepath.startswith(base_dir): if not savepath.startswith(base_dir):
return HttpResponse("only for post") return HttpResponse("only for post")
# bjy: 将文件内容写入磁盘
with open(savepath, 'wb+') as wfile: with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks(): for chunk in request.FILES[filename].chunks():
wfile.write(chunk) wfile.write(chunk)
# bjy: 如果是图片,则进行压缩优化
if isimage: if isimage:
from PIL import Image from PIL import Image
image = Image.open(savepath) image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True) image.save(savepath, quality=20, optimize=True)
# bjy: 生成文件的静态URL
url = static(savepath) url = static(savepath)
response.append(url) response.append(url)
# bjy: 返回包含所有文件URL的响应
return HttpResponse(response) return HttpResponse(response)
else: else:
# bjy: 非POST请求返回错误信息
return HttpResponse("only for post") return HttpResponse("only for post")
# bjy: 自定义404错误处理视图
def page_not_found_view( def page_not_found_view(
request, request,
exception, exception,
@ -355,6 +469,7 @@ def page_not_found_view(
status=404) status=404)
# bjy: 自定义500服务器错误处理视图
def server_error_view(request, template_name='blog/error_page.html'): def server_error_view(request, template_name='blog/error_page.html'):
return render(request, return render(request,
template_name, template_name,
@ -363,6 +478,7 @@ def server_error_view(request, template_name='blog/error_page.html'):
status=500) status=500)
# bjy: 自定义403权限拒绝错误处理视图
def permission_denied_view( def permission_denied_view(
request, request,
exception, exception,
@ -375,6 +491,7 @@ def permission_denied_view(
'statuscode': '403'}, status=403) 'statuscode': '403'}, status=403)
# bjy: 清除缓存的视图
def clean_cache_view(request): def clean_cache_view(request):
cache.clear() cache.clear()
return HttpResponse('ok') return HttpResponse('ok')

@ -1,49 +1,81 @@
# zy: 评论管理模块 - 配置Django后台管理中评论模型的显示和操作功能
from django.contrib import admin from django.contrib import admin
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# zy: 禁用评论状态 - 管理员动作函数,用于批量禁用评论
def disable_commentstatus(modeladmin, request, queryset): def disable_commentstatus(modeladmin, request, queryset):
# zy: 将选中的评论集的is_enable字段更新为False禁用
queryset.update(is_enable=False) queryset.update(is_enable=False)
# zy: 启用评论状态 - 管理员动作函数,用于批量启用评论
def enable_commentstatus(modeladmin, request, queryset): def enable_commentstatus(modeladmin, request, queryset):
# zy: 将选中的评论集的is_enable字段更新为True启用
queryset.update(is_enable=True) queryset.update(is_enable=True)
# zy: 设置动作在后台显示的描述文字(支持国际化)
disable_commentstatus.short_description = _('Disable comments') disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments') enable_commentstatus.short_description = _('Enable comments')
# zy: 评论管理类 - 自定义Django后台的评论管理界面
class CommentAdmin(admin.ModelAdmin): class CommentAdmin(admin.ModelAdmin):
# zy: 设置列表页每页显示20条评论
list_per_page = 20 list_per_page = 20
# zy: 定义列表页显示的字段列
list_display = ( list_display = (
'id', 'id', # zy: 评论ID
'body', 'body', # zy: 评论内容
'link_to_userinfo', 'link_to_userinfo', # zy: 自定义方法 - 用户信息链接
'link_to_article', 'link_to_article', # zy: 自定义方法 - 文章链接
'is_enable', 'is_enable', # zy: 是否启用状态
'creation_time') 'creation_time' # zy: 创建时间
)
# zy: 设置哪些字段可以作为链接点击进入编辑页
list_display_links = ('id', 'body', 'is_enable') list_display_links = ('id', 'body', 'is_enable')
list_filter = ('is_enable',)
# zy: 设置右侧过滤侧边栏的过滤条件
list_filter = ('is_enable',) # zy: 按启用状态过滤
# zy: 排除在编辑表单中显示的字段(系统自动管理的字段)
exclude = ('creation_time', 'last_modify_time') exclude = ('creation_time', 'last_modify_time')
# zy: 定义批量操作动作列表
actions = [disable_commentstatus, enable_commentstatus] actions = [disable_commentstatus, enable_commentstatus]
# zy: 设置使用弹出窗口选择关联对象的字段(优化性能)
raw_id_fields = ('author', 'article') raw_id_fields = ('author', 'article')
search_fields = ('body',)
# zy: 设置可搜索的字段
search_fields = ('body',) # zy: 按评论内容搜索
# zy: 自定义方法 - 生成用户信息链接
def link_to_userinfo(self, obj): def link_to_userinfo(self, obj):
# zy: 获取作者模型的app名称和模型名称
info = (obj.author._meta.app_label, obj.author._meta.model_name) info = (obj.author._meta.app_label, obj.author._meta.model_name)
# zy: 生成作者编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# zy: 返回HTML链接显示用户昵称或邮箱
return format_html( return format_html(
u'<a href="%s">%s</a>' % u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email)) (link, obj.author.nickname if obj.author.nickname else obj.author.email))
# zy: 自定义方法 - 生成文章链接
def link_to_article(self, obj): def link_to_article(self, obj):
# zy: 获取文章模型的app名称和模型名称
info = (obj.article._meta.app_label, obj.article._meta.model_name) info = (obj.article._meta.app_label, obj.article._meta.model_name)
# zy: 生成文章编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# zy: 返回HTML链接显示文章标题
return format_html( return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title)) u'<a href="%s">%s</a>' % (link, obj.article.title))
# zy: 设置自定义方法在列表页显示的列标题(支持国际化)
link_to_userinfo.short_description = _('User') link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article') link_to_article.short_description = _('Article')

@ -1,5 +1,8 @@
# zy: 评论应用配置模块 - 定义comments应用的配置信息
from django.apps import AppConfig from django.apps import AppConfig
# zy: 评论应用配置类 - 继承自Django的AppConfig基类
class CommentsConfig(AppConfig): class CommentsConfig(AppConfig):
# zy: 应用名称 - 指定Django内部使用的应用标识
# 这个名称应该与INSTALLED_APPS中的名称一致
name = 'comments' name = 'comments'

@ -1,13 +1,17 @@
# zy: 评论表单模块 - 定义评论相关的表单类和验证逻辑
from django import forms from django import forms
from django.forms import ModelForm from django.forms import ModelForm
# zy: 导入评论模型,用于创建基于模型的自定义表单
from .models import Comment from .models import Comment
# zy: 评论表单类 - 继承自ModelForm用于处理评论的创建和验证
class CommentForm(ModelForm): class CommentForm(ModelForm):
# zy: 父评论ID字段 - 用于实现评论回复功能
# 这是一个隐藏字段,不在页面显示,用于记录回复的父评论
parent_comment_id = forms.IntegerField( parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False) widget=forms.HiddenInput, required=False)
# zy: 表单元数据配置类 - 定义表单与模型的关联关系
class Meta: class Meta:
model = Comment model = Comment
fields = ['body'] fields = ['body']

@ -1,38 +1,44 @@
# zy: 评论数据模型模块 - 定义评论的数据结构和数据库表结构
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# zy: 导入博客文章模型,用于建立评论与文章的关联
from blog.models import Article from blog.models import Article
# Create your models here.
# zy: 评论数据模型类 - 继承自Django的Model基类对应数据库中的评论表
class Comment(models.Model): class Comment(models.Model):
body = models.TextField('正文', max_length=300) body = models.TextField('正文', max_length=300)
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# zy: 作者外键字段 - 关联到用户模型,删除用户时级联删除其评论
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
on_delete=models.CASCADE) on_delete=models.CASCADE)
# zy: 文章外键字段 - 关联到博客文章模型,删除文章时级联删除相关评论
article = models.ForeignKey( article = models.ForeignKey(
Article, Article,
verbose_name=_('article'), verbose_name=_('article'),
on_delete=models.CASCADE) on_delete=models.CASCADE)
# zy: 父评论自关联字段 - 实现评论回复功能,允许评论有父评论
parent_comment = models.ForeignKey( parent_comment = models.ForeignKey(
'self', 'self',
verbose_name=_('parent comment'), verbose_name=_('parent comment'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE)
# zy: 启用状态字段 - 控制评论是否显示,用于评论审核机制
is_enable = models.BooleanField(_('enable'), is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False) default=False, blank=False, null=False)
# zy: 模型元数据配置类 - 定义模型的数据库和行为配置
class Meta: class Meta:
ordering = ['-id'] ordering = ['-id'] # zy: 默认排序规则 - 按ID降序排列新的评论显示在前面
verbose_name = _('comment') verbose_name = _('comment') # zy: 模型在Admin中的单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # zy: 模型在Admin中的复数显示名称
get_latest_by = 'id' get_latest_by = 'id'
def __str__(self): def __str__(self):

@ -1,6 +1,7 @@
# zy: 评论模块测试文件 - 测试评论功能的各项业务逻辑
from django.test import Client, RequestFactory, TransactionTestCase from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse from django.urls import reverse
# zy: 导入相关模型类,用于测试数据准备
from accounts.models import BlogUser from accounts.models import BlogUser
from blog.models import Category, Article from blog.models import Category, Article
from comments.models import Comment from comments.models import Comment
@ -8,102 +9,137 @@ from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid from djangoblog.utils import get_max_articleid_commentid
# Create your tests here.
# zy: 评论功能测试类 - 测试评论的创建、验证、回复等功能
class CommentsTest(TransactionTestCase): class CommentsTest(TransactionTestCase):
# zy: 测试初始化方法 - 在每个测试方法执行前运行,准备测试数据
def setUp(self): def setUp(self):
self.client = Client() self.client = Client() # zy: 创建测试客户端用于模拟HTTP请求
self.factory = RequestFactory() self.factory = RequestFactory() # zy: 创建请求工厂,用于构建请求对象
# zy: 配置博客设置,设置评论需要审核
from blog.models import BlogSettings from blog.models import BlogSettings
value = BlogSettings() value = BlogSettings()
value.comment_need_review = True value.comment_need_review = True
value.save() value.save()
# zy: 创建测试超级用户,用于登录和权限测试
self.user = BlogUser.objects.create_superuser( self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", email="liangliangyy1@gmail.com",
username="liangliangyy1", username="liangliangyy1",
password="liangliangyy1") password="liangliangyy1")
def update_article_comment_status(self, article): # zy: 辅助方法 - 更新文章评论的审核状态为已通过
@staticmethod
def update_article_comment_status(article):
comments = article.comment_set.all() comments = article.comment_set.all()
for comment in comments: for comment in comments:
comment.is_enable = True comment.is_enable = True
comment.save() comment.save()
# zy: 测试评论验证功能 - 测试评论的创建、回复和显示
def test_validate_comment(self): def test_validate_comment(self):
self.client.login(username='liangliangyy1', password='liangliangyy1') self.client.login(username='liangliangyy1', password='liangliangyy1')
# zy: 创建测试分类
category = Category() category = Category()
category.name = "categoryccc" category.name = "categoryccc"
category.save() category.save()
# zy: 创建测试文章
article = Article() article = Article()
article.title = "nicetitleccc" article.title = "nicetitleccc"
article.body = "nicecontentccc" article.body = "nicecontentccc"
article.author = self.user article.author = self.user # zy: 设置文章作者为测试用户
article.category = category article.category = category # zy: 设置文章分类
article.type = 'a' article.type = 'a' # zy: 设置文章类型
article.status = 'p' article.status = 'p' # zy: 设置文章状态为发布
article.save() article.save()
# zy: 获取发表评论的URL地址
comment_url = reverse( comment_url = reverse(
'comments:postcomment', kwargs={ 'comments:postcomment', kwargs={
'article_id': article.id}) 'article_id': article.id})
# zy: 测试1: 发表第一条评论
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
'body': '123ffffffffff' 'body': '123ffffffffff' # zy: 评论内容
}) })
# zy: 断言响应状态码为302重定向表示评论提交成功
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
# zy: 重新从数据库获取文章对象,确保获取最新数据
article = Article.objects.get(pk=article.pk) article = Article.objects.get(pk=article.pk)
# zy: 断言评论列表为空(因为评论需要审核,默认不显示)
self.assertEqual(len(article.comment_list()), 0) self.assertEqual(len(article.comment_list()), 0)
# zy: 更新评论状态为已审核通过
self.update_article_comment_status(article) self.update_article_comment_status(article)
# zy: 断言现在评论列表中有1条评论
self.assertEqual(len(article.comment_list()), 1) self.assertEqual(len(article.comment_list()), 1)
# zy: 测试2: 发表第二条评论
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
'body': '123ffffffffff', 'body': '123ffffffffff', # zy: 第二条评论内容
}) })
# zy: 断言响应状态码为302重定向
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
# zy: 重新获取文章并更新评论状态
article = Article.objects.get(pk=article.pk) article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article) self.update_article_comment_status(article)
# zy: 断言现在有2条评论
self.assertEqual(len(article.comment_list()), 2) self.assertEqual(len(article.comment_list()), 2)
# zy: 获取第一条评论的ID用于回复测试
parent_comment_id = article.comment_list()[0].id parent_comment_id = article.comment_list()[0].id
# zy: 测试3: 发表带格式的回复评论
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
'body': ''' 'body': '''
# Title1 # Title1
```python ```python
import os import os
``` ```
[url](https://www.lylinux.net/) [url](https://www.lylinux.net/)
[ddd](http://www.baidu.com) [ddd](http://www.baidu.com)
''', ''', # zy: 测试包含Markdown格式的评论内容
'parent_comment_id': parent_comment_id 'parent_comment_id': parent_comment_id # zy: 设置父评论ID表示这是回复
}) })
# zy: 断言响应状态码为302重定向
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
# zy: 更新评论状态并重新获取文章
self.update_article_comment_status(article) self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk) article = Article.objects.get(pk=article.pk)
# zy: 断言现在有3条评论2条顶级评论 + 1条回复
self.assertEqual(len(article.comment_list()), 3) self.assertEqual(len(article.comment_list()), 3)
# zy: 测试评论树解析功能
comment = Comment.objects.get(id=parent_comment_id) comment = Comment.objects.get(id=parent_comment_id)
# zy: 解析评论树结构,检查回复关系
tree = parse_commenttree(article.comment_list(), comment) tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1) self.assertEqual(len(tree), 1) # zy: 断言评论树解析正确
# zy: 测试评论项显示功能
data = show_comment_item(comment, True) data = show_comment_item(comment, True)
self.assertIsNotNone(data) self.assertIsNotNone(data) # zy: 断言评论项数据不为空
# zy: 测试获取最大文章ID和评论ID的工具函数
s = get_max_articleid_commentid() s = get_max_articleid_commentid()
self.assertIsNotNone(s) self.assertIsNotNone(s) # zy: 断言返回值不为空
# zy: 测试评论邮件发送功能
from comments.utils import send_comment_email from comments.utils import send_comment_email
send_comment_email(comment) send_comment_email(comment) # zy: 发送评论通知邮件

@ -1,11 +1,17 @@
# zy: 评论模块URL配置 - 定义评论相关的URL路由和视图映射
from django.urls import path from django.urls import path
# zy: 导入评论视图模块,包含评论相关的视图类
from . import views from . import views
# zy: 定义应用命名空间 - 用于URL反向解析时避免命名冲突
app_name = "comments" app_name = "comments"
# zy: URL模式列表 - 定义评论模块的所有URL路由规则
urlpatterns = [ urlpatterns = [
# zy: 发表评论URL路由 - 处理用户发表评论的请求
path( path(
'article/<int:article_id>/postcomment', 'article/<int:article_id>/postcomment', # zy: URL模式包含文章ID参数
views.CommentPostView.as_view(), views.CommentPostView.as_view(), # zy: 关联的基于类的视图
name='postcomment'), name='postcomment'), # zy: URL名称用于反向解析
] ]

@ -1,13 +1,14 @@
import logging import logging# zy: 评论邮件通知模块 - 处理评论相关的邮件发送功能
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site# zy: 导入项目工具函数,用于获取站点信息和发送邮件
from djangoblog.utils import send_email from djangoblog.utils import send_email
# zy: 创建日志记录器,用于记录邮件发送过程中的错误
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# zy: 发送评论邮件函数 - 处理评论发表和回复的邮件通知
def send_comment_email(comment): def send_comment_email(comment):
site = get_current_site().domain site = get_current_site().domain
subject = _('Thanks for your comment') subject = _('Thanks for your comment')

@ -1,4 +1,4 @@
# Create your views here. # zy: 评论视图模块 - 处理评论相关的视图逻辑和请求处理
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@ -6,26 +6,30 @@ from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
# zy: 导入相关模型类
from accounts.models import BlogUser from accounts.models import BlogUser
from blog.models import Article from blog.models import Article
from .forms import CommentForm from .forms import CommentForm
from .models import Comment from .models import Comment
# zy: 评论发表视图类 - 基于FormView处理评论提交
class CommentPostView(FormView): class CommentPostView(FormView):
form_class = CommentForm form_class = CommentForm
template_name = 'blog/article_detail.html' template_name = 'blog/article_detail.html'
# zy: 添加CSRF保护装饰器防止跨站请求伪造攻击
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs) return super(CommentPostView, self).dispatch(*args, **kwargs)
# zy: 处理GET请求 - 当用户直接访问评论URL时重定向到文章页面
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url() url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments") return HttpResponseRedirect(url + "#comments")
# zy: 表单验证失败时的处理逻辑
def form_invalid(self, form): def form_invalid(self, form):
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
@ -35,6 +39,7 @@ class CommentPostView(FormView):
'article': article 'article': article
}) })
# zy: 表单验证成功时的处理逻辑 - 保存评论数据
def form_valid(self, form): def form_valid(self, form):
"""提交的数据验证合法后的逻辑""" """提交的数据验证合法后的逻辑"""
user = self.request.user user = self.request.user
@ -42,6 +47,7 @@ class CommentPostView(FormView):
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
# zy: 检查文章是否允许评论
if article.comment_status == 'c' or article.status == 'c': if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.") raise ValidationError("该文章评论已关闭.")
comment = form.save(False) comment = form.save(False)
@ -52,6 +58,7 @@ class CommentPostView(FormView):
comment.is_enable = True comment.is_enable = True
comment.author = author comment.author = author
# zy: 处理评论回复逻辑
if form.cleaned_data['parent_comment_id']: if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get( parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']) pk=form.cleaned_data['parent_comment_id'])

@ -1,3 +1,31 @@
"""`xjj`
DjangoBlog 管理站点配置
========
该模块定义了 DjangoBlog 项目的自定义管理站点配置它继承自 Django 的默认 AdminSite
并注册了项目中所有需要在管理后台进行管理的模型
主要功能
~~~~~~~~
- 创建自定义管理站点
- 设置管理站点的标题和头部显示文本
- 限制只有超级用户才能访问管理站点
- 注册各个应用中的模型到管理站点
注册的模型包括
~~~~~~~~
- 博客相关文章分类标签链接侧边栏博客设置
- 用户相关博客用户
- 评论相关评论
- OAuth 相关 OAuth 用户 OAuth 配置
- 其他命令邮件发送日志位置跟踪日志站点信息管理日志
使用方式
~~~~~~~~
通过 admin_site 实例来访问自定义的管理站点
"""
from django.contrib.admin import AdminSite from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin from django.contrib.sites.admin import SiteAdmin
@ -18,13 +46,38 @@ from servermanager.models import *
class DjangoBlogAdminSite(AdminSite): class DjangoBlogAdminSite(AdminSite):
"""`xjj`
自定义 Django 管理站点类
继承自 Django 默认的 AdminSite 用于为 DjangoBlog 项目提供定制化的管理后台
"""
site_header = 'djangoblog administration' site_header = 'djangoblog administration'
site_title = 'djangoblog site admin' site_title = 'djangoblog site admin'
def __init__(self, name='admin'): def __init__(self, name='admin'):
"""`xjj`
初始化管理站点
Args:
name (str): 管理站点的名称默认为 'admin'
"""
super().__init__(name) super().__init__(name)
def has_permission(self, request): def has_permission(self, request):
"""`xjj`
检查用户是否有访问管理站点的权限
重写了父类的方法只允许超级用户访问管理站点
Args:
request: HTTP 请求对象
Returns:
bool: 如果用户是超级用户则返回 **True**否则返回 **False**
"""
return request.user.is_superuser return request.user.is_superuser
# def get_urls(self): # def get_urls(self):

@ -1,10 +1,46 @@
"""`xjj`
Django 应用配置模块
========
该模块定义了 djangoblog 应用的配置类负责应用的初始化设置和插件加载
Django 项目启动时会自动调用配置类中的 ``ready()`` 方法来完成必要的初始化工作
包括加载自定义插件等操作
配置项说明
~~~~~~~~
- ``default_auto_field``: 设置默认的主键字段类型为 ``BigAutoField``
- ``name``: 指定应用名称为 'djangoblog'
初始化逻辑
~~~~~~~~
- ``ready()`` 方法中导入并执行插件加载函数 ``load_plugins()``
"""
from django.apps import AppConfig from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig): class DjangoblogAppConfig(AppConfig):
"""`xjj`
Django 博客应用配置类
该类继承自 Django AppConfig 用于配置 jangoblog 应用的基本信息和初始化操作
主要负责设置默认的自动字段类型和应用名称并在应用准备就绪时加载插件
"""
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog' name = 'djangoblog'
def ready(self): def ready(self):
"""`xjj`
应用准备就绪时的回调方法
Django 应用完全加载后调用此方法主要用于执行应用级别的初始化操作
此方法会导入并加载所有已注册的插件
Returns:
None
"""
super().ready() super().ready()
# Import and load plugins here # Import and load plugins here
from .plugin_manage.loader import load_plugins from .plugin_manage.loader import load_plugins

@ -1,3 +1,24 @@
"""`xjj`
博客信号处理模块
========
该模块负责处理 Django 博客应用中的各种信号包括
- 邮件发送信号处理
- OAuth 用户登录信号处理
- 模型保存后的回调处理
- 用户登录 / 登出事件处理
主要功能
~~~~~~~~
1. ``send_email_signal_handler``: 处理发送邮件信号支持 HTML 格式邮件发送并记录发送日志
2. ``oauth_user_login_signal_handler``: 处理 OAuth 用户登录包括用户头像保存等操作
3. ``model_post_save_callback``: 模型保存后的通用回调处理包括搜索引擎通知缓存清理等
4. ``user_auth_callback``: 用户认证状态变更处理主要负责清理侧边栏缓存
该模块通过 Django 信号机制实现解耦确保各功能模块的独立性
"""
import _thread import _thread
import logging import logging
@ -25,6 +46,18 @@ send_email_signal = django.dispatch.Signal(
@receiver(send_email_signal) @receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs): def send_email_signal_handler(sender, **kwargs):
"""`xjj`
发送邮件信号的处理函数
Args:
sender: 信号发送者
**kwargs: 包含邮件相关信息的字典参数
- emailto (list): 收件人邮箱列表
- title (str): 邮件标题
- content (str): 邮件内容
"""
emailto = kwargs['emailto'] emailto = kwargs['emailto']
title = kwargs['title'] title = kwargs['title']
content = kwargs['content'] content = kwargs['content']
@ -53,6 +86,16 @@ def send_email_signal_handler(sender, **kwargs):
@receiver(oauth_user_login_signal) @receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs): def oauth_user_login_signal_handler(sender, **kwargs):
"""`xjj`
OAuth 用户登录信号处理函数
OAuth 用户登录时该函数会被触发执行相关处理逻辑包括更新用户头像和清除侧边栏缓存
Args:
sender: 信号发送者
**kwargs: 包含信号传递的额外参数必须包含 ``id`` 键表示 OAuth 用户的 ID
"""
id = kwargs['id'] id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id) oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain site = get_current_site().domain
@ -73,6 +116,19 @@ def model_post_save_callback(
using, using,
update_fields, update_fields,
**kwargs): **kwargs):
"""`xjj`
模型保存后的回调函数用于处理缓存清理和搜索引擎通知等操作
Args:
sender: 发送信号的模型类
instance: 保存的模型实例
created: 布尔值指示实例是新建还是更新
raw: 布尔值指示实例是否从序列化数据加载
using: 数据库别名
update_fields: 更新字段集合如果为 None 表示所有字段都被更新
**kwargs: 其他关键字参数
"""
clearcache = False clearcache = False
if isinstance(instance, LogEntry): if isinstance(instance, LogEntry):
return return

@ -1,3 +1,24 @@
"""`xjj`
用于 Django 博客搜索的 Elasticsearch 后端
========
该模块实现了基于 Elasticsearch 的搜索后端用于博客文章的全文搜索功能
主要包含以下组件
- ``ElasticSearchBackend``: 核心搜索后端实现处理文档的创建更新删除和搜索操作
- ``ElasticSearchQuery``: 搜索查询处理类负责构建和执行搜索查询
- ``ElasticSearchModelSearchForm``: 搜索表单类处理用户搜索输入和选项
- ``ElasticSearchEngine``: 搜索引擎类整合后端和查询组件
功能特性
~~~~~~~~
- 支持文章内容和标题的全文搜索
- 提供搜索建议和拼写纠正功能
- 支持搜索结果分页和过滤
- 集成 Haystack 搜索框架
"""
from django.utils.encoding import force_str from django.utils.encoding import force_str
from elasticsearch_dsl import Q from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
@ -12,6 +33,14 @@ logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend): class ElasticSearchBackend(BaseSearchBackend):
"""`xjj`
ElasticSearch 搜索后端实现类用于与 Elasticsearch 进行交互提供搜索更新删除等操作
:param connection_alias: 连接别名用于标识不同的搜索引擎连接
:param connection_options: 其他连接选项以关键字参数形式传入
"""
def __init__(self, connection_alias, **connection_options): def __init__(self, connection_alias, **connection_options):
super( super(
ElasticSearchBackend, ElasticSearchBackend,
@ -22,35 +51,81 @@ class ElasticSearchBackend(BaseSearchBackend):
self.include_spelling = True self.include_spelling = True
def _get_models(self, iterable): def _get_models(self, iterable):
"""`xjj`
获取模型对象并转换为文档格式
:param iterable: 可迭代的模型对象列表若为空则默认获取所有 Article 对象
:return: 转换后的文档对象列表
"""
models = iterable if iterable and iterable[0] else Article.objects.all() models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models) docs = self.manager.convert_to_doc(models)
return docs return docs
def _create(self, models): def _create(self, models):
"""`xjj`
创建索引并将模型数据重建到 Elasticsearch
:param models: 模型对象列表
"""
self.manager.create_index() self.manager.create_index()
docs = self._get_models(models) docs = self._get_models(models)
self.manager.rebuild(docs) self.manager.rebuild(docs)
def _delete(self, models): def _delete(self, models):
"""`xjj`
删除指定模型对象
:param models: 待删除的模型对象列表
:return: 始终返回 True
"""
for m in models: for m in models:
m.delete() m.delete()
return True return True
def _rebuild(self, models): def _rebuild(self, models):
"""`xjj`
重建指定模型的索引
:param models: 模型对象列表如果为空则默认获取所有 Article 对象
"""
models = models if models else Article.objects.all() models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models) docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs) self.manager.update_docs(docs)
def update(self, index, iterable, commit=True): def update(self, index, iterable, commit=True):
"""`xjj`
更新索引中的文档
:param index: 索引名称
:param iterable: 待更新的模型对象可迭代对象
:param commit: 是否提交更改
"""
models = self._get_models(iterable) models = self._get_models(iterable)
self.manager.update_docs(models) self.manager.update_docs(models)
def remove(self, obj_or_string): def remove(self, obj_or_string):
"""`xjj`
从索引中移除指定对象
:param obj_or_string: 待移除的对象或字符串标识
"""
models = self._get_models([obj_or_string]) models = self._get_models([obj_or_string])
self._delete(models) self._delete(models)
def clear(self, models=None, commit=True): def clear(self, models=None, commit=True):
"""`xjj`
清空索引中的内容
:param models: 待清除的模型对象未使用
:param commit: 是否提交更改未使用
"""
self.remove(None) self.remove(None)
@staticmethod @staticmethod
@ -73,6 +148,14 @@ class ElasticSearchBackend(BaseSearchBackend):
@log_query @log_query
def search(self, query_string, **kwargs): def search(self, query_string, **kwargs):
"""`xjj`
执行搜索操作支持推荐词搜索和分页
:param query_string: 查询关键词字符串
:param kwargs: 其他参数包括 start_offset end_offset 用于分页
:return: 包含搜索结果命中数 facet 信息及拼写建议的字典
"""
logger.info('search query_string:' + query_string) logger.info('search query_string:' + query_string)
start_offset = kwargs.get('start_offset') start_offset = kwargs.get('start_offset')
@ -124,6 +207,16 @@ class ElasticSearchBackend(BaseSearchBackend):
class ElasticSearchQuery(BaseSearchQuery): class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date): def _convert_datetime(self, date):
"""`xjj`
将日期时间对象转换为字符串格式
Args:
date: 日期时间对象
Returns:
str: 格式化后的日期时间字符串包含小时分钟秒或默认为 000000
"""
if hasattr(date, 'hour'): if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S')) return force_str(date.strftime('%Y%m%d%H%M%S'))
else: else:
@ -155,23 +248,79 @@ class ElasticSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words) return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value): def build_query_fragment(self, field, filter_type, value):
"""`xjj`
构建查询片段
Args:
field: 查询字段
filter_type: 过滤器类型
value: 查询值对象
Returns:
str: 查询字符串
"""
return value.query_string return value.query_string
def get_count(self): def get_count(self):
"""`xjj`
获取查询结果的数量
Returns:
int: 结果数量如果没有结果则返回0
"""
results = self.get_results() results = self.get_results()
return len(results) if results else 0 return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None): def get_spelling_suggestion(self, preferred_query=None):
"""`xjj`
获取拼写建议
Args:
preferred_query (str, optional): 首选查询字符串
Returns:
str: 拼写建议
"""
return self._spelling_suggestion return self._spelling_suggestion
def build_params(self, spelling_query=None): def build_params(self, spelling_query=None):
"""`xjj`
构建查询参数
Args:
spelling_query (str, optional): 拼写查询字符串
Returns:
dict: 构建的查询参数字典
"""
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm): class ElasticSearchModelSearchForm(ModelSearchForm):
"""`xjj`
ElasticSearch 模型搜索表单类
该类继承自 ``ModelSearchForm`` 用于处理 ElasticSearch 相关的搜索功能
特别是支持建议搜索功能的配置
"""
def search(self): def search(self):
"""`xjj`
执行搜索操作
该方法首先根据表单数据配置是否启用建议搜索功能
然后调用父类的 ``search`` 方法执行实际的搜索操作
Returns:
SearchQuerySet: 搜索结果查询集
"""
# 是否建议搜索 # 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search() sqs = super().search()
@ -179,5 +328,16 @@ class ElasticSearchModelSearchForm(ModelSearchForm):
class ElasticSearchEngine(BaseEngine): class ElasticSearchEngine(BaseEngine):
"""`xjj`
ElasticSearch 搜索引擎引擎类
该类继承自 ``BaseEngine`` 用于实现 ElasticSearch 搜索引擎的核心功能
包含后端存储和查询处理的配置
Attributes:
backend: ElasticSearch 后端存储类用于数据的存储和检索
query: ElasticSearch 查询类用于构建和执行搜索查询
"""
backend = ElasticSearchBackend backend = ElasticSearchBackend
query = ElasticSearchQuery query = ElasticSearchQuery

@ -1,3 +1,18 @@
"""`xjj`
DjangoBlog RSS 订阅模块
========
该模块实现了博客文章的 RSS 订阅功能基于 Django syndication 框架
提供文章标题内容发布链接等信息的 RSS 输出方便读者订阅博客更新
主要功能
~~~~~~~~
- 生成 RSS 2.0 格式的订阅源
- 提供最新的 5 篇已发布文章信息
- 支持文章标题内容作者版权等元数据展示
"""
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.utils import timezone from django.utils import timezone
@ -8,6 +23,13 @@ from djangoblog.utils import CommonMarkdown
class DjangoBlogFeed(Feed): class DjangoBlogFeed(Feed):
"""`xjj`
Django 博客 RSS 订阅源类
该类继承自 Django ``Feed`` 用于生成博客文章的 RSS 订阅源
提供博客文章的 RSS feed 功能包含文章标题描述作者信息等
"""
feed_type = Rss201rev2Feed feed_type = Rss201rev2Feed
description = '大巧无工,重剑无锋.' description = '大巧无工,重剑无锋.'
@ -15,25 +37,83 @@ class DjangoBlogFeed(Feed):
link = "/feed/" link = "/feed/"
def author_name(self): def author_name(self):
"""`xjj`
获取 RSS 订阅源的作者名称
Returns:
str: 博客第一个用户的昵称如果不存在则返回 None
"""
return get_user_model().objects.first().nickname return get_user_model().objects.first().nickname
def author_link(self): def author_link(self):
"""`xjj`
获取 RSS 订阅源作者的链接地址
Returns:
str: 博客第一个用户的绝对 URL 地址
"""
return get_user_model().objects.first().get_absolute_url() return get_user_model().objects.first().get_absolute_url()
def items(self): def items(self):
"""`xjj`
获取 RSS 订阅源的文章列表
Returns:
QuerySet: 按发布时间倒序排列的前 5 篇已发布文章
"""
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item): def item_title(self, item):
"""`xjj`
获取单篇文章的标题
Args:
item (Article): 文章对象
Returns:
str: 文章的标题
"""
return item.title return item.title
def item_description(self, item): def item_description(self, item):
"""`xjj`
获取单篇文章的描述内容
Args:
item (Article): 文章对象
Returns:
str: 文章正文内容转换为 Markdown 格式后的 HTML
"""
return CommonMarkdown.get_markdown(item.body) return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self): def feed_copyright(self):
"""`xjj`
获取 RSS 订阅源的版权信息
Returns:
str: 包含当前年份的版权声明
"""
now = timezone.now() now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year) return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item): def item_link(self, item):
"""`xjj`
获取单篇文章的链接地址
Args:
item (Article): 文章对象
Returns:
str: 文章的绝对URL地址
"""
return item.get_absolute_url() return item.get_absolute_url()
def item_guid(self, item): def item_guid(self, item):

@ -1,3 +1,20 @@
"""`xjj`
Django Admin 日志条目管理模块
========
该模块提供了对 Django 管理后台操作日志的可视化管理界面
主要功能包括
- 显示所有管理操作的日志记录包括创建修改和删除操作
- 提供日志记录的搜索和过滤功能
- 在列表中显示指向相关对象和用户的链接
- 限制权限以防止意外修改日志数据
该模块扩展了 Django 的内置 LogEntry 模型管理界面
增强了日志查看体验并保护日志不被修改
"""
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.models import DELETION from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -9,6 +26,12 @@ from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin): class LogEntryAdmin(admin.ModelAdmin):
"""`xjj`
管理后台中用于展示和管理日志条目 LogEntry ``ModelAdmin``
该类自定义了日志条目的展示方式过滤条件搜索字段以及权限控制等
"""
list_filter = [ list_filter = [
'content_type' 'content_type'
] ]
@ -34,6 +57,17 @@ class LogEntryAdmin(admin.ModelAdmin):
return False return False
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
"""`xjj`
控制用户是否有权限修改日志条目
Args:
request (HttpRequest): 当前的 HTTP 请求对象
obj (LogEntry, optional): 要检查权限的具体日志条目对象
Returns:
bool: 如果用户是超级用户或具有 ``admin.change_logentry`` 权限并且请求方法不是 **POST** 则返回 **True**
"""
return ( return (
request.user.is_superuser or request.user.is_superuser or
request.user.has_perm('admin.change_logentry') request.user.has_perm('admin.change_logentry')
@ -43,6 +77,16 @@ class LogEntryAdmin(admin.ModelAdmin):
return False return False
def object_link(self, obj): def object_link(self, obj):
"""`xjj`
生成一个指向相关对象的链接如果可能否则返回对象的字符串表示
Args:
obj (LogEntry): 日志条目实例
Return:
str: 包含HTML链接或纯文本的对象表示
"""
object_link = escape(obj.object_repr) object_link = escape(obj.object_repr)
content_type = obj.content_type content_type = obj.content_type
@ -63,6 +107,16 @@ class LogEntryAdmin(admin.ModelAdmin):
object_link.short_description = _('object') object_link.short_description = _('object')
def user_link(self, obj): def user_link(self, obj):
"""`xjj`
生成一个指向用户编辑页面的链接
Args:
obj (LogEntry): 日志条目实例
Returns:
str: 包含HTML链接的用户名称
"""
content_type = ContentType.objects.get_for_model(type(obj.user)) content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user)) user_link = escape(force_str(obj.user))
try: try:
@ -81,10 +135,30 @@ class LogEntryAdmin(admin.ModelAdmin):
user_link.short_description = _('user') user_link.short_description = _('user')
def get_queryset(self, request): def get_queryset(self, request):
"""`xjj`
获取查询集并进行优化预加载 ``content_type`` 以减少数据库查询
Args:
request (HttpRequest): 当前的 HTTP 请求对象
Returns:
QuerySet: 经过 ``prefetch_related`` 优化的查询集
"""
queryset = super(LogEntryAdmin, self).get_queryset(request) queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type') return queryset.prefetch_related('content_type')
def get_actions(self, request): def get_actions(self, request):
"""`xjj`
获取可用的操作列表并移除删除选中项操作
Args:
request (HttpRequest): 当前的 HTTP 请求对象
Returns:
dict: 可用的操作字典
"""
actions = super(LogEntryAdmin, self).get_actions(request) actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions: if 'delete_selected' in actions:
del actions['delete_selected'] del actions['delete_selected']

@ -15,8 +15,24 @@ from pathlib import Path
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import environ
from dotenv import load_dotenv
load_dotenv()
env = environ.Env()
def env_to_bool(env, default): def env_to_bool(env, default):
"""`xjj`
将环境变量转换为布尔值
Args:
env (str): 环境变量名称
default (bool): 当环境变量不存在时返回的默认值
Returns:
bool: 环境变量的布尔值表示如果环境变量不存在则返回默认值
"""
str_val = os.environ.get(env) str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True' return default if str_val is None else str_val == 'True'
@ -106,18 +122,29 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application'
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases # https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = { if TESTING:
'default': { DATABASES = {
'ENGINE': 'django.db.backends.mysql', 'default': {
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', 'ENGINE': 'django.db.backends.sqlite3',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', 'NAME': ':memory:',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', }
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', }
'PORT': int( else:
os.environ.get('DJANGO_MYSQL_PORT') or 3306), DATABASES = {
'OPTIONS': { 'default': {
'charset': 'utf8mb4'}, 'ENGINE': 'django.db.backends.mysql',
}} 'NAME': env('DJANGO_MYSQL_DATABASE'),
'USER': env('DJANGO_MYSQL_USER'),
'PASSWORD': env('DJANGO_MYSQL_PASSWORD'),
'HOST': env('DJANGO_MYSQL_HOST'),
'PORT': int(
env('DJANGO_MYSQL_PORT')),
'OPTIONS': {
'charset': 'utf8mb4',
'ssl_mode': 'VERIFY_IDENTITY',
'ssl': {'ca': env('DJANGO_MYSQL_SSL_CA')}
},
}}
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

@ -1,3 +1,21 @@
"""`xjj`
Django博客站点地图模块
========
该模块定义了网站的 sitemap 配置用于帮助搜索引擎更好地抓取网站内容
包含了以下几种类型的sitemap
1. 静态页面站点地图 (``StaticViewSitemap``) - 包含网站首页等静态页面
2. 文章站点地图 (``ArticleSiteMap``) - 包含所有已发布的文章页面
3. 分类站点地图 (``CategorySiteMap``) - 包含所有文章分类页面
4. 标签站点地图 (``TagSiteMap``) - 包含所有标签页面
5. 用户站点地图 (``UserSiteMap``) - 包含所有作者页面
每个站点地图类都继承自Django的Sitemap基类并根据不同的内容类型设置了相应的
更新频率 (``changefreq``) 和优先级 (``priority``) 参数
"""
from django.contrib.sitemaps import Sitemap from django.contrib.sitemaps import Sitemap
from django.urls import reverse from django.urls import reverse
@ -5,55 +23,182 @@ from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap): class StaticViewSitemap(Sitemap):
"""`xjj`
静态视图站点地图类
~~~~~~~~
该类用于生成静态页面的 sitemap 信息继承自 Django ``Sitemap``
"""
priority = 0.5 priority = 0.5
changefreq = 'daily' changefreq = 'daily'
def items(self): def items(self):
"""`xjj`
获取站点地图项列表
返回包含 URL 名称的列表这些 URL 将被包含在 sitemap
Returns:
list: 包含 URL 名称的列表
"""
return ['blog:index', ] return ['blog:index', ]
def location(self, item): def location(self, item):
"""`xjj`
根据 URL 名称获取实际的 URL 路径
通过 Django ``reverse`` 函数将 URL 名称转换为实际的 URL 路径
Args:
item (str): URL 名称
Returns:
str: 对应的 URL 路径
"""
return reverse(item) return reverse(item)
class ArticleSiteMap(Sitemap): class ArticleSiteMap(Sitemap):
"""`xjj`
文章站点地图类用于生成文章页面的 ``sitemap.xml`` 文件
该类继承自 Django ``Sitemap`` 定义了文章页面在站点地图中的相关属性
和数据获取方法便于搜索引擎爬虫发现和索引文章内容
"""
changefreq = "monthly" changefreq = "monthly"
priority = "0.6" priority = "0.6"
def items(self): def items(self):
"""`xjj`
获取 sitemap 中包含的所有文章对象列表
Returns:
QuerySet: 包含所有已发布状态文章的查询集
"""
return Article.objects.filter(status='p') return Article.objects.filter(status='p')
def lastmod(self, obj): def lastmod(self, obj):
"""`xjj`
获取指定文章对象的最后修改时间
Args:
obj: 文章对象实例
Returns:
datetime: 文章的最后修改时间
"""
return obj.last_modify_time return obj.last_modify_time
class CategorySiteMap(Sitemap): class CategorySiteMap(Sitemap):
"""`xjj`
分类站点地图类
~~~~~~~~
该类用于生成网站分类页面的 sitemap 信息继承自 Django ``Sitemap``
"""
changefreq = "Weekly" changefreq = "Weekly"
priority = "0.6" priority = "0.6"
def items(self): def items(self):
"""`xjj`
获取所有分类对象
Returns:
QuerySet: 包含所有 Category 对象的查询集
"""
return Category.objects.all() return Category.objects.all()
def lastmod(self, obj): def lastmod(self, obj):
"""`xjj`
获取分类对象的最后修改时间
Args:
obj (Category): 分类对象实例
Returns:
datetime: 分类的最后修改时间
"""
return obj.last_modify_time return obj.last_modify_time
class TagSiteMap(Sitemap): class TagSiteMap(Sitemap):
"""`xjj`
标签站点地图类用于生成标签页面的 sitemap.xml 文件
该类继承自 Django Sitemap 定义了标签页面在站点地图中的相关属性
和数据获取方法
"""
changefreq = "Weekly" changefreq = "Weekly"
priority = "0.3" priority = "0.3"
def items(self): def items(self):
"""`xjj`
获取所有标签对象列表
Returns:
QuerySet: 包含所有 Tag 对象的查询集
"""
return Tag.objects.all() return Tag.objects.all()
def lastmod(self, obj): def lastmod(self, obj):
"""`xjj`
获取指定标签对象的最后修改时间
Args:
obj (Tag): 标签对象实例
Returns:
datetime: 标签对象的最后修改时间
"""
return obj.last_modify_time return obj.last_modify_time
class UserSiteMap(Sitemap): class UserSiteMap(Sitemap):
"""`xjj`
用户站点地图类用于生成用户页面的 sitemap.xml 文件
Attributes:
changefreq (str): 页面更新频率设置为 `Weekly`
priority (str): 页面优先级设置为 `0.3`
"""
changefreq = "Weekly" changefreq = "Weekly"
priority = "0.3" priority = "0.3"
def items(self): def items(self):
"""`xjj`
获取所有需要包含在 sitemap 中的用户对象列表
通过获取所有文章的作者来构建用户列表并去重
Returns:
list: 包含所有文章作者用户的去重列表
"""
return list(set(map(lambda x: x.author, Article.objects.all()))) return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj): def lastmod(self, obj):
"""`xjj`
获取指定用户对象的最后修改时间
Args:
obj: 用户对象
Returns:
datetime: 用户的注册时间作为最后修改时间
"""
return obj.date_joined return obj.date_joined

@ -1,3 +1,23 @@
"""`xjj`
Spider Notify Module
========
此模块提供了向搜索引擎通知网站更新的功能主要用于 SEO 优化
当前支持百度搜索引擎的主动推送功能
主要功能
~~~~~~~~
- 向百度搜索引擎主动推送网站URL更新
- 记录推送结果和异常日志
依赖
~~~~~~~~
- ``requests``: 用于发送 HTTP 请求
- ``django.conf.settings``: 获取配置参数
- ``logging``: 记录日志信息
"""
import logging import logging
import requests import requests
@ -7,8 +27,18 @@ logger = logging.getLogger(__name__)
class SpiderNotify(): class SpiderNotify():
"""`xjj`
爬虫通知类用于向搜索引擎提交 URL 通知
"""
@staticmethod @staticmethod
def baidu_notify(urls): def baidu_notify(urls):
"""`xjj`
向百度搜索引擎提交 URL 通知
:param urls: 要提交的 URL 列表
"""
try: try:
data = '\n'.join(urls) data = '\n'.join(urls)
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
@ -18,4 +48,10 @@ class SpiderNotify():
@staticmethod @staticmethod
def notify(url): def notify(url):
"""`xjj`
通用通知方法调用百度通知接口
:param url: 要通知的 URL 列表
"""
SpiderNotify.baidu_notify(url) SpiderNotify.baidu_notify(url)

@ -1,13 +1,46 @@
"""`xjj`
DjangoBlog 测试模块
========
该模块包含对 DjangoBlog 应用各个功能组件的单元测试
主要用于验证工具函数模型视图等的正确性和稳定性
当前测试类:
- ``DjangoBlogTest``: 主要测试工具函数的功能正确性
测试覆盖的工具函数包括:
- ``get_sha256``: 测试 SHA256 哈希值生成功能
- ``CommonMarkdown.get_markdown``: 测试 Markdown 文本转换为 HTML 的功能
- ``parse_dict_to_url``: 测试字典转换为 URL 查询参数字符串的功能
"""
from django.test import TestCase from django.test import TestCase
from djangoblog.utils import * from djangoblog.utils import *
class DjangoBlogTest(TestCase): class DjangoBlogTest(TestCase):
"""`xjj`
Django 博客测试类
用于测试博客应用的各种功能模块
继承自 Django ``TestCase``
"""
def setUp(self): def setUp(self):
pass pass
def test_utils(self): def test_utils(self):
"""`xjj`
测试工具函数功能
验证以下功能
1. SHA256哈希函数
2. Markdown文本转换功能
3. 字典转URL参数功能
"""
md5 = get_sha256('test') md5 = get_sha256('test')
self.assertIsNotNone(md5) self.assertIsNotNone(md5)
c = CommonMarkdown.get_markdown(''' c = CommonMarkdown.get_markdown('''

@ -1,5 +1,21 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
"""`xjj`
DjangoBlog 工具模块
========
本模块包含项目中使用的各种通用工具函数和辅助类主要功能包括
- 缓存装饰器和缓存管理
- Markdown 内容转换和处理
- 邮件发送功能
- 网站设置管理
- 用户头像处理
- HTML安全过滤
- URL和数据处理工具
该模块为整个 DjangoBlog 项目提供基础工具支持被多个应用组件调用
"""
import logging import logging
@ -21,19 +37,73 @@ logger = logging.getLogger(__name__)
def get_max_articleid_commentid(): def get_max_articleid_commentid():
"""`xjj`
获取最新的文章 ID 和评论 ID
该函数从数据库中查询最新创建的文章和评论记录
并返回它们的主键值
Returns:
tuple: 包含两个元素的元组
- 第一个元素是最新文章的主键 (pk)
- 第二个元素是最新评论的主键 (pk)
"""
from blog.models import Article from blog.models import Article
from comments.models import Comment from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk) return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str): def get_sha256(str):
"""`xjj`
计算字符串的 SHA256 哈希值
Args:
str (str): 需要计算哈希值的输入字符串
Returns:
str: 输入字符串的 SHA256 哈希值以十六进制字符串形式表示
"""
m = sha256(str.encode('utf-8')) m = sha256(str.encode('utf-8'))
return m.hexdigest() return m.hexdigest()
def cache_decorator(expiration=3 * 60): def cache_decorator(expiration=3 * 60):
"""`xjj`
缓存装饰器工厂函数用于创建具有指定过期时间的缓存装饰器
Args:
expiration (int): 缓存过期时间单位为秒默认为 3 分钟
Returns:
function: 返回一个装饰器函数 ``wrapper``
"""
def wrapper(func): def wrapper(func):
"""`xjj`
装饰器函数用于包装需要缓存的函数
Args:
func (function): 被装饰的函数
Returns:
function: 返回包装后的函数 ``news``
"""
def news(*args, **kwargs): def news(*args, **kwargs):
"""`xjj`
实际执行缓存逻辑的函数
Args:
*args: 被装饰函数的位置参数
**kwargs: 被装饰函数的关键字参数
Returns:
任意类型: 返回被装饰函数的执行结果或缓存的结果
"""
try: try:
view = args[0] view = args[0]
key = view.get_cache_key() key = view.get_cache_key()
@ -94,13 +164,47 @@ def expire_view_cache(path, servername, serverport, key_prefix=None):
@cache_decorator() @cache_decorator()
def get_current_site(): def get_current_site():
"""
获取当前站点信息
该函数通过 Django Sites 框架获取当前站点对象并使用缓存装饰器进行性能优化
函数会自动识别请求上下文中的站点信息避免重复查询数据库
Returns:
Site: 返回当前站点对象包含站点的基本信息如域名名称等
注意:
- 依赖 Django Sites 框架
- 使用了缓存机制提高性能
- 需要确保 SITE_ID 设置正确
"""
site = Site.objects.get_current() site = Site.objects.get_current()
return site return site
class CommonMarkdown: class CommonMarkdown:
"""`xjj`
Markdown 处理工具类
~~~~~~~~
提供 Markdown 文本转换为 HTML 的功能支持目录生成代码高亮等扩展功能
"""
@staticmethod @staticmethod
def _convert_markdown(value): def _convert_markdown(value):
"""`xjj`
Markdown 文本转换为 HTML 并生成目录
Args:
value (str): 需要转换的 Markdown 格式文本
Returns:
tuple: 包含两个元素的元组
- str: 转换后的 HTML 正文内容
- str: 生成的目录 HTML 内容
"""
md = markdown.Markdown( md = markdown.Markdown(
extensions=[ extensions=[
'extra', 'extra',
@ -115,16 +219,50 @@ class CommonMarkdown:
@staticmethod @staticmethod
def get_markdown_with_toc(value): def get_markdown_with_toc(value):
"""`xjj`
转换 Markdown 文本并返回正文和目录
Args:
value (str): 需要转换的 Markdown 格式文本
Returns:
tuple: 包含两个元素的元组
- str: 转换后的 HTML 正文内容
- str: 生成的目录 HTML 内容
"""
body, toc = CommonMarkdown._convert_markdown(value) body, toc = CommonMarkdown._convert_markdown(value)
return body, toc return body, toc
@staticmethod @staticmethod
def get_markdown(value): def get_markdown(value):
"""`xjj`
转换 Markdown 文本并只返回正文内容
Args:
value (str): 需要转换的 Markdown 格式文本
Returns:
str: 转换后的 HTML 正文内容
"""
body, toc = CommonMarkdown._convert_markdown(value) body, toc = CommonMarkdown._convert_markdown(value)
return body return body
def send_email(emailto, title, content): def send_email(emailto, title, content):
"""`xjj`
发送邮件函数
该函数通过触发 Django 信号来发送邮件实际的邮件发送逻辑在信号处理器中实现
Args:
emailto (str): 收件人邮箱地址
title (str): 邮件标题
content (str): 邮件内容
"""
from djangoblog.blog_signals import send_email_signal from djangoblog.blog_signals import send_email_signal
send_email_signal.send( send_email_signal.send(
send_email.__class__, send_email.__class__,
@ -139,6 +277,16 @@ def generate_code() -> str:
def parse_dict_to_url(dict): def parse_dict_to_url(dict):
"""`xjj`
将字典转换为 URL 查询字符串格式
Args:
dict (dict): 包含键值对的字典用于生成URL查询参数
Returns:
str: 格式化后的 URL 查询字符串格式为 `key1=value1&key2=value2...`
"""
from urllib.parse import quote from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()]) for k, v in dict.items()])
@ -146,6 +294,16 @@ def parse_dict_to_url(dict):
def get_blog_setting(): def get_blog_setting():
"""`xjj`
获取博客设置信息
该函数首先尝试从缓存中获取博客设置如果缓存中不存在则从数据库中获取
如果数据库中没有博客设置记录则创建一条默认配置记录
Returns:
BlogSettings: 博客设置对象包含网站名称描述 SEO 设置文章显示配置等信息
"""
value = cache.get('get_blog_setting') value = cache.get('get_blog_setting')
if value: if value:
return value return value
@ -202,6 +360,13 @@ def save_user_avatar(url):
def delete_sidebar_cache(): def delete_sidebar_cache():
"""`xjj`
删除侧边栏缓存
该函数用于删除所有侧边栏相关的缓存数据通过遍历 ``LinkShowType`` 枚举值
构造对应的缓存键名并逐一删除
"""
from blog.models import LinkShowType from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values] keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys: for k in keys:
@ -210,12 +375,30 @@ def delete_sidebar_cache():
def delete_view_cache(prefix, keys): def delete_view_cache(prefix, keys):
"""`xjj`
删除视图缓存
该函数用于删除 Django 模板片段缓存通过构建缓存键并从缓存中删除对应的数据
Args:
prefix (str): 模板片段缓存的前缀名称
keys (list): 用于生成缓存键的变量列表
"""
from django.core.cache.utils import make_template_fragment_key from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys) key = make_template_fragment_key(prefix, keys)
cache.delete(key) cache.delete(key)
def get_resource_url(): def get_resource_url():
"""`xjj`
获取资源文件的 URL 地址
该函数优先返回配置中的静态资源 URL 如果未配置则根据当前站点信息构造默认的静态资源 URL
Returns:
str: 静态资源的完整 URL 地址
"""
if settings.STATIC_URL: if settings.STATIC_URL:
return settings.STATIC_URL return settings.STATIC_URL
else: else:
@ -236,6 +419,20 @@ ALLOWED_CLASSES = [
] ]
def class_filter(tag, name, value): def class_filter(tag, name, value):
"""`xjj`
自定义 class 属性过滤器
该函数用于过滤 HTML 标签的 class 属性值只允许预定义的安全 class 值通过
Args:
tag: HTML 标签对象
name: 属性名称
value: 属性值
Returns:
str | Any : 过滤后的 class 属性值如果没有任何允许的 class 则返回 **False**如果不是 class 属性则返回原始值
"""
"""自定义class属性过滤器""" """自定义class属性过滤器"""
if name == 'class': if name == 'class':
# 只允许预定义的安全class值 # 只允许预定义的安全class值

@ -1,4 +1,25 @@
# encoding: utf-8 # encoding: utf-8
"""`xjj`
Whoosh 中文搜索后端模块
该模块为 Django Haystack 提供了一个基于 Whoosh 的中文搜索引擎后端实现
主要特性包括
- 支持中文分词搜索使用 jieba 分词器
- 实现了完整的搜索后端功能包括索引创建更新删除和查询
- 支持多种数据类型的字段映射文本数字日期布尔值等
- 提供搜索结果高亮显示功能
- 支持搜索建议和拼写纠正
- 支持多模型搜索和过滤
- 实现了 more_like_this 相似文档查找功能
依赖
~~~~~~~~
- Whoosh >= 2.5.0
- jieba (用于中文分词)
- Django Haystack
"""
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
@ -145,6 +166,18 @@ class WhooshSearchBackend(BaseSearchBackend):
self.setup_complete = True self.setup_complete = True
def build_schema(self, fields): def build_schema(self, fields):
"""`xjj`
构建用于搜索索引的 Whoosh Schema 对象
根据传入的字段定义创建对应的 Whoosh 字段类型并最终构建一个完整的 Schema 实例
:param fields: 包含字段名与字段配置对象的字典键是字段名称值是一个具有 field_typestoredboost 等属性的对象
:return: 第一个元素是文档内容字段名第二个是构建好的 Whoosh Schema 对象
:exception SearchBackendError: 当没有找到任何字段时抛出异常
"""
schema_fields = { schema_fields = {
ID: WHOOSH_ID(stored=True, unique=True), ID: WHOOSH_ID(stored=True, unique=True),
DJANGO_CT: WHOOSH_ID(stored=True), DJANGO_CT: WHOOSH_ID(stored=True),
@ -200,6 +233,17 @@ class WhooshSearchBackend(BaseSearchBackend):
return (content_field_name, Schema(**schema_fields)) return (content_field_name, Schema(**schema_fields))
def update(self, index, iterable, commit=True): def update(self, index, iterable, commit=True):
"""`xjj`
更新索引中的文档数据
遍历可迭代对象中的模型实例将其转换成可用于索引的数据格式并更新到索引中
Args:
index (SearchIndex): Haystack 的索引类实例
iterable (iterable): 要被索引的模型实例集合
commit (bool): 是否在更新后提交更改默认为 **True**
"""
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
@ -245,6 +289,14 @@ class WhooshSearchBackend(BaseSearchBackend):
writer.commit() writer.commit()
def remove(self, obj_or_string, commit=True): def remove(self, obj_or_string, commit=True):
"""`xjj`
从索引中删除指定文档
Args:
obj_or_string (Model | str): 要删除的模型实例或其唯一标识符字符串
commit (bool): 是否立即提交更改默认为 **True**
"""
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
@ -267,6 +319,14 @@ class WhooshSearchBackend(BaseSearchBackend):
exc_info=True) exc_info=True)
def clear(self, models=None, commit=True): def clear(self, models=None, commit=True):
"""`xjj`
清除整个索引或特定模型类型的索引数据
Args:
models (list | tuple): 可选要清除的模型列表
commit (bool): 是否立即提交更改默认为 **True**
"""
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
@ -304,6 +364,10 @@ class WhooshSearchBackend(BaseSearchBackend):
"Failed to clear Whoosh index: %s", e, exc_info=True) "Failed to clear Whoosh index: %s", e, exc_info=True)
def delete_index(self): def delete_index(self):
"""`xjj`
完全删除当前索引目录下的文件并重新初始化索引结构
"""
# Per the Whoosh mailing list, if wiping out everything from the index, # Per the Whoosh mailing list, if wiping out everything from the index,
# it's much more efficient to simply delete the index files. # it's much more efficient to simply delete the index files.
if self.use_file_storage and os.path.exists(self.path): if self.use_file_storage and os.path.exists(self.path):
@ -315,6 +379,10 @@ class WhooshSearchBackend(BaseSearchBackend):
self.setup() self.setup()
def optimize(self): def optimize(self):
"""`xjj`
对现有索引执行优化操作提升查询性能
"""
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
@ -322,6 +390,17 @@ class WhooshSearchBackend(BaseSearchBackend):
self.index.optimize() self.index.optimize()
def calculate_page(self, start_offset=0, end_offset=None): def calculate_page(self, start_offset=0, end_offset=None):
"""`xjj`
根据偏移量计算分页信息
Args:
start_offset (int): 查询起始位置
end_offset (int): 查询结束位置
Returns:
tuple: 分别表示页码和每页长度
"""
# Prevent against Whoosh throwing an error. Requires an end_offset # Prevent against Whoosh throwing an error. Requires an end_offset
# greater than 0. # greater than 0.
if end_offset is not None and end_offset <= 0: if end_offset is not None and end_offset <= 0:
@ -366,6 +445,33 @@ class WhooshSearchBackend(BaseSearchBackend):
limit_to_registered_models=None, limit_to_registered_models=None,
result_class=None, result_class=None,
**kwargs): **kwargs):
"""`xjj`
执行全文检索操作
Args:
query_string (str): 用户输入的查询语句
sort_by (list): 排序字段列表
start_offset (int): 结果偏移起点
end_offset (int): 结果偏移终点
fields (str): 指定返回字段
highlight (bool): 是否启用高亮显示
facets (dict): 分面统计参数
date_facets (dict): 时间范围分面参数
query_facets (dict): 自定义查询分面参数
narrow_queries (set): 过滤条件集合
spelling_query (str): 用于拼写建议的原始查询
within (tuple): 地理位置过滤参数
dwithin (tuple): 距离范围内地理查询参数
distance_point (tuple): 中心点坐标
models (list): 限定搜索模型列表
limit_to_registered_models (bool): 是否仅限注册过的模型
result_class (class): 自定义结果封装类
**kwargs: 其他扩展参数
Returns:
dict: 包括匹配结果命中数拼写建议等信息
"""
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
@ -570,6 +676,23 @@ class WhooshSearchBackend(BaseSearchBackend):
limit_to_registered_models=None, limit_to_registered_models=None,
result_class=None, result_class=None,
**kwargs): **kwargs):
"""`xjj`
查找与给定模型实例相似的内容
Args:
model_instance (Model): 目标模型实例
additional_query_string (str): 补充查询语句
start_offset (int): 起始偏移
end_offset (int): 终止偏移
models (list): 限制查找的模型列表
limit_to_registered_models (bool): 是否只考虑已注册模型
result_class (class): 自定义结果类
**kwargs: 扩展参数
Returns:
dict: 包含相似文档的结果集
"""
if not self.setup_complete: if not self.setup_complete:
self.setup() self.setup()
@ -682,6 +805,20 @@ class WhooshSearchBackend(BaseSearchBackend):
query_string='', query_string='',
spelling_query=None, spelling_query=None,
result_class=None): result_class=None):
"""`xjj`
处理原始搜索结果转换为标准输出格式
Args:
raw_page (ResultsPage): 来自 Whoosh 的原始页面结果
highlight (bool): 是否需要高亮关键词
query_string (str): 查询语句
spelling_query (str): 用于拼写的查询语句
result_class (class): 自定义结果包装类
Returns:
dict: 标准化的搜索响应结构
"""
from haystack import connections from haystack import connections
results = [] results = []
@ -768,6 +905,16 @@ class WhooshSearchBackend(BaseSearchBackend):
} }
def create_spelling_suggestion(self, query_string): def create_spelling_suggestion(self, query_string):
"""`xjj`
生成针对用户查询的拼写修正建议
Args:
query_string (str): 输入的查询语句
Returns:
str: 拼写修正后的建议语句
"""
spelling_suggestion = None spelling_suggestion = None
reader = self.index.reader() reader = self.index.reader()
corrector = reader.corrector(self.content_field_name) corrector = reader.corrector(self.content_field_name)
@ -872,6 +1019,16 @@ class WhooshSearchBackend(BaseSearchBackend):
class WhooshSearchQuery(BaseSearchQuery): class WhooshSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date): def _convert_datetime(self, date):
"""`xjj`
将日期时间对象转换为 Whoosh 可识别的字符串格式
Args:
date: 日期或日期时间对象
Returns:
str: 格式化后的日期时间字符串格式为 `YYYYMMDDHHMMSS`
"""
if hasattr(date, 'hour'): if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S')) return force_str(date.strftime('%Y%m%d%H%M%S'))
else: else:
@ -903,6 +1060,20 @@ class WhooshSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words) return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value): def build_query_fragment(self, field, filter_type, value):
"""`xjj`
构建单个查询片段
根据字段名过滤类型和值构建相应的查询表达式片段用于组合成完整的搜索查询
Args:
field (str): 要查询的字段名
filter_type (str): 过滤类型 `contains` `exact` `startswith`
value: 要查询的值可以是字符串列表或其他数据类型
Returns:
str: 构建好的查询片段字符串
"""
from haystack import connections from haystack import connections
query_frag = '' query_frag = ''
is_datetime = False is_datetime = False
@ -1040,5 +1211,16 @@ class WhooshSearchQuery(BaseSearchQuery):
class WhooshEngine(BaseEngine): class WhooshEngine(BaseEngine):
"""`xjj`
Whoosh 搜索引擎引擎类
该类继承自 ``BaseEngine``用于集成 Whoosh 搜索功能
包含搜索后端和查询处理的相关配置
Attributes:
backend: 搜索后端类设置为 `WhooshSearchBackend`
query: 查询类设置为 `WhooshSearchQuery`
"""
backend = WhooshSearchBackend backend = WhooshSearchBackend
query = WhooshSearchQuery query = WhooshSearchQuery

@ -0,0 +1,37 @@
import json
import subprocess
import sys
from datetime import datetime
def generate_quality_report():
report = {
'project': 'OAuth Module',
'analysis_date': datetime.now().isoformat(),
'tools_used': ['flake8', 'pylint'],
'summary': {}
}
# Flake8 分析
result = subprocess.run([
sys.executable, '-m', 'flake8', 'oauth/', '--max-line-length=120', '--statistics', '--exit-zero'
], capture_output=True, text=True)
report['flake8_issues'] = result.stdout.strip().split('\n')
# Pylint 分析
result = subprocess.run([
sys.executable, '-m', 'pylint', 'oauth/', '--output-format=json', '--exit-zero'
], capture_output=True, text=True)
try:
report['pylint_issues'] = json.loads(result.stdout)
except:
report['pylint_issues'] = result.stdout
# 保存报告
with open('oauth_quality_report.json', 'w', encoding='utf-8') as f:
json.dump(report, f, indent=2, ensure_ascii=False)
print('代码质量报告已生成: oauth_quality_report.json')
generate_quality_report()

@ -1,3 +1,9 @@
"""
lrj
OAuth 认证后台管理模块
配置Django Admin后台中OAuth相关模型的显示和操作方式
"""
import logging import logging
from django.contrib import admin from django.contrib import admin
@ -5,50 +11,128 @@ from django.contrib import admin
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
# 配置日志记录器
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class OAuthUserAdmin(admin.ModelAdmin): class OAuthUserAdmin(admin.ModelAdmin):
"""
lrj
OAuth用户模型后台管理配置类
自定义OAuthUser模型在Django Admin中的显示和行为
"""
# lrj搜索字段配置支持按昵称和邮箱搜索
search_fields = ('nickname', 'email') search_fields = ('nickname', 'email')
# 列表页每页显示记录数
list_per_page = 20 list_per_page = 20
# 列表页显示的字段
list_display = ( list_display = (
'id', 'id', # 用户ID
'nickname', 'nickname', # 用户昵称
'link_to_usermodel', 'link_to_usermodel', # lrj关联用户链接自定义方法
'show_user_image', 'show_user_image', #lrj 用户头像显示(自定义方法)
'type', 'type', # lrjOAuth平台类型
'email', 'email', # lrj用户邮箱
) )
# lrj列表页中可点击进入编辑页的字段
list_display_links = ('id', 'nickname') list_display_links = ('id', 'nickname')
# lrj右侧过滤器配置支持按关联用户和平台类型过滤
list_filter = ('author', 'type',) list_filter = ('author', 'type',)
# lrj只读字段列表初始为空
readonly_fields = [] readonly_fields = []
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
"""lrj
动态获取只读字段列表
确保在编辑页所有字段都为只读防止误操作
Args:
request: HTTP请求对象
obj: 模型实例对象
Returns:
list: 只读字段列表
"""
# 将所有模型字段和多对多字段都设为只读
return list(self.readonly_fields) + \ return list(self.readonly_fields) + \
[field.name for field in obj._meta.fields] + \ [field.name for field in obj._meta.fields] + \
[field.name for field in obj._meta.many_to_many] [field.name for field in obj._meta.many_to_many]
def has_add_permission(self, request): def has_add_permission(self, request):
"""lrj
禁用添加权限
OAuth用户只能通过OAuth流程自动创建不能手动添加
Args:
request: HTTP请求对象
Returns:
bool: 是否允许添加这里始终返回False
"""
return False return False
def link_to_usermodel(self, obj): def link_to_usermodel(self, obj):
"""lrj
显示关联用户链接的自定义方法
在列表页显示关联的本站用户链接
Args:
obj: OAuthUser模型实例
Returns:
str: 格式化的HTML链接或None
"""
if obj.author: if obj.author:
# lrj构建关联用户的管理后台编辑链接
info = (obj.author._meta.app_label, obj.author._meta.model_name) info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# lrj返回格式化的HTML链接显示用户昵称或邮箱
return format_html( return format_html(
u'<a href="%s">%s</a>' % u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email)) (link, obj.author.nickname if obj.author.nickname else obj.author.email))
def show_user_image(self, obj): def show_user_image(self, obj):
img = obj.picture """lrj
显示用户头像的自定义方法
在列表页以缩略图形式显示用户头像
Args:
obj: OAuthUser模型实例
Returns:
str: 格式化的HTML图片标签
"""
img = obj.picture # 获取头像URL
return format_html( return format_html(
u'<img src="%s" style="width:50px;height:50px"></img>' % u'<img src="%s" style="width:50px;height:50px"></img>' %
(img)) (img))
link_to_usermodel.short_description = '用户' # 设置自定义方法在Admin中的显示名称
show_user_image.short_description = '用户头像' link_to_usermodel.short_description = '用户' # 关联用户列标题
show_user_image.short_description = '用户头像' # 用户头像列标题
class OAuthConfigAdmin(admin.ModelAdmin): class OAuthConfigAdmin(admin.ModelAdmin):
list_display = ('type', 'appkey', 'appsecret', 'is_enable') """lrj
OAuth配置模型后台管理配置类
自定义OAuthConfig模型在Django Admin中的显示
"""
# lrj列表页显示的字段
list_display = (
'type', # lrj平台类型
'appkey', # lrj应用Key
'appsecret', # lrj应用Secret
'is_enable' # lrj是否启用
)
# lrj右侧过滤器配置支持按平台类型过滤
list_filter = ('type',) list_filter = ('type',)

@ -3,3 +3,4 @@ from django.apps import AppConfig
class OauthConfig(AppConfig): class OauthConfig(AppConfig):
name = 'oauth' name = 'oauth'

@ -1,12 +1,40 @@
"""lrj
OAuth 认证表单模块
定义OAuth认证过程中使用的Django表单类
"""
from django.contrib.auth.forms import forms from django.contrib.auth.forms import forms
from django.forms import widgets from django.forms import widgets
class RequireEmailForm(forms.Form): class RequireEmailForm(forms.Form):
"""lrj
邮箱补充表单类
当OAuth用户没有提供邮箱时用于收集用户邮箱信息
"""
# lrj邮箱字段必需字段标签为'电子邮箱'
email = forms.EmailField(label='电子邮箱', required=True) email = forms.EmailField(label='电子邮箱', required=True)
# lrjOAuth用户ID字段隐藏字段非必需用于关联OAuth用户记录
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False) oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""lrj
表单初始化方法
自定义表单字段的widget属性添加CSS类和占位符
Args:
*args: 位置参数
**kwargs: 关键字参数
"""
#lrj调用父类初始化方法
super(RequireEmailForm, self).__init__(*args, **kwargs) super(RequireEmailForm, self).__init__(*args, **kwargs)
# lrj自定义邮箱字段的widget添加Bootstrap样式和占位符
self.fields['email'].widget = widgets.EmailInput( self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"}) attrs={
'placeholder': "email", # lrj输入框占位符文本
"class": "form-control" # lrjBootstrap表单控件CSS类
})

@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2025-11-11 10:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('oauth', '0003_alter_oauthuser_nickname'),
]
operations = [
migrations.AlterModelOptions(
name='oauthconfig',
options={'ordering': ['-creation_time'], 'verbose_name': 'OAuth配置', 'verbose_name_plural': 'OAuth配置'},
),
]

@ -1,4 +1,8 @@
# Create your models here. """lrj
OAuth 认证模块数据模型定义
提供第三方登录用户信息和配置管理的数据结构
"""
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
@ -7,61 +11,114 @@ from django.utils.translation import gettext_lazy as _
class OAuthUser(models.Model): class OAuthUser(models.Model):
"""lrj
OAuth 第三方登录用户信息模型
存储通过第三方平台(微博GitHub等)登录的用户信息
"""
# lrj关联本站用户可为空(用户首次第三方登录时尚未绑定本站账号)
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'), # lrj翻译作者
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE) # lrj级联删除本站用户删除时同步删除OAuth关联
# lrj第三方平台的用户唯一标识
openid = models.CharField(max_length=50) openid = models.CharField(max_length=50)
# lrj第三方平台的用户昵称
nickname = models.CharField(max_length=50, verbose_name=_('nick name')) nickname = models.CharField(max_length=50, verbose_name=_('nick name'))
# lrjOAuth访问令牌用于调用第三方平台API
token = models.CharField(max_length=150, null=True, blank=True) token = models.CharField(max_length=150, null=True, blank=True)
# lrj用户头像URL
picture = models.CharField(max_length=350, blank=True, null=True) picture = models.CharField(max_length=350, blank=True, null=True)
# lrj第三方平台类型weibo, github, google等
type = models.CharField(blank=False, null=False, max_length=50) type = models.CharField(blank=False, null=False, max_length=50)
# lrj用户邮箱(从第三方平台获取)
email = models.CharField(max_length=50, null=True, blank=True) email = models.CharField(max_length=50, null=True, blank=True)
# lrj原始元数据存储从第三方平台返回的完整用户信息(JSON格式)
metadata = models.TextField(null=True, blank=True) metadata = models.TextField(null=True, blank=True)
# lrj记录创建时间自动设置为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
#lrj 最后修改时间,自动更新为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
def __str__(self): def __str__(self):
"""lrj管理员界面显示的用户标识"""
return self.nickname return self.nickname
class Meta: class Meta:
verbose_name = _('oauth user') """lrj模型元数据配置"""
verbose_name_plural = verbose_name verbose_name = _('oauth user') # lrj单数显示名称
ordering = ['-creation_time'] verbose_name_plural = verbose_name # lrj复数显示名称
ordering = ['-creation_time'] # lrj默认按创建时间降序排列
class OAuthConfig(models.Model): class OAuthConfig(models.Model):
"""lrj
OAuth 应用配置模型
存储各个第三方平台的OAuth应用配置信息
"""
# lrj支持的第三方平台类型选项
TYPE = ( TYPE = (
('weibo', _('weibo')), ('weibo', _('weibo')), # lrj微博
('google', _('google')), ('google', _('google')), # 谷歌
('github', 'GitHub'), ('github', 'GitHub'), # lrjGitHub
('facebook', 'FaceBook'), ('facebook', 'FaceBook'), # lrjFacebook
('qq', 'QQ'), ('qq', 'QQ'), # lrjQQ
) )
# lrj平台类型选择
type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
# lrjOAuth应用的AppKey/Client ID
appkey = models.CharField(max_length=200, verbose_name='AppKey') appkey = models.CharField(max_length=200, verbose_name='AppKey')
# lrjOAuth应用的AppSecret/Client Secret
appsecret = models.CharField(max_length=200, verbose_name='AppSecret') appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
# lrjOAuth回调URL用于接收授权码
callback_url = models.CharField( callback_url = models.CharField(
max_length=200, max_length=200,
verbose_name=_('callback url'), verbose_name=_('callback url'),
blank=False, blank=False,
default='') default='')
# lrj是否启用该平台配置
is_enable = models.BooleanField( is_enable = models.BooleanField(
_('is enable'), default=True, blank=False, null=False) _('is enable'), default=True, blank=False, null=False)
# lrj配置创建时间
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
# lrj配置最后修改时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
def clean(self): def clean(self):
"""lrj
数据验证方法确保同类型平台配置唯一
避免重复配置同一个第三方平台
"""
if OAuthConfig.objects.filter( if OAuthConfig.objects.filter(
type=self.type).exclude(id=self.id).count(): type=self.type).exclude(id=self.id).count():
raise ValidationError(_(self.type + _('already exists'))) raise ValidationError(_(self.type + _('already exists')))
def __str__(self): def __str__(self):
"""lrj管理员界面显示的配置标识"""
return self.type return self.type
class Meta: class Meta:
verbose_name = 'oauth配置' """lrj模型元数据配置"""
verbose_name_plural = verbose_name verbose_name = 'OAuth配置' # lrj单数显示名称
ordering = ['-creation_time'] verbose_name_plural = verbose_name # lrj复数显示名称
ordering = ['-creation_time'] # lrj默认按创建时间降序排列

@ -0,0 +1,67 @@
# Create your models here.
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
class OAuthUser(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_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)
def __str__(self):
return self.nickname
class Meta:
verbose_name = _('oauth user')
verbose_name_plural = verbose_name
ordering = ['-creation_time']
class OAuthConfig(models.Model):
TYPE = (
('weibo', _('weibo')),
('google', _('google')),
('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')
callback_url = models.CharField(
max_length=200,
verbose_name=_('callback url'),
blank=False,
default='')
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)
def clean(self):
if OAuthConfig.objects.filter(
type=self.type).exclude(id=self.id).count():
raise ValidationError(_(self.type + _('already exists')))
def __str__(self):
return self.type
class Meta:
verbose_name = 'oauth配置'
verbose_name_plural = verbose_name
ordering = ['-creation_time']

@ -1,3 +1,9 @@
"""lrj
OAuth 认证管理器模块
提供多平台OAuth2.0认证的抽象基类和具体实现
支持微博GoogleGitHubFacebookQQ等第三方登录
"""
import json import json
import logging import logging
import os import os
@ -7,8 +13,9 @@ from abc import ABCMeta, abstractmethod
import requests import requests
from djangoblog.utils import cache_decorator from djangoblog.utils import cache_decorator
from oauth.models import OAuthUser, OAuthConfig from oauth.models import OAuthConfig, OAuthUser
# lrj配置日志记录器
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -19,76 +26,166 @@ class OAuthAccessTokenException(Exception):
class BaseOauthManager(metaclass=ABCMeta): class BaseOauthManager(metaclass=ABCMeta):
"""获取用户授权""" """lrj
OAuth管理器抽象基类
定义所有OAuth平台必须实现的接口和方法
"""
#lrj 授权页面URL - 需要子类实现
AUTH_URL = None AUTH_URL = None
"""获取token"""
# lrj获取访问令牌URL - 需要子类实现
TOKEN_URL = None TOKEN_URL = None
"""获取用户信息"""
# lrj获取用户信息API URL - 需要子类实现
API_URL = None API_URL = None
'''icon图标名'''
# lrj平台图标名称标识 - 需要子类实现
ICON_NAME = None ICON_NAME = None
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
"""lrj
初始化OAuth管理器
Args:
access_token: 访问令牌可为None
openid: 第三方平台用户ID可为None
"""
self.access_token = access_token self.access_token = access_token
self.openid = openid self.openid = openid
@property @property
def is_access_token_set(self): def is_access_token_set(self):
"""lrj检查access_token是否已设置"""
return self.access_token is not None return self.access_token is not None
@property @property
def is_authorized(self): def is_authorized(self):
"""lrj检查是否已完成授权包含access_token和openid"""
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
@abstractmethod @abstractmethod
def get_authorization_url(self, nexturl='/'): def get_authorization_url(self, nexturl='/'):
"""lrj
获取授权页面URL - 抽象方法
Args:
nexturl: 授权成功后跳转的URL
Returns:
str: 完整的授权页面URL
"""
pass pass
@abstractmethod @abstractmethod
def get_access_token_by_code(self, code): def get_access_token_by_code(self, code):
"""lrj
通过授权码获取访问令牌 - 抽象方法
Args:
code: OAuth回调返回的授权码
Returns:
str: 访问令牌
"""
pass pass
@abstractmethod @abstractmethod
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
"""lrj
获取用户信息 - 抽象方法
Returns:
OAuthUser: 用户信息对象
"""
pass pass
@abstractmethod @abstractmethod
def get_picture(self, metadata): def get_picture(self, metadata):
"""lrj
从元数据中提取用户头像URL - 抽象方法
Args:
metadata: 用户元数据JSON字符串
Returns:
str: 头像URL
"""
pass pass
def do_get(self, url, params, headers=None): def do_get(self, url, params, headers=None):
"""lrj
执行GET请求到第三方API
Args:
url: 请求URL
params: 请求参数
headers: 请求头可选
Returns:
str: 响应文本
"""
rsp = requests.get(url=url, params=params, headers=headers) rsp = requests.get(url=url, params=params, headers=headers)
logger.info(rsp.text) logger.info(rsp.text) # lrj记录响应日志
return rsp.text return rsp.text
def do_post(self, url, params, headers=None): def do_post(self, url, params, headers=None):
"""lrj
执行POST请求到第三方API
Args:
url: 请求URL
params: 请求参数
headers: 请求头可选
Returns:
str: 响应文本
"""
rsp = requests.post(url, params, headers=headers) rsp = requests.post(url, params, headers=headers)
logger.info(rsp.text) logger.info(rsp.text) # lrj记录响应日志
return rsp.text return rsp.text
def get_config(self): def get_config(self):
"""lrj
从数据库获取当前平台的OAuth配置
Returns:
OAuthConfig: 配置对象或None
"""
value = OAuthConfig.objects.filter(type=self.ICON_NAME) value = OAuthConfig.objects.filter(type=self.ICON_NAME)
return value[0] if value else None return value[0] if value else None
class WBOauthManager(BaseOauthManager): class WBOauthManager(BaseOauthManager):
"""lrj
微博OAuth管理器
实现微博平台的OAuth2.0认证流程
"""
# lrj微博OAuth接口URL
AUTH_URL = 'https://api.weibo.com/oauth2/authorize' AUTH_URL = 'https://api.weibo.com/oauth2/authorize'
TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' TOKEN_URL = 'https://api.weibo.com/oauth2/access_token'
API_URL = 'https://api.weibo.com/2/users/show.json' API_URL = 'https://api.weibo.com/2/users/show.json'
ICON_NAME = 'weibo' ICON_NAME = 'weibo'
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
"""lrj初始化微博OAuth配置"""
config = self.get_config() config = self.get_config()
self.client_id = config.appkey if config else '' self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else '' self.client_secret = config.appsecret if config else ''
self.callback_url = config.callback_url if config else '' self.callback_url = config.callback_url if config else ''
super( super(WBOauthManager, self).__init__(access_token=access_token, openid=openid)
WBOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, nexturl='/'): def get_authorization_url(self, nexturl='/'):
"""lrj
生成微博授权页面URL
Args:
nexturl: 授权成功后跳转URL
Returns:
str: 微博授权URL
"""
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'response_type': 'code', 'response_type': 'code',
@ -98,7 +195,18 @@ class WBOauthManager(BaseOauthManager):
return url return url
def get_access_token_by_code(self, code): def get_access_token_by_code(self, code):
"""lrj
使用授权码获取微博访问令牌
Args:
code: 授权码
Returns:
OAuthUser: 用户信息对象
Raises:
OAuthAccessTokenException: 获取令牌失败时抛出
"""
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.client_secret, 'client_secret': self.client_secret,
@ -117,8 +225,15 @@ class WBOauthManager(BaseOauthManager):
raise OAuthAccessTokenException(rsp) raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
"""lrj
获取微博用户信息
Returns:
OAuthUser: 微博用户信息对象
"""
if not self.is_authorized: if not self.is_authorized:
return None return None
params = { params = {
'uid': self.openid, 'uid': self.openid,
'access_token': self.access_token 'access_token': self.access_token
@ -127,14 +242,17 @@ class WBOauthManager(BaseOauthManager):
try: try:
datas = json.loads(rsp) datas = json.loads(rsp)
user = OAuthUser() user = OAuthUser()
user.metadata = rsp user.metadata = rsp # lrj保存原始响应数据
user.picture = datas['avatar_large'] user.picture = datas['avatar_large'] # lrj用户头像
user.nickname = datas['screen_name'] user.nickname = datas['screen_name'] # lrj用户昵称
user.openid = datas['id'] user.openid = datas['id'] # lrj微博用户ID
user.type = 'weibo' user.type = 'weibo' # lrj平台类型
user.token = self.access_token user.token = self.access_token # lrj访问令牌
# lrj可选邮箱信息
if 'email' in datas and datas['email']: if 'email' in datas and datas['email']:
user.email = datas['email'] user.email = datas['email']
return user return user
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
@ -142,12 +260,27 @@ class WBOauthManager(BaseOauthManager):
return None return None
def get_picture(self, metadata): def get_picture(self, metadata):
"""lrj
从微博用户元数据中提取头像URL
Args:
metadata: 微博用户元数据JSON字符串
Returns:
str: 头像URL
"""
datas = json.loads(metadata) datas = json.loads(metadata)
return datas['avatar_large'] return datas['avatar_large']
class ProxyManagerMixin: class ProxyManagerMixin:
"""lrj
代理管理器混入类
为需要代理访问的OAuth平台提供代理支持
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""lrj初始化代理配置"""
if os.environ.get("HTTP_PROXY"): if os.environ.get("HTTP_PROXY"):
self.proxies = { self.proxies = {
"http": os.environ.get("HTTP_PROXY"), "http": os.environ.get("HTTP_PROXY"),
@ -157,239 +290,34 @@ class ProxyManagerMixin:
self.proxies = None self.proxies = None
def do_get(self, url, params, headers=None): def do_get(self, url, params, headers=None):
"""lrj带代理的GET请求"""
rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies) rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies)
logger.info(rsp.text) logger.info(rsp.text)
return rsp.text return rsp.text
def do_post(self, url, params, headers=None): def do_post(self, url, params, headers=None):
"""lrj带代理的POST请求"""
rsp = requests.post(url, params, headers=headers, proxies=self.proxies) rsp = requests.post(url, params, headers=headers, proxies=self.proxies)
logger.info(rsp.text) logger.info(rsp.text)
return rsp.text return rsp.text
class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
ICON_NAME = 'google'
def __init__(self, access_token=None, openid=None):
config = self.get_config()
self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else ''
self.callback_url = config.callback_url if config else ''
super(
GoogleOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, nexturl='/'):
params = {
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': self.callback_url,
'scope': 'openid email',
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
def get_access_token_by_code(self, code):
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': self.callback_url
}
rsp = self.do_post(self.TOKEN_URL, params)
obj = json.loads(rsp)
if 'access_token' in obj: # lrj它们的结构类似都是继承BaseOauthManager并实现抽象方法
self.access_token = str(obj['access_token'])
self.openid = str(obj['id_token'])
logger.info(self.ICON_NAME + ' oauth ' + rsp)
return self.access_token
else:
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self): class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
if not self.is_authorized: """Google OAuth管理器实现"""
return None #lrj ... 实现细节类似微博管理器
params = {
'access_token': self.access_token
}
rsp = self.do_get(self.API_URL, params)
try:
datas = json.loads(rsp)
user = OAuthUser()
user.metadata = rsp
user.picture = datas['picture']
user.nickname = datas['name']
user.openid = datas['sub']
user.token = self.access_token
user.type = 'google'
if datas['email']:
user.email = datas['email']
return user
except Exception as e:
logger.error(e)
logger.error('google oauth error.rsp:' + rsp)
return None
def get_picture(self, metadata):
datas = json.loads(metadata)
return datas['picture']
class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
AUTH_URL = 'https://github.com/login/oauth/authorize' """GitHub OAuth管理器实现"""
TOKEN_URL = 'https://github.com/login/oauth/access_token' # lrj... 实现细节类似微博管理器
API_URL = 'https://api.github.com/user'
ICON_NAME = 'github'
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 ''
super(
GitHubOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, next_url='/'):
params = {
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': f'{self.callback_url}&next_url={next_url}',
'scope': 'user'
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
def get_access_token_by_code(self, code):
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'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)
if 'access_token' in r:
self.access_token = (r['access_token'][0])
return self.access_token
else:
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self):
rsp = self.do_get(self.API_URL, params={}, headers={
"Authorization": "token " + self.access_token
})
try:
datas = json.loads(rsp)
user = OAuthUser()
user.picture = datas['avatar_url']
user.nickname = datas['name']
user.openid = datas['id']
user.type = 'github'
user.token = self.access_token
user.metadata = rsp
if 'email' in datas and datas['email']:
user.email = datas['email']
return user
except Exception as e:
logger.error(e)
logger.error('github oauth error.rsp:' + rsp)
return None
def get_picture(self, metadata):
datas = json.loads(metadata)
return datas['avatar_url']
class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth' """Facebook OAuth管理器实现"""
TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token' # lrj... 实现细节类似微博管理器
API_URL = 'https://graph.facebook.com/me'
ICON_NAME = 'facebook'
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 ''
super(
FaceBookOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, next_url='/'):
params = {
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': self.callback_url,
'scope': 'email,public_profile'
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
def get_access_token_by_code(self, code):
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
# 'grant_type': 'authorization_code',
'code': code,
'redirect_uri': self.callback_url
}
rsp = self.do_post(self.TOKEN_URL, params)
obj = json.loads(rsp)
if 'access_token' in obj:
token = str(obj['access_token'])
self.access_token = token
return self.access_token
else:
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self):
params = {
'access_token': self.access_token,
'fields': 'id,name,picture,email'
}
try:
rsp = self.do_get(self.API_URL, params)
datas = json.loads(rsp)
user = OAuthUser()
user.nickname = datas['name']
user.openid = datas['id']
user.type = 'facebook'
user.token = self.access_token
user.metadata = rsp
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'])
return user
except Exception as e:
logger.error(e)
return None
def get_picture(self, metadata):
datas = json.loads(metadata)
return str(datas['picture']['data']['url'])
class QQOauthManager(BaseOauthManager): class QQOauthManager(BaseOauthManager):
@ -483,22 +411,41 @@ class QQOauthManager(BaseOauthManager):
@cache_decorator(expiration=100 * 60) @cache_decorator(expiration=100 * 60)
def get_oauth_apps(): def get_oauth_apps():
"""lrj
获取所有启用的OAuth应用列表带缓存
Returns:
list: OAuth管理器实例列表
"""
configs = OAuthConfig.objects.filter(is_enable=True).all() configs = OAuthConfig.objects.filter(is_enable=True).all()
if not configs: if not configs:
return [] return []
# lrj提取已启用平台的类型列表
configtypes = [x.type for x in configs] configtypes = [x.type for x in configs]
# lrj获取所有OAuth管理器子类
applications = BaseOauthManager.__subclasses__() applications = BaseOauthManager.__subclasses__()
#lrj 过滤出已配置的平台管理器
apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes] apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes]
return apps return apps
def get_manager_by_type(type): def get_manager_by_type(type):
"""lrj
根据平台类型获取对应的OAuth管理器
Args:
type: 平台类型字符串'weibo', 'github'
Returns:
BaseOauthManager: 对应的管理器实例或None
"""
applications = get_oauth_apps() applications = get_oauth_apps()
if applications: if applications:
finds = list( finds = list(filter(lambda x: x.ICON_NAME.lower() == type.lower(), applications))
filter(
lambda x: x.ICON_NAME.lower() == type.lower(),
applications))
if finds: if finds:
return finds[0] return finds[0]
return None return None

@ -1,3 +1,8 @@
"""lrj
OAuth 认证测试模块
包含OAuth配置登录流程和各平台OAuth功能的单元测试
"""
import json import json
from unittest.mock import patch from unittest.mock import patch
@ -13,43 +18,63 @@ from oauth.oauthmanager import BaseOauthManager
# Create your tests here. # Create your tests here.
class OAuthConfigTest(TestCase): class OAuthConfigTest(TestCase):
"""lrj
OAuth配置模型测试类
测试OAuth配置相关的功能
"""
def setUp(self): def setUp(self):
self.client = Client() """lrj测试初始化设置"""
self.factory = RequestFactory() self.client = Client() # lrjDjango测试客户端
self.factory = RequestFactory() # lrj请求工厂
def test_oauth_login_test(self): def test_oauth_login_test(self):
"""lrj测试OAuth登录流程"""
# lrj创建微博OAuth配置
c = OAuthConfig() c = OAuthConfig()
c.type = 'weibo' c.type = 'weibo'
c.appkey = 'appkey' c.appkey = 'appkey'
c.appsecret = 'appsecret' c.appsecret = 'appsecret'
c.save() c.save()
# lrj测试OAuth登录重定向
response = self.client.get('/oauth/oauthlogin?type=weibo') response = self.client.get('/oauth/oauthlogin?type=weibo')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302) # lrj验证重定向状态码
self.assertTrue("api.weibo.com" in response.url) self.assertTrue("api.weibo.com" in response.url) # lrj验证重定向到微博授权页面
# lrj测试授权回调处理
response = self.client.get('/oauth/authorize?type=weibo&code=code') response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302) # lrj验证重定向状态码
self.assertEqual(response.url, '/') self.assertEqual(response.url, '/') # lrj验证重定向到首页
class OauthLoginTest(TestCase): class OauthLoginTest(TestCase):
"""lrj
OAuth登录流程测试类
测试各平台OAuth登录的完整流程
"""
def setUp(self) -> None: def setUp(self) -> None:
self.client = Client() """lrj测试初始化设置"""
self.factory = RequestFactory() self.client = Client() # lrjDjango测试客户端
self.apps = self.init_apps() self.factory = RequestFactory() # lrj请求工厂
self.apps = self.init_apps() # lrj初始化所有OAuth应用配置
def init_apps(self): def init_apps(self):
"""lrj初始化所有OAuth平台配置"""
# lrj获取所有OAuth管理器子类并实例化
applications = [p() for p in BaseOauthManager.__subclasses__()] applications = [p() for p in BaseOauthManager.__subclasses__()]
for application in applications: for application in applications:
# lrj为每个平台创建配置
c = OAuthConfig() c = OAuthConfig()
c.type = application.ICON_NAME.lower() c.type = application.ICON_NAME.lower() # lrj平台类型
c.appkey = 'appkey' c.appkey = 'appkey' # lrj测试用AppKey
c.appsecret = 'appsecret' c.appsecret = 'appsecret' # lrj测试用AppSecret
c.save() c.save()
return applications return applications
def get_app_by_type(self, type): def get_app_by_type(self, type):
"""lrj根据平台类型获取对应的OAuth应用"""
for app in self.apps: for app in self.apps:
if app.ICON_NAME.lower() == type: if app.ICON_NAME.lower() == type:
return app return app
@ -57,9 +82,15 @@ class OauthLoginTest(TestCase):
@patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get") @patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_login(self, mock_do_get, mock_do_post): def test_weibo_login(self, mock_do_get, mock_do_post):
"""lrj测试微博OAuth登录流程"""
# lrj获取微博OAuth应用
weibo_app = self.get_app_by_type('weibo') weibo_app = self.get_app_by_type('weibo')
assert weibo_app assert weibo_app #lrj 验证应用存在
# lrj测试授权URL生成
url = weibo_app.get_authorization_url() url = weibo_app.get_authorization_url()
# lrj模拟微博API响应
mock_do_post.return_value = json.dumps({"access_token": "access_token", mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid" "uid": "uid"
}) })
@ -69,16 +100,24 @@ class OauthLoginTest(TestCase):
"id": "id", "id": "id",
"email": "email", "email": "email",
}) })
# lrj测试获取访问令牌和用户信息
userinfo = weibo_app.get_access_token_by_code('code') userinfo = weibo_app.get_access_token_by_code('code')
self.assertEqual(userinfo.token, 'access_token') self.assertEqual(userinfo.token, 'access_token') # lrj验证访问令牌
self.assertEqual(userinfo.openid, 'id') self.assertEqual(userinfo.openid, 'id') #lrj 验证用户OpenID
@patch("oauth.oauthmanager.GoogleOauthManager.do_post") @patch("oauth.oauthmanager.GoogleOauthManager.do_post")
@patch("oauth.oauthmanager.GoogleOauthManager.do_get") @patch("oauth.oauthmanager.GoogleOauthManager.do_get")
def test_google_login(self, mock_do_get, mock_do_post): def test_google_login(self, mock_do_get, mock_do_post):
"""lrj测试Google OAuth登录流程"""
# lrj获取Google OAuth应用
google_app = self.get_app_by_type('google') google_app = self.get_app_by_type('google')
assert google_app assert google_app # lrj验证应用存在
# lrj测试授权URL生成
url = google_app.get_authorization_url() url = google_app.get_authorization_url()
# lrj模拟Google API响应
mock_do_post.return_value = json.dumps({ mock_do_post.return_value = json.dumps({
"access_token": "access_token", "access_token": "access_token",
"id_token": "id_token", "id_token": "id_token",
@ -91,17 +130,23 @@ class OauthLoginTest(TestCase):
}) })
token = google_app.get_access_token_by_code('code') token = google_app.get_access_token_by_code('code')
userinfo = google_app.get_oauth_userinfo() userinfo = google_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token') self.assertEqual(userinfo.token, 'access_token') # lrj验证访问令牌
self.assertEqual(userinfo.openid, 'sub') self.assertEqual(userinfo.openid, 'sub') # lrj验证用户OpenID
@patch("oauth.oauthmanager.GitHubOauthManager.do_post") @patch("oauth.oauthmanager.GitHubOauthManager.do_post")
@patch("oauth.oauthmanager.GitHubOauthManager.do_get") @patch("oauth.oauthmanager.GitHubOauthManager.do_get")
def test_github_login(self, mock_do_get, mock_do_post): def test_github_login(self, mock_do_get, mock_do_post):
"""测试GitHub OAuth登录流程"""
# lrj获取GitHub OAuth应用
github_app = self.get_app_by_type('github') github_app = self.get_app_by_type('github')
assert github_app assert github_app # lrj验证应用存在
# lrj测试授权URL生成
url = github_app.get_authorization_url() url = github_app.get_authorization_url()
self.assertTrue("github.com" in url) self.assertTrue("github.com" in url) # lrj验证GitHub域名
self.assertTrue("client_id" in url) self.assertTrue("client_id" in url) #lrj 验证包含client_id参数
# lrj模拟GitHub API响应
mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer" mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer"
mock_do_get.return_value = json.dumps({ mock_do_get.return_value = json.dumps({
"avatar_url": "avatar_url", "avatar_url": "avatar_url",
@ -111,16 +156,22 @@ class OauthLoginTest(TestCase):
}) })
token = github_app.get_access_token_by_code('code') token = github_app.get_access_token_by_code('code')
userinfo = github_app.get_oauth_userinfo() userinfo = github_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') # lrj验证访问令牌
self.assertEqual(userinfo.openid, 'id') self.assertEqual(userinfo.openid, 'id') # lrj验证用户OpenID
@patch("oauth.oauthmanager.FaceBookOauthManager.do_post") @patch("oauth.oauthmanager.FaceBookOauthManager.do_post")
@patch("oauth.oauthmanager.FaceBookOauthManager.do_get") @patch("oauth.oauthmanager.FaceBookOauthManager.do_get")
def test_facebook_login(self, mock_do_get, mock_do_post): def test_facebook_login(self, mock_do_get, mock_do_post):
"""lrj测试Facebook OAuth登录流程"""
# lrj获取Facebook OAuth应用
facebook_app = self.get_app_by_type('facebook') facebook_app = self.get_app_by_type('facebook')
assert facebook_app assert facebook_app # lrj验证应用存在
# lrj测试授权URL生成
url = facebook_app.get_authorization_url() url = facebook_app.get_authorization_url()
self.assertTrue("facebook.com" in url) self.assertTrue("facebook.com" in url) # lrj验证Facebook域名
# lrj模拟Facebook API响应
mock_do_post.return_value = json.dumps({ mock_do_post.return_value = json.dumps({
"access_token": "access_token", "access_token": "access_token",
}) })
@ -136,31 +187,38 @@ class OauthLoginTest(TestCase):
}) })
token = facebook_app.get_access_token_by_code('code') token = facebook_app.get_access_token_by_code('code')
userinfo = facebook_app.get_oauth_userinfo() userinfo = facebook_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token') self.assertEqual(userinfo.token, 'access_token') # lrj验证访问令牌
@patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[ @patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[
'access_token=access_token&expires_in=3600', 'access_token=access_token&expires_in=3600', # lrj获取token响应
'callback({"client_id":"appid","openid":"openid"} );', 'callback({"client_id":"appid","openid":"openid"} );', # lrj获取openid响应
json.dumps({ json.dumps({
"nickname": "nickname", "nickname": "nickname",
"email": "email", "email": "email",
"figureurl": "figureurl", "figureurl": "figureurl",
"openid": "openid", "openid": "openid",
}) }) # lrj获取用户信息响应
]) ])
def test_qq_login(self, mock_do_get): def test_qq_login(self, mock_do_get):
"""lrj测试QQ OAuth登录流程"""
# lrj获取QQ OAuth应用
qq_app = self.get_app_by_type('qq') qq_app = self.get_app_by_type('qq')
assert qq_app assert qq_app # lrj验证应用存在
# lrj测试授权URL生成
url = qq_app.get_authorization_url() url = qq_app.get_authorization_url()
self.assertTrue("qq.com" in url) self.assertTrue("qq.com" in url) # lrj验证QQ域名
# lrj测试获取访问令牌和用户信息使用side_effect模拟多次调用
token = qq_app.get_access_token_by_code('code') token = qq_app.get_access_token_by_code('code')
userinfo = qq_app.get_oauth_userinfo() userinfo = qq_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token') self.assertEqual(userinfo.token, 'access_token') # lrj验证访问令牌
@patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get") @patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post): def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post):
"""lrj测试带邮箱的微博授权登录完整流程"""
# lrj模拟微博API响应
mock_do_post.return_value = json.dumps({"access_token": "access_token", mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid" "uid": "uid"
}) })
@ -172,35 +230,43 @@ class OauthLoginTest(TestCase):
} }
mock_do_get.return_value = json.dumps(mock_user_info) mock_do_get.return_value = json.dumps(mock_user_info)
# lrj测试OAuth登录重定向
response = self.client.get('/oauth/oauthlogin?type=weibo') response = self.client.get('/oauth/oauthlogin?type=weibo')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302) # lrj验证重定向状态码
self.assertTrue("api.weibo.com" in response.url) self.assertTrue("api.weibo.com" in response.url) # lrj验证重定向到微博
#lrj 测试授权回调处理
response = self.client.get('/oauth/authorize?type=weibo&code=code') response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302) #lrj 验证重定向状态码
self.assertEqual(response.url, '/') self.assertEqual(response.url, '/') # lrj验证重定向到首页
# lrj验证用户已登录
user = auth.get_user(self.client) user = auth.get_user(self.client)
assert user.is_authenticated assert user.is_authenticated # lrj验证用户已认证
self.assertTrue(user.is_authenticated) self.assertTrue(user.is_authenticated)
self.assertEqual(user.username, mock_user_info['screen_name']) self.assertEqual(user.username, mock_user_info['screen_name']) # lrj验证用户名
self.assertEqual(user.email, mock_user_info['email']) self.assertEqual(user.email, mock_user_info['email']) # lrj验证邮箱
# lrj登出用户
self.client.logout() self.client.logout()
# lrj再次测试登录测试重复登录情况
response = self.client.get('/oauth/authorize?type=weibo&code=code') response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302) # lrj验证重定向状态码
self.assertEqual(response.url, '/') self.assertEqual(response.url, '/') # lrj证重定向到首页
# lrj再次验证用户已登录
user = auth.get_user(self.client) user = auth.get_user(self.client)
assert user.is_authenticated assert user.is_authenticated # lrj验证用户已认证
self.assertTrue(user.is_authenticated) self.assertTrue(user.is_authenticated)
self.assertEqual(user.username, mock_user_info['screen_name']) self.assertEqual(user.username, mock_user_info['screen_name']) # lrj验证用户名
self.assertEqual(user.email, mock_user_info['email']) self.assertEqual(user.email, mock_user_info['email']) # lrj验证邮箱
@patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get") @patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post): def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post):
"""lrj测试不带邮箱的微博授权登录完整流程需要补充邮箱"""
# lrj模拟微博API响应不含邮箱
mock_do_post.return_value = json.dumps({"access_token": "access_token", mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid" "uid": "uid"
}) })
@ -211,39 +277,48 @@ class OauthLoginTest(TestCase):
} }
mock_do_get.return_value = json.dumps(mock_user_info) mock_do_get.return_value = json.dumps(mock_user_info)
# lrj测试OAuth登录重定向
response = self.client.get('/oauth/oauthlogin?type=weibo') response = self.client.get('/oauth/oauthlogin?type=weibo')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302) # lrj验证重定向状态码
self.assertTrue("api.weibo.com" in response.url) self.assertTrue("api.weibo.com" in response.url) # lrj验证重定向到微博
# lrj测试授权回调处理应该重定向到邮箱补充页面
response = self.client.get('/oauth/authorize?type=weibo&code=code') response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302) # lrj验证重定向状态码
self.assertEqual(response.status_code, 302) # lrj解析OAuth用户ID
oauth_user_id = int(response.url.split('/')[-1].split('.')[0]) oauth_user_id = int(response.url.split('/')[-1].split('.')[0])
self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html') self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html') #lrj 验证重定向到邮箱补充页面
# lrj测试邮箱补充表单提交
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})
self.assertEqual(response.status_code, 302) # lrj验证重定向状态码
self.assertEqual(response.status_code, 302) #lrj 生成安全签名
sign = get_sha256(settings.SECRET_KEY + sign = get_sha256(settings.SECRET_KEY +
str(oauth_user_id) + settings.SECRET_KEY) str(oauth_user_id) + settings.SECRET_KEY)
# lrj验证重定向到绑定成功页面
url = reverse('oauth:bindsuccess', kwargs={ url = reverse('oauth:bindsuccess', kwargs={
'oauthid': oauth_user_id, 'oauthid': oauth_user_id,
}) })
self.assertEqual(response.url, f'{url}?type=email') self.assertEqual(response.url, f'{url}?type=email')
# lrj测试邮箱确认链接
path = reverse('oauth:email_confirm', kwargs={ path = reverse('oauth:email_confirm', kwargs={
'id': oauth_user_id, 'id': oauth_user_id,
'sign': sign 'sign': sign
}) })
response = self.client.get(path) response = self.client.get(path)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302) #lrj 验证重定向状态码
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') #lrj 验证重定向到绑定成功页面
# lrj验证用户已登录
user = auth.get_user(self.client) user = auth.get_user(self.client)
from oauth.models import OAuthUser from oauth.models import OAuthUser
oauth_user = OAuthUser.objects.get(author=user) oauth_user = OAuthUser.objects.get(author=user)
self.assertTrue(user.is_authenticated) self.assertTrue(user.is_authenticated) # lrj验证用户已认证
self.assertEqual(user.username, mock_user_info['screen_name']) self.assertEqual(user.username, mock_user_info['screen_name']) # lrj验证用户名
self.assertEqual(user.email, 'test@gmail.com') self.assertEqual(user.email, 'test@gmail.com') #lrj 验证补充的邮箱
self.assertEqual(oauth_user.pk, oauth_user_id) self.assertEqual(oauth_user.pk, oauth_user_id) # lrj验证OAuth用户ID匹配

@ -1,25 +1,54 @@
"""lrj
OAuth 认证URL路由配置模块
定义OAuth认证相关的URL路由和视图映射关系
"""
from django.urls import path from django.urls import path
from . import views from . import views
# lrj应用命名空间用于URL反向解析时区分不同应用的同名URL
app_name = "oauth" app_name = "oauth"
# lrjURL模式配置列表
urlpatterns = [ urlpatterns = [
#lrj OAuth授权回调处理URL
# lrj路径/oauth/authorize
# lrj处理第三方平台回调完成用户认证和账号绑定
path( path(
r'oauth/authorize', r'oauth/authorize', # lrjURL路径模式
views.authorize), views.authorize), # lrj对应的视图函数
# lrj邮箱补充页面URL
#lrj 路径:/oauth/requireemail/<oauthid>.html
# lrj当OAuth用户没有邮箱时显示表单要求用户补充邮箱信息
path( path(
r'oauth/requireemail/<int:oauthid>.html', r'oauth/requireemail/<int:oauthid>.html', # lrjURL路径模式包含整数类型的oauthid参数
views.RequireEmailView.as_view(), views.RequireEmailView.as_view(), #lrj 对应的类视图使用as_view()方法)
name='require_email'), name='require_email'), # lrjURL名称用于反向解析
# lrj邮箱确认链接URL
# lrj路径/oauth/emailconfirm/<id>/<sign>.html
# lrj用户点击邮件中的确认链接完成邮箱绑定和用户登录
path( path(
r'oauth/emailconfirm/<int:id>/<sign>.html', r'oauth/emailconfirm/<int:id>/<sign>.html', # lrjURL路径模式包含整数类型的id参数和字符串类型的sign签名参数
views.emailconfirm, views.emailconfirm, #lrj 对应的视图函数
name='email_confirm'), name='email_confirm'), # lrjURL名称用于反向解析
# lrj绑定成功提示页面URL
# lrj路径/oauth/bindsuccess/<oauthid>.html
# lrj显示绑定成功或等待确认的提示信息
path( path(
r'oauth/bindsuccess/<int:oauthid>.html', r'oauth/bindsuccess/<int:oauthid>.html', #lrj URL路径模式包含整数类型的oauthid参数
views.bindsuccess, views.bindsuccess, # lrj对应的视图函数
name='bindsuccess'), name='bindsuccess'), # lrjURL名称用于反向解析
# lrjOAuth登录入口URL
#lrj 路径:/oauth/oauthlogin
#lrj 重定向用户到第三方平台的授权页面
path( path(
r'oauth/oauthlogin', r'oauth/oauthlogin', # lrjURL路径模式
views.oauthlogin, views.oauthlogin, #lrj 对应的视图函数
name='oauthlogin')] name='oauthlogin') # lrjURL名称用于反向解析
]

@ -1,39 +1,55 @@
"""lrj
OAuth 认证视图模块
处理第三方登录的完整流程授权回调用户绑定邮件验证等
"""
import logging import logging
# Create your views here.
from urllib.parse import urlparse from urllib.parse import urlparse
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model, login
from django.contrib.auth import login
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction from django.db import transaction
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView from django.views.generic import FormView
from djangoblog.blog_signals import oauth_user_login_signal from djangoblog.blog_signals import oauth_user_login_signal
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site, get_sha256, send_email
from djangoblog.utils import send_email, get_sha256
from oauth.forms import RequireEmailForm from oauth.forms import RequireEmailForm
from .models import OAuthUser from .models import OAuthUser
from .oauthmanager import get_manager_by_type, OAuthAccessTokenException from .oauthmanager import OAuthAccessTokenException, get_manager_by_type
# lrj配置日志记录器
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_redirecturl(request): def get_redirecturl(request):
"""lrj
获取安全的重定向URL防止开放重定向漏洞
Args:
request: Django请求对象
Returns:
str: 安全的跳转URL
"""
nexturl = request.GET.get('next_url', None) nexturl = request.GET.get('next_url', None)
# lrj处理特殊路径避免登录循环
if not nexturl or nexturl == '/login/' or nexturl == '/login': if not nexturl or nexturl == '/login/' or nexturl == '/login':
nexturl = '/' nexturl = '/'
return nexturl return nexturl
# lrj解析URL并验证域名安全性
p = urlparse(nexturl) p = urlparse(nexturl)
if p.netloc: if p.netloc:
site = get_current_site().domain site = get_current_site().domain
# 比较域名忽略www前缀防止跨站重定向
if not p.netloc.replace('www.', '') == site.replace('www.', ''): if not p.netloc.replace('www.', '') == site.replace('www.', ''):
logger.info('非法url:' + nexturl) logger.info('非法url:' + nexturl)
return "/" return "/"
@ -41,26 +57,54 @@ def get_redirecturl(request):
def oauthlogin(request): def oauthlogin(request):
"""lrj
OAuth登录入口视图
重定向用户到第三方平台的授权页面
Args:
request: Django请求对象
Returns:
HttpResponseRedirect: 重定向到第三方授权页面或首页
"""
type = request.GET.get('type', None) type = request.GET.get('type', None)
if not type: if not type:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
# lrj根据平台类型获取对应的OAuth管理器
manager = get_manager_by_type(type) manager = get_manager_by_type(type)
if not manager: if not manager:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
#lrj 获取安全的重定向URL并生成授权页面URL
nexturl = get_redirecturl(request) nexturl = get_redirecturl(request)
authorizeurl = manager.get_authorization_url(nexturl) authorizeurl = manager.get_authorization_url(nexturl)
return HttpResponseRedirect(authorizeurl) return HttpResponseRedirect(authorizeurl)
def authorize(request): def authorize(request):
"""lrj
OAuth授权回调处理视图
处理第三方平台回调完成用户认证和账号绑定
Args:
request: Django请求对象
Returns:
HttpResponseRedirect: 重定向到相应页面
"""
type = request.GET.get('type', None) type = request.GET.get('type', None)
if not type: if not type:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
manager = get_manager_by_type(type) manager = get_manager_by_type(type)
if not manager: if not manager:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
# lrj获取授权码
code = request.GET.get('code', None) code = request.GET.get('code', None)
try: try:
# lrj使用授权码获取访问令牌
rsp = manager.get_access_token_by_code(code) rsp = manager.get_access_token_by_code(code)
except OAuthAccessTokenException as e: except OAuthAccessTokenException as e:
logger.warning("OAuthAccessTokenException:" + str(e)) logger.warning("OAuthAccessTokenException:" + str(e))
@ -68,74 +112,109 @@ def authorize(request):
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
rsp = None rsp = None
nexturl = get_redirecturl(request) nexturl = get_redirecturl(request)
if not rsp: if not rsp:
# lrj获取令牌失败重新跳转到授权页面
return HttpResponseRedirect(manager.get_authorization_url(nexturl)) return HttpResponseRedirect(manager.get_authorization_url(nexturl))
#lrj 获取用户信息
user = manager.get_oauth_userinfo() user = manager.get_oauth_userinfo()
if user: if user:
# lrj处理空昵称情况
if not user.nickname or not user.nickname.strip(): if not user.nickname or not user.nickname.strip():
user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
try: try:
# lrj检查是否已存在该OAuth用户
temp = OAuthUser.objects.get(type=type, openid=user.openid) temp = OAuthUser.objects.get(type=type, openid=user.openid)
#lrj 更新用户信息
temp.picture = user.picture temp.picture = user.picture
temp.metadata = user.metadata temp.metadata = user.metadata
temp.nickname = user.nickname temp.nickname = user.nickname
user = temp user = temp
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass # lrj新用户继续处理
# facebook的token过长
#lrj Facebook的token过长特殊处理
if type == 'facebook': if type == 'facebook':
user.token = '' user.token = ''
# lrj如果用户有邮箱直接完成绑定和登录
if user.email: if user.email:
with transaction.atomic(): with transaction.atomic(): # lrj使用事务保证数据一致性
author = None author = None
try: try:
author = get_user_model().objects.get(id=user.author_id) author = get_user_model().objects.get(id=user.author_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
if not author: if not author:
# lrj创建或获取用户账号
result = get_user_model().objects.get_or_create(email=user.email) result = get_user_model().objects.get_or_create(email=user.email)
author = result[0] author = result[0]
if result[1]: if result[1]: # lrj是新创建的用户
try: try:
#lrj 检查用户名是否已存在
get_user_model().objects.get(username=user.nickname) get_user_model().objects.get(username=user.nickname)
except ObjectDoesNotExist: except ObjectDoesNotExist:
author.username = user.nickname author.username = user.nickname
else: else:
# lrj用户名冲突生成唯一用户名
author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
author.source = 'authorize' author.source = 'authorize'
author.save() author.save()
# lrj关联OAuth用户和本站用户
user.author = author user.author = author
user.save() user.save()
# lrj发送登录信号
oauth_user_login_signal.send( oauth_user_login_signal.send(
sender=authorize.__class__, id=user.id) sender=authorize.__class__, id=user.id)
# lrj登录用户
login(request, author) login(request, author)
return HttpResponseRedirect(nexturl) return HttpResponseRedirect(nexturl)
else: else:
#lrj 没有邮箱,需要用户补充邮箱信息
user.save() user.save()
url = reverse('oauth:require_email', kwargs={ url = reverse('oauth:require_email', kwargs={
'oauthid': user.id 'oauthid': user.id
}) })
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
return HttpResponseRedirect(nexturl) return HttpResponseRedirect(nexturl)
def emailconfirm(request, id, sign): def emailconfirm(request, id, sign):
"""lrj
邮箱确认视图
通过邮件链接完成邮箱绑定和用户登录
Args:
request: Django请求对象
id: OAuth用户ID
sign: 安全签名
Returns:
HttpResponse: 重定向或错误响应
"""
if not sign: if not sign:
return HttpResponseForbidden() return HttpResponseForbidden()
#lrj 验证签名安全性
if not get_sha256(settings.SECRET_KEY + if not get_sha256(settings.SECRET_KEY +
str(id) + str(id) +
settings.SECRET_KEY).upper() == sign.upper(): settings.SECRET_KEY).upper() == sign.upper():
return HttpResponseForbidden() return HttpResponseForbidden()
oauthuser = get_object_or_404(OAuthUser, pk=id) oauthuser = get_object_or_404(OAuthUser, pk=id)
with transaction.atomic(): with transaction.atomic():
if oauthuser.author: if oauthuser.author:
author = get_user_model().objects.get(pk=oauthuser.author_id) author = get_user_model().objects.get(pk=oauthuser.author_id)
else: else:
# lrj创建新用户账号
result = get_user_model().objects.get_or_create(email=oauthuser.email) result = get_user_model().objects.get_or_create(email=oauthuser.email)
author = result[0] author = result[0]
if result[1]: if result[1]:
@ -143,13 +222,20 @@ def emailconfirm(request, id, sign):
author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip( author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip(
) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') ) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
author.save() author.save()
# lrj完成绑定
oauthuser.author = author oauthuser.author = author
oauthuser.save() oauthuser.save()
# lrj发送登录信号
oauth_user_login_signal.send( oauth_user_login_signal.send(
sender=emailconfirm.__class__, sender=emailconfirm.__class__,
id=oauthuser.id) id=oauthuser.id)
# lrj登录用户
login(request, author) login(request, author)
#lrj 发送绑定成功邮件
site = 'http://' + get_current_site().domain site = 'http://' + get_current_site().domain
content = _(''' content = _('''
<p>Congratulations, you have successfully bound your email address. You can use <p>Congratulations, you have successfully bound your email address. You can use
@ -163,6 +249,8 @@ def emailconfirm(request, id, sign):
''') % {'oauthuser_type': oauthuser.type, 'site': site} ''') % {'oauthuser_type': oauthuser.type, 'site': site}
send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content) send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content)
# 跳转到绑定成功页面
url = reverse('oauth:bindsuccess', kwargs={ url = reverse('oauth:bindsuccess', kwargs={
'oauthid': id 'oauthid': id
}) })
@ -171,19 +259,27 @@ def emailconfirm(request, id, sign):
class RequireEmailView(FormView): class RequireEmailView(FormView):
"""lrj
要求邮箱表单视图
处理用户补充邮箱信息的流程
"""
form_class = RequireEmailForm form_class = RequireEmailForm
template_name = 'oauth/require_email.html' template_name = 'oauth/require_email.html'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""lrjGET请求处理"""
oauthid = self.kwargs['oauthid'] oauthid = self.kwargs['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid) oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
# lrj如果已有邮箱可能直接跳转当前注释掉了
if oauthuser.email: if oauthuser.email:
pass pass
# return HttpResponseRedirect('/') # lrjreturn HttpResponseRedirect('/')
return super(RequireEmailView, self).get(request, *args, **kwargs) return super(RequireEmailView, self).get(request, *args, **kwargs)
def get_initial(self): def get_initial(self):
"""lrj设置表单初始值"""
oauthid = self.kwargs['oauthid'] oauthid = self.kwargs['oauthid']
return { return {
'email': '', 'email': '',
@ -191,6 +287,7 @@ class RequireEmailView(FormView):
} }
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""lrj添加上下文数据"""
oauthid = self.kwargs['oauthid'] oauthid = self.kwargs['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid) oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
if oauthuser.picture: if oauthuser.picture:
@ -198,13 +295,18 @@ class RequireEmailView(FormView):
return super(RequireEmailView, self).get_context_data(**kwargs) return super(RequireEmailView, self).get_context_data(**kwargs)
def form_valid(self, form): def form_valid(self, form):
"""lrj表单验证通过后的处理"""
email = form.cleaned_data['email'] email = form.cleaned_data['email']
oauthid = form.cleaned_data['oauthid'] oauthid = form.cleaned_data['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid) oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
oauthuser.email = email oauthuser.email = email
oauthuser.save() oauthuser.save()
# lrj生成安全签名
sign = get_sha256(settings.SECRET_KEY + sign = get_sha256(settings.SECRET_KEY +
str(oauthuser.id) + settings.SECRET_KEY) str(oauthuser.id) + settings.SECRET_KEY)
# lrj构建确认链接
site = get_current_site().domain site = get_current_site().domain
if settings.DEBUG: if settings.DEBUG:
site = '127.0.0.1:8000' site = '127.0.0.1:8000'
@ -214,6 +316,7 @@ class RequireEmailView(FormView):
}) })
url = "http://{site}{path}".format(site=site, path=path) url = "http://{site}{path}".format(site=site, path=path)
# lrj发送确认邮件
content = _(""" content = _("""
<p>Please click the link below to bind your email</p> <p>Please click the link below to bind your email</p>
@ -226,6 +329,8 @@ class RequireEmailView(FormView):
%(url)s %(url)s
""") % {'url': url} """) % {'url': url}
send_email(emailto=[email, ], title=_('Bind your email'), content=content) send_email(emailto=[email, ], title=_('Bind your email'), content=content)
# lrj跳转到绑定成功提示页面
url = reverse('oauth:bindsuccess', kwargs={ url = reverse('oauth:bindsuccess', kwargs={
'oauthid': oauthid 'oauthid': oauthid
}) })
@ -234,8 +339,20 @@ class RequireEmailView(FormView):
def bindsuccess(request, oauthid): def bindsuccess(request, oauthid):
"""lrj
绑定成功提示页面
Args:
request: Django请求对象
oauthid: OAuth用户ID
Returns:
HttpResponse: 渲染的绑定成功页面
"""
type = request.GET.get('type', None) type = request.GET.get('type', None)
oauthuser = get_object_or_404(OAuthUser, pk=oauthid) oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
#lrj 根据绑定类型显示不同内容
if type == 'email': if type == 'email':
title = _('Bind your email') title = _('Bind your email')
content = _( content = _(
@ -247,7 +364,9 @@ def bindsuccess(request, oauthid):
"Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s" "Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s"
" to directly log in to this website without a password. You are welcome to continue to follow this site." % { " to directly log in to this website without a password. You are welcome to continue to follow this site." % {
'oauthuser_type': oauthuser.type}) 'oauthuser_type': oauthuser.type})
return render(request, 'oauth/bindsuccess.html', { return render(request, 'oauth/bindsuccess.html', {
'title': title, 'title': title,
'content': content 'content': content
}) })

File diff suppressed because it is too large Load Diff

Binary file not shown.

@ -8,6 +8,16 @@
<div id="content" role="main"> <div id="content" role="main">
{% load_article_detail article False user %} {% load_article_detail article False user %}
<!-- 点赞按钮 -->
<button id="like-button" data-article-id="{{ article.pk }}"
class="btn {% if request.user in article.users_like.all %}btn-primary{% else %}btn-outline-primary{% endif %}">
{% if request.user in article.users_like.all %}
👍 已赞同 <span class="like-count">{{ article.users_like.count }}</span>
{% else %}
👍 赞同 <span class="like-count">{{ article.users_like.count }}</span>
{% endif %}
</button>
{% if article.type == 'a' %} {% if article.type == 'a' %}
<nav class="nav-single"> <nav class="nav-single">
<h3 class="assistive-text">文章导航</h3> <h3 class="assistive-text">文章导航</h3>
@ -45,6 +55,57 @@
{% endif %} {% endif %}
</div><!-- #primary --> </div><!-- #primary -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
$('#like-button').click(function() {
const articleId = $(this).data('article-id'); // 获取文章ID
const likeButton = $(this); // 获取按钮本身
const likeCountSpan = $('#like-count'); // 获取点赞数显示的span
// 禁用按钮防止重复点击
likeButton.prop('disabled', true).text('处理中...');
// 发送AJAX POST请求
$.ajax({
url: "{% url 'blog:like_article' %}", // 使用Django模板标签生成URL确保url的name是'like_article'
type: "POST",
data: {
'article_id': articleId,
'csrfmiddlewaretoken': '{{ csrf_token }}' // Django CSRF令牌必须携带
},
dataType: 'json',
success: function(response) {
if (response.state === 200) {
// 更新点赞数量
likeCountSpan.text(response.like_sum);
// 根据操作类型(点赞或取消)更新按钮样式
if (response.type === 1) {
likeButton.removeClass('btn-outline-primary').addClass('btn-primary')
.html('👍 已赞同 <span class="like-count">' + response.like_sum + '</span>');
} else {
likeButton.removeClass('btn-primary').addClass('btn-outline-primary')
.html('👍 赞同 <span class="like-count">' + response.like_sum + '</span>');
}
} else {
alert(response.data); // 处理错误信息,例如"文章不存在"
}
},
error: function(xhr, status, error) {
// 处理请求失败的情况,例如网络问题
console.error("AJAX request failed: " + status + ", " + error);
alert('操作失败,请稍后重试。');
},
complete: function() {
// 重新启用按钮
likeButton.prop('disabled', false);
}
});
});
});
</script>
{% endblock %} {% endblock %}
{% block sidebar %} {% block sidebar %}

@ -19,11 +19,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="format-detection" content="telephone=no"/> <meta name="format-detection" content="telephone=no"/>
<meta name="theme-color" content="#21759b"/> <meta name="theme-color" content="#21759b"/>
{% load blog_tags %} {% load blog_tags %}
{% head_meta %} {% head_meta %}
{% block header %} {% block header %}
<!-- SEO插件会自动生成title、description、keywords等标签 --> <!-- SEO插件会自动生成title、description、keywords等标签 -->
{% endblock %} {% endblock %}
<link rel="profile" href="http://gmpg.org/xfn/11"/> <link rel="profile" href="http://gmpg.org/xfn/11"/>
<!-- 资源提示和预加载优化 --> <!-- 资源提示和预加载优化 -->
@ -31,7 +34,6 @@
<link rel="dns-prefetch" href="//cdn.jsdelivr.net"/> <link rel="dns-prefetch" href="//cdn.jsdelivr.net"/>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin/> <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin/>
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="{% static 'blog/js/html5.js' %}" type="text/javascript"></script> <script src="{% static 'blog/js/html5.js' %}" type="text/javascript"></script>
<![endif]--> <![endif]-->
@ -44,21 +46,40 @@
<!-- 本地字体加载 --> <!-- 本地字体加载 -->
<link rel="stylesheet" href="{% static 'blog/fonts/open-sans.css' %}"> <link rel="stylesheet" href="{% static 'blog/fonts/open-sans.css' %}">
{% compress css %}
<link rel='stylesheet' id='twentytwelve-style-css' href='{% static 'blog/css/style.css' %}' type='text/css'
media='all'/> <!-- 主题预加载脚本 - 防止闪烁 -->
<script>
(function() {
'use strict';
const storedTheme = localStorage.getItem('theme');
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const initialTheme = storedTheme || (systemDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', initialTheme);
})();
</script>
<link rel='stylesheet' id='twentytwelve-style-css' href='{% static 'blog/css/style.css' %}' type='text/css' media='all'/>
<link href="{% static 'blog/css/oauth_style.css' %}" rel="stylesheet"> <link href="{% static 'blog/css/oauth_style.css' %}" rel="stylesheet">
<link href="{% static 'blog/css/maupassant.css' %}" rel="stylesheet">
<!-- 新增主题 CSS -->
<link href="{% static 'blog/css/theme.css' %}" rel="stylesheet">
{% comment %}<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>{% endcomment %} {% comment %}<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>{% endcomment %}
<!--[if lt IE 9]> <!--[if lt IE 9]>
<link rel='stylesheet' id='twentytwelve-ie-css' href='{% static 'blog/css/ie.css' %}' type='text/css' media='all' /> <link rel='stylesheet' id='twentytwelve-ie-css' href='{% static 'blog/css/ie.css' %}' type='text/css' media='all' />
<![endif]--> <![endif]-->
<link rel="stylesheet" href="{% static 'pygments/default.css' %}"/> <link rel="stylesheet" href="{% static 'pygments/default.css' %}"/>
<link rel="stylesheet" href="{% static 'blog/css/nprogress.css' %}"> <link rel="stylesheet" href="{% static 'blog/css/nprogress.css' %}">
{% block compress_css %} {% block compress_css %}
{% endblock %} {% endblock %}
<!-- 插件CSS文件 - 集成到压缩系统 --> <!-- 插件CSS文件 - 集成到压缩系统 -->
{% plugin_compressed_css %} {% plugin_compressed_css %}
{% endcompress %}
{% if GLOBAL_HEADER %} {% if GLOBAL_HEADER %}
{{ GLOBAL_HEADER|safe }} {{ GLOBAL_HEADER|safe }}
@ -66,32 +87,73 @@
<!-- 插件head资源 --> <!-- 插件head资源 -->
{% plugin_head_resources %} {% plugin_head_resources %}
{% block extra_css %}{% endblock %}
</head> </head>
<body class="home blog custom-font-enabled"> <body class="home blog custom-font-enabled" data-theme="light">
<div id="page" class="hfeed site"> <div id="page" class="hfeed site">
<header id="masthead" class="site-header" role="banner"> <header id="masthead" class="site-header" role="banner">
<hgroup> <hgroup>
<h1 class="site-title"><a href="/" title="{{ SITE_NAME }}" rel="home">{{ SITE_NAME }}</a> <h1 class="site-title"><a href="/" title="{{ SITE_NAME }}" rel="home">{{ SITE_NAME }}</a></h1>
</h1>
<h2 class="site-description">{{ SITE_DESCRIPTION }}</h2> <h2 class="site-description">{{ SITE_DESCRIPTION }}</h2>
</hgroup> </hgroup>
{% load i18n %}
{% include 'share_layout/nav.html' %}
<!-- 修改导航栏:集成主题切换和用户菜单 -->
<nav class="main-navigation" role="navigation">
<div class="nav-container">
<!-- 原有的导航菜单 -->
<div class="primary-nav">
{% include 'share_layout/nav.html' %}
</div>
<!-- 右侧功能区域 -->
<div class="nav-actions">
<!-- 主题切换按钮 -->
<div class="theme-toggle-container">
<button id="theme-toggle" class="theme-toggle-btn" title="切换主题" aria-label="切换主题">
<span class="theme-icon">🌙</span>
</button>
</div>
<!-- 用户菜单 - 修复版本 -->
<div class="user-menu">
{% if user.is_authenticated %}
<div class="user-dropdown">
<span class="username">
<i class="user-icon">👤</i>
{{ user.username }}
</span>
<div class="dropdown-content">
<!-- 使用安全的URL路径 -->
<a href="/accounts/profile/">
<i class="icon">⚙️</i>个人资料
</a>
<div class="dropdown-divider"></div>
<a href="/accounts/logout/">
<i class="icon">🚪</i>退出
</a>
</div>
</div>
{% else %}
<a class="login-link" href="/accounts/login/">
<i class="icon">🔑</i>登录
</a>
{% endif %}
</div>
</div>
</div>
</nav>
</header><!-- #masthead --> </header><!-- #masthead -->
<div id="main" class="wrapper">
<div id="main" class="wrapper">
{% block content %} {% block content %}
{% endblock %} {% endblock %}
{% block sidebar %} {% block sidebar %}
{% endblock %} {% endblock %}
</div><!-- #main .wrapper --> </div><!-- #main .wrapper -->
{% include 'share_layout/footer.html' %} {% include 'share_layout/footer.html' %}
</div><!-- #page --> </div><!-- #page -->
@ -101,8 +163,12 @@
<script src="{% static 'blog/js/nprogress.js' %}"></script> <script src="{% static 'blog/js/nprogress.js' %}"></script>
<script src="{% static 'blog/js/blog.js' %}"></script> <script src="{% static 'blog/js/blog.js' %}"></script>
<script src="{% static 'blog/js/navigation.js' %}"></script> <script src="{% static 'blog/js/navigation.js' %}"></script>
<!-- 新增主题切换JS -->
<script src="{% static 'blog/js/theme-switcher.js' %}"></script>
{% block compress_js %} {% block compress_js %}
{% endblock %} {% endblock %}
<!-- 插件JS文件 - 集成到压缩系统 --> <!-- 插件JS文件 - 集成到压缩系统 -->
{% plugin_compressed_js %} {% plugin_compressed_js %}
{% endcompress %} {% endcompress %}
@ -115,5 +181,7 @@
<!-- 插件body资源 --> <!-- 插件body资源 -->
{% plugin_body_resources %} {% plugin_body_resources %}
{% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

@ -0,0 +1,9 @@
@echo off
echo Updating Git subtree...
git subtree pull --prefix=src/DjangoBlog DjangoBlog g3f-CodeEdit --squash -m "update subtree"
if %errorlevel% equ 0 (
echo Subtree update successful!
) else (
echo Subtree update failed!
)
pause
Loading…
Cancel
Save