Compare commits

..

14 Commits

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -5,7 +5,6 @@
"version": "0.2.0",
"configurations": [
{
"name": "Python 调试程序: 当前文件",

@ -1,15 +1,9 @@
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
from django import forms
from django.contrib.auth.admin import UserAdmin # 导入Django默认的用户管理Admin类基础模板
<<<<<<< HEAD
=======
from django import forms
<<<<<<< HEAD
from django.contrib.auth.admin import UserAdmin # 导入Django默认的用户管理Admin类基础模板
>>>>>>> f82823537c318f20f273c53570c0d37f22c7a751
from django.contrib.auth.forms import UserChangeForm # 导入默认的用户编辑表单(用于继承修改)
from django.contrib.auth.forms import UsernameField# 用户名字段的专用类(自带验证逻辑)
from django.utils.translation import gettext_lazy as _# 国际化支持,文本可翻译
@ -24,7 +18,6 @@ from django.utils.translation import gettext_lazy as _# 导入国际化翻译工
# Register your models here.
from .models import BlogUser # 导入自定义的用户模型替代Django默认User
=======
<<<<<<< HEAD
from django.contrib.auth.forms import UserChangeForm # 导入默认的用户编辑表单(用于继承修改)
from django.contrib.auth.forms import UsernameField # 用户名字段的专用类(自带验证逻辑)
from django.utils.translation import gettext_lazy as _ # 国际化支持,文本可翻译
@ -54,16 +47,6 @@ from .models import BlogUser# 从当前目录的 models 模块导入自定义的
from .models import BlogUser # 导入自定义的用户模型替代Django默认User)
<<<<<<< HEAD
=======
from django.contrib.auth.admin import UserAdmin # xh:导入Django默认的用户管理Admin类基础模板
from django.contrib.auth.forms import UserChangeForm # xh:导入默认的用户编辑表单(用于继承修改)
from django.contrib.auth.forms import UsernameField # xh:用户名字段的专用类(自带验证逻辑)
from django.utils.translation import gettext_lazy as _ # xh:国际化支持,文本可翻译
# Register your models here.
from .models import BlogUser # xh:导入自定义的用户模型替代Django默认User)
>>>>>>> f82823537c318f20f273c53570c0d37f22c7a751
>>>>>>> xh_branch
# 自定义用户创建表单(用于在后台添加新用户)
@ -72,7 +55,6 @@ class BlogUserCreationForm(forms.ModelForm):
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)# 确认密码字段(再次输入密码)
class Meta:
<<<<<<< HEAD
model = BlogUser # 绑定自定义的BlogUser模型
fields = ('email',) # 新增用户时,默认显示的核心字段(仅邮箱,其他字段可后续编辑)
# 密码验证逻辑:检查两次输入的密码是否一致
@ -90,14 +72,6 @@ class BlogUserCreationForm(forms.ModelForm):# 添加一个密码字段label
>>>>>>> psy_branch
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")# 获取第一个密码框的清洗后数据
=======
model = BlogUser # xh:绑定自定义的BlogUser模型
fields = ('email',) # xh:新增用户时,默认显示的核心字段(仅邮箱,其他字段可后续编辑)
def clean_password2(self):
# xh:密码验证逻辑:检查两次输入的密码是否一致
password1 = self.cleaned_data.get("password1")# xh:获取第一个密码框的清洗后数据
>>>>>>> xh_branch
password2 = self.cleaned_data.get("password2")
<<<<<<< HEAD
if password1 and password2 and password1 != password2:
@ -108,20 +82,6 @@ class BlogUserCreationForm(forms.ModelForm):# 添加一个密码字段label
raise forms.ValidationError(_("passwords do not match"))
return password2
=======
<<<<<<< HEAD
=======
from django import forms# 从 django 核心模块导入 forms 用于表单处理
from django.contrib.auth.admin import UserAdmin# 从 django 内置的 auth 管理模块导入 UserAdmin用于用户管理界面的定制
from django.contrib.auth.forms import UserChangeForm# 从 django 内置的 auth 表单模块导入 UserChangeForm用于用户信息修改表单的定制
from django.contrib.auth.forms import UsernameField# 从 django 内置的 auth 表单模块导入 UsernameField用于用户名字段的处理
from django.utils.translation import gettext_lazy as _# 从 django 翻译工具模块导入 gettext_lazy 并别名成 _用于实现国际化翻译延迟翻译
<<<<<<< HEAD
# Register your models here.
from .models import BlogUser# 从当前目录的 models 模块导入自定义的 BlogUser 模型
>>>>>>> f82823537c318f20f273c53570c0d37f22c7a751
class BlogUserCreationForm(forms.ModelForm):# 定义 BlogUserCreationForm 类,继承自 forms.ModelForm用于创建 BlogUser 的表单
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) # 定义 password1 字段,类型为 CharField标签为“password”小部件使用 PasswordInput密码输入框内容会隐藏
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)# 定义 password2 字段,类型为 CharField标签为“Enter password again”小部件同样使用 PasswordInput
@ -146,13 +106,6 @@ class BlogUserCreationForm(forms.ModelForm):# 定义 BlogUserCreationForm 类,
>>>>>>> psy_branch
# Save the provided password in hashed format
<<<<<<< HEAD
<<<<<<< HEAD
=======
=======
def save(self, commit=True):
#xh:将提供的密码以哈希格式保存
>>>>>>> xh_branch
>>>>>>> f82823537c318f20f273c53570c0d37f22c7a751
user = super().save(commit=False)
# 使用 Django 内置方法加密密码
user.set_password(self.cleaned_data["password1"])

@ -1,8 +1,5 @@
from django.apps import AppConfig # xh:导入Django的应用配置基类
from django.apps import AppConfig
class AccountsConfig(AppConfig):
#xh:Accounts应用的配置类,继承自Django的AppConfig基类用于配置accounts应用的各种设置
# xh:应用名称Django使用这个名称来识别应用
# xh:必须与应用的目录名保持一致
name = 'accounts'

@ -9,91 +9,62 @@ from .models import BlogUser
class LoginForm(AuthenticationForm):
#xh:用户登录表单,继承自Django内置的AuthenticationForm提供用户认证功能
def __init__(self, *args, **kwargs):
#xh:初始化方法自定义表单字段的widget属性,设置输入框的placeholder和CSS类
super(LoginForm, self).__init__(*args, **kwargs)
# xh:设置用户名字段的输入框属性
self.fields['username'].widget =widgets.TextInput(
attrs={
'placeholder': "username", # xh:占位符文本
"class": "form-control" # xh:Bootstrap样式类
})
# 设置密码字段的输入框属性
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['password'].widget = widgets.PasswordInput(
attrs={
'placeholder': "password", # xh:占位符文本
"class": "form-control" # xh:Bootstrap样式类
})
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
#xh:用户注册表单,继承自Django内置的UserCreationForm提供用户注册功能,自动包含密码验证逻辑(密码强度、两次密码一致性等)
def __init__(self, *args, **kwargs):
#xh:初始化方法自定义所有表单字段的widget属性
super(RegisterForm, self).__init__(*args, **kwargs)
# xh:设置用户名字段的输入框属性
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# xh:设置邮箱字段的输入框属性
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
# xh:设置密码字段的输入框属性
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# xh:设置确认密码字段的输入框属性
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
#xh:邮箱字段验证方法,确保邮箱地址在系统中唯一
email = self.cleaned_data['email']
# xh:检查邮箱是否已存在
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
#xh:表单元数据配置"""
model = get_user_model() # xh:使用项目中配置的用户模型
fields = ("username", "email") # xh:表单包含的字段
model = get_user_model()
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
#xh:忘记密码表单(用于验证码和密码重置),包含新密码设置、邮箱验证和验证码验证
# xh:新密码字段
new_password1 = forms.CharField(
label=_("New password"), # xh:字段标签(支持国际化)
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control", # xh:Bootstrap样式
'placeholder': _("New password") # xh:占位符文本
"class": "form-control",
'placeholder': _("New password")
}
),
)
# xh:确认密码字段
new_password2 = forms.CharField(
label="确认密码", # xh:中文标签
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password") # xh:英文占位符
'placeholder': _("Confirm password")
}
),
)
# xh:邮箱字段(用于验证用户身份)
email = forms.EmailField(
label='邮箱', # xh:中文标签
widget=forms.TextInput( # xh:使用TextInput而不是EmailInput以便更好地控制样式
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
@ -101,9 +72,8 @@ class ForgetPasswordForm(forms.Form):
),
)
# xh:验证码字段(用于二次验证)
code = forms.CharField(
label=_('Code'), # xh:验证码标签
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
@ -113,44 +83,35 @@ class ForgetPasswordForm(forms.Form):
)
def clean_new_password2(self):
#xh:确认密码验证方法,验证两次输入的密码是否一致,并检查密码强度
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
# xh:检查两次密码是否一致
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
# xh:使用Django内置的密码验证器验证密码强度
password_validation.validate_password(password2)
return password2
def clean_email(self):
#xh:邮箱验证方法,验证邮箱是否在系统中已注册
user_email = self.cleaned_data.get("email")
# xh:检查邮箱是否存在(是否已注册)
if not BlogUser.objects.filter(email=user_email).exists():
# TODO: 这里的报错提示可能会暴露邮箱是否注册,根据安全需求可以修改
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
#xh:验证码验证方法,调用utils模块的验证函数检查验证码是否正确
code = self.cleaned_data.get("code")
# xh:调用验证工具函数检查验证码
error = utils.verify(
email=self.cleaned_data.get("email"), # xh:传入邮箱
code=code, # xh:传入验证码
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error) # xh:验证失败,抛出错误
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
#xh:忘记密码验证码请求表单,简化版表单,仅用于请求发送密码重置验证码
email = forms.EmailField(
label=_('Email'), # xh:只需要邮箱字段来请求发送验证码
)
label=_('Email'),
)

@ -1,5 +1,4 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# xh:自动生成的Django迁移文件记录数据库结构变更
import django.contrib.auth.models
import django.contrib.auth.validators
@ -8,102 +7,43 @@ import django.utils.timezone
class Migration(migrations.Migration):
#xh:数据库迁移类用于创建BlogUser用户模型
initial = True # xh:标记为初始迁移
initial = True
# xh:依赖的迁移文件
dependencies = [
('auth', '0012_alter_user_first_name_max_length'), # xh:依赖Django的auth应用
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='BlogUser', # xh:创建BlogUser模型表
name='BlogUser',
fields=[
# xh:主键ID自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# xh:Django认证系统必需的密码字段
('password', models.CharField(max_length=128, verbose_name='password')),
# xh:最后登录时间,可为空
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
# xh:超级用户标志
('is_superuser', models.BooleanField(default=False,
help_text='Designates that this user has all permissions without explicitly assigning them.',
verbose_name='superuser status')),
# xh:用户名字段,具有唯一性验证和字符限制
('username', models.CharField(
error_messages={'unique': 'A user with that username already exists.'},
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
max_length=150,
unique=True,
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
verbose_name='username')),
# xh:名字字段,可为空
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
# xh:姓氏字段,可为空
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
# xh:邮箱字段,可为空
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
# xh:员工状态决定是否可以登录admin后台
('is_staff', models.BooleanField(default=False,
help_text='Designates whether the user can log into this admin site.',
verbose_name='staff status')),
# xh:活跃状态,用于软删除
('is_active', models.BooleanField(default=True,
help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.',
verbose_name='active')),
# xh:账户创建时间
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
# xh:自定义字段:昵称
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
# 自定义字段:记录创建时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# xh:自定义字段:最后修改时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# xh:自定义字段:用户创建来源(如:网站注册、第三方登录等)
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
# xh:用户组多对多关系
('groups', models.ManyToManyField(blank=True,
help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
related_name='user_set',
related_query_name='user',
to='auth.group',
verbose_name='groups')),
# xh:用户权限多对多关系
('user_permissions', models.ManyToManyField(blank=True,
help_text='Specific permissions for this user.',
related_name='user_set',
related_query_name='user',
to='auth.permission',
verbose_name='user permissions')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
# xh:模型元数据配置
options={
'verbose_name': '用户', # xh:单数显示名称
'verbose_name_plural': '用户', # xh:复数显示名称
'ordering': ['-id'], # xh:默认按ID降序排列
'get_latest_by': 'id', # xh:指定获取最新记录的字段
'verbose_name': '用户',
'verbose_name_plural': '用户',
'ordering': ['-id'],
'get_latest_by': 'id',
},
# xh:指定模型管理器
managers=[
('objects', django.contrib.auth.models.UserManager()), # xh:使用Django默认的用户管理器
('objects', django.contrib.auth.models.UserManager()),
],
),
]
]

@ -1,81 +1,46 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
# xh:自动生成的Django迁移文件用于修改BlogUser模型结构
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
#xh:数据库迁移类用于修改BlogUser模型的字段和配置
# xh:依赖之前的初始迁移文件
dependencies = [
('accounts', '0001_initial'), # xh:依赖accounts应用的初始迁移
('accounts', '0001_initial'),
]
operations = [
# xh:修改模型的元数据选项
migrations.AlterModelOptions(
name='bloguser',
options={
'get_latest_by': 'id', # xh:指定按id字段获取最新记录
'ordering': ['-id'], # xh:按id降序排列
'verbose_name': 'user', # xh:修改单数显示名称为英文'user'
'verbose_name_plural': 'user', # xh:修改复数显示名称为英文'user'
},
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
# xh:删除旧的创建时间字段
migrations.RemoveField(
model_name='bloguser',
name='created_time', # xh:移除原有的created_time字段
name='created_time',
),
# xh:删除旧的修改时间字段
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time', # xh:移除原有的last_mod_time字段
name='last_mod_time',
),
# xh:添加新的创建时间字段(重命名)
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(
default=django.utils.timezone.now, # xh:默认值为当前时间
verbose_name='creation time' # xh:字段显示名称为英文
),
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# xh:添加新的最后修改时间字段(重命名)
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(
default=django.utils.timezone.now, # xh:默认值为当前时间
verbose_name='last modify time' # xh:字段显示名称为英文
),
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
# xh:修改昵称字段的显示名称
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(
blank=True,
max_length=100,
verbose_name='nick name' # xh:从中文'昵称'改为英文'nick name'
),
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
# xh:修改来源字段的显示名称
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(
blank=True,
max_length=100,
verbose_name='create source' # xh:从中文'创建来源'改为英文'create source'
),
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]
]

@ -1,64 +1,35 @@
# xh:导入必要的模块
from django.contrib.auth.models import AbstractUser # xh:Django抽象用户基类
from django.db import models # xh:Django模型字段
from django.urls import reverse # xh:URL反向解析
from django.utils.timezone import now # xh:获取当前时间
from django.utils.translation import gettext_lazy as _ # xh:国际化翻译
from djangoblog.utils import get_current_site # xh:自定义工具函数,获取当前站点
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
#xh:自定义用户模型,继承自Django的AbstractUser扩展了博客系统的用户功能
# xh:昵称字段 - 用户的显示名称
nickname = models.CharField(
_('nick name'), # xh:字段显示名称(支持国际化)
max_length=100, # xh:最大长度100字符
blank=True # xh:允许为空
)
# xh:创建时间 - 记录用户注册时间
creation_time = models.DateTimeField(
_('creation time'), # xh:字段显示名称
default=now # xh:默认值为当前时间
)
# xh:最后修改时间 - 记录用户信息最后修改时间
last_modify_time = models.DateTimeField(
_('last modify time'), # xh:字段显示名称
default=now # xh:默认值为当前时间
)
# xh:创建来源 - 记录用户的注册渠道
source = models.CharField(
_('create source'), # xh:字段显示名称
max_length=100, # xh:最大长度100字符
blank=True # xh:允许为空
)
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
#xh:获取用户的绝对URL用于后台管理等场景,返回用户详情页的URL
return reverse(
'blog:author_detail', # xh:URL配置的名称
kwargs={'author_name': self.username} # xh:传递用户名参数
)
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
#xh:对象的字符串表示方法,在Django admin和管理界面中显示
return self.email # xh:使用邮箱作为对象的显示标识
return self.email
def get_full_url(self):
#xh:获取用户的完整URL包含域名,用于生成可在站外访问的完整用户链接
site = get_current_site().domain # xh:获取当前站点的域名
url = "https://{site}{path}".format(
site=site, # xh:域名
path=self.get_absolute_url() # xh:相对路径
)
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
#xh:模型的元数据配置,定义模型的各种行为和显示选项
ordering = ['-id'] # xh:默认按ID降序排列最新的在前
verbose_name = _('user') # xh:单数显示名称
verbose_name_plural = verbose_name # xh:复数显示名称(与单数相同)
get_latest_by = 'id' # xh:指定按id字段获取最新记录
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'

@ -9,104 +9,81 @@ from djangoblog.utils import *
from . import utils
# Create your tests here.
class AccountTest(TestCase):
#xh:用户账户功能测试类,测试用户注册、登录、密码重置等核心功能
def setUp(self):
#xh:测试前置设置方法,在每个测试方法执行前运行,用于创建测试数据
self.client = Client() # xh:Django测试客户端用于模拟HTTP请求
self.factory = RequestFactory() # xh:用于创建请求对象
# xh:创建测试用户
self.client = Client()
self.factory = RequestFactory()
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--=" # xh:测试用的新密码
self.new_test = "xxx123--="
def test_validate_account(self):
#xh:测试账户验证功能,验证超级用户创建、登录、文章管理等功能
site = get_current_site().domain
# xh:创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
# xh:测试用户登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True) # xh:断言登录成功
# xh:测试访问管理员页面
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200) # xh:断言可以访问admin
self.assertEqual(response.status_code, 200)
# xh:创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# xh:创建测试文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a' # xh:文章类型
article.status = 'p' # xh:发布状态
article.type = 'a'
article.status = 'p'
article.save()
# xh:测试访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200) # xh:断言可以访问文章管理页
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
#xh:测试用户注册流程,验证用户注册、邮箱验证、登录、权限管理等完整流程
# xh:验证注册前用户不存在
self.assertEquals(
0, len(BlogUser.objects.filter(email='user123@user.com')))
# xh:提交注册表单
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T', # xh:密码1
'password2': 'password123!q@wE#R$T', # xh:确认密码
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
# xh:验证用户创建成功
self.assertEquals(
1, len(BlogUser.objects.filter(email='user123@user.com')))
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
user = BlogUser.objects.filter(email='user123@user.com')[0]
# xh:生成邮箱验证签名(模拟验证流程)
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
# xh:访问验证结果页面
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# xh:测试用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
# xh:提升用户权限为超级用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache() # xh:清理侧边栏缓存
# xh:创建测试数据
delete_sidebar_cache()
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
@ -118,110 +95,90 @@ class AccountTest(TestCase):
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
# xh:测试文章管理页面访问
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# xh:测试用户登出
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200]) # xh:重定向或成功
self.assertIn(response.status_code, [301, 302, 200])
# xh:登出后访问管理页面应该被重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# xh:测试错误密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123' # xh:错误密码
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
# xh:错误密码登录后仍无法访问管理页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
#xh:测试邮箱验证码功能,验证验证码的生成、发送和验证流程
to_email = "admin@admin.com"
code = generate_code() # xh:生成验证码
# xh:保存验证码到缓存/数据库
code = generate_code()
utils.set_code(to_email, code)
# xh:发送验证邮件(测试环境可能不会实际发送)
utils.send_verify_email(to_email, code)
# xh:测试正确验证码验证
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None) # xh:断言验证成功(无错误)
self.assertEqual(err, None)
# xh:测试错误邮箱验证
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) # xh:断言返回错误信息
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
#xh:测试忘记密码验证码请求成功情况
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com") # xh:正确邮箱格式
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200) # xh:断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") # xh:断言返回成功消息
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
#xh:测试忘记密码验证码请求失败情况,验证空数据和错误邮箱格式的处理
# xh:测试空数据提交
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict() # xh:空数据
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# xh:测试错误邮箱格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com") # xh:无效邮箱格式
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
#xh:测试密码重置成功流程,验证完整的密码重置功能
code = generate_code()
utils.set_code(self.blog_user.email, code) # xh:设置验证码
# xh:构造密码重置数据
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test, # xh:新密码
new_password2=self.new_test, # xh:确认密码
email=self.blog_user.email, # xh:用户邮箱
code=code, # xh:正确验证码
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
# xh:提交密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302) # xh:断言重定向(成功)
self.assertEqual(resp.status_code, 302)
# xh:验证用户密码是否修改成功
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None) # xh:断言用户存在
self.assertEqual(blog_user.check_password(data["new_password1"]), True) # xh:断言密码修改成功
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
#xh:测试不存在的用户密码重置,验证系统对不存在用户的处理
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com", # xh:不存在的邮箱
email="123@123.com",
code="123456",
)
resp = self.client.post(
@ -229,22 +186,22 @@ class AccountTest(TestCase):
data=data
)
self.assertEqual(resp.status_code, 200) # xh:断言返回错误页面(非重定向)
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
#xh:测试验证码错误情况,验证错误验证码的处理
code = generate_code()
utils.set_code(self.blog_user.email, code) # xh:设置正确验证码
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", # xh:错误验证码
code="111111",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) # xh:断言验证失败(非重定向)
self.assertEqual(resp.status_code, 200)

@ -1,54 +1,28 @@
# xh:导入URL路由相关的模块
from django.urls import path # xh:用于简单路径匹配
from django.urls import re_path # xh:用于正则表达式路径匹配
from django.urls import path
from django.urls import re_path
# xh:导入当前应用的视图和表单
from . import views # xh:导入当前目录下的views模块
from .forms import LoginForm # xh:导入自定义登录表单
from . import views
from .forms import LoginForm
# xh:应用命名空间用于URL反向解析时区分不同应用的相同URL名称
app_name = "accounts"
# xh:URL模式配置列表定义URL路径与视图的映射关系
urlpatterns = [
# xh:用户登录路由 - 使用正则表达式匹配
re_path(r'^login/$', # xh:匹配以/login/结尾的URL
views.LoginView.as_view( # xh:使用类视图,转换为视图函数
success_url='/' # xh:登录成功后的重定向URL首页
),
name='login', # xh:URL名称用于反向解析
kwargs={'authentication_form': LoginForm} # xh:额外参数:指定登录表单
),
# xh:用户注册路由
re_path(r'^register/$', # xh:匹配以/register/结尾的URL
views.RegisterView.as_view(
success_url="/" # xh:注册成功后的重定向URL首页
),
name='register' # xh:URL名称register
),
# xh:用户登出路由
re_path(r'^logout/$', # xh:匹配以/logout/结尾的URL
views.LogoutView.as_view(), # xh:登出视图,无额外参数
name='logout' # xh:URL名称logout
),
# xh:账户操作结果页面路由 - 使用path更简洁的路径语法
path(r'account/result.html', # xh:匹配固定路径/account/result.html
views.account_result, # xh:使用函数视图
name='result' # xh:URL名称result
),
# xh:忘记密码页面路由(提交密码重置表单)
re_path(r'^forget_password/$', # xh:匹配以/forget_password/结尾的URL
views.ForgetPasswordView.as_view(), # xh:密码重置视图
name='forget_password' # xh:URL名称forget_password
),
# xh:忘记密码验证码请求路由(获取验证码)
re_path(r'^forget_password_code/$', # xh:匹配以/forget_password_code/结尾的URL
views.ForgetPasswordEmailCode.as_view(), # xh:验证码请求视图
name='forget_password_code' # xh:URL名称forget_password_code
),
]
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'),
]

@ -1,39 +1,26 @@
# xh:导入必要的模块
from django.contrib.auth import get_user_model # xh:获取项目中使用的用户模型
from django.contrib.auth.backends import ModelBackend # xh:Django认证后端基类
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
#xh:自定义认证后端,允许用户使用用户名或邮箱地址进行登录,继承自Django的ModelBackend扩展了认证方式
"""
允许使用用户名或邮箱登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
#xh:用户认证方法,根据用户名或邮箱验证用户身份
# xh:判断输入的是邮箱还是用户名
if '@' in username:
# xh:如果包含@符号,按邮箱处理
kwargs = {'email': username}
else:
# xh:否则按用户名处理
kwargs = {'username': username}
try:
# xh:根据用户名或邮箱查找用户
user = get_user_model().objects.get(**kwargs)
# xh:验证密码是否正确(自动处理哈希比较)
if user.check_password(password):
return user # xh:认证成功,返回用户对象
return user
except get_user_model().DoesNotExist:
# xh:用户不存在返回None表示认证失败
return None
def get_user(self, user_id):
#xh:根据用户ID获取用户对象,用于会话认证,在请求处理期间获取用户信息
def get_user(self, username):
try:
# xh:根据主键ID查找用户
return get_user_model().objects.get(pk=user_id)
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
# xh:用户不存在返回None
return None

@ -26,222 +26,179 @@ from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
# xh:配置日志记录器
logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
#xh:用户注册视图,处理新用户的注册流程,包括表单验证、用户创建和邮箱验证
form_class = RegisterForm # xh:使用的表单类
template_name = 'account/registration_form.html' # xh:模板路径
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect) # xh:CSRF保护防止跨站请求伪造
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
#xh:表单验证通过后的处理逻辑
if form.is_valid():
# xh:保存用户但不提交到数据库commit=False
user = form.save(False)
user.is_active = False # xh:设置用户为未激活状态,需要邮箱验证
user.source = 'Register' # xh:记录用户来源
user.save(True) # xh:保存到数据库
# xh:获取当前站点信息
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
# xh:生成邮箱验证签名使用双重SHA256加密
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# xh:调试模式下使用本地地址
if settings.DEBUG:
site = '127.0.0.1:8000'
# xh:构建验证URL
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# xh:邮件内容模板
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# xh:发送验证邮件
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
send_email(
emailto=[user.email],
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
# xh:重定向到结果页面
url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
# xh:表单无效,重新渲染表单页
return self.render_to_response({'form': form})
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
#xh:用户登出视图,处理用户登出逻辑,清理会话和缓存
url = '/login/' # xh:登出后重定向的URL
url = '/login/'
@method_decorator(never_cache) # xh:禁止缓存,确保每次都是最新状态
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
#xh:GET请求处理执行登出操作
logout(request) # xh:Django内置登出函数清理会话
delete_sidebar_cache() # xh:清理侧边栏缓存
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
#xh:用户登录视图,处理用户认证和登录逻辑
form_class = LoginForm # xh:登录表单
template_name = 'account/login.html' # xh:登录模板
success_url = '/' # xh:登录成功默认重定向URL
redirect_field_name = REDIRECT_FIELD_NAME # xh:重定向字段名(默认:'next'
login_ttl = 2626560 # xh:会话过期时间:一个月(秒)
# xh:方法装饰器:增强安全性
@method_decorator(sensitive_post_parameters('password')) # xh:敏感参数保护
@method_decorator(csrf_protect) #xh:CSRF保护
@method_decorator(never_cache) # xh:禁止缓存
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
#xh:添加上下文数据重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/' # xh:默认重定向到首页
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
#xh:表单验证通过后的登录处理
# xh:使用Django内置的认证表单进行验证
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
# xh:清理侧边栏缓存
delete_sidebar_cache()
logger.info(self.redirect_field_name)
# xh:执行用户登录(创建会话)
auth.login(self.request, form.get_user())
# xh:处理"记住我"功能
if self.request.POST.get("remember"):
# xh:设置会话过期时间为一个月
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
# xh:认证失败,重新显示表单
return self.render_to_response({'form': form})
return self.render_to_response({
'form': form
})
def get_success_url(self):
#xh:获取登录成功后的重定向URL,进行安全验证,防止开放重定向攻击
redirect_to = self.request.POST.get(self.redirect_field_name)
# xh:验证重定向URL的安全性
if not url_has_allowed_host_and_scheme(
url=redirect_to,
allowed_hosts=[self.request.get_host()]):
redirect_to = self.success_url # xh:不安全则使用默认URL
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url
return redirect_to
def account_result(request):
#xh:账户操作结果页面,显示注册结果或处理邮箱验证
type = request.GET.get('type') # xh:操作类型register 或 validation
id = request.GET.get('id') # xh:用户ID
type = request.GET.get('type')
id = request.GET.get('id')
# xh:获取用户对象不存在则返回404
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
# xh:如果用户已激活,直接重定向到首页
if user.is_active:
return HttpResponseRedirect('/')
# xh:处理注册结果或邮箱验证
if type and type in ['register', 'validation']:
if type == 'register':
# xh:注册成功页面
content = '恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。'
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# xh:邮箱验证处理
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
# xh:验证签名,防止篡改
if sign != c_sign:
return HttpResponseForbidden() # 签名不匹配,拒绝访问
# xh:激活用户账户
return HttpResponseForbidden()
user.is_active = True
user.save()
content = '恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。'
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
# xh:渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
# xh:无效类型,重定向到首页
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
#xh:忘记密码视图,处理密码重置请求
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
#xh:表单验证通过后的密码重置处理
if form.is_valid():
# xh:根据邮箱查找用户
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# xh:对新密码进行哈希处理并保存
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
# xh:重定向到登录页面
return HttpResponseRedirect('/login/')
else:
# xh:表单无效,重新显示
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
#xh:忘记密码验证码发送视图,处理密码重置验证码的发送
def post(self, request: HttpRequest):
#xh:处理POST请求发送密码重置验证码
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱") # xh:表单验证失败
to_email = form.cleaned_data["email"] # xh:获取邮箱地址
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
# xh:生成并发送验证码
code = generate_code() # xh:生成6位随机验证码
utils.send_verify_email(to_email, code) # xh:发送验证邮件
utils.set_code(to_email, code) # xh:保存验证码到缓存/数据库
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok") # xh:返回成功响应
return HttpResponse("ok")

@ -1,91 +1,86 @@
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model # aq: 导入获取用户模型的工具(适配自定义用户模型)
from django.urls import reverse # aq: 导入URL反向解析函数
from django.utils.html import format_html # aq: 导入HTML格式化工具用于生成Admin中的链接
from django.utils.translation import gettext_lazy as _ # aq: 导入国际化翻译函数
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article # aq: 导入文章模型需确保Category、Tag等模型也已在models中定义
from .models import Article
class ArticleForm(forms.ModelForm): # aq: 文章的Admin表单类可自定义字段渲染逻辑
# body = forms.CharField(widget=AdminPagedownWidget()) # aq: 注释掉的Markdown编辑器组件配置
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article # aq: 关联的模型
fields = '__all__' # aq: 显示所有字段
model = Article
fields = '__all__'
# aq: 自定义Admin批量操作——将选中文章设为“已发布”状态
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p')
# aq: 自定义Admin批量操作——将选中文章设为“草稿”状态
def draft_article(modeladmin, request, queryset):
queryset.update(status='d')
# aq: 自定义Admin批量操作——关闭选中文章的评论
def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c')
# aq: 自定义Admin批量操作——开放选中文章的评论
def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o')
# aq: 批量操作的显示名称(支持国际化)
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')
class ArticlelAdmin(admin.ModelAdmin): # aq: 文章的Admin管理类配置后台展示/操作逻辑)
list_per_page = 20 # aq: 每页显示20条数据
search_fields = ('body', 'title') # aq: 可搜索字段(文章正文、标题)
form = ArticleForm # aq: 使用自定义的ArticleForm表单
list_display = ( # aq: 列表页显示的字段
class ArticlelAdmin(admin.ModelAdmin):
list_per_page = 20
search_fields = ('body', 'title')
form = ArticleForm
list_display = (
'id',
'title',
'author',
'link_to_category', # aq: 自定义字段——分类跳转链接
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
list_display_links = ('id', 'title') # aq: 列表页可点击跳转的字段(链接到编辑页)
list_filter = ('status', 'type', 'category') # aq: 侧边筛选器字段
filter_horizontal = ('tags',) # aq: 多对多字段的水平选择器(标签字段)
exclude = ('creation_time', 'last_modify_time') # aq: 编辑页隐藏的字段(自动生成,无需手动输入)
view_on_site = True # aq: 显示“在站点上查看”按钮
actions = [ # aq: 启用的批量操作
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj): # aq: 自定义列表字段——分类名称带后台编辑链接
def link_to_category(self, obj):
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name)) # aq: 修复原代码语法错误,正确拼接链接
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
link_to_category.short_description = _('category') # aq: 自定义字段的显示名称
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs): # aq: 自定义表单——作者字段仅显示超级管理员
def get_form(self, request, obj=None, **kwargs):
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change): # aq: 保存模型时的钩子(此处调用父类方法,可扩展自定义逻辑)
def save_model(self, request, obj, form, change):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None): # aq: 自定义“在站点上查看”的链接(跳转到文章前台详情页)
def get_view_on_site_url(self, obj=None):
if obj:
url = obj.get_full_url()
return url
@ -95,24 +90,23 @@ class ArticlelAdmin(admin.ModelAdmin): # aq: 文章的Admin管理类配置
return site
class TagAdmin(admin.ModelAdmin): # aq: 标签的Admin管理类
exclude = ('slug', 'last_mod_time', 'creation_time') # aq: 隐藏自动生成的字段slug自动生成
class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin): # aq: 分类的Admin管理类
list_display = ('name', 'parent_category', 'index') # aq: 列表页显示字段(名称、父分类、排序权重)
exclude = ('slug', 'last_mod_time', 'creation_time') # aq: 隐藏自动生成的字段
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_category', 'index')
exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin): # aq: 友情链接的Admin管理类
exclude = ('last_mod_time', 'creation_time') # aq: 隐藏自动生成的时间字段
class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin): # aq: 侧边栏的Admin管理类
list_display = ('name', 'content', 'is_enable', 'sequence') # aq: 列表页显示字段(标题、内容、是否启用、排序)
exclude = ('last_mod_time', 'creation_time') # aq: 隐藏自动生成的时间字段
class SideBarAdmin(admin.ModelAdmin):
list_display = ('name', 'content', 'is_enable', 'sequence')
exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin): # aq: 博客配置的Admin管理类使用默认配置
class BlogSettingsAdmin(admin.ModelAdmin):
pass

@ -1,5 +1,5 @@
from django.apps import AppConfig # aq: 导入Django的应用配置基类
from django.apps import AppConfig
class BlogConfig(AppConfig): # aq: 博客应用的配置类(用于定义应用元信息)
name = 'blog' # aq: 应用名称必须与应用目录名一致Django通过此识别应用
class BlogConfig(AppConfig):
name = 'blog'

@ -1,48 +1,43 @@
import logging
from django.utils import timezone # aq: 导入时区时间工具
from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting # aq: 导入缓存工具和获取博客配置的函数
from .models import Category, Article # aq: 导入分类、文章模型
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__) # aq: 初始化当前模块的日志对象
logger = logging.getLogger(__name__)
def seo_processor(requests): # aq: Django上下文处理器向模板注入SEO/站点配置信息
key = 'seo_processor' # aq: 缓存键名
value = cache.get(key) # aq: 尝试从缓存中获取数据
if value: # aq: 缓存存在则直接返回
def seo_processor(requests):
key = 'seo_processor'
value = cache.get(key)
if value:
return value
else:
logger.info('set processor cache.') # aq: 记录缓存设置日志
setting = get_blog_setting() # aq: 获取博客的全局配置
# aq: 构造要注入模板的上下文数据
logger.info('set processor cache.')
setting = get_blog_setting()
value = {
'SITE_NAME': setting.site_name, # aq: 站点名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # aq: 是否显示Google广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # aq: Google广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # aq: 站点SEO描述
'SITE_DESCRIPTION': setting.site_description, # aq: 站点描述
'SITE_KEYWORDS': setting.site_keywords, # aq: 站点关键词
# aq: 站点基础URL协议+域名)
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # aq: 文章摘要长度
'nav_category_list': Category.objects.all(), # aq: 导航分类列表
# aq: 导航页面列表(类型为页面、状态为已发布)
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'nav_pages': Article.objects.filter(
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment, # aq: 是否开放全站评论
'BEIAN_CODE': setting.beian_code, # aq: 备案号
'ANALYTICS_CODE': setting.analytics_code, # aq: 网站统计代码
"BEIAN_CODE_GONGAN": setting.gongan_beiancode, # aq: 公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code, # aq: 是否显示公安备案号
"CURRENT_YEAR": timezone.now().year, # aq: 当前年份(用于版权信息)
"GLOBAL_HEADER": setting.global_header, # aq: 公共头部HTML
"GLOBAL_FOOTER": setting.global_footer, # aq: 公共尾部HTML
"COMMENT_NEED_REVIEW": setting.comment_need_review, # aq: 评论是否需要审核
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,
'ANALYTICS_CODE': setting.analytics_code,
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
"SHOW_GONGAN_CODE": setting.show_gongan_code,
"CURRENT_YEAR": timezone.now().year,
"GLOBAL_HEADER": setting.global_header,
"GLOBAL_FOOTER": setting.global_footer,
"COMMENT_NEED_REVIEW": setting.comment_need_review,
}
cache.set(key, value, 60 * 60 * 10) # aq: 缓存数据10小时
return value
cache.set(key, value, 60 * 60 * 10)
return value

@ -1,17 +1,15 @@
import time
import elasticsearch.client
from django.conf import settings # aq: 导入Django项目配置
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean # aq: 导入Elasticsearch DSL的文档/字段类型
from elasticsearch_dsl.connections import connections # aq: 导入ES连接工具
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
from blog.models import Article # aq: 导入博客文章模型
from blog.models import Article
# aq: 判断是否启用Elasticsearch检查配置中是否有ELASTICSEARCH_DSL
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
# aq: 创建ES连接使用配置中的地址
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
@ -19,11 +17,10 @@ if ELASTICSEARCH_ENABLED:
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
c = IngestClient(es) # aq: 初始化ES的Ingest客户端用于管道操作
c = IngestClient(es)
try:
c.get_pipeline('geoip') # aq: 检查是否存在名为geoip的管道
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
# aq: 若不存在创建geoip管道用于解析IP的地理位置信息
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -36,75 +33,73 @@ if ELASTICSEARCH_ENABLED:
}''')
class GeoIp(InnerDoc): # aq: 定义地理位置信息的内部文档(嵌套结构)
continent_name = Keyword() # aq: 大陆名称Keyword类型精确匹配
country_iso_code = Keyword() # aq: 国家ISO编码
country_name = Keyword() # aq: 国家名称
location = GeoPoint() # aq: 经纬度坐标
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
class UserAgentBrowser(InnerDoc): # aq: 定义用户代理中浏览器信息的内部文档
Family = Keyword() # aq: 浏览器家族如Chrome
Version = Keyword() # aq: 浏览器版本
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
class UserAgentOS(UserAgentBrowser): # aq: 定义用户代理中操作系统信息的内部文档(继承浏览器结构)
class UserAgentOS(UserAgentBrowser):
pass
class UserAgentDevice(InnerDoc): # aq: 定义用户代理中设备信息的内部文档
Family = Keyword() # aq: 设备家族如iPhone
Brand = Keyword() # aq: 设备品牌
Model = Keyword() # aq: 设备型号
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgent(InnerDoc): # aq: 定义用户代理的内部文档(包含浏览器、系统、设备)
browser = Object(UserAgentBrowser, required=False) # aq: 浏览器信息Object类型嵌套UserAgentBrowser
os = Object(UserAgentOS, required=False) # aq: 操作系统信息
device = Object(UserAgentDevice, required=False) # aq: 设备信息
string = Text() # aq: 原始用户代理字符串
is_bot = Boolean() # aq: 是否为爬虫机器人
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
class ElapsedTimeDocument(Document): # aq: 定义页面耗时统计的ES文档对应performance索引
url = Keyword() # aq: 请求URL
time_taken = Long() # aq: 页面加载耗时(毫秒)
log_datetime = Date() # aq: 日志记录时间
ip = Keyword() # aq: 客户端IP
geoip = Object(GeoIp, required=False) # aq: 地理位置信息嵌套GeoIp
useragent = Object(UserAgent, required=False) # aq: 用户代理信息嵌套UserAgent
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
class Index:
name = 'performance' # aq: 索引名称
name = 'performance'
settings = {
"number_of_shards": 1, # aq: 分片数
"number_of_replicas": 0 # aq: 副本数开发环境通常设为0
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'ElapsedTime' # aq: 文档类型ES 7+后可省略)
doc_type = 'ElapsedTime'
class ElaspedTimeDocumentManager: # aq: 页面耗时文档的管理类封装ES操作
class ElaspedTimeDocumentManager:
@staticmethod
def build_index(): # aq: 检查并创建performance索引
def build_index():
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
if not res:
ElapsedTimeDocument.init() # aq: 初始化索引根据ElapsedTimeDocument的定义
ElapsedTimeDocument.init()
@staticmethod
def delete_index(): # aq: 删除performance索引
def delete_index():
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404]) # aq: 忽略不存在/错误
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip): # aq: 创建一条耗时记录
ElaspedTimeDocumentManager.build_index() # aq: 确保索引存在
# aq: 构造UserAgent对象填充浏览器、系统、设备信息
def create(url, time_taken, log_datetime, useragent, ip):
ElaspedTimeDocumentManager.build_index()
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
@ -121,7 +116,6 @@ class ElaspedTimeDocumentManager: # aq: 页面耗时文档的管理类(封装
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
# aq: 构造ElapsedTimeDocument实例用时间戳作为ID避免重复
doc = ElapsedTimeDocument(
meta={
'id': int(
@ -133,65 +127,61 @@ class ElaspedTimeDocumentManager: # aq: 页面耗时文档的管理类(封装
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
doc.save(pipeline="geoip") # aq: 保存时使用geoip管道解析IP
doc.save(pipeline="geoip")
class ArticleDocument(Document): # aq: 定义文章的ES文档对应blog索引
# aq: 文章正文/标题使用ik分词器max_word细分词smart粗分词
class ArticleDocument(Document):
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# aq: 作者信息(嵌套结构)
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# aq: 分类信息(嵌套结构)
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# aq: 标签信息(嵌套结构)
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
pub_time = Date() # aq: 发布时间
status = Text() # aq: 文章状态
comment_status = Text() # aq: 评论状态
type = Text() # aq: 内容类型
views = Integer() # aq: 阅读量
article_order = Integer() # aq: 排序权重
pub_time = Date()
status = Text()
comment_status = Text()
type = Text()
views = Integer()
article_order = Integer()
class Index:
name = 'blog' # aq: 索引名称
name = 'blog'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article' # aq: 文档类型
doc_type = 'Article'
class ArticleDocumentManager(): # aq: 文章文档的管理类
class ArticleDocumentManager():
def __init__(self): # aq: 初始化时创建索引
def __init__(self):
self.create_index()
def create_index(self): # aq: 创建blog索引
def create_index(self):
ArticleDocument.init()
def delete_index(self): # aq: 删除blog索引
def delete_index(self):
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles): # aq: 将Django的Article模型转换为ArticleDocument
def convert_to_doc(self, articles):
return [
ArticleDocument(
meta={
'id': article.id}, # aq: 用文章ID作为ES文档ID
'id': article.id},
body=article.body,
title=article.title,
author={
@ -200,7 +190,7 @@ class ArticleDocumentManager(): # aq: 文章文档的管理类
category={
'name': article.category.name,
'id': article.category.id},
tags=[ # aq: 转换多对多的标签为列表
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
@ -211,13 +201,13 @@ class ArticleDocumentManager(): # aq: 文章文档的管理类
views=article.views,
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None): # aq: 重建文章索引(默认同步所有文章)
def rebuild(self, articles=None):
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
for doc in docs:
doc.save()
def update_docs(self, docs): # aq: 批量更新文章文档
def update_docs(self, docs):
for doc in docs:
doc.save()
doc.save()

@ -1,26 +1,19 @@
import logging
from django import forms # aq: 导入Django表单基类
from haystack.forms import SearchForm # aq: 导入Haystack搜索框架的基础搜索表单
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__) # aq: 初始化当前模块的日志对象
logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm): # aq: 自定义博客搜索表单继承Haystack的SearchForm
# aq: 定义搜索关键词字段required=True表示该字段不能为空
class BlogSearchForm(SearchForm):
querydata = forms.CharField(required=True)
def search(self): # aq: 重写搜索方法,自定义搜索逻辑
# aq: 调用父类的search方法获取初始搜索结果
def search(self):
datas = super(BlogSearchForm, self).search()
# aq: 如果表单验证不通过,返回“无结果”的默认处理
if not self.is_valid():
return self.no_query_found()
# aq: 如果搜索关键词存在,记录关键词到日志
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
# aq: 返回最终的搜索结果
return datas
return datas

@ -1,52 +1,42 @@
import logging
import time # aq: 导入时间模块,用于计算页面渲染耗时
import time
from ipware import get_client_ip # aq: 导入获取客户端IP的工具
from user_agents import parse # aq: 导入解析用户代理UA的工具
from ipware import get_client_ip
from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager # aq: 导入ES开关和耗时记录的Document管理器
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__) # aq: 初始化当前模块的日志对象
logger = logging.getLogger(__name__)
class OnlineMiddleware(object): # aq: 自定义Django中间件用于统计页面加载耗时、记录访问信息
def __init__(self, get_response=None): # aq: 中间件初始化方法
self.get_response = get_response # aq: 保存Django的响应处理函数
class OnlineMiddleware(object):
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request): # aq: 中间件核心方法,处理请求/响应流程
''' page render time ''' # aq: 功能说明:统计页面渲染耗时
start_time = time.time() # aq: 记录请求处理的开始时间
response = self.get_response(request) # aq: 调用后续中间件/视图,获取响应
# aq: 从请求头中获取用户代理UA字符串
def __call__(self, request):
''' page render time '''
start_time = time.time()
response = self.get_response(request)
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
ip, _ = get_client_ip(request) # aq: 获取客户端IP地址
user_agent = parse(http_user_agent) # aq: 解析UA字符串得到设备/浏览器信息
# aq: 非流式响应如普通HTML响应才处理
ip, _ = get_client_ip(request)
user_agent = parse(http_user_agent)
if not response.streaming:
try:
cast_time = time.time() - start_time # aq: 计算请求处理总耗时(秒)
# aq: 如果启用了Elasticsearch记录耗时信息
cast_time = time.time() - start_time
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2) # aq: 转换为毫秒并保留2位小数
url = request.path # aq: 获取当前请求的URL路径
time_taken = round((cast_time) * 1000, 2)
url = request.path
from django.utils import timezone
# aq: 向ES中创建一条耗时记录
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# aq: 将页面中的<!!LOAD_TIMES!!>占位符替换为实际耗时取前5位
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e: # aq: 捕获异常并记录日志
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
return response # aq: 返回处理后的响应
return response

@ -4,133 +4,134 @@ from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields # aq: 导入Markdown编辑器专用字段类型
import mdeditor.fields
class Migration(migrations.Migration):
initial = True # aq: 标记为初始迁移,负责创建博客核心数据表结构
initial = True
dependencies = [ # aq: 迁移依赖——关联Django用户模型支持自定义用户模型
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [ # aq: 迁移核心操作:创建所有博客相关数据表
migrations.CreateModel( # aq: 1. 创建网站配置表BlogSettings
operations = [
migrations.CreateModel(
name='BlogSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # aq: 自增主键BigInt类型
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')), # aq: 网站名称
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')), # aq: 网站描述
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')), # aq: SEO优化描述
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')), # aq: 网站搜索关键字
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')), # aq: 文章摘要默认长度
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')), # aq: 侧边栏显示文章数量
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')), # aq: 侧边栏显示评论数量
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')), # aq: 文章页默认显示评论数
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')), # aq: 是否启用谷歌广告
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')), # aq: 谷歌广告代码
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')), # aq: 是否开放全站评论
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')), # aq: 网站备案号
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')), # aq: 网站统计(如百度统计)代码
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')), # aq: 是否显示公安备案号
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')), # aq: 公安备案号
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置', # aq: 后台显示名称(单数/复数)
'verbose_name_plural': '网站配置',
},
),
migrations.CreateModel( # aq: 2. 创建友情链接表Links
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # aq: 自增主键
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')), # aq: 友情链接名称(唯一)
('link', models.URLField(verbose_name='链接地址')), # aq: 链接URL地址
('sequence', models.IntegerField(unique=True, verbose_name='排序')), # aq: 排序序号(唯一,决定显示顺序)
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), # aq: 是否启用该链接
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')), # aq: 链接展示位置
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # aq: 创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # aq: 最后修改时间
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
('link', models.URLField(verbose_name='链接地址')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'], # aq: 按排序序号升序排列
'ordering': ['sequence'],
},
),
migrations.CreateModel( # aq: 3. 创建侧边栏表SideBar
migrations.CreateModel(
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # aq: 自增主键
('name', models.CharField(max_length=100, verbose_name='标题')), # aq: 侧边栏标题
('content', models.TextField(verbose_name='内容')), # aq: 侧边栏内容支持HTML
('sequence', models.IntegerField(unique=True, verbose_name='排序')), # aq: 排序序号(唯一)
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')), # aq: 是否启用该侧边栏
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # aq: 创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # aq: 最后修改时间
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='标题')),
('content', models.TextField(verbose_name='内容')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏',
'ordering': ['sequence'], # aq: 按排序序号升序排列
'ordering': ['sequence'],
},
),
migrations.CreateModel( # aq: 4. 创建标签表Tag
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)), # aq: 自增主键Int类型
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # aq: 创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # aq: 最后修改时间
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')), # aq: 标签名称(唯一)
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), # aq: URL友好的slug名称自动生成
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'], # aq: 按标签名称升序排列
'ordering': ['name'],
},
),
migrations.CreateModel( # aq: 5. 创建分类表Category
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)), # aq: 自增主键
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # aq: 创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # aq: 最后修改时间
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')), # aq: 分类名称(唯一)
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), # aq: URL友好的slug名称
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')), # aq: 排序权重(数值越大越靠前)
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')), # aq: 父分类(自关联,支持多级分类)
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['-index'], # aq: 按权重降序排列
'ordering': ['-index'],
},
),
migrations.CreateModel( # aq: 6. 创建文章表Article——核心数据表
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)), # aq: 自增主键
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # aq: 创建时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # aq: 最后修改时间
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')), # aq: 文章标题(唯一)
('body', mdeditor.fields.MDTextField(verbose_name='正文')), # aq: 文章正文Markdown格式
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')), # aq: 发布时间
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')), # aq: 文章状态(草稿/发表)
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')), # aq: 评论状态(打开/关闭)
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')), # aq: 内容类型(文章/独立页面)
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')), # aq: 阅读量(非负整数)
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')), # aq: 文章排序权重
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')), # aq: 是否显示文章目录
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), # aq: 关联作者(用户模型)
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')), # aq: 关联分类(多对一)
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')), # aq: 关联标签(多对多)
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'], # aq: 先按排序权重降序,再按发布时间降序
'get_latest_by': 'id', # aq: 按ID字段获取最新记录
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
},
),
]
]

@ -1,21 +1,23 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models # aq: 导入Django数据库迁移核心模块
from django.db import migrations, models
class Migration(migrations.Migration): # aq: 数据库迁移类,用于修改数据库表结构
dependencies = [ # aq: 迁移依赖关系需先执行0001_initial初始迁移
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [ # aq: 迁移具体操作列表
migrations.AddField( # aq: 给BlogSettings模型新增字段
model_name='blogsettings', # aq: 目标模型(博客配置模型)
name='global_footer', # aq: 新增字段名公共尾部HTML
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), # aq: 字段类型为文本,允许为空,默认值空字符串,后台显示名称“公共尾部”
operations = [
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
migrations.AddField( # aq: 给BlogSettings模型新增字段
model_name='blogsettings', # aq: 目标模型(博客配置模型)
name='global_header', # aq: 新增字段名公共头部HTML
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), # aq: 字段类型为文本,允许为空,默认值空字符串,后台显示名称“公共头部”
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]
]

@ -1,17 +1,17 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models # aq: 导入Django迁移相关模块
from django.db import migrations, models
class Migration(migrations.Migration): # aq: 数据库迁移类,用于更新数据库结构
dependencies = [ # aq: 迁移依赖需先执行0002号迁移
class Migration(migrations.Migration):
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
operations = [ # aq: 迁移操作列表
migrations.AddField( # aq: 新增字段操作
model_name='blogsettings', # aq: 目标模型(博客配置模型)
name='comment_need_review', # aq: 新增字段名(评论是否需要审核)
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), # aq: 字段类型布尔值默认False后台显示名称
operations = [
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]
]

@ -1,26 +1,27 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations # aq: 导入Django数据库迁移核心模块
from django.db import migrations
class Migration(migrations.Migration): # aq: 数据库迁移类,用于修改字段名称
dependencies = [ # aq: 迁移依赖——需先执行0003号迁移新增评论审核字段
class Migration(migrations.Migration):
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
operations = [ # aq: 迁移操作列表——批量重命名BlogSettings模型的字段统一为下划线命名规范
operations = [
migrations.RenameField(
model_name='blogsettings', # aq: 目标模型(博客配置模型)
old_name='analyticscode', # aq: 原字段名(驼峰命名)
new_name='analytics_code', # aq: 新字段名下划线命名符合Python规范
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode', # aq: 原字段名(驼峰命名)
new_name='beian_code', # aq: 新字段名(下划线命名)
old_name='beiancode',
new_name='beian_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='sitename', # aq: 原字段名(驼峰命名)
new_name='site_name', # aq: 新字段名(下划线命名)
old_name='sitename',
new_name='site_name',
),
]
]

@ -4,17 +4,17 @@ from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields # aq: 导入Markdown编辑器字段类型
import mdeditor.fields
class Migration(migrations.Migration):
dependencies = [ # aq: 迁移依赖——关联用户模型+依赖0004号字段重命名迁移
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [ # aq: 迁移核心操作——统一模型配置字段名、verbose_name国际化
# aq: 1. 修改模型选项将verbose_name改为英文适配国际化
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
@ -35,8 +35,6 @@ class Migration(migrations.Migration):
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# aq: 2. 删除旧的时间字段(驼峰命名)
migrations.RemoveField(
model_name='article',
name='created_time',
@ -69,17 +67,15 @@ class Migration(migrations.Migration):
model_name='tag',
name='last_mod_time',
),
# aq: 3. 新增统一命名的时间字段(下划线命名+英文verbose_name
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # aq: 创建时间
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), # aq: 最后修改时间
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='category',
@ -111,8 +107,6 @@ class Migration(migrations.Migration):
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# aq: 4. 修改所有字段的verbose_name为英文适配国际化翻译
migrations.AlterField(
model_name='article',
name='article_order',
@ -303,4 +297,4 @@ class Migration(migrations.Migration):
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]
]

@ -1,15 +1,17 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations # aq: 导入Django数据库迁移核心模块
from django.db import migrations
class Migration(migrations.Migration): # aq: 数据库迁移类,用于修改模型元信息
dependencies = [ # aq: 迁移依赖——需先执行0005号迁移统一其他模型国际化配置
class Migration(migrations.Migration):
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
operations = [ # aq: 迁移核心操作——修改BlogSettings模型的verbose_name适配国际化
operations = [
migrations.AlterModelOptions(
name='blogsettings', # aq: 目标模型(博客配置模型)
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, # aq: 中英文统一为“网站配置”的英文表述,保持全局一致性
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]
]

@ -1,126 +1,123 @@
import logging
import re
from abc import abstractmethod # aq: 导入抽象方法装饰器,用于定义抽象基类
from abc import abstractmethod
from django.conf import settings # aq: 导入Django项目的配置对象
from django.core.exceptions import ValidationError # aq: 导入Django的验证错误类
from django.db import models # aq: 导入Django的模型基类用于定义数据库表
from django.urls import reverse # aq: 导入URL反向解析函数生成URL
from django.utils.timezone import now # aq: 导入当前时间函数(带时区)
from django.utils.translation import gettext_lazy as _ # aq: 导入国际化翻译函数
from mdeditor.fields import MDTextField # aq: 导入Markdown编辑器字段
from slugify import slugify # aq: 导入slug生成工具将标题转为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 _
from mdeditor.fields import MDTextField
from slugify import slugify
from djangoblog.utils import cache_decorator, cache # aq: 导入自定义缓存装饰器和缓存工具
from djangoblog.utils import get_current_site # aq: 导入获取当前站点域名的工具
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__) # aq: 初始化日志对象,用于记录当前模块的日志
logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices): # aq: 定义链接的展示位置枚举类
I = ('i', _('index')) # aq: 首页展示
L = ('l', _('list')) # aq: 列表页展示
P = ('p', _('post')) # aq: 文章详情页展示
A = ('a', _('all')) # aq: 所有页面展示
S = ('s', _('slide')) # aq: 轮播区域展示
class LinkShowType(models.TextChoices):
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
class BaseModel(models.Model): # aq: 抽象基类模型,封装公共字段和方法
id = models.AutoField(primary_key=True) # aq: 自增主键ID
creation_time = models.DateTimeField(_('creation time'), default=now) # aq: 记录创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) # aq: 记录最后修改时间
class BaseModel(models.Model):
id = models.AutoField(primary_key=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now)
def save(self, *args, **kwargs): # aq: 重写保存方法,扩展功能
# aq: 判断是否是更新文章阅读量的操作
def save(self, *args, **kwargs):
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# aq: 仅更新阅读量(避免触发其他逻辑)
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# aq: 如果模型有slug字段自动生成slug从title/name字段转换
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs) # aq: 调用父类保存方法
super().save(*args, **kwargs)
def get_full_url(self): # aq: 生成模型对象的完整URL含域名
site = get_current_site().domain # aq: 获取当前站点域名
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
abstract = True # aq: 标记为抽象类,不会生成数据库表
abstract = True
@abstractmethod # aq: 定义抽象方法,子类必须实现
@abstractmethod
def get_absolute_url(self):
pass
class Article(BaseModel): # aq: 文章模型继承BaseModel
class Article(BaseModel):
"""文章"""
STATUS_CHOICES = ( # aq: 文章状态选项(草稿/已发布)
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
COMMENT_STATUS = ( # aq: 评论状态选项(开放/关闭)
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
TYPE = ( # aq: 内容类型选项(文章/页面)
TYPE = (
('a', _('Article')),
('p', _('Page')),
)
title = models.CharField(_('title'), max_length=200, unique=True) # aq: 文章标题(唯一)
body = MDTextField(_('body')) # aq: 文章正文Markdown格式
pub_time = models.DateTimeField( # aq: 发布时间
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
status = models.CharField( # aq: 文章状态
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
comment_status = models.CharField( # aq: 评论状态
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # aq: 内容类型
views = models.PositiveIntegerField(_('views'), default=0) # aq: 阅读量(非负整数)
author = models.ForeignKey( # aq: 文章作者(关联用户模型)
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
article_order = models.IntegerField( # aq: 文章排序权重
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # aq: 是否显示目录
category = models.ForeignKey( # aq: 文章分类关联Category模型
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # aq: 文章标签多对多关联Tag
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
def body_to_string(self): # aq: 返回正文内容的字符串形式
def body_to_string(self):
return self.body
def __str__(self): # aq: 模型实例的字符串表示(返回标题)
def __str__(self):
return self.title
class Meta:
ordering = ['-article_order', '-pub_time'] # aq: 默认排序先按order降序再按发布时间降序
verbose_name = _('article') # aq: 模型的可读名称
verbose_name_plural = verbose_name # aq: 模型的复数可读名称
get_latest_by = 'id' # aq: 获取最新记录的字段
ordering = ['-article_order', '-pub_time']
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def get_absolute_url(self): # aq: 实现抽象方法生成文章详情页URL
def get_absolute_url(self):
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -128,82 +125,85 @@ class Article(BaseModel): # aq: 文章模型继承BaseModel
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10) # aq: 缓存10小时
def get_category_tree(self): # aq: 获取文章分类的层级结构(含父分类)
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs): # aq: 重写保存方法(这里调用父类方法)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
def viewed(self): # aq: 增加阅读量仅更新views字段
def viewed(self):
self.views += 1
self.save(update_fields=['views'])
def comment_list(self): # aq: 获取文章的评论列表(带缓存)
def comment_list(self):
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
comments = self.comment_set.filter(is_enable=True).order_by('-id') # aq: 筛选启用的评论按ID降序
cache.set(cache_key, comments, 60 * 100) # aq: 缓存100分钟
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self): # aq: 生成文章在后台管理的编辑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,))
@cache_decorator(expiration=60 * 100) # aq: 缓存100分钟
def next_article(self): # aq: 获取下一篇文章ID更大、已发布
@cache_decorator(expiration=60 * 100)
def next_article(self):
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100) # aq: 缓存100分钟
def prev_article(self): # aq: 获取上一篇文章ID更小、已发布
@cache_decorator(expiration=60 * 100)
def prev_article(self):
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self): # aq: 从正文提取第一张图片的URL
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) # aq: 匹配Markdown图片语法
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
class Category(BaseModel): # aq: 分类模型继承BaseModel
class Category(BaseModel):
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True) # aq: 分类名称(唯一)
parent_category = models.ForeignKey( # aq: 父分类(自关联)
name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # aq: 分类的slugURL友好名称
index = models.IntegerField(default=0, verbose_name=_('index')) # aq: 分类排序权重
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
ordering = ['-index'] # aq: 默认按index降序排序
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self): # aq: 实现抽象方法生成分类详情页URL
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self): # aq: 模型实例的字符串表示(返回分类名称)
def __str__(self):
return self.name
@cache_decorator(60 * 60 * 10) # aq: 缓存10小时
def get_category_tree(self): # aq: 递归获取分类的父级层级结构
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
@ -218,8 +218,8 @@ class Category(BaseModel): # aq: 分类模型继承BaseModel
parse(self)
return categorys
@cache_decorator(60 * 60 * 10) # aq: 缓存10小时
def get_sub_categorys(self): # aq: 递归获取当前分类的所有子分类
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
@ -230,7 +230,7 @@ class Category(BaseModel): # aq: 分类模型继承BaseModel
def parse(category):
if category not in categorys:
categorys.append(category)
childs = all_categorys.filter(parent_category=category) # aq: 筛选子分类
childs = all_categorys.filter(parent_category=category)
for child in childs:
if category not in categorys:
categorys.append(child)
@ -240,137 +240,137 @@ class Category(BaseModel): # aq: 分类模型继承BaseModel
return categorys
class Tag(BaseModel): # aq: 标签模型继承BaseModel
class Tag(BaseModel):
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True) # aq: 标签名称(唯一)
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # aq: 标签的slug
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
def __str__(self): # aq: 模型实例的字符串表示(返回标签名称)
def __str__(self):
return self.name
def get_absolute_url(self): # aq: 实现抽象方法生成标签详情页URL
def get_absolute_url(self):
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10) # aq: 缓存10小时
def get_article_count(self): # aq: 获取该标签关联的文章数量
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name'] # aq: 默认按名称排序
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
class Links(models.Model): # aq: 友情链接模型
class Links(models.Model):
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True) # aq: 链接名称(唯一)
link = models.URLField(_('link')) # aq: 链接URL
sequence = models.IntegerField(_('order'), unique=True) # aq: 排序序号(唯一)
is_enable = models.BooleanField( # aq: 是否启用该链接
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
show_type = models.CharField( # aq: 链接展示位置关联LinkShowType枚举
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
creation_time = models.DateTimeField(_('creation time'), default=now) # aq: 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # aq: 最后修改时间
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence'] # aq: 按排序序号升序排列
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self): # aq: 模型实例的字符串表示(返回链接名称)
def __str__(self):
return self.name
class SideBar(models.Model): # aq: 侧边栏模型
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100) # aq: 侧边栏标题
content = models.TextField(_('content')) # aq: 侧边栏内容HTML
sequence = models.IntegerField(_('order'), unique=True) # aq: 排序序号(唯一)
is_enable = models.BooleanField(_('is enable'), default=True) # aq: 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) # aq: 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # aq: 最后修改时间
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(_('is enable'), default=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence'] # aq: 按排序序号升序排列
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self): # aq: 模型实例的字符串表示(返回侧边栏标题)
def __str__(self):
return self.name
class BlogSettings(models.Model): # aq: 博客配置模型
class BlogSettings(models.Model):
"""blog的配置"""
site_name = models.CharField( # aq: 站点名称
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
site_description = models.TextField( # aq: 站点描述
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
site_seo_description = models.TextField( # aq: 站点SEO描述
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField( # aq: 站点关键词
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
article_sub_length = models.IntegerField(_('article sub length'), default=300) # aq: 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # aq: 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # aq: 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) # aq: 文章页评论数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) # aq: 是否显示Google广告
google_adsense_codes = models.TextField( # aq: Google广告代码
article_sub_length = models.IntegerField(_('article sub length'), default=300)
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
article_comment_count = models.IntegerField(_('article comment count'), default=5)
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('open site comment'), default=True) # aq: 是否开放全站评论
global_header = models.TextField("公共头部", null=True, blank=True, default='') # aq: 公共头部HTML
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # aq: 公共尾部HTML
beian_code = models.CharField( # aq: 备案号
open_site_comment = models.BooleanField(_('open site comment'), default=True)
global_header = models.TextField("公共头部", null=True, blank=True, default='')
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
analytics_code = models.TextField( # aq: 网站统计代码
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
show_gongan_code = models.BooleanField( # aq: 是否显示公安备案号
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
gongan_beiancode = models.TextField( # aq: 公安备案号
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
comment_need_review = models.BooleanField( # aq: 评论是否需要审核
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
class Meta:
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
def __str__(self): # aq: 模型实例的字符串表示(返回站点名称)
def __str__(self):
return self.site_name
def clean(self): # aq: 验证方法,确保仅存在一个配置实例
def clean(self):
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs): # aq: 重写保存方法,保存后清空缓存
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
cache.clear()

@ -1,14 +1,13 @@
from haystack import indexes # aq: 导入Haystack搜索框架的索引基类
from haystack import indexes
from blog.models import Article # aq: 导入博客核心文章模型
from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable): # aq: 文章搜索索引类继承Haystack核心索引类
# aq: 核心搜索字段document=True标记为主字段use_template=True表示使用模板定义搜索范围
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
def get_model(self): # aq: 必须实现的方法指定索引关联的Django模型
def get_model(self):
return Article
def index_queryset(self, using=None): # aq: 定义索引的查询范围(仅索引已发布的文章)
return self.get_model().objects.filter(status='p') # aq: status='p'对应文章“已发布”状态
def index_queryset(self, using=None):
return self.get_model().objects.filter(status='p')

@ -1,84 +1,73 @@
import os
from django.conf import settings # aq: 导入Django项目配置
from django.core.files.uploadedfile import SimpleUploadedFile # aq: 导入简单文件上传类,用于测试图片上传
from django.core.management import call_command # aq: 导入Django命令调用工具用于测试自定义命令
from django.core.paginator import Paginator # aq: 导入分页工具,用于测试分页逻辑
from django.templatetags.static import static # aq: 导入静态文件路径生成工具
from django.test import Client, RequestFactory, TestCase # aq: 导入Django测试核心类模拟客户端、请求工厂、测试用例
from django.urls import reverse # aq: 导入URL反向解析函数
from django.utils import timezone # aq: 导入时区时间工具
from accounts.models import BlogUser # aq: 导入用户模型(自定义用户表)
from blog.forms import BlogSearchForm # aq: 导入博客搜索表单
from blog.models import Article, Category, Tag, SideBar, Links # aq: 导入博客核心模型
from blog.templatetags.blog_tags import load_pagination_info, load_articletags # aq: 导入自定义模板标签
from djangoblog.utils import get_current_site, get_sha256 # aq: 导入工具函数获取站点、SHA256加密
from oauth.models import OAuthUser, OAuthConfig # aq: 导入OAuth相关模型第三方登录
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.core.paginator import Paginator
from django.templatetags.static import static
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
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
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
class ArticleTest(TestCase): # aq: 文章模块综合测试类(覆盖模型、视图、表单、模板标签等)
def setUp(self): # aq: 测试初始化方法,每次测试前执行
self.client = Client() # aq: 初始化测试客户端模拟HTTP请求
self.factory = RequestFactory() # aq: 初始化请求工厂(构造自定义请求)
def test_validate_article(self): # aq: 核心测试方法——验证文章相关全流程(创建、访问、搜索等)
site = get_current_site().domain # aq: 获取当前站点域名
# aq: 创建/获取测试超级用户
class ArticleTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
def test_validate_article(self):
site = get_current_site().domain
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy") # aq: 设置用户密码
user.is_staff = True # aq: 标记为工作人员(可访问后台)
user.is_superuser = True # aq: 标记为超级管理员
user.set_password("liangliangyy")
user.is_staff = True
user.is_superuser = True
user.save()
# aq: 测试访问用户详情页
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200) # aq: 断言状态码为200正常访问
# aq: 测试访问后台相关页面(无需登录,仅验证路由存在)
self.assertEqual(response.status_code, 200)
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
# aq: 创建测试侧边栏
s = SideBar()
s.sequence = 1 # aq: 排序序号
s.name = 'test' # aq: 侧边栏标题
s.content = 'test content' # aq: 侧边栏内容
s.is_enable = True # aq: 启用侧边栏
s.sequence = 1
s.name = 'test'
s.content = 'test content'
s.is_enable = True
s.save()
# aq: 创建测试分类
category = Category()
category.name = "category" # aq: 分类名称
category.creation_time = timezone.now() # aq: 创建时间
category.last_mod_time = timezone.now() # aq: 最后修改时间
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
# aq: 创建测试标签
tag = Tag()
tag.name = "nicetag" # aq: 标签名称
tag.name = "nicetag"
tag.save()
# aq: 创建测试文章
article = Article()
article.title = "nicetitle" # aq: 文章标题
article.body = "nicecontent" # aq: 文章正文
article.author = user # aq: 关联作者
article.category = category # aq: 关联分类
article.type = 'a' # aq: 类型为“文章”
article.status = 'p' # aq: 状态为“已发布”
article.save()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
# aq: 测试文章标签关联(初始无标签,添加后断言数量)
article.save()
self.assertEqual(0, article.tags.count())
article.tags.add(tag)
article.save()
self.assertEqual(1, article.tags.count())
# aq: 批量创建20篇测试文章用于测试分页
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@ -90,44 +79,32 @@ class ArticleTest(TestCase): # aq: 文章模块综合测试类(覆盖模型
article.save()
article.tags.add(tag)
article.save()
# aq: 若启用Elasticsearch测试搜索功能
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index") # aq: 执行创建索引命令
response = self.client.get('/search', {'q': 'nicetitle'}) # aq: 发起搜索请求
call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
# aq: 测试访问文章详情页
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# aq: 测试爬虫通知功能(百度收录通知)
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
# aq: 测试访问标签、分类详情页
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# aq: 测试搜索不存在的关键词(验证页面正常响应)
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
# aq: 测试自定义模板标签load_articletags
s = load_articletags(article)
self.assertIsNotNone(s) # aq: 断言返回结果非空
self.assertIsNotNone(s)
# aq: 测试用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
# aq: 测试访问归档页面
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# aq: 测试各类分页场景(全部文章、标签归档、作者归档、分类归档)
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
@ -142,19 +119,16 @@ class ArticleTest(TestCase): # aq: 文章模块综合测试类(覆盖模型
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
# aq: 测试搜索表单(空查询)
f = BlogSearchForm()
f.search()
# aq: 测试百度爬虫通知批量URL
# self.client.login(username='liangliangyy', password='liangliangyy')
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
# aq: 测试头像相关模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com') # aq: 生成Gravatar头像URL
u = gravatar('liangliangyy@gmail.com') # aq: 生成Gravatar头像HTML
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
# aq: 创建测试友情链接并测试访问
link = Links(
sequence=1,
name="lylinux",
@ -163,71 +137,56 @@ class ArticleTest(TestCase): # aq: 文章模块综合测试类(覆盖模型
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# aq: 测试RSS订阅和站点地图
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# aq: 测试后台文章删除、日志访问
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value): # aq: 分页测试辅助方法
def check_pagination(self, p, type, value):
for page in range(1, p.num_pages + 1):
# aq: 获取分页信息(通过自定义模板标签)
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s) # aq: 断言分页信息非空
# aq: 测试上一页链接(存在则访问)
self.assertIsNotNone(s)
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
# aq: 测试下一页链接(存在则访问)
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self): # aq: 测试图片上传功能
import requests # aq: 导入requests库用于下载测试图片
# aq: 下载Python官方Logo作为测试图片
def test_image(self):
import requests
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png') # aq: 图片保存路径
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file:
file.write(rsp.content)
# aq: 未带签名访问上传接口预期403禁止访问
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
# aq: 生成上传签名双重SHA256加密
sign = get_sha256(get_sha256(settings.SECRET_KEY))
# aq: 构造文件上传数据
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile}
# aq: 带签名上传图片(跟随重定向)
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200) # aq: 断言上传成功
os.remove(imagepath) # aq: 删除本地测试图片
# aq: 测试邮件发送和用户头像保存工具函数
self.assertEqual(rsp.status_code, 200)
os.remove(imagepath)
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent') # aq: 发送测试邮件
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png') # aq: 保存远程头像
'https://www.python.org/static/img/python-logo.png')
def test_errorpage(self): # aq: 测试错误页面404页面
rsp = self.client.get('/eee') # aq: 访问不存在的路由
self.assertEqual(rsp.status_code, 404) # aq: 断言返回404状态码
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_commands(self): # aq: 测试自定义Django管理命令
# aq: 创建测试超级用户
def test_commands(self):
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -236,26 +195,23 @@ class ArticleTest(TestCase): # aq: 文章模块综合测试类(覆盖模型
user.is_superuser = True
user.save()
# aq: 创建测试OAuth配置QQ登录
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
# aq: 创建测试OAuth用户关联本地用户
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
u.user = user
u.picture = static("/blog/img/avatar.png") # aq: 本地静态头像
u.picture = static("/blog/img/avatar.png")
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}''' # aq: OAuth用户元数据
}'''
u.save()
# aq: 创建另一个测试OAuth用户远程头像
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -266,12 +222,11 @@ class ArticleTest(TestCase): # aq: 文章模块综合测试类(覆盖模型
}'''
u.save()
# aq: 执行各类自定义命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index") # aq: 构建ES搜索索引
call_command("ping_baidu", "all") # aq: 通知百度收录
call_command("create_testdata") # aq: 创建测试数据
call_command("clear_cache") # aq: 清空缓存
call_command("sync_user_avatar") # aq: 同步用户头像
call_command("build_search_words") # aq: 构建搜索关键词
call_command("build_index")
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")

@ -1,75 +1,62 @@
from django.urls import path
from django.views.decorators.cache import cache_page # aq: 导入缓存装饰器,用于页面缓存
from django.views.decorators.cache import cache_page
from . import views # aq: 导入当前app的视图模块
from . import views
app_name = "blog" # aq: 定义URL命名空间避免路由名称冲突
app_name = "blog"
urlpatterns = [
# aq: 首页路由默认第1页
path(
r'',
views.IndexView.as_view(),
name='index'),
# aq: 首页分页路由(指定页码)
path(
r'page/<<int:page>/',
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
# aq: 文章详情页路由(按日期+文章ID访问
path(
r'article/<<int:year>/<<int:month>/<<int:day>/<<int:article_id>.html',
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
# aq: 分类详情页路由默认第1页
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
# aq: 分类详情页分页路由
path(
r'category/<slug:category_name>/<<int:page>.html',
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
# aq: 作者文章列表路由默认第1页
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
# aq: 作者文章列表分页路由
path(
r'author/<author_name>/<<int:page>.html',
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
# aq: 标签文章列表路由默认第1页
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
# aq: 标签文章列表分页路由
path(
r'tag/<slug:tag_name>/<<int:page>.html',
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
# aq: 文章归档页面缓存1小时减轻服务器压力
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
# aq: 友情链接页面路由
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
# aq: 图片上传接口路由
path(
r'upload',
views.fileupload,
name='upload'),
# aq: 缓存清理接口路由
path(
r'clean',
views.clean_cache_view,
name='clean'),
]
]

@ -2,26 +2,26 @@ import logging
import os
import uuid
from django.conf import settings # aq: 导入Django项目全局配置
from django.core.paginator import Paginator # aq: 导入分页工具,用于评论分页
from django.http import HttpResponse, HttpResponseForbidden # aq: 导入HTTP响应类正常响应/禁止访问)
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.shortcuts import render # aq: 导入模板渲染函数
from django.templatetags.static import static # aq: 生成静态文件访问URL
from django.utils import timezone # aq: 导入时区时间工具,用于文件存储目录命名
from django.utils.translation import gettext_lazy as _ # aq: 导入国际化翻译函数
from django.views.decorators.csrf import csrf_exempt # aq: 豁免CSRF验证适配外部上传调用
from django.views.generic.detail import DetailView # aq: 导入详情页通用视图基类
from django.views.generic.list import ListView # aq: 导入列表页通用视图基类
from haystack.views import SearchView # aq: 导入Haystack搜索视图基类
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
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
from haystack.views import SearchView
from blog.models import Article, Category, LinkShowType, Links, Tag # aq: 导入博客核心数据模型
from comments.forms import CommentForm # aq: 导入评论表单类
from djangoblog.plugin_manage import hooks # aq: 导入插件钩子管理工具
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME # aq: 导入文章内容钩子常量
from djangoblog.utils import cache, get_blog_setting, get_sha256 # aq: 导入工具函数(缓存/博客配置/加密)
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
logger = logging.getLogger(__name__) # aq: 初始化当前模块日志对象
logger = logging.getLogger(__name__)
class ArticleListView(ListView):
@ -33,31 +33,31 @@ class ArticleListView(ListView):
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY # aq: 每页显示数量(读取全局配置)
page_kwarg = 'page' # aq: 分页参数名URL中用page传递页码
link_type = LinkShowType.L # aq: 友情链接展示类型(列表页默认)
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
def get_view_cache_key(self):
return self.request.get['pages'] # aq: 原代码语法错误get应为GET保留原逻辑
return self.request.get['pages']
@property
def page_number(self):
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page # aq: 计算当前页码优先URL参数其次GET参数默认1
return page
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError() # aq: 抽象方法,强制子类实现
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError() # aq: 抽象方法,强制子类实现
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
@ -71,7 +71,7 @@ class ArticleListView(ListView):
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list) # aq: 缓存查询结果
cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
@ -82,10 +82,10 @@ class ArticleListView(ListView):
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value # aq: 重写父类方法,优先从缓存获取数据
return value
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type # aq: 向模板添加友情链接类型标识
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
@ -94,34 +94,33 @@ class IndexView(ArticleListView):
首页
'''
# 友情链接类型
link_type = LinkShowType.I # aq: 首页专属友情链接类型
link_type = LinkShowType.I
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
return article_list # aq: 查询首页文章(已发布的普通文章)
return article_list
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key # aq: 生成首页缓存键(含页码,区分不同分页)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
template_name = 'blog/article_detail.html' # aq: 详情页模板
model = Article # aq: 关联的模型类
pk_url_kwarg = 'article_id' # aq: URL中主键参数名对应路由的article_id
context_object_name = "article" # aq: 模板中文章对象的变量名
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
def get_context_data(self, **kwargs):
comment_form = CommentForm() # aq: 初始化评论表单
article_comments = self.object.comment_list() # aq: 获取文章评论(带缓存)
parent_comments = article_comments.filter(parent_comment=None) # aq: 筛选顶级评论
blog_setting = get_blog_setting() # aq: 获取博客全局配置
paginator = Paginator(parent_comments, blog_setting.article_comment_count) # aq: 评论分页
comment_form = CommentForm()
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
page = 1
@ -148,8 +147,8 @@ class ArticleDetailView(DetailView):
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
kwargs['next_article'] = self.object.next_article # aq: 下一篇文章
kwargs['prev_article'] = self.object.prev_article # aq: 上一篇文章
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
@ -159,18 +158,18 @@ class ArticleDetailView(DetailView):
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
return context # aq: 向模板添加评论、上下篇等扩展数据
return context
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档" # aq: 页面类型标识(模板展示用)
page_type = "分类目录归档"
def get_queryset_data(self):
slug = self.kwargs['category_name'] # aq: 从URL获取分类slug
category = get_object_or_404(Category, slug=slug) # aq: 获取分类不存在返回404
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
@ -178,7 +177,7 @@ class CategoryDetailView(ArticleListView):
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list # aq: 查询分类及子分类下的已发布文章
return article_list
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
@ -187,60 +186,60 @@ class CategoryDetailView(ArticleListView):
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key # aq: 生成分类缓存键(含分类名+页码)
return cache_key
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1] # aq: 处理多级分类,取最后一级名称
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs) # aq: 向模板添加分类标识
return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档' # aq: 页面类型标识
page_type = '作者文章归档'
def get_queryset_cache_key(self):
from slugify import slugify
author_name = slugify(self.kwargs['author_name']) # aq: 作者名转slug适配URL
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key # aq: 生成作者缓存键含作者名slug+页码)
return cache_key
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 # aq: 查询指定作者的已发布文章
return article_list
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs) # aq: 向模板添加作者标识
return super(AuthorDetailView, self).get_context_data(**kwargs)
class TagDetailView(ArticleListView):
'''
标签列表页面
'''
page_type = '分类标签归档' # aq: 页面类型标识
page_type = '分类标签归档'
def get_queryset_data(self):
slug = self.kwargs['tag_name'] # aq: 从URL获取标签slug
tag = get_object_or_404(Tag, slug=slug) # aq: 获取标签不存在返回404
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list # aq: 查询指定标签的已发布文章
return article_list
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
@ -249,56 +248,56 @@ class TagDetailView(ArticleListView):
self.name = tag_name
cache_key = 'tag_{tag_name}_{page}'.format(
tag_name=tag_name, page=self.page_number)
return cache_key # aq: 生成标签缓存键(含标签名+页码)
return cache_key
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs) # aq: 向模板添加标签标识
return super(TagDetailView, self).get_context_data(**kwargs)
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
page_type = '文章归档' # aq: 页面类型标识
paginate_by = None # aq: 归档页不分页
page_kwarg = None # aq: 无分页参数
template_name = 'blog/article_archives.html' # aq: 归档页专用模板
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
def get_queryset_data(self):
return Article.objects.filter(status='p').all() # aq: 查询所有已发布文章
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key # aq: 归档页缓存键(固定值)
return cache_key
class LinkListView(ListView):
model = Links # aq: 关联友情链接模型
template_name = 'blog/links_list.html' # aq: 友情链接模板
model = Links
template_name = 'blog/links_list.html'
def get_queryset(self):
return Links.objects.filter(is_enable=True) # aq: 筛选启用的友情链接
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
def get_context(self):
paginator, page = self.build_page() # aq: 构建搜索结果分页
paginator, page = self.build_page()
context = {
"query": self.query, # aq: 搜索关键词
"form": self.form, # aq: 搜索表单
"page": page, # aq: 当前页搜索结果
"paginator": paginator, # aq: 分页器
"suggestion": None, # aq: 拼写建议(默认无)
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context()) # aq: 添加额外上下文
context.update(self.extra_context())
return context # aq: 向模板添加搜索相关数据
return context
@csrf_exempt
@ -309,17 +308,17 @@ def fileupload(request):
:return:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None) # aq: 获取上传签名(防止非法上传)
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden() # aq: 签名无效返回403
return HttpResponseForbidden()
response = []
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d') # aq: 按日期创建存储目录
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 # aq: 判断是否为图片
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
@ -328,17 +327,17 @@ def fileupload(request):
return HttpResponse("only for post")
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk) # aq: 分块写入文件
wfile.write(chunk)
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True) # aq: 图片压缩质量20%
url = static(savepath) # aq: 生成文件静态URL
image.save(savepath, quality=20, optimize=True)
url = static(savepath)
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post") # aq: 仅支持POST请求
return HttpResponse("only for post")
def page_not_found_view(
@ -346,13 +345,13 @@ def page_not_found_view(
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception) # aq: 记录异常日志
logger.error(exception)
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?'),
'statuscode': '404'},
status=404) # aq: 404页面处理视图
status=404)
def server_error_view(request, template_name='blog/error_page.html'):
@ -360,7 +359,7 @@ def server_error_view(request, template_name='blog/error_page.html'):
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500) # aq: 500页面处理视图
status=500)
def permission_denied_view(
@ -368,13 +367,13 @@ def permission_denied_view(
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception) # aq: 记录异常日志
logger.error(exception)
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403) # aq: 403页面处理视图
'statuscode': '403'}, status=403)
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
return HttpResponse('ok')

@ -0,0 +1,51 @@
1. 待分析开源软件概述
#psy: 需明确软件核心定位(如工具类、业务系统、框架),为后续质量分析聚焦重点。
#psy: 编程语言需标注具体版本(如 Python 3.9、Java 11避免因版本差异导致分析偏差。
#psy: 代码整体情况补充 “核心模块数量”“第三方依赖个数”,帮助评估分析复杂度。
2. 软件质量分析方法及工具
2.1 分析的软件质量要素
#psy: 内部质量建议新增 “代码可维护性”“可扩展性”,外部质量补充 “兼容性”“性能”,覆盖更全面的质量维度。
#psy: 明确各要素的优先级(如核心功能优先分析 “功能正确性”,长期维护项目优先 “可维护性”)。
2.2 软件质量分析方法
#psy: 人工阅读代码可按 “模块拆分 + 交叉复核” 模式,减少单人分析遗漏。
#psy: 外部质量分析建议新增 “场景化测试”(如高并发、异常输入场景),模拟真实使用场景。
#psy: 可引入 “对比分析” 方法,参考同类型优秀开源软件的质量标准,定位差距。
2.3 软件质量分析工具
#psy: 工具选择需匹配分析要素(如性能分析用 JMeter、编码规范检测用 Pylint避免工具与需求不匹配。
#psy: 注明工具版本及配置参数(如 SonarQube 9.9、检测规则集选择 “开源通用规则”),保证分析结果可复现。
#psy: 若使用多款工具,需说明工具间的互补性(如人工 + 工具结合,覆盖不同类型质量问题)。
3. 人工分析代码的质量情况
#psy: 整体说明需给出 “质量等级初步判定”(如优秀、良好、一般、需优化),结合关键问题占比支撑结论。
3.1 代码风格
#psy: 需先明确参考的编码规范(如 PEP 8、Google Java Style确保分析有统一标准。
#psy: 代码片段示例需标注 “文件路径 + 行号”,方便追溯;违规点需说明 “不符合的规范条款” 及 “优化建议”。
#psy: 统计 “规范符合率”(如 85% 代码符合编码风格),量化分析结果。
3.2 代码设计
#psy: 模块化分析需说明 “模块划分逻辑”,判断是否符合 “单一职责原则”。
#psy: 高内聚、低耦合分析可结合 “类间依赖次数”“方法调用复杂度” 等具体指标。
#psy: 代码片段需聚焦核心设计(如类的继承关系、模块间接口设计),避免冗余示例。
3.3 编程技能
#psy: 需区分 “基础编程技巧” 和 “进阶优化技巧”,重点突出与项目场景适配的技能(如大数据处理用分布式编程技巧)。
#psy: 代码示例需说明 “该技巧带来的优势”(如使用缓存技巧提升查询性能、设计模式降低耦合)。
#psy: 可补充 “技巧复用性评估”,判断该技能是否可迁移到同类项目。
3.4 代码质量问题
#psy: 按 “严重程度” 分级(致命、严重、一般、轻微),优先列出影响功能和稳定性的问题。
#psy: 每个问题需包含 “问题描述、代码位置、影响范围、修复方案、修复状态”,确保可落地。
#psy: 统计 “问题修复率” 及 “未修复原因”(如技术复杂度高、需兼容旧版本)。
4. 工具分析代码的质量情况
#psy: 先概述工具分析的 “整体得分”“问题总数”“高优先级问题占比”,给出宏观结论。
#psy: 代表性问题需关联 “工具检测规则 ID”如 SonarQube 的 S100 命名违规),方便查阅详细说明。
#psy: 对比 “工具检测结果” 与 “人工分析结果”,说明一致性和差异点(如工具未检测到的逻辑问题)。
5. 小组分工
#psy: 分工需明确 “责任模块 + 分析内容 + 完成时间 + 交付物”,避免职责重叠或遗漏。
#psy: 可新增 “交叉复核分工”,确保分析结果的准确性(如 A 成员分析的模块由 B 成员复核)。
#psy: 用列表形式呈现,格式统一(如 “成员 1用户模块 - 代码风格分析 + 工具检测 - 完成时间xx - 交付物:问题清单”)。
6. 下一步的改进计划
#psy: 按 “紧急程度 + 实施难度” 排序,优先推进 “低成本高收益” 的改进项。
#psy: 每个改进项需明确 “目标、具体措施、责任人、时间节点、验收标准”,确保可执行。
#psy: 补充 “改进效果预估”(如修复致命问题后,系统稳定性提升 30%;优化代码设计后,维护效率提升 20%)。

@ -1,23 +1,35 @@
#psy: 模块级注释:此模块用于配置 Django Admin 后台中 Comment评论模型的管理界面。
#psy: 主要功能包括:
#psy: 1. 定义了两个批量操作启用enable和禁用disable评论。
#psy: 2. 通过 CommentAdmin 类,高度自定义了评论列表的显示方式、可交互字段、过滤器和排除字段。
#psy: 3. 实现了在评论列表中直接链接到评论作者User和所属文章Article详情页的功能。
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 _
#psy: 定义一个批量操作函数,用于将选中的评论设置为“禁用”状态。
#psy: modeladmin: 当前的 ModelAdmin 实例。
#psy: request: 当前的 HTTP 请求对象。
#psy: queryset: 管理员在界面上选中的评论对象集合。
def disable_commentstatus(modeladmin, request, queryset):
#psy: 使用 queryset.update() 高效地批量更新数据库记录,避免循环操作。
queryset.update(is_enable=False)
#psy: 定义一个批量操作函数,用于将选中的评论设置为“启用”状态。
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
#psy: 为批量操作函数设置在 Admin 界面上显示的名称,并支持国际化。
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
#psy: 自定义 Comment 模型的 Admin 配置类。
class CommentAdmin(admin.ModelAdmin):
#psy: 配置列表页每页显示 20 条评论记录。
list_per_page = 20
#psy: 定义列表页要显示的字段。
#psy: 'link_to_userinfo' 和 'link_to_article' 是下面定义的自定义方法,用于生成链接。
list_display = (
'id',
'body',
@ -25,23 +37,34 @@ class CommentAdmin(admin.ModelAdmin):
'link_to_article',
'is_enable',
'creation_time')
#psy: 定义列表页中哪些字段是可点击的链接,点击后进入该评论的编辑页面。
list_display_links = ('id', 'body', 'is_enable')
#psy: 添加过滤器侧边栏,允许管理员按 'is_enable' 字段(是否启用)来筛选评论。
list_filter = ('is_enable',)
#psy: 在评论添加和编辑表单中,排除 'creation_time' 和 'last_modify_time' 字段,
#psy: 这些字段通常由系统自动管理,不允许管理员手动修改。
exclude = ('creation_time', 'last_modify_time')
#psy: 将上面定义的两个批量操作函数注册到 Admin 界面。
actions = [disable_commentstatus, enable_commentstatus]
#psy: 自定义列表字段,用于在评论列表中显示指向用户信息的链接。
#psy: obj 参数是当前遍历到的 Comment 实例。
def link_to_userinfo(self, obj):
#psy: 获取评论作者obj.author所属模型的 app_label 和 model_name
#psy: 这是 Django Admin URL 反向解析所必需的。
info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
#psy: 使用 reverse 生成管理员编辑该用户的 URL。
#psy: 格式通常是 'admin:app_label_model_name_change'。
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
#psy: 自定义列表字段,用于在评论列表中显示指向文章的链接。
def link_to_article(self, obj):
info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
#psy: 返回一个指向文章编辑页面的链接,链接文本为文章标题。
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
#psy: 为自定义的列表字段设置在 Admin 界面上显示的列名,并支持国际化。
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -1,5 +1,13 @@
from django.apps import AppConfig
#psy: 统一团队 Python 代码编写标准,减少因风格差异导致的阅读成本和维护难度。
#psy: 对齐 PEP 8 核心规范,降低规范违规引发的语法错误、逻辑隐患等质量问题。
#psy: 提升代码可读性、可复用性和可扩展性,适配多人协作开发场景。
#psy: 为后续代码质量分析(如工具检测、人工核查)提供明确判定依据。
class CommentsConfig(AppConfig):
name = 'comments'
#psy: 覆盖团队所有 Python 项目,包括新开发项目、开源项目二次开发、旧项目重构。
#psy: 适配 Python 3.6 及以上版本,明确版本差异带来的规范调整(如 f-string 语法使用)。
#psy: 适用于所有开发环节,包括代码编写、评审、测试、提交及文档配套。
#psy: 涵盖所有代码类型,含业务逻辑代码、工具类代码、配置文件、测试用例等。

@ -1,4 +1 @@
# yrh指定当前 Django 应用djangoblog的默认 AppConfig 类
# yrh作用告诉 Django 启动时,使用 'djangoblog.apps' 模块下的 'DjangoblogAppConfig' 类来配置当前应用
# yrh通常用于在应用就绪ready 方法)时执行初始化操作,比如加载插件、注册信号等
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -1,22 +1,13 @@
# yrh从 Django 内置的 admin 模块中导入 AdminSite 类
# yrhAdminSite 是所有自定义 Admin 站点的基类
from django.contrib.admin import AdminSite
# yrh导入 LogEntry 模型,它用于记录 Admin 后台的操作日志
from django.contrib.admin.models import LogEntry
# yrh导入 Django Sites 框架的 Admin 和 Model
# yrhSites 框架允许一个 Django 项目管理多个网站
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
# yrh从当前项目的各个应用中导入 Admin 配置和 Model
# yrh这种通配符导入 (*) 在项目内部是常见的,但在公共库中应避免
# yrh它假设了在这些 admin.py 文件中已经定义好了相应的 ModelAdmin 类
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
# yrh导入自定义的 LogEntryAdmin用于自定义操作日志的显示
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
@ -27,76 +18,28 @@ from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
"""yrh
自定义的 Admin 站点类
通过继承 AdminSite可以完全定制 Admin 后台的行为和外观
例如修改站点标题添加自定义权限检查集成自定义视图等
"""
# yrh定义 Admin 站点顶部的标题文字
site_header = 'djangoblog administration'
# yrh定义浏览器标签页上的标题文字
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
"""yrh
构造函数
这里调用了父类的构造函数并传入了一个默认的站点名称 'admin'
这个名称用于 URL 解析等内部操作
"""
super().__init__(name)
def has_permission(self, request):
"""yrh
重写权限检查方法
这个方法决定了一个请求是否有权访问 Admin 站点
默认实现是检查用户是否登录 (request.user.is_staff)
这里的自定义逻辑是
只有超级管理员 (superuser) 才能访问这个 Admin 站点
这是一个比默认更严格的权限控制
:param request: HTTP 请求对象
:return: 如果用户有权访问则返回 True否则返回 False
"""
return request.user.is_superuser
# def get_urls(self):
# """
# (已注释)重写 URL 配置方法。
#
# 这个方法允许你向 Admin 站点添加自定义的 URL 路由。
# 例如,这里的示例代码尝试添加一个 '/admin/refresh/' 的 URL
# 用于触发某些缓存刷新操作。
#
# 被注释掉意味着当前未启用此功能。
# """
# urls = super().get_urls()
# from django.urls import path
# from blog.views import refresh_memcache
#
# my_urls = [
# # 使用 self.admin_view() 包装自定义视图,可以确保它受到 Admin 权限保护
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# # 将自定义 URL 添加到默认的 Admin URL 列表前面
# return urls + my_urls
# yrh创建自定义 Admin 站点的实例
# yrh这个实例将作为我们项目的 Admin 入口点
admin_site = DjangoBlogAdminSite(name='admin')
# yrh--- 将各个应用的模型注册到自定义的 admin_site 上 ---
# yrh格式是admin_site.register(ModelClass, ModelAdminClass)
# yrhModelClass: 要管理的数据模型
# yrhModelAdminClass: 定义了该模型在 Admin 中如何显示和操作的类
# yrh注册 blog 应用的模型
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
@ -104,26 +47,18 @@ admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
#yrh 注册 servermanager 应用的模型
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
# yrh注册 accounts 应用的模型
admin_site.register(BlogUser, BlogUserAdmin)
# yrh注册 comments 应用的模型
admin_site.register(Comment, CommentAdmin)
# yrh注册 oauth 应用的模型
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
# yrh注册 owntracks 应用的模型
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
# yrh注册 Django 内置的 Site 模型
admin_site.register(Site, SiteAdmin)
# yrh注册自定义的 LogEntry 模型管理
# yrh这允许管理员查看和管理 Admin 操作日志
admin_site.register(LogEntry, LogEntryAdmin)
admin_site.register(LogEntry, LogEntryAdmin)

@ -1,45 +1,11 @@
# yrh从 Django 框架中导入 AppConfig 类
# yrhAppConfig 是 Django 1.7 引入的,用于配置和管理整个应用的生命周期
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
"""yrh
DjangoBlog 应用的自定义配置类
这个类继承自 Django AppConfig用于定义应用的元数据和初始化行为
它取代了旧版的 AppConfig 配置方式提供了更强大和统一的接口
"""
# yrh指定默认的主键字段类型。
# yrh在 Django 3.2 及以后版本中,这是一个必填项(或在 settings.py 中设置 DEFAULT_AUTO_FIELD
# yrhBigAutoField 是一个自增的 BigIntegerField适合未来可能有大量数据的表。
default_auto_field = 'django.db.models.BigAutoField'
# yrh应用的完整 Python 模块名。
# yrh这个值必须与应用的目录名和 Python 包名一致Django 通过它来识别应用。
name = 'djangoblog'
def ready(self):
"""yrh
应用就绪时执行的钩子方法
这个方法会在 Django 应用完全加载所有模型都已注册之后被调用一次
这是执行应用级初始化代码的最佳时机例如
- 注册信号处理器 (Signals)
- 加载动态配置或插件
- 启动后台任务
**注意**为了避免在某些命令 `migrate`执行时产生不必要的副作用
Django 建议在此方法中进行的导入应放在方法内部而不是模块顶部
"""
# yrh调用父类的 ready 方法,这是一个良好的实践,确保 Django 内部的初始化逻辑得以执行
super().ready()
# yrh在应用就绪后导入并执行插件加载函数
# yrh这里使用了延迟导入Lazy Import将 import 语句放在 ready 方法内部。
# yrh这样做可以防止在 Django 应用未完全准备好之前就加载插件模块,从而避免潜在的循环依赖或导入错误。
# Import and load plugins here
from .plugin_manage.loader import load_plugins
# yrh执行插件加载函数这个函数会根据配置动态导入并初始化所有激活的插件
load_plugins()
load_plugins()

@ -1,22 +1,14 @@
# yrh导入用于多线程处理的模块这里用于异步发送邮件
import _thread
import logging
# yrh导入 Django 的信号相关模块
import django.dispatch
from django.conf import settings
# yrh导入 Admin 操作日志模型
from django.contrib.admin.models import LogEntry
# yrh导入 Django 内置的用户登录和登出信号
from django.contrib.auth.signals import user_logged_in, user_logged_out
# yrh导入 Django 的邮件发送工具
from django.core.mail import EmailMultiAlternatives
# yrh导入 Django 模型保存后的信号
from django.db.models.signals import post_save
# yrh导入用于注册信号接收器的装饰器
from django.dispatch import receiver
# yrh从项目的其他应用中导入所需的模型、工具和函数
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
@ -24,92 +16,51 @@ from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, del
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
# yrh创建一个日志记录器
logger = logging.getLogger(__name__)
# yrh--- 定义自定义信号 ---
# yrh定义一个 OAuth 用户登录成功后的信号
# yrh这个信号在 OAuth 认证流程完成后被发送,用于触发后续操作
oauth_user_login_signal = django.dispatch.Signal(['id'])
# yrh定义一个发送邮件的信号
# yrh其他部分的代码可以通过发送这个信号来异步发送邮件而无需直接调用邮件发送函数
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
# yrh--- 定义信号接收器 (Signal Handlers) ---
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
"""yrh
发送邮件信号的接收器
当任何地方发送 `send_email_signal` 信号时这个函数就会被触发
它负责构建并发送 HTML 格式的邮件并记录发送日志
:param sender: 发送信号的对象或类
:param kwargs: 信号发送时附带的关键字参数包含 'emailto', 'title', 'content'
"""
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
# yrh创建一个 EmailMultiAlternatives 对象,支持发送纯文本和 HTML 内容
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL, # yrh发件人从配置中读取
to=emailto) # yrh收件人列表
msg.content_subtype = "html" # yrh指定邮件内容为 HTML 格式
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
msg.content_subtype = "html"
# yrh创建一条邮件发送日志记录
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto) # yrh将列表转换为字符串存入数据库
log.emailto = ','.join(emailto)
try:
# yrh发送邮件
result = msg.send()
# yrh根据发送结果发送成功的邮件数量更新日志
log.send_result = result > 0
except Exception as e:
# yrh如果发送失败记录详细的错误日志
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
# yrh保存日志记录到数据库
log.save()
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
"""yrh
OAuth 用户登录信号的接收器
当用户通过 OAuth ( GitHub, Google) 登录成功后此函数被触发
其主要作用是处理用户头像如果头像是外部链接则下载并保存到本地
:param sender: 发送信号的对象
:param kwargs: 包含 'id' (OAuthUser ID)
"""
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
# yrh获取当前站点的域名
site = get_current_site().domain
# yrh检查用户头像是否存在并且不是本站的链接
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
# yrh下载并保存头像到本地并更新用户记录
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
# yrh删除侧边栏缓存因为用户信息可能显示在侧边栏如登录用户头像
delete_sidebar_cache()
@ -122,81 +73,42 @@ def model_post_save_callback(
using,
update_fields,
**kwargs):
"""yrh
Django 模型保存后 (post_save) 信号的通用接收器
这是一个非常强大的信号几乎所有模型在调用 .save() 方法后都会触发它
该函数通过判断 `instance` 的类型对不同模型的保存事件做出不同的响应
:param sender: 发送信号的模型类 (e.g., Article, Comment)
:param instance: 被保存的模型实例
:param created: 布尔值表示是创建新记录 (True) 还是更新现有记录 (False)
:param raw: 布尔值如果是通过 fixtures SQL 直接导入的数据则为 True
:param using: 正在使用的数据库别名
:param update_fields: 被更新的字段名称集合如果是新建则为 None
:param kwargs: 其他关键字参数
"""
clearcache = False # yrh标记是否需要清除全局缓存
# yrh如果保存的是 Admin 操作日志,则直接返回,不做任何处理
clearcache = False
if isinstance(instance, LogEntry):
return
# yrh检查被保存的实例是否有 'get_full_url' 方法(通常是 Article 等内容模型)
if 'get_full_url' in dir(instance):
# yrh判断是否仅仅是更新了 'views' (浏览量) 字段
is_update_views = update_fields == {'views'}
# yrh如果不是测试环境并且不是更新浏览量
if not settings.TESTING and not is_update_views:
try:
# yrh获取文章的完整 URL
notify_url = instance.get_full_url()
# yrh通知搜索引擎如百度有新内容或内容已更新
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
# yrh如果不是更新浏览量则标记需要清除缓存
if not is_update_views:
clearcache = True
# yrh如果被保存的实例是 Comment (评论)
if isinstance(instance, Comment):
# yrh只有当评论被设为“启用”状态时才进行处理
if instance.is_enable:
# yrh获取评论所属文章的 URL
path = instance.article.get_absolute_url()
site = get_current_site().domain
# yrh处理带有端口号的域名在某些开发环境中
if site.find(':') > 0:
site = site[0:site.find(':')]
# yrh清除该文章详情页的缓存
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
# yrh清除 SEO 处理器的缓存
if cache.get('seo_processor'):
cache.delete('seo_processor')
# yrh清除该文章的评论缓存
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
# yrh清除侧边栏缓存因为侧边栏可能显示最新评论
delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)])
# yrh启动一个新线程来发送评论通知邮件避免阻塞当前请求
_thread.start_new_thread(send_comment_email, (instance,))
# yrh如果任何条件触发了缓存清除标记则执行全局缓存清除
if clearcache:
cache.clear()
@ -204,21 +116,7 @@ def model_post_save_callback(
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
"""yrh
用户登录和登出信号的接收器
当用户成功登录或登出时此函数被触发
主要用于清除与用户状态相关的缓存以确保显示最新的用户信息
:param sender: 发送信号的对象
:param request: 当前的 HTTP 请求对象
:param user: 登录或登出的用户对象
:param kwargs: 其他关键字参数
"""
if user and user.username:
logger.info(user)
# yrh删除侧边栏缓存因为侧边栏可能显示登录用户信息或根据登录状态显示不同内容
delete_sidebar_cache()
# yrh注释掉的 cache.clear() 会清除所有缓存,这可能代价太高。
# yrh只删除受影响的特定缓存如 sidebar是更优的做法。
# yrhcache.clear()
# cache.clear()

@ -1,164 +1,89 @@
# yrh导入 Django 和 Haystack 的核心模块
from django.utils.encoding import force_str
from elasticsearch_dsl import Q # yrh导入 Elasticsearch DSL 的查询构建器
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
# yrh导入项目自定义的 Elasticsearch 文档类和管理器,以及相关模型
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
# yrh创建日志记录器
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
"""yrh
自定义的 Elasticsearch 搜索后端
这个类继承自 Haystack BaseSearchBackend Haystack Elasticsearch 通信的核心
它负责执行实际的搜索操作索引的创建更新和删除
"""
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
"""yrh
初始化后端实例
:param connection_alias: Haystack 配置中定义的连接别名
:param connection_options: 连接 Elasticsearch 所需的选项如主机端口等
"""
super(ElasticSearchBackend, self).__init__(connection_alias, **connection_options)
# yrh初始化自定义的文档管理器它封装了与 Elasticsearch 索引交互的具体逻辑
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager()
# yrh标记是否启用拼写建议功能
self.include_spelling = True
def _get_models(self, iterable):
"""yrh
辅助方法将模型实例列表转换为 Elasticsearch 文档对象列表
:param iterable: 模型实例或模型主键的可迭代对象
:return: ArticleDocument 对象列表
"""
# yrh如果提供了可迭代对象且不为空则使用它否则默认获取所有 Article 对象
models = iterable if iterable and iterable[0] else Article.objects.all()
# yrh通过管理器将模型实例转换为 Elasticsearch 文档
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
"""yrh
创建索引并添加模型数据
:param models: 要索引的模型实例列表
"""
self.manager.create_index() # 确保索引存在
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs) # 重建索引(会删除旧数据)
self.manager.rebuild(docs)
def _delete(self, models):
"""yrh
从索引中删除模型数据
:param models: 要删除的模型实例列表
:return: 操作是否成功
"""
# yrh这里的实现假设 ArticleDocumentManager 或模型的 delete 方法已经与 ES 集成
# yrh实际中更可能是调用 self.manager.delete_docs(models)
for m in models:
m.delete()
return True
def _rebuild(self, models):
"""yrh
重建索引非破坏性更新
:param models: 要索引的模型实例列表
"""
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
def update(self, index, iterable, commit=True):
"""yrh
Haystack 调用此方法来更新索引
:param index: 索引名称此处未使用由管理器处理
:param iterable: 要更新的模型实例可迭代对象
:param commit: 是否立即提交此处未使用
"""
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
"""yrh
Haystack 调用此方法来从索引中移除对象
:param obj_or_string: 要移除的模型实例或其主键
"""
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
"""yrh
清除索引
:param models: 可选要清除的模型类
:param commit: 是否立即提交
"""
self.remove(None)
@staticmethod
def get_suggestion(query: str) -> str:
"""yrh
获取 Elasticsearch 的拼写/术语建议
"""获取推荐词, 如果没有找到添加原搜索词"""
:param query: 用户输入的原始查询字符串
:return: 由推荐词组成的新查询字符串
"""
# yrh使用 Elasticsearch DSL 构建一个带有 "term" 建议的搜索
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
# yrh解析建议结果
for suggest in search.suggest.suggest_search:
if suggest["options"]:
# yrh如果有建议项取分数最高的那一个
keywords.append(suggest["options"][0]["text"])
else:
# yrh如果没有建议项使用原始词
keywords.append(suggest["text"])
return ' '.join(keywords)
@log_query
def search(self, query_string, **kwargs):
"""yrh
执行实际的搜索
这是后端最核心的方法接收查询字符串和参数构建 Elasticsearch 查询
执行搜索并将结果格式化为 Haystack 期望的格式
:param query_string: 用户输入的查询字符串
:param kwargs: 包含搜索选项的字典如分页过滤条件等
:return: 一个包含搜索结果的字典
"""
logger.info('search query_string:' + query_string)
# yrh从参数中获取分页信息
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# yrh推荐词搜索
# 推荐词搜索
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
# yrh使用 Elasticsearch DSL 构建复杂的布尔查询
# yrhshould 子句表示“或”关系,匹配 body 或 title 字段
# yrhminimum_should_match="70%" 表示至少需要匹配 70% 的 should 子句
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
@ -168,9 +93,8 @@ class ElasticSearchBackend(BaseSearchBackend):
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
# yrh执行搜索
results = search.execute()
# yrh解析搜索结果
hits = results['hits'].total
raw_results = []
for raw_result in results['hits']['hits']:
@ -181,7 +105,6 @@ class ElasticSearchBackend(BaseSearchBackend):
result_class = SearchResult
result = result_class(
# yrh为每个结果创建一个 Haystack SearchResult 对象
app_label,
model_name,
raw_result['_id'],
@ -190,7 +113,7 @@ class ElasticSearchBackend(BaseSearchBackend):
raw_results.append(result)
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion
# yrh构建返回字典符合 Haystack 的规范
return {
'results': raw_results,
'hits': hits,
@ -200,34 +123,20 @@ class ElasticSearchBackend(BaseSearchBackend):
class ElasticSearchQuery(BaseSearchQuery):
"""yrh
自定义的 Haystack 查询类
这个类负责解析用户的查询字符串构建查询参数并与后端交互
它是 Haystack 抽象查询逻辑与具体后端查询实现之间的桥梁
"""
def _convert_datetime(self, date):
"""yrh
将日期时间对象转换为 Elasticsearch 可以理解的字符串格式
:param date: datetime 对象
:return: 格式化的日期时间字符串
"""
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
def clean(self, query_fragment):
"""yrh
清理用户输入的查询片段处理保留字和特殊字符
注意这个实现是为 Whoosh 设计的可能不完全适用于 Elasticsearch
Elasticsearch 有自己的查询字符串语法可能需要不同的清理逻辑
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
:param query_fragment: 用户输入的查询片段
:return: 清理后的查询片段
Whoosh 1.X differs here in that you can no longer use a backslash
to escape reserved characters. Instead, the whole word should be
quoted.
"""
words = query_fragment.split()
cleaned_words = []
@ -246,80 +155,29 @@ class ElasticSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
"""yrh
构建查询片段
当使用复杂过滤如在 SearchQuerySet 中使用 .filter()Haystack 会调用此方法
这个简单的实现直接返回了 value query_string 属性
:param field: 要过滤的字段
:param filter_type: 过滤类型 'exact', 'contains'
:param value: 过滤的值
:return: 查询片段
"""
return value.query_string
def get_count(self):
"""yrh
获取搜索结果的总数
:return: 结果总数
"""
results = self.get_results()
return results['hits'] if results else 0
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
"""yrh
获取拼写建议
:param preferred_query: 首选的查询字符串
:return: 拼写建议
"""
# yrh这个方法需要从后端获取建议这里的实现比较简单
# yrh实际中可能需要调用 backend 的搜索方法并检查返回的 'spelling_suggestion'
return self._spelling_suggestion
def build_params(self, spelling_query=None):
"""yrh
构建传递给后端 search 方法的参数
:param spelling_query: 用于拼写检查的查询字符串
:return: 参数字典
"""
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
"""yrh
自定义的搜索表单
继承自 Haystack ModelSearchForm用于处理搜索请求的表单提交和验证
这里添加了对是否启用拼写建议功能的支持
"""
def search(self):
"""yrh
重写搜索方法在执行搜索前设置拼写建议的开关
:return: SearchQuerySet 对象
"""
# yrh从表单数据中获取 'is_suggest' 参数,判断是否需要启用拼写建议
# yrh如果前端传递 "is_suggest=no",则关闭建议功能
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
# yrh调用父类的 search 方法执行搜索
sqs = super().search()
return sqs
class ElasticSearchEngine(BaseEngine):
"""yrh
自定义的 Haystack 引擎类
这个类是 Haystack 入口点它定义了使用哪个 Backend Query
settings.py 中配置 Haystack 你会指定使用这个引擎
"""
# yrh指定该引擎使用的后端类
backend = ElasticSearchBackend
# yrh指定该引擎使用的查询类
query = ElasticSearchQuery
query = ElasticSearchQuery

@ -1,111 +1,40 @@
# yrh导入 Django 核心模块和自定义工具
from django.contrib.auth import get_user_model # 用于获取当前项目的 User 模型
from django.contrib.syndication.views import Feed # Django 内置的 Feed 生成基类
from django.utils import timezone # 用于处理时间和时区
from django.utils.feedgenerator import Rss201rev2Feed # RSS 2.0 规范的生成器
from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed
# yrh导入项目相关的模型和工具
from blog.models import Article # 博客文章模型
from djangoblog.utils import CommonMarkdown # 自定义的 Markdown 解析工具
from blog.models import Article
from djangoblog.utils import CommonMarkdown
class DjangoBlogFeed(Feed):
"""yrh
自定义的 RSS Feed 生成器类
这个类继承自 Django Feed 基类通过重写其方法来定义 RSS Feed 的内容和结构
当用户访问对应的 URL Django 会自动调用这个类的方法来生成并返回 RSS XML 文档
"""
# yrh指定 Feed 的类型为 RSS 2.0。这是最常用的 RSS 版本。
feed_type = Rss201rev2Feed
# --- Feed 级别的元数据 ---
# yrhFeed 的描述信息,会出现在 RSS 阅读器的源描述中。
description = '大巧无工,重剑无锋.'
# yrhFeed 的标题,会出现在 RSS 阅读器的源列表中。
title = "且听风吟 大巧无工,重剑无锋. "
# yrhFeed 的链接,通常是博客的首页或 Feed 的专属页面。
link = "/feed/"
def author_name(self):
"""yrh
返回 Feed 的作者名称
这里默认获取数据库中第一个用户的昵称作为 Feed 的作者
在实际应用中你可能希望从网站设置或一个固定的管理员账户中获取
"""
return get_user_model().objects.first().nickname
def author_link(self):
"""yrh
返回 Feed 作者的链接
"""
return get_user_model().objects.first().get_absolute_url()
# --- Feed 内容Items/Entries---
def items(self):
"""yrh
返回构成 Feed 条目的对象列表
这是 Feed 类最核心的方法之一它定义了哪些文章会被包含在 RSS Feed
:return: 查询集 (QuerySet)包含了最新的 5 篇已发布文章
"""
return Article.objects.filter(
type='a', # yrh筛选类型为 'a' (article文章) 的内容
status='p' # yrh筛选状态为 'p' (published已发布) 的内容
).order_by('-pub_time')[:5] # yrh按发布时间降序排序取最新的 5 篇
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
"""yrh
返回单个条目的标题
:param item: items() 方法返回的列表中的一个对象这里是 Article 实例
:return: 文章的标题字符串
"""
return item.title
def item_description(self, item):
"""yrh
返回单个条目的描述内容摘要
:param item: Article 实例
:return: 经过 Markdown 解析的 HTML 格式的文章内容
"""
# yrh假设文章内容是用 Markdown 编写的,这里将其转换为 HTML 以便 RSS 阅读器正确显示。
return CommonMarkdown.get_markdown(item.body)
# --- 其他可选但推荐的方法 ---
def feed_copyright(self):
"""yrh
返回 Feed 的版权信息
"""
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
"""yrh
返回单个条目的链接即文章的永久链接
:param item: Article 实例
:return: 文章的绝对 URL 字符串
"""
return item.get_absolute_url()
def item_guid(self, item):
"""yrh
返回单个条目的全局唯一标识符 (GUID)
GUID 用于帮助 RSS 阅读器判断一个条目是否已经被阅读过避免重复
这里的实现返回 NoneDjango 会默认使用 item_link 的值作为 GUID
更严谨的做法是返回一个不会改变的唯一字符串例如文章的 ID 加上一个固定的前缀
:param item: Article 实例
:return: None (使用默认行为)
"""
return
return

@ -1,164 +1,91 @@
# yrh导入 Django Admin 相关的核心模块
from django.contrib import admin
from django.contrib.admin.models import DELETION # yrh用于标识“删除”操作
from django.contrib.contenttypes.models import ContentType # yrh用于获取模型的元数据
from django.urls import reverse, NoReverseMatch # yrh用于反向解析 URL
from django.utils.encoding import force_str # yrh用于安全地转换字符串编码
from django.utils.html import escape # yrh用于转义 HTML 特殊字符,防止 XSS 攻击
from django.utils.safestring import mark_safe # yrh用于标记字符串为安全的 HTML可以直接渲染
from django.utils.translation import gettext_lazy as _ # yrh用于国际化翻译
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
"""yrh
自定义的 LogEntry ModelAdmin
LogEntry Django 内置的模型用于记录 Admin 后台的所有操作如创建修改删除
这个自定义的 Admin 类用于美化 LogEntry 的列表显示使其更具可读性和交互性
"""
# yrh--- 列表页过滤和搜索配置 ---
# yrh右侧过滤器允许按“内容类型”即按不同的模型过滤日志
list_filter = [
'content_type'
]
# yrh搜索框的搜索字段允许根据对象表示和变更消息来搜索日志
search_fields = [
'object_repr', # yrh对象的字符串表示如文章标题
'change_message' # yrh变更的详细消息
'object_repr',
'change_message'
]
# yrh--- 列表页显示配置 ---
# yrh列表中可点击的字段点击后进入详情页这里 LogEntry 通常没有详情页,所以主要是为了样式)
list_display_links = [
'action_time', # yrh操作时间
'get_change_message', # yrh变更消息这是一个自定义方法
'action_time',
'get_change_message',
]
# yrh列表页要显示的字段
list_display = [
'action_time', # yrh操作时间
'user_link', # yrh操作用户自定义方法显示为链接
'content_type', # yrh操作的内容类型如 blog.Article
'object_link', # yrh操作的对象自定义方法显示为链接
'get_change_message', # yrh变更消息
'action_time',
'user_link',
'content_type',
'object_link',
'get_change_message',
]
# yrh--- 权限控制 ---
def has_add_permission(self, request):
"""yrh
禁止添加新的 LogEntry
日志是系统自动生成的不应该由用户手动添加
"""
return False
def has_change_permission(self, request, obj=None):
"""
控制修改权限
仅允许超级管理员或拥有 'change_logentry' 权限的用户以非 POST 方式访问即仅允许查看
这实际上是禁止了对日志的任何修改操作
"""
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
def has_delete_permission(self, request, obj=None):
"""yrh
禁止删除 LogEntry
操作日志是重要的审计线索应该被永久保留不允许删除
"""
return False
# yrh--- 自定义列表字段 ---
def object_link(self, obj):
"""yrh
自定义列表字段将操作对象显示为指向该对象 Admin 编辑页面的链接
:param obj: LogEntry 实例
:return: 一个可以安全渲染为 HTML 的字符串包含 <a> 标签
"""
# yrh先获取对象的字符串表示并进行 HTML 转义,防止 XSS
object_link = escape(obj.object_repr)
content_type = obj.content_type
# yrh如果操作不是“删除”并且内容类型存在
if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
try:
# yrh反向解析出该对象在 Admin 中的编辑页面 URL
# yrhURL 名称格式为 'admin:app_label_modelName_change'
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
)
# yrh构建 HTML 链接
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
# yrh如果 URL 解析失败(例如,该模型没有在 Admin 中注册),则不做处理
pass
# yrh使用 mark_safe 告诉 Django 这个字符串是安全的,可以直接作为 HTML 渲染
return mark_safe(object_link)
# yrh为自定义字段设置属性
object_link.admin_order_field = 'object_repr' # yrh允许根据对象表示进行排序
object_link.short_description = _('object') # yrh列标题支持国际化
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
def user_link(self, obj):
"""yrh
自定义列表字段将操作用户显示为指向该用户 Admin 编辑页面的链接
:param obj: LogEntry 实例
:return: 一个可以安全渲染为 HTML 的字符串包含 <a> 标签
"""
# yrh获取用户模型的 ContentType
content_type = ContentType.objects.get_for_model(type(obj.user))
# yrh获取用户的字符串表示并转义
user_link = escape(force_str(obj.user))
try:
# yrh反向解析出用户的 Admin 编辑页面 URL
# try returning an actual link instead of object repr string
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
)
# yrh构建 HTML 链接
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
pass
return mark_safe(user_link)
# yrh为自定义字段设置属性
user_link.admin_order_field = 'user' # yrh允许根据用户进行排序
user_link.short_description = _('user') # yrh列标题
# yrh--- 性能优化和其他配置 ---
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
def get_queryset(self, request):
"""yrh
重写查询集方法用于性能优化
使用 prefetch_related 提前加载 'content_type' 关联对象
避免在列表页渲染时为每一行都执行一次数据库查询N+1 查询问题
"""
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
def get_actions(self, request):
"""yrh
重写获取操作的方法
从列表顶部的操作下拉菜单中移除 'delete_selected'删除选中项操作
进一步确保日志不会被误删
"""
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
return actions

@ -1,87 +1,41 @@
# yrh导入 Python 内置的 logging 模块,用于记录日志信息
import logging
# yrh创建一个名为当前模块名的 logger 实例
# yrh使用 __name__ 作为 logger 名称是一个好习惯,它能帮助你在日志中清晰地定位到消息的来源 。
logger = logging.getLogger(__name__)
class BasePlugin:
"""yrh
所有插件的基类抽象类
这个类定义了插件系统的核心规范和通用功能任何具体的插件都应该继承自这个类
并实现或重写相关方法它采用了模板方法Template Method设计模式
定义了插件的生命周期初始化 -> 注册钩子子类可以在固定的流程中填充自己的逻辑
"""
# yrh--- 插件元数据 (类属性) ---
# yrh这些属性是每个插件必须提供的基本信息用于在插件管理器中识别和展示插件。
# yrh它们被定义为类级别的属性强制要求子类进行重写。
# yrh插件的名称 (必须是唯一的字符串)
class BasePlugin:
# 插件元数据
PLUGIN_NAME = None
# yrh插件的详细描述
PLUGIN_DESCRIPTION = None
# yrh插件的版本号 (建议遵循语义化版本规则, e.g., "1.0.0")
PLUGIN_VERSION = None
def __init__(self):
"""yrh
插件实例的构造函数
这个方法在插件被实例化时自动调用它负责执行插件的基本设置流程
1. 验证子类是否正确设置了所有必需的元数据
2. 调用 `init_plugin` 方法进行子类特有的初始化
3. 调用 `register_hooks` 方法让子类注册其功能钩子
"""
# yrh检查所有必需的元数据是否都已被子类定义。
# yrh如果有任何一个为 None则抛出 ValueError 异常,防止创建不完整的插件实例。
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
# yrh调用初始化方法。这是一个模板方法子类可以重写它来实现自己的初始化逻辑。
self.init_plugin()
# yrh调用钩子注册方法。子类应重写此方法来向主程序注册事件处理函数。
self.register_hooks()
def init_plugin(self):
"""yrh
插件的初始化钩子
这是一个模板方法提供了一个默认实现记录一条日志
子类可以根据需要重写这个方法以执行插件启动前所需的任何设置
例如加载配置文件连接数据库等
"""
# yrh默认行为在插件初始化完成后记录一条 INFO 级别的日志。
插件初始化逻辑
子类可以重写此方法来实现特定的初始化操作
"""
logger.info(f'{self.PLUGIN_NAME} initialized.')
def register_hooks(self):
"""yrh
钩子注册方法
这是插件系统的核心子类必须重写此方法并在其中调用主程序提供的
钩子注册接口将自己的功能函数绑定到特定的事件上
例如一个处理文章内容的插件可能会在这里注册一个函数到 'the_content' 钩子上
基类中的这个方法是空的pass因为它不知道具体的注册逻辑这由子类决定
"""
注册插件钩子
子类可以重写此方法来注册特定的钩子
"""
pass
def get_plugin_info(self):
"""yrh
获取插件的元数据信息
这个方法提供了一个标准化的方式来获取插件的名称描述和版本
插件管理器可以调用此方法来收集所有已加载插件的信息用于展示或管理
:return: 一个包含插件信息的字典
:rtype: dict
"""
获取插件信息
:return: 包含插件元数据的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
}
}

@ -1,12 +1,7 @@
# yrh加载文章详情时使用的 Action 类型标识
ARTICLE_DETAIL_LOAD = 'article_detail_load'
# yrh创建新文章时使用的 Action 类型标识
ARTICLE_CREATE = 'article_create'
# yrh更新现有文章时使用的 Action 类型标识
ARTICLE_UPDATE = 'article_update'
# yrh删除文章时使用的 Action 类型标识
ARTICLE_DELETE = 'article_delete'
# yrh用于文章内容处理的钩子Hook名称通常用于过滤或修改文章内容
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -1,100 +1,44 @@
# yrh导入 Python 内置的 logging 模块,用于记录系统运行日志
import logging
# yrh创建一个与当前模块同名的 logger 实例,方便在日志中定位来源
logger = logging.getLogger(__name__)
# yrh定义一个私有的字典 _hooks用于存储所有注册的钩子。
# yrh字典的键 (key) 是钩子名称 (hook_name)。
# yrh字典的值 (value) 是一个列表,存储了所有注册到该钩子的回调函数 (callback)。
# yrh使用下划线 '_' 开头命名,暗示这是一个模块内部的私有变量,不应该被外部直接访问或修改。
_hooks = {}
def register(hook_name: str, callback: callable):
"""yrh
注册一个回调函数到指定的钩子上
当某个钩子被触发时所有注册到该钩子的回调函数都会被执行
:param hook_name: 钩子的名称是一个字符串
:param callback: 要注册的回调函数这个函数将在钩子被触发时被调用
"""
# yrh检查 _hooks 字典中是否已经存在该钩子名称
注册一个钩子回调
"""
if hook_name not in _hooks:
# 如果不存在,则为这个钩子名称创建一个新的空列表,用于存放后续注册的回调函数
_hooks[hook_name] = []
# yrh将回调函数追加到对应钩子名称的列表中
_hooks[hook_name].append(callback)
# yrh记录一条调试日志说明哪个回调函数已成功注册到哪个钩子
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""yrh
触发一个 **动作钩子 (Action Hook)**
动作钩子用于通知插件或其他代码某个事件已经发生它会依次执行所有注册到该钩子的回调函数
但不关心这些函数的返回值也不会对它们的返回值做任何处理
:param hook_name: 要触发的钩子名称
:param args: 传递给回调函数的位置参数
:param kwargs: 传递给回调函数的关键字参数
"""
# yrh检查是否有任何回调函数注册到了这个钩子上
执行一个 Action Hook
它会按顺序执行所有注册到该钩子上的回调函数
"""
if hook_name in _hooks:
# yrh记录一条调试日志表明正在执行该动作钩子
logger.debug(f"Running action hook '{hook_name}'")
# yrh遍历并执行该钩子下所有已注册的回调函数
for callback in _hooks[hook_name]:
try:
# yrh使用 *args 和 **kwargs 将接收到的参数原封不动地传递给回调函数
callback(*args, **kwargs)
except Exception as e:
# yrh如果某个回调函数执行时发生异常记录一条错误日志
# yrh并使用 exc_info=True 来打印完整的堆栈跟踪信息,这对于调试至关重要。
# yrh捕获异常是为了防止一个插件的错误导致整个钩子系统崩溃。
logger.error(
f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}",
exc_info=True
)
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""yrh
触发一个 **过滤器钩子 (Filter Hook)**
过滤器钩子用于修改或处理某个值它会将一个初始值依次传递给所有注册的回调函数
每个函数的输出会成为下一个函数的输入最终经过所有函数处理后的值将被返回
:param hook_name: 要触发的钩子名称
:param value: 要被过滤或处理的初始值
:param args: 传递给回调函数的额外位置参数
:param kwargs: 传递给回调函数的额外关键字参数
:return: 经过所有过滤器处理后的最终值
"""
# yrh检查是否有任何回调函数注册到了这个钩子上
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
"""
if hook_name in _hooks:
# yrh记录一条调试日志表明正在应用该过滤器钩子
logger.debug(f"Applying filter hook '{hook_name}'")
# yrh遍历并执行该钩子下所有已注册的回调函数
for callback in _hooks[hook_name]:
try:
# yrh将当前的值 (value) 和其他参数传递给回调函数
# yrh然后将回调函数的返回值重新赋值给 value
# yrh这样下一个回调函数就能接收到上一个函数处理过的值
value = callback(value, *args, **kwargs)
except Exception as e:
# yrh同样捕获并记录任何在过滤器执行过程中发生的异常
logger.error(
f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}",
exc_info=True
)
# yrh返回经过所有过滤器处理后的最终值
return value
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
return value

@ -1,50 +1,19 @@
# yrh导入必要的模块
import os
import logging
from django.conf import settings
# yrh创建一个日志记录器用于记录插件加载过程中的信息和错误
logger = logging.getLogger(__name__)
def load_plugins():
"""yrh
动态加载并初始化 'plugins' 目录下的插件
这个函数会遍历 Django 设置 (settings.py) `ACTIVE_PLUGINS` 列表里的每个插件名称
对于每个插件它会检查其目录结构是否合法即存在一个名为 'plugin.py' 的文件
如果合法则尝试导入该文件插件的初始化逻辑例如注册钩子通常在其被导入时自动执行
该函数应在 Django 应用注册表 (app registry) 就绪后调用
例如在某个 AppConfig `ready()` 方法中调用以确保所有 Django 核心组件都已加载完毕
"""
# yrh遍历在 settings.py 中配置的 ACTIVE_PLUGINS 列表
# yrhACTIVE_PLUGINS 应该是一个字符串列表,每个字符串代表一个插件的名称
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
for plugin_name in settings.ACTIVE_PLUGINS:
# yrh构建插件的绝对路径
# yrhsettings.PLUGINS_DIR 应该是一个指向项目插件根目录的路径字符串
# yrh例如如果 PLUGINS_DIR 是 'path/to/project/plugins'
# yrh且 plugin_name 是 'hello_world'
# yrh那么 plugin_path 就是 'path/to/project/plugins/hello_world'
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
# yrh检查路径是否为一个目录并且该目录下是否存在 'plugin.py' 文件
# yrh这是一个基本的插件结构校验
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# yrh动态导入插件的主模块
# yrh__import__ 函数用于以字符串形式导入模块
# yrh'plugins.{plugin_name}.plugin' 是插件主模块的完整 Python 路径
# yrh例如对于 'hello_world' 插件,它会尝试导入 'plugins.hello_world.plugin'
# yrh导入这个模块会执行其顶层代码包括插件类的定义和实例化
# yrh通常插件会在其 __init__.py 或 plugin.py 的顶层创建一个插件类的实例,
# yrh从而触发 BasePlugin 的 __init__ 方法,完成初始化和钩子注册。
__import__(f'plugins.{plugin_name}.plugin')
# yrh如果导入成功记录一条成功日志
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
# yrh如果在导入过程中发生错误例如模块不存在、语法错误等
# yrh记录一条详细的错误日志包括异常信息和堆栈跟踪
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -1,4 +1,4 @@
"""yrh
"""
Django settings for djangoblog project.
Generated by 'django-admin startproject' using Django 1.10.2.
@ -12,107 +12,72 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
import os
import sys
from pathlib import Path
#yrh导入Django国际化翻译工具
from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
"""yrh
从环境变量获取布尔值的工具函数
:param env: 环境变量名
:param default: 环境变量不存在时的默认值
:return: 环境变量对应的布尔值True/False
"""
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# yrhBuild paths inside the project like this: BASE_DIR / 'subdir'.
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# yrhQuick-start development settings - unsuitable for production
# yrhSee https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# yrhSECURITY WARNING: keep the secret key used in production secret!
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# yrhSECURITY WARNING: don't run with debug turned on in production!
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# yrhDEBUG = False
# DEBUG = False
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# yrhALLOWED_HOSTS = []
# ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# yrhdjango 4.0新增配置
# django 4.0新增配置
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# yrhApplication definition
# Application definition
INSTALLED_APPS = [
# yrhDjango内置Admin精简版配置
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
# yrhDjango内置用户认证系统
'django.contrib.auth',
# yrh内容类型框架用于模型权限管理
'django.contrib.contenttypes',
# yrh会话框架用于用户登录状态保持
'django.contrib.sessions',
# yrh消息框架用于页面提示信息
'django.contrib.messages',
# yrh静态文件管理框架
'django.contrib.staticfiles',
# yrh站点框架用于多站点管理如sitemap
'django.contrib.sites',
# yrh站点地图框架
'django.contrib.sitemaps',
# yrh第三方应用Markdown编辑器
'mdeditor',
# yrh第三方应用全文搜索框架
'haystack',
# yrh自定义应用博客核心功能
'blog',
# yrh自定义应用用户账户管理
'accounts',
# yrh自定义应用评论功能
'comments',
# yrh自定义应用第三方登录OAuth
'oauth',
# yrh自定义应用服务器管理
'servermanager',
# yrh自定义应用位置追踪
'owntracks',
# yrh第三方应用静态文件压缩CSS/JS
'compressor',
# yrh自定义应用项目核心配置
'djangoblog'
]
MIDDLEWARE = [
# yrh安全中间件处理HTTPS、XSS防护等
'django.middleware.security.SecurityMiddleware',
# yrh会话中间件管理用户会话
'django.contrib.sessions.middleware.SessionMiddleware',
# yrh国际化中间件处理多语言切换
'django.middleware.locale.LocaleMiddleware',
# yrhGZip压缩中间件压缩响应内容提升性能
'django.middleware.gzip.GZipMiddleware',
# yrh缓存更新中间件注释按需启用用于页面缓存
# yrh'django.middleware.cache.UpdateCacheMiddleware',
# yrh通用中间件处理URL重写、请求转发等
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# yrh缓存获取中间件注释按需启用与UpdateCacheMiddleware配对使用
# yrh'django.middleware.cache.FetchFromCacheMiddleware',
# yrhCSRF防护中间件防止跨站请求伪造
# 'django.middleware.cache.FetchFromCacheMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
# yrh认证中间件管理用户认证状态
'django.contrib.auth.middleware.AuthenticationMiddleware',
# yrh消息中间件传递用户操作提示
'django.contrib.messages.middleware.MessageMiddleware',
# yrh点击劫持防护中间件限制iframe嵌入
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# yrh条件GET中间件处理304 Not Modified响应提升性能
'django.middleware.http.ConditionalGetMiddleware',
# yrh自定义中间件在线用户统计
'blog.middleware.OnlineMiddleware'
]
@ -120,36 +85,25 @@ ROOT_URLCONF = 'djangoblog.urls'
TEMPLATES = [
{
# yrh模板引擎使用Django内置模板引擎
'BACKEND': 'django.template.backends.django.DjangoTemplates',
# yrh自定义模板目录项目根目录下的templates文件夹
'DIRS': [os.path.join(BASE_DIR, 'templates')],
# yrh是否允许从应用内的templates目录加载模板
'APP_DIRS': True,
# yrh模板引擎选项
'OPTIONS': {
# yrh上下文处理器向所有模板传递全局变量
'context_processors': [
# yrh调试上下文处理器开发环境传递调试信息
'django.template.context_processors.debug',
# yrh请求上下文处理器向模板传递request对象
'django.template.context_processors.request',
# yrh认证上下文处理器向模板传递user对象
'django.contrib.auth.context_processors.auth',
# yrh消息上下文处理器向模板传递messages对象
'django.contrib.messages.context_processors.messages',
# yrh自定义上下文处理器SEO相关配置
'blog.context_processors.seo_processor'
],
},
},
]
# yrhWSGI应用入口用于部署如Gunicorn、uWSGI
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# yrhDatabase
# yrhhttps://docs.djangoproject.com/en/1.10/ref/settings/#databases
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
@ -163,73 +117,56 @@ DATABASES = {
}
}
# yrhPassword validation
# yrhhttps://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
# yrh用户名相似度验证密码不能与用户名太相似
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
# yrh密码长度验证密码不能太短
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
# yrh常见密码验证密码不能是常见弱密码
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
# yrh纯数字密码验证密码不能全是数字
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# yrh支持的语言列表
LANGUAGES = (
('en', _('English')), # yrh英语
('zh-hans', _('Simplified Chinese')), # yrh简体中文
('zh-hant', _('Traditional Chinese')), # yrh繁体中文
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
# yrh翻译文件目录项目根目录下的locale文件夹
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
# yrh默认语言简体中文
LANGUAGE_CODE = 'zh-hans'
# yrh时区亚洲/上海)
TIME_ZONE = 'Asia/Shanghai'
# yrh是否启用国际化
USE_I18N = True
# yrh是否启用本地化日期、时间格式等
USE_L10N = True
# yrh是否使用UTC时区False表示使用本地时区
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
HAYSTACK_CONNECTIONS = {
'default': {
# yrh搜索引擎自定义Whoosh引擎支持中文
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
# yrh索引文件存储路径当前配置文件所在目录下的whoosh_index文件夹
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
# yrhAutomatically update searching index
# Automatically update searching index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# yrhAllow user login with username and password
# Allow user login with username and password
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
@ -238,28 +175,22 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
# yrh自定义用户模型替换Django默认User模型
AUTH_USER_MODEL = 'accounts.BlogUser'
# yrh登录URL未登录用户访问需登录页面时跳转
LOGIN_URL = '/login/'
# yrh时间显示格式年-月-日 时:分:秒)
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
# yrh日期显示格式年-月-日)
DATE_TIME_FORMAT = '%Y-%m-%d'
# yrhbootstrap color styles
# bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# yrhpaginate
# paginate
PAGINATE_BY = 10
# yrhhttp cache timeout
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# yrhcache setting
# cache setting
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
@ -267,7 +198,7 @@ CACHES = {
'LOCATION': 'unique-snowflake',
}
}
# yrh使用redis作为缓存
# 使用redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
@ -275,51 +206,31 @@ if os.environ.get("DJANGO_REDIS_URL"):
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
# yrh站点ID多站点时用于区分不同站点
SITE_ID = 1
# yrh百度链接提交通知URL优先从环境变量获取用于新文章收录
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# yrh邮件后端使用SMTP服务
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# yrh是否使用TLS加密优先从环境变量获取默认关闭
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
# yrh是否使用SSL加密优先从环境变量获取默认开启
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
# yrh邮件服务器地址优先从环境变量获取默认阿里云企业邮箱
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
# yrh邮件服务器端口优先从环境变量获取SSL默认465
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
# yrh邮件账号优先从环境变量获取
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
# yrh邮件密码/授权码(优先从环境变量获取)
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
# yrh默认发件人邮箱与EMAIL_HOST_USER一致
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
# yrh服务器错误通知邮箱
SERVER_EMAIL = EMAIL_HOST_USER
# yrh管理员邮箱用于接收系统错误通知优先从环境变量获取
# Setting debug=false did NOT handle except email notifications
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# yrh微信管理员密码两次MD5加密后的值优先从环境变量获取
# WX ADMIN password(Two times md5)
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
# yrh日志文件存储目录项目根目录下的logs文件夹
LOG_PATH = os.path.join(BASE_DIR, 'logs')
# yrh若日志目录不存在则创建exist_ok=True避免重复创建报错
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
# yrh日志配置
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
@ -382,61 +293,49 @@ LOGGING = {
}
STATICFILES_FINDERS = (
# yrh从项目根目录下的STATICFILES_DIRS配置目录查找静态文件
'django.contrib.staticfiles.finders.FileSystemFinder',
# yrh从各个已安装应用下的static目录查找静态文件
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# yrh第三方查找器用于查找django-compressor管理的压缩静态文件
# other
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# yrhCOMPRESS_OFFLINE = True
# COMPRESS_OFFLINE = True
COMPRESS_CSS_FILTERS = [
# yrhcreates absolute urls from relative ones
# creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter',
# yrhcss minimizer
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
]
# yrhJS压缩过滤器定义压缩JS文件时执行的处理逻辑
COMPRESS_JS_FILTERS = [
# yrh过滤器压缩JS去除空格、注释、简化变量名等减小文件体积
'compressor.filters.jsmin.JSMinFilter'
]
# yrh媒体文件存储根目录用户上传的文件如图片、附件将保存到项目根目录下的uploads文件夹
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
# yrh媒体文件访问URL通过http://域名/media/路径访问用户上传的文件
MEDIA_URL = '/media/'
# yrhX-Frame-Options响应头限制当前站点页面被哪些页面嵌入iframe
# yrhSAMEORIGIN=仅允许被同域名下的页面嵌入(防止点击劫持攻击)
X_FRAME_OPTIONS = 'SAMEORIGIN'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# yrh若环境变量配置了Elasticsearch主机地址则启用Elasticsearch作为搜索后端
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
# yrhElasticsearch DSL配置连接Elasticsearch服务
'default': {
# yrhElasticsearch服务地址
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
},
}
# yrh覆盖Haystack搜索连接配置使用自定义的Elasticsearch引擎
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# yrhPlugin System
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright', # yrh文章版权插件如添加版权声明
'reading_time', # yrh阅读时间插件估算文章阅读时长
'external_links', # yrh外部链接插件处理文章中的外部链接如添加target="_blank"
'view_count', # yrh阅读量统计插件统计文章被查看次数
'seo_optimizer' # yrhSEO优化插件优化文章SEO相关属性如标题、关键词
'article_copyright',
'reading_time',
'external_links',
'view_count',
'seo_optimizer'
]

@ -1,130 +1,59 @@
# yrh导入Django站点地图核心类和URL反向解析工具
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
# yrh导入博客相关模型文章、分类、标签
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
"""yrh
静态页面站点地图类
用于收录网站中URL固定内容不常变的静态页面如首页
"""
# yrh页面优先级0.0-1.01.0为最高静态页面优先级设为0.5
priority = 0.5
# yrh页面内容更新频率可选值always, hourly, daily, weekly, monthly, yearly, never
changefreq = 'daily'
def items(self):
"""yrh
返回需要收录的静态页面URL名称列表对应urls.py中name参数
这里仅收录首页URL名称为'blog:index'
"""
return ['blog:index', ]
def location(self, item):
"""yrh
根据items()返回的URL名称反向解析出完整URL路径
:param item: items()返回列表中的单个URL名称
:return: 静态页面的绝对URL路径
"""
return reverse(item)
class ArticleSiteMap(Sitemap):
"""yrh
文章页面站点地图类
用于收录所有已发布的博客文章页面帮助搜索引擎抓取文章内容
"""
# yrh文章页面更新频率设为“每月”根据博客更新频率可调整
changefreq = "monthly"
# yrh文章页面优先级设为0.6(高于分类、标签,低于首页等核心页面)
priority = "0.6"
def items(self):
return Article.objects.filter(status='p')
def lastmod(self, obj):
"""yrh
返回单个文章的最后修改时间帮助搜索引擎判断是否需要重新抓取页面
:param obj: items()返回列表中的单个Article对象
:return: 文章最后修改时间模型中的last_modify_time字段
"""
return obj.last_modify_time
class CategorySiteMap(Sitemap):
"""yrh
分类页面站点地图类
用于收录博客的所有分类页面
"""
# yrh分类页面更新频率设为“每周”
changefreq = "Weekly"
# yrh分类页面优先级设为0.6(与文章同级或略低,根据需求调整)
priority = "0.6"
def items(self):
"""yrh
返回所有分类对象列表无筛选收录全部分类
"""
return Category.objects.all()
def lastmod(self, obj):
"""yrh
返回单个分类的最后修改时间模型中的last_modify_time字段
:param obj: items()返回列表中的单个Category对象
:return: 分类最后修改时间
"""
return obj.last_modify_time
class TagSiteMap(Sitemap):
"""yrh
标签页面站点地图类
用于收录博客的所有标签页面
"""
# yrh标签页面更新频率设为“每周”
changefreq = "Weekly"
# yrh标签页面优先级设为0.3(低于文章、分类,属于辅助导航页面)
priority = "0.3"
def items(self):
"""yrh
返回所有标签对象列表无筛选收录全部标签
"""
return Tag.objects.all()
def lastmod(self, obj):
"""yrh
返回单个标签的最后修改时间模型中的last_modify_time字段
:param obj: items()返回列表中的单个Tag对象
:return: 标签最后修改时间
"""
return obj.last_modify_time
class UserSiteMap(Sitemap):
"""yrh
用户页面站点地图类
用于收录所有发表过文章的作者用户页面
"""
# yrh用户页面更新频率设为“每周”
changefreq = "Weekly"
# yrh用户页面优先级设为0.3(属于辅助页面,优先级较低)
priority = "0.3"
def items(self):
"""yrh
返回所有发表过文章的用户对象列表去重避免同一用户多次出现
逻辑1. 获取所有已发布文章 2. 提取每篇文章的作者 3. 用set去重 4. 转为列表
"""
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
"""yrh
返回单个用户的站点地图更新时间这里用用户注册时间date_joined作为参考
可根据需求改为用户最后发表文章时间等更精准的时间
:param obj: items()返回列表中的单个用户对象
:return: 用户注册时间
"""
return obj.date_joined

@ -1,48 +1,21 @@
# yrh导入日志模块用于记录通知过程中的信息和错误
import logging
# yrh导入requests库用于发送HTTP请求向搜索引擎提交链接
import requests
# yrh导入Django配置用于获取百度链接提交通知URL
from django.conf import settings
# yrh创建日志记录器用于记录当前模块的日志
logger = logging.getLogger(__name__)
class SpiderNotify():
"""yrh
搜索引擎爬虫通知类
用于向搜索引擎如百度主动提交网站新增/更新的页面链接
加速搜索引擎对页面的抓取和收录
"""
@staticmethod
def baidu_notify(urls):
"""yrh
向百度搜索引擎提交链接
调用百度站长平台提供的链接提交接口批量提交页面URL
:param urls: 要提交的URL列表['https://example.com/article/1/', ...]
"""
try:
# yrh将URL列表转换为百度接口要求的格式每行一个URL
data = '\n'.join(urls)
# yrh向百度链接提交通知URL发送POST请求提交URL数据
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
# yrh记录百度接口返回的响应信息成功时为提交结果方便调试
logger.info(result.text)
except Exception as e:
# yrh捕获提交过程中的所有异常如网络错误、接口不可用等并记录错误日志
logger.error(e)
@staticmethod
def notify(url):
"""yrh
通用通知方法简化接口
接收单个URL或URL列表调用百度链接提交方法
:param url: 单个URL字符串或URL列表
"""
# yrh直接调用百度链接提交方法若传入单个URL会自动转为列表后拼接成多行字符串
SpiderNotify.baidu_notify(url)
SpiderNotify.baidu_notify(url)

@ -1,37 +1,15 @@
# yrh导入Django内置的测试基类用于编写单元测试
from django.test import TestCase
# yrh导入项目工具模块中的所有工具函数/类(用于测试工具的正确性)
from djangoblog.utils import *
class DjangoBlogTest(TestCase):
"""yrh
项目工具函数单元测试类
用于测试`djangoblog.utils`模块中核心工具函数的功能正确性
确保工具在项目中使用时能稳定工作
"""
def setUp(self):
"""yrh
测试前置准备方法
在每个测试方法执行前自动调用用于初始化测试环境如创建测试数据
本测试类暂无需前置准备故留空
"""
pass
def test_utils(self):
"""yrh
核心工具函数综合测试方法
测试`utils`模块中常用的3个工具SHA256加密Markdown解析字典转URL参数
"""
# yrh1. 测试SHA256加密函数get_sha256
# yrh对字符串'test'进行加密断言加密结果不为None验证函数能正常返回结果
md5 = get_sha256('test')
self.assertIsNotNone(md5)
# yrh2. 测试Markdown解析工具CommonMarkdown.get_markdown
# yrh定义一段包含标题、代码块、链接的Markdown文本解析后断言结果不为None
c = CommonMarkdown.get_markdown('''
# Title1

@ -13,75 +13,52 @@ 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'))
"""
# yrh: 导入Django核心配置、国际化URL、静态文件URL相关模块
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
# yrh: 导入站点地图视图、URL路径匹配、搜索视图工厂等
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
# yrh: 导入项目自定义视图、Admin站点、搜索表单、Feed、站点地图等组件
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# yrh: 定义站点地图字典:键为站点地图标识,值为对应的站点地图类
sitemaps = {
'blog': ArticleSiteMap, # yrh: 文章页面站点地图
'Category': CategorySiteMap, # yrh: 分类页面站点地图
'Tag': TagSiteMap, # yrh: 标签页面站点地图
'User': UserSiteMap, # yrh: 用户页面站点地图
'static': StaticViewSitemap # yrh: 静态页面站点地图
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
}
# yrh: 404页面未找到指定处理视图
handler404 = 'blog.views.page_not_found_view'
# yrh: 500服务器内部错误指定处理视图
handler500 = 'blog.views.server_error_view'
# yrh: 403权限拒绝指定处理视图
handler403 = 'blog.views.permission_denied_view'
handle403 = 'blog.views.permission_denied_view'
# yrh: 非国际化URL列表不随语言切换变化的URL
urlpatterns = [
# yrh: 国际化切换接口:提供语言切换功能(如/en/、/zh-hans/
path('i18n/', include('django.conf.urls.i18n')),
]
# yrh: 国际化URL列表URL会自动添加语言前缀如/zh-hans/admin/
urlpatterns += i18n_patterns(
# yrh: 自定义Admin站点替代Django默认admin路径为/admin/
re_path(r'^admin/', admin_site.urls),
# yrh: 博客核心URL包含文章列表、详情等命名空间为\'blog\'
re_path(r'', include('blog.urls', namespace='blog')),
# yrh: Markdown编辑器URL集成mdeditor富文本编辑器
re_path(r'', include('blog.urls', namespace='blog')),
# yrh:Markdown编辑器URL集成mdeditor富文本编辑器
re_path(r'mdeditor/', include('mdeditor.urls')),
# yrh:评论功能URL命名空间为'comment'
re_path(r'', include('comments.urls', namespace='comment')),
# yrh:用户账户URL登录、注册、个人中心等命名空间为'account'
re_path(r'', include('accounts.urls', namespace='account')),
# yrh:第三方登录URLOAuth登录如GitHub、Google命名空间为'oauth'
re_path(r'', include('oauth.urls', namespace='oauth')),
# yrh:站点地图URL生成sitemap.xml供搜索引擎抓取
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
# yrh:RSS订阅1路径/feed/返回博客文章Feed
re_path(r'^feed/$', DjangoBlogFeed()),
# yrh:RSS订阅2路径/rss/,与/feed/功能一致,提供备用地址
re_path(r'^rss/$', DjangoBlogFeed()),
# yrh:全文搜索URL使用自定义Elasticsearch搜索视图和表单
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
# yrh:服务器管理URL如命令执行、邮件日志等命名空间为'servermanager'
re_path(r'', include('servermanager.urls', namespace='servermanager')),
# yrh:位置追踪URLowntracks相关功能命名空间为'owntracks'
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# yrh:仅调试模式DEBUG=True下启用配置用户上传文件媒体文件的访问URL
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
document_root=settings.MEDIA_ROOT)

@ -1,5 +1,5 @@
#yrh:!/usr/bin/env python
# yrh:encoding: utf-8
#!/usr/bin/env python
# encoding: utf-8
import logging
@ -16,77 +16,46 @@ from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
# yrh:创建一个日志记录器,用于记录当前模块的日志信息
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
"""yrh:
获取最新文章和最新评论的ID
这个函数用于获取数据库中最新创建的文章和评论的主键ID
通常在需要进行某些数据同步或初始化操作时使用
:return: 一个元组包含(最新文章ID, 最新评论ID)
"""
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
"""yrh:
对输入的字符串进行SHA-256哈希计算
:param str: 需要哈希的原始字符串
:return: 哈希后的十六进制字符串
"""
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
"""yrh:
一个通用的缓存装饰器工厂函数
这个高阶函数用于装饰其他函数以实现其返回值的自动缓存
当被装饰的函数被调用时它会先检查缓存中是否存在结果如果存在则直接返回
否则执行原函数并将结果存入缓存
:param expiration: 缓存过期时间单位为秒默认为3分钟
:return: 一个装饰器函数
"""
def wrapper(func):
def news(*args, **kwargs):
try:
# yrh:尝试从第一个参数通常是一个类实例如View获取缓存键
view = args[0]
key = view.get_cache_key()
except:
# yrh:如果失败例如被装饰的不是一个View方法则根据函数名和参数生成一个唯一键
key = None
if not key:
# yrh:将函数对象、参数和关键字参数组合成一个唯一字符串
unique_str = repr((func, args, kwargs))
# yrh:对该字符串进行哈希,生成一个固定长度的缓存键
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
# yrh:尝试从缓存中获取数据
value = cache.get(key)
if value is not None:
# yrh:如果缓存命中检查是否是代表None的特殊值
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
# yrh:如果缓存未命中,执行原函数
logger.debug('cache_decorator set cache:%s key:%s' % (func.__name__, key))
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
# yrh:存入缓存如果原函数返回None则存入一个特殊标记
if value is None:
cache.set(key, '__default_cache_value__', expiration)
else:
@ -99,7 +68,7 @@ def cache_decorator(expiration=3 * 60):
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''yrh:
'''
刷新视图缓存
:param path:url路径
:param servername:host
@ -125,39 +94,19 @@ def expire_view_cache(path, servername, serverport, key_prefix=None):
@cache_decorator()
def get_current_site():
"""yrh:
获取当前站点的信息并进行缓存
由于站点信息如域名不常变化使用`cache_decorator`进行缓存
避免重复查询数据库
:return: 当前的`Site`对象
"""
site = Site.objects.get_current()
return site
class CommonMarkdown:
"""yrh:
一个封装了Markdown转换功能的工具类
提供静态方法来将Markdown格式的文本转换为HTML并支持生成目录TOC
"""
@staticmethod
def _convert_markdown(value):
"""yrh:
内部辅助方法执行实际的Markdown转换
:param value: Markdown格式的原始文本
:return: 一个元组包含转换后的HTML正文和目录HTML
"""
# yrh:初始化Markdown解析器并启用多个扩展
md = markdown.Markdown(
extensions=[
'extra', # yrh:启用额外的Markdown语法
'codehilite', # yrh:启用代码高亮
'toc', # yrh:启用目录生成
'tables', # yrh:启用表格支持
'extra',
'codehilite',
'toc',
'tables',
]
)
body = md.convert(value)
@ -166,12 +115,6 @@ class CommonMarkdown:
@staticmethod
def get_markdown_with_toc(value):
"""yrh:
将Markdown文本转换为HTML并返回正文和目录
:param value: Markdown格式的原始文本
:return: (body_html, toc_html)
"""
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@ -180,17 +123,8 @@ class CommonMarkdown:
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
"""yrh:
发送邮件的封装函数
该函数不直接调用邮件发送API而是发送一个`send_email_signal`信号
这种设计将邮件发送的触发者和执行者解耦提高了代码的灵活性和可测试性
:param emailto: 收件人邮箱列表
:param title: 邮件标题
:param content: 邮件内容HTML格式
"""
def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
@ -200,17 +134,11 @@ def send_email(emailto, title, content):
def generate_code() -> str:
"""yrh:生成随机数验证码"""
"""生成随机数验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
"""yrh:
将一个字典转换为URL查询字符串
:param dict: 包含键值对的字典
:return: URL编码后的查询字符串 "key1=value1&key2=value2"
"""
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
@ -218,21 +146,12 @@ def parse_dict_to_url(dict):
def get_blog_setting():
"""yrh:
获取博客的全局设置并进行缓存
如果缓存中没有则从数据库读取如果数据库中也没有首次运行
则创建一个默认的设置实例并保存到数据库
:return: `BlogSettings`对象
"""
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings
if not BlogSettings.objects.count():
# yrh:如果数据库中没有设置,创建默认设置
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
@ -248,7 +167,6 @@ def get_blog_setting():
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
# yrh:获取设置并缓存
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
@ -256,7 +174,7 @@ def get_blog_setting():
def save_user_avatar(url):
'''yrh:
'''
保存用户头像
:param url:头像url
:return: 本地路径
@ -264,37 +182,26 @@ def save_user_avatar(url):
logger.info(url)
try:
# yrh:定义头像保存的本地目录
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir)
# yrh:检查URL是否以常见的图片扩展名结尾
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
# yrh:确定文件扩展名
ext = os.path.splitext(url)[1] if isimage else '.jpg'
# yrh:生成一个唯一的文件名
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename)
# yrh:写入文件
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
# yrh:返回静态文件URL
return static('avatar/' + save_filename)
except Exception as e:
logger.error(e)
# yrh:如果发生任何错误,返回默认头像
return static('blog/img/avatar.png')
def delete_sidebar_cache():
"""yrh:
删除与侧边栏相关的所有缓存
当网站内容如文章评论链接发生变化时需要调用此函数来刷新侧边栏
"""
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
@ -303,42 +210,23 @@ def delete_sidebar_cache():
def delete_view_cache(prefix, keys):
"""yrh:
删除使用`template fragment cache`缓存的内容
:param prefix: 在模板中使用的缓存片段名称
:param keys: 一个列表包含构建缓存键所需的参数
"""
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
"""yrh:
获取静态资源的基础URL
如果`STATIC_URL`已配置则直接使用否则根据当前站点域名构建
:return: 静态资源的基础URL字符串
"""
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
# yrh:定义在清理HTML时允许保留的HTML标签和属性
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
def sanitize_html(html):
"""yrh:
使用bleach库清理HTML只保留允许的标签和属性防止XSS攻击
:param html: 需要清理的原始HTML字符串
:return: 清理后的安全HTML字符串
"""
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

File diff suppressed because it is too large Load Diff

@ -7,20 +7,10 @@ For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
# yrh:导入操作系统相关模块,用于设置环境变量
import os
# yrh:从Django核心模块中导入获取WSGI应用的函数
# yrh:WSGIWeb Server Gateway Interface是Python Web应用与Web服务器通信的标准接口
from django.core.wsgi import get_wsgi_application
# yrh:设置Django项目的配置模块环境变量
# yrh:"DJANGO_SETTINGS_MODULE" 是Django约定的环境变量名用于指定项目的设置文件路径
# yrh:这里指定配置文件为 "djangoblog.settings"即项目根目录下djangoblog文件夹中的settings.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
# yrh:创建WSGI应用实例
# yrh:get_wsgi_application() 函数会根据上面设置的配置模块加载项目配置并返回一个WSGI兼容的应用对象
# yrh:这个对象将被Web服务器如Gunicorn、uWSGI等调用以运行Django项目
application = get_wsgi_application()

@ -1,10 +1,15 @@
#!/usr/bin/env python
# 声明脚本解释器路径告诉系统用Python执行该脚本
import os
import sys
if __name__ == "__main__":
# 1. 核心配置设置Django项目的默认配置模块路径
# 作用让Django知道该加载哪个项目的配置如数据库、路由等
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
try:
# 2. 关键导入导入Django的命令行执行函数
# 作用:后续通过该函数解析并执行`python manage.py xxx`类命令
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the

@ -5,12 +5,12 @@ from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
logger = logging.getLogger(__name__)#徐悦然__name__ 是 Python 的一个内置变量,代表当前模块的名称。使用它可以确保日志信息能够被准确地归因于当前文件,这是一种最佳实践。
logger = logging.getLogger(__name__)
class OAuthUserAdmin(admin.ModelAdmin):#徐悦然:定义一个用于 OAuthUser 模型的 Admin 配置类。它继承自 admin.ModelAdmin这是 Django 中所有模型 Admin 配置的基类。通过创建这个类并注册到 OAuthUser 模型,你就可以完全控制该模型在 Admin 后台的显示和操作行为。
search_fields = ('nickname', 'email')#徐悦然:启用搜索功能。在 Admin 列表页的顶部会出现一个搜索框。用户可以输入关键字Django 会自动在 nickname 和 email 这两个字段中进行模糊查询。
list_per_page = 20#徐悦然:当 OAuthUser 的记录超过 20 条时Admin 会自动进行分页,每页显示 20 条记录,提高页面加载速度和可用性。
class OAuthUserAdmin(admin.ModelAdmin):
search_fields = ('nickname', 'email')
list_per_page = 20
list_display = (
'id',
'nickname',
@ -20,8 +20,8 @@ class OAuthUserAdmin(admin.ModelAdmin):#徐悦然:定义一个用于 OAuthUser
'email',
)
list_display_links = ('id', 'nickname')
list_filter = ('author', 'type',)#徐悦然:用户可以根据 author关联的本地用户和 typeOAuth 类型,如 GitHub、微博对记录进行快速筛选。
readonly_fields = []#徐悦然:定义在详情页中哪些字段是只读的(不可编辑的)。
list_filter = ('author', 'type',)
readonly_fields = []
def get_readonly_fields(self, request, obj=None):
return list(self.readonly_fields) + \
@ -29,7 +29,7 @@ class OAuthUserAdmin(admin.ModelAdmin):#徐悦然:定义一个用于 OAuthUser
[field.name for field in obj._meta.many_to_many]
def has_add_permission(self, request):
return False#徐悦然:重写此方法以完全禁止在 Admin 中创建新的 OAuthUser 记录。
return False
def link_to_usermodel(self, obj):
if obj.author:
@ -37,10 +37,10 @@ class OAuthUserAdmin(admin.ModelAdmin):#徐悦然:定义一个用于 OAuthUser
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))#徐悦然:这是一个自定义的列表显示方法,用于在列表页将 author 字段显示为一个可点击的链接,直接跳转到关联的本地用户的 Admin 修改页面。
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
def show_user_image(self, obj):
img = obj.picture#徐悦然obj: 代表当前行的 OAuthUser 实例。obj.author: 获取与 OAuthUser 关联的本地 User 对象。
img = obj.picture
return format_html(
u'<img src="%s" style="width:50px;height:50px"></img>' %
(img))
@ -51,4 +51,4 @@ class OAuthUserAdmin(admin.ModelAdmin):#徐悦然:定义一个用于 OAuthUser
class OAuthConfigAdmin(admin.ModelAdmin):
list_display = ('type', 'appkey', 'appsecret', 'is_enable')
list_filter = ('type',)#徐悦然:这是一个相对简单的 Admin 配置类,用于管理 OAuthConfig 模型。
list_filter = ('type',)

@ -2,4 +2,4 @@ from django.apps import AppConfig
class OauthConfig(AppConfig):
name = 'oauth'#徐悦然name 属性是 AppConfig 中唯一必须的属性。
name = 'oauth'

@ -2,15 +2,11 @@ from django.contrib.auth.forms import forms
from django.forms import widgets
class RequireEmailForm(forms.Form):#徐悦然:定义一个名为 RequireEmailForm 的表单类。它继承自 Django 的 forms.Form这是所有非模型绑定表单即不直接与数据库模型关联的表单的基类。这个表单的核心目的是获取用户的电子邮箱地址。
email = forms.EmailField(label='电子邮箱', required=True)#徐悦然:定义一个 email 字段。forms.EmailField: 这是一个专门用于验证电子邮件地址格式的字段。它会自动检查输入内容是否符合标准的 email 格式(例如,包含 @ 符号和域名。label='电子邮箱': 设置该字段在 HTML 表单中显示的标签label为 “电子邮箱”。required=True: 标记该字段为必填项。如果用户未填写,表单提交时会报错。
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)#徐悦然forms.IntegerField: 用于接收一个整数值。
#徐悦然widget=forms.HiddenInput: 关键属性。它将这个字段渲染为一个隐藏的 HTML 输入元素 (<input type="hidden">)。用户在页面上看不到这个字段,但它的值会随着表单一起提交。
#徐悦然required=False: 该字段不是必填的。
class RequireEmailForm(forms.Form):
email = forms.EmailField(label='电子邮箱', required=True)
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
def __init__(self, *args, **kwargs):#徐悦然super(RequireEmailForm, self).__init__(*args, **kwargs): 首先调用父类 forms.Form 的构造函数,确保表单的正常初始化。这是重写方法时的标准做法。
#徐悦然:重写 __init__ 方法的主要目的是在表单实例化时,对其字段进行一些动态修改。
def __init__(self, *args, **kwargs):
super(RequireEmailForm, self).__init__(*args, **kwargs)
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})#徐悦然placeholder="email": 在输入框中显示灰色的提示文本 “email”。
#徐悦然class="form-control": 这是一个 Bootstrap 框架的 CSS 类,用于将输入框设置为统一的、美观的样式。这表明该项目可能在使用 Bootstrap 进行前端开发。
attrs={'placeholder': "email", "class": "form-control"})

@ -8,50 +8,50 @@ import django.utils.timezone
class Migration(migrations.Migration):
initial = True#徐悦然: Django 这是一个 “初始迁移”
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]#徐悦然定义了当前迁移所依赖的其他迁移。在执行当前迁移之前Django 必须先执行完所有依赖的迁移。
]
operations = [#徐悦然包含了当前迁移要执行的具体数据库操作。Django 会按顺序执行这些操作。这里的操作是 CreateModel用于创建新的数据表。
operations = [
migrations.CreateModel(
name='OAuthConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),#徐悦然:一个自增的大整数类型(适合数据量较大的表)。#徐悦然:标识此字段为主键。
('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),#徐悦然:定义了一个下拉选择框的选项,('数据库存储值', '后台显示的友好名称')。
('appkey', models.CharField(max_length=200, verbose_name='AppKey')),#徐悦然:是一个比较安全的长度,足以容纳大多数服务商提供的密钥
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),
('appkey', models.CharField(max_length=200, verbose_name='AppKey')),
('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')),
('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')),#徐悦然:用于存储 OAuth 授权成功后的回调地址。
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),#徐悦然:表示新添加的配置默认是启用的。
('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],#徐悦然:这是两个常见的审计字段,用于记录记录的创建和最后修改时间。
],
options={
'verbose_name': 'oauth配置',
'verbose_name_plural': 'oauth配置',#徐悦然:定义了在 Django Admin 中显示的模型名称(单数和复数)。
'ordering': ['-created_time'],#徐悦然:指定了在查询该模型时的默认排序方式。['-created_time'] 表示按创建时间降序排列,即最新创建的记录排在最前面。
'verbose_name_plural': 'oauth配置',
'ordering': ['-created_time'],
},
),
migrations.CreateModel(
name='OAuthUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('openid', models.CharField(max_length=50)),#徐悦然:存储用户在第三方 OAuth 服务商平台上的唯一标识符OpenID
('nickname', models.CharField(max_length=50, verbose_name='昵称')),#徐悦然: 存储用户在第三方平台的昵称。
('token', models.CharField(blank=True, max_length=150, null=True)),#徐悦然:存储 OAuth 访问令牌Access Token
('picture', models.CharField(blank=True, max_length=350, null=True)),#徐悦然: 存储用户在第三方平台的头像图片 URL。
('type', models.CharField(max_length=50)),#徐悦然:标识该记录关联的 OAuth 服务商类型(如 'github', 'weibo')。
('email', models.CharField(blank=True, max_length=50, null=True)),#徐悦然:存储从第三方平台获取的用户邮箱。
('metadata', models.TextField(blank=True, null=True)),#徐悦然:使用 TextField 可以存储任意长度的文本,通常会将复杂的信息(如用户的详细资料)序列化为 JSON 字符串后存入。
('openid', models.CharField(max_length=50)),
('nickname', models.CharField(max_length=50, verbose_name='昵称')),
('token', models.CharField(blank=True, max_length=150, null=True)),
('picture', models.CharField(blank=True, max_length=350, null=True)),
('type', models.CharField(max_length=50)),
('email', models.CharField(blank=True, max_length=50, null=True)),
('metadata', models.TextField(blank=True, null=True)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],#徐悦然:定义了级联删除行为。如果本地用户被删除,那么关联的这个 OAuthUser 记录也会被一起删除,这通常是合理的。
],
options={
'verbose_name': 'oauth用户',
'verbose_name_plural': 'oauth用户',
'ordering': ['-created_time'],
},
),
]
]

@ -14,7 +14,7 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterModelOptions(#徐悦然:主要是修改排序字段和显示名称。
migrations.AlterModelOptions(
name='oauthconfig',
options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'},
),
@ -22,7 +22,7 @@ class Migration(migrations.Migration):
name='oauthuser',
options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'},
),
migrations.RemoveField(#徐悦然:删除了旧的时间戳字段。
migrations.RemoveField(
model_name='oauthconfig',
name='created_time',
),
@ -38,7 +38,7 @@ class Migration(migrations.Migration):
model_name='oauthuser',
name='last_mod_time',
),
migrations.AddField(#徐悦然:添加了新的时间戳字段。
migrations.AddField(
model_name='oauthconfig',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
@ -58,7 +58,7 @@ class Migration(migrations.Migration):
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AlterField(#徐悦然修改了多个现有字段的默认值、verbose_name 等属性。
migrations.AlterField(
model_name='oauthconfig',
name='callback_url',
field=models.CharField(default='', max_length=200, verbose_name='callback url'),
@ -83,4 +83,4 @@ class Migration(migrations.Migration):
name='nickname',
field=models.CharField(max_length=50, verbose_name='nickname'),
),
]
]

@ -1,18 +1,18 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations, models#徐悦然: 导入 Django 迁移模块和模型字段模块,这是所有迁移文件都必需的。
from django.db import migrations, models
class Migration(migrations.Migration):#徐悦然: 定义迁移类,所有迁移逻辑都在这个类中。
class Migration(migrations.Migration):
dependencies = [
('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'),
]#徐悦然:它依赖于 oauth 应用中名为 0002_alter_oauthconfig_options_alter_oauthuser_options_and_more 的迁移。这意味着 Django 必须先执行完 0002_... 迁移,才能执行当前这个迁移(假设当前是 0003_...)。这是因为我们要修改的 OAuthUser 模型及其 nickname 字段,正是在之前的迁移(很可能是 0001_initial 或 0002_...)中被创建或最后修改的。
]
operations = [
migrations.AlterField(
model_name='oauthuser',
name='nickname',#徐悦然:它将该字段在 Django Admin 界面等地方显示的名称从之前的 'nickname'(一个单词)修改为了 'nick name'(两个单词,中间有空格)。
name='nickname',
field=models.CharField(max_length=50, verbose_name='nick name'),
),
]

@ -26,11 +26,9 @@ class OAuthUser(models.Model):
def __str__(self):
return self.nickname
class Meta:#徐悦然class Meta: 这是 Django 模型中一个非常重要的内部类,用于定义模型的元数据和行为。
class Meta:
verbose_name = _('oauth user')
verbose_name_plural = verbose_name#徐悦然:它们用于定义模型在 Django Admin 界面等地方显示的友好名称(单数和复数)。
verbose_name_plural = verbose_name
ordering = ['-creation_time']
@ -51,11 +49,11 @@ class OAuthConfig(models.Model):
blank=False,
default='')
is_enable = models.BooleanField(
_('is enable'), default=True, blank=False, null=False)#徐悦然:用户首次通过 OAuth 登录时,系统可以先创建一个 OAuthUser 记录(存储其 openid 等信息然后引导用户注册或绑定已有账号。在此过程完成前author 字段可以为 null。
_('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):#徐悦然: 这是 Django 模型的一个内置方法用于在模型实例被保存save())之前进行自定义验证。
def clean(self):
if OAuthConfig.objects.filter(
type=self.type).exclude(id=self.id).count():
raise ValidationError(_(self.type + _('already exists')))
@ -66,4 +64,4 @@ class OAuthConfig(models.Model):
class Meta:
verbose_name = 'oauth配置'
verbose_name_plural = verbose_name
ordering = ['-creation_time']
ordering = ['-creation_time']

@ -9,6 +9,95 @@ import requests
from djangoblog.utils import cache_decorator
from oauth.models import OAuthUser, OAuthConfig
logger = logging.getLogger(__name__)
class OAuthAccessTokenException(Exception):
'''
oauth授权失败异常
'''
class BaseOauthManager(metaclass=ABCMeta):
"""获取用户授权"""
AUTH_URL = None
"""获取token"""
TOKEN_URL = None
"""获取用户信息"""
API_URL = None
'''icon图标名'''
ICON_NAME = None
def __init__(self, access_token=None, openid=None):
self.access_token = access_token
self.openid = openid
@property
def is_access_token_set(self):
return self.access_token is not None
@property
def is_authorized(self):
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='/'):
pass
@abstractmethod
def get_access_token_by_code(self, code):
pass
@abstractmethod
def get_oauth_userinfo(self):
pass
@abstractmethod
def get_picture(self, metadata):
pass
def do_get(self, url, params, headers=None):
rsp = requests.get(url=url, params=params, headers=headers)
logger.info(rsp.text)
return rsp.text
def do_post(self, url, params, headers=None):
rsp = requests.post(url, params, headers=headers)
logger.info(rsp.text)
return rsp.text
def get_config(self):
value = OAuthConfig.objects.filter(type=self.ICON_NAME)
return value[0] if value else None
class WBOauthManager(BaseOauthManager):
AUTH_URL = 'https://api.weibo.com/oauth2/authorize'
TOKEN_URL = 'https://api.weibo.com/oauth2/access_token'
API_URL = 'https://api.weibo.com/2/users/show.json'
ICON_NAME = 'weibo'
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(
WBOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, nexturl='/'):
params = {
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': self.callback_url + '&next_url=' + nexturl
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
def get_access_token_by_code(self, code):
params = {
'client_id': self.client_id,
@ -88,7 +177,7 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
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 '' # 徐悦然:调用父类构造函数(注意:这里会先调用 ProxyManagerMixin 的构造函数)
self.callback_url = config.callback_url if config else ''
super(
GoogleOauthManager,
self).__init__(
@ -100,7 +189,7 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': self.callback_url,
'scope': 'openid email', # 徐悦然:请求的权限范围
'scope': 'openid email',
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
@ -178,7 +267,7 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': f'{self.callback_url}&next_url={next_url}',
'scope': 'user'# 徐悦然:请求 user 权限
'scope': 'user'
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
@ -192,7 +281,7 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
'redirect_uri': self.callback_url
}
rsp = self.do_post(self.TOKEN_URL, params) #徐悦然: GitHub 返回的是表单编码的字符串,需要解析
rsp = self.do_post(self.TOKEN_URL, params)
from urllib import parse
r = parse.parse_qs(rsp)
@ -202,7 +291,7 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
else:
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self): #徐悦然: GitHub API 需要在请求头中携带令牌
def get_oauth_userinfo(self):
rsp = self.do_get(self.API_URL, params={}, headers={
"Authorization": "token " + self.access_token
@ -230,7 +319,7 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth'# 徐悦然Facebook 授权页面
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'
@ -251,7 +340,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
'client_id': self.client_id,
'response_type': 'code',
'redirect_uri': self.callback_url,
'scope': 'email,public_profile' # 徐悦然:请求的权限范围
'scope': 'email,public_profile'
}
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
return url
@ -259,7 +348,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
def get_access_token_by_code(self, code):
params = {
'client_id': self.client_id,
'client_secret': self.client_secret, #徐悦然: Facebook 不需要 grant_type 参数
'client_secret': self.client_secret,
# 'grant_type': 'authorization_code',
'code': code,
@ -278,7 +367,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
def get_oauth_userinfo(self):
params = {
'access_token': self.access_token,
'fields': 'id,name,picture,email' #徐悦然:指定需要获取的字段
'fields': 'id,name,picture,email'
}
try:
rsp = self.do_get(self.API_URL, params)
@ -290,7 +379,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
user.token = self.access_token
user.metadata = rsp
if 'email' in datas and datas['email']:
user.email = datas['email']# 徐悦然:处理头像 URLFacebook 返回的结构较深)
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
@ -304,11 +393,11 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
class QQOauthManager(BaseOauthManager):
AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize' # 徐悦然QQ 授权页面
TOKEN_URL = 'https://graph.qq.com/oauth2.0/token'# 徐悦然:获取令牌 URL
API_URL = 'https://graph.qq.com/user/get_user_info'# 徐悦然:获取用户信息 URL
OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' # 徐悦然:获取 OpenID 的 URL
ICON_NAME = 'qq'# 徐悦然:图标名称
AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize'
TOKEN_URL = 'https://graph.qq.com/oauth2.0/token'
API_URL = 'https://graph.qq.com/user/get_user_info'
OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me'
ICON_NAME = 'qq'
def __init__(self, access_token=None, openid=None):
config = self.get_config()
@ -337,9 +426,9 @@ class QQOauthManager(BaseOauthManager):
'client_secret': self.client_secret,
'code': code,
'redirect_uri': self.callback_url
} # 徐悦然QQ 使用 GET 请求获取令牌
}
rsp = self.do_get(self.TOKEN_URL, params)
if rsp:#徐悦然: 解析响应(表单编码格式)
if rsp:
d = urllib.parse.parse_qs(rsp)
if 'access_token' in d:
token = d['access_token']
@ -354,7 +443,7 @@ class QQOauthManager(BaseOauthManager):
'access_token': self.access_token
}
rsp = self.do_get(self.OPEN_ID_URL, params)
if rsp:# 徐悦然QQ 返回的是 callback 包裹的 JSON需要处理
if rsp:
rsp = rsp.replace(
'callback(', '').replace(
')', '').replace(
@ -393,23 +482,23 @@ class QQOauthManager(BaseOauthManager):
@cache_decorator(expiration=100 * 60)
def get_oauth_apps():#徐悦然:查询所有启用的 OAuth 配置
def get_oauth_apps():
configs = OAuthConfig.objects.filter(is_enable=True).all()
if not configs:
return []# 徐悦然:获取配置的类型列表
configtypes = [x.type for x in configs] # 徐悦然:获取所有 OAuth 管理器子类
applications = BaseOauthManager.__subclasses__()# 徐悦然:筛选出配置中存在的 OAuth 应用
return []
configtypes = [x.type for x in configs]
applications = BaseOauthManager.__subclasses__()
apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes]
return apps
def get_manager_by_type(type):
applications = get_oauth_apps()
if applications:# 徐悦然:筛选出类型匹配的管理器
if applications:
finds = list(
filter(
lambda x: x.ICON_NAME.lower() == type.lower(),
applications))
if finds:
return finds[0]
return None
return None

@ -19,10 +19,10 @@ class OAuthConfigTest(TestCase):
def test_oauth_login_test(self):
c = OAuthConfig()
c.type = 'weibo'#徐悦然: 配置类型为微博
c.appkey = 'appkey'# 徐悦然:模拟的 appkey
c.appsecret = 'appsecret'# 徐悦然:模拟的 appsecret
c.save()# 徐悦然:保存到数据库
c.type = 'weibo'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
response = self.client.get('/oauth/oauthlogin?type=weibo')
self.assertEqual(response.status_code, 302)
@ -47,7 +47,7 @@ class OauthLoginTest(TestCase):
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
return applications# 徐悦然:返回所有平台管理器实例列表
return applications
def get_app_by_type(self, type):
for app in self.apps:
@ -149,10 +149,10 @@ class OauthLoginTest(TestCase):
})
])
def test_qq_login(self, mock_do_get):
qq_app = self.get_app_by_type('qq') # 徐悦然:生成 QQ 授权 URL
qq_app = self.get_app_by_type('qq')
assert qq_app
url = qq_app.get_authorization_url()
self.assertTrue("qq.com" in url) # 徐悦然:验证授权 URL 包含 QQ 域名
self.assertTrue("qq.com" in url)
token = qq_app.get_access_token_by_code('code')
userinfo = qq_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token')
@ -203,7 +203,7 @@ class OauthLoginTest(TestCase):
mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid"
})# 徐悦然:模拟不包含邮箱的用户信息
})
mock_user_info = {
"avatar_large": "avatar_large",
"screen_name": "screen_name1",
@ -217,7 +217,7 @@ class OauthLoginTest(TestCase):
response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302) # 徐悦然:解析重定向 URL获取 OAuthUser 的 ID用于后续绑定邮箱
self.assertEqual(response.status_code, 302)
oauth_user_id = int(response.url.split('/')[-1].split('.')[0])
self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html')
@ -226,7 +226,7 @@ class OauthLoginTest(TestCase):
self.assertEqual(response.status_code, 302)
sign = get_sha256(settings.SECRET_KEY +
str(oauth_user_id) + settings.SECRET_KEY) # 徐悦然:验证提交邮箱后,重定向到绑定成功提示页(提示查收邮件)
str(oauth_user_id) + settings.SECRET_KEY)
url = reverse('oauth:bindsuccess', kwargs={
'oauthid': oauth_user_id,
@ -238,12 +238,12 @@ class OauthLoginTest(TestCase):
'sign': sign
})
response = self.client.get(path)
self.assertEqual(response.status_code, 302)#徐悦然: 验证确认后重定向
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success')
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)
self.assertEqual(user.username, mock_user_info['screen_name'])
self.assertEqual(user.email, 'test@gmail.com')
self.assertEqual(oauth_user.pk, oauth_user_id)

@ -2,26 +2,24 @@ from django.urls import path
from . import views
app_name = "oauth" # 徐悦然OAuth 授权回调 URL
app_name = "oauth"
urlpatterns = [
path(
r'oauth/authorize',
views.authorize),# 徐悦然:对应的视图函数:处理第三方平台授权后的回调逻辑
views.authorize),
path(
r'oauth/requireemail/<int:oauthid>.html', #徐悦然: 路径包含参数 oauthid整数类型用于标识 OAuthUser 对象
views.RequireEmailView.as_view(),# 徐悦然:对应的类视图:通过 as_view() 方法转换为可调用的视图函数
name='require_email'),#徐悦然: URL 名称,用于反向解析(如 reverse('oauth:require_email', kwargs={'oauthid': 1})
),
r'oauth/requireemail/<int:oauthid>.html',
views.RequireEmailView.as_view(),
name='require_email'),
path(
r'oauth/emailconfirm/<int:id>/<sign>.html', #徐悦然:路径包含参数 oauthid整数类型用于标识 OAuthUser 对象
views.emailconfirm, # 徐悦然:对应的类视图:通过 as_view() 方法转换为可调用的视图函数
name='email_confirm'), # 徐悦然URL 名称,用于反向解析(如 reverse('oauth:require_email', kwargs={'oauthid': 1})
r'oauth/emailconfirm/<int:id>/<sign>.html',
views.emailconfirm,
name='email_confirm'),
path(
r'oauth/bindsuccess/<int:oauthid>.html',#徐悦然路径包含两个参数idOAuthUser 主键)和 sign签名用于验证链接合法性
views.bindsuccess,#徐悦然:对应的视图函数:处理邮箱绑定确认逻辑
name='bindsuccess'),#徐悦然URL 名称,用于反向解析确认链接
r'oauth/bindsuccess/<int:oauthid>.html',
views.bindsuccess,
name='bindsuccess'),
path(
r'oauth/oauthlogin',#徐悦然:路径包含参数 oauthid用于标识绑定的 OAuthUser 对象
views.oauthlogin,#徐悦然:对应的视图函数:显示绑定成功或邮件发送成功的提示信息
name='oauthlogin')]
r'oauth/oauthlogin',
views.oauthlogin,
name='oauthlogin')]

@ -34,7 +34,7 @@ def get_redirecturl(request):
p = urlparse(nexturl)
if p.netloc:
site = get_current_site().domain
if not p.netloc.replace('www.', '') == site.replace('www.', ''): #徐悦然: 比较 URL 的域名和当前站点的域名(去除 'www.' 前缀后比较,不区分 www 和非 www
if not p.netloc.replace('www.', '') == site.replace('www.', ''):
logger.info('非法url:' + nexturl)
return "/"
return nexturl
@ -67,7 +67,7 @@ def authorize(request):
return HttpResponseRedirect('/')
except Exception as e:
logger.error(e)
rsp = None # 徐悦然:捕获其他未知异常,记录错误日志并将 rsp 设为 None
rsp = None
nexturl = get_redirecturl(request)
if not rsp:
return HttpResponseRedirect(manager.get_authorization_url(nexturl))
@ -231,7 +231,7 @@ class RequireEmailView(FormView):
})
url = url + '?type=email'
return HttpResponseRedirect(url)
# 徐悦然:获取对应的 OAuthUser 对象
def bindsuccess(request, oauthid):
type = request.GET.get('type', None)
@ -246,8 +246,8 @@ def bindsuccess(request, oauthid):
content = _(
"Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s"
" to directly log in to this website without a password. You are welcome to continue to follow this site." % {
'oauthuser_type': oauthuser.type}) #徐悦然: 渲染绑定成功页面,传递标题和内容到模板
'oauthuser_type': oauthuser.type})
return render(request, 'oauth/bindsuccess.html', {
'title': title,
'content': content
})
})

@ -1,10 +0,0 @@
[run]
source = .
include = *.py
omit =
*migrations*
*tests*
*.html
*whoosh_cn_backend*
*settings.py*
*venv*

@ -1,11 +0,0 @@
bin/data/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
uploads/
settings_production.py
*.md
docs/
logs/
static/

@ -1,6 +0,0 @@
blog/static/* linguist-vendored
*.js linguist-vendored
*.css linguist-vendored
* text=auto
*.sh text eol=lf
*.conf text eol=lf

@ -1,18 +0,0 @@
<!--
如果你不认真勾选下面的内容,我可能会直接关闭你的 Issue。
提问之前,建议先阅读 https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way
-->
**我确定我已经查看了** (标注`[ ]`为`[x]`)
- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)
----
**我要申请** (标注`[ ]`为`[x]`)
- [ ] BUG 反馈
- [ ] 添加新的特性或者功能
- [ ] 请求技术支持

@ -1,47 +0,0 @@
name: "CodeQL"
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- '**/*.yml'
- '**/*.txt'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- '**/*.yml'
- '**/*.txt'
schedule:
- cron: '30 1 * * 0'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

@ -1,136 +0,0 @@
name: Django CI
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
jobs:
build-normal:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.10","3.11" ]
steps:
- name: Start MySQL
uses: samin/mysql-action@v1.3
with:
host port: 3306
container port: 3306
character set server: utf8mb4
collation server: utf8mb4_general_ci
mysql version: latest
mysql root password: root
mysql database: djangoblog
mysql user: root
mysql password: root
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
run: |
python manage.py makemigrations
python manage.py migrate
python manage.py test
build-with-es:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.10","3.11" ]
steps:
- name: Start MySQL
uses: samin/mysql-action@v1.3
with:
host port: 3306
container port: 3306
character set server: utf8mb4
collation server: utf8mb4_general_ci
mysql version: latest
mysql root password: root
mysql database: djangoblog
mysql user: root
mysql password: root
- name: Configure sysctl limits
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
- uses: miyataka/elasticsearch-github-actions@1
with:
stack-version: '7.12.1'
plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200
run: |
python manage.py makemigrations
python manage.py migrate
coverage run manage.py test
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: false
tags: djangoblog/djangoblog:dev

@ -1,43 +0,0 @@
name: docker
on:
push:
paths-ignore:
- '**/*.md'
- '**/*.yml'
branches:
- 'master'
- 'dev'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set env to docker dev tag
if: endsWith(github.ref, '/dev')
run: |
echo "DOCKER_TAG=test" >> $GITHUB_ENV
- name: Set env to docker latest tag
if: endsWith(github.ref, '/master')
run: |
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}}

@ -1,39 +0,0 @@
name: publish release
on:
release:
types: [ published ]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: name/app
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
platforms: |
linux/amd64
linux/arm64
linux/arm/v7
linux/arm/v6
linux/386
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}

@ -1,80 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.pot
# Django stuff:
*.log
logs/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# PyCharm
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
static/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
google93fd32dbd906620a.html
baidu_verify_FlHL7cUyC9.html
BingSiteAuth.xml
cb9339dbe2ff86a5aa169d28dba5f615.txt
werobot_session.*
django.jpg
uploads/
settings_production.py
werobot_session.db
bin/datas/

@ -1,17 +0,0 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python 调试程序: 当前文件",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}

@ -1,4 +0,0 @@
{
"python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": []
}

@ -1,15 +0,0 @@
FROM python:3.11
ENV PYTHONUNBUFFERED 1
WORKDIR /code/djangoblog/
RUN apt-get update && \
apt-get install default-libmysqlclient-dev gettext -y && \
rm -rf /var/lib/apt/lists/*
ADD requirements.txt requirements.txt
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir gunicorn[gevent] && \
pip cache purge
ADD . .
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]

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

@ -1,158 +0,0 @@
# DjangoBlog
<p align="center">
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml/badge.svg" alt="Django CI"></a>
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml/badge.svg" alt="CodeQL"></a>
<a href="https://codecov.io/gh/liangliangyy/DjangoBlog"><img src="https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg" alt="codecov"></a>
<a href="https://github.com/liangliangyy/DjangoBlog/blob/master/LICENSE"><img src="https://img.shields.io/github/license/liangliangyy/djangoblog.svg" alt="license"></a>
</p>
<p align="center">
<b>一款功能强大、设计优雅的现代化博客系统</b>
<br>
<a href="/docs/README-en.md">English</a><b>简体中文</b>
</p>
---
DjangoBlog 是一款基于 Python 3.10 和 Django 4.0 构建的高性能博客平台。它不仅提供了传统博客的所有核心功能还通过一个灵活的插件系统让您可以轻松扩展和定制您的网站。无论您是个人博主、技术爱好者还是内容创作者DjangoBlog 都旨在为您提供一个稳定、高效且易于维护的写作和发布环境。
## ✨ 特性亮点
- **强大的内容管理**: 支持文章、独立页面、分类和标签的完整管理。内置强大的 Markdown 编辑器,支持代码语法高亮。
- **全文搜索**: 集成搜索引擎,提供快速、精准的文章内容搜索。
- **互动评论系统**: 支持回复、邮件提醒等功能,评论内容同样支持 Markdown。
- **灵活的侧边栏**: 可自定义展示最新文章、最多阅读、标签云等模块。
- **社交化登录**: 内置 OAuth 支持,已集成 Google, GitHub, Facebook, 微博, QQ 等主流平台。
- **高性能缓存**: 原生支持 Redis 缓存,并提供自动刷新机制,确保网站高速响应。
- **SEO 友好**: 具备基础 SEO 功能,新内容发布后可自动通知 Google 和百度。
- **便捷的插件系统**: 通过创建独立的插件来扩展博客功能代码解耦易于维护。我们已经通过插件实现了文章浏览计数、SEO 优化等功能!
- **集成图床**: 内置简单的图床功能,方便图片上传和管理。
- **自动化前端**: 集成 `django-compressor`,自动压缩和优化 CSS 及 JavaScript 文件。
- **健壮的运维**: 内置网站异常邮件提醒和微信公众号管理功能。
## 🛠️ 技术栈
- **后端**: Python 3.10, Django 4.0
- **数据库**: MySQL, SQLite (可配置)
- **缓存**: Redis
- **前端**: HTML5, CSS3, JavaScript
- **搜索**: Whoosh, Elasticsearch (可配置)
- **编辑器**: Markdown (mdeditor)
## 🚀 快速开始
### 1. 环境准备
确保您的系统中已安装 Python 3.10+ 和 MySQL/MariaDB。
### 2. 克隆与安装
```bash
# 克隆项目到本地
git clone https://github.com/liangliangyy/DjangoBlog.git
cd DjangoBlog
# 安装依赖
pip install -r requirements.txt
```
### 3. 项目配置
- **数据库**:
打开 `djangoblog/settings.py` 文件,找到 `DATABASES` 配置项,修改为您的 MySQL 连接信息。
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangoblog',
'USER': 'root',
'PASSWORD': 'your_password',
'HOST': '127.0.0.1',
'PORT': 3306,
}
}
```
在 MySQL 中创建数据库:
```sql
CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
- **更多配置**:
关于邮件发送、OAuth 登录、缓存等更多高级配置,请参阅我们的 [详细配置文档](/docs/config.md)。
### 4. 初始化数据库
```bash
python manage.py makemigrations
python manage.py migrate
# 创建一个超级管理员账户
python manage.py createsuperuser
```
### 5. 运行项目
```bash
# (可选) 生成一些测试数据
python manage.py create_testdata
# (可选) 收集和压缩静态文件
python manage.py collectstatic --noinput
python manage.py compress --force
# 启动开发服务器
python manage.py runserver
```
现在,在您的浏览器中访问 `http://127.0.0.1:8000/`,您应该能看到 DjangoBlog 的首页了!
## 部署
- **传统部署**: 我们为您准备了非常详细的 [服务器部署教程](https://www.lylinux.net/article/2019/8/5/58.html)。
- **Docker 部署**: 项目已全面支持 Docker。如果您熟悉容器化技术请参考 [Docker 部署文档](/docs/docker.md) 来快速启动。
- **Kubernetes 部署**: 我们也提供了完整的 [Kubernetes 部署指南](/docs/k8s.md),助您轻松上云。
## 🧩 插件系统
插件系统是 DjangoBlog 的核心特色之一。它允许您在不修改核心代码的情况下,通过编写独立的插件来为您的博客添加新功能。
- **工作原理**: 插件通过在预定义的“钩子”上注册回调函数来工作。例如,当一篇文章被渲染时,`after_article_body_get` 钩子会被触发,所有注册到此钩子的函数都会被执行。
- **现有插件**: `view_count`(浏览计数), `seo_optimizer`SEO优化等都是通过插件系统实现的。
- **开发您自己的插件**: 只需在 `plugins` 目录下创建一个新的文件夹,并编写您的 `plugin.py`。欢迎探索并为 DjangoBlog 社区贡献您的创意!
## 🤝 贡献指南
我们热烈欢迎任何形式的贡献!如果您有好的想法或发现了 Bug请随时提交 Issue 或 Pull Request。
## 📄 许可证
本项目基于 [MIT License](LICENSE) 开源。
---
## ❤️ 支持与赞助
如果您觉得这个项目对您有帮助,并且希望支持我继续维护和开发新功能,欢迎请我喝杯咖啡!您的每一份支持都是我前进的最大动力。
<p align="center">
<img src="/docs/imgs/alipay.jpg" width="150" alt="支付宝赞助">
<img src="/docs/imgs/wechat.jpg" width="150" alt="微信赞助">
</p>
<p align="center">
<i>(左) 支付宝 / (右) 微信</i>
</p>
## 🙏 鸣谢
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。
<p align="center">
<a href="https://www.jetbrains.com/?from=DjangoBlog">
<img src="/docs/imgs/pycharm_logo.png" width="150" alt="JetBrains Logo">
</a>
</p>
---
> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save