Squashed 'src/DjangoBlog/' changes from 13ebbc8..0bb6193

0bb6193 Generated by Django 5.2.7 on 2025-11-11 10:33
1de2b8d Revert "优化代码"
4a31b12 Merge remote-tracking branch 'DjangoBlog/g3f' into g3f-CodeEdit
46182cd Merge branch 'lrj_branch' into develop
8fb4c0f feat: [lrj] 完成OAuth模块代码质量分析和注释
0317508 feat: 完成OAuth模块代码质量分析和注释
1a46bfe 在更新后的代码基础上添加oauth注释
772627f Merge branch 'zy_branch' into develop
2ea3470 Merge remote-tracking branch 'origin/shw_branch' into develop
f31d984 Merge remote-tracking branch 'origin/bjy_branch' into develop
890a35a Merge commit 'eac243818d651281e841188481859d3e6e251cc8' into xjj_branch
930ea2b blog注释
e0b8ad3 Merge commit '001bc85a8157f3742d79d513e5300bcf3b975edb' into xjj_branch
77ced79 accounts代码注释
0265de5 代码注释
0619247 代码注释
b32484e 代码注释
676e04a Merge commit '4e5377d89454be93a12bdc5e1d147b806ae35205' as 'src/DjangoBlog'
e50afe5 移除子树以便重新添加其他分支
436a4e1 Merge remote-tracking branch 'DjangoBlog/master' into g3f
6f8f33f Merge commit '524e7d1ed53ff015743c250b8695258775e4ee8e' as 'src/DjangoBlog'
524e7d1 Squashed 'src/DjangoBlog/' content from commit ef67f8d
40f9915 删除子模块引用
1410b5c Merge remote-tracking branch 'origin/develop'
4eefae6 完善文档
05188c4 Merge remote-tracking branch 'origin/xjj_branch' into develop
20bb6cb 重新上传文件
ac34e1f 删除异常的文件
52a8438 重新上传上次提交
0b6d110 完成软件界面设计说明书
398a282 Merge branch 'xjj_branch' into develop
9fcc4d5 修改数据模型报告
700f5f7 Merge branch 'xjj_branch' into develop
2fc3891 撰写报告部分内容
6b840e8 chore: 重构项目结构
4fc3fd7 建模系统数据模型,第五周大作业
674cbb7 第五周大作业
94e7af9 docs: 推送数据模型设计文档初稿
ace2182 完成第四周作业
0059353 合并 lrj_branch
e7faf82 docs: 删除无用文件
c6e972f 合并 bjy_branch
efadb34 docs: 删除无用文件
f401f26 合并 shw_branch
7e7b376 docs: 删除无用文件
4cb4b01 docs: 删除无用文件
cd3fc6f 合并 zy_branch
b94181e docs: 删除无用文件
7734127 完成泛读报告
15b9d12 docs: 更新泛读报告
b72011b docs: 完成泛读报告
9b30a5b Merge branch 'master' into g3f
4359e3f 完成代码泛读报告第一项及第二项
707d204 docs(architecture): 添加项目架构图和模块类图
bd7d84e Update 345.py
dbb933d 改了一行代码
114143f Merge remote-tracking branch 'origin/develop' into zy_branch
89e3b1b 添加 DjangoBlog 子模块
df6020c feat(settings): 引入环境变量配置支持 - 添加 django-environ 和 python-dotenv依赖 - 配置文件中集成环境变量加载 - 数据库配置项改用环境变量读取 - 更新 .gitignore 忽略 .env 文件
ebfac63 chore(submodule): 添加 DjangoBlog 子模块
9e524cf 测试提交是否成功
efeca12 132
4e76e19 船舰草稿
9a54f8a chore: 新建文件夹
c4b01b8 docs(README): 更新项目标题为中文
177a703 chore(.gitignore): 添加.idea目录到忽略文件
e4a3779 Initial commit

git-subtree-dir: src/DjangoBlog
git-subtree-split: 0bb61937df7dc53156d445d5245a8bf965c7352b
develop
dynastxu 3 months ago
parent eac243818d
commit 72fddfe377

2
.gitignore vendored

@ -77,3 +77,5 @@ uploads/
settings_production.py
werobot_session.db
bin/datas/
.env

@ -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,27 +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(
@ -32,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(
@ -54,7 +77,7 @@ class ForgetPasswordForm(forms.Form):
)
new_password2 = forms.CharField(
label="确认密码",
label="确认密码", #shw 这里使用了中文硬编码,建议使用 _("Confirm password") 以支持国际化
widget=forms.PasswordInput(
attrs={
"class": "form-control",
@ -64,7 +87,7 @@ class ForgetPasswordForm(forms.Form):
)
email = forms.EmailField(
label='邮箱',
label='邮箱', #shw 这里使用了中文硬编码,建议使用 _("Email")
widget=forms.TextInput(
attrs={
'class': 'form-control',
@ -84,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 邮箱字段,标签支持国际化
)

@ -2,8 +2,8 @@
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):

@ -1,7 +1,7 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
import django.utils.timezone
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):

@ -1,36 +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,47 +1,64 @@
#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):
get_current_site().domain
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
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"
@ -51,38 +68,47 @@ 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(
email='user123@user.com')))
self.client.post(reverse('account:register'), {
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'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()
@ -94,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,
@ -164,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(
@ -185,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,19 +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")
return None
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,135 +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):
#shw 发送忘记密码验证码的视图继承自基础的View。
@staticmethod
def post(request: HttpRequest):
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
#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
@ -6,109 +7,160 @@ from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article
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__'
def makr_article_publish(queryset):
# bjy: 定义一个admin动作用于将选中的文章发布
def makr_article_publish(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的状态为'p'(已发布)
queryset.update(status='p')
def draft_article(queryset):
# bjy: 定义一个admin动作用于将选中的文章设为草稿
def draft_article(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的状态为'd'(草稿)
queryset.update(status='d')
def close_article_commentstatus(queryset):
# bjy: 定义一个admin动作用于关闭选中文章的评论功能
def close_article_commentstatus(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的评论状态为'c'(关闭)
queryset.update(comment_status='c')
def open_article_commentstatus(queryset):
# 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,60 +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'
class ArticleDocumentManager:
# bjy: 定义一个管理类用于操作ArticleDocument索引
class ArticleDocumentManager():
def __init__(self):
# bjy: 初始化时创建索引
self.create_index()
@staticmethod
def create_index():
def create_index(self):
# bjy: 创建'blog'索引
ArticleDocument.init()
@staticmethod
def delete_index():
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])
@staticmethod
def convert_to_doc(articles):
def convert_to_doc(self, articles):
# bjy: 将Django的Article查询集转换为ArticleDocument对象列表
return [
ArticleDocument(
meta={
@ -205,13 +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()
@staticmethod
def update_docs(docs):
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 blog.models import Article, Tag, Category
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,31 +28,43 @@ class Command(BaseCommand):
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
# 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,48 +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'
# 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 """
# 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:
time_taken = round(cast_time * 1000, 2)
# 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,9 +1,10 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
import django.utils.timezone
import mdeditor.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):

@ -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,9 +1,10 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
import django.utils.timezone
import mdeditor.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):

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

@ -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,96 @@ 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)
# 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 +174,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 +182,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 +204,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 +234,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 +284,7 @@ class Category(BaseModel):
parse(self)
return categorys
# bjy: 获取当前分类的所有子分类,带缓存
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
@ -240,136 +307,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;
}
}

@ -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;
});

@ -1,5 +1,7 @@
import hashlib
import logging
import random
import urllib
from django import template
from django.conf import settings
@ -12,11 +14,11 @@ from django.utils.safestring import mark_safe
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
from comments.models import Comment
from djangoblog.plugin_manage import hooks
from djangoblog.utils import CommonMarkdown, sanitize_html
from djangoblog.utils import cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__)
@ -166,7 +168,7 @@ def load_breadcrumb(article):
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
get_current_site().domain
site = get_current_site().domain
names.append((blogsetting.site_name, '/'))
names = names[::-1]
@ -361,7 +363,7 @@ def load_article_detail(article, isindex, user):
# 返回用户头像URL
# 模板使用方法: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email):
def gravatar_url(email, size=40):
"""获得用户头像 - 优先使用OAuth头像否则使用默认头像"""
cachekey = 'avatar/' + email
url = cache.get(cachekey)
@ -398,7 +400,7 @@ def gravatar_url(email):
@register.filter
def gravatar(email, size=40):
"""获得用户头像HTML标签"""
url = gravatar_url(email)
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d" class="avatar" alt="用户头像">' %
(url, size, size))

@ -1,5 +1,7 @@
# bjy: 导入操作系统接口模块
import os
# bjy: 从Django中导入设置、文件上传、命令调用、分页器、静态文件、测试工具、URL反向解析和时区工具
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
@ -9,34 +11,44 @@ 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 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):
get_current_site().domain
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)
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('admin/admin/logentry/')
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
s = SideBar()
s.sequence = 1
s.name = 'test'
@ -44,16 +56,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 +78,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 +98,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
gravatar_url('liangliangyy@gmail.com')
gravatar('liangliangyy@gmail.com')
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,18 +225,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)
# bjy: 测试自定义的管理命令
@staticmethod
def test_commands():
# bjy: 创建一个超级用户(如果不存在)
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -196,12 +250,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'
@ -213,6 +269,7 @@ class ArticleTest(TestCase):
}'''
u.save()
# bjy: 创建另一个OAuth用户用于测试
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -223,9 +280,11 @@ 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")

@ -1,60 +1,77 @@
# 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,

@ -1,7 +1,9 @@
# bjy: 导入日志、操作系统和UUID模块
import logging
import os
import uuid
# bjy: 从Django中导入设置、分页器、HTTP响应、快捷函数、静态文件、时区、国际化、CSRF豁免和基于类的视图
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
@ -13,114 +15,140 @@ from django.utils.translation import gettext_lazy as _
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):
"""
'''
缓存页面数据
:param cache_key: 缓存key
:return:
"""
'''
value = cache.get(cache_key)
if value:
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):
"""
'''
重写默认从缓存获取数据
:return:
"""
'''
key = self.get_queryset_cache_key()
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):
"""
'''
首页
"""
'''
# 友情链接类型
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):
"""
'''
文章详情页面
"""
'''
template_name = 'blog/article_detail.html'
model = Article
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:
@ -130,55 +158,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)
@ -188,10 +229,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
@ -200,12 +243,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'])
@ -213,12 +258,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
@ -226,12 +273,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)
@ -241,6 +290,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)
@ -250,6 +300,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
@ -258,32 +309,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 = {
@ -293,6 +352,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())
@ -300,6 +360,7 @@ class EsSearchView(SearchView):
return context
# bjy: 文件上传视图使用csrf_exempt豁免CSRF验证
@csrf_exempt
def fileupload(request):
"""
@ -308,45 +369,59 @@ 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,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
request.get_full_path()
url = request.get_full_path()
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
@ -354,6 +429,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,
@ -362,6 +438,7 @@ def server_error_view(request, template_name='blog/error_page.html'):
status=500)
# bjy: 自定义403权限拒绝错误处理视图
def permission_denied_view(
request,
exception,
@ -374,6 +451,7 @@ def permission_denied_view(
'statuscode': '403'}, status=403)
def clean_cache_view():
# bjy: 清除缓存的视图
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')

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

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

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

@ -1,8 +1,9 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):

@ -1,8 +1,9 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):

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

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

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

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

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

@ -45,7 +45,7 @@ send_email_signal = django.dispatch.Signal(
@receiver(send_email_signal)
def send_email_signal_handler(**kwargs):
def send_email_signal_handler(sender, **kwargs):
"""`xjj`
发送邮件信号的处理函数
@ -85,7 +85,7 @@ def send_email_signal_handler(**kwargs):
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(**kwargs):
def oauth_user_login_signal_handler(sender, **kwargs):
"""`xjj`
OAuth 用户登录信号处理函数
@ -109,8 +109,13 @@ def oauth_user_login_signal_handler(**kwargs):
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
update_fields):
created,
raw,
using,
update_fields,
**kwargs):
"""`xjj`
模型保存后的回调函数用于处理缓存清理和搜索引擎通知等操作
@ -166,7 +171,7 @@ def model_post_save_callback(
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(user):
def user_auth_callback(sender, request, user, **kwargs):
if user and user.username:
logger.info(user)
delete_sidebar_cache()

@ -73,8 +73,7 @@ class ElasticSearchBackend(BaseSearchBackend):
docs = self._get_models(models)
self.manager.rebuild(docs)
@staticmethod
def _delete(models):
def _delete(self, models):
"""`xjj`
删除指定模型对象
@ -207,8 +206,7 @@ class ElasticSearchBackend(BaseSearchBackend):
class ElasticSearchQuery(BaseSearchQuery):
@staticmethod
def _convert_datetime(date):
def _convert_datetime(self, date):
"""`xjj`
将日期时间对象转换为字符串格式

@ -36,8 +36,7 @@ class DjangoBlogFeed(Feed):
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
@staticmethod
def author_name():
def author_name(self):
"""`xjj`
获取 RSS 订阅源的作者名称
@ -47,8 +46,7 @@ class DjangoBlogFeed(Feed):
"""
return get_user_model().objects.first().nickname
@staticmethod
def author_link():
def author_link(self):
"""`xjj`
获取 RSS 订阅源作者的链接地址
@ -58,8 +56,7 @@ class DjangoBlogFeed(Feed):
"""
return get_user_model().objects.first().get_absolute_url()
@staticmethod
def items():
def items(self):
"""`xjj`
获取 RSS 订阅源的文章列表
@ -95,8 +92,7 @@ class DjangoBlogFeed(Feed):
"""
return CommonMarkdown.get_markdown(item.body)
@staticmethod
def feed_copyright():
def feed_copyright(self):
"""`xjj`
获取 RSS 订阅源的版权信息

@ -112,28 +112,23 @@ class BasePlugin:
"""渲染文章底部组件"""
return None
@staticmethod
def render_article_top_widget():
def render_article_top_widget(self, context, **kwargs):
"""渲染文章顶部组件"""
return None
@staticmethod
def render_header_widget():
def render_header_widget(self, context, **kwargs):
"""渲染页头组件"""
return None
@staticmethod
def render_footer_widget():
def render_footer_widget(self, context, **kwargs):
"""渲染页脚组件"""
return None
@staticmethod
def render_comment_before_widget():
def render_comment_before_widget(self, context, **kwargs):
"""渲染评论前组件"""
return None
@staticmethod
def render_comment_after_widget():
def render_comment_after_widget(self, context, **kwargs):
"""渲染评论后组件"""
return None
@ -174,13 +169,11 @@ class BasePlugin:
"""获取插件JavaScript文件列表"""
return []
@staticmethod
def get_head_html():
def get_head_html(self, context=None):
"""获取需要插入到<head>中的HTML内容"""
return ""
@staticmethod
def get_body_html():
def get_body_html(self, context=None):
"""获取需要插入到<body>底部的HTML内容"""
return ""

@ -1,6 +1,5 @@
import logging
import os
import logging
from django.conf import settings
logger = logging.getLogger(__name__)

@ -15,6 +15,11 @@ 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`
@ -120,14 +125,17 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application'
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',
'NAME': env('DJANGO_MYSQL_DATABASE'),
'USER': env('DJANGO_MYSQL_USER'),
'PASSWORD': env('DJANGO_MYSQL_PASSWORD'),
'HOST': env('DJANGO_MYSQL_HOST'),
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
env('DJANGO_MYSQL_PORT')),
'OPTIONS': {
'charset': 'utf8mb4'},
'charset': 'utf8mb4',
'ssl_mode': 'VERIFY_IDENTITY',
'ssl': {'ca': env('DJANGO_MYSQL_SSL_CA')}
},
}}
# Password validation

@ -82,8 +82,7 @@ class ArticleSiteMap(Sitemap):
"""
return Article.objects.filter(status='p')
@staticmethod
def lastmod(obj):
def lastmod(self, obj):
"""`xjj`
获取指定文章对象的最后修改时间
@ -118,8 +117,7 @@ class CategorySiteMap(Sitemap):
"""
return Category.objects.all()
@staticmethod
def lastmod(obj):
def lastmod(self, obj):
"""`xjj`
获取分类对象的最后修改时间
@ -154,8 +152,7 @@ class TagSiteMap(Sitemap):
"""
return Tag.objects.all()
@staticmethod
def lastmod(obj):
def lastmod(self, obj):
"""`xjj`
获取指定标签对象的最后修改时间
@ -193,8 +190,7 @@ class UserSiteMap(Sitemap):
"""
return list(set(map(lambda x: x.author, Article.objects.all())))
@staticmethod
def lastmod(obj):
def lastmod(self, obj):
"""`xjj`
获取指定用户对象的最后修改时间

@ -26,7 +26,7 @@ from django.conf import settings
logger = logging.getLogger(__name__)
class SpiderNotify:
class SpiderNotify():
"""`xjj`
爬虫通知类用于向搜索引擎提交 URL 通知

@ -13,16 +13,15 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
import time
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.http import JsonResponse
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from django.http import JsonResponse
import time
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
@ -44,7 +43,7 @@ handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
def health_check():
def health_check(request):
"""
健康检查接口
简单返回服务健康状态

@ -52,7 +52,7 @@ def get_max_articleid_commentid():
"""
from blog.models import Article
from comments.models import Comment
return Article.objects.latest().pk, Comment.objects.latest().pk
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
@ -138,14 +138,14 @@ def cache_decorator(expiration=3 * 60):
def expire_view_cache(path, servername, serverport, key_prefix=None):
"""
'''
刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
"""
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
@ -332,11 +332,11 @@ def get_blog_setting():
def save_user_avatar(url):
"""
'''
保存用户头像
:param url:头像url
:return: 本地路径
"""
'''
logger.info(url)
try:
@ -418,7 +418,7 @@ ALLOWED_CLASSES = [
's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il'
]
def class_filter(name, value):
def class_filter(tag, name, value):
"""`xjj`
自定义 class 属性过滤器

@ -29,11 +29,11 @@ import re
import shutil
import threading
import warnings
from datetime import datetime
import six
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from datetime import datetime
from django.utils.encoding import force_str
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
from haystack.constants import DJANGO_CT, DJANGO_ID, ID
@ -155,7 +155,7 @@ class WhooshSearchBackend(BaseSearchBackend):
connections[self.connection_alias].get_unified_index().all_searchfields())
self.parser = QueryParser(self.content_field_name, schema=self.schema)
if new_index:
if new_index is True:
self.index = self.storage.create_index(self.schema)
else:
try:
@ -230,7 +230,7 @@ class WhooshSearchBackend(BaseSearchBackend):
raise SearchBackendError(
"No fields were found in any search_indexes. Please correct this before attempting to search.")
return content_field_name, Schema(**schema_fields)
return (content_field_name, Schema(**schema_fields))
def update(self, index, iterable, commit=True):
"""`xjj`
@ -389,8 +389,7 @@ class WhooshSearchBackend(BaseSearchBackend):
self.index = self.index.refresh()
self.index.optimize()
@staticmethod
def calculate_page(start_offset=0, end_offset=None):
def calculate_page(self, start_offset=0, end_offset=None):
"""`xjj`
根据偏移量计算分页信息
@ -699,7 +698,7 @@ class WhooshSearchBackend(BaseSearchBackend):
# Deferred models will have a different class ("RealClass_Deferred_fieldname")
# which won't be in our registry:
model_instance._meta.concrete_model
model_klass = model_instance._meta.concrete_model
field_name = self.content_field_name
narrow_queries = set()
@ -944,8 +943,7 @@ class WhooshSearchBackend(BaseSearchBackend):
spelling_suggestion = ' '.join(suggested_words)
return spelling_suggestion
@staticmethod
def _from_python(value):
def _from_python(self, value):
"""
Converts Python values to a string for Whoosh.
@ -968,8 +966,7 @@ class WhooshSearchBackend(BaseSearchBackend):
value = force_str(value)
return value
@staticmethod
def _to_python(value):
def _to_python(self, value):
"""
Converts values from Whoosh to native Python values.
@ -1021,8 +1018,7 @@ class WhooshSearchBackend(BaseSearchBackend):
class WhooshSearchQuery(BaseSearchQuery):
@staticmethod
def _convert_datetime(date):
def _convert_datetime(self, date):
"""`xjj`
将日期时间对象转换为 Whoosh 可识别的字符串格式
@ -1079,7 +1075,7 @@ class WhooshSearchQuery(BaseSearchQuery):
str: 构建好的查询片段字符串
"""
from haystack import connections
''
query_frag = ''
is_datetime = False
if not hasattr(value, 'input_type_name'):
@ -1124,7 +1120,7 @@ class WhooshSearchQuery(BaseSearchQuery):
'fuzzy': u'%s~',
}
if not value.post_process:
if value.post_process is False:
query_frag = prepared_value
else:
if filter_type in [
@ -1143,7 +1139,7 @@ class WhooshSearchQuery(BaseSearchQuery):
if isinstance(prepared_value, six.string_types):
possible_values = prepared_value.split(' ')
else:
if is_datetime:
if is_datetime is True:
prepared_value = self._convert_datetime(
prepared_value)
@ -1169,7 +1165,7 @@ class WhooshSearchQuery(BaseSearchQuery):
pv = self.backend._from_python(possible_value)
if is_datetime:
if is_datetime is True:
pv = self._convert_datetime(pv)
if isinstance(pv, six.string_types) and not is_datetime:
@ -1196,7 +1192,7 @@ class WhooshSearchQuery(BaseSearchQuery):
prepared_value = Exact(prepared_value).prepare(self)
query_frag = filter_types[filter_type] % prepared_value
else:
if is_datetime:
if is_datetime is True:
prepared_value = self._convert_datetime(prepared_value)
query_frag = filter_types[filter_type] % prepared_value

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

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

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

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

@ -1,8 +1,9 @@
# Generated by Django 4.1.7 on 2023-03-07 09:53
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):

@ -1,8 +1,9 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):

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

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

@ -0,0 +1,67 @@
# Create your models here.
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
class OAuthUser(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=True,
null=True,
on_delete=models.CASCADE)
openid = models.CharField(max_length=50)
nickname = models.CharField(max_length=50, verbose_name=_('nick name'))
token = models.CharField(max_length=150, null=True, blank=True)
picture = models.CharField(max_length=350, blank=True, null=True)
type = models.CharField(blank=False, null=False, max_length=50)
email = models.CharField(max_length=50, null=True, blank=True)
metadata = models.TextField(null=True, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
def __str__(self):
return self.nickname
class Meta:
verbose_name = _('oauth user')
verbose_name_plural = verbose_name
ordering = ['-creation_time']
class OAuthConfig(models.Model):
TYPE = (
('weibo', _('weibo')),
('google', _('google')),
('github', 'GitHub'),
('facebook', 'FaceBook'),
('qq', 'QQ'),
)
type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
appkey = models.CharField(max_length=200, verbose_name='AppKey')
appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
callback_url = models.CharField(
max_length=200,
verbose_name=_('callback url'),
blank=False,
default='')
is_enable = models.BooleanField(
_('is enable'), default=True, blank=False, null=False)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
def clean(self):
if OAuthConfig.objects.filter(
type=self.type).exclude(id=self.id).count():
raise ValidationError(_(self.type + _('already exists')))
def __str__(self):
return self.type
class Meta:
verbose_name = 'oauth配置'
verbose_name_plural = verbose_name
ordering = ['-creation_time']

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

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

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

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

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
import django.utils.timezone
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):

@ -42,7 +42,7 @@ class OwnTrackLogTest(TestCase):
rsp = self.client.get('/owntracks/show_maps')
self.assertEqual(rsp.status_code, 302)
BlogUser.objects.create_superuser(
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")

@ -1,5 +1,5 @@
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
@ -14,8 +14,7 @@ class ArticleCopyrightPlugin(BasePlugin):
# 在这里将插件的方法注册到指定的钩子上
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_copyright_to_content)
@staticmethod
def add_copyright_to_content(content, **kwargs):
def add_copyright_to_content(self, content, *args, **kwargs):
"""
这个方法会被注册到 'the_content' 过滤器钩子上
它接收原始内容并返回添加了版权信息的新内容

@ -1,9 +1,8 @@
import logging
from blog.models import Article
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
from blog.models import Article
logger = logging.getLogger(__name__)
@ -34,7 +33,7 @@ class ArticleRecommendationPlugin(BasePlugin):
"""注册钩子"""
hooks.register(ARTICLE_DETAIL_LOAD, self.on_article_detail_load)
def on_article_detail_load(self, article, context):
def on_article_detail_load(self, article, context, request, *args, **kwargs):
"""文章详情页加载时的处理"""
# 可以在这里预加载推荐数据到context中
recommendations = self.get_recommendations(article)
@ -195,8 +194,7 @@ class ArticleRecommendationPlugin(BasePlugin):
return valid_recommendations[:count]
@staticmethod
def get_popular_articles(count=3):
def get_popular_articles(self, count=3):
"""获取热门文章"""
return list(Article.objects.filter(
status='p'

@ -1,8 +1,7 @@
import re
from urllib.parse import urlparse
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
@ -15,8 +14,7 @@ class ExternalLinksPlugin(BasePlugin):
def register_hooks(self):
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.process_external_links)
@staticmethod
def process_external_links(content):
def process_external_links(self, content, *args, **kwargs):
from djangoblog.utils import get_current_site
site_domain = get_current_site().domain

@ -1,9 +1,8 @@
import hashlib
import re
import hashlib
from urllib.parse import urlparse
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
@ -28,7 +27,7 @@ class ImageOptimizationPlugin(BasePlugin):
def register_hooks(self):
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.optimize_images)
def optimize_images(self, content):
def optimize_images(self, content, *args, **kwargs):
"""
优化文章中的图片标签
"""
@ -64,8 +63,7 @@ class ImageOptimizationPlugin(BasePlugin):
return optimized_content
@staticmethod
def _parse_img_attributes(attr_string):
def _parse_img_attributes(self, attr_string):
"""
解析 img 标签的属性
"""
@ -152,8 +150,7 @@ class ImageOptimizationPlugin(BasePlugin):
return attrs
@staticmethod
def _build_img_tag(attrs):
def _build_img_tag(self, attrs):
"""
重新构建 img 标签
"""
@ -170,8 +167,7 @@ class ImageOptimizationPlugin(BasePlugin):
return f'<img {" ".join(attr_strings)}>'
@staticmethod
def _get_current_domain():
def _get_current_domain(self):
"""
获取当前网站域名
"""

@ -1,8 +1,7 @@
import math
import re
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
@ -15,8 +14,7 @@ class ReadingTimePlugin(BasePlugin):
def register_hooks(self):
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_reading_time)
@staticmethod
def add_reading_time(content, **kwargs):
def add_reading_time(self, content, *args, **kwargs):
"""
计算阅读时间并添加到内容开头
只在文章详情页显示首页文章列表页不显示

@ -1,10 +1,9 @@
import json
from django.utils.html import strip_tags
from blog.models import Article, Category
from djangoblog.plugin_manage import hooks
from django.template.defaultfilters import truncatewords
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from blog.models import Article, Category, Tag
from djangoblog.utils import get_blog_setting
@ -17,8 +16,7 @@ class SeoOptimizerPlugin(BasePlugin):
def register_hooks(self):
hooks.register('head_meta', self.dispatch_seo_generation)
@staticmethod
def _get_article_seo_data(context, request, blog_setting):
def _get_article_seo_data(self, context, request, blog_setting):
article = context.get('article')
if not isinstance(article, Article):
return None
@ -63,8 +61,7 @@ class SeoOptimizerPlugin(BasePlugin):
"json_ld": structured_data
}
@staticmethod
def _get_category_seo_data(context, request, blog_setting):
def _get_category_seo_data(self, context, request, blog_setting):
category_name = context.get('tag_name')
if not category_name:
return None
@ -95,8 +92,7 @@ class SeoOptimizerPlugin(BasePlugin):
"json_ld": structured_data
}
@staticmethod
def _get_default_seo_data(request, blog_setting):
def _get_default_seo_data(self, context, request, blog_setting):
# Homepage and other default pages
structured_data = {
"@context": "https://schema.org",
@ -133,7 +129,7 @@ class SeoOptimizerPlugin(BasePlugin):
seo_data = self._get_category_seo_data(context, request, blog_setting)
if not seo_data:
seo_data = self._get_default_seo_data(request, blog_setting)
seo_data = self._get_default_seo_data(context, request, blog_setting)
json_ld_script = f'<script type="application/ld+json">{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}</script>'

@ -1,5 +1,5 @@
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
class ViewCountPlugin(BasePlugin):
@ -11,8 +11,7 @@ class ViewCountPlugin(BasePlugin):
def register_hooks(self):
hooks.register('after_article_body_get', self.record_view)
@staticmethod
def record_view(article):
def record_view(self, article, *args, **kwargs):
article.viewed()

Binary file not shown.

@ -14,8 +14,7 @@ class BlogApi:
sqs = sqs.load_all()
return sqs[:self.__max_takecount__]
@staticmethod
def get_category_lists():
def get_category_lists(self):
return Category.objects.all()
def get_category_articles(self, categoryname):

@ -44,8 +44,7 @@ class CommandHandler:
else:
return "未找到相关命令请输入hepme获得帮助。"
@staticmethod
def __run_command__(cmd):
def __run_command__(self, cmd):
try:
res = os.popen(cmd).read()
return res

@ -47,7 +47,7 @@ def convert_to_article_reply(articles, message):
@robot.filter(re.compile(r"^\?.*"))
def search(message):
def search(message, session):
s = message.content
searchstr = str(s).replace('?', '')
result = blogapi.search_articles(searchstr)
@ -60,14 +60,14 @@ def search(message):
@robot.filter(re.compile(r'^category\s*$', re.I))
def category():
def category(message, session):
categorys = blogapi.get_category_lists()
content = ','.join(map(lambda x: x.name, categorys))
return '所有文章分类目录:' + content
@robot.filter(re.compile(r'^recent\s*$', re.I))
def recents(message):
def recents(message, session):
articles = blogapi.get_recent_articles()
if articles:
reply = convert_to_article_reply(articles, message)
@ -77,7 +77,7 @@ def recents(message):
@robot.filter(re.compile('^help$', re.I))
def help():
def help(message, session):
return '''欢迎关注!
默认会与图灵机器人聊天~~
你可以通过下面这些命令来获得信息
@ -99,12 +99,12 @@ def help():
@robot.filter(re.compile(r'^weather\:.*$', re.I))
def weather():
def weather(message, session):
return "建设中..."
@robot.filter(re.compile(r'^idcard\:.*$', re.I))
def idcard():
def idcard(message, session):
return "建设中..."
@ -123,7 +123,7 @@ class MessageHandler:
try:
info = session[userid]
self.userinfo = jsonpickle.decode(info)
except Exception:
except Exception as e:
userinfo = WxUserInfo()
self.userinfo = userinfo
@ -179,7 +179,7 @@ class MessageHandler:
return ChatGPT.chat(info)
class WxUserInfo:
class WxUserInfo():
def __init__(self):
self.isAdmin = False
self.isPasswordSet = False

@ -1,4 +1,5 @@
from django.test import Client, RequestFactory, TestCase
from django.utils import timezone
from werobot.messages.messages import TextMessage
from accounts.models import BlogUser
@ -41,10 +42,10 @@ class ServerManagerTest(TestCase):
article.save()
s = TextMessage([])
s.content = "nice"
search(s)
rsp = category()
rsp = search(s, None)
rsp = category(None, None)
self.assertIsNotNone(rsp)
rsp = recents(None)
rsp = recents(None, None)
self.assertTrue(rsp != '暂时还没有文章')
cmd = commands()

Loading…
Cancel
Save