Compare commits

..

No commits in common. 'master' and 'yrh-branch' have entirely different histories.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

@ -1,70 +1,11 @@
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
from django import forms
from django.contrib.auth.admin import UserAdmin # 导入Django默认的用户管理Admin类基础模板
<<<<<<< HEAD
=======
from django import forms
<<<<<<< HEAD
from django.contrib.auth.admin import UserAdmin # 导入Django默认的用户管理Admin类基础模板
>>>>>>> f82823537c318f20f273c53570c0d37f22c7a751
from django.contrib.auth.forms import UserChangeForm # 导入默认的用户编辑表单(用于继承修改)
from django.contrib.auth.forms import UsernameField# 用户名字段的专用类(自带验证逻辑)
from django.utils.translation import gettext_lazy as _# 国际化支持,文本可翻译
=======
from django import forms# 导入 Django 表单基类
from django.contrib.auth.admin import UserAdmin# 导入 Django 默认的用户管理类
from django.contrib.auth.forms import UserChangeForm# 导入 Django 默认的用户修改表单
from django.contrib.auth.forms import UsernameField# 导入 Django 默认的用户名字段类型
from django.utils.translation import gettext_lazy as _# 导入国际化翻译工具
>>>>>>> yrh-branch
# Register your models here.
from .models import BlogUser # 导入自定义的用户模型替代Django默认User
=======
<<<<<<< HEAD
from django.contrib.auth.forms import UserChangeForm # 导入默认的用户编辑表单(用于继承修改)
from django.contrib.auth.forms import UsernameField # 用户名字段的专用类(自带验证逻辑)
from django.utils.translation import gettext_lazy as _ # 国际化支持,文本可翻译
=======
from django import forms# 从 django 核心模块导入 forms 用于表单处理
from django.contrib.auth.admin import UserAdmin# 从 django 内置的 auth 管理模块导入 UserAdmin用于用户管理界面的定制
from django.contrib.auth.forms import UserChangeForm# 从 django 内置的 auth 表单模块导入 UserChangeForm用于用户信息修改表单的定制
from django.contrib.auth.forms import UsernameField# 从 django 内置的 auth 表单模块导入 UsernameField用于用户名字段的处理
from django.utils.translation import gettext_lazy as _# 从 django 翻译工具模块导入 gettext_lazy 并别名成 _用于实现国际化翻译延迟翻译
=======
from django import forms# 导入 Django 的表单基类
from django.contrib.auth.admin import UserAdmin# 导入 Django 默认的用户管理类,我们将继承它来进行自定义
from django.contrib.auth.forms import UserChangeForm# 导入 Django 默认的用户修改表单,我们将继承它
from django.contrib.auth.forms import UsernameField# 导入 Django 用于处理用户名字段的专用类,它包含了一些验证逻辑
from django.utils.translation import gettext_lazy as _# 导入国际化函数,用于将文本标记为可翻译的,这是一个好习惯
# 这是一个标准的注释,提醒开发者在这里注册自己的模型到 Admin 站点
# Register your models here.
from .models import BlogUser# 从当前应用的 models.py 文件中导入我们自定义的用户模型 BlogUser
>>>>>>> psy_branch
# Register your models here.
from .models import BlogUser# 从当前目录的 models 模块导入自定义的 BlogUser 模型
>>>>>>> xyr-branch
<<<<<<< HEAD
# Register your models here.
from .models import BlogUser # 导入自定义的用户模型替代Django默认User)
<<<<<<< HEAD
=======
from django.contrib.auth.admin import UserAdmin # xh:导入Django默认的用户管理Admin类基础模板
from django.contrib.auth.forms import UserChangeForm # xh:导入默认的用户编辑表单(用于继承修改)
from django.contrib.auth.forms import UsernameField # xh:用户名字段的专用类(自带验证逻辑)
from django.utils.translation import gettext_lazy as _ # xh:国际化支持,文本可翻译
# Register your models here.
from .models import BlogUser # xh:导入自定义的用户模型替代Django默认User)
>>>>>>> f82823537c318f20f273c53570c0d37f22c7a751
>>>>>>> xh_branch
from .models import BlogUser
# 自定义用户创建表单(用于在后台添加新用户)
class BlogUserCreationForm(forms.ModelForm):
@ -72,126 +13,41 @@ class BlogUserCreationForm(forms.ModelForm):
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)# 确认密码字段(再次输入密码)
class Meta:
<<<<<<< HEAD
model = BlogUser # 绑定自定义的BlogUser模型
fields = ('email',) # 新增用户时,默认显示的核心字段(仅邮箱,其他字段可后续编辑)
# 密码验证逻辑:检查两次输入的密码是否一致
def clean_password2(self):
=======
class BlogUserCreationForm(forms.ModelForm):# 添加一个密码字段label 使用了国际化widget 指定为密码输入框(输入内容会被掩码)
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)# 添加一个密码字段label 使用了国际化widget 指定为密码输入框(输入内容会被掩码)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) # 添加一个确认密码字段
class Meta: # Meta 类用于定义 ModelForm 的元数据
model = BlogUser # 指定这个表单对应的模型是 BlogUser
model = BlogUser
fields = ('email',)
def clean_password2(self): # 自定义密码验证方法Django 会自动调用名为 clean_<field_name> 的方法
>>>>>>> psy_branch
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")# 获取第一个密码框的清洗后数据
=======
model = BlogUser # xh:绑定自定义的BlogUser模型
fields = ('email',) # xh:新增用户时,默认显示的核心字段(仅邮箱,其他字段可后续编辑)
def clean_password2(self):
# xh:密码验证逻辑:检查两次输入的密码是否一致
password1 = self.cleaned_data.get("password1")# xh:获取第一个密码框的清洗后数据
>>>>>>> xh_branch
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
<<<<<<< HEAD
if password1 and password2 and password1 != password2:
# 如果密码不一致,抛出验证错误
=======
if password1 and password2 and password1 != password2:# 检查两个密码是否都存在且不相等
>>>>>>> psy_branch
raise forms.ValidationError(_("passwords do not match"))
return password2
=======
<<<<<<< HEAD
=======
from django import forms# 从 django 核心模块导入 forms 用于表单处理
from django.contrib.auth.admin import UserAdmin# 从 django 内置的 auth 管理模块导入 UserAdmin用于用户管理界面的定制
from django.contrib.auth.forms import UserChangeForm# 从 django 内置的 auth 表单模块导入 UserChangeForm用于用户信息修改表单的定制
from django.contrib.auth.forms import UsernameField# 从 django 内置的 auth 表单模块导入 UsernameField用于用户名字段的处理
from django.utils.translation import gettext_lazy as _# 从 django 翻译工具模块导入 gettext_lazy 并别名成 _用于实现国际化翻译延迟翻译
<<<<<<< HEAD
# Register your models here.
from .models import BlogUser# 从当前目录的 models 模块导入自定义的 BlogUser 模型
>>>>>>> f82823537c318f20f273c53570c0d37f22c7a751
class BlogUserCreationForm(forms.ModelForm):# 定义 BlogUserCreationForm 类,继承自 forms.ModelForm用于创建 BlogUser 的表单
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) # 定义 password1 字段,类型为 CharField标签为“password”小部件使用 PasswordInput密码输入框内容会隐藏
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)# 定义 password2 字段,类型为 CharField标签为“Enter password again”小部件同样使用 PasswordInput
<<<<<<< HEAD
class Meta:# Meta 类用于定义模型表单的元数据
model = BlogUser # 关联的模型是 BlogUser
fields = ('email',) # 表单中包含的字段是 'email'
def clean_password2(self): # 定义 clean_password2 方法,用于验证两个密码是否一致
# Check that the two password entries match
password1 = self.cleaned_data.get("password1") # 获取清洗后的 password1 数据
password2 = self.cleaned_data.get("password2") # 获取清洗后的 password2 数据
if password1 and password2 and password1 != password2:# 如果 password1 和 password2 都存在且不相等
raise forms.ValidationError(_("passwords do not match"))# 抛出验证错误,提示“密码不匹配”
return password2 # 返回 password2
>>>>>>> xyr-branch
def save(self, commit=True): # 定义 save 方法用于保存用户数据commit 参数控制是否立即提交到数据库
=======
def save(self, commit=True): # 自定义保存方法,用于处理密码加密
>>>>>>> psy_branch
# Save the provided password in hashed format
<<<<<<< HEAD
<<<<<<< HEAD
=======
=======
def save(self, commit=True):
#xh:将提供的密码以哈希格式保存
>>>>>>> xh_branch
>>>>>>> f82823537c318f20f273c53570c0d37f22c7a751
# Save the provided password in hashed format
user = super().save(commit=False)
# 使用 Django 内置方法加密密码
user.set_password(self.cleaned_data["password1"])
if commit:# 如果 commit 参数为 True默认情况则将用户对象保存到数据库
if commit:
user.source = 'adminsite'
user.save()
return user
=======
user = super().save(commit=False) # 调用父类的 save 方法commit=False 表示先不提交到数据库
user.set_password(self.cleaned_data["password1"]) # 对 password1 进行哈希处理后设置到用户对象中
if commit: # 如果 commit 为 True
user.source = 'adminsite' # 设置用户的来源为 'adminsite'
user.save() # 保存用户对象到数据库
return user # 返回用户对象
>>>>>>> xyr-branch
<<<<<<< HEAD
# 自定义用户修改表单(用于在后台编辑用户信息)
class BlogUserChangeForm(UserChangeForm):
=======
class BlogUserChangeForm(UserChangeForm):# 2. 自定义用户修改表单 (用于在 Admin 后台编辑已存在的用户)
>>>>>>> psy_branch
class Meta:
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):# 自定义初始化方法
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
<<<<<<< HEAD
# 自定义用户管理类(用于在后台管理用户)
class BlogUserAdmin(UserAdmin):
# 指定编辑用户时使用的表单
=======
class BlogUserAdmin(UserAdmin):# 3. 自定义用户管理类 (用于定义在 Admin 后台如何管理和显示 BlogUser)
>>>>>>> psy_branch
form = BlogUserChangeForm
# 指定添加用户时使用的表单
add_form = BlogUserCreationForm
@ -204,49 +60,7 @@ class BlogUserAdmin(UserAdmin):# 3. 自定义用户管理类 (用于定义在 Ad
'last_login',
'date_joined',
'source')
<<<<<<< HEAD
# 列表页中可点击的字段(用于进入详情页)
list_display_links = ('id', 'username')
# 默认排序方式(按 ID 降序)
ordering = ('-id',)
# Meta 类用于定义模型表单的元数据
class Meta:
# 关联的模型是 BlogUser
model = BlogUser
# 表单中包含的字段是 'email'
fields = ('email',)
# 定义 clean_password2 方法,用于验证两个密码是否一致
def clean_password2(self):
# 注释:检查两个密码输入是否匹配
# 获取清洗后的 password1 数据
password1 = self.cleaned_data.get("password1")
# 获取清洗后的 password2 数据
password2 = self.cleaned_data.get("password2")
# 如果 password1 和 password2 都存在且不相等
if password1 and password2 and password1 != password2:
# 抛出验证错误,提示“密码不匹配”
raise forms.ValidationError(_("passwords do not match"))
# 返回 password2
return password2
# 定义 save 方法用于保存用户数据commit 参数控制是否立即提交到数据库
def save(self, commit=True):
# 注释:以哈希格式保存提供的密码
# 调用父类的 save 方法commit=False 表示先不提交到数据库
user = super().save(commit=False)
# 对 password1 进行哈希处理后设置到用户对象中
user.set_password(self.cleaned_data["password1"])
# 如果 commit 为 True
if commit:
# 设置用户的来源为 'adminsite'
user.source = 'adminsite'
# 保存用户对象到数据库
user.save()
# 返回用户对象
return user
=======
list_display_links = ('id', 'username')# 定义在列表页中,哪些字段可以点击进入详情页
ordering = ('-id',)# 定义列表页的默认排序字段,'-id' 表示按 ID 降序排列(即最新的用户在最前面)
>>>>>>> psy_branch

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,59 +0,0 @@
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
fields = ('email',)
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
class Meta:
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)

@ -1,5 +0,0 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

@ -1,117 +0,0 @@
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
model = get_user_model()
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
password_validation.validate_password(password2)
return password2
def clean_email(self):
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
email = forms.EmailField(
label=_('Email'),
)

@ -1,49 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='BlogUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': '用户',
'verbose_name_plural': '用户',
'ordering': ['-id'],
'get_latest_by': 'id',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -1,46 +0,0 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='bloguser',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]

@ -1,35 +0,0 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
return self.email
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'

@ -1,207 +0,0 @@
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# Create your tests here.
class AccountTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--="
def test_validate_account(self):
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)

@ -1,28 +0,0 @@
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts"
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]

@ -1,26 +0,0 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
user = get_user_model().objects.get(**kwargs)
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
return None
def get_user(self, username):
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None

@ -1,49 +0,0 @@
import typing
from datetime import timedelta
from django.core.cache import cache
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
Args:
to_mail: 接受邮箱
subject: 邮件主题
code: 验证码
"""
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
Args:
email: 请求邮箱
code: 验证码
Return:
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
cache_code = get_code(email)
if cache_code != code:
return gettext("Verification code error")
def set_code(email: str, code: str):
"""设置code"""
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
return cache.get(email)

@ -1,204 +0,0 @@
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
if form.is_valid():
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
send_email(
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
url = '/login/'
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache()
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user())
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url
return redirect_to
def account_result(request):
type = request.GET.get('type')
id = request.GET.get('id')
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
if user.is_active:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden()
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
def post(self, request: HttpRequest):
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok")

@ -1,112 +0,0 @@
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
fields = '__all__'
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o')
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
list_per_page = 20
search_fields = ('body', 'title')
form = ArticleForm
list_display = (
'id',
'title',
'author',
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj):
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs):
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
if obj:
url = obj.get_full_url()
return url
else:
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_category', 'index')
exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin):
list_display = ('name', 'content', 'is_enable', 'sequence')
exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin):
pass

@ -1,5 +0,0 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
name = 'blog'

@ -1,43 +0,0 @@
import logging
from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__)
def seo_processor(requests):
key = 'seo_processor'
value = cache.get(key)
if value:
return value
else:
logger.info('set processor cache.')
setting = get_blog_setting()
value = {
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'nav_pages': Article.objects.filter(
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,
'ANALYTICS_CODE': setting.analytics_code,
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
"SHOW_GONGAN_CODE": setting.show_gongan_code,
"CURRENT_YEAR": timezone.now().year,
"GLOBAL_HEADER": setting.global_header,
"GLOBAL_FOOTER": setting.global_footer,
"COMMENT_NEED_REVIEW": setting.comment_need_review,
}
cache.set(key, value, 60 * 60 * 10)
return value

@ -1,213 +0,0 @@
import time
import elasticsearch.client
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
from blog.models import Article
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
c = IngestClient(es)
try:
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip"
}
}
]
}''')
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
class UserAgentOS(UserAgentBrowser):
pass
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
class Index:
name = 'performance'
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'])
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
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
1000))
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
doc.save(pipeline="geoip")
class ArticleDocument(Document):
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
pub_time = Date()
status = Text()
comment_status = Text()
type = Text()
views = Integer()
article_order = Integer()
class Index:
name = 'blog'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article'
class ArticleDocumentManager():
def __init__(self):
self.create_index()
def create_index(self):
ArticleDocument.init()
def delete_index(self):
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
return [
ArticleDocument(
meta={
'id': article.id},
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
category={
'name': article.category.name,
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
for doc in docs:
doc.save()
def update_docs(self, docs):
for doc in docs:
doc.save()

@ -1,19 +0,0 @@
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm):
querydata = forms.CharField(required=True)
def search(self):
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,18 +0,0 @@
from django.core.management.base import BaseCommand
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化
class Command(BaseCommand):
help = 'build search index'
def handle(self, *args, **options):
if ELASTICSEARCH_ENABLED:
ElaspedTimeDocumentManager.build_index()
manager = ElapsedTimeDocument()
manager.init()
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()

@ -1,13 +0,0 @@
from django.core.management.base import BaseCommand
from blog.models import Tag, Category
# TODO 参数化
class Command(BaseCommand):
help = 'build search words'
def handle(self, *args, **options):
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
print('\n'.join(datas))

@ -1,11 +0,0 @@
from django.core.management.base import BaseCommand
from djangoblog.utils import cache
class Command(BaseCommand):
help = 'clear the whole cache'
def handle(self, *args, **options):
cache.clear()
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -1,40 +0,0 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand
from blog.models import Article, Tag, Category
class Command(BaseCommand):
help = 'create test datas'
def handle(self, *args, **options):
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
category.save()
basetag = Tag()
basetag.name = "标签"
basetag.save()
for i in range(1, 20):
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
article.tags.add(tag)
article.tags.add(basetag)
article.save()
from djangoblog.utils import cache
cache.clear()
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -1,50 +0,0 @@
from django.core.management.base import BaseCommand
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
site = get_current_site().domain
class Command(BaseCommand):
help = 'notify baidu url'
def add_arguments(self, parser):
parser.add_argument(
'data_type',
type=str,
choices=[
'all',
'article',
'tag',
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
def get_full_url(self, path):
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
type = options['data_type']
self.stdout.write('start get %s' % type)
urls = []
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))

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

Loading…
Cancel
Save