diff --git a/src/DjangoBlog/accounts/admin.py b/src/DjangoBlog/accounts/admin.py
index 32e483c..59e6124 100644
--- a/src/DjangoBlog/accounts/admin.py
+++ b/src/DjangoBlog/accounts/admin.py
@@ -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',)
diff --git a/src/DjangoBlog/accounts/apps.py b/src/DjangoBlog/accounts/apps.py
index 9b3fc5a..7750a08 100644
--- a/src/DjangoBlog/accounts/apps.py
+++ b/src/DjangoBlog/accounts/apps.py
@@ -2,4 +2,5 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
+ #lht:指定应用的名称,Django会根据这个名称找到对应的应用目录
name = 'accounts'
diff --git a/src/DjangoBlog/accounts/forms.py b/src/DjangoBlog/accounts/forms.py
index fce4137..4900b6d 100644
--- a/src/DjangoBlog/accounts/forms.py
+++ b/src/DjangoBlog/accounts/forms.py
@@ -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'),
)
diff --git a/src/DjangoBlog/accounts/migrations/0001_initial.py b/src/DjangoBlog/accounts/migrations/0001_initial.py
index d2fbcab..8bcfcba 100644
--- a/src/DjangoBlog/accounts/migrations/0001_initial.py
+++ b/src/DjangoBlog/accounts/migrations/0001_initial.py
@@ -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()),
],
diff --git a/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
index 1a9f509..8bcfcba 100644
--- a/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
+++ b/src/DjangoBlog/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
@@ -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()),
+ ],
),
]
diff --git a/src/DjangoBlog/accounts/models.py b/src/DjangoBlog/accounts/models.py
index 3baddbb..00f2896 100644
--- a/src/DjangoBlog/accounts/models.py
+++ b/src/DjangoBlog/accounts/models.py
@@ -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: 获取最新记录的字段
diff --git a/src/DjangoBlog/accounts/tests.py b/src/DjangoBlog/accounts/tests.py
index 6893411..9d8ea97 100644
--- a/src/DjangoBlog/accounts/tests.py
+++ b/src/DjangoBlog/accounts/tests.py
@@ -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)
-
diff --git a/src/DjangoBlog/accounts/urls.py b/src/DjangoBlog/accounts/urls.py
index 107a801..abd6127 100644
--- a/src/DjangoBlog/accounts/urls.py
+++ b/src/DjangoBlog/accounts/urls.py
@@ -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'),
- ]
+]
diff --git a/src/DjangoBlog/accounts/user_login_backend.py b/src/DjangoBlog/accounts/user_login_backend.py
index 73cdca1..3656921 100644
--- a/src/DjangoBlog/accounts/user_login_backend.py
+++ b/src/DjangoBlog/accounts/user_login_backend.py
@@ -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:
diff --git a/src/DjangoBlog/accounts/utils.py b/src/DjangoBlog/accounts/utils.py
index 4b94bdf..04fb6ba 100644
--- a/src/DjangoBlog/accounts/utils.py
+++ b/src/DjangoBlog/accounts/utils.py
@@ -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)
diff --git a/src/DjangoBlog/accounts/views.py b/src/DjangoBlog/accounts/views.py
index ae67aec..0b2bf5f 100644
--- a/src/DjangoBlog/accounts/views.py
+++ b/src/DjangoBlog/accounts/views.py
@@ -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 = """
请点击下面链接验证您的邮箱
@@ -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("错误的邮箱")
diff --git a/src/DjangoBlog/blog/admin.py b/src/DjangoBlog/blog/admin.py
index 46c3420..27ae7da 100644
--- a/src/DjangoBlog/blog/admin.py
+++ b/src/DjangoBlog/blog/admin.py
@@ -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'%s' % (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:注意:在你提供的代码中,模型注册部分被省略了,通常应该包含如下代码:
+#zf:admin.site.register(Article, ArticlelAdmin)
+#zf:admin.site.register(Tag, TagAdmin)
+#zf:admin.site.register(Category, CategoryAdmin)
+#zf:admin.site.register(Links, LinksAdmin)
+#zf:admin.site.register(SideBar, SideBarAdmin)
+#zf:admin.site.register(BlogSettings, BlogSettingsAdmin)
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/apps.py b/src/DjangoBlog/blog/apps.py
index 7930587..08c1cb6 100644
--- a/src/DjangoBlog/blog/apps.py
+++ b/src/DjangoBlog/blog/apps.py
@@ -1,5 +1,9 @@
+#zf:导入Django的应用配置基类
from django.apps import AppConfig
+#zf:定义博客应用的配置类,继承自AppConfig
class BlogConfig(AppConfig):
- name = 'blog'
+ #zf:设置应用的名称为'blog'
+ #zf:这个名称需要与Django项目中应用的目录名称保持一致
+ name = 'blog'
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/context_processors.py b/src/DjangoBlog/blog/context_processors.py
index 73e3088..1debf0f 100644
--- a/src/DjangoBlog/blog/context_processors.py
+++ b/src/DjangoBlog/blog/context_processors.py
@@ -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
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/documents.py b/src/DjangoBlog/blog/documents.py
index 0f1db7b..75c1b49 100644
--- a/src/DjangoBlog/blog/documents.py
+++ b/src/DjangoBlog/blog/documents.py
@@ -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)
-
+ #zf:URL地址
+ url = Keyword()
+ #zf:耗时(毫秒)
+ time_taken = Long()
+ #zf:记录时间
+ log_datetime = Date()
+ #zf:IP地址
+ 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'),
+ #zf:ID
+ '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'),
+ #zf:ID
+ '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'),
+ #zf:ID
+ '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()
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/forms.py b/src/DjangoBlog/blog/forms.py
index 715be76..850da68 100644
--- a/src/DjangoBlog/blog/forms.py
+++ b/src/DjangoBlog/blog/forms.py
@@ -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
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/middleware.py b/src/DjangoBlog/blog/middleware.py
index 94dd70c..6dbc086 100644
--- a/src/DjangoBlog/blog/middleware.py
+++ b/src/DjangoBlog/blog/middleware.py
@@ -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'', 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")
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/models.py b/src/DjangoBlog/blog/models.py
index 083788b..8b9baae 100644
--- a/src/DjangoBlog/blog/models.py
+++ b/src/DjangoBlog/blog/models.py
@@ -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
+ #zf:latest()方法使用的字段
+ 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)
+ #zf:slug字段
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)
+ #zf:slug字段
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()
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/search_indexes.py b/src/DjangoBlog/blog/search_indexes.py
index 7f1dfac..3b3b024 100644
--- a/src/DjangoBlog/blog/search_indexes.py
+++ b/src/DjangoBlog/blog/search_indexes.py
@@ -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')
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/tests.py b/src/DjangoBlog/blog/tests.py
index ee13505..5aef939 100644
--- a/src/DjangoBlog/blog/tests.py
+++ b/src/DjangoBlog/blog/tests.py
@@ -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")
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/urls.py b/src/DjangoBlog/blog/urls.py
index adf2703..763d82b 100644
--- a/src/DjangoBlog/blog/urls.py
+++ b/src/DjangoBlog/blog/urls.py
@@ -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//',
- views.IndexView.as_view(),
- name='index_page'),
- path(
- r'article////.html',
- views.ArticleDetailView.as_view(),
- name='detailbyid'),
- path(
- r'category/.html',
- views.CategoryDetailView.as_view(),
- name='category_detail'),
- path(
- r'category//.html',
- views.CategoryDetailView.as_view(),
- name='category_detail_page'),
- path(
- r'author/.html',
- views.AuthorDetailView.as_view(),
- name='author_detail'),
- path(
- r'author//.html',
- views.AuthorDetailView.as_view(),
- name='author_detail_page'),
- path(
- r'tag/.html',
- views.TagDetailView.as_view(),
- name='tag_detail'),
- path(
- r'tag//.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")
\ No newline at end of file
diff --git a/src/DjangoBlog/blog/views.py b/src/DjangoBlog/blog/views.py
index d5dc7ec..761cb6d 100644
--- a/src/DjangoBlog/blog/views.py
+++ b/src/DjangoBlog/blog/views.py
@@ -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")
\ No newline at end of file
diff --git a/src/DjangoBlog/comments/admin.py b/src/DjangoBlog/comments/admin.py
index a814f3f..1e2134a 100644
--- a/src/DjangoBlog/comments/admin.py
+++ b/src/DjangoBlog/comments/admin.py
@@ -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'%s' %
(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'%s' % (link, obj.article.title))
+ #zr 设置自定义字段的显示名称
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')
diff --git a/src/DjangoBlog/comments/apps.py b/src/DjangoBlog/comments/apps.py
index ff01b77..b473173 100644
--- a/src/DjangoBlog/comments/apps.py
+++ b/src/DjangoBlog/comments/apps.py
@@ -1,5 +1,6 @@
from django.apps import AppConfig
-
+#zr 评论应用配置类
class CommentsConfig(AppConfig):
- name = 'comments'
+ #zr 定义应用名称
+ name = 'comments'
\ No newline at end of file
diff --git a/src/DjangoBlog/comments/forms.py b/src/DjangoBlog/comments/forms.py
index e83737d..1a98d76 100644
--- a/src/DjangoBlog/comments/forms.py
+++ b/src/DjangoBlog/comments/forms.py
@@ -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']
\ No newline at end of file
diff --git a/src/DjangoBlog/comments/models.py b/src/DjangoBlog/comments/models.py
index 7c3bbc8..16292a6 100644
--- a/src/DjangoBlog/comments/models.py
+++ b/src/DjangoBlog/comments/models.py
@@ -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
diff --git a/src/DjangoBlog/comments/templatetags/comments_tags.py b/src/DjangoBlog/comments/templatetags/comments_tags.py
index fde02b4..4b222ac 100644
--- a/src/DjangoBlog/comments/templatetags/comments_tags.py
+++ b/src/DjangoBlog/comments/templatetags/comments_tags.py
@@ -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,
diff --git a/src/DjangoBlog/comments/tests.py b/src/DjangoBlog/comments/tests.py
index 2a7f55f..e3bdb0c 100644
--- a/src/DjangoBlog/comments/tests.py
+++ b/src/DjangoBlog/comments/tests.py
@@ -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)
diff --git a/src/DjangoBlog/comments/urls.py b/src/DjangoBlog/comments/urls.py
index 7df3fab..de67ba7 100644
--- a/src/DjangoBlog/comments/urls.py
+++ b/src/DjangoBlog/comments/urls.py
@@ -2,8 +2,11 @@ from django.urls import path
from . import views
+#zr 定义评论应用的命名空间
app_name = "comments"
+#zr 评论模块URL路由配置
urlpatterns = [
+ #zr 文章评论提交路由
path(
'article//postcomment',
views.CommentPostView.as_view(),
diff --git a/src/DjangoBlog/comments/utils.py b/src/DjangoBlog/comments/utils.py
index f01dba7..d30b8c9 100644
--- a/src/DjangoBlog/comments/utils.py
+++ b/src/DjangoBlog/comments/utils.py
@@ -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 = _("""Thank you very much for your comments on this site
You can visit %(article_title)s
to review your comments,
@@ -19,10 +25,14 @@ def send_comment_email(comment):
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 %(article_title)s
has
received a reply.
%(comment_body)s
@@ -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)
diff --git a/src/DjangoBlog/comments/views.py b/src/DjangoBlog/comments/views.py
index ad9b2b9..4afed72 100644
--- a/src/DjangoBlog/comments/views.py
+++ b/src/DjangoBlog/comments/views.py
@@ -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))
diff --git a/src/DjangoBlog/djangoblog/__init__.py b/src/DjangoBlog/djangoblog/__init__.py
index 1e205f4..4592301 100644
--- a/src/DjangoBlog/djangoblog/__init__.py
+++ b/src/DjangoBlog/djangoblog/__init__.py
@@ -1 +1,2 @@
+# szy:此文件用于将当前目录识别为一个Python包
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
diff --git a/src/DjangoBlog/djangoblog/admin_site.py b/src/DjangoBlog/djangoblog/admin_site.py
index f120405..7f1194e 100644
--- a/src/DjangoBlog/djangoblog/admin_site.py
+++ b/src/DjangoBlog/djangoblog/admin_site.py
@@ -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)
diff --git a/src/DjangoBlog/djangoblog/apps.py b/src/DjangoBlog/djangoblog/apps.py
index d29e318..469dbdd 100644
--- a/src/DjangoBlog/djangoblog/apps.py
+++ b/src/DjangoBlog/djangoblog/apps.py
@@ -1,9 +1,11 @@
from django.apps import AppConfig
+# szy:Django应用配置类,用于加载插件
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
+ # szy:应用准备时加载插件
def ready(self):
super().ready()
# Import and load plugins here
diff --git a/src/DjangoBlog/djangoblog/blog_signals.py b/src/DjangoBlog/djangoblog/blog_signals.py
index 393f441..fa381a9 100644
--- a/src/DjangoBlog/djangoblog/blog_signals.py
+++ b/src/DjangoBlog/djangoblog/blog_signals.py
@@ -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']
diff --git a/src/DjangoBlog/djangoblog/elasticsearch_backend.py b/src/DjangoBlog/djangoblog/elasticsearch_backend.py
index 4afe498..fbc92b4 100644
--- a/src/DjangoBlog/djangoblog/elasticsearch_backend.py
+++ b/src/DjangoBlog/djangoblog/elasticsearch_backend.py
@@ -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
diff --git a/src/DjangoBlog/djangoblog/feeds.py b/src/DjangoBlog/djangoblog/feeds.py
index 8c4e851..2675951 100644
--- a/src/DjangoBlog/djangoblog/feeds.py
+++ b/src/DjangoBlog/djangoblog/feeds.py
@@ -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
diff --git a/src/DjangoBlog/djangoblog/logentryadmin.py b/src/DjangoBlog/djangoblog/logentryadmin.py
index 2f6a535..3b14550 100644
--- a/src/DjangoBlog/djangoblog/logentryadmin.py
+++ b/src/DjangoBlog/djangoblog/logentryadmin.py
@@ -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:
diff --git a/src/DjangoBlog/djangoblog/plugin_manage/base_plugin.py b/src/DjangoBlog/djangoblog/plugin_manage/base_plugin.py
index 2b4be5c..63cbbe6 100644
--- a/src/DjangoBlog/djangoblog/plugin_manage/base_plugin.py
+++ b/src/DjangoBlog/djangoblog/plugin_manage/base_plugin.py
@@ -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):
"""
获取插件信息
diff --git a/src/DjangoBlog/djangoblog/plugin_manage/hook_constants.py b/src/DjangoBlog/djangoblog/plugin_manage/hook_constants.py
index 6685b7c..ccc9813 100644
--- a/src/DjangoBlog/djangoblog/plugin_manage/hook_constants.py
+++ b/src/DjangoBlog/djangoblog/plugin_manage/hook_constants.py
@@ -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"
\ No newline at end of file
diff --git a/src/DjangoBlog/djangoblog/plugin_manage/hooks.py b/src/DjangoBlog/djangoblog/plugin_manage/hooks.py
index d712540..49ab083 100644
--- a/src/DjangoBlog/djangoblog/plugin_manage/hooks.py
+++ b/src/DjangoBlog/djangoblog/plugin_manage/hooks.py
@@ -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
diff --git a/src/DjangoBlog/djangoblog/plugin_manage/loader.py b/src/DjangoBlog/djangoblog/plugin_manage/loader.py
index 12e824b..a732c75 100644
--- a/src/DjangoBlog/djangoblog/plugin_manage/loader.py
+++ b/src/DjangoBlog/djangoblog/plugin_manage/loader.py
@@ -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)
\ No newline at end of file
diff --git a/src/DjangoBlog/djangoblog/settings.py b/src/DjangoBlog/djangoblog/settings.py
index d076bb6..50f46b8 100644
--- a/src/DjangoBlog/djangoblog/settings.py
+++ b/src/DjangoBlog/djangoblog/settings.py
@@ -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
+# szy:MySQL数据库配置
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 = [
diff --git a/src/DjangoBlog/djangoblog/sitemap.py b/src/DjangoBlog/djangoblog/sitemap.py
index 8b7d446..bb2ed3b 100644
--- a/src/DjangoBlog/djangoblog/sitemap.py
+++ b/src/DjangoBlog/djangoblog/sitemap.py
@@ -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
diff --git a/src/DjangoBlog/djangoblog/spider_notify.py b/src/DjangoBlog/djangoblog/spider_notify.py
index 7b909e9..c2a8864 100644
--- a/src/DjangoBlog/djangoblog/spider_notify.py
+++ b/src/DjangoBlog/djangoblog/spider_notify.py
@@ -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)
diff --git a/src/DjangoBlog/djangoblog/tests.py b/src/DjangoBlog/djangoblog/tests.py
index 01237d9..9bb0876 100644
--- a/src/DjangoBlog/djangoblog/tests.py
+++ b/src/DjangoBlog/djangoblog/tests.py
@@ -3,13 +3,18 @@ from django.test import TestCase
from djangoblog.utils import *
+# szy:DjangoBlog测试类,用于测试工具函数
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'
diff --git a/src/DjangoBlog/djangoblog/urls.py b/src/DjangoBlog/djangoblog/urls.py
index 4aae58a..d414ff4 100644
--- a/src/DjangoBlog/djangoblog/urls.py
+++ b/src/DjangoBlog/djangoblog/urls.py
@@ -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')),
+ # szy:Markdown编辑器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')),
+ # szy:OAuth认证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'),
+ # szy:Feed订阅URL
re_path(r'^feed/$', DjangoBlogFeed()),
+ # szy:RSS订阅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)
diff --git a/src/DjangoBlog/djangoblog/utils.py b/src/DjangoBlog/djangoblog/utils.py
index 57f63dc..f2c9a3a 100644
--- a/src/DjangoBlog/djangoblog/utils.py
+++ b/src/DjangoBlog/djangoblog/utils.py
@@ -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)
diff --git a/src/DjangoBlog/djangoblog/whoosh_cn_backend.py b/src/DjangoBlog/djangoblog/whoosh_cn_backend.py
index 04e3f7f..88f1770 100644
--- a/src/DjangoBlog/djangoblog/whoosh_cn_backend.py
+++ b/src/DjangoBlog/djangoblog/whoosh_cn_backend.py
@@ -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\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\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>'
+# szy:Whoosh搜索后端主类
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
+# szy:Whoosh搜索查询类
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)
+# szy:Whoosh搜索引擎类
class WhooshEngine(BaseEngine):
backend = WhooshSearchBackend
query = WhooshSearchQuery
diff --git a/src/DjangoBlog/djangoblog/wsgi.py b/src/DjangoBlog/djangoblog/wsgi.py
index 2295efd..6795850 100644
--- a/src/DjangoBlog/djangoblog/wsgi.py
+++ b/src/DjangoBlog/djangoblog/wsgi.py
@@ -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()
diff --git a/src/DjangoBlog/owntracks/admin.py b/src/DjangoBlog/owntracks/admin.py
index 655b535..91e6673 100644
--- a/src/DjangoBlog/owntracks/admin.py
+++ b/src/DjangoBlog/owntracks/admin.py
@@ -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
diff --git a/src/DjangoBlog/owntracks/apps.py b/src/DjangoBlog/owntracks/apps.py
index 1bc5f12..ec8282d 100644
--- a/src/DjangoBlog/owntracks/apps.py
+++ b/src/DjangoBlog/owntracks/apps.py
@@ -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'
diff --git a/src/DjangoBlog/owntracks/migrations/0001_initial.py b/src/DjangoBlog/owntracks/migrations/0001_initial.py
index 9eee55c..6596a45 100644
--- a/src/DjangoBlog/owntracks/migrations/0001_initial.py
+++ b/src/DjangoBlog/owntracks/migrations/0001_initial.py
@@ -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',
},
),
]
+
+
diff --git a/src/DjangoBlog/owntracks/migrations/0002_alter_owntracklog_options_and_more.py b/src/DjangoBlog/owntracks/migrations/0002_alter_owntracklog_options_and_more.py
index b4f8dec..433840c 100644
--- a/src/DjangoBlog/owntracks/migrations/0002_alter_owntracklog_options_and_more.py
+++ b/src/DjangoBlog/owntracks/migrations/0002_alter_owntracklog_options_and_more.py
@@ -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
),
]
diff --git a/src/DjangoBlog/owntracks/models.py b/src/DjangoBlog/owntracks/models.py
index 760942c..05bfebb 100644
--- a/src/DjangoBlog/owntracks/models.py
+++ b/src/DjangoBlog/owntracks/models.py
@@ -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,最大长度100,null=False表示不允许为空,verbose_name设置字段显示名称
tid = models.CharField(max_length=100, null=False, verbose_name='用户')
+ # 纬度字段,浮点数类型 #zqx: 定义lat字段,类型为FloatField,verbose_name设置字段显示名称
lat = models.FloatField(verbose_name='纬度')
+ # 经度字段,浮点数类型 #zqx: 定义lon字段,类型为FloatField,verbose_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'
diff --git a/src/DjangoBlog/owntracks/owntracks b/src/DjangoBlog/owntracks/owntracks
new file mode 100644
index 0000000..e69de29
diff --git a/src/DjangoBlog/owntracks/tests.py b/src/DjangoBlog/owntracks/tests.py
index 3b4b9d8..b3d4a3a 100644
--- a/src/DjangoBlog/owntracks/tests.py
+++ b/src/DjangoBlog/owntracks/tests.py
@@ -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,表示请求成功
diff --git a/src/DjangoBlog/owntracks/urls.py b/src/DjangoBlog/owntracks/urls.py
index c19ada8..b36a3a1 100644
--- a/src/DjangoBlog/owntracks/urls.py
+++ b/src/DjangoBlog/owntracks/urls.py
@@ -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')
]
+
+
diff --git a/src/DjangoBlog/owntracks/views.py b/src/DjangoBlog/owntracks/views.py
index 4c72bdd..ade1103 100644
--- a/src/DjangoBlog/owntracks/views.py
+++ b/src/DjangoBlog/owntracks/views.py
@@ -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)