注释源代码

master
施周易 4 months ago
parent 3bfc16a831
commit b8d07e1474

@ -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}),
re_path(r'^register/$',
#lht: 注册URL
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
#lht: 登出URL
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
#lht: 账户操作结果页面
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
#lht: 忘记密码页面
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
#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("错误的邮箱")

@ -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):
name = 'blog'
#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)
return value
#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):
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
#zf大洲名称
continent_name = Keyword()
#zf国家ISO代码
country_iso_code = Keyword()
#zf国家名称
country_name = Keyword()
#zf地理位置坐标
location = GeoPoint()
#zf定义UserAgentBrowser内部文档类用于存储浏览器信息
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
#zf浏览器家族
Family = Keyword()
#zf浏览器版本
Version = Keyword()
#zf定义UserAgentOS内部文档类继承自UserAgentBrowser用于存储操作系统信息
class UserAgentOS(UserAgentBrowser):
pass
#zf定义UserAgentDevice内部文档类用于存储设备信息
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
#zf设备家族
Family = Keyword()
#zf设备品牌
Brand = Keyword()
#zf设备型号
Model = Keyword()
#zf定义UserAgent内部文档类用于存储用户代理信息
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
#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):
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
#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:
name = 'performance'
#zf索引名称
name = 'performance'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
#zf分片数量
"number_of_shards": 1,
#zf副本数量
"number_of_replicas": 0
}
#zf定义文档元数据
class Meta:
doc_type = 'ElapsedTime'
#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={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
#zf昵称
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
#zfID
'id': Integer()
})
#zf分类信息
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
#zf分类名
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
#zfID
'id': Integer()
})
#zf标签信息
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
#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:
name = 'blog'
#zf索引名称
name = 'blog'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
#zf分片数量
"number_of_shards": 1,
#zf副本数量
"number_of_replicas": 0
}
#zf定义文档元数据
class Meta:
doc_type = 'Article'
#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={
'id': article.id},
body=article.body,
title=article.title,
#zf使用文章ID作为文档ID
'id': article.id},
#zf文章正文
body=article.body,
#zf文章标题
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
#zf作者昵称
'nickname': article.author.username,
#zf作者ID
'id': article.author.id},
category={
'name': article.category.name,
'id': article.category.id},
#zf分类名
'name': article.category.name,
#zf分类ID
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
#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):
ArticleDocument.init()
#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()
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
# 返回搜索结果
return datas

@ -1,42 +1,406 @@
import logging
import time
from ipware import get_client_ip
from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
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
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导入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,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):
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
#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 = (
('d', _('Draft')),
('p', _('Published')),
#zf草稿
('d', _('Draft')),
#zf已发布
('p', _('Published')),
)
#zf评论状态选项
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
#zf开启评论
('o', _('Open')),
#zf关闭评论
('c', _('Close')),
)
#zf文章类型选项
TYPE = (
('a', _('Article')),
('p', _('Page')),
#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:
ordering = ['-article_order', '-pub_time']
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
#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:
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
#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:
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
#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:
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
#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:
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
#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:
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
#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
cache.clear()
#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):
return self.get_model().objects.filter(status='p')
#zf:返回所有状态为已发布('p')的文章
return self.get_model().objects.filter(status='p')

@ -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")
call_command("build_search_words")
#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")

@ -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):
name = 'comments'
#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
fields = ['body']
#zr 指定表单包含的字段
fields = ['body']

@ -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 +1,2 @@
# szy:此文件用于将当前目录识别为一个Python包
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -1,3 +1,4 @@
# szy功能描述自定义Django后台管理站点并注册各个模型
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
@ -16,14 +17,16 @@ from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
# szy自定义Django后台管理站点并注册各个模型
class DjangoBlogAdminSite(AdminSite):
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
# szy初始化管理站点设置站点名称
def __init__(self, name='admin'):
super().__init__(name)
# szy检查用户权限是否为超级管理员
def has_permission(self, request):
return request.user.is_superuser
@ -37,7 +40,7 @@ class DjangoBlogAdminSite(AdminSite):
# ]
# return urls + my_urls
# szy注册各个模型到后台管理
admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin)

@ -1,9 +1,11 @@
from django.apps import AppConfig
# szyDjango应用配置类用于加载插件
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
# szy应用准备时加载插件
def ready(self):
super().ready()
# Import and load plugins here

@ -1,3 +1,4 @@
# szy定义Django信号并处理相关业务逻辑
import _thread
import logging
@ -22,7 +23,7 @@ oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
# szy处理发送邮件的信号
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
@ -50,7 +51,7 @@ def send_email_signal_handler(sender, **kwargs):
log.send_result = False
log.save()
# szy处理OAuth用户登录信号
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
id = kwargs['id']

@ -10,7 +10,7 @@ from blog.models import Article
logger = logging.getLogger(__name__)
# szy定义Elasticsearch后端处理索引和查询
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(
@ -21,38 +21,46 @@ class ElasticSearchBackend(BaseSearchBackend):
self.manager = ArticleDocumentManager()
self.include_spelling = True
# szy获取要索引的模型数据
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
# szy创建索引
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
# szy删除索引
def _delete(self, models):
for m in models:
m.delete()
return True
# szy重组索引
def _rebuild(self, models):
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
# szy更新索引
def update(self, index, iterable, commit=True):
models = self._get_models(iterable)
self.manager.update_docs(models)
# szy移除索引
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
# szy清空索引
def clear(self, models=None, commit=True):
self.remove(None)
# szy获取搜索建议词
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
@ -71,6 +79,7 @@ class ElasticSearchBackend(BaseSearchBackend):
return ' '.join(keywords)
# szy执行搜索并返回结果
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
@ -84,10 +93,13 @@ class ElasticSearchBackend(BaseSearchBackend):
else:
suggestion = query_string
# szy构建查询条件匹配标题或正文设置最小匹配度
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
# szy执行搜索查询过滤已发布的状态和文章类型
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
@ -97,6 +109,8 @@ class ElasticSearchBackend(BaseSearchBackend):
results = search.execute()
hits = results['hits'].total
raw_results = []
# szy处理搜索结果构建SearchResult对象
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
@ -112,6 +126,8 @@ class ElasticSearchBackend(BaseSearchBackend):
**additional_fields)
raw_results.append(result)
facets = {}
# szy设置拼写建议如果查询词与建议词不同则返回建议词
spelling_suggestion = None if query_string == suggestion else suggestion
return {
@ -121,7 +137,7 @@ class ElasticSearchBackend(BaseSearchBackend):
'spelling_suggestion': spelling_suggestion,
}
# szy定义Elasticsearch查询类
class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
@ -129,6 +145,7 @@ class ElasticSearchQuery(BaseSearchQuery):
else:
return force_str(date.strftime('%Y%m%d000000'))
# szy清理查询片段处理保留字和特殊字符
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
@ -154,30 +171,35 @@ class ElasticSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words)
# szy构建查询片段
def build_query_fragment(self, field, filter_type, value):
return value.query_string
# szy获取查询结果数量
def get_count(self):
results = self.get_results()
return len(results) if results else 0
# szy获取拼写建议
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
# szy构建查询参数
def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
# szy定义Elasticsearch模型搜索表单
class ElasticSearchModelSearchForm(ModelSearchForm):
# szy重写搜索方法支持建议搜索功能
def search(self):
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
return sqs
# szy定义Elasticsearch搜索引擎
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery

@ -6,35 +6,44 @@ from django.utils.feedgenerator import Rss201rev2Feed
from blog.models import Article
from djangoblog.utils import CommonMarkdown
# szy定义Django博客的RSS订阅类
class DjangoBlogFeed(Feed):
feed_type = Rss201rev2Feed
# szy设置Feed的描述、标题和链接
description = '大巧无工,重剑无锋.'
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
# szy获取作者名称博客主昵称
def author_name(self):
return get_user_model().objects.first().nickname
# szy获取作者链接地址
def author_link(self):
return get_user_model().objects.first().get_absolute_url()
# szy获取要包含在Feed中的文章项
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
# szy获取文章项的标题
def item_title(self, item):
return item.title
# szy获取文章项的描述使用Markdown渲染正文
def item_description(self, item):
return CommonMarkdown.get_markdown(item.body)
# szy设置Feed的版权信息
def feed_copyright(self):
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
# szy获取文章项的链接地址
def item_link(self, item):
return item.get_absolute_url()
# szy获取文章项的全局唯一标识符
def item_guid(self, item):
return

@ -7,21 +7,26 @@ from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
# szy定义日志条目管理类
class LogEntryAdmin(admin.ModelAdmin):
# szy设置列表页过滤器字段
list_filter = [
'content_type'
]
# szy设置搜索字段
search_fields = [
'object_repr',
'change_message'
]
# szy设置列表页可点击链接的字段
list_display_links = [
'action_time',
'get_change_message',
]
# szy设置列表页显示的字段
list_display = [
'action_time',
'user_link',
@ -30,18 +35,22 @@ class LogEntryAdmin(admin.ModelAdmin):
'get_change_message',
]
# szy禁用添加权限
def has_add_permission(self, request):
return False
# szy设置修改权限仅超级用户或有特定权限的用户可以查看
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'
# szy禁用删除权限
def has_delete_permission(self, request, obj=None):
return False
# szy创建对象链接如果是删除操作则不生成链接
def object_link(self, obj):
object_link = escape(obj.object_repr)
content_type = obj.content_type
@ -62,6 +71,7 @@ class LogEntryAdmin(admin.ModelAdmin):
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
# szy创建用户链接链接到用户编辑页面
def user_link(self, obj):
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user))
@ -80,10 +90,12 @@ class LogEntryAdmin(admin.ModelAdmin):
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
# szy优化查询集预取关联的内容类型数据
def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
# szy重写获取actions的方法移除删除选中项的action
def get_actions(self, request):
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:

@ -2,19 +2,21 @@ import logging
logger = logging.getLogger(__name__)
# szy基础插件类所有插件都应该继承此类
class BasePlugin:
# 插件元数据
# szy插件元数据定义
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
# szy插件初始化方法
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.init_plugin()
self.register_hooks()
# szy插件初始化逻辑子类可重写实现特定初始化
def init_plugin(self):
"""
插件初始化逻辑
@ -22,6 +24,7 @@ class BasePlugin:
"""
logger.info(f'{self.PLUGIN_NAME} initialized.')
# szy注册插件钩子子类可重写实现特定钩子注册
def register_hooks(self):
"""
注册插件钩子
@ -29,6 +32,7 @@ class BasePlugin:
"""
pass
# szy获取插件信息
def get_plugin_info(self):
"""
获取插件信息

@ -1,7 +1,10 @@
# szy文章详情加载事件钩子名称
ARTICLE_DETAIL_LOAD = 'article_detail_load'
# szy文章创建事件钩子名称
ARTICLE_CREATE = 'article_create'
# szy文章更新事件钩子名称
ARTICLE_UPDATE = 'article_update'
# szy文章删除事件钩子名称
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"
# szy文章内容处理钩子名称
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -1,7 +1,7 @@
import logging
logger = logging.getLogger(__name__)
# szy全局钩子字典存储所有注册的钩子
_hooks = {}
@ -34,11 +34,15 @@ def apply_filters(hook_name: str, value, *args, **kwargs):
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
"""
# szy检查钩子是否存在存在则依次应用所有过滤器
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
for callback in _hooks[hook_name]:
try:
# szy将当前值传递给回调函数进行处理并更新值
value = callback(value, *args, **kwargs)
except Exception as e:
# szy捕获并记录过滤器执行过程中的异常
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
# szy返回处理后的最终值
return value

@ -9,11 +9,16 @@ 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.
"""
# szy遍历settings中配置的激活插件列表
for plugin_name in settings.ACTIVE_PLUGINS:
# szy构建插件路径
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
# szy检查插件目录是否存在且包含plugin.py文件
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# szy动态导入插件模块
__import__(f'plugins.{plugin_name}.plugin')
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
# szy记录插件导入失败的错误信息
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -15,12 +15,12 @@ from pathlib import Path
from django.utils.translation import gettext_lazy as _
# szy将环境变量转换为布尔值
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# szy构建项目基础路径
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@ -41,7 +41,7 @@ ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
# szy已安装的应用列表
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
@ -64,6 +64,7 @@ INSTALLED_APPS = [
'djangoblog'
]
# szy中间件配置
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
@ -83,6 +84,7 @@ MIDDLEWARE = [
ROOT_URLCONF = 'djangoblog.urls'
# szy模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -106,6 +108,7 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application'
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
# szyMySQL数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
@ -119,6 +122,7 @@ DATABASES = {
'charset': 'utf8mb4'},
}}
# szy密码验证配置
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
@ -137,23 +141,26 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# szy国际化配置 - 支持的语言
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
# szy本地化文件路径
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
# szy默认语言代码
LANGUAGE_CODE = 'zh-hans'
# szy时区设置
TIME_ZONE = 'Asia/Shanghai'
# szy启用国际化
USE_I18N = True
# szy启用本地化
USE_L10N = True
# szy使用时区支持
USE_TZ = False
# Static files (CSS, JavaScript, Images)
@ -180,6 +187,7 @@ STATICFILES = os.path.join(BASE_DIR, 'static')
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
# szy时间和日期格式
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
@ -188,11 +196,13 @@ BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# szy分页设置
# paginate
PAGINATE_BY = 10
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
# szy缓存配置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
@ -208,11 +218,12 @@ if os.environ.get("DJANGO_REDIS_URL"):
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
# szy站点ID
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'
# szy邮件配置
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
@ -229,6 +240,7 @@ ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
# szy日志配置
LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
@ -300,6 +312,7 @@ STATICFILES_FINDERS = (
# other
'compressor.finders.CompressorFinder',
)
# szy启用压缩
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
@ -314,6 +327,7 @@ COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
]
# szy媒体文件配置
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
@ -332,6 +346,7 @@ if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
},
}
# szy插件系统配置
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [

@ -3,57 +3,72 @@ from django.urls import reverse
from blog.models import Article, Category, Tag
# szy静态视图站点地图类
class StaticViewSitemap(Sitemap):
priority = 0.5
changefreq = 'daily'
# szy定义包含在站点地图中的项
def items(self):
return ['blog:index', ]
# szy获取项的URL位置
def location(self, item):
return reverse(item)
# szy文章站点地图类
class ArticleSiteMap(Sitemap):
# szy设置文章更新频率和优先级
changefreq = "monthly"
priority = "0.6"
# szy获取所有已发布的文章
def items(self):
return Article.objects.filter(status='p')
# szy获取文章的最后修改时间
def lastmod(self, obj):
return obj.last_modify_time
# szy分类站点地图类
class CategorySiteMap(Sitemap):
# szy设置分类更新频率和优先级
changefreq = "Weekly"
priority = "0.6"
# szy获取所有分类
def items(self):
return Category.objects.all()
# szy获取分类的最后修改时间
def lastmod(self, obj):
return obj.last_modify_time
# szy标签站点地图类
class TagSiteMap(Sitemap):
# szy设置标签更新频率和优先级
changefreq = "Weekly"
priority = "0.3"
# szy获取所有标签
def items(self):
return Tag.objects.all()
# szy获取标签的最后修改时间
def lastmod(self, obj):
return obj.last_modify_time
# szy用户站点地图类
class UserSiteMap(Sitemap):
# szy设置用户更新频率和优先级
changefreq = "Weekly"
priority = "0.3"
# szy获取所有有文章的作者去重
def items(self):
return list(set(map(lambda x: x.author, Article.objects.all())))
# szy获取用户的注册时间
def lastmod(self, obj):
return obj.date_joined

@ -6,16 +6,21 @@ from django.conf import settings
logger = logging.getLogger(__name__)
# szy蜘蛛爬虫通知类用于向搜索引擎推送URL
class SpiderNotify():
@staticmethod
def baidu_notify(urls):
try:
# szy将URL列表转换为换行分隔的字符串
data = '\n'.join(urls)
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
# szy记录推送结果日志
logger.info(result.text)
except Exception as e:
# szy记录推送异常日志
logger.error(e)
# szy推送单个URL的便捷方法
@staticmethod
def notify(url):
SpiderNotify.baidu_notify(url)

@ -3,13 +3,18 @@ from django.test import TestCase
from djangoblog.utils import *
# szyDjangoBlog测试类用于测试工具函数
class DjangoBlogTest(TestCase):
# szy测试初始化设置
def setUp(self):
pass
# szy测试工具函数功能
def test_utils(self):
# szy测试SHA256加密函数
md5 = get_sha256('test')
self.assertIsNotNone(md5)
# szy测试Markdown解析函数
c = CommonMarkdown.get_markdown('''
# Title1
@ -24,6 +29,8 @@ class DjangoBlogTest(TestCase):
''')
self.assertIsNotNone(c)
# szy测试字典转URL参数字符串函数
d = {
'd': 'key1',
'd2': 'key2'

@ -27,6 +27,7 @@ from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# szy定义站点地图配置
sitemaps = {
'blog': ArticleSiteMap,
@ -36,29 +37,45 @@ sitemaps = {
'static': StaticViewSitemap
}
# szy定义错误处理视图
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
# szy基础URL模式配置
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
]
# szy国际化URL模式配置
urlpatterns += i18n_patterns(
# szy管理员后台URL
re_path(r'^admin/', admin_site.urls),
# szy博客应用URL
re_path(r'', include('blog.urls', namespace='blog')),
# szyMarkdown编辑器URL
re_path(r'mdeditor/', include('mdeditor.urls')),
# szy评论系统URL
re_path(r'', include('comments.urls', namespace='comment')),
# szy账户管理URL
re_path(r'', include('accounts.urls', namespace='account')),
# szyOAuth认证URL
re_path(r'', include('oauth.urls', namespace='oauth')),
# szy站点地图URL
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
# szyFeed订阅URL
re_path(r'^feed/$', DjangoBlogFeed()),
# szyRSS订阅URL
re_path(r'^rss/$', DjangoBlogFeed()),
# szy搜索功能URL使用ElasticSearch视图和表单
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
# szy服务器管理URL
re_path(r'', include('servermanager.urls', namespace='servermanager')),
# szy位置追踪URL
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# szy调试模式下添加媒体文件服务
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -19,18 +19,18 @@ from django.templatetags.static import static
logger = logging.getLogger(__name__)
# szy获取最大文章ID和评论ID
def get_max_articleid_commentid():
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
# szy计算字符串的SHA256哈希值
def get_sha256(str):
m = sha256(str.encode('utf-8'))
return m.hexdigest()
# szy缓存装饰器用于函数结果缓存
def cache_decorator(expiration=3 * 60):
def wrapper(func):
def news(*args, **kwargs):
@ -67,6 +67,7 @@ def cache_decorator(expiration=3 * 60):
return wrapper
# szy刷新视图缓存
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
@ -91,14 +92,15 @@ def expire_view_cache(path, servername, serverport, key_prefix=None):
return True
return False
# szy获取当前站点信息带缓存
@cache_decorator()
def get_current_site():
site = Site.objects.get_current()
return site
# szy通用Markdown处理类
class CommonMarkdown:
# szy转换Markdown文本为HTML
@staticmethod
def _convert_markdown(value):
md = markdown.Markdown(
@ -113,17 +115,21 @@ class CommonMarkdown:
toc = md.toc
return body, toc
# szy获取带目录的Markdown内容
@staticmethod
def get_markdown_with_toc(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
# szy获取Markdown内容
@staticmethod
def get_markdown(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body
# szy发送邮件函数
def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
@ -132,12 +138,13 @@ def send_email(emailto, title, content):
title=title,
content=content)
# szy生成6位随机数字验证码
def generate_code() -> str:
"""生成随机数验证码"""
return ''.join(random.sample(string.digits, 6))
# szy将字典转换为URL参数字符串
def parse_dict_to_url(dict):
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
@ -145,6 +152,7 @@ def parse_dict_to_url(dict):
return url
# szy获取博客设置
def get_blog_setting():
value = cache.get('get_blog_setting')
if value:
@ -173,6 +181,7 @@ def get_blog_setting():
return value
# szy保存用户头像到本地
def save_user_avatar(url):
'''
保存用户头像
@ -201,6 +210,7 @@ def save_user_avatar(url):
return static('blog/img/avatar.png')
# szy删除侧边栏缓存
def delete_sidebar_cache():
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
@ -209,12 +219,14 @@ def delete_sidebar_cache():
cache.delete(k)
# szy删除视图缓存
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)
# szy获取资源URL
def get_resource_url():
if settings.STATIC_URL:
return settings.STATIC_URL
@ -227,6 +239,6 @@ ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'l
'h2', 'p']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
# szy清洗HTML内容移除不安全的标签和属性
def sanitize_html(html):
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

@ -40,6 +40,7 @@ except ImportError:
raise MissingDependency(
"The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
# szy处理最低版本要求
# Handle minimum requirement.
if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
raise MissingDependency(
@ -47,12 +48,13 @@ if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
# Bubble up the correct error.
# szy日期时间正则表达式用于解析日期字符串
DATETIME_REGEX = re.compile(
'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(\.\d{3,6}Z?)?$')
LOCALS = threading.local()
LOCALS.RAM_STORE = None
# szy自定义Whoosh HTML格式化器用于保持跨后端一致的高亮结果
class WhooshHtmlFormatter(HtmlFormatter):
"""
This is a HtmlFormatter simpler than the whoosh.HtmlFormatter.
@ -62,6 +64,7 @@ class WhooshHtmlFormatter(HtmlFormatter):
template = '<%(tag)s>%(t)s</%(tag)s>'
# szyWhoosh搜索后端主类
class WhooshSearchBackend(BaseSearchBackend):
# Word reserved by Whoosh for special use.
RESERVED_WORDS = (
@ -79,6 +82,7 @@ class WhooshSearchBackend(BaseSearchBackend):
'[', ']', '^', '"', '~', '*', '?', ':', '.',
)
# szy初始化Whoosh后端
def __init__(self, connection_alias, **connection_options):
super(
WhooshSearchBackend,
@ -103,6 +107,7 @@ class WhooshSearchBackend(BaseSearchBackend):
self.log = logging.getLogger('haystack')
# szy设置Whoosh索引和配置
def setup(self):
"""
Defers loading until needed.
@ -110,6 +115,7 @@ class WhooshSearchBackend(BaseSearchBackend):
from haystack import connections
new_index = False
# szy确保索引目录存在如果不存在则创建
# Make sure the index is there.
if self.use_file_storage and not os.path.exists(self.path):
os.makedirs(self.path)
@ -120,6 +126,7 @@ class WhooshSearchBackend(BaseSearchBackend):
"The path to your Whoosh index '%s' is not writable for the current user/group." %
self.path)
# szy根据配置选择文件存储或内存存储
if self.use_file_storage:
self.storage = FileStorage(self.path)
else:
@ -134,6 +141,7 @@ class WhooshSearchBackend(BaseSearchBackend):
connections[self.connection_alias].get_unified_index().all_searchfields())
self.parser = QueryParser(self.content_field_name, schema=self.schema)
# szy创建或打开索引
if new_index is True:
self.index = self.storage.create_index(self.schema)
else:
@ -144,6 +152,7 @@ class WhooshSearchBackend(BaseSearchBackend):
self.setup_complete = True
# szy构建Whoosh schema定义字段类型
def build_schema(self, fields):
schema_fields = {
ID: WHOOSH_ID(stored=True, unique=True),
@ -199,6 +208,7 @@ class WhooshSearchBackend(BaseSearchBackend):
return (content_field_name, Schema(**schema_fields))
# szy更新索引
def update(self, index, iterable, commit=True):
if not self.setup_complete:
self.setup()
@ -206,6 +216,7 @@ class WhooshSearchBackend(BaseSearchBackend):
self.index = self.index.refresh()
writer = AsyncWriter(self.index)
# szy遍历对象并更新索引
for obj in iterable:
try:
doc = index.full_prepare(obj)
@ -244,6 +255,7 @@ class WhooshSearchBackend(BaseSearchBackend):
# otherwise.
writer.commit()
# szy从索引中移除对象
def remove(self, obj_or_string, commit=True):
if not self.setup_complete:
self.setup()
@ -266,6 +278,7 @@ class WhooshSearchBackend(BaseSearchBackend):
e,
exc_info=True)
# szy清空索引
def clear(self, models=None, commit=True):
if not self.setup_complete:
self.setup()
@ -303,6 +316,8 @@ class WhooshSearchBackend(BaseSearchBackend):
self.log.error(
"Failed to clear Whoosh index: %s", e, exc_info=True)
# szy删除整个索引
def delete_index(self):
# Per the Whoosh mailing list, if wiping out everything from the index,
# it's much more efficient to simply delete the index files.
@ -311,9 +326,11 @@ class WhooshSearchBackend(BaseSearchBackend):
elif not self.use_file_storage:
self.storage.clean()
# szy重新创建所有内容
# Recreate everything.
self.setup()
# szy优化索引
def optimize(self):
if not self.setup_complete:
self.setup()
@ -321,12 +338,14 @@ class WhooshSearchBackend(BaseSearchBackend):
self.index = self.index.refresh()
self.index.optimize()
# szy计算分页信息
def calculate_page(self, start_offset=0, end_offset=None):
# Prevent against Whoosh throwing an error. Requires an end_offset
# greater than 0.
if end_offset is not None and end_offset <= 0:
end_offset = 1
# szy确定页码
# Determine the page.
page_num = 0
@ -345,6 +364,8 @@ class WhooshSearchBackend(BaseSearchBackend):
page_num += 1
return page_num, page_length
# szy执行搜索查询
@log_query
def search(
self,
@ -388,6 +409,8 @@ class WhooshSearchBackend(BaseSearchBackend):
reverse = False
# szy处理排序
if sort_by is not None:
# Determine if we need to reverse the results and if Whoosh can
# handle what it's being asked to sort by. Reversing is an
@ -560,6 +583,8 @@ class WhooshSearchBackend(BaseSearchBackend):
'spelling_suggestion': spelling_suggestion,
}
# szy实现"更多类似此结果"功能
def more_like_this(
self,
model_instance,
@ -675,6 +700,8 @@ class WhooshSearchBackend(BaseSearchBackend):
return results
# szy处理原始搜索结果转换为Haystack格式
def _process_results(
self,
raw_page,
@ -767,6 +794,8 @@ class WhooshSearchBackend(BaseSearchBackend):
'spelling_suggestion': spelling_suggestion,
}
# szy创建拼写建议
def create_spelling_suggestion(self, query_string):
spelling_suggestion = None
reader = self.index.reader()
@ -819,6 +848,8 @@ class WhooshSearchBackend(BaseSearchBackend):
value = force_str(value)
return value
# szy将Whoosh值转换为Python原生值
def _to_python(self, value):
"""
Converts values from Whoosh to native Python values.
@ -870,6 +901,7 @@ class WhooshSearchBackend(BaseSearchBackend):
return value
# szyWhoosh搜索查询类
class WhooshSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
@ -877,6 +909,8 @@ class WhooshSearchQuery(BaseSearchQuery):
else:
return force_str(date.strftime('%Y%m%d000000'))
# szy清理查询片段处理保留字和特殊字符
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
@ -902,6 +936,8 @@ class WhooshSearchQuery(BaseSearchQuery):
return ' '.join(cleaned_words)
# szy构建查询片段
def build_query_fragment(self, field, filter_type, value):
from haystack import connections
query_frag = ''
@ -1039,6 +1075,7 @@ class WhooshSearchQuery(BaseSearchQuery):
# value = self.backend._from_python(value)
# szyWhoosh搜索引擎类
class WhooshEngine(BaseEngine):
backend = WhooshSearchBackend
query = WhooshSearchQuery

@ -11,6 +11,8 @@ import os
from django.core.wsgi import get_wsgi_application
# szy设置Django的默认设置模块环境变量
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
# szy获取WSGI应用实例用于服务器部署
application = get_wsgi_application()

@ -1,7 +1,9 @@
# 导入Django管理后台模块 #zqx: 引入Django的admin模块用于注册和管理模型
from django.contrib import admin
# Register your models here.
# 注册你的模型到管理后台(待实现) #zqx: 这是一个占位注释,提示需要注册模型到管理后台
# 定义OwnTrackLogs模型在Django管理后台中的配置类 #zqx: 创建OwnTrackLogsAdmin类继承自ModelAdmin用于配置OwnTrackLog模型在管理后台的行为
class OwnTrackLogsAdmin(admin.ModelAdmin):
# 目前为空,后续可以添加管理后台的自定义配置 #zqx: 当前类体为空,预留空间用于添加管理后台的自定义配置选项
pass

@ -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,32 @@
# 导入Django数据库模型模块 #zqx: 引入Django的models模块用于定义数据库模型
from django.db import models
# 从Django时区工具中导入now函数用于获取当前时间 #zqx: 从django.utils.timezone导入now函数用于设置默认时间值
from django.utils.timezone import now
# Create your models here. #zqx: Django模型定义的标准注释标记模型定义区域开始
# Create your models here.
# 定义OwnTrackLog数据模型继承自Django的Model基类 #zqx: 定义OwnTrackLog类继承自models.Model创建一个数据库模型
class OwnTrackLog(models.Model):
# 用户标识字段字符类型最大长度100不允许为空 #zqx: 定义tid字段类型为CharField最大长度100null=False表示不允许为空verbose_name设置字段显示名称
tid = models.CharField(max_length=100, null=False, verbose_name='用户')
# 纬度字段,浮点数类型 #zqx: 定义lat字段类型为FloatFieldverbose_name设置字段显示名称
lat = models.FloatField(verbose_name='纬度')
# 经度字段,浮点数类型 #zqx: 定义lon字段类型为FloatFieldverbose_name设置字段显示名称
lon = models.FloatField(verbose_name='经度')
# 创建时间字段,日期时间类型,默认值为当前时间 #zqx: 定义creation_time字段类型为DateTimeField第一个参数是字段名default设置默认值为now函数
creation_time = models.DateTimeField('创建时间', default=now)
# 定义对象的字符串表示方法返回用户的tid #zqx: 定义__str__方法返回对象的tid属性用于在管理后台等地方显示对象信息
def __str__(self):
return self.tid
# 定义模型的元数据选项 #zqx: 定义Meta内部类用于配置模型的元数据选项
class Meta:
# 设置查询结果的默认排序方式,按创建时间升序排列 #zqx: 设置ordering属性指定查询结果按creation_time字段升序排列
ordering = ['creation_time']
# 设置模型在管理后台显示的单数名称 #zqx: 设置verbose_name属性指定模型在管理后台的单数显示名称
verbose_name = "OwnTrackLogs"
# 设置模型在管理后台显示的复数名称,这里与单数名称相同 #zqx: 设置verbose_name_plural属性指定模型在管理后台的复数显示名称这里与单数名称相同
verbose_name_plural = verbose_name
# 设置获取最新记录时依据的字段 #zqx: 设置get_latest_by属性指定获取最新记录时使用的字段为creation_time
get_latest_by = 'creation_time'

@ -1,64 +1,83 @@
# 导入json模块用于处理JSON数据 #zqx: 引入json模块用于处理JSON格式数据的编码和解码
import json
# 从Django测试模块导入测试客户端、请求工厂和测试用例基类 #zqx: 从django.test导入Client(测试客户端)、RequestFactory(请求工厂)和TestCase(测试用例基类)
from django.test import Client, RequestFactory, TestCase
# 从accounts应用导入BlogUser模型 #zqx: 从accounts应用的models模块导入BlogUser用户模型
from accounts.models import BlogUser
# 从当前应用导入OwnTrackLog模型 #zqx: 从当前应用(.)的models模块导入OwnTrackLog模型
from .models import OwnTrackLog
# Create your tests here. #zqx: Django测试文件的标准注释标记测试代码区域开始
# Create your tests here.
# 定义OwnTrackLogTest测试类继承自Django的TestCase #zqx: 定义OwnTrackLogTest测试类继承Django的TestCase类用于测试OwnTrackLog相关功能
class OwnTrackLogTest(TestCase):
# 测试初始化方法,在每个测试方法执行前运行 #zqx: setUp方法在每个测试方法执行前自动调用用于初始化测试环境
def setUp(self):
# 创建测试客户端实例 #zqx: 创建Client实例用于模拟HTTP请求
self.client = Client()
# 创建请求工厂实例 #zqx: 创建RequestFactory实例用于创建测试请求对象
self.factory = RequestFactory()
# 测试owntracks功能的主要测试方法 #zqx: 定义test_own_track_log测试方法用于测试owntracks功能
def test_own_track_log(self):
# 创建包含完整位置信息的测试数据 #zqx: 创建包含tid、lat、lon字段的字典对象作为完整位置信息测试数据
o = {
'tid': 12,
'lat': 123.123,
'lon': 134.341
'tid': 12, #zqx: 用户ID字段值为12
'lat': 123.123, #zqx: 纬度字段值为123.123
'lon': 134.341 #zqx: 经度字段值为134.341
}
# 使用客户端发送POST请求将位置数据以JSON格式发送到/logtracks端点 #zqx: 使用client.post方法向/owntracks/logtracks路径发送POST请求数据为JSON格式
self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
length = len(OwnTrackLog.objects.all())
self.assertEqual(length, 1)
'/owntracks/logtracks', #zqx: 请求的目标URL路径
json.dumps(o), #zqx: 将字典o转换为JSON字符串
content_type='application/json') #zqx: 设置请求的内容类型为application/json
# 检查数据库中OwnTrackLog记录数量是否为1 #zqx: 查询OwnTrackLog模型的所有记录检查记录数量是否为1
length = len(OwnTrackLog.objects.all()) #zqx: 获取OwnTrackLog所有对象的数量
self.assertEqual(length, 1) #zqx: 断言记录数量等于1
# 创建不完整的位置数据(缺少经度) #zqx: 创建缺少lon字段的字典对象作为不完整位置信息测试数据
o = {
'tid': 12,
'lat': 123.123
'tid': 12, #zqx: 用户ID字段值为12
'lat': 123.123 #zqx: 纬度字段值为123.123
}
# 再次发送POST请求 #zqx: 使用client.post方法再次发送POST请求数据为不完整的JSON格式
self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
length = len(OwnTrackLog.objects.all())
self.assertEqual(length, 1)
'/owntracks/logtracks', #zqx: 请求的目标URL路径
json.dumps(o), #zqx: 将不完整的字典o转换为JSON字符串
content_type='application/json') #zqx: 设置请求的内容类型为application/json
# 检查数据库记录数量是否仍为1不完整数据应该不被保存 #zqx: 查询OwnTrackLog模型的所有记录检查记录数量是否仍为1
length = len(OwnTrackLog.objects.all()) #zqx: 获取OwnTrackLog所有对象的数量
self.assertEqual(length, 1) #zqx: 断言记录数量仍等于1验证不完整数据未被保存
rsp = self.client.get('/owntracks/show_maps')
self.assertEqual(rsp.status_code, 302)
# 测试未登录用户访问/show_maps端点应该返回302重定向 #zqx: 测试未登录用户访问/show_maps端点的行为
rsp = self.client.get('/owntracks/show_maps') #zqx: 使用client.get方法向/owntracks/show_maps路径发送GET请求
self.assertEqual(rsp.status_code, 302) #zqx: 断言响应状态码为302表示重定向
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
# 创建超级用户用于测试 #zqx: 使用create_superuser方法创建超级用户用于后续测试
user = BlogUser.objects.create_superuser( #zqx: 调用BlogUser模型的create_superuser方法
email="liangliangyy1@gmail.com", #zqx: 设置用户邮箱
username="liangliangyy1", #zqx: 设置用户名
password="liangliangyy1") #zqx: 设置用户密码
self.client.login(username='liangliangyy1', password='liangliangyy1')
s = OwnTrackLog()
s.tid = 12
s.lon = 123.234
s.lat = 34.234
s.save()
# 使用创建的用户登录 #zqx: 使用client.login方法以创建的用户身份登录
self.client.login(username='liangliangyy1', password='liangliangyy1') #zqx: 使用用户名和密码登录
# 手动创建并保存一个OwnTrackLog实例 #zqx: 手动创建OwnTrackLog对象并保存到数据库
s = OwnTrackLog() #zqx: 创建OwnTrackLog实例
s.tid = 12 #zqx: 设置tid属性为12
s.lon = 123.234 #zqx: 设置lon属性为123.234
s.lat = 34.234 #zqx: 设置lat属性为34.234
s.save() #zqx: 保存对象到数据库
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)
# 测试已登录用户访问各个端点都应该返回200成功状态码 #zqx: 测试已登录用户访问不同端点的响应状态
rsp = self.client.get('/owntracks/show_dates') #zqx: 向/owntracks/show_dates路径发送GET请求
self.assertEqual(rsp.status_code, 200) #zqx: 断言响应状态码为200表示请求成功
rsp = self.client.get('/owntracks/show_maps') #zqx: 向/owntracks/show_maps路径发送GET请求
self.assertEqual(rsp.status_code, 200) #zqx: 断言响应状态码为200表示请求成功
rsp = self.client.get('/owntracks/get_datas') #zqx: 向/owntracks/get_datas路径发送GET请求
self.assertEqual(rsp.status_code, 200) #zqx: 断言响应状态码为200表示请求成功
rsp = self.client.get('/owntracks/get_datas?date=2018-02-26') #zqx: 向带日期参数的/owntracks/get_datas路径发送GET请求
self.assertEqual(rsp.status_code, 200) #zqx: 断言响应状态码为200表示请求成功

@ -1,12 +1,22 @@
# 从Django URL模块导入path函数用于定义URL模式 #zqx: 从django.urls模块导入path函数用于定义URL路由模式
from django.urls import path
# 从当前应用导入视图模块 #zqx: 从当前目录(.)导入views模块包含处理请求的视图函数
from . import views
# 定义应用命名空间为"owntracks" #zqx: 设置app_name变量为"owntracks",定义该应用的命名空间
app_name = "owntracks"
# 定义URL模式列表 #zqx: 定义urlpatterns列表包含该应用的所有URL路由模式
urlpatterns = [
# 定义日志跟踪接口URL将请求路由到manage_owntrack_log视图函数 #zqx: 使用path函数定义URL模式将'owntracks/logtracks'路径映射到views.manage_owntrack_log函数命名为'logtracks'
path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'),
# 定义地图展示页面URL将请求路由到show_maps视图函数 #zqx: 使用path函数定义URL模式将'owntracks/show_maps'路径映射到views.show_maps函数命名为'show_maps'
path('owntracks/show_maps', views.show_maps, name='show_maps'),
# 定义数据获取接口URL将请求路由到get_datas视图函数 #zqx: 使用path函数定义URL模式将'owntracks/get_datas'路径映射到views.get_datas函数命名为'get_datas'
path('owntracks/get_datas', views.get_datas, name='get_datas'),
# 定义日期展示页面URL将请求路由到show_log_dates视图函数 #zqx: 使用path函数定义URL模式将'owntracks/show_dates'路径映射到views.show_log_dates函数命名为'show_dates'
path('owntracks/show_dates', views.show_log_dates, name='show_dates')
]

@ -1,127 +1,161 @@
# Create your views here.
import datetime
import itertools
import json
import logging
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.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
# Create your views here. #zqx: Django视图文件标准注释标记视图代码开始
# 导入所需的Python标准库和第三方库 #zqx: 导入项目需要的各种标准库和第三方库
import datetime #zqx: 导入datetime模块用于处理日期时间相关操作
import itertools #zqx: 导入itertools模块用于高效的循环迭代操作
import json #zqx: 导入json模块用于处理JSON数据格式
import logging #zqx: 导入logging模块用于记录日志信息
from datetime import timezone #zqx: 从datetime模块导入timezone用于处理时区相关操作
from itertools import groupby #zqx: 从itertools模块导入groupby用于对数据进行分组操作
import django #zqx: 导入django模块
import requests #zqx: 导入requests库用于发送HTTP请求
# 导入Django的装饰器、HTTP响应类和视图相关模块 #zqx: 导入Django框架的各种视图相关组件
from django.contrib.auth.decorators import login_required #zqx: 从django.contrib.auth.decorators导入login_required装饰器用于限制视图只能由登录用户访问
from django.http import HttpResponse #zqx: 从django.http导入HttpResponse用于返回HTTP响应
from django.http import JsonResponse #zqx: 从django.http导入JsonResponse用于返回JSON格式的HTTP响应
from django.shortcuts import render #zqx: 从django.shortcuts导入render函数用于渲染模板
from django.views.decorators.csrf import csrf_exempt #zqx: 从django.views.decorators导入csrf_exempt装饰器用于免除CSRF验证
# 导入当前应用的OwnTrackLog模型 #zqx: 从当前应用的models模块导入OwnTrackLog数据模型
from .models import OwnTrackLog
# 获取日志记录器实例 #zqx: 获取名为__name__的日志记录器实例
logger = logging.getLogger(__name__)
# 装饰器免除CSRF验证用于接收外部系统POST请求 #zqx: 使用@csrf_exempt装饰器免除该视图函数的CSRF验证允许外部系统POST请求
@csrf_exempt
def manage_owntrack_log(request):
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')
except Exception as e:
logger.error(e)
return HttpResponse('error')
def manage_owntrack_log(request): #zqx: 定义manage_owntrack_log视图函数接收request参数
try: #zqx: 开始异常处理块
# 解析请求体中的JSON数据 #zqx: 解析HTTP请求体中的JSON数据
s = json.loads(request.read().decode('utf-8')) #zqx: 读取请求体内容并解码为utf-8然后解析为JSON对象
tid = s['tid'] #zqx: 从JSON对象中获取tid字段值用户标识
lat = s['lat'] #zqx: 从JSON对象中获取lat字段值纬度
lon = s['lon'] #zqx: 从JSON对象中获取lon字段值经度
# 记录接收到的位置信息日志 #zqx: 记录接收到的位置信息到日志
logger.info( #zqx: 使用logger记录info级别的日志信息
'tid:{tid}.lat:{lat}.lon:{lon}'.format( #zqx: 格式化日志信息字符串
tid=tid, lat=lat, lon=lon)) #zqx: 填充格式化参数
# 验证必要字段是否存在 #zqx: 验证必需的字段是否存在且不为空
if tid and lat and lon: #zqx: 判断tid、lat、lon三个字段是否都存在且不为空
# 创建并保存位置记录 #zqx: 创建OwnTrackLog实例并保存位置记录
m = OwnTrackLog() #zqx: 创建OwnTrackLog模型实例
m.tid = tid #zqx: 设置实例的tid属性
m.lat = lat #zqx: 设置实例的lat属性
m.lon = lon #zqx: 设置实例的lon属性
m.save() #zqx: 保存实例到数据库
return HttpResponse('ok') #zqx: 返回'ok'字符串响应
else: #zqx: 如果必要字段不完整
return HttpResponse('data error') #zqx: 返回'data error'字符串响应
except Exception as e: #zqx: 捕获所有异常
# 记录错误日志并返回错误响应 #zqx: 记录错误日志并返回错误响应
logger.error(e) #zqx: 使用logger记录error级别的异常信息
return HttpResponse('error') #zqx: 返回'error'字符串响应
# 装饰器,要求用户登录才能访问 #zqx: 使用@login_required装饰器限制该视图只能由登录用户访问
@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)
context = {
'date': date
def show_maps(request): #zqx: 定义show_maps视图函数接收request参数
# 检查用户是否为超级用户 #zqx: 检查当前登录用户是否为超级用户
if request.user.is_superuser: #zqx: 判断请求用户是否为超级用户
# 设置默认日期为当前UTC日期 #zqx: 设置默认日期为当前UTC日期
defaultdate = str(datetime.datetime.now(timezone.utc).date()) #zqx: 获取当前UTC时间的日期部分并转换为字符串
# 从GET参数获取日期如果没有则使用默认日期 #zqx: 从请求GET参数中获取date参数如果没有则使用默认日期
date = request.GET.get('date', defaultdate) #zqx: 获取GET参数中的date值不存在时使用defaultdate
# 构造上下文数据 #zqx: 构造传递给模板的上下文数据
context = { #zqx: 定义context字典
'date': date #zqx: 将date变量添加到context字典中
}
return render(request, 'owntracks/show_maps.html', context)
else:
from django.http import HttpResponseForbidden
return HttpResponseForbidden()
# 渲染地图展示页面 #zqx: 渲染show_maps.html模板并返回响应
return render(request, 'owntracks/show_maps.html', context) #zqx: 使用render函数渲染模板并返回响应
else: #zqx: 如果用户不是超级用户
# 非超级用户返回403禁止访问 #zqx: 为非超级用户返回403禁止访问响应
from django.http import HttpResponseForbidden #zqx: 从django.http导入HttpResponseForbidden
return HttpResponseForbidden() #zqx: 返回403禁止访问响应
# 装饰器,要求用户登录才能访问 #zqx: 使用@login_required装饰器限制该视图只能由登录用户访问
@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
def show_log_dates(request): #zqx: 定义show_log_dates视图函数接收request参数
# 从数据库获取所有记录的创建时间 #zqx: 从数据库中查询OwnTrackLog模型的所有creation_time字段值
dates = OwnTrackLog.objects.values_list('creation_time', flat=True) #zqx: 使用values_list获取creation_time字段值flat=True返回扁平化结果
# 提取日期部分并去重排序 #zqx: 提取日期部分,去重并排序
results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates)))) #zqx: 使用map提取日期格式化字符串set去重sorted排序list转换为列表
# 构造上下文数据 #zqx: 构造传递给模板的上下文数据
context = { #zqx: 定义context字典
'results': results #zqx: 将results变量添加到context字典中
}
return render(request, 'owntracks/show_log_dates.html', context)
def convert_to_amap(locations):
convert_result = []
it = iter(locations)
item = list(itertools.islice(it, 30))
while item:
datas = ';'.join(
set(map(lambda x: str(x.lon) + ',' + str(x.lat), item)))
key = '8440a376dfc9743d8924bf0ad141f28e'
api = 'http://restapi.amap.com/v3/assistant/coordinate/convert'
query = {
'key': key,
'locations': datas,
'coordsys': 'gps'
# 渲染日期展示页面 #zqx: 渲染show_log_dates.html模板并返回响应
return render(request, 'owntracks/show_log_dates.html', context) #zqx: 使用render函数渲染模板并返回响应
# 将GPS坐标转换为高德地图坐标批量处理每次30个 #zqx: 定义convert_to_amap函数用于将GPS坐标批量转换为高德地图坐标每次处理30个点
def convert_to_amap(locations): #zqx: 定义convert_to_amap函数接收locations参数位置列表
convert_result = [] #zqx: 初始化转换结果列表
# 创建迭代器 #zqx: 创建locations列表的迭代器
it = iter(locations) #zqx: 使用iter函数创建locations的迭代器
# 每次取30个位置点进行处理 #zqx: 每次从迭代器中取出30个位置点进行处理
item = list(itertools.islice(it, 30)) #zqx: 使用itertools.islice从迭代器中取出前30个元素
while item: #zqx: 当item列表不为空时循环处理
# 将经纬度格式化为高德API需要的格式 #zqx: 将经纬度数据格式化为高德API所需的格式
datas = ';'.join( #zqx: 使用';'连接符连接所有坐标字符串
set(map(lambda x: str(x.lon) + ',' + str(x.lat), item))) #zqx: 使用map提取每个位置的经度和纬度并格式化set去重join连接
# 高德地图API配置 #zqx: 配置高德地图坐标转换API的参数
key = '8440a376dfc9743d8924bf0ad141f28e' #zqx: 设置高德地图API的key
api = 'http://restapi.amap.com/v3/assistant/coordinate/convert' #zqx: 设置高德地图API的URL
query = { #zqx: 定义API请求参数字典
'key': key, #zqx: API密钥参数
'locations': datas, #zqx: 需要转换的坐标数据
'coordsys': 'gps' #zqx: 源坐标系为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)
# 发送请求到高德API #zqx: 向高德地图API发送GET请求
rsp = requests.get(url=api, params=query) #zqx: 使用requests.get发送带参数的GET请求
result = json.loads(rsp.text) #zqx: 解析API响应的JSON数据
# 处理API响应结果 #zqx: 处理API返回的结果
if "locations" in result: #zqx: 判断响应结果中是否包含locations字段
convert_result.append(result['locations']) #zqx: 如果包含则将locations值添加到转换结果列表中
# 继续处理下一批数据 #zqx: 继续处理下一批30个数据
item = list(itertools.islice(it, 30)) #zqx: 从迭代器中继续取出下一批30个元素
# 返回转换后的坐标字符串 #zqx: 返回所有转换后的坐标字符串,用分号连接
return ";".join(convert_result) #zqx: 使用";"连接符连接所有转换结果并返回
# 装饰器,要求用户登录才能访问 #zqx: 使用@login_required装饰器限制该视图只能由登录用户访问
@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):
def get_datas(request): #zqx: 定义get_datas视图函数接收request参数
# 获取当前UTC时间并设置为当天0点 #zqx: 获取当前UTC时间并设置为当天的0点0分0秒
now = django.utils.timezone.now().replace(tzinfo=timezone.utc) #zqx: 获取当前时间并设置时区为UTC
querydate = django.utils.timezone.datetime( #zqx: 创建查询开始日期时间对象
now.year, now.month, now.day, 0, 0, 0) #zqx: 设置为当年当月当日的0时0分0秒
# 如果GET参数中有指定日期则使用指定日期 #zqx: 如果请求GET参数中包含date则使用指定日期
if request.GET.get('date', None): #zqx: 判断GET参数中是否存在date参数
date = list(map(lambda x: int(x), request.GET.get('date').split('-'))) #zqx: 将date参数按'-'分割并转换为整数列表
querydate = django.utils.timezone.datetime( #zqx: 根据指定日期创建查询开始日期时间对象
date[0], date[1], date[2], 0, 0, 0) #zqx: 使用指定年月日创建datetime对象
# 计算查询结束时间第二天0点 #zqx: 计算查询结束时间为查询开始时间的下一天0点
nextdate = querydate + datetime.timedelta(days=1) #zqx: 查询结束时间为开始时间加上1天
# 查询指定日期范围内的位置记录 #zqx: 查询creation_time在指定日期范围内的OwnTrackLog记录
models = OwnTrackLog.objects.filter( #zqx: 使用filter方法筛选记录
creation_time__range=(querydate, nextdate)) #zqx: 筛选creation_time在querydate到nextdate范围内的记录
result = list() #zqx: 初始化结果列表
# 如果查询到数据,则按用户分组处理 #zqx: 如果查询到数据则按用户进行分组处理
if models and len(models): #zqx: 判断models是否存在且不为空
for tid, item in groupby( #zqx: 使用groupby按tid分组遍历models
sorted(models, key=lambda k: k.tid), key=lambda k: k.tid): #zqx: 先按tid排序然后按tid分组
d = dict() #zqx: 创建字典对象存储用户轨迹数据
d["name"] = tid #zqx: 设置字典的name字段为用户标识tid
paths = list() #zqx: 初始化路径坐标列表
# 目前使用原始GPS坐标注释掉的代码是使用高德转换坐标的部分 #zqx: 当前使用原始GPS坐标注释掉的是高德坐标转换的代码
# locations = convert_to_amap( #zqx: 调用convert_to_amap函数转换坐标已注释
# sorted(item, key=lambda x: x.creation_time)) #zqx: 按创建时间排序后转换(已注释)
# for i in locations.split(';'): #zqx: 遍历转换后的坐标字符串(已注释)
# paths.append(i.split(',')) #zqx: 将坐标分割后添加到路径列表(已注释)
# 使用GPS原始经纬度按时间排序 #zqx: 使用原始GPS坐标按时间排序
for location in sorted(item, key=lambda x: x.creation_time): #zqx: 遍历分组后的记录并按创建时间排序
paths.append([str(location.lon), str(location.lat)]) #zqx: 将经度和纬度转换为字符串并添加到路径列表
d["path"] = paths #zqx: 设置字典的path字段为路径坐标列表
result.append(d) #zqx: 将用户轨迹数据字典添加到结果列表
# 返回JSON格式的轨迹数据 #zqx: 返回JSON格式的轨迹数据响应
return JsonResponse(result, safe=False) #zqx: 使用JsonResponse返回结果safe=False允许非字典对象
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)
return JsonResponse(result, safe=False)

Loading…
Cancel
Save