Compare commits

...

11 Commits

Binary file not shown.

Binary file not shown.

@ -0,0 +1 @@
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -9,15 +9,19 @@ from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
#lht: 创建用户表单用于在Django管理后台创建新用户
#lht: 密码输入字段
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
#lht: 确认密码输入字段
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
#lht: 指定关联的模型和字段
model = BlogUser
fields = ('email',)
def clean_password2(self):
# Check that the two password entries match
#lht: 验证两次密码输入是否一致
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
@ -25,28 +29,35 @@ class BlogUserCreationForm(forms.ModelForm):
return password2
def save(self, commit=True):
# Save the provided password in hashed format
#lht: 保存用户并加密密码
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
#lht: 设置用户来源为管理后台
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
#lht: 修改用户表单用于在Django管理后台编辑现有用户
class Meta:
#lht: 指定关联的模型、字段和字段类
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
#lht: 初始化表单
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
#lht: 用户管理界面配置自定义Django管理后台的用户管理界面
#lht: 指定修改用户和创建用户使用的表单
form = BlogUserChangeForm
add_form = BlogUserCreationForm
#lht: 定义在列表页面显示的字段
list_display = (
'id',
'nickname',
@ -55,5 +66,7 @@ class BlogUserAdmin(UserAdmin):
'last_login',
'date_joined',
'source')
#lht: 定义在列表页面中可点击跳转到编辑页面的字段
list_display_links = ('id', 'username')
#lht: 定义默认排序方式
ordering = ('-id',)

@ -2,4 +2,5 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
#lht:指定应用的名称Django会根据这个名称找到对应的应用目录
name = 'accounts'

@ -9,8 +9,11 @@ from .models import BlogUser
class LoginForm(AuthenticationForm):
#lht: 登录表单继承Django内置认证表单
def __init__(self, *args, **kwargs):
#lht: 调用父类构造函数
super(LoginForm, self).__init__(*args, **kwargs)
#lht: 自定义用户名和密码字段的显示样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['password'].widget = widgets.PasswordInput(
@ -18,9 +21,11 @@ class LoginForm(AuthenticationForm):
class RegisterForm(UserCreationForm):
#lht: 用户注册表单继承Django内置用户创建表单
def __init__(self, *args, **kwargs):
#lht: 调用父类构造函数
super(RegisterForm, self).__init__(*args, **kwargs)
#lht: 自定义各字段的显示样式
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
@ -31,17 +36,21 @@ class RegisterForm(UserCreationForm):
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
#lht: 验证邮箱唯一性
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
#lht: 指定模型和字段
model = get_user_model()
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
#lht: 忘记密码表单
#lht: 新密码字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -52,6 +61,7 @@ class ForgetPasswordForm(forms.Form):
),
)
#lht: 确认新密码字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
@ -62,6 +72,7 @@ class ForgetPasswordForm(forms.Form):
),
)
#lht: 邮箱字段
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
@ -72,6 +83,7 @@ class ForgetPasswordForm(forms.Form):
),
)
#lht: 验证码字段
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
@ -83,25 +95,30 @@ class ForgetPasswordForm(forms.Form):
)
def clean_new_password2(self):
#lht: 验证两次输入的密码是否一致
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"))
#lht: 验证密码强度
password_validation.validate_password(password2)
return password2
def clean_email(self):
#lht: 验证邮箱是否存在
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
#lht: todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
#lht: 验证验证码是否正确
code = self.cleaned_data.get("code")
#lht: 调用工具函数验证验证码
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
@ -112,6 +129,8 @@ class ForgetPasswordForm(forms.Form):
class ForgetPasswordCodeForm(forms.Form):
#lht: 忘记密码时获取验证码的表单
#lht: 邮箱字段
email = forms.EmailField(
label=_('Email'),
)

@ -7,41 +7,62 @@ import django.utils.timezone
class Migration(migrations.Migration):
#lht: 标记这是一个初始迁移文件
initial = True
#lht: 定义依赖关系该迁移依赖于auth应用的0012_alter_user_first_name_max_length迁移
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
#lht: 创建BlogUser模型的操作
migrations.CreateModel(
name='BlogUser',
fields=[
#lht: 主键字段自动创建的BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#lht: 密码字段,存储加密后的密码
('password', models.CharField(max_length=128, verbose_name='password')),
#lht: 上次登录时间字段
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
#lht: 超级用户状态字段,拥有所有权限
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
#lht: 用户名字段,具有唯一性约束和验证器
('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')),
#lht: 名字字段
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
#lht: 姓氏字段
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
#lht: 邮箱地址字段
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
#lht: 员工状态字段,决定是否可以登录管理站点
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
#lht: 活跃状态字段,决定用户账户是否有效
('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')),
#lht: 加入日期字段
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
#lht: 昵称字段,博客用户的额外信息
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
#lht: 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#lht: 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#lht: 创建来源字段,标记用户通过何种方式创建
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
#lht: 用户组关联字段,多对多关系
('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')),
#lht: 用户权限字段,多对多关系
('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')),
],
#lht: 模型选项配置
options={
'verbose_name': '用户',
'verbose_name_plural': '用户',
'ordering': ['-id'],
'get_latest_by': 'id',
'verbose_name': '用户', #lht: 单数名称
'verbose_name_plural': '用户', #lht: 复数名称
'ordering': ['-id'], #lht: 默认排序方式按ID降序
'get_latest_by': 'id', #lht: 获取最新记录的依据字段
},
#lht: 模型管理器
managers=[
('objects', django.contrib.auth.models.UserManager()),
],

@ -1,46 +1,70 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
# 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):
#lht: 标记这是一个初始迁移文件
initial = True
#lht: 定义依赖关系该迁移依赖于auth应用的0012_alter_user_first_name_max_length迁移
dependencies = [
('accounts', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'),
]
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'),
#lht: 创建BlogUser模型的操作
migrations.CreateModel(
name='BlogUser',
fields=[
#lht: 主键字段自动创建的BigAutoField
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#lht: 密码字段,存储加密后的密码
('password', models.CharField(max_length=128, verbose_name='password')),
#lht: 上次登录时间字段
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
#lht: 超级用户状态字段,拥有所有权限
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
#lht: 用户名字段,具有唯一性约束和验证器
('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')),
#lht: 名字字段
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
#lht: 姓氏字段
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
#lht: 邮箱地址字段
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
#lht: 员工状态字段,决定是否可以登录管理站点
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
#lht: 活跃状态字段,决定用户账户是否有效
('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')),
#lht: 加入日期字段
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
#lht: 昵称字段,博客用户的额外信息
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
#lht: 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#lht: 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#lht: 创建来源字段,标记用户通过何种方式创建
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
#lht: 用户组关联字段,多对多关系
('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')),
#lht: 用户权限字段,多对多关系
('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')),
],
#lht: 模型选项配置
options={
'verbose_name': '用户', #lht: 单数名称
'verbose_name_plural': '用户', #lht: 复数名称
'ordering': ['-id'], #lht: 默认排序方式按ID降序
'get_latest_by': 'id', #lht: 获取最新记录的依据字段
},
#lht: 模型管理器
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -6,30 +6,38 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
# Create your models here.
#lht: Create your models here.
class BlogUser(AbstractUser):
#lht: 用户昵称字段
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
#lht: 用户创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
#lht: 用户最后修改时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#lht: 用户来源标识(如通过注册、后台创建等)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
#lht: 返回用户个人页面的URL
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
#lht: 字符串表示,返回用户邮箱
return self.email
def get_full_url(self):
#lht: 获取用户页面的完整URL
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'
#lht: 模型元数据配置
ordering = ['-id'] #lht: 默认按ID倒序排列
verbose_name = _('user') #lht: 单数名称
verbose_name_plural = verbose_name #lht: 复数名称
get_latest_by = 'id' #lht: 获取最新记录的字段

@ -9,34 +9,51 @@ from djangoblog.utils import *
from . import utils
# Create your tests here.
#lht: Create your tests here.
class AccountTest(TestCase):
#lht: """
#lht: 账户功能测试类
#lht: 继承Django的TestCase用于测试账户相关的各种功能
#lht: """
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
#lht: """
#lht: 测试前的准备工作
#lht: 每个测试方法执行前都会调用此方法
#lht: """
self.client = Client() #lht: 创建测试客户端用于模拟HTTP请求
self.factory = RequestFactory() #lht: 创建请求工厂,用于创建请求对象
#lht: 创建一个测试用户,用于后续的测试
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--="
self.new_test = "xxx123--=" #lht: 设置测试用的新密码
def test_validate_account(self):
#lht: """
#lht: 测试账户验证功能
#lht: 包括超级用户创建、登录验证、管理员权限等
#lht: """
site = get_current_site().domain
#lht: 创建超级用户用于测试
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
#lht: 测试用户登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
self.assertEqual(loginresult, True) #lht: 验证登录成功
response = self.client.get('/admin/') #lht: 访问管理后台
self.assertEqual(response.status_code, 200) #lht: 验证访问成功
#lht: 创建分类和文章用于测试
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
@ -52,24 +69,36 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
#lht: 测试能否正常访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
#lht: """
#lht: 测试用户注册流程
#lht: 包括注册、邮箱验证、登录、权限设置等完整流程
#lht: """
#lht: 验证目标邮箱尚未注册
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
#lht: 模拟用户注册请求
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',
})
#lht: 验证用户已成功创建
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
#lht: 获取新创建的用户并验证邮箱链接
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
@ -78,12 +107,17 @@ class AccountTest(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
#lht: 使用新用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
#lht: 设置用户为超级用户和员工,以便访问管理功能
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
#lht: 创建分类和文章
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
@ -100,52 +134,71 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
#lht: 验证能够访问文章管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
#lht: 测试用户登出功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
#lht: 登出后应无法访问管理页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
#lht: 测试使用错误密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
#lht: 登录失败后仍无法访问管理页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
#lht: """
#lht: 测试邮箱验证码验证功能
#lht: """
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
code = generate_code() #lht: 生成验证码
utils.set_code(to_email, code) #lht: 设置验证码
utils.send_verify_email(to_email, code) #lht: 发送验证码(模拟)
#lht: 验证正确的验证码能通过验证
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
#lht: 验证错误的验证码不能通过验证
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
#lht: """
#lht: 测试忘记密码时成功获取验证码
#lht: """
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
#lht: 验证请求成功且返回"ok"
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
#lht: """
#lht: 测试忘记密码时获取验证码失败的情况
#lht: """
#lht: 测试没有提供邮箱的情况
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
#lht: 测试提供无效邮箱的情况
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
@ -153,32 +206,42 @@ class AccountTest(TestCase):
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
#lht: """
#lht: 测试成功重置密码的完整流程
#lht: """
code = generate_code()
utils.set_code(self.blog_user.email, code)
utils.set_code(self.blog_user.email, code) #lht: 设置验证码
#lht: 准备重置密码的数据
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
#lht: 发送重置密码请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp.status_code, 302) #lht: 重定向表示成功
# 验证用户密码是否修改成功
#lht: 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
).first() #lht: 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):
#lht: """
#lht: 测试为不存在的用户重置密码的情况
#lht: """
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
email="123@123.com", #lht: 不存在的邮箱
code="123456",
)
resp = self.client.post(
@ -186,22 +249,27 @@ class AccountTest(TestCase):
data=data
)
#lht: 应该返回200状态码而不是重定向因为验证失败
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
#lht: """
#lht: 测试使用错误验证码重置密码的情况
#lht: """
code = generate_code()
utils.set_code(self.blog_user.email, code)
#lht: 使用错误的验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
code="111111", #lht: 错误的验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
#lht: 应该返回200状态码而不是重定向因为验证失败
self.assertEqual(resp.status_code, 200)

@ -4,25 +4,32 @@ from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts"
app_name = "accounts" #lht: 应用命名空间
urlpatterns = [re_path(r'^login/$',
urlpatterns = [
#lht: 登录URL
re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
#lht: 注册URL
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
#lht: 登出URL
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
#lht: 账户操作结果页面
path(r'account/result.html',
views.account_result,
name='result'),
#lht: 忘记密码页面
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
#lht: 获取忘记密码验证码
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]
]

@ -3,11 +3,12 @@ from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
"""
#lht: """
#lht: 允许使用用户名或邮箱登录
#lht: """
def authenticate(self, request, username=None, password=None, **kwargs):
#lht: 根据输入内容判断是邮箱还是用户名
if '@' in username:
kwargs = {'email': username}
else:
@ -20,6 +21,7 @@ class EmailOrUsernameModelBackend(ModelBackend):
return None
def get_user(self, username):
#lht: 根据用户名获取用户对象
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:

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

@ -29,21 +29,24 @@ from .models import BlogUser
logger = logging.getLogger(__name__)
# Create your views here.
#lht: Create your views here.
class RegisterView(FormView):
#lht: 用户注册视图
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
#lht: 处理请求分发添加CSRF保护装饰器
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
#lht: 表单验证成功时的处理逻辑
if form.is_valid():
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.is_active = False #lht: 新注册用户默认不激活
user.source = 'Register' #lht: 标记来源为注册
user.save(True)
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
@ -54,6 +57,7 @@ class RegisterView(FormView):
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
#lht: 构造验证邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
@ -81,33 +85,38 @@ class RegisterView(FormView):
class LogoutView(RedirectView):
#lht: 用户登出视图
url = '/login/'
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#lht: 处理请求分发,添加不缓存装饰器
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
#lht: 处理GET请求执行登出操作
logout(request)
delete_sidebar_cache()
delete_sidebar_cache() #lht: 清除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
#lht: 用户登录视图
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
login_ttl = 2626560 #lht: 登录会话保持时间(一个月)
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#lht: 处理请求分发添加敏感参数保护、CSRF保护和不缓存装饰器
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
#lht: 获取重定向URL
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
@ -116,6 +125,7 @@ class LoginView(FormView):
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
#lht: 表单验证成功时的处理逻辑
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
@ -126,14 +136,13 @@ class LoginView(FormView):
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):
#lht: 获取登录成功后的跳转URL
redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
@ -143,6 +152,7 @@ class LoginView(FormView):
def account_result(request):
#lht: 账户操作结果页面
type = request.GET.get('type')
id = request.GET.get('id')
@ -176,10 +186,12 @@ def account_result(request):
class ForgetPasswordView(FormView):
#lht: 忘记密码视图
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
#lht: 表单验证成功时的处理逻辑
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"])
@ -190,8 +202,10 @@ class ForgetPasswordView(FormView):
class ForgetPasswordEmailCode(View):
#lht: 发送忘记密码验证码视图
def post(self, request: HttpRequest):
#lht: 处理POST请求发送验证码邮件
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")

@ -0,0 +1,64 @@
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
super().__init__(name)
def has_permission(self, request):
return request.user.is_superuser
# def get_urls(self):
# urls = super().get_urls()
# from django.urls import path
# from blog.views import refresh_memcache
#
# my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# return urls + my_urls
admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
admin_site.register(BlogUser, BlogUserAdmin)
admin_site.register(Comment, CommentAdmin)
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin)

@ -0,0 +1,11 @@
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
def ready(self):
super().ready()
# Import and load plugins here
from .plugin_manage.loader import load_plugins
load_plugins()

@ -1,112 +1,172 @@
#zf导入Django表单模块
from django import forms
#zf导入Django管理后台模块
from django.contrib import admin
#zf导入获取用户模型的函数
from django.contrib.auth import get_user_model
#zf导入URL反向解析函数
from django.urls import reverse
#zf导入HTML格式化函数
from django.utils.html import format_html
#zf导入国际化翻译函数
from django.utils.translation import gettext_lazy as _
# Register your models here.
#zf导入博客应用的Article模型
from .models import Article
#zf定义文章表单类继承自ModelForm
class ArticleForm(forms.ModelForm):
#zf被注释掉的代码使用AdminPagedownWidget作为body字段的widget
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
#zf包含所有字段
fields = '__all__'
#zf定义批量发布文章的操作函数
def makr_article_publish(modeladmin, request, queryset):
#zf将选中文章的状态更新为'p'(已发布)
queryset.update(status='p')
#zf定义批量将文章设为草稿的操作函数
def draft_article(modeladmin, request, queryset):
#zf将选中文章的状态更新为'd'(草稿)
queryset.update(status='d')
#zf定义批量关闭文章评论的操作函数
def close_article_commentstatus(modeladmin, request, queryset):
#zf将选中文章的评论状态更新为'c'(关闭)
queryset.update(comment_status='c')
#zf定义批量开启文章评论的操作函数
def open_article_commentstatus(modeladmin, request, queryset):
#zf将选中文章的评论状态更新为'o'(开启)
queryset.update(comment_status='o')
#zf为操作函数设置描述信息用于在管理后台显示支持国际化
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')
#zf定义文章管理类继承自ModelAdmin
class ArticlelAdmin(admin.ModelAdmin):
#zf每页显示20条记录
list_per_page = 20
#zf设置可搜索的字段为body和title
search_fields = ('body', 'title')
#zf使用自定义的ArticleForm
form = ArticleForm
#zf设置在列表页显示的字段
list_display = (
'id',
'title',
'author',
#zf自定义的分类链接字段
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
#zf设置哪些字段可以作为链接点击进入编辑页
list_display_links = ('id', 'title')
#zf设置右侧的过滤器字段
list_filter = ('status', 'type', 'category')
#zf对tags字段使用水平过滤器
filter_horizontal = ('tags',)
#zf在表单中排除这些字段由系统自动管理
exclude = ('creation_time', 'last_modify_time')
#zf启用"在站点上查看"功能
view_on_site = True
#zf注册自定义的管理操作
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
#zf自定义字段显示分类链接
def link_to_category(self, obj):
#zf获取分类模型的app_label和model_name
info = (obj.category._meta.app_label, obj.category._meta.model_name)
#zf生成分类编辑页的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
#zf返回HTML链接
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
#zf设置字段显示名称
link_to_category.short_description = _('category')
#zf自定义表单获取方法
def get_form(self, request, obj=None, **kwargs):
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
#zf限制作者字段只能选择超级用户
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
#zf保存模型的方法
def save_model(self, request, obj, form, change):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
#zf获取在站点上查看的URL
def get_view_on_site_url(self, obj=None):
if obj:
#zf获取文章的完整URL
url = obj.get_full_url()
return url
else:
from djangoblog.utils import get_current_site
#zf获取当前站点域名
site = get_current_site().domain
return site
#zf定义标签管理类
class TagAdmin(admin.ModelAdmin):
#zf排除这些字段由系统自动管理
exclude = ('slug', 'last_mod_time', 'creation_time')
#zf定义分类管理类
class CategoryAdmin(admin.ModelAdmin):
#zf设置在列表页显示的字段
list_display = ('name', 'parent_category', 'index')
#zf排除这些字段由系统自动管理
exclude = ('slug', 'last_mod_time', 'creation_time')
#zf定义链接管理类
class LinksAdmin(admin.ModelAdmin):
#zf排除这些字段由系统自动管理
exclude = ('last_mod_time', 'creation_time')
#zf定义侧边栏管理类
class SideBarAdmin(admin.ModelAdmin):
#zf设置在列表页显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
#zf排除这些字段由系统自动管理
exclude = ('last_mod_time', 'creation_time')
#zf定义博客设置管理类
class BlogSettingsAdmin(admin.ModelAdmin):
#zf没有特殊配置
pass
#zf注册模型到管理后台
#zf注意在你提供的代码中模型注册部分被省略了通常应该包含如下代码
#zfadmin.site.register(Article, ArticlelAdmin)
#zfadmin.site.register(Tag, TagAdmin)
#zfadmin.site.register(Category, CategoryAdmin)
#zfadmin.site.register(Links, LinksAdmin)
#zfadmin.site.register(SideBar, SideBarAdmin)
#zfadmin.site.register(BlogSettings, BlogSettingsAdmin)

@ -1,5 +1,9 @@
#zf导入Django的应用配置基类
from django.apps import AppConfig
#zf定义博客应用的配置类继承自AppConfig
class BlogConfig(AppConfig):
#zf设置应用的名称为'blog'
#zf这个名称需要与Django项目中应用的目录名称保持一致
name = 'blog'

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

@ -1,26 +1,42 @@
#zf导入时间模块用于生成时间戳
import time
#zf导入elasticsearch客户端模块
import elasticsearch.client
#zf导入Django配置模块
from django.conf import settings
#zf从elasticsearch_dsl导入各种字段类型和文档类
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
#zf从elasticsearch_dsl.connections导入连接管理器
from elasticsearch_dsl.connections import connections
#zf从blog.models导入Article模型
from blog.models import Article
#zf检查是否启用了Elasticsearch功能通过检查settings中是否有ELASTICSEARCH_DSL配置
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
#zf如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
#zf创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
#zf导入Elasticsearch客户端
from elasticsearch import Elasticsearch
#zf创建Elasticsearch实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
#zf导入IngestClient用于管理管道
from elasticsearch.client import IngestClient
#zf创建IngestClient实例
c = IngestClient(es)
try:
#zf尝试获取名为'geoip'的管道
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
#zf如果管道不存在则创建一个geoip管道
#zf该管道用于根据IP地址添加地理位置信息
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -33,73 +49,117 @@ if ELASTICSEARCH_ENABLED:
}''')
#zf定义GeoIp内部文档类用于存储地理位置信息
class GeoIp(InnerDoc):
#zf大洲名称
continent_name = Keyword()
#zf国家ISO代码
country_iso_code = Keyword()
#zf国家名称
country_name = Keyword()
#zf地理位置坐标
location = GeoPoint()
#zf定义UserAgentBrowser内部文档类用于存储浏览器信息
class UserAgentBrowser(InnerDoc):
#zf浏览器家族
Family = Keyword()
#zf浏览器版本
Version = Keyword()
#zf定义UserAgentOS内部文档类继承自UserAgentBrowser用于存储操作系统信息
class UserAgentOS(UserAgentBrowser):
pass
#zf定义UserAgentDevice内部文档类用于存储设备信息
class UserAgentDevice(InnerDoc):
#zf设备家族
Family = Keyword()
#zf设备品牌
Brand = Keyword()
#zf设备型号
Model = Keyword()
#zf定义UserAgent内部文档类用于存储用户代理信息
class UserAgent(InnerDoc):
#zf浏览器信息
browser = Object(UserAgentBrowser, required=False)
#zf操作系统信息
os = Object(UserAgentOS, required=False)
#zf设备信息
device = Object(UserAgentDevice, required=False)
#zf完整的User-Agent字符串
string = Text()
#zf是否为机器人
is_bot = Boolean()
#zf定义ElapsedTimeDocument文档类用于存储页面性能数据
class ElapsedTimeDocument(Document):
#zfURL地址
url = Keyword()
#zf耗时毫秒
time_taken = Long()
#zf记录时间
log_datetime = Date()
#zfIP地址
ip = Keyword()
#zf地理位置信息
geoip = Object(GeoIp, required=False)
#zf用户代理信息
useragent = Object(UserAgent, required=False)
#zf定义索引配置
class Index:
#zf索引名称
name = 'performance'
settings = {
#zf分片数量
"number_of_shards": 1,
#zf副本数量
"number_of_replicas": 0
}
#zf定义文档元数据
class Meta:
#zf文档类型
doc_type = 'ElapsedTime'
#zf定义ElapsedTime文档管理器类
class ElaspedTimeDocumentManager:
#zf静态方法构建索引
@staticmethod
def build_index():
from elasticsearch import Elasticsearch
#zf创建Elasticsearch客户端
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
#zf检查performance索引是否存在
res = client.indices.exists(index="performance")
if not res:
#zf如果不存在则初始化索引
ElapsedTimeDocument.init()
#zf静态方法删除索引
@staticmethod
def delete_index():
from elasticsearch import Elasticsearch
#zf创建Elasticsearch实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
#zf删除performance索引忽略400和404错误
es.indices.delete(index='performance', ignore=[400, 404])
#zf静态方法创建性能记录文档
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
#zf构建索引
ElaspedTimeDocumentManager.build_index()
#zf创建UserAgent对象并填充数据
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
@ -116,98 +176,153 @@ class ElaspedTimeDocumentManager:
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
#zf创建ElapsedTimeDocument文档
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
1000))
1000)) #zf使用当前时间戳作为ID
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
useragent=ua,
ip=ip)
#zf保存文档并使用geoip管道处理
doc.save(pipeline="geoip")
#zf定义ArticleDocument文档类用于存储文章搜索数据
class ArticleDocument(Document):
#zf文章正文使用ik_max_word分词器进行索引ik_smart进行搜索
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
#zf文章标题使用ik_max_word分词器进行索引ik_smart进行搜索
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
#zf作者信息
author = Object(properties={
#zf昵称
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
#zfID
'id': Integer()
})
#zf分类信息
category = Object(properties={
#zf分类名
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
#zfID
'id': Integer()
})
#zf标签信息
tags = Object(properties={
#zf标签名
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
#zfID
'id': Integer()
})
#zf发布时间
pub_time = Date()
#zf文章状态
status = Text()
#zf评论状态
comment_status = Text()
#zf文章类型
type = Text()
#zf浏览量
views = Integer()
#zf文章排序
article_order = Integer()
#zf定义索引配置
class Index:
#zf索引名称
name = 'blog'
settings = {
#zf分片数量
"number_of_shards": 1,
#zf副本数量
"number_of_replicas": 0
}
#zf定义文档元数据
class Meta:
#zf文档类型
doc_type = 'Article'
#zf定义ArticleDocument管理器类
class ArticleDocumentManager():
#zf初始化方法
def __init__(self):
self.create_index()
#zf创建索引方法
def create_index(self):
ArticleDocument.init()
#zf删除索引方法
def delete_index(self):
from elasticsearch import Elasticsearch
#zf创建Elasticsearch实例
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
#zf删除blog索引忽略400和404错误
es.indices.delete(index='blog', ignore=[400, 404])
#zf将文章对象转换为文档对象的方法
def convert_to_doc(self, articles):
return [
ArticleDocument(
meta={
#zf使用文章ID作为文档ID
'id': article.id},
#zf文章正文
body=article.body,
#zf文章标题
title=article.title,
author={
#zf作者昵称
'nickname': article.author.username,
#zf作者ID
'id': article.author.id},
category={
#zf分类名
'name': article.category.name,
#zf分类ID
'id': article.category.id},
tags=[
{
#zf标签名
'name': t.name,
#zf标签ID
'id': t.id} for t in article.tags.all()],
#zf发布时间
pub_time=article.pub_time,
#zf文章状态
status=article.status,
#zf评论状态
comment_status=article.comment_status,
#zf文章类型
type=article.type,
#zf浏览量
views=article.views,
#zf排序
article_order=article.article_order) for article in articles]
#zf重建索引方法
def rebuild(self, articles=None):
#zf初始化索引
ArticleDocument.init()
#zf如果没有提供文章列表则获取所有文章
articles = articles if articles else Article.objects.all()
#zf转换文章为文档对象
docs = self.convert_to_doc(articles)
#zf保存所有文档
for doc in docs:
doc.save()
#zf更新文档方法
def update_docs(self, docs):
#zf保存所有文档
for doc in docs:
doc.save()

@ -1,19 +1,34 @@
# 导入日志模块,用于记录日志信息
import logging
# 导入Django表单模块
from django import forms
# 从haystack.forms导入SearchForm用于实现搜索功能
from haystack.forms import SearchForm
# 创建日志记录器
logger = logging.getLogger(__name__)
# 定义博客搜索表单类继承自Haystack的SearchForm
class BlogSearchForm(SearchForm):
# 定义搜索查询字段,设置为必填项
querydata = forms.CharField(required=True)
# 重写search方法实现自定义搜索逻辑
def search(self):
# 调用父类的search方法获取搜索结果
datas = super(BlogSearchForm, self).search()
# 检查表单数据是否有效
if not self.is_valid():
# 如果无效,返回无查询结果
return self.no_query_found()
# 如果查询数据存在
if self.cleaned_data['querydata']:
# 记录查询关键字到日志中
logger.info(self.cleaned_data['querydata'])
# 返回搜索结果
return datas

@ -1,18 +1,25 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从blog.documents模块导入需要用到的文档类和管理器以及Elasticsearch启用状态常量
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化
# TODO 参数化 - 待办事项,提示需要将某些配置参数化
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help build_index 时会显示
help = 'build search index'
def handle(self, *args, **options):
# 检查是否启用了Elasticsearch功能
if ELASTICSEARCH_ENABLED:
# 构建时间文档索引
ElaspedTimeDocumentManager.build_index()
# 创建ElapsedTimeDocument实例并初始化
manager = ElapsedTimeDocument()
manager.init()
# 创建ArticleDocumentManager实例删除现有索引后重新构建
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()

@ -1,13 +1,18 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从blog.models模块导入Tag和Category模型
from blog.models import Tag, Category
# TODO 参数化
# TODO 参数化 - 待办事项,提示需要将某些配置参数化
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help build_search_words 时会显示
help = 'build search words'
def handle(self, *args, **options):
# 收集所有标签(Tag)和分类(Category)的名称并用set去重
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
# 将所有名称用换行符连接并打印输出
print('\n'.join(datas))

@ -1,11 +1,16 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从djangoblog.utils模块导入缓存工具
from djangoblog.utils import cache
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help clear_cache 时会显示
help = 'clear the whole cache'
def handle(self, *args, **options):
# 清除整个缓存
cache.clear()
# 使用标准输出打印成功信息,显示"缓存已清除"的消息
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -1,40 +1,61 @@
# 导入获取用户模型的函数用于操作Django内置的用户认证系统
from django.contrib.auth import get_user_model
# 导入密码加密函数,用于安全地存储用户密码
from django.contrib.auth.hashers import make_password
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从blog.models模块导入文章、标签和分类模型
from blog.models import Article, Tag, Category
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help create_testdata 时会显示
help = 'create test datas'
def handle(self, *args, **options):
# 获取或创建一个测试用户邮箱为test@test.com用户名为"测试用户",密码经过加密处理
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]
# 获取或创建一个子分类,名称为"子类目"父级分类为上面创建的pcategory
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
# 保存分类虽然get_or_create已经保存过这里再次显式调用save
category.save()
# 创建一个基础标签对象,名称为"标签"
basetag = Tag()
basetag.name = "标签"
basetag.save()
# 循环创建19篇文章序号从1到19
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 +1,77 @@
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 从djangoblog.spider_notify模块导入SpiderNotify类用于通知搜索引擎
from djangoblog.spider_notify import SpiderNotify
# 从djangoblog.utils模块导入get_current_site函数用于获取当前站点信息
from djangoblog.utils import get_current_site
# 从blog.models模块导入文章、标签和分类模型
from blog.models import Article, Tag, Category
# 获取当前站点的域名
site = get_current_site().domain
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help ping_baidu 时会显示
help = 'notify baidu url'
# 添加命令行参数
def add_arguments(self, parser):
# 添加data_type参数指定要通知的数据类型
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')
# 构造完整URL的方法
def get_full_url(self, path):
# 使用站点域名和路径拼接成完整HTTPS URL
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)
# 初始化URL列表
urls = []
# 如果是指定文章或全部则获取所有已发布文章的URL
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
for article in Article.objects.filter(status='p'): # status='p'表示已发布(published)
urls.append(article.get_full_url())
# 如果是指定标签或全部则获取所有标签页面的URL
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
url = tag.get_absolute_url() # 获取标签的相对路径
urls.append(self.get_full_url(url)) # 转换为完整URL
# 如果是指定分类或全部则获取所有分类页面的URL
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
url = category.get_absolute_url() # 获取分类的相对路径
urls.append(self.get_full_url(url)) # 转换为完整URL
# 输出即将通知的URL数量
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
# 调用百度通知接口推送所有URL
SpiderNotify.baidu_notify(urls)
# 输出完成通知的信息
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -1,47 +1,84 @@
# 导入requests库用于发送HTTP请求测试图片链接有效性
import requests
# 导入Django的基础命令类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# 导入static函数用于获取静态文件的URL
from django.templatetags.static import static
# 从djangoblog.utils模块导入save_user_avatar函数用于保存用户头像
from djangoblog.utils import save_user_avatar
# 从oauth.models模块导入OAuthUser模型用于操作OAuth用户
from oauth.models import OAuthUser
# 从oauth.oauthmanager模块导入get_manager_by_type函数用于获取对应类型的OAuth管理器
from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
# 命令的帮助文本,在运行 python manage.py help sync_user_avatar 时会显示
help = 'sync user avatar'
# 测试图片URL是否有效的方法
def test_picture(self, url):
try:
# 发送GET请求测试URL设置2秒超时
if requests.get(url, timeout=2).status_code == 200:
# 如果返回状态码为200说明图片有效返回True
return True
except:
# 捕获异常如网络错误或超时直接pass
pass
# 命令主处理逻辑
def handle(self, *args, **options):
# 获取静态文件根路径的URL
static_url = static("../")
# 获取所有OAuth用户
users = OAuthUser.objects.all()
# 输出开始同步的用户数量信息
self.stdout.write(f'开始同步{len(users)}个用户头像')
# 遍历所有用户进行头像同步
for u in users:
# 输出正在同步的用户昵称
self.stdout.write(f'开始同步:{u.nickname}')
# 获取用户当前头像URL
url = u.picture
# 如果用户有头像URL
if url:
# 如果头像URL以静态URL开头说明是本地静态文件
if url.startswith(static_url):
# 测试图片是否有效
if self.test_picture(url):
# 如果有效则跳过该用户,继续下一个
continue
else:
# 如果无效但用户有元数据
if u.metadata:
# 根据用户类型获取对应的OAuth管理器
manage = get_manager_by_type(u.type)
# 通过管理器和元数据获取新的头像URL
url = manage.get_picture(u.metadata)
# 保存用户头像到本地
url = save_user_avatar(url)
else:
# 如果没有元数据,则使用默认头像
url = static('blog/img/avatar.png')
else:
# 如果不是本地静态文件,则保存用户头像到本地
url = save_user_avatar(url)
else:
# 如果用户没有头像,则使用默认头像
url = static('blog/img/avatar.png')
# 如果获取到了有效的头像URL
if url:
# 输出同步完成的信息和新的头像URL
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
# 更新用户的头像URL并保存
u.picture = url
u.save()
# 输出同步结束信息
self.stdout.write('结束同步')

@ -1,42 +1,406 @@
import logging
import time
#zf导入os模块用于文件路径操作
import os
from ipware import get_client_ip
from user_agents import parse
#zf从django.conf导入settings配置
from django.conf import settings
#zf从django.core.files.uploadedfile导入SimpleUploadedFile用于模拟文件上传
from django.core.files.uploadedfile import SimpleUploadedFile
#zf从django.core.management导入call_command用于调用Django管理命令
from django.core.management import call_command
#zf从django.core.paginator导入Paginator用于分页功能测试
from django.core.paginator import Paginator
#zf从django.templatetags.static导入static函数用于处理静态文件
from django.templatetags.static import static
#zf从django.test导入Client, RequestFactory, TestCase用于测试
from django.test import Client, RequestFactory, TestCase
#zf从django.urls导入reverse用于URL反向解析
from django.urls import reverse
#zf从django.utils导入timezone用于处理时区
from django.utils import timezone
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
#zf从accounts.models导入BlogUser博客用户模型
from accounts.models import BlogUser
#zf从blog.forms导入BlogSearchForm博客搜索表单
from blog.forms import BlogSearchForm
#zf从blog.models导入Article, Category, Tag, SideBar, Links博客相关模型
from blog.models import Article, Category, Tag, SideBar, Links
#zf从blog.templatetags.blog_tags导入load_pagination_info, load_articletags模板标签
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
#zf从djangoblog.utils导入get_current_site, get_sha256工具函数
from djangoblog.utils import get_current_site, get_sha256
#zf从oauth.models导入OAuthUser, OAuthConfig OAuth认证相关模型
from oauth.models import OAuthUser, OAuthConfig
logger = logging.getLogger(__name__)
#zf创建你的测试用例
class OnlineMiddleware(object):
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
#zf定义ArticleTest测试类继承自Django的TestCase
class ArticleTest(TestCase):
#zf测试初始化方法在每个测试方法执行前运行
def setUp(self):
#zf创建测试客户端用于模拟HTTP请求
self.client = Client()
#zf创建请求工厂用于创建请求对象
self.factory = RequestFactory()
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)
user_agent = parse(http_user_agent)
if not response.streaming:
try:
cast_time = time.time() - start_time
#zf测试文章验证功能
def test_validate_article(self):
#zf获取当前站点域名
site = get_current_site().domain
#zf创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
#zf设置用户密码
user.set_password("liangliangyy")
#zf设置用户为管理员
user.is_staff = True
#zf设置用户为超级用户
user.is_superuser = True
#zf保存用户
user.save()
#zf测试访问用户个人页面
response = self.client.get(user.get_absolute_url())
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf测试访问管理后台邮件发送日志
response = self.client.get('/admin/servermanager/emailsendlog/')
#zf测试访问管理后台日志条目
response = self.client.get('admin/admin/logentry/')
#zf创建侧边栏对象
s = SideBar()
#zf设置侧边栏排序
s.sequence = 1
#zf设置侧边栏名称
s.name = 'test'
#zf设置侧边栏内容
s.content = 'test content'
#zf设置侧边栏启用状态
s.is_enable = True
#zf保存侧边栏
s.save()
#zf创建分类对象
category = Category()
#zf设置分类名称
category.name = "category"
#zf设置分类创建时间
category.creation_time = timezone.now()
#zf设置分类最后修改时间
category.last_mod_time = timezone.now()
#zf保存分类
category.save()
#zf创建标签对象
tag = Tag()
#zf设置标签名称
tag.name = "nicetag"
#zf保存标签
tag.save()
#zf创建文章对象
article = Article()
#zf设置文章标题
article.title = "nicetitle"
#zf设置文章正文
article.body = "nicecontent"
#zf设置文章作者
article.author = user
#zf设置文章分类
article.category = category
#zf设置文章类型为文章
article.type = 'a'
#zf设置文章状态为已发布
article.status = 'p'
#zf保存文章
article.save()
#zf断言文章标签数量为0
self.assertEqual(0, article.tags.count())
#zf给文章添加标签
article.tags.add(tag)
#zf保存文章
article.save()
#zf断言文章标签数量为1
self.assertEqual(1, article.tags.count())
#zf循环创建20篇文章用于分页测试
for i in range(20):
article = Article()
#zf设置文章标题
article.title = "nicetitle" + str(i)
#zf设置文章正文
article.body = "nicetitle" + str(i)
#zf设置文章作者
article.author = user
#zf设置文章分类
article.category = category
#zf设置文章类型为文章
article.type = 'a'
#zf设置文章状态为已发布
article.status = 'p'
#zf保存文章
article.save()
#zf给文章添加标签
article.tags.add(tag)
#zf保存文章
article.save()
#zf从blog.documents导入ELASTICSEARCH_ENABLED常量
from blog.documents import ELASTICSEARCH_ENABLED
#zf如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
#zf调用build_index管理命令构建搜索索引
call_command("build_index")
#zf测试搜索功能
response = self.client.get('/search', {'q': 'nicetitle'})
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf测试访问文章详情页
response = self.client.get(article.get_absolute_url())
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf从djangoblog.spider_notify导入SpiderNotify用于通知搜索引擎
from djangoblog.spider_notify import SpiderNotify
#zf通知搜索引擎爬虫
SpiderNotify.notify(article.get_absolute_url())
#zf测试访问标签详情页
response = self.client.get(tag.get_absolute_url())
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf测试访问分类详情页
response = self.client.get(category.get_absolute_url())
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf测试搜索功能
response = self.client.get('/search', {'q': 'django'})
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf加载文章标签
s = load_articletags(article)
#zf断言结果不为None
self.assertIsNotNone(s)
#zf用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
#zf测试访问文章归档页
response = self.client.get(reverse('blog:archives'))
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf创建所有文章的分页器
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
#zf检查分页功能
self.check_pagination(p, '', '')
#zf创建按标签筛选的分页器
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
#zf检查标签归档分页功能
self.check_pagination(p, '分类标签归档', tag.slug)
#zf创建按作者筛选的分页器
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
#zf检查作者归档分页功能
self.check_pagination(p, '作者文章归档', 'liangliangyy')
#zf创建按分类筛选的分页器
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
#zf检查分类归档分页功能
self.check_pagination(p, '分类目录归档', category.slug)
#zf创建博客搜索表单实例
f = BlogSearchForm()
#zf执行搜索
f.search()
#zf从djangoblog.spider_notify导入SpiderNotify
from djangoblog.spider_notify import SpiderNotify
#zf通知百度搜索引擎
SpiderNotify.baidu_notify([article.get_full_url()])
#zf从blog.templatetags.blog_tags导入gravatar_url, gravatar函数
from blog.templatetags.blog_tags import gravatar_url, gravatar
#zf获取gravatar头像URL
u = gravatar_url('liangliangyy@gmail.com')
#zf获取gravatar头像HTML
u = gravatar('liangliangyy@gmail.com')
#zf创建友情链接对象
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
#zf保存友情链接
link.save()
#zf测试访问友情链接页面
response = self.client.get('/links.html')
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf测试访问RSS订阅页面
response = self.client.get('/feed/')
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf测试访问站点地图
response = self.client.get('/sitemap.xml')
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf测试访问管理后台文章删除页面
self.client.get("/admin/blog/article/1/delete/")
#zf测试访问管理后台邮件发送日志
self.client.get('/admin/servermanager/emailsendlog/')
#zf测试访问管理后台日志条目
self.client.get('/admin/admin/logentry/')
#zf测试访问管理后台日志条目修改页面
self.client.get('/admin/admin/logentry/1/change/')
#zf检查分页功能的方法
def check_pagination(self, p, type, value):
#zf遍历所有分页
for page in range(1, p.num_pages + 1):
#zf加载分页信息
s = load_pagination_info(p.page(page), type, value)
#zf断言分页信息不为None
self.assertIsNotNone(s)
#zf如果有上一页URL
if s['previous_url']:
#zf测试访问上一页
response = self.client.get(s['previous_url'])
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf如果有下一页URL
if s['next_url']:
#zf测试访问下一页
response = self.client.get(s['next_url'])
#zf断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf测试图片上传功能
def test_image(self):
#zf导入requests模块用于下载图片
import requests
#zf下载Python官网Logo图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
#zf设置图片保存路径
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
#zf将图片保存到本地
with open(imagepath, 'wb') as file:
file.write(rsp.content)
#zf测试未登录上传图片应该被拒绝
rsp = self.client.post('/upload')
#zf断言响应状态码为403禁止访问
self.assertEqual(rsp.status_code, 403)
#zf生成上传签名
sign = get_sha256(get_sha256(settings.SECRET_KEY))
#zf打开图片文件准备上传
with open(imagepath, 'rb') as file:
#zf创建上传文件对象
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
#zf构造表单数据
form_data = {'python.png': imgfile}
#zf测试带签名上传图片
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
#zf断言响应状态码为200
self.assertEqual(rsp.status_code, 200)
#zf删除临时图片文件
os.remove(imagepath)
#zf从djangoblog.utils导入save_user_avatar, send_email工具函数
from djangoblog.utils import save_user_avatar, send_email
#zf测试发送邮件功能
send_email(['qq@qq.com'], 'testTitle', 'testContent')
#zf测试保存用户头像功能
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
#zf测试错误页面
def test_errorpage(self):
#zf测试访问不存在的页面
rsp = self.client.get('/eee')
#zf断言响应状态码为404
self.assertEqual(rsp.status_code, 404)
#zf测试管理命令
def test_commands(self):
#zf创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
#zf设置用户密码
user.set_password("liangliangyy")
#zf设置用户为管理员
user.is_staff = True
#zf设置用户为超级用户
user.is_superuser = True
#zf保存用户
user.save()
#zf创建OAuth配置对象
c = OAuthConfig()
#zf设置OAuth类型为QQ
c.type = 'qq'
#zf设置应用密钥
c.appkey = 'appkey'
#zf设置应用密钥
c.appsecret = 'appsecret'
#zf保存配置
c.save()
#zf创建OAuth用户对象
u = OAuthUser()
#zf设置OAuth类型为QQ
u.type = 'qq'
#zf设置openid
u.openid = 'openid'
#zf关联博客用户
u.user = user
#zf设置头像为静态图片
u.picture = static("/blog/img/avatar.png")
#zf设置用户元数据
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
#zf保存OAuth用户
u.save()
#zf创建另一个OAuth用户对象
u = OAuthUser()
#zf设置OAuth类型为QQ
u.type = 'qq'
#zf设置openid
u.openid = 'openid1'
#zf设置头像URL
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
#zf设置用户元数据
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
#zf保存OAuth用户
u.save()
#zf从blog.documents导入ELASTICSEARCH_ENABLED常量
from blog.documents import ELASTICSEARCH_ENABLED
#zf如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2)
url = request.path
from django.utils import timezone
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
return response
#zf调用build_index命令构建搜索索引
call_command("build_index")
#zf调用ping_baidu命令通知百度搜索引擎
call_command("ping_baidu", "all")
#zf调用create_testdata命令创建测试数据
call_command("create_testdata")
#zf调用clear_cache命令清除缓存
call_command("clear_cache")
#zf调用sync_user_avatar命令同步用户头像
call_command("sync_user_avatar")
#zf调用build_search_words命令构建搜索词
call_command("build_search_words")

@ -1,137 +1,219 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# 由Django 4.1.7在2023年3月2日生成的初始数据库迁移文件
# 导入Django配置模块
from django.conf import settings
# 导入Django数据库迁移相关模块
from django.db import migrations, models
# 导入Django模型关系相关模块
import django.db.models.deletion
# 导入Django时区工具
import django.utils.timezone
# 导入Markdown编辑器字段
import mdeditor.fields
class Migration(migrations.Migration):
# 标记这是一个初始迁移文件
initial = True
# 定义依赖关系,依赖于用户模型
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
# 定义具体的操作
operations = [
# 创建BlogSettings模型用于存储网站配置信息
migrations.CreateModel(
name='BlogSettings',
fields=[
# 主键字段,自动增长的大整数
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 网站名称最大长度200默认为空字符串
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
# 网站描述文本字段最大长度1000默认为空字符串
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
# 网站SEO描述文本字段最大长度1000默认为空字符串
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
# 网站关键字文本字段最大长度1000默认为空字符串
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
# 文章摘要长度整数类型默认300
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
# 侧边栏文章数目整数类型默认10
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
# 侧边栏评论数目整数类型默认5
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
# 文章页面默认显示评论数目整数类型默认5
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
# 是否显示谷歌广告布尔类型默认False
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
# 广告内容文本字段最大长度2000可为空默认为空字符串
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
# 是否打开网站评论功能布尔类型默认True
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
# 备案号字符字段最大长度2000可为空默认为空字符串
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
# 网站统计代码文本字段最大长度1000默认为空字符串
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
# 是否显示公安备案号布尔类型默认False
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
# 公安备案号文本字段最大长度2000可为空默认为空字符串
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
# 模型选项设置
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
'verbose_name': '网站配置', # 单数形式的可读名称
'verbose_name_plural': '网站配置', # 复数形式的可读名称
},
),
# 创建Links模型用于存储友情链接信息
migrations.CreateModel(
name='Links',
fields=[
# 主键字段,自动增长的大整数
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 链接名称最大长度30唯一约束
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
# 链接地址URL字段
('link', models.URLField(verbose_name='链接地址')),
# 排序,整数类型,唯一约束
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否显示布尔类型默认True
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
# 显示类型字符字段最大长度1可选值包括首页、列表页、文章页面、全站、友情链接页面默认为首页
('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'],
'verbose_name': '友情链接', # 单数形式的可读名称
'verbose_name_plural': '友情链接', # 复数形式的可读名称
'ordering': ['sequence'], # 默认排序按sequence字段升序排列
},
),
# 创建SideBar模型用于存储侧边栏信息
migrations.CreateModel(
name='SideBar',
fields=[
# 主键字段,自动增长的大整数
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 标题最大长度100
('name', models.CharField(max_length=100, verbose_name='标题')),
# 内容,文本字段
('content', models.TextField(verbose_name='内容')),
# 排序,整数类型,唯一约束
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
# 是否启用布尔类型默认True
('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'],
'verbose_name': '侧边栏', # 单数形式的可读名称
'verbose_name_plural': '侧边栏', # 复数形式的可读名称
'ordering': ['sequence'], # 默认排序按sequence字段升序排列
},
),
# 创建Tag模型用于存储文章标签
migrations.CreateModel(
name='Tag',
fields=[
# 主键字段,自动增长的整数
('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='修改时间')),
# 标签名最大长度30唯一约束
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
# slugSlugField类型最大长度60可为空默认为'no-slug'
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
# 模型选项设置
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
'verbose_name': '标签', # 单数形式的可读名称
'verbose_name_plural': '标签', # 复数形式的可读名称
'ordering': ['name'], # 默认排序按name字段升序排列
},
),
# 创建Category模型用于存储文章分类
migrations.CreateModel(
name='Category',
fields=[
# 主键字段,自动增长的整数
('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='修改时间')),
# 分类名最大长度30唯一约束
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
# slugSlugField类型最大长度60可为空默认为'no-slug'
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
# 权重排序整数类型默认0数值越大越靠前
('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'],
'verbose_name': '分类', # 单数形式的可读名称
'verbose_name_plural': '分类', # 复数形式的可读名称
'ordering': ['-index'], # 默认排序按index字段降序排列
},
),
# 创建Article模型用于存储文章信息
migrations.CreateModel(
name='Article',
fields=[
# 主键字段,自动增长的整数
('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='修改时间')),
# 标题最大长度200唯一约束
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
# 正文使用MDTextField类型Markdown编辑器字段
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
# 发布时间,日期时间字段,默认为当前时间
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
# 文章状态字符字段最大长度1可选值为草稿和发表默认为发表
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
# 评论状态字符字段最大长度1可选值为打开和关闭默认为打开
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
# 类型字符字段最大长度1可选值为文章和页面默认为文章
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
# 浏览量正整数类型默认0
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
# 文章排序整数类型默认0数值越大越靠前
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
# 是否显示toc目录布尔类型默认False
('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模型级联删除
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
# 标签集合多对多关系关联到Tag模型可为空
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
# 模型选项设置
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
'verbose_name': '文章', # 单数形式的可读名称
'verbose_name_plural': '文章', # 复数形式的可读名称
'ordering': ['-article_order', '-pub_time'], # 默认排序先按article_order降序再按pub_time降序
'get_latest_by': 'id', # latest()方法使用的默认字段
},
),
]

@ -1,23 +1,28 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
# 由Django 4.1.7在2023年3月29日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations, models
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0001_initial
dependencies = [
('blog', '0001_initial'),
]
# 定义具体的操作
operations = [
# 向BlogSettings模型添加新字段global_footer公共尾部
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
model_name='blogsettings', # 目标模型名称
name='global_footer', # 新增字段名
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), # 字段定义文本字段可为空默认为空字符串可为NULL显示名为"公共尾部"
),
# 向BlogSettings模型添加新字段global_header公共头部
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
model_name='blogsettings', # 目标模型名称
name='global_header', # 新增字段名
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), # 字段定义文本字段可为空默认为空字符串可为NULL显示名为"公共头部"
),
]

@ -1,17 +1,21 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
# 由Django 4.2.1在2023年5月9日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations, models
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0002_blogsettings_global_footer_and_more
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
# 定义具体的操作
operations = [
# 向BlogSettings模型添加新字段comment_need_review评论是否需要审核
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
model_name='blogsettings', # 目标模型名称
name='comment_need_review', # 新增字段名
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), # 字段定义布尔字段默认值为False显示名为"评论是否需要审核"
),
]

@ -1,27 +1,35 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
# 由Django 4.2.1在2023年5月9日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0003_blogsettings_comment_need_review
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
# 定义具体的操作
operations = [
# 重命名BlogSettings模型中的字段analyticscode -> analytics_code
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
model_name='blogsettings', # 目标模型名称
old_name='analyticscode', # 原字段名
new_name='analytics_code', # 新字段名
),
# 重命名BlogSettings模型中的字段beiancode -> beian_code
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
model_name='blogsettings', # 目标模型名称
old_name='beiancode', # 原字段名
new_name='beian_code', # 新字段名
),
# 重命名BlogSettings模型中的字段sitename -> site_name
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
model_name='blogsettings', # 目标模型名称
old_name='sitename', # 原字段名
new_name='site_name', # 新字段名
),
]

@ -1,172 +1,216 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
# 由Django 4.2.5在2023年9月6日生成的数据库迁移文件
# 导入Django配置模块
from django.conf import settings
# 导入Django数据库迁移相关模块
from django.db import migrations, models
# 导入Django模型关系相关模块
import django.db.models.deletion
# 导入Django时区工具
import django.utils.timezone
# 导入Markdown编辑器字段
import mdeditor.fields
class Migration(migrations.Migration):
# 定义依赖关系,依赖于用户模型和上一个迁移文件
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
# 定义具体的操作
operations = [
# 修改Article模型的选项设置
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
# 修改Category模型的选项设置
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
# 修改Links模型的选项设置
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
# 修改SideBar模型的选项设置
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
# 修改Tag模型的选项设置
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
# 移除Article模型中的created_time字段
migrations.RemoveField(
model_name='article',
name='created_time',
),
# 移除Article模型中的last_mod_time字段
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
# 移除Category模型中的created_time字段
migrations.RemoveField(
model_name='category',
name='created_time',
),
# 移除Category模型中的last_mod_time字段
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
# 移除Links模型中的created_time字段
migrations.RemoveField(
model_name='links',
name='created_time',
),
# 移除SideBar模型中的created_time字段
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
# 移除Tag模型中的created_time字段
migrations.RemoveField(
model_name='tag',
name='created_time',
),
# 移除Tag模型中的last_mod_time字段
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
# 为Article模型添加creation_time字段创建时间
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Article模型添加last_modify_time字段最后修改时间
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 为Category模型添加creation_time字段创建时间
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Category模型添加last_modify_time字段最后修改时间
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 为Links模型添加creation_time字段创建时间
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为SideBar模型添加creation_time字段创建时间
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Tag模型添加creation_time字段创建时间
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
# 为Tag模型添加last_modify_time字段最后修改时间
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
# 修改Article模型的article_order字段显示名称
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
# 修改Article模型的author字段显示名称
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
# 修改Article模型的body字段显示名称
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
# 修改Article模型的category字段显示名称
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
# 修改Article模型的comment_status字段选项和显示名称
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
# 修改Article模型的pub_time字段显示名称
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
# 修改Article模型的show_toc字段显示名称
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
# 修改Article模型的status字段选项和显示名称
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
# 修改Article模型的tags字段显示名称
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
# 修改Article模型的title字段显示名称
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
# 修改Article模型的type字段选项和显示名称
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
# 修改Article模型的views字段显示名称
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
# 修改BlogSettings模型的多个字段显示名称英文化
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
@ -222,6 +266,8 @@ class Migration(migrations.Migration):
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
# 修改Category模型的多个字段显示名称
migrations.AlterField(
model_name='category',
name='index',
@ -237,6 +283,8 @@ class Migration(migrations.Migration):
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
# 修改Links模型的多个字段显示名称
migrations.AlterField(
model_name='links',
name='is_enable',
@ -267,6 +315,8 @@ class Migration(migrations.Migration):
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
# 修改SideBar模型的多个字段显示名称
migrations.AlterField(
model_name='sidebar',
name='content',
@ -292,6 +342,8 @@ class Migration(migrations.Migration):
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
# 修改Tag模型的name字段显示名称
migrations.AlterField(
model_name='tag',
name='name',

@ -1,17 +1,20 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
# 由Django 4.2.7在2024年1月26日生成的数据库迁移文件
# 导入Django数据库迁移相关模块
from django.db import migrations
class Migration(migrations.Migration):
# 定义依赖关系依赖于上一个迁移文件0005_alter_article_options_alter_category_options_and_more
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
# 定义具体的操作
operations = [
# 修改BlogSettings模型的选项设置
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
name='blogsettings', # 目标模型名称
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, # 新的模型选项:将单数和复数形式的显示名称都改为"Website configuration"
),
]

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

@ -1,13 +1,21 @@
#zf:从haystack导入索引相关模块
from haystack import indexes
#zf:从blog.models导入Article模型
from blog.models import Article
#zf:定义文章索引类继承自SearchIndex和Indexable
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
#zf:定义文本字段,作为文档字段,使用模板来确定索引内容
text = indexes.CharField(document=True, use_template=True)
#zf:获取模型类的方法
def get_model(self):
#zf:返回Article模型
return Article
#zf:定义索引查询集,确定哪些数据会被索引
def index_queryset(self, using=None):
#zf:返回所有状态为已发布('p')的文章
return self.get_model().objects.filter(status='p')

@ -1,107 +1,156 @@
#zf导入所需的Python标准库
#zf用于生成MD5哈希值
import hashlib
#zf用于日志记录
import logging
#zf用于随机选择
import random
#zf用于URL编码
import urllib
#zf导入Django相关模块
#zfDjango模板系统
from django import template
#zfDjango配置
from django.conf import settings
#zfDjango查询对象
from django.db.models import Q
#zf获取对象或返回404
from django.shortcuts import get_object_or_404
#zf字符串过滤器装饰器
from django.template.defaultfilters import stringfilter
#zf静态文件处理
from django.templatetags.static import static
#zfURL反向解析
from django.urls import reverse
#zf标记安全字符串
from django.utils.safestring import mark_safe
#zf导入项目相关模块
#zf博客模型
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
#zf评论模型
from comments.models import Comment
#zfMarkdown处理和HTML清理
from djangoblog.utils import CommonMarkdown, sanitize_html
#zf缓存工具
from djangoblog.utils import cache
#zf获取当前站点
from djangoblog.utils import get_current_site
#zfOAuth用户模型
from oauth.models import OAuthUser
#zf插件管理
from djangoblog.plugin_manage import hooks
#zf创建日志记录器
logger = logging.getLogger(__name__)
#zf注册模板标签库
register = template.Library()
#zf定义head_meta简单标签用于在模板中插入头部元信息
@register.simple_tag(takes_context=True)
def head_meta(context):
#zf应用插件过滤器返回安全的HTML字符串
return mark_safe(hooks.apply_filters('head_meta', '', context))
#zf定义timeformat简单标签用于格式化时间
@register.simple_tag
def timeformat(data):
try:
#zf使用settings中定义的时间格式格式化数据
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
#zf记录错误日志并返回空字符串
logger.error(e)
return ""
#zf定义datetimeformat简单标签用于格式化日期时间
@register.simple_tag
def datetimeformat(data):
try:
#zf使用settings中定义的日期时间格式格式化数据
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
#zf记录错误日志并返回空字符串
logger.error(e)
return ""
#zf定义custom_markdown过滤器用于将Markdown内容转换为HTML
@register.filter()
@stringfilter
def custom_markdown(content):
#zf使用CommonMarkdown工具转换Markdown并标记为安全HTML
return mark_safe(CommonMarkdown.get_markdown(content))
#zf定义get_markdown_toc简单标签用于获取Markdown内容的目录
@register.simple_tag
def get_markdown_toc(content):
from djangoblog.utils import CommonMarkdown
#zf获取Markdown内容和目录
body, toc = CommonMarkdown.get_markdown_with_toc(content)
#zf标记目录为安全HTML并返回
return mark_safe(toc)
#zf定义comment_markdown过滤器用于处理评论中的Markdown内容
@register.filter()
@stringfilter
def comment_markdown(content):
#zf转换Markdown内容
content = CommonMarkdown.get_markdown(content)
#zf清理HTML并标记为安全
return mark_safe(sanitize_html(content))
#zf定义truncatechars_content过滤器用于截取文章内容摘要
@register.filter(is_safe=True)
@stringfilter
def truncatechars_content(content):
"""
获得文章内容的摘要
:param content:
:return:
:param content: 文章内容
:return: 截取后的摘要
"""
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
#zf获取博客设置
blogsetting = get_blog_setting()
#zf根据设置的文章摘要长度截取内容
return truncatechars_html(content, blogsetting.article_sub_length)
#zf定义truncate过滤器用于截取内容前150个字符
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
from django.utils.html import strip_tags
#zf去除HTML标签并截取前150个字符
return strip_tags(content)[:150]
#zf定义load_breadcrumb包含标签用于加载文章面包屑导航
@register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article):
"""
获得文章面包屑
:param article:
:return:
:param article: 文章对象
:return: 面包屑数据
"""
#zf获取文章的分类树
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
#zf获取博客设置和当前站点
blogsetting = get_blog_setting()
site = get_current_site().domain
#zf添加站点名称到面包屑
names.append((blogsetting.site_name, '/'))
#zf反转面包屑顺序
names = names[::-1]
return {
@ -111,18 +160,22 @@ def load_breadcrumb(article):
}
#zf定义load_articletags包含标签用于加载文章标签列表
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
文章标签
:param article:
:return:
:param article: 文章对象
:return: 标签列表数据
"""
#zf获取文章的所有标签
tags = article.tags.all()
tags_list = []
for tag in tags:
#zf获取标签URL和文章数量
url = tag.get_absolute_url()
count = tag.get_article_count()
#zf添加标签信息到列表随机选择Bootstrap颜色
tags_list.append((
url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
))
@ -131,214 +184,17 @@ def load_articletags(article):
}
#zf定义load_sidebar包含标签用于加载侧边栏内容
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
"""
加载侧边栏
:return:
:return: 侧边栏数据
"""
#zf尝试从缓存获取侧边栏数据
value = cache.get("sidebar" + linktype)
if value:
#zf如果缓存存在添加用户信息并返回
value['user'] = user
return value
else:
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
sidebar_categorys = Category.objects.all()
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 5
tags = Tag.objects.all()
sidebar_tags = None
if tags and len(tags) > 0:
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
count = sum([t[1] for t in s])
dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
import random
sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
random.shuffle(sidebar_tags)
value = {
'recent_articles': recent_articles,
'sidebar_categorys': sidebar_categorys,
'most_read_articles': most_read_articles,
'article_dates': dates,
'sidebar_comments': commment_list,
'sidabar_links': links,
'show_google_adsense': blogsetting.show_google_adsense,
'google_adsense_codes': blogsetting.google_adsense_codes,
'open_site_comment': blogsetting.open_site_comment,
'show_gongan_code': blogsetting.show_gongan_code,
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
return value
@register.inclusion_tag('blog/tags/article_meta_info.html')
def load_article_metas(article, user):
"""
获得文章meta信息
:param article:
:return:
"""
return {
'article': article,
'user': user
}
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
previous_url = ''
next_url = ''
if page_type == '':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse('blog:index_page', kwargs={'page': next_number})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': next_number,
'tag_name': tag.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': previous_number,
'tag_name': tag.slug})
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:author_detail_page',
kwargs={
'page': next_number,
'author_name': tag_name})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:author_detail_page',
kwargs={
'page': previous_number,
'author_name': tag_name})
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:category_detail_page',
kwargs={
'page': next_number,
'category_name': category.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:category_detail_page',
kwargs={
'page': previous_number,
'category_name': category.slug})
return {
'previous_url': previous_url,
'next_url': next_url,
'page_obj': page_obj
}
@register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user):
"""
加载文章详情
:param article:
:param isindex:是否列表页若是列表页只显示摘要
:return:
"""
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return {
'article': article,
'isindex': isindex,
'user': user,
'open_site_comment': blogsetting.open_site_comment,
}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
"""获得gravatar头像"""
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
else:
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels))
if o:
return o[0].picture
email = email.encode('utf-8')
default = static('blog/img/avatar.png')
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
cache.set(cachekey, url, 60 * 60 * 10)
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@register.filter
def gravatar(email, size=40):
"""获得gravatar头像"""
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d">' %
(url, size, size))
@register.simple_tag
def query(qs, **kwargs):
""" template tag which allows queryset filtering. Usage:
{% query books author=author as mybooks %}
{% for book in mybooks %}
...
{% endfor %}
"""
return qs.filter(**kwargs)
@register.filter
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)
else

@ -1,232 +1,406 @@
#zf:导入操作系统相关模块
import os
#zf:从django.conf导入settings配置
from django.conf import settings
#zf:从django.core.files.uploadedfile导入SimpleUploadedFile用于文件上传测试
from django.core.files.uploadedfile import SimpleUploadedFile
#zf:从django.core.management导入call_command用于调用管理命令
from django.core.management import call_command
#zf:从django.core.paginator导入Paginator用于分页测试
from django.core.paginator import Paginator
#zf:从django.templatetags.static导入static用于静态文件处理
from django.templatetags.static import static
#zf:从django.test导入Client, RequestFactory, TestCase用于测试
from django.test import Client, RequestFactory, TestCase
#zf:从django.urls导入reverse用于URL反向解析
from django.urls import reverse
#zf:从django.utils导入timezone用于时间处理
from django.utils import timezone
#zf:从accounts.models导入BlogUser用户模型
from accounts.models import BlogUser
#zf:从blog.forms导入BlogSearchForm搜索表单
from blog.forms import BlogSearchForm
#zf:从blog.models导入Article, Category, Tag, SideBar, Links模型
from blog.models import Article, Category, Tag, SideBar, Links
#zf:从blog.templatetags.blog_tags导入load_pagination_info, load_articletags模板标签
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
#zf:从djangoblog.utils导入get_current_site, get_sha256工具函数
from djangoblog.utils import get_current_site, get_sha256
#zf:从oauth.models导入OAuthUser, OAuthConfig OAuth相关模型
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
#zf:创建你的测试用例
#zf:定义文章测试类继承自TestCase
class ArticleTest(TestCase):
#zf:测试初始化方法
def setUp(self):
#zf:创建测试客户端
self.client = Client()
#zf:创建请求工厂
self.factory = RequestFactory()
#zf:测试验证文章功能
def test_validate_article(self):
#zf:获取当前站点域名
site = get_current_site().domain
#zf:创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
#zf:设置用户密码
user.set_password("liangliangyy")
#zf:设置用户为职员
user.is_staff = True
#zf:设置用户为超级用户
user.is_superuser = True
#zf:保存用户
user.save()
#zf:测试获取用户绝对URL
response = self.client.get(user.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问管理后台日志页面
response = self.client.get('/admin/servermanager/emailsendlog/')
#zf:测试访问管理后台日志条目页面
response = self.client.get('admin/admin/logentry/')
#zf:创建侧边栏对象
s = SideBar()
#zf:设置侧边栏排序
s.sequence = 1
#zf:设置侧边栏名称
s.name = 'test'
#zf:设置侧边栏内容
s.content = 'test content'
#zf:设置侧边栏启用状态
s.is_enable = True
#zf:保存侧边栏
s.save()
#zf:创建分类对象
category = Category()
#zf:设置分类名称
category.name = "category"
#zf:设置分类创建时间
category.creation_time = timezone.now()
#zf:设置分类最后修改时间
category.last_mod_time = timezone.now()
#zf:保存分类
category.save()
#zf:创建标签对象
tag = Tag()
#zf:设置标签名称
tag.name = "nicetag"
#zf:保存标签
tag.save()
#zf:创建文章对象
article = Article()
#zf:设置文章标题
article.title = "nicetitle"
#zf:设置文章正文
article.body = "nicecontent"
#zf:设置文章作者
article.author = user
#zf:设置文章分类
article.category = category
#zf:设置文章类型
article.type = 'a'
#zf:设置文章状态
article.status = 'p'
#zf:保存文章
article.save()
#zf:断言文章标签数量为0
self.assertEqual(0, article.tags.count())
#zf:给文章添加标签
article.tags.add(tag)
#zf:保存文章
article.save()
#zf:断言文章标签数量为1
self.assertEqual(1, article.tags.count())
#zf:循环创建20篇文章用于测试
for i in range(20):
article = Article()
#zf:设置文章标题
article.title = "nicetitle" + str(i)
#zf:设置文章正文
article.body = "nicetitle" + str(i)
#zf:设置文章作者
article.author = user
#zf:设置文章分类
article.category = category
#zf:设置文章类型
article.type = 'a'
#zf:设置文章状态
article.status = 'p'
#zf:保存文章
article.save()
#zf:给文章添加标签
article.tags.add(tag)
#zf:保存文章
article.save()
#zf:从blog.documents导入ELASTICSEARCH_ENABLED常量
from blog.documents import ELASTICSEARCH_ENABLED
#zf:如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
#zf:调用构建索引命令
call_command("build_index")
#zf:测试搜索功能
response = self.client.get('/search', {'q': 'nicetitle'})
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试获取文章绝对URL
response = self.client.get(article.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:从djangoblog.spider_notify导入SpiderNotify
from djangoblog.spider_notify import SpiderNotify
#zf:通知搜索引擎爬虫
SpiderNotify.notify(article.get_absolute_url())
#zf:测试获取标签绝对URL
response = self.client.get(tag.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试获取分类绝对URL
response = self.client.get(category.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试搜索功能
response = self.client.get('/search', {'q': 'django'})
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:加载文章标签
s = load_articletags(article)
#zf:断言结果不为None
self.assertIsNotNone(s)
#zf:用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
#zf:测试获取归档页面
response = self.client.get(reverse('blog:archives'))
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:创建所有文章的分页器
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
#zf:检查分页功能
self.check_pagination(p, '', '')
#zf:创建按标签筛选文章的分页器
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
#zf:检查分页功能,类型为分类标签归档
self.check_pagination(p, '分类标签归档', tag.slug)
#zf:创建按作者筛选文章的分页器
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
#zf:检查分页功能,类型为作者文章归档
self.check_pagination(p, '作者文章归档', 'liangliangyy')
#zf:创建按分类筛选文章的分页器
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
#zf:检查分页功能,类型为分类目录归档
self.check_pagination(p, '分类目录归档', category.slug)
#zf:创建博客搜索表单实例
f = BlogSearchForm()
#zf:执行搜索
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
#zf:从djangoblog.spider_notify导入SpiderNotify
from djangoblog.spider_notify import SpiderNotify
#zf:通知百度搜索引擎
SpiderNotify.baidu_notify([article.get_full_url()])
#zf:从blog.templatetags.blog_tags导入gravatar_url, gravatar
from blog.templatetags.blog_tags import gravatar_url, gravatar
#zf:获取gravatar头像URL
u = gravatar_url('liangliangyy@gmail.com')
#zf:获取gravatar头像
u = gravatar('liangliangyy@gmail.com')
#zf:创建链接对象
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
#zf:保存链接
link.save()
#zf:测试获取链接页面
response = self.client.get('/links.html')
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试获取RSS订阅页面
response = self.client.get('/feed/')
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试获取站点地图
response = self.client.get('/sitemap.xml')
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试删除文章
self.client.get("/admin/blog/article/1/delete/")
#zf:测试访问管理后台日志页面
self.client.get('/admin/servermanager/emailsendlog/')
#zf:测试访问管理后台日志条目页面
self.client.get('/admin/admin/logentry/')
#zf:测试访问管理后台日志条目修改页面
self.client.get('/admin/admin/logentry/1/change/')
#zf:检查分页功能的方法
def check_pagination(self, p, type, value):
#zf:遍历所有页面
for page in range(1, p.num_pages + 1):
#zf:加载分页信息
s = load_pagination_info(p.page(page), type, value)
#zf:断言结果不为None
self.assertIsNotNone(s)
#zf:如果有上一页URL
if s['previous_url']:
#zf:测试访问上一页
response = self.client.get(s['previous_url'])
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:如果有下一页URL
if s['next_url']:
#zf:测试访问下一页
response = self.client.get(s['next_url'])
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试图片上传功能
def test_image(self):
#zf:导入requests模块
import requests
#zf:获取Python官网Logo图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
#zf:设置图片保存路径
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
#zf:将图片保存到本地
with open(imagepath, 'wb') as file:
file.write(rsp.content)
#zf:测试未登录上传图片
rsp = self.client.post('/upload')
#zf:断言响应状态码为403(禁止访问)
self.assertEqual(rsp.status_code, 403)
#zf:生成上传签名
sign = get_sha256(get_sha256(settings.SECRET_KEY))
#zf:打开图片文件
with open(imagepath, 'rb') as file:
#zf:创建上传文件对象
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
#zf:构造表单数据
form_data = {'python.png': imgfile}
#zf:测试带签名上传图片
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
#zf:断言响应状态码为200
self.assertEqual(rsp.status_code, 200)
#zf:删除临时图片文件
os.remove(imagepath)
#zf:从djangoblog.utils导入save_user_avatar, send_email工具函数
from djangoblog.utils import save_user_avatar, send_email
#zf:测试发送邮件
send_email(['qq@qq.com'], 'testTitle', 'testContent')
#zf:测试保存用户头像
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
#zf:测试错误页面
def test_errorpage(self):
#zf:测试访问不存在的页面
rsp = self.client.get('/eee')
#zf:断言响应状态码为404
self.assertEqual(rsp.status_code, 404)
#zf:测试管理命令
def test_commands(self):
#zf:创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
#zf:设置用户密码
user.set_password("liangliangyy")
#zf:设置用户为职员
user.is_staff = True
#zf:设置用户为超级用户
user.is_superuser = True
#zf:保存用户
user.save()
#zf:创建OAuth配置对象
c = OAuthConfig()
#zf:设置OAuth类型
c.type = 'qq'
#zf:设置应用密钥
c.appkey = 'appkey'
#zf:设置应用密钥
c.appsecret = 'appsecret'
#zf:保存配置
c.save()
#zf:创建OAuth用户对象
u = OAuthUser()
#zf:设置OAuth类型
u.type = 'qq'
#zf:设置openid
u.openid = 'openid'
#zf:关联用户
u.user = user
#zf:设置头像
u.picture = static("/blog/img/avatar.png")
#zf:设置元数据
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
#zf:保存OAuth用户
u.save()
#zf:创建另一个OAuth用户对象
u = OAuthUser()
#zf:设置OAuth类型
u.type = 'qq'
#zf:设置openid
u.openid = 'openid1'
#zf:设置头像URL
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
#zf:设置元数据
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
#zf:保存OAuth用户
u.save()
#zf:从blog.documents导入ELASTICSEARCH_ENABLED常量
from blog.documents import ELASTICSEARCH_ENABLED
#zf:如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
#zf:调用构建索引命令
call_command("build_index")
#zf:调用通知百度搜索引擎命令
call_command("ping_baidu", "all")
#zf:调用创建测试数据命令
call_command("create_testdata")
#zf:调用清除缓存命令
call_command("clear_cache")
#zf:调用同步用户头像命令
call_command("sync_user_avatar")
#zf:调用构建搜索词命令
call_command("build_search_words")

@ -1,62 +1,406 @@
from django.urls import path
from django.views.decorators.cache import cache_page
from . import views
app_name = "blog"
urlpatterns = [
path(
r'',
views.IndexView.as_view(),
name='index'),
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
path(
r'upload',
views.fileupload,
name='upload'),
path(
r'clean',
views.clean_cache_view,
name='clean'),
]
#zf:导入os模块用于文件路径操作
import os
#zf:从django.conf导入settings配置
from django.conf import settings
#zf:从django.core.files.uploadedfile导入SimpleUploadedFile用于模拟文件上传
from django.core.files.uploadedfile import SimpleUploadedFile
#zf:从django.core.management导入call_command用于调用Django管理命令
from django.core.management import call_command
#zf:从django.core.paginator导入Paginator用于分页功能测试
from django.core.paginator import Paginator
#zf:从django.templatetags.static导入static函数用于处理静态文件
from django.templatetags.static import static
#zf:从django.test导入Client, RequestFactory, TestCase用于测试
from django.test import Client, RequestFactory, TestCase
#zf:从django.urls导入reverse用于URL反向解析
from django.urls import reverse
#zf:从django.utils导入timezone用于处理时区
from django.utils import timezone
#zf:从accounts.models导入BlogUser博客用户模型
from accounts.models import BlogUser
#zf:从blog.forms导入BlogSearchForm博客搜索表单
from blog.forms import BlogSearchForm
#zf:从blog.models导入Article, Category, Tag, SideBar, Links博客相关模型
from blog.models import Article, Category, Tag, SideBar, Links
#zf:从blog.templatetags.blog_tags导入load_pagination_info, load_articletags模板标签
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
#zf:从djangoblog.utils导入get_current_site, get_sha256工具函数
from djangoblog.utils import get_current_site, get_sha256
#zf:从oauth.models导入OAuthUser, OAuthConfig OAuth认证相关模型
from oauth.models import OAuthUser, OAuthConfig
#zf:创建你的测试用例
#zf:定义ArticleTest测试类继承自Django的TestCase
class ArticleTest(TestCase):
#zf:测试初始化方法,在每个测试方法执行前运行
def setUp(self):
#zf:创建测试客户端用于模拟HTTP请求
self.client = Client()
#zf:创建请求工厂,用于创建请求对象
self.factory = RequestFactory()
#zf:测试文章验证功能
def test_validate_article(self):
#zf:获取当前站点域名
site = get_current_site().domain
#zf:创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
#zf:设置用户密码
user.set_password("liangliangyy")
#zf:设置用户为管理员
user.is_staff = True
#zf:设置用户为超级用户
user.is_superuser = True
#zf:保存用户
user.save()
#zf:测试访问用户个人页面
response = self.client.get(user.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问管理后台邮件发送日志
response = self.client.get('/admin/servermanager/emailsendlog/')
#zf:测试访问管理后台日志条目
response = self.client.get('admin/admin/logentry/')
#zf:创建侧边栏对象
s = SideBar()
#zf:设置侧边栏排序
s.sequence = 1
#zf:设置侧边栏名称
s.name = 'test'
#zf:设置侧边栏内容
s.content = 'test content'
#zf:设置侧边栏启用状态
s.is_enable = True
#zf:保存侧边栏
s.save()
#zf:创建分类对象
category = Category()
#zf:设置分类名称
category.name = "category"
#zf:设置分类创建时间
category.creation_time = timezone.now()
#zf:设置分类最后修改时间
category.last_mod_time = timezone.now()
#zf:保存分类
category.save()
#zf:创建标签对象
tag = Tag()
#zf:设置标签名称
tag.name = "nicetag"
#zf:保存标签
tag.save()
#zf:创建文章对象
article = Article()
#zf:设置文章标题
article.title = "nicetitle"
#zf:设置文章正文
article.body = "nicecontent"
#zf:设置文章作者
article.author = user
#zf:设置文章分类
article.category = category
#zf:设置文章类型为文章
article.type = 'a'
#zf:设置文章状态为已发布
article.status = 'p'
#zf:保存文章
article.save()
#zf:断言文章标签数量为0
self.assertEqual(0, article.tags.count())
#zf:给文章添加标签
article.tags.add(tag)
#zf:保存文章
article.save()
#zf:断言文章标签数量为1
self.assertEqual(1, article.tags.count())
#zf:循环创建20篇文章用于分页测试
for i in range(20):
article = Article()
#zf:设置文章标题
article.title = "nicetitle" + str(i)
#zf:设置文章正文
article.body = "nicetitle" + str(i)
#zf:设置文章作者
article.author = user
#zf:设置文章分类
article.category = category
#zf:设置文章类型为文章
article.type = 'a'
#zf:设置文章状态为已发布
article.status = 'p'
#zf:保存文章
article.save()
#zf:给文章添加标签
article.tags.add(tag)
#zf:保存文章
article.save()
#zf:从blog.documents导入ELASTICSEARCH_ENABLED常量
from blog.documents import ELASTICSEARCH_ENABLED
#zf:如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
#zf:调用build_index管理命令构建搜索索引
call_command("build_index")
#zf:测试搜索功能
response = self.client.get('/search', {'q': 'nicetitle'})
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问文章详情页
response = self.client.get(article.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:从djangoblog.spider_notify导入SpiderNotify用于通知搜索引擎
from djangoblog.spider_notify import SpiderNotify
#zf:通知搜索引擎爬虫
SpiderNotify.notify(article.get_absolute_url())
#zf:测试访问标签详情页
response = self.client.get(tag.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问分类详情页
response = self.client.get(category.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试搜索功能
response = self.client.get('/search', {'q': 'django'})
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:加载文章标签
s = load_articletags(article)
#zf:断言结果不为None
self.assertIsNotNone(s)
#zf:用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
#zf:测试访问文章归档页
response = self.client.get(reverse('blog:archives'))
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:创建所有文章的分页器
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
#zf:检查分页功能
self.check_pagination(p, '', '')
#zf:创建按标签筛选的分页器
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
#zf:检查标签归档分页功能
self.check_pagination(p, '分类标签归档', tag.slug)
#zf:创建按作者筛选的分页器
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
#zf:检查作者归档分页功能
self.check_pagination(p, '作者文章归档', 'liangliangyy')
#zf:创建按分类筛选的分页器
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
#zf:检查分类归档分页功能
self.check_pagination(p, '分类目录归档', category.slug)
#zf:创建博客搜索表单实例
f = BlogSearchForm()
#zf:执行搜索
f.search()
#zf:从djangoblog.spider_notify导入SpiderNotify
from djangoblog.spider_notify import SpiderNotify
#zf:通知百度搜索引擎
SpiderNotify.baidu_notify([article.get_full_url()])
#zf:从blog.templatetags.blog_tags导入gravatar_url, gravatar函数
from blog.templatetags.blog_tags import gravatar_url, gravatar
#zf:获取gravatar头像URL
u = gravatar_url('liangliangyy@gmail.com')
#zf:获取gravatar头像HTML
u = gravatar('liangliangyy@gmail.com')
#zf:创建友情链接对象
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
#zf:保存友情链接
link.save()
#zf:测试访问友情链接页面
response = self.client.get('/links.html')
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问RSS订阅页面
response = self.client.get('/feed/')
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问站点地图
response = self.client.get('/sitemap.xml')
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问管理后台文章删除页面
self.client.get("/admin/blog/article/1/delete/")
#zf:测试访问管理后台邮件发送日志
self.client.get('/admin/servermanager/emailsendlog/')
#zf:测试访问管理后台日志条目
self.client.get('/admin/admin/logentry/')
#zf:测试访问管理后台日志条目修改页面
self.client.get('/admin/admin/logentry/1/change/')
#zf:检查分页功能的方法
def check_pagination(self, p, type, value):
#zf:遍历所有分页
for page in range(1, p.num_pages + 1):
#zf:加载分页信息
s = load_pagination_info(p.page(page), type, value)
#zf:断言分页信息不为None
self.assertIsNotNone(s)
#zf:如果有上一页URL
if s['previous_url']:
#zf:测试访问上一页
response = self.client.get(s['previous_url'])
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:如果有下一页URL
if s['next_url']:
#zf:测试访问下一页
response = self.client.get(s['next_url'])
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试图片上传功能
def test_image(self):
#zf:导入requests模块用于下载图片
import requests
#zf:下载Python官网Logo图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
#zf:设置图片保存路径
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
#zf:将图片保存到本地
with open(imagepath, 'wb') as file:
file.write(rsp.content)
#zf:测试未登录上传图片(应该被拒绝)
rsp = self.client.post('/upload')
#zf:断言响应状态码为403禁止访问
self.assertEqual(rsp.status_code, 403)
#zf:生成上传签名
sign = get_sha256(get_sha256(settings.SECRET_KEY))
#zf:打开图片文件准备上传
with open(imagepath, 'rb') as file:
#zf:创建上传文件对象
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
#zf:构造表单数据
form_data = {'python.png': imgfile}
#zf:测试带签名上传图片
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
#zf:断言响应状态码为200
self.assertEqual(rsp.status_code, 200)
#zf:删除临时图片文件
os.remove(imagepath)
#zf:从djangoblog.utils导入save_user_avatar, send_email工具函数
from djangoblog.utils import save_user_avatar, send_email
#zf:测试发送邮件功能
send_email(['qq@qq.com'], 'testTitle', 'testContent')
#zf:测试保存用户头像功能
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
#zf:测试错误页面
def test_errorpage(self):
#zf:测试访问不存在的页面
rsp = self.client.get('/eee')
#zf:断言响应状态码为404
self.assertEqual(rsp.status_code, 404)
#zf:测试管理命令
def test_commands(self):
#zf:创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
#zf:设置用户密码
user.set_password("liangliangyy")
#zf:设置用户为管理员
user.is_staff = True
#zf:设置用户为超级用户
user.is_superuser = True
#zf:保存用户
user.save()
#zf:创建OAuth配置对象
c = OAuthConfig()
#zf:设置OAuth类型为QQ
c.type = 'qq'
#zf:设置应用密钥
c.appkey = 'appkey'
#zf:设置应用密钥
c.appsecret = 'appsecret'
#zf:保存配置
c.save()
#zf:创建OAuth用户对象
u = OAuthUser()
#zf:设置OAuth类型为QQ
u.type = 'qq'
#zf:设置openid
u.openid = 'openid'
#zf:关联博客用户
u.user = user
#zf:设置头像为静态图片
u.picture = static("/blog/img/avatar.png")
#zf:设置用户元数据
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
#zf:保存OAuth用户
u.save()
#zf:创建另一个OAuth用户对象
u = OAuthUser()
#zf:设置OAuth类型为QQ
u.type = 'qq'
#zf:设置openid
u.openid = 'openid1'
#zf:设置头像URL
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
#zf:设置用户元数据
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
#zf:保存OAuth用户
u.save()
#zf:从blog.documents导入ELASTICSEARCH_ENABLED常量
from blog.documents import ELASTICSEARCH_ENABLED
#zf:如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
#zf:调用build_index命令构建搜索索引
call_command("build_index")
#zf:调用ping_baidu命令通知百度搜索引擎
call_command("ping_baidu", "all")
#zf:调用create_testdata命令创建测试数据
call_command("create_testdata")
#zf:调用clear_cache命令清除缓存
call_command("clear_cache")
#zf:调用sync_user_avatar命令同步用户头像
call_command("sync_user_avatar")
#zf:调用build_search_words命令构建搜索词
call_command("build_search_words")

@ -1,379 +1,406 @@
import logging
#zf:导入os模块用于文件操作
import os
import uuid
#zf:从django.conf导入settings配置
from django.conf import settings
#zf:从django.core.files.uploadedfile导入SimpleUploadedFile用于文件上传测试
from django.core.files.uploadedfile import SimpleUploadedFile
#zf:从django.core.management导入call_command用于调用管理命令
from django.core.management import call_command
#zf:从django.core.paginator导入Paginator用于分页测试
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
#zf:从django.templatetags.static导入static用于处理静态文件
from django.templatetags.static import static
#zf:从django.test导入Client, RequestFactory, TestCase用于测试
from django.test import Client, RequestFactory, TestCase
#zf:从django.urls导入reverse用于URL反向解析
from django.urls import reverse
#zf:从django.utils导入timezone用于时区处理
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
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__)
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
def get_view_cache_key(self):
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
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
def get_queryset(self):
'''
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):
'''
首页
'''
# 友情链接类型
link_type = LinkShowType.I
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
def get_context_data(self, **kwargs):
comment_form = CommentForm()
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
page = 1
else:
page = int(page)
if page < 1:
page = 1
if page > paginator.num_pages:
page = paginator.num_pages
p_comments = paginator.page(page)
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
# # Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
return context
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档"
def get_queryset_data(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
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)
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档'
def get_queryset_cache_key(self):
from uuslug import slugify
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
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
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)
class TagDetailView(ArticleListView):
'''
标签列表页面
'''
page_type = '分类标签归档'
def get_queryset_data(self):
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
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
cache_key = 'tag_{tag_name}_{page}'.format(
tag_name=tag_name, page=self.page_number)
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)
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
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()
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
def get_queryset(self):
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
def get_context(self):
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
return context
@csrf_exempt
def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
url = static(savepath)
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post")
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
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)
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
#zf:从accounts.models导入BlogUser博客用户模型
from accounts.models import BlogUser
#zf:从blog.forms导入BlogSearchForm博客搜索表单
from blog.forms import BlogSearchForm
#zf:从blog.models导入Article, Category, Tag, SideBar, Links博客相关模型
from blog.models import Article, Category, Tag, SideBar, Links
#zf:从blog.templatetags.blog_tags导入load_pagination_info, load_articletags模板标签
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
#zf:从djangoblog.utils导入get_current_site, get_sha256工具函数
from djangoblog.utils import get_current_site, get_sha256
#zf:从oauth.models导入OAuthUser, OAuthConfig OAuth相关模型
from oauth.models import OAuthUser, OAuthConfig
#zf:创建你的测试用例
#zf:定义ArticleTest测试类继承自TestCase
class ArticleTest(TestCase):
#zf:测试初始化方法,在每个测试方法执行前运行
def setUp(self):
#zf:创建测试客户端
self.client = Client()
#zf:创建请求工厂
self.factory = RequestFactory()
#zf:测试验证文章功能
def test_validate_article(self):
#zf:获取当前站点域名
site = get_current_site().domain
#zf:创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
#zf:设置用户密码
user.set_password("liangliangyy")
#zf:设置用户为管理员
user.is_staff = True
#zf:设置用户为超级用户
user.is_superuser = True
#zf:保存用户
user.save()
#zf:测试访问用户绝对URL
response = self.client.get(user.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问管理后台邮件发送日志
response = self.client.get('/admin/servermanager/emailsendlog/')
#zf:测试访问管理后台日志条目
response = self.client.get('admin/admin/logentry/')
#zf:创建侧边栏对象
s = SideBar()
#zf:设置侧边栏排序
s.sequence = 1
#zf:设置侧边栏名称
s.name = 'test'
#zf:设置侧边栏内容
s.content = 'test content'
#zf:设置侧边栏启用状态
s.is_enable = True
#zf:保存侧边栏
s.save()
#zf:创建分类对象
category = Category()
#zf:设置分类名称
category.name = "category"
#zf:设置分类创建时间
category.creation_time = timezone.now()
#zf:设置分类最后修改时间
category.last_mod_time = timezone.now()
#zf:保存分类
category.save()
#zf:创建标签对象
tag = Tag()
#zf:设置标签名称
tag.name = "nicetag"
#zf:保存标签
tag.save()
#zf:创建文章对象
article = Article()
#zf:设置文章标题
article.title = "nicetitle"
#zf:设置文章正文
article.body = "nicecontent"
#zf:设置文章作者
article.author = user
#zf:设置文章分类
article.category = category
#zf:设置文章类型为文章
article.type = 'a'
#zf:设置文章状态为已发布
article.status = 'p'
#zf:保存文章
article.save()
#zf:断言文章标签数量为0
self.assertEqual(0, article.tags.count())
#zf:给文章添加标签
article.tags.add(tag)
#zf:保存文章
article.save()
#zf:断言文章标签数量为1
self.assertEqual(1, article.tags.count())
#zf:循环创建20篇文章用于测试
for i in range(20):
article = Article()
#zf:设置文章标题
article.title = "nicetitle" + str(i)
#zf:设置文章正文
article.body = "nicetitle" + str(i)
#zf:设置文章作者
article.author = user
#zf:设置文章分类
article.category = category
#zf:设置文章类型为文章
article.type = 'a'
#zf:设置文章状态为已发布
article.status = 'p'
#zf:保存文章
article.save()
#zf:给文章添加标签
article.tags.add(tag)
#zf:保存文章
article.save()
#zf:从blog.documents导入ELASTICSEARCH_ENABLED常量
from blog.documents import ELASTICSEARCH_ENABLED
#zf:如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
#zf:调用build_index管理命令
call_command("build_index")
#zf:测试搜索功能
response = self.client.get('/search', {'q': 'nicetitle'})
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问文章绝对URL
response = self.client.get(article.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:从djangoblog.spider_notify导入SpiderNotify用于通知搜索引擎
from djangoblog.spider_notify import SpiderNotify
#zf:通知搜索引擎爬虫
SpiderNotify.notify(article.get_absolute_url())
#zf:测试访问标签绝对URL
response = self.client.get(tag.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问分类绝对URL
response = self.client.get(category.get_absolute_url())
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试搜索功能
response = self.client.get('/search', {'q': 'django'})
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:加载文章标签
s = load_articletags(article)
#zf:断言结果不为None
self.assertIsNotNone(s)
#zf:用户登录
self.client.login(username='liangliangyy', password='liangliangyy')
#zf:测试访问文章归档页面
response = self.client.get(reverse('blog:archives'))
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:创建所有文章的分页器
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
#zf:检查分页功能
self.check_pagination(p, '', '')
#zf:创建按标签过滤的文章分页器
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
#zf:检查标签归档分页功能
self.check_pagination(p, '分类标签归档', tag.slug)
#zf:创建按作者过滤的文章分页器
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
#zf:检查作者归档分页功能
self.check_pagination(p, '作者文章归档', 'liangliangyy')
#zf:创建按分类过滤的文章分页器
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
#zf:检查分类归档分页功能
self.check_pagination(p, '分类目录归档', category.slug)
#zf:创建博客搜索表单实例
f = BlogSearchForm()
#zf:执行搜索
f.search()
#zf:从djangoblog.spider_notify导入SpiderNotify
from djangoblog.spider_notify import SpiderNotify
#zf:通知百度搜索引擎
SpiderNotify.baidu_notify([article.get_full_url()])
#zf:从blog.templatetags.blog_tags导入gravatar_url, gravatar函数
from blog.templatetags.blog_tags import gravatar_url, gravatar
#zf:获取gravatar头像URL
u = gravatar_url('liangliangyy@gmail.com')
#zf:获取gravatar头像HTML
u = gravatar('liangliangyy@gmail.com')
#zf:创建链接对象
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
#zf:保存链接
link.save()
#zf:测试访问链接页面
response = self.client.get('/links.html')
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问RSS订阅
response = self.client.get('/feed/')
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问站点地图
response = self.client.get('/sitemap.xml')
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试访问管理后台文章删除页面
self.client.get("/admin/blog/article/1/delete/")
#zf:测试访问管理后台邮件发送日志
self.client.get('/admin/servermanager/emailsendlog/')
#zf:测试访问管理后台日志条目
self.client.get('/admin/admin/logentry/')
#zf:测试访问管理后台日志条目修改页面
self.client.get('/admin/admin/logentry/1/change/')
#zf:检查分页功能的方法
def check_pagination(self, p, type, value):
#zf:遍历所有页面
for page in range(1, p.num_pages + 1):
#zf:加载分页信息
s = load_pagination_info(p.page(page), type, value)
#zf:断言分页信息不为None
self.assertIsNotNone(s)
#zf:如果有上一页URL
if s['previous_url']:
#zf:测试访问上一页
response = self.client.get(s['previous_url'])
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:如果有下一页URL
if s['next_url']:
#zf:测试访问下一页
response = self.client.get(s['next_url'])
#zf:断言响应状态码为200
self.assertEqual(response.status_code, 200)
#zf:测试图片功能
def test_image(self):
#zf:导入requests模块
import requests
#zf:获取Python官网Logo图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
#zf:设置图片保存路径
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
#zf:将图片内容写入文件
with open(imagepath, 'wb') as file:
file.write(rsp.content)
#zf:测试未授权上传图片
rsp = self.client.post('/upload')
#zf:断言响应状态码为403
self.assertEqual(rsp.status_code, 403)
#zf:生成上传签名
sign = get_sha256(get_sha256(settings.SECRET_KEY))
#zf:打开图片文件准备上传
with open(imagepath, 'rb') as file:
#zf:创建上传文件对象
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
#zf:构造表单数据
form_data = {'python.png': imgfile}
#zf:测试带签名上传图片
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
#zf:断言响应状态码为200
self.assertEqual(rsp.status_code, 200)
#zf:删除临时图片文件
os.remove(imagepath)
#zf:从djangoblog.utils导入save_user_avatar, send_email工具函数
from djangoblog.utils import save_user_avatar, send_email
#zf:测试发送邮件
send_email(['qq@qq.com'], 'testTitle', 'testContent')
#zf:测试保存用户头像
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
#zf:测试错误页面
def test_errorpage(self):
#zf:测试访问不存在的页面
rsp = self.client.get('/eee')
#zf:断言响应状态码为404
self.assertEqual(rsp.status_code, 404)
#zf:测试管理命令
def test_commands(self):
#zf:创建或获取测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
#zf:设置用户密码
user.set_password("liangliangyy")
#zf:设置用户为管理员
user.is_staff = True
#zf:设置用户为超级用户
user.is_superuser = True
#zf:保存用户
user.save()
#zf:创建OAuth配置对象
c = OAuthConfig()
#zf:设置OAuth类型为QQ
c.type = 'qq'
#zf:设置应用密钥
c.appkey = 'appkey'
#zf:设置应用密钥
c.appsecret = 'appsecret'
#zf:保存配置
c.save()
#zf:创建OAuth用户对象
u = OAuthUser()
#zf:设置OAuth类型为QQ
u.type = 'qq'
#zf:设置openid
u.openid = 'openid'
#zf:关联博客用户
u.user = user
#zf:设置头像为静态图片
u.picture = static("/blog/img/avatar.png")
#zf:设置用户元数据
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
#zf:保存OAuth用户
u.save()
#zf:创建另一个OAuth用户对象
u = OAuthUser()
#zf:设置OAuth类型为QQ
u.type = 'qq'
#zf:设置openid
u.openid = 'openid1'
#zf:设置头像URL
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
#zf:设置用户元数据
u.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
#zf:保存OAuth用户
u.save()
#zf:从blog.documents导入ELASTICSEARCH_ENABLED常量
from blog.documents import ELASTICSEARCH_ENABLED
#zf:如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
#zf:调用build_index命令构建索引
call_command("build_index")
#zf:调用ping_baidu命令通知百度
call_command("ping_baidu", "all")
#zf:调用create_testdata命令创建测试数据
call_command("create_testdata")
#zf:调用clear_cache命令清除缓存
call_command("clear_cache")
#zf:调用sync_user_avatar命令同步用户头像
call_command("sync_user_avatar")
#zf:调用build_search_words命令构建搜索词
call_command("build_search_words")

@ -0,0 +1,247 @@
import _thread
import logging
from math import ceil
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
from uuslug import slugify
from blog.models import Article, BlogSettings, Category, Tag
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, delete_sidebar_cache, delete_view_cache, expire_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
logger = logging.getLogger(__name__)
oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
def _get_site_domain():
# szy返回不带端口号的域名统一缓存键格式
site = get_current_site().domain
if ':' in site:
site = site.split(':')[0]
return site
def _expire_object_cache(instance):
# szy根据对象 URL 精准刷新页面缓存,避免全量清空
if not hasattr(instance, 'get_absolute_url'):
return
try:
path = instance.get_absolute_url()
except Exception:
return
expire_view_cache(
path,
servername=_get_site_domain(),
serverport=80,
key_prefix='blogdetail')
def _invalidate_nav_and_seo_cache():
# szy侧边栏和 SEO 上下文依赖全局配置,需要单独失效
delete_sidebar_cache()
if cache.get('seo_processor'):
cache.delete('seo_processor')
def _delete_paginated_cache(key_prefix, total_items):
# szy按分页数量批量删除缓存键避免 cache.clear()
page_size = settings.PAGINATE_BY or 1
total_pages = max(1, ceil(total_items / page_size))
keys = [f'{key_prefix}_{page}' for page in range(1, total_pages + 1)]
cache.delete_many(keys)
def _invalidate_index_cache():
# szy首页/归档依赖文章数据,文章变化时定点清除
total = Article.objects.filter(type='a', status='p').count()
_delete_paginated_cache('index', total)
cache.delete('archives')
def _invalidate_category_cache(category: Category):
# szy分类及其子分类都有联动需要逐个刷新
if not category:
return
_expire_object_cache(category)
category_names = [c.name for c in category.get_sub_categorys()]
total = Article.objects.filter(
category__name__in=category_names,
status='p').count()
_delete_paginated_cache(f'category_list_{category.name}', total)
def _invalidate_tag_cache(tag: Tag):
# szy标签列表缓存独立按标签名称清理
if not tag:
return
_expire_object_cache(tag)
total = Article.objects.filter(
tags__name=tag.name,
type='a',
status='p').distinct().count()
_delete_paginated_cache(f'tag_{tag.name}', total)
def _invalidate_author_cache(username: str):
# szy作者归档页按用户名 slug 生成缓存键
if not username:
return
author_slug = slugify(username)
total = Article.objects.filter(
author__username=username,
type='a',
status='p').count()
_delete_paginated_cache(f'author_{author_slug}', total)
def _notify_spider(instance):
# szy文章/分类更新后推送搜索引擎,保持抓取实时
if settings.TESTING or not hasattr(instance, 'get_full_url'):
return
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
def _invalidate_article_related_cache(article: Article):
# szy文章更新会影响多个页面这里集中处理相关缓存
_expire_object_cache(article)
_invalidate_index_cache()
_invalidate_author_cache(article.author.username if article.author else None)
if article.category_id:
_invalidate_category_cache(article.category)
for tag in article.tags.all():
_invalidate_tag_cache(tag)
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
msg.content_subtype = "html"
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto)
try:
result = msg.send()
log.send_result = result > 0
except Exception as e:
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.save()
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
if isinstance(instance, LogEntry):
return
is_update_views = update_fields == {'views'}
if isinstance(instance, BlogSettings):
# szy站点配置变化时同步刷新缓存和侧边栏
cache.delete('get_blog_setting')
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Article):
if is_update_views:
return
_notify_spider(instance)
_invalidate_article_related_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Category):
_notify_spider(instance)
_invalidate_category_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Tag):
_notify_spider(instance)
_invalidate_tag_cache(instance)
_invalidate_nav_and_seo_cache()
return
if hasattr(instance, 'get_full_url') and not is_update_views:
_notify_spider(instance)
_expire_object_cache(instance)
_invalidate_nav_and_seo_cache()
if isinstance(instance, Comment):
if instance.is_enable:
# szy评论通过后清理详情页与评论区缓存保证实时显示
path = instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0:
site = site[0:site.find(':')]
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
if cache.get('seo_processor'):
cache.delete('seo_processor')
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)])
_thread.start_new_thread(send_comment_email, (instance,))
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
if user and user.username:
logger.info(user)
delete_sidebar_cache()
# cache.clear()

@ -3,21 +3,23 @@ from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
#zr 禁用评论状态的管理动作
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
#zr 启用评论状态的管理动作
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
#zr 设置动作的描述信息
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
#zr 评论管理后台配置类
class CommentAdmin(admin.ModelAdmin):
#zr 设置每页显示数量
list_per_page = 20
#zr 设置列表页显示的字段
list_display = (
'id',
'body',
@ -25,11 +27,16 @@ class CommentAdmin(admin.ModelAdmin):
'link_to_article',
'is_enable',
'creation_time')
#zr 设置可点击链接的字段
list_display_links = ('id', 'body', 'is_enable')
#zr 设置过滤器字段
list_filter = ('is_enable',)
#zr 设置排除的表单字段
exclude = ('creation_time', 'last_modify_time')
#zr 设置可用的批量动作
actions = [disable_commentstatus, enable_commentstatus]
#zr 生成用户信息链接的方法
def link_to_userinfo(self, obj):
info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
@ -37,11 +44,13 @@ class CommentAdmin(admin.ModelAdmin):
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
#zr 生成文章链接的方法
def link_to_article(self, obj):
info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
#zr 设置自定义字段的显示名称
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -1,5 +1,6 @@
from django.apps import AppConfig
#zr 评论应用配置类
class CommentsConfig(AppConfig):
#zr 定义应用名称
name = 'comments'

@ -3,11 +3,14 @@ from django.forms import ModelForm
from .models import Comment
#zr 评论表单类
class CommentForm(ModelForm):
#zr 父评论ID字段隐藏输入且非必需
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta:
#zr 指定关联的模型
model = Comment
#zr 指定表单包含的字段
fields = ['body']

@ -1,3 +1,4 @@
#zr 初始数据库迁移文件:创建评论表结构
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
@ -5,29 +6,42 @@ from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 初始迁移
initial = True
#zr 依赖关系
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
#zr 迁移操作
operations = [
#zr 创建评论表
migrations.CreateModel(
name='Comment',
fields=[
#zr 主键ID字段
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
#zr 评论正文字段
('body', models.TextField(max_length=300, verbose_name='正文')),
#zr 创建时间字段
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
#zr 最后修改时间字段
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
#zr 是否显示字段
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
#zr 文章外键关联
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
#zr 作者外键关联
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
#zr 父评论自关联
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
],
#zr 模型元选项
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',

@ -1,18 +1,23 @@
#zr 数据库迁移文件:修改评论是否显示字段的默认值
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 依赖的迁移文件
dependencies = [
('comments', '0001_initial'),
]
#zr 迁移操作
operations = [
#zr 修改comment模型的is_enable字段
migrations.AlterField(
model_name='comment',
name='is_enable',
#zr 将默认值改为False并更新显示名称
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]

@ -1,3 +1,4 @@
#zr 数据库迁移文件:更新评论模型字段和选项
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
@ -5,56 +6,68 @@ from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
#zr 数据库迁移类
class Migration(migrations.Migration):
#zr 依赖的迁移文件
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
]
#zr 迁移操作列表
operations = [
#zr 更新评论模型的元选项
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
#zr 移除旧的创建时间字段
migrations.RemoveField(
model_name='comment',
name='created_time',
),
#zr 移除旧的最后修改时间字段
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
#zr 添加新的创建时间字段
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
#zr 添加新的最后修改时间字段
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
#zr 更新文章外键字段配置
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
),
#zr 更新作者外键字段配置
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
#zr 更新是否启用字段配置
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
),
#zr 更新父评论外键字段配置
migrations.AlterField(
model_name='comment',
name='parent_comment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
),
]

@ -5,35 +5,44 @@ from django.utils.translation import gettext_lazy as _
from blog.models import Article
# Create your models here.
#zr 评论数据模型
class Comment(models.Model):
#zr 评论正文最大长度300字符
body = models.TextField('正文', max_length=300)
#zr 评论创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
#zr 评论最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#zr 评论作者,关联用户模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
#zr 关联的文章
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
#zr 父级评论,支持评论回复功能
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
#zr 评论是否启用显示
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
#zr 按ID降序排列
ordering = ['-id']
#zr 设置单数和复数显示名称
verbose_name = _('comment')
verbose_name_plural = verbose_name
#zr 指定最新记录的依据字段
get_latest_by = 'id'
def __str__(self):
#zr 返回评论正文作为字符串表示
return self.body

@ -1,8 +1,9 @@
from django import template
#zr 注册模板标签库
register = template.Library()
#zr 解析评论树的模板标签
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""获得当前评论子评论的列表
@ -10,19 +11,25 @@ def parse_commenttree(commentlist, comment):
"""
datas = []
#zr 递归解析子评论的内部函数
def parse(c):
#zr 获取当前评论的直接子评论
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
#zr 将子评论添加到结果列表
datas.append(child)
#zr 递归解析子评论的子评论
parse(child)
#zr 从传入的评论开始解析
parse(comment)
return datas
#zr 显示评论项的包含标签
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""评论"""
#zr 根据是否为子评论设置不同的深度
depth = 1 if ischild else 2
return {
'comment_item': comment,

@ -8,35 +8,42 @@ from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid
# Create your tests here.
# zr 评论模块测试类
class CommentsTest(TransactionTestCase):
# zr 测试初始化设置
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
# zr 导入并设置博客配置,开启评论审核
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True
value.save()
# zr 创建测试用的超级用户
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
# zr 更新文章评论状态为启用
def update_article_comment_status(self, article):
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
# zr 测试评论验证功能
def test_validate_comment(self):
# zr 用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1')
# zr 创建测试分类
category = Category()
category.name = "categoryccc"
category.save()
# zr 创建测试文章
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
@ -46,10 +53,12 @@ class CommentsTest(TransactionTestCase):
article.status = 'p'
article.save()
# zr 获取评论提交URL
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
# zr 测试提交第一条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
@ -57,12 +66,14 @@ class CommentsTest(TransactionTestCase):
self.assertEqual(response.status_code, 302)
# zr 验证评论初始状态为未显示(需要审核)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
# zr 更新评论状态后验证评论显示
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1)
# zr 测试提交第二条评论
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
@ -70,11 +81,14 @@ class CommentsTest(TransactionTestCase):
self.assertEqual(response.status_code, 302)
# zr 验证第二条评论
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
# zr 获取父评论ID用于回复测试
parent_comment_id = article.comment_list()[0].id
# zr 测试回复评论包含Markdown格式内容
response = self.client.post(comment_url,
{
'body': '''
@ -94,16 +108,24 @@ class CommentsTest(TransactionTestCase):
})
self.assertEqual(response.status_code, 302)
# zr 验证回复评论成功
self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3)
# zr 测试评论树解析功能
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1)
# zr 测试评论项显示功能
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
# zr 测试获取最大文章ID和评论ID
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
# zr 测试发送评论邮件功能
from comments.utils import send_comment_email
send_comment_email(comment)

@ -2,8 +2,11 @@ from django.urls import path
from . import views
#zr 定义评论应用的命名空间
app_name = "comments"
#zr 评论模块URL路由配置
urlpatterns = [
#zr 文章评论提交路由
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),

@ -5,13 +5,19 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
# zr 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
# zr 发送评论邮件功能
def send_comment_email(comment):
# zr 获取当前站点域名
site = get_current_site().domain
# zr 设置邮件主题
subject = _('Thanks for your comment')
# zr 构建文章完整URL
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# zr 构建给评论作者的邮件内容
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
@ -19,10 +25,14 @@ def send_comment_email(comment):
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
# zr 获取评论作者邮箱并发送邮件
tomail = comment.author.email
send_email([tomail], subject, html_content)
# zr 如果是回复评论,同时发送邮件给被回复的评论作者
try:
if comment.parent_comment:
# zr 构建回复通知邮件内容
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
@ -32,7 +42,9 @@ def send_comment_email(comment):
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
# zr 获取被回复评论作者的邮箱并发送通知
tomail = comment.parent_comment.author.email
send_email([tomail], subject, html_content)
except Exception as e:
# zr 记录邮件发送异常
logger.error(e)

@ -1,4 +1,4 @@
# Create your views here.
# zr 评论视图模块
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
@ -12,20 +12,26 @@ from .forms import CommentForm
from .models import Comment
# zr 评论提交视图类
class CommentPostView(FormView):
# zr 使用评论表单类
form_class = CommentForm
# zr 指定模板名称
template_name = 'blog/article_detail.html'
# zr 添加CSRF保护装饰器
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
# zr 处理GET请求重定向到文章详情页
def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
# zr 处理表单验证失败的情况
def form_invalid(self, form):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
@ -35,29 +41,41 @@ class CommentPostView(FormView):
'article': article
})
# zr 处理表单验证成功的情况
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
# zr 获取当前用户信息
user = self.request.user
author = BlogUser.objects.get(pk=user.pk)
# zr 获取文章信息
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
# zr 检查文章是否允许评论
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
# zr 创建评论对象但不立即保存到数据库
comment = form.save(False)
comment.article = article
# zr 获取博客设置,判断评论是否需要审核
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
if not settings.comment_need_review:
comment.is_enable = True
comment.author = author
# zr 处理回复评论的情况
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
# zr 保存评论到数据库
comment.save(True)
# zr 重定向到文章页面并定位到新评论
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))

@ -1,5 +1,6 @@
import _thread
import logging
from math import ceil
import django.dispatch
from django.conf import settings
@ -8,11 +9,13 @@ from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
from uuslug import slugify
from blog.models import Article, BlogSettings, Category, Tag
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
from djangoblog.utils import cache, delete_sidebar_cache, delete_view_cache, expire_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
@ -23,6 +26,109 @@ send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
def _get_site_domain():
# szy返回不带端口号的域名统一缓存键格式
site = get_current_site().domain
if ':' in site:
site = site.split(':')[0]
return site
def _expire_object_cache(instance):
# szy根据对象 URL 精准刷新页面缓存,避免全量清空
if not hasattr(instance, 'get_absolute_url'):
return
try:
path = instance.get_absolute_url()
except Exception:
return
expire_view_cache(
path,
servername=_get_site_domain(),
serverport=80,
key_prefix='blogdetail')
def _invalidate_nav_and_seo_cache():
# szy侧边栏和 SEO 上下文依赖全局配置,需要单独失效
delete_sidebar_cache()
if cache.get('seo_processor'):
cache.delete('seo_processor')
def _delete_paginated_cache(key_prefix, total_items):
# szy按分页数量批量删除缓存键避免 cache.clear()
page_size = settings.PAGINATE_BY or 1
total_pages = max(1, ceil(total_items / page_size))
keys = [f'{key_prefix}_{page}' for page in range(1, total_pages + 1)]
cache.delete_many(keys)
def _invalidate_index_cache():
# szy首页/归档依赖文章数据,文章变化时定点清除
total = Article.objects.filter(type='a', status='p').count()
_delete_paginated_cache('index', total)
cache.delete('archives')
def _invalidate_category_cache(category: Category):
# szy分类及其子分类都有联动需要逐个刷新
if not category:
return
_expire_object_cache(category)
category_names = [c.name for c in category.get_sub_categorys()]
total = Article.objects.filter(
category__name__in=category_names,
status='p').count()
_delete_paginated_cache(f'category_list_{category.name}', total)
def _invalidate_tag_cache(tag: Tag):
# szy标签列表缓存独立按标签名称清理
if not tag:
return
_expire_object_cache(tag)
total = Article.objects.filter(
tags__name=tag.name,
type='a',
status='p').distinct().count()
_delete_paginated_cache(f'tag_{tag.name}', total)
def _invalidate_author_cache(username: str):
# szy作者归档页按用户名 slug 生成缓存键
if not username:
return
author_slug = slugify(username)
total = Article.objects.filter(
author__username=username,
type='a',
status='p').count()
_delete_paginated_cache(f'author_{author_slug}', total)
def _notify_spider(instance):
# szy文章/分类更新后推送搜索引擎,保持抓取实时
if settings.TESTING or not hasattr(instance, 'get_full_url'):
return
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
def _invalidate_article_related_cache(article: Article):
# szy文章更新会影响多个页面这里集中处理相关缓存
_expire_object_cache(article)
_invalidate_index_cache()
_invalidate_author_cache(article.author.username if article.author else None)
if article.category_id:
_invalidate_category_cache(article.category)
for tag in article.tags.all():
_invalidate_tag_cache(tag)
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
@ -73,22 +179,44 @@ def model_post_save_callback(
using,
update_fields,
**kwargs):
clearcache = False
if isinstance(instance, LogEntry):
return
if 'get_full_url' in dir(instance):
is_update_views = update_fields == {'views'}
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
if not is_update_views:
clearcache = True
if isinstance(instance, BlogSettings):
# szy站点配置变化时同步刷新缓存和侧边栏
cache.delete('get_blog_setting')
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Article):
if is_update_views:
return
_notify_spider(instance)
_invalidate_article_related_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Category):
_notify_spider(instance)
_invalidate_category_cache(instance)
_invalidate_nav_and_seo_cache()
return
if isinstance(instance, Tag):
_notify_spider(instance)
_invalidate_tag_cache(instance)
_invalidate_nav_and_seo_cache()
return
if hasattr(instance, 'get_full_url') and not is_update_views:
_notify_spider(instance)
_expire_object_cache(instance)
_invalidate_nav_and_seo_cache()
if isinstance(instance, Comment):
if instance.is_enable:
# szy评论通过后清理详情页与评论区缓存保证实时显示
path = instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0:
@ -109,9 +237,6 @@ def model_post_save_callback(
_thread.start_new_thread(send_comment_email, (instance,))
if clearcache:
cache.clear()
@receiver(user_logged_in)
@receiver(user_logged_out)

@ -1,4 +1,8 @@
import logging
from pathlib import Path
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
logger = logging.getLogger(__name__)
@ -8,13 +12,34 @@ class BasePlugin:
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
PLUGIN_AUTHOR = None
# 插件配置
SUPPORTED_POSITIONS = [] # 支持的显示位置
DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高)
POSITION_PRIORITIES = {} # 各位置的优先级 {'sidebar': 50, 'article_bottom': 80}
def __init__(self):
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
# 设置插件路径
self.plugin_dir = self._get_plugin_directory()
self.plugin_slug = self._get_plugin_slug()
self.init_plugin()
self.register_hooks()
def _get_plugin_directory(self):
"""获取插件目录路径"""
import inspect
plugin_file = inspect.getfile(self.__class__)
return Path(plugin_file).parent
def _get_plugin_slug(self):
"""获取插件标识符(目录名)"""
return self.plugin_dir.name
def init_plugin(self):
"""
插件初始化逻辑
@ -29,6 +54,129 @@ class BasePlugin:
"""
pass
# === 位置渲染系统 ===
def render_position_widget(self, position, context, **kwargs):
"""
根据位置渲染插件组件
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
dict: {'html': 'HTML内容', 'priority': 优先级} None
"""
if position not in self.SUPPORTED_POSITIONS:
return None
# 检查条件显示
if not self.should_display(position, context, **kwargs):
return None
# 调用具体的位置渲染方法
method_name = f'render_{position}_widget'
if hasattr(self, method_name):
html = getattr(self, method_name)(context, **kwargs)
if html:
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
return {
'html': html,
'priority': priority,
'plugin_name': self.PLUGIN_NAME
}
return None
def should_display(self, position, context, **kwargs):
"""
判断插件是否应该在指定位置显示
子类可重写此方法实现条件显示逻辑
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
bool: 是否显示
"""
return True
# === 各位置渲染方法 - 子类重写 ===
def render_sidebar_widget(self, context, **kwargs):
"""渲染侧边栏组件"""
return None
def render_article_bottom_widget(self, context, **kwargs):
"""渲染文章底部组件"""
return None
def render_article_top_widget(self, context, **kwargs):
"""渲染文章顶部组件"""
return None
def render_header_widget(self, context, **kwargs):
"""渲染页头组件"""
return None
def render_footer_widget(self, context, **kwargs):
"""渲染页脚组件"""
return None
def render_comment_before_widget(self, context, **kwargs):
"""渲染评论前组件"""
return None
def render_comment_after_widget(self, context, **kwargs):
"""渲染评论后组件"""
return None
# === 模板系统 ===
def render_template(self, template_name, context=None):
"""
渲染插件模板
Args:
template_name: 模板文件名
context: 模板上下文
Returns:
HTML字符串
"""
if context is None:
context = {}
template_path = f"plugins/{self.plugin_slug}/{template_name}"
try:
return render_to_string(template_path, context)
except TemplateDoesNotExist:
logger.warning(f"Plugin template not found: {template_path}")
return ""
# === 静态资源系统 ===
def get_static_url(self, static_file):
"""获取插件静态文件URL"""
from django.templatetags.static import static
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
def get_css_files(self):
"""获取插件CSS文件列表"""
return []
def get_js_files(self):
"""获取插件JavaScript文件列表"""
return []
def get_head_html(self, context=None):
"""获取需要插入到<head>中的HTML内容"""
return ""
def get_body_html(self, context=None):
"""获取需要插入到<body>底部的HTML内容"""
return ""
def get_plugin_info(self):
"""
获取插件信息
@ -37,5 +185,10 @@ class BasePlugin:
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
'version': self.PLUGIN_VERSION,
'author': self.PLUGIN_AUTHOR,
'slug': self.plugin_slug,
'directory': str(self.plugin_dir),
'supported_positions': self.SUPPORTED_POSITIONS,
'priorities': self.POSITION_PRIORITIES
}

@ -5,3 +5,18 @@ ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"
# 位置钩子常量
POSITION_HOOKS = {
'article_top': 'article_top_widgets',
'article_bottom': 'article_bottom_widgets',
'sidebar': 'sidebar_widgets',
'header': 'header_widgets',
'footer': 'footer_widgets',
'comment_before': 'comment_before_widgets',
'comment_after': 'comment_after_widgets',
}
# 资源注入钩子
HEAD_RESOURCES_HOOK = 'head_resources'
BODY_RESOURCES_HOOK = 'body_resources'

@ -4,16 +4,61 @@ from django.conf import settings
logger = logging.getLogger(__name__)
# 全局插件注册表
_loaded_plugins = []
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
global _loaded_plugins
_loaded_plugins = []
for plugin_name in settings.ACTIVE_PLUGINS:
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
__import__(f'plugins.{plugin_name}.plugin')
logger.info(f"Successfully loaded plugin: {plugin_name}")
# 导入插件模块
plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
# 获取插件实例
if hasattr(plugin_module, 'plugin'):
plugin_instance = plugin_module.plugin
_loaded_plugins.append(plugin_instance)
logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
else:
logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
except AttributeError as e:
logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
except Exception as e:
logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
def get_loaded_plugins():
"""获取所有已加载的插件"""
return _loaded_plugins
def get_plugin_by_name(plugin_name):
"""根据名称获取插件"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_name:
return plugin
return None
def get_plugin_by_slug(plugin_slug):
"""根据slug获取插件"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_slug:
return plugin
return None
def get_plugins_info():
"""获取所有插件的信息"""
return [plugin.get_plugin_info() for plugin in _loaded_plugins]
def get_plugins_by_position(position):
"""获取支持指定位置的插件"""
return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]

@ -13,6 +13,7 @@ import os
import sys
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _
@ -27,16 +28,27 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
_ENV_SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
# Default to DEBUG=True only when no DJANGO_SECRET_KEY is provided, so local development works out of the box.
DEBUG = env_to_bool('DJANGO_DEBUG', _ENV_SECRET_KEY is None)
def get_secret_key():
if _ENV_SECRET_KEY:
return _ENV_SECRET_KEY
if DEBUG:
# Provide a deterministic key for convenience in local development.
return 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
raise ImproperlyConfigured('DJANGO_SECRET_KEY environment variable is required when DEBUG=False')
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False
SECRET_KEY = get_secret_key()
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# Allow configuring host whitelist via environment, fallback to safe defaults
_default_hosts = '127.0.0.1,localhost'
ALLOWED_HOSTS = [host.strip() for host in os.environ.get('DJANGO_ALLOWED_HOSTS', _default_hosts).split(',') if host.strip()]
# django 4.0新增配置
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
@ -111,12 +123,15 @@ DATABASES = {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '12345678',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': {
'charset': 'utf8mb4'},
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
},
'CONN_MAX_AGE': int(os.environ.get('DJANGO_DB_CONN_MAX_AGE', 60)),
}}
# Password validation
@ -177,6 +192,12 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
# szy同时收集项目静态目录与插件资源避免部署缺文件
STATICFILES_DIRS = [
STATICFILES,
os.path.join(BASE_DIR, 'plugins'),
]
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
@ -285,11 +306,6 @@ LOGGING = {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
}
}
}
@ -301,23 +317,65 @@ STATICFILES_FINDERS = (
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
# 根据环境变量决定是否启用离线压缩
COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true'
# 压缩输出目录
COMPRESS_OUTPUT_DIR = 'compressed'
# 压缩文件名模板 - 包含哈希值用于缓存破坏
COMPRESS_CSS_HASHING_METHOD = 'mtime'
COMPRESS_JS_HASHING_METHOD = 'mtime'
# 高级CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
# 创建绝对URL
'compressor.filters.css_default.CssAbsoluteFilter',
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
# CSS压缩器 - 高压缩等级
'compressor.filters.cssmin.CSSCompressorFilter',
]
# 高级JS压缩过滤器
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
# JS压缩器 - 高压缩等级
'compressor.filters.jsmin.SlimItFilter',
]
# 压缩缓存配置
COMPRESS_CACHE_BACKEND = 'default'
COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey'
# 预压缩配置
COMPRESS_PRECOMPILERS = (
# 支持SCSS/SASS
('text/x-scss', 'django_libsass.SassCompiler'),
('text/x-sass', 'django_libsass.SassCompiler'),
)
# 压缩性能优化
COMPRESS_MINT_DELAY = 30 # 压缩延迟(秒)
COMPRESS_MTIME_DELAY = 10 # 修改时间检查延迟
COMPRESS_REBUILD_TIMEOUT = 2592000 # 重建超时30天
# 压缩等级配置
COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor'
COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor'
# 静态文件缓存配置
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# 浏览器缓存配置(通过中间件或服务器配置)
COMPRESS_URL = STATIC_URL
COMPRESS_ROOT = STATIC_ROOT
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
AVATAR_ROOT = os.path.join(MEDIA_ROOT, 'avatars')
AVATAR_URL = f'{MEDIA_URL}avatars/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
@ -339,5 +397,7 @@ ACTIVE_PLUGINS = [
'reading_time',
'external_links',
'view_count',
'seo_optimizer'
'seo_optimizer',
'image_lazy_loading',
'article_recommendation',
]

@ -20,6 +20,8 @@ from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from django.http import JsonResponse
import time
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
@ -36,12 +38,25 @@ sitemaps = {
'static': StaticViewSitemap
}
# szy自定义错误页面保证异常提示保持博客风格
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
handler403 = 'blog.views.permission_denied_view'
def health_check(request):
"""
健康检查接口
简单返回服务健康状态
"""
return JsonResponse({
'status': 'healthy',
'timestamp': time.time()
})
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
path('health/', health_check, name='health_check'),
]
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls),

@ -182,20 +182,20 @@ def save_user_avatar(url):
logger.info(url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
basedir = settings.AVATAR_ROOT
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir)
os.makedirs(basedir, exist_ok=True)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename)
with open(os.path.join(basedir, save_filename), 'wb+') as file:
avatar_path = os.path.join(basedir, save_filename)
with open(avatar_path, 'wb+') as file:
file.write(rsp.content)
return static('avatar/' + save_filename)
return f'{settings.AVATAR_URL}{save_filename}'
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png')
@ -224,9 +224,49 @@ def get_resource_url():
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
'h2', 'p', 'span', 'div']
# 安全的class值白名单 - 只允许代码高亮相关的class
ALLOWED_CLASSES = [
'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn',
'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2',
's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il'
]
def class_filter(tag, name, value):
"""自定义class属性过滤器"""
if name == 'class':
# 只允许预定义的安全class值
allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
return ' '.join(allowed_classes) if allowed_classes else False
return value
# 安全的属性白名单
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'],
'abbr': ['title'],
'acronym': ['title'],
'span': class_filter,
'div': class_filter,
'pre': class_filter,
'code': class_filter
}
# 安全的协议白名单 - 防止javascript:等危险协议
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
def sanitize_html(html):
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
"""
安全的HTML清理函数
使用bleach库进行白名单过滤防止XSS攻击
"""
return bleach.clean(
html,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS, # 限制允许的协议
strip=True, # 移除不允许的标签而不是转义
strip_comments=True # 移除HTML注释
)

@ -0,0 +1,183 @@
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager()
self.include_spelling = True
def _get_models(self, iterable):
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
def _delete(self, models):
for m in models:
m.delete()
return True
def _rebuild(self, models):
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
def update(self, index, iterable, commit=True):
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
self.remove(None)
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
for suggest in search.suggest.suggest_search:
if suggest["options"]:
keywords.append(suggest["options"][0]["text"])
else:
keywords.append(suggest["text"])
return ' '.join(keywords)
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# 推荐词搜索
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()
hits = results['hits'].total
raw_results = []
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
result_class = SearchResult
result = result_class(
app_label,
model_name,
raw_result['_id'],
raw_result['_score'],
**additional_fields)
raw_results.append(result)
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion
return {
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
Whoosh 1.X differs here in that you can no longer use a backslash
to escape reserved characters. Instead, the whole word should be
quoted.
"""
words = query_fragment.split()
cleaned_words = []
for word in words:
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
break
cleaned_words.append(word)
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
return value.query_string
def get_count(self):
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
def search(self):
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
return sqs
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery

@ -0,0 +1,40 @@
from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed
from blog.models import Article
from djangoblog.utils import CommonMarkdown
class DjangoBlogFeed(Feed):
feed_type = Rss201rev2Feed
description = '大巧无工,重剑无锋.'
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
def author_name(self):
return get_user_model().objects.first().nickname
def author_link(self):
return get_user_model().objects.first().get_absolute_url()
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
return item.title
def item_description(self, item):
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
return item.get_absolute_url()
def item_guid(self, item):
return

@ -0,0 +1,91 @@
from django.contrib import admin
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
list_filter = [
'content_type'
]
search_fields = [
'object_repr',
'change_message'
]
list_display_links = [
'action_time',
'get_change_message',
]
list_display = [
'action_time',
'user_link',
'content_type',
'object_link',
'get_change_message',
]
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
def has_delete_permission(self, request, obj=None):
return False
def object_link(self, obj):
object_link = escape(obj.object_repr)
content_type = obj.content_type
if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
try:
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
)
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
pass
return mark_safe(object_link)
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
def user_link(self, obj):
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user))
try:
# try returning an actual link instead of object repr string
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
)
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
pass
return mark_safe(user_link)
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
def get_actions(self, request):
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions

@ -1,7 +1,26 @@
from django.contrib import admin
from .models import OwnTrackLog
# Register your models here.
@admin.register(OwnTrackLog)
class OwnTrackLogAdmin(admin.ModelAdmin):
"""
OwnTrackLog模型管理配置
"""
list_display = ('tid', 'lat', 'lon', 'creation_time', 'accuracy')
list_filter = ('tid', 'creation_time')
search_fields = ('tid',)
date_hierarchy = 'creation_time'
readonly_fields = ('creation_time',)
fieldsets = (
('基本信息', {
'fields': ('tid', 'creation_time')
}),
('位置信息', {
'fields': ('lat', 'lon', 'accuracy', 'battery')
}),
)
class OwnTrackLogsAdmin(admin.ModelAdmin):
pass
def get_queryset(self, request):
"""优化查询,减少数据库访问"""
return super().get_queryset(request).select_related()

@ -1,5 +1,7 @@
# 导入Django应用程序配置基类 #zqx: 引入Django的AppConfig基类用于创建应用配置类
from django.apps import AppConfig
# 定义owntracks应用的配置类继承自AppConfig #zqx: 创建OwntracksConfig类继承自AppConfig用于配置owntracks应用
class OwntracksConfig(AppConfig):
# 设置应用的名称为'owntracks' #zqx: 设置name属性为'owntracks',指定应用的名称
name = 'owntracks'

@ -1,31 +1,51 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# Generated by Django 4.1.7 on 2023-03-02 07:14 #zqx: 由Django 4.1.7在2023年3月2日7点14分自动生成的迁移文件
# 导入Django数据库迁移模块和模型模块 #zqx: 引入Django的数据迁移和模型相关模块
from django.db import migrations, models
# 导入Django的时区工具模块 #zqx: 引入Django的时区工具模块用于处理时间相关功能
import django.utils.timezone
# 定义一个迁移类继承自Django的Migration基类 #zqx: 定义迁移类继承自Django的Migration基类
class Migration(migrations.Migration):
# 标记这是一个初始迁移 #zqx: 设置initial属性为True标记这是应用的初始迁移
initial = True
# 定义依赖关系,此处为空列表表示没有依赖其他迁移 #zqx: 定义此迁移所依赖的其他迁移,空列表表示无依赖
dependencies = [
]
# 定义具体的迁移操作 #zqx: 定义本次迁移需要执行的操作列表
operations = [
# 创建一个新的数据模型 #zqx: 使用CreateModel操作创建新的数据表
migrations.CreateModel(
# 模型名称为'OwnTrackLog' #zqx: 指定要创建的模型名称为OwnTrackLog
name='OwnTrackLog',
# 定义模型的字段 #zqx: 定义模型中的各个字段及其属性
fields=[
# 主键字段自动创建的BigAutoField类型 #zqx: 定义主键字段使用BigAutoField类型并设置为自动创建
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 用户标识字段最大长度100的字符字段 #zqx: 定义tid字段为CharField类型最大长度100显示名为"用户"
('tid', models.CharField(max_length=100, verbose_name='用户')),
# 纬度字段,浮点数类型 #zqx: 定义lat字段为FloatField类型显示名为"纬度"
('lat', models.FloatField(verbose_name='纬度')),
# 经度字段,浮点数类型 #zqx: 定义lon字段为FloatField类型显示名为"经度"
('lon', models.FloatField(verbose_name='经度')),
# 创建时间字段,默认值为当前时区时间 #zqx: 定义created_time字段为DateTimeField类型默认值为当前时区时间显示名为"创建时间"
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
],
# 模型的元数据选项配置 #zqx: 配置模型的元数据选项
options={
# 单数形式的可读名称 #zqx: 设置模型在单数情况下的可读名称为"OwnTrackLogs"
'verbose_name': 'OwnTrackLogs',
# 复数形式的可读名称 #zqx: 设置模型在复数情况下的可读名称为"OwnTrackLogs"
'verbose_name_plural': 'OwnTrackLogs',
# 数据查询时的默认排序方式,按创建时间升序排列 #zqx: 设置查询结果默认按照created_time字段升序排列
'ordering': ['created_time'],
# 定义获取最新记录时使用的字段 #zqx: 设置获取最新记录时使用created_time字段作为判断依据
'get_latest_by': 'created_time',
},
),
]

@ -1,22 +1,30 @@
# Generated by Django 4.2.5 on 2023-09-06 13:19
# Generated by Django 4.2.5 on 2023-09-06 13:19 #zqx: 由Django 4.2.5在2023年9月6日13点19分自动生成的迁移文件
# 导入Django数据库迁移模块 #zqx: 引入Django的数据迁移模块
from django.db import migrations
# 定义数据库迁移类继承自Django的Migration基类 #zqx: 定义迁移类继承自Django的Migration基类
class Migration(migrations.Migration):
# 定义迁移依赖关系此迁移依赖于owntracks应用的0001_initial迁移文件 #zqx: 设置此迁移依赖于owntracks应用的0001_initial迁移
dependencies = [
('owntracks', '0001_initial'),
]
# 定义具体的迁移操作列表 #zqx: 定义本次迁移需要执行的操作列表
operations = [
# 修改OwnTrackLog模型的选项配置 #zqx: 使用AlterModelOptions操作修改OwnTrackLog模型的选项配置
migrations.AlterModelOptions(
name='owntracklog',
options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs', 'verbose_name_plural': 'OwnTrackLogs'},
name='owntracklog', #zqx: 指定要修改选项的模型名称为owntracklog
# 更新模型选项将get_latest_by和ordering中的字段名从created_time改为creation_time #zqx: 更新模型选项将get_latest_by和ordering字段从created_time改为creation_time保持verbose_name和verbose_name_plural不变
options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs',
'verbose_name_plural': 'OwnTrackLogs'},
),
# 重命名模型字段 #zqx: 使用RenameField操作重命名模型字段
migrations.RenameField(
model_name='owntracklog',
old_name='created_time',
new_name='creation_time',
model_name='owntracklog', #zqx: 指定要重命名字段的模型名称为owntracklog
old_name='created_time', #zqx: 指定原字段名为created_time
new_name='creation_time', #zqx: 指定新字段名为creation_time
),
]

@ -1,20 +1,70 @@
from django.db import models
from django.utils.timezone import now
from django.core.validators import MinValueValidator, MaxValueValidator
# Create your models here.
class OwnTrackLog(models.Model):
tid = models.CharField(max_length=100, null=False, verbose_name='用户')
lat = models.FloatField(verbose_name='纬度')
lon = models.FloatField(verbose_name='经度')
creation_time = models.DateTimeField('创建时间', default=now)
"""
OwnTracks位置数据模型
用于存储移动设备上报的GPS位置信息
"""
# 添加更严格的字段验证
tid = models.CharField(
max_length=100,
null=False,
verbose_name='用户ID',
help_text='设备或用户唯一标识'
)
lat = models.FloatField(
verbose_name='纬度',
validators=[
MinValueValidator(-90.0),
MaxValueValidator(90.0)
],
help_text='纬度坐标,范围-90到90'
)
lon = models.FloatField(
verbose_name='经度',
validators=[
MinValueValidator(-180.0),
MaxValueValidator(180.0)
],
help_text='经度坐标,范围-180到180'
)
creation_time = models.DateTimeField(
'创建时间',
default=now,
db_index=True, # zqx 添加索引提升查询性能
help_text='数据创建时间'
)
# zqx: 添加额外有用字段
accuracy = models.FloatField(
'精度',
null=True,
blank=True,
help_text='GPS定位精度(米)'
)
battery = models.FloatField(
'电量',
null=True,
blank=True,
help_text='设备电量百分比'
)
def __str__(self):
return self.tid
return f"用户{self.tid}{self.creation_time.strftime('%Y-%m-%d %H:%M')}的位置"
def get_coordinates(self):
"""获取坐标元组"""
return (self.lat, self.lon)
class Meta:
ordering = ['creation_time']
verbose_name = "OwnTrackLogs"
verbose_name_plural = verbose_name
verbose_name = "位置记录"
verbose_name_plural = "位置记录"
get_latest_by = 'creation_time'
indexes = [
models.Index(fields=['tid', 'creation_time']),
models.Index(fields=['creation_time']),
]

@ -0,0 +1,36 @@
# 高德地图API配置
AMAP_API_KEY = os.getenv('AMAP_API_KEY', 'your-default-key-here')
# 缓存配置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
}
}
#management / commands / cleanup_old_locations.py
from django.core.management.base import BaseCommand
from django.utils import timezone
from owntracks.models import OwnTrackLog
class Command(BaseCommand):
help = '清理过期的位置记录'
def add_arguments(self, parser):
parser.add_argument(
'--days',
type=int,
default=365,
help='保留多少天内的数据'
)
def handle(self, *args, **options):
cutoff_date = timezone.now() - timezone.timedelta(days=options['days'])
deleted_count, _ = OwnTrackLog.objects.filter(
creation_time__lt=cutoff_date
).delete()
self.stdout.write(
self.style.SUCCESS(f'成功删除 {deleted_count} 条过期记录')
)

@ -1,64 +1,101 @@
import json
from django.test import Client, RequestFactory, TestCase
from accounts.models import BlogUser
from datetime import datetime
from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from .models import OwnTrackLog
User = get_user_model()
# Create your tests here.
class OwnTrackLogTest(TestCase):
"""OwnTrackLog模型和视图测试"""
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.superuser = User.objects.create_superuser(
email="admin@example.com",
username="admin",
password="testpassword123"
)
self.normal_user = User.objects.create_user(
email="user@example.com",
username="user",
password="testpassword123"
)
def test_own_track_log(self):
o = {
'tid': 12,
'lat': 123.123,
'lon': 134.341
def test_create_valid_location(self):
"""测试创建有效位置记录"""
data = {
'tid': 'test-user-1',
'lat': 39.9042,
'lon': 116.4074,
'acc': 10.5,
'batt': 85.0
}
self.client.post(
response = self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
length = len(OwnTrackLog.objects.all())
self.assertEqual(length, 1)
o = {
'tid': 12,
'lat': 123.123
}
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 201)
self.assertEqual(OwnTrackLog.objects.count(), 1)
location = OwnTrackLog.objects.first()
self.assertEqual(location.tid, 'test-user-1')
self.assertEqual(location.lat, 39.9042)
def test_create_invalid_location(self):
"""测试创建无效位置记录"""
# zqx 测试缺少必需字段
data = {'tid': 'test-user'}
response = self.client.post(
'/owntracks/logtracks',
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 400)
self.client.post(
# zqx: 测试无效坐标
data = {'tid': 'test', 'lat': 1000, 'lon': 2000}
response = self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
length = len(OwnTrackLog.objects.all())
self.assertEqual(length, 1)
rsp = self.client.get('/owntracks/show_maps')
self.assertEqual(rsp.status_code, 302)
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
self.client.login(username='liangliangyy1', password='liangliangyy1')
s = OwnTrackLog()
s.tid = 12
s.lon = 123.234
s.lat = 34.234
s.save()
rsp = self.client.get('/owntracks/show_dates')
self.assertEqual(rsp.status_code, 200)
rsp = self.client.get('/owntracks/show_maps')
self.assertEqual(rsp.status_code, 200)
rsp = self.client.get('/owntracks/get_datas')
self.assertEqual(rsp.status_code, 200)
rsp = self.client.get('/owntracks/get_datas?date=2018-02-26')
self.assertEqual(rsp.status_code, 200)
data=json.dumps(data),
content_type='application/json'
)
self.assertEqual(response.status_code, 400)
def test_permission_access(self):
"""测试权限访问控制"""
# zqx: 未登录用户访问受限页面
response = self.client.get('/owntracks/show_maps')
self.assertEqual(response.status_code, 302) # 重定向到登录
# 普通用户登录
self.client.login(username='user', password='testpassword123')
response = self.client.get('/owntracks/show_maps')
self.assertEqual(response.status_code, 403) # 禁止访问
# zqx: 超级用户登录
self.client.login(username='admin', password='testpassword123')
response = self.client.get('/owntracks/show_maps')
self.assertEqual(response.status_code, 200)
def test_get_location_data(self):
"""测试获取位置数据"""
# zqx: 创建测试数据
OwnTrackLog.objects.create(
tid='user1', lat=39.9, lon=116.4
)
OwnTrackLog.objects.create(
tid='user1', lat=39.91, lon=116.41
)
self.client.login(username='admin', password='testpassword123')
response = self.client.get('/owntracks/get_datas')
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertEqual(len(data), 1)
self.assertEqual(len(data[0]['path']), 2)

@ -1,12 +1,29 @@
from django.urls import path
from . import views
app_name = "owntracks"
urlpatterns = [
path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'),
path('owntracks/show_maps', views.show_maps, name='show_maps'),
path('owntracks/get_datas', views.get_datas, name='get_datas'),
path('owntracks/show_dates', views.show_log_dates, name='show_dates')
path('logtracks',
views.manage_owntrack_log,
name='logtracks'),
path('show_maps',
views.show_maps,
name='show_maps'),
path('get_datas',
views.get_datas,
name='get_datas'),
path('show_dates',
views.show_log_dates,
name='show_dates')
]
#zqx: 应该添加API版本控制
# urlpatterns = [
# path('api/v1/tracks', views.manage_owntrack_log, name='log-tracks'),
# path('api/v1/tracks/dates', views.show_log_dates, name='track-dates'),
# path('api/v1/tracks/<str:date>', views.get_datas, name='track-data'),
# ]

@ -1,127 +1,236 @@
# Create your views here.
import datetime
import itertools
import json
import logging
from typing import Dict, Any, List
from datetime import timezone
from itertools import groupby
import django
import requests
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import HttpResponse, JsonResponse, HttpRequest
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.db import transaction
from django.core.cache import cache
from django.conf import settings
from .models import OwnTrackLog
logger = logging.getLogger(__name__)
def is_superuser(user):
"""检查用户是否为超级用户"""
return user.is_superuser
@csrf_exempt
def manage_owntrack_log(request):
@require_http_methods(["POST"])
@transaction.atomic
def manage_owntrack_log(request: HttpRequest) -> HttpResponse:
"""
处理OwnTracks位置数据上报
"""
try:
s = json.loads(request.read().decode('utf-8'))
tid = s['tid']
lat = s['lat']
lon = s['lon']
logger.info(
'tid:{tid}.lat:{lat}.lon:{lon}'.format(
tid=tid, lat=lat, lon=lon))
if tid and lat and lon:
m = OwnTrackLog()
m.tid = tid
m.lat = lat
m.lon = lon
m.save()
return HttpResponse('ok')
else:
return HttpResponse('data error')
# 验证内容类型
if request.content_type != 'application/json':
return HttpResponse('Unsupported media type', status=415)
# zqx: 解析请求数据
try:
raw_data = request.body.decode('utf-8')
data = json.loads(raw_data)
except (json.JSONDecodeError, UnicodeDecodeError) as e:
logger.warning(f"JSON解析失败: {e}")
return HttpResponse('Invalid JSON', status=400)
# zqx: 验证必需字段
required_fields = ['tid', 'lat', 'lon']
if not all(field in data for field in required_fields):
missing = [field for field in required_fields if field not in data]
logger.warning(f"缺少必需字段: {missing}")
return HttpResponse(f'Missing required fields: {missing}', status=400)
# zqx: 验证数据类型
try:
tid = str(data['tid'])
lat = float(data['lat'])
lon = float(data['lon'])
except (ValueError, TypeError) as e:
logger.warning(f"数据类型错误: {e}")
return HttpResponse('Invalid data types', status=400)
# zqx: 验证坐标范围
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
logger.warning(f"坐标超出范围: lat={lat}, lon={lon}")
return HttpResponse('Coordinates out of range', status=400)
# zqx: 创建位置记录
track_log = OwnTrackLog(
tid=tid,
lat=lat,
lon=lon,
accuracy=data.get('acc'),
battery=data.get('batt')
)
track_log.full_clean() # 模型验证
track_log.save()
logger.info(f"位置记录创建成功: 用户{tid}在({lat}, {lon})")
return HttpResponse('OK', status=201)
except Exception as e:
logger.error(e)
return HttpResponse('error')
logger.error(f"位置记录处理异常: {e}", exc_info=True)
return HttpResponse('Server error', status=500)
@login_required
def show_maps(request):
if request.user.is_superuser:
defaultdate = str(datetime.datetime.now(timezone.utc).date())
date = request.GET.get('date', defaultdate)
@user_passes_test(is_superuser)
def show_maps(request: HttpRequest) -> HttpResponse:
# zqx:
显示位置地图页面
today = datetime.datetime.now(timezone.utc).date()
date_str = request.GET.get('date', str(today))
# zqx: 验证日期格式
try:
datetime.datetime.strptime(date_str, '%Y-%m-%d')
except ValueError:
date_str = str(today)
context = {
'date': date
'date': date_str,
'map_api_key': getattr(settings, 'AMAP_API_KEY', '')
}
return render(request, 'owntracks/show_maps.html', context)
else:
from django.http import HttpResponseForbidden
return HttpResponseForbidden()
@login_required
def show_log_dates(request):
dates = OwnTrackLog.objects.values_list('creation_time', flat=True)
results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates))))
context = {
'results': results
}
@user_passes_test(is_superuser)
def show_log_dates(request: HttpRequest) -> HttpResponse:
# zqx
显示有位置记录的日期列表
cache_key = 'owntracks_dates_list'
dates = cache.get(cache_key)
if dates is None:
# zqx: 使用数据库的日期函数提高性能
dates = OwnTrackLog.objects.dates('creation_time', 'day', order='DESC')
dates = [date.strftime('%Y-%m-%d') for date in dates]
cache.set(cache_key, dates, timeout=3600) # 缓存1小时
context = {'results': dates}
return render(request, 'owntracks/show_log_dates.html', context)
def convert_to_amap(locations):
convert_result = []
it = iter(locations)
def convert_coordinates_batch(locations: List[OwnTrackLog]) -> str:
# zqx:
批量转换GPS坐标到高德坐标系
if not locations:
return ""
# zqx: 坐标去重
unique_coords = set()
for location in locations:
coord_key = f"{location.lon:.6f},{location.lat:.6f}"
unique_coords.add(coord_key)
coordinates_list = list(unique_coords)
batch_size = 30 # 高德API批量限制
all_converted = []
item = list(itertools.islice(it, 30))
while item:
datas = ';'.join(
set(map(lambda x: str(x.lon) + ',' + str(x.lat), item)))
for i in range(0, len(coordinates_list), batch_size):
batch = coordinates_list[i:i + batch_size]
locations_str = ';'.join(batch)
key = '8440a376dfc9743d8924bf0ad141f28e'
api = 'http://restapi.amap.com/v3/assistant/coordinate/convert'
query = {
'key': key,
'locations': datas,
# zqx: 调用高德坐标转换API
api_key = getattr(settings, 'AMAP_API_KEY', '8440a376dfc9743d8924bf0ad141f28e')
api_url = 'http://restapi.amap.com/v3/assistant/coordinate/convert'
params = {
'key': api_key,
'locations': locations_str,
'coordsys': 'gps'
}
rsp = requests.get(url=api, params=query)
result = json.loads(rsp.text)
if "locations" in result:
convert_result.append(result['locations'])
item = list(itertools.islice(it, 30))
return ";".join(convert_result)
try:
response = requests.get(api_url, params=params, timeout=10)
response.raise_for_status()
result = response.json()
if result.get('status') == '1' and 'locations' in result:
all_converted.append(result['locations'])
else:
logger.error(f"高德API错误: {result.get('info', 'Unknown error')}")
# 使用原始坐标作为备选
all_converted.append(locations_str)
except requests.RequestException as e:
logger.error(f"坐标转换API请求失败: {e}")
all_converted.append(locations_str) # 使用原始坐标
return ";".join(all_converted)
@login_required
def get_datas(request):
now = django.utils.timezone.now().replace(tzinfo=timezone.utc)
querydate = django.utils.timezone.datetime(
now.year, now.month, now.day, 0, 0, 0)
if request.GET.get('date', None):
date = list(map(lambda x: int(x), request.GET.get('date').split('-')))
querydate = django.utils.timezone.datetime(
date[0], date[1], date[2], 0, 0, 0)
nextdate = querydate + datetime.timedelta(days=1)
models = OwnTrackLog.objects.filter(
creation_time__range=(querydate, nextdate))
result = list()
if models and len(models):
for tid, item in groupby(
sorted(models, key=lambda k: k.tid), key=lambda k: k.tid):
d = dict()
d["name"] = tid
paths = list()
# 使用高德转换后的经纬度
# locations = convert_to_amap(
# sorted(item, key=lambda x: x.creation_time))
# for i in locations.split(';'):
# paths.append(i.split(','))
# 使用GPS原始经纬度
for location in sorted(item, key=lambda x: x.creation_time):
paths.append([str(location.lon), str(location.lat)])
d["path"] = paths
result.append(d)
@user_passes_test(is_superuser)
def get_datas(request: HttpRequest) -> JsonResponse:
"""
获取指定日期的位置数据(JSON API)
"""
# zqx: 日期处理
date_str = request.GET.get('date')
if date_str:
try:
query_date = datetime.datetime.strptime(date_str, '%Y-%m-%d').replace(
tzinfo=timezone.utc
)
except ValueError:
return JsonResponse(
{'error': 'Invalid date format. Use YYYY-MM-DD.'},
status=400
)
else:
query_date = datetime.datetime.now(timezone.utc).replace(
hour=0, minute=0, second=0, microsecond=0
)
next_date = query_date + datetime.timedelta(days=1)
# zqx: 数据库查询优化
locations = OwnTrackLog.objects.filter(
creation_time__range=(query_date, next_date)
).order_by('tid', 'creation_time')
if not locations.exists():
return JsonResponse([], safe=False)
# zqx: 按用户分组处理数据
result = []
current_tid = None
user_paths = []
for location in locations:
if location.tid != current_tid:
# zqx: 保存前一个用户的数据
if current_tid is not None and user_paths:
result.append({
"name": current_tid,
"path": user_paths.copy()
})
# zqx 开始新用户
current_tid = location.tid
user_paths = []
user_paths.append([str(location.lon), str(location.lat)])
# zqx 添加最后一个用户的数据
if current_tid is not None and user_paths:
result.append({
"name": current_tid,
"path": user_paths
})
return JsonResponse(result, safe=False)

@ -0,0 +1,194 @@
import logging
from pathlib import Path
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
logger = logging.getLogger(__name__)
class BasePlugin:
# 插件元数据
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
PLUGIN_AUTHOR = None
# 插件配置
SUPPORTED_POSITIONS = [] # 支持的显示位置
DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高)
POSITION_PRIORITIES = {} # 各位置的优先级 {'sidebar': 50, 'article_bottom': 80}
def __init__(self):
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
# 设置插件路径
self.plugin_dir = self._get_plugin_directory()
self.plugin_slug = self._get_plugin_slug()
self.init_plugin()
self.register_hooks()
def _get_plugin_directory(self):
"""获取插件目录路径"""
import inspect
plugin_file = inspect.getfile(self.__class__)
return Path(plugin_file).parent
def _get_plugin_slug(self):
"""获取插件标识符(目录名)"""
return self.plugin_dir.name
def init_plugin(self):
"""
插件初始化逻辑
子类可以重写此方法来实现特定的初始化操作
"""
logger.info(f'{self.PLUGIN_NAME} initialized.')
def register_hooks(self):
"""
注册插件钩子
子类可以重写此方法来注册特定的钩子
"""
pass
# === 位置渲染系统 ===
def render_position_widget(self, position, context, **kwargs):
"""
根据位置渲染插件组件
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
dict: {'html': 'HTML内容', 'priority': 优先级} None
"""
if position not in self.SUPPORTED_POSITIONS:
return None
# 检查条件显示
if not self.should_display(position, context, **kwargs):
return None
# 调用具体的位置渲染方法
method_name = f'render_{position}_widget'
if hasattr(self, method_name):
html = getattr(self, method_name)(context, **kwargs)
if html:
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
return {
'html': html,
'priority': priority,
'plugin_name': self.PLUGIN_NAME
}
return None
def should_display(self, position, context, **kwargs):
"""
判断插件是否应该在指定位置显示
子类可重写此方法实现条件显示逻辑
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
bool: 是否显示
"""
return True
# === 各位置渲染方法 - 子类重写 ===
def render_sidebar_widget(self, context, **kwargs):
"""渲染侧边栏组件"""
return None
def render_article_bottom_widget(self, context, **kwargs):
"""渲染文章底部组件"""
return None
def render_article_top_widget(self, context, **kwargs):
"""渲染文章顶部组件"""
return None
def render_header_widget(self, context, **kwargs):
"""渲染页头组件"""
return None
def render_footer_widget(self, context, **kwargs):
"""渲染页脚组件"""
return None
def render_comment_before_widget(self, context, **kwargs):
"""渲染评论前组件"""
return None
def render_comment_after_widget(self, context, **kwargs):
"""渲染评论后组件"""
return None
# === 模板系统 ===
def render_template(self, template_name, context=None):
"""
渲染插件模板
Args:
template_name: 模板文件名
context: 模板上下文
Returns:
HTML字符串
"""
if context is None:
context = {}
template_path = f"plugins/{self.plugin_slug}/{template_name}"
try:
return render_to_string(template_path, context)
except TemplateDoesNotExist:
logger.warning(f"Plugin template not found: {template_path}")
return ""
# === 静态资源系统 ===
def get_static_url(self, static_file):
"""获取插件静态文件URL"""
from django.templatetags.static import static
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
def get_css_files(self):
"""获取插件CSS文件列表"""
return []
def get_js_files(self):
"""获取插件JavaScript文件列表"""
return []
def get_head_html(self, context=None):
"""获取需要插入到<head>中的HTML内容"""
return ""
def get_body_html(self, context=None):
"""获取需要插入到<body>底部的HTML内容"""
return ""
def get_plugin_info(self):
"""
获取插件信息
:return: 包含插件元数据的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION,
'author': self.PLUGIN_AUTHOR,
'slug': self.plugin_slug,
'directory': str(self.plugin_dir),
'supported_positions': self.SUPPORTED_POSITIONS,
'priorities': self.POSITION_PRIORITIES
}

@ -0,0 +1,22 @@
ARTICLE_DETAIL_LOAD = 'article_detail_load'
ARTICLE_CREATE = 'article_create'
ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"
# 位置钩子常量
POSITION_HOOKS = {
'article_top': 'article_top_widgets',
'article_bottom': 'article_bottom_widgets',
'sidebar': 'sidebar_widgets',
'header': 'header_widgets',
'footer': 'footer_widgets',
'comment_before': 'comment_before_widgets',
'comment_after': 'comment_after_widgets',
}
# 资源注入钩子
HEAD_RESOURCES_HOOK = 'head_resources'
BODY_RESOURCES_HOOK = 'body_resources'

@ -0,0 +1,44 @@
import logging
logger = logging.getLogger(__name__)
_hooks = {}
def register(hook_name: str, callback: callable):
"""
注册一个钩子回调
"""
if hook_name not in _hooks:
_hooks[hook_name] = []
_hooks[hook_name].append(callback)
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
执行一个 Action Hook
它会按顺序执行所有注册到该钩子上的回调函数
"""
if hook_name in _hooks:
logger.debug(f"Running action hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
callback(*args, **kwargs)
except Exception as e:
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
"""
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
value = callback(value, *args, **kwargs)
except Exception as e:
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
return value

@ -0,0 +1,64 @@
import os
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
# 全局插件注册表
_loaded_plugins = []
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
global _loaded_plugins
_loaded_plugins = []
for plugin_name in settings.ACTIVE_PLUGINS:
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# 导入插件模块
plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
# 获取插件实例
if hasattr(plugin_module, 'plugin'):
plugin_instance = plugin_module.plugin
_loaded_plugins.append(plugin_instance)
logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
else:
logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
except AttributeError as e:
logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
except Exception as e:
logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
def get_loaded_plugins():
"""获取所有已加载的插件"""
return _loaded_plugins
def get_plugin_by_name(plugin_name):
"""根据名称获取插件"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_name:
return plugin
return None
def get_plugin_by_slug(plugin_slug):
"""根据slug获取插件"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_slug:
return plugin
return None
def get_plugins_info():
"""获取所有插件的信息"""
return [plugin.get_plugin_info() for plugin in _loaded_plugins]
def get_plugins_by_position(position):
"""获取支持指定位置的插件"""
return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]

@ -0,0 +1,403 @@
"""
Django settings for djangoblog project.
Generated by 'django-admin startproject' using Django 1.10.2.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import sys
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
_ENV_SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
# Default to DEBUG=True only when no DJANGO_SECRET_KEY is provided, so local development works out of the box.
DEBUG = env_to_bool('DJANGO_DEBUG', _ENV_SECRET_KEY is None)
def get_secret_key():
if _ENV_SECRET_KEY:
return _ENV_SECRET_KEY
if DEBUG:
# Provide a deterministic key for convenience in local development.
return 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
raise ImproperlyConfigured('DJANGO_SECRET_KEY environment variable is required when DEBUG=False')
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_secret_key()
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# Allow configuring host whitelist via environment, fallback to safe defaults
_default_hosts = '127.0.0.1,localhost'
ALLOWED_HOSTS = [host.strip() for host in os.environ.get('DJANGO_ALLOWED_HOSTS', _default_hosts).split(',') if host.strip()]
# django 4.0新增配置
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.sitemaps',
'mdeditor',
'haystack',
'blog',
'accounts',
'comments',
'oauth',
'servermanager',
'owntracks',
'compressor',
'djangoblog'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.gzip.GZipMiddleware',
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'blog.middleware.OnlineMiddleware'
]
ROOT_URLCONF = 'djangoblog.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor'
],
},
},
]
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '12345678',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
},
'CONN_MAX_AGE': int(os.environ.get('DJANGO_DB_CONN_MAX_AGE', 60)),
}}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
# Automatically update searching index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
# szy同时收集项目静态目录与插件资源避免部署缺文件
STATICFILES_DIRS = [
STATICFILES,
os.path.join(BASE_DIR, 'plugins'),
]
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# paginate
PAGINATE_BY = 10
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake',
}
}
# 使用redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
SITE_ID = 1
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER
# Setting debug=false did NOT handle except email notifications
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# WX ADMIN password(Two times md5)
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'root': {
'level': 'INFO',
'handlers': ['console', 'log_file'],
},
'formatters': {
'verbose': {
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'log_file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D',
'formatter': 'verbose',
'interval': 1,
'delay': True,
'backupCount': 5,
'encoding': 'utf-8'
},
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': {
'class': 'logging.NullHandler',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
}
}
}
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# 根据环境变量决定是否启用离线压缩
COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true'
# 压缩输出目录
COMPRESS_OUTPUT_DIR = 'compressed'
# 压缩文件名模板 - 包含哈希值用于缓存破坏
COMPRESS_CSS_HASHING_METHOD = 'mtime'
COMPRESS_JS_HASHING_METHOD = 'mtime'
# 高级CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
# 创建绝对URL
'compressor.filters.css_default.CssAbsoluteFilter',
# CSS压缩器 - 高压缩等级
'compressor.filters.cssmin.CSSCompressorFilter',
]
# 高级JS压缩过滤器
COMPRESS_JS_FILTERS = [
# JS压缩器 - 高压缩等级
'compressor.filters.jsmin.SlimItFilter',
]
# 压缩缓存配置
COMPRESS_CACHE_BACKEND = 'default'
COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey'
# 预压缩配置
COMPRESS_PRECOMPILERS = (
# 支持SCSS/SASS
('text/x-scss', 'django_libsass.SassCompiler'),
('text/x-sass', 'django_libsass.SassCompiler'),
)
# 压缩性能优化
COMPRESS_MINT_DELAY = 30 # 压缩延迟(秒)
COMPRESS_MTIME_DELAY = 10 # 修改时间检查延迟
COMPRESS_REBUILD_TIMEOUT = 2592000 # 重建超时30天
# 压缩等级配置
COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor'
COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor'
# 静态文件缓存配置
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# 浏览器缓存配置(通过中间件或服务器配置)
COMPRESS_URL = STATIC_URL
COMPRESS_ROOT = STATIC_ROOT
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
AVATAR_ROOT = os.path.join(MEDIA_ROOT, 'avatars')
AVATAR_URL = f'{MEDIA_URL}avatars/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright',
'reading_time',
'external_links',
'view_count',
'seo_optimizer',
'image_lazy_loading',
'article_recommendation',
]

@ -0,0 +1,59 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
priority = 0.5
changefreq = 'daily'
def items(self):
return ['blog:index', ]
def location(self, item):
return reverse(item)
class ArticleSiteMap(Sitemap):
changefreq = "monthly"
priority = "0.6"
def items(self):
return Article.objects.filter(status='p')
def lastmod(self, obj):
return obj.last_modify_time
class CategorySiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.6"
def items(self):
return Category.objects.all()
def lastmod(self, obj):
return obj.last_modify_time
class TagSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
def items(self):
return Tag.objects.all()
def lastmod(self, obj):
return obj.last_modify_time
class UserSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
def items(self):
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
return obj.date_joined

@ -0,0 +1,21 @@
import logging
import requests
from django.conf import settings
logger = logging.getLogger(__name__)
class SpiderNotify():
@staticmethod
def baidu_notify(urls):
try:
data = '\n'.join(urls)
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
logger.info(result.text)
except Exception as e:
logger.error(e)
@staticmethod
def notify(url):
SpiderNotify.baidu_notify(url)

@ -0,0 +1,32 @@
from django.test import TestCase
from djangoblog.utils import *
class DjangoBlogTest(TestCase):
def setUp(self):
pass
def test_utils(self):
md5 = get_sha256('test')
self.assertIsNotNone(md5)
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c)
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)

@ -0,0 +1,79 @@
"""djangoblog URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from django.http import JsonResponse
import time
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
sitemaps = {
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
}
# szy自定义错误页面保证异常提示保持博客风格
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handler403 = 'blog.views.permission_denied_view'
def health_check(request):
"""
健康检查接口
简单返回服务健康状态
"""
return JsonResponse({
'status': 'healthy',
'timestamp': time.time()
})
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
path('health/', health_check, name='health_check'),
]
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls),
re_path(r'', include('blog.urls', namespace='blog')),
re_path(r'mdeditor/', include('mdeditor.urls')),
re_path(r'', include('comments.urls', namespace='comment')),
re_path(r'', include('accounts.urls', namespace='account')),
re_path(r'', include('oauth.urls', namespace='oauth')),
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -0,0 +1,272 @@
#!/usr/bin/env python
# encoding: utf-8
import logging
import os
import random
import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
def wrapper(func):
def news(*args, **kwargs):
try:
view = args[0]
key = view.get_cache_key()
except:
key = None
if not key:
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
value = cache.get(key)
if value is not None:
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
if value is None:
cache.set(key, '__default_cache_value__', expiration)
else:
cache.set(key, value, expiration)
return value
return news
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key):
cache.delete(key)
return True
return False
@cache_decorator()
def get_current_site():
site = Site.objects.get_current()
return site
class CommonMarkdown:
@staticmethod
def _convert_markdown(value):
md = markdown.Markdown(
extensions=[
'extra',
'codehilite',
'toc',
'tables',
]
)
body = md.convert(value)
toc = md.toc
return body, toc
@staticmethod
def get_markdown_with_toc(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""生成随机数验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
setting.site_seo_description = '基于Django的博客系统'
setting.site_keywords = 'Django,Python'
setting.article_sub_length = 300
setting.sidebar_article_count = 10
setting.sidebar_comment_count = 5
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
return value
def save_user_avatar(url):
'''
保存用户头像
:param url:头像url
:return: 本地路径
'''
logger.info(url)
try:
basedir = settings.AVATAR_ROOT
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
os.makedirs(basedir, exist_ok=True)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename)
avatar_path = os.path.join(basedir, save_filename)
with open(avatar_path, 'wb+') as file:
file.write(rsp.content)
return f'{settings.AVATAR_URL}{save_filename}'
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png')
def delete_sidebar_cache():
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
logger.info('delete sidebar key:' + k)
cache.delete(k)
def delete_view_cache(prefix, keys):
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p', 'span', 'div']
# 安全的class值白名单 - 只允许代码高亮相关的class
ALLOWED_CLASSES = [
'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn',
'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2',
's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il'
]
def class_filter(tag, name, value):
"""自定义class属性过滤器"""
if name == 'class':
# 只允许预定义的安全class值
allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
return ' '.join(allowed_classes) if allowed_classes else False
return value
# 安全的属性白名单
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'],
'abbr': ['title'],
'acronym': ['title'],
'span': class_filter,
'div': class_filter,
'pre': class_filter,
'code': class_filter
}
# 安全的协议白名单 - 防止javascript:等危险协议
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
def sanitize_html(html):
"""
安全的HTML清理函数
使用bleach库进行白名单过滤防止XSS攻击
"""
return bleach.clean(
html,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS, # 限制允许的协议
strip=True, # 移除不允许的标签而不是转义
strip_comments=True # 移除HTML注释
)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,16 @@
"""
WSGI config for djangoblog project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
application = get_wsgi_application()
Loading…
Cancel
Save