Compare commits

..

4 Commits

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

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

@ -1,274 +1,166 @@
""" # 导入Django表单模块
用户认证表单模块
本模块定义用户相关的Django表单包括
- 用户登录表单
- 用户注册表单
- 密码重置表单
- 验证码表单
所有表单都包含Bootstrap样式类提供一致的用户界面体验
"""
from django import forms from django import forms
# 导入Django认证相关函数和表单
from django.contrib.auth import get_user_model, password_validation from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
# 导入验证异常类
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
# 导入表单控件
from django.forms import widgets from django.forms import widgets
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# 导入自定义工具模块
from . import utils from . import utils
# 导入自定义用户模型
from .models import BlogUser from .models import BlogUser
# 登录表单继承自Django的AuthenticationForm
class LoginForm(AuthenticationForm): class LoginForm(AuthenticationForm):
"""
用户登录表单
继承自Django的AuthenticationForm添加Bootstrap样式支持
用于用户通过用户名和密码登录系统
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" # 调用父类的初始化方法
初始化表单设置字段的widget属性添加Bootstrap样式
Args:
*args: 可变位置参数
**kwargs: 可变关键字参数
"""
# 调用父类初始化方法
super(LoginForm, self).__init__(*args, **kwargs) super(LoginForm, self).__init__(*args, **kwargs)
# 自定义用户名字段的控件属性
# 设置用户名字段的输入控件和样式
self.fields['username'].widget = widgets.TextInput( self.fields['username'].widget = widgets.TextInput(
attrs={ attrs={'placeholder': "username", "class": "form-control"})
'placeholder': "username", # 输入框占位符文本 # 自定义密码字段的控件属性
"class": "form-control" # Bootstrap表单控件样式类
})
# 设置密码字段的输入控件和样式
self.fields['password'].widget = widgets.PasswordInput( self.fields['password'].widget = widgets.PasswordInput(
attrs={ attrs={'placeholder': "password", "class": "form-control"})
'placeholder': "password", # 输入框占位符文本
"class": "form-control" # Bootstrap表单控件样式类
})
# 注册表单继承自Django的UserCreationForm
class RegisterForm(UserCreationForm): class RegisterForm(UserCreationForm):
"""
用户注册表单
继承自Django的UserCreationForm扩展邮箱字段和样式支持
用于新用户注册账号包含用户名邮箱和密码确认功能
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" # 调用父类的初始化方法
初始化表单设置所有字段的widget属性添加Bootstrap样式
Args:
*args: 可变位置参数
**kwargs: 可变关键字参数
"""
# 调用父类初始化方法
super(RegisterForm, self).__init__(*args, **kwargs) super(RegisterForm, self).__init__(*args, **kwargs)
# 设置用户名字段的输入控件和样式 # 自定义用户名字段的控件属性
self.fields['username'].widget = widgets.TextInput( self.fields['username'].widget = widgets.TextInput(
attrs={ attrs={'placeholder': "username", "class": "form-control"})
'placeholder': "username", # 输入框占位符文本 # 自定义邮箱字段的控件属性
"class": "form-control" # Bootstrap表单控件样式类
})
# 设置邮箱字段的输入控件和样式
self.fields['email'].widget = widgets.EmailInput( self.fields['email'].widget = widgets.EmailInput(
attrs={ attrs={'placeholder': "email", "class": "form-control"})
'placeholder': "email", # 输入框占位符文本 # 自定义密码字段的控件属性
"class": "form-control" # Bootstrap表单控件样式类
})
# 设置密码字段的输入控件和样式
self.fields['password1'].widget = widgets.PasswordInput( self.fields['password1'].widget = widgets.PasswordInput(
attrs={ attrs={'placeholder': "password", "class": "form-control"})
'placeholder': "password", # 输入框占位符文本 # 自定义确认密码字段的控件属性
"class": "form-control" # Bootstrap表单控件样式类
})
# 设置密码确认字段的输入控件和样式
self.fields['password2'].widget = widgets.PasswordInput( self.fields['password2'].widget = widgets.PasswordInput(
attrs={ attrs={'placeholder': "repeat password", "class": "form-control"})
'placeholder': "repeat password", # 输入框占位符文本
"class": "form-control" # Bootstrap表单控件样式类
})
# 邮箱字段的清理和验证方法
def clean_email(self): def clean_email(self):
""" # 获取清理后的邮箱数据
邮箱字段验证方法
验证邮箱是否已被注册确保邮箱地址的唯一性
Returns:
str: 验证通过的邮箱地址
Raises:
ValidationError: 当邮箱已被注册时抛出验证错误
"""
# 获取清洗后的邮箱数据
email = self.cleaned_data['email'] email = self.cleaned_data['email']
# 检查邮箱是否已存在 # 检查邮箱是否已存在
if get_user_model().objects.filter(email=email).exists(): if get_user_model().objects.filter(email=email).exists():
# 抛出验证错误,提示邮箱已存在 # 如果邮箱已存在,抛出验证错误
raise ValidationError(_("email already exists")) raise ValidationError(_("email already exists"))
# 返回验证通过的邮箱 # 返回验证通过的邮箱
return email return email
# 定义表单的元数据
class Meta: class Meta:
"""表单元数据配置""" # 指定表单对应的模型使用get_user_model获取当前用户模型
# 指定关联的用户模型
model = get_user_model() model = get_user_model()
# 指定表单包含的字段 # 指定表单包含的字段
fields = ("username", "email") fields = ("username", "email")
# 忘记密码表单继承自forms.Form
class ForgetPasswordForm(forms.Form): class ForgetPasswordForm(forms.Form):
""" # 新密码字段
忘记密码重置表单
用于用户通过邮箱和验证码重置密码包含密码强度验证和验证码校验
"""
# 新密码字段1
new_password1 = forms.CharField( new_password1 = forms.CharField(
label=_("New password"), # 字段标签 label=_("New password"), # 字段标签
widget=forms.PasswordInput( widget=forms.PasswordInput( # 使用密码输入控件
attrs={ attrs={
"class": "form-control", # Bootstrap样式类 "class": "form-control", # CSS类名
'placeholder': _("New password") # 占位符文本 'placeholder': _("New password") # 占位符文本
} }
), ),
) )
# 新密码字段2 - 用于密码确认 # 确认新密码字段
new_password2 = forms.CharField( new_password2 = forms.CharField(
label="确认密码", # 中文标签 label="确认密码", # 字段标签(中文)
widget=forms.PasswordInput( widget=forms.PasswordInput( # 使用密码输入控件
attrs={ attrs={
"class": "form-control", # Bootstrap样式类 "class": "form-control", # CSS类名
'placeholder': _("Confirm password") # 占位符文本 'placeholder': _("Confirm password") # 占位符文本
} }
), ),
) )
# 邮箱字段 - 用于标识用户和发送验证码 # 邮箱字段
email = forms.EmailField( email = forms.EmailField(
label='邮箱', # 中文标签 label='邮箱', # 字段标签(中文)
widget=forms.TextInput( widget=forms.TextInput( # 使用文本输入控件
attrs={ attrs={
'class': 'form-control', # Bootstrap样式类 'class': 'form-control', # CSS类名
'placeholder': _("Email") # 占位符文本 'placeholder': _("Email") # 占位符文本
} }
), ),
) )
# 验证码字段 - 用于验证用户身份 # 验证码字段
code = forms.CharField( code = forms.CharField(
label=_('Code'), # 字段标签 label=_('Code'), # 字段标签
widget=forms.TextInput( widget=forms.TextInput( # 使用文本输入控件
attrs={ attrs={
'class': 'form-control', # Bootstrap样式类 'class': 'form-control', # CSS类名
'placeholder': _("Code") # 占位符文本 'placeholder': _("Code") # 占位符文本
} }
), ),
) )
# 确认密码字段的清理和验证方法
def clean_new_password2(self): def clean_new_password2(self):
""" # 从原始数据中获取两个密码字段的值
密码确认字段验证方法
验证两次输入的密码是否一致并检查密码强度
Returns:
str: 验证通过的密码
Raises:
ValidationError: 当密码不匹配或强度不足时抛出验证错误
"""
# 从请求数据中获取两个密码字段的值
password1 = self.data.get("new_password1") password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2") password2 = self.data.get("new_password2")
# 检查两个密码是否匹配
# 检查两个密码是否存在且相等
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
# 密码不匹配时抛出验证错误 # 如果不匹配,抛出验证错误
raise ValidationError(_("passwords do not match")) raise ValidationError(_("passwords do not match"))
# 使用Django的密码验证器验证密码强度
# 使用Django内置密码验证器验证密码强度
password_validation.validate_password(password2) password_validation.validate_password(password2)
# 返回验证通过的密码 # 返回验证通过的密码
return password2 return password2
# 邮箱字段的清理和验证方法
def clean_email(self): def clean_email(self):
""" # 获取清理后的邮箱数据
邮箱字段验证方法
验证邮箱是否在系统中注册过
Returns:
str: 验证通过的邮箱地址
Raises:
ValidationError: 当邮箱未注册时抛出验证错误
"""
# 获取清洗后的邮箱数据
user_email = self.cleaned_data.get("email") user_email = self.cleaned_data.get("email")
# 检查邮箱是否存在对应的用户
# 检查邮箱是否存在于用户数据库中 if not BlogUser.objects.filter(
if not BlogUser.objects.filter(email=user_email).exists(): email=user_email
# TODO: 这里会暴露邮箱是否注册的信息,根据安全需求可修改提示 ).exists():
# 如果邮箱不存在,抛出验证错误
# 注释说明:这里的报错提示可能会暴露邮箱是否注册,可根据安全需求修改
raise ValidationError(_("email does not exist")) raise ValidationError(_("email does not exist"))
# 返回验证通过的邮箱
return user_email return user_email
# 验证码字段的清理和验证方法
def clean_code(self): def clean_code(self):
""" # 获取清理后的验证码数据
验证码字段验证方法
验证邮箱和验证码的匹配关系
Returns:
str: 验证通过的验证码
Raises:
ValidationError: 当验证码无效或过期时抛出验证错误
"""
# 获取清洗后的验证码数据
code = self.cleaned_data.get("code") code = self.cleaned_data.get("code")
# 使用工具函数验证验证码的有效性
# 调用utils模块的verify函数验证验证码
error = utils.verify( error = utils.verify(
email=self.cleaned_data.get("email"), # 邮箱地址 email=self.cleaned_data.get("email"), # 传入邮箱
code=code, # 验证码 code=code, # 传入验证码
) )
# 如果验证返回错误信息
# 如果验证返回错误信息,抛出验证错误
if error: if error:
# 抛出验证错误
raise ValidationError(error) raise ValidationError(error)
# 返回验证通过的验证码
return code return code
# 忘记密码验证码请求表单继承自forms.Form
class ForgetPasswordCodeForm(forms.Form): class ForgetPasswordCodeForm(forms.Form):
""" # 邮箱字段
忘记密码验证码请求表单
用于用户请求发送密码重置验证码仅包含邮箱字段
"""
# 邮箱字段 - 用于发送验证码
email = forms.EmailField( email = forms.EmailField(
label=_('Email'), # 字段标签 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 # Generated by Django 4.1.7 on 2023-03-02 07:14
# 导入Django内置模块
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
from django.db import migrations, models from django.db import migrations, models
@ -20,118 +8,65 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
""" # 初始迁移标记
用户账户应用初始迁移类
继承自migrations.Migration定义自定义用户模型的数据库表创建操作
initial = True 表示这是该应用的第一个迁移文件
主要功能
- 创建自定义用户模型BlogUser的数据库表
- 继承Django认证系统的所有基础字段
- 添加博客系统特有的自定义字段
- 设置模型的管理器和配置选项
"""
# 标记为初始迁移文件Django迁移系统会首先执行此文件
initial = True initial = True
# 定义迁移依赖关系 # 依赖的迁移文件
dependencies = [ dependencies = [
# 声明对Django认证系统组的依赖
# 使用auth应用的0012迁移文件确保用户权限系统正常工作
('auth', '0012_alter_user_first_name_max_length'), ('auth', '0012_alter_user_first_name_max_length'),
] ]
# 定义迁移操作 # 迁移操作列表
operations = [ operations = [
# 创建博客用户表的迁移操作 # 创建模型的操作
migrations.CreateModel( migrations.CreateModel(
# 模型名称 - 对应数据库表名 accounts_bloguser name='BlogUser', # 自定义用户模型名称
name='BlogUser',
# 定义模型字段列表
fields=[ fields=[
# 主键字段 - 使用BigAutoField作为自增主键 # 主键ID自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 密码字段使用Django的密码哈希存储
# 密码字段 - Django认证系统标准字段存储加密后的密码
('password', models.CharField(max_length=128, verbose_name='password')), ('password', models.CharField(max_length=128, verbose_name='password')),
# 最后登录时间,可为空
# 最后登录时间字段 - 记录用户最后一次登录的时间
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
# 超级用户标志位
# 超级用户标志字段 - 标识用户是否拥有所有权限 ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('is_superuser', models.BooleanField(default=False, # 用户名字段,具有唯一性验证和字符格式验证
help_text='Designates that this user has all permissions without explicitly assigning them.', ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
verbose_name='superuser status')), # 名字字段,可为空
# 用户名字段 - 唯一标识用户的字段,包含验证器和错误消息
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'},
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
max_length=150, unique=True,
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
verbose_name='username')),
# 名字字段 - 用户的名字(西方命名习惯)
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
# 姓氏字段,可为空
# 姓氏字段 - 用户的姓氏(西方命名习惯)
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
# 邮箱字段,可为空
# 邮箱字段 - 用户的电子邮箱地址
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
# 管理员标志位决定能否登录admin后台
# 员工状态字段 - 标识用户是否可以登录管理后台 ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_staff', models.BooleanField(default=False, # 活跃状态标志位,用于软删除
help_text='Designates whether the user can log into this admin site.', ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
verbose_name='staff status')), # 账户创建时间,默认使用当前时间
# 活跃状态字段 - 标识用户账号是否激活(软删除机制)
('is_active', models.BooleanField(default=True,
help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.',
verbose_name='active')),
# 注册时间字段 - 记录用户账号创建的时间
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
# 自定义字段:昵称
# 昵称字段 - 博客系统自定义字段,用户显示名称
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')), ('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
# 自定义字段:记录创建时间
# 创建时间字段 - 博客系统自定义字段,记录创建时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, 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='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 自定义字段:用户创建来源
# 来源字段 - 博客系统自定义字段记录用户创建来源如注册、OAuth等
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')), ('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')),
('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.', ('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')),
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')),
], ],
# 模型元数据配置 # 模型元数据配置
options={ options={
# 管理后台单数显示名称(中文) 'verbose_name': '用户', # 单数显示名称
'verbose_name': '用户', 'verbose_name_plural': '用户', # 复数显示名称
# 管理后台复数显示名称(中文) 'ordering': ['-id'], # 默认按ID倒序排列
'verbose_name_plural': '用户', 'get_latest_by': 'id', # 指定最新记录的依据字段
# 默认排序规则 - 按ID倒序排列最新的记录在前
'ordering': ['-id'],
# 指定获取最新记录的字段 - 使用id字段确定最新记录
'get_latest_by': 'id',
}, },
# 模型管理器 # 指定模型管理器
managers=[ managers=[
# 使用Django内置的UserManager管理用户对象 # 使用Django默认的用户管理器
# 提供create_user、create_superuser等用户管理方法
('objects', django.contrib.auth.models.UserManager()), ('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 # Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models from django.db import migrations, models
@ -16,85 +5,56 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
"""
用户模型字段优化迁移类
对BlogUser模型进行字段级别的优化和改进 # 声明本迁移依赖的前一个迁移文件
- 标准化字段命名约定
- 改进国际化支持
- 优化时间字段的语义清晰度
此迁移依赖于accounts应用的0001_initial迁移文件
"""
# 定义迁移依赖关系 - 依赖于本应用的初始迁移
dependencies = [ dependencies = [
# 依赖accounts应用的第一个迁移文件确保BlogUser表已创建 ('accounts', '0001_initial'), # 依赖于accounts应用中的初始迁移
('accounts', '0001_initial'),
] ]
# 定义迁移操作序列 - 按顺序执行以下数据库变更 # 定义本迁移要执行的操作序列
operations = [ operations = [
# 修改模型选项 - 更新管理后台显示名称 # 修改模型的元选项配置
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='bloguser', name='bloguser', # 指定要修改的模型名称
options={ options={
# 指定获取最新记录的字段 - 保持使用id字段 'get_latest_by': 'id', # 指定获取最新记录的依据字段为id
'get_latest_by': 'id', 'ordering': ['-id'], # 设置默认排序为按id倒序排列
# 保持默认排序规则 - 按ID倒序排列 'verbose_name': 'user', # 设置单数形式的显示名称
'ordering': ['-id'], 'verbose_name_plural': 'user', # 设置复数形式的显示名称
# 更新单数显示名称为英文
'verbose_name': 'user',
# 更新复数显示名称为英文
'verbose_name_plural': 'user'
}, },
), ),
# 删除模型中的字段:创建时间字段
# 移除字段 - 删除created_time字段
# 该字段功能被creation_time字段替代
migrations.RemoveField( migrations.RemoveField(
model_name='bloguser', model_name='bloguser', # 指定要删除字段的模型
name='created_time', name='created_time', # 要删除的字段名称
), ),
# 删除模型中的字段:最后修改时间字段
# 移除字段 - 删除last_mod_time字段
# 该字段功能被last_modify_time字段替代
migrations.RemoveField( migrations.RemoveField(
model_name='bloguser', model_name='bloguser', # 指定要删除字段的模型
name='last_mod_time', name='last_mod_time', # 要删除的字段名称
), ),
# 添加新的字段:创建时间(重命名字段)
# 添加新字段 - 创建时间字段(新命名)
migrations.AddField( migrations.AddField(
model_name='bloguser', model_name='bloguser', # 指定要添加字段的模型
name='creation_time', name='creation_time', # 新字段名称
# 使用DateTimeField存储完整的时间戳 field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # 日期时间字段,默认值为当前时间
# default参数使用Django的时区感知当前时间
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 添加新的字段:最后修改时间(重命名字段)
# 添加新字段 - 最后修改时间字段(新命名)
migrations.AddField( migrations.AddField(
model_name='bloguser', model_name='bloguser', # 指定要添加字段的模型
name='last_modify_time', name='last_modify_time', # 新字段名称
# 使用DateTimeField存储完整的时间戳 field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # 日期时间字段,默认值为当前时间
# default参数使用Django的时区感知当前时间
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
), ),
# 修改现有字段的属性:昵称字段
# 修改字段选项 - 更新昵称字段的显示名称
migrations.AlterField( migrations.AlterField(
model_name='bloguser', model_name='bloguser', # 指定要修改字段的模型
name='nickname', name='nickname', # 要修改的字段名称
# 保持字段类型和约束不变仅更新verbose_name为英文 field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), # 更新字段的显示名称
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
), ),
# 修改现有字段的属性:来源字段
# 修改字段选项 - 更新来源字段的显示名称
migrations.AlterField( migrations.AlterField(
model_name='bloguser', model_name='bloguser', # 指定要修改字段的模型
name='source', name='source', # 要修改的字段名称
# 保持字段类型和约束不变仅更新verbose_name为英文 field=models.CharField(blank=True, max_length=100, verbose_name='create source'), # 更新字段的显示名称
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
), ),
] ]

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

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

@ -1,60 +1,52 @@
""" # 导入Django URL路由相关模块
用户账户应用URL配置模块
本模块定义用户账户相关的所有URL路由包括登录注册登出
密码重置等用户认证相关的端点
URL模式使用正则表达式和路径转换器来匹配不同的用户操作请求
"""
from django.urls import path from django.urls import path
from django.urls import re_path from django.urls import re_path
# 导入视图模块 # 导入当前应用的视图模块
from . import views from . import views
# 导入自定义登录表单 # 导入自定义登录表单
from .forms import LoginForm from .forms import LoginForm
# 应用命名空间用于URL反向解析 # 定义应用命名空间
app_name = "accounts" app_name = "accounts"
# URL模式列表定义请求URL与视图的映射关系 # 定义URL模式列表
urlpatterns = [ urlpatterns = [
# 用户登录URL # 登录URL,使用正则表达式匹配
re_path(r'^login/$', # 匹配 /login/ 路径 re_path(r'^login/$',
# 使用类视图,设置登录成功后的重定向URL为首页 # 使用类视图,设置登录成功后跳转到首页
views.LoginView.as_view(success_url='/'), views.LoginView.as_view(success_url='/'),
name='login', # URL名称,用于反向解析 name='login', # URL名称
# 传递额外参数,指定使用自定义登录表单 # 传递额外参数,指定认证表单为自定义的LoginForm
kwargs={'authentication_form': LoginForm}), kwargs={'authentication_form': LoginForm}),
# 用户注册URL # 注册URL,使用正则表达式匹配
re_path(r'^register/$', # 匹配 /register/ 路径 re_path(r'^register/$',
# 使用类视图,设置注册成功后的重定向URL为首页 # 使用类视图,设置注册成功后跳转到首页
views.RegisterView.as_view(success_url="/"), views.RegisterView.as_view(success_url="/"),
name='register'), # URL名称,用于反向解析 name='register'), # URL名称
# 用户登出URL # 登出URL,使用正则表达式匹配
re_path(r'^logout/$', # 匹配 /logout/ 路径 re_path(r'^logout/$',
# 使用类视图,处理用户登出逻辑 # 使用类视图
views.LogoutView.as_view(), views.LogoutView.as_view(),
name='logout'), # URL名称,用于反向解析 name='logout'), # URL名称
# 账户操作结果页面URL # 账户结果页面URL使用path匹配精确路径
path(r'account/result.html', # 匹配 /account/result.html 路径 path(r'account/result.html',
# 使用函数视图,显示账户操作结果(如注册成功、验证结果等) # 使用函数视图
views.account_result, views.account_result,
name='result'), # URL名称,用于反向解析 name='result'), # URL名称
# 忘记密码页面URL # 忘记密码URL,使用正则表达式匹配
re_path(r'^forget_password/$', # 匹配 /forget_password/ 路径 re_path(r'^forget_password/$',
# 使用类视图,处理密码重置请求 # 使用类视图
views.ForgetPasswordView.as_view(), views.ForgetPasswordView.as_view(),
name='forget_password'), # URL名称,用于反向解析 name='forget_password'), # URL名称
# 忘记密码验证码请求URL # 忘记密码验证码请求URL,使用正则表达式匹配
re_path(r'^forget_password_code/$', # 匹配 /forget_password_code/ 路径 re_path(r'^forget_password_code/$',
# 使用类视图,处理发送密码重置验证码的请求 # 使用类视图
views.ForgetPasswordEmailCode.as_view(), 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 from django.contrib.auth import get_user_model
# 导入Django认证后端基类
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
# 自定义认证后端,允许使用邮箱或用户名登录
class EmailOrUsernameModelBackend(ModelBackend): class EmailOrUsernameModelBackend(ModelBackend):
""" """
自定义用户认证后端 - 支持用户名或邮箱登录 允许使用用户名或邮箱登录
继承自Django的ModelBackend扩展认证功能
- 允许用户使用用户名或邮箱地址进行登录
- 自动检测输入的是用户名还是邮箱格式
- 保持与Django原生认证系统的兼容性
使用场景
当用户输入包含'@'符号时系统将其识别为邮箱进行认证
否则将其识别为用户名进行认证
""" """
# 用户认证方法
def authenticate(self, request, username=None, password=None, **kwargs): 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: if '@' in username:
# 如果包含'@'符号,按邮箱处理 # 如果是邮箱格式使用email字段进行查询
kwargs = {'email': username} kwargs = {'email': username}
else: else:
# 否则按用户名处理 # 如果不是邮箱格式使用username字段进行查询
kwargs = {'username': username} kwargs = {'username': username}
try: try:
# 根据用户名或邮箱查找用户 # 根据用户名或邮箱获取用户对象
user = get_user_model().objects.get(**kwargs) user = get_user_model().objects.get(**kwargs)
# 验证密码是否正确 # 验证密码是否正确
if user.check_password(password): if user.check_password(password):
# 密码验证成功,返回用户对象 # 密码正确,返回用户对象
return user return user
# 捕获用户不存在的异常
except get_user_model().DoesNotExist: except get_user_model().DoesNotExist:
# 用户不存在返回None表示认证失败 # 用户不存在返回None
return None return None
# 根据用户ID获取用户对象的方法
def get_user(self, username): def get_user(self, username):
"""
根据用户ID获取用户对象
重写用户获取方法通过用户ID主键获取用户实例
Args:
username (int/str): 用户的ID主键值
Returns:
User: 对应的用户对象
None: 用户不存在时返回None
Note:
这里的参数名username实际上是用户ID这是为了保持与父类接口一致
"""
try: try:
# 根据主键用户ID查找用户 # 根据主键用户ID获取用户对象
return get_user_model().objects.get(pk=username) return get_user_model().objects.get(pk=username)
# 捕获用户不存在的异常
except get_user_model().DoesNotExist: except get_user_model().DoesNotExist:
# 用户不存在返回None # 用户不存在返回None
return None return None

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

@ -1,101 +1,88 @@
""" # 导入日志模块
用户账户视图模块
本模块包含用户账户相关的所有视图处理逻辑包括
- 用户注册登录登出
- 邮箱验证
- 密码重置
- 验证码发送
使用类视图和函数视图结合的方式处理用户认证流程
"""
import logging import logging
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# 导入Django配置
from django.conf import settings from django.conf import settings
# 导入Django认证相关模块
from django.contrib import auth from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth import logout from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
# 导入HTTP响应类
from django.http import HttpResponseRedirect, HttpResponseForbidden from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.http.response import HttpResponse from django.http.response import HttpResponse
# 导入快捷函数
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.shortcuts import render from django.shortcuts import render
# 导入URL反向解析
from django.urls import reverse from django.urls import reverse
# 导入方法装饰器
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
# 导入URL安全验证工具
from django.utils.http import url_has_allowed_host_and_scheme from django.utils.http import url_has_allowed_host_and_scheme
# 导入基于类的视图
from django.views import View from django.views import View
# 导入视图装饰器
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView from django.views.generic import FormView, RedirectView
# 导入自定义工具函数
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
# 导入当前应用的工具模块
from . import utils from . import utils
# 导入自定义表单
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
# 导入用户模型
from .models import BlogUser from .models import BlogUser
# 配置日志记录 # 获取日志
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RegisterView(FormView): # Create your views here.
"""
用户注册视图
处理新用户注册流程包括表单验证用户创建邮箱验证邮件发送等
"""
# 注册视图继承自FormView
class RegisterView(FormView):
# 指定使用的表单类 # 指定使用的表单类
form_class = RegisterForm form_class = RegisterForm
# 指定注册页面模板 # 指定模板文件
template_name = 'account/registration_form.html' template_name = 'account/registration_form.html'
# 使用CSRF保护装饰器
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
""" # 调用父类的dispatch方法
请求分发方法添加CSRF保护装饰器
确保注册请求受到CSRF保护防止跨站请求伪造攻击
"""
return super(RegisterView, self).dispatch(*args, **kwargs) return super(RegisterView, self).dispatch(*args, **kwargs)
# 表单验证通过后的处理
def form_valid(self, form): def form_valid(self, form):
""" # 检查表单是否有效
表单验证通过后的处理逻辑
创建新用户发送验证邮件重定向到结果页面
Args:
form: 验证通过的注册表单实例
Returns:
HttpResponseRedirect: 重定向到结果页面
"""
if form.is_valid(): if form.is_valid():
# 创建用户但不立即保存到数据库 # 保存用户但不提交到数据库commit=False
user = form.save(False) user = form.save(False)
# 设置用户为非激活状态,等待邮箱验证 # 设置用户为非活跃状态(需要邮箱验证)
user.is_active = False user.is_active = False
# 记录用户来源为注册页面 # 设置用户来源为注册
user.source = 'Register' user.source = 'Register'
# 保存用户到数据库 # 保存用户到数据库
user.save(True) user.save(True)
# 获取当前站点域名 # 获取当前站点域名
site = get_current_site().domain site = get_current_site().domain
# 生成邮箱验证签名 # 生成验证签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 调试模式下使用本地地址 # 如果是调试模式,使用本地地址
if settings.DEBUG: if settings.DEBUG:
site = '127.0.0.1:8000' site = '127.0.0.1:8000'
# 获取结果页面的URL路径
# 构建验证URL
path = reverse('account:result') path = reverse('account:result')
# 构建完整的验证URL
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign) site=site, path=path, id=user.id, sign=sign)
@ -110,7 +97,6 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器 如果上面链接无法打开请将此链接复制至浏览器
{url} {url}
""".format(url=url) """.format(url=url)
# 发送验证邮件 # 发送验证邮件
send_email( send_email(
emailto=[ emailto=[
@ -119,217 +105,168 @@ class RegisterView(FormView):
title='验证您的电子邮箱', title='验证您的电子邮箱',
content=content) content=content)
# 重定向到注册结果页面 # 构建注册成功重定向URL
url = reverse('accounts:result') + \ url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id) '?type=register&id=' + str(user.id)
# 重定向到结果页面
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
# 表单验证失败,重新渲染表单页面 # 表单无效,重新渲染表单页面
return self.render_to_response({ return self.render_to_response({
'form': form 'form': form
}) })
# 登出视图继承自RedirectView
class LogoutView(RedirectView): class LogoutView(RedirectView):
""" # 设置登出后重定向的URL
用户登出视图
处理用户登出逻辑清理会话和缓存
"""
# 登出后重定向到的URL
url = '/login/' url = '/login/'
# 使用永不缓存装饰器
@method_decorator(never_cache) @method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""
请求分发方法添加不缓存装饰器
确保登出页面不会被浏览器缓存
"""
return super(LogoutView, self).dispatch(request, *args, **kwargs) return super(LogoutView, self).dispatch(request, *args, **kwargs)
# 处理GET请求
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
""" # 执行登出操作
处理GET请求的登出逻辑
执行用户登出操作清理侧边栏缓存
"""
# 执行用户登出
logout(request) logout(request)
# 清理侧边栏缓存 # 删除侧边栏缓存
delete_sidebar_cache() delete_sidebar_cache()
# 调用父类方法进行重定向 # 调用父类的GET方法进行重定向
return super(LogoutView, self).get(request, *args, **kwargs) return super(LogoutView, self).get(request, *args, **kwargs)
# 登录视图继承自FormView
class LoginView(FormView): class LoginView(FormView):
"""
用户登录视图
处理用户登录认证支持记住登录状态功能
"""
# 指定使用的表单类 # 指定使用的表单类
form_class = LoginForm form_class = LoginForm
# 指定登录页面模板 # 指定模板文件
template_name = 'account/login.html' template_name = 'account/login.html'
# 登录成功后的默认重定向URL # 设置登录成功后的默认重定向URL
success_url = '/' success_url = '/'
# 重定向字段名 # 重定向字段名
redirect_field_name = REDIRECT_FIELD_NAME redirect_field_name = REDIRECT_FIELD_NAME
# 记住登录状态的会话有效期(一个月) # 登录会话有效期(一个月)
login_ttl = 2626560 login_ttl = 2626560
@method_decorator(sensitive_post_parameters('password')) # 使用多个装饰器保护视图
@method_decorator(csrf_protect) @method_decorator(sensitive_post_parameters('password')) # 敏感参数保护
@method_decorator(never_cache) @method_decorator(csrf_protect) # CSRF保护
@method_decorator(never_cache) # 永不缓存
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""
请求分发方法添加安全装饰器
- sensitive_post_parameters: 保护密码参数
- csrf_protect: CSRF保护
- never_cache: 禁止缓存
"""
return super(LoginView, self).dispatch(request, *args, **kwargs) return super(LoginView, self).dispatch(request, *args, **kwargs)
# 获取上下文数据
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" # 获取重定向URL
获取模板上下文数据
添加重定向URL到上下文
"""
# 从GET参数获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name) redirect_to = self.request.GET.get(self.redirect_field_name)
# 如果没有重定向URL使用默认首页
if redirect_to is None: if redirect_to is None:
redirect_to = '/' redirect_to = '/'
# 将重定向URL添加到上下文
kwargs['redirect_to'] = redirect_to kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs) return super(LoginView, self).get_context_data(**kwargs)
# 表单验证通过后的处理
def form_valid(self, form): def form_valid(self, form):
""" # 创建认证表单实例
表单验证通过后的处理逻辑
执行用户登录认证处理记住登录状态
"""
# 使用Django认证表单进行验证
form = AuthenticationForm(data=self.request.POST, request=self.request) form = AuthenticationForm(data=self.request.POST, request=self.request)
# 检查表单是否有效
if form.is_valid(): if form.is_valid():
# 清理侧边栏缓存 # 删除侧边栏缓存
delete_sidebar_cache() delete_sidebar_cache()
# 记录日志
logger.info(self.redirect_field_name) logger.info(self.redirect_field_name)
# 执行用户登录 # 执行登录操作
auth.login(self.request, form.get_user()) auth.login(self.request, form.get_user())
# 如果用户选择"记住我",设置会话过期时间
# 处理记住登录状态
if self.request.POST.get("remember"): if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl) self.request.session.set_expiry(self.login_ttl)
# 调用父类的form_valid方法进行重定向
return super(LoginView, self).form_valid(form) return super(LoginView, self).form_valid(form)
else: else:
# 登录失败,重新渲染登录页面 # 表单无效,重新渲染表单页面
return self.render_to_response({ return self.render_to_response({
'form': form 'form': form
}) })
# 获取成功登录后的重定向URL
def get_success_url(self): def get_success_url(self):
""" # 从POST数据中获取重定向URL
获取登录成功后的重定向URL
验证重定向URL的安全性防止开放重定向攻击
"""
# 从POST数据获取重定向URL
redirect_to = self.request.POST.get(self.redirect_field_name) redirect_to = self.request.POST.get(self.redirect_field_name)
# 验证重定向URL是否安全
# 验证URL是否安全同源策略
if not url_has_allowed_host_and_scheme( if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[ url=redirect_to, allowed_hosts=[
self.request.get_host()]): self.request.get_host()]):
# 如果不安全使用默认成功URL
redirect_to = self.success_url redirect_to = self.success_url
return redirect_to return redirect_to
# 账户结果页面视图函数
def account_result(request): def account_result(request):
""" # 获取结果类型
账户操作结果页面视图
显示注册结果或处理邮箱验证
Args:
request: HTTP请求对象
Returns:
HttpResponse: 结果页面响应
"""
# 获取操作类型和用户ID
type = request.GET.get('type') type = request.GET.get('type')
# 获取用户ID
id = request.GET.get('id') id = request.GET.get('id')
# 获取用户对象,不存在返回404 # 获取用户对象如果不存在返回404
user = get_object_or_404(get_user_model(), id=id) user = get_object_or_404(get_user_model(), id=id)
# 记录日志
logger.info(type) logger.info(type)
# 如果用户已经是活跃状态,重定向到首页
# 如果用户已激活,重定向到首页
if user.is_active: if user.is_active:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
# 处理注册和验证类型 # 处理注册和验证类型
if type and type in ['register', 'validation']: if type and type in ['register', 'validation']:
if type == 'register': if type == 'register':
# 注册成功页面 # 注册成功的内容
content = ''' content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站 恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
''' '''
title = '注册成功' title = '注册成功'
else: else:
# 邮箱验证处理 # 生成验证签名
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 获取请求中的签名
sign = request.GET.get('sign') sign = request.GET.get('sign')
# 验证签名 # 验证签名是否匹配
if sign != c_sign: if sign != c_sign:
return HttpResponseForbidden() return HttpResponseForbidden()
# 激活用户账 # 激活用户账
user.is_active = True user.is_active = True
user.save() user.save()
# 验证成功的内容
content = ''' content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站 恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
''' '''
title = '验证成功' title = '验证成功'
# 渲染结果页面 # 渲染结果页面
return render(request, 'account/result.html', { return render(request, 'account/result.html', {
'title': title, 'title': title,
'content': content 'content': content
}) })
else: else:
# 无效类型,重定向到首页 # 其他情况重定向到首页
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
# 忘记密码视图继承自FormView
class ForgetPasswordView(FormView): class ForgetPasswordView(FormView):
"""
忘记密码重置视图
处理用户密码重置请求验证验证码并更新密码
"""
# 指定使用的表单类 # 指定使用的表单类
form_class = ForgetPasswordForm form_class = ForgetPasswordForm
# 指定模板名称 # 指定模板文件
template_name = 'account/forget_password.html' template_name = 'account/forget_password.html'
# 表单验证通过后的处理
def form_valid(self, form): def form_valid(self, form):
""" # 检查表单是否有效
表单验证通过后的处理逻辑
重置用户密码并重定向到登录页面
"""
if form.is_valid(): if form.is_valid():
# 根据邮箱查找用户 # 根据邮箱获取用户对象
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# 使用新密码的哈希值更新用户密码 # 使用新密码的哈希值更新用户密码
blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.password = make_password(form.cleaned_data["new_password2"])
@ -338,32 +275,21 @@ class ForgetPasswordView(FormView):
# 重定向到登录页面 # 重定向到登录页面
return HttpResponseRedirect('/login/') return HttpResponseRedirect('/login/')
else: else:
# 表单验证失败,重新渲染表单 # 表单无效,重新渲染表单页面
return self.render_to_response({'form': form}) return self.render_to_response({'form': form})
# 忘记密码验证码发送视图继承自View
class ForgetPasswordEmailCode(View): class ForgetPasswordEmailCode(View):
"""
忘记密码验证码发送视图
处理密码重置验证码的发送请求
"""
# 处理POST请求
def post(self, request: HttpRequest): def post(self, request: HttpRequest):
""" # 创建表单实例
处理POST请求发送密码重置验证码
Args:
request: HTTP请求对象
Returns:
HttpResponse: 操作结果响应
"""
# 验证表单数据
form = ForgetPasswordCodeForm(request.POST) form = ForgetPasswordCodeForm(request.POST)
# 检查表单是否有效
if not form.is_valid(): if not form.is_valid():
# 表单无效,返回错误信息
return HttpResponse("错误的邮箱") return HttpResponse("错误的邮箱")
# 获取邮箱地址 # 获取邮箱地址
to_email = form.cleaned_data["email"] to_email = form.cleaned_data["email"]
@ -371,7 +297,8 @@ class ForgetPasswordEmailCode(View):
code = generate_code() code = generate_code()
# 发送验证邮件 # 发送验证邮件
utils.send_verify_email(to_email, code) utils.send_verify_email(to_email, code)
# 验证码到缓存 # 存验证码到缓存
utils.set_code(to_email, code) utils.set_code(to_email, code)
# 返回成功响应
return HttpResponse("ok") 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 from django import forms
# 导入Django管理员模块
from django.contrib import admin from django.contrib import admin
# 导入获取用户模型的函数
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
# 导入URL反向解析
from django.urls import reverse from django.urls import reverse
# 导入HTML格式化工具
from django.utils.html import format_html from django.utils.html import format_html
# 导入国际化翻译函数
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
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): class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
# 定义表单的元数据
class Meta: class Meta:
# 指定表单对应的模型
model = Article model = Article
fields = '__all__' # 表示表单包含模型的所有字段 # 包含所有字段
fields = '__all__'
# 批量操作:发布文章
# 发布文章的管理动作函数
def makr_article_publish(modeladmin, request, queryset): def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p') # 将选中文章状态改为 'p'ublished queryset.update(status='p')
makr_article_publish.short_description = _('发布选中的文章')
# 批量操作:草稿文章 # 将文章设为草稿的管理动作函数
def draft_article(modeladmin, request, queryset): def draft_article(modeladmin, request, queryset):
queryset.update(status='d') # 草稿状态 queryset.update(status='d')
draft_article.short_description = _('将选中文章设为草稿')
# 批量操作:关闭文章评论
# 关闭文章评论的管理动作函数
def close_article_commentstatus(modeladmin, request, queryset): def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c') # 关闭评论 queryset.update(comment_status='c')
close_article_commentstatus.short_description = _('关闭文章评论')
# 批量操作:开放文章评论 # 打开文章评论的管理动作函数
def open_article_commentstatus(modeladmin, request, queryset): def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o') # 开放评论 queryset.update(comment_status='o')
open_article_commentstatus.short_description = _('开放文章评论')
# 设置管理动作的显示名称
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条 class ArticlelAdmin(admin.ModelAdmin):
search_fields = ('body', 'title') # 可以搜索正文和标题 # 每页显示20条记录
list_per_page = 20
# 搜索字段
search_fields = ('body', 'title')
# 指定使用的表单
form = ArticleForm form = ArticleForm
list_display = ( # 列表页显示的字段 # 列表页显示的字段
'id', 'title', 'author', 'link_to_category', 'creation_time', list_display = (
'views', 'like_count', 'status', 'type', 'article_order' 'id',
) 'title',
list_display_links = ('id', 'title') # 哪些字段可点击进入编辑页 'author',
list_filter = ('status', 'type', 'category') # 右侧过滤器 'link_to_category',
date_hierarchy = 'creation_time' # 按创建时间分层筛选 'creation_time',
filter_horizontal = ('tags',) # 多对多字段(标签)以横向过滤器展示 'views',
exclude = ('creation_time', 'last_modify_time') # 后台不显示这两个字段 'status',
view_on_site = True # 显示“查看站点”按钮 'type',
actions = [makr_article_publish, draft_article, close_article_commentstatus, open_article_commentstatus] # 批量操作 'article_order')
raw_id_fields = ('author', 'category') # 作者和分类以 ID 输入框展示,适合外键多的情况 # 可作为链接点击的字段
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): def link_to_category(self, obj):
# 获取分类模型的app和model信息
info = (obj.category._meta.app_label, obj.category._meta.model_name) info = (obj.category._meta.app_label, obj.category._meta.model_name)
# 生成分类编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) 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)) 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): def get_form(self, request, obj=None, **kwargs):
form = super(ArticleAdmin, self).get_form(request, obj, **kwargs) form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model().objects.filter(is_superuser=True) # 限制作者字段只能选择超级用户
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form 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): def get_view_on_site_url(self, obj=None):
if obj: if obj:
# 返回文章的完整URL
url = obj.get_full_url() url = obj.get_full_url()
return url return url
else: else:
# 如果没有指定对象,返回站点域名
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
site = get_current_site().domain site = get_current_site().domain
return site return site
# 其它模型(如 Tag、Category、Links、SideBar、BlogSettings的 Admin 配置
# 标签模型的管理类
class TagAdmin(admin.ModelAdmin): class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time') # 排除的字段
exclude = ('slug', 'last_modify_time', 'creation_time')
# 分类模型的管理类
class CategoryAdmin(admin.ModelAdmin): class CategoryAdmin(admin.ModelAdmin):
# 列表页显示的字段
list_display = ('name', 'parent_category', 'index') list_display = ('name', 'parent_category', 'index')
# 排除的字段
exclude = ('slug', 'last_modify_time', 'creation_time')
# 链接模型的管理类
class LinksAdmin(admin.ModelAdmin): class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time') # 排除的字段
exclude = ('last_modify_time', 'creation_time')
# 侧边栏模型的管理类
class SideBarAdmin(admin.ModelAdmin): class SideBarAdmin(admin.ModelAdmin):
# 列表页显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence') list_display = ('name', 'content', 'is_enable', 'sequence')
# 排除的字段
exclude = ('last_modify_time', 'creation_time')
# 博客设置模型的管理类
class BlogSettingsAdmin(admin.ModelAdmin): class BlogSettingsAdmin(admin.ModelAdmin):
pass # 博客设置后台,暂时无特殊配置 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')

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

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

@ -1,401 +1,245 @@
# 导入时间模块
import time import time
import logging
# 导入Elasticsearch客户端
import elasticsearch.client import elasticsearch.client
# 导入Django配置
from django.conf import settings from django.conf import settings
# 导入Elasticsearch DSL相关类
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections from elasticsearch_dsl.connections import connections
# 导入博客文章模型
from blog.models import Article from blog.models import Article
logger = logging.getLogger(__name__) # 检查是否启用了Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') # 是否启用 Elasticsearch
# 如果启用,则建立连接
if ELASTICSEARCH_ENABLED: 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 from elasticsearch import Elasticsearch
# 创建Elasticsearch客户端实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient from elasticsearch.client import IngestClient
# 创建Ingest客户端用于管道处理
c = IngestClient(es) c = IngestClient(es)
try: try:
# 检查geoip管道是否存在
c.get_pipeline('geoip') c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError: except elasticsearch.exceptions.NotFoundError:
c.put_pipeline('geoip', body=''' # 如果不存在则创建geoip管道
{ c.put_pipeline('geoip', body='''{
"description": "添加IP地理位置信息", "description" : "Add geoip info",
"processors": [ "processors" : [
{ "geoip": { "field": "ip" } } {
] "geoip" : {
} "field" : "ip"
''') }
}
]
# 定义 IP 地理位置信息内部文档 }''')
# 定义GeoIP内部文档类
class GeoIp(InnerDoc): class GeoIp(InnerDoc):
continent_name = Keyword() continent_name = Keyword() # 大洲名称
country_iso_code = Keyword() country_iso_code = Keyword() # 国家ISO代码
country_name = Keyword() country_name = Keyword() # 国家名称
location = GeoPoint() location = GeoPoint() # 地理位置坐标
# 用户代理浏览器/设备/操作系统)相关内部类 # 定义用户代理浏览器内部文档
class UserAgentBrowser(InnerDoc): class UserAgentBrowser(InnerDoc):
Family = Keyword() Family = Keyword() # 浏览器家族
Version = Keyword() Version = Keyword() # 浏览器版本
# 定义用户代理操作系统内部文档类继承自UserAgentBrowser
class UserAgentOS(UserAgentBrowser): class UserAgentOS(UserAgentBrowser):
pass pass
# 定义用户代理设备内部文档类
class UserAgentDevice(InnerDoc): class UserAgentDevice(InnerDoc):
Family = Keyword() Family = Keyword() # 设备家族
Brand = Keyword() Brand = Keyword() # 设备品牌
Model = Keyword() Model = Keyword() # 设备型号
# 定义用户代理内部文档类
class UserAgent(InnerDoc): class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False) browser = Object(UserAgentBrowser, required=False) # 浏览器信息
os = Object(UserAgentOS, required=False) os = Object(UserAgentOS, required=False) # 操作系统信息
device = Object(UserAgentDevice, required=False) device = Object(UserAgentDevice, required=False) # 设备信息
string = Text() string = Text() # 原始用户代理字符串
is_bot = Boolean() is_bot = Boolean() # 是否为机器人
# 性能监控文档:记录每个请求的 URL、耗时、IP、用户代理等 # 定义耗时记录文档类
class ElapsedTimeDocument(Document): class ElapsedTimeDocument(Document):
url = Keyword() url = Keyword() # 请求URL
time_taken = Long() # 请求耗时(毫秒) time_taken = Long() # 耗时(毫秒)
log_datetime = Date() log_datetime = Date() # 日志时间
ip = Keyword() ip = Keyword() # IP地址
geoip = Object(GeoIp, required=False) geoip = Object(GeoIp, required=False) # GeoIP信息
useragent = Object(UserAgent, required=False) useragent = Object(UserAgent, required=False) # 用户代理信息
class Index: class Index:
name = 'performance' name = 'performance' # 索引名称
settings = {"number_of_shards": 1, "number_of_replicas": 0} 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): class ArticleDocument(Document):
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 使用IK中文分词 body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 正文使用IK分词器
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 标题使用IK分词器
author = Object(properties={'nickname': Text(analyzer='ik_max_word'), 'id': Integer()}) author = Object(properties={
category = Object(properties={'name': Text(analyzer='ik_max_word'), 'id': Integer()}) 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 作者昵称
tags = Object(properties={'name': Text(analyzer='ik_max_word'), 'id': Integer()}) 'id': Integer() # 作者ID
pub_time = Date() })
status = Text() category = Object(properties={
comment_status = Text() 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 分类名称
type = Text() 'id': Integer() # 分类ID
views = Integer() })
article_order = Integer() 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: class Index:
name = 'blog' name = 'blog' # 索引名称
settings = {"number_of_shards": 1, "number_of_replicas": 0} 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
def create_all_indices(self): class Meta:
""" doc_type = 'Article' # 文档类型
创建所有索引
"""
if not self.es_enabled:
return False
try:
# 创建文章索引
ArticleDocument.init()
# 创建性能监控索引 # 文章文档管理器类
ElapsedTimeDocument.init() class ArticleDocumentManager():
logger.info("All Elasticsearch indices created successfully") def __init__(self):
return True self.create_index()
except Exception as e: def create_index(self):
logger.error(f"Failed to create indices: {e}") # 创建文章索引
return False ArticleDocument.init()
def delete_all_indices(self): def delete_index(self):
""" from elasticsearch import Elasticsearch
删除所有索引 es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
""" # 删除索引忽略400和404错误
if not self.es_enabled: es.indices.delete(index='blog', ignore=[400, 404])
return False
def convert_to_doc(self, articles):
try: # 将文章模型转换为文档对象
ArticleDocument._index.delete(ignore=404) return [
ElapsedTimeDocument._index.delete(ignore=404) ArticleDocument(
meta={
logger.info("All Elasticsearch indices deleted successfully") 'id': article.id}, # 使用文章ID作为文档ID
return True body=article.body,
title=article.title,
except Exception as e: author={
logger.error(f"Failed to delete indices: {e}") 'nickname': article.author.username,
return False 'id': article.author.id},
category={
def refresh_all_indices(self): 'name': article.category.name,
""" 'id': article.category.id},
刷新所有索引 tags=[
""" {
if not self.es_enabled: 'name': t.name,
return False 'id': t.id} for t in article.tags.all()], # 转换标签列表
pub_time=article.pub_time,
try: status=article.status,
from elasticsearch.client import IndicesClient comment_status=article.comment_status,
client = IndicesClient(connections.get_connection()) type=article.type,
client.refresh(index='_all') views=article.views,
article_order=article.article_order) for article in articles]
logger.info("All Elasticsearch indices refreshed successfully")
return True def rebuild(self, articles=None):
# 重建索引
except Exception as e: ArticleDocument.init()
logger.error(f"Failed to refresh indices: {e}") articles = articles if articles else Article.objects.all() # 如果没有指定文章,则获取所有文章
return False docs = self.convert_to_doc(articles) # 转换为文档对象
for doc in docs:
def get_cluster_health(self): doc.save() # 保存文档
"""
获取集群健康状态 def update_docs(self, docs):
""" # 更新文档
if not self.es_enabled: for doc in docs:
return {} doc.save()
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()

@ -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): class BlogSearchForm(SearchForm):
# 定义查询数据字段,设置为必填
querydata = forms.CharField(required=True) querydata = forms.CharField(required=True)
# 重写搜索方法
def search(self): 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 from django.core.management.base import BaseCommand
# 导入 Elasticsearch 相关的文档模型和管理器 # 导入Elasticsearch相关文档和管理器
from blog.documents import ( from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ElapsedTimeDocument, # 假设是一个时间相关的文档模型 ELASTICSEARCH_ENABLED
ArticleDocumentManager, # 文章的文档管理器,用于操作 Elasticsearch 中的文章索引
ElaspedTimeDocumentManager, # 时间相关的文档管理器注意疑似拼写错误应为ElapsedTime
ELASTICSEARCH_ENABLED # 全局开关,控制是否启用 Elasticsearch 功能
)
# TODO 参数化
# 自定义管理命令类,用于构建搜索索引
class Command(BaseCommand): class Command(BaseCommand):
help = '构建搜索索引' # 命令的帮助信息,显示在 python manage.py help 中 # 命令的帮助信息
help = 'build search index'
# 命令的主要处理逻辑
def handle(self, *args, **options): def handle(self, *args, **options):
""" # 检查Elasticsearch是否启用
命令的主要执行逻辑 if ELASTICSEARCH_ENABLED:
""" # 构建耗时文档的索引
if ELASTICSEARCH_ENABLED: # 只有在启用了 Elasticsearch 的情况下才执行
# 构建 “时间” 相关的索引
ElaspedTimeDocumentManager.build_index() ElaspedTimeDocumentManager.build_index()
# 创建耗时文档管理器实例并初始化
# 获取并初始化 “时间” 相关的文档管理对象
manager = ElapsedTimeDocument() manager = ElapsedTimeDocument()
manager.init() # 初始化索引或相关数据 manager.init()
# 创建文章文档管理器实例
# 获取文章的文档管理器,并先删除旧索引,然后重建新的文章索引
manager = ArticleDocumentManager() 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 from django.core.management.base import BaseCommand
# 从 blog 应用中导入 Tag 和 Category 模型 # 导入博客模型
from blog.models import Tag, Category from blog.models import Tag, Category
# TODO 参数化
# 自定义管理命令类,用于构建搜索关键词
class Command(BaseCommand): class Command(BaseCommand):
help = '构建搜索关键词' # 用于生成所有标签和分类名称,作为搜索关键词 # 命令的帮助信息
help = 'build search words'
# 命令的主要处理逻辑
def handle(self, *args, **options): def handle(self, *args, **options):
""" # 构建数据集:获取所有标签名称和分类名称,并使用集合去重
获取所有标签和分类的名称去重后打印出来供后续用作搜索词库 datas = set([t.name for t in Tag.objects.all()] +
""" [t.name for t in Category.objects.all()])
# 取出所有 Tag 的名称 和 所有 Category 的名称,放入一个集合中自动去重 # 将去重后的数据按行打印输出
datas = set([
t.name for t in Tag.objects.all() # 所有标签名称
+
[t.name for t in Category.objects.all()] # 所有分类名称
])
# 将所有关键词用换行符连接,并打印到控制台
print('\n'.join(datas)) print('\n'.join(datas))

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

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

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

@ -1,67 +1,65 @@
import time # 添加这行 # 导入日志模块
import logging 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): def __init__(self, get_response=None):
# 初始化中间件保存get_response函数
self.get_response = get_response self.get_response = get_response
super().__init__()
def __call__(self, request): def __call__(self, request):
''' page render time '''
# 记录请求开始时间 # 记录请求开始时间
start_time = time.time() start_time = time.time()
# 调用后续中间件和视图处理请求,获取响应
# 处理请求
response = self.get_response(request) response = self.get_response(request)
# 获取用户代理字符串
# 计算耗时,记录并显示到页面 http_user_agent = request.META.get('HTTP_USER_AGENT', '')
duration = time.time() - start_time # 获取客户端IP地址
ip, _ = get_client_ip(request)
# 获取客户端IP # 解析用户代理信息
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') user_agent = parse(http_user_agent)
if x_forwarded_for: # 检查响应是否为流式响应(非流式响应才能处理内容)
ip = x_forwarded_for.split(',')[0] if not response.streaming:
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:
try: try:
from blog.documents import ElapsedTimeDocument # 计算页面渲染耗时
doc = ElapsedTimeDocument( cast_time = time.time() - start_time
url=request.path, # 如果启用了Elasticsearch
time_taken=int(duration * 1000), if ELASTICSEARCH_ENABLED:
log_datetime=timezone.now(), # 将耗时转换为毫秒并保留两位小数
ip=ip, time_taken = round((cast_time) * 1000, 2)
useragent={'string': user_agent} # 获取请求的URL路径
) url = request.path
doc.save() # 导入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: except Exception as e:
logger.warning(f"Failed to save to Elasticsearch: {e}") # 记录处理过程中的错误
logger.error("Error OnlineMiddleware: %s" % e)
# 添加处理时间到响应头
response['X-Response-Time'] = f'{duration:.3f}s'
# 返回响应
return response return response
def process_exception(self, request, exception):
"""处理异常"""
logger.error(f"Middleware exception: {exception}")
return None

@ -1,146 +1,226 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14 # Generated by Django 4.1.7 on 2023-03-02 07:14
# 导入Django设置
from django.conf import settings from django.conf import settings
# 导入数据库迁移相关模块
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
# 导入时间工具
import django.utils.timezone import django.utils.timezone
# 导入Markdown编辑器字段
import mdeditor.fields import mdeditor.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True # 表示这是第一个迁移文件 # 标记为初始迁移
initial = True
# 声明依赖的迁移
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型,通常是内置的 User 或自定义用户模型 # 依赖可交换的用户模型
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
# 定义迁移操作序列
operations = [ operations = [
# 创建 BlogSettings 模型:网站全局配置表 # 创建网站配置模型
migrations.CreateModel( migrations.CreateModel(
name='BlogSettings', name='BlogSettings',
fields=[ fields=[
# 主键ID自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 网站名称字段
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')), ('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
# 网站描述字段
('site_description', models.TextField(default='', max_length=1000, 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_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
# 网站关键词字段
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')), ('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
# 文章摘要长度字段
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')), ('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
# 侧边栏文章数量字段
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')), ('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
# 侧边栏评论数量字段
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')), ('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
# 文章页面评论数量字段
('article_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='是否显示谷歌广告')), ('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
# 谷歌广告代码字段
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, 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='是否打开网站评论功能')), ('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
# 备案号字段
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=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='网站统计代码')), ('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
# 是否显示公安备案字段
('show_gongan_code', models.BooleanField(default=False, 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='公安备案号')), ('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
], ],
options={ options={
# 模型显示名称(单数)
'verbose_name': '网站配置', 'verbose_name': '网站配置',
# 模型显示名称(复数)
'verbose_name_plural': '网站配置', 'verbose_name_plural': '网站配置',
}, },
), ),
# 创建友情链接模型
# 创建 Links 模型:友情链接
migrations.CreateModel( migrations.CreateModel(
name='Links', name='Links',
fields=[ fields=[
# 主键ID自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 链接名称字段,唯一
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')), ('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
# 链接地址字段
('link', models.URLField(verbose_name='链接地址')), ('link', models.URLField(verbose_name='链接地址')),
# 排序字段,唯一
('sequence', models.IntegerField(unique=True, verbose_name='排序')), ('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否启用字段
('is_enable', models.BooleanField(default=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='显示类型')), ('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='创建时间')), ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
], ],
options={ options={
# 模型显示名称(单数)
'verbose_name': '友情链接', 'verbose_name': '友情链接',
# 模型显示名称(复数)
'verbose_name_plural': '友情链接', 'verbose_name_plural': '友情链接',
# 默认按排序字段升序排列
'ordering': ['sequence'], 'ordering': ['sequence'],
}, },
), ),
# 创建侧边栏模型
# 创建 SideBar 模型:侧边栏内容
migrations.CreateModel( migrations.CreateModel(
name='SideBar', name='SideBar',
fields=[ fields=[
# 主键ID自增BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 标题字段
('name', models.CharField(max_length=100, verbose_name='标题')), ('name', models.CharField(max_length=100, verbose_name='标题')),
# 内容字段
('content', models.TextField(verbose_name='内容')), ('content', models.TextField(verbose_name='内容')),
# 排序字段,唯一
('sequence', models.IntegerField(unique=True, verbose_name='排序')), ('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否启用字段
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')), ('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
# 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, 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='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
], ],
options={ options={
# 模型显示名称(单数)
'verbose_name': '侧边栏', 'verbose_name': '侧边栏',
# 模型显示名称(复数)
'verbose_name_plural': '侧边栏', 'verbose_name_plural': '侧边栏',
# 默认按排序字段升序排列
'ordering': ['sequence'], 'ordering': ['sequence'],
}, },
), ),
# 创建标签模型
# 创建 Tag 模型:文章标签
migrations.CreateModel( migrations.CreateModel(
name='Tag', name='Tag',
fields=[ fields=[
# 主键ID自增AutoField
('id', models.AutoField(primary_key=True, serialize=False)), ('id', models.AutoField(primary_key=True, serialize=False)),
# 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, 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='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 标签名称字段,唯一
('name', models.CharField(max_length=30, unique=True, 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)), ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
], ],
options={ options={
# 模型显示名称(单数)
'verbose_name': '标签', 'verbose_name': '标签',
# 模型显示名称(复数)
'verbose_name_plural': '标签', 'verbose_name_plural': '标签',
# 默认按名称升序排列
'ordering': ['name'], 'ordering': ['name'],
}, },
), ),
# 创建分类模型
# 创建 Category 模型:文章分类
migrations.CreateModel( migrations.CreateModel(
name='Category', name='Category',
fields=[ fields=[
# 主键ID自增AutoField
('id', models.AutoField(primary_key=True, serialize=False)), ('id', models.AutoField(primary_key=True, serialize=False)),
# 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, 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='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 分类名称字段,唯一
('name', models.CharField(max_length=30, unique=True, 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)), ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
# 权重排序字段,越大越靠前
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')), ('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='父级分类')), ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
], ],
options={ options={
# 模型显示名称(单数)
'verbose_name': '分类', 'verbose_name': '分类',
# 模型显示名称(复数)
'verbose_name_plural': '分类', 'verbose_name_plural': '分类',
# 默认按权重倒序排列
'ordering': ['-index'], 'ordering': ['-index'],
}, },
), ),
# 创建文章模型
# 创建 Article 模型:文章内容
migrations.CreateModel( migrations.CreateModel(
name='Article', name='Article',
fields=[ fields=[
# 主键ID自增AutoField
('id', models.AutoField(primary_key=True, serialize=False)), ('id', models.AutoField(primary_key=True, serialize=False)),
# 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, 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='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 文章标题字段,唯一
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')), ('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
# 文章正文字段使用Markdown编辑器
('body', mdeditor.fields.MDTextField(verbose_name='正文')), ('body', mdeditor.fields.MDTextField(verbose_name='正文')),
# 发布时间字段
('pub_time', models.DateTimeField(default=django.utils.timezone.now, 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='文章状态')), ('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='评论状态')), ('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='类型')), ('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
# 浏览量字段,正整数
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')), ('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
# 文章排序字段,数字越大越靠前
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')), ('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
# 是否显示TOC目录字段
('show_toc', models.BooleanField(default=False, 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='作者')), ('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='分类')), ('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='标签集合')), ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
], ],
options={ options={
# 模型显示名称(单数)
'verbose_name': '文章', 'verbose_name': '文章',
# 模型显示名称(复数)
'verbose_name_plural': '文章', 'verbose_name_plural': '文章',
# 默认按文章排序倒序、发布时间倒序排列
'ordering': ['-article_order', '-pub_time'], 'ordering': ['-article_order', '-pub_time'],
# 指定获取最新记录的依据字段
'get_latest_by': 'id', 'get_latest_by': 'id',
}, },
), ),

@ -1,25 +1,27 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08 # Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
# 声明本迁移依赖的前一个迁移文件
dependencies = [ dependencies = [
('blog', '0001_initial'), # 依赖于第一个迁移文件 ('blog', '0001_initial'), # 依赖于blog应用的初始迁移
] ]
# 定义本迁移要执行的操作序列
operations = [ operations = [
# 字段:global_footer用于存放网站公共尾部 HTML 内容(如版权信息等) # 向BlogSettings模型添加新字段:公共尾部
migrations.AddField( migrations.AddField(
model_name='blogsettings', model_name='blogsettings', # 指定要修改的模型名称
name='global_footer', name='global_footer', # 新字段名称
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), # 文本字段,允许为空,默认值为空字符串
), ),
# 向BlogSettings模型添加新字段公共头部
# 新增字段global_header用于存放网站公共头部 HTML 内容(如导航栏上面的内容)
migrations.AddField( migrations.AddField(
model_name='blogsettings', model_name='blogsettings', # 指定要修改的模型名称
name='global_header', name='global_header', # 新字段名称
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), 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 # Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
# 声明本迁移依赖的前一个迁移文件
dependencies = [ dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'), # 依赖于上一个迁移 ('blog', '0002_blogsettings_global_footer_and_more'), # 依赖于blog应用的第二个迁移
] ]
# 定义本迁移要执行的操作序列
operations = [ operations = [
# 新增字段comment_need_review布尔值默认 False表示评论默认不需要审核 # 向BlogSettings模型添加新字段评论审核开关
migrations.AddField( migrations.AddField(
model_name='blogsettings', model_name='blogsettings', # 指定要修改的模型名称
name='comment_need_review', name='comment_need_review', # 新字段名称
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), # 布尔字段默认值为False不需要审核
), ),
] ]

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

@ -1,4 +1,5 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13 # Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -8,110 +9,355 @@ import mdeditor.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
# 声明本迁移依赖的前一个迁移文件
dependencies = [ dependencies = [
# 依赖可交换的用户模型
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
# 依赖于blog应用的第四个迁移
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'), ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
] ]
# 定义本迁移要执行的操作序列
operations = [ operations = [
# 调整多个模型的 Meta 选项比如排序方式、verbose_name 等 # 修改Article模型的元选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='article', name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'}, options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
), ),
# 修改Category模型的元选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='category', name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'}, options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
), ),
# 修改Links模型的元选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='links', name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'}, options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
), ),
# 修改Sidebar模型的元选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='sidebar', name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'}, options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
), ),
# 修改Tag模型的元选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='tag', name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'}, options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
), ),
# 删除Article模型的created_time字段
# 删除旧的时间字段created_time / last_mod_time migrations.RemoveField(
migrations.RemoveField(model_name='article', name='created_time'), model_name='article',
migrations.RemoveField(model_name='article', name='last_mod_time'), name='created_time',
migrations.RemoveField(model_name='category', name='created_time'), ),
migrations.RemoveField(model_name='category', name='last_mod_time'), # 删除Article模型的last_mod_time字段
migrations.RemoveField(model_name='links', name='created_time'), migrations.RemoveField(
migrations.RemoveField(model_name='sidebar', name='created_time'), model_name='article',
migrations.RemoveField(model_name='tag', name='created_time'), name='last_mod_time',
migrations.RemoveField(model_name='tag', name='last_mod_time'), ),
# 删除Category模型的created_time字段
# 新增新的时间字段creation_time创建时间、last_modify_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( migrations.AddField(
model_name='article', model_name='article',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 向Article模型添加last_modify_time字段
migrations.AddField( migrations.AddField(
model_name='article', model_name='article',
name='last_modify_time', name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
), ),
# 向Category模型添加creation_time字段
migrations.AddField( migrations.AddField(
model_name='category', model_name='category',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 向Category模型添加last_modify_time字段
migrations.AddField( migrations.AddField(
model_name='category', model_name='category',
name='last_modify_time', name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
), ),
# 向Links模型添加creation_time字段
migrations.AddField( migrations.AddField(
model_name='links', model_name='links',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 向Sidebar模型添加creation_time字段
migrations.AddField( migrations.AddField(
model_name='sidebar', model_name='sidebar',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 向Tag模型添加creation_time字段
migrations.AddField( migrations.AddField(
model_name='tag', model_name='tag',
name='creation_time', name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
), ),
# 向Tag模型添加last_modify_time字段
migrations.AddField( migrations.AddField(
model_name='tag', model_name='tag',
name='last_modify_time', name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
), ),
# 修改Article模型的article_order字段的显示名称
# 对多个字段进行字段选项优化,比如 choices 的英文显示、字段名称的 verbose_name 等 migrations.AlterField(
# (此处省略详细每一个 AlterField因为数量较多但都是对字段显示名、选项、类型等的微调 model_name='article',
# 例如:将 comment_status 的 '打开'/'关闭' 改为 'Open'/'Close',将 status 的 '草稿'/'发表' 改为 'Draft'/'Published' 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( migrations.AlterField(
model_name='article', model_name='article',
name='comment_status', name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_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( migrations.AlterField(
model_name='article', model_name='article',
name='status', name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_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( migrations.AlterField(
model_name='blogsettings', model_name='blogsettings',
name='article_comment_count', name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'), field=models.IntegerField(default=5, verbose_name='article comment count'),
), ),
# ...(其它 blogsettings 字段也做了字段选项的优化调整,比如 verbose_name 更清晰) # 修改BlogSettings模型的article_sub_length字段的显示名称
migrations.AlterField(
# 对 Category、Links、Sidebar、Tag 等模型字段也做了类似的字段选项优化 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 # Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
# 声明本迁移依赖的前一个迁移文件
dependencies = [ dependencies = [
# 依赖于blog应用的第五个迁移
('blog', '0005_alter_article_options_alter_category_options_and_more'), ('blog', '0005_alter_article_options_alter_category_options_and_more'),
] ]
# 定义本迁移要执行的操作序列
operations = [ operations = [
# 修改 BlogSettings 模型在后台显示的名称,从中文「网站配置」改为英文 'Website configuration' # 修改BlogSettings模型的元选项国际化
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='blogsettings', name='blogsettings', # 指定要修改的模型名称
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, 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 logging
# 导入正则表达式模块
import re import re
# 导入抽象方法装饰器
from abc import abstractmethod from abc import abstractmethod
# 导入Django配置和模型相关模块
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# 导入Markdown编辑器字段
from mdeditor.fields import MDTextField from mdeditor.fields import MDTextField
# 导入slug生成工具
from uuslug import slugify from uuslug import slugify
# 导入自定义工具函数
from djangoblog.utils import cache_decorator, cache from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
# 获取日志记录器
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 链接显示类型选择类
class LinkShowType(models.TextChoices): class LinkShowType(models.TextChoices):
I = ('i', _('首页')) I = ('i', _('index')) # 首页显示
L = ('l', _('列表页')) L = ('l', _('list')) # 列表页显示
P = ('p', _('文章页')) P = ('p', _('post')) # 文章页面显示
A = ('a', _('全部')) A = ('a', _('all')) # 全站显示
S = ('s', _('幻灯片')) S = ('s', _('slide')) # 友情链接页面显示
# 基础模型抽象类
class BaseModel(models.Model): class BaseModel(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True) # 自增主键
creation_time = models.DateTimeField(_('创建时间'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_modify_time = models.DateTimeField(_('修改时间'), default=now) last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
def save(self, *args, **kwargs): 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: if is_update_views:
# 如果是视图更新,直接更新数据库
Article.objects.filter(pk=self.pk).update(views=self.views) Article.objects.filter(pk=self.pk).update(views=self.views)
else: else:
# 否则正常保存并处理slug字段
if 'slug' in self.__dict__: if 'slug' in self.__dict__:
slug_source = getattr(self, 'title', '') if 'title' in self.__dict__ else getattr(self, 'name', '') slug = getattr(
setattr(self, 'slug', slugify(slug_source)) self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_full_url(self): def get_full_url(self):
# 获取完整URL
site = get_current_site().domain 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 return url
class Meta: class Meta:
abstract = True abstract = True # 标记为抽象基类
@abstractmethod @abstractmethod
def get_absolute_url(self): def get_absolute_url(self):
# 抽象方法,子类必须实现
pass pass
# 文章模型类
class Article(BaseModel): class Article(BaseModel):
"""文章"""
STATUS_CHOICES = ( STATUS_CHOICES = (
('d', _('草稿')), ('d', _('Draft')), # 草稿状态
('p', _('发布')), ('p', _('Published')), # 已发布状态
) )
COMMENT_STATUS = ( COMMENT_STATUS = (
('o', _('开放评论')), ('o', _('Open')), # 评论开启
('c', _('关闭评论')), ('c', _('Close')), # 评论关闭
) )
TYPE = ( TYPE = (
('a', _('文章')), ('a', _('Article')), # 文章类型
('p', _('页面')), ('p', _('Page')), # 页面类型
) )
title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题
title = models.CharField(_('标题'), max_length=200, unique=True) body = MDTextField(_('body')) # 文章正文使用Markdown编辑器
body = MDTextField(_('内容')) pub_time = models.DateTimeField(
pub_time = models.DateTimeField(_('发布时间'), blank=False, null=False, default=now) _('publish time'), blank=False, null=False, default=now) # 发布时间
status = models.CharField(_('状态'), max_length=1, choices=STATUS_CHOICES, default='p') status = models.CharField(
comment_status = models.CharField(_('评论状态'), max_length=1, choices=COMMENT_STATUS, default='o') _('status'),
type = models.CharField(_('类型'), max_length=1, choices=TYPE, default='a') max_length=1,
views = models.PositiveIntegerField(_('浏览量'), default=0) choices=STATUS_CHOICES,
like_count = models.PositiveIntegerField(_('点赞数'), default=0) default='p') # 文章状态
favorite_count = models.PositiveIntegerField(_('收藏数'), default=0) comment_status = models.CharField(
author = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('作者'), on_delete=models.CASCADE) _('comment status'),
article_order = models.IntegerField(_('排序'), default=0) max_length=1,
show_toc = models.BooleanField(_('显示目录'), default=False) choices=COMMENT_STATUS,
category = models.ForeignKey('Category', verbose_name=_('分类'), on_delete=models.CASCADE) default='o') # 评论状态
tags = models.ManyToManyField('Tag', verbose_name=_('标签'), blank=True) type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 内容类型
views = models.PositiveIntegerField(_('views'), default=0) # 浏览量
# ========== 点赞相关方法 ========== author = models.ForeignKey(
def is_liked_by(self, user): settings.AUTH_USER_MODEL,
"""检查用户是否已点赞此文章""" verbose_name=_('author'),
if user and user.is_authenticated: blank=False,
return self.article_likes.filter(user=user).exists() null=False,
return False on_delete=models.CASCADE) # 作者外键
article_order = models.IntegerField(
@property _('order'), blank=False, null=False, default=0) # 文章排序
def likes_count(self): show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录
"""获取点赞数量""" category = models.ForeignKey(
return self.article_likes.count() 'Category',
verbose_name=_('category'),
def toggle_like(self, user): on_delete=models.CASCADE,
"""切换点赞状态""" blank=False,
if not user or not user.is_authenticated: null=False) # 分类外键
return False, 0 tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 标签多对多关系
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
# ========== 结束新增 ==========
def body_to_string(self): def body_to_string(self):
return self.body return self.body
@ -152,12 +128,13 @@ class Article(BaseModel):
return self.title return self.title
class Meta: class Meta:
ordering = ['-article_order', '-pub_time'] ordering = ['-article_order', '-pub_time'] # 默认排序
verbose_name = _('文章') verbose_name = _('article') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
get_latest_by = 'id' get_latest_by = 'id' # 获取最新记录的依据字段
def get_absolute_url(self): def get_absolute_url(self):
# 获取文章绝对URL
return reverse('blog:detailbyid', kwargs={ return reverse('blog:detailbyid', kwargs={
'article_id': self.id, 'article_id': self.id,
'year': self.creation_time.year, 'year': self.creation_time.year,
@ -165,20 +142,24 @@ class Article(BaseModel):
'day': self.creation_time.day 'day': self.creation_time.day
}) })
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self): def get_category_tree(self):
# 获取分类树
tree = self.category.get_category_tree() tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names return names
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def viewed(self): def viewed(self):
# 增加浏览量
self.views += 1 self.views += 1
self.save(update_fields=['views']) self.save(update_fields=['views'])
def comment_list(self): def comment_list(self):
# 获取评论列表,带缓存
cache_key = 'article_comments_{id}'.format(id=self.id) cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key) value = cache.get(cache_key)
if value: if value:
@ -186,49 +167,71 @@ class Article(BaseModel):
return value return value
else: else:
comments = self.comment_set.filter(is_enable=True).order_by('-id') 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)) logger.info('set article comments:{id}'.format(id=self.id))
return comments return comments
def get_admin_url(self): def get_admin_url(self):
# 获取管理后台URL
info = (self._meta.app_label, self._meta.model_name) info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,)) return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100) @cache_decorator(expiration=60 * 100) # 缓存100分钟
def next_article(self): 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): def prev_article(self):
# 获取上一篇文章
return Article.objects.filter(id__lt=self.id, status='p').first() return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self): def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
"""
# 从文章正文中提取第一张图片URL
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match: if match:
return match.group(1) return match.group(1)
return "" return ""
# 分类模型类
class Category(BaseModel): class Category(BaseModel):
name = models.CharField(_('分类名称'), max_length=30, unique=True) """文章分类"""
parent_category = models.ForeignKey('self', verbose_name=_('父级分类'), blank=True, null=True, name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称
on_delete=models.CASCADE) parent_category = models.ForeignKey(
slug = models.SlugField(default='no-slug', max_length=60, blank=True) 'self',
index = models.IntegerField(default=0, verbose_name=_('排序')) 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: class Meta:
ordering = ['-index'] ordering = ['-index'] # 按索引倒序排列
verbose_name = _('分类') verbose_name = _('category') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
def get_absolute_url(self): 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): def __str__(self):
return self.name return self.name
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self): def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
"""
categorys = [] categorys = []
def parse(category): def parse(category):
@ -239,8 +242,12 @@ class Category(BaseModel):
parse(self) parse(self)
return categorys return categorys
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self): def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
"""
categorys = [] categorys = []
all_categorys = Category.objects.all() all_categorys = Category.objects.all()
@ -249,7 +256,7 @@ class Category(BaseModel):
categorys.append(category) categorys.append(category)
childs = all_categorys.filter(parent_category=category) childs = all_categorys.filter(parent_category=category)
for child in childs: for child in childs:
if child not in categorys: if category not in categorys:
categorys.append(child) categorys.append(child)
parse(child) parse(child)
@ -257,146 +264,144 @@ class Category(BaseModel):
return categorys return categorys
# 标签模型类
class Tag(BaseModel): 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): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
# 获取标签绝对URL
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) 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): def get_article_count(self):
# 获取标签下的文章数量
return Article.objects.filter(tags__name=self.name).distinct().count() return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta: class Meta:
ordering = ['name'] ordering = ['name'] # 按名称排序
verbose_name = _('标签') verbose_name = _('tag') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
# 友情链接模型类
class Links(models.Model): class Links(models.Model):
name = models.CharField(_('链接名称'), max_length=30, unique=True) """友情链接"""
link = models.URLField(_('链接地址'))
sequence = models.IntegerField(_('排序'), unique=True) name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称
is_enable = models.BooleanField(_('是否显示'), default=True) link = models.URLField(_('link')) # 链接地址
show_type = models.CharField(_('显示位置'), max_length=1, choices=LinkShowType.choices, default=LinkShowType.I) sequence = models.IntegerField(_('order'), unique=True) # 排序序号
creation_time = models.DateTimeField(_('创建时间'), default=now) is_enable = models.BooleanField(
last_mod_time = models.DateTimeField(_('修改时间'), default=now) _('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: class Meta:
ordering = ['sequence'] ordering = ['sequence'] # 按序号排序
verbose_name = _('友情链接') verbose_name = _('link') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
def __str__(self): def __str__(self):
return self.name return self.name
# 侧边栏模型类
class SideBar(models.Model): class SideBar(models.Model):
name = models.CharField(_('标题'), max_length=100) """侧边栏,可以展示一些html内容"""
content = models.TextField(_('内容')) name = models.CharField(_('title'), max_length=100) # 侧边栏标题
sequence = models.IntegerField(_('排序'), unique=True) content = models.TextField(_('content')) # 侧边栏内容
is_enable = models.BooleanField(_('是否启用'), default=True) sequence = models.IntegerField(_('order'), unique=True) # 排序序号
creation_time = models.DateTimeField(_('创建时间'), default=now) is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用
last_mod_time = models.DateTimeField(_('修改时间'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta: class Meta:
ordering = ['sequence'] ordering = ['sequence'] # 按序号排序
verbose_name = _('侧边栏') verbose_name = _('sidebar') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
def __str__(self): def __str__(self):
return self.name 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): class BlogSettings(models.Model):
site_name = models.CharField(_('站点名称'), max_length=200, null=False, blank=False, default='') """blog的配置"""
site_description = models.TextField(_('站点描述'), max_length=1000, null=False, blank=False, default='') site_name = models.CharField(
site_seo_description = models.TextField(_('SEO描述'), max_length=1000, null=False, blank=False, default='') _('site name'),
site_keywords = models.TextField(_('站点关键词'), max_length=1000, null=False, blank=False, default='') max_length=200,
article_sub_length = models.IntegerField(_('文章摘要长度'), default=300) null=False,
sidebar_article_count = models.IntegerField(_('侧边栏文章数量'), default=10) blank=False,
sidebar_comment_count = models.IntegerField(_('侧边栏评论数量'), default=5) default='') # 网站名称
article_comment_count = models.IntegerField(_('文章评论数量'), default=5) site_description = models.TextField(
show_google_adsense = models.BooleanField(_('显示Google广告'), default=False) _('site description'),
google_adsense_codes = models.TextField(_('Google广告代码'), max_length=2000, null=True, blank=True, default='') max_length=1000,
open_site_comment = models.BooleanField(_('开放站点评论'), default=True) null=False,
global_header = models.TextField(_("公共头部"), null=True, blank=True, default='') blank=False,
global_footer = models.TextField(_("公共尾部"), null=True, blank=True, default='') default='') # 网站描述
beian_code = models.CharField(_('备案号'), max_length=2000, null=True, blank=True, default='') site_seo_description = models.TextField(
analytics_code = models.TextField(_("网站统计代码"), max_length=1000, null=False, blank=False, default='') _('site seo description'), max_length=1000, null=False, blank=False, default='') # 网站SEO描述
show_gongan_code = models.BooleanField(_('是否显示公安备案号'), default=False) site_keywords = models.TextField(
gongan_beiancode = models.TextField(_('公安备案号'), max_length=2000, null=True, blank=True, default='') _('site keywords'),
comment_need_review = models.BooleanField(_('评论是否需要审核'), default=False) 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: class Meta:
verbose_name = _('网站配置') verbose_name = _('Website configuration') # 单数显示名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 复数显示名称
def __str__(self): def __str__(self):
return self.site_name return self.site_name
def clean(self): def clean(self):
# 验证只能有一个配置实例
if BlogSettings.objects.exclude(id=self.id).count(): if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('只能存在一个配置')) raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
from djangoblog.utils import cache from djangoblog.utils import cache
cache.clear() cache.clear() # 保存配置后清除缓存

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

@ -1,53 +1,75 @@
# 导入哈希库
import hashlib import hashlib
# 导入日志模块
import logging import logging
# 导入随机数模块
import random import random
# 导入URL处理模块
import urllib import urllib
# 导入Django模板相关模块
from django import template from django import template
from django.conf import settings from django.conf import settings
# 导入数据库查询模块
from django.db.models import Q from django.db.models import Q
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
# 导入模板过滤器
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
# 导入静态文件处理
from django.templatetags.static import static from django.templatetags.static import static
from django.urls import reverse from django.urls import reverse
# 导入安全字符串处理
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
# 导入博客模型
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
# 导入评论模型
from comments.models import Comment from comments.models import Comment
# 导入自定义工具函数
from djangoblog.utils import CommonMarkdown, sanitize_html from djangoblog.utils import CommonMarkdown, sanitize_html
from djangoblog.utils import cache from djangoblog.utils import cache
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
# 导入OAuth用户模型
from oauth.models import OAuthUser from oauth.models import OAuthUser
# 导入插件管理模块
from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage import hooks
# 获取日志记录器
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 创建模板库注册器
register = template.Library() register = template.Library()
# 注册头部meta标签的简单标签接收上下文
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def head_meta(context): def head_meta(context):
return mark_safe(hooks.apply_filters('head_meta', '', context)) return mark_safe(hooks.apply_filters('head_meta', '', context))
# 注册时间格式化简单标签
@register.simple_tag @register.simple_tag
def timeformat(data): def timeformat(data):
try: try:
# 使用设置中的时间格式
return data.strftime(settings.TIME_FORMAT) return data.strftime(settings.TIME_FORMAT)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
return "" return ""
# 注册日期时间格式化简单标签
@register.simple_tag @register.simple_tag
def datetimeformat(data): def datetimeformat(data):
try: try:
# 使用设置中的日期时间格式
return data.strftime(settings.DATE_TIME_FORMAT) return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
return "" return ""
# 注册自定义Markdown过滤器自动处理字符串
@register.filter() @register.filter()
@stringfilter @stringfilter
def custom_markdown(content): def custom_markdown(content):
@ -55,6 +77,7 @@ def custom_markdown(content):
通用markdown过滤器应用文章内容插件 通用markdown过滤器应用文章内容插件
主要用于文章内容处理 主要用于文章内容处理
""" """
# 将Markdown内容转换为HTML
html_content = CommonMarkdown.get_markdown(content) html_content = CommonMarkdown.get_markdown(content)
# 然后应用插件过滤器优化HTML # 然后应用插件过滤器优化HTML
@ -65,6 +88,7 @@ def custom_markdown(content):
return mark_safe(optimized_html) return mark_safe(optimized_html)
# 注册侧边栏Markdown过滤器
@register.filter() @register.filter()
@stringfilter @stringfilter
def sidebar_markdown(content): def sidebar_markdown(content):
@ -72,6 +96,7 @@ def sidebar_markdown(content):
return mark_safe(html_content) return mark_safe(html_content)
# 注册文章内容渲染标签,接收上下文
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def render_article_content(context, article, is_summary=False): def render_article_content(context, article, is_summary=False):
""" """
@ -122,6 +147,7 @@ def render_article_content(context, article, is_summary=False):
return mark_safe(optimized_html) return mark_safe(optimized_html)
# 注册获取Markdown目录的简单标签
@register.simple_tag @register.simple_tag
def get_markdown_toc(content): def get_markdown_toc(content):
from djangoblog.utils import CommonMarkdown from djangoblog.utils import CommonMarkdown
@ -129,6 +155,7 @@ def get_markdown_toc(content):
return mark_safe(toc) return mark_safe(toc)
# 注册评论Markdown过滤器
@register.filter() @register.filter()
@stringfilter @stringfilter
def comment_markdown(content): def comment_markdown(content):
@ -136,6 +163,7 @@ def comment_markdown(content):
return mark_safe(sanitize_html(content)) return mark_safe(sanitize_html(content))
# 注册内容截断过滤器标记为安全HTML
@register.filter(is_safe=True) @register.filter(is_safe=True)
@stringfilter @stringfilter
def truncatechars_content(content): def truncatechars_content(content):
@ -150,6 +178,7 @@ def truncatechars_content(content):
return truncatechars_html(content, blogsetting.article_sub_length) return truncatechars_html(content, blogsetting.article_sub_length)
# 注册简单截断过滤器标记为安全HTML
@register.filter(is_safe=True) @register.filter(is_safe=True)
@stringfilter @stringfilter
def truncate(content): def truncate(content):
@ -158,6 +187,7 @@ def truncate(content):
return strip_tags(content)[:150] return strip_tags(content)[:150]
# 注册面包屑导航包含标签
@register.inclusion_tag('blog/tags/breadcrumb.html') @register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article): def load_breadcrumb(article):
""" """
@ -179,6 +209,7 @@ def load_breadcrumb(article):
} }
# 注册文章标签列表包含标签
@register.inclusion_tag('blog/tags/article_tag_list.html') @register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article): def load_articletags(article):
""" """
@ -199,6 +230,7 @@ def load_articletags(article):
} }
# 注册侧边栏包含标签
@register.inclusion_tag('blog/tags/sidebar.html') @register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype): def load_sidebar(user, linktype):
""" """
@ -213,16 +245,23 @@ def load_sidebar(user, linktype):
logger.info('load sidebar') logger.info('load sidebar')
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting() blogsetting = get_blog_setting()
# 获取最近文章
recent_articles = Article.objects.filter( recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count] status='p')[:blogsetting.sidebar_article_count]
# 获取所有分类
sidebar_categorys = Category.objects.all() sidebar_categorys = Category.objects.all()
# 获取额外的侧边栏内容
extra_sidebars = SideBar.objects.filter( extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence') is_enable=True).order_by('sequence')
# 获取最多阅读文章
most_read_articles = Article.objects.filter(status='p').order_by( most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count] '-views')[:blogsetting.sidebar_article_count]
# 获取文章归档日期
dates = Article.objects.datetimes('creation_time', 'month', order='DESC') dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
# 获取友情链接
links = Links.objects.filter(is_enable=True).filter( links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A)) Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
# 获取最新评论
commment_list = Comment.objects.filter(is_enable=True).order_by( commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count] '-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小 # 标签云 计算字体大小
@ -253,12 +292,14 @@ def load_sidebar(user, linktype):
'sidebar_tags': sidebar_tags, 'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars 'extra_sidebars': extra_sidebars
} }
# 设置缓存3小时过期
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3) cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype)) logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user value['user'] = user
return value return value
# 注册文章meta信息包含标签
@register.inclusion_tag('blog/tags/article_meta_info.html') @register.inclusion_tag('blog/tags/article_meta_info.html')
def load_article_metas(article, user): def load_article_metas(article, user):
""" """
@ -272,10 +313,12 @@ def load_article_metas(article, user):
} }
# 注册分页信息包含标签
@register.inclusion_tag('blog/tags/article_pagination.html') @register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name): def load_pagination_info(page_obj, page_type, tag_name):
previous_url = '' previous_url = ''
next_url = '' next_url = ''
# 处理首页分页
if page_type == '': if page_type == '':
if page_obj.has_next(): if page_obj.has_next():
next_number = page_obj.next_page_number() next_number = page_obj.next_page_number()
@ -285,6 +328,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
previous_url = reverse( previous_url = reverse(
'blog:index_page', kwargs={ 'blog:index_page', kwargs={
'page': previous_number}) 'page': previous_number})
# 处理标签分页
if page_type == '分类标签归档': if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name) tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next(): if page_obj.has_next():
@ -301,6 +345,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
kwargs={ kwargs={
'page': previous_number, 'page': previous_number,
'tag_name': tag.slug}) 'tag_name': tag.slug})
# 处理作者文章分页
if page_type == '作者文章归档': if page_type == '作者文章归档':
if page_obj.has_next(): if page_obj.has_next():
next_number = page_obj.next_page_number() next_number = page_obj.next_page_number()
@ -316,7 +361,7 @@ def load_pagination_info(page_obj, page_type, tag_name):
kwargs={ kwargs={
'page': previous_number, 'page': previous_number,
'author_name': tag_name}) 'author_name': tag_name})
# 处理分类目录分页
if page_type == '分类目录归档': if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name) category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next(): 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') @register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user): def load_article_detail(article, isindex, user):
""" """
@ -360,7 +406,7 @@ def load_article_detail(article, isindex, user):
} }
# 返回用户头像URL # 返回用户头像URL的过滤器
# 模板使用方法: {{ email|gravatar_url:150 }} # 模板使用方法: {{ email|gravatar_url:150 }}
@register.filter @register.filter
def gravatar_url(email, size=40): def gravatar_url(email, size=40):
@ -380,7 +426,8 @@ def gravatar_url(email, size=40):
default_avatar_path = static('blog/img/avatar.png') 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] selected_user = non_default_users[0] if non_default_users else users_with_picture[0]
url = selected_user.picture url = selected_user.picture
@ -397,6 +444,7 @@ def gravatar_url(email, size=40):
return url return url
# 返回用户头像HTML标签的过滤器
@register.filter @register.filter
def gravatar(email, size=40): def gravatar(email, size=40):
"""获得用户头像HTML标签""" """获得用户头像HTML标签"""
@ -406,6 +454,7 @@ def gravatar(email, size=40):
(url, size, size)) (url, size, size))
# 注册查询集过滤简单标签
@register.simple_tag @register.simple_tag
def query(qs, **kwargs): def query(qs, **kwargs):
""" template tag which allows queryset filtering. Usage: """ template tag which allows queryset filtering. Usage:
@ -417,6 +466,7 @@ def query(qs, **kwargs):
return qs.filter(**kwargs) return qs.filter(**kwargs)
# 注册字符串连接过滤器
@register.filter @register.filter
def addstr(arg1, arg2): def addstr(arg1, arg2):
"""concatenate arg1 & arg2""" """concatenate arg1 & arg2"""

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

@ -1,100 +1,91 @@
# 导入Django URL路由相关模块
from django.urls import path 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 = [ urlpatterns = [
# 首页 # 首页URL使用类视图
path( path(
r'', r'',
views.IndexView.as_view(), views.IndexView.as_view(),
name='index'), name='index'),
# 首页分页
# 首页分页URL支持页码参数
path( path(
r'page/<int:page>/', r'page/<int:page>/',
views.IndexView.as_view(), views.IndexView.as_view(),
name='index_page'), name='index_page'),
# 文章详情页通过年、月、日、文章ID定位
# 文章详情页URL包含年月日和文章ID参数
path( path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html', r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(), views.ArticleDetailView.as_view(),
name='detailbyid'), name='detailbyid'),
# 文章点赞API
path( # 分类详情页URL使用slug格式的分类名称
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'),
# 分类目录详情页,通过分类别名
path( path(
r'category/<slug:category_name>.html', r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(),
name='category_detail'), name='category_detail'),
# 分类目录详情页(带分页)
# 分类详情分页URL支持分类名称和页码参数
path( path(
r'category/<slug:category_name>/<int:page>.html', r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(),
name='category_detail_page'), name='category_detail_page'),
# 作者文章详情页,通过作者名称
# 作者详情页URL使用作者名称参数
path( path(
r'author/<author_name>.html', r'author/<author_name>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(),
name='author_detail'), name='author_detail'),
# 作者文章详情页(带分页)
# 作者详情分页URL支持作者名称和页码参数
path( path(
r'author/<author_name>/<int:page>.html', r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(),
name='author_detail_page'), name='author_detail_page'),
# 标签详情页,通过标签别名
# 标签详情页URL使用slug格式的标签名称
path( path(
r'tag/<slug:tag_name>.html', r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(),
name='tag_detail'), name='tag_detail'),
# 标签详情页(带分页)
# 标签详情分页URL支持标签名称和页码参数
path( path(
r'tag/<slug:tag_name>/<int:page>.html', r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(),
name='tag_detail_page'), name='tag_detail_page'),
# 文章归档页使用缓存60分钟
# 归档页面URL使用缓存装饰器缓存1小时
path( path(
'archives.html', 'archives.html',
cache_page(60 * 60)(views.ArchivesView.as_view()), cache_page(
60 * 60)( # 缓存1小时60分钟 * 60秒
views.ArchivesView.as_view()),
name='archives'), name='archives'),
# 友情链接页
# 友情链接页面URL
path( path(
r'links.html', 'links.html',
views.LinkListView.as_view(), views.LinkListView.as_view(),
name='links'), name='links'),
# 文件上传接口(用于图床等功能)
# 文件上传URL使用函数视图
path( path(
r'upload', r'upload',
views.fileupload, views.fileupload,
name='upload'), name='upload'),
# 清理缓存接口
# 清理缓存URL
path( path(
r'clean', r'clean',
views.clean_cache_view, views.clean_cache_view,

@ -1,55 +1,82 @@
# 导入日志模块
import logging import logging
# 导入操作系统接口模块
import os import os
# 导入UUID生成模块
import uuid import uuid
# 导入Django配置和核心模块
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import F from django.http import HttpResponse, HttpResponseForbidden
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, render from django.shortcuts import render
from django.templatetags.static import static from django.templatetags.static import static
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt 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.detail import DetailView
from django.views.generic.list import ListView from django.views.generic.list import ListView
# 导入Haystack搜索视图
from haystack.views import SearchView 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 comments.forms import CommentForm
# 导入插件管理模块
from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
# 导入自定义工具函数
from djangoblog.utils import cache, get_blog_setting, get_sha256 from djangoblog.utils import cache, get_blog_setting, get_sha256
# 获取日志记录器
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 文章列表视图基类
class ArticleListView(ListView): class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html' template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list' context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
page_type = '' page_type = ''
paginate_by = settings.PAGINATE_BY paginate_by = settings.PAGINATE_BY # 分页大小
page_kwarg = 'page' page_kwarg = 'page' # 页码参数名
link_type = LinkShowType.L link_type = LinkShowType.L # 链接显示类型
def get_view_cache_key(self): def get_view_cache_key(self):
return self.request.GET.get('pages', '') return self.request.get['pages']
@property @property
def page_number(self): def page_number(self):
# 获取当前页码
page_kwarg = self.page_kwarg 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 return page
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError() raise NotImplementedError()
def get_queryset_data(self): def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError() raise NotImplementedError()
def get_queryset_from_cache(self, cache_key): def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
value = cache.get(cache_key) value = cache.get(cache_key)
if value: if value:
logger.info('get view cache.key:{key}'.format(key=cache_key)) logger.info('get view cache.key:{key}'.format(key=cache_key))
@ -61,41 +88,63 @@ class ArticleListView(ListView):
return article_list return article_list
def get_queryset(self): def get_queryset(self):
'''
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key() key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key) value = self.get_queryset_from_cache(key)
return value return value
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# 添加上下文数据
kwargs['linktype'] = self.link_type kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs) return super(ArticleListView, self).get_context_data(**kwargs)
# 首页视图
class IndexView(ArticleListView): class IndexView(ArticleListView):
'''
首页
'''
# 友情链接类型
link_type = LinkShowType.I link_type = LinkShowType.I
def get_queryset_data(self): def get_queryset_data(self):
# 获取已发布的文章列表
article_list = Article.objects.filter(type='a', status='p') article_list = Article.objects.filter(type='a', status='p')
return article_list return article_list
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
# 生成首页缓存键
cache_key = 'index_{page}'.format(page=self.page_number) cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key return cache_key
# 文章详情视图
class ArticleDetailView(DetailView): class ArticleDetailView(DetailView):
'''
文章详情页面
'''
template_name = 'blog/article_detail.html' template_name = 'blog/article_detail.html'
model = Article model = Article
pk_url_kwarg = 'article_id' pk_url_kwarg = 'article_id' # URL中的文章ID参数名
context_object_name = "article" context_object_name = "article" # 上下文中的文章对象名
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# 创建评论表单
comment_form = CommentForm() comment_form = CommentForm()
# 获取文章评论列表
article_comments = self.object.comment_list() article_comments = self.object.comment_list()
# 获取父级评论
parent_comments = article_comments.filter(parent_comment=None) parent_comments = article_comments.filter(parent_comment=None)
# 获取博客设置
blog_setting = get_blog_setting() blog_setting = get_blog_setting()
# 对父级评论进行分页
paginator = Paginator(parent_comments, blog_setting.article_comment_count) paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1') page = self.request.GET.get('comment_page', '1')
# 验证页码
if not page.isnumeric(): if not page.isnumeric():
page = 1 page = 1
else: else:
@ -105,69 +154,55 @@ class ArticleDetailView(DetailView):
if page > paginator.num_pages: if page > paginator.num_pages:
page = paginator.num_pages page = paginator.num_pages
# 获取当前页评论
p_comments = paginator.page(page) p_comments = paginator.page(page)
# 计算下一页和上一页
next_page = p_comments.next_page_number() if p_comments.has_next() else None next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
# 构建评论分页URL
if next_page: if next_page:
kwargs[ kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container' 'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page: if prev_page:
kwargs[ kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container' 'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 添加上下文数据
kwargs['form'] = comment_form kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(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['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_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) context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request) hooks.run_action('after_article_body_get', article=article, request=self.request)
return context return context
# 分类详情视图
class CategoryDetailView(ArticleListView): class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档" page_type = "分类目录归档"
def get_queryset_data(self): def get_queryset_data(self):
# 获取分类slug
slug = self.kwargs['category_name'] slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug) category = get_object_or_404(Category, slug=slug)
categoryname = category.name categoryname = category.name
self.categoryname = categoryname self.categoryname = categoryname
# 获取所有子分类名称
categorynames = list( categorynames = list(
map(lambda c: c.name, category.get_sub_categorys())) map(lambda c: c.name, category.get_sub_categorys()))
# 获取该分类下的所有文章
article_list = Article.objects.filter( article_list = Article.objects.filter(
category__name__in=categorynames, status='p') category__name__in=categorynames, status='p')
return article_list return article_list
@ -192,7 +227,11 @@ class CategoryDetailView(ArticleListView):
return super(CategoryDetailView, self).get_context_data(**kwargs) return super(CategoryDetailView, self).get_context_data(**kwargs)
# 作者详情视图
class AuthorDetailView(ArticleListView): class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档' page_type = '作者文章归档'
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
@ -215,7 +254,11 @@ class AuthorDetailView(ArticleListView):
return super(AuthorDetailView, self).get_context_data(**kwargs) return super(AuthorDetailView, self).get_context_data(**kwargs)
# 标签详情视图
class TagDetailView(ArticleListView): class TagDetailView(ArticleListView):
'''
标签列表页面
'''
page_type = '分类标签归档' page_type = '分类标签归档'
def get_queryset_data(self): def get_queryset_data(self):
@ -243,10 +286,14 @@ class TagDetailView(ArticleListView):
return super(TagDetailView, self).get_context_data(**kwargs) return super(TagDetailView, self).get_context_data(**kwargs)
# 归档视图
class ArchivesView(ArticleListView): class ArchivesView(ArticleListView):
'''
文章归档页面
'''
page_type = '文章归档' page_type = '文章归档'
paginate_by = None paginate_by = None # 不分页
page_kwarg = None page_kwarg = None # 无页码参数
template_name = 'blog/article_archives.html' template_name = 'blog/article_archives.html'
def get_queryset_data(self): def get_queryset_data(self):
@ -257,6 +304,7 @@ class ArchivesView(ArticleListView):
return cache_key return cache_key
# 友情链接视图
class LinkListView(ListView): class LinkListView(ListView):
model = Links model = Links
template_name = 'blog/links_list.html' template_name = 'blog/links_list.html'
@ -265,56 +313,78 @@ class LinkListView(ListView):
return Links.objects.filter(is_enable=True) return Links.objects.filter(is_enable=True)
# Elasticsearch搜索视图
class EsSearchView(SearchView): class EsSearchView(SearchView):
def get_context(self): def get_context(self):
# 构建分页
paginator, page = self.build_page() paginator, page = self.build_page()
context = { context = {
"query": self.query, "query": self.query, # 搜索查询
"form": self.form, "form": self.form, # 搜索表单
"page": page, "page": page, # 当前页
"paginator": paginator, "paginator": paginator, # 分页器
"suggestion": None, "suggestion": None, # 搜索建议
} }
# 添加拼写建议
if hasattr(self.results, "query") and self.results.query.backend.include_spelling: if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion() context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context()) context.update(self.extra_context())
return context return context
# 文件上传视图免除CSRF保护
@csrf_exempt @csrf_exempt
def fileupload(request): def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
"""
if request.method == 'POST': if request.method == 'POST':
# 验证签名
sign = request.GET.get('sign', None) sign = request.GET.get('sign', None)
if not sign: if not sign:
return HttpResponseForbidden() return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden() return HttpResponseForbidden()
response = [] response = []
# 处理所有上传的文件
for filename in request.FILES: for filename in request.FILES:
# 生成时间路径
timestr = timezone.now().strftime('%Y/%m/%d') timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # 图片扩展名
fname = u''.join(str(filename)) fname = u''.join(str(filename))
# 判断是否为图片
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
# 构建保存路径
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr) base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir): if not os.path.exists(base_dir):
os.makedirs(base_dir) os.makedirs(base_dir)
# 生成唯一文件名
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}")) savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
# 安全检查
if not savepath.startswith(base_dir): if not savepath.startswith(base_dir):
return HttpResponse("only for post") return HttpResponse("only for post")
# 保存文件
with open(savepath, 'wb+') as wfile: with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks(): for chunk in request.FILES[filename].chunks():
wfile.write(chunk) wfile.write(chunk)
# 如果是图片,进行压缩优化
if isimage: if isimage:
from PIL import Image from PIL import Image
image = Image.open(savepath) image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True) image.save(savepath, quality=20, optimize=True)
# 生成静态文件URL
url = static(savepath) url = static(savepath)
response.append(url) response.append(url)
return HttpResponse(response) return HttpResponse(response)
else: else:
return HttpResponse("only for post") return HttpResponse("only for post")
# 404错误页面视图
def page_not_found_view( def page_not_found_view(
request, request,
exception, exception,
@ -329,6 +399,7 @@ def page_not_found_view(
status=404) status=404)
# 500错误页面视图
def server_error_view(request, template_name='blog/error_page.html'): def server_error_view(request, template_name='blog/error_page.html'):
return render(request, return render(request,
template_name, template_name,
@ -337,6 +408,7 @@ def server_error_view(request, template_name='blog/error_page.html'):
status=500) status=500)
# 403权限拒绝视图
def permission_denied_view( def permission_denied_view(
request, request,
exception, exception,
@ -349,204 +421,7 @@ def permission_denied_view(
'statuscode': '403'}, status=403) 'statuscode': '403'}, status=403)
# 清理缓存视图
def clean_cache_view(request): def clean_cache_view(request):
cache.clear() cache.clear()
return HttpResponse('ok') 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)

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

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

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

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

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

Loading…
Cancel
Save