Compare commits

...

46 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
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
wei664 cf58b9ef18 accounts代码注释
3 months ago
wei664 7c41266ac6 Merge branch 'develop' into shw_branch
3 months ago
wei664 285ac07e41 测试提交是否成功
3 months ago
dynastxu 7e7ba6f503 chore(submodule): 添加 DjangoBlog 子模块
3 months ago

9
.gitignore vendored

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

@ -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
werobot_session.db
bin/datas/
.env
# 目前似乎仅有测试代码会涉及到修改此文件夹,所以暂不进行版本管理
static/avatar/

@ -1,60 +1,87 @@
#shw 导入Django的表单模块
from django import forms
#shw 导入Django后台管理的基础用户管理类
from django.contrib.auth.admin import UserAdmin
#shw 导入Django后台用于修改用户信息的表单
from django.contrib.auth.forms import UserChangeForm
#shw 导入Django后台用于用户名的字段类
from django.contrib.auth.forms import UsernameField
#shw 导入Django的国际化和翻译工具
from django.utils.translation import gettext_lazy as _
# Register your models here.
#shw 注册你的模型到这里(这是一个注释提示,实际注册在文件末尾)
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
#shw 自定义用户创建表单用于在Django Admin后台添加新用户。
#shw 它继承自 ModelForm并增加了密码输入和确认的逻辑。
#shw 定义第一个密码字段使用PasswordInput控件隐藏输入内容
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
#shw 定义第二个密码字段,用于确认密码
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
fields = ('email',)
#shw Meta类用于配置表单与模型的关联
model = BlogUser #shw 指定该表单对应的模型是 BlogUser
fields = ('email',) #shw 在创建用户时,除了密码外,只显示邮箱字段
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
#shw 自定义验证方法,用于检查两次输入的密码是否一致
#shw Check that the two password entries match
password1 = self.cleaned_data.get("password1") #shw 从已清洗的数据中获取第一个密码
password2 = self.cleaned_data.get("password2") #shw 从已清洗的数据中获取第二个密码
#shw 如果两个密码都存在且不相等,则抛出验证错误
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
return password2 #shw 返回第二个密码作为清洗后的数据
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
#shw 重写save方法以便在保存用户时处理密码哈希
#shw Save the provided password in hashed format
user = super().save(commit=False) #shw 调用父类的save方法但先不提交到数据库commit=False
user.set_password(self.cleaned_data["password1"]) #shw 使用Django的set_password方法将明文密码加密后存储
if commit:
user.source = 'adminsite'
user.save()
return user
user.source = 'adminsite' #shw 如果决定提交,则设置用户的来源为 'adminsite'
user.save() #shw 将用户对象保存到数据库
return user #shw 返回保存后的用户对象
class BlogUserChangeForm(UserChangeForm):
#shw 自定义用户修改表单用于在Django Admin后台编辑现有用户信息。
#shw 它继承自Django的UserChangeForm以复用大部分功能。
class Meta:
model = BlogUser
fields = '__all__'
#shw Meta类用于配置表单与模型的关联
model = BlogUser #shw 指定该表单对应的模型是 BlogUser
fields = '__all__' #shw 在修改用户时,显示模型中的所有字段
#shw 指定 'username' 字段使用的字段类为 UsernameField
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
#shw 重写初始化方法,可以在这里添加自定义的初始化逻辑
super().__init__(*args, **kwargs) #shw 调用父类的初始化方法
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
#shw 自定义用户管理类用于在Django Admin后台定制 BlogUser 模型的显示和操作方式。
#shw 它继承自Django的UserAdmin以复用用户管理的标准功能。
form = BlogUserChangeForm #shw 指定修改用户时使用的表单
add_form = BlogUserCreationForm #shw 指定添加用户时使用的表单
#shw 定义在用户列表页面显示的列
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
'id', #shw 用户ID
'nickname', #shw 昵称
'username', #shw 用户名
'email', #shw 邮箱
'last_login', #shw 最后登录时间
'date_joined', #shw 注册时间
'source') #shw 创建来源
#shw 定义在列表页面中,哪些列可以点击进入编辑页面
list_display_links = ('id', 'username')
#shw 定义用户列表的默认排序方式按ID降序排列
ordering = ('-id',)
#shw 定义搜索框可以搜索的字段
search_fields = ('username', 'nickname', 'email')

@ -1,5 +1,11 @@
#shw 导入Django的应用配置基类
from django.apps import AppConfig
class AccountsConfig(AppConfig):
#shw 这是accounts应用的配置类。
#shw 它用于定义该应用的各种元数据和行为。
#shw 指定这个配置类所属的应用的完整Python路径。
#shw Django通过这个name来找到并加载这个应用。
name = 'accounts'

@ -1,26 +1,44 @@
#shw 导入Django的表单模块
from django import forms
#shw 导入获取当前激活用户模型的函数
from django.contrib.auth import get_user_model, password_validation
#shw 导入Django内置的认证表单和用户创建表单
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
#shw 导入Django的验证错误类
from django.core.exceptions import ValidationError
#shw 导入Django的表单控件模块
from django.forms import widgets
#shw 导入Django的国际化和翻译工具
from django.utils.translation import gettext_lazy as _
from . import utils
#shw 导入本地的BlogUser模型
from .models import BlogUser
class LoginForm(AuthenticationForm):
#shw 自定义登录表单继承自Django的AuthenticationForm。
#shw 主要用于自定义前端显示的样式。
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
#shw 重写初始化方法,以自定义字段的控件属性。
super(LoginForm, self).__init__(*args, **kwargs) #shw 调用父类的初始化方法
#shw 自定义 'username' 字段的控件添加placeholder和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
#shw 自定义 'password' 字段的控件添加placeholder和CSS类
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
#shw 自定义注册表单继承自Django的UserCreationForm。
#shw 增加了邮箱唯一性验证和前端样式自定义。
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
#shw 重写初始化方法,以自定义字段的控件属性。
super(RegisterForm, self).__init__(*args, **kwargs) #shw 调用父类的初始化方法
#shw 为各个字段添加Bootstrap风格的CSS类和placeholder
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
@ -31,17 +49,23 @@ class RegisterForm(UserCreationForm):
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
email = self.cleaned_data['email']
#shw 自定义邮箱字段的验证方法,确保邮箱在系统中是唯一的。
email = self.cleaned_data['email'] #shw 获取清洗后的邮箱数据
#shw 检查数据库中是否已存在该邮箱
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
raise ValidationError(_("email already exists")) #shw 如果存在,抛出验证错误
return email #shw 返回清洗后的邮箱
class Meta:
model = get_user_model()
fields = ("username", "email")
#shw Meta类用于配置表单与模型的关联
model = get_user_model() #shw 动态获取用户模型而不是硬编码BlogUser更具可复用性
fields = ("username", "email") #shw 指定注册表单中显示的字段
class ForgetPasswordForm(forms.Form):
#shw 忘记密码/重置密码表单继承自基础的Form类。
#shw 它不直接与模型关联,用于处理通过邮箱和验证码重置密码的流程。
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -53,7 +77,7 @@ class ForgetPasswordForm(forms.Form):
)
new_password2 = forms.CharField(
label="确认密码",
label="确认密码", #shw 这里使用了中文硬编码,建议使用 _("Confirm password") 以支持国际化
widget=forms.PasswordInput(
attrs={
"class": "form-control",
@ -63,7 +87,7 @@ class ForgetPasswordForm(forms.Form):
)
email = forms.EmailField(
label='邮箱',
label='邮箱', #shw 这里使用了中文硬编码,建议使用 _("Email")
widget=forms.TextInput(
attrs={
'class': 'form-control',
@ -83,35 +107,47 @@ class ForgetPasswordForm(forms.Form):
)
def clean_new_password2(self):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
#shw 自定义验证方法,检查两次输入的新密码是否一致,并验证密码强度。
password1 = self.data.get("new_password1") #shw 从原始数据中获取密码1
password2 = self.data.get("new_password2") #shw 从原始数据中获取密码2
#shw 检查两次密码是否一致
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
#shw 使用Django内置的密码验证器来检查密码强度
password_validation.validate_password(password2)
return password2
return password2 #shw 返回验证通过的新密码
def clean_email(self):
user_email = self.cleaned_data.get("email")
#shw 自定义验证方法,检查输入的邮箱是否存在于数据库中。
user_email = self.cleaned_data.get("email") #shw 获取清洗后的邮箱
#shw 检查该邮箱是否已注册
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
#shwtodo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
#shw 这是一个安全提示,直接告诉攻击者邮箱未注册可能会被利用。
raise ValidationError(_("email does not exist"))
return user_email
return user_email #shw 返回清洗后的邮箱
def clean_code(self):
code = self.cleaned_data.get("code")
#shw 自定义验证方法,验证邮箱验证码是否正确。
code = self.cleaned_data.get("code") #shw 获取清洗后的验证码
#shw 调用工具函数验证邮箱和验证码是否匹配
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
#shw 如果工具函数返回错误信息,则抛出验证错误
if error:
raise ValidationError(error)
return code
return code #shw 返回验证通过的验证码
class ForgetPasswordCodeForm(forms.Form):
#shw 发送忘记密码验证码的表单。
#shw 它只包含一个邮箱字段,用于用户输入接收验证码的邮箱地址。
email = forms.EmailField(
label=_('Email'),
label=_('Email'), #shw 邮箱字段,标签支持国际化
)

@ -1,35 +1,54 @@
#shw 导入Django内置的抽象用户模型基类
from django.contrib.auth.models import AbstractUser
#shw 导入Django的数据库模型模块
from django.db import models
#shw 导入Django的URL反向解析函数
from django.urls import reverse
#shw 导入Django的时区工具用于获取当前时间
from django.utils.timezone import now
#shw 导入Django的国际化和翻译工具
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
# Create your models here.
#shw 在这里创建你的模型。
class BlogUser(AbstractUser):
#shw 自定义用户模型继承自Django的AbstractUser。
#shw 它扩展了默认用户模型,增加了博客系统所需的额外字段。
#shw 用户昵称字段,可为空
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
#shw 用户创建时间字段,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
#shw 用户最后修改时间字段,默认为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#shw 用户创建来源字段(如:'adminsite', 'register'),可为空
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
#shw 定义获取用户详情页绝对路径的方法。
#shw Django Admin和其他地方会使用这个方法来获取对象的URL。
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
'author_name': self.username}) #shw 反向解析到博客应用的作者详情页URL参数为用户名
def __str__(self):
return self.email
#shw 定义对象的字符串表示形式。
#shw 在Django Admin或打印对象时会显示这个字符串。
return self.email #shw 返回用户的邮箱作为其字符串表示
def get_full_url(self):
site = get_current_site().domain
#shw 定义获取用户详情页完整URL包含域名的方法。
site = get_current_site().domain #shw 获取当前站点的域名
#shw 拼接协议、域名和绝对路径形成完整的URL
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'
#shw Meta类用于定义模型的元数据选项。
ordering = ['-id'] #shw 默认按ID降序排列
verbose_name = _('user') #shw 在Django Admin中显示的单数名称支持国际化
verbose_name_plural = verbose_name #shw 在Django Admin中显示的复数名称
get_latest_by = 'id' #shw 当使用 .latest() 方法时,默认按 'id' 字段查找

@ -1,26 +1,39 @@
#shw 导入Django的测试客户端、请求工厂和测试用例基类
from django.test import Client, RequestFactory, TestCase
#shw 导入Django的URL反向解析函数
from django.urls import reverse
#shw 导入Django的时区工具
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
#shw 导入本地的BlogUser模型
from accounts.models import BlogUser
#shw 导入博客应用的Article和Category模型
from blog.models import Article, Category
#shw 从项目工具模块导入所有函数
from djangoblog.utils import *
#shw 导入本地的工具模块
from . import utils
# Create your tests here.
#shw 在这里创建你的测试。
class AccountTest(TestCase):
#shw 账户应用的测试用例集继承自Django的TestCase。
#shw TestCase提供了数据库事务回滚和客户端模拟等功能。
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
#shw 每个测试方法执行前都会运行的初始化方法。
#shw 用于创建测试所需的公共数据和环境。
self.client = Client() #shw 创建一个模拟的HTTP客户端用于发送请求
self.factory = RequestFactory() #shw 创建一个请求工厂,用于生成请求对象
#shw 创建一个普通用户用于测试
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--="
self.new_test = "xxx123--=" #shw 定义一个测试用的新密码
def test_validate_account(self):
site = get_current_site().domain
@ -28,21 +41,24 @@ class AccountTest(TestCase):
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
testuser = BlogUser.objects.get(username='liangliangyy1') #shw 从数据库中获取刚创建的超级用户
#shw 使用client模拟登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
self.assertEqual(loginresult, True) #shw 断言登录成功
response = self.client.get('/admin/') #shw 模拟访问后台管理页面
self.assertEqual(response.status_code, 200) #shw 断言访问成功状态码为200
#shw 创建一个文章分类用于测试
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
#shw 创建一篇文章用于测试
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
@ -52,10 +68,13 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
#shw 模拟访问文章的后台编辑页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 200) #shw 断言访问成功
def test_validate_register(self):
#shw 测试用户注册、邮箱验证、登录、登出等一系列流程。
#shw 断言注册前,数据库中不存在该邮箱的用户
self.assertEquals(
0, len(
BlogUser.objects.filter(
@ -66,24 +85,30 @@ class AccountTest(TestCase):
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
#shw 断言注册后,数据库中存在该邮箱的用户
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
user = BlogUser.objects.filter(email='user123@user.com')[0]
user = BlogUser.objects.filter(email='user123@user.com')[0] #shw 获取新注册的用户
#shw 生成用于邮箱验证的签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
path = reverse('accounts:result') #shw 获取验证结果页面的URL路径
#shw 构造完整的验证URL
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response = self.client.get(url) #shw 模拟用户点击邮箱中的验证链接
self.assertEqual(response.status_code, 200) #shw 断言访问成功
#shw 模拟用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user = BlogUser.objects.filter(email='user123@user.com')[0] #shw 重新获取用户对象
user.is_superuser = True #shw 将用户提升为超级用户,以便访问后台
user.is_staff = True
user.save()
delete_sidebar_cache()
delete_sidebar_cache() #shw 删除侧边栏缓存
#shw 创建测试用的分类和文章
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
@ -95,66 +120,79 @@ class AccountTest(TestCase):
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
#shw 登录状态下访问文章后台页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
#shw 模拟用户登出
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
self.assertIn(response.status_code, [301, 302, 200]) #shw 断言登出成功重定向或OK
#shw 登出后再次访问文章后台页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
self.assertIn(response.status_code, [301, 302, 200]) #shw 断言被重定向到登录页
#shw 模拟使用错误的密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
'password': 'password123' #shw 错误的密码
})
self.assertIn(response.status_code, [301, 302, 200])
self.assertIn(response.status_code, [301, 302, 200]) #shw 断言登录失败,页面重定向
#shw 登录失败后访问文章后台页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
self.assertIn(response.status_code, [301, 302, 200]) #shw 断言依然无法访问
def test_verify_email_code(self):
#shw 测试邮箱验证码的生成、发送和验证功能。
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
code = generate_code() #shw 生成一个验证码
utils.set_code(to_email, code) #shw 将验证码与邮箱关联(通常是存入缓存或数据库)
utils.send_verify_email(to_email, code) #shw 发送验证码邮件(测试环境中可能不会真的发送)
#shw 使用正确的邮箱和验证码进行验证
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
self.assertEqual(err, None) #shw 断言验证成功,无错误信息返回
#shw 使用错误的邮箱进行验证
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
self.assertEqual(type(err), str) #shw 断言验证失败,返回一个字符串类型的错误信息
def test_forget_password_email_code_success(self):
#shw 测试成功发送忘记密码验证码的场景。
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
data=dict(email="admin@admin.com") #shw 使用一个已存在的邮箱
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
self.assertEqual(resp.status_code, 200) #shw 断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") #shw 断言返回内容为"ok"
def test_forget_password_email_code_fail(self):
#shw 测试发送忘记密码验证码失败的场景(如邮箱格式错误)。
#shw 测试不提供邮箱的情况
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #shw 断言返回错误提示
#shw 测试提供格式错误的邮箱的情况
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #shw 断言返回错误提示
def test_forget_password_email_success(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
#shw 测试成功重置密码的场景。
code = generate_code() #shw 生成一个验证码
utils.set_code(self.blog_user.email, code) #shw 为测试用户设置验证码
#shw 构造重置密码的请求数据
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
@ -165,20 +203,21 @@ class AccountTest(TestCase):
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp.status_code, 302) #shw 断言请求成功并重定向
# 验证用户密码是否修改成功
#shw 验证用户密码是否真的被修改了
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
self.assertNotEqual(blog_user, None) #shw 断言用户依然存在
self.assertEqual(blog_user.check_password(data["new_password1"]), True) #shw 断言新密码是正确的
def test_forget_password_email_not_user(self):
#shw 测试重置一个不存在用户的密码的场景。
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
email="123@123.com", #shw 使用一个不存在的邮箱
code="123456",
)
resp = self.client.post(
@ -186,22 +225,23 @@ class AccountTest(TestCase):
data=data
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.status_code, 200) #shw 断言请求未重定向,停留在原页面并显示错误
def test_forget_password_email_code_error(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
#shw 测试使用错误验证码重置密码的场景。
code = generate_code() #shw 生成一个验证码
utils.set_code(self.blog_user.email, code) #shw 为测试用户设置验证码
#shw 构造重置密码的请求数据,但验证码是错误的
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
code="111111", #shw 错误的验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.status_code, 200) #shw 断言请求未重定向,停留在原页面并显示错误

@ -1,28 +1,44 @@
from django.urls import path
from django.urls import re_path
#shw 导入Django的path和re_path函数用于定义URL路由
from django.urls import path, re_path
#shw 导入本地的视图模块
from . import views
#shw 导入本地的LoginForm表单用于传递给登录视图
from .forms import LoginForm
#shw 定义应用的命名空间用于在模板中反向解析URL如 {% url 'accounts:login' %}
app_name = "accounts"
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]
#shw 定义URL模式列表
urlpatterns = [
#shw 定义登录页面的URL路由
re_path(r'^login/$',
views.LoginView.as_view(success_url='/'), #shw 关联到LoginView类视图并指定登录成功后重定向到根路径
name='login', #shw 为这个URL模式命名为 'login'
kwargs={'authentication_form': LoginForm}), #shw 向LoginView传递额外的关键字参数指定使用自定义的LoginForm
#shw 定义注册页面的URL路由
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"), #shw 关联到RegisterView类视图并指定注册成功后重定向到根路径
name='register'), #shw 命名为 'register'
#shw 定义登出页面的URL路由
re_path(r'^logout/$',
views.LogoutView.as_view(), #shw 关联到LogoutView类视图
name='logout'), #shw 命名为 'logout'
#shw 定义注册/操作结果页面的URL路由
path(r'account/result.html',
views.account_result, #shw 关联到account_result函数视图
name='result'), #shw 命名为 'result'
#shw 定义忘记密码页面的URL路由用于输入新密码和验证码
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(), #shw 关联到ForgetPasswordView类视图
name='forget_password'), #shw 命名为 'forget_password'
#shw 定义发送忘记密码验证码的URL路由
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(), #shw 关联到ForgetPasswordEmailCode类视图
name='forget_password_code'), #shw 命名为 'forget_password_code'
]

@ -1,4 +1,6 @@
#shw 导入Django的获取用户模型函数
from django.contrib.auth import get_user_model
#shw 导入Django默认的基于模型的认证后端基类
from django.contrib.auth.backends import ModelBackend
@ -6,21 +8,33 @@ class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
"""
#shw 自定义认证后端继承自ModelBackend。
#shw 它扩展了Django默认的认证功能使用户既可以使用用户名也可以使用邮箱进行登录。
def authenticate(self, request, username=None, password=None, **kwargs):
#shw 重写authenticate方法这是认证的核心逻辑。
#shw Django的login视图会调用这个方法来验证用户身份。
#shw 判断用户输入的 'username' 字段是否包含 '@' 符号,以此来区分是邮箱还是用户名
if '@' in username:
kwargs = {'email': username}
kwargs = {'email': username} #shw 如果是邮箱,则设置查询条件为 email
else:
kwargs = {'username': username}
kwargs = {'username': username} #shw 如果是用户名,则设置查询条件为 username
try:
#shw 使用动态构建的查询条件email或username去数据库中查找用户
user = get_user_model().objects.get(**kwargs)
#shw 如果找到了用户,则调用 check_password 方法来验证密码是否正确
if user.check_password(password):
return user
return user #shw 密码正确,返回用户对象,认证成功
except get_user_model().DoesNotExist:
return None
#shw 如果根据email或username找不到用户捕获异常
return None #shw 返回None表示认证失败
def get_user(self, username):
#shw 重写get_user方法。
#shw Django的认证中间件会在每个请求中调用此方法根据session中的user_id来获取用户对象。
try:
return get_user_model().objects.get(pk=username)
#shw 注意这里的参数名是username但实际传入的是用户的主键pk通常是ID
return get_user_model().objects.get(pk=username) #shw 根据主键pk查找用户
except get_user_model().DoesNotExist:
return None
#shw 如果根据主键找不到用户,捕获异常
return None #shw 返回None

@ -1,12 +1,19 @@
#shw 导入类型提示模块,用于增强代码可读性和健壮性
import typing
#shw 导入时间间隔类,用于定义验证码有效期
from datetime import timedelta
#shw 导入Django的缓存模块
from django.core.cache import cache
#shw 导入Django的翻译函数
from django.utils.translation import gettext
#shw 导入Django的延迟翻译函数用于在类或模型定义等场景
from django.utils.translation import gettext_lazy as _
#shw 从项目工具模块导入发送邮件的函数
from djangoblog.utils import send_email
#shw 定义验证码的生存时间Time To Live为5分钟
_code_ttl = timedelta(minutes=5)
@ -17,9 +24,11 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
subject: 邮件主题
code: 验证码
"""
#shw 构造邮件的HTML内容使用国际化字符串并将验证码动态插入
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
#shw 调用项目通用的邮件发送函数来完成发送
send_email([to_mail], subject, html_content)
@ -32,18 +41,23 @@ def verify(email: str, code: str) -> typing.Optional[str]:
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方需要对error进行处理
否测调用方需要对error进行处理
"""
#shw 从缓存中获取指定邮箱对应的验证码
cache_code = get_code(email)
#shw 比较用户输入的验证码和缓存中的验证码是否一致
if cache_code != code:
#shw 如果不一致,返回一个翻译后的错误信息字符串
return gettext("Verification code error")
def set_code(email: str, code: str):
"""设置code"""
#shw 将验证码存入缓存键为邮箱值为验证码并设置5分钟的过期时间
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
#shw 从缓存中根据邮箱(键)获取验证码(值)
return cache.get(email)

@ -1,59 +1,84 @@
#shw 导入日志模块
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
#shw 导入Django的认证模块
from django.contrib import auth
#shw 导入登录后重定向字段的常量名
from django.contrib.auth import REDIRECT_FIELD_NAME
#shw 导入获取用户模型的函数
from django.contrib.auth import get_user_model
#shw 导入登出函数
from django.contrib.auth import logout
#shw 导入Django内置的认证表单
from django.contrib.auth.forms import AuthenticationForm
#shw 导入密码哈希生成函数
from django.contrib.auth.hashers import make_password
#shw 导入HTTP响应相关类
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
#shw 导入Django的快捷函数
from django.shortcuts import get_object_or_404
from django.shortcuts import render
#shw 导入URL反向解析函数
from django.urls import reverse
#shw 导入方法装饰器
from django.utils.decorators import method_decorator
#shw 导入URL安全检查函数
from django.utils.http import url_has_allowed_host_and_scheme
#shw 导入Django的视图基类
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
#shw 从项目工具模块导入所需函数
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
#shw 导入本地的工具模块和表单
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
#shw 导入本地的模型
from .models import BlogUser
#shw 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
# Create your views here.
#shw 在这里创建你的视图。
class RegisterView(FormView):
form_class = RegisterForm
template_name = 'account/registration_form.html'
#shw 用户注册视图继承自FormView用于处理用户注册逻辑。
form_class = RegisterForm #shw 指定使用的表单类
template_name = 'account/registration_form.html' #shw 指定渲染的模板
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
#shw 为视图的dispatch方法添加CSRF保护
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
if form.is_valid():
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
#shw 当表单验证通过时执行此方法
if form.is_valid(): #shw 再次确认表单有效
user = form.save(False) #shw 保存表单数据但先不提交到数据库commit=False
user.is_active = False #shw 将用户状态设为未激活,需要邮箱验证
user.source = 'Register' #shw 设置用户来源为注册
user.save(True) #shw 现在将用户对象保存到数据库
site = get_current_site().domain #shw 获取当前站点域名
#shw 生成用于邮箱验证的双重哈希签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
#shw 如果是调试模式,则使用本地地址
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result')
path = reverse('account:result') #shw 获取结果页面的URL路径
#shw 构造完整的邮箱验证链接
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
#shw 构造邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
@ -64,6 +89,7 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
#shw 发送验证邮件
send_email(
emailto=[
user.email,
@ -71,134 +97,159 @@ class RegisterView(FormView):
title='验证您的电子邮箱',
content=content)
#shw 构造注册成功后的跳转URL提示用户去查收邮件
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
return HttpResponseRedirect(url) #shw 重定向到结果页面
else:
#shw 如果表单无效,重新渲染注册页面并显示错误
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
url = '/login/'
#shw 用户登出视图继承自RedirectView用于处理用户登出逻辑。
url = '/login/' #shw 登出后重定向的URL
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#shw 为视图添加never_cache装饰器确保该页面不被缓存
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
logout(request) #shw 调用Django的logout函数清除session信息
delete_sidebar_cache() #shw 删除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs) #shw 执行重定向
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
#shw 用户登录视图继承自FormView用于处理用户登录逻辑。
form_class = LoginForm #shw 指定使用的表单类
template_name = 'account/login.html' #shw 指定渲染的模板
success_url = '/' #shw 登录成功后默认的重定向URL
redirect_field_name = REDIRECT_FIELD_NAME #shw 指定包含重定向URL的GET参数名
login_ttl = 2626560 # 一个月的时间用于“记住我”功能的session过期时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#shw 为视图添加多个装饰器保护密码参数、CSRF保护、禁止缓存
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
redirect_to = self.request.GET.get(self.redirect_field_name)
#shw 向模板上下文中添加额外的数据
redirect_to = self.request.GET.get(self.redirect_field_name) #shw 获取GET参数中的重定向URL
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
redirect_to = '/' #shw 如果没有,则默认为根路径
kwargs['redirect_to'] = redirect_to #shw 将重定向URL添加到上下文
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
#shw 当表单验证通过时执行此方法
#shw 使用Django内置的AuthenticationForm再次验证因为它会调用自定义的认证后端
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache()
logger.info(self.redirect_field_name)
delete_sidebar_cache() #shw 删除侧边栏缓存
logger.info(self.redirect_field_name) #shw 记录日志
auth.login(self.request, form.get_user())
auth.login(self.request, form.get_user()) #shw 调用Django的login函数将用户信息存入session
#shw 如果用户勾选了“记住我”
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
self.request.session.set_expiry(self.login_ttl) #shw 设置session的过期时间为一个月
return super(LoginView, self).form_valid(form) #shw 调用父类方法,处理重定向
else:
#shw 如果验证失败,重新渲染登录页面并显示错误
return self.render_to_response({
'form': form
})
def get_success_url(self):
redirect_to = self.request.POST.get(self.redirect_field_name)
#shw 获取登录成功后应重定向的URL
redirect_to = self.request.POST.get(self.redirect_field_name) #shw 从POST数据中获取重定向URL
#shw 检查URL是否安全防止开放重定向攻击
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url
redirect_to = self.success_url #shw 如果URL不安全则使用默认的success_url
return redirect_to
def account_result(request):
type = request.GET.get('type')
id = request.GET.get('id')
#shw 函数视图,用于处理注册和邮箱验证的结果展示。
type = request.GET.get('type') #shw 获取URL参数中的类型
id = request.GET.get('id') #shw 获取URL参数中的用户ID
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
if user.is_active:
user = get_object_or_404(get_user_model(), id=id) #shw 根据ID获取用户对象如果不存在则返回404
logger.info(type) #shw 记录日志
if user.is_active: #shw 如果用户已经激活,则直接跳转到首页
return HttpResponseRedirect('/')
#shw 处理两种类型:注册成功提示和邮箱验证
if type and type in ['register', 'validation']:
if type == 'register':
#shw 如果是注册类型,显示注册成功、请查收邮件的提示
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
#shw 如果是验证类型,需要验证签名
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) #shw 重新计算正确的签名
sign = request.GET.get('sign') #shw 获取URL中的签名
if sign != c_sign: #shw 比较签名如果不一致则返回403禁止访问
return HttpResponseForbidden()
user.is_active = True
user.save()
user.is_active = True #shw 激活用户
user.save() #shw 保存用户状态
#shw 显示验证成功的提示
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
#shw 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
#shw 如果类型不匹配,则跳转到首页
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
#shw 忘记密码视图,用于处理通过验证码重置密码的逻辑。
form_class = ForgetPasswordForm #shw 指定使用的表单
template_name = 'account/forget_password.html' #shw 指定渲染的模板
def form_valid(self, form):
if form.is_valid():
#shw 当表单验证通过时执行此方法
if form.is_valid(): #shw 再次确认表单有效
#shw 根据邮箱获取用户对象
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
#shw 使用make_password对新密码进行哈希处理
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/')
blog_user.save() #shw 保存用户的新密码
return HttpResponseRedirect('/login/') #shw 重定向到登录页面
else:
#shw 如果表单无效,重新渲染页面并显示错误
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
def post(self, request: HttpRequest):
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
#shw 发送忘记密码验证码的视图继承自基础的View。
@staticmethod
def post(request: HttpRequest):
#shw 只处理POST请求
form = ForgetPasswordCodeForm(request.POST) #shw 用POST数据实例化表单
if not form.is_valid(): #shw 验证表单(主要是验证邮箱格式)
return HttpResponse("错误的邮箱") #shw 如果无效,返回错误信息
to_email = form.cleaned_data["email"] #shw 获取清洗后的邮箱
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
code = generate_code() #shw 生成一个验证码
utils.send_verify_email(to_email, code) #shw 调用工具函数发送验证邮件
utils.set_code(to_email, code) #shw 调用工具函数将验证码存入缓存
return HttpResponse("ok")
return HttpResponse("ok") #shw 返回成功信息

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

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

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

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

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

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

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

@ -1,11 +1,18 @@
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# bjy: 从项目工具模块导入cache实例用于操作缓存
from djangoblog.utils import cache
# bjy: 定义一个继承自BaseCommand的命令类用于执行清空缓存的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"清空所有缓存"
help = 'clear the whole cache'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 调用cache实例的clear方法清空所有缓存
cache.clear()
# bjy: 使用成功样式向标准输出写入操作成功的信息,并附带换行符
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
# bjy: 从Django的auth模块导入make_password函数用于创建加密后的密码哈希
from django.contrib.auth.hashers import make_password
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# bjy: 从当前应用的models模块导入Article, Tag, Category模型用于创建测试数据
from blog.models import Article, Tag, Category
# bjy: 定义一个继承自BaseCommand的命令类用于执行创建测试数据的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"创建测试数据"
help = 'create test datas'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 获取或创建一个测试用户,如果不存在则创建,密码已加密
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# bjy: 获取或创建一个父级分类parent_category为None表示它是顶级分类
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
# bjy: 获取或创建一个子分类,并设置其父分类为上面创建的父级分类
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
# bjy: 显式保存子分类实例确保数据已写入数据库虽然get_or_create通常会保存
category.save()
# bjy: 创建一个基础标签,所有文章都将共用此标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
# bjy: 循环19次创建19篇测试文章和对应的标签
for i in range(1, 20):
# bjy: 获取或创建一篇文章,关联到上面创建的分类、用户,并设置标题和内容
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
# bjy: 为每篇文章创建一个专属标签
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
# bjy: 将专属标签和基础标签都添加到当前文章的标签集合中
article.tags.add(tag)
article.tags.add(basetag)
# bjy: 保存文章,使标签关联生效
article.save()
# bjy: 导入项目的cache工具用于清理缓存
from djangoblog.utils import cache
# bjy: 清空所有缓存,以确保新创建的数据能被正确加载
cache.clear()
# bjy: 使用成功样式向标准输出写入操作完成的信息
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 djangoblog.spider_notify import SpiderNotify
# bjy: 从项目工具模块导入get_current_site函数用于获取当前站点域名等信息
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
# bjy: 获取当前站点的域名用于拼接完整URL
site = get_current_site().domain
# bjy: 定义一个继承自BaseCommand的命令类用于执行通知百度抓取URL的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"通知百度URL"
help = 'notify baidu url'
# bjy: 为命令添加参数,允许用户指定通知的数据类型
def add_arguments(self, parser):
# bjy: 添加一个名为data_type的位置参数类型为字符串且只能从给定的选项中选择
parser.add_argument(
'data_type',
type=str,
@ -21,30 +28,43 @@ class Command(BaseCommand):
'category'],
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)
return url
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 获取用户指定的data_type参数决定通知哪些类型的URL
type = options['data_type']
# bjy: 输出开始获取指定类型URL的信息
self.stdout.write('start get %s' % type)
# bjy: 初始化一个空列表用于收集所有待通知的URL
urls = []
# bjy: 如果类型为article或all则收集所有已发布文章的完整URL
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
# bjy: 如果类型为tag或all则收集所有标签的完整URL
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
# bjy: 如果类型为category或all则收集所有分类的完整URL
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
# bjy: 输出开始通知URL的数量信息使用成功样式
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
# bjy: 调用SpiderNotify的百度通知方法将收集到的URL发送给百度
SpiderNotify.baidu_notify(urls)
# bjy: 输出通知完成的信息,使用成功样式
self.stdout.write(self.style.SUCCESS('finish notify'))

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

@ -1,42 +1,68 @@
# bjy: 导入日志模块
import logging
# bjy: 导入时间模块,用于计算页面渲染时间
import time
# bjy: 从ipware库导入get_client_ip函数用于获取客户端真实IP
from ipware import get_client_ip
# bjy: 从user_agents库导入parse函数用于解析User-Agent字符串
from user_agents import parse
# bjy: 从blog应用的documents模块中导入Elasticsearch是否启用的标志和性能文档管理器
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个中间件类,用于记录页面性能和在线用户信息
class OnlineMiddleware(object):
# bjy: Django 1.10+ 兼容的初始化方法
def __init__(self, get_response=None):
# bjy: 保存get_response可调用对象它是Django请求-响应链中的下一个处理器
self.get_response = get_response
# bjy: 调用父类的初始化方法
super().__init__()
# bjy: 中间件的核心调用方法,每个请求都会经过这里
def __call__(self, request):
''' page render time '''
""" page render time """
# bjy: 记录页面渲染开始时间
start_time = time.time()
# bjy: 调用下一个中间件或视图,获取响应对象
response = self.get_response(request)
# bjy: 从请求头中获取User-Agent字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# bjy: 使用ipware库获取客户端的IP地址
ip, _ = get_client_ip(request)
# bjy: 解析User-Agent字符串得到结构化的用户代理信息
user_agent = parse(http_user_agent)
# bjy: 检查响应是否为流式响应(如文件下载),如果不是,则进行处理
if not response.streaming:
try:
# bjy: 计算页面渲染耗时(秒)
cast_time = time.time() - start_time
# bjy: 如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
# bjy: 将耗时转换为毫秒并四舍五入
time_taken = round((cast_time) * 1000, 2)
# bjy: 获取请求的URL路径
url = request.path
# bjy: 导入Django的时区工具
from django.utils import timezone
# bjy: 调用文档管理器将性能数据保存到Elasticsearch
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# bjy: 将页面渲染耗时替换到响应内容的特定占位符<!!LOAD_TIMES!!>中
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
# bjy: 捕获并记录处理过程中可能发生的任何异常
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
# bjy: 返回最终的响应对象
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
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0001_initial迁移确保基础表已存在
dependencies = [
('blog', '0001_initial'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作1为BlogSettings模型添加一个名为'global_footer'的字段
migrations.AddField(
# bjy: 指定要操作的模型名称
model_name='blogsettings',
# bjy: 指定新字段的名称
name='global_footer',
# bjy: 定义新字段的类型和属性文本类型可为空默认空字符串并设置verbose_name
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
# bjy: 操作2为BlogSettings模型添加一个名为'global_header'的字段
migrations.AddField(
# bjy: 指定要操作的模型名称
model_name='blogsettings',
# bjy: 指定新字段的名称
name='global_header',
# bjy: 定义新字段的类型和属性文本类型可为空默认空字符串并设置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
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0002_blogsettings_global_footer_and_more迁移
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作为BlogSettings模型添加一个新字段
migrations.AddField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定新字段的名称为'comment_need_review'
name='comment_need_review',
# bjy: 定义新字段的类型和属性布尔类型默认值为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
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0003_blogsettings_comment_need_review迁移
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作1重命名BlogSettings模型中的一个字段
migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定字段的原始名称为'analyticscode'
old_name='analyticscode',
# bjy: 指定字段的新名称为'analytics_code'
new_name='analytics_code',
),
# bjy: 操作2重命名BlogSettings模型中的另一个字段
migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定字段的原始名称为'beiancode'
old_name='beiancode',
# bjy: 指定字段的新名称为'beian_code'
new_name='beian_code',
),
# bjy: 操作3重命名BlogSettings模型中的第三个字段
migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定字段的原始名称为'sitename'
old_name='sitename',
# bjy: 指定字段的新名称为'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
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0005迁移
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作修改BlogSettings模型的Meta选项更新verbose_name为英文
migrations.AlterModelOptions(
# bjy: 指定要操作的模型名称为'blogsettings'
name='blogsettings',
# bjy: 更新模型的verbose_name和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
# bjy: 导入正则表达式模块
import re
# bjy: 导入抽象基类模块,用于定义抽象方法
from abc import abstractmethod
# bjy: 从Django中导入设置、异常、模型、URL反向解析、时区和国际化工具
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
# bjy: 从Django MDEditor中导入Markdown文本字段
from mdeditor.fields import MDTextField
# bjy: 从uuslug中导入slugify函数用于生成URL友好的slug
from uuslug import slugify
# bjy: 从项目工具模块中导入缓存装饰器和缓存对象
from djangoblog.utils import cache_decorator, cache
# bjy: 从项目工具模块中导入获取当前站点的函数
from djangoblog.utils import get_current_site
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个文本选择类,用于链接显示类型
class LinkShowType(models.TextChoices):
# bjy: 首页
I = ('i', _('index'))
# bjy: 列表页
L = ('l', _('list'))
# bjy: 文章页
P = ('p', _('post'))
# bjy: 所有页面
A = ('a', _('all'))
# bjy: 幻灯片
S = ('s', _('slide'))
# bjy: 定义一个基础模型类,作为其他模型的父类
class BaseModel(models.Model):
# bjy: 自增主键
id = models.AutoField(primary_key=True)
# bjy: 创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('modify time'), default=now)
# bjy: 重写save方法以实现自定义逻辑
def save(self, *args, **kwargs):
# bjy: 检查是否是更新文章浏览量的操作
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# bjy: 如果是则直接更新数据库中的views字段避免触发其他save逻辑
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# bjy: 如果模型有slug字段则根据title或name自动生成slug
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
# bjy: 调用父类的save方法
super().save(*args, **kwargs)
# bjy: 获取模型的完整URL包括域名
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
@ -51,72 +76,102 @@ class BaseModel(models.Model):
return url
class Meta:
# bjy: 设置为抽象模型,不会在数据库中创建表
abstract = True
# bjy: 定义一个抽象方法,要求子类必须实现
@abstractmethod
def get_absolute_url(self):
pass
# bjy: 定义文章模型
class Article(BaseModel):
"""文章"""
# bjy: 文章状态选择
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
# bjy: 评论状态选择
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
# bjy: 文章类型选择
TYPE = (
('a', _('Article')),
('p', _('Page')),
)
# bjy: 文章标题,唯一
title = models.CharField(_('title'), max_length=200, unique=True)
# bjy: 文章正文使用Markdown编辑器
body = MDTextField(_('body'))
# bjy: 发布时间
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
# bjy: 文章状态
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
# bjy: 评论状态
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
# bjy: 文章类型
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
# bjy: 浏览量
views = models.PositiveIntegerField(_('views'), default=0)
# bjy: 作者,外键关联到用户模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
# bjy: 文章排序权重
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
# bjy: 是否显示目录
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
# bjy: 分类外键关联到Category模型
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
# bjy: 标签多对多关联到Tag模型
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):
return self.body
# bjy: 定义文章的字符串表示
def __str__(self):
return self.title
class Meta:
# bjy: 默认排序方式
ordering = ['-article_order', '-pub_time']
# bjy: 模型的单数和复数名称
verbose_name = _('article')
verbose_name_plural = verbose_name
# bjy: 指定按哪个字段获取最新对象
get_latest_by = 'id'
# bjy: 获取文章的绝对URL
def get_absolute_url(self):
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
@ -125,6 +180,7 @@ class Article(BaseModel):
'day': self.creation_time.day
})
# bjy: 获取文章的分类树路径,带缓存
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
tree = self.category.get_category_tree()
@ -132,13 +188,16 @@ class Article(BaseModel):
return names
# bjy: 重写save方法
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# bjy: 增加浏览量
def viewed(self):
self.views += 1
self.save(update_fields=['views'])
# bjy: 获取文章的评论列表,带缓存
def comment_list(self):
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
@ -151,21 +210,25 @@ class Article(BaseModel):
logger.info('set article comments:{id}'.format(id=self.id))
return comments
# bjy: 获取文章在Admin后台的编辑URL
def get_admin_url(self):
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
# bjy: 获取下一篇文章,带缓存
@cache_decorator(expiration=60 * 100)
def next_article(self):
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
# bjy: 获取上一篇文章,带缓存
@cache_decorator(expiration=60 * 100)
def prev_article(self):
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
# bjy: 从文章正文中提取第一张图片的URL
def get_first_image_url(self):
"""
Get the first image url from article.body.
@ -177,31 +240,40 @@ class Article(BaseModel):
return ""
# bjy: 定义分类模型
class Category(BaseModel):
"""文章分类"""
# bjy: 分类名称,唯一
name = models.CharField(_('category name'), max_length=30, unique=True)
# bjy: 父分类,自关联外键
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
# bjy: URL友好的别名
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# bjy: 排序索引
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
# bjy: 按索引降序排列
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
# bjy: 获取分类的绝对URL
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
# bjy: 定义分类的字符串表示
def __str__(self):
return self.name
# bjy: 递归获取分类的所有父级分类,带缓存
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
@ -218,6 +290,7 @@ class Category(BaseModel):
parse(self)
return categorys
# bjy: 获取当前分类的所有子分类,带缓存
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
@ -240,136 +313,186 @@ class Category(BaseModel):
return categorys
# bjy: 定义标签模型
class Tag(BaseModel):
"""文章标签"""
# bjy: 标签名称,唯一
name = models.CharField(_('tag name'), max_length=30, unique=True)
# bjy: URL友好的别名
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# bjy: 定义标签的字符串表示
def __str__(self):
return self.name
# bjy: 获取标签的绝对URL
def get_absolute_url(self):
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
# bjy: 获取使用该标签的文章数量,带缓存
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
# bjy: 按名称排序
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
# bjy: 定义友情链接模型
class Links(models.Model):
"""友情链接"""
# bjy: 链接名称,唯一
name = models.CharField(_('link name'), max_length=30, unique=True)
# bjy: 链接URL
link = models.URLField(_('link'))
# bjy: 排序权重,唯一
sequence = models.IntegerField(_('order'), unique=True)
# bjy: 是否启用
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
# bjy: 显示类型
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
# bjy: 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
# bjy: 按排序权重升序排列
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
# bjy: 定义链接的字符串表示
def __str__(self):
return self.name
# bjy: 定义侧边栏模型
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
# bjy: 侧边栏标题
name = models.CharField(_('title'), max_length=100)
# bjy: 侧边栏内容HTML
content = models.TextField(_('content'))
# bjy: 排序权重,唯一
sequence = models.IntegerField(_('order'), unique=True)
# bjy: 是否启用
is_enable = models.BooleanField(_('is enable'), default=True)
# bjy: 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
# bjy: 按排序权重升序排列
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
# bjy: 定义侧边栏的字符串表示
def __str__(self):
return self.name
# bjy: 定义博客设置模型
class BlogSettings(models.Model):
"""blog的配置"""
# bjy: 网站名称
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
# bjy: 网站描述
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
# bjy: SEO描述
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
# bjy: 网站关键词
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
# bjy: 文章摘要长度
article_sub_length = models.IntegerField(_('article sub length'), default=300)
# bjy: 侧边栏文章数量
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
# bjy: 侧边栏评论数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
# bjy: 文章页评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5)
# bjy: 是否显示Google AdSense
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
# bjy: Google AdSense代码
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
# bjy: 是否开启全站评论
open_site_comment = models.BooleanField(_('open site comment'), default=True)
# bjy: 公共头部HTML代码
global_header = models.TextField("公共头部", null=True, blank=True, default='')
# bjy: 公共尾部HTML代码
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
# bjy: ICP备案号
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
# bjy: 网站统计代码
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
# bjy: 是否显示公安备案号
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
# bjy: 公安备案号
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
# bjy: 评论是否需要审核
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
class Meta:
# bjy: 模型的单数和复数名称
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
# bjy: 定义设置的字符串表示
def __str__(self):
return self.site_name
# bjy: 重写clean方法用于模型验证
def clean(self):
# bjy: 确保数据库中只能有一条配置记录
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
# bjy: 重写save方法保存后清除缓存
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
from djangoblog.utils import cache

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

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

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

@ -8,44 +8,64 @@
* 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.
// bjy: 使用一个立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
(function () {
// bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作
'use strict';
// bjy: 定义一个函数用于从用户代理字符串中获取IE的模拟版本号
function emulatedIEMajorVersion() {
// bjy: 使用正则表达式匹配用户代理字符串中的 "MSIE x.x" 部分
var groups = /MSIE ([0-9.]+)/.exec(window.navigator.userAgent)
// bjy: 如果匹配不到说明不是IE或版本号无法识别返回null
if (groups === null) {
return null
}
// bjy: 将匹配到的版本号字符串(如 "10.0")转换为整数
var ieVersionNum = parseInt(groups[1], 10)
// bjy: 取整数部分作为主版本号
var ieMajorVersion = Math.floor(ieVersionNum)
// bjy: 返回模拟的IE主版本号
return ieMajorVersion
}
// bjy: 定义一个函数用于检测当前浏览器实际运行的IE版本即使它处于旧版IE的模拟模式下
function actualNonEmulatedIEMajorVersion() {
// Detects the actual version of IE in use, even if it's in an older-IE emulation mode.
// IE JavaScript conditional compilation docs: 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: 此函数通过IE特有的JScript条件编译来检测真实版本
// bjy: IE JavaScript条件编译文档: https://msdn.microsoft.com/library/121hztk3%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
// bjy: 如果jscriptVersion未定义说明是IE11或更高版本且不在模拟模式下
if (jscriptVersion === undefined) {
return 11 // IE11+ not in emulation mode
}
// bjy: 如果JScript版本小于9则判断为IE8或更低版本
if (jscriptVersion < 9) {
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
}
// bjy: 获取当前浏览器的用户代理字符串
var ua = window.navigator.userAgent
// bjy: 检查用户代理中是否包含'Opera'或'Presto'Opera的旧版渲染引擎
if (ua.indexOf('Opera') > -1 || ua.indexOf('Presto') > -1) {
return // Opera, which might pretend to be IE
}
// bjy: 调用函数获取模拟的IE版本号
var emulated = emulatedIEMajorVersion()
// bjy: 如果模拟版本为null说明不是IE浏览器直接返回
if (emulated === null) {
return // Not IE
}
// bjy: 调用函数获取实际的IE版本号
var nonEmulated = actualNonEmulatedIEMajorVersion()
// bjy: 比较模拟版本和实际版本如果不相同说明IE正处于模拟模式下
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!')
}
})();

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

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

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

@ -1,305 +1,378 @@
/* bjy: Google 图标样式,定义背景图位置 */
.icon-sn-google {
background-position: 0 -28px;
}
/* bjy: Google 背景图标样式,定义背景色和背景图位置 */
.icon-sn-bg-google {
background-color: #4285f4;
background-position: 0 0;
}
/* bjy: Google 字体图标如FontAwesome样式定义颜色 */
.fa-sn-google {
color: #4285f4;
}
/* bjy: GitHub 图标样式 */
.icon-sn-github {
background-position: -28px -28px;
}
/* bjy: GitHub 背景图标样式 */
.icon-sn-bg-github {
background-color: #333;
background-position: -28px 0;
}
/* bjy: GitHub 字体图标样式 */
.fa-sn-github {
color: #333;
}
/* bjy: 微博图标样式 */
.icon-sn-weibo {
background-position: -56px -28px;
}
/* bjy: 微博背景图标样式 */
.icon-sn-bg-weibo {
background-color: #e90d24;
background-position: -56px 0;
}
/* bjy: 微博字体图标样式 */
.fa-sn-weibo {
color: #e90d24;
}
/* bjy: QQ图标样式 */
.icon-sn-qq {
background-position: -84px -28px;
}
/* bjy: QQ背景图标样式 */
.icon-sn-bg-qq {
background-color: #0098e6;
background-position: -84px 0;
}
/* bjy: QQ字体图标样式 */
.fa-sn-qq {
color: #0098e6;
}
/* bjy: Twitter图标样式 */
.icon-sn-twitter {
background-position: -112px -28px;
}
/* bjy: Twitter背景图标样式 */
.icon-sn-bg-twitter {
background-color: #50abf1;
background-position: -112px 0;
}
/* bjy: Twitter字体图标样式 */
.fa-sn-twitter {
color: #50abf1;
}
/* bjy: Facebook图标样式 */
.icon-sn-facebook {
background-position: -140px -28px;
}
/* bjy: Facebook背景图标样式 */
.icon-sn-bg-facebook {
background-color: #4862a3;
background-position: -140px 0;
}
/* bjy: Facebook字体图标样式 */
.fa-sn-facebook {
color: #4862a3;
}
/* bjy: 人人网图标样式 */
.icon-sn-renren {
background-position: -168px -28px;
}
/* bjy: 人人网背景图标样式 */
.icon-sn-bg-renren {
background-color: #197bc8;
background-position: -168px 0;
}
/* bjy: 人人网字体图标样式 */
.fa-sn-renren {
color: #197bc8;
}
/* bjy: 腾讯微博图标样式 */
.icon-sn-tqq {
background-position: -196px -28px;
}
/* bjy: 腾讯微博背景图标样式 */
.icon-sn-bg-tqq {
background-color: #1f9ed2;
background-position: -196px 0;
}
/* bjy: 腾讯微博字体图标样式 */
.fa-sn-tqq {
color: #1f9ed2;
}
/* bjy: 豆瓣图标样式 */
.icon-sn-douban {
background-position: -224px -28px;
}
/* bjy: 豆瓣背景图标样式 */
.icon-sn-bg-douban {
background-color: #279738;
background-position: -224px 0;
}
/* bjy: 豆瓣字体图标样式 */
.fa-sn-douban {
color: #279738;
}
/* bjy: 微信图标样式 */
.icon-sn-weixin {
background-position: -252px -28px;
}
/* bjy: 微信背景图标样式 */
.icon-sn-bg-weixin {
background-color: #00b500;
background-position: -252px 0;
}
/* bjy: 微信字体图标样式 */
.fa-sn-weixin {
color: #00b500;
}
/* bjy: 省略号图标样式 */
.icon-sn-dotted {
background-position: -280px -28px;
}
/* bjy: 省略号背景图标样式 */
.icon-sn-bg-dotted {
background-color: #eee;
background-position: -280px 0;
}
/* bjy: 省略号字体图标样式 */
.fa-sn-dotted {
color: #eee;
}
/* bjy: 网站图标样式 */
.icon-sn-site {
background-position: -308px -28px;
}
/* bjy: 网站背景图标样式 */
.icon-sn-bg-site {
background-color: #00b500;
background-position: -308px 0;
}
/* bjy: 网站字体图标样式 */
.fa-sn-site {
color: #00b500;
}
/* bjy: LinkedIn图标样式 */
.icon-sn-linkedin {
background-position: -336px -28px;
}
/* bjy: LinkedIn背景图标样式 */
.icon-sn-bg-linkedin {
background-color: #0077b9;
background-position: -336px 0;
}
/* bjy: LinkedIn字体图标样式 */
.fa-sn-linkedin {
color: #0077b9;
}
/* bjy: 所有社交网络图标的通用样式 */
[class*=icon-sn-] {
display: inline-block;
/* bjy: 设置背景图片为一张包含所有图标的SVG精灵图 */
background-image: url('../img/icon-sn.svg');
background-repeat: no-repeat;
width: 28px;
height: 28px;
vertical-align: middle;
/* bjy: 设置背景图大小高度固定为56px宽度自适应 */
background-size: auto 56px;
}
/* bjy: 所有社交网络图标在鼠标悬停时的效果 */
[class*=icon-sn-]:hover {
opacity: .8;
/* bjy: 兼容旧版IE的透明度滤镜 */
filter: alpha(opacity=80);
}
/* bjy: Google按钮样式 */
.btn-sn-google {
background: #4285f4;
}
/* bjy: Google按钮在激活、聚焦或悬停时的样式 */
.btn-sn-google:active, .btn-sn-google:focus, .btn-sn-google:hover {
background: #2a75f3;
}
/* bjy: GitHub按钮样式 */
.btn-sn-github {
background: #333;
}
/* bjy: GitHub按钮在激活、聚焦或悬停时的样式 */
.btn-sn-github:active, .btn-sn-github:focus, .btn-sn-github:hover {
background: #262626;
}
/* bjy: 微博按钮样式 */
.btn-sn-weibo {
background: #e90d24;
}
/* bjy: 微博按钮在激活、聚焦或悬停时的样式 */
.btn-sn-weibo:active, .btn-sn-weibo:focus, .btn-sn-weibo:hover {
background: #d10c20;
}
/* bjy: QQ按钮样式 */
.btn-sn-qq {
background: #0098e6;
}
/* bjy: QQ按钮在激活、聚焦或悬停时的样式 */
.btn-sn-qq:active, .btn-sn-qq:focus, .btn-sn-qq:hover {
background: #0087cd;
}
/* bjy: Twitter按钮样式 */
.btn-sn-twitter {
background: #50abf1;
}
/* bjy: Twitter按钮在激活、聚焦或悬停时的样式 */
.btn-sn-twitter:active, .btn-sn-twitter:focus, .btn-sn-twitter:hover {
background: #38a0ef;
}
/* bjy: Facebook按钮样式 */
.btn-sn-facebook {
background: #4862a3;
}
/* bjy: Facebook按钮在激活、聚焦或悬停时的样式 */
.btn-sn-facebook:active, .btn-sn-facebook:focus, .btn-sn-facebook:hover {
background: #405791;
}
/* bjy: 人人网按钮样式 */
.btn-sn-renren {
background: #197bc8;
}
/* bjy: 人人网按钮在激活、聚焦或悬停时的样式 */
.btn-sn-renren:active, .btn-sn-renren:focus, .btn-sn-renren:hover {
background: #166db1;
}
/* bjy: 腾讯微博按钮样式 */
.btn-sn-tqq {
background: #1f9ed2;
}
/* bjy: 腾讯微博按钮在激活、聚焦或悬停时的样式 */
.btn-sn-tqq:active, .btn-sn-tqq:focus, .btn-sn-tqq:hover {
background: #1c8dbc;
}
/* bjy: 豆瓣按钮样式 */
.btn-sn-douban {
background: #279738;
}
/* bjy: 豆瓣按钮在激活、聚焦或悬停时的样式 */
.btn-sn-douban:active, .btn-sn-douban:focus, .btn-sn-douban:hover {
background: #228330;
}
/* bjy: 微信按钮样式 */
.btn-sn-weixin {
background: #00b500;
}
/* bjy: 微信按钮在激活、聚焦或悬停时的样式 */
.btn-sn-weixin:active, .btn-sn-weixin:focus, .btn-sn-weixin:hover {
background: #009c00;
}
/* bjy: 省略号按钮样式 */
.btn-sn-dotted {
background: #eee;
}
/* bjy: 省略号按钮在激活、聚焦或悬停时的样式 */
.btn-sn-dotted:active, .btn-sn-dotted:focus, .btn-sn-dotted:hover {
background: #e1e1e1;
}
/* bjy: 网站按钮样式 */
.btn-sn-site {
background: #00b500;
}
/* bjy: 网站按钮在激活、聚焦或悬停时的样式 */
.btn-sn-site:active, .btn-sn-site:focus, .btn-sn-site:hover {
background: #009c00;
}
/* bjy: LinkedIn按钮样式 */
.btn-sn-linkedin {
background: #0077b9;
}
/* bjy: LinkedIn按钮在激活、聚焦或悬停时的样式 */
.btn-sn-linkedin:active, .btn-sn-linkedin:focus, .btn-sn-linkedin:hover {
background: #0067a0;
}
/* bjy: 所有社交网络按钮的通用样式,包括其状态 */
[class*=btn-sn-], [class*=btn-sn-]:active, [class*=btn-sn-]:focus, [class*=btn-sn-]:hover {
border: none;
color: #fff;
}
/* bjy: “更多”按钮的特殊样式,移除内边距 */
.btn-sn-more {
padding: 0;
}
/* bjy: “更多”按钮及其状态的特殊样式,移除阴影 */
.btn-sn-more, .btn-sn-more:active, .btn-sn-more:hover {
box-shadow: none;
}
/* bjy: 当社交网络图标被用作按钮内部元素时,移除其背景色 */
[class*=btn-sn-] [class*=icon-sn-] {
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) {
// bjy: 在控制台打印父评论ID用于调试
console.log(parentid);
// bjy: 将父评论ID设置到隐藏的表单字段中
$("#id_parent_comment_id").val(parentid)
// bjy: 将评论表单移动到指定评论的下方
$("#commentform").appendTo($("#div-comment-" + parentid));
// bjy: 隐藏“发表评论”的标题
$("#reply-title").hide();
// bjy: 显示“取消回复”的链接
$("#cancel_comment").show();
}
// bjy: 定义一个函数,用于取消评论回复
function cancel_reply() {
// bjy: 显示“发表评论”的标题
$("#reply-title").show();
// bjy: 隐藏“取消回复”的链接
$("#cancel_comment").hide();
// bjy: 清空隐藏的父评论ID字段
$("#id_parent_comment_id").val('')
// bjy: 将评论表单移回原来的位置
$("#commentform").appendTo($("#respond"));
}
// bjy: 页面加载时启动NProgress进度条
NProgress.start();
// bjy: 设置进度条进度为40%
NProgress.set(0.4);
//Increment
// bjy: 设置一个定时器每1000毫秒1秒增加一点进度
var interval = setInterval(function () {
NProgress.inc();
}, 1000);
// bjy: 当文档结构加载完成时
$(document).ready(function () {
// bjy: 完成进度条加载
NProgress.done();
// bjy: 清除定时器
clearInterval(interval);
});
/** 侧边栏回到顶部 */
/** bjy: 侧边栏回到顶部功能 */
// bjy: 获取火箭图标的jQuery对象
var rocket = $('#rocket');
// bjy: 监听窗口的滚动事件并使用debounce函数进行防抖处理
$(window).on('scroll', debounce(slideTopSet, 300));
// bjy: 定义一个防抖函数,用于限制函数的执行频率
function debounce(func, wait) {
var timeout;
return function () {
@ -43,49 +62,67 @@ function debounce(func, wait) {
};
}
// bjy: 定义一个函数,根据滚动位置来决定是否显示“回到顶部”按钮
function slideTopSet() {
// bjy: 获取当前文档滚动的垂直距离
var top = $(document).scrollTop();
// bjy: 如果滚动距离大于200像素
if (top > 200) {
// bjy: 显示火箭图标(通过添加'show'类)
rocket.addClass('show');
} else {
// bjy: 否则,隐藏火箭图标(通过移除'show'类)
rocket.removeClass('show');
}
}
// bjy: 监听文档上的点击事件,如果点击的是火箭图标
$(document).on('click', '#rocket', function (event) {
// bjy: 为火箭图标添加'move'类,触发向上飞行的动画
rocket.addClass('move');
// bjy: 使用animate动画将页面平滑滚动回顶部
$('body, html').animate({
scrollTop: 0
}, 800);
});
// bjy: 监听标准动画结束事件
$(document).on('animationEnd', function () {
// bjy: 动画结束后延迟400毫秒移除'move'类,以便下次可以再次触发
setTimeout(function () {
rocket.removeClass('move');
}, 400);
});
// bjy: 监听webkit内核的动画结束事件用于兼容性
$(document).on('webkitAnimationEnd', function () {
// bjy: 动画结束后延迟400毫秒移除'move'类
setTimeout(function () {
rocket.removeClass('move');
}, 400);
});
// bjy: 当整个页面(包括图片等资源)加载完成后执行
window.onload = function () {
// bjy: 获取页面上所有“回复”链接
var replyLinks = document.querySelectorAll(".comment-reply-link");
// bjy: 遍历所有“回复”链接
for (var i = 0; i < replyLinks.length; i++) {
// bjy: 为每个链接绑定点击事件
replyLinks[i].onclick = function () {
// bjy: 从链接的data-pk属性中获取要回复的评论ID
var pk = this.getAttribute("data-pk");
// bjy: 调用do_reply函数来处理回复逻辑
do_reply(pk);
};
}
};
// bjy: 以下代码被注释掉,可能用于国际化语言切换功能
// $(document).ready(function () {
// var form = $('#i18n-form');
// var selector = $('.i18n-select');
// selector.on('change', function () {
// form.submit();
// });
// });
// });

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

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

@ -1,37 +1,56 @@
/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
* @license MIT */
/* bjy: NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
* bjy: @license MIT */
// bjy: UMDUniversal Module Definition模式包装使库能在多种模块系统AMD, CommonJS, 浏览器全局变量)下工作
;(function(root, factory) {
// bjy: 如果环境支持AMD如RequireJS则使用define定义模块
if (typeof define === 'function' && define.amd) {
define(factory);
// bjy: 如果环境支持CommonJS如Node.js则将模块导出
} else if (typeof exports === 'object') {
module.exports = factory();
// bjy: 否则将库挂载到全局对象浏览器中的window
} else {
root.NProgress = factory();
}
// bjy: 传入this在浏览器中为window作为root并调用工厂函数
})(this, function() {
// bjy: 创建NProgress对象作为库的命名空间
var NProgress = {};
// bjy: 定义NProgress的版本号
NProgress.version = '0.2.0';
// bjy: 定义默认配置项并挂载到NProgress.settings上
var Settings = NProgress.settings = {
// bjy: 进度条最小值,防止进度条在开始时看起来像没动
minimum: 0.08,
// bjy: 动画缓动函数
easing: 'linear',
// bjy: 进度条定位方式,由程序自动检测
positionUsing: '',
// bjy: 动画速度(毫秒)
speed: 200,
// bjy: 是否开启自动递增trickle效果
trickle: true,
// bjy: 自动递增的频率(毫秒)
trickleSpeed: 200,
// bjy: 是否显示右上角的加载旋转图标
showSpinner: true,
// bjy: 进度条条形的选择器
barSelector: '[role="bar"]',
// bjy: 加载旋转图标的选择器
spinnerSelector: '[role="spinner"]',
// bjy: 进度条的父容器默认为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>'
};
/**
* Updates configuration.
* bjy: 更新配置
*
* NProgress.configure({
* minimum: 0.1
@ -39,130 +58,154 @@
*/
NProgress.configure = function(options) {
var key, value;
// bjy: 遍历传入的配置项
for (key in options) {
value = options[key];
// bjy: 将有效的配置项更新到Settings对象中
if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
}
// bjy: 返回NProgress自身支持链式调用
return this;
};
/**
* Last number.
* bjy: 存储当前进度状态的变量
*/
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(1.0);
*/
NProgress.set = function(n) {
// bjy: 检查进度条是否已经启动
var started = NProgress.isStarted();
// bjy: 将进度值n限制在minimum和1之间
n = clamp(n, Settings.minimum, 1);
// bjy: 更新状态如果进度为1则将status设为null表示完成
NProgress.status = (n === 1 ? null : n);
// bjy: 如果进度条未渲染,则先渲染它
var progress = NProgress.render(!started),
bar = progress.querySelector(Settings.barSelector),
speed = Settings.speed,
ease = Settings.easing;
// bjy: 触发重排以确保后续的CSS过渡效果能正确应用
progress.offsetWidth; /* Repaint */
// bjy: 使用队列来管理动画,确保动画按顺序执行
queue(function(next) {
// Set positionUsing if it hasn't already been set
// bjy: 如果定位方式未设置则自动检测最佳的CSS定位方式
if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
// Add transition
// bjy: 为进度条添加CSS过渡效果更新其位置
css(bar, barPositionCSS(n, speed, ease));
// bjy: 如果进度达到100%
if (n === 1) {
// Fade out
// bjy: 先设置一个无过渡的状态并设置opacity为1然后触发重排
css(progress, {
transition: 'none',
opacity: 1
});
progress.offsetWidth; /* Repaint */
// bjy: 延迟后,添加淡出动画
setTimeout(function() {
css(progress, {
transition: 'all ' + speed + 'ms linear',
opacity: 0
});
// bjy: 在淡出动画完成后,移除进度条元素,并执行队列的下一个任务
setTimeout(function() {
NProgress.remove();
next();
}, speed);
}, speed);
} else {
// bjy: 如果未完成,则在动画持续时间后执行下一个任务
setTimeout(next, speed);
}
});
// bjy: 返回NProgress自身支持链式调用
return this;
};
// bjy: 检查进度条是否已经启动即status是否为数字
NProgress.isStarted = function() {
return typeof NProgress.status === 'number';
};
/**
* Shows the progress bar.
* This is the same as setting the status to 0%, except that it doesn't go backwards.
* bjy: 显示进度条
* bjy: 这与将状态设置为0%相同只是它不会向后退
*
* NProgress.start();
*
*/
NProgress.start = function() {
// bjy: 如果进度条未启动则将其设置为0%
if (!NProgress.status) NProgress.set(0);
// bjy: 定义一个递归函数用于实现trickle自动递增效果
var work = function() {
setTimeout(function() {
// bjy: 如果进度条已被手动关闭,则停止递增
if (!NProgress.status) return;
// bjy: 调用trickle方法增加一点进度
NProgress.trickle();
// bjy: 递归调用自身
work();
}, Settings.trickleSpeed);
};
// bjy: 如果配置中开启了trickle则开始执行
if (Settings.trickle) work();
return this;
};
/**
* Hides the progress bar.
* This is the *sort of* the same as setting the status to 100%, with the
* difference being `done()` makes some placebo effect of some realistic motion.
* bjy: 隐藏进度条
* bjy: 这与将状态设置为100%类似`done()`会制造一些更逼真的动画效果
*
* NProgress.done();
*
* If `true` is passed, it will show the progress bar even if its hidden.
* bjy: 如果传入`true`即使进度条是隐藏的它也会显示并完成
*
* NProgress.done(true);
*/
NProgress.done = function(force) {
// bjy: 如果没有强制完成且进度条未启动,则直接返回
if (!force && !NProgress.status) return this;
// bjy: 先增加一点随机进度然后设置进度为100%
return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
};
/**
* Increments by a random amount.
* bjy: 以一个随机量增加进度
*/
NProgress.inc = function(amount) {
var n = NProgress.status;
// bjy: 如果进度条未启动,则启动它
if (!n) {
return NProgress.start();
} else if(n > 1) {
// bjy: 如果进度已超过100%,不做任何事
} else {
// bjy: 如果没有指定增加的量,则根据当前进度计算一个合适的增量
if (typeof amount !== 'number') {
if (n >= 0 && n < 0.2) { amount = 0.1; }
else if (n >= 0.2 && n < 0.5) { amount = 0.04; }
@ -171,18 +214,20 @@
else { amount = 0; }
}
// bjy: 计算新的进度值并限制在0到0.994之间防止达到100%后还有动画
n = clamp(n + amount, 0, 0.994);
return NProgress.set(n);
}
};
// bjy: trickle方法内部调用inc
NProgress.trickle = function() {
return NProgress.inc();
};
/**
* Waits for all supplied jQuery promises and
* increases the progress as the promises resolve.
* bjy: 等待所有提供的jQuery promise解决
* bjy: 并在promise解决时增加进度
*
* @param $promise jQUery Promise
*/
@ -190,23 +235,30 @@
var initial = 0, current = 0;
NProgress.promise = function($promise) {
// bjy: 如果promise不存在或已经解决则直接返回
if (!$promise || $promise.state() === "resolved") {
return this;
}
// bjy: 如果这是第一个promise则启动进度条
if (current === 0) {
NProgress.start();
}
// bjy: 增加总promise计数和当前计数
initial++;
current++;
// bjy: 为promise添加always回调无论成功或失败都会执行
$promise.always(function() {
// bjy: 当前计数减一
current--;
// bjy: 如果所有promise都已完成
if (current === 0) {
initial = 0;
NProgress.done();
} else {
// bjy: 否则根据已完成的promise比例更新进度条
NProgress.set((initial - current) / initial);
}
});
@ -217,55 +269,65 @@
})();
/**
* (Internal) renders the progress bar markup based on the `template`
* setting.
* bjy: (内部) 根据`template`设置渲染进度条的HTML标记
*/
NProgress.render = function(fromStart) {
// bjy: 如果进度条已经渲染,则直接返回已存在的元素
if (NProgress.isRendered()) return document.getElementById('nprogress');
// bjy: 给html元素添加'nprogress-busy'类,可用于样式控制
addClass(document.documentElement, 'nprogress-busy');
// bjy: 创建进度条的容器div
var progress = document.createElement('div');
progress.id = 'nprogress';
// bjy: 使用模板设置其innerHTML
progress.innerHTML = Settings.template;
var bar = progress.querySelector(Settings.barSelector),
// bjy: 如果是从头开始,则初始位置在-100%,否则根据当前状态计算位置
perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0),
parent = document.querySelector(Settings.parent),
spinner;
// bjy: 设置进度条条的初始位置和过渡效果
css(bar, {
transition: 'all 0 linear',
transform: 'translate3d(' + perc + '%,0,0)'
});
// bjy: 如果配置中不显示旋转图标,则将其移除
if (!Settings.showSpinner) {
spinner = progress.querySelector(Settings.spinnerSelector);
spinner && removeElement(spinner);
}
// bjy: 如果父容器不是body则给父容器添加自定义类
if (parent != document.body) {
addClass(parent, 'nprogress-custom-parent');
}
// bjy: 将进度条元素添加到父容器中
parent.appendChild(progress);
return progress;
};
/**
* Removes the element. Opposite of render().
* bjy: 移除元素与render()相反
*/
NProgress.remove = function() {
// bjy: 移除html和父容器上的辅助类
removeClass(document.documentElement, 'nprogress-busy');
removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent');
// bjy: 从DOM中移除进度条元素
var progress = document.getElementById('nprogress');
progress && removeElement(progress);
};
/**
* Checks if the progress bar is rendered.
* bjy: 检查进度条是否已渲染
*/
NProgress.isRendered = function() {
@ -273,35 +335,36 @@
};
/**
* Determine which positioning CSS rule to use.
* bjy: 确定使用哪种定位CSS规则
*/
NProgress.getPositioningCSS = function() {
// Sniff on document.body.style
// bjy: 检查body的style属性以嗅探浏览器支持
var bodyStyle = document.body.style;
// Sniff prefixes
// bjy: 嗅探浏览器支持的CSS前缀
var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
('MozTransform' in bodyStyle) ? 'Moz' :
('msTransform' in bodyStyle) ? 'ms' :
('OTransform' in bodyStyle) ? 'O' : '';
// bjy: 如果支持3D变换则使用translate3d
if (vendorPrefix + 'Perspective' in bodyStyle) {
// Modern browsers with 3D support, e.g. Webkit, IE10
return 'translate3d';
// bjy: 如果只支持2D变换则使用translate
} else if (vendorPrefix + 'Transform' in bodyStyle) {
// Browsers without 3D support, e.g. IE9
return 'translate';
// bjy: 否则回退到使用margin性能较差
} else {
// Browsers without translate() support, e.g. IE7-8
return 'margin';
}
};
/**
* Helpers
* bjy: 辅助函数
*/
// bjy: 将一个数值限制在最小值和最大值之间
function clamp(n, min, max) {
if (n < min) return min;
if (n > max) return max;
@ -309,8 +372,7 @@
}
/**
* (Internal) converts a percentage (`0..1`) to a bar translateX
* percentage (`-100%..0%`).
* bjy: (内部) 将百分比 (`0..1`) 转换为进度条的translateX百分比 (`-100%..0%`)
*/
function toBarPerc(n) {
@ -319,13 +381,14 @@
/**
* (Internal) returns the correct CSS for changing the bar's
* position given an n percentage, and speed and ease from Settings
* bjy: (内部) 返回用于改变进度条位置的CSS
* bjy: 根据给定的百分比n以及Settings中的速度和缓动函数
*/
function barPositionCSS(n, speed, ease) {
var barCSS;
// bjy: 根据定位方式生成不同的CSS
if (Settings.positionUsing === 'translate3d') {
barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
} else if (Settings.positionUsing === 'translate') {
@ -334,18 +397,20 @@
barCSS = { 'margin-left': toBarPerc(n)+'%' };
}
// bjy: 添加过渡效果
barCSS.transition = 'all '+speed+'ms '+ease;
return barCSS;
}
/**
* (Internal) Queues a function to be executed.
* bjy: (内部) 将一个函数排队等待执行
*/
var queue = (function() {
var pending = [];
// bjy: 从队列中取出第一个函数并执行
function next() {
var fn = pending.shift();
if (fn) {
@ -353,30 +418,35 @@
}
}
// bjy: 返回一个函数,用于向队列中添加新任务
return function(fn) {
pending.push(fn);
// bjy: 如果这是队列中唯一的任务,则立即开始执行
if (pending.length == 1) next();
};
})();
/**
* (Internal) Applies css properties to an element, similar to the jQuery
* css method.
* bjy: (内部) 将CSS属性应用到元素上类似于jQuery的css方法
*
* While this helper does assist with vendor prefixed property names, it
* does not perform any manipulation of values prior to setting styles.
* bjy: 虽然这个辅助函数有助于处理带供应商前缀的属性名
* bjy: 但它在设置样式之前不会对值进行任何操作
*/
var css = (function() {
// bjy: 常见的CSS前缀列表
var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
// bjy: 缓存已检测的CSS属性名
cssProps = {};
// bjy: 将连字符格式的字符串转换为驼峰格式
function camelCase(string) {
return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
return letter.toUpperCase();
});
}
// bjy: 获取带正确前缀的CSS属性名
function getVendorProp(name) {
var style = document.body.style;
if (name in style) return name;
@ -384,6 +454,7 @@
var i = cssPrefixes.length,
capName = name.charAt(0).toUpperCase() + name.slice(1),
vendorName;
// bjy: 遍历前缀,检查哪个前缀的属性被支持
while (i--) {
vendorName = cssPrefixes[i] + capName;
if (vendorName in style) return vendorName;
@ -392,34 +463,39 @@
return name;
}
// bjy: 获取最终的样式属性名(带缓存)
function getStyleProp(name) {
name = camelCase(name);
return cssProps[name] || (cssProps[name] = getVendorProp(name));
}
// bjy: 应用单个CSS属性到元素
function applyCss(element, prop, value) {
prop = getStyleProp(prop);
element.style[prop] = value;
}
// bjy: 暴露的css函数支持单个或多个属性设置
return function(element, properties) {
var args = arguments,
prop,
value;
// bjy: 如果传入两个参数element, properties对象
if (args.length == 2) {
for (prop in properties) {
value = properties[prop];
if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
}
} else {
// bjy: 如果传入三个参数element, prop, value
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) {
@ -428,7 +504,7 @@
}
/**
* (Internal) Adds a class to an element.
* bjy: (内部) 给一个元素添加类名
*/
function addClass(element, name) {
@ -437,12 +513,12 @@
if (hasClass(oldList, name)) return;
// Trim the opening space.
// bjy: 去掉开头的空格
element.className = newList.substring(1);
}
/**
* (Internal) Removes a class from an element.
* bjy: (内部) 从一个元素移除类名
*/
function removeClass(element, name) {
@ -451,17 +527,16 @@
if (!hasClass(element, name)) return;
// Replace the class name.
// bjy: 替换掉要移除的类名
newList = oldList.replace(' ' + name + ' ', ' ');
// Trim the opening and closing spaces.
// bjy: 去掉开头和结尾的空格
element.className = newList.substring(1, newList.length - 1);
}
/**
* (Internal) Gets a space separated list of the class names on the element.
* The list is wrapped with a single space on each end to facilitate finding
* matches within the list.
* bjy: (内部) 获取元素上所有类名的空格分隔列表
* bjy: 列表的首尾都包裹一个空格以便于在列表中查找匹配项
*/
function classList(element) {
@ -469,12 +544,13 @@
}
/**
* (Internal) Removes an element from the DOM.
* bjy: (内部) 从DOM中移除一个元素
*/
function removeElement(element) {
element && element.parentNode && element.parentNode.removeChild(element);
}
// bjy: 返回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
from unittest.mock import patch, MagicMock
# bjy: 从Django中导入设置、文件上传、命令调用、分页器、静态文件、测试工具、URL反向解析和时区工具
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
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.utils import timezone
# bjy: 从项目中导入用户模型、博客表单、博客模型、自定义模板标签和工具函数
from accounts.models import BlogUser
from blog.forms import BlogSearchForm
from blog.models import Article, Category, Tag, SideBar, Links
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
# bjy: 从项目中导入OAuth相关模型
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
# bjy: 在此处创建测试。
# bjy: 定义一个针对文章功能的测试类继承自Django的TestCase
class ArticleTest(TestCase):
# bjy: setUp方法在每个测试方法执行前运行用于初始化测试环境
def setUp(self):
# bjy: 创建一个测试客户端实例,用于模拟浏览器请求
self.client = Client()
# bjy: 创建一个请求工厂实例,用于生成请求对象
self.factory = RequestFactory()
# bjy: 定义一个测试方法,用于验证文章相关的功能
def test_validate_article(self):
site = get_current_site().domain
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
# bjy: 设置用户密码
user.set_password("liangliangyy")
# bjy: 设置用户为员工和管理员
user.is_staff = True
user.is_superuser = True
user.save()
# bjy: 测试用户详情页是否能正常访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get('/admin/servermanager/emailsendlog/')
@ -44,16 +59,19 @@ class ArticleTest(TestCase):
s.is_enable = True
s.save()
# bjy: 创建并保存一个分类实例
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
# bjy: 创建并保存一个标签实例
tag = Tag()
tag.name = "nicetag"
tag.save()
# bjy: 创建并保存一篇文章
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
@ -63,11 +81,15 @@ class ArticleTest(TestCase):
article.status = 'p'
article.save()
# bjy: 验证文章初始标签数量为0
self.assertEqual(0, article.tags.count())
# bjy: 给文章添加标签并保存
article.tags.add(tag)
article.save()
# bjy: 验证文章标签数量变为1
self.assertEqual(1, article.tags.count())
# bjy: 循环创建20篇文章用于测试分页等功能
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@ -79,96 +101,126 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
# bjy: 如果启用了Elasticsearch则重建索引并测试搜索功能
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
# bjy: 测试文章详情页
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# bjy: 测试蜘蛛通知功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
# bjy: 测试标签页
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
# bjy: 测试分类页
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# bjy: 测试搜索页
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
# bjy: 测试加载文章标签的模板标签
s = load_articletags(article)
self.assertIsNotNone(s)
# bjy: 以超级用户身份登录
self.client.login(username='liangliangyy', password='liangliangyy')
# bjy: 测试文章归档页
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# bjy: 测试文章列表的分页信息
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
# bjy: 测试按标签筛选后的文章分页
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
# bjy: 测试按作者筛选后的文章分页
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
# bjy: 测试按分类筛选后的文章分页
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
# bjy: 测试搜索表单
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
# bjy: 测试百度通知功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
# bjy: 测试获取Gravatar头像的模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
# bjy: 创建并保存一个友情链接
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
link.save()
# bjy: 测试友情链接页面
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# bjy: 测试RSS订阅页面
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
# bjy: 测试网站地图
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# bjy: 测试一些Admin后台的删除和变更操作
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
# bjy: 辅助方法,用于检查分页导航链接是否正确
def check_pagination(self, p, type, value):
# bjy: 遍历所有页码
for page in range(1, p.num_pages + 1):
# bjy: 加载当前页的分页信息
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
# bjy: 如果存在上一页链接,则测试其可访问性
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
# bjy: 如果存在下一页链接,则测试其可访问性
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
# bjy: 测试图片上传功能
def test_image(self):
# bjy: 下载一个网络图片到本地
import requests
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file:
file.write(rsp.content)
# bjy: 测试无签名上传预期返回403
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
# bjy: 生成签名
sign = get_sha256(get_sha256(settings.SECRET_KEY))
# bjy: 使用签名上传图片
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
@ -176,17 +228,23 @@ class ArticleTest(TestCase):
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
# bjy: 删除本地临时图片
os.remove(imagepath)
# bjy: 测试发送邮件和保存用户头像的工具函数
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
# bjy: 测试404错误页面
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_commands(self):
# bjy: 测试自定义的管理命令
@staticmethod
def test_commands():
# bjy: 创建一个超级用户(如果不存在)
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -195,12 +253,14 @@ class ArticleTest(TestCase):
user.is_superuser = True
user.save()
# bjy: 创建并保存一个OAuth配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
# bjy: 创建并保存一个OAuth用户关联到超级用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
@ -212,6 +272,7 @@ class ArticleTest(TestCase):
}'''
u.save()
# bjy: 创建另一个OAuth用户用于测试
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -222,11 +283,294 @@ class ArticleTest(TestCase):
}'''
u.save()
# bjy: 如果启用了Elasticsearch则重建索引
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
# bjy: 调用并测试一系列自定义管理命令
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
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.views.decorators.cache import cache_page
# bjy: 从当前应用中导入视图模块
from . import views
# bjy: 定义应用的命名空间用于在模板中反向解析URL时避免冲突
app_name = "blog"
# bjy: 定义URL模式列表
urlpatterns = [
# bjy: 首页路由指向IndexView视图
path(
r'',
views.IndexView.as_view(),
name='index'),
# bjy: 首页分页路由指向IndexView视图并接收页码参数
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
# bjy: 文章详情页路由包含年、月、日和文章ID指向ArticleDetailView视图
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
# bjy: 分类详情页路由接收分类的slug指向CategoryDetailView视图
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
# bjy: 分类详情页分页路由接收分类slug和页码指向CategoryDetailView视图
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
# bjy: 作者详情页路由接收作者名指向AuthorDetailView视图
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
# bjy: 作者详情页分页路由接收作者名和页码指向AuthorDetailView视图
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
# bjy: 标签详情页路由接收标签的slug指向TagDetailView视图
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
# bjy: 标签详情页分页路由接收标签slug和页码指向TagDetailView视图
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
# bjy: 文章归档页路由使用cache_page装饰器缓存1小时指向ArchivesView视图
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
# bjy: 友情链接页路由指向LinkListView视图
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
# bjy: 文件上传路由指向fileupload视图函数
path(
r'upload',
views.fileupload,
name='upload'),
# bjy: 清除缓存路由指向clean_cache_view视图函数
path(
r'clean',
views.clean_cache_view,
name='clean'),
path(
'like_article/',
views.LikeArticle.as_view(),
name='like_article'),
]

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

@ -29,7 +29,8 @@ class CommentsTest(TransactionTestCase):
password="liangliangyy1")
# zy: 辅助方法 - 更新文章评论的审核状态为已通过
def update_article_comment_status(self, article):
@staticmethod
def update_article_comment_status(article):
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True

@ -1,3 +1,31 @@
"""`xjj`
DjangoBlog 管理站点配置
========
该模块定义了 DjangoBlog 项目的自定义管理站点配置它继承自 Django 的默认 AdminSite
并注册了项目中所有需要在管理后台进行管理的模型
主要功能
~~~~~~~~
- 创建自定义管理站点
- 设置管理站点的标题和头部显示文本
- 限制只有超级用户才能访问管理站点
- 注册各个应用中的模型到管理站点
注册的模型包括
~~~~~~~~
- 博客相关文章分类标签链接侧边栏博客设置
- 用户相关博客用户
- 评论相关评论
- OAuth 相关 OAuth 用户 OAuth 配置
- 其他命令邮件发送日志位置跟踪日志站点信息管理日志
使用方式
~~~~~~~~
通过 admin_site 实例来访问自定义的管理站点
"""
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
@ -18,13 +46,38 @@ from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
"""`xjj`
自定义 Django 管理站点类
继承自 Django 默认的 AdminSite 用于为 DjangoBlog 项目提供定制化的管理后台
"""
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
"""`xjj`
初始化管理站点
Args:
name (str): 管理站点的名称默认为 'admin'
"""
super().__init__(name)
def has_permission(self, request):
"""`xjj`
检查用户是否有访问管理站点的权限
重写了父类的方法只允许超级用户访问管理站点
Args:
request: HTTP 请求对象
Returns:
bool: 如果用户是超级用户则返回 **True**否则返回 **False**
"""
return request.user.is_superuser
# 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
class DjangoblogAppConfig(AppConfig):
"""`xjj`
Django 博客应用配置类
该类继承自 Django AppConfig 用于配置 jangoblog 应用的基本信息和初始化操作
主要负责设置默认的自动字段类型和应用名称并在应用准备就绪时加载插件
"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
def ready(self):
"""`xjj`
应用准备就绪时的回调方法
Django 应用完全加载后调用此方法主要用于执行应用级别的初始化操作
此方法会导入并加载所有已注册的插件
Returns:
None
"""
super().ready()
# Import and load plugins here
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 logging
@ -25,6 +46,18 @@ send_email_signal = django.dispatch.Signal(
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
"""`xjj`
发送邮件信号的处理函数
Args:
sender: 信号发送者
**kwargs: 包含邮件相关信息的字典参数
- emailto (list): 收件人邮箱列表
- title (str): 邮件标题
- content (str): 邮件内容
"""
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
@ -53,6 +86,16 @@ def send_email_signal_handler(sender, **kwargs):
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
"""`xjj`
OAuth 用户登录信号处理函数
OAuth 用户登录时该函数会被触发执行相关处理逻辑包括更新用户头像和清除侧边栏缓存
Args:
sender: 信号发送者
**kwargs: 包含信号传递的额外参数必须包含 ``id`` 键表示 OAuth 用户的 ID
"""
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
@ -73,6 +116,19 @@ def model_post_save_callback(
using,
update_fields,
**kwargs):
"""`xjj`
模型保存后的回调函数用于处理缓存清理和搜索引擎通知等操作
Args:
sender: 发送信号的模型类
instance: 保存的模型实例
created: 布尔值指示实例是新建还是更新
raw: 布尔值指示实例是否从序列化数据加载
using: 数据库别名
update_fields: 更新字段集合如果为 None 表示所有字段都被更新
**kwargs: 其他关键字参数
"""
clearcache = False
if isinstance(instance, LogEntry):
return

@ -1,3 +1,24 @@
"""`xjj`
用于 Django 博客搜索的 Elasticsearch 后端
========
该模块实现了基于 Elasticsearch 的搜索后端用于博客文章的全文搜索功能
主要包含以下组件
- ``ElasticSearchBackend``: 核心搜索后端实现处理文档的创建更新删除和搜索操作
- ``ElasticSearchQuery``: 搜索查询处理类负责构建和执行搜索查询
- ``ElasticSearchModelSearchForm``: 搜索表单类处理用户搜索输入和选项
- ``ElasticSearchEngine``: 搜索引擎类整合后端和查询组件
功能特性
~~~~~~~~
- 支持文章内容和标题的全文搜索
- 提供搜索建议和拼写纠正功能
- 支持搜索结果分页和过滤
- 集成 Haystack 搜索框架
"""
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
@ -12,6 +33,14 @@ logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
"""`xjj`
ElasticSearch 搜索后端实现类用于与 Elasticsearch 进行交互提供搜索更新删除等操作
:param connection_alias: 连接别名用于标识不同的搜索引擎连接
:param connection_options: 其他连接选项以关键字参数形式传入
"""
def __init__(self, connection_alias, **connection_options):
super(
ElasticSearchBackend,
@ -22,35 +51,81 @@ class ElasticSearchBackend(BaseSearchBackend):
self.include_spelling = True
def _get_models(self, iterable):
"""`xjj`
获取模型对象并转换为文档格式
:param iterable: 可迭代的模型对象列表若为空则默认获取所有 Article 对象
:return: 转换后的文档对象列表
"""
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
"""`xjj`
创建索引并将模型数据重建到 Elasticsearch
:param models: 模型对象列表
"""
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
def _delete(self, models):
"""`xjj`
删除指定模型对象
:param models: 待删除的模型对象列表
:return: 始终返回 True
"""
for m in models:
m.delete()
return True
def _rebuild(self, models):
"""`xjj`
重建指定模型的索引
:param models: 模型对象列表如果为空则默认获取所有 Article 对象
"""
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
def update(self, index, iterable, commit=True):
"""`xjj`
更新索引中的文档
:param index: 索引名称
:param iterable: 待更新的模型对象可迭代对象
:param commit: 是否提交更改
"""
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
"""`xjj`
从索引中移除指定对象
:param obj_or_string: 待移除的对象或字符串标识
"""
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
"""`xjj`
清空索引中的内容
:param models: 待清除的模型对象未使用
:param commit: 是否提交更改未使用
"""
self.remove(None)
@staticmethod
@ -73,6 +148,14 @@ class ElasticSearchBackend(BaseSearchBackend):
@log_query
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)
start_offset = kwargs.get('start_offset')
@ -124,6 +207,16 @@ class ElasticSearchBackend(BaseSearchBackend):
class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
"""`xjj`
将日期时间对象转换为字符串格式
Args:
date: 日期时间对象
Returns:
str: 格式化后的日期时间字符串包含小时分钟秒或默认为 000000
"""
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
@ -155,23 +248,79 @@ class ElasticSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
"""`xjj`
构建查询片段
Args:
field: 查询字段
filter_type: 过滤器类型
value: 查询值对象
Returns:
str: 查询字符串
"""
return value.query_string
def get_count(self):
"""`xjj`
获取查询结果的数量
Returns:
int: 结果数量如果没有结果则返回0
"""
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
"""`xjj`
获取拼写建议
Args:
preferred_query (str, optional): 首选查询字符串
Returns:
str: 拼写建议
"""
return self._spelling_suggestion
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)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
"""`xjj`
ElasticSearch 模型搜索表单类
该类继承自 ``ModelSearchForm`` 用于处理 ElasticSearch 相关的搜索功能
特别是支持建议搜索功能的配置
"""
def search(self):
"""`xjj`
执行搜索操作
该方法首先根据表单数据配置是否启用建议搜索功能
然后调用父类的 ``search`` 方法执行实际的搜索操作
Returns:
SearchQuerySet: 搜索结果查询集
"""
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
@ -179,5 +328,16 @@ class ElasticSearchModelSearchForm(ModelSearchForm):
class ElasticSearchEngine(BaseEngine):
"""`xjj`
ElasticSearch 搜索引擎引擎类
该类继承自 ``BaseEngine`` 用于实现 ElasticSearch 搜索引擎的核心功能
包含后端存储和查询处理的配置
Attributes:
backend: ElasticSearch 后端存储类用于数据的存储和检索
query: ElasticSearch 查询类用于构建和执行搜索查询
"""
backend = ElasticSearchBackend
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.syndication.views import Feed
from django.utils import timezone
@ -8,6 +23,13 @@ from djangoblog.utils import CommonMarkdown
class DjangoBlogFeed(Feed):
"""`xjj`
Django 博客 RSS 订阅源类
该类继承自 Django ``Feed`` 用于生成博客文章的 RSS 订阅源
提供博客文章的 RSS feed 功能包含文章标题描述作者信息等
"""
feed_type = Rss201rev2Feed
description = '大巧无工,重剑无锋.'
@ -15,25 +37,83 @@ class DjangoBlogFeed(Feed):
link = "/feed/"
def author_name(self):
"""`xjj`
获取 RSS 订阅源的作者名称
Returns:
str: 博客第一个用户的昵称如果不存在则返回 None
"""
return get_user_model().objects.first().nickname
def author_link(self):
"""`xjj`
获取 RSS 订阅源作者的链接地址
Returns:
str: 博客第一个用户的绝对 URL 地址
"""
return get_user_model().objects.first().get_absolute_url()
def items(self):
"""`xjj`
获取 RSS 订阅源的文章列表
Returns:
QuerySet: 按发布时间倒序排列的前 5 篇已发布文章
"""
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
"""`xjj`
获取单篇文章的标题
Args:
item (Article): 文章对象
Returns:
str: 文章的标题
"""
return item.title
def item_description(self, item):
"""`xjj`
获取单篇文章的描述内容
Args:
item (Article): 文章对象
Returns:
str: 文章正文内容转换为 Markdown 格式后的 HTML
"""
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
"""`xjj`
获取 RSS 订阅源的版权信息
Returns:
str: 包含当前年份的版权声明
"""
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
"""`xjj`
获取单篇文章的链接地址
Args:
item (Article): 文章对象
Returns:
str: 文章的绝对URL地址
"""
return item.get_absolute_url()
def item_guid(self, item):

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

@ -15,8 +15,24 @@ from pathlib import Path
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):
"""`xjj`
将环境变量转换为布尔值
Args:
env (str): 环境变量名称
default (bool): 当环境变量不存在时返回的默认值
Returns:
bool: 环境变量的布尔值表示如果环境变量不存在则返回默认值
"""
str_val = os.environ.get(env)
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
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': {
'charset': 'utf8mb4'},
}}
if TESTING:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
else:
DATABASES = {
'default': {
'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
# 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.urls import reverse
@ -5,55 +23,182 @@ from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
"""`xjj`
静态视图站点地图类
~~~~~~~~
该类用于生成静态页面的 sitemap 信息继承自 Django ``Sitemap``
"""
priority = 0.5
changefreq = 'daily'
def items(self):
"""`xjj`
获取站点地图项列表
返回包含 URL 名称的列表这些 URL 将被包含在 sitemap
Returns:
list: 包含 URL 名称的列表
"""
return ['blog:index', ]
def location(self, item):
"""`xjj`
根据 URL 名称获取实际的 URL 路径
通过 Django ``reverse`` 函数将 URL 名称转换为实际的 URL 路径
Args:
item (str): URL 名称
Returns:
str: 对应的 URL 路径
"""
return reverse(item)
class ArticleSiteMap(Sitemap):
"""`xjj`
文章站点地图类用于生成文章页面的 ``sitemap.xml`` 文件
该类继承自 Django ``Sitemap`` 定义了文章页面在站点地图中的相关属性
和数据获取方法便于搜索引擎爬虫发现和索引文章内容
"""
changefreq = "monthly"
priority = "0.6"
def items(self):
"""`xjj`
获取 sitemap 中包含的所有文章对象列表
Returns:
QuerySet: 包含所有已发布状态文章的查询集
"""
return Article.objects.filter(status='p')
def lastmod(self, obj):
"""`xjj`
获取指定文章对象的最后修改时间
Args:
obj: 文章对象实例
Returns:
datetime: 文章的最后修改时间
"""
return obj.last_modify_time
class CategorySiteMap(Sitemap):
"""`xjj`
分类站点地图类
~~~~~~~~
该类用于生成网站分类页面的 sitemap 信息继承自 Django ``Sitemap``
"""
changefreq = "Weekly"
priority = "0.6"
def items(self):
"""`xjj`
获取所有分类对象
Returns:
QuerySet: 包含所有 Category 对象的查询集
"""
return Category.objects.all()
def lastmod(self, obj):
"""`xjj`
获取分类对象的最后修改时间
Args:
obj (Category): 分类对象实例
Returns:
datetime: 分类的最后修改时间
"""
return obj.last_modify_time
class TagSiteMap(Sitemap):
"""`xjj`
标签站点地图类用于生成标签页面的 sitemap.xml 文件
该类继承自 Django Sitemap 定义了标签页面在站点地图中的相关属性
和数据获取方法
"""
changefreq = "Weekly"
priority = "0.3"
def items(self):
"""`xjj`
获取所有标签对象列表
Returns:
QuerySet: 包含所有 Tag 对象的查询集
"""
return Tag.objects.all()
def lastmod(self, obj):
"""`xjj`
获取指定标签对象的最后修改时间
Args:
obj (Tag): 标签对象实例
Returns:
datetime: 标签对象的最后修改时间
"""
return obj.last_modify_time
class UserSiteMap(Sitemap):
"""`xjj`
用户站点地图类用于生成用户页面的 sitemap.xml 文件
Attributes:
changefreq (str): 页面更新频率设置为 `Weekly`
priority (str): 页面优先级设置为 `0.3`
"""
changefreq = "Weekly"
priority = "0.3"
def items(self):
"""`xjj`
获取所有需要包含在 sitemap 中的用户对象列表
通过获取所有文章的作者来构建用户列表并去重
Returns:
list: 包含所有文章作者用户的去重列表
"""
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
"""`xjj`
获取指定用户对象的最后修改时间
Args:
obj: 用户对象
Returns:
datetime: 用户的注册时间作为最后修改时间
"""
return obj.date_joined

@ -1,3 +1,23 @@
"""`xjj`
Spider Notify Module
========
此模块提供了向搜索引擎通知网站更新的功能主要用于 SEO 优化
当前支持百度搜索引擎的主动推送功能
主要功能
~~~~~~~~
- 向百度搜索引擎主动推送网站URL更新
- 记录推送结果和异常日志
依赖
~~~~~~~~
- ``requests``: 用于发送 HTTP 请求
- ``django.conf.settings``: 获取配置参数
- ``logging``: 记录日志信息
"""
import logging
import requests
@ -7,8 +27,18 @@ logger = logging.getLogger(__name__)
class SpiderNotify():
"""`xjj`
爬虫通知类用于向搜索引擎提交 URL 通知
"""
@staticmethod
def baidu_notify(urls):
"""`xjj`
向百度搜索引擎提交 URL 通知
:param urls: 要提交的 URL 列表
"""
try:
data = '\n'.join(urls)
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
@ -18,4 +48,10 @@ class SpiderNotify():
@staticmethod
def notify(url):
"""`xjj`
通用通知方法调用百度通知接口
:param url: 要通知的 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 djangoblog.utils import *
class DjangoBlogTest(TestCase):
"""`xjj`
Django 博客测试类
用于测试博客应用的各种功能模块
继承自 Django ``TestCase``
"""
def setUp(self):
pass
def test_utils(self):
"""`xjj`
测试工具函数功能
验证以下功能
1. SHA256哈希函数
2. Markdown文本转换功能
3. 字典转URL参数功能
"""
md5 = get_sha256('test')
self.assertIsNotNone(md5)
c = CommonMarkdown.get_markdown('''

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

@ -1,4 +1,25 @@
# 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
@ -145,6 +166,18 @@ class WhooshSearchBackend(BaseSearchBackend):
self.setup_complete = True
def build_schema(self, fields):
"""`xjj`
构建用于搜索索引的 Whoosh Schema 对象
根据传入的字段定义创建对应的 Whoosh 字段类型并最终构建一个完整的 Schema 实例
:param fields: 包含字段名与字段配置对象的字典键是字段名称值是一个具有 field_typestoredboost 等属性的对象
:return: 第一个元素是文档内容字段名第二个是构建好的 Whoosh Schema 对象
:exception SearchBackendError: 当没有找到任何字段时抛出异常
"""
schema_fields = {
ID: WHOOSH_ID(stored=True, unique=True),
DJANGO_CT: WHOOSH_ID(stored=True),
@ -200,6 +233,17 @@ class WhooshSearchBackend(BaseSearchBackend):
return (content_field_name, Schema(**schema_fields))
def update(self, index, iterable, commit=True):
"""`xjj`
更新索引中的文档数据
遍历可迭代对象中的模型实例将其转换成可用于索引的数据格式并更新到索引中
Args:
index (SearchIndex): Haystack 的索引类实例
iterable (iterable): 要被索引的模型实例集合
commit (bool): 是否在更新后提交更改默认为 **True**
"""
if not self.setup_complete:
self.setup()
@ -245,6 +289,14 @@ class WhooshSearchBackend(BaseSearchBackend):
writer.commit()
def remove(self, obj_or_string, commit=True):
"""`xjj`
从索引中删除指定文档
Args:
obj_or_string (Model | str): 要删除的模型实例或其唯一标识符字符串
commit (bool): 是否立即提交更改默认为 **True**
"""
if not self.setup_complete:
self.setup()
@ -267,6 +319,14 @@ class WhooshSearchBackend(BaseSearchBackend):
exc_info=True)
def clear(self, models=None, commit=True):
"""`xjj`
清除整个索引或特定模型类型的索引数据
Args:
models (list | tuple): 可选要清除的模型列表
commit (bool): 是否立即提交更改默认为 **True**
"""
if not self.setup_complete:
self.setup()
@ -304,6 +364,10 @@ class WhooshSearchBackend(BaseSearchBackend):
"Failed to clear Whoosh index: %s", e, exc_info=True)
def delete_index(self):
"""`xjj`
完全删除当前索引目录下的文件并重新初始化索引结构
"""
# Per the Whoosh mailing list, if wiping out everything from the index,
# it's much more efficient to simply delete the index files.
if self.use_file_storage and os.path.exists(self.path):
@ -315,6 +379,10 @@ class WhooshSearchBackend(BaseSearchBackend):
self.setup()
def optimize(self):
"""`xjj`
对现有索引执行优化操作提升查询性能
"""
if not self.setup_complete:
self.setup()
@ -322,6 +390,17 @@ class WhooshSearchBackend(BaseSearchBackend):
self.index.optimize()
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
# greater than 0.
if end_offset is not None and end_offset <= 0:
@ -366,6 +445,33 @@ class WhooshSearchBackend(BaseSearchBackend):
limit_to_registered_models=None,
result_class=None,
**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:
self.setup()
@ -570,6 +676,23 @@ class WhooshSearchBackend(BaseSearchBackend):
limit_to_registered_models=None,
result_class=None,
**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:
self.setup()
@ -682,6 +805,20 @@ class WhooshSearchBackend(BaseSearchBackend):
query_string='',
spelling_query=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
results = []
@ -768,6 +905,16 @@ class WhooshSearchBackend(BaseSearchBackend):
}
def create_spelling_suggestion(self, query_string):
"""`xjj`
生成针对用户查询的拼写修正建议
Args:
query_string (str): 输入的查询语句
Returns:
str: 拼写修正后的建议语句
"""
spelling_suggestion = None
reader = self.index.reader()
corrector = reader.corrector(self.content_field_name)
@ -872,6 +1019,16 @@ class WhooshSearchBackend(BaseSearchBackend):
class WhooshSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
"""`xjj`
将日期时间对象转换为 Whoosh 可识别的字符串格式
Args:
date: 日期或日期时间对象
Returns:
str: 格式化后的日期时间字符串格式为 `YYYYMMDDHHMMSS`
"""
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
@ -903,6 +1060,20 @@ class WhooshSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words)
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
query_frag = ''
is_datetime = False
@ -1040,5 +1211,16 @@ class WhooshSearchQuery(BaseSearchQuery):
class WhooshEngine(BaseEngine):
"""`xjj`
Whoosh 搜索引擎引擎类
该类继承自 ``BaseEngine``用于集成 Whoosh 搜索功能
包含搜索后端和查询处理的相关配置
Attributes:
backend: 搜索后端类设置为 `WhooshSearchBackend`
query: 查询类设置为 `WhooshSearchQuery`
"""
backend = WhooshSearchBackend
query = WhooshSearchQuery

@ -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配置'},
),
]

@ -20,11 +20,9 @@ logger = logging.getLogger(__name__)
class OAuthAccessTokenException(Exception):
"""lrj
OAuth授权令牌异常类
当获取access_token失败时抛出此异常
"""
pass
'''
oauth授权失败异常
'''
class BaseOauthManager(metaclass=ABCMeta):
@ -323,8 +321,92 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
class QQOauthManager(BaseOauthManager):
"""QQ OAuth管理器实现"""
# lrj... 实现细节类似微博管理器,但不需要代理
AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize'
TOKEN_URL = 'https://graph.qq.com/oauth2.0/token'
API_URL = 'https://graph.qq.com/user/get_user_info'
OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me'
ICON_NAME = 'qq'
def __init__(self, access_token=None, openid=None):
config = self.get_config()
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(
QQOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, next_url='/'):
params = {
'response_type': 'code',
'client_id': self.client_id,
'redirect_uri': self.callback_url + '&next_url=' + next_url,
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
def get_access_token_by_code(self, code):
params = {
'grant_type': 'authorization_code',
'client_id': self.client_id,
'client_secret': self.client_secret,
'code': code,
'redirect_uri': self.callback_url
}
rsp = self.do_get(self.TOKEN_URL, params)
if rsp:
d = urllib.parse.parse_qs(rsp)
if 'access_token' in d:
token = d['access_token']
self.access_token = token[0]
return token
else:
raise OAuthAccessTokenException(rsp)
def get_open_id(self):
if self.is_access_token_set:
params = {
'access_token': self.access_token
}
rsp = self.do_get(self.OPEN_ID_URL, params)
if rsp:
rsp = rsp.replace(
'callback(', '').replace(
')', '').replace(
';', '')
obj = json.loads(rsp)
openid = str(obj['openid'])
self.openid = openid
return openid
def get_oauth_userinfo(self):
openid = self.get_open_id()
if openid:
params = {
'access_token': self.access_token,
'oauth_consumer_key': self.client_id,
'openid': self.openid
}
rsp = self.do_get(self.API_URL, params)
logger.info(rsp)
obj = json.loads(rsp)
user = OAuthUser()
user.nickname = obj['nickname']
user.openid = openid
user.type = 'qq'
user.token = self.access_token
user.metadata = rsp
if 'email' in obj:
user.email = obj['email']
if 'figureurl' in obj:
user.picture = str(obj['figureurl'])
return user
def get_picture(self, metadata):
datas = json.loads(metadata)
return str(datas['figureurl'])
@cache_decorator(expiration=100 * 60)

@ -128,8 +128,6 @@ class OauthLoginTest(TestCase):
"sub": "sub",
"email": "email",
})
# lrj测试获取访问令牌和用户信息
token = google_app.get_access_token_by_code('code')
userinfo = google_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token') # lrj验证访问令牌
@ -156,8 +154,6 @@ class OauthLoginTest(TestCase):
"id": "id",
"email": "email",
})
# lrj测试获取访问令牌和用户信息
token = github_app.get_access_token_by_code('code')
userinfo = github_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') # lrj验证访问令牌
@ -189,8 +185,6 @@ class OauthLoginTest(TestCase):
}
}
})
#lrj 测试获取访问令牌和用户信息
token = facebook_app.get_access_token_by_code('code')
userinfo = facebook_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token') # lrj验证访问令牌

Binary file not shown.

@ -8,6 +8,16 @@
<div id="content" role="main">
{% 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' %}
<nav class="nav-single">
<h3 class="assistive-text">文章导航</h3>
@ -45,6 +55,57 @@
{% endif %}
</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 %}
{% block sidebar %}

@ -19,11 +19,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="format-detection" content="telephone=no"/>
<meta name="theme-color" content="#21759b"/>
{% load blog_tags %}
{% head_meta %}
{% block header %}
<!-- SEO插件会自动生成title、description、keywords等标签 -->
{% endblock %}
<link rel="profile" href="http://gmpg.org/xfn/11"/>
<!-- 资源提示和预加载优化 -->
@ -31,7 +34,6 @@
<link rel="dns-prefetch" href="//cdn.jsdelivr.net"/>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin/>
<!--[if lt IE 9]>
<script src="{% static 'blog/js/html5.js' %}" type="text/javascript"></script>
<![endif]-->
@ -44,21 +46,40 @@
<!-- 本地字体加载 -->
<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/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 %}
<!--[if lt IE 9]>
<link rel='stylesheet' id='twentytwelve-ie-css' href='{% static 'blog/css/ie.css' %}' type='text/css' media='all' />
<![endif]-->
<link rel="stylesheet" href="{% static 'pygments/default.css' %}"/>
<link rel="stylesheet" href="{% static 'blog/css/nprogress.css' %}">
{% block compress_css %}
{% endblock %}
<!-- 插件CSS文件 - 集成到压缩系统 -->
{% plugin_compressed_css %}
{% endcompress %}
{% if GLOBAL_HEADER %}
{{ GLOBAL_HEADER|safe }}
@ -66,32 +87,73 @@
<!-- 插件head资源 -->
{% plugin_head_resources %}
{% block extra_css %}{% endblock %}
</head>
<body class="home blog custom-font-enabled">
<body class="home blog custom-font-enabled" data-theme="light">
<div id="page" class="hfeed site">
<header id="masthead" class="site-header" role="banner">
<hgroup>
<h1 class="site-title"><a href="/" title="{{ SITE_NAME }}" rel="home">{{ SITE_NAME }}</a>
</h1>
<h1 class="site-title"><a href="/" title="{{ SITE_NAME }}" rel="home">{{ SITE_NAME }}</a></h1>
<h2 class="site-description">{{ SITE_DESCRIPTION }}</h2>
</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 -->
<div id="main" class="wrapper">
<div id="main" class="wrapper">
{% block content %}
{% endblock %}
{% block sidebar %}
{% endblock %}
</div><!-- #main .wrapper -->
{% include 'share_layout/footer.html' %}
</div><!-- #page -->
@ -101,8 +163,12 @@
<script src="{% static 'blog/js/nprogress.js' %}"></script>
<script src="{% static 'blog/js/blog.js' %}"></script>
<script src="{% static 'blog/js/navigation.js' %}"></script>
<!-- 新增主题切换JS -->
<script src="{% static 'blog/js/theme-switcher.js' %}"></script>
{% block compress_js %}
{% endblock %}
<!-- 插件JS文件 - 集成到压缩系统 -->
{% plugin_compressed_js %}
{% endcompress %}
@ -115,5 +181,7 @@
<!-- 插件body资源 -->
{% plugin_body_resources %}
{% block extra_js %}{% endblock %}
</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