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' +# 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)