Compare commits

..

4 Commits

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,152 +1,96 @@
"""
Django管理后台用户模型配置模块
本模块配置BlogUser模型在Django管理后台的显示和编辑行为
包括自定义表单验证列表显示字段搜索过滤等功能
主要组件
- BlogUserCreationForm: 用户创建表单处理密码验证和设置
- BlogUserChangeForm: 用户信息修改表单
- BlogUserAdmin: 用户模型管理后台配置类
"""
# 导入Django表单模块
from django import forms
# 导入Django默认的用户管理类
from django.contrib.auth.admin import UserAdmin
# 导入Django默认的用户修改表单
from django.contrib.auth.forms import UserChangeForm
# 导入用户名字段
from django.contrib.auth.forms import UsernameField
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 导入自定义用户模型
# Register your models here.
# 导入自定义的用户模型
from .models import BlogUser
# 自定义用户创建表单继承自ModelForm
class BlogUserCreationForm(forms.ModelForm):
"""
博客用户创建表单
扩展自ModelForm专门用于在Django管理后台创建新用户
提供密码确认验证和密码哈希处理功能
"""
# 密码输入字段1 - 使用PasswordInput控件隐藏输入
# 密码字段1使用密码输入控件
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
# 密码输入字段2 - 用于密码确认
# 密码确认字段,使用密码输入控件
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
# 定义表单的元数据
class Meta:
"""表单元数据配置"""
# 指定关联的模型
# 指定表单对应的模型
model = BlogUser
# 指定表单包含的字段 - 仅包含email字段
# 指定表单包含的字段
fields = ('email',)
# 密码确认字段的清理和验证方法
def clean_password2(self):
"""
密码确认字段验证方法
验证两次输入的密码是否一致确保用户输入正确的密码
Returns:
str: 验证通过的密码
Raises:
forms.ValidationError: 当两次密码输入不一致时抛出验证错误
"""
# 从清洗后的数据中获取两个密码字段的值
# 从清理后的数据中获取两个密码字段的值
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
# 检查两个密码是否存在且相等
# 检查两个密码是否匹配
if password1 and password2 and password1 != password2:
# 密码不匹配时抛出验证错误
# 如果不匹配,抛出验证错误
raise forms.ValidationError(_("passwords do not match"))
# 返回验证通过的密码
# 返回确认密码的值
return password2
# 保存用户实例的方法
def save(self, commit=True):
"""
表单保存方法
重写保存逻辑处理密码哈希化和设置用户来源
Args:
commit (bool): 是否立即保存到数据库默认为True
Returns:
BlogUser: 保存后的用户实例
"""
# 调用父类保存方法,但不立即提交到数据库
# 调用父类的save方法但不立即提交到数据库
user = super().save(commit=False)
# 使用Django的密码哈希方法设置密码
# 设置用户的密码(会自动进行哈希处理)
user.set_password(self.cleaned_data["password1"])
# 如果设置为立即提交,则保存用户并设置来源
# 如果设置为立即提交
if commit:
# 标记用户创建来源为管理后台
# 设置用户来源为管理员站点
user.source = 'adminsite'
# 保存用户到数据库
user.save()
# 返回用户实例
return user
# 自定义用户修改表单继承自Django的UserChangeForm
class BlogUserChangeForm(UserChangeForm):
"""
博客用户信息修改表单
继承自Django的UserChangeForm用于在管理后台编辑现有用户信息
保持与Django原生用户管理表单的兼容性
"""
# 定义表单的元数据
class Meta:
"""表单元数据配置"""
# 指定关联的模型
# 指定表单对应的模型
model = BlogUser
# 包含所有字段
fields = '__all__'
# 指定用户名字段使用Django的UsernameField类型
# 指定字段类型映射
field_classes = {'username': UsernameField}
# 初始化方法
def __init__(self, *args, **kwargs):
"""
表单初始化方法
可以在此处添加自定义的表单初始化逻辑
"""
# 调用父类初始化方法
# 调用父类的初始化方法
super().__init__(*args, **kwargs)
# 自定义用户管理类继承自Django的UserAdmin
class BlogUserAdmin(UserAdmin):
"""
博客用户管理后台配置类
继承自Django的UserAdmin自定义BlogUser模型在管理后台的显示和行为
配置列表显示搜索排序等管理界面功能
"""
# 指定用户编辑表单
# 指定修改用户时使用的表单
form = BlogUserChangeForm
# 指定用户创建表单
# 指定创建用户时使用的表单
add_form = BlogUserCreationForm
# 配置列表页面显示的字段
# 定义在管理列表页面显示的字段
list_display = (
'id', # 用户ID
'nickname', # 用户昵称
'username', # 用户名
'email', # 邮箱地址
'last_login', # 最后登录时间
'date_joined', # 注册时间
'source' # 用户来源
)
# 配置列表中可点击跳转到编辑页面的字段
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
# 定义可以作为链接点击进入编辑页面的字段
list_display_links = ('id', 'username')
# 配置默认排序规则 - 按ID倒序排列最新的在前
# 定义默认排序字段按id倒序
ordering = ('-id',)
# 配置搜索框可搜索的字段
# 定义可搜索的字段
search_fields = ('username', 'nickname', 'email')

@ -1,37 +1,8 @@
"""
Django应用配置模块
本模块定义accounts应用的配置类用于配置应用级别的设置和元数据
"""
# 导入Django的应用配置基类
from django.apps import AppConfig
# 定义accounts应用的配置类
class AccountsConfig(AppConfig):
"""
用户账户应用配置类
继承自Django的AppConfig类用于配置accounts应用的各项设置
包括应用名称显示名称初始化逻辑等
属性:
name (str): 应用的Python路径标识符Django使用此名称来识别应用
"""
# 应用名称 - 使用Python路径格式Django通过此名称识别应用
# 此名称必须与应用的目录名和INSTALLED_APPS中的配置一致
name = 'accounts'
# 可选应用的可读名称用于Django管理后台显示
# verbose_name = '用户账户'
# 可选:应用初始化方法
# def ready(self):
# """
# 应用初始化完成时调用的方法
#
# 当Django启动完成所有应用加载完毕后会自动调用此方法。
# 常用于信号注册、配置检查等初始化操作。
# """
# # 导入并注册信号处理器
# from . import signals
# 指定应用的Python路径Django内部使用的标识
name = 'accounts'

@ -1,274 +1,166 @@
"""
用户认证表单模块
本模块定义用户相关的Django表单包括
- 用户登录表单
- 用户注册表单
- 密码重置表单
- 验证码表单
所有表单都包含Bootstrap样式类提供一致的用户界面体验
"""
# 导入Django表单模块
from django import forms
# 导入Django认证相关函数和表单
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
# 导入验证异常类
from django.core.exceptions import ValidationError
# 导入表单控件
from django.forms import widgets
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 导入自定义工具模块
from . import utils
# 导入自定义用户模型
from .models import BlogUser
# 登录表单继承自Django的AuthenticationForm
class LoginForm(AuthenticationForm):
"""
用户登录表单
继承自Django的AuthenticationForm添加Bootstrap样式支持
用于用户通过用户名和密码登录系统
"""
def __init__(self, *args, **kwargs):
"""
初始化表单设置字段的widget属性添加Bootstrap样式
Args:
*args: 可变位置参数
**kwargs: 可变关键字参数
"""
# 调用父类初始化方法
# 调用父类的初始化方法
super(LoginForm, self).__init__(*args, **kwargs)
# 设置用户名字段的输入控件和样式
# 自定义用户名字段的控件属性
self.fields['username'].widget = widgets.TextInput(
attrs={
'placeholder': "username", # 输入框占位符文本
"class": "form-control" # Bootstrap表单控件样式类
})
# 设置密码字段的输入控件和样式
attrs={'placeholder': "username", "class": "form-control"})
# 自定义密码字段的控件属性
self.fields['password'].widget = widgets.PasswordInput(
attrs={
'placeholder': "password", # 输入框占位符文本
"class": "form-control" # Bootstrap表单控件样式类
})
attrs={'placeholder': "password", "class": "form-control"})
# 注册表单继承自Django的UserCreationForm
class RegisterForm(UserCreationForm):
"""
用户注册表单
继承自Django的UserCreationForm扩展邮箱字段和样式支持
用于新用户注册账号包含用户名邮箱和密码确认功能
"""
def __init__(self, *args, **kwargs):
"""
初始化表单设置所有字段的widget属性添加Bootstrap样式
Args:
*args: 可变位置参数
**kwargs: 可变关键字参数
"""
# 调用父类初始化方法
# 调用父类的初始化方法
super(RegisterForm, self).__init__(*args, **kwargs)
# 设置用户名字段的输入控件和样式
# 自定义用户名字段的控件属性
self.fields['username'].widget = widgets.TextInput(
attrs={
'placeholder': "username", # 输入框占位符文本
"class": "form-control" # Bootstrap表单控件样式类
})
# 设置邮箱字段的输入控件和样式
attrs={'placeholder': "username", "class": "form-control"})
# 自定义邮箱字段的控件属性
self.fields['email'].widget = widgets.EmailInput(
attrs={
'placeholder': "email", # 输入框占位符文本
"class": "form-control" # Bootstrap表单控件样式类
})
# 设置密码字段的输入控件和样式
attrs={'placeholder': "email", "class": "form-control"})
# 自定义密码字段的控件属性
self.fields['password1'].widget = widgets.PasswordInput(
attrs={
'placeholder': "password", # 输入框占位符文本
"class": "form-control" # Bootstrap表单控件样式类
})
# 设置密码确认字段的输入控件和样式
attrs={'placeholder': "password", "class": "form-control"})
# 自定义确认密码字段的控件属性
self.fields['password2'].widget = widgets.PasswordInput(
attrs={
'placeholder': "repeat password", # 输入框占位符文本
"class": "form-control" # Bootstrap表单控件样式类
})
attrs={'placeholder': "repeat password", "class": "form-control"})
# 邮箱字段的清理和验证方法
def clean_email(self):
"""
邮箱字段验证方法
验证邮箱是否已被注册确保邮箱地址的唯一性
Returns:
str: 验证通过的邮箱地址
Raises:
ValidationError: 当邮箱已被注册时抛出验证错误
"""
# 获取清洗后的邮箱数据
# 获取清理后的邮箱数据
email = self.cleaned_data['email']
# 检查邮箱是否已存在
if get_user_model().objects.filter(email=email).exists():
# 抛出验证错误,提示邮箱已存在
# 如果邮箱已存在,抛出验证错误
raise ValidationError(_("email already exists"))
# 返回验证通过的邮箱
return email
# 定义表单的元数据
class Meta:
"""表单元数据配置"""
# 指定关联的用户模型
# 指定表单对应的模型使用get_user_model获取当前用户模型
model = get_user_model()
# 指定表单包含的字段
# 指定表单包含的字段
fields = ("username", "email")
# 忘记密码表单继承自forms.Form
class ForgetPasswordForm(forms.Form):
"""
忘记密码重置表单
用于用户通过邮箱和验证码重置密码包含密码强度验证和验证码校验
"""
# 新密码字段1
# 新密码字段
new_password1 = forms.CharField(
label=_("New password"), # 字段标签
widget=forms.PasswordInput(
widget=forms.PasswordInput( # 使用密码输入控件
attrs={
"class": "form-control", # Bootstrap样式类
"class": "form-control", # CSS类名
'placeholder': _("New password") # 占位符文本
}
),
)
# 新密码字段2 - 用于密码确认
# 确认新密码字段
new_password2 = forms.CharField(
label="确认密码", # 中文标签
widget=forms.PasswordInput(
label="确认密码", # 字段标签(中文)
widget=forms.PasswordInput( # 使用密码输入控件
attrs={
"class": "form-control", # Bootstrap样式类
"class": "form-control", # CSS类名
'placeholder': _("Confirm password") # 占位符文本
}
),
)
# 邮箱字段 - 用于标识用户和发送验证码
# 邮箱字段
email = forms.EmailField(
label='邮箱', # 中文标签
widget=forms.TextInput(
label='邮箱', # 字段标签(中文)
widget=forms.TextInput( # 使用文本输入控件
attrs={
'class': 'form-control', # Bootstrap样式类
'class': 'form-control', # CSS类名
'placeholder': _("Email") # 占位符文本
}
),
)
# 验证码字段 - 用于验证用户身份
# 验证码字段
code = forms.CharField(
label=_('Code'), # 字段标签
widget=forms.TextInput(
widget=forms.TextInput( # 使用文本输入控件
attrs={
'class': 'form-control', # Bootstrap样式类
'class': 'form-control', # CSS类名
'placeholder': _("Code") # 占位符文本
}
),
)
# 确认密码字段的清理和验证方法
def clean_new_password2(self):
"""
密码确认字段验证方法
验证两次输入的密码是否一致并检查密码强度
Returns:
str: 验证通过的密码
Raises:
ValidationError: 当密码不匹配或强度不足时抛出验证错误
"""
# 从请求数据中获取两个密码字段的值
# 从原始数据中获取两个密码字段的值
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
# 检查两个密码是否存在且相等
# 检查两个密码是否匹配
if password1 and password2 and password1 != password2:
# 密码不匹配时抛出验证错误
# 如果不匹配,抛出验证错误
raise ValidationError(_("passwords do not match"))
# 使用Django内置密码验证器验证密码强度
# 使用Django的密码验证器验证密码强度
password_validation.validate_password(password2)
# 返回验证通过的密码
return password2
# 邮箱字段的清理和验证方法
def clean_email(self):
"""
邮箱字段验证方法
验证邮箱是否在系统中注册过
Returns:
str: 验证通过的邮箱地址
Raises:
ValidationError: 当邮箱未注册时抛出验证错误
"""
# 获取清洗后的邮箱数据
# 获取清理后的邮箱数据
user_email = self.cleaned_data.get("email")
# 检查邮箱是否存在于用户数据库中
if not BlogUser.objects.filter(email=user_email).exists():
# TODO: 这里会暴露邮箱是否注册的信息,根据安全需求可修改提示
# 检查邮箱是否存在对应的用户
if not BlogUser.objects.filter(
email=user_email
).exists():
# 如果邮箱不存在,抛出验证错误
# 注释说明:这里的报错提示可能会暴露邮箱是否注册,可根据安全需求修改
raise ValidationError(_("email does not exist"))
# 返回验证通过的邮箱
return user_email
# 验证码字段的清理和验证方法
def clean_code(self):
"""
验证码字段验证方法
验证邮箱和验证码的匹配关系
Returns:
str: 验证通过的验证码
Raises:
ValidationError: 当验证码无效或过期时抛出验证错误
"""
# 获取清洗后的验证码数据
# 获取清理后的验证码数据
code = self.cleaned_data.get("code")
# 调用utils模块的verify函数验证验证码
# 使用工具函数验证验证码的有效性
error = utils.verify(
email=self.cleaned_data.get("email"), # 邮箱地址
code=code, # 验证码
email=self.cleaned_data.get("email"), # 传入邮箱
code=code, # 传入验证码
)
# 如果验证返回错误信息,抛出验证错误
# 如果验证返回错误信息
if error:
# 抛出验证错误
raise ValidationError(error)
# 返回验证通过的验证码
return code
# 忘记密码验证码请求表单继承自forms.Form
class ForgetPasswordCodeForm(forms.Form):
"""
忘记密码验证码请求表单
用于用户请求发送密码重置验证码仅包含邮箱字段
"""
# 邮箱字段 - 用于发送验证码
# 邮箱字段
email = forms.EmailField(
label=_('Email'), # 字段标签
# 可以添加widget配置来设置样式
)

@ -1,18 +1,6 @@
"""
用户账户应用数据库迁移文件
本迁移文件由Django自动生成用于创建自定义用户模型的数据库表结构
扩展了Django内置的AbstractUser模型添加了博客系统特有的用户字段
生成的表结构
- accounts_bloguser: 自定义博客用户表继承Django用户认证系统的所有功能
迁移依赖
- 依赖于Django auth应用的group和permission模型
"""
# Generated by Django 4.1.7 on 2023-03-02 07:14
# 导入Django内置模块
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
@ -20,118 +8,65 @@ import django.utils.timezone
class Migration(migrations.Migration):
"""
用户账户应用初始迁移类
继承自migrations.Migration定义自定义用户模型的数据库表创建操作
initial = True 表示这是该应用的第一个迁移文件
主要功能
- 创建自定义用户模型BlogUser的数据库表
- 继承Django认证系统的所有基础字段
- 添加博客系统特有的自定义字段
- 设置模型的管理器和配置选项
"""
# 标记为初始迁移文件Django迁移系统会首先执行此文件
# 初始迁移标记
initial = True
# 定义迁移依赖关系
# 依赖的迁移文件
dependencies = [
# 声明对Django认证系统组的依赖
# 使用auth应用的0012迁移文件确保用户权限系统正常工作
('auth', '0012_alter_user_first_name_max_length'),
]
# 定义迁移操作
# 迁移操作列表
operations = [
# 创建博客用户表的迁移操作
# 创建模型的操作
migrations.CreateModel(
# 模型名称 - 对应数据库表名 accounts_bloguser
name='BlogUser',
# 定义模型字段列表
name='BlogUser', # 自定义用户模型名称
fields=[
# 主键字段 - 使用BigAutoField作为自增主键
# 主键ID自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 密码字段 - Django认证系统标准字段存储加密后的密码
# 密码字段使用Django的密码哈希存储
('password', models.CharField(max_length=128, verbose_name='password')),
# 最后登录时间字段 - 记录用户最后一次登录的时间
# 最后登录时间,可为空
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
# 超级用户标志字段 - 标识用户是否拥有所有权限
('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')),
# 名字字段 - 用户的名字(西方命名习惯)
# 超级用户标志位
('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')),
# 姓氏字段 - 用户的姓氏(西方命名习惯)
# 姓氏字段,可为空
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
# 邮箱字段 - 用户的电子邮箱地址
# 邮箱字段,可为空
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
# 员工状态字段 - 标识用户是否可以登录管理后台
('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')),
# 注册时间字段 - 记录用户账号创建的时间
# 管理员标志位决定能否登录admin后台
('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')),
# 昵称字段 - 博客系统自定义字段,用户显示名称
# 自定义字段:昵称
('nickname', models.CharField(blank=True, max_length=100, 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='修改时间')),
# 来源字段 - 博客系统自定义字段记录用户创建来源如注册、OAuth等
# 自定义字段:用户创建来源
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
# 组关联字段 - Django权限系统的组多对多关联
('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')),
# 权限关联字段 - Django权限系统的用户权限多对多关联
('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')),
],
# 模型元数据配置
options={
# 管理后台单数显示名称(中文)
'verbose_name': '用户',
# 管理后台复数显示名称(中文)
'verbose_name_plural': '用户',
# 默认排序规则 - 按ID倒序排列最新的记录在前
'ordering': ['-id'],
# 指定获取最新记录的字段 - 使用id字段确定最新记录
'get_latest_by': 'id',
'verbose_name': '用户', # 单数显示名称
'verbose_name_plural': '用户', # 复数显示名称
'ordering': ['-id'], # 默认按ID倒序排列
'get_latest_by': 'id', # 指定最新记录的依据字段
},
# 模型管理器
# 指定模型管理器
managers=[
# 使用Django内置的UserManager管理用户对象
# 提供create_user、create_superuser等用户管理方法
# 使用Django默认的用户管理器
('objects', django.contrib.auth.models.UserManager()),
],
),

@ -1,14 +1,3 @@
"""
用户账户应用数据库迁移文件 - 字段优化更新
本迁移文件对初始用户模型进行字段优化和国际化改进
- 重命名时间字段使用更清晰的英文命名
- 更新字段显示名称统一使用英文verbose_name
- 移除冗余字段优化数据库结构
这是对0001_initial迁移的后续更新依赖于初始迁移创建的表结构
"""
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
@ -16,85 +5,56 @@ import django.utils.timezone
class Migration(migrations.Migration):
"""
用户模型字段优化迁移类
对BlogUser模型进行字段级别的优化和改进
- 标准化字段命名约定
- 改进国际化支持
- 优化时间字段的语义清晰度
此迁移依赖于accounts应用的0001_initial迁移文件
"""
# 定义迁移依赖关系 - 依赖于本应用的初始迁移
# 声明本迁移依赖的前一个迁移文件
dependencies = [
# 依赖accounts应用的第一个迁移文件确保BlogUser表已创建
('accounts', '0001_initial'),
('accounts', '0001_initial'), # 依赖于accounts应用中的初始迁移
]
# 定义迁移操作序列 - 按顺序执行以下数据库变更
# 定义本迁移要执行的操作序列
operations = [
# 修改模型选项 - 更新管理后台显示名称
# 修改模型的元选项配置
migrations.AlterModelOptions(
name='bloguser',
name='bloguser', # 指定要修改的模型名称
options={
# 指定获取最新记录的字段 - 保持使用id字段
'get_latest_by': 'id',
# 保持默认排序规则 - 按ID倒序排列
'ordering': ['-id'],
# 更新单数显示名称为英文
'verbose_name': 'user',
# 更新复数显示名称为英文
'verbose_name_plural': 'user'
'get_latest_by': 'id', # 指定获取最新记录的依据字段为id
'ordering': ['-id'], # 设置默认排序为按id倒序排列
'verbose_name': 'user', # 设置单数形式的显示名称
'verbose_name_plural': 'user', # 设置复数形式的显示名称
},
),
# 移除字段 - 删除created_time字段
# 该字段功能被creation_time字段替代
# 删除模型中的字段:创建时间字段
migrations.RemoveField(
model_name='bloguser',
name='created_time',
model_name='bloguser', # 指定要删除字段的模型
name='created_time', # 要删除的字段名称
),
# 移除字段 - 删除last_mod_time字段
# 该字段功能被last_modify_time字段替代
# 删除模型中的字段:最后修改时间字段
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
model_name='bloguser', # 指定要删除字段的模型
name='last_mod_time', # 要删除的字段名称
),
# 添加新字段 - 创建时间字段(新命名)
# 添加新的字段:创建时间(重命名字段)
migrations.AddField(
model_name='bloguser',
name='creation_time',
# 使用DateTimeField存储完整的时间戳
# default参数使用Django的时区感知当前时间
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
model_name='bloguser', # 指定要添加字段的模型
name='creation_time', # 新字段名称
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # 日期时间字段,默认值为当前时间
),
# 添加新字段 - 最后修改时间字段(新命名)
# 添加新的字段:最后修改时间(重命名字段)
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
# 使用DateTimeField存储完整的时间戳
# default参数使用Django的时区感知当前时间
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
model_name='bloguser', # 指定要添加字段的模型
name='last_modify_time', # 新字段名称
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # 日期时间字段,默认值为当前时间
),
# 修改字段选项 - 更新昵称字段的显示名称
# 修改现有字段的属性:昵称字段
migrations.AlterField(
model_name='bloguser',
name='nickname',
# 保持字段类型和约束不变仅更新verbose_name为英文
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
model_name='bloguser', # 指定要修改字段的模型
name='nickname', # 要修改的字段名称
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), # 更新字段的显示名称
),
# 修改字段选项 - 更新来源字段的显示名称
# 修改现有字段的属性:来源字段
migrations.AlterField(
model_name='bloguser',
name='source',
# 保持字段类型和约束不变仅更新verbose_name为英文
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
model_name='bloguser', # 指定要修改字段的模型
name='source', # 要修改的字段名称
field=models.CharField(blank=True, max_length=100, verbose_name='create source'), # 更新字段的显示名称
),
]

@ -1,125 +1,56 @@
"""
自定义用户模型模块
本模块定义博客系统的自定义用户模型BlogUser扩展Django内置的AbstractUser模型
添加博客系统特有的用户字段和方法
"""
# 导入Django认证系统的抽象用户基类
from django.contrib.auth.models import AbstractUser
# 导入Django数据库模型
from django.db import models
# 导入URL反向解析函数
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
class BlogUser(AbstractUser):
"""
博客系统自定义用户模型
继承自Django的AbstractUser在标准用户模型基础上添加博客系统特有的字段
- 昵称字段
- 创建时间字段
- 最后修改时间字段
- 用户来源字段
同时提供获取用户相关URL的便捷方法
"""
# 昵称字段 - 用户的显示名称,可以为空
nickname = models.CharField(
_('nick name'), # 字段显示名称(支持国际化)
max_length=100, # 最大长度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, # 最大长度100字符
blank=True # 允许为空(非必填字段)
)
# Create your models here.
# 自定义博客用户模型继承自Django的AbstractUser
class BlogUser(AbstractUser):
# 昵称字段最大长度100字符允许为空
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)
# 用户创建来源字段最大长度100字符允许为空
source = models.CharField(_('create source'), max_length=100, blank=True)
# 获取用户详情页的绝对URL不含域名
def get_absolute_url(self):
"""
获取用户的绝对URL相对路径
用于Django的通用视图和模板中生成用户详情页链接
Returns:
str: 用户详情页的URL路径
Example:
>>> user.get_absolute_url()
'/author/admin/'
"""
# 使用reverse函数通过URL名称和参数生成URL路径
return reverse(
'blog:author_detail', # URL配置的名称
kwargs={
'author_name': self.username # URL参数作者用户名
})
'blog:author_detail', kwargs={
'author_name': self.username})
# 定义对象的字符串表示形式
def __str__(self):
"""
对象字符串表示方法
定义模型实例在Django管理后台和shell中的显示内容
Returns:
str: 用户的邮箱地址
"""
return self.email
# 获取用户详情页的完整URL包含域名
def get_full_url(self):
"""
获取用户的完整URL包含域名
生成包含协议和域名的完整用户详情页URL用于外部链接
Returns:
str: 完整的用户详情页URL
Example:
>>> user.get_full_url()
'https://example.com/author/admin/'
"""
# 获取当前站点的域名
site = get_current_site().domain
# 生成完整的URL包含HTTPS协议和域名
url = "https://{site}{path}".format(
site=site, # 站点域名
path=self.get_absolute_url() # 相对路径
)
# 构建完整的URL
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
# 定义模型的元数据
class Meta:
"""
模型元数据配置类
定义模型的数据库表配置和Django管理后台显示选项
"""
# 默认排序规则 - 按ID倒序排列最新的记录在前
# 默认按id倒序排列
ordering = ['-id']
# 管理后台单数显示名称(支持国际化)
# 单数形式的显示名称
verbose_name = _('user')
# 管理后台复数显示名称 - 使用与单数相同的名称
# 复数形式的显示名称(与单数相同)
verbose_name_plural = verbose_name
# 指定获取最新记录的字段 - 使用id字段确定最新记录1
# 指定获取最新记录的依据字段
get_latest_by = 'id'

@ -1,42 +1,30 @@
"""
用户账户应用测试模块
本模块包含用户账户相关的所有测试用例覆盖用户注册登录密码重置
邮箱验证等核心功能的测试
"""
# 导入Django测试相关模块
from django.test import Client, RequestFactory, TestCase
# 导入URL反向解析
from django.urls import reverse
# 导入时间处理工具
from django.utils import timezone
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 导入自定义模型
from accounts.models import BlogUser
from blog.models import Article, Category
# 导入自定义工具函数
from djangoblog.utils import *
# 导入当前应用的工具模块
from . import utils
class AccountTest(TestCase):
"""
用户账户功能测试类
测试用户账户相关的所有功能包括
- 用户认证和登录
- 用户注册流程
- 邮箱验证码功能
- 密码重置流程
- 权限访问控制
"""
# Create your tests here.
# 账户测试类继承自TestCase
class AccountTest(TestCase):
# 测试前的初始化方法
def setUp(self):
"""
测试初始化方法
在每个测试方法执行前运行创建测试所需的初始数据和环境
"""
# 创建测试客户端用于模拟HTTP请求
# 创建测试客户端
self.client = Client()
# 创建请求工厂,用于构建请求对象
# 创建请求工厂
self.factory = RequestFactory()
# 创建测试用户
self.blog_user = BlogUser.objects.create_user(
@ -44,33 +32,30 @@ class AccountTest(TestCase):
email="admin@admin.com",
password="12345678"
)
# 设置测试用的新密码
# 设置测试密码
self.new_test = "xxx123--="
# 测试账户验证功能
def test_validate_account(self):
"""
测试账户验证和权限功能
验证超级用户的创建登录管理后台访问和文章管理权限
"""
# 创建超级用户
# 获取当前站点域名
site = get_current_site().domain
# 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
# 从数据库获取刚创建的用户
# 获取刚创建的用户
testuser = BlogUser.objects.get(username='liangliangyy1')
# 测试用户登录功能
# 测试登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
# 断言登录成功
self.assertEqual(loginresult, True)
# 测试管理后台访问权限
# 访问管理员页面
response = self.client.get('/admin/')
# 断言页面访问成功
self.assertEqual(response.status_code, 200)
# 创建测试分类
@ -90,129 +75,112 @@ class AccountTest(TestCase):
article.status = 'p' # 发布状态
article.save()
# 测试文章管理页面访问权限
# 访问文章管理页面
response = self.client.get(article.get_admin_url())
# 断言页面访问成功
self.assertEqual(response.status_code, 200)
# 测试注册功能
def test_validate_register(self):
"""
测试用户注册完整流程
验证用户注册邮箱验证登录权限提升和文章管理的完整流程
"""
# 验证注册前用户不存在
# 断言注册前邮箱不存在
self.assertEquals(
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',
'password2': 'password123!q@wE#R$T',
})
# 验证用户已成功创建
# 断言注册后邮箱存在
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 获取新创建的用户
# 获取刚注册的用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 生成邮箱验证签名
# 生成验证签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
# 构建验证URL
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
# 访问验证结果页面
# 访问验证页面
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# 测试用户登录
# 登录新注册的用户
self.client.login(username='user1233', password='password123!q@wE#R$T')
# 提升用户权限为超级用户
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 设置为超级用户和管理员
user.is_superuser = True
user.is_staff = True
user.save()
# 清理侧边栏缓存
# 删除侧边栏缓存
delete_sidebar_cache()
# 创建测试分类
# 创建分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章
# 创建文章
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a' # 文章类型
article.status = 'p' # 发布状态
article.save()
# 测试文章管理页面访问
# 访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 测试用户登出
# 测试登出功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
# 验证登出后无法访问管理页面
# 登出后访问文章管理页面应该重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# 测试错误密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123' # 错误密码
'password': 'password123' # 错误密码
})
self.assertIn(response.status_code, [301, 302, 200])
# 验证错误密码登录后仍无法访问管理页面
# 用错误密码登录后访问文章管理页面应该重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# 测试邮箱验证码功能
def test_verify_email_code(self):
"""
测试邮箱验证码功能
验证验证码的生成发送存储和验证流程
"""
to_email = "admin@admin.com"
# 生成验证码
code = generate_code()
# 存储验证码
# 设置验证码
utils.set_code(to_email, code)
# 发送验证邮件
utils.send_verify_email(to_email, code)
# 测试正确验证码验证
# 测试正确验证码
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None) # 验证成功应返回None
self.assertEqual(err, None)
# 测试错误邮箱验证
# 测试错误邮箱
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) # 验证失败应返回错误信息字符串
self.assertEqual(type(err), str) # 返回错误信息字符串
# 测试成功发送忘记密码验证码
def test_forget_password_email_code_success(self):
"""
测试忘记密码验证码请求成功场景
验证正确邮箱地址的验证码请求处理
"""
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
@ -221,64 +189,50 @@ class AccountTest(TestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
# 测试发送忘记密码验证码失败的情况
def test_forget_password_email_code_fail(self):
"""
测试忘记密码验证码请求失败场景
验证空邮箱和错误格式邮箱的请求处理
"""
# 测试空邮箱提交
# 测试空数据
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict() # 空数据
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试错误格式邮箱提交
# 测试错误邮箱格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com") # 无效邮箱格式
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试成功重置密码
def test_forget_password_email_success(self):
"""
测试忘记密码重置成功场景
验证正确的验证码和密码重置流程
"""
# 生成并设置验证码
code = generate_code()
# 设置验证码
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=code,
)
# 提交密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302) # 重定向响应
# 应该重定向到成功页面
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
# 检查新密码是否正确设置
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
# 测试不存在的用户重置密码
def test_forget_password_email_not_user(self):
"""
测试不存在的用户密码重置
验证对不存在用户的密码重置请求处理
"""
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
@ -290,14 +244,10 @@ class AccountTest(TestCase):
data=data
)
self.assertEqual(resp.status_code, 200) # 应返回表单错误页面
self.assertEqual(resp.status_code, 200) # 应该停留在当前页面
# 测试验证码错误的情况
def test_forget_password_email_code_error(self):
"""
测试错误验证码的密码重置
验证错误验证码的密码重置请求处理
"""
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
@ -311,4 +261,4 @@ class AccountTest(TestCase):
data=data
)
self.assertEqual(resp.status_code, 200) # 应返回表单错误页面
self.assertEqual(resp.status_code, 200) # 应该停留在当前页面

@ -1,60 +1,52 @@
"""
用户账户应用URL配置模块
本模块定义用户账户相关的所有URL路由包括登录注册登出
密码重置等用户认证相关的端点
URL模式使用正则表达式和路径转换器来匹配不同的用户操作请求
"""
# 导入Django URL路由相关模块
from django.urls import path
from django.urls import re_path
# 导入视图模块
# 导入当前应用的视图模块
from . import views
# 导入自定义登录表单
from .forms import LoginForm
# 应用命名空间用于URL反向解析
# 定义应用命名空间
app_name = "accounts"
# URL模式列表定义请求URL与视图的映射关系
# 定义URL模式列表
urlpatterns = [
# 用户登录URL
re_path(r'^login/$', # 匹配 /login/ 路径
# 使用类视图,设置登录成功后的重定向URL为首页
# 登录URL,使用正则表达式匹配
re_path(r'^login/$',
# 使用类视图,设置登录成功后跳转到首页
views.LoginView.as_view(success_url='/'),
name='login', # URL名称,用于反向解析
# 传递额外参数,指定使用自定义登录表单
name='login', # URL名称
# 传递额外参数,指定认证表单为自定义的LoginForm
kwargs={'authentication_form': LoginForm}),
# 用户注册URL
re_path(r'^register/$', # 匹配 /register/ 路径
# 使用类视图,设置注册成功后的重定向URL为首页
# 注册URL,使用正则表达式匹配
re_path(r'^register/$',
# 使用类视图,设置注册成功后跳转到首页
views.RegisterView.as_view(success_url="/"),
name='register'), # URL名称,用于反向解析
name='register'), # URL名称
# 用户登出URL
re_path(r'^logout/$', # 匹配 /logout/ 路径
# 使用类视图,处理用户登出逻辑
# 登出URL,使用正则表达式匹配
re_path(r'^logout/$',
# 使用类视图
views.LogoutView.as_view(),
name='logout'), # URL名称,用于反向解析
name='logout'), # URL名称
# 账户操作结果页面URL
path(r'account/result.html', # 匹配 /account/result.html 路径
# 使用函数视图,显示账户操作结果(如注册成功、验证结果等)
# 账户结果页面URL使用path匹配精确路径
path(r'account/result.html',
# 使用函数视图
views.account_result,
name='result'), # URL名称,用于反向解析
name='result'), # URL名称
# 忘记密码页面URL
re_path(r'^forget_password/$', # 匹配 /forget_password/ 路径
# 使用类视图,处理密码重置请求
# 忘记密码URL,使用正则表达式匹配
re_path(r'^forget_password/$',
# 使用类视图
views.ForgetPasswordView.as_view(),
name='forget_password'), # URL名称,用于反向解析
name='forget_password'), # URL名称
# 忘记密码验证码请求URL
re_path(r'^forget_password_code/$', # 匹配 /forget_password_code/ 路径
# 使用类视图,处理发送密码重置验证码的请求
# 忘记密码验证码请求URL,使用正则表达式匹配
re_path(r'^forget_password_code/$',
# 使用类视图
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'), # URL名称,用于反向解析
name='forget_password_code'), # URL名称
]

@ -1,91 +1,42 @@
"""
自定义用户认证后端模块
本模块提供扩展的用户认证功能支持使用用户名或邮箱进行登录
扩展了Django标准的ModelBackend认证后端
"""
# 导入获取用户模型的函数
from django.contrib.auth import get_user_model
# 导入Django认证后端基类
from django.contrib.auth.backends import ModelBackend
# 自定义认证后端,允许使用邮箱或用户名登录
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义用户认证后端 - 支持用户名或邮箱登录
继承自Django的ModelBackend扩展认证功能
- 允许用户使用用户名或邮箱地址进行登录
- 自动检测输入的是用户名还是邮箱格式
- 保持与Django原生认证系统的兼容性
使用场景
当用户输入包含'@'符号时系统将其识别为邮箱进行认证
否则将其识别为用户名进行认证
允许使用用户名或邮箱登录
"""
# 用户认证方法
def authenticate(self, request, username=None, password=None, **kwargs):
"""
用户认证方法
重写认证逻辑支持通过用户名或邮箱进行用户身份验证
Args:
request: HttpRequest对象包含请求信息
username (str): 用户输入的用户名或邮箱地址
password (str): 用户输入的密码
**kwargs: 其他关键字参数
Returns:
User: 认证成功的用户对象
None: 认证失败返回None
Example:
>>> backend = EmailOrUsernameModelBackend()
>>> # 使用用户名认证
>>> user = backend.authenticate(request, username='admin', password='password')
>>> # 使用邮箱认证
>>> user = backend.authenticate(request, username='admin@example.com', password='password')
"""
# 判断输入的是邮箱还是用户名
# 判断输入的用户名是否包含@符号(即是否为邮箱格式)
if '@' in username:
# 如果包含'@'符号,按邮箱处理
# 如果是邮箱格式使用email字段进行查询
kwargs = {'email': username}
else:
# 否则按用户名处理
# 如果不是邮箱格式使用username字段进行查询
kwargs = {'username': username}
try:
# 根据用户名或邮箱查找用户
# 根据用户名或邮箱获取用户对象
user = get_user_model().objects.get(**kwargs)
# 验证密码是否正确
if user.check_password(password):
# 密码验证成功,返回用户对象
# 密码正确,返回用户对象
return user
# 捕获用户不存在的异常
except get_user_model().DoesNotExist:
# 用户不存在返回None表示认证失败
# 用户不存在返回None
return None
# 根据用户ID获取用户对象的方法
def get_user(self, username):
"""
根据用户ID获取用户对象
重写用户获取方法通过用户ID主键获取用户实例
Args:
username (int/str): 用户的ID主键值
Returns:
User: 对应的用户对象
None: 用户不存在时返回None
Note:
这里的参数名username实际上是用户ID这是为了保持与父类接口一致
"""
try:
# 根据主键用户ID查找用户
# 根据主键用户ID获取用户对象
return get_user_model().objects.get(pk=username)
# 捕获用户不存在的异常
except get_user_model().DoesNotExist:
# 用户不存在返回None
return None

@ -1,127 +1,66 @@
"""
邮箱验证码工具模块
本模块提供邮箱验证码的生成发送验证和缓存管理功能
用于用户注册密码重置等需要邮箱验证的场景
主要功能
- 发送验证码邮件
- 验证码的存储和读取
- 验证码有效性验证
"""
# 导入类型提示模块
import typing
# 导入时间间隔类
from datetime import timedelta
# 导入Django缓存模块
from django.core.cache import cache
# 导入国际化翻译函数
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
# 导入自定义邮件发送工具
from djangoblog.utils import send_email
# 验证码有效期配置 - 5分钟
# 定义验证码的生存时间5分钟
_code_ttl = timedelta(minutes=5)
# 发送验证邮件的函数
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""
发送验证码邮件
向指定邮箱发送包含验证码的邮件用于用户身份验证
"""发送重设密码验证码
Args:
to_mail (str): 接收邮件的邮箱地址
code (str): 要发送的验证码
subject (str): 邮件主题默认为"Verify Email"
Example:
>>> send_verify_email("user@example.com", "123456")
# 向user@example.com发送验证码123456
to_mail: 接受邮箱
subject: 邮件主题
code: 验证码
"""
# 构建邮件HTML内容包含验证码信息
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
# 调用邮件发送工具发送邮件
# 调用邮件发送函数
send_email([to_mail], subject, html_content)
# 验证验证码的函数
def verify(email: str, code: str) -> typing.Optional[str]:
"""
验证验证码是否有效
检查用户输入的验证码与缓存中存储的是否一致并验证有效性
"""验证code是否有效
Args:
email (str): 用户邮箱地址作为缓存键
code (str): 用户输入的验证码
Returns:
typing.Optional[str]:
- None: 验证码正确且有效
- str: 错误信息字符串验证码错误或无效
Note:
当前错误处理方式不够合理应该使用异常抛出机制
这样调用方可以通过try-except处理错误而不是检查返回值
Example:
>>> result = verify("user@example.com", "123456")
>>> if result:
>>> print(f"验证失败: {result}")
>>> else:
>>> print("验证成功")
email: 请求邮箱
code: 验证码
Return:
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
# 从缓存中获取该邮箱对应的验证码
cache_code = get_code(email)
# 比较用户输入的验证码与缓存中的验证码
# 比较输入的验证码和缓存中的验证码
if cache_code != code:
# 验证码不匹配,返回错误信息
# 如果不匹配,返回错误信息
return gettext("Verification code error")
# 验证成功返回None
# 设置验证码到缓存的函数
def set_code(email: str, code: str):
"""
设置验证码到缓存
将验证码存储到Django缓存系统中并设置有效期
Args:
email (str): 邮箱地址作为缓存键
code (str): 要存储的验证码
Example:
>>> set_code("user@example.com", "123456")
# 将验证码123456存储到缓存键为"user@example.com"
"""
# 使用Django缓存系统存储验证码设置5分钟有效期
"""设置code"""
# 将验证码存入缓存设置过期时间为_code_ttl定义的秒数
cache.set(email, code, _code_ttl.seconds)
# 从缓存获取验证码的函数
def get_code(email: str) -> typing.Optional[str]:
"""
从缓存中获取验证码
根据邮箱地址从缓存中获取对应的验证码
Args:
email (str): 邮箱地址作为缓存键
Returns:
typing.Optional[str]:
- str: 找到的验证码
- None: 验证码不存在或已过期
Example:
>>> code = get_code("user@example.com")
>>> if code:
>>> print(f"验证码是: {code}")
>>> else:
>>> print("验证码不存在或已过期")
"""
# 从Django缓存系统中获取验证码
"""获取code"""
# 从缓存中获取指定邮箱的验证码
return cache.get(email)

@ -1,101 +1,88 @@
"""
用户账户视图模块
本模块包含用户账户相关的所有视图处理逻辑包括
- 用户注册登录登出
- 邮箱验证
- 密码重置
- 验证码发送
使用类视图和函数视图结合的方式处理用户认证流程
"""
# 导入日志模块
import logging
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 导入Django配置
from django.conf import settings
# 导入Django认证相关模块
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
# 导入HTTP响应类
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
# 导入快捷函数
from django.shortcuts import get_object_or_404
from django.shortcuts import render
# 导入URL反向解析
from django.urls import reverse
# 导入方法装饰器
from django.utils.decorators import method_decorator
# 导入URL安全验证工具
from django.utils.http import url_has_allowed_host_and_scheme
# 导入基于类的视图
from django.views import View
# 导入视图装饰器
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
# 导入自定义工具函数
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
# 导入当前应用的工具模块
from . import utils
# 导入自定义表单
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
# 导入用户模型
from .models import BlogUser
# 配置日志记录
# 获取日志
logger = logging.getLogger(__name__)
class RegisterView(FormView):
"""
用户注册视图
处理新用户注册流程包括表单验证用户创建邮箱验证邮件发送等
"""
# Create your views here.
# 注册视图继承自FormView
class RegisterView(FormView):
# 指定使用的表单类
form_class = RegisterForm
# 指定注册页面模板
# 指定模板文件
template_name = 'account/registration_form.html'
# 使用CSRF保护装饰器
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""
请求分发方法添加CSRF保护装饰器
确保注册请求受到CSRF保护防止跨站请求伪造攻击
"""
# 调用父类的dispatch方法
return super(RegisterView, self).dispatch(*args, **kwargs)
# 表单验证通过后的处理
def form_valid(self, form):
"""
表单验证通过后的处理逻辑
创建新用户发送验证邮件重定向到结果页面
Args:
form: 验证通过的注册表单实例
Returns:
HttpResponseRedirect: 重定向到结果页面
"""
# 检查表单是否有效
if form.is_valid():
# 创建用户但不立即保存到数据库
# 保存用户但不提交到数据库commit=False
user = form.save(False)
# 设置用户为非激活状态,等待邮箱验证
# 设置用户为非活跃状态(需要邮箱验证)
user.is_active = False
# 记录用户来源为注册页面
# 设置用户来源为注册
user.source = 'Register'
# 保存用户到数据库
user.save(True)
# 获取当前站点域名
site = get_current_site().domain
# 生成邮箱验证签名
# 生成验证签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 调试模式下使用本地地址
# 如果是调试模式,使用本地地址
if settings.DEBUG:
site = '127.0.0.1:8000'
# 构建验证URL
# 获取结果页面的URL路径
path = reverse('account:result')
# 构建完整的验证URL
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
@ -110,7 +97,6 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# 发送验证邮件
send_email(
emailto=[
@ -119,217 +105,168 @@ class RegisterView(FormView):
title='验证您的电子邮箱',
content=content)
# 重定向到注册结果页面
# 构建注册成功重定向URL
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
# 重定向到结果页面
return HttpResponseRedirect(url)
else:
# 表单验证失败,重新渲染表单页面
# 表单无效,重新渲染表单页面
return self.render_to_response({
'form': form
})
# 登出视图继承自RedirectView
class LogoutView(RedirectView):
"""
用户登出视图
处理用户登出逻辑清理会话和缓存
"""
# 登出后重定向到的URL
# 设置登出后重定向的URL
url = '/login/'
# 使用永不缓存装饰器
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""
请求分发方法添加不缓存装饰器
确保登出页面不会被浏览器缓存
"""
return super(LogoutView, self).dispatch(request, *args, **kwargs)
# 处理GET请求
def get(self, request, *args, **kwargs):
"""
处理GET请求的登出逻辑
执行用户登出操作清理侧边栏缓存
"""
# 执行用户登出
# 执行登出操作
logout(request)
# 清理侧边栏缓存
# 删除侧边栏缓存
delete_sidebar_cache()
# 调用父类方法进行重定向
# 调用父类的GET方法进行重定向
return super(LogoutView, self).get(request, *args, **kwargs)
# 登录视图继承自FormView
class LoginView(FormView):
"""
用户登录视图
处理用户登录认证支持记住登录状态功能
"""
# 指定使用的表单类
form_class = LoginForm
# 指定登录页面模板
# 指定模板文件
template_name = 'account/login.html'
# 登录成功后的默认重定向URL
# 设置登录成功后的默认重定向URL
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)
# 使用多个装饰器保护视图
@method_decorator(sensitive_post_parameters('password')) # 敏感参数保护
@method_decorator(csrf_protect) # CSRF保护
@method_decorator(never_cache) # 永不缓存
def dispatch(self, request, *args, **kwargs):
"""
请求分发方法添加安全装饰器
- sensitive_post_parameters: 保护密码参数
- csrf_protect: CSRF保护
- never_cache: 禁止缓存
"""
return super(LoginView, self).dispatch(request, *args, **kwargs)
# 获取上下文数据
def get_context_data(self, **kwargs):
"""
获取模板上下文数据
添加重定向URL到上下文
"""
# 从GET参数获取重定向URL
# 获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
# 如果没有重定向URL使用默认首页
if redirect_to is None:
redirect_to = '/'
# 将重定向URL添加到上下文
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
# 表单验证通过后的处理
def form_valid(self, form):
"""
表单验证通过后的处理逻辑
执行用户登录认证处理记住登录状态
"""
# 使用Django认证表单进行验证
# 创建认证表单实例
form = AuthenticationForm(data=self.request.POST, request=self.request)
# 检查表单是否有效
if form.is_valid():
# 清理侧边栏缓存
# 删除侧边栏缓存
delete_sidebar_cache()
# 记录日志
logger.info(self.redirect_field_name)
# 执行用户登录
# 执行登录操作
auth.login(self.request, form.get_user())
# 处理记住登录状态
# 如果用户选择"记住我",设置会话过期时间
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
# 调用父类的form_valid方法进行重定向
return super(LoginView, self).form_valid(form)
else:
# 登录失败,重新渲染登录页面
# 表单无效,重新渲染表单页面
return self.render_to_response({
'form': form
})
# 获取成功登录后的重定向URL
def get_success_url(self):
"""
获取登录成功后的重定向URL
验证重定向URL的安全性防止开放重定向攻击
"""
# 从POST数据获取重定向URL
# 从POST数据中获取重定向URL
redirect_to = self.request.POST.get(self.redirect_field_name)
# 验证URL是否安全同源策略
# 验证重定向URL是否安全
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
# 如果不安全使用默认成功URL
redirect_to = self.success_url
return redirect_to
# 账户结果页面视图函数
def account_result(request):
"""
账户操作结果页面视图
显示注册结果或处理邮箱验证
Args:
request: HTTP请求对象
Returns:
HttpResponse: 结果页面响应
"""
# 获取操作类型和用户ID
# 获取结果类型
type = request.GET.get('type')
# 获取用户ID
id = request.GET.get('id')
# 获取用户对象,不存在返回404
# 获取用户对象如果不存在返回404
user = get_object_or_404(get_user_model(), id=id)
# 记录日志
logger.info(type)
# 如果用户已激活,重定向到首页
# 如果用户已经是活跃状态,重定向到首页
if user.is_active:
return HttpResponseRedirect('/')
# 处理注册和验证类型
if type and type in ['register', 'validation']:
if type == 'register':
# 注册成功页面
# 注册成功的内容
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# 邮箱验证处理
# 生成验证签名
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 获取请求中的签名
sign = request.GET.get('sign')
# 验证签名
# 验证签名是否匹配
if sign != c_sign:
return HttpResponseForbidden()
# 激活用户账
# 激活用户账
user.is_active = True
user.save()
# 验证成功的内容
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
# 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
# 无效类型,重定向到首页
# 其他情况重定向到首页
return HttpResponseRedirect('/')
# 忘记密码视图继承自FormView
class ForgetPasswordView(FormView):
"""
忘记密码重置视图
处理用户密码重置请求验证验证码并更新密码
"""
# 指定使用的表单类
form_class = ForgetPasswordForm
# 指定模板名称
# 指定模板文件
template_name = 'account/forget_password.html'
# 表单验证通过后的处理
def form_valid(self, form):
"""
表单验证通过后的处理逻辑
重置用户密码并重定向到登录页面
"""
# 检查表单是否有效
if form.is_valid():
# 根据邮箱查找用户
# 根据邮箱获取用户对象
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# 使用新密码的哈希值更新用户密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
@ -338,32 +275,21 @@ class ForgetPasswordView(FormView):
# 重定向到登录页面
return HttpResponseRedirect('/login/')
else:
# 表单验证失败,重新渲染表单
# 表单无效,重新渲染表单页面
return self.render_to_response({'form': form})
# 忘记密码验证码发送视图继承自View
class ForgetPasswordEmailCode(View):
"""
忘记密码验证码发送视图
处理密码重置验证码的发送请求
"""
# 处理POST请求
def post(self, request: HttpRequest):
"""
处理POST请求发送密码重置验证码
Args:
request: HTTP请求对象
Returns:
HttpResponse: 操作结果响应
"""
# 验证表单数据
# 创建表单实例
form = ForgetPasswordCodeForm(request.POST)
# 检查表单是否有效
if not form.is_valid():
# 表单无效,返回错误信息
return HttpResponse("错误的邮箱")
# 获取邮箱地址
to_email = form.cleaned_data["email"]
@ -371,7 +297,8 @@ class ForgetPasswordEmailCode(View):
code = generate_code()
# 发送验证邮件
utils.send_verify_email(to_email, code)
# 验证码到缓存
# 存验证码到缓存
utils.set_code(to_email, code)
# 返回成功响应
return HttpResponse("ok")

@ -0,0 +1,377 @@
# analyze_models_final.py
import os
import sys
import django
import json
def setup_django():
"""配置Django环境"""
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, current_dir)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoblog.settings')
django.setup()
print("✅ Django环境配置成功")
def serialize_field_default(default_value):
"""序列化字段的默认值"""
if callable(default_value):
return f"<function {default_value.__name__}>"
elif hasattr(default_value, '__class__'):
return str(default_value)
else:
return default_value
def comprehensive_model_analysis():
"""全面分析Django模型"""
from django.apps import apps
from django.db import models
all_models = apps.get_models()
print("🔍 Django Blog 数据模型全面分析")
print("=" * 80)
# 按应用分组
apps_data = {}
for model in all_models:
app_label = model._meta.app_label
if app_label not in apps_data:
apps_data[app_label] = []
model_info = {
'name': model.__name__,
'db_table': model._meta.db_table,
'fields': [],
'relationships': [],
'meta': {
'managed': model._meta.managed,
'abstract': model._meta.abstract,
}
}
# 分析字段
for field in model._meta.fields:
try:
field_info = {
'name': field.name,
'type': field.get_internal_type(),
'primary_key': getattr(field, 'primary_key', False),
'unique': getattr(field, 'unique', False),
'null': getattr(field, 'null', False),
'blank': getattr(field, 'blank', False),
'default': serialize_field_default(field.default) if hasattr(field,
'default') and field.default != models.NOT_PROVIDED else None,
'max_length': getattr(field, 'max_length', None),
'related_model': field.related_model.__name__ if field.related_model else None,
'relationship': None
}
# 确定关系类型
if field.is_relation:
if field.many_to_one:
field_info['relationship'] = 'ForeignKey'
elif field.one_to_one:
field_info['relationship'] = 'OneToOneField'
model_info['fields'].append(field_info)
# 添加到关系列表
if field.related_model:
model_info['relationships'].append({
'type': field_info['relationship'],
'target': field.related_model.__name__,
'field': field.name
})
except Exception as e:
print(f"⚠️ 分析字段 {field.name} 时出错: {e}")
continue
# 分析多对多字段
for field in model._meta.many_to_many:
try:
field_info = {
'name': field.name,
'type': 'ManyToManyField',
'primary_key': False,
'unique': False,
'null': getattr(field, 'null', False),
'blank': getattr(field, 'blank', False),
'related_model': field.related_model.__name__ if field.related_model else None,
}
model_info['fields'].append(field_info)
if field.related_model:
model_info['relationships'].append({
'type': 'ManyToManyField',
'target': field.related_model.__name__,
'field': field.name
})
except Exception as e:
print(f"⚠️ 分析多对多字段 {field.name} 时出错: {e}")
continue
apps_data[app_label].append(model_info)
return apps_data
def generate_markdown_documentation(apps_data):
"""生成Markdown格式的详细文档"""
content = [
"# Django Blog 数据模型设计文档",
"",
"## 1. 系统概述",
"",
"本文档详细描述了Django Blog项目的数据库模型设计。",
"",
"## 2. 模型统计",
"",
f"系统共包含 **{sum(len(models) for models in apps_data.values())}** 个数据模型,分布在 **{len(apps_data)}** 个应用中。",
"",
"### 应用列表",
""
]
# 应用统计
for app_label, models in apps_data.items():
content.append(f"- **{app_label}**: {len(models)} 个模型")
content.extend([
"",
"## 3. 核心实体关系图",
"",
"```mermaid",
"erDiagram",
""
])
# 生成Mermaid ER图
for app_label, models in apps_data.items():
for model_info in models:
content.append(f" {model_info['name']} {{")
# 显示关键字段
for field in model_info['fields'][:5]: # 只显示前5个字段避免过于复杂
if field.get('primary_key'):
content.append(f" {field['type']} {field['name']} PK")
elif field['type'] in ['CharField', 'DateTimeField', 'BooleanField', 'TextField']:
content.append(f" {field['type']} {field['name']}")
content.append(" }")
content.append("")
# 添加主要关系
relationships_added = set()
for app_label, models in apps_data.items():
for model_info in models:
for rel in model_info['relationships']:
if rel.get('type') == 'ForeignKey':
rel_str = f" {model_info['name']} ||--o{{ {rel['target']} : \"{rel['field']}\""
if rel_str not in relationships_added:
relationships_added.add(rel_str)
content.append(rel_str)
content.extend([
"```",
"",
"## 4. 详细数据字典",
""
])
# 每个模型的详细说明
for app_label, models in apps_data.items():
content.append(f"### {app_label} 应用")
content.append("")
for model_info in models:
content.append(f"#### {model_info['name']}")
content.append("")
content.append(f"- **数据库表**: `{model_info['db_table']}`")
content.append(f"- **管理方式**: {'Django管理' if model_info['meta']['managed'] else '非Django管理'}")
content.append("")
content.append("| 字段名 | 数据类型 | 约束 | 关联模型 | 说明 |")
content.append("|--------|----------|------|----------|------|")
for field in model_info['fields']:
try:
constraints = []
if field.get('primary_key'):
constraints.append("PK")
if field.get('unique'):
constraints.append("UNIQUE")
if field.get('null'):
constraints.append("NULL")
if field.get('blank'):
constraints.append("BLANK")
constraint_str = ", ".join(constraints) if constraints else ""
related_model = field.get('related_model') or ""
field_type = field.get('relationship') or field.get('type', 'Unknown')
content.append(f"| {field['name']} | {field_type} | {constraint_str} | {related_model} | |")
except Exception as e:
content.append(f"| {field.get('name', 'Unknown')} | Error | | | 字段分析错误 |")
content.append("")
# 模型关系分析
content.extend([
"## 5. 模型关系分析",
"",
"### 5.1 一对一关系 (OneToOne)",
""
])
one_to_one_rels = []
for app_label, models in apps_data.items():
for model_info in models:
for rel in model_info['relationships']:
if rel.get('type') == 'OneToOneField':
one_to_one_rels.append(f"- `{model_info['name']}.{rel['field']}` → `{rel['target']}`")
if one_to_one_rels:
content.extend(one_to_one_rels)
else:
content.append("无一对一关系")
content.extend([
"",
"### 5.2 一对多关系 (ForeignKey)",
""
])
foreign_key_rels = []
for app_label, models in apps_data.items():
for model_info in models:
for rel in model_info['relationships']:
if rel.get('type') == 'ForeignKey':
foreign_key_rels.append(f"- `{model_info['name']}.{rel['field']}` → `{rel['target']}`")
if foreign_key_rels:
content.extend(foreign_key_rels)
else:
content.append("无一对多关系")
content.extend([
"",
"### 5.3 多对多关系 (ManyToMany)",
""
])
many_to_many_rels = []
for app_label, models in apps_data.items():
for model_info in models:
for rel in model_info['relationships']:
if rel.get('type') == 'ManyToManyField':
many_to_many_rels.append(f"- `{model_info['name']}.{rel['field']}` ↔ `{rel['target']}`")
if many_to_many_rels:
content.extend(many_to_many_rels)
else:
content.append("无多对多关系")
content.extend([
"",
"## 6. 索引设计",
"",
"### 6.1 主键索引",
"- 所有模型默认使用自增ID作为主键",
"",
"### 6.2 外键索引",
"- Django自动为所有ForeignKey和OneToOneField创建索引",
"",
"### 6.3 业务索引",
"- 需要根据具体查询需求添加数据库索引",
"",
"## 7. 总结",
"",
"本文档详细描述了Django Blog项目的数据库模型设计包括",
"- 完整的模型字段定义",
"- 模型间的关联关系",
"- 数据表结构设计",
"- 索引设计建议",
"",
"该设计支持博客系统的核心功能,包括用户管理、文章发布、评论系统等。"
])
return "\n".join(content)
def main():
"""主函数"""
print("🚀 开始Django Blog数据模型分析...")
try:
# 设置Django环境
setup_django()
# 执行分析
print("📊 分析数据模型中...")
apps_data = comprehensive_model_analysis()
# 生成Markdown文档
print("📝 生成文档中...")
markdown_content = generate_markdown_documentation(apps_data)
# 保存文档
with open('数据模型设计文档.md', 'w', encoding='utf-8') as f:
f.write(markdown_content)
print("✅ 数据模型设计文档已生成: 数据模型设计文档.md")
# 同时保存JSON数据用于其他用途修复序列化问题
try:
# 创建一个可序列化的版本
serializable_data = {}
for app_label, models in apps_data.items():
serializable_data[app_label] = []
for model in models:
serializable_model = model.copy()
serializable_model['fields'] = []
for field in model['fields']:
serializable_field = field.copy()
# 确保所有值都是可序列化的
for key, value in serializable_field.items():
if callable(value):
serializable_field[key] = f"<function {value.__name__}>"
serializable_model['fields'].append(serializable_field)
serializable_data[app_label].append(serializable_model)
with open('models_analysis.json', 'w', encoding='utf-8') as f:
json.dump(serializable_data, f, indent=2, ensure_ascii=False)
print("✅ 模型分析数据已保存: models_analysis.json")
except Exception as json_error:
print(f"⚠️ JSON保存失败但文档已生成: {json_error}")
# 打印简要统计
total_models = sum(len(models) for models in apps_data.values())
total_fields = sum(sum(len(model['fields']) for model in models) for models in apps_data.values())
total_relationships = sum(sum(len(model['relationships']) for model in models) for models in apps_data.values())
print(f"\n📊 分析统计:")
print(f" 应用数量: {len(apps_data)}")
print(f" 模型数量: {total_models}")
print(f" 字段数量: {total_fields}")
print(f" 关系数量: {total_relationships}")
# 显示发现的模型
print(f"\n📁 发现的模型:")
for app_label, models in apps_data.items():
print(f" {app_label}:")
for model in models:
print(f" - {model['name']} ({len(model['fields'])} 字段)")
print(f"\n🎉 分析完成!")
print("📄 生成的文档: 数据模型设计文档.md")
except Exception as e:
print(f"❌ 分析过程中出现错误: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()

@ -1,128 +1,165 @@
# 导入Django表单模块
from django import forms
# 导入Django管理员模块
from django.contrib import admin
# 导入获取用户模型的函数
from django.contrib.auth import get_user_model
# 导入URL反向解析
from django.urls import reverse
# 导入HTML格式化工具
from django.utils.html import format_html
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
from .models import Article, Category, Tag, Links, SideBar, BlogSettings, ArticleLike, ArticleFavorite
# Register your models here.
# 导入博客模型
from .models import Article, Category, Tag, Links, SideBar, BlogSettings
# 文章表单(可扩展,比如集成富文本编辑器)
# 自定义文章表单
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
# 定义表单的元数据
class Meta:
# 指定表单对应的模型
model = Article
fields = '__all__' # 表示表单包含模型的所有字段
# 包含所有字段
fields = '__all__'
# 批量操作:发布文章
# 发布文章的管理动作函数
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p') # 将选中文章状态改为 'p'ublished
makr_article_publish.short_description = _('发布选中的文章')
queryset.update(status='p')
# 批量操作:草稿文章
# 将文章设为草稿的管理动作函数
def draft_article(modeladmin, request, queryset):
queryset.update(status='d') # 草稿状态
draft_article.short_description = _('将选中文章设为草稿')
queryset.update(status='d')
# 批量操作:关闭文章评论
# 关闭文章评论的管理动作函数
def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c') # 关闭评论
close_article_commentstatus.short_description = _('关闭文章评论')
queryset.update(comment_status='c')
# 批量操作:开放文章评论
# 打开文章评论的管理动作函数
def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o') # 开放评论
open_article_commentstatus.short_description = _('开放文章评论')
queryset.update(comment_status='o')
# 设置管理动作的显示名称
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 ArticleAdmin(admin.ModelAdmin):
list_per_page = 20 # 每页显示20条
search_fields = ('body', 'title') # 可以搜索正文和标题
# 文章模型的管理类
class ArticlelAdmin(admin.ModelAdmin):
# 每页显示20条记录
list_per_page = 20
# 搜索字段
search_fields = ('body', 'title')
# 指定使用的表单
form = ArticleForm
list_display = ( # 列表页显示的字段
'id', 'title', 'author', 'link_to_category', 'creation_time',
'views', 'like_count', 'status', 'type', 'article_order'
)
list_display_links = ('id', 'title') # 哪些字段可点击进入编辑页
list_filter = ('status', 'type', 'category') # 右侧过滤器
date_hierarchy = 'creation_time' # 按创建时间分层筛选
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] # 批量操作
raw_id_fields = ('author', 'category') # 作者和分类以 ID 输入框展示,适合外键多的情况
# 自定义方法:分类显示为可点击链接
# 列表页显示的字段
list_display = (
'id',
'title',
'author',
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
# 可作为链接点击的字段
list_display_links = ('id', 'title')
# 右侧过滤器字段
list_filter = ('status', 'type', 'category')
# 日期层次导航
date_hierarchy = 'creation_time'
# 水平选择器字段
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]
# 使用原始ID字段显示搜索框而不是下拉选择
raw_id_fields = ('author', 'category',)
# 自定义方法:显示分类链接
def link_to_category(self, obj):
# 获取分类模型的app和model信息
info = (obj.category._meta.app_label, obj.category._meta.model_name)
# 生成分类编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
# 返回HTML链接
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
link_to_category.short_description = _('分类')
# 限制文章作者只能选择超级用户
# 设置自定义方法的显示名称
link_to_category.short_description = _('category')
# 重写获取表单的方法
def get_form(self, request, obj=None, **kwargs):
form = super(ArticleAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model().objects.filter(is_superuser=True)
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):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
# 重写获取站点查看URL的方法
def get_view_on_site_url(self, obj=None):
if obj:
# 返回文章的完整URL
url = obj.get_full_url()
return url
else:
# 如果没有指定对象,返回站点域名
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
# 其它模型(如 Tag、Category、Links、SideBar、BlogSettings的 Admin 配置
# 标签模型的管理类
class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time')
# 排除的字段
exclude = ('slug', 'last_modify_time', 'creation_time')
# 分类模型的管理类
class CategoryAdmin(admin.ModelAdmin):
# 列表页显示的字段
list_display = ('name', 'parent_category', 'index')
# 排除的字段
exclude = ('slug', 'last_modify_time', 'creation_time')
# 链接模型的管理类
class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time')
# 排除的字段
exclude = ('last_modify_time', 'creation_time')
# 侧边栏模型的管理类
class SideBarAdmin(admin.ModelAdmin):
# 列表页显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
# 排除的字段
exclude = ('last_modify_time', 'creation_time')
# 博客设置模型的管理类
class BlogSettingsAdmin(admin.ModelAdmin):
pass # 博客设置后台,暂时无特殊配置
# 文章点赞管理后台类
class ArticleLikeAdmin(admin.ModelAdmin):
list_display = ('article', 'user', 'created_time')
list_filter = ('created_time',)
search_fields = ('article__title', 'user__username', 'user__email')
date_hierarchy = 'created_time'
readonly_fields = ('created_time',)
raw_id_fields = ('article', 'user')
def has_add_permission(self, request):
# 点赞记录通常由用户在前端创建,后台可选禁止添加
return True
# 自定义方法:显示文章标题和用户名
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('article', 'user')
# 文章收藏管理后台类
class ArticleFavoriteAdmin(admin.ModelAdmin):
list_display = ('article', 'user', 'created_time')
list_filter = ('created_time',)
search_fields = ('article__title', 'user__username', 'user__email')
date_hierarchy = 'created_time'
readonly_fields = ('created_time',)
raw_id_fields = ('article', 'user')
def has_add_permission(self, request):
# 收藏记录通常由用户在前端创建,后台可选禁止添加
return True
# 自定义方法:显示文章标题和用户名
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('article', 'user')
pass

@ -1,4 +1,8 @@
# 导入Django应用配置基类
from django.apps import AppConfig
# 定义blog应用的配置类
class BlogConfig(AppConfig):
name = 'blog' # 应用名称
# 指定应用的Python路径Django内部使用的标识
name = 'blog'

@ -1,39 +1,57 @@
# 导入日志模块
import logging
# 导入Django时区工具
from django.utils import timezone
# 导入自定义工具函数
from djangoblog.utils import cache, get_blog_setting
# 导入模型类
from .models import Category, Article
# 获取日志记录器
logger = logging.getLogger(__name__)
# 上下文处理器:为每个模板注入全局 SEO 和导航相关变量
# SEO上下文处理器函数
def seo_processor(requests):
# 缓存键名
key = 'seo_processor'
value = cache.get(key) # 先从缓存中读取
# 尝试从缓存获取数据
value = cache.get(key)
if value:
# 如果缓存存在,直接返回缓存数据
return value
else:
logger.info('设置处理器缓存。')
setting = get_blog_setting() # 获取博客配置
# 缓存不存在,记录日志并生成新数据
logger.info('set processor cache.')
# 获取博客设置
setting = get_blog_setting()
# 构建上下文数据字典
value = {
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'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,
'SITE_NAME': setting.site_name, # 网站名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示Google广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # Google广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 网站SEO描述
'SITE_DESCRIPTION': setting.site_description, # 网站描述
'SITE_KEYWORDS': setting.site_keywords, # 网站关键词
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # 网站基础URL
'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度
'nav_category_list': Category.objects.all(), # 导航分类列表
'nav_pages': Article.objects.filter(
type='p', # 页面类型
status='p'), # 已发布状态
'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启网站评论
'BEIAN_CODE': setting.beian_code, # 备案号
'ANALYTICS_CODE': setting.analytics_code, # 统计代码(如 Google Analytics
'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,
"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) # 缓存10小时
# 将数据存入缓存有效期10小时
cache.set(key, value, 60 * 60 * 10)
# 返回上下文数据
return value

@ -1,401 +1,245 @@
# 导入时间模块
import time
import logging
# 导入Elasticsearch客户端
import elasticsearch.client
# 导入Django配置
from django.conf import settings
# 导入Elasticsearch DSL相关类
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
logger = logging.getLogger(__name__)
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') # 是否启用 Elasticsearch
# 检查是否启用了Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
# 如果启用,则建立连接
if ELASTICSEARCH_ENABLED:
connections.create_connection(hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
# 创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
# 创建Elasticsearch客户端实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
# 创建Ingest客户端用于管道处理
c = IngestClient(es)
try:
# 检查geoip管道是否存在
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
c.put_pipeline('geoip', body='''
{
"description": "添加IP地理位置信息",
"processors": [
{ "geoip": { "field": "ip" } }
]
}
''')
# 定义 IP 地理位置信息内部文档
# 如果不存在则创建geoip管道
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip"
}
}
]
}''')
# 定义GeoIP内部文档类
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
continent_name = Keyword() # 大洲名称
country_iso_code = Keyword() # 国家ISO代码
country_name = Keyword() # 国家名称
location = GeoPoint() # 地理位置坐标
# 用户代理浏览器/设备/操作系统)相关内部类
# 定义用户代理浏览器内部文档
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
Family = Keyword() # 浏览器家族
Version = Keyword() # 浏览器版本
# 定义用户代理操作系统内部文档类继承自UserAgentBrowser
class UserAgentOS(UserAgentBrowser):
pass
# 定义用户代理设备内部文档类
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
Family = Keyword() # 设备家族
Brand = Keyword() # 设备品牌
Model = Keyword() # 设备型号
# 定义用户代理内部文档类
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
browser = Object(UserAgentBrowser, required=False) # 浏览器信息
os = Object(UserAgentOS, required=False) # 操作系统信息
device = Object(UserAgentDevice, required=False) # 设备信息
string = Text() # 原始用户代理字符串
is_bot = Boolean() # 是否为机器人
# 性能监控文档:记录每个请求的 URL、耗时、IP、用户代理等
# 定义耗时记录文档类
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long() # 请求耗时(毫秒)
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
url = Keyword() # 请求URL
time_taken = Long() # 耗时(毫秒)
log_datetime = Date() # 日志时间
ip = Keyword() # IP地址
geoip = Object(GeoIp, required=False) # GeoIP信息
useragent = Object(UserAgent, required=False) # 用户代理信息
class Index:
name = 'performance'
settings = {"number_of_shards": 1, "number_of_replicas": 0}
name = 'performance' # 索引名称
settings = {
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
}
class Meta:
doc_type = 'ElapsedTime' # 文档类型
# 文章搜索文档:用于全文检索
# 耗时记录文档管理器类
class ElaspedTimeDocumentManager:
@staticmethod
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()
@staticmethod
def delete_index():
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 删除索引忽略400和404错误
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
# 构建索引
ElaspedTimeDocumentManager.build_index()
# 创建用户代理对象
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family # 浏览器家族
ua.browser.Version = useragent.browser.version_string # 浏览器版本
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family # 操作系统家族
ua.os.Version = useragent.os.version_string # 操作系统版本
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family # 设备家族
ua.device.Brand = useragent.device.brand # 设备品牌
ua.device.Model = useragent.device.model # 设备型号
ua.string = useragent.ua_string # 原始用户代理字符串
ua.is_bot = useragent.is_bot # 是否为机器人
# 创建文档对象使用时间戳作为ID
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
1000)) # 使用当前时间戳作为文档ID
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
# 保存文档使用geoip管道处理
doc.save(pipeline="geoip")
# 定义文章文档类
class ArticleDocument(Document):
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 使用IK中文分词
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
author = Object(properties={'nickname': Text(analyzer='ik_max_word'), 'id': Integer()})
category = Object(properties={'name': Text(analyzer='ik_max_word'), 'id': Integer()})
tags = Object(properties={'name': Text(analyzer='ik_max_word'), 'id': Integer()})
pub_time = Date()
status = Text()
comment_status = Text()
type = Text()
views = Integer()
article_order = Integer()
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 正文使用IK分词器
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 标题使用IK分词器
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 作者昵称
'id': Integer() # 作者ID
})
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 分类名称
'id': Integer() # 分类ID
})
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 标签名称
'id': Integer() # 标签ID
})
pub_time = Date() # 发布时间
status = Text() # 文章状态
comment_status = Text() # 评论状态
type = Text() # 文章类型
views = Integer() # 浏览量
article_order = Integer() # 文章排序
class Index:
name = 'blog'
settings = {"number_of_shards": 1, "number_of_replicas": 0}
# 新增ArticleDocumentManager 类
class ArticleDocumentManager:
"""
ArticleDocument 管理器类
用于处理 Elasticsearch 相关的文章搜索操作
"""
def __init__(self, document_class=ArticleDocument):
self.document_class = document_class
self.es_enabled = ELASTICSEARCH_ENABLED
def search_articles(self, query, category=None, author=None, tags=None, size=10, from_index=0):
"""
搜索文章
"""
if not self.es_enabled:
# 如果 Elasticsearch 未启用,返回空结果
return [], 0
try:
search = self.document_class.search()
if query:
# 多字段匹配搜索,标题权重更高
search = search.query(
"multi_match",
query=query,
fields=[
'title^3', # 标题权重最高
'body^2', # 正文权重次之
'tags.name^2', # 标签权重
'category.name^1.5', # 分类权重
'author.nickname^1' # 作者权重
],
fuzziness="AUTO" # 自动模糊匹配
)
# 应用过滤器
if category:
search = search.filter('term', category__name=category)
if author:
search = search.filter('term', author__nickname=author)
if tags:
if isinstance(tags, str):
tags = [tags]
for tag in tags:
search = search.filter('term', tags__name=tag)
# 只搜索已发布的文章
search = search.filter('term', status='p')
# 获取总数
total = search.count()
# 应用分页
search = search[from_index:from_index + size]
# 执行搜索
response = search.execute()
return response, total
except Exception as e:
logger.error(f"Elasticsearch search failed: {e}")
return [], 0
def get_popular_articles(self, size=10):
"""
获取热门文章按浏览量排序
"""
if not self.es_enabled:
return []
try:
search = self.document_class.search()
search = search.filter('term', status='p')
search = search.sort('-views') # 按浏览量降序
search = search[:size]
response = search.execute()
return response
except Exception as e:
logger.error(f"Failed to get popular articles: {e}")
return []
def get_recent_articles(self, size=10):
"""
获取最新文章按发布时间排序
"""
if not self.es_enabled:
return []
try:
search = self.document_class.search()
search = search.filter('term', status='p')
search = search.sort('-pub_time') # 按发布时间降序
search = search[:size]
response = search.execute()
return response
except Exception as e:
logger.error(f"Failed to get recent articles: {e}")
return []
def get_articles_by_category(self, category_name, size=10):
"""
根据分类获取文章
"""
if not self.es_enabled:
return []
try:
search = self.document_class.search()
search = search.filter('term', category__name=category_name)
search = search.filter('term', status='p')
search = search.sort('-pub_time')
search = search[:size]
response = search.execute()
return response
except Exception as e:
logger.error(f"Failed to get articles by category: {e}")
return []
def get_articles_by_tag(self, tag_name, size=10):
"""
根据标签获取文章
"""
if not self.es_enabled:
return []
try:
search = self.document_class.search()
search = search.filter('term', tags__name=tag_name)
search = search.filter('term', status='p')
search = search.sort('-pub_time')
search = search[:size]
response = search.execute()
return response
except Exception as e:
logger.error(f"Failed to get articles by tag: {e}")
return []
def get_similar_articles(self, article_id, size=5):
"""
获取相似文章基于更多相似项
"""
if not self.es_enabled:
return []
try:
search = self.document_class.search()
# 使用 more_like_this 查询找到相似文章
search = search.query(
'more_like_this',
fields=['title', 'body', 'tags.name', 'category.name'],
like={"_id": article_id},
min_term_freq=1,
max_query_terms=12
)
search = search.filter('term', status='p')
search = search.exclude('term', _id=article_id) # 排除当前文章
search = search[:size]
response = search.execute()
return response
except Exception as e:
logger.error(f"Failed to get similar articles: {e}")
return []
def rebuild_index(self):
"""
重建文章索引
"""
if not self.es_enabled:
return False
try:
# 删除现有索引
self.document_class._index.delete(ignore=404)
# 创建新索引
self.document_class._index.create()
# 重新索引所有文章
self.document_class.init()
logger.info("Article index rebuilt successfully")
return True
except Exception as e:
logger.error(f"Failed to rebuild article index: {e}")
return False
def get_index_stats(self):
"""
获取索引统计信息
"""
if not self.es_enabled:
return {}
try:
from elasticsearch.client import IndicesClient
client = IndicesClient(connections.get_connection())
stats = client.stats(index='blog')
return stats
except Exception as e:
logger.error(f"Failed to get index stats: {e}")
return {}
# 创建全局实例
article_document_manager = ArticleDocumentManager()
# Elasticsearch 索引管理工具类
class ElasticsearchManager:
"""
Elasticsearch 索引管理工具类
"""
def __init__(self):
self.es_enabled = ELASTICSEARCH_ENABLED
name = 'blog' # 索引名称
settings = {
"number_of_shards": 1, # 分片数量
"number_of_replicas": 0 # 副本数量
}
def create_all_indices(self):
"""
创建所有索引
"""
if not self.es_enabled:
return False
class Meta:
doc_type = 'Article' # 文档类型
try:
# 创建文章索引
ArticleDocument.init()
# 创建性能监控索引
ElapsedTimeDocument.init()
# 文章文档管理器类
class ArticleDocumentManager():
logger.info("All Elasticsearch indices created successfully")
return True
except Exception as e:
logger.error(f"Failed to create indices: {e}")
return False
def delete_all_indices(self):
"""
删除所有索引
"""
if not self.es_enabled:
return False
try:
ArticleDocument._index.delete(ignore=404)
ElapsedTimeDocument._index.delete(ignore=404)
logger.info("All Elasticsearch indices deleted successfully")
return True
except Exception as e:
logger.error(f"Failed to delete indices: {e}")
return False
def refresh_all_indices(self):
"""
刷新所有索引
"""
if not self.es_enabled:
return False
try:
from elasticsearch.client import IndicesClient
client = IndicesClient(connections.get_connection())
client.refresh(index='_all')
logger.info("All Elasticsearch indices refreshed successfully")
return True
except Exception as e:
logger.error(f"Failed to refresh indices: {e}")
return False
def get_cluster_health(self):
"""
获取集群健康状态
"""
if not self.es_enabled:
return {}
try:
health = connections.get_connection().cluster.health()
return health
except Exception as e:
logger.error(f"Failed to get cluster health: {e}")
return {}
# 创建全局实例
elasticsearch_manager = ElasticsearchManager()
def __init__(self):
self.create_index()
def create_index(self):
# 创建文章索引
ArticleDocument.init()
def delete_index(self):
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 删除索引忽略400和404错误
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
# 将文章模型转换为文档对象
return [
ArticleDocument(
meta={
'id': article.id}, # 使用文章ID作为文档ID
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
category={
'name': article.category.name,
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()], # 转换标签列表
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
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):
# 更新文档
for doc in docs:
doc.save()

@ -1,6 +1,31 @@
# 继承 Haystack 搜索表单,自定义查询字段
# 导入日志模块
import logging
# 导入Django表单相关模块
from django import forms
# 导入Haystack搜索表单基类
from haystack.forms import SearchForm
# 获取日志记录器
logger = logging.getLogger(__name__)
# 自定义博客搜索表单继承自Haystack的SearchForm
class BlogSearchForm(SearchForm):
# 定义查询数据字段,设置为必填
querydata = forms.CharField(required=True)
# 重写搜索方法
def search(self):
# 可加入日志等处理
return super().search()
# 调用父类的搜索方法获取基础数据
datas = super(BlogSearchForm, self).search()
# 检查表单数据是否有效
if not self.is_valid():
# 如果表单无效,返回无查询结果
return self.no_query_found()
# 如果查询数据存在,记录日志
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
# 返回搜索结果数据
return datas

@ -1,30 +1,29 @@
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 导入 Elasticsearch 相关的文档模型和管理器
from blog.documents import (
ElapsedTimeDocument, # 假设是一个时间相关的文档模型
ArticleDocumentManager, # 文章的文档管理器,用于操作 Elasticsearch 中的文章索引
ElaspedTimeDocumentManager, # 时间相关的文档管理器注意疑似拼写错误应为ElapsedTime
ELASTICSEARCH_ENABLED # 全局开关,控制是否启用 Elasticsearch 功能
)
# 导入Elasticsearch相关文档和管理器
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化
# 自定义管理命令类,用于构建搜索索引
class Command(BaseCommand):
help = '构建搜索索引' # 命令的帮助信息,显示在 python manage.py help 中
# 命令的帮助信息
help = 'build search index'
# 命令的主要处理逻辑
def handle(self, *args, **options):
"""
命令的主要执行逻辑
"""
if ELASTICSEARCH_ENABLED: # 只有在启用了 Elasticsearch 的情况下才执行
# 构建 “时间” 相关的索引
# 检查Elasticsearch是否启用
if ELASTICSEARCH_ENABLED:
# 构建耗时文档的索引
ElaspedTimeDocumentManager.build_index()
# 获取并初始化 “时间” 相关的文档管理对象
# 创建耗时文档管理器实例并初始化
manager = ElapsedTimeDocument()
manager.init() # 初始化索引或相关数据
# 获取文章的文档管理器,并先删除旧索引,然后重建新的文章索引
manager.init()
# 创建文章文档管理器实例
manager = ArticleDocumentManager()
manager.delete_index() # 删除已有的文章索引
manager.rebuild() # 重建文章索引,通常包括从数据库读取数据并批量导入到 ES
# 删除现有索引
manager.delete_index()
# 重新构建索引
manager.rebuild()

@ -1,21 +1,20 @@
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 从 blog 应用中导入 Tag 和 Category 模型
# 导入博客模型
from blog.models import Tag, Category
# TODO 参数化
# 自定义管理命令类,用于构建搜索关键词
class Command(BaseCommand):
help = '构建搜索关键词' # 用于生成所有标签和分类名称,作为搜索关键词
# 命令的帮助信息
help = 'build search words'
# 命令的主要处理逻辑
def handle(self, *args, **options):
"""
获取所有标签和分类的名称去重后打印出来供后续用作搜索词库
"""
# 取出所有 Tag 的名称 和 所有 Category 的名称,放入一个集合中自动去重
datas = set([
t.name for t in Tag.objects.all() # 所有标签名称
+
[t.name for t in Category.objects.all()] # 所有分类名称
])
# 将所有关键词用换行符连接,并打印到控制台
# 构建数据集:获取所有标签名称和分类名称,并使用集合去重
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
# 将去重后的数据按行打印输出
print('\n'.join(datas))

@ -1,16 +1,18 @@
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 引入项目自定义的缓存工具模块
# 导入缓存工具
from djangoblog.utils import cache
# 自定义管理命令类,用于清除整个缓存
class Command(BaseCommand):
help = '清空全部缓存' # 一键清除应用程序中的所有缓存数据
# 命令的帮助信息
help = 'clear the whole cache'
# 命令的主要处理逻辑
def handle(self, *args, **options):
"""
调用缓存工具的 clear 方法清空缓存并输出成功提示
"""
cache.clear() # 执行缓存清理
# 输出成功信息,使用 Django 管理命令的样式输出
self.stdout.write(self.style.SUCCESS('缓存已清空\n'))
# 清除所有缓存
cache.clear()
# 输出成功信息到标准输出
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -1,62 +1,60 @@
from django.contrib.auth import get_user_model # Django 提供的获取用户模型的方法
from django.contrib.auth.hashers import make_password # 用于密码加密
# 导入获取用户模型的函数
from django.contrib.auth import get_user_model
# 导入密码哈希函数
from django.contrib.auth.hashers import make_password
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 引入文章、标签、分类模型
# 导入博客模型
from blog.models import Article, Tag, Category
# 自定义管理命令类,用于创建测试数据
class Command(BaseCommand):
help = '创建测试数据' # 用于生成一些假数据,便于前端展示和功能测试
# 命令的帮助信息
help = 'create test datas'
# 命令的主要处理逻辑
def handle(self, *args, **options):
# 创建或获取一个测试用户
# 获取或创建测试用户
user = get_user_model().objects.get_or_create(
email='test@test.com',
username='测试用户',
password=make_password('test!q@w#eTYU') # 对明文密码进行哈希加密
)[0]
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# 创建或获取一个父级分类
# 获取或创建父类目
pcategory = Category.objects.get_or_create(
name='我是父类目',
parent_category=None # 表示没有父分类,即为顶级分类
)[0]
name='我是父类目', parent_category=None)[0]
# 创建或获取一个子分类,其父分类为上面创建的 pcategory
# 获取或创建子类目
category = Category.objects.get_or_create(
name='子类目',
parent_category=pcategory
)[0]
category.save() # 保存分类对象
name='子类目', parent_category=pcategory)[0]
# 创建一个基础标签
# 保存子类目
category.save()
# 创建基础标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
# 循环创建 1~19 号文章,每篇文章都绑定到上面创建的子分类
# 循环创建20篇测试文章
for i in range(1, 20):
# 获取或创建文章
article = Article.objects.get_or_create(
category=category, # 关联分类
title='nice title ' + str(i), # 标题
body='nice content ' + str(i), # 正文内容
author=user # 作者
)[0]
# 为每篇文章创建一个独立标签,如 标签1、标签2 ...
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
# 创建新标签
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
# 将独立标签和基础标签都添加到文章的标签集合中
# 为文章添加标签
article.tags.add(tag)
article.tags.add(basetag)
article.save() # 保存文章与标签的关联关系
# 保存文章
article.save()
# 清空缓存,确保新生成的数据能及时被正确索引或展示
# 导入缓存工具
from djangoblog.utils import cache
# 清除缓存
cache.clear()
# 输出成功提示
self.stdout.write(self.style.SUCCESS('已创建测试数据 \n'))
# 输出成功信息
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -1,69 +1,69 @@
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
# 导入站点工具与蜘蛛通知工具
# 导入蜘蛛通知工具
from djangoblog.spider_notify import SpiderNotify
# 导入获取当前站点工具
from djangoblog.utils import get_current_site
# 导入博客模型
from blog.models import Article, Tag, Category
# 获取当前站点域名
# 获取当前站点域名
site = get_current_site().domain
# 自定义管理命令类用于通知百度搜索引擎URL更新
class Command(BaseCommand):
help = '通知百度收录相关 URL' # 用于将站点内的文章、标签、分类等 URL 提交给百度站长平台,加快收录
# 命令的帮助信息
help = 'notify baidu url'
# 添加命令行参数
def add_arguments(self, parser):
"""
添加命令行参数允许用户选择要通知的类型文章 / 标签 / 分类 / 全部
"""
parser.add_argument(
'data_type',
type=str,
choices=['all', 'article', 'tag', 'category'], # 可选参数值
help='article所有文章, tag所有标签, category所有分类, all全部'
)
'data_type', # 参数名称
type=str, # 参数类型
choices=[
'all', # 所有类型
'article', # 仅文章
'tag', # 仅标签
'category'], # 仅分类
help='article : all article,tag : all tag,category: all category,all: All of these') # 帮助信息
# 根据相对路径获取完整URL的方法
def get_full_url(self, path):
"""
拼接完整的 URLhttps://example.com/path/
"""
url = "https://{site}{path}".format(site=site, path=path)
return url
# 命令的主要处理逻辑
def handle(self, *args, **options):
# 获取用户传入的参数,决定要通知哪些类型的数据
data_type = options['data_type']
self.stdout.write('开始处理 %s' % data_type)
# 获取命令行参数中的数据类型
type = options['data_type']
# 输出开始处理的信息
self.stdout.write('start get %s' % type)
urls = [] # 用于存储所有需要提交的 URL
if data_type == 'article' or data_type == 'all':
# 如果是文章或全部将所有已发布status='p')的文章的完整 URL 加入列表
for article in Article.objects.filter(status='p'):
# 初始化URL列表
urls = []
# 如果类型是文章或全部获取所有已发布文章的URL
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'): # 只获取已发布的文章
urls.append(article.get_full_url())
if data_type == 'tag' or data_type == 'all':
# 如果是标签或全部,将所有标签的绝对 URL 加入列表
# 如果类型是标签或全部获取所有标签的URL
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
if data_type == 'category' or data_type == 'all':
# 如果是分类或全部,将所有分类的绝对 URL 加入列表
url = tag.get_absolute_url() # 获取标签的相对URL
urls.append(self.get_full_url(url)) # 转换为完整URL并添加到列表
# 如果类型是分类或全部获取所有分类的URL
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
url = category.get_absolute_url() # 获取分类的相对URL
urls.append(self.get_full_url(url)) # 转换为完整URL并添加到列表
# 输出即将提交的通知数量
# 输出开始通知的信息显示URL数量
self.stdout.write(
self.style.SUCCESS(
'开始通知百度收录 %d 个 URL' %
len(urls)
)
)
# 调用百度通知工具,提交所有 URL
'start notify %d urls' %
len(urls)))
# 调用百度蜘蛛通知接口批量提交URL
SpiderNotify.baidu_notify(urls)
# 提交完成提示
self.stdout.write(self.style.SUCCESS('完成通知百度收录\n'))
# 输出完成通知的信息
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -1,57 +1,82 @@
import requests # 用于发送 HTTP 请求,检测头像链接是否有效
# 导入requests库用于HTTP请求
import requests
# 导入Django管理命令基类
from django.core.management.base import BaseCommand
from django.templatetags.static import static # 用于获取 Django 的静态文件路径
# 导入静态文件URL处理
from django.templatetags.static import static
# 引入自定义工具函数和 OAuth 用户模型
from djangoblog.utils import save_user_avatar # 用于下载并保存用户头像到本地或 CDN
from oauth.models import OAuthUser # 第三方登录用户表
from oauth.oauthmanager import get_manager_by_type # 根据第三方类型获取对应的 OAuth 管理器
# 导入保存用户头像的工具函数
from djangoblog.utils import save_user_avatar
# 导入OAuth用户模型
from oauth.models import OAuthUser
# 导入根据类型获取OAuth管理器的函数
from oauth.oauthmanager import get_manager_by_type
# 自定义管理命令类,用于同步用户头像
class Command(BaseCommand):
help = '同步用户头像' # 将用户头像从第三方平台同步到本地或统一存储
# 命令的帮助信息
help = 'sync user avatar'
# 测试图片URL是否可访问的方法
def test_picture(self, url):
"""
测试头像链接是否有效返回状态码 200超时时间为 2
"""
try:
# 发送HTTP GET请求测试图片URL设置2秒超时
if requests.get(url, timeout=2).status_code == 200:
# 返回200状态码表示图片可访问
return True
except:
# 发生任何异常超时、连接错误等都返回False
pass
return False
# 命令的主要处理逻辑
def handle(self, *args, **options):
# 获取静态资源的基础路径(用于判断是否为本地静态头像)
# 获取静态文件基础URL
static_url = static("../")
users = OAuthUser.objects.all() # 获取所有的 OAuth 用户
self.stdout.write(f'开始同步 {len(users)} 个用户的头像')
# 获取所有OAuth用户
users = OAuthUser.objects.all()
# 输出开始同步的用户数量
self.stdout.write(f'开始同步{len(users)}个用户头像')
# 遍历每个用户
for u in users:
self.stdout.write(f'开始同步用户:{u.nickname}')
url = u.picture # 获取用户当前的头像链接
if url: # 如果头像链接存在
if url.startswith(static_url): # 如果头像来自本地静态资源
if self.test_picture(url): # 如果本地头像有效,则无需更新
# 输出开始同步当前用户的信息
self.stdout.write(f'开始同步:{u.nickname}')
# 获取用户当前的头像URL
url = u.picture
# 如果当前有头像URL
if url:
# 检查URL是否以静态文件路径开头
if url.startswith(static_url):
# 测试静态图片是否可访问
if self.test_picture(url):
# 如果可以访问,跳过此用户,继续下一个
continue
else: # 如果本地头像失效,则尝试通过第三方平台重新获取
if u.metadata: # 如果用户有 metadata用于识别第三方账号信息
manager = get_manager_by_type(u.type) # 获取对应平台的 OAuth 管理器
url = manager.get_picture(u.metadata) # 从第三方平台获取最新头像链接
url = save_user_avatar(url) # 下载并保存头像
else: # 如果没有 metadata则使用默认头像
else:
# 如果静态图片不可访问,检查是否有元数据
if u.metadata:
# 根据用户类型获取对应的OAuth管理器
manage = get_manager_by_type(u.type)
# 从元数据中获取新的头像URL
url = manage.get_picture(u.metadata)
# 保存新头像并返回本地URL
url = save_user_avatar(url)
else:
# 没有元数据,使用默认头像
url = static('blog/img/avatar.png')
else: # 如果头像不是来自本地静态资源,则直接尝试保存
else:
# 如果不是静态文件URL直接保存头像并返回本地URL
url = save_user_avatar(url)
else: # 如果用户没有头像链接,则使用默认头像
else:
# 如果没有头像URL使用默认头像
url = static('blog/img/avatar.png')
if url: # 如果得到了有效的头像链接
self.stdout.write(f'完成同步用户:{u.nickname},新头像链接:{url}')
u.picture = url # 更新用户头像字段
u.save() # 保存到数据库
self.stdout.write('所有用户头像同步完成')
# 如果成功获取到头像URL
if url:
# 输出同步完成的信息
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
# 更新用户的头像URL
u.picture = url
# 保存用户信息
u.save()
# 输出同步结束信息
self.stdout.write('结束同步')

@ -1,67 +1,65 @@
import time # 添加这行
# 导入日志模块
import logging
from django.conf import settings
from django.utils import timezone
# 导入时间模块
import time
logger = logging.getLogger(__name__)
# 导入IP获取工具
from ipware import get_client_ip
# 导入用户代理解析工具
from user_agents import parse
# 导入Elasticsearch相关配置和管理器
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
# 获取日志记录器
logger = logging.getLogger(__name__)
class OnlineMiddleware:
"""
在线用户中间件 - 记录每个请求的加载时间IP用户代理可选地存入 Elasticsearch
"""
# 在线中间件类,用于记录页面渲染时间和用户访问信息
class OnlineMiddleware(object):
def __init__(self, get_response=None):
# 初始化中间件保存get_response函数
self.get_response = get_response
super().__init__()
def __call__(self, request):
''' page render time '''
# 记录请求开始时间
start_time = time.time()
# 处理请求
# 调用后续中间件和视图处理请求,获取响应
response = self.get_response(request)
# 计算耗时,记录并显示到页面
duration = time.time() - start_time
# 获取客户端IP
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
# 获取用户代理
user_agent = request.META.get('HTTP_USER_AGENT', '')
# 记录日志
logger.info(
f"Request: {request.method} {request.path} - "
f"IP: {ip} - "
f"Time: {duration:.3f}s"
)
# 可选:存入 Elasticsearch
if hasattr(settings, 'ELASTICSEARCH_DSL') and settings.ELASTICSEARCH_DSL:
# 获取用户代理字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# 获取客户端IP地址
ip, _ = get_client_ip(request)
# 解析用户代理信息
user_agent = parse(http_user_agent)
# 检查响应是否为流式响应(非流式响应才能处理内容)
if not response.streaming:
try:
from blog.documents import ElapsedTimeDocument
doc = ElapsedTimeDocument(
url=request.path,
time_taken=int(duration * 1000),
log_datetime=timezone.now(),
ip=ip,
useragent={'string': user_agent}
)
doc.save()
# 计算页面渲染耗时
cast_time = time.time() - start_time
# 如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
# 将耗时转换为毫秒并保留两位小数
time_taken = round((cast_time) * 1000, 2)
# 获取请求的URL路径
url = request.path
# 导入Django时区工具
from django.utils import timezone
# 创建耗时记录文档
ElaspedTimeDocumentManager.create(
url=url, # 请求URL
time_taken=time_taken, # 耗时(毫秒)
log_datetime=timezone.now(), # 当前时间
useragent=user_agent, # 用户代理信息
ip=ip) # 客户端IP
# 在响应内容中替换占位符,显示页面加载时间
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
logger.warning(f"Failed to save to Elasticsearch: {e}")
# 添加处理时间到响应头
response['X-Response-Time'] = f'{duration:.3f}s'
return response
# 记录处理过程中的错误
logger.error("Error OnlineMiddleware: %s" % e)
def process_exception(self, request, exception):
"""处理异常"""
logger.error(f"Middleware exception: {exception}")
return None
# 返回响应
return response

@ -1,146 +1,226 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# 导入Django设置
from django.conf import settings
# 导入数据库迁移相关模块
from django.db import migrations, models
import django.db.models.deletion
# 导入时间工具
import django.utils.timezone
# 导入Markdown编辑器字段
import mdeditor.fields
class Migration(migrations.Migration):
initial = True # 表示这是第一个迁移文件
# 标记为初始迁移
initial = True
# 声明依赖的迁移
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型,通常是内置的 User 或自定义用户模型
# 依赖可交换的用户模型
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 定义迁移操作序列
operations = [
# 创建 BlogSettings 模型:网站全局配置表
# 创建网站配置模型
migrations.CreateModel(
name='BlogSettings',
fields=[
# 主键ID自增BigAutoField
('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='网站描述')),
# 网站SEO描述字段
('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': '网站配置',
},
),
# 创建 Links 模型:友情链接
# 创建友情链接模型
migrations.CreateModel(
name='Links',
fields=[
# 主键ID自增BigAutoField
('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'],
},
),
# 创建 SideBar 模型:侧边栏内容
# 创建侧边栏模型
migrations.CreateModel(
name='SideBar',
fields=[
# 主键ID自增BigAutoField
('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'],
},
),
# 创建 Tag 模型:文章标签
# 创建标签模型
migrations.CreateModel(
name='Tag',
fields=[
# 主键ID自增AutoField
('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字段用于URL
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
options={
# 模型显示名称(单数)
'verbose_name': '标签',
# 模型显示名称(复数)
'verbose_name_plural': '标签',
# 默认按名称升序排列
'ordering': ['name'],
},
),
# 创建 Category 模型:文章分类
# 创建分类模型
migrations.CreateModel(
name='Category',
fields=[
# 主键ID自增AutoField
('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字段用于URL
('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'],
},
),
# 创建 Article 模型:文章内容
# 创建文章模型
migrations.CreateModel(
name='Article',
fields=[
# 主键ID自增AutoField
('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='标题')),
# 文章正文字段使用Markdown编辑器
('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='排序,数字越大越靠前')),
# 是否显示TOC目录字段
('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'],
# 指定获取最新记录的依据字段
'get_latest_by': 'id',
},
),

@ -1,25 +1,27 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models
class Migration(migrations.Migration):
# 声明本迁移依赖的前一个迁移文件
dependencies = [
('blog', '0001_initial'), # 依赖于第一个迁移文件
('blog', '0001_initial'), # 依赖于blog应用的初始迁移
]
# 定义本迁移要执行的操作序列
operations = [
# 字段:global_footer用于存放网站公共尾部 HTML 内容(如版权信息等)
# 向BlogSettings模型添加新字段:公共尾部
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
model_name='blogsettings', # 指定要修改的模型名称
name='global_footer', # 新字段名称
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), # 文本字段,允许为空,默认值为空字符串
),
# 新增字段global_header用于存放网站公共头部 HTML 内容(如导航栏上面的内容)
# 向BlogSettings模型添加新字段公共头部
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
model_name='blogsettings', # 指定要修改的模型名称
name='global_header', # 新字段名称
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), # 文本字段,允许为空,默认值为空字符串
),
]

@ -1,18 +1,20 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models
class Migration(migrations.Migration):
# 声明本迁移依赖的前一个迁移文件
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'), # 依赖于上一个迁移
('blog', '0002_blogsettings_global_footer_and_more'), # 依赖于blog应用的第二个迁移
]
# 定义本迁移要执行的操作序列
operations = [
# 新增字段comment_need_review布尔值默认 False表示评论默认不需要审核
# 向BlogSettings模型添加新字段评论审核开关
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
model_name='blogsettings', # 指定要修改的模型名称
name='comment_need_review', # 新字段名称
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), # 布尔字段默认值为False不需要审核
),
]

@ -1,32 +1,32 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations
class Migration(migrations.Migration):
# 声明本迁移依赖的前一个迁移文件
dependencies = [
('blog', '0003_blogsettings_comment_need_review'), # 依赖于上一个迁移
('blog', '0003_blogsettings_comment_need_review'), # 依赖于blog应用的第三个迁移
]
# 定义本迁移要执行的操作序列
operations = [
# 将 analyticscode 字段重命名为 analytics_code提升代码可读性
# 重命名字段将analyticscode改为analytics_code
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
model_name='blogsettings', # 指定要修改的模型名称
old_name='analyticscode', # 原字段名称
new_name='analytics_code', # 新字段名称
),
# 将 beiancode 字段重命名为 beian_code
# 重命名字段将beiancode改为beian_code
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
model_name='blogsettings', # 指定要修改的模型名称
old_name='beiancode', # 原字段名称
new_name='beian_code', # 新字段名称
),
# 将 sitename 字段重命名为 site_name
# 重命名字段将sitename改为site_name
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
model_name='blogsettings', # 指定要修改的模型名称
old_name='sitename', # 原字段名称
new_name='site_name', # 新字段名称
),
]

@ -1,4 +1,5 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
@ -8,110 +9,355 @@ import mdeditor.fields
class Migration(migrations.Migration):
# 声明本迁移依赖的前一个迁移文件
dependencies = [
# 依赖可交换的用户模型
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
# 依赖于blog应用的第四个迁移
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
# 定义本迁移要执行的操作序列
operations = [
# 调整多个模型的 Meta 选项比如排序方式、verbose_name 等
# 修改Article模型的元选项国际化
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
# 修改Category模型的元选项国际化
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
# 修改Links模型的元选项国际化
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
# 修改Sidebar模型的元选项国际化
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
# 修改Tag模型的元选项国际化
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# 删除旧的时间字段created_time / last_mod_time
migrations.RemoveField(model_name='article', name='created_time'),
migrations.RemoveField(model_name='article', name='last_mod_time'),
migrations.RemoveField(model_name='category', name='created_time'),
migrations.RemoveField(model_name='category', name='last_mod_time'),
migrations.RemoveField(model_name='links', name='created_time'),
migrations.RemoveField(model_name='sidebar', name='created_time'),
migrations.RemoveField(model_name='tag', name='created_time'),
migrations.RemoveField(model_name='tag', name='last_mod_time'),
# 新增新的时间字段creation_time创建时间、last_modify_time最后修改时间
# 删除Article模型的created_time字段
migrations.RemoveField(
model_name='article',
name='created_time',
),
# 删除Article模型的last_mod_time字段
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
# 删除Category模型的created_time字段
migrations.RemoveField(
model_name='category',
name='created_time',
),
# 删除Category模型的last_mod_time字段
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
# 删除Links模型的created_time字段
migrations.RemoveField(
model_name='links',
name='created_time',
),
# 删除Sidebar模型的created_time字段
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
# 删除Tag模型的created_time字段
migrations.RemoveField(
model_name='tag',
name='created_time',
),
# 删除Tag模型的last_mod_time字段
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
# 向Article模型添加creation_time字段
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 向Article模型添加last_modify_time字段
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 向Category模型添加creation_time字段
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 向Category模型添加last_modify_time字段
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 向Links模型添加creation_time字段
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 向Sidebar模型添加creation_time字段
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 向Tag模型添加creation_time字段
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 向Tag模型添加last_modify_time字段
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 对多个字段进行字段选项优化,比如 choices 的英文显示、字段名称的 verbose_name 等
# (此处省略详细每一个 AlterField因为数量较多但都是对字段显示名、选项、类型等的微调
# 例如:将 comment_status 的 '打开'/'关闭' 改为 'Open'/'Close',将 status 的 '草稿'/'发表' 改为 'Draft'/'Published'
# 目的是让系统更加国际化或统一字段语义
# 示例(节选,实际迁移中包含所有字段的类似调整):
# 修改Article模型的article_order字段的显示名称
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
# 修改Article模型的author字段的显示名称
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
# 修改Article模型的body字段的显示名称
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
# 修改Article模型的category字段的显示名称
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
# 修改Article模型的comment_status字段的显示名称和选项值
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
# 修改Article模型的pub_time字段的显示名称
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
# 修改Article模型的show_toc字段的显示名称
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
# 修改Article模型的status字段的显示名称和选项值
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
# ...(其他字段类似调整,包括 article_order、show_toc、author、category、tags、views 等)
# 修改Article模型的tags字段的显示名称
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
# 修改Article模型的title字段的显示名称
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
# 修改Article模型的type字段的显示名称和选项值
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
# 修改Article模型的views字段的显示名称
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
# 修改BlogSettings模型的article_comment_count字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
# ...(其它 blogsettings 字段也做了字段选项的优化调整,比如 verbose_name 更清晰)
# 对 Category、Links、Sidebar、Tag 等模型字段也做了类似的字段选项优化
# 修改BlogSettings模型的article_sub_length字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
# 修改BlogSettings模型的google_adsense_codes字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
# 修改BlogSettings模型的open_site_comment字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
# 修改BlogSettings模型的show_google_adsense字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
# 修改BlogSettings模型的sidebar_article_count字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
# 修改BlogSettings模型的sidebar_comment_count字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
# 修改BlogSettings模型的site_description字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
# 修改BlogSettings模型的site_keywords字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
# 修改BlogSettings模型的site_name字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
# 修改BlogSettings模型的site_seo_description字段的显示名称
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
# 修改Category模型的index字段的显示名称
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
# 修改Category模型的name字段的显示名称
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
# 修改Category模型的parent_category字段的显示名称
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
# 修改Links模型的is_enable字段的显示名称
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
# 修改Links模型的last_mod_time字段的显示名称
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 修改Links模型的link字段的显示名称
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
# 修改Links模型的name字段的显示名称
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
# 修改Links模型的sequence字段的显示名称
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
# 修改Links模型的show_type字段的显示名称和选项值
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
# 修改Sidebar模型的content字段的显示名称
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
# 修改Sidebar模型的is_enable字段的显示名称
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
# 修改Sidebar模型的last_mod_time字段的显示名称
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 修改Sidebar模型的name字段的显示名称
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
# 修改Sidebar模型的sequence字段的显示名称
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
# 修改Tag模型的name字段的显示名称
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]

@ -1,17 +1,24 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations
class Migration(migrations.Migration):
# 声明本迁移依赖的前一个迁移文件
dependencies = [
# 依赖于blog应用的第五个迁移
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
# 定义本迁移要执行的操作序列
operations = [
# 修改 BlogSettings 模型在后台显示的名称,从中文「网站配置」改为英文 'Website configuration'
# 修改BlogSettings模型的元选项国际化
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
name='blogsettings', # 指定要修改的模型名称
options={
'verbose_name': 'Website configuration', # 单数形式的显示名称(英文)
'verbose_name_plural': 'Website configuration' # 复数形式的显示名称(英文)
},
),
]

@ -1,197 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-16 17:01
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': '文章', 'verbose_name_plural': '文章'},
),
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': '网站配置', 'verbose_name_plural': '网站配置'},
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': '分类', 'verbose_name_plural': '分类'},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': '友情链接', 'verbose_name_plural': '友情链接'},
),
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': '侧边栏', 'verbose_name_plural': '侧边栏'},
),
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': '标签', 'verbose_name_plural': '标签'},
),
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='排序'),
),
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='内容'),
),
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', '开放评论'), ('c', '关闭评论')], default='o', max_length=1, verbose_name='评论状态'),
),
migrations.AlterField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间'),
),
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='显示目录'),
),
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', '草稿'), ('p', '发布')], default='p', max_length=1, verbose_name='状态'),
),
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='文章评论数量'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='Google广告代码'),
),
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='开放站点评论'),
),
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='显示Google广告'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='侧边栏文章数量'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='侧边栏评论数量'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='站点描述'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='站点关键词'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='站点名称'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='SEO描述'),
),
migrations.AlterField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='排序'),
),
migrations.AlterField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='分类名称'),
),
migrations.AlterField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页'), ('a', '全部'), ('s', '幻灯片')], default='i', max_length=1, verbose_name='显示位置'),
),
migrations.AlterField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间'),
),
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='标签名称'),
),
migrations.CreateModel(
name='ArticleLike',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='点赞时间')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_likes', to='blog.article', verbose_name='文章')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],
options={
'verbose_name': '文章点赞',
'verbose_name_plural': '文章点赞',
'ordering': ['-created_time'],
'unique_together': {('article', 'user')},
},
),
]

@ -1,18 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-16 17:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0007_alter_article_options_alter_blogsettings_options_and_more'),
]
operations = [
migrations.AddField(
model_name='article',
name='like_count',
field=models.PositiveIntegerField(default=0, verbose_name='点赞数'),
),
]

@ -1,32 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-16 18:43
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0008_article_like_count'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ArticleFavorite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='收藏时间')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_favorites', to='blog.article', verbose_name='文章')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],
options={
'verbose_name': '文章收藏',
'verbose_name_plural': '文章收藏',
'ordering': ['-created_time'],
'unique_together': {('article', 'user')},
},
),
]

@ -1,18 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-16 19:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0009_articlefavorite'),
]
operations = [
migrations.AddField(
model_name='article',
name='favorite_count',
field=models.PositiveIntegerField(default=0, verbose_name='收藏数'),
),
]

@ -1,149 +1,125 @@
# 导入日志模块
import logging
# 导入正则表达式模块
import re
# 导入抽象方法装饰器
from abc import abstractmethod
# 导入Django配置和模型相关模块
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 _
# 导入Markdown编辑器字段
from mdeditor.fields import MDTextField
# 导入slug生成工具
from uuslug import slugify
# 导入自定义工具函数
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
# 获取日志记录器
logger = logging.getLogger(__name__)
# 链接显示类型选择类
class LinkShowType(models.TextChoices):
I = ('i', _('首页'))
L = ('l', _('列表页'))
P = ('p', _('文章页'))
A = ('a', _('全部'))
S = ('s', _('幻灯片'))
I = ('i', _('index')) # 首页显示
L = ('l', _('list')) # 列表页显示
P = ('p', _('post')) # 文章页面显示
A = ('a', _('all')) # 全站显示
S = ('s', _('slide')) # 友情链接页面显示
# 基础模型抽象类
class BaseModel(models.Model):
id = models.AutoField(primary_key=True)
creation_time = models.DateTimeField(_('创建时间'), default=now)
last_modify_time = models.DateTimeField(_('修改时间'), default=now)
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):
is_update_views = isinstance(self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == [
'views']
# 检查是否为文章视图更新操作
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# 如果是视图更新,直接更新数据库
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# 否则正常保存并处理slug字段
if 'slug' in self.__dict__:
slug_source = getattr(self, 'title', '') if 'title' in self.__dict__ else getattr(self, 'name', '')
setattr(self, 'slug', slugify(slug_source))
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs)
def get_full_url(self):
# 获取完整URL
site = get_current_site().domain
url = "https://{site}{path}".format(site=site, path=self.get_absolute_url())
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
abstract = True
abstract = True # 标记为抽象基类
@abstractmethod
def get_absolute_url(self):
# 抽象方法,子类必须实现
pass
# 文章模型类
class Article(BaseModel):
"""文章"""
STATUS_CHOICES = (
('d', _('草稿')),
('p', _('发布')),
('d', _('Draft')), # 草稿状态
('p', _('Published')), # 已发布状态
)
COMMENT_STATUS = (
('o', _('开放评论')),
('c', _('关闭评论')),
('o', _('Open')), # 评论开启
('c', _('Close')), # 评论关闭
)
TYPE = (
('a', _('文章')),
('p', _('页面')),
('a', _('Article')), # 文章类型
('p', _('Page')), # 页面类型
)
title = models.CharField(_('标题'), max_length=200, unique=True)
body = MDTextField(_('内容'))
pub_time = models.DateTimeField(_('发布时间'), blank=False, null=False, default=now)
status = models.CharField(_('状态'), max_length=1, choices=STATUS_CHOICES, default='p')
comment_status = models.CharField(_('评论状态'), max_length=1, choices=COMMENT_STATUS, default='o')
type = models.CharField(_('类型'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('浏览量'), default=0)
like_count = models.PositiveIntegerField(_('点赞数'), default=0)
favorite_count = models.PositiveIntegerField(_('收藏数'), default=0)
author = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('作者'), on_delete=models.CASCADE)
article_order = models.IntegerField(_('排序'), default=0)
show_toc = models.BooleanField(_('显示目录'), default=False)
category = models.ForeignKey('Category', verbose_name=_('分类'), on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', verbose_name=_('标签'), blank=True)
# ========== 点赞相关方法 ==========
def is_liked_by(self, user):
"""检查用户是否已点赞此文章"""
if user and user.is_authenticated:
return self.article_likes.filter(user=user).exists()
return False
@property
def likes_count(self):
"""获取点赞数量"""
return self.article_likes.count()
def toggle_like(self, user):
"""切换点赞状态"""
if not user or not user.is_authenticated:
return False, 0
like, created = self.article_likes.get_or_create(user=user)
if not created:
like.delete()
liked = False
else:
liked = True
self.like_count = self.article_likes.count()
self.save(update_fields=['like_count'])
return liked, self.like_count
# ========== 收藏相关方法 ==========
def is_favorited_by(self, user):
"""检查用户是否已收藏此文章"""
if user and user.is_authenticated:
return self.article_favorites.filter(user=user).exists()
return False
@property
def favorites_count(self):
"""获取收藏数量"""
return self.article_favorites.count()
def toggle_favorite(self, user):
"""切换收藏状态"""
if not user or not user.is_authenticated:
return False, 0
favorite, created = self.article_favorites.get_or_create(user=user)
if not created:
favorite.delete()
favorited = False
else:
favorited = True
self.favorite_count = self.article_favorites.count()
self.save(update_fields=['favorite_count'])
return favorited, self.favorite_count
# ========== 结束新增 ==========
title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题
body = MDTextField(_('body')) # 文章正文使用Markdown编辑器
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now) # 发布时间
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p') # 文章状态
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o') # 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 内容类型
views = models.PositiveIntegerField(_('views'), default=0) # 浏览量
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE) # 作者外键
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0) # 文章排序
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) # 标签多对多关系
def body_to_string(self):
return self.body
@ -152,12 +128,13 @@ class Article(BaseModel):
return self.title
class Meta:
ordering = ['-article_order', '-pub_time']
verbose_name = _('文章')
verbose_name_plural = verbose_name
get_latest_by = 'id'
ordering = ['-article_order', '-pub_time'] # 默认排序
verbose_name = _('article') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
get_latest_by = 'id' # 获取最新记录的依据字段
def get_absolute_url(self):
# 获取文章绝对URL
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -165,20 +142,24 @@ class Article(BaseModel):
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存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):
super().save(*args, **kwargs)
def viewed(self):
# 增加浏览量
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
# 获取评论列表,带缓存
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
@ -186,49 +167,71 @@ class Article(BaseModel):
return value
else:
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
cache.set(cache_key, comments, 60 * 100) # 缓存100分钟
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
# 获取管理后台URL
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100)
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def next_article(self):
return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first()
# 获取下一篇文章
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def prev_article(self):
# 获取上一篇文章
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
"""
# 从文章正文中提取第一张图片URL
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
# 分类模型类
class Category(BaseModel):
name = models.CharField(_('分类名称'), max_length=30, unique=True)
parent_category = models.ForeignKey('self', verbose_name=_('父级分类'), blank=True, null=True,
on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
index = models.IntegerField(default=0, verbose_name=_('排序'))
"""文章分类"""
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) # URL别名
index = models.IntegerField(default=0, verbose_name=_('index')) # 排序索引
class Meta:
ordering = ['-index']
verbose_name = _('分类')
verbose_name_plural = verbose_name
ordering = ['-index'] # 按索引倒序排列
verbose_name = _('category') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
def get_absolute_url(self):
return reverse('blog:category_detail', kwargs={'category_name': self.slug})
# 获取分类绝对URL
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
return self.name
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
"""
categorys = []
def parse(category):
@ -239,8 +242,12 @@ class Category(BaseModel):
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
"""
categorys = []
all_categorys = Category.objects.all()
@ -249,7 +256,7 @@ class Category(BaseModel):
categorys.append(category)
childs = all_categorys.filter(parent_category=category)
for child in childs:
if child not in categorys:
if category not in categorys:
categorys.append(child)
parse(child)
@ -257,146 +264,144 @@ class Category(BaseModel):
return categorys
# 标签模型类
class Tag(BaseModel):
name = models.CharField(_('标签名称'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL别名
def __str__(self):
return self.name
def get_absolute_url(self):
# 获取标签绝对URL
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_article_count(self):
# 获取标签下的文章数量
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name']
verbose_name = _('标签')
verbose_name_plural = verbose_name
ordering = ['name'] # 按名称排序
verbose_name = _('tag') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
# 友情链接模型类
class Links(models.Model):
name = models.CharField(_('链接名称'), max_length=30, unique=True)
link = models.URLField(_('链接地址'))
sequence = models.IntegerField(_('排序'), unique=True)
is_enable = models.BooleanField(_('是否显示'), default=True)
show_type = models.CharField(_('显示位置'), max_length=1, choices=LinkShowType.choices, default=LinkShowType.I)
creation_time = models.DateTimeField(_('创建时间'), default=now)
last_mod_time = models.DateTimeField(_('修改时间'), default=now)
"""友情链接"""
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(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I) # 显示类型
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta:
ordering = ['sequence']
verbose_name = _('友情链接')
verbose_name_plural = verbose_name
ordering = ['sequence'] # 按序号排序
verbose_name = _('link') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
def __str__(self):
return self.name
# 侧边栏模型类
class SideBar(models.Model):
name = models.CharField(_('标题'), max_length=100)
content = models.TextField(_('内容'))
sequence = models.IntegerField(_('排序'), unique=True)
is_enable = models.BooleanField(_('是否启用'), default=True)
creation_time = models.DateTimeField(_('创建时间'), default=now)
last_mod_time = models.DateTimeField(_('修改时间'), default=now)
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100) # 侧边栏标题
content = models.TextField(_('content')) # 侧边栏内容
sequence = models.IntegerField(_('order'), unique=True) # 排序序号
is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta:
ordering = ['sequence']
verbose_name = _('侧边栏')
verbose_name_plural = verbose_name
ordering = ['sequence'] # 按序号排序
verbose_name = _('sidebar') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
def __str__(self):
return self.name
class ArticleLike(models.Model):
article = models.ForeignKey(
'Article',
verbose_name=_('文章'),
on_delete=models.CASCADE,
related_name='article_likes'
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('用户'),
on_delete=models.CASCADE
)
created_time = models.DateTimeField(_('点赞时间'), default=now)
class Meta:
verbose_name = _('文章点赞')
verbose_name_plural = verbose_name
unique_together = ('article', 'user')
ordering = ['-created_time']
def __str__(self):
return f'{self.user.username} 点赞了 {self.article.title}'
class ArticleFavorite(models.Model):
"""
文章收藏模型
记录用户对文章的收藏关系每个用户对同一篇文章只能收藏一次
"""
article = models.ForeignKey(
'Article',
verbose_name=_('文章'),
on_delete=models.CASCADE,
related_name='article_favorites'
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('用户'),
on_delete=models.CASCADE
)
created_time = models.DateTimeField(_('收藏时间'), default=now)
class Meta:
verbose_name = _('文章收藏')
verbose_name_plural = verbose_name
unique_together = ('article', 'user')
ordering = ['-created_time']
def __str__(self):
return f'{self.user.username} 收藏了 {self.article.title}'
# 博客设置模型类
class BlogSettings(models.Model):
site_name = models.CharField(_('站点名称'), max_length=200, null=False, blank=False, default='')
site_description = models.TextField(_('站点描述'), max_length=1000, null=False, blank=False, default='')
site_seo_description = models.TextField(_('SEO描述'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField(_('站点关键词'), max_length=1000, null=False, blank=False, default='')
article_sub_length = models.IntegerField(_('文章摘要长度'), default=300)
sidebar_article_count = models.IntegerField(_('侧边栏文章数量'), default=10)
sidebar_comment_count = models.IntegerField(_('侧边栏评论数量'), default=5)
article_comment_count = models.IntegerField(_('文章评论数量'), default=5)
show_google_adsense = models.BooleanField(_('显示Google广告'), default=False)
google_adsense_codes = models.TextField(_('Google广告代码'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('开放站点评论'), 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(_("网站统计代码"), max_length=1000, null=False, blank=False, default='')
show_gongan_code = models.BooleanField(_('是否显示公安备案号'), default=False)
gongan_beiancode = models.TextField(_('公安备案号'), max_length=2000, null=True, blank=True, default='')
comment_need_review = models.BooleanField(_('评论是否需要审核'), default=False)
"""blog的配置"""
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='') # 网站名称
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='') # 网站描述
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='') # 网站SEO描述
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='') # 网站关键词
article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页面评论数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示Google广告
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='') # Google广告代码
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(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='') # 网站统计代码
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False) # 是否显示公安备案
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='') # 公安备案号
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False) # 评论是否需要审核
class Meta:
verbose_name = _('网站配置')
verbose_name_plural = verbose_name
verbose_name = _('Website configuration') # 单数显示名称
verbose_name_plural = verbose_name # 复数显示名称
def __str__(self):
return self.site_name
def clean(self):
# 验证只能有一个配置实例
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('只能存在一个配置'))
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
cache.clear() # 保存配置后清除缓存

@ -1,12 +1,28 @@
# 导入Haystack搜索索引相关模块
from haystack import indexes
# 导入文章模型
from blog.models import Article
# 定义文章搜索索引类继承自SearchIndex和Indexable
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
# 定义主搜索字段document=True表示这是主要的搜索字段
# use_template=True表示使用模板文件来构建搜索内容
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
"""
返回与此索引关联的Django模型类
:return: Article模型类
"""
return Article
def index_queryset(self, using=None):
"""
返回要建立索引的查询集
这里只对已发布(status='p')的文章建立索引
:param using: 使用的搜索引擎别名
:return: 已发布文章的查询集
"""
return self.get_model().objects.filter(status='p')

@ -1,53 +1,75 @@
# 导入哈希库
import hashlib
# 导入日志模块
import logging
# 导入随机数模块
import random
# 导入URL处理模块
import urllib
# 导入Django模板相关模块
from django import template
from django.conf import settings
# 导入数据库查询模块
from django.db.models import Q
from django.shortcuts import get_object_or_404
# 导入模板过滤器
from django.template.defaultfilters import stringfilter
# 导入静态文件处理
from django.templatetags.static import static
from django.urls import reverse
# 导入安全字符串处理
from django.utils.safestring import mark_safe
# 导入博客模型
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
# 导入评论模型
from comments.models import Comment
# 导入自定义工具函数
from djangoblog.utils import CommonMarkdown, sanitize_html
from djangoblog.utils import cache
from djangoblog.utils import get_current_site
# 导入OAuth用户模型
from oauth.models import OAuthUser
# 导入插件管理模块
from djangoblog.plugin_manage import hooks
# 获取日志记录器
logger = logging.getLogger(__name__)
# 创建模板库注册器
register = template.Library()
# 注册头部meta标签的简单标签接收上下文
@register.simple_tag(takes_context=True)
def head_meta(context):
return mark_safe(hooks.apply_filters('head_meta', '', context))
# 注册时间格式化简单标签
@register.simple_tag
def timeformat(data):
try:
# 使用设置中的时间格式
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
# 注册日期时间格式化简单标签
@register.simple_tag
def datetimeformat(data):
try:
# 使用设置中的日期时间格式
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
# 注册自定义Markdown过滤器自动处理字符串
@register.filter()
@stringfilter
def custom_markdown(content):
@ -55,16 +77,18 @@ def custom_markdown(content):
通用markdown过滤器应用文章内容插件
主要用于文章内容处理
"""
# 将Markdown内容转换为HTML
html_content = CommonMarkdown.get_markdown(content)
# 然后应用插件过滤器优化HTML
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content)
return mark_safe(optimized_html)
# 注册侧边栏Markdown过滤器
@register.filter()
@stringfilter
def sidebar_markdown(content):
@ -72,11 +96,12 @@ def sidebar_markdown(content):
return mark_safe(html_content)
# 注册文章内容渲染标签,接收上下文
@register.simple_tag(takes_context=True)
def render_article_content(context, article, is_summary=False):
"""
渲染文章内容包含完整的上下文信息供插件使用
Args:
context: 模板上下文
article: 文章对象
@ -84,44 +109,45 @@ def render_article_content(context, article, is_summary=False):
"""
if not article or not hasattr(article, 'body'):
return ''
# 先转换Markdown为HTML
html_content = CommonMarkdown.get_markdown(article.body)
# 如果是摘要模式,先截断内容再应用插件
if is_summary:
# 截断HTML内容到合适的长度约300字符
from django.utils.html import strip_tags
from django.template.defaultfilters import truncatechars
# 先去除HTML标签截断纯文本然后重新转换为HTML
plain_text = strip_tags(html_content)
truncated_text = truncatechars(plain_text, 300)
# 重新转换截断后的文本为HTML简化版避免复杂的插件处理
html_content = CommonMarkdown.get_markdown(truncated_text)
# 然后应用插件过滤器,传递完整的上下文
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
# 获取request对象
request = context.get('request')
# 应用所有文章内容相关的插件
# 注意:摘要模式下某些插件(如版权声明)可能不适用
optimized_html = hooks.apply_filters(
ARTICLE_CONTENT_HOOK_NAME,
html_content,
article=article,
ARTICLE_CONTENT_HOOK_NAME,
html_content,
article=article,
request=request,
context=context,
is_summary=is_summary # 传递摘要标志,插件可以据此调整行为
)
return mark_safe(optimized_html)
# 注册获取Markdown目录的简单标签
@register.simple_tag
def get_markdown_toc(content):
from djangoblog.utils import CommonMarkdown
@ -129,6 +155,7 @@ def get_markdown_toc(content):
return mark_safe(toc)
# 注册评论Markdown过滤器
@register.filter()
@stringfilter
def comment_markdown(content):
@ -136,6 +163,7 @@ def comment_markdown(content):
return mark_safe(sanitize_html(content))
# 注册内容截断过滤器标记为安全HTML
@register.filter(is_safe=True)
@stringfilter
def truncatechars_content(content):
@ -150,6 +178,7 @@ def truncatechars_content(content):
return truncatechars_html(content, blogsetting.article_sub_length)
# 注册简单截断过滤器标记为安全HTML
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
@ -158,6 +187,7 @@ def truncate(content):
return strip_tags(content)[:150]
# 注册面包屑导航包含标签
@register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article):
"""
@ -179,6 +209,7 @@ def load_breadcrumb(article):
}
# 注册文章标签列表包含标签
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
@ -199,6 +230,7 @@ def load_articletags(article):
}
# 注册侧边栏包含标签
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
"""
@ -213,16 +245,23 @@ def load_sidebar(user, linktype):
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
# 获取最近文章
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
# 获取所有分类
sidebar_categorys = Category.objects.all()
# 获取额外的侧边栏内容
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
# 获取最多阅读文章
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
# 获取文章归档日期
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
# 获取友情链接
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
# 获取最新评论
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
@ -253,12 +292,14 @@ def load_sidebar(user, linktype):
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
# 设置缓存3小时过期
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
return value
# 注册文章meta信息包含标签
@register.inclusion_tag('blog/tags/article_meta_info.html')
def load_article_metas(article, user):
"""
@ -272,10 +313,12 @@ def load_article_metas(article, user):
}
# 注册分页信息包含标签
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
previous_url = ''
next_url = ''
# 处理首页分页
if page_type == '':
if page_obj.has_next():
next_number = page_obj.next_page_number()
@ -285,6 +328,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
# 处理标签分页
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
@ -301,6 +345,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
kwargs={
'page': previous_number,
'tag_name': tag.slug})
# 处理作者文章分页
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
@ -316,7 +361,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
kwargs={
'page': previous_number,
'author_name': tag_name})
# 处理分类目录分页
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
@ -341,6 +386,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
}
# 注册文章详情包含标签
@register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user):
"""
@ -360,7 +406,7 @@ def load_article_detail(article, isindex, user):
}
# 返回用户头像URL
# 返回用户头像URL的过滤器
# 模板使用方法: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
@ -369,7 +415,7 @@ def gravatar_url(email, size=40):
url = cache.get(cachekey)
if url:
return url
# 检查OAuth用户是否有自定义头像
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
@ -378,18 +424,19 @@ def gravatar_url(email, size=40):
if users_with_picture:
# 获取默认头像路径用于比较
default_avatar_path = static('blog/img/avatar.png')
# 优先选择非默认头像的用户,否则选择第一个
non_default_users = [u for u in users_with_picture if u.picture != default_avatar_path and not u.picture.endswith('/avatar.png')]
non_default_users = [u for u in users_with_picture if
u.picture != default_avatar_path and not u.picture.endswith('/avatar.png')]
selected_user = non_default_users[0] if non_default_users else users_with_picture[0]
url = selected_user.picture
cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
avatar_type = 'non-default' if non_default_users else 'default'
logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type))
return url
# 使用默认头像
url = static('blog/img/avatar.png')
cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
@ -397,6 +444,7 @@ def gravatar_url(email, size=40):
return url
# 返回用户头像HTML标签的过滤器
@register.filter
def gravatar(email, size=40):
"""获得用户头像HTML标签"""
@ -406,6 +454,7 @@ def gravatar(email, size=40):
(url, size, size))
# 注册查询集过滤简单标签
@register.simple_tag
def query(qs, **kwargs):
""" template tag which allows queryset filtering. Usage:
@ -417,7 +466,8 @@ def query(qs, **kwargs):
return qs.filter(**kwargs)
# 注册字符串连接过滤器
@register.filter
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)
return str(arg1) + str(arg2)

@ -1,47 +1,56 @@
# 导入操作系统接口模块
import os
# 导入Django配置和文件处理相关模块
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command # 执行Django管理命令
from django.core.paginator import Paginator # 分页器
from django.templatetags.static import static # 静态文件URL生成
from django.test import Client, RequestFactory, TestCase # Django测试客户端与测试用例
from django.urls import reverse # URL反向解析
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 # OAuth相关模型
# 文章相关测试类
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
# 导入OAuth相关模型
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
# 文章测试类
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")
user.is_staff = True
user.is_superuser = True
user.is_staff = True # 设置为管理员
user.is_superuser = True # 设置为超级用户
user.save()
# 访问用户详情页
# 测试用户详情页访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 以下几行尝试访问不存在的admin页面可能用于测试404
# 测试管理员页面访问
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
# 创建一个侧边栏实例并保存
# 创建侧边栏
s = SideBar()
s.sequence = 1
s.name = 'test'
@ -49,143 +58,155 @@ class ArticleTest(TestCase):
s.is_enable = True
s.save()
# 创建一个分类并保存
# 创建分类
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
# 创建一个标签并保存
# 创建标签
tag = Tag()
tag.name = "nicetag"
tag.save()
# 创建一篇文章并保存
# 创建文章
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.type = 'a' # 文章类型
article.status = 'p' # 发布状态
article.save()
self.assertEqual(0, article.tags.count()) # 初始应无标签
article.tags.add(tag) # 添加标签
# 验证初始标签数量为0
self.assertEqual(0, article.tags.count())
# 添加标签
article.tags.add(tag)
article.save()
self.assertEqual(1, article.tags.count()) # 应有1个标签
# 验证标签数量为1
self.assertEqual(1, article.tags.count())
# 批量创建20篇文章,均添加同一标签
# 批量创建20篇文章
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
article.body = "nicetitle" + str(i)
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.type = 'a' # 文章类型
article.status = 'p' # 发布状态
article.save()
article.tags.add(tag)
article.save()
# 如果启用了Elasticsearch则构建索引并测试搜索
# 检查是否启用Elasticsearch
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
# 构建搜索索引
call_command("build_index")
# 测试搜索功能
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
# 访问某篇文章详情页
# 测试文章详情页访问
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 模拟通知爬虫(如搜索引擎)该文章已更新
# 测试蜘蛛通知功能
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
# 访问标签详情页
# 测试标签页访问
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)
# 搜索关键词'django'
# 测试搜索页面
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
# 获取文章的标签模板标签结果
# 测试文章标签模板标签
s = load_articletags(article)
self.assertIsNotNone(s)
# 登录用户后访问归档页
# 用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
# 测试归档页面
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# 测试分页功能
# 测试首页分页
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
# 标签分页
# 测试标签分页
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
# 按作者分页
# 测试作者文章分页
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
# 按分类分页
# 测试分类目录分页
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
# 测试搜索表单
f = BlogSearchForm()
f.search()
# 模拟百度通知
# 测试百度蜘蛛通知
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
# 测试模板标签gravatar头像URL
# 测试头像相关模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
# 创建并保存一个友情链接
# 创建友情链接
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
link.save()
# 访问友情链接页面
# 测试友情链接页面
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# 访问RSS Feed页面
# 测试RSS订阅
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
# 访问Sitemap页面
# 测试站点地图
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# 尝试删除一篇文章
# 测试管理员页面访问
self.client.get("/admin/blog/article/1/delete/")
# 访问一些不存在的admin页面
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):
# 遍历所有分页,检查前后页链接是否有效
"""
检查分页功能
:param p: Paginator分页器对象
:param type: 分页类型
:param value: 分页值标签名分类名等
"""
for page in range(1, p.num_pages + 1):
# 加载分页信息
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
# 测试上一页链接
if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
# 测试下一页链接
if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
@ -193,39 +214,41 @@ class ArticleTest(TestCase):
def test_image(self):
# 测试图片上传功能
import requests
# 下载Python官方Logo
# 下载测试图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
# 保存图片到本地
with open(imagepath, 'wb') as file:
file.write(rsp.content)
# 试未授权上传
# 试未授权上传
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
# 生成签名
sign = get_sha256(get_sha256(settings.SECRET_KEY))
# 测试授权上传
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile}
# 带签名上传
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
# 清理测试文件
os.remove(imagepath)
# 测试发送邮件与保存头像功能
# 测试工具函数
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
def test_errorpage(self):
# 测试访问不存在的页面,应返回404
# 测试404错误页面
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_commands(self):
# 创建超级用户
# 测试管理命令
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -234,13 +257,14 @@ class ArticleTest(TestCase):
user.is_superuser = True
user.save()
# 创建OAuth配置与用户
# 创建OAuth配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
# 创建OAuth用户使用默认头像
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
@ -252,6 +276,7 @@ class ArticleTest(TestCase):
}'''
u.save()
# 创建另一个OAuth用户使用QQ头像
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
@ -262,13 +287,12 @@ class ArticleTest(TestCase):
}'''
u.save()
# 如果启用了Elasticsearch构建索引
# 执行各种管理命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
# 执行一系列管理命令如Ping百度、创建测试数据、清理缓存等
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")
call_command("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,100 +1,91 @@
# 导入Django URL路由相关模块
from django.urls import path
from django.views.decorators.cache import cache_page # Django缓存视图装饰器
# 导入缓存页面装饰器
from django.views.decorators.cache import cache_page
from . import views # 导入当前应用的视图
# 导入当前应用的视图模块
from . import views
app_name = "blog" # 应用命名空间
# 定义应用的命名空间
app_name = "blog"
# 定义URL模式列表
urlpatterns = [
# 首页
# 首页URL使用类视图
path(
r'',
views.IndexView.as_view(),
name='index'),
# 首页分页
# 首页分页URL支持页码参数
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
# 文章详情页通过年、月、日、文章ID定位
# 文章详情页URL包含年月日和文章ID参数
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
# 文章点赞API
path(
r'article/<int:article_id>/like/',
views.article_like,
name='article_like'),
# 文章收藏API
path(
r'article/<int:article_id>/favorite/',
views.article_favorite,
name='article_favorite'),
# 我的喜欢列表
path(
r'my-likes/',
views.MyLikesView.as_view(),
name='my_likes'),
path(
r'my-likes/<int:page>/',
views.MyLikesView.as_view(),
name='my_likes_page'),
# 我的收藏列表
path(
r'my-favorites/',
views.MyFavoritesView.as_view(),
name='my_favorites'),
path(
r'my-favorites/<int:page>/',
views.MyFavoritesView.as_view(),
name='my_favorites_page'),
# 分类目录详情页,通过分类别名
# 分类详情页URL使用slug格式的分类名称
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
# 分类目录详情页(带分页)
# 分类详情分页URL支持分类名称和页码参数
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
# 作者文章详情页,通过作者名称
# 作者详情页URL使用作者名称参数
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
# 作者文章详情页(带分页)
# 作者详情分页URL支持作者名称和页码参数
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
# 标签详情页,通过标签别名
# 标签详情页URL使用slug格式的标签名称
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
# 标签详情页(带分页)
# 标签详情分页URL支持标签名称和页码参数
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
# 文章归档页使用缓存60分钟
# 归档页面URL使用缓存装饰器缓存1小时
path(
'archives.html',
cache_page(60 * 60)(views.ArchivesView.as_view()),
cache_page(
60 * 60)( # 缓存1小时60分钟 * 60秒
views.ArchivesView.as_view()),
name='archives'),
# 友情链接页
# 友情链接页面URL
path(
r'links.html',
'links.html',
views.LinkListView.as_view(),
name='links'),
# 文件上传接口(用于图床等功能)
# 文件上传URL使用函数视图
path(
r'upload',
views.fileupload,
name='upload'),
# 清理缓存接口
# 清理缓存URL
path(
r'clean',
views.clean_cache_view,

@ -1,55 +1,82 @@
# 导入日志模块
import logging
# 导入操作系统接口模块
import os
# 导入UUID生成模块
import uuid
# 导入Django配置和核心模块
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import F
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
# 导入Haystack搜索视图
from haystack.views import SearchView
from blog.models import Article, Category, LinkShowType, Links, Tag, ArticleLike, ArticleFavorite
# 导入博客模型
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__)
# 文章列表视图基类
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
paginate_by = settings.PAGINATE_BY # 分页大小
page_kwarg = 'page' # 页码参数名
link_type = LinkShowType.L # 链接显示类型
def get_view_cache_key(self):
return self.request.GET.get('pages', '')
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
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(key=cache_key))
@ -61,41 +88,63 @@ class ArticleListView(ListView):
return article_list
def get_queryset(self):
'''
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
# 添加上下文数据
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
# 首页视图
class IndexView(ArticleListView):
'''
首页
'''
# 友情链接类型
link_type = LinkShowType.I
def get_queryset_data(self):
# 获取已发布的文章列表
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
# 生成首页缓存键
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
# 文章详情视图
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
pk_url_kwarg = 'article_id' # URL中的文章ID参数名
context_object_name = "article" # 上下文中的文章对象名
def get_context_data(self, **kwargs):
# 创建评论表单
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
else:
@ -105,69 +154,55 @@ class ArticleDetailView(DetailView):
if page > paginator.num_pages:
page = paginator.num_pages
# 获取当前页评论
p_comments = paginator.page(page)
# 计算下一页和上一页
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
# 构建评论分页URL
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 添加上下文数据
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(article_comments) if article_comments else 0
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
# ========== 修复点赞相关信息 ==========
user = self.request.user
if user.is_authenticated:
# 使用正确的方法名
is_liked = self.object.is_liked_by(user)
kwargs['article_liked_class'] = 'liked' if is_liked else ''
kwargs['like_icon_class'] = 'fa fa-heart' if is_liked else 'fa fa-heart-o'
kwargs['like_text'] = '已点赞' if is_liked else '点赞'
# 收藏相关信息
is_favorited = self.object.is_favorited_by(user)
kwargs['article_favorited_class'] = 'favorited' if is_favorited else ''
kwargs['favorite_icon_class'] = 'fa fa-star' if is_favorited else 'fa fa-star-o'
kwargs['favorite_text'] = '已收藏' if is_favorited else '收藏'
else:
kwargs['article_liked_class'] = ''
kwargs['like_icon_class'] = 'fa fa-heart-o'
kwargs['like_text'] = '点赞'
kwargs['article_favorited_class'] = ''
kwargs['favorite_icon_class'] = 'fa fa-star-o'
kwargs['favorite_text'] = '收藏'
# 添加点赞和收藏数量
kwargs['like_count'] = self.object.like_count
kwargs['favorite_count'] = self.object.favorite_count
# ========== 结束修复 ==========
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
return context
# 分类详情视图
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档"
def get_queryset_data(self):
# 获取分类slug
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
# 获取所有子分类名称
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
# 获取该分类下的所有文章
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
@ -192,7 +227,11 @@ class CategoryDetailView(ArticleListView):
return super(CategoryDetailView, self).get_context_data(**kwargs)
# 作者详情视图
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档'
def get_queryset_cache_key(self):
@ -215,7 +254,11 @@ class AuthorDetailView(ArticleListView):
return super(AuthorDetailView, self).get_context_data(**kwargs)
# 标签详情视图
class TagDetailView(ArticleListView):
'''
标签列表页面
'''
page_type = '分类标签归档'
def get_queryset_data(self):
@ -243,10 +286,14 @@ class TagDetailView(ArticleListView):
return super(TagDetailView, self).get_context_data(**kwargs)
# 归档视图
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
page_type = '文章归档'
paginate_by = None
page_kwarg = None
paginate_by = None # 不分页
page_kwarg = None # 无页码参数
template_name = 'blog/article_archives.html'
def get_queryset_data(self):
@ -257,6 +304,7 @@ class ArchivesView(ArticleListView):
return cache_key
# 友情链接视图
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
@ -265,56 +313,78 @@ class LinkListView(ListView):
return Links.objects.filter(is_enable=True)
# Elasticsearch搜索视图
class EsSearchView(SearchView):
def get_context(self):
# 构建分页
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
"query": self.query, # 搜索查询
"form": self.form, # 搜索表单
"page": page, # 当前页
"paginator": paginator, # 分页器
"suggestion": None, # 搜索建议
}
# 添加拼写建议
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
return context
# 文件上传视图免除CSRF保护
@csrf_exempt
def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
"""
if request.method == 'POST':
# 验证签名
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
# 处理所有上传的文件
for filename in request.FILES:
# 生成时间路径
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # 图片扩展名
fname = u''.join(str(filename))
# 判断是否为图片
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
# 构建保存路径
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
# 生成唯一文件名
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 安全检查
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
# 保存文件
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
# 如果是图片,进行压缩优化
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
# 生成静态文件URL
url = static(savepath)
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post")
# 404错误页面视图
def page_not_found_view(
request,
exception,
@ -329,6 +399,7 @@ def page_not_found_view(
status=404)
# 500错误页面视图
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
@ -337,6 +408,7 @@ def server_error_view(request, template_name='blog/error_page.html'):
status=500)
# 403权限拒绝视图
def permission_denied_view(
request,
exception,
@ -349,204 +421,7 @@ def permission_denied_view(
'statuscode': '403'}, status=403)
# 清理缓存视图
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
# 文章点赞API视图
@login_required
def article_like(request, article_id):
"""
文章点赞/取消点赞 API
用户可以通过该接口对文章进行点赞或取消点赞
如果用户已点赞再次点赞将取消点赞如果未点赞则添加点赞
Args:
request: HTTP请求对象
article_id: 文章ID
Returns:
JsonResponse: 返回JSON格式的响应
"""
# 只接受POST请求
if request.method != 'POST':
return JsonResponse({
'success': False,
'message': _('只支持POST请求')
}, status=405)
# 获取文章对象
article = get_object_or_404(Article, id=article_id, status='p')
user = request.user
try:
# 尝试获取点赞记录
like = ArticleLike.objects.filter(article=article, user=user).first()
if like:
# 如果已点赞,则取消点赞
like.delete()
# 减少点赞数使用F表达式避免竞态条件
Article.objects.filter(id=article_id).update(like_count=F('like_count') - 1)
article.refresh_from_db()
return JsonResponse({
'success': True,
'liked': False,
'like_count': article.like_count,
'message': _('取消点赞成功')
})
else:
# 如果未点赞,则添加点赞
ArticleLike.objects.create(article=article, user=user)
# 增加点赞数使用F表达式避免竞态条件
Article.objects.filter(id=article_id).update(like_count=F('like_count') + 1)
article.refresh_from_db()
return JsonResponse({
'success': True,
'liked': True,
'like_count': article.like_count,
'message': _('点赞成功')
})
except Exception as e:
logger.error(f'点赞失败: {str(e)}')
return JsonResponse({
'success': False,
'message': _('点赞失败,请稍后再试')
}, status=500)
# 文章收藏API视图
@login_required
def article_favorite(request, article_id):
"""
文章收藏/取消收藏 API
用户可以通过该接口对文章进行收藏或取消收藏
如果用户已收藏再次收藏将取消收藏如果未收藏则添加收藏
Args:
request: HTTP请求对象
article_id: 文章ID
Returns:
JsonResponse: 返回JSON格式的响应
"""
# 只接受POST请求
if request.method != 'POST':
return JsonResponse({
'success': False,
'message': _('只支持POST请求')
}, status=405)
# 获取文章对象
article = get_object_or_404(Article, id=article_id, status='p')
user = request.user
try:
# 尝试获取收藏记录
favorite = ArticleFavorite.objects.filter(article=article, user=user).first()
if favorite:
# 如果已收藏,则取消收藏
favorite.delete()
# 减少收藏数使用F表达式避免竞态条件
Article.objects.filter(id=article_id).update(favorite_count=F('favorite_count') - 1)
article.refresh_from_db()
return JsonResponse({
'success': True,
'favorited': False,
'favorite_count': article.favorite_count,
'message': _('取消收藏成功')
})
else:
# 如果未收藏,则添加收藏
ArticleFavorite.objects.create(article=article, user=user)
# 增加收藏数使用F表达式避免竞态条件
Article.objects.filter(id=article_id).update(favorite_count=F('favorite_count') + 1)
article.refresh_from_db()
return JsonResponse({
'success': True,
'favorited': True,
'favorite_count': article.favorite_count,
'message': _('收藏成功')
})
except Exception as e:
logger.error(f'收藏失败: {str(e)}')
return JsonResponse({
'success': False,
'message': _('收藏失败,请稍后再试')
}, status=500)
# 我的喜欢列表视图
class MyLikesView(ArticleListView):
"""显示当前用户点赞的所有文章"""
template_name = 'blog/article_index.html'
page_type = '我的喜欢'
def get_queryset_data(self):
if not self.request.user.is_authenticated:
return Article.objects.none()
# 获取用户点赞的所有文章ID
liked_article_ids = ArticleLike.objects.filter(
user=self.request.user
).values_list('article_id', flat=True)
# 返回对应的文章列表
return Article.objects.filter(
id__in=liked_article_ids,
status='p'
).order_by('-pub_time')
def get_queryset_cache_key(self):
# 不使用缓存,因为点赞列表会频繁变化
return None
def get_queryset_from_cache(self, cache_key):
# 直接返回数据,不使用缓存
return self.get_queryset_data()
def get_context_data(self, **kwargs):
kwargs['page_type'] = self.page_type
return super(MyLikesView, self).get_context_data(**kwargs)
# 我的收藏列表视图
class MyFavoritesView(ArticleListView):
"""显示当前用户收藏的所有文章"""
template_name = 'blog/article_index.html'
page_type = '我的收藏'
def get_queryset_data(self):
if not self.request.user.is_authenticated:
return Article.objects.none()
# 获取用户收藏的所有文章ID
favorited_article_ids = ArticleFavorite.objects.filter(
user=self.request.user
).values_list('article_id', flat=True)
# 返回对应的文章列表
return Article.objects.filter(
id__in=favorited_article_ids,
status='p'
).order_by('-pub_time')
def get_queryset_cache_key(self):
# 不使用缓存,因为收藏列表会频繁变化
return None
def get_queryset_from_cache(self, cache_key):
# 直接返回数据,不使用缓存
return self.get_queryset_data()
def get_context_data(self, **kwargs):
kwargs['page_type'] = self.page_type
return super(MyFavoritesView, self).get_context_data(**kwargs)
return HttpResponse('ok')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,26 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/(function(){'use strict';if(navigator.userAgent.match(/IEMobile\/10\.0/)){var msViewportStyle=document.createElement('style')
msViewportStyle.appendChild(document.createTextNode('@-ms-viewport{width:auto!important}'))
document.querySelector('head').appendChild(msViewportStyle)}})();;/*!
* Copyright 2014-2015 Twitter, Inc.
*
* Licensed under the Creative Commons Attribution 3.0 Unported License. For
* details, see https://creativecommons.org/licenses/by/3.0/.
*/(function(){'use strict';function emulatedIEMajorVersion(){var groups=/MSIE ([0-9.]+)/.exec(window.navigator.userAgent)
if(groups===null){return null}
var ieVersionNum=parseInt(groups[1],10)
var ieMajorVersion=Math.floor(ieVersionNum)
return ieMajorVersion}
function actualNonEmulatedIEMajorVersion(){var jscriptVersion=new Function('/*@cc_on return @_jscript_version; @*/')()
if(jscriptVersion===undefined){return 11}
if(jscriptVersion<9){return 8}
return jscriptVersion}
var ua=window.navigator.userAgent
if(ua.indexOf('Opera')>-1||ua.indexOf('Presto')>-1){return}
var emulated=emulatedIEMajorVersion()
if(emulated===null){return}
var nonEmulated=actualNonEmulatedIEMajorVersion()
if(emulated!==nonEmulated){window.alert('WARNING: You appear to be using IE'+nonEmulated+' in IE'+emulated+' emulation mode.\nIE emulation modes can behave significantly differently from ACTUAL older versions of IE.\nPLEASE DON\'T FILE BOOTSTRAP BUGS based on testing in IE emulation modes!')}})();;

@ -1,79 +1,75 @@
# 模块级注释Django管理后台配置模块 - 评论管理
# 本模块定义了评论模型在Django管理后台的显示配置和操作功能
# 导入Django管理员模块
from django.contrib import admin
# 导入URL反向解析
from django.urls import reverse
# 导入HTML格式化工具
from django.utils.html import format_html
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# 函数级注释:禁用评论状态操作
# 管理员动作函数,用于批量禁用选中的评论
# 禁用评论状态的管理动作函数
def disable_commentstatus(modeladmin, request, queryset):
# 核心代码将查询集中所有评论的is_enable字段更新为False
queryset.update(is_enable=False)
# 函数级注释:启用评论状态操作
# 管理员动作函数,用于批量启用选中的评论
# 启用评论状态的管理动作函数
def enable_commentstatus(modeladmin, request, queryset):
# 核心代码将查询集中所有评论的is_enable字段更新为True
queryset.update(is_enable=True)
# 设置动作函数的显示名称(国际化)
# 设置管理动作的显示名称
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
# 类级注释:评论管理类
# 继承自admin.ModelAdmin自定义评论模型在Django管理后台的显示和行为
# 评论模型的管理类
class CommentAdmin(admin.ModelAdmin):
# 每页显示记录数配置
# 每页显示20条记录
list_per_page = 20
# 列表页显示的字段配置
# 列表页显示的字段
list_display = (
'id',
'body',
'link_to_userinfo', # 自定义方法显示用户链接
'link_to_article', # 自定义方法显示文章链接
'is_enable',
'creation_time')
# 可点击进入编辑页的字段
'id', # 评论ID
'body', # 评论内容
'link_to_userinfo', # 用户信息链接(自定义字段)
'link_to_article', # 文章链接(自定义字段)
'is_enable', # 是否启用
'creation_time' # 创建时间
)
# 可作为链接点击的字段
list_display_links = ('id', 'body', 'is_enable')
# 右侧过滤器配置
list_filter = ('is_enable',)
# 编辑页排除的字段(不显示)
# 右侧过滤器字段
list_filter = ('is_enable',) # 按启用状态过滤
# 排除的字段(不在表单中显示)
exclude = ('creation_time', 'last_modify_time')
# 批量操作动作列表
# 可用的管理动作
actions = [disable_commentstatus, enable_commentstatus]
# 使用原始ID输入框的外键字段(提升大表性能
# 使用原始ID字段(显示搜索框而不是下拉选择
raw_id_fields = ('author', 'article')
# 搜索字段配置
search_fields = ('body',)
# 搜索字段
search_fields = ('body',) # 按评论内容搜索
# 方法级注释:用户信息链接显示
# 自定义方法,在列表页显示带链接的用户信息
# 自定义方法:显示用户信息链接
def link_to_userinfo(self, obj):
# 核心代码获取用户模型的app_label和model_name
# 获取用户模型的app和model信息
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 核心代码:生成用户编辑页面的URL
# 生成用户编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 核心代码返回带HTML链接的格式化字符串
# 返回HTML链接显示用户昵称如果没有昵称则显示邮箱
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
# 方法级注释:文章链接显示
# 自定义方法,在列表页显示带链接的文章标题
# 自定义方法:显示文章链接
def link_to_article(self, obj):
# 核心代码获取文章模型的app_label和model_name
# 获取文章模型的app和model信息
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 核心代码:生成文章编辑页面的URL
# 生成文章编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# 核心代码返回带HTML链接的格式化字符串
# 返回HTML链接显示文章标题
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
# 设置自定义方法在列表页的显示名称(国际化)
# 设置自定义方法的显示名称
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -1,11 +1,8 @@
# 模块级注释Django应用配置模块
# 本模块定义了comments应用的配置信息用于Django应用注册和初始化设置
# 导入Django应用配置基类
from django.apps import AppConfig
# 类级注释:评论应用配置类
# 继承自AppConfig用于配置comments应用的基本信息和启动行为
# 定义comments应用的配置类
class CommentsConfig(AppConfig):
# 应用名称字段定义应用的完整Python路径
# 此名称用于Django内部识别和应用引用
# 指定应用的Python路径Django内部使用的标识
name = 'comments'

@ -1,24 +1,21 @@
# 模块级注释Django表单定义模块 - 评论功能
# 本模块定义了评论相关的表单类,用于前端评论数据的验证和处理
# 导入Django表单模块
from django import forms
from django.forms import ModelForm
# 导入评论模型,用于构建模型表单
# 导入评论模型
from .models import Comment
# 类级注释:评论表单类
# 继承自ModelForm基于Comment模型自动生成表单字段和验证规则
# 评论表单类继承自ModelForm
class CommentForm(ModelForm):
# 父级评论ID字段隐藏输入字段用于处理评论回复功能
# 存储被回复评论的ID用户不可见但表单会处理
# 父评论ID字段使用隐藏输入控件非必填
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
widget=forms.HiddenInput, # 使用隐藏输入控件在HTML中不可见
required=False) # 非必填字段
# 元数据类:配置模型表单的基本行为
# 定义表单的元数据
class Meta:
# 指定关联的模型Comment模型
# 指定表单对应的模型
model = Comment
# 定义表单中包含的字段:只包含评论正文字段
# 其他字段如作者、文章等通过其他方式自动设置
# 指定表单包含的字段(只包含评论正文字段)
fields = ['body']

@ -1,65 +1,57 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# 模块级注释Django数据库迁移文件
# 本模块定义了评论功能的数据库迁移操作,包括创建评论表和相关字段
# 导入Django设置
from django.conf import settings
# 导入数据库迁移相关模块
from django.db import migrations, models
import django.db.models.deletion
# 导入时间工具
import django.utils.timezone
# 类级注释:数据库迁移类
# 继承自migrations.Migration定义数据库结构变更的完整操作序列
class Migration(migrations.Migration):
# 标记为初始迁移
# 表示这是comments应用的第一个迁移文件
initial = True
# 依赖关系定义
# 指定本迁移执行前需要先完成的依赖迁移
# 声明依赖的迁移
dependencies = [
# 依赖blog应用的初始迁移确保文章表已创建
('blog', '0001_initial'),
# 依赖用户模型迁移,确保用户表已存在
('blog', '0001_initial'), # 依赖于blog应用的初始迁移
# 依赖可交换的用户模型
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 迁移操作列表
# 按顺序执行的数据库操作集合
# 定义迁移操作序列
operations = [
# 创建模型操作
# 定义Comment模型的数据库表结构
# 创建评论模型
migrations.CreateModel(
name='Comment',
fields=[
# 主键字段自增BigAutoField作为评论的唯一标识
# 主键ID自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 正文字段:存储评论内容限制最大长度300字符
# 评论正文字段最大长度300字符
('body', models.TextField(max_length=300, 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='修改时间')),
# 启用状态字段控制评论是否显示布尔类型默认True
# 是否启用字段,控制评论是否显示
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 外键字段:关联到文章模型,级联删除确保数据一致性
# 文章外键,关联到博客文章,级联删除
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
# 外键字段:关联到用户模型,记录评论作者
# 作者外键,关联到用户模型,级联删除
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
# 自关联外键:支持评论回复功能,允许空值表示顶级评论
# 父级评论外键,支持评论回复功能,允许为空
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
# 模型元选项配置
# 定义模型在admin中的显示名称和默认排序等行为
options={
# 单数显示名称在Django admin中显示的单数名称
# 模型显示名称(单数)
'verbose_name': '评论',
# 复数显示名称在Django admin中显示的复数名称
# 模型显示名称(复数)
'verbose_name_plural': '评论',
# 默认排序按ID倒序排列最新评论显示在最前面
# 默认按ID倒序排列最新的评论在前
'ordering': ['-id'],
# 最新记录定义:指定按id字段获取最新记录
# 指定获取最新记录的依据字段
'get_latest_by': 'id',
},
),

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

Loading…
Cancel
Save