From ef493f43496bf1ae99bf4389327ac0ae80bb9ae1 Mon Sep 17 00:00:00 2001 From: jyn <3080239289@qq.com> Date: Fri, 7 Nov 2025 16:51:15 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/accounts/admin.py | 34 ++-- src/django-master/accounts/apps.py | 4 +- src/django-master/accounts/forms.py | 42 ++-- src/django-master/accounts/models.py | 34 ++-- src/django-master/accounts/tests.py | 136 ++++++------- src/django-master/accounts/urls.py | 48 ++--- .../accounts/user_login_backend.py | 22 +-- src/django-master/accounts/utils.py | 24 +-- src/django-master/accounts/views.py | 182 +++++++++--------- 9 files changed, 263 insertions(+), 263 deletions(-) diff --git a/src/django-master/accounts/admin.py b/src/django-master/accounts/admin.py index bc63321..bd5ed47 100644 --- a/src/django-master/accounts/admin.py +++ b/src/django-master/accounts/admin.py @@ -4,7 +4,7 @@ from django.contrib.auth.forms import UserChangeForm from django.contrib.auth.forms import UsernameField from django.utils.translation import gettext_lazy as _ -# 导入自定义用户模型 +# jyn:导入自定义用户模型 from .models import BlogUser @@ -13,15 +13,15 @@ class BlogUserCreationForm(forms.ModelForm): 自定义用户创建表单,用于在管理员界面添加新用户 继承自ModelForm,提供密码验证功能 """ - # 密码字段,使用PasswordInput小部件确保输入不可见 + # jyn:密码字段,使用PasswordInput小部件确保输入不可见 password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) - # 确认密码字段,用于验证两次输入的密码是否一致 + # jyn:确认密码字段,用于验证两次输入的密码是否一致 password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) class Meta: - # 指定关联的模型 + # jyn:指定关联的模型 model = BlogUser - # 表单中包含的字段,这里只显示邮箱 + # jyn:表单中包含的字段,这里只显示邮箱 fields = ('email',) def clean_password2(self): @@ -31,7 +31,7 @@ class BlogUserCreationForm(forms.ModelForm): """ password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") - # 检查密码是否存在且不一致 + # jyn:检查密码是否存在且不一致 if password1 and password2 and password1 != password2: raise forms.ValidationError(_("passwords do not match")) return password2 @@ -41,12 +41,12 @@ class BlogUserCreationForm(forms.ModelForm): 重写保存方法,确保密码以哈希形式存储 而不是明文存储 """ - # 先调用父类方法获取用户对象,但不立即保存到数据库 + # jyn:先调用父类方法获取用户对象,但不立即保存到数据库 user = super().save(commit=False) - # 使用set_password方法对密码进行哈希处理 + # jyn:使用set_password方法对密码进行哈希处理 user.set_password(self.cleaned_data["password1"]) if commit: - # 标记用户来源为管理员站点 + # jyn:标记用户来源为管理员站点 user.source = 'adminsite' user.save() return user @@ -58,11 +58,11 @@ class BlogUserChangeForm(UserChangeForm): 继承自Django内置的UserChangeForm """ class Meta: - # 指定关联的模型 + # jyn:指定关联的模型 model = BlogUser - # 显示所有字段 + # jyn:显示所有字段 fields = '__all__' - # 指定用户名字段的处理类 + # jyn:指定用户名字段的处理类 field_classes = {'username': UsernameField} def __init__(self, *args, **kwargs): @@ -75,11 +75,11 @@ class BlogUserAdmin(UserAdmin): 自定义用户管理员类,用于在Django管理后台配置用户模型的显示和操作 继承自Django内置的UserAdmin """ - # 指定编辑用户时使用的表单 + # jyn:指定编辑用户时使用的表单 form = BlogUserChangeForm - # 指定添加用户时使用的表单 + # jyn:指定添加用户时使用的表单 add_form = BlogUserCreationForm - # 列表页面显示的字段 + # jyn:列表页面显示的字段 list_display = ( 'id', 'nickname', @@ -89,7 +89,7 @@ class BlogUserAdmin(UserAdmin): 'date_joined', 'source' ) - # 列表页面中可点击跳转的字段 + # jyn:列表页面中可点击跳转的字段 list_display_links = ('id', 'username') - # 排序方式,按id降序排列(最新的用户在前) + # jyn:排序方式,按id降序排列(最新的用户在前) ordering = ('-id',) \ No newline at end of file diff --git a/src/django-master/accounts/apps.py b/src/django-master/accounts/apps.py index 34121a3..2a51746 100644 --- a/src/django-master/accounts/apps.py +++ b/src/django-master/accounts/apps.py @@ -8,6 +8,6 @@ class AccountsConfig(AppConfig): 每个Django应用都需要一个配置类,用于设置应用的各种属性和行为 通常放在应用目录下的apps.py文件中 """ - # 应用的名称,必须与应用目录名一致 - # 这个名称会被Django用来识别和管理应用 + # jyn:应用的名称,必须与应用目录名一致 + # jyn:这个名称会被Django用来识别和管理应用 name = 'accounts' \ No newline at end of file diff --git a/src/django-master/accounts/forms.py b/src/django-master/accounts/forms.py index 713edf5..0373b3c 100644 --- a/src/django-master/accounts/forms.py +++ b/src/django-master/accounts/forms.py @@ -4,8 +4,8 @@ from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.core.exceptions import ValidationError from django.forms import widgets from django.utils.translation import gettext_lazy as _ -from . import utils # 导入自定义工具模块,可能用于验证码验证等功能 -from .models import BlogUser # 导入自定义用户模型 +from . import utils # jyn:导入自定义工具模块,可能用于验证码验证等功能 +from .models import BlogUser # jyn:导入自定义用户模型 class LoginForm(AuthenticationForm): @@ -14,14 +14,14 @@ class LoginForm(AuthenticationForm): 用于处理用户登录验证,主要扩展了表单字段的样式 """ def __init__(self, *args, **kwargs): - # 调用父类构造方法初始化表单 + # jyn:调用父类构造方法初始化表单 super(LoginForm, self).__init__(*args, **kwargs) - # 自定义用户名输入框:添加占位符和CSS类 + # jyn:自定义用户名输入框:添加占位符和CSS类 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) - # 自定义密码输入框:添加占位符和CSS类 + # jyn:自定义密码输入框:添加占位符和CSS类 self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) @@ -32,10 +32,10 @@ class RegisterForm(UserCreationForm): 用于处理用户注册逻辑,包含用户名、邮箱和密码验证 """ def __init__(self, *args, **kwargs): - # 调用父类构造方法初始化表单 + # jyn:调用父类构造方法初始化表单 super(RegisterForm, self).__init__(*args, **kwargs) - # 自定义各字段的输入控件,添加样式和占位符 + # jyn:自定义各字段的输入控件,添加样式和占位符 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) self.fields['email'].widget = widgets.EmailInput( @@ -51,14 +51,14 @@ class RegisterForm(UserCreationForm): 表单验证机制中,以clean_为前缀的方法会自动被调用 """ email = self.cleaned_data['email'] - # 检查该邮箱是否已存在于数据库中 + # jyn:检查该邮箱是否已存在于数据库中 if get_user_model().objects.filter(email=email).exists(): - raise ValidationError(_("email already exists")) # 抛出验证错误 + raise ValidationError(_("email already exists")) #jyn: 抛出验证错误 return email class Meta: - model = get_user_model() # 使用项目配置的用户模型(可能是自定义的BlogUser) - fields = ("username", "email") # 注册表单中显示的字段 + model = get_user_model() # jyn:使用项目配置的用户模型(可能是自定义的BlogUser) + fields = ("username", "email") # jyn:注册表单中显示的字段 class ForgetPasswordForm(forms.Form): @@ -66,7 +66,7 @@ class ForgetPasswordForm(forms.Form): 忘记密码表单,用于用户重置密码的流程 包含新密码、确认密码、邮箱和验证码字段 """ - # 新密码字段 + # jyn:新密码字段 new_password1 = forms.CharField( label=_("New password"), widget=forms.PasswordInput( @@ -77,7 +77,7 @@ class ForgetPasswordForm(forms.Form): ), ) - # 确认新密码字段 + # jyn:确认新密码字段 new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -88,7 +88,7 @@ class ForgetPasswordForm(forms.Form): ), ) - # 邮箱字段(用于验证用户身份) + # jyn:邮箱字段(用于验证用户身份) email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -99,7 +99,7 @@ class ForgetPasswordForm(forms.Form): ), ) - # 验证码字段(用于身份验证) + # jyn:验证码字段(用于身份验证) code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -115,11 +115,11 @@ class ForgetPasswordForm(forms.Form): password1 = self.data.get("new_password1") password2 = self.data.get("new_password2") - # 检查两次密码是否一致 + # jyn:检查两次密码是否一致 if password1 and password2 and password1 != password2: raise ValidationError(_("passwords do not match")) - # 使用Django内置的密码验证器验证密码强度 + # jyn:使用Django内置的密码验证器验证密码强度 password_validation.validate_password(password2) return password2 @@ -127,22 +127,22 @@ class ForgetPasswordForm(forms.Form): def clean_email(self): """验证邮箱是否已注册""" user_email = self.cleaned_data.get("email") - # 检查该邮箱是否存在于系统中 + # jyn:检查该邮箱是否存在于系统中 if not BlogUser.objects.filter(email=user_email).exists(): - # 提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露) + # jyn:提示邮箱不存在(实际应用中可能需要模糊提示以避免信息泄露) raise ValidationError(_("email does not exist")) return user_email def clean_code(self): """验证验证码是否有效""" code = self.cleaned_data.get("code") - # 调用工具函数验证邮箱和验证码是否匹配 + # jyn:调用工具函数验证邮箱和验证码是否匹配 error = utils.verify( email=self.cleaned_data.get("email"), code=code, ) if error: - raise ValidationError(error) # 验证码无效时抛出错误 + raise ValidationError(error) # jyn:验证码无效时抛出错误 return code diff --git a/src/django-master/accounts/models.py b/src/django-master/accounts/models.py index c1279eb..4e35e93 100644 --- a/src/django-master/accounts/models.py +++ b/src/django-master/accounts/models.py @@ -3,28 +3,28 @@ from django.db import models from django.urls import reverse from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具函数 +from djangoblog.utils import get_current_site # jyn:导入获取当前站点信息的工具函数 # Create your models here. class BlogUser(AbstractUser): """ - 自定义用户模型,继承自Django内置的AbstractUser + jyn:自定义用户模型,继承自Django内置的AbstractUser 扩展了默认用户模型的字段,以满足博客系统的特定需求 """ - # 昵称字段,允许为空 + # jyn:昵称字段,允许为空 nickname = models.CharField(_('nick name'), max_length=100, blank=True) - # 账号创建时间,默认为当前时间 + # jyn:账号创建时间,默认为当前时间 creation_time = models.DateTimeField(_('creation time'), default=now) - # 最后修改时间,默认为当前时间(通常需要在保存时更新) + # jyn:最后修改时间,默认为当前时间(通常需要在保存时更新) last_modify_time = models.DateTimeField(_('last modify time'), default=now) - # 账号创建来源(如管理员添加、前台注册等),用于追踪用户注册渠道 + # jyn:账号创建来源(如管理员添加、前台注册等),用于追踪用户注册渠道 source = models.CharField(_('create source'), max_length=100, blank=True) def get_absolute_url(self): """ - 返回用户详情页的URL + jyn:返回用户详情页的URL Django推荐为模型定义此方法,用于获取对象的标准URL """ return reverse( @@ -33,23 +33,23 @@ class BlogUser(AbstractUser): ) def __str__(self): - """模型的字符串表示,这里返回用户的邮箱""" + """jyn:模型的字符串表示,这里返回用户的邮箱""" return self.email def get_full_url(self): - """获取用户详情页的完整URL(包含域名)""" - # 获取当前站点的域名 + """jyn:获取用户详情页的完整URL(包含域名)""" + # jyn:获取当前站点的域名 site = get_current_site().domain - # 拼接完整URL(协议+域名+路径) + # jyn:拼接完整URL(协议+域名+路径) url = "https://{site}{path}".format( site=site, - path=self.get_absolute_url() # 调用get_absolute_url获取相对路径 + path=self.get_absolute_url() # jyn:调用get_absolute_url获取相对路径 ) return url class Meta: - """模型的元数据配置""" - ordering = ['-id'] # 默认排序方式:按id降序(最新创建的用户在前) - verbose_name = _('user') # 模型的单数显示名称(支持国际化) - verbose_name_plural = verbose_name # 模型的复数显示名称(与单数相同) - get_latest_by = 'id' # 指定使用id字段获取最新对象(用于Model.objects.latest()) \ No newline at end of file + """jyn:模型的元数据配置""" + ordering = ['-id'] # jyn:默认排序方式:按id降序(最新创建的用户在前) + verbose_name = _('user') # jyn:模型的单数显示名称(支持国际化) + verbose_name_plural = verbose_name # jyn:模型的复数显示名称(与单数相同) + get_latest_by = 'id' # jyn:指定使用id字段获取最新对象(用于Model.objects.latest()) \ No newline at end of file diff --git a/src/django-master/accounts/tests.py b/src/django-master/accounts/tests.py index 70b2776..ac8263b 100644 --- a/src/django-master/accounts/tests.py +++ b/src/django-master/accounts/tests.py @@ -3,10 +3,10 @@ from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from accounts.models import BlogUser # 导入自定义用户模型 -from blog.models import Article, Category # 导入博客相关模型 -from djangoblog.utils import * # 导入项目工具函数 -from . import utils # 导入当前应用的工具函数 +from accounts.models import BlogUser # jyn:导入自定义用户模型 +from blog.models import Article, Category # jyn:导入博客相关模型 +from djangoblog.utils import * # jyn:导入项目工具函数 +from . import utils # jyn:导入当前应用的工具函数 # Create your tests here. @@ -21,68 +21,68 @@ class AccountTest(TestCase): 测试前的初始化方法,会在每个测试方法执行前运行 用于创建测试所需的基础数据 """ - self.client = Client() # 创建测试客户端,用于模拟用户请求 - self.factory = RequestFactory() # 创建请求工厂,用于构建请求对象 - # 创建一个测试用户 + self.client = Client() # jyn:创建测试客户端,用于模拟用户请求 + self.factory = RequestFactory() # jyn:创建请求工厂,用于构建请求对象 + # jyn:创建一个测试用户 self.blog_user = BlogUser.objects.create_user( username="test", email="admin@admin.com", password="12345678" ) - self.new_test = "xxx123--=" # 测试用的新密码 + self.new_test = "xxx123--=" #jyn: 测试用的新密码 def test_validate_account(self): """测试用户账号验证相关功能,包括登录和管理员权限""" - site = get_current_site().domain # 获取当前站点域名 + site = get_current_site().domain # jyn:获取当前站点域名 - # 创建一个超级用户 + # jyn:创建一个超级用户 user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="qwer!@#$ggg") - # 获取创建的用户 + # jyn:获取创建的用户 testuser = BlogUser.objects.get(username='liangliangyy1') - # 测试登录功能 + # jyn:测试登录功能 loginresult = self.client.login( username='liangliangyy1', password='qwer!@#$ggg') - self.assertEqual(loginresult, True) # 断言登录成功 + self.assertEqual(loginresult, True) #jyn: 断言登录成功 - # 测试访问管理员页面 + # jyn:测试访问管理员页面 response = self.client.get('/admin/') - self.assertEqual(response.status_code, 200) # 断言页面访问成功 + self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功 - # 创建测试分类 + # jyn:创建测试分类 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() category.save() - # 创建测试文章 + # jyn:创建测试文章 article = Article() article.title = "nicetitleaaa" article.body = "nicecontentaaa" article.author = user article.category = category - article.type = 'a' # 假设'a'表示文章类型 - article.status = 'p' # 假设'p'表示已发布 + article.type = 'a' # jyn:假设'a'表示文章类型 + article.status = 'p' # jyn:假设'p'表示已发布 article.save() - # 测试访问文章管理页面 + # jyn:测试访问文章管理页面 response = self.client.get(article.get_admin_url()) - self.assertEqual(response.status_code, 200) # 断言页面访问成功 + self.assertEqual(response.status_code, 200) # jyn:断言页面访问成功 def test_validate_register(self): """测试用户注册功能,包括注册流程、邮箱验证和权限控制""" - # 初始状态下,该邮箱应不存在 + # jyn:初始状态下,该邮箱应不存在 self.assertEquals( 0, len( BlogUser.objects.filter( email='user123@user.com'))) - # 模拟用户注册提交 + # jyn:模拟用户注册提交 response = self.client.post(reverse('account:register'), { 'username': 'user1233', 'email': 'user123@user.com', @@ -90,43 +90,43 @@ class AccountTest(TestCase): 'password2': 'password123!q@wE#R$T', }) - # 注册后,该邮箱应存在 + #jyn: 注册后,该邮箱应存在 self.assertEquals( 1, len( BlogUser.objects.filter( email='user123@user.com'))) - # 获取刚注册的用户 + #jyn: 获取刚注册的用户 user = BlogUser.objects.filter(email='user123@user.com')[0] - # 生成验证链接(模拟邮箱验证流程) + # jyn:生成验证链接(模拟邮箱验证流程) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) path = reverse('accounts:result') url = '{path}?type=validation&id={id}&sign={sign}'.format( path=path, id=user.id, sign=sign) - # 访问验证链接 + # jyn:访问验证链接 response = self.client.get(url) - self.assertEqual(response.status_code, 200) # 断言验证页面访问成功 + self.assertEqual(response.status_code, 200) # jyn:断言验证页面访问成功 - # 登录新注册用户 + # jyn:登录新注册用户 self.client.login(username='user1233', password='password123!q@wE#R$T') - # 提升用户权限 + # jyn:提升用户权限 user = BlogUser.objects.filter(email='user123@user.com')[0] user.is_superuser = True user.is_staff = True user.save() - # 清除缓存 + # jyn:清除缓存 delete_sidebar_cache() - # 创建测试分类 + # jyn:创建测试分类 category = Category() category.name = "categoryaaa" category.creation_time = timezone.now() category.last_modify_time = timezone.now() category.save() - # 创建测试文章 + # jyn:创建测试文章 article = Article() article.category = category article.title = "nicetitle333" @@ -136,94 +136,94 @@ class AccountTest(TestCase): article.status = 'p' article.save() - # 测试访问文章管理页面 + # jyn:测试访问文章管理页面 response = self.client.get(article.get_admin_url()) self.assertEqual(response.status_code, 200) - # 测试登出功能 + # jyn:测试登出功能 response = self.client.get(reverse('account:logout')) self.assertIn(response.status_code, [301, 302, 200]) # 登出通常是重定向 - # 登出后访问管理页面(应被拒绝或重定向) + # jyn:登出后访问管理页面(应被拒绝或重定向) response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) - # 使用错误密码登录 + # jyn:使用错误密码登录 response = self.client.post(reverse('account:login'), { 'username': 'user1233', 'password': 'password123' }) self.assertIn(response.status_code, [301, 302, 200]) - # 错误登录后访问管理页面 + # jyn:错误登录后访问管理页面 response = self.client.get(article.get_admin_url()) self.assertIn(response.status_code, [301, 302, 200]) def test_verify_email_code(self): """测试邮箱验证码验证功能""" to_email = "admin@admin.com" - code = generate_code() # 生成验证码 - utils.set_code(to_email, code) # 存储验证码 - utils.send_verify_email(to_email, code) # 发送验证邮件 + code = generate_code() # jyn:生成验证码 + utils.set_code(to_email, code) # jyn:存储验证码 + utils.send_verify_email(to_email, code) # jyn:发送验证邮件 - # 验证正确的邮箱和验证码 + # jyn:验证正确的邮箱和验证码 err = utils.verify("admin@admin.com", code) - self.assertEqual(err, None) # 应无错误 + self.assertEqual(err, None) # jyn:应无错误 - # 验证错误的邮箱和正确的验证码 + # jyn:验证错误的邮箱和正确的验证码 err = utils.verify("admin@123.com", code) - self.assertEqual(type(err), str) # 应返回错误信息 + self.assertEqual(type(err), str) # jyn:应返回错误信息 def test_forget_password_email_code_success(self): """测试发送密码重置验证码成功的情况""" resp = self.client.post( path=reverse("account:forget_password_code"), - data=dict(email="admin@admin.com") # 使用已存在的邮箱 + data=dict(email="admin@admin.com") # jyn:使用已存在的邮箱 ) - self.assertEqual(resp.status_code, 200) # 断言请求成功 - self.assertEqual(resp.content.decode("utf-8"), "ok") # 断言返回成功信息 + self.assertEqual(resp.status_code, 200) # jyn:断言请求成功 + self.assertEqual(resp.content.decode("utf-8"), "ok") # jyn:断言返回成功信息 def test_forget_password_email_code_fail(self): """测试发送密码重置验证码失败的情况""" - # 不提供邮箱 + # jyn:不提供邮箱 resp = self.client.post( path=reverse("account:forget_password_code"), data=dict() ) - self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误信息 + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # jyn:断言返回错误信息 - # 提供无效格式的邮箱 + # jyn:提供无效格式的邮箱 resp = self.client.post( path=reverse("account:forget_password_code"), data=dict(email="admin@com") ) - self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误信息 + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #jyn: 断言返回错误信息 def test_forget_password_email_success(self): """测试密码重置成功的情况""" - code = generate_code() # 生成验证码 - utils.set_code(self.blog_user.email, code) # 存储验证码 - # 准备重置密码的数据 + code = generate_code() # jyn:生成验证码 + utils.set_code(self.blog_user.email, code) #jyn: 存储验证码 + # jyn:准备重置密码的数据 data = dict( new_password1=self.new_test, new_password2=self.new_test, email=self.blog_user.email, code=code, ) - # 提交密码重置请求 + # jyn:提交密码重置请求 resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 302) # 成功重置后通常重定向 + self.assertEqual(resp.status_code, 302) # jyn:成功重置后通常重定向 - # 验证密码是否已更新 + # jyn:验证密码是否已更新 blog_user = BlogUser.objects.filter( email=self.blog_user.email, - ).first() # type: BlogUser - self.assertNotEqual(blog_user, None) # 断言用户存在 - # 断言密码修改成功 + ).first() # jyn:type: BlogUser + self.assertNotEqual(blog_user, None) # jyn:断言用户存在 + # jyn:断言密码修改成功 self.assertEqual(blog_user.check_password(data["new_password1"]), True) def test_forget_password_email_not_user(self): @@ -231,7 +231,7 @@ class AccountTest(TestCase): data = dict( new_password1=self.new_test, new_password2=self.new_test, - email="123@123.com", # 不存在的邮箱 + email="123@123.com", # jyn:不存在的邮箱 code="123456", ) resp = self.client.post( @@ -239,22 +239,22 @@ class AccountTest(TestCase): data=data ) - self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码 + self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码 def test_forget_password_email_code_error(self): """测试使用错误的验证码重置密码的情况""" - code = generate_code() # 生成正确验证码 - utils.set_code(self.blog_user.email, code) # 存储验证码 - # 使用错误的验证码 + code = generate_code() # jyn:生成正确验证码 + utils.set_code(self.blog_user.email, code) #jyn: 存储验证码 + # jyn:使用错误的验证码 data = dict( new_password1=self.new_test, new_password2=self.new_test, email=self.blog_user.email, - code="111111", # 错误的验证码 + code="111111", # jyn:错误的验证码 ) resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 200) # 应返回页面但不重置密码 \ No newline at end of file + self.assertEqual(resp.status_code, 200) # jyn:应返回页面但不重置密码 \ No newline at end of file diff --git a/src/django-master/accounts/urls.py b/src/django-master/accounts/urls.py index dcde6e9..36b9b7d 100644 --- a/src/django-master/accounts/urls.py +++ b/src/django-master/accounts/urls.py @@ -1,49 +1,49 @@ from django.urls import path -from django.urls import re_path # 用于正则表达式匹配URL +from django.urls import re_path # jyn:用于正则表达式匹配URL -from . import views # 导入当前应用的视图函数/类 -from .forms import LoginForm # 导入自定义的登录表单 +from . import views # jyn:导入当前应用的视图函数/类 +from .forms import LoginForm # jyn:导入自定义的登录表单 -# 定义应用命名空间,避免URL名称冲突 +# jyn:定义应用命名空间,避免URL名称冲突 app_name = "accounts" -# URL路由配置列表,映射URL路径到对应的视图 +# jyn:URL路由配置列表,映射URL路径到对应的视图 urlpatterns = [ - # 登录页面路由:使用正则匹配以login/结尾的路径 + # jyn:登录页面路由:使用正则匹配以login/结尾的路径 re_path(r'^login/$', - # 调用LoginView类视图,指定登录成功后重定向到首页(/) + # jyn:调用LoginView类视图,指定登录成功后重定向到首页(/) views.LoginView.as_view(success_url='/'), - name='login', # URL的名称,用于反向解析 - # 向视图传递额外参数:指定登录表单为自定义的LoginForm + name='login', # jyn:URL的名称,用于反向解析 + # jyn:向视图传递额外参数:指定登录表单为自定义的LoginForm kwargs={'authentication_form': LoginForm}), - # 注册页面路由:匹配以register/结尾的路径 + # jyn:注册页面路由:匹配以register/结尾的路径 re_path(r'^register/$', - # 调用RegisterView类视图,注册成功后重定向到首页 + # jyn:调用RegisterView类视图,注册成功后重定向到首页 views.RegisterView.as_view(success_url="/"), - name='register'), # URL名称,用于反向解析 + name='register'), # jyn:URL名称,用于反向解析 - # 登出功能路由:匹配以logout/结尾的路径 + # jyn:登出功能路由:匹配以logout/结尾的路径 re_path(r'^logout/$', - # 调用LogoutView类视图(Django内置或自定义) + # jyn:调用LogoutView类视图(Django内置或自定义) views.LogoutView.as_view(), - name='logout'), # URL名称 + name='logout'), #jyn: URL名称 - # 账号操作结果页面路由:精确匹配account/result.html路径 + # jyn:账号操作结果页面路由:精确匹配account/result.html路径 path(r'account/result.html', - # 调用函数视图account_result + # jyn:调用函数视图account_result views.account_result, - name='result'), # URL名称,用于展示注册/验证等结果 + name='result'), #jyn: URL名称,用于展示注册/验证等结果 - # 忘记密码页面路由:匹配以forget_password/结尾的路径 + # jyn:忘记密码页面路由:匹配以forget_password/结尾的路径 re_path(r'^forget_password/$', - # 调用ForgetPasswordView类视图 + # jyn:调用ForgetPasswordView类视图 views.ForgetPasswordView.as_view(), - name='forget_password'), # URL名称 + name='forget_password'), #jyn: URL名称 - # 发送密码重置验证码页面路由:匹配以forget_password_code/结尾的路径 + # jyn:发送密码重置验证码页面路由:匹配以forget_password_code/结尾的路径 re_path(r'^forget_password_code/$', - # 调用ForgetPasswordEmailCode类视图(处理发送验证码逻辑) + # jyn:调用ForgetPasswordEmailCode类视图(处理发送验证码逻辑) views.ForgetPasswordEmailCode.as_view(), - name='forget_password_code'), # URL名称 + name='forget_password_code'), # jyn:URL名称 ] \ No newline at end of file diff --git a/src/django-master/accounts/user_login_backend.py b/src/django-master/accounts/user_login_backend.py index 1b2211c..47c2316 100644 --- a/src/django-master/accounts/user_login_backend.py +++ b/src/django-master/accounts/user_login_backend.py @@ -1,5 +1,5 @@ -from django.contrib.auth import get_user_model # 获取项目配置的用户模型(支持自定义模型) -from django.contrib.auth.backends import ModelBackend # 导入Django内置的模型认证后端 +from django.contrib.auth import get_user_model # jyn:获取项目配置的用户模型(支持自定义模型) +from django.contrib.auth.backends import ModelBackend # jyn:导入Django内置的模型认证后端 class EmailOrUsernameModelBackend(ModelBackend): @@ -17,22 +17,22 @@ class EmailOrUsernameModelBackend(ModelBackend): - password:前端传入的密码(明文) 返回:验证成功返回用户对象,失败返回None """ - # 判断输入的「username」是否包含@符号,以此区分邮箱和用户名 + # jyn:判断输入的「username」是否包含@符号,以此区分邮箱和用户名 if '@' in username: - # 若包含@,则按邮箱字段查询用户 + # jyn:若包含@,则按邮箱字段查询用户 kwargs = {'email': username} else: - # 若不包含@,则按用户名字段查询用户 + # jyn:若不包含@,则按用户名字段查询用户 kwargs = {'username': username} try: - # 根据上述条件从数据库查询唯一用户 + # jyn:根据上述条件从数据库查询唯一用户 user = get_user_model().objects.get(**kwargs) - # 验证密码:check_password会自动将明文密码与数据库中存储的哈希密码比对 + # jyn:验证密码:check_password会自动将明文密码与数据库中存储的哈希密码比对 if user.check_password(password): - return user # 密码正确,返回用户对象(认证成功) + return user # jyn:密码正确,返回用户对象(认证成功) except get_user_model().DoesNotExist: - # 若查询不到用户(用户名/邮箱不存在),返回None(认证失败) + # jyn:若查询不到用户(用户名/邮箱不存在),返回None(认证失败) return None def get_user(self, username): @@ -43,8 +43,8 @@ class EmailOrUsernameModelBackend(ModelBackend): 返回:存在则返回用户对象,不存在返回None """ try: - # 通过主键(ID)查询用户 + # jyn:通过主键(ID)查询用户 return get_user_model().objects.get(pk=username) except get_user_model().DoesNotExist: - # 若用户不存在,返回None + # jyn:若用户不存在,返回None return None \ No newline at end of file diff --git a/src/django-master/accounts/utils.py b/src/django-master/accounts/utils.py index 858b241..95eb6c3 100644 --- a/src/django-master/accounts/utils.py +++ b/src/django-master/accounts/utils.py @@ -1,13 +1,13 @@ import typing from datetime import timedelta -from django.core.cache import cache # 导入Django缓存模块,用于存储验证码 -from django.utils.translation import gettext # 用于获取即时翻译文本 -from django.utils.translation import gettext_lazy as _ # 用于延迟翻译文本(支持国际化) +from django.core.cache import cache # jyn:导入Django缓存模块,用于存储验证码 +from django.utils.translation import gettext # jyn:用于获取即时翻译文本 +from django.utils.translation import gettext_lazy as _ # jyn:用于延迟翻译文本(支持国际化) -from djangoblog.utils import send_email # 导入项目自定义的发送邮件工具函数 +from djangoblog.utils import send_email #jyn: 导入项目自定义的发送邮件工具函数 -# 验证码有效期:5分钟(全局变量,统一控制时效) +# jyn:验证码有效期:5分钟(全局变量,统一控制时效) _code_ttl = timedelta(minutes=5) @@ -19,11 +19,11 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")) code: 生成的随机验证码 subject: 邮件主题,默认值为“Verify Email”(支持国际化) """ - # 构造邮件HTML内容,包含验证码和有效期提示,使用%(code)s占位符注入验证码 + # jyn:构造邮件HTML内容,包含验证码和有效期提示,使用%(code)s占位符注入验证码 html_content = _( "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it " "properly") % {'code': code} - # 调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容 + # jyn:调用发送邮件函数,参数依次为:收件人列表、邮件主题、邮件内容 send_email([to_mail], subject, html_content) @@ -39,11 +39,11 @@ def verify(email: str, code: str) -> typing.Optional[str]: 代码注释中指出当前错误处理逻辑不合理:应使用raise抛出异常,而非返回错误字符串 若返回错误字符串,调用方需额外判断返回值是否为错误,增加了代码耦合度 """ - # 从缓存中获取该邮箱对应的验证码 + # jyn:从缓存中获取该邮箱对应的验证码 cache_code = get_code(email) - # 对比用户输入的验证码与缓存中的验证码 + # jyn:对比用户输入的验证码与缓存中的验证码 if cache_code != code: - # 验证码不匹配时,返回国际化的错误提示 + #jyn: 验证码不匹配时,返回国际化的错误提示 return gettext("Verification code error") @@ -54,7 +54,7 @@ def set_code(email: str, code: str): email: 作为缓存key的邮箱地址(确保一个邮箱对应一个验证码) code: 需要存入缓存的验证码 """ - # 调用Django缓存的set方法:key=邮箱,value=验证码,timeout=有效期(秒) + # jyn:调用Django缓存的set方法:key=邮箱,value=验证码,timeout=有效期(秒) cache.set(email, code, _code_ttl.seconds) @@ -66,5 +66,5 @@ def get_code(email: str) -> typing.Optional[str]: Return: 缓存中存在该邮箱对应的验证码时返回字符串,不存在时返回None """ - # 调用Django缓存的get方法,根据邮箱key获取验证码 + # jyn:调用Django缓存的get方法,根据邮箱key获取验证码 return cache.get(email) \ No newline at end of file diff --git a/src/django-master/accounts/views.py b/src/django-master/accounts/views.py index 810e352..4a2d837 100644 --- a/src/django-master/accounts/views.py +++ b/src/django-master/accounts/views.py @@ -2,45 +2,45 @@ import logging from django.utils.translation import gettext_lazy as _ from django.conf import settings from django.contrib import auth -from django.contrib.auth import REDIRECT_FIELD_NAME # 登录后重定向字段名常量 -from django.contrib.auth import get_user_model # 获取项目配置的用户模型 -from django.contrib.auth import logout # 登出功能函数 -from django.contrib.auth.forms import AuthenticationForm # Django内置登录表单 -from django.contrib.auth.hashers import make_password # 密码哈希处理函数 -from django.http import HttpResponseRedirect, HttpResponseForbidden # HTTP响应类 +from django.contrib.auth import REDIRECT_FIELD_NAME # jyn:登录后重定向字段名常量 +from django.contrib.auth import get_user_model # jyn:获取项目配置的用户模型 +from django.contrib.auth import logout # jyn:登出功能函数 +from django.contrib.auth.forms import AuthenticationForm # jyn:Django内置登录表单 +from django.contrib.auth.hashers import make_password # jyn:密码哈希处理函数 +from django.http import HttpResponseRedirect, HttpResponseForbidden # jyn:HTTP响应类 from django.http.request import HttpRequest from django.http.response import HttpResponse -from django.shortcuts import get_object_or_404, render # 快捷函数 -from django.urls import reverse # URL反向解析 -from django.utils.decorators import method_decorator # 类视图装饰器工具 -from django.utils.http import url_has_allowed_host_and_scheme # 验证重定向URL安全性 -from django.views import View # 基础视图类 -from django.views.decorators.cache import never_cache # 禁止缓存装饰器 -from django.views.decorators.csrf import csrf_protect # CSRF保护装饰器 -from django.views.decorators.debug import sensitive_post_parameters # 敏感参数保护装饰器 -from django.views.generic import FormView, RedirectView # 通用类视图 +from django.shortcuts import get_object_or_404, render # jyn:快捷函数 +from django.urls import reverse # jyn:URL反向解析 +from django.utils.decorators import method_decorator # jyn:类视图装饰器工具 +from django.utils.http import url_has_allowed_host_and_scheme # jyn:验证重定向URL安全性 +from django.views import View # jyn:基础视图类 +from django.views.decorators.cache import never_cache # jyn:禁止缓存装饰器 +from django.views.decorators.csrf import csrf_protect #jyn: CSRF保护装饰器 +from django.views.decorators.debug import sensitive_post_parameters # jyn:敏感参数保护装饰器 +from django.views.generic import FormView, RedirectView # jyn:通用类视图 from djangoblog.utils import (send_email, get_sha256, get_current_site, - generate_code, delete_sidebar_cache) # 项目工具函数 -from . import utils # 当前应用工具函数(验证码相关) + generate_code, delete_sidebar_cache) #jyn: 项目工具函数 +from . import utils # jyn:当前应用工具函数(验证码相关) from .forms import (RegisterForm, LoginForm, ForgetPasswordForm, - ForgetPasswordCodeForm) # 当前应用表单类 -from .models import BlogUser # 自定义用户模型 + ForgetPasswordCodeForm) # jyn:当前应用表单类 +from .models import BlogUser # jyn:自定义用户模型 -logger = logging.getLogger(__name__) # 初始化日志记录器 +logger = logging.getLogger(__name__) # jyn:初始化日志记录器 -# Create your views here. +# jyn:Create your views here. class RegisterView(FormView): """ 用户注册类视图,继承自FormView(处理表单提交的通用视图) 负责用户注册表单展示、数据验证、发送验证邮件及注册结果跳转 """ - form_class = RegisterForm # 指定使用的注册表单 - template_name = 'account/registration_form.html' # 注册页面模板路径 + form_class = RegisterForm # jyn:指定使用的注册表单 + template_name = 'account/registration_form.html' # jyn:注册页面模板路径 - @method_decorator(csrf_protect) # 为视图添加CSRF保护 + @method_decorator(csrf_protect) # jyn:为视图添加CSRF保护 def dispatch(self, *args, **kwargs): """重写分发方法,添加装饰器后调用父类逻辑""" return super(RegisterView, self).dispatch(*args, **kwargs) @@ -48,26 +48,26 @@ class RegisterView(FormView): def form_valid(self, form): """表单验证通过后执行的逻辑(注册核心流程)""" if form.is_valid(): - # 1. 暂存用户数据,不立即保存(is_active设为False,需邮箱验证后激活) + # jyn:1. 暂存用户数据,不立即保存(is_active设为False,需邮箱验证后激活) user = form.save(False) - user.is_active = False # 初始状态:未激活(需邮箱验证) - user.source = 'Register' # 标记注册来源为“前台注册” - user.save(True) # 保存用户到数据库 + user.is_active = False # jyn:初始状态:未激活(需邮箱验证) + user.source = 'Register' # jyn:标记注册来源为“前台注册” + user.save(True) # jyn:保存用户到数据库 - # 2. 生成邮箱验证链接(包含签名,防止篡改) - site = get_current_site().domain # 获取当前站点域名 - # 双重SHA256加密:用SECRET_KEY+用户ID生成签名,确保链接安全性 + # jyn:2. 生成邮箱验证链接(包含签名,防止篡改) + site = get_current_site().domain # jyn:获取当前站点域名 + # jyn:双重SHA256加密:用SECRET_KEY+用户ID生成签名,确保链接安全性 sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) - # 开发环境下替换域名(适配本地测试) + # jyn:开发环境下替换域名(适配本地测试) if settings.DEBUG: site = '127.0.0.1:8000' - # 反向解析结果页URL,拼接完整验证链接 + # jyn:反向解析结果页URL,拼接完整验证链接 path = reverse('account:result') url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( site=site, path=path, id=user.id, sign=sign) - # 3. 构造验证邮件内容并发送 + # jyn:3. 构造验证邮件内容并发送 content = """

请点击下面链接验证您的邮箱

@@ -79,16 +79,16 @@ class RegisterView(FormView): {url} """.format(url=url) send_email( - emailto=[user.email], # 收件人邮箱(新注册用户的邮箱) - title='验证您的电子邮箱', # 邮件标题 - content=content # 邮件HTML内容 + emailto=[user.email], # jyn:收件人邮箱(新注册用户的邮箱) + title='验证您的电子邮箱', #jyn: 邮件标题 + content=content #jyn: 邮件HTML内容 ) - # 4. 跳转到注册结果页(提示用户查收验证邮件) + # jyn:4. 跳转到注册结果页(提示用户查收验证邮件) url = reverse('accounts:result') + f'?type=register&id={str(user.id)}' return HttpResponseRedirect(url) else: - # 表单验证失败,重新渲染表单并显示错误 + # jyn:表单验证失败,重新渲染表单并显示错误 return self.render_to_response({'form': form}) @@ -97,18 +97,18 @@ class LogoutView(RedirectView): 用户登出类视图,继承自RedirectView(处理重定向的通用视图) 负责清除用户会话、缓存,并重定向到登录页 """ - url = '/login/' # 登出后默认重定向地址(登录页) + url = '/login/' # jyn:登出后默认重定向地址(登录页) - @method_decorator(never_cache) # 禁止缓存登出页面,避免浏览器缓存导致的问题 + @method_decorator(never_cache) # jyn:禁止缓存登出页面,避免浏览器缓存导致的问题 def dispatch(self, request, *args, **kwargs): """重写分发方法,添加装饰器后调用父类逻辑""" return super(LogoutView, self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): """处理GET请求(登出核心逻辑)""" - logout(request) # 清除用户会话,实现登出 - delete_sidebar_cache() # 删除侧边栏缓存(可能存储了用户相关信息) - return super(LogoutView, self).get(request, *args, **kwargs) # 执行重定向 + logout(request) # jyn:清除用户会话,实现登出 + delete_sidebar_cache() # jyn:删除侧边栏缓存(可能存储了用户相关信息) + return super(LogoutView, self).get(request, *args, **kwargs) # jyn:执行重定向 class LoginView(FormView): @@ -116,14 +116,14 @@ class LoginView(FormView): 用户登录类视图,继承自FormView 负责登录表单展示、数据验证、用户认证、记住登录状态及重定向 """ - form_class = LoginForm # 指定使用的自定义登录表单 - template_name = 'account/login.html' # 登录页面模板路径 - success_url = '/' # 登录成功默认重定向地址(首页) - redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名(默认next) - login_ttl = 2626560 # “记住登录”状态的有效期(秒),约等于1个月 - - # 为视图添加多重装饰器:敏感参数保护、CSRF保护、禁止缓存 - @method_decorator(sensitive_post_parameters('password')) # 保护密码参数,避免日志泄露 + form_class = LoginForm #jyn: 指定使用的自定义登录表单 + template_name = 'account/login.html' # jyn:登录页面模板路径 + success_url = '/' # jyn:登录成功默认重定向地址(首页) + redirect_field_name = REDIRECT_FIELD_NAME # jyn:重定向字段名(默认next) + login_ttl = 2626560 #jyn: “记住登录”状态的有效期(秒),约等于1个月 + + # jyn:为视图添加多重装饰器:敏感参数保护、CSRF保护、禁止缓存 + @method_decorator(sensitive_post_parameters('password')) # jyn:保护密码参数,避免日志泄露 @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): @@ -131,41 +131,41 @@ class LoginView(FormView): def get_context_data(self, **kwargs): """添加额外上下文数据(重定向地址)到模板""" - # 获取URL中的重定向参数(如登录前访问的受保护页面) + # jyn:获取URL中的重定向参数(如登录前访问的受保护页面) redirect_to = self.request.GET.get(self.redirect_field_name) if redirect_to is None: - redirect_to = '/' # 默认重定向到首页 - kwargs['redirect_to'] = redirect_to # 将重定向地址传入模板上下文 + redirect_to = '/' # jyn:默认重定向到首页 + kwargs['redirect_to'] = redirect_to # jyn:将重定向地址传入模板上下文 return super(LoginView, self).get_context_data(** kwargs) def form_valid(self, form): """表单验证通过后执行的逻辑(登录核心流程)""" - # 用Django内置AuthenticationForm重新验证(确保认证逻辑符合默认规范) + # jyn:用Django内置AuthenticationForm重新验证(确保认证逻辑符合默认规范) form = AuthenticationForm(data=self.request.POST, request=self.request) if form.is_valid(): - delete_sidebar_cache() # 删除侧边栏缓存(更新用户登录状态) - logger.info(self.redirect_field_name) # 日志记录重定向字段名 + delete_sidebar_cache() # jyn:删除侧边栏缓存(更新用户登录状态) + logger.info(self.redirect_field_name) # jyn:日志记录重定向字段名 - # 执行登录:将用户信息存入会话 + # jyn:执行登录:将用户信息存入会话 auth.login(self.request, form.get_user()) - # 处理“记住我”功能:若勾选,设置会话有效期为1个月 + # jyn:处理“记住我”功能:若勾选,设置会话有效期为1个月 if self.request.POST.get("remember"): self.request.session.set_expiry(self.login_ttl) - # 调用父类form_valid,执行重定向 + # jyn:调用父类form_valid,执行重定向 return super(LoginView, self).form_valid(form) else: - # 表单验证失败(如密码错误),重新渲染表单并显示错误 + # jyn:表单验证失败(如密码错误),重新渲染表单并显示错误 return self.render_to_response({'form': form}) def get_success_url(self): """自定义登录成功后的重定向地址(优先使用URL中的next参数)""" - # 获取POST请求中的重定向地址(用户登录前尝试访问的页面) + # jyn:获取POST请求中的重定向地址(用户登录前尝试访问的页面) redirect_to = self.request.POST.get(self.redirect_field_name) - # 验证重定向地址的安全性:避免跳转到外部恶意网站 + # jyn:验证重定向地址的安全性:避免跳转到外部恶意网站 if not url_has_allowed_host_and_scheme( url=redirect_to, allowed_hosts=[self.request.get_host()]): - redirect_to = self.success_url # 不安全则使用默认重定向地址 + redirect_to = self.success_url # jyn:不安全则使用默认重定向地址 return redirect_to @@ -174,48 +174,48 @@ def account_result(request): 账号操作结果视图函数(函数视图) 处理注册成功提示、邮箱验证逻辑,并展示结果页面 """ - # 从URL参数中获取操作类型(register/validation)和用户ID + # jyn:从URL参数中获取操作类型(register/validation)和用户ID type = request.GET.get('type') id = request.GET.get('id') - # 获取对应的用户,若不存在则返回404 + # jyn:获取对应的用户,若不存在则返回404 user = get_object_or_404(get_user_model(), id=id) logger.info(type) # 日志记录操作类型 - # 若用户已激活,直接重定向到首页(避免重复验证) + # jyn:若用户已激活,直接重定向到首页(避免重复验证) if user.is_active: return HttpResponseRedirect('/') - # 处理合法的操作类型(注册成功提示/邮箱验证) + # jyn:处理合法的操作类型(注册成功提示/邮箱验证) if type and type in ['register', 'validation']: if type == 'register': - # 注册成功:提示用户查收验证邮件 + # jyn:注册成功:提示用户查收验证邮件 content = ''' 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 ''' title = '注册成功' else: - # 邮箱验证:验证签名是否正确,正确则激活用户 - # 重新计算签名,与URL中的签名对比(防止链接篡改) + # jyn:邮箱验证:验证签名是否正确,正确则激活用户 + # jyn:重新计算签名,与URL中的签名对比(防止链接篡改) c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = request.GET.get('sign') if sign != c_sign: - return HttpResponseForbidden() # 签名不匹配,返回403禁止访问 - # 激活用户:将is_active设为True + return HttpResponseForbidden() # jyn:签名不匹配,返回403禁止访问 + # jyn:激活用户:将is_active设为True user.is_active = True user.save() - # 验证成功:提示用户可登录 + # jyn:验证成功:提示用户可登录 content = ''' 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 ''' title = '验证成功' - # 渲染结果页面,传递标题和内容 + # jyn:渲染结果页面,传递标题和内容 return render(request, 'account/result.html', { 'title': title, 'content': content }) else: - # 操作类型不合法,重定向到首页 + # jyn:操作类型不合法,重定向到首页 return HttpResponseRedirect('/') @@ -224,23 +224,23 @@ class ForgetPasswordView(FormView): 忘记密码类视图,继承自FormView 负责密码重置表单展示、数据验证,及更新用户密码 """ - form_class = ForgetPasswordForm # 指定使用的密码重置表单 - template_name = 'account/forget_password.html' # 密码重置页面模板路径 + form_class = ForgetPasswordForm # jyn:指定使用的密码重置表单 + template_name = 'account/forget_password.html' # jyn:密码重置页面模板路径 def form_valid(self, form): """表单验证通过后执行的逻辑(密码重置核心流程)""" if form.is_valid(): - # 1. 获取表单中的邮箱,查询对应的用户 + # jyn:1. 获取表单中的邮箱,查询对应的用户 blog_user = BlogUser.objects.filter( email=form.cleaned_data.get("email") ).get() - # 2. 对新密码进行哈希处理,并更新用户密码 + # jyn:2. 对新密码进行哈希处理,并更新用户密码 blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.save() - # 3. 密码重置成功,重定向到登录页 + # jyn:3. 密码重置成功,重定向到登录页 return HttpResponseRedirect('/login/') else: - # 表单验证失败(如验证码错误、密码不一致),重新渲染表单 + # jyn:表单验证失败(如验证码错误、密码不一致),重新渲染表单 return self.render_to_response({'form': form}) @@ -251,16 +251,16 @@ class ForgetPasswordEmailCode(View): """ def post(self, request: HttpRequest): """处理POST请求(发送验证码核心逻辑)""" - # 1. 验证邮箱表单数据 + #jyn: 1. 验证邮箱表单数据 form = ForgetPasswordCodeForm(request.POST) if not form.is_valid(): - return HttpResponse("错误的邮箱") # 邮箱格式不合法,返回错误提示 + return HttpResponse("错误的邮箱") # jyn:邮箱格式不合法,返回错误提示 - # 2. 生成验证码并发送邮件 - to_email = form.cleaned_data["email"] # 获取合法的邮箱地址 - code = generate_code() # 生成随机验证码 - utils.send_verify_email(to_email, code) # 发送验证码邮件 - utils.set_code(to_email, code) # 将验证码存入缓存(设置有效期) + # jyn:2. 生成验证码并发送邮件 + to_email = form.cleaned_data["email"] # jyn:获取合法的邮箱地址 + code = generate_code() # jyn:生成随机验证码 + utils.send_verify_email(to_email, code) # jyn:发送验证码邮件 + utils.set_code(to_email, code) # jyn:将验证码存入缓存(设置有效期) - # 3. 操作成功,返回“ok”提示 + # jyn:3. 操作成功,返回“ok”提示 return HttpResponse("ok") \ No newline at end of file From d043028005e4c699516165ad6cfd26e9db6f6613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=82=A6?= Date: Fri, 7 Nov 2025 22:25:01 +0800 Subject: [PATCH 2/8] =?UTF-8?q?zy=E4=BB=A3=E7=A0=81=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/oauth/admin.py | 25 +-- src/django-master/oauth/apps.py | 2 +- src/django-master/oauth/forms.py | 6 +- .../oauth/migrations/0001_initial.py | 54 ++--- ...ptions_alter_oauthuser_options_and_more.py | 54 ++--- .../0003_alter_oauthuser_nickname.py | 6 +- src/django-master/oauth/models.py | 53 ++--- src/django-master/oauth/oauthmanager.py | 187 +++++++++--------- .../oauth/templatetags/oauth_tags.py | 22 +-- src/django-master/oauth/tests.py | 138 ++++++------- src/django-master/oauth/urls.py | 30 +-- src/django-master/oauth/views.py | 120 +++++------ 12 files changed, 347 insertions(+), 350 deletions(-) diff --git a/src/django-master/oauth/admin.py b/src/django-master/oauth/admin.py index 57eab5f..4fcc25c 100644 --- a/src/django-master/oauth/admin.py +++ b/src/django-master/oauth/admin.py @@ -1,7 +1,6 @@ import logging from django.contrib import admin -# Register your models here. from django.urls import reverse from django.utils.html import format_html @@ -9,29 +8,30 @@ logger = logging.getLogger(__name__) class OAuthUserAdmin(admin.ModelAdmin): - search_fields = ('nickname', 'email') - list_per_page = 20 + search_fields = ('nickname', 'email') # zy: 管理员搜索字段配置 + list_per_page = 20 # zy: 分页设置,每页20条记录 list_display = ( 'id', 'nickname', - 'link_to_usermodel', - 'show_user_image', + 'link_to_usermodel', # zy: 自定义字段-关联用户链接 + 'show_user_image', # zy: 自定义字段-显示用户头像 'type', 'email', ) - list_display_links = ('id', 'nickname') - list_filter = ('author', 'type',) - readonly_fields = [] + list_display_links = ('id', 'nickname') # zy: 可点击进入编辑页的字段 + list_filter = ('author', 'type',) # zy: 右侧筛选器字段 def get_readonly_fields(self, request, obj=None): + # zy: 重要:将所有字段设为只读,防止管理员修改OAuth用户数据 return list(self.readonly_fields) + \ [field.name for field in obj._meta.fields] + \ [field.name for field in obj._meta.many_to_many] def has_add_permission(self, request): - return False + return False # zy: 关键:禁止在admin中添加OAuth用户,只能通过OAuth流程创建 def link_to_usermodel(self, obj): + # zy: 核心功能:生成关联用户的可点击链接 if obj.author: info = (obj.author._meta.app_label, obj.author._meta.model_name) link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) @@ -40,15 +40,16 @@ class OAuthUserAdmin(admin.ModelAdmin): (link, obj.author.nickname if obj.author.nickname else obj.author.email)) def show_user_image(self, obj): + # zy: 核心功能:在admin列表中显示用户头像图片 img = obj.picture return format_html( u'' % (img)) - link_to_usermodel.short_description = '用户' + link_to_usermodel.short_description = '用户' # zy: 自定义列显示名称 show_user_image.short_description = '用户头像' class OAuthConfigAdmin(admin.ModelAdmin): - list_display = ('type', 'appkey', 'appsecret', 'is_enable') - list_filter = ('type',) + list_display = ('type', 'appkey', 'appsecret', 'is_enable') # zy: OAuth配置列表显示字段 + list_filter = ('type',) # zy: 按OAuth类型筛选配置 \ No newline at end of file diff --git a/src/django-master/oauth/apps.py b/src/django-master/oauth/apps.py index 17fcea2..0fdf8a1 100644 --- a/src/django-master/oauth/apps.py +++ b/src/django-master/oauth/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class OauthConfig(AppConfig): - name = 'oauth' + name = 'oauth' # zy:定义Django应用的名称,必须与文件夹名称一致 \ No newline at end of file diff --git a/src/django-master/oauth/forms.py b/src/django-master/oauth/forms.py index 0e4ede3..e839af7 100644 --- a/src/django-master/oauth/forms.py +++ b/src/django-master/oauth/forms.py @@ -3,10 +3,10 @@ from django.forms import widgets class RequireEmailForm(forms.Form): - email = forms.EmailField(label='电子邮箱', required=True) - oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False) + email = forms.EmailField(label='电子邮箱', required=True) # zy:必填邮箱字段,用于OAuth登录时补充邮箱信息 + oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False) # zy:隐藏字段,用于传递OAuth用户ID def __init__(self, *args, **kwargs): super(RequireEmailForm, self).__init__(*args, **kwargs) self.fields['email'].widget = widgets.EmailInput( - attrs={'placeholder': "email", "class": "form-control"}) + attrs={'placeholder': "email", "class": "form-control"}) # zy:自定义邮箱输入框样式,添加Bootstrap类 diff --git a/src/django-master/oauth/migrations/0001_initial.py b/src/django-master/oauth/migrations/0001_initial.py index 3aa3e03..7db6ef6 100644 --- a/src/django-master/oauth/migrations/0001_initial.py +++ b/src/django-master/oauth/migrations/0001_initial.py @@ -8,50 +8,50 @@ import django.utils.timezone class Migration(migrations.Migration): - initial = True + initial = True # zy: 重要 - 标记为初始迁移文件 dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # zy: 关键依赖 - 依赖用户模型 ] operations = [ migrations.CreateModel( name='OAuthConfig', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')), - ('appkey', models.CharField(max_length=200, verbose_name='AppKey')), - ('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')), - ('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')), - ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), - ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # zy: 主键字段 - 自增BigAutoField + ('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')), # zy: 关键字段 - OAuth类型选择,包含五种服务商 + ('appkey', models.CharField(max_length=200, verbose_name='AppKey')), # zy: 重要字段 - 应用密钥ID + ('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')), # zy: 重要字段 - 应用密钥,需安全存储 + ('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')), # zy: 关键字段 - OAuth回调地址,默认值为百度 + ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), # zy: 控制字段 - 是否启用该OAuth配置 + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # zy: 时间字段 - 记录创建时间 + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # zy: 时间字段 - 记录最后修改时间 ], options={ - 'verbose_name': 'oauth配置', - 'verbose_name_plural': 'oauth配置', - 'ordering': ['-created_time'], + 'verbose_name': 'oauth配置', # zy: 单数显示名称 + 'verbose_name_plural': 'oauth配置', # zy: 复数显示名称 + 'ordering': ['-created_time'], # zy: 默认排序 - 按创建时间倒序 }, ), migrations.CreateModel( name='OAuthUser', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('openid', models.CharField(max_length=50)), - ('nickname', models.CharField(max_length=50, verbose_name='昵称')), - ('token', models.CharField(blank=True, max_length=150, null=True)), - ('picture', models.CharField(blank=True, max_length=350, null=True)), - ('type', models.CharField(max_length=50)), - ('email', models.CharField(blank=True, max_length=50, null=True)), - ('metadata', models.TextField(blank=True, null=True)), - ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), - ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # zy: 主键字段 + ('openid', models.CharField(max_length=50)), # zy: 关键字段 - OAuth服务商提供的用户唯一标识 + ('nickname', models.CharField(max_length=50, verbose_name='昵称')), # zy: 用户昵称字段 + ('token', models.CharField(blank=True, max_length=150, null=True)), # zy: 令牌字段 - 存储访问令牌,可为空 + ('picture', models.CharField(blank=True, max_length=350, null=True)), # zy: 头像字段 - 存储头像URL地址 + ('type', models.CharField(max_length=50)), # zy: 关键字段 - OAuth服务商类型 + ('email', models.CharField(blank=True, max_length=50, null=True)), # zy: 邮箱字段 - 用户邮箱,可为空 + ('metadata', models.TextField(blank=True, null=True)), # zy: 元数据字段 - 存储完整的OAuth用户信息 + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # zy: 创建时间 + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # zy: 修改时间 + ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), # zy: 关键关联 - 关联系统用户,可为空(未绑定状态),级联删除 ], options={ - 'verbose_name': 'oauth用户', - 'verbose_name_plural': 'oauth用户', - 'ordering': ['-created_time'], + 'verbose_name': 'oauth用户', # zy: 单数显示名称 + 'verbose_name_plural': 'oauth用户', # zy: 复数显示名称 + 'ordering': ['-created_time'], # zy: 默认排序 - 按创建时间倒序 }, ), ] diff --git a/src/django-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py b/src/django-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py index d5cc70e..c1cf762 100644 --- a/src/django-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py +++ b/src/django-master/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py @@ -9,78 +9,78 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('oauth', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # zy: 关键依赖 - 依赖用户模型 + ('oauth', '0001_initial'), # zy: 重要依赖 - 依赖初始迁移文件 ] operations = [ migrations.AlterModelOptions( name='oauthconfig', - options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'}, + options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'}, # zy: 修改排序字段为creation_time,保持中文显示名称 ), migrations.AlterModelOptions( name='oauthuser', - options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'}, + options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'}, # zy: 修改排序字段为creation_time,改为英文显示名称 ), migrations.RemoveField( model_name='oauthconfig', - name='created_time', + name='created_time', # zy: 删除旧字段 - 原创建时间字段 ), migrations.RemoveField( model_name='oauthconfig', - name='last_mod_time', + name='last_mod_time', # zy: 删除旧字段 - 原修改时间字段 ), migrations.RemoveField( model_name='oauthuser', - name='created_time', + name='created_time', # zy: 删除旧字段 - 原创建时间字段 ), migrations.RemoveField( model_name='oauthuser', - name='last_mod_time', + name='last_mod_time', # zy: 删除旧字段 - 原修改时间字段 ), migrations.AddField( model_name='oauthconfig', - name='creation_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + name='creation_time', # zy: 新增字段 - 标准化的创建时间字段 + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # zy: 使用国际化字段名称 ), migrations.AddField( model_name='oauthconfig', - name='last_modify_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + name='last_modify_time', # zy: 新增字段 - 标准化的最后修改时间字段 + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # zy: 使用国际化字段名称 ), migrations.AddField( model_name='oauthuser', - name='creation_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + name='creation_time', # zy: 新增字段 - 标准化的创建时间字段 + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # zy: 使用国际化字段名称 ), migrations.AddField( model_name='oauthuser', - name='last_modify_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + name='last_modify_time', # zy: 新增字段 - 标准化的最后修改时间字段 + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # zy: 使用国际化字段名称 ), migrations.AlterField( model_name='oauthconfig', - name='callback_url', - field=models.CharField(default='', max_length=200, verbose_name='callback url'), + name='callback_url', # zy: 修改字段 - 回调地址字段 + field=models.CharField(default='', max_length=200, verbose_name='callback url'), # zy: 重要变更 - 默认值改为空字符串,使用国际化字段名称 ), migrations.AlterField( model_name='oauthconfig', - name='is_enable', - field=models.BooleanField(default=True, verbose_name='is enable'), + name='is_enable', # zy: 修改字段 - 启用状态字段 + field=models.BooleanField(default=True, verbose_name='is enable'), # zy: 使用国际化字段名称 ), migrations.AlterField( model_name='oauthconfig', - name='type', - field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'), + name='type', # zy: 修改字段 - 类型选择字段 + field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'), # zy: 重要变更 - 选项值改为英文,使用国际化字段名称 ), migrations.AlterField( model_name='oauthuser', - name='author', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), + name='author', # zy: 修改字段 - 关联用户字段 + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), # zy: 使用国际化字段名称 ), migrations.AlterField( model_name='oauthuser', - name='nickname', - field=models.CharField(max_length=50, verbose_name='nickname'), + name='nickname', # zy: 修改字段 - 昵称字段 + field=models.CharField(max_length=50, verbose_name='nickname'), # zy: 使用国际化字段名称 ), - ] + ] \ No newline at end of file diff --git a/src/django-master/oauth/migrations/0003_alter_oauthuser_nickname.py b/src/django-master/oauth/migrations/0003_alter_oauthuser_nickname.py index 6af08eb..973b9c0 100644 --- a/src/django-master/oauth/migrations/0003_alter_oauthuser_nickname.py +++ b/src/django-master/oauth/migrations/0003_alter_oauthuser_nickname.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'), + ('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'), # zy: 依赖 - 依赖之前的第二次迁移文件 ] operations = [ migrations.AlterField( model_name='oauthuser', - name='nickname', - field=models.CharField(max_length=50, verbose_name='nick name'), + name='nickname', # zy: 修改字段 - 用户昵称字段 + field=models.CharField(max_length=50, verbose_name='nick name'), # zy: 细微调整 - 将字段显示名称从'nickname'改为'nick name'(添加空格) ), ] diff --git a/src/django-master/oauth/models.py b/src/django-master/oauth/models.py index be838ed..3190d89 100644 --- a/src/django-master/oauth/models.py +++ b/src/django-master/oauth/models.py @@ -12,24 +12,24 @@ class OAuthUser(models.Model): verbose_name=_('author'), blank=True, null=True, - on_delete=models.CASCADE) - openid = models.CharField(max_length=50) - nickname = models.CharField(max_length=50, verbose_name=_('nick name')) - token = models.CharField(max_length=150, null=True, blank=True) - picture = models.CharField(max_length=350, blank=True, null=True) - type = models.CharField(blank=False, null=False, max_length=50) - email = models.CharField(max_length=50, null=True, blank=True) - metadata = models.TextField(null=True, blank=True) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_modify_time = models.DateTimeField(_('last modify time'), default=now) + on_delete=models.CASCADE) # zy: 核心关联 - 关联系统用户,可为空(未绑定状态) + openid = models.CharField(max_length=50) # zy: 关键字段 - OAuth服务商提供的用户唯一标识 + nickname = models.CharField(max_length=50, verbose_name=_('nick name')) # zy: 用户昵称 + token = models.CharField(max_length=150, null=True, blank=True) # zy: 重要 - 访问令牌,用于API调用 + picture = models.CharField(max_length=350, blank=True, null=True) # zy: 用户头像URL + type = models.CharField(blank=False, null=False, max_length=50) # zy: 关键 - OAuth类型(weibo、github等) + email = models.CharField(max_length=50, null=True, blank=True) # zy: 用户邮箱 + metadata = models.TextField(null=True, blank=True) # zy: 重要 - 存储完整的OAuth用户信息JSON + creation_time = models.DateTimeField(_('creation time'), default=now) # zy: 记录创建时间 + last_modify_time = models.DateTimeField(_('last modify time'), default=now) # zy: 最后修改时间 def __str__(self): - return self.nickname + return self.nickname # zy: 对象显示为昵称 class Meta: - verbose_name = _('oauth user') - verbose_name_plural = verbose_name - ordering = ['-creation_time'] + verbose_name = _('oauth user') # zy: 单数显示名称 + verbose_name_plural = verbose_name # zy: 复数显示名称 + ordering = ['-creation_time'] # zy: 默认按创建时间倒序排列 class OAuthConfig(models.Model): @@ -39,29 +39,30 @@ class OAuthConfig(models.Model): ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ'), - ) - type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') - appkey = models.CharField(max_length=200, verbose_name='AppKey') - appsecret = models.CharField(max_length=200, verbose_name='AppSecret') + ) # zy: 核心配置 - 支持的OAuth类型选项 + type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') # zy: 关键 - OAuth服务商类型 + appkey = models.CharField(max_length=200, verbose_name='AppKey') # zy: 重要 - 应用密钥ID + appsecret = models.CharField(max_length=200, verbose_name='AppSecret') # zy: 重要 - 应用密钥,需加密存储 callback_url = models.CharField( max_length=200, verbose_name=_('callback url'), blank=False, - default='') + default='') # zy: 关键 - OAuth回调URL is_enable = models.BooleanField( - _('is enable'), default=True, blank=False, null=False) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_modify_time = models.DateTimeField(_('last modify time'), default=now) + _('is enable'), default=True, blank=False, null=False) # zy: 重要 - 控制该配置是否启用 + creation_time = models.DateTimeField(_('creation time'), default=now) # zy: 配置创建时间 + last_modify_time = models.DateTimeField(_('last modify time'), default=now) # zy: 配置最后修改时间 def clean(self): + # zy: 核心验证 - 确保同类型OAuth配置唯一,防止重复配置 if OAuthConfig.objects.filter( type=self.type).exclude(id=self.id).count(): raise ValidationError(_(self.type + _('already exists'))) def __str__(self): - return self.type + return self.type # zy: 对象显示为类型名称 class Meta: - verbose_name = 'oauth配置' - verbose_name_plural = verbose_name - ordering = ['-creation_time'] + verbose_name = 'oauth配置' # zy: 中文显示名称 + verbose_name_plural = verbose_name # zy: 复数显示名称 + ordering = ['-creation_time'] # zy: 按创建时间倒序排列 \ No newline at end of file diff --git a/src/django-master/oauth/oauthmanager.py b/src/django-master/oauth/oauthmanager.py index 2e7ceef..1d95317 100644 --- a/src/django-master/oauth/oauthmanager.py +++ b/src/django-master/oauth/oauthmanager.py @@ -15,73 +15,73 @@ logger = logging.getLogger(__name__) class OAuthAccessTokenException(Exception): ''' oauth授权失败异常 - ''' + ''' # zy: 核心异常类 - 专门处理OAuth授权过程中的令牌获取失败 class BaseOauthManager(metaclass=ABCMeta): """获取用户授权""" - AUTH_URL = None + AUTH_URL = None # zy: 关键URL - OAuth授权页面地址,子类必须实现 """获取token""" - TOKEN_URL = None + TOKEN_URL = None # zy: 关键URL - 获取访问令牌的API地址,子类必须实现 """获取用户信息""" - API_URL = None + API_URL = None # zy: 关键URL - 获取用户信息的API地址,子类必须实现 '''icon图标名''' - ICON_NAME = None + ICON_NAME = None # zy: 标识符 - OAuth服务商类型标识,用于匹配配置 def __init__(self, access_token=None, openid=None): - self.access_token = access_token - self.openid = openid + self.access_token = access_token # zy: 核心属性 - OAuth访问令牌 + self.openid = openid # zy: 核心属性 - 用户在OAuth服务商的唯一ID @property def is_access_token_set(self): - return self.access_token is not None + return self.access_token is not None # zy: 重要检查 - 判断访问令牌是否已设置 @property def is_authorized(self): - return self.is_access_token_set and self.access_token is not None and self.openid is not None + return self.is_access_token_set and self.access_token is not None and self.openid is not None # zy: 核心检查 - 判断是否已完成授权 @abstractmethod def get_authorization_url(self, nexturl='/'): - pass + pass # zy: 抽象方法 - 生成授权URL,子类必须实现 @abstractmethod def get_access_token_by_code(self, code): - pass + pass # zy: 抽象方法 - 通过授权码获取访问令牌,子类必须实现 @abstractmethod def get_oauth_userinfo(self): - pass + pass # zy: 抽象方法 - 获取用户信息,子类必须实现 @abstractmethod def get_picture(self, metadata): - pass + pass # zy: 抽象方法 - 从元数据中提取头像,子类必须实现 def do_get(self, url, params, headers=None): - rsp = requests.get(url=url, params=params, headers=headers) - logger.info(rsp.text) + rsp = requests.get(url=url, params=params, headers=headers) # zy: 核心方法 - 执行GET请求 + logger.info(rsp.text) # zy: 重要日志 - 记录API响应 return rsp.text def do_post(self, url, params, headers=None): - rsp = requests.post(url, params, headers=headers) - logger.info(rsp.text) + rsp = requests.post(url, params, headers=headers) # zy: 核心方法 - 执行POST请求 + logger.info(rsp.text) # zy: 重要日志 - 记录API响应 return rsp.text def get_config(self): - value = OAuthConfig.objects.filter(type=self.ICON_NAME) - return value[0] if value else None + value = OAuthConfig.objects.filter(type=self.ICON_NAME) # zy: 关键查询 - 获取对应类型的OAuth配置 + return value[0] if value else None # zy: 返回第一个配置或None class WBOauthManager(BaseOauthManager): - AUTH_URL = 'https://api.weibo.com/oauth2/authorize' - TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' - API_URL = 'https://api.weibo.com/2/users/show.json' - ICON_NAME = 'weibo' + AUTH_URL = 'https://api.weibo.com/oauth2/authorize' # zy: 微博授权URL + TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' # zy: 微博令牌获取URL + API_URL = 'https://api.weibo.com/2/users/show.json' # zy: 微博用户信息API + ICON_NAME = 'weibo' # zy: 微博标识 def __init__(self, access_token=None, openid=None): config = self.get_config() - self.client_id = config.appkey if config else '' - self.client_secret = config.appsecret if config else '' - self.callback_url = config.callback_url if config else '' + self.client_id = config.appkey if config else '' # zy: 关键配置 - 应用Key + self.client_secret = config.appsecret if config else '' # zy: 关键配置 - 应用Secret + self.callback_url = config.callback_url if config else '' # zy: 关键配置 - 回调地址 super( WBOauthManager, self).__init__( @@ -92,13 +92,12 @@ class WBOauthManager(BaseOauthManager): params = { 'client_id': self.client_id, 'response_type': 'code', - 'redirect_uri': self.callback_url + '&next_url=' + nexturl + 'redirect_uri': self.callback_url + '&next_url=' + nexturl # zy: 重要参数 - 携带next_url用于跳转 } - url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) + url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) # zy: 生成完整的授权URL return url def get_access_token_by_code(self, code): - params = { 'client_id': self.client_id, 'client_secret': self.client_secret, @@ -106,19 +105,19 @@ class WBOauthManager(BaseOauthManager): 'code': code, 'redirect_uri': self.callback_url } - rsp = self.do_post(self.TOKEN_URL, params) + rsp = self.do_post(self.TOKEN_URL, params) # zy: 关键调用 - 获取访问令牌 obj = json.loads(rsp) if 'access_token' in obj: - self.access_token = str(obj['access_token']) - self.openid = str(obj['uid']) - return self.get_oauth_userinfo() + self.access_token = str(obj['access_token']) # zy: 保存访问令牌 + self.openid = str(obj['uid']) # zy: 保存用户ID + return self.get_oauth_userinfo() # zy: 重要流程 - 获取令牌后立即获取用户信息 else: - raise OAuthAccessTokenException(rsp) + raise OAuthAccessTokenException(rsp) # zy: 令牌获取失败抛出异常 def get_oauth_userinfo(self): if not self.is_authorized: - return None + return None # zy: 安全检查 - 确保已授权 params = { 'uid': self.openid, 'access_token': self.access_token @@ -126,24 +125,24 @@ class WBOauthManager(BaseOauthManager): rsp = self.do_get(self.API_URL, params) try: datas = json.loads(rsp) - user = OAuthUser() - user.metadata = rsp - user.picture = datas['avatar_large'] - user.nickname = datas['screen_name'] - user.openid = datas['id'] - user.type = 'weibo' - user.token = self.access_token + user = OAuthUser() # zy: 创建OAuth用户对象 + user.metadata = rsp # zy: 保存原始响应数据 + user.picture = datas['avatar_large'] # zy: 设置用户头像 + user.nickname = datas['screen_name'] # zy: 设置用户昵称 + user.openid = datas['id'] # zy: 设置开放ID + user.type = 'weibo' # zy: 设置类型 + user.token = self.access_token # zy: 设置访问令牌 if 'email' in datas and datas['email']: - user.email = datas['email'] + user.email = datas['email'] # zy: 设置邮箱(如果有) return user except Exception as e: logger.error(e) - logger.error('weibo oauth error.rsp:' + rsp) + logger.error('weibo oauth error.rsp:' + rsp) # zy: 重要错误日志 return None def get_picture(self, metadata): datas = json.loads(metadata) - return datas['avatar_large'] + return datas['avatar_large'] # zy: 从元数据中提取头像URL class ProxyManagerMixin: @@ -151,27 +150,27 @@ class ProxyManagerMixin: if os.environ.get("HTTP_PROXY"): self.proxies = { "http": os.environ.get("HTTP_PROXY"), - "https": os.environ.get("HTTP_PROXY") + "https": os.environ.get("HTTP_PROXY") # zy: 重要配置 - 设置HTTP代理 } else: self.proxies = None def do_get(self, url, params, headers=None): - rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies) + rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies) # zy: 带代理的GET请求 logger.info(rsp.text) return rsp.text def do_post(self, url, params, headers=None): - rsp = requests.post(url, params, headers=headers, proxies=self.proxies) + rsp = requests.post(url, params, headers=headers, proxies=self.proxies) # zy: 带代理的POST请求 logger.info(rsp.text) return rsp.text class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): - AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth' - TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' - API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo' - ICON_NAME = 'google' + AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth' # zy: Google授权URL + TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' # zy: Google令牌URL + API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo' # zy: Google用户信息API + ICON_NAME = 'google' # zy: Google标识 def __init__(self, access_token=None, openid=None): config = self.get_config() @@ -189,7 +188,7 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): 'client_id': self.client_id, 'response_type': 'code', 'redirect_uri': self.callback_url, - 'scope': 'openid email', + 'scope': 'openid email', # zy: 重要参数 - 请求openid和email权限 } url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) return url @@ -200,7 +199,6 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): 'client_secret': self.client_secret, 'grant_type': 'authorization_code', 'code': code, - 'redirect_uri': self.callback_url } rsp = self.do_post(self.TOKEN_URL, params) @@ -209,9 +207,9 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): if 'access_token' in obj: self.access_token = str(obj['access_token']) - self.openid = str(obj['id_token']) + self.openid = str(obj['id_token']) # zy: Google使用id_token作为openid logger.info(self.ICON_NAME + ' oauth ' + rsp) - return self.access_token + return self.access_token # zy: 返回访问令牌 else: raise OAuthAccessTokenException(rsp) @@ -223,13 +221,12 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): } rsp = self.do_get(self.API_URL, params) try: - datas = json.loads(rsp) user = OAuthUser() user.metadata = rsp - user.picture = datas['picture'] + user.picture = datas['picture'] # zy: Google头像字段 user.nickname = datas['name'] - user.openid = datas['sub'] + user.openid = datas['sub'] # zy: Google用户ID字段 user.token = self.access_token user.type = 'google' if datas['email']: @@ -246,10 +243,10 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): - AUTH_URL = 'https://github.com/login/oauth/authorize' - TOKEN_URL = 'https://github.com/login/oauth/access_token' - API_URL = 'https://api.github.com/user' - ICON_NAME = 'github' + AUTH_URL = 'https://github.com/login/oauth/authorize' # zy: GitHub授权URL + TOKEN_URL = 'https://github.com/login/oauth/access_token' # zy: GitHub令牌URL + API_URL = 'https://api.github.com/user' # zy: GitHub用户信息API + ICON_NAME = 'github' # zy: GitHub标识 def __init__(self, access_token=None, openid=None): config = self.get_config() @@ -266,8 +263,8 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): params = { 'client_id': self.client_id, 'response_type': 'code', - 'redirect_uri': f'{self.callback_url}&next_url={next_url}', - 'scope': 'user' + 'redirect_uri': f'{self.callback_url}&next_url={next_url}', # zy: 使用f-string格式化URL + 'scope': 'user' # zy: 请求user权限范围 } url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) return url @@ -278,13 +275,12 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): 'client_secret': self.client_secret, 'grant_type': 'authorization_code', 'code': code, - 'redirect_uri': self.callback_url } rsp = self.do_post(self.TOKEN_URL, params) from urllib import parse - r = parse.parse_qs(rsp) + r = parse.parse_qs(rsp) # zy: 重要 - GitHub返回的是查询字符串格式 if 'access_token' in r: self.access_token = (r['access_token'][0]) return self.access_token @@ -292,14 +288,13 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): raise OAuthAccessTokenException(rsp) def get_oauth_userinfo(self): - rsp = self.do_get(self.API_URL, params={}, headers={ - "Authorization": "token " + self.access_token + "Authorization": "token " + self.access_token # zy: 关键 - GitHub需要在header中传递token }) try: datas = json.loads(rsp) user = OAuthUser() - user.picture = datas['avatar_url'] + user.picture = datas['avatar_url'] # zy: GitHub头像字段 user.nickname = datas['name'] user.openid = datas['id'] user.type = 'github' @@ -319,10 +314,10 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): - AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth' - TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token' - API_URL = 'https://graph.facebook.com/me' - ICON_NAME = 'facebook' + AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth' # zy: Facebook授权URL(指定API版本) + TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token' # zy: Facebook令牌URL + API_URL = 'https://graph.facebook.com/me' # zy: Facebook用户信息API + ICON_NAME = 'facebook' # zy: Facebook标识 def __init__(self, access_token=None, openid=None): config = self.get_config() @@ -340,7 +335,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): 'client_id': self.client_id, 'response_type': 'code', 'redirect_uri': self.callback_url, - 'scope': 'email,public_profile' + 'scope': 'email,public_profile' # zy: 请求邮箱和公开资料权限 } url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) return url @@ -349,9 +344,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): params = { 'client_id': self.client_id, 'client_secret': self.client_secret, - # 'grant_type': 'authorization_code', 'code': code, - 'redirect_uri': self.callback_url } rsp = self.do_post(self.TOKEN_URL, params) @@ -367,7 +360,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): def get_oauth_userinfo(self): params = { 'access_token': self.access_token, - 'fields': 'id,name,picture,email' + 'fields': 'id,name,picture,email' # zy: 重要 - 指定需要返回的字段 } try: rsp = self.do_get(self.API_URL, params) @@ -381,7 +374,7 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): if 'email' in datas and datas['email']: user.email = datas['email'] if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']: - user.picture = str(datas['picture']['data']['url']) + user.picture = str(datas['picture']['data']['url']) # zy: Facebook头像嵌套在data对象中 return user except Exception as e: logger.error(e) @@ -393,11 +386,11 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): class QQOauthManager(BaseOauthManager): - AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize' - TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' - API_URL = 'https://graph.qq.com/user/get_user_info' - OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' - ICON_NAME = 'qq' + AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize' # zy: QQ授权URL + TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' # zy: QQ令牌URL + API_URL = 'https://graph.qq.com/user/get_user_info' # zy: QQ用户信息API + OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' # zy: 关键 - QQ需要单独获取openid + ICON_NAME = 'qq' # zy: QQ标识 def __init__(self, access_token=None, openid=None): config = self.get_config() @@ -429,7 +422,7 @@ class QQOauthManager(BaseOauthManager): } rsp = self.do_get(self.TOKEN_URL, params) if rsp: - d = urllib.parse.parse_qs(rsp) + d = urllib.parse.parse_qs(rsp) # zy: 重要 - QQ返回查询字符串格式 if 'access_token' in d: token = d['access_token'] self.access_token = token[0] @@ -447,18 +440,18 @@ class QQOauthManager(BaseOauthManager): rsp = rsp.replace( 'callback(', '').replace( ')', '').replace( - ';', '') + ';', '') # zy: 关键处理 - 清理JSONP响应格式 obj = json.loads(rsp) openid = str(obj['openid']) self.openid = openid return openid def get_oauth_userinfo(self): - openid = self.get_open_id() + openid = self.get_open_id() # zy: 重要 - 先获取openid if openid: params = { 'access_token': self.access_token, - 'oauth_consumer_key': self.client_id, + 'oauth_consumer_key': self.client_id, # zy: QQ需要传递client_id作为oauth_consumer_key 'openid': self.openid } rsp = self.do_get(self.API_URL, params) @@ -473,7 +466,7 @@ class QQOauthManager(BaseOauthManager): if 'email' in obj: user.email = obj['email'] if 'figureurl' in obj: - user.picture = str(obj['figureurl']) + user.picture = str(obj['figureurl']) # zy: QQ头像字段 return user def get_picture(self, metadata): @@ -483,22 +476,22 @@ class QQOauthManager(BaseOauthManager): @cache_decorator(expiration=100 * 60) def get_oauth_apps(): - configs = OAuthConfig.objects.filter(is_enable=True).all() + configs = OAuthConfig.objects.filter(is_enable=True).all() # zy: 关键查询 - 只获取启用的OAuth配置 if not configs: return [] - configtypes = [x.type for x in configs] - applications = BaseOauthManager.__subclasses__() - apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes] + configtypes = [x.type for x in configs] # zy: 提取配置类型列表 + applications = BaseOauthManager.__subclasses__() # zy: 重要 - 获取所有子类(OAuth管理器) + apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes] # zy: 创建对应的管理器实例 return apps def get_manager_by_type(type): - applications = get_oauth_apps() + applications = get_oauth_apps() # zy: 获取所有可用的OAuth应用 if applications: finds = list( filter( - lambda x: x.ICON_NAME.lower() == type.lower(), + lambda x: x.ICON_NAME.lower() == type.lower(), # zy: 按类型过滤 applications)) if finds: - return finds[0] - return None + return finds[0] # zy: 返回第一个匹配的管理器 + return None # zy: 未找到返回None \ No newline at end of file diff --git a/src/django-master/oauth/templatetags/oauth_tags.py b/src/django-master/oauth/templatetags/oauth_tags.py index 7b687d5..19c8cea 100644 --- a/src/django-master/oauth/templatetags/oauth_tags.py +++ b/src/django-master/oauth/templatetags/oauth_tags.py @@ -3,20 +3,20 @@ from django.urls import reverse from oauth.oauthmanager import get_oauth_apps -register = template.Library() +register = template.Library() # zy: 重要 - 创建Django模板标签库实例 -@register.inclusion_tag('oauth/oauth_applications.html') -def load_oauth_applications(request): - applications = get_oauth_apps() - if applications: - baseurl = reverse('oauth:oauthlogin') - path = request.get_full_path() +@register.inclusion_tag('oauth/oauth_applications.html') # zy: 核心装饰器 - 注册包含标签,指定渲染模板 +def load_oauth_applications(request): # zy: 关键函数 - 加载OAuth应用列表的模板标签 + applications = get_oauth_apps() # zy: 重要调用 - 获取所有启用的OAuth应用管理器 + if applications: # zy: 安全检查 - 确保有可用的OAuth应用 + baseurl = reverse('oauth:oauthlogin') # zy: 关键调用 - 生成OAuth登录入口URL + path = request.get_full_path() # zy: 获取当前完整路径,用于登录后跳转 apps = list(map(lambda x: (x.ICON_NAME, '{baseurl}?type={type}&next_url={next}'.format( - baseurl=baseurl, type=x.ICON_NAME, next=path)), applications)) + baseurl=baseurl, type=x.ICON_NAME, next=path)), applications)) # zy: 核心逻辑 - 生成每个OAuth应用的登录URL else: - apps = [] + apps = [] # zy: 默认值 - 无可用应用时返回空列表 return { - 'apps': apps - } + 'apps': apps # zy: 返回字典 - 包含应用列表供模板使用 + } \ No newline at end of file diff --git a/src/django-master/oauth/tests.py b/src/django-master/oauth/tests.py index bb23b9b..7abe687 100644 --- a/src/django-master/oauth/tests.py +++ b/src/django-master/oauth/tests.py @@ -14,64 +14,64 @@ from oauth.oauthmanager import BaseOauthManager # Create your tests here. class OAuthConfigTest(TestCase): def setUp(self): - self.client = Client() - self.factory = RequestFactory() + self.client = Client() # zy: 测试客户端 - 用于模拟HTTP请求 + self.factory = RequestFactory() # zy: 请求工厂 - 用于创建请求对象 def test_oauth_login_test(self): - c = OAuthConfig() - c.type = 'weibo' - c.appkey = 'appkey' - c.appsecret = 'appsecret' - c.save() + c = OAuthConfig() # zy: 创建OAuth配置对象 + c.type = 'weibo' # zy: 设置OAuth类型为微博 + c.appkey = 'appkey' # zy: 设置应用Key + c.appsecret = 'appsecret' # zy: 设置应用Secret + c.save() # zy: 保存配置到数据库 - response = self.client.get('/oauth/oauthlogin?type=weibo') - self.assertEqual(response.status_code, 302) - self.assertTrue("api.weibo.com" in response.url) + response = self.client.get('/oauth/oauthlogin?type=weibo') # zy: 模拟OAuth登录请求 + self.assertEqual(response.status_code, 302) # zy: 断言重定向状态码 + self.assertTrue("api.weibo.com" in response.url) # zy: 断言跳转到微博授权页面 - response = self.client.get('/oauth/authorize?type=weibo&code=code') - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, '/') + response = self.client.get('/oauth/authorize?type=weibo&code=code') # zy: 模拟授权回调请求 + self.assertEqual(response.status_code, 302) # zy: 断言重定向状态码 + self.assertEqual(response.url, '/') # zy: 断言跳转到首页 class OauthLoginTest(TestCase): def setUp(self) -> None: self.client = Client() self.factory = RequestFactory() - self.apps = self.init_apps() + self.apps = self.init_apps() # zy: 初始化所有OAuth应用 def init_apps(self): - applications = [p() for p in BaseOauthManager.__subclasses__()] + applications = [p() for p in BaseOauthManager.__subclasses__()] # zy: 重要 - 获取所有OAuth管理器子类的实例 for application in applications: c = OAuthConfig() - c.type = application.ICON_NAME.lower() - c.appkey = 'appkey' - c.appsecret = 'appsecret' - c.save() + c.type = application.ICON_NAME.lower() # zy: 设置配置类型 + c.appkey = 'appkey' # zy: 模拟应用Key + c.appsecret = 'appsecret' # zy: 模拟应用Secret + c.save() # zy: 保存每个OAuth配置 return applications def get_app_by_type(self, type): for app in self.apps: - if app.ICON_NAME.lower() == type: + if app.ICON_NAME.lower() == type: # zy: 按类型查找对应的OAuth管理器 return app - @patch("oauth.oauthmanager.WBOauthManager.do_post") - @patch("oauth.oauthmanager.WBOauthManager.do_get") + @patch("oauth.oauthmanager.WBOauthManager.do_post") # zy: 关键 - 模拟微博POST请求 + @patch("oauth.oauthmanager.WBOauthManager.do_get") # zy: 关键 - 模拟微博GET请求 def test_weibo_login(self, mock_do_get, mock_do_post): - weibo_app = self.get_app_by_type('weibo') + weibo_app = self.get_app_by_type('weibo') # zy: 获取微博OAuth管理器 assert weibo_app - url = weibo_app.get_authorization_url() + url = weibo_app.get_authorization_url() # zy: 获取授权URL mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid" - }) + }) # zy: 模拟令牌接口返回 mock_do_get.return_value = json.dumps({ "avatar_large": "avatar_large", "screen_name": "screen_name", "id": "id", "email": "email", - }) - userinfo = weibo_app.get_access_token_by_code('code') - self.assertEqual(userinfo.token, 'access_token') - self.assertEqual(userinfo.openid, 'id') + }) # zy: 模拟用户信息接口返回 + userinfo = weibo_app.get_access_token_by_code('code') # zy: 关键调用 - 通过授权码获取用户信息 + self.assertEqual(userinfo.token, 'access_token') # zy: 断言令牌正确 + self.assertEqual(userinfo.openid, 'id') # zy: 断言用户ID正确 @patch("oauth.oauthmanager.GoogleOauthManager.do_post") @patch("oauth.oauthmanager.GoogleOauthManager.do_get") @@ -81,18 +81,18 @@ class OauthLoginTest(TestCase): url = google_app.get_authorization_url() mock_do_post.return_value = json.dumps({ "access_token": "access_token", - "id_token": "id_token", + "id_token": "id_token", # zy: Google特有字段 - ID令牌 }) mock_do_get.return_value = json.dumps({ "picture": "picture", "name": "name", - "sub": "sub", + "sub": "sub", # zy: Google用户ID字段 "email": "email", }) token = google_app.get_access_token_by_code('code') - userinfo = google_app.get_oauth_userinfo() + userinfo = google_app.get_oauth_userinfo() # zy: 重要 - 分开获取用户信息 self.assertEqual(userinfo.token, 'access_token') - self.assertEqual(userinfo.openid, 'sub') + self.assertEqual(userinfo.openid, 'sub') # zy: 断言Google用户ID @patch("oauth.oauthmanager.GitHubOauthManager.do_post") @patch("oauth.oauthmanager.GitHubOauthManager.do_get") @@ -100,18 +100,18 @@ class OauthLoginTest(TestCase): github_app = self.get_app_by_type('github') assert github_app url = github_app.get_authorization_url() - self.assertTrue("github.com" in url) - self.assertTrue("client_id" in url) - mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer" + self.assertTrue("github.com" in url) # zy: 断言GitHub授权URL + self.assertTrue("client_id" in url) # zy: 断言包含client_id参数 + mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer" # zy: 重要 - GitHub返回查询字符串格式 mock_do_get.return_value = json.dumps({ - "avatar_url": "avatar_url", + "avatar_url": "avatar_url", # zy: GitHub头像字段 "name": "name", "id": "id", "email": "email", }) token = github_app.get_access_token_by_code('code') userinfo = github_app.get_oauth_userinfo() - self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') + self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') # zy: 断言GitHub令牌格式 self.assertEqual(userinfo.openid, 'id') @patch("oauth.oauthmanager.FaceBookOauthManager.do_post") @@ -120,7 +120,7 @@ class OauthLoginTest(TestCase): facebook_app = self.get_app_by_type('facebook') assert facebook_app url = facebook_app.get_authorization_url() - self.assertTrue("facebook.com" in url) + self.assertTrue("facebook.com" in url) # zy: 断言Facebook授权URL mock_do_post.return_value = json.dumps({ "access_token": "access_token", }) @@ -130,7 +130,7 @@ class OauthLoginTest(TestCase): "email": "email", "picture": { "data": { - "url": "url" + "url": "url" # zy: Facebook头像嵌套结构 } } }) @@ -139,20 +139,20 @@ class OauthLoginTest(TestCase): self.assertEqual(userinfo.token, 'access_token') @patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[ - 'access_token=access_token&expires_in=3600', - 'callback({"client_id":"appid","openid":"openid"} );', + 'access_token=access_token&expires_in=3600', # zy: 第一次调用 - 获取令牌 + 'callback({"client_id":"appid","openid":"openid"} );', # zy: 第二次调用 - 获取openid(JSONP格式) json.dumps({ "nickname": "nickname", "email": "email", - "figureurl": "figureurl", + "figureurl": "figureurl", # zy: QQ头像字段 "openid": "openid", - }) + }) # zy: 第三次调用 - 获取用户信息 ]) def test_qq_login(self, mock_do_get): qq_app = self.get_app_by_type('qq') assert qq_app url = qq_app.get_authorization_url() - self.assertTrue("qq.com" in url) + self.assertTrue("qq.com" in url) # zy: 断言QQ授权URL token = qq_app.get_access_token_by_code('code') userinfo = qq_app.get_oauth_userinfo() self.assertEqual(userinfo.token, 'access_token') @@ -160,7 +160,7 @@ class OauthLoginTest(TestCase): @patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_get") def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post): - + # zy: 重要测试 - 测试带邮箱的微博登录完整流程 mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid" }) @@ -168,7 +168,7 @@ class OauthLoginTest(TestCase): "avatar_large": "avatar_large", "screen_name": "screen_name1", "id": "id", - "email": "email", + "email": "email", # zy: 包含邮箱信息 } mock_do_get.return_value = json.dumps(mock_user_info) @@ -176,17 +176,18 @@ class OauthLoginTest(TestCase): self.assertEqual(response.status_code, 302) self.assertTrue("api.weibo.com" in response.url) - response = self.client.get('/oauth/authorize?type=weibo&code=code') + response = self.client.get('/oauth/authorize?type=weibo&code=code') # zy: 模拟授权回调 self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, '/') + self.assertEqual(response.url, '/') # zy: 直接跳转首页(有邮箱) - user = auth.get_user(self.client) - assert user.is_authenticated + user = auth.get_user(self.client) # zy: 获取当前登录用户 + assert user.is_authenticated # zy: 断言用户已认证 self.assertTrue(user.is_authenticated) - self.assertEqual(user.username, mock_user_info['screen_name']) - self.assertEqual(user.email, mock_user_info['email']) - self.client.logout() + self.assertEqual(user.username, mock_user_info['screen_name']) # zy: 断言用户名 + self.assertEqual(user.email, mock_user_info['email']) # zy: 断言邮箱 + self.client.logout() # zy: 注销用户 + # zy: 重复登录测试 response = self.client.get('/oauth/authorize?type=weibo&code=code') self.assertEqual(response.status_code, 302) self.assertEqual(response.url, '/') @@ -200,7 +201,7 @@ class OauthLoginTest(TestCase): @patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_get") def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post): - + # zy: 重要测试 - 测试不带邮箱的微博登录流程(需要补充邮箱) mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid" }) @@ -208,6 +209,7 @@ class OauthLoginTest(TestCase): "avatar_large": "avatar_large", "screen_name": "screen_name1", "id": "id", + # zy: 故意不包含邮箱字段 } mock_do_get.return_value = json.dumps(mock_user_info) @@ -219,31 +221,31 @@ class OauthLoginTest(TestCase): self.assertEqual(response.status_code, 302) - oauth_user_id = int(response.url.split('/')[-1].split('.')[0]) - self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html') + oauth_user_id = int(response.url.split('/')[-1].split('.')[0]) # zy: 从URL中提取OAuth用户ID + self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html') # zy: 断言跳转到邮箱补充页面 - response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id}) + response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id}) # zy: 提交邮箱表单 self.assertEqual(response.status_code, 302) sign = get_sha256(settings.SECRET_KEY + - str(oauth_user_id) + settings.SECRET_KEY) + str(oauth_user_id) + settings.SECRET_KEY) # zy: 重要 - 生成邮箱确认签名 url = reverse('oauth:bindsuccess', kwargs={ 'oauthid': oauth_user_id, }) - self.assertEqual(response.url, f'{url}?type=email') + self.assertEqual(response.url, f'{url}?type=email') # zy: 断言跳转到绑定成功页面 path = reverse('oauth:email_confirm', kwargs={ 'id': oauth_user_id, - 'sign': sign + 'sign': sign # zy: 使用签名进行邮箱确认 }) - response = self.client.get(path) + response = self.client.get(path) # zy: 模拟邮箱确认链接点击 self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success') + self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success') # zy: 最终成功页面 user = auth.get_user(self.client) from oauth.models import OAuthUser - oauth_user = OAuthUser.objects.get(author=user) - self.assertTrue(user.is_authenticated) - self.assertEqual(user.username, mock_user_info['screen_name']) - self.assertEqual(user.email, 'test@gmail.com') - self.assertEqual(oauth_user.pk, oauth_user_id) + oauth_user = OAuthUser.objects.get(author=user) # zy: 查询关联的OAuth用户 + self.assertTrue(user.is_authenticated) # zy: 断言用户已登录 + self.assertEqual(user.username, mock_user_info['screen_name']) # zy: 断言用户名 + self.assertEqual(user.email, 'test@gmail.com') # zy: 断言补充的邮箱 + self.assertEqual(oauth_user.pk, oauth_user_id) # zy: 断言OAuth用户ID匹配 \ No newline at end of file diff --git a/src/django-master/oauth/urls.py b/src/django-master/oauth/urls.py index c4a12a0..58db0df 100644 --- a/src/django-master/oauth/urls.py +++ b/src/django-master/oauth/urls.py @@ -2,24 +2,24 @@ from django.urls import path from . import views -app_name = "oauth" +app_name = "oauth" # zy: 重要 - 定义应用命名空间,用于URL反向解析 urlpatterns = [ path( - r'oauth/authorize', - views.authorize), + r'oauth/authorize', # zy: 核心路径 - OAuth授权回调处理 + views.authorize), # zy: 关键视图 - 处理OAuth服务商回调,获取用户信息 path( - r'oauth/requireemail/.html', - views.RequireEmailView.as_view(), - name='require_email'), + r'oauth/requireemail/.html', # zy: 动态路径 - 需要补充邮箱页面(带OAuth用户ID参数) + views.RequireEmailView.as_view(), # zy: 类视图 - 处理邮箱补充表单 + name='require_email'), # zy: URL名称 - 用于反向解析 path( - r'oauth/emailconfirm//.html', - views.emailconfirm, - name='email_confirm'), + r'oauth/emailconfirm//.html', # zy: 关键路径 - 邮箱确认链接(带ID和签名参数) + views.emailconfirm, # zy: 重要视图 - 验证邮箱确认签名并完成绑定 + name='email_confirm'), # zy: URL名称 - 邮箱确认功能 path( - r'oauth/bindsuccess/.html', - views.bindsuccess, - name='bindsuccess'), + r'oauth/bindsuccess/.html', # zy: 成功页面 - OAuth绑定成功提示 + views.bindsuccess, # zy: 视图函数 - 显示绑定成功信息 + name='bindsuccess'), # zy: URL名称 - 绑定成功页面 path( - r'oauth/oauthlogin', - views.oauthlogin, - name='oauthlogin')] + r'oauth/oauthlogin', # zy: 入口路径 - OAuth登录入口 + views.oauthlogin, # zy: 入口视图 - 跳转到OAuth服务商授权页面 + name='oauthlogin')] # zy: URL名称 - OAuth登录功能 diff --git a/src/django-master/oauth/views.py b/src/django-master/oauth/views.py index 12e3a6e..22889a6 100644 --- a/src/django-master/oauth/views.py +++ b/src/django-master/oauth/views.py @@ -27,29 +27,29 @@ logger = logging.getLogger(__name__) def get_redirecturl(request): - nexturl = request.GET.get('next_url', None) + nexturl = request.GET.get('next_url', None) # zy: 获取重定向URL参数 if not nexturl or nexturl == '/login/' or nexturl == '/login': - nexturl = '/' + nexturl = '/' # zy: 默认重定向到首页 return nexturl - p = urlparse(nexturl) + p = urlparse(nexturl) # zy: 解析URL防止开放重定向攻击 if p.netloc: site = get_current_site().domain if not p.netloc.replace('www.', '') == site.replace('www.', ''): - logger.info('非法url:' + nexturl) + logger.info('非法url:' + nexturl) # zy: 安全记录 - 记录非法URL return "/" return nexturl def oauthlogin(request): - type = request.GET.get('type', None) + type = request.GET.get('type', None) # zy: 获取OAuth类型参数 if not type: return HttpResponseRedirect('/') - manager = get_manager_by_type(type) + manager = get_manager_by_type(type) # zy: 关键调用 - 获取对应类型的OAuth管理器 if not manager: return HttpResponseRedirect('/') nexturl = get_redirecturl(request) - authorizeurl = manager.get_authorization_url(nexturl) - return HttpResponseRedirect(authorizeurl) + authorizeurl = manager.get_authorization_url(nexturl) # zy: 核心功能 - 生成授权URL + return HttpResponseRedirect(authorizeurl) # zy: 重定向到OAuth服务商授权页面 def authorize(request): @@ -59,96 +59,96 @@ def authorize(request): manager = get_manager_by_type(type) if not manager: return HttpResponseRedirect('/') - code = request.GET.get('code', None) + code = request.GET.get('code', None) # zy: 关键参数 - OAuth服务商返回的授权码 try: - rsp = manager.get_access_token_by_code(code) + rsp = manager.get_access_token_by_code(code) # zy: 核心调用 - 使用授权码获取访问令牌 except OAuthAccessTokenException as e: - logger.warning("OAuthAccessTokenException:" + str(e)) + logger.warning("OAuthAccessTokenException:" + str(e)) # zy: 重要日志 - 令牌获取异常 return HttpResponseRedirect('/') except Exception as e: logger.error(e) rsp = None nexturl = get_redirecturl(request) if not rsp: - return HttpResponseRedirect(manager.get_authorization_url(nexturl)) - user = manager.get_oauth_userinfo() + return HttpResponseRedirect(manager.get_authorization_url(nexturl)) # zy: 失败时重新授权 + user = manager.get_oauth_userinfo() # zy: 关键调用 - 获取用户信息 if user: if not user.nickname or not user.nickname.strip(): - user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') + user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') # zy: 生成默认昵称 try: - temp = OAuthUser.objects.get(type=type, openid=user.openid) + temp = OAuthUser.objects.get(type=type, openid=user.openid) # zy: 检查是否已存在该OAuth用户 temp.picture = user.picture temp.metadata = user.metadata temp.nickname = user.nickname - user = temp + user = temp # zy: 使用已存在的用户记录 except ObjectDoesNotExist: pass # facebook的token过长 if type == 'facebook': - user.token = '' - if user.email: - with transaction.atomic(): + user.token = '' # zy: 特殊处理 - Facebook令牌过长,清空存储 + if user.email: # zy: 关键判断 - 用户有邮箱直接登录 + with transaction.atomic(): # zy: 重要 - 数据库事务保证数据一致性 author = None try: - author = get_user_model().objects.get(id=user.author_id) + author = get_user_model().objects.get(id=user.author_id) # zy: 查找已关联的用户 except ObjectDoesNotExist: pass if not author: - result = get_user_model().objects.get_or_create(email=user.email) + result = get_user_model().objects.get_or_create(email=user.email) # zy: 根据邮箱获取或创建用户 author = result[0] - if result[1]: + if result[1]: # zy: 判断是否为新创建的用户 try: get_user_model().objects.get(username=user.nickname) except ObjectDoesNotExist: - author.username = user.nickname + author.username = user.nickname # zy: 使用OAuth昵称作为用户名 else: - author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') - author.source = 'authorize' + author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') # zy: 昵称冲突时生成唯一用户名 + author.source = 'authorize' # zy: 标记用户来源 author.save() - user.author = author + user.author = author # zy: 关联OAuth用户到系统用户 user.save() oauth_user_login_signal.send( - sender=authorize.__class__, id=user.id) - login(request, author) + sender=authorize.__class__, id=user.id) # zy: 重要 - 发送登录信号 + login(request, author) # zy: 核心功能 - 登录用户 return HttpResponseRedirect(nexturl) - else: + else: # zy: 用户没有邮箱,需要补充 user.save() url = reverse('oauth:require_email', kwargs={ 'oauthid': user.id - }) + }) # zy: 生成邮箱补充页面URL - return HttpResponseRedirect(url) + return HttpResponseRedirect(url) # zy: 重定向到邮箱补充页面 else: return HttpResponseRedirect(nexturl) def emailconfirm(request, id, sign): if not sign: - return HttpResponseForbidden() + return HttpResponseForbidden() # zy: 安全拒绝 - 无签名参数 if not get_sha256(settings.SECRET_KEY + str(id) + - settings.SECRET_KEY).upper() == sign.upper(): + settings.SECRET_KEY).upper() == sign.upper(): # zy: 重要 - 验证签名防止篡改 return HttpResponseForbidden() oauthuser = get_object_or_404(OAuthUser, pk=id) with transaction.atomic(): if oauthuser.author: author = get_user_model().objects.get(pk=oauthuser.author_id) else: - result = get_user_model().objects.get_or_create(email=oauthuser.email) + result = get_user_model().objects.get_or_create(email=oauthuser.email) # zy: 创建系统用户 author = result[0] if result[1]: - author.source = 'emailconfirm' + author.source = 'emailconfirm' # zy: 标记来源为邮箱确认 author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip( - ) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') + ) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') # zy: 设置用户名 author.save() - oauthuser.author = author + oauthuser.author = author # zy: 完成关联 oauthuser.save() oauth_user_login_signal.send( sender=emailconfirm.__class__, - id=oauthuser.id) - login(request, author) + id=oauthuser.id) # zy: 发送登录信号 + login(request, author) # zy: 登录用户 site = 'http://' + get_current_site().domain content = _(''' @@ -162,22 +162,22 @@ def emailconfirm(request, id, sign): %(site)s ''') % {'oauthuser_type': oauthuser.type, 'site': site} - send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content) + send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content) # zy: 发送绑定成功邮件 url = reverse('oauth:bindsuccess', kwargs={ 'oauthid': id }) url = url + '?type=success' - return HttpResponseRedirect(url) + return HttpResponseRedirect(url) # zy: 重定向到成功页面 class RequireEmailView(FormView): - form_class = RequireEmailForm - template_name = 'oauth/require_email.html' + form_class = RequireEmailForm # zy: 使用邮箱表单类 + template_name = 'oauth/require_email.html' # zy: 模板路径 def get(self, request, *args, **kwargs): - oauthid = self.kwargs['oauthid'] + oauthid = self.kwargs['oauthid'] # zy: 获取URL参数中的OAuth用户ID oauthuser = get_object_or_404(OAuthUser, pk=oauthid) - if oauthuser.email: + if oauthuser.email: # zy: 安全检查 - 如果已有邮箱直接跳过 pass # return HttpResponseRedirect('/') @@ -187,32 +187,32 @@ class RequireEmailView(FormView): oauthid = self.kwargs['oauthid'] return { 'email': '', - 'oauthid': oauthid + 'oauthid': oauthid # zy: 初始化表单数据 } def get_context_data(self, **kwargs): oauthid = self.kwargs['oauthid'] oauthuser = get_object_or_404(OAuthUser, pk=oauthid) if oauthuser.picture: - kwargs['picture'] = oauthuser.picture + kwargs['picture'] = oauthuser.picture # zy: 传递用户头像到模板 return super(RequireEmailView, self).get_context_data(**kwargs) def form_valid(self, form): - email = form.cleaned_data['email'] + email = form.cleaned_data['email'] # zy: 获取验证后的邮箱 oauthid = form.cleaned_data['oauthid'] oauthuser = get_object_or_404(OAuthUser, pk=oauthid) - oauthuser.email = email + oauthuser.email = email # zy: 保存邮箱到OAuth用户 oauthuser.save() sign = get_sha256(settings.SECRET_KEY + - str(oauthuser.id) + settings.SECRET_KEY) + str(oauthuser.id) + settings.SECRET_KEY) # zy: 生成邮箱确认签名 site = get_current_site().domain if settings.DEBUG: - site = '127.0.0.1:8000' + site = '127.0.0.1:8000' # zy: 开发环境域名 path = reverse('oauth:email_confirm', kwargs={ 'id': oauthid, 'sign': sign }) - url = "http://{site}{path}".format(site=site, path=path) + url = "http://{site}{path}".format(site=site, path=path) # zy: 生成完整的确认链接 content = _("""

Please click the link below to bind your email

@@ -225,29 +225,29 @@ class RequireEmailView(FormView):
%(url)s """) % {'url': url} - send_email(emailto=[email, ], title=_('Bind your email'), content=content) + send_email(emailto=[email, ], title=_('Bind your email'), content=content) # zy: 发送邮箱确认邮件 url = reverse('oauth:bindsuccess', kwargs={ 'oauthid': oauthid }) - url = url + '?type=email' - return HttpResponseRedirect(url) + url = url + '?type=email' # zy: 添加类型参数 + return HttpResponseRedirect(url) # zy: 重定向到绑定成功页面 def bindsuccess(request, oauthid): - type = request.GET.get('type', None) + type = request.GET.get('type', None) # zy: 获取成功类型 oauthuser = get_object_or_404(OAuthUser, pk=oauthid) if type == 'email': title = _('Bind your email') content = _( 'Congratulations, the binding is just one step away. ' - 'Please log in to your email to check the email to complete the binding. Thank you.') + 'Please log in to your email to check the email to complete the binding. Thank you.') # zy: 等待邮箱确认提示 else: title = _('Binding successful') content = _( "Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s" " to directly log in to this website without a password. You are welcome to continue to follow this site." % { - 'oauthuser_type': oauthuser.type}) + 'oauthuser_type': oauthuser.type}) # zy: 绑定成功提示 return render(request, 'oauth/bindsuccess.html', { 'title': title, - 'content': content - }) + 'content': content # zy: 渲染成功页面 + }) \ No newline at end of file From ee8428a382832e2969d90cd28c02cb0eb008fedc Mon Sep 17 00:00:00 2001 From: zyy <1339568841@qq.com> Date: Fri, 7 Nov 2025 22:26:58 +0800 Subject: [PATCH 3/8] =?UTF-8?q?ZYY=E4=BB=A3=E7=A0=81=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/djangoblog/admin_site.py | 81 ++- src/django-master/djangoblog/apps.py | 22 +- src/django-master/djangoblog/blog_signals.py | 82 ++- .../djangoblog/elasticsearch_backend.py | 100 ++- src/django-master/djangoblog/feeds.py | 35 +- src/django-master/djangoblog/logentryadmin.py | 48 +- .../djangoblog/plugin_manage/base_plugin.py | 33 +- .../plugin_manage/hook_constants.py | 17 +- .../djangoblog/plugin_manage/hooks.py | 45 +- .../djangoblog/plugin_manage/loader.py | 19 +- src/django-master/djangoblog/settings.py | 468 +++++++------- src/django-master/djangoblog/sitemap.py | 40 +- src/django-master/djangoblog/spider_notify.py | 15 +- src/django-master/djangoblog/tests.py | 12 +- src/django-master/djangoblog/urls.py | 74 ++- src/django-master/djangoblog/utils.py | 69 +- .../djangoblog/whoosh_cn_backend.py | 595 +++++++++--------- src/django-master/djangoblog/wsgi.py | 10 +- 18 files changed, 1034 insertions(+), 731 deletions(-) diff --git a/src/django-master/djangoblog/admin_site.py b/src/django-master/djangoblog/admin_site.py index f120405..9a3bbbb 100644 --- a/src/django-master/djangoblog/admin_site.py +++ b/src/django-master/djangoblog/admin_site.py @@ -1,22 +1,25 @@ +# ZYY 导入 Django 内置的 AdminSite 和 LogEntry 模型 from django.contrib.admin import AdminSite -from django.contrib.admin.models import LogEntry -from django.contrib.sites.admin import SiteAdmin -from django.contrib.sites.models import Site - -from accounts.admin import * -from blog.admin import * -from blog.models import * -from comments.admin import * -from comments.models import * -from djangoblog.logentryadmin import LogEntryAdmin -from oauth.admin import * -from oauth.models import * -from owntracks.admin import * -from owntracks.models import * -from servermanager.admin import * -from servermanager.models import * - - +from django.contrib.admin.models import LogEntry # ZYY操作日志模型 +from django.contrib.sites.admin import SiteAdmin # ZYYDjango 内置站点管理 +from django.contrib.sites.models import Site # ZYY多站点支持模型 + +# ZYY 导入自定义应用的 admin 和 models +from accounts.admin import * #ZYY 用户账户管理 +from blog.admin import *# ZYY博客核心管理 +from blog.models import * # ZYY博客数据模型 +from comments.admin import *#ZYY 评论管理 +from comments.models import * # ZYY评论数据模型 +# ZYY 导入自定义的 LogEntryAdmin +from djangoblog.logentryadmin import LogEntryAdmin # ZYY自定义日志管理 +from oauth.admin import * # ZYY第三方登录管理 +from oauth.models import * # ZYY第三方登录模型 +from owntracks.admin import * #ZYY 位置跟踪管理 +from owntracks.models import *# ZYY位置跟踪模型 +from servermanager.admin import * #ZYY 服务器管理 +from servermanager.models import *# ZYY服务器模型 + +# ZYY 自定义 AdminSite 类 class DjangoBlogAdminSite(AdminSite): site_header = 'djangoblog administration' site_title = 'djangoblog site admin' @@ -27,6 +30,7 @@ class DjangoBlogAdminSite(AdminSite): def has_permission(self, request): return request.user.is_superuser + # ZYY 自定义 URL 的示例(已注释) # def get_urls(self): # urls = super().get_urls() # from django.urls import path @@ -37,28 +41,37 @@ class DjangoBlogAdminSite(AdminSite): # ] # return urls + my_urls - +# ZYY 实例化自定义 AdminSite admin_site = DjangoBlogAdminSite(name='admin') -admin_site.register(Article, ArticlelAdmin) -admin_site.register(Category, CategoryAdmin) -admin_site.register(Tag, TagAdmin) -admin_site.register(Links, LinksAdmin) -admin_site.register(SideBar, SideBarAdmin) -admin_site.register(BlogSettings, BlogSettingsAdmin) +# ZYY 注册 blog 应用的模型和管理类 +admin_site.register(Article, ArticlelAdmin)# ZYY文章管理 +admin_site.register(Category, CategoryAdmin) # ZYY分类管理 +admin_site.register(Tag, TagAdmin) #ZYY 标签管理 +admin_site.register(Links, LinksAdmin) # ZYY友情链接 +admin_site.register(SideBar, SideBarAdmin)# ZYY侧边栏配置 +admin_site.register(BlogSettings, BlogSettingsAdmin)# ZYY博客全局设置 + + #ZYY 注册 servermanager 应用的模型和管理类 +admin_site.register(commands, CommandsAdmin) #ZYY 命令记录 +admin_site.register(EmailSendLog, EmailSendLogAdmin)# ZYY邮件日志 -admin_site.register(commands, CommandsAdmin) -admin_site.register(EmailSendLog, EmailSendLogAdmin) +# ZYY 注册 accounts 应用的模型和管理类 +admin_site.register(BlogUser, BlogUserAdmin) # ZYY博客用户 -admin_site.register(BlogUser, BlogUserAdmin) +# ZYY 注册 comments 应用的模型和管理类 +admin_site.register(Comment, CommentAdmin)#ZYY 评论内容 -admin_site.register(Comment, CommentAdmin) -admin_site.register(OAuthUser, OAuthUserAdmin) -admin_site.register(OAuthConfig, OAuthConfigAdmin) +# ZYY 注册 oauth 应用的模型和管理类 +admin_site.register(OAuthUser, OAuthUserAdmin) #ZYY绑定账号 +admin_site.register(OAuthConfig, OAuthConfigAdmin) #ZYY 平台配置 -admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) +# ZYY 注册 owntracks 应用的模型和管理类 +admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # ZYY移动轨迹 -admin_site.register(Site, SiteAdmin) +# ZYY 注册 Django 内置的 Site 模型和管理类 +admin_site.register(Site, SiteAdmin) # ZYY多站点配置 -admin_site.register(LogEntry, LogEntryAdmin) +# ZYY 注册 Django 内置的 LogEntry 模型和自定义 LogEntryAdmin +admin_site.register(LogEntry, LogEntryAdmin) # ZYY管理操作日志 diff --git a/src/django-master/djangoblog/apps.py b/src/django-master/djangoblog/apps.py index d29e318..51c7f9f 100644 --- a/src/django-master/djangoblog/apps.py +++ b/src/django-master/djangoblog/apps.py @@ -1,11 +1,27 @@ +# ZYYDjango 应用配置类 from django.apps import AppConfig class DjangoblogAppConfig(AppConfig): + # ZYY: 指定默认主键字段类型为BigAutoField(64位自增ID) + # ZYY替代旧版AutoField(32位),适合数据量大的应用 default_auto_field = 'django.db.models.BigAutoField' + + # ZYY: 应用唯一标识,需与项目目录名一致 + # 用于Django内部识别应用(如管理后台、迁移等) name = 'djangoblog' def ready(self): - super().ready() - # Import and load plugins here + """ZYY: 应用启动时的初始化钩子 + - Django在完成应用注册后会自动调用 + - 适合执行启动时加载的任务(如插件系统、信号注册等) + - 注意:此方法可能被多次调用(特别是在开发服务器热重载时) + """ + super().ready() # 确保父类初始化逻辑执行 + + # ZYY: 插件系统加载入口 + # ZYY设计说明: + # ZYY1. 延迟导入避免循环依赖(AppConfig初始化阶段不宜大量导入) + # ZYY2. 插件系统应实现幂等性(应对ready()多次调用) + # ZYY3. 建议添加异常处理防止插件加载失败影响应用启动 from .plugin_manage.loader import load_plugins - load_plugins() \ No newline at end of file + load_plugins() \ No newline at end of file diff --git a/src/django-master/djangoblog/blog_signals.py b/src/django-master/djangoblog/blog_signals.py index 393f441..ab597cb 100644 --- a/src/django-master/djangoblog/blog_signals.py +++ b/src/django-master/djangoblog/blog_signals.py @@ -1,34 +1,41 @@ -import _thread +# ZYY信号处理与系统通知模块 +import _thread # ZYY: 使用底层线程处理耗时操作(如邮件发送),避免阻塞主请求 import logging import django.dispatch from django.conf import settings -from django.contrib.admin.models import LogEntry +from django.contrib.admin.models import LogEntry # ZYY: 排除管理后台操作日志的缓存清理 from django.contrib.auth.signals import user_logged_in, user_logged_out from django.core.mail import EmailMultiAlternatives from django.db.models.signals import post_save from django.dispatch import receiver from comments.models import Comment -from comments.utils import send_comment_email -from djangoblog.spider_notify import SpiderNotify +from comments.utils import send_comment_email # ZYY: 异步发送评论通知邮件 +from djangoblog.spider_notify import SpiderNotify # ZYY: 搜索引擎推送接口 from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache from djangoblog.utils import get_current_site from oauth.models import OAuthUser logger = logging.getLogger(__name__) -oauth_user_login_signal = django.dispatch.Signal(['id']) -send_email_signal = django.dispatch.Signal( - ['emailto', 'title', 'content']) +# ZYY: 自定义信号定义 +oauth_user_login_signal = django.dispatch.Signal(['id']) # ZYY: OAuth用户登录后处理信号 +send_email_signal = django.dispatch.Signal(['emailto', 'title', 'content']) # ZYY: 邮件发送信号 @receiver(send_email_signal) def send_email_signal_handler(sender, **kwargs): + """ZYY: 邮件发送信号处理器 + - 使用信号机制解耦邮件发送逻辑 + - 自动记录发送日志到数据库 + - 捕获异常避免影响主流程 + """ emailto = kwargs['emailto'] title = kwargs['title'] content = kwargs['content'] + # ZYY: 构造多部分邮件(支持HTML内容) msg = EmailMultiAlternatives( title, content, @@ -36,6 +43,7 @@ def send_email_signal_handler(sender, **kwargs): to=emailto) msg.content_subtype = "html" + # ZYY: 记录邮件发送日志 from servermanager.models import EmailSendLog log = EmailSendLog() log.title = title @@ -44,7 +52,7 @@ def send_email_signal_handler(sender, **kwargs): try: result = msg.send() - log.send_result = result > 0 + log.send_result = result > 0 # ZYY: 根据返回值判断是否发送成功 except Exception as e: logger.error(f"失败邮箱号: {emailto}, {e}") log.send_result = False @@ -53,62 +61,78 @@ def send_email_signal_handler(sender, **kwargs): @receiver(oauth_user_login_signal) def oauth_user_login_signal_handler(sender, **kwargs): + """ZYY: OAuth用户登录后处理 + - 自动处理头像域名适配 + - 清理侧边栏缓存 + """ id = kwargs['id'] oauthuser = OAuthUser.objects.get(id=id) site = get_current_site().domain + + # ZYY: 处理头像URL域名适配(避免混合内容警告) if oauthuser.picture and not oauthuser.picture.find(site) >= 0: from djangoblog.utils import save_user_avatar oauthuser.picture = save_user_avatar(oauthuser.picture) oauthuser.save() - delete_sidebar_cache() + delete_sidebar_cache() # ZYY: 用户信息变更后清理相关缓存 @receiver(post_save) -def model_post_save_callback( - sender, - instance, - created, - raw, - using, - update_fields, - **kwargs): +def model_post_save_callback(sender, instance, created, raw, using, update_fields, **kwargs): + """ZYY: 模型保存后通用处理器 + - 处理内容更新后的缓存清理 + - 搜索引擎URL提交 + - 评论通知的异步处理 + """ clearcache = False + + # ZYY: 排除管理后台日志对象 if isinstance(instance, LogEntry): return + + # ZYY: 处理支持URL获取的模型(如文章、页面等) if 'get_full_url' in dir(instance): - is_update_views = update_fields == {'views'} + is_update_views = update_fields == {'views'} # ZYY: 仅浏览量更新时不触发完整处理 + + # ZYY: 非测试环境且非浏览量更新时推送搜索引擎 if not settings.TESTING and not is_update_views: try: notify_url = instance.get_full_url() - SpiderNotify.baidu_notify([notify_url]) + SpiderNotify.baidu_notify([notify_url]) # ZYY: 百度站长推送 except Exception as ex: logger.error("notify sipder", ex) + if not is_update_views: - clearcache = True + clearcache = True # ZYY: 标记需要清理缓存 + # ZYY: 评论处理特别逻辑 if isinstance(instance, Comment): - if instance.is_enable: + if instance.is_enable: # ZYY: 仅处理已启用的评论 path = instance.article.get_absolute_url() site = get_current_site().domain - if site.find(':') > 0: + if site.find(':') > 0: # ZYY: 去除端口号 site = site[0:site.find(':')] + # ZYY: 清理多级缓存(文章详情页、SEO数据、评论列表等) expire_view_cache( path, servername=site, serverport=80, key_prefix='blogdetail') + if cache.get('seo_processor'): cache.delete('seo_processor') - comment_cache_key = 'article_comments_{id}'.format( - id=instance.article.id) + + comment_cache_key = 'article_comments_{id}'.format(id=instance.article.id) cache.delete(comment_cache_key) delete_sidebar_cache() delete_view_cache('article_comments', [str(instance.article.pk)]) + # ZYY: 使用独立线程发送评论通知邮件 _thread.start_new_thread(send_comment_email, (instance,)) + # ZYY: 通用缓存清理(当内容更新时) if clearcache: cache.clear() @@ -116,7 +140,11 @@ def model_post_save_callback( @receiver(user_logged_in) @receiver(user_logged_out) def user_auth_callback(sender, request, user, **kwargs): + """ZYY: 用户认证状态变化处理 + - 记录用户登录日志 + - 清理用户相关缓存 + """ if user and user.username: - logger.info(user) - delete_sidebar_cache() - # cache.clear() + logger.info(user) # ZYY: 记录用户认证行为 + delete_sidebar_cache() # ZYY: 用户状态变化后清理缓存 + # cache.clear() # ZYY: 注释掉的激进缓存清理策略 \ No newline at end of file diff --git a/src/django-master/djangoblog/elasticsearch_backend.py b/src/django-master/djangoblog/elasticsearch_backend.py index 4afe498..052b035 100644 --- a/src/django-master/djangoblog/elasticsearch_backend.py +++ b/src/django-master/djangoblog/elasticsearch_backend.py @@ -13,49 +13,83 @@ logger = logging.getLogger(__name__) class ElasticSearchBackend(BaseSearchBackend): def __init__(self, connection_alias, **connection_options): + # ZYY 初始化Elasticsearch搜索后端 + # ZYY参数: + # ZYY connection_alias: 连接别名 + # ZYY connection_options: 连接选项 super( ElasticSearchBackend, self).__init__( connection_alias, **connection_options) - self.manager = ArticleDocumentManager() - self.include_spelling = True + self.manager = ArticleDocumentManager()# ZYY 文章文档管理器实例 + self.include_spelling = True # ZYY 是否包含拼写建议 def _get_models(self, iterable): + # ZYY 获取模型列表并转换为文档格式 + # ZYY参数: + # ZYY iterable: 可迭代对象,包含要处理的模型实例 + # ZYY返回: + # ZYY 转换后的文档列表 models = iterable if iterable and iterable[0] else Article.objects.all() docs = self.manager.convert_to_doc(models) return docs def _create(self, models): + # ZYY 创建索引并重建文档 + # 参数: + # models: 要处理的模型列表 self.manager.create_index() docs = self._get_models(models) self.manager.rebuild(docs) def _delete(self, models): + # ZYY 删除模型对应的文档 + # 参数: + # models: 要删除的模型列表 for m in models: m.delete() return True def _rebuild(self, models): + # ZYY 重建索引 + # 参数: + # models: 要重建的模型列表,为空则处理所有文章 models = models if models else Article.objects.all() docs = self.manager.convert_to_doc(models) self.manager.update_docs(docs) def update(self, index, iterable, commit=True): + # ZYY 更新索引 + # 参数: + # index: 索引名称 + # iterable: 可迭代对象,包含要更新的模型实例 + # commit: 是否立即提交 models = self._get_models(iterable) self.manager.update_docs(models) def remove(self, obj_or_string): + # ZYY 从索引中移除对象 + # 参数: + # obj_or_string: 要移除的对象或字符串 models = self._get_models([obj_or_string]) self._delete(models) def clear(self, models=None, commit=True): + # ZYY 清空索引 + # 参数: + # models: 要清空的模型列表(未使用,始终清空所有) + # commit: 是否立即提交 self.remove(None) @staticmethod def get_suggestion(query: str) -> str: - """获取推荐词, 如果没有找到添加原搜索词""" + """ZYY获取推荐词, 如果没有找到添加原搜索词 + 参数: + query: 原始查询字符串 + 返回: + 推荐的搜索词字符串""" search = ArticleDocument.search() \ .query("match", body=query) \ @@ -73,17 +107,27 @@ class ElasticSearchBackend(BaseSearchBackend): @log_query def search(self, query_string, **kwargs): + # ZYY 执行搜索 + # 参数: + # query_string: 查询字符串 + # **kwargs: 其他搜索参数,包括: + # start_offset: 起始偏移量 + # end_offset: 结束偏移量 logger.info('search query_string:' + query_string) start_offset = kwargs.get('start_offset') end_offset = kwargs.get('end_offset') - # 推荐词搜索 + # ZYY推荐词搜索 if getattr(self, "is_suggest", None): suggestion = self.get_suggestion(query_string) else: suggestion = query_string + # ZYY构建复合查询: + # ZYY1. 应该匹配body或title字段 + # ZYY2. 最小匹配度为70% + # ZYY3. 过滤状态为'p'(已发布)和类型为'a'(文章) q = Q('bool', should=[Q('match', body=suggestion), Q('match', title=suggestion)], minimum_should_match="70%") @@ -95,15 +139,16 @@ class ElasticSearchBackend(BaseSearchBackend): .source(False)[start_offset: end_offset] results = search.execute() - hits = results['hits'].total + hits = results['hits'].total # ZYY 总命中数 raw_results = [] for raw_result in results['hits']['hits']: - app_label = 'blog' - model_name = 'Article' - additional_fields = {} + app_label = 'blog'# ZYY 应用标签 + model_name = 'Article'# ZYY 模型名称 + additional_fields = {}# ZYY 额外字段(当前未使用) result_class = SearchResult + # ZYY: 构建搜索结果对象并收集到列表中 result = result_class( app_label, model_name, @@ -111,25 +156,32 @@ class ElasticSearchBackend(BaseSearchBackend): raw_result['_score'], **additional_fields) raw_results.append(result) + # ZYY: 初始化分面数据和拼写建议 facets = {} spelling_suggestion = None if query_string == suggestion else suggestion - + # ZYY: 返回标准化的搜索结果结构 return { - 'results': raw_results, - 'hits': hits, - 'facets': facets, - 'spelling_suggestion': spelling_suggestion, + 'results': raw_results, # ZYY: 实际搜索结果对象列表 + 'hits': hits,# ZYY: 总命中数 + 'facets': facets,# ZYY: 分面统计数据(当前为空) + 'spelling_suggestion': spelling_suggestion,# ZYY: 拼写纠正建议 } class ElasticSearchQuery(BaseSearchQuery): + # ZYY: 将datetime对象转换为ES兼容的字符串格式 def _convert_datetime(self, date): if hasattr(date, 'hour'): - return force_str(date.strftime('%Y%m%d%H%M%S')) + return force_str(date.strftime('%Y%m%d%H%M%S')) # ZYY: 包含时间的完整格式 else: - return force_str(date.strftime('%Y%m%d000000')) + return force_str(date.strftime('%Y%m%d000000')) # ZYY: 仅日期的格式 + # ZYY: 清理用户输入的查询片段 def clean(self, query_fragment): + """ + ZYY: 提供在将用户输入呈现给后端之前进行净化的机制 + Whoosh 1.X在此有所不同,不能再使用反斜杠转义保留字符,而是应该引用整个单词 + """ """ Provides a mechanism for sanitizing user input before presenting the value to the backend. @@ -142,9 +194,11 @@ class ElasticSearchQuery(BaseSearchQuery): cleaned_words = [] for word in words: + # ZYY: 处理保留字(转为小写) if word in self.backend.RESERVED_WORDS: word = word.replace(word, word.lower()) + # ZYY: 处理保留字符(用单引号包裹整个词) for char in self.backend.RESERVED_CHARACTERS: if char in word: word = "'%s'" % word @@ -153,31 +207,35 @@ class ElasticSearchQuery(BaseSearchQuery): cleaned_words.append(word) return ' '.join(cleaned_words) - + # ZYY: 构建查询片段(这里直接返回原始查询字符串) def build_query_fragment(self, field, filter_type, value): return value.query_string +# ZYY: 获取结果总数 def get_count(self): results = self.get_results() return len(results) if results else 0 + # ZYY: 获取拼写建议 def get_spelling_suggestion(self, preferred_query=None): return self._spelling_suggestion + # ZYY: 构建查询参数 def build_params(self, spelling_query=None): kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) return kwargs - +# ZYY: 扩展ModelSearchForm以支持Elasticsearch特定功能 class ElasticSearchModelSearchForm(ModelSearchForm): def search(self): # 是否建议搜索 + # ZYY: 根据请求参数设置是否启用搜索建议 self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" - sqs = super().search() + sqs = super().search()# ZYY: 调用父类搜索方法 return sqs - +# ZYY: Elasticsearch搜索引擎实现 class ElasticSearchEngine(BaseEngine): - backend = ElasticSearchBackend - query = ElasticSearchQuery + backend = ElasticSearchBackend # ZYY: 指定使用的后端 + query = ElasticSearchQuery # ZYY: 指定使用的查询类 diff --git a/src/django-master/djangoblog/feeds.py b/src/django-master/djangoblog/feeds.py index 8c4e851..62907c1 100644 --- a/src/django-master/djangoblog/feeds.py +++ b/src/django-master/djangoblog/feeds.py @@ -6,35 +6,60 @@ from django.utils.feedgenerator import Rss201rev2Feed from blog.models import Article from djangoblog.utils import CommonMarkdown - +# ZYY: Django内置Feed类,用于生成RSS/Atom订阅源 class DjangoBlogFeed(Feed): + # ZYY: 指定使用RSS 2.0规范(支持命名空间扩展) feed_type = Rss201rev2Feed - description = '大巧无工,重剑无锋.' - title = "且听风吟 大巧无工,重剑无锋. " - link = "/feed/" + # ZYY: ================ 订阅源元数据配置 ================ + description = '大巧无工,重剑无锋.'# ZYY: 订阅源副标题/描述 + title = "且听风吟 大巧无工,重剑无锋. "# ZYY: 订阅源主标题 + link = "/feed/" # ZYY: 订阅源自引用URL(实际应为网站根URL) + # ZYY: ================ 作者信息方法 ================ + # ZYY: 注意:这些方法在每次生成feed时都会查询数据库 def author_name(self): + # ZYY: 获取站点作者昵称(潜在问题:未处理无用户情况) return get_user_model().objects.first().nickname def author_link(self): + # ZYY: 获取作者个人页面URL(假设用户模型有get_absolute_url方法) return get_user_model().objects.first().get_absolute_url() + # ZYY: ================ 订阅内容核心方法 ================ def items(self): + # ZYY: 筛选条件: + # ZYYtype='a' - 只包含文章类型(可能区分文章/页面等) + # ZYYstatus='p' - 只包含已发布状态(避免草稿泄露) + # ZYYorder_by('-pub_time') - 按发布时间降序 + # ZYY[:5] - 限制最近5篇 return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] + # ZYY: ================ 单个条目字段映射 ================ def item_title(self, item): + # ZYY: 直接使用文章标题作为条目标题 return item.title def item_description(self, item): + # ZYY: 将文章内容通过Markdown渲染后作为描述 + # 注意:可能产生XSS风险,需确保CommonMarkdown有净化处理 return CommonMarkdown.get_markdown(item.body) + # ZYY: ================ 订阅源附加信息 ================ def feed_copyright(self): + # ZYY: 动态生成版权声明(自动更新年份) now = timezone.now() return "Copyright© {year} 且听风吟".format(year=now.year) + def item_link(self, item): + # ZYY: 使用文章自身的绝对URL作为条目链接 return item.get_absolute_url() + # ZYY: ================ 条目唯一标识 ================ def item_guid(self, item): - return + # ZYY: 原代码不完整,通常应实现为: + # ZYYreturn item.get_absolute_url() # 使用URL作为唯一标识 + #ZYY 或 return str(item.id) # 使用数据库ID + # ZYY当前实现缺失会导致某些阅读器无法识别条目更新 + return # ZYY: 注意这里缺少返回值,实际使用会报错 diff --git a/src/django-master/djangoblog/logentryadmin.py b/src/django-master/djangoblog/logentryadmin.py index 2f6a535..109b7d7 100644 --- a/src/django-master/djangoblog/logentryadmin.py +++ b/src/django-master/djangoblog/logentryadmin.py @@ -9,83 +9,107 @@ from django.utils.translation import gettext_lazy as _ class LogEntryAdmin(admin.ModelAdmin): + # ZYY: 列表过滤器配置 - 允许按内容类型筛选日志 list_filter = [ 'content_type' ] + # ZYY: 搜索字段配置 - 允许按对象表示和变更消息搜索 search_fields = [ 'object_repr', 'change_message' ] + # ZYY: 可点击的列表显示字段 - 指定哪些字段可以点击进入详情页 list_display_links = [ 'action_time', 'get_change_message', ] + + # ZYY: 列表显示字段配置 - 定义在日志列表中显示的字段 list_display = [ - 'action_time', - 'user_link', - 'content_type', - 'object_link', - 'get_change_message', + 'action_time', #ZYY 操作时间 + 'user_link', # ZYY用户链接(自定义方法) + 'content_type', # ZYY内容类型 + 'object_link', # ZYY对象链接(自定义方法) + 'get_change_message', #ZYY 变更消息 ] + # ZYY: 权限控制 - 禁止通过admin添加日志条目 def has_add_permission(self, request): return False + # ZYY: 权限控制 - 允许超级用户或有特定权限的用户修改日志(但禁止POST请求) 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' + # ZYY: 权限控制 - 禁止通过admin删除日志条目 def has_delete_permission(self, request, obj=None): return False + # ZYY: 自定义方法 - 生成对象链接(如果是删除操作则显示纯文本) def object_link(self, obj): + # ZYY: 初始化为转义后的对象表示字符串(防XSS) object_link = escape(obj.object_repr) content_type = obj.content_type + # ZYY: 如果不是删除操作且有内容类型,尝试生成可点击链接 if obj.action_flag != DELETION and content_type is not None: - # try returning an actual link instead of object repr string try: + # ZYY: 构建admin修改页面的URL url = reverse( 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), args=[obj.object_id] ) + # ZYY: 生成HTML链接(后续会标记为安全) object_link = '{}'.format(url, object_link) except NoReverseMatch: + # ZYY: 如果URL反转失败,保持纯文本显示 pass + # ZYY: 标记字符串为安全HTML(因为我们已经正确转义和构建) return mark_safe(object_link) - object_link.admin_order_field = 'object_repr' - object_link.short_description = _('object') + # ZYY: 配置列表排序和描述信息 + object_link.admin_order_field = 'object_repr' # 按object_repr字段排序 + object_link.short_description = _('object') # 列标题显示为"object" + # ZYY: 自定义方法 - 生成用户链接 def user_link(self, obj): + # ZYY: 获取用户模型的内容类型 content_type = ContentType.objects.get_for_model(type(obj.user)) + # ZYY: 初始化为转义后的用户字符串表示 user_link = escape(force_str(obj.user)) try: - # try returning an actual link instead of object repr string + # ZYY: 尝试构建用户admin修改页面的URL url = reverse( 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), args=[obj.user.pk] ) + # ZYY: 生成HTML链接 user_link = '{}'.format(url, user_link) except NoReverseMatch: + # ZYY: URL反转失败时保持纯文本 pass + # ZYY: 标记为安全HTML return mark_safe(user_link) - user_link.admin_order_field = 'user' - user_link.short_description = _('user') + # ZYY: 配置列表排序和描述信息 + user_link.admin_order_field = 'user' # ZYY按user字段排序 + user_link.short_description = _('user') # ZYY列标题显示为"user" + # ZYY: 优化查询集 - 预取关联的内容类型减少查询次数 def get_queryset(self, request): queryset = super(LogEntryAdmin, self).get_queryset(request) return queryset.prefetch_related('content_type') + # ZYY: 操作控制 - 移除默认的批量删除操作 def get_actions(self, request): actions = super(LogEntryAdmin, self).get_actions(request) if 'delete_selected' in actions: del actions['delete_selected'] - return actions + return actions \ No newline at end of file diff --git a/src/django-master/djangoblog/plugin_manage/base_plugin.py b/src/django-master/djangoblog/plugin_manage/base_plugin.py index 2b4be5c..571e392 100644 --- a/src/django-master/djangoblog/plugin_manage/base_plugin.py +++ b/src/django-master/djangoblog/plugin_manage/base_plugin.py @@ -1,41 +1,60 @@ +# ZYY: 导入Python标准库的logging模块,用于记录日志 import logging +# ZYY: 获取当前模块的日志记录器 +# ZYY: __name__表示当前模块名,logging会根据模块名自动组织日志层次 logger = logging.getLogger(__name__) +# ZYY: 定义插件基类,所有插件应继承此类 class BasePlugin: - # 插件元数据 + # ZYY: 插件元数据字段声明(类属性) + # ZYY: PLUGIN_NAME: 插件名称(必须) PLUGIN_NAME = None + # ZYY: PLUGIN_DESCRIPTION: 插件描述(必须) PLUGIN_DESCRIPTION = None + # ZYY: PLUGIN_VERSION: 插件版本(必须) PLUGIN_VERSION = None + # ZYY: 构造函数,在实例化插件时自动调用 def __init__(self): + # ZYY: 检查必需的元数据是否已定义 + # ZYY: 如果任何元数据为None,则抛出ValueError异常 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.") + + # ZYY: 调用插件初始化方法 self.init_plugin() + # ZYY: 调用钩子注册方法 self.register_hooks() + # ZYY: 插件初始化方法(可被子类重写) def init_plugin(self): """ - 插件初始化逻辑 - 子类可以重写此方法来实现特定的初始化操作 + ZYY: 插件初始化逻辑 + ZYY: 子类可以重写此方法来实现特定的初始化操作 """ + # ZYY: 记录插件初始化日志 logger.info(f'{self.PLUGIN_NAME} initialized.') + # ZYY: 插件钩子注册方法(可被子类重写) def register_hooks(self): """ - 注册插件钩子 - 子类可以重写此方法来注册特定的钩子 + ZYY: 注册插件钩子 + ZYY: 子类可以重写此方法来注册特定的钩子 """ + # ZYY: 基类不实现具体钩子注册逻辑 pass + # ZYY: 获取插件信息的方法 def get_plugin_info(self): """ - 获取插件信息 + ZYY: 获取插件信息 :return: 包含插件元数据的字典 """ + # ZYY: 返回包含插件名称、描述和版本的字典 return { 'name': self.PLUGIN_NAME, 'description': self.PLUGIN_DESCRIPTION, 'version': self.PLUGIN_VERSION - } + } \ No newline at end of file diff --git a/src/django-master/djangoblog/plugin_manage/hook_constants.py b/src/django-master/djangoblog/plugin_manage/hook_constants.py index 6685b7c..789fe82 100644 --- a/src/django-master/djangoblog/plugin_manage/hook_constants.py +++ b/src/django-master/djangoblog/plugin_manage/hook_constants.py @@ -1,7 +1,20 @@ +# ZYY: 定义文章详情页加载的钩子名称常量 +# ZYY: 用于标识文章详情页加载事件,插件可监听此事件执行自定义逻辑 ARTICLE_DETAIL_LOAD = 'article_detail_load' + +# ZYY: 定义文章创建的钩子名称常量 +# ZYY: 用于标识文章创建事件,通常在文章保存前触发 ARTICLE_CREATE = 'article_create' + +# ZYY: 定义文章更新的钩子名称常量 +# ZYY: 用于标识文章更新事件,通常在文章修改后触发 ARTICLE_UPDATE = 'article_update' -ARTICLE_DELETE = 'article_delete' -ARTICLE_CONTENT_HOOK_NAME = "the_content" +# ZYY: 定义文章删除的钩子名称常量 +# ZYY: 用于标识文章删除事件,通常在文章被删除前触发 +ARTICLE_DELETE = 'article_delete' +# ZYY: 定义文章内容处理的钩子名称常量 +# ZYY: 参考WordPress的the_content过滤器概念,用于在文章内容渲染时进行处理 +# ZYY: 插件可以通过此钩子修改文章最终显示的内容 +ARTICLE_CONTENT_HOOK_NAME = "the_content" \ No newline at end of file diff --git a/src/django-master/djangoblog/plugin_manage/hooks.py b/src/django-master/djangoblog/plugin_manage/hooks.py index d712540..1108eea 100644 --- a/src/django-master/djangoblog/plugin_manage/hooks.py +++ b/src/django-master/djangoblog/plugin_manage/hooks.py @@ -1,44 +1,77 @@ +# ZYY: 导入Python标准库logging模块,用于记录钩子系统的运行日志 import logging +# ZYY: 获取当前模块的日志记录器,用于记录调试和错误信息 logger = logging.getLogger(__name__) +# ZYY: 定义全局钩子存储字典,键为钩子名称,值为回调函数列表 _hooks = {} +# ZYY: 钩子注册函数,用于将回调函数注册到指定钩子 def register(hook_name: str, callback: callable): """ - 注册一个钩子回调。 + ZYY: 注册一个钩子回调函数到指定钩子名称。 + ZYY: 如果钩子不存在则创建新列表,然后将回调添加到对应钩子的回调列表中。 + ZYY: @param hook_name: 要注册的钩子名称 + ZYY: @param callback: 要注册的回调函数(必须是可调用对象) """ + # ZYY: 检查钩子是否已存在,不存在则初始化空列表 if hook_name not in _hooks: _hooks[hook_name] = [] + + # ZYY: 将回调函数添加到对应钩子的回调列表 _hooks[hook_name].append(callback) + + # ZYY: 记录调试日志,显示成功注册的钩子和回调函数名 logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") +# ZYY: Action钩子执行函数,用于触发指定名称的所有Action钩子回调 def run_action(hook_name: str, *args, **kwargs): """ - 执行一个 Action Hook。 - 它会按顺序执行所有注册到该钩子上的回调函数。 + ZYY: 执行一个Action Hook(无返回值的钩子)。 + ZYY: 会按注册顺序依次执行所有回调函数,不处理返回值。 + ZYY: @param hook_name: 要触发的钩子名称 + ZYY: @param args: 传递给回调函数的位置参数 + ZYY: @param kwargs: 传递给回调函数的关键字参数 """ + # ZYY: 检查是否有回调注册到该钩子 if hook_name in _hooks: logger.debug(f"Running action hook '{hook_name}'") + + # ZYY: 遍历并执行所有注册的回调函数 for callback in _hooks[hook_name]: try: + # ZYY: 执行回调函数,可能抛出异常 callback(*args, **kwargs) except Exception as e: + # ZYY: 捕获并记录回调执行中的异常,避免影响其他回调 logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) +# ZYY: Filter钩子执行函数,用于触发指定名称的所有Filter钩子回调并处理返回值 def apply_filters(hook_name: str, value, *args, **kwargs): """ - 执行一个 Filter Hook。 - 它会把 value 依次传递给所有注册的回调函数进行处理。 + ZYY: 执行一个Filter Hook(有返回值的钩子)。 + ZYY: 会将初始值依次传递给所有回调函数处理,最终返回处理后的值。 + ZYY: @param hook_name: 要触发的钩子名称 + ZYY: @param value: 要处理的初始值 + ZYY: @param args: 传递给回调函数的位置参数 + ZYY: @param kwargs: 传递给回调函数的关键字参数 + ZYY: @return: 经过所有回调处理后的最终值 """ + # ZYY: 检查是否有回调注册到该钩子 if hook_name in _hooks: logger.debug(f"Applying filter hook '{hook_name}'") + + # ZYY: 遍历并执行所有注册的回调函数,每次用返回值更新value for callback in _hooks[hook_name]: try: value = callback(value, *args, **kwargs) except Exception as e: + # ZYY: 捕获并记录回调执行中的异常,避免影响其他回调 logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True) - return value + + # ZYY: 返回最终处理后的值 + return value \ No newline at end of file diff --git a/src/django-master/djangoblog/plugin_manage/loader.py b/src/django-master/djangoblog/plugin_manage/loader.py index 12e824b..8def52d 100644 --- a/src/django-master/djangoblog/plugin_manage/loader.py +++ b/src/django-master/djangoblog/plugin_manage/loader.py @@ -1,19 +1,32 @@ +# ZYY: 导入Python标准库os模块,用于处理文件和目录路径 import os +# ZYY: 导入Python标准库logging模块,用于记录插件加载过程中的日志信息 import logging +# ZYY: 导入Django配置模块,用于访问Django项目的设置参数 from django.conf import settings +# ZYY: 获取当前模块的日志记录器,用于记录插件加载的调试和错误信息 logger = logging.getLogger(__name__) +# ZYY: 定义插件加载函数,用于动态加载和初始化Django插件 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. + ZYY: 动态加载并初始化位于'plugins'目录中的插件。 + ZYY: 该函数应在Django应用注册表就绪后调用,以确保所有依赖项可用。 + ZYY: 加载过程会读取settings.ACTIVE_PLUGINS配置,并尝试加载每个指定的插件。 """ + # ZYY: 遍历settings.ACTIVE_PLUGINS中配置的所有活跃插件名称 for plugin_name in settings.ACTIVE_PLUGINS: + # ZYY: 构建插件的完整路径(目录路径) plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) + + # ZYY: 检查插件目录是否存在且包含plugin.py文件(插件主模块) if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')): try: + # ZYY: 动态导入插件模块(格式:plugins..plugin) __import__(f'plugins.{plugin_name}.plugin') + # ZYY: 记录成功加载插件的日志信息 logger.info(f"Successfully loaded plugin: {plugin_name}") except ImportError as e: - logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file + # ZYY: 捕获并记录插件导入失败的错误信息(包含异常堆栈) + logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) \ No newline at end of file diff --git a/src/django-master/djangoblog/settings.py b/src/django-master/djangoblog/settings.py index d076bb6..a226a6f 100644 --- a/src/django-master/djangoblog/settings.py +++ b/src/django-master/djangoblog/settings.py @@ -12,332 +12,344 @@ https://docs.djangoproject.com/en/1.10/ref/settings/ import os import sys from pathlib import Path - from django.utils.translation import gettext_lazy as _ - +# ZYY 将环境变量字符串转换为布尔值 def env_to_bool(env, default): - str_val = os.environ.get(env) - return default if str_val is None else str_val == 'True' - + str_val = os.environ.get(env) + return default if str_val is None else str_val == 'True' -# Build paths inside the project like this: BASE_DIR / 'subdir'. +# ZYY 在项目内部构建路径,例如:BASE_DIR / 'subdir' BASE_DIR = Path(__file__).resolve().parent.parent -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! +# ZYY 快速启动开发设置 - 不适用于生产环境 +# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ +# ZYY 安全警告: 请保密生产环境中使用的密钥! SECRET_KEY = os.environ.get( - 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' -# SECURITY WARNING: don't run with debug turned on in production! + 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@ (mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' + +# ZYY 安全警告: 不要在生产环境中开启调试模式! DEBUG = env_to_bool('DJANGO_DEBUG', True) # DEBUG = False + +# ZYY 检测是否为测试环境 TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' +# ZYY 允许的主机列表 # ALLOWED_HOSTS = [] ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] -# django 4.0新增配置 + +# ZYY Django 4.0新增配置: CSRF信任来源 CSRF_TRUSTED_ORIGINS = ['http://example.com'] -# Application definition - +# ZYY 应用定义 INSTALLED_APPS = [ - # 'django.contrib.admin', - 'django.contrib.admin.apps.SimpleAdminConfig', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'mdeditor', - 'haystack', - 'blog', - 'accounts', - 'comments', - 'oauth', - 'servermanager', - 'owntracks', - 'compressor', - 'djangoblog' + # 'django.contrib.admin', + 'django.contrib.admin.apps.SimpleAdminConfig', + 'django.contrib.auth', + 'django. contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + 'django.contrib.sitemaps', + 'mdeditor', + 'haystack', + 'blog', + 'accounts', + 'comments', + 'oauth', + ' servermanager', + 'owntracks', + 'compressor', + 'djangoblog' ] +# ZYY 中间件配置 MIDDLEWARE = [ - - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.gzip.GZipMiddleware', - # 'django.middleware.cache.UpdateCacheMiddleware', - 'django.middleware.common.CommonMiddleware', - # 'django.middleware.cache.FetchFromCacheMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.http.ConditionalGetMiddleware', - 'blog.middleware.OnlineMiddleware' + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.gzip.GZipMiddleware', + # 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.common.CommonMiddleware', + # 'django.middleware.cache.FetchFromCacheMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib. messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.http.ConditionalGetMiddleware', + 'blog.middleware.OnlineMiddleware' ] +# ZYY 根URL配置 ROOT_URLCONF = 'djangoblog.urls' +# ZYY 模板配置 TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'blog.context_processors.seo_processor' - ], - }, - }, + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR , 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'blog.context_processors.seo_processor' + ], + }, + }, ] +# ZYY WSGI应用配置 WSGI_APPLICATION = 'djangoblog.wsgi.application' -# Database -# https://docs.djangoproject.com/en/1.10/ref/settings/#databases - - +# ZYY 数据库配置 +# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', - 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', - 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', - 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', - 'PORT': int( - os.environ.get('DJANGO_MYSQL_PORT') or 3306), - 'OPTIONS': { - 'charset': 'utf8mb4'}, - }} - -# Password validation -# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators - + 'default': { + 'ENGINE': ' django.db.backends.mysql', + 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', + 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', + 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', + 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', + 'PORT': int( + os.environ.get('DJANGO_MYSQL_PORT') or 3306), + 'OPTIONS': { + 'charset': 'utf8mb4'}, + }} + +# ZYY 密码验证配置 +# ZYY 参考链接: https://docs.djang oproject.com/en/1.10/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, ] +# ZYY 语言配置 LANGUAGES = ( - ('en', _('English')), - ('zh-hans', _('Simplified Chinese')), - ('zh-hant', _('Traditional Chinese')), + ('en', _('English')), + ('zh-hans', _('Simplified Chinese')), + ('zh-hant', _('Traditional Chinese')), ) LOCALE_PATHS = ( - os.path.join(BASE_DIR, 'locale'), + os.path.join(BASE_DIR, 'locale'), ) - LANGUAGE_CODE = 'zh-hans' - TIME_ZONE = 'Asia/Shanghai' - USE_I18N = True - USE_L10N = True - USE_TZ = False -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.10/howto/static-files/ - - +# ZYY 静态文件配置(CSS, JavaScript, Images) +# ZYY 参考链接: https://docs.djangoproject.com/en/1.10/howto /static-files/ HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', - 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), - }, + 'default': { + 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', + 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), + }, } -# Automatically update searching index + +# ZYY 自动更新搜索索引 HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' -# Allow user login with username and password + +# ZYY 允许用户使用用户名和密码登录 AUTHENTICATION_BACKENDS = [ - 'accounts.user_login_backend.EmailOrUsernameModelBackend'] + 'accounts.user_login_backend.EmailOrUsernameModelBackend'] +# ZYY 静态文件根目录 STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') - STATIC_URL = '/static/' STATICFILES = os.path.join(BASE_DIR, 'static') +# ZYY 自定义用户模型 AUTH_USER_MODEL = 'accounts.BlogUser' LOGIN_URL = '/login/' - TIME_FORMAT = '%Y-%m-%d %H:%M:%S' DATE_TIME_FORMAT = '%Y-%m-%d' - -# bootstrap color styles + +# ZYY Bootstrap颜色样式 BOOTSTRAP_COLOR_TYPES = [ - 'default', 'primary', 'success', 'info', 'warning', 'danger' + 'default', 'primary', 'success', 'info', 'warning', 'danger' ] -# paginate +# ZYY 分页配置 PAGINATE_BY = 10 -# http cache timeout + +# ZYY HTTP缓存超时时间 CACHE_CONTROL_MAX_AGE = 2592000 -# cache setting + +# ZYY 缓存配置 CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'TIMEOUT': 10800, - 'LOCATION': 'unique-snowflake', - } + 'default': { + 'BACKEND': 'django.core .cache.backends.locmem.LocMemCache', + 'TIMEOUT': 10800, + 'LOCATION': 'unique-snowflake', + } } -# 使用redis作为缓存 + +# ZYY 使用redis作为缓存 if os.environ.get("DJANGO_REDIS_URL"): - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.redis.RedisCache', - 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', - } - } + CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', + } + } SITE_ID = 1 -BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ - or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' -# Email: +# ZYY 百度推送URL配置 +BAIDU_NOTIFY_URL = os.environ .get('DJANGO_BAIDU_NOTIFY_URL') \ + or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' + +# ZYY 邮件配置 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) -EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' +EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com ' EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') DEFAULT_FROM_EMAIL = EMAIL_HOST_USER SERVER_EMAIL = EMAIL_HOST_USER -# Setting debug=false did NOT handle except email notifications + +# ZYY 设置debug=false时不会处理异常邮件通知 ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] -# WX ADMIN password(Two times md5) + +# ZYY 微信管理员密码(两次md5 加密) WXADMIN = os.environ.get( - 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' + 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' +# ZYY 日志目录配置 LOG_PATH = os.path.join(BASE_DIR, 'logs') if not os.path.exists(LOG_PATH): - os.makedirs(LOG_PATH, exist_ok=True) + os.makedirs(LOG_PATH, exist_ok=True) +# ZYY 日志配置 LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'root': { - 'level': 'INFO', - 'handlers': ['console', 'log_file'], - }, - 'formatters': { - 'verbose': { - 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', - } - }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - }, - 'require_debug_true': { - '()': 'django.utils.log.RequireDebugTrue', - }, - }, - 'handlers': { - 'log_file': { - 'level': 'INFO', - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), - 'when': 'D', - 'formatter': 'verbose', - 'interval': 1, - 'delay': True, - 'backupCount': 5, - 'encoding': 'utf-8' - }, - 'console': { - 'level': 'DEBUG', - 'filters': ['require_debug_true'], - 'class': 'logging.StreamHandler', - 'formatter': 'verbose' - }, - 'null': { - 'class': 'logging.NullHandler', - }, - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'djangoblog': { - 'handlers': ['log_file', 'console'], - 'level': 'INFO', - 'propagate': True, - }, - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': False, - } - } + 'version': 1, + 'disable_existing_loggers': False, + 'root': { + 'level': 'INFO', + 'handlers': ['console', 'log_file'], + }, + 'formatters': { + 'verbose': { + 'format': '[%( asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', + } + }, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + 'handlers': { + 'log_file': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), + 'when': 'D', + 'formatter': 'verbose', + 'interval': 1, + 'delay': True , + 'backupCount': 5, + 'encoding': 'utf-8' + }, + 'console': { + 'level': 'DEBUG', + 'filters': ['require_debug_true'], + 'class': 'logging.StreamHandler', + 'formatter': 'verbose' + }, + 'null': { + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'djangoblog': { + 'handlers': ['log_file', 'console'], + 'level': 'INFO', + 'propagate': True, + }, + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + } + } } +# ZYY 静态文件查找器配置 STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - # other - 'compressor.finders.CompressorFinder', -) + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # other + 'compressor.finders.CompressorFinder', +) + +# ZYY 压缩配置 COMPRESS_ENABLED = True # COMPRESS_OFFLINE = True - - COMPRESS_CSS_FILTERS = [ - # creates absolute urls from relative ones - 'compressor.filters.css_default.CssAbsoluteFilter', - # css minimizer - 'compressor.filters.cssmin.CSSMinFilter' + # 创建绝对URL + 'compressor.filters.css_default.CssAbsoluteFilter', + # CSS压缩 + 'compressor.filters.cssmin.CSSMinFilter' ] COMPRESS_JS_FILTERS = [ - 'compressor.filters.jsmin.JSMinFilter' + 'compressor.filters.jsmin.JSMinFilter' ] +# ZYY 媒体文件配置 MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') MEDIA_URL = '/media/' + +# ZYY X-Frame-Options配置 X_FRAME_OPTIONS = 'SAMEORIGIN' +# ZYY 默认自增字段类型 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# ZYY Elasticsearch配置 if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): - ELASTICSEARCH_DSL = { - 'default': { - 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') - }, - } - HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', - }, - } - -# Plugin System + ELASTICSEARCH_DSL = { + 'default': { + 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_ HOST') + }, + } + HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', + }, + } + +# ZYY 插件系统配置 PLUGINS_DIR = BASE_DIR / 'plugins' ACTIVE_PLUGINS = [ - 'article_copyright', - 'reading_time', - 'external_links', - 'view_count', - 'seo_optimizer' + 'article_copyright', + 'reading_time', + ' external_links', + 'view_count', + 'seo_optimizer' ] \ No newline at end of file diff --git a/src/django-master/djangoblog/sitemap.py b/src/django-master/djangoblog/sitemap.py index 8b7d446..5c0e70b 100644 --- a/src/django-master/djangoblog/sitemap.py +++ b/src/django-master/djangoblog/sitemap.py @@ -1,59 +1,73 @@ +# ZYY 导入必要的模块和类 from django.contrib.sitemaps import Sitemap from django.urls import reverse - from blog.models import Article, Category, Tag - +# ZYY 定义静态视图站点地图类,用于生成博客首页等静态页面的站点地图 class StaticViewSitemap(Sitemap): - priority = 0.5 - changefreq = 'daily' + priority = 0.5 # ZYY 优先级设置为0.5 + changefreq = 'daily' # ZYY 更新频率设置为每天 def items(self): + # ZYY 返回需要生成站点地图的URL名称列表,这里只有博客首页 return ['blog:index', ] def location(self, item): + # ZYY 根据URL名称生成具体的URL地址 return reverse(item) +# ZYY 定义文章站点地图类,用于生成文章页面的站点地图 class ArticleSiteMap(Sitemap): - changefreq = "monthly" - priority = "0.6" + changefreq = "monthly" # ZYY 更新频率设置为每月 + priority = "0.6" # ZYY 优先级设置为0.6 def items(self): + # ZYY 返回所有状态为'p'(已发布)的文章对象 return Article.objects.filter(status='p') def lastmod(self, obj): + # ZYY 返回文章的最后修改时间 return obj.last_modify_time +# ZYY 定义分类站点地图类,用于生成分类页面的站点地图 class CategorySiteMap(Sitemap): - changefreq = "Weekly" - priority = "0.6" + changefreq = "Weekly" # ZYY 更新频率设置为每周 + priority = "0.6" # ZYY 优先级设置为0.6 def items(self): + # ZYY 返回所有的分类对象 return Category.objects.all() def lastmod(self, obj): + # ZYY 返回分类的最后修改时间 return obj.last_modify_time +# ZYY 定义标签站点地图类,用于生成标签页面的站点地图 class TagSiteMap(Sitemap): - changefreq = "Weekly" - priority = "0.3" + changefreq = "Weekly" # ZYY 更新频率设置为每周 + priority = "0.3" # ZYY 优先级设置为0.3 def items(self): + # ZYY 返回所有的标签对象 return Tag.objects.all() def lastmod(self, obj): + # ZYY 返回标签的最后修改时间 return obj.last_modify_time +# ZYY 定义用户站点地图类,用于生成用户页面的站点地图 class UserSiteMap(Sitemap): - changefreq = "Weekly" - priority = "0.3" + changefreq = "Weekly" # ZYY 更新频率设置为每周 + priority = "0.3" # ZYY 优先级设置为0.3 def items(self): + # ZYY 返回所有文章的作者集合(去重) return list(set(map(lambda x: x.author, Article.objects.all()))) def lastmod(self, obj): - return obj.date_joined + # ZYY 返回用户的注册时间作为最后修改时间 + return obj.date_joined \ No newline at end of file diff --git a/src/django-master/djangoblog/spider_notify.py b/src/django-master/djangoblog/spider_notify.py index 7b909e9..12723a1 100644 --- a/src/django-master/djangoblog/spider_notify.py +++ b/src/django-master/djangoblog/spider_notify.py @@ -1,21 +1,32 @@ +# ZYY 导入日志模块,用于记录程序运行状态 import logging - +# ZYY 导入requests库,用于发送HTTP请求 import requests +# ZYY 导入Django配置,用于获取项目设置 from django.conf import settings +# ZYY 获取当前模块的日志记录器 logger = logging.getLogger(__name__) +# ZYY 定义蜘蛛通知类,用于向搜索引擎提交URL通知 class SpiderNotify(): @staticmethod + # ZYY 静态方法,用于通知百度蜘蛛抓取指定URL列表 def baidu_notify(urls): try: + # ZYY 将URL列表拼接成字符串,每个URL占一行 data = '\n'.join(urls) + # ZYY 向百度推送接口发送POST请求,提交URL数据 result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) + # ZYY 记录百度接口返回的结果(成功响应) logger.info(result.text) except Exception as e: + # ZYY 捕获并记录所有异常(如网络错误、接口错误等) logger.error(e) @staticmethod + # ZYY 静态方法,提供统一的通知接口(目前仅调用百度通知) def notify(url): - SpiderNotify.baidu_notify(url) + # ZYY 调用百度通知方法(注意:这里参数名是url,但实际传入的是单个URL,可能存在命名歧义) + SpiderNotify.baidu_notify(url) \ No newline at end of file diff --git a/src/django-master/djangoblog/tests.py b/src/django-master/djangoblog/tests.py index 01237d9..4efac59 100644 --- a/src/django-master/djangoblog/tests.py +++ b/src/django-master/djangoblog/tests.py @@ -5,11 +5,18 @@ from djangoblog.utils import * class DjangoBlogTest(TestCase): def setUp(self): + # ZYY: 测试用例初始化方法,当前为空实现 pass def test_utils(self): + # ZYY: 测试utils模块中的工具函数 + + # ZYY: 测试get_sha256函数,验证字符串'test'的sha256哈希值不为空 md5 = get_sha256('test') self.assertIsNotNone(md5) + + # ZYY: 测试CommonMarkdown类的get_markdown方法 + # ZYY: 输入包含标题、代码块和链接的Markdown文本,验证输出不为空 c = CommonMarkdown.get_markdown(''' # Title1 @@ -24,9 +31,12 @@ class DjangoBlogTest(TestCase): ''') self.assertIsNotNone(c) + + # ZYY: 测试parse_dict_to_url函数 + # ZYY: 创建测试字典,验证字典转URL参数字符串的功能 d = { 'd': 'key1', 'd2': 'key2' } data = parse_dict_to_url(d) - self.assertIsNotNone(data) + self.assertIsNotNone(data)# ZYY: 验证转换结果不为空 diff --git a/src/django-master/djangoblog/urls.py b/src/django-master/djangoblog/urls.py index 4aae58a..523478e 100644 --- a/src/django-master/djangoblog/urls.py +++ b/src/django-master/djangoblog/urls.py @@ -13,52 +13,92 @@ Including another URLconf 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ +# ZYY: 导入Django基础配置模块 from django.conf import settings +# ZYY: 导入国际化URL模式工具 from django.conf.urls.i18n import i18n_patterns +# ZYY: 导入静态文件URL处理工具 from django.conf.urls.static import static +# ZYY: 导入站点地图视图 from django.contrib.sitemaps.views import sitemap +# ZYY: 导入URL路径处理函数 from django.urls import path, include +# ZYY: 导入正则表达式URL模式 from django.urls import re_path +# ZYY: 导入Haystack搜索视图工厂 from haystack.views import search_view_factory +# ZYY: 导入自定义视图和模块 from blog.views import EsSearchView from djangoblog.admin_site import admin_site from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm from djangoblog.feeds import DjangoBlogFeed from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap +# ZYY: 定义站点地图配置,包含各类内容的站点地图 sitemaps = { - - 'blog': ArticleSiteMap, - 'Category': CategorySiteMap, - 'Tag': TagSiteMap, - 'User': UserSiteMap, - 'static': StaticViewSitemap + 'blog': ArticleSiteMap, # ZYY: 文章站点地图 + 'Category': CategorySiteMap, # ZYY: 分类站点地图 + 'Tag': TagSiteMap, # ZYY: 标签站点地图 + 'User': UserSiteMap, # ZYY: 用户站点地图 + 'static': StaticViewSitemap # ZYY: 静态页面站点地图 } -handler404 = 'blog.views.page_not_found_view' -handler500 = 'blog.views.server_error_view' -handle403 = 'blog.views.permission_denied_view' +# ZYY: 定义自定义错误处理视图 +handler404 = 'blog.views.page_not_found_view' # ZYY: 404错误处理 +handler500 = 'blog.views.server_error_view' # ZYY: 500错误处理 +handle403 = 'blog.views.permission_denied_view' # ZYY: 403错误处理 +# ZYY: 基础URL模式配置 urlpatterns = [ + # ZYY: 国际化URL前缀处理 path('i18n/', include('django.conf.urls.i18n')), ] + +# ZYY: 添加国际化URL模式 urlpatterns += i18n_patterns( - re_path(r'^admin/', admin_site.urls), + # ZYY: 管理后台URL(使用自定义admin_site) + re_path(r'admin/', admin_site.urls), + + # ZYY: 博客应用URL(命名空间为blog) re_path(r'', include('blog.urls', namespace='blog')), + + # ZYY: Markdown编辑器URL re_path(r'mdeditor/', include('mdeditor.urls')), + + # ZYY: 评论系统URL(命名空间为comment) re_path(r'', include('comments.urls', namespace='comment')), + + # ZYY: 账户系统URL(命名空间为account) re_path(r'', include('accounts.urls', namespace='account')), + + # ZYY: 第三方登录URL(命名空间为oauth) re_path(r'', include('oauth.urls', namespace='oauth')), - re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, + + # ZYY: 站点地图URL(生成sitemap.xml) + re_path(r'sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'), - re_path(r'^feed/$', DjangoBlogFeed()), - re_path(r'^rss/$', DjangoBlogFeed()), - re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), + + # ZYY: RSS订阅URL(两种访问路径) + re_path(r'feed/$', DjangoBlogFeed()), + re_path(r'rss/$', DjangoBlogFeed()), + + # ZYY: 搜索功能URL(使用Elasticsearch搜索视图) + re_path('search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), name='search'), + + # ZYY: 服务器管理URL(命名空间为servermanager) re_path(r'', include('servermanager.urls', namespace='servermanager')), - re_path(r'', include('owntracks.urls', namespace='owntracks')) - , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + + # ZYY: OwnTracks应用URL(命名空间为owntracks) + re_path(r'', include('owntracks.urls', namespace='owntracks')), + + # ZYY: 禁用默认语言前缀(prefix_default_language=False) + prefix_default_language=False +) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # ZYY: 添加静态文件URL处理 + +# ZYY: 开发环境专用配置(DEBUG模式下) if settings.DEBUG: + # ZYY: 添加媒体文件URL处理(仅开发环境) urlpatterns += static(settings.MEDIA_URL, - document_root=settings.MEDIA_ROOT) + document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/src/django-master/djangoblog/utils.py b/src/django-master/djangoblog/utils.py index 57f63dc..7749382 100644 --- a/src/django-master/djangoblog/utils.py +++ b/src/django-master/djangoblog/utils.py @@ -8,7 +8,6 @@ import random import string import uuid from hashlib import sha256 - import bleach import markdown import requests @@ -16,21 +15,20 @@ from django.conf import settings from django.contrib.sites.models import Site from django.core.cache import cache from django.templatetags.static import static - logger = logging.getLogger(__name__) - +# ZYY: 获取最新的文章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) - + from blog.models import Article + from comments.models import Comment + return (Article.objects.latest().pk, Comment.objects.latest().pk) +# ZYY: 获取字符串的SHA256哈希值 def get_sha256(str): m = sha256(str.encode('utf-8')) return m.hexdigest() - +# ZYY: 缓存装饰器 ,用于缓存函数结果 def cache_decorator(expiration=3 * 60): def wrapper(func): def news(*args, **kwargs): @@ -41,7 +39,6 @@ def cache_decorator(expiration=3 * 60): key = None if not key: unique_str = repr((func, args, kwargs)) - m = sha256(unique_str.encode('utf-8')) key = m.hexdigest() value = cache.get(key) @@ -53,7 +50,7 @@ def cache_decorator(expiration=3 * 60): return value else: logger.debug( - 'cache_decorator set cache:%s key:%s' % + 'cache_decorator set cache:%s key:%s' % (func.__name__, key)) value = func(*args, **kwargs) if value is None: @@ -61,44 +58,40 @@ def cache_decorator(expiration=3 * 60): else: cache.set(key, value, expiration) return value - return news - return wrapper - +# ZYY: 刷新视图缓存 def expire_view_cache(path, servername, serverport, key_prefix=None): ''' 刷新视图缓存 :param path:url路径 :param servername:host :param serverport:端口 - :param key_prefix:前缀 + : param key_prefix:前缀 :return:是否成功 ''' from django.http import HttpRequest from django.utils.cache import get_cache_key - request = HttpRequest() - request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport} + request.META = {'SERVER_NAME': servername , 'SERVER_PORT': serverport} request.path = path - key = get_cache_key(request, key_prefix=key_prefix, cache=cache) if key: logger.info('expire_view_cache:get key:{path}'.format(path=path)) if cache.get(key): cache.delete(key) - return True + return True return False - +# ZYY: 使用缓存装饰器获取当前站点信息 @cache_decorator() def get_current_site(): - site = Site.objects.get_current() + site = Site.objects.get_current () return site - class CommonMarkdown: + # ZYY: 将Markdown文本转换为HTML,并生成目录 @staticmethod def _convert_markdown(value): md = markdown.Markdown( @@ -113,17 +106,19 @@ class CommonMarkdown: toc = md.toc return body, toc + # ZYY: 获取带有目录的Markdown转换结果 @staticmethod def get_markdown_with_toc(value): - body, toc = CommonMarkdown._convert_markdown(value) + body, toc = CommonMarkdown._convert_markdown(value) return body, toc + # ZYY: 获取Markdown转换结果(不带目录) @staticmethod def get_markdown(value): body, toc = CommonMarkdown._convert_markdown(value) return body - +# ZYY: 发送邮件 def send_email(emailto, title, content): from djangoblog.blog_signals import send_email_signal send_email_signal.send( @@ -132,19 +127,19 @@ def send_email(emailto, title, content): title=title, content=content) - +# ZYY: 生成随机数验证码 def generate_code() -> str: """生成随机数验证码""" - return ''.join(random.sample(string.digits, 6)) - + return ''.join(random .sample(string.digits, 6)) +# ZYY: 将字典解析为URL字符串 def parse_dict_to_url(dict): from urllib.parse import quote url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) for k, v in dict.items()]) return url - +# ZYY: 获取博客设置,如果缓存中没有则从数据库加载并设置缓存 def get_blog_setting(): value = cache.get('get_blog_setting') if value: @@ -163,7 +158,7 @@ def get_blog_setting(): setting.show_google_adsense = False setting.open_site_comment = True setting.analytics_code = '' - setting.beian_code = '' + setting.beian_code = '' setting.show_gongan_code = False setting.comment_need_review = False setting.save() @@ -172,7 +167,7 @@ def get_blog_setting(): cache.set('get_blog_setting', value) return value - +# ZYY: 保存用户头像到本地 def save_user_avatar(url): ''' 保存用户头像 @@ -180,14 +175,12 @@ def save_user_avatar(url): :return: 本地路径 ''' logger.info(url) - try: basedir = os.path.join(settings.STATICFILES, 'avatar') rsp = requests.get(url, timeout=2) if rsp.status_code == 200: if not os.path.exists(basedir): os.makedirs(basedir) - image_extensions = ['.jpg', '.png', 'jpeg', '.gif'] isimage = len([i for i in image_extensions if url.endswith(i)]) > 0 ext = os.path.splitext(url)[1] if isimage else '.jpg' @@ -195,12 +188,12 @@ def save_user_avatar(url): logger.info('保存用户头像:' + basedir + save_filename) with open(os.path.join(basedir, save_filename), 'wb+') as file: file.write(rsp.content) - return static('avatar/' + save_filename) + return static('avatar /' + save_filename) except Exception as e: logger.error(e) return static('blog/img/avatar.png') - +# ZYY: 删除侧边栏缓存 def delete_sidebar_cache(): from blog.models import LinkShowType keys = ["sidebar" + x for x in LinkShowType.values] @@ -208,13 +201,13 @@ def delete_sidebar_cache(): logger.info('delete sidebar key:' + k) cache.delete(k) - +# ZYY: 删除视图缓存 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) - +# ZYY: 获取静态资源URL def get_resource_url(): if settings.STATIC_URL: return settings.STATIC_URL @@ -222,11 +215,11 @@ def get_resource_url(): site = get_current_site() return 'http://' + site.domain + '/static/' - +# ZYY: 允许的HTML标签和属性 ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', 'h2', 'p'] ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} - +# ZYY: 清理HTML,只保留允许的标签和属性 def sanitize_html(html): - return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) + return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) \ No newline at end of file diff --git a/src/django-master/djangoblog/whoosh_cn_backend.py b/src/django-master/djangoblog/whoosh_cn_backend.py index 04e3f7f..09ec789 100644 --- a/src/django-master/djangoblog/whoosh_cn_backend.py +++ b/src/django-master/djangoblog/whoosh_cn_backend.py @@ -1,7 +1,7 @@ # encoding: utf-8 from __future__ import absolute_import, division, print_function, unicode_literals - +# ZYY: 导入必要的模块和函数 import json import os import re @@ -34,12 +34,14 @@ from whoosh.qparser import QueryParser from whoosh.searching import ResultsPage from whoosh.writing import AsyncWriter +# ZYY: 尝试导入whoosh模块,如果失败则抛出异常 try: import whoosh except ImportError: raise MissingDependency( "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") +# ZYY: 检查whoosh版本是否满足要求 # Handle minimum requirement. if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): raise MissingDependency( @@ -52,16 +54,20 @@ DATETIME_REGEX = re.compile( LOCALS = threading.local() LOCALS.RAM_STORE = None - +# ZYY: 自定义的HtmlFormatter类,用于在whoosh中统一结果格式 class WhooshHtmlFormatter(HtmlFormatter): """ This is a HtmlFormatter simpler than the whoosh.HtmlFormatter. We use it to have consistent results across backends. Specifically, Solr, Xapian and Elasticsearch are using this formatting. """ + """ + ZYY: 这是一个比whoosh.HtmlFormatter更简单的HtmlFormatter。 + 我们使用它来确保不同后端(如Solr, Xapian和Elasticsearch)之间的结果一致性。 + """ template = '<%(tag)s>%(t)s' - +# ZYY: Whoosh搜索后端类 class WhooshSearchBackend(BaseSearchBackend): # Word reserved by Whoosh for special use. RESERVED_WORDS = ( @@ -70,7 +76,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'OR', 'TO', ) - + # ZYY: Whoosh保留的特殊用途字符 # Characters reserved by Whoosh for special use. # The '\\' must come first, so as not to overwrite the other slash # replacements. @@ -80,6 +86,7 @@ class WhooshSearchBackend(BaseSearchBackend): ) def __init__(self, connection_alias, **connection_options): + # ZYY: 初始化Whoosh搜索后端 super( WhooshSearchBackend, self).__init__( @@ -107,9 +114,13 @@ class WhooshSearchBackend(BaseSearchBackend): """ Defers loading until needed. """ + """ + ZYY: 延迟加载直到需要时才加载 + """ from haystack import connections new_index = False + # ZYY: 确保索引存在 # Make sure the index is there. if self.use_file_storage and not os.path.exists(self.path): os.makedirs(self.path) @@ -144,12 +155,14 @@ class WhooshSearchBackend(BaseSearchBackend): self.setup_complete = True +# ZYY: 构建索引模式 def build_schema(self, fields): schema_fields = { ID: WHOOSH_ID(stored=True, unique=True), DJANGO_CT: WHOOSH_ID(stored=True), DJANGO_ID: WHOOSH_ID(stored=True), } + # ZYY: 获取Haystack中硬编码的键数量,用于后续的错误处理 # Grab the number of keys that are hard-coded into Haystack. # We'll use this to (possibly) fail slightly more gracefully later. initial_key_count = len(schema_fields) @@ -173,6 +186,7 @@ class WhooshSearchBackend(BaseSearchBackend): schema_fields[field_class.index_fieldname] = NUMERIC( stored=field_class.stored, numtype=float, field_boost=field_class.boost) elif field_class.field_type == 'boolean': + # ZYY: BOOLEAN字段不支持field_boost # Field boost isn't supported on BOOLEAN as of 1.8.2. schema_fields[field_class.index_fieldname] = BOOLEAN( stored=field_class.stored) @@ -184,13 +198,14 @@ class WhooshSearchBackend(BaseSearchBackend): stored=field_class.stored, field_boost=field_class.boost) else: + # ZYY: 使用中文分析器 # schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True) schema_fields[field_class.index_fieldname] = TEXT( stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True) if field_class.document is True: content_field_name = field_class.index_fieldname schema_fields[field_class.index_fieldname].spelling = True - + # ZYY: 如果没有找到任何字段,则抛出错误 # Fail more gracefully than relying on the backend to die if no fields # are found. if len(schema_fields) <= initial_key_count: @@ -198,7 +213,7 @@ class WhooshSearchBackend(BaseSearchBackend): "No fields were found in any search_indexes. Please correct this before attempting to search.") return (content_field_name, Schema(**schema_fields)) - + # ZYY: 更新索引 def update(self, index, iterable, commit=True): if not self.setup_complete: self.setup() @@ -212,11 +227,13 @@ class WhooshSearchBackend(BaseSearchBackend): except SkipDocument: self.log.debug(u"Indexing for object `%s` skipped", obj) else: + # ZYY: 确保所有数据都是unicode格式 # Really make sure it's unicode, because Whoosh won't have it any # other way. for key in doc: doc[key] = self._from_python(doc[key]) + # ZYY: Whoosh 2.5.0+不支持文档boost # Document boosts aren't supported in Whoosh 2.5.0+. if 'boost' in doc: del doc['boost'] @@ -227,6 +244,7 @@ class WhooshSearchBackend(BaseSearchBackend): if not self.silently_fail: raise + # ZYY: 记录错误信息 # We'll log the object identifier but won't include the actual object # to avoid the possibility of that generating encoding errors while # processing the log message: @@ -240,10 +258,12 @@ class WhooshSearchBackend(BaseSearchBackend): "object": get_identifier(obj)}}) if len(iterable) > 0: + # ZYY: 无论如何都提交更改,以避免锁定问题 # For now, commit no matter what, as we run into locking issues # otherwise. writer.commit() + # ZYY: 从索引中删除文档 def remove(self, obj_or_string, commit=True): if not self.setup_complete: self.setup() @@ -266,6 +286,7 @@ class WhooshSearchBackend(BaseSearchBackend): e, exc_info=True) + # ZYY: 清空索引 def clear(self, models=None, commit=True): if not self.setup_complete: self.setup() @@ -302,8 +323,9 @@ class WhooshSearchBackend(BaseSearchBackend): else: self.log.error( "Failed to clear Whoosh index: %s", e, exc_info=True) - + # ZYY: 删除索引文件 def delete_index(self): + # ZYY: 根据Whoosh邮件列表的建议,如果要从索引中删除所有内容,更高效的方法是直接删除索引文件 # Per the Whoosh mailing list, if wiping out everything from the index, # it's much more efficient to simply delete the index files. if self.use_file_storage and os.path.exists(self.path): @@ -311,9 +333,11 @@ class WhooshSearchBackend(BaseSearchBackend): elif not self.use_file_storage: self.storage.clean() + # ZYY: 重新创建索引 # Recreate everything. self.setup() - + + # ZYY: 优化索引 def optimize(self): if not self.setup_complete: self.setup() @@ -321,12 +345,15 @@ class WhooshSearchBackend(BaseSearchBackend): self.index = self.index.refresh() self.index.optimize() + # ZYY: 计算分页 def calculate_page(self, start_offset=0, end_offset=None): # Prevent against Whoosh throwing an error. Requires an end_offset # greater than 0. + # ZYY: 防止Whoosh抛出错误,要求end_offset大于0 if end_offset is not None and end_offset <= 0: end_offset = 1 - + + # ZYY: 确定页码 # Determine the page. page_num = 0 @@ -341,6 +368,7 @@ class WhooshSearchBackend(BaseSearchBackend): if page_length and page_length > 0: page_num = int(start_offset / page_length) + # ZYY: Whoosh使用1-based的页码 # Increment because Whoosh uses 1-based page numbers. page_num += 1 return page_num, page_length @@ -368,7 +396,8 @@ class WhooshSearchBackend(BaseSearchBackend): **kwargs): if not self.setup_complete: self.setup() - + + # ZYY: 空查询应该返回无结果 # A zero length query should return no results. if len(query_string) == 0: return { @@ -377,7 +406,7 @@ class WhooshSearchBackend(BaseSearchBackend): } query_string = force_str(query_string) - + # ZYY: 单字符查询(非通配符)会被停用词过滤器过滤掉,应该返回无结果 # A one-character query (non-wildcard) gets nabbed by a stopwords # filter and should yield zero results. if len(query_string) <= 1 and query_string != u'*': @@ -389,6 +418,7 @@ class WhooshSearchBackend(BaseSearchBackend): reverse = False if sort_by is not None: + # ZYY: 确定是否需要反转结果,以及Whoosh是否能处理排序要求 # Determine if we need to reverse the results and if Whoosh can # handle what it's being asked to sort by. Reversing is an # all-or-nothing action, unfortunately. @@ -445,6 +475,7 @@ class WhooshSearchBackend(BaseSearchBackend): if models and len(models): model_choices = sorted(get_model_ct(model) for model in models) elif limit_to_registered_models: + # ZYY: 使用窄查询限制结果到当前路由器处理的模型 # Using narrow queries, limit the results to only models handled # with the current routers. model_choices = self.build_models_list() @@ -461,6 +492,7 @@ class WhooshSearchBackend(BaseSearchBackend): narrow_searcher = None if narrow_queries is not None: + # ZYY: 潜在的高开销操作 # Potentially expensive? I don't see another way to do it in # Whoosh... narrow_searcher = self.index.searcher() @@ -486,6 +518,7 @@ class WhooshSearchBackend(BaseSearchBackend): searcher = self.index.searcher() parsed_query = self.parser.parse(query_string) + # ZYY: 处理无效/停用词查询,优雅恢复 # In the event of an invalid/stopworded query, recover gracefully. if parsed_query is None: return { @@ -502,6 +535,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'reverse': reverse, } + # ZYY: 处理已缩小范围的查询结果 # Handle the case where the results have been narrowed. if narrowed_results is not None: search_kwargs['filter'] = narrowed_results @@ -521,7 +555,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'hits': 0, 'spelling_suggestion': None, } - + # ZYY: Whoosh 2.5.1如果请求的页码过高,会返回错误的页码 # Because as of Whoosh 2.5.1, it will return the wrong page of # results if you request something too high. :( if raw_page.pagenum < page_num: @@ -559,7 +593,7 @@ class WhooshSearchBackend(BaseSearchBackend): 'hits': 0, 'spelling_suggestion': spelling_suggestion, } - + # ZYY: 查找类似文档 def more_like_this( self, model_instance, @@ -572,7 +606,7 @@ class WhooshSearchBackend(BaseSearchBackend): **kwargs): if not self.setup_complete: self.setup() - +# ZYY: 延迟模型将具有不同的类名("RealClass_Deferred_fieldname"),该类名不会在我们的注册表中 # Deferred models will have a different class ("RealClass_Deferred_fieldname") # which won't be in our registry: model_klass = model_instance._meta.concrete_model @@ -589,6 +623,7 @@ class WhooshSearchBackend(BaseSearchBackend): if models and len(models): model_choices = sorted(get_model_ct(model) for model in models) elif limit_to_registered_models: + # ZYY: 使用窄查询限制结果到当前路由器处理的模型 # Using narrow queries, limit the results to only models handled # with the current routers. model_choices = self.build_models_list() @@ -608,6 +643,7 @@ class WhooshSearchBackend(BaseSearchBackend): narrow_searcher = None if narrow_queries is not None: + # ZYY: 潜在的高开销操作 # Potentially expensive? I don't see another way to do it in # Whoosh... narrow_searcher = self.index.searcher() @@ -642,10 +678,11 @@ class WhooshSearchBackend(BaseSearchBackend): raw_results = results[0].more_like_this( field_name, top=end_offset) + # ZYY: 处理已缩小范围的查询结果 # Handle the case where the results have been narrowed. if narrowed_results is not None and hasattr(raw_results, 'filter'): raw_results.filter(narrowed_results) - + # ZYY: Whoosh 2.5.1如果请求的页码过高,会返回错误的页码 try: raw_page = ResultsPage(raw_results, page_num, page_length) except ValueError: @@ -674,7 +711,7 @@ class WhooshSearchBackend(BaseSearchBackend): narrow_searcher.close() return results - +# ZYY: 处理查询结果 def _process_results( self, raw_page, @@ -684,174 +721,153 @@ class WhooshSearchBackend(BaseSearchBackend): result_class=None): from haystack import connections results = [] - + # ZYY: 获取命中数 # It's important to grab the hits first before slicing. Otherwise, this # can cause pagination failures. hits = len(raw_page) - if result_class is None: - result_class = SearchResult - - facets = {} - spelling_suggestion = None - unified_index = connections[self.connection_alias].get_unified_index() - indexed_models = unified_index.get_indexed_models() - - for doc_offset, raw_result in enumerate(raw_page): - score = raw_page.score(doc_offset) or 0 - app_label, model_name = raw_result[DJANGO_CT].split('.') - additional_fields = {} - model = haystack_get_model(app_label, model_name) - - if model and model in indexed_models: - for key, value in raw_result.items(): - index = unified_index.get_index(model) - string_key = str(key) - - if string_key in index.fields and hasattr( - index.fields[string_key], 'convert'): - # Special-cased due to the nature of KEYWORD fields. - if index.fields[string_key].is_multivalued: - if value is None or len(value) == 0: - additional_fields[string_key] = [] - else: - additional_fields[string_key] = value.split( - ',') - else: - additional_fields[string_key] = index.fields[string_key].convert( - value) + # ZYY: 如果result_class为None,则将其设置为SearchResult +if result_class is None: + result_class = SearchResult +facets = {} +spelling_suggestion = None +# ZYY: 获取统一索引 +unified_index = connections[self.connection_alias].get_unified_index() +# ZYY: 获取已索引的模型列表 +indexed_models = unified_index.get_indexed_models() +for doc_offset, raw_result in enumerate(raw_page): + # ZYY: 获取文档得分,如果没有则为0 + score = raw_page.score(doc_offset) or 0 + app_label, model_name = raw_result[DJANGO_CT].split('.') + additional_fields = {} + # ZYY: 根据app_label和model_name获取模型 + model = haystack_get_model(app_label, model_name) + if model and model in indexed_models: + for key, value in raw_result.items(): + index = unified_index.get_index(model) + string_key = str(key) + # ZYY : 检查字段是否在索引中且具有convert方法,特殊处理KEYWORD字段 + if string_key in index.fields and hasattr( + index.fields[string_key], 'convert'): + # ZYY: 处理多值字段 + if index.fields[string_key].is_multivalued: + if value is None or len(value) == 0: + additional_fields[string_key] = [] else: - additional_fields[string_key] = self._to_python(value) - - del (additional_fields[DJANGO_CT]) - del (additional_fields[DJANGO_ID]) - - if highlight: - sa = StemmingAnalyzer() - formatter = WhooshHtmlFormatter('em') - terms = [token.text for token in sa(query_string)] - - whoosh_result = whoosh_highlight( - additional_fields.get(self.content_field_name), - terms, - sa, - ContextFragmenter(), - formatter - ) - additional_fields['highlighted'] = { - self.content_field_name: [whoosh_result], - } - - result = result_class( - app_label, - model_name, - raw_result[DJANGO_ID], - score, - **additional_fields) - results.append(result) - else: - hits -= 1 - - if self.include_spelling: - if spelling_query: - spelling_suggestion = self.create_spelling_suggestion( - spelling_query) + additional_fields[string_key] = value.split( + ',') + else: + additional_fields[string_key] = index.fields[string_key].convert( + value) else: - spelling_suggestion = self.create_spelling_suggestion( - query_string) - - return { - 'results': results, - 'hits': hits, - 'facets': facets, - 'spelling_suggestion': spelling_suggestion, - } - - def create_spelling_suggestion(self, query_string): - spelling_suggestion = None - reader = self.index.reader() - corrector = reader.corrector(self.content_field_name) - cleaned_query = force_str(query_string) - - if not query_string: - return spelling_suggestion - - # Clean the string. - for rev_word in self.RESERVED_WORDS: - cleaned_query = cleaned_query.replace(rev_word, '') - - for rev_char in self.RESERVED_CHARACTERS: - cleaned_query = cleaned_query.replace(rev_char, '') - - # Break it down. - query_words = cleaned_query.split() - suggested_words = [] - - for word in query_words: - suggestions = corrector.suggest(word, limit=1) - - if len(suggestions) > 0: - suggested_words.append(suggestions[0]) - - spelling_suggestion = ' '.join(suggested_words) + additional_fields[string_key] = self._to_python(value) + del (additional_fields[DJANGO_CT]) + del (additional_fields[DJANGO_ID]) + if highlight: + sa = StemmingAnalyzer() + formatter = WhooshHtmlFormatter('em') + terms = [token .text for token in sa(query_string)] + # ZYY: 使用Whoosh进行高亮显示 + whoosh_result = whoosh_highlight( + additional_fields.get(self.content_field_name), + terms, + sa, + ContextFragmenter(), + formatter + ) + additional_fields['highlighted'] = { + self.content_field_name: [whoosh_result], + } + result = result_class( + app_label, + model_name, + raw_result[DJANGO_ID], + score, + **additional_fields) + results.append(result) + else: + hits -= 1 +if self.include_spelling: + if spelling_query: + # ZYY: 根据spelling_query创建拼写建议 + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + # ZYY: 根据query_string创建拼写建议 + spelling_suggestion = self.create_spelling_suggestion( + query_string) +return { + 'results': results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, +} + +# ZYY: 创建拼写建议的方法 +def create_spelling_suggestion(self, query_string): + spelling_suggestion = None + reader = self.index.reader() + corrector = reader.corrector(self.content_field_name) + cleaned_query = force_str(query_string) + if not query_string: return spelling_suggestion - - def _from_python(self, value): - """ - Converts Python values to a string for Whoosh. - - Code courtesy of pysolr. - """ - if hasattr(value, 'strftime'): - if not hasattr(value, 'hour'): - value = datetime(value.year, value.month, value.day, 0, 0, 0) - elif isinstance(value, bool): - if value: - value = 'true' - else: - value = 'false' - elif isinstance(value, (list, tuple)): - value = u','.join([force_str(v) for v in value]) - elif isinstance(value, (six.integer_types, float)): - # Leave it alone. - pass + # ZYY: 清理字符串,移除保留字和保留字符 + for rev_word in self.RESERVED_WORDS: + cleaned_query = cleaned_query.replace(rev_word, '') + for rev_char in self.RESERVED_CHARACTERS: + cleaned_query = cleaned_query.replace(rev_char, '') + # ZYY: 分解查询词 + query_words = cleaned_query.split() + suggested_words = [] + for word in query_words: + suggestions = corrector.suggest(word, limit=1) + if len(suggestions) > 0: + suggested_words.append(suggestions[0]) + spelling_suggestion = ' '.join(suggested_words) + return spelling_suggestion + +# ZYY: 将Python值转换为Whoosh使用的字符串 +def _from_python(self, value): + if hasattr(value, 'strftime'): + if not hasattr(value, 'hour'): + value = datetime(value.year, value.month, value.day, 0, 0, 0) + elif isinstance(value, bool): + if value: + value = ' true' else: - value = force_str(value) - return value - - def _to_python(self, value): - """ - Converts values from Whoosh to native Python values. - - A port of the same method in pysolr, as they deal with data the same way. - """ - if value == 'true': - return True - elif value == 'false': - return False - - if value and isinstance(value, six.string_types): - possible_datetime = DATETIME_REGEX.search(value) - - if possible_datetime: - date_values = possible_datetime.groupdict() - - for dk, dv in date_values.items(): - date_values[dk] = int(dv) - - return datetime( - date_values['year'], - date_values['month'], - date_values['day'], - date_values['hour'], - date_values['minute'], - date_values['second']) - + value = 'false' + elif isinstance(value, (list, tuple)): + value = u','.join([force_str(v) for v in value]) + elif isinstance(value, (six.integer_types, float)): + # ZYY: 整数和浮点数保持不变 + pass + else: + value = force_str(value) + return value + +# ZYY: 将Whoosh的值转换为Python原生值 +def _to_python(self, value): + if value == 'true ': + return True + elif value == 'false': + return False + if value and isinstance(value, six.string_types): + possible_datetime = DATETIME_REGEX.search(value) + if possible_datetime: + date_values = possible_datetime.groupdict() + for dk, dv in date_values.items(): + date_values[dk] = int(dv) + return datetime( + date_values['year'], + date_values['month'], + date_values ['day'], + date_values['hour'], + date_values['minute'], + date_values['second']) try: - # Attempt to use json to load the values. + # ZYY: 尝试使用json加载值 converted_value = json.loads(value) - - # Try to handle most built-in types. + # ZYY: 尝试处理大多数内置类型 if isinstance( converted_value, (list, @@ -863,182 +879,139 @@ class WhooshSearchBackend(BaseSearchBackend): complex)): return converted_value except BaseException: - # If it fails (SyntaxError or its ilk) or we don't trust it, - # continue on. - pass - - return value + # ZYY: 如果失败或不信任,则继续 + pass + return value class WhooshSearchQuery(BaseSearchQuery): + # ZYY: 将datetime对象转换为Whoosh使用的字符串 def _convert_datetime(self, date): if hasattr(date, 'hour'): return force_str(date.strftime('%Y%m%d%H%M%S')) else: - return force_str(date.strftime('%Y%m%d000000')) + return force_str(date.strftime('%Y%m%d000000')) + # ZYY: 清理查询片段,处理保留字和保留字符 def clean(self, query_fragment): - """ - Provides a mechanism for sanitizing user input before presenting the - value to the backend. - - Whoosh 1.X differs here in that you can no longer use a backslash - to escape reserved characters. Instead, the whole word should be - quoted. - """ words = query_fragment.split() cleaned_words = [] - for word in words: if word in self.backend.RESERVED_WORDS: word = word.replace(word, word.lower()) - for char in self.backend.RESERVED_CHARACTERS: if char in word: word = "'%s'" % word break - - cleaned_words.append(word) - + cleaned_words. append(word) return ' '.join(cleaned_words) + # ZYY: 构建查询片段 def build_query_fragment(self, field, filter_type, value): from haystack import connections query_frag = '' is_datetime = False - if not hasattr(value, 'input_type_name'): - # Handle when we've got a ``ValuesListQuerySet``... + # ZYY: 处理ValuesListQuerySet的情况 if hasattr(value, 'values_list'): value = list(value) - - if hasattr(value, 'strftime'): + if hasattr(value, ' strftime'): is_datetime = True - if isinstance(value, six.string_types) and value != ' ': - # It's not an ``InputType``. Assume ``Clean``. + # ZYY: 假设为Clean类型 value = Clean(value) else: value = PythonData(value) - - # Prepare the query using the InputType. - prepared_value = value.prepare(self) - - if not isinstance(prepared_value, (set, list, tuple)): - # Then convert whatever we get back to what pysolr wants if needed. - prepared_value = self.backend._from_python(prepared_value) - - # 'content' is a special reserved word, much like 'pk' in - # Django's ORM layer. It indicates 'no special field'. - if field == 'content': - index_fieldname = '' - else: - index_fieldname = u'%s:' % connections[self._using].get_unified_index( - ).get_index_fieldname(field) - - filter_types = { - 'content': '%s', - 'contains': '*%s*', - 'endswith': "*%s", - 'startswith': "%s*", - 'exact': '%s', - 'gt': "{%s to}", - 'gte': "[%s to]", - 'lt': "{to %s}", - 'lte': "[to %s]", - 'fuzzy': u'%s~', - } - - if value.post_process is False: - query_frag = prepared_value - else: - if filter_type in [ - 'content', - 'contains', - 'startswith', - 'endswith', - 'fuzzy']: - if value.input_type_name == 'exact': - query_frag = prepared_value - else: - # Iterate over terms & incorportate the converted form of - # each into the query. - terms = [] - - if isinstance(prepared_value, six.string_types): - possible_values = prepared_value.split(' ') + # ZYY: 使用InputType准备查询 + prepared_value = value.prepare(self) + if not isinstance(prepared_value, (set, list, tuple )): + # ZYY: 转换为pysolr需要的格式 + prepared_value = self.backend._from_python(prepared_value) + # ZYY: 'content'是特殊保留字,表示无特殊字段 + if field == 'content': + index_fieldname = '' + else: + index_fieldname = u'%s:' % connections[self._using].get_unified_index( + ).get_index_fieldname(field) + filter_types = { + 'content': '%s', + 'contains': '*%s*', + ' endswith': "*%s", + 'startswith': "%s*", + 'exact': '%s', + 'gt': "{%s to}", + 'gte': "[%s to]", + 'lt': "{to %s}", + 'lte': "[to %s]", + 'fuzzy': u'%s~', + } + if value.post_process is False: + query_frag = prepared_value + else: + if filter_type in [ + 'content', + 'contains', + 'startswith ', + 'endswith', + 'fuzzy']: + if value.input_type_name == 'exact': + query_frag = prepared_value else: + # ZYY: 遍历词项并合并转换后的形式 + terms = [] + if isinstance(prepared_value, six.string_types): + possible_values = prepared_value.split(' ') + else: + if is_datetime is True: + prepared_value = self._convert_datetime( + prepared_value) + possible_values = [prepared_value] + for possible_value in possible_values: + terms.append( + filter_types[filter_type] % + self.backend._from_python(possible_value)) + if len(terms) == 1: + query_frag = terms[0] + else: + query_frag = u"(%s)" % " AND ".join(terms) + elif filter_type == 'in': + in_options = [] + for possible_value in prepared_value: + is_datetime = False + if hasattr(possible_value, 'strftime'): + is_datetime = True + pv = self.backend._from_python (possible_value) if is_datetime is True: - prepared_value = self._convert_datetime( - prepared_value) - - possible_values = [prepared_value] - - for possible_value in possible_values: - terms.append( - filter_types[filter_type] % - self.backend._from_python(possible_value)) - - if len(terms) == 1: - query_frag = terms[0] - else: - query_frag = u"(%s)" % " AND ".join(terms) - elif filter_type == 'in': - in_options = [] - - for possible_value in prepared_value: - is_datetime = False - - if hasattr(possible_value, 'strftime'): - is_datetime = True - - pv = self.backend._from_python(possible_value) - - if is_datetime is True: - pv = self._convert_datetime(pv) - - if isinstance(pv, six.string_types) and not is_datetime: - in_options.append('"%s"' % pv) + pv = self._convert_datetime(pv) + if isinstance(pv, six.string_types) and not is_datetime: + in_options.append('"%s"' % pv) + else: + in_options.append('%s' % pv) + query_frag = "(%s)" % " OR ".join(in_options) + elif filter_type == 'range': + start = self.backend._from_python(prepared_value[0]) + end = self.backend._from_python(prepared_value[1]) + if hasattr(prepared_value [0], 'strftime'): + start = self._convert_datetime(start) + if hasattr(prepared_value[1], 'strftime'): + end = self._convert_datetime(end) + query_frag = u"[%s to %s]" % (start, end) + elif filter_type == 'exact': + if value.input_type_name == 'exact': + query_frag = prepared_value else: - in_options.append('%s' % pv) - - query_frag = "(%s)" % " OR ".join(in_options) - elif filter_type == 'range': - start = self.backend._from_python(prepared_value[0]) - end = self.backend._from_python(prepared_value[1]) - - if hasattr(prepared_value[0], 'strftime'): - start = self._convert_datetime(start) - - if hasattr(prepared_value[1], 'strftime'): - end = self._convert_datetime(end) - - query_frag = u"[%s to %s]" % (start, end) - elif filter_type == 'exact': - if value.input_type_name == 'exact': - query_frag = prepared_value + prepared_value = Exact(prepared_value).prepare(self) + query_frag = filter_types[filter_type] % prepared_value else: - prepared_value = Exact(prepared_value).prepare(self) + if is_datetime is True: + prepared_value = self._convert_datetime(prepared_value) query_frag = filter_types[filter_type] % prepared_value - else: - if is_datetime is True: - prepared_value = self._convert_datetime(prepared_value) - - query_frag = filter_types[filter_type] % prepared_value - - if len(query_frag) and not isinstance(value, Raw): - if not query_frag.startswith('(') and not query_frag.endswith(')'): - query_frag = "(%s)" % query_frag - + if len(query_frag) and not isinstance(value, Raw): + if not query_frag.startswith('(') and not query_frag.endswith(')'): + query_frag = "(%s)" % query_frag return u"%s%s" % (index_fieldname, query_frag) - # if not filter_type in ('in', 'range'): - # # 'in' is a bit of a special case, as we don't want to - # # convert a valid list/tuple to string. Defer handling it - # # until later... - # value = self.backend._from_python(value) - class WhooshEngine(BaseEngine): backend = WhooshSearchBackend - query = WhooshSearchQuery + query = WhooshSearchQuery \ No newline at end of file diff --git a/src/django-master/djangoblog/wsgi.py b/src/django-master/djangoblog/wsgi.py index 2295efd..8ee9ba4 100644 --- a/src/django-master/djangoblog/wsgi.py +++ b/src/django-master/djangoblog/wsgi.py @@ -6,11 +6,19 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ """ - +# ZYY: 导入操作系统模块,用于与操作系统交互(如环境变量设置) import os +# ZYY: 从Django核心模块导入WSGI应用获取函数 +# ZYY: WSGI (Web Server Gateway Interface) 是Python web应用与服务器之间的标准接口 from django.core.wsgi import get_wsgi_application +# ZYY: 设置默认环境变量DJANGO_SETTINGS_MODULE +# ZYY: 该变量指定Django项目的设置模块路径(格式为"项目名.settings") +# ZYY: os.environ.setdefault()表示如果环境变量已存在则不覆盖 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") +# ZYY: 获取WSGI应用实例并赋值给application变量 +# ZYY: 这是WSGI服务器的入口点,服务器将通过这个变量调用Django应用 +# ZYY: get_wsgi_application()会加载Django设置并初始化应用 application = get_wsgi_application() From c6856732b39cce6b1aab30e6649dcdb806b75b9f Mon Sep 17 00:00:00 2001 From: ymq <2832214169@qq.com> Date: Sat, 8 Nov 2025 11:50:39 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=89=B9=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/blog/admin.py | 63 ++++- src/django-master/blog/apps.py | 4 +- src/django-master/blog/context_processors.py | 56 +++-- src/django-master/blog/documents.py | 140 +++++++---- src/django-master/blog/forms.py | 13 + .../blog/management/commands/build_index.py | 13 + .../management/commands/build_search_words.py | 9 +- .../blog/management/commands/clear_cache.py | 8 +- .../management/commands/create_testdata.py | 32 ++- .../blog/management/commands/ping_baidu.py | 31 ++- .../management/commands/sync_user_avatar.py | 45 +++- src/django-master/blog/middleware.py | 44 +++- .../blog/migrations/0001_initial.py | 37 ++- ...002_blogsettings_global_footer_and_more.py | 13 +- .../0003_blogsettings_comment_need_review.py | 16 +- ...de_blogsettings_analytics_code_and_more.py | 24 +- ...options_alter_category_options_and_more.py | 25 +- .../0006_alter_blogsettings_options.py | 8 +- src/django-master/blog/models.py | 223 ++++++++++-------- src/django-master/blog/search_indexes.py | 9 +- .../blog/templatetags/blog_tags.py | 51 ++-- src/django-master/blog/tests.py | 203 ++++++++++------ src/django-master/blog/urls.py | 32 ++- src/django-master/blog/views.py | 193 +++++++++------ 24 files changed, 919 insertions(+), 373 deletions(-) diff --git a/src/django-master/blog/admin.py b/src/django-master/blog/admin.py index 46c3420..c637bfc 100644 --- a/src/django-master/blog/admin.py +++ b/src/django-master/blog/admin.py @@ -1,48 +1,76 @@ from django import forms +#ymq:导入Django的forms模块,用于创建表单 from django.contrib import admin +#ymq:导入Django的admin模块,用于后台管理配置 from django.contrib.auth import get_user_model +#ymq:导入获取用户模型的函数,便于灵活引用用户模型 from django.urls import reverse +#ymq:导入reverse函数,用于生成URL反向解析 from django.utils.html import format_html +#ymq:导入format_html函数,用于安全生成HTML内容 from django.utils.translation import gettext_lazy as _ +#ymq:导入国际化翻译函数,将文本标记为可翻译 # Register your models here. from .models import Article +#ymq:从当前应用的models模块导入Article模型 class ArticleForm(forms.ModelForm): + #ymq:定义Article模型对应的表单类,继承自ModelForm # body = forms.CharField(widget=AdminPagedownWidget()) + #ymq:注释掉的代码,原本计划为body字段使用AdminPagedownWidget编辑器 class Meta: + #ymq:Meta类用于配置表单元数据 model = Article + #ymq:指定表单关联的模型为Article fields = '__all__' + #ymq:指定表单包含模型的所有字段 def makr_article_publish(modeladmin, request, queryset): + #ymq:定义批量发布文章的动作函数 queryset.update(status='p') + #ymq:将选中的文章状态更新为'p'(发布状态) def draft_article(modeladmin, request, queryset): + #ymq:定义批量设为草稿的动作函数 queryset.update(status='d') + #ymq:将选中的文章状态更新为'd'(草稿状态) def close_article_commentstatus(modeladmin, request, queryset): + #ymq:定义批量关闭评论的动作函数 queryset.update(comment_status='c') + #ymq:将选中的文章评论状态更新为'c'(关闭状态) def open_article_commentstatus(modeladmin, request, queryset): + #ymq:定义批量开启评论的动作函数 queryset.update(comment_status='o') + #ymq:将选中的文章评论状态更新为'o'(开启状态) makr_article_publish.short_description = _('Publish selected articles') +#ymq:设置发布动作在admin中的显示名称(支持国际化) draft_article.short_description = _('Draft selected articles') +#ymq:设置草稿动作在admin中的显示名称(支持国际化) close_article_commentstatus.short_description = _('Close article comments') +#ymq:设置关闭评论动作在admin中的显示名称(支持国际化) open_article_commentstatus.short_description = _('Open article comments') +#ymq:设置开启评论动作在admin中的显示名称(支持国际化) class ArticlelAdmin(admin.ModelAdmin): + #ymq:定义Article模型的admin管理类,继承自ModelAdmin list_per_page = 20 + #ymq:设置每页显示20条记录 search_fields = ('body', 'title') + #ymq:设置可搜索的字段为body和title form = ArticleForm + #ymq:指定使用自定义的ArticleForm表单 list_display = ( 'id', 'title', @@ -53,60 +81,93 @@ class ArticlelAdmin(admin.ModelAdmin): 'status', 'type', 'article_order') + #ymq:设置列表页显示的字段 list_display_links = ('id', 'title') + #ymq:设置列表页中可点击跳转编辑页的字段 list_filter = ('status', 'type', 'category') + #ymq:设置可用于筛选的字段 filter_horizontal = ('tags',) + #ymq:设置多对多字段的水平筛选器(tags字段) exclude = ('creation_time', 'last_modify_time') + #ymq:设置编辑页中排除的字段(不显示) view_on_site = True + #ymq:启用"在站点上查看"功能 actions = [ makr_article_publish, draft_article, close_article_commentstatus, open_article_commentstatus] + #ymq:注册批量操作动作 def link_to_category(self, obj): + #ymq:自定义列表页中分类字段的显示方式(转为链接) info = (obj.category._meta.app_label, obj.category._meta.model_name) + #ymq:获取分类模型的应用标签和模型名称 link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) + #ymq:生成分类的编辑页URL return format_html(u'%s' % (link, obj.category.name)) + #ymq:返回HTML链接,点击可跳转到分类编辑页 link_to_category.short_description = _('category') + #ymq:设置自定义字段在列表页的显示名称(支持国际化) def get_form(self, request, obj=None, **kwargs): - form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) + #ymq:重写获取表单的方法,自定义表单字段 + form = super(ArticlelAdmin, self).get_form(request, obj,** kwargs) + #ymq:调用父类方法获取表单 form.base_fields['author'].queryset = get_user_model( ).objects.filter(is_superuser=True) + #ymq:限制作者字段只能选择超级用户 return form + #ymq:返回修改后的表单 def save_model(self, request, obj, form, change): + #ymq:重写保存模型的方法(可在此添加自定义保存逻辑) super(ArticlelAdmin, self).save_model(request, obj, form, change) + #ymq:调用父类的保存方法完成默认保存 def get_view_on_site_url(self, obj=None): + #ymq:重写"在站点上查看"的URL生成方法 if obj: + #ymq:如果有具体对象,返回对象的完整URL url = obj.get_full_url() return url else: + #ymq:如果无对象,返回当前站点域名 from djangoblog.utils import get_current_site site = get_current_site().domain return site class TagAdmin(admin.ModelAdmin): + #ymq:定义Tag模型的admin管理类 exclude = ('slug', 'last_mod_time', 'creation_time') + #ymq:编辑页排除slug、最后修改时间和创建时间字段 class CategoryAdmin(admin.ModelAdmin): + #ymq:定义Category模型的admin管理类 list_display = ('name', 'parent_category', 'index') + #ymq:列表页显示名称、父分类和排序索引字段 exclude = ('slug', 'last_mod_time', 'creation_time') + #ymq:编辑页排除slug、最后修改时间和创建时间字段 class LinksAdmin(admin.ModelAdmin): + #ymq:定义Links模型的admin管理类 exclude = ('last_mod_time', 'creation_time') + #ymq:编辑页排除最后修改时间和创建时间字段 class SideBarAdmin(admin.ModelAdmin): + #ymq:定义SideBar模型的admin管理类 list_display = ('name', 'content', 'is_enable', 'sequence') + #ymq:列表页显示名称、内容、是否启用和排序序号字段 exclude = ('last_mod_time', 'creation_time') + #ymq:编辑页排除最后修改时间和创建时间字段 class BlogSettingsAdmin(admin.ModelAdmin): + #ymq:定义BlogSettings模型的admin管理类 pass + #ymq:暂未设置特殊配置,使用默认admin行为 \ No newline at end of file diff --git a/src/django-master/blog/apps.py b/src/django-master/blog/apps.py index 7930587..d7e57a1 100644 --- a/src/django-master/blog/apps.py +++ b/src/django-master/blog/apps.py @@ -1,5 +1,7 @@ from django.apps import AppConfig - +#ymq:导入Django的AppConfig类,用于定义应用的配置信息 class BlogConfig(AppConfig): + #ymq:定义博客应用的配置类,继承自AppConfig name = 'blog' + #ymq:指定应用的名称为'blog',Django通过该名称识别此应用 \ No newline at end of file diff --git a/src/django-master/blog/context_processors.py b/src/django-master/blog/context_processors.py index 73e3088..ebe7d09 100644 --- a/src/django-master/blog/context_processors.py +++ b/src/django-master/blog/context_processors.py @@ -1,43 +1,65 @@ import logging +#ymq:导入logging模块,用于日志记录 from django.utils import timezone +#ymq:导入Django的timezone模块,用于处理时间相关操作 from djangoblog.utils import cache, get_blog_setting +#ymq:从项目工具模块导入缓存工具和获取博客设置的函数 from .models import Category, Article +#ymq:从当前应用的models模块导入分类和文章模型 logger = logging.getLogger(__name__) +#ymq:创建当前模块的日志记录器实例 def seo_processor(requests): + #ymq:定义SEO上下文处理器,用于向模板全局注入通用数据 key = 'seo_processor' + #ymq:缓存键名,用于标识当前处理器的缓存数据 value = cache.get(key) + #ymq:尝试从缓存中获取数据 + if value: + #ymq:如果缓存存在,直接返回缓存数据 return value else: + #ymq:如果缓存不存在,重新生成数据 logger.info('set processor cache.') + #ymq:记录日志,提示正在设置缓存 + setting = get_blog_setting() + #ymq:获取博客的全局设置信息 + + #ymq:构建需要传递给模板的上下文数据字典 value = { - 'SITE_NAME': setting.site_name, - 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, - 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, - 'SITE_SEO_DESCRIPTION': setting.site_seo_description, - 'SITE_DESCRIPTION': setting.site_description, - 'SITE_KEYWORDS': setting.site_keywords, + 'SITE_NAME': setting.site_name, # 网站名称 + 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 是否显示谷歌广告 + 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # 谷歌广告代码 + 'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 网站SEO描述 + 'SITE_DESCRIPTION': setting.site_description, # 网站描述 + 'SITE_KEYWORDS': setting.site_keywords, # 网站关键词 + # 网站基础URL(协议+域名) 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', - 'ARTICLE_SUB_LENGTH': setting.article_sub_length, - 'nav_category_list': Category.objects.all(), + 'ARTICLE_SUB_LENGTH': setting.article_sub_length, # 文章摘要长度 + 'nav_category_list': Category.objects.all(), # 导航分类列表 + # 导航页面列表(类型为页面且状态为已发布) 'nav_pages': Article.objects.filter( type='p', status='p'), - 'OPEN_SITE_COMMENT': setting.open_site_comment, - 'BEIAN_CODE': setting.beian_code, - 'ANALYTICS_CODE': setting.analytics_code, - "BEIAN_CODE_GONGAN": setting.gongan_beiancode, - "SHOW_GONGAN_CODE": setting.show_gongan_code, - "CURRENT_YEAR": timezone.now().year, - "GLOBAL_HEADER": setting.global_header, - "GLOBAL_FOOTER": setting.global_footer, - "COMMENT_NEED_REVIEW": setting.comment_need_review, + 'OPEN_SITE_COMMENT': setting.open_site_comment, # 是否开启网站评论 + 'BEIAN_CODE': setting.beian_code, # 网站备案号 + 'ANALYTICS_CODE': setting.analytics_code, # 统计分析代码 + "BEIAN_CODE_GONGAN": setting.gongan_beiancode, # 公安备案号 + "SHOW_GONGAN_CODE": setting.show_gongan_code, # 是否显示公安备案号 + "CURRENT_YEAR": timezone.now().year, # 当前年份(用于页脚等位置) + "GLOBAL_HEADER": setting.global_header, # 全局头部代码 + "GLOBAL_FOOTER": setting.global_footer, # 全局底部代码 + "COMMENT_NEED_REVIEW": setting.comment_need_review, # 评论是否需要审核 } + cache.set(key, value, 60 * 60 * 10) + #ymq:将生成的上下文数据存入缓存,有效期10小时(60秒*60分*10小时) + return value + #ymq:返回构建好的上下文数据字典 \ No newline at end of file diff --git a/src/django-master/blog/documents.py b/src/django-master/blog/documents.py index 0f1db7b..38db391 100644 --- a/src/django-master/blog/documents.py +++ b/src/django-master/blog/documents.py @@ -1,26 +1,37 @@ import time +#ymq:导入time模块,用于处理时间相关操作(如生成唯一ID) import elasticsearch.client +#ymq:导入elasticsearch客户端模块,用于操作Elasticsearch的Ingest API from django.conf import settings +#ymq:导入Django的settings模块,用于获取项目配置 from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean +#ymq:导入elasticsearch-dsl相关类,用于定义Elasticsearch文档结构和字段类型 from elasticsearch_dsl.connections import connections +#ymq:导入elasticsearch-dsl的连接管理工具,用于创建与Elasticsearch的连接 from blog.models import Article +#ymq:从blog应用导入Article模型,用于同步数据到Elasticsearch +#ymq:判断是否启用Elasticsearch(检查settings中是否配置了ELASTICSEARCH_DSL) ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') if ELASTICSEARCH_ENABLED: + #ymq:如果启用Elasticsearch,创建连接 connections.create_connection( hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) from elasticsearch import Elasticsearch - + #ymq:创建Elasticsearch客户端实例 es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) from elasticsearch.client import IngestClient + #ymq:创建Ingest客户端,用于管理数据处理管道 c = IngestClient(es) try: + #ymq:尝试获取名为'geoip'的管道,检查是否已存在 c.get_pipeline('geoip') except elasticsearch.exceptions.NotFoundError: + #ymq:如果管道不存在,则创建它(用于解析IP地址的地理位置信息) c.put_pipeline('geoip', body='''{ "description" : "Add geoip info", "processors" : [ @@ -34,72 +45,85 @@ if ELASTICSEARCH_ENABLED: class GeoIp(InnerDoc): - continent_name = Keyword() - country_iso_code = Keyword() - country_name = Keyword() - location = GeoPoint() + #ymq:定义地理位置信息的内部文档(嵌套结构) + continent_name = Keyword() # 大陆名称(关键字类型,不分词) + country_iso_code = Keyword() # 国家ISO代码(关键字类型) + country_name = Keyword() # 国家名称(关键字类型) + location = GeoPoint() # 地理位置坐标(经纬度) class UserAgentBrowser(InnerDoc): - Family = Keyword() - Version = Keyword() + #ymq:定义用户代理中浏览器信息的内部文档 + Family = Keyword() # 浏览器家族(如Chrome、Firefox) + Version = Keyword() # 浏览器版本 class UserAgentOS(UserAgentBrowser): + #ymq:定义用户代理中操作系统信息的内部文档(继承浏览器结构) pass class UserAgentDevice(InnerDoc): - Family = Keyword() - Brand = Keyword() - Model = Keyword() + #ymq:定义用户代理中设备信息的内部文档 + Family = Keyword() # 设备家族 + Brand = Keyword() # 设备品牌 + Model = Keyword() # 设备型号 class UserAgent(InnerDoc): - browser = Object(UserAgentBrowser, required=False) - os = Object(UserAgentOS, required=False) - device = Object(UserAgentDevice, required=False) - string = Text() - is_bot = Boolean() + #ymq:定义用户代理完整信息的内部文档(嵌套结构) + browser = Object(UserAgentBrowser, required=False) # 浏览器信息 + os = Object(UserAgentOS, required=False) # 操作系统信息 + device = Object(UserAgentDevice, required=False) # 设备信息 + string = Text() # 原始用户代理字符串 + is_bot = Boolean() # 是否为爬虫 class ElapsedTimeDocument(Document): - url = Keyword() - time_taken = Long() - log_datetime = Date() - ip = Keyword() - geoip = Object(GeoIp, required=False) - useragent = Object(UserAgent, required=False) + #ymq:定义用于记录性能耗时的Elasticsearch文档 + url = Keyword() # 访问的URL(关键字类型) + time_taken = Long() # 耗时(毫秒) + log_datetime = Date() # 日志记录时间 + ip = Keyword() # 访问IP地址 + geoip = Object(GeoIp, required=False) # 地理位置信息(嵌套) + useragent = Object(UserAgent, required=False) # 用户代理信息(嵌套) class Index: - name = 'performance' + #ymq:定义索引配置 + name = 'performance' # 索引名称 settings = { - "number_of_shards": 1, - "number_of_replicas": 0 + "number_of_shards": 1, # 分片数量 + "number_of_replicas": 0 # 副本数量 } class Meta: - doc_type = 'ElapsedTime' + doc_type = 'ElapsedTime' # 文档类型(Elasticsearch 7.x后逐渐废弃) class ElaspedTimeDocumentManager: + #ymq:性能耗时文档的管理类,用于索引的创建、删除和数据插入 @staticmethod def build_index(): + #ymq:创建索引(如果不存在) from elasticsearch import Elasticsearch client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) - res = client.indices.exists(index="performance") + res = client.indices.exists(index="performance") # 检查索引是否存在 if not res: - ElapsedTimeDocument.init() + ElapsedTimeDocument.init() # 初始化索引 @staticmethod def delete_index(): + #ymq:删除performance索引 from elasticsearch import Elasticsearch es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) - es.indices.delete(index='performance', ignore=[400, 404]) + es.indices.delete(index='performance', ignore=[400, 404]) # 忽略不存在的情况 @staticmethod def create(url, time_taken, log_datetime, useragent, ip): - ElaspedTimeDocumentManager.build_index() + #ymq:创建一条性能耗时记录 + ElaspedTimeDocumentManager.build_index() # 确保索引存在 + + #ymq:构建用户代理信息对象 ua = UserAgent() ua.browser = UserAgentBrowser() ua.browser.Family = useragent.browser.family @@ -116,98 +140,112 @@ class ElaspedTimeDocumentManager: ua.string = useragent.ua_string ua.is_bot = useragent.is_bot + #ymq:创建文档实例,使用时间戳作为唯一ID doc = ElapsedTimeDocument( meta={ 'id': int( round( time.time() * - 1000)) + 1000)) # 毫秒级时间戳作为ID }, url=url, time_taken=time_taken, log_datetime=log_datetime, useragent=ua, ip=ip) + #ymq:保存文档时应用geoip管道解析IP地址 doc.save(pipeline="geoip") class ArticleDocument(Document): + #ymq:定义文章信息的Elasticsearch文档(用于搜索) + #ymq:body和title使用IK分词器(max_word分词更细,smart更简洁) body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + #ymq:嵌套作者信息 author = Object(properties={ 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'id': Integer() }) + #ymq:嵌套分类信息 category = Object(properties={ 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'id': Integer() }) + #ymq:嵌套标签信息(数组) tags = Object(properties={ 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'id': Integer() }) - pub_time = Date() - status = Text() - comment_status = Text() - type = Text() - views = Integer() - article_order = Integer() + pub_time = Date() # 发布时间 + status = Text() # 状态(发布/草稿) + comment_status = Text() # 评论状态(开启/关闭) + type = Text() # 类型(文章/页面) + views = Integer() # 浏览量 + article_order = Integer() # 排序序号 class Index: - name = 'blog' + name = 'blog' # 索引名称 settings = { "number_of_shards": 1, "number_of_replicas": 0 } class Meta: - doc_type = 'Article' + doc_type = 'Article' # 文档类型 class ArticleDocumentManager(): + #ymq:文章文档的管理类,用于索引操作和数据同步 def __init__(self): + #ymq:初始化时创建索引 self.create_index() def create_index(self): + #ymq:初始化文章索引 ArticleDocument.init() def delete_index(self): + #ymq:删除blog索引 from elasticsearch import Elasticsearch es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es.indices.delete(index='blog', ignore=[400, 404]) def convert_to_doc(self, articles): + #ymq:将Django模型对象转换为Elasticsearch文档对象 return [ ArticleDocument( - meta={ - 'id': article.id}, + meta={'id': article.id}, # 使用文章ID作为文档ID body=article.body, title=article.title, author={ 'nickname': article.author.username, - 'id': article.author.id}, + 'id': article.author.id + }, category={ 'name': article.category.name, - 'id': article.category.id}, - tags=[ - { - 'name': t.name, - 'id': t.id} for t in article.tags.all()], + '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] + article_order=article.article_order + ) for article in articles + ] def rebuild(self, articles=None): + #ymq:重建索引(默认同步所有文章,可指定文章列表) ArticleDocument.init() - articles = articles if articles else Article.objects.all() - docs = self.convert_to_doc(articles) + articles = articles if articles else Article.objects.all() # 获取文章数据 + docs = self.convert_to_doc(articles) # 转换为文档对象 for doc in docs: - doc.save() + doc.save() # 保存到Elasticsearch def update_docs(self, docs): + #ymq:批量更新文档 for doc in docs: - doc.save() + doc.save() \ No newline at end of file diff --git a/src/django-master/blog/forms.py b/src/django-master/blog/forms.py index 715be76..e39870e 100644 --- a/src/django-master/blog/forms.py +++ b/src/django-master/blog/forms.py @@ -1,19 +1,32 @@ import logging +#ymq:导入logging模块,用于记录搜索相关日志 from django import forms +#ymq:导入Django的forms模块,用于创建自定义表单 from haystack.forms import SearchForm +#ymq:导入Haystack的SearchForm基类,扩展实现博客搜索表单 logger = logging.getLogger(__name__) +#ymq:创建当前模块的日志记录器实例 class BlogSearchForm(SearchForm): + #ymq:定义博客搜索表单类,继承自Haystack的SearchForm querydata = forms.CharField(required=True) + #ymq:定义搜索关键词字段,required=True表示该字段为必填项 def search(self): + #ymq:重写父类的search方法,自定义搜索逻辑 datas = super(BlogSearchForm, self).search() + #ymq:调用父类search方法,获取基础搜索结果 + if not self.is_valid(): + #ymq:如果表单数据验证不通过,返回无结果响应 return self.no_query_found() if self.cleaned_data['querydata']: + #ymq:如果存在合法的搜索关键词,记录关键词日志 logger.info(self.cleaned_data['querydata']) + return datas + #ymq:返回最终的搜索结果集 \ No newline at end of file diff --git a/src/django-master/blog/management/commands/build_index.py b/src/django-master/blog/management/commands/build_index.py index 3c4acd7..69a4490 100644 --- a/src/django-master/blog/management/commands/build_index.py +++ b/src/django-master/blog/management/commands/build_index.py @@ -1,18 +1,31 @@ from django.core.management.base import BaseCommand +#ymq:导入Django的BaseCommand类,用于创建自定义管理命令 from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \ ELASTICSEARCH_ENABLED +#ymq:从blog.documents导入Elasticsearch相关的文档类和管理器,以及启用状态常量 # TODO 参数化 class Command(BaseCommand): + #ymq:定义自定义管理命令类,继承自BaseCommand help = 'build search index' + #ymq:定义命令的帮助信息,使用python manage.py help build_index时显示 def handle(self, *args, **options): + #ymq:命令的核心处理方法,执行实际的索引构建逻辑 if ELASTICSEARCH_ENABLED: + #ymq:仅当Elasticsearch启用时执行以下操作 ElaspedTimeDocumentManager.build_index() + #ymq:调用性能耗时文档管理器构建索引(若不存在) + manager = ElapsedTimeDocument() manager.init() + #ymq:初始化ElapsedTimeDocument对应的索引结构 + manager = ArticleDocumentManager() manager.delete_index() + #ymq:删除已存在的文章索引(重建前清理) + manager.rebuild() + #ymq:重建文章索引,将数据库中的文章数据同步到Elasticsearch \ No newline at end of file diff --git a/src/django-master/blog/management/commands/build_search_words.py b/src/django-master/blog/management/commands/build_search_words.py index cfe7e0d..35d0c6e 100644 --- a/src/django-master/blog/management/commands/build_search_words.py +++ b/src/django-master/blog/management/commands/build_search_words.py @@ -1,13 +1,20 @@ from django.core.management.base import BaseCommand +#ymq:导入Django的BaseCommand类,用于创建自定义管理命令 from blog.models import Tag, Category +#ymq:从blog应用导入Tag(标签)和Category(分类)模型 # TODO 参数化 class Command(BaseCommand): + #ymq:定义自定义管理命令类,继承自BaseCommand help = 'build search words' + #ymq:命令的帮助信息,说明该命令用于生成搜索词 def handle(self, *args, **options): + #ymq:命令的核心处理方法,执行生成搜索词的逻辑 + # 从标签和分类中提取名称,使用set去重 datas = set([t.name for t in Tag.objects.all()] + [t.name for t in Category.objects.all()]) - print('\n'.join(datas)) + # 按行打印所有去重后的名称(作为搜索词) + print('\n'.join(datas)) \ No newline at end of file diff --git a/src/django-master/blog/management/commands/clear_cache.py b/src/django-master/blog/management/commands/clear_cache.py index 0d66172..6366680 100644 --- a/src/django-master/blog/management/commands/clear_cache.py +++ b/src/django-master/blog/management/commands/clear_cache.py @@ -1,11 +1,17 @@ from django.core.management.base import BaseCommand +#ymq:导入Django的BaseCommand类,用于创建自定义管理命令 from djangoblog.utils import cache +#ymq:从项目工具模块导入缓存工具 class Command(BaseCommand): + #ymq:定义清除缓存的自定义命令类,继承自BaseCommand help = 'clear the whole cache' + #ymq:命令的帮助信息,说明该命令用于清除所有缓存 def handle(self, *args, **options): - cache.clear() + #ymq:命令的核心处理方法,执行清除缓存操作 + cache.clear() # 调用缓存工具的clear方法,清除所有缓存数据 self.stdout.write(self.style.SUCCESS('Cleared cache\n')) + #ymq:向标准输出写入成功信息,使用Django的SUCCESS样式(通常为绿色) \ No newline at end of file diff --git a/src/django-master/blog/management/commands/create_testdata.py b/src/django-master/blog/management/commands/create_testdata.py index 675d2ba..a3dcce8 100644 --- a/src/django-master/blog/management/commands/create_testdata.py +++ b/src/django-master/blog/management/commands/create_testdata.py @@ -1,40 +1,62 @@ from django.contrib.auth import get_user_model +#ymq:导入获取用户模型的函数,便于灵活引用用户模型 from django.contrib.auth.hashers import make_password +#ymq:导入密码加密函数,用于安全存储密码 from django.core.management.base import BaseCommand +#ymq:导入Django的BaseCommand类,用于创建自定义管理命令 from blog.models import Article, Tag, Category +#ymq:从blog应用导入文章、标签、分类模型 class Command(BaseCommand): + #ymq:定义创建测试数据的自定义命令类,继承自BaseCommand help = 'create test datas' + #ymq:命令的帮助信息,说明该命令用于创建测试数据 def handle(self, *args, **options): + #ymq:命令的核心处理方法,执行创建测试数据的逻辑 + # 创建或获取测试用户(邮箱、用户名、密码加密存储) user = get_user_model().objects.get_or_create( email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0] + # 创建或获取父分类 pcategory = Category.objects.get_or_create( name='我是父类目', parent_category=None)[0] + # 创建或获取子分类(关联父分类) category = Category.objects.get_or_create( name='子类目', parent_category=pcategory)[0] - category.save() + category.save() # 保存子分类 + + # 创建基础标签 basetag = Tag() basetag.name = "标签" basetag.save() + + # 批量创建20篇测试文章 for i in range(1, 20): + # 创建或获取文章(关联分类、作者) article = Article.objects.get_or_create( category=category, - title='nice title ' + str(i), - body='nice content ' + str(i), + title='nice title ' + str(i), # 文章标题带序号 + body='nice content ' + str(i), # 文章内容带序号 author=user)[0] + + # 创建带序号的标签 tag = Tag() tag.name = "标签" + str(i) tag.save() + + # 给文章添加标签(包含基础标签和序号标签) article.tags.add(tag) article.tags.add(basetag) - article.save() + article.save() # 保存文章 + # 清除缓存,确保测试数据立即生效 from djangoblog.utils import cache cache.clear() - self.stdout.write(self.style.SUCCESS('created test datas \n')) + + # 输出成功信息 + self.stdout.write(self.style.SUCCESS('created test datas \n')) \ No newline at end of file diff --git a/src/django-master/blog/management/commands/ping_baidu.py b/src/django-master/blog/management/commands/ping_baidu.py index 2c7fbdd..092ed48 100644 --- a/src/django-master/blog/management/commands/ping_baidu.py +++ b/src/django-master/blog/management/commands/ping_baidu.py @@ -1,16 +1,24 @@ from django.core.management.base import BaseCommand +#ymq:导入Django的BaseCommand类,用于创建自定义管理命令 from djangoblog.spider_notify import SpiderNotify +#ymq:导入蜘蛛通知工具类,用于向搜索引擎提交URL from djangoblog.utils import get_current_site +#ymq:导入获取当前站点信息的工具函数 from blog.models import Article, Tag, Category +#ymq:从blog应用导入文章、标签、分类模型 site = get_current_site().domain +#ymq:获取当前站点的域名,用于构建完整URL class Command(BaseCommand): + #ymq:定义百度URL提交命令类,继承自BaseCommand help = 'notify baidu url' + #ymq:命令的帮助信息,说明该命令用于向百度提交URL def add_arguments(self, parser): + #ymq:定义命令参数,指定提交的数据类型 parser.add_argument( 'data_type', type=str, @@ -20,31 +28,46 @@ class Command(BaseCommand): 'tag', 'category'], help='article : all article,tag : all tag,category: all category,all: All of these') + #ymq:参数说明:article-所有文章,tag-所有标签,category-所有分类,all-全部 def get_full_url(self, path): + #ymq:构建包含域名的完整URL url = "https://{site}{path}".format(site=site, path=path) return url def handle(self, *args, **options): - type = options['data_type'] - self.stdout.write('start get %s' % type) + #ymq:命令核心处理方法,执行URL收集和提交 + type = options['data_type'] # 获取用户指定的数据类型 + self.stdout.write('start get %s' % type) # 输出开始收集信息的提示 - urls = [] + urls = [] # 存储待提交的URL列表 + + # 根据数据类型收集对应的URL if type == 'article' or type == 'all': + # 收集已发布文章的URL for article in Article.objects.filter(status='p'): urls.append(article.get_full_url()) + if type == 'tag' or type == 'all': + # 收集所有标签页的URL for tag in Tag.objects.all(): url = tag.get_absolute_url() urls.append(self.get_full_url(url)) + if type == 'category' or type == 'all': + # 收集所有分类页的URL for category in Category.objects.all(): url = category.get_absolute_url() urls.append(self.get_full_url(url)) + # 输出待提交的URL数量 self.stdout.write( self.style.SUCCESS( 'start notify %d urls' % len(urls))) + + # 调用工具类向百度提交URL SpiderNotify.baidu_notify(urls) - self.stdout.write(self.style.SUCCESS('finish notify')) + + # 输出提交完成的提示 + self.stdout.write(self.style.SUCCESS('finish notify')) \ No newline at end of file diff --git a/src/django-master/blog/management/commands/sync_user_avatar.py b/src/django-master/blog/management/commands/sync_user_avatar.py index d0f4612..f51a404 100644 --- a/src/django-master/blog/management/commands/sync_user_avatar.py +++ b/src/django-master/blog/management/commands/sync_user_avatar.py @@ -1,47 +1,70 @@ import requests +#ymq:导入requests库,用于发送HTTP请求测试图片URL有效性 from django.core.management.base import BaseCommand +#ymq:导入Django的BaseCommand类,用于创建自定义管理命令 from django.templatetags.static import static +#ymq:导入static标签,用于获取静态文件URL from djangoblog.utils import save_user_avatar +#ymq:导入保存用户头像的工具函数 from oauth.models import OAuthUser +#ymq:从oauth应用导入OAuthUser模型,存储第三方用户信息 from oauth.oauthmanager import get_manager_by_type +#ymq:导入获取对应第三方登录管理器的函数 class Command(BaseCommand): + #ymq:定义同步用户头像的自定义命令类,继承自BaseCommand help = 'sync user avatar' + #ymq:命令的帮助信息,说明该命令用于同步用户头像 def test_picture(self, url): + #ymq:测试图片URL是否有效(状态码200) try: if requests.get(url, timeout=2).status_code == 200: - return True + return True # URL有效返回True except: - pass + pass # 异常或状态码非200返回None def handle(self, *args, **options): - static_url = static("../") - users = OAuthUser.objects.all() - self.stdout.write(f'开始同步{len(users)}个用户头像') + #ymq:命令核心处理方法,执行用户头像同步逻辑 + static_url = static("../") # 获取静态文件基础URL + users = OAuthUser.objects.all() # 获取所有第三方用户 + self.stdout.write(f'开始同步{len(users)}个用户头像') # 输出待同步用户数量 + for u in users: - self.stdout.write(f'开始同步:{u.nickname}') - url = u.picture + #ymq:遍历每个用户进行头像同步 + self.stdout.write(f'开始同步:{u.nickname}') # 输出当前同步的用户名 + url = u.picture # 获取用户当前头像URL + if url: + # 处理已有头像URL的情况 if url.startswith(static_url): + # 头像URL是本地静态文件 if self.test_picture(url): + # 图片有效,跳过同步 continue else: + # 图片无效,重新获取 if u.metadata: + # 有元数据,通过第三方管理器获取头像 manage = get_manager_by_type(u.type) url = manage.get_picture(u.metadata) - url = save_user_avatar(url) + url = save_user_avatar(url) # 保存头像并获取本地URL else: + # 无元数据,使用默认头像 url = static('blog/img/avatar.png') else: + # 头像URL是外部链接,保存到本地 url = save_user_avatar(url) else: + # 无头像URL,使用默认头像 url = static('blog/img/avatar.png') + if url: - self.stdout.write( - f'结束同步:{u.nickname}.url:{url}') + # 保存更新后的头像URL + self.stdout.write(f'结束同步:{u.nickname}.url:{url}') u.picture = url u.save() - self.stdout.write('结束同步') + + self.stdout.write('结束同步') # 输出同步完成提示 \ No newline at end of file diff --git a/src/django-master/blog/middleware.py b/src/django-master/blog/middleware.py index 94dd70c..2c2bf83 100644 --- a/src/django-master/blog/middleware.py +++ b/src/django-master/blog/middleware.py @@ -1,42 +1,62 @@ import logging import time +#ymq:导入logging用于日志记录,time用于计算页面加载时间 from ipware import get_client_ip +#ymq:导入get_client_ip工具,用于获取客户端IP地址 from user_agents import parse +#ymq:导入parse函数,用于解析用户代理字符串 from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager +#ymq:从博客文档模块导入Elasticsearch启用状态和性能日志管理器 logger = logging.getLogger(__name__) +#ymq:创建当前模块的日志记录器实例 class OnlineMiddleware(object): + #ymq:定义在线中间件类,用于记录页面加载性能和访问信息 def __init__(self, get_response=None): + #ymq:初始化中间件,接收Django的响应处理器 self.get_response = get_response super().__init__() def __call__(self, request): + #ymq:中间件核心方法,处理请求并返回响应 ''' 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) + #ymq:记录页面渲染时间的逻辑 + start_time = time.time() # 记录请求处理开始时间 + response = self.get_response(request) # 调用后续中间件或视图处理请求 + + #ymq:获取用户代理和IP地址 + http_user_agent = request.META.get('HTTP_USER_AGENT', '') # 获取用户代理字符串 + ip, _ = get_client_ip(request) # 获取客户端IP地址 + user_agent = parse(http_user_agent) # 解析用户代理信息(浏览器、设备等) + + #ymq:非流式响应才处理(流式响应无法修改内容) if not response.streaming: try: - cast_time = time.time() - start_time + cast_time = time.time() - start_time # 计算页面加载耗时(秒) + + #ymq:如果启用了Elasticsearch,记录性能数据 if ELASTICSEARCH_ENABLED: - time_taken = round((cast_time) * 1000, 2) - url = request.path + time_taken = round((cast_time) * 1000, 2) #ymq: 转换为毫秒并保留两位小数 + url = request.path # 获取请求的URL路径 from django.utils import timezone + #ymq:调用管理器创建性能日志记录 ElaspedTimeDocumentManager.create( url=url, time_taken=time_taken, - log_datetime=timezone.now(), - useragent=user_agent, - ip=ip) + log_datetime=timezone.now(), #ymq: 记录当前时间 + useragent=user_agent, #ymq: 已解析的用户代理信息 + ip=ip) #ymq: 客户端IP + + #ymq:替换响应内容中的标记为实际加载时间(保留前5位字符) response.content = response.content.replace( b'', str.encode(str(cast_time)[:5])) + except Exception as e: + #ymq:捕获并记录处理过程中的异常 logger.error("Error OnlineMiddleware: %s" % e) - return response + return response #ymq: 返回处理后的响应 \ No newline at end of file diff --git a/src/django-master/blog/migrations/0001_initial.py b/src/django-master/blog/migrations/0001_initial.py index 3d391b6..a63b9e7 100644 --- a/src/django-master/blog/migrations/0001_initial.py +++ b/src/django-master/blog/migrations/0001_initial.py @@ -1,25 +1,34 @@ # Generated by Django 4.1.7 on 2023-03-02 07:14 +#ymq:该迁移文件由Django 4.1.7自动生成,生成时间为2023-03-02 07:14 from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import mdeditor.fields +#ymq:导入Django迁移相关模块、时间工具和markdown编辑器字段 class Migration(migrations.Migration): + #ymq:定义迁移类,继承自migrations.Migration initial = True + #ymq:标记为初始迁移(第一次创建模型时生成) dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), + #ymq:依赖于用户模型,确保用户表先创建 ] operations = [ + #ymq:定义数据库操作列表,按顺序执行创建模型的操作 + migrations.CreateModel( + #ymq:创建BlogSettings模型(网站配置) name='BlogSettings', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + #ymq:自增主键字段 ('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')), ('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')), ('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')), @@ -35,13 +44,17 @@ class Migration(migrations.Migration): ('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')), ('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')), ('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')), + #ymq:以上为网站配置的各个字段,包含网站基本信息、显示设置、备案信息等 ], options={ 'verbose_name': '网站配置', 'verbose_name_plural': '网站配置', + #ymq:模型的显示名称 }, ), + migrations.CreateModel( + #ymq:创建Links模型(友情链接) name='Links', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), @@ -52,14 +65,18 @@ class Migration(migrations.Migration): ('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')), ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + #ymq:友情链接字段,包含名称、URL、排序、显示位置等 ], options={ 'verbose_name': '友情链接', 'verbose_name_plural': '友情链接', 'ordering': ['sequence'], + #ymq:按排序号升序排列 }, ), + migrations.CreateModel( + #ymq:创建SideBar模型(侧边栏) name='SideBar', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), @@ -69,14 +86,18 @@ class Migration(migrations.Migration): ('is_enable', models.BooleanField(default=True, verbose_name='是否启用')), ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), + #ymq:侧边栏字段,包含标题、内容、排序等 ], options={ 'verbose_name': '侧边栏', 'verbose_name_plural': '侧边栏', 'ordering': ['sequence'], + #ymq:按排序号升序排列 }, ), + migrations.CreateModel( + #ymq:创建Tag模型(标签) name='Tag', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), @@ -84,14 +105,18 @@ class Migration(migrations.Migration): ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')), ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), + #ymq:标签字段,包含名称、URL友好标识(slug)等 ], options={ 'verbose_name': '标签', 'verbose_name_plural': '标签', 'ordering': ['name'], + #ymq:按标签名升序排列 }, ), + migrations.CreateModel( + #ymq:创建Category模型(分类) name='Category', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), @@ -101,14 +126,18 @@ class Migration(migrations.Migration): ('slug', models.SlugField(blank=True, default='no-slug', max_length=60)), ('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')), ('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')), + #ymq:分类字段,支持多级分类(自关联外键)、权重排序等 ], options={ 'verbose_name': '分类', 'verbose_name_plural': '分类', 'ordering': ['-index'], + #ymq:按权重降序排列(权重越大越靠前) }, ), + migrations.CreateModel( + #ymq:创建Article模型(文章) name='Article', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), @@ -116,6 +145,7 @@ class Migration(migrations.Migration): ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ('title', models.CharField(max_length=200, unique=True, verbose_name='标题')), ('body', mdeditor.fields.MDTextField(verbose_name='正文')), + #ymq:使用markdown编辑器字段存储文章正文 ('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')), ('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')), ('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')), @@ -124,14 +154,19 @@ class Migration(migrations.Migration): ('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')), ('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')), ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), + #ymq:关联用户模型(外键),级联删除 ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')), + #ymq:关联分类模型(外键),级联删除 ('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')), + #ymq:多对多关联标签模型 ], options={ 'verbose_name': '文章', 'verbose_name_plural': '文章', 'ordering': ['-article_order', '-pub_time'], + #ymq:先按排序号降序,再按发布时间降序 'get_latest_by': 'id', + #ymq:按id获取最新记录 }, ), - ] + ] \ No newline at end of file diff --git a/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py b/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py index adbaa36..1304b8a 100644 --- a/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py +++ b/src/django-master/blog/migrations/0002_blogsettings_global_footer_and_more.py @@ -1,23 +1,34 @@ # Generated by Django 4.1.7 on 2023-03-29 06:08 +#ymq:该迁移文件由Django 4.1.7自动生成,生成时间为2023-03-29 06:08 from django.db import migrations, models +#ymq:导入Django迁移相关模块 class Migration(migrations.Migration): + #ymq:定义迁移类,继承自migrations.Migration dependencies = [ ('blog', '0001_initial'), + #ymq:依赖于blog应用的0001_initial迁移文件,确保先执行初始迁移 ] operations = [ + #ymq:定义数据库操作列表,添加新字段 + migrations.AddField( + #ymq:向BlogSettings模型添加global_footer字段 model_name='blogsettings', name='global_footer', field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'), + #ymq:字段类型为文本字段,允许为空,默认值为空字符串,verbose_name为"公共尾部" ), + migrations.AddField( + #ymq:向BlogSettings模型添加global_header字段 model_name='blogsettings', name='global_header', field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'), + #ymq:字段类型为文本字段,允许为空,默认值为空字符串,verbose_name为"公共头部" ), - ] + ] \ No newline at end of file diff --git a/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py b/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py index e9f5502..908f852 100644 --- a/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py +++ b/src/django-master/blog/migrations/0003_blogsettings_comment_need_review.py @@ -1,17 +1,25 @@ # Generated by Django 4.2.1 on 2023-05-09 07:45 +#ymq:该迁移文件由Django 4.2.1自动生成,生成时间为2023-05-09 07:45 from django.db import migrations, models - +#ymq:导入Django迁移相关模块 class Migration(migrations.Migration): + #ymq:定义迁移类,继承自migrations.Migration + dependencies = [ ('blog', '0002_blogsettings_global_footer_and_more'), + #ymq:依赖于blog应用的0002号迁移文件,确保先执行该迁移 ] operations = [ + #ymq:定义数据库操作,此处为添加字段 + migrations.AddField( - model_name='blogsettings', - name='comment_need_review', + #ymq:向BlogSettings模型添加comment_need_review字段 + model_name='blogsettings', # 目标模型名称 + name='comment_need_review', # 新字段名称 field=models.BooleanField(default=False, verbose_name='评论是否需要审核'), + #ymq:字段类型为布尔值,默认值为False(不需要审核),后台显示名称为"评论是否需要审核" ), - ] + ] \ No newline at end of file diff --git a/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py index ceb1398..f6465d8 100644 --- a/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py +++ b/src/django-master/blog/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py @@ -1,27 +1,39 @@ # Generated by Django 4.2.1 on 2023-05-09 07:51 +#ymq:该迁移文件由Django 4.2.1自动生成,生成时间为2023-05-09 07:51 from django.db import migrations +#ymq:导入Django迁移相关模块 class Migration(migrations.Migration): + #ymq:定义迁移类,继承自migrations.Migration + dependencies = [ ('blog', '0003_blogsettings_comment_need_review'), + #ymq:依赖于blog应用的0003号迁移文件,确保先执行该迁移 ] operations = [ + #ymq:定义数据库操作列表,主要是重命名字段 + migrations.RenameField( - model_name='blogsettings', - old_name='analyticscode', - new_name='analytics_code', + #ymq:重命名BlogSettings模型的analyticscode字段 + model_name='blogsettings', # 目标模型名称 + old_name='analyticscode', # 旧字段名 + new_name='analytics_code', # 新字段名(改为下划线命名规范) ), + migrations.RenameField( + #ymq:重命名BlogSettings模型的beiancode字段 model_name='blogsettings', old_name='beiancode', - new_name='beian_code', + new_name='beian_code', # 改为下划线命名规范 ), + migrations.RenameField( + #ymq:重命名BlogSettings模型的sitename字段 model_name='blogsettings', old_name='sitename', - new_name='site_name', + new_name='site_name', # 改为下划线命名规范 ), - ] + ] \ No newline at end of file diff --git a/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py index d08e853..d06b10a 100644 --- a/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py +++ b/src/django-master/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py @@ -1,20 +1,27 @@ # Generated by Django 4.2.5 on 2023-09-06 13:13 +#ymq:该迁移文件由Django 4.2.5自动生成,生成时间为2023-09-06 13:13 from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import mdeditor.fields +#ymq:导入Django迁移相关模块、时间工具和markdown编辑器字段 class Migration(migrations.Migration): + #ymq:定义迁移类,继承自migrations.Migration dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'), + #ymq:依赖于用户模型和blog应用的0004号迁移文件 ] operations = [ + #ymq:定义数据库操作列表,包含模型选项修改、字段删除、添加和修改 + + # 修改模型的元数据选项(主要是verbose_name的国际化调整) migrations.AlterModelOptions( name='article', options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'}, @@ -35,6 +42,8 @@ class Migration(migrations.Migration): name='tag', options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'}, ), + + # 删除旧的时间字段(命名方式调整) migrations.RemoveField( model_name='article', name='created_time', @@ -67,6 +76,8 @@ class Migration(migrations.Migration): model_name='tag', name='last_mod_time', ), + + # 添加新的时间字段(统一命名为creation_time和last_modify_time) migrations.AddField( model_name='article', name='creation_time', @@ -107,6 +118,8 @@ class Migration(migrations.Migration): name='last_modify_time', field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'), ), + + # 修改Article模型的字段属性(主要是verbose_name国际化) migrations.AlterField( model_name='article', name='article_order', @@ -167,6 +180,8 @@ class Migration(migrations.Migration): name='views', field=models.PositiveIntegerField(default=0, verbose_name='views'), ), + + # 修改BlogSettings模型的字段属性(verbose_name国际化) migrations.AlterField( model_name='blogsettings', name='article_comment_count', @@ -222,6 +237,8 @@ class Migration(migrations.Migration): name='site_seo_description', field=models.TextField(default='', max_length=1000, verbose_name='site seo description'), ), + + # 修改Category模型的字段属性 migrations.AlterField( model_name='category', name='index', @@ -237,6 +254,8 @@ class Migration(migrations.Migration): name='parent_category', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'), ), + + # 修改Links模型的字段属性 migrations.AlterField( model_name='links', name='is_enable', @@ -267,6 +286,8 @@ class Migration(migrations.Migration): name='show_type', field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'), ), + + # 修改SideBar模型的字段属性 migrations.AlterField( model_name='sidebar', name='content', @@ -292,9 +313,11 @@ class Migration(migrations.Migration): name='sequence', field=models.IntegerField(unique=True, verbose_name='order'), ), + + # 修改Tag模型的字段属性 migrations.AlterField( model_name='tag', name='name', field=models.CharField(max_length=30, unique=True, verbose_name='tag name'), ), - ] + ] \ No newline at end of file diff --git a/src/django-master/blog/migrations/0006_alter_blogsettings_options.py b/src/django-master/blog/migrations/0006_alter_blogsettings_options.py index e36feb4..207d123 100644 --- a/src/django-master/blog/migrations/0006_alter_blogsettings_options.py +++ b/src/django-master/blog/migrations/0006_alter_blogsettings_options.py @@ -1,17 +1,23 @@ # Generated by Django 4.2.7 on 2024-01-26 02:41 +#ymq:该迁移文件由Django 4.2.7自动生成,生成时间为2024年1月26日02:41 from django.db import migrations +#ymq:导入Django迁移相关模块 class Migration(migrations.Migration): + #ymq:定义迁移类,继承自migrations.Migration dependencies = [ ('blog', '0005_alter_article_options_alter_category_options_and_more'), + #ymq:依赖于blog应用的0005号迁移文件,确保先执行该迁移 ] operations = [ + #ymq:定义数据库操作,此处为修改模型选项 migrations.AlterModelOptions( name='blogsettings', + #ymq:修改BlogSettings模型的显示名称,改为英文"Website configuration" options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'}, ), - ] + ] \ No newline at end of file diff --git a/src/django-master/blog/models.py b/src/django-master/blog/models.py index 083788b..d92d098 100644 --- a/src/django-master/blog/models.py +++ b/src/django-master/blog/models.py @@ -1,6 +1,7 @@ import logging import re from abc import abstractmethod +#ymq:导入logging用于日志记录,re用于正则表达式操作,abstractmethod用于定义抽象方法 from django.conf import settings from django.core.exceptions import ValidationError @@ -8,36 +9,43 @@ from django.db import models from django.urls import reverse from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from mdeditor.fields import MDTextField -from uuslug import slugify +from mdeditor.fields import MDTextField # 导入markdown编辑器字段 +from uuslug import slugify # 导入slug生成工具 -from djangoblog.utils import cache_decorator, cache -from djangoblog.utils import get_current_site +from djangoblog.utils import cache_decorator, cache # 导入缓存相关工具 +from djangoblog.utils import get_current_site # 导入获取当前站点信息的工具 -logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # 创建当前模块的日志记录器 class LinkShowType(models.TextChoices): - I = ('i', _('index')) - L = ('l', _('list')) - P = ('p', _('post')) - A = ('a', _('all')) - S = ('s', _('slide')) + #ymq:定义链接展示位置的枚举类 + I = ('i', _('index')) # 首页展示 + L = ('l', _('list')) # 列表页展示 + P = ('p', _('post')) # 文章页展示 + A = ('a', _('all')) # 所有页面展示 + S = ('s', _('slide')) # 幻灯片展示 class BaseModel(models.Model): - id = models.AutoField(primary_key=True) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_modify_time = models.DateTimeField(_('modify time'), default=now) + #ymq:定义模型基类,封装公共字段和方法(抽象类) + id = models.AutoField(primary_key=True) # 自增主键 + creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 + last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 def save(self, *args, **kwargs): + #ymq:重写保存方法,处理slug生成和特殊更新逻辑 + # 判断是否是更新文章浏览量的操作 is_update_views = isinstance( self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] if is_update_views: + # 单独处理浏览量更新,提高性能 Article.objects.filter(pk=self.pk).update(views=self.views) else: + # 自动生成slug(用于URL友好化) if 'slug' in self.__dict__: + # 根据title或name字段生成slug slug = getattr( self, 'title') if 'title' in self.__dict__ else getattr( self, 'name') @@ -45,79 +53,88 @@ class BaseModel(models.Model): super().save(*args, **kwargs) def get_full_url(self): + #ymq:生成包含域名的完整URL site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url class Meta: - abstract = True + abstract = True # 声明为抽象模型,不生成数据库表 @abstractmethod def get_absolute_url(self): + #ymq:抽象方法,子类必须实现,用于生成模型实例的URL pass class Article(BaseModel): - """文章""" + """文章模型""" + # 状态选项:草稿/已发布 STATUS_CHOICES = ( ('d', _('Draft')), ('p', _('Published')), ) + # 评论状态选项:开启/关闭 COMMENT_STATUS = ( ('o', _('Open')), ('c', _('Close')), ) + # 类型选项:文章/页面 TYPE = ( ('a', _('Article')), ('p', _('Page')), ) - title = models.CharField(_('title'), max_length=200, unique=True) - body = MDTextField(_('body')) + + title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题 + body = MDTextField(_('body')) # 文章内容(使用markdown编辑器) pub_time = models.DateTimeField( - _('publish time'), blank=False, null=False, default=now) + _('publish time'), blank=False, null=False, default=now) # 发布时间 status = models.CharField( _('status'), max_length=1, choices=STATUS_CHOICES, - default='p') + default='p') # 发布状态 comment_status = models.CharField( _('comment status'), max_length=1, choices=COMMENT_STATUS, - default='o') - type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') - views = models.PositiveIntegerField(_('views'), default=0) + default='o') # 评论状态 + type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 内容类型 + views = models.PositiveIntegerField(_('views'), default=0) # 浏览量 author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=False, null=False, - on_delete=models.CASCADE) + on_delete=models.CASCADE) # 关联作者(外键) article_order = models.IntegerField( - _('order'), blank=False, null=False, default=0) - show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) + _('order'), blank=False, null=False, default=0) # 排序序号 + show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录 category = models.ForeignKey( 'Category', verbose_name=_('category'), on_delete=models.CASCADE, blank=False, - null=False) - tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) + null=False) # 关联分类(外键) + tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 关联标签(多对多) def body_to_string(self): + #ymq:返回文章内容字符串 return self.body def __str__(self): + #ymq:模型实例的字符串表示(文章标题) return self.title class Meta: - ordering = ['-article_order', '-pub_time'] + ordering = ['-article_order', '-pub_time'] # 默认排序:先按排序号降序,再按发布时间降序 verbose_name = _('article') verbose_name_plural = verbose_name - get_latest_by = 'id' + get_latest_by = 'id' # 按id获取最新记录 def get_absolute_url(self): + #ymq:生成文章详情页的URL return reverse('blog:detailbyid', kwargs={ 'article_id': self.id, 'year': self.creation_time.year, @@ -125,21 +142,24 @@ class Article(BaseModel): 'day': self.creation_time.day }) - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_category_tree(self): + #ymq:获取当前文章所属分类的层级结构(含父级分类) tree = self.category.get_category_tree() names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) - return names def save(self, *args, **kwargs): + #ymq:重写保存方法(可扩展自定义逻辑) super().save(*args, **kwargs) def viewed(self): + #ymq:增加浏览量并保存 self.views += 1 - self.save(update_fields=['views']) + self.save(update_fields=['views']) # 只更新views字段,提高性能 def comment_list(self): + #ymq:获取文章的评论列表(带缓存) cache_key = 'article_comments_{id}'.format(id=self.id) value = cache.get(cache_key) if value: @@ -147,67 +167,64 @@ class Article(BaseModel): return value else: comments = self.comment_set.filter(is_enable=True).order_by('-id') - cache.set(cache_key, comments, 60 * 100) + cache.set(cache_key, comments, 60 * 100) # 缓存100分钟 logger.info('set article comments:{id}'.format(id=self.id)) return comments def get_admin_url(self): + #ymq:生成文章在admin后台的编辑URL info = (self._meta.app_label, self._meta.model_name) return reverse('admin:%s_%s_change' % info, args=(self.pk,)) - @cache_decorator(expiration=60 * 100) + @cache_decorator(expiration=60 * 100) # 缓存100分钟 def next_article(self): - # 下一篇 + #ymq:获取下一篇文章(ID更大的已发布文章) return Article.objects.filter( id__gt=self.id, status='p').order_by('id').first() - @cache_decorator(expiration=60 * 100) + @cache_decorator(expiration=60 * 100) # 缓存100分钟 def prev_article(self): - # 前一篇 + #ymq:获取上一篇文章(ID更小的已发布文章) return Article.objects.filter(id__lt=self.id, status='p').first() def get_first_image_url(self): - """ - Get the first image url from article.body. - :return: - """ - match = re.search(r'!\[.*?\]\((.+?)\)', self.body) + """从文章内容中提取第一张图片的URL""" + match = re.search(r'!\[.*?\]\((.+?)\)', self.body) # 匹配markdown图片语法 if match: return match.group(1) return "" class Category(BaseModel): - """文章分类""" - name = models.CharField(_('category name'), max_length=30, unique=True) + """文章分类模型""" + name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称 parent_category = models.ForeignKey( 'self', verbose_name=_('parent category'), blank=True, null=True, - on_delete=models.CASCADE) - slug = models.SlugField(default='no-slug', max_length=60, blank=True) - index = models.IntegerField(default=0, verbose_name=_('index')) + on_delete=models.CASCADE) # 父分类(自关联,支持多级分类) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好化标识 + index = models.IntegerField(default=0, verbose_name=_('index')) # 排序索引 class Meta: - ordering = ['-index'] + ordering = ['-index'] # 按索引降序排列 verbose_name = _('category') verbose_name_plural = verbose_name def get_absolute_url(self): + #ymq:生成分类详情页的URL return reverse( 'blog:category_detail', kwargs={ 'category_name': self.slug}) def __str__(self): + #ymq:模型实例的字符串表示(分类名称) return self.name - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_category_tree(self): - """ - 递归获得分类目录的父级 - :return: - """ + """递归获取当前分类的所有父级分类,形成层级结构""" categorys = [] def parse(category): @@ -218,12 +235,9 @@ class Category(BaseModel): parse(self) return categorys - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_sub_categorys(self): - """ - 获得当前分类目录所有子集 - :return: - """ + """获取当前分类的所有子分类(含多级子分类)""" categorys = [] all_categorys = Category.objects.all() @@ -241,136 +255,143 @@ class Category(BaseModel): class Tag(BaseModel): - """文章标签""" - name = models.CharField(_('tag name'), max_length=30, unique=True) - slug = models.SlugField(default='no-slug', max_length=60, blank=True) + """文章标签模型""" + name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称 + slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好化标识 def __str__(self): + #ymq:模型实例的字符串表示(标签名称) return self.name def get_absolute_url(self): + #ymq:生成标签详情页的URL return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_article_count(self): + #ymq:获取该标签关联的文章数量 return Article.objects.filter(tags__name=self.name).distinct().count() class Meta: - ordering = ['name'] + ordering = ['name'] # 按名称排序 verbose_name = _('tag') verbose_name_plural = verbose_name class Links(models.Model): - """友情链接""" - - name = models.CharField(_('link name'), max_length=30, unique=True) - link = models.URLField(_('link')) - sequence = models.IntegerField(_('order'), unique=True) + """友情链接模型""" + name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称 + link = models.URLField(_('link')) # 链接URL + sequence = models.IntegerField(_('order'), unique=True) # 排序序号 is_enable = models.BooleanField( - _('is show'), default=True, blank=False, null=False) + _('is show'), default=True, blank=False, null=False) # 是否显示 show_type = models.CharField( _('show type'), max_length=1, choices=LinkShowType.choices, - default=LinkShowType.I) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_mod_time = models.DateTimeField(_('modify time'), default=now) + default=LinkShowType.I) # 展示位置 + creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 + last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 class Meta: - ordering = ['sequence'] + ordering = ['sequence'] # 按排序序号排列 verbose_name = _('link') verbose_name_plural = verbose_name def __str__(self): + #ymq:模型实例的字符串表示(链接名称) return self.name class SideBar(models.Model): - """侧边栏,可以展示一些html内容""" - name = models.CharField(_('title'), max_length=100) - content = models.TextField(_('content')) - sequence = models.IntegerField(_('order'), unique=True) - is_enable = models.BooleanField(_('is enable'), default=True) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_mod_time = models.DateTimeField(_('modify time'), default=now) + """侧边栏模型(可展示自定义HTML内容)""" + name = models.CharField(_('title'), max_length=100) # 侧边栏标题 + content = models.TextField(_('content')) # 侧边栏内容(HTML) + sequence = models.IntegerField(_('order'), unique=True) # 排序序号 + is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用 + creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 + last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 class Meta: - ordering = ['sequence'] + ordering = ['sequence'] # 按排序序号排列 verbose_name = _('sidebar') verbose_name_plural = verbose_name def __str__(self): + #ymq:模型实例的字符串表示(侧边栏标题) return self.name class BlogSettings(models.Model): - """blog的配置""" + """博客全局配置模型""" site_name = models.CharField( _('site name'), max_length=200, null=False, blank=False, - default='') + default='') # 网站名称 site_description = models.TextField( _('site description'), max_length=1000, null=False, blank=False, - default='') + default='') # 网站描述 site_seo_description = models.TextField( - _('site seo description'), max_length=1000, null=False, blank=False, default='') + _('site seo description'), max_length=1000, null=False, blank=False, default='') # SEO描述 site_keywords = models.TextField( _('site keywords'), max_length=1000, null=False, blank=False, - default='') - article_sub_length = models.IntegerField(_('article sub length'), default=300) - sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) - sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) - article_comment_count = models.IntegerField(_('article comment count'), default=5) - show_google_adsense = models.BooleanField(_('show adsense'), default=False) + default='') # 网站关键词 + article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度 + sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量 + sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量 + article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页评论数量 + show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示谷歌广告 google_adsense_codes = models.TextField( - _('adsense code'), max_length=2000, null=True, blank=True, default='') - open_site_comment = models.BooleanField(_('open site comment'), default=True) - global_header = models.TextField("公共头部", null=True, blank=True, default='') - global_footer = models.TextField("公共尾部", null=True, blank=True, default='') + _('adsense code'), max_length=2000, null=True, blank=True, default='') # 谷歌广告代码 + open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启网站评论 + global_header = models.TextField("公共头部", null=True, blank=True, default='') # 全局头部代码 + global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 全局尾部代码 beian_code = models.CharField( '备案号', max_length=2000, null=True, blank=True, - default='') + default='') # 网站备案号 analytics_code = models.TextField( "网站统计代码", max_length=1000, null=False, blank=False, - default='') + default='') # 统计分析代码 show_gongan_code = models.BooleanField( - '是否显示公安备案号', default=False, null=False) + '是否显示公安备案号', default=False, null=False) # 是否显示公安备案号 gongan_beiancode = models.TextField( '公安备案号', max_length=2000, null=True, blank=True, - default='') + default='') # 公安备案号 comment_need_review = models.BooleanField( - '评论是否需要审核', default=False, null=False) + '评论是否需要审核', default=False, null=False) # 评论是否需要审核 class Meta: verbose_name = _('Website configuration') verbose_name_plural = verbose_name def __str__(self): + #ymq:模型实例的字符串表示(网站名称) return self.site_name def clean(self): + #ymq:数据验证,确保全局配置只能有一条记录 if BlogSettings.objects.exclude(id=self.id).count(): raise ValidationError(_('There can only be one configuration')) def save(self, *args, **kwargs): + #ymq:保存配置后清除缓存,确保配置立即生效 super().save(*args, **kwargs) from djangoblog.utils import cache - cache.clear() + cache.clear() \ No newline at end of file diff --git a/src/django-master/blog/search_indexes.py b/src/django-master/blog/search_indexes.py index 7f1dfac..f492392 100644 --- a/src/django-master/blog/search_indexes.py +++ b/src/django-master/blog/search_indexes.py @@ -1,13 +1,20 @@ from haystack import indexes +#ymq:导入Haystack的indexes模块,用于定义搜索索引 from blog.models import Article +#ymq:从blog应用导入Article模型,为其创建搜索索引 class ArticleIndex(indexes.SearchIndex, indexes.Indexable): + #ymq:定义文章搜索索引类,继承自SearchIndex和Indexable + #ymq: document=True表示该字段是主要搜索字段,use_template=True表示使用模板定义字段内容 text = indexes.CharField(document=True, use_template=True) def get_model(self): + #ymq:指定该索引对应的模型 return Article def index_queryset(self, using=None): - return self.get_model().objects.filter(status='p') + #ymq:定义需要被索引的数据集 + #ymq: 只索引状态为'p'(已发布)的文章 + return self.get_model().objects.filter(status='p') \ No newline at end of file diff --git a/src/django-master/blog/templatetags/blog_tags.py b/src/django-master/blog/templatetags/blog_tags.py index d6cd5d5..087c485 100644 --- a/src/django-master/blog/templatetags/blog_tags.py +++ b/src/django-master/blog/templatetags/blog_tags.py @@ -23,15 +23,18 @@ from djangoblog.plugin_manage import hooks logger = logging.getLogger(__name__) register = template.Library() +#ymq:注册模板标签库,用于在Django模板中使用自定义标签和过滤器 @register.simple_tag(takes_context=True) def head_meta(context): + #ymq:自定义简单标签,用于生成页面头部元信息(通过插件钩子处理) return mark_safe(hooks.apply_filters('head_meta', '', context)) @register.simple_tag def timeformat(data): + #ymq:格式化时间(仅时间部分),使用settings中定义的TIME_FORMAT try: return data.strftime(settings.TIME_FORMAT) except Exception as e: @@ -41,6 +44,7 @@ def timeformat(data): @register.simple_tag def datetimeformat(data): + #ymq:格式化日期时间,使用settings中定义的DATE_TIME_FORMAT try: return data.strftime(settings.DATE_TIME_FORMAT) except Exception as e: @@ -51,11 +55,13 @@ def datetimeformat(data): @register.filter() @stringfilter def custom_markdown(content): + #ymq:将内容转换为Markdown格式并标记为安全HTML(用于文章内容) return mark_safe(CommonMarkdown.get_markdown(content)) @register.simple_tag def get_markdown_toc(content): + #ymq:获取Markdown内容的目录(TOC)并标记为安全HTML from djangoblog.utils import CommonMarkdown body, toc = CommonMarkdown.get_markdown_with_toc(content) return mark_safe(toc) @@ -64,6 +70,7 @@ def get_markdown_toc(content): @register.filter() @stringfilter def comment_markdown(content): + #ymq:处理评论内容的Markdown转换,并过滤不安全HTML标签 content = CommonMarkdown.get_markdown(content) return mark_safe(sanitize_html(content)) @@ -76,6 +83,7 @@ def truncatechars_content(content): :param content: :return: """ + #ymq:按网站设置的长度截断文章内容(保留HTML标签) from django.template.defaultfilters import truncatechars_html from djangoblog.utils import get_blog_setting blogsetting = get_blog_setting() @@ -85,8 +93,8 @@ def truncatechars_content(content): @register.filter(is_safe=True) @stringfilter def truncate(content): + #ymq:截断内容为150字符并去除HTML标签(用于生成纯文本摘要) from django.utils.html import strip_tags - return strip_tags(content)[:150] @@ -97,12 +105,13 @@ def load_breadcrumb(article): :param article: :return: """ + #ymq:生成文章面包屑导航数据,包含分类层级和网站名称 names = article.get_category_tree() from djangoblog.utils import get_blog_setting blogsetting = get_blog_setting() site = get_current_site().domain names.append((blogsetting.site_name, '/')) - names = names[::-1] + names = names[::-1] # 反转列表,使层级从网站到当前分类 return { 'names': names, @@ -118,6 +127,7 @@ def load_articletags(article): :param article: :return: """ + #ymq:获取文章关联的标签列表,包含标签URL、文章数和随机样式 tags = article.tags.all() tags_list = [] for tag in tags: @@ -137,6 +147,7 @@ def load_sidebar(user, linktype): 加载侧边栏 :return: """ + #ymq:加载侧边栏数据(带缓存),包含文章列表、分类、标签等 value = cache.get("sidebar" + linktype) if value: value['user'] = user @@ -145,6 +156,7 @@ def load_sidebar(user, linktype): logger.info('load sidebar') from djangoblog.utils import get_blog_setting blogsetting = get_blog_setting() + # 获取最近文章、分类、热门文章等数据 recent_articles = Article.objects.filter( status='p')[:blogsetting.sidebar_article_count] sidebar_categorys = Category.objects.all() @@ -157,8 +169,8 @@ def load_sidebar(user, linktype): Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A)) commment_list = Comment.objects.filter(is_enable=True).order_by( '-id')[:blogsetting.sidebar_comment_count] - # 标签云 计算字体大小 - # 根据总数计算出平均值 大小为 (数目/平均值)*步长 + + # 处理标签云(按文章数计算字体大小) increment = 5 tags = Tag.objects.all() sidebar_tags = None @@ -166,7 +178,6 @@ def load_sidebar(user, linktype): s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]] count = sum([t[1] for t in s]) dd = 1 if (count == 0 or not len(tags)) else count / len(tags) - import random sidebar_tags = list( map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s)) random.shuffle(sidebar_tags) @@ -185,6 +196,7 @@ def load_sidebar(user, linktype): 'sidebar_tags': sidebar_tags, 'extra_sidebars': extra_sidebars } + # 缓存侧边栏数据3小时 cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3) logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype)) value['user'] = user @@ -198,6 +210,7 @@ def load_article_metas(article, user): :param article: :return: """ + #ymq:加载文章元信息(作者、发布时间等)供模板使用 return { 'article': article, 'user': user @@ -206,9 +219,11 @@ def load_article_metas(article, user): @register.inclusion_tag('blog/tags/article_pagination.html') def load_pagination_info(page_obj, page_type, tag_name): + #ymq:生成分页导航链接,支持首页、标签、作者、分类等不同页面类型 previous_url = '' next_url = '' if page_type == '': + # 首页分页 if page_obj.has_next(): next_number = page_obj.next_page_number() next_url = reverse('blog:index_page', kwargs={'page': next_number}) @@ -218,6 +233,7 @@ def load_pagination_info(page_obj, page_type, tag_name): 'blog:index_page', kwargs={ 'page': previous_number}) if page_type == '分类标签归档': + # 标签页分页 tag = get_object_or_404(Tag, name=tag_name) if page_obj.has_next(): next_number = page_obj.next_page_number() @@ -234,6 +250,7 @@ def load_pagination_info(page_obj, page_type, tag_name): 'page': previous_number, 'tag_name': tag.slug}) if page_type == '作者文章归档': + # 作者页分页 if page_obj.has_next(): next_number = page_obj.next_page_number() next_url = reverse( @@ -250,6 +267,7 @@ def load_pagination_info(page_obj, page_type, tag_name): 'author_name': tag_name}) if page_type == '分类目录归档': + # 分类页分页 category = get_object_or_404(Category, name=tag_name) if page_obj.has_next(): next_number = page_obj.next_page_number() @@ -281,6 +299,7 @@ def load_article_detail(article, isindex, user): :param isindex:是否列表页,若是列表页只显示摘要 :return: """ + #ymq:加载文章详情数据,区分列表页(显示摘要)和详情页(显示全文) from djangoblog.utils import get_blog_setting blogsetting = get_blog_setting() @@ -292,35 +311,35 @@ def load_article_detail(article, isindex, user): } -# return only the URL of the gravatar -# TEMPLATE USE: {{ email|gravatar_url:150 }} @register.filter def gravatar_url(email, size=40): - """获得gravatar头像""" + """获得gravatar头像URL""" + #ymq:获取用户头像URL(优先使用第三方登录头像,否则使用Gravatar) cachekey = 'gravatat/' + email url = cache.get(cachekey) if url: return url else: + # 检查是否有第三方登录用户的头像 usermodels = OAuthUser.objects.filter(email=email) if usermodels: o = list(filter(lambda x: x.picture is not None, usermodels)) if o: return o[0].picture + # 生成Gravatar头像URL email = email.encode('utf-8') - - default = static('blog/img/avatar.png') - + default = static('blog/img/avatar.png') # 默认头像 url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5( email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)})) - cache.set(cachekey, url, 60 * 60 * 10) + cache.set(cachekey, url, 60 * 60 * 10) # 缓存头像URL 10小时 logger.info('set gravatar cache.key:{key}'.format(key=cachekey)) return url @register.filter def gravatar(email, size=40): - """获得gravatar头像""" + """获得gravatar头像img标签""" + #ymq:生成头像img标签(调用gravatar_url获取URL) url = gravatar_url(email, size) return mark_safe( '' % @@ -335,10 +354,12 @@ def query(qs, **kwargs): ... {% endfor %} """ - return qs.filter(**kwargs) + #ymq:模板中过滤查询集的标签(支持动态传参过滤) + return qs.filter(** kwargs) @register.filter def addstr(arg1, arg2): """concatenate arg1 & arg2""" - return str(arg1) + str(arg2) + #ymq:字符串拼接过滤器(将两个参数转换为字符串并拼接) + return str(arg1) + str(arg2) \ No newline at end of file diff --git a/src/django-master/blog/tests.py b/src/django-master/blog/tests.py index ee13505..fd67d7d 100644 --- a/src/django-master/blog/tests.py +++ b/src/django-master/blog/tests.py @@ -1,73 +1,101 @@ import os +#ymq:导入os模块,用于文件路径和文件操作 from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile +#ymq:导入文件上传相关类,用于模拟文件上传测试 from django.core.management import call_command +#ymq:导入call_command,用于调用Django管理命令 from django.core.paginator import Paginator +#ymq:导入分页类,用于测试分页功能 from django.templatetags.static import static +#ymq:导入static标签,用于获取静态文件路径 from django.test import Client, RequestFactory, TestCase +#ymq:导入测试相关类,Client用于模拟HTTP请求,TestCase提供测试框架 from django.urls import reverse +#ymq:导入reverse,用于反向解析URL from django.utils import timezone +#ymq:导入timezone,用于处理时间相关测试数据 from accounts.models import BlogUser +#ymq:从accounts应用导入用户模型 from blog.forms import BlogSearchForm +#ymq:从blog应用导入搜索表单 from blog.models import Article, Category, Tag, SideBar, Links +#ymq:从blog应用导入模型类,用于测试数据创建和查询 from blog.templatetags.blog_tags import load_pagination_info, load_articletags +#ymq:导入自定义模板标签函数,用于测试模板标签功能 from djangoblog.utils import get_current_site, get_sha256 +#ymq:导入工具函数,用于获取站点信息和加密 from oauth.models import OAuthUser, OAuthConfig +#ymq:从oauth应用导入模型,用于测试第三方登录相关功能 # Create your tests here. - class ArticleTest(TestCase): + #ymq:定义文章相关的测试类,继承自TestCase def setUp(self): - self.client = Client() - self.factory = RequestFactory() + #ymq:测试前置方法,在每个测试方法执行前运行,初始化测试客户端和工厂 + self.client = Client() # 创建测试客户端,用于模拟HTTP请求 + self.factory = RequestFactory() # 创建请求工厂,用于构造请求对象 def test_validate_article(self): - site = get_current_site().domain + #ymq:测试文章相关功能的完整性,包括创建、查询、页面访问等 + site = get_current_site().domain # 获取当前站点域名 + # 创建或获取测试用户 user = BlogUser.objects.get_or_create( email="liangliangyy@gmail.com", username="liangliangyy")[0] - user.set_password("liangliangyy") - user.is_staff = True - user.is_superuser = True - user.save() + user.set_password("liangliangyy") # 设置用户密码 + user.is_staff = True # 设为 staff,允许访问admin + user.is_superuser = True # 设为超级用户 + user.save() # 保存用户 + + # 测试用户个人页面访问 response = self.client.get(user.get_absolute_url()) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) # 断言页面正常响应 + + # 测试admin相关页面访问(未登录状态) response = self.client.get('/admin/servermanager/emailsendlog/') response = self.client.get('admin/admin/logentry/') + + # 创建侧边栏测试数据 s = SideBar() s.sequence = 1 s.name = 'test' s.content = 'test content' s.is_enable = True s.save() - + + # 创建分类测试数据 category = Category() category.name = "category" category.creation_time = timezone.now() category.last_mod_time = timezone.now() category.save() - + + # 创建标签测试数据 tag = Tag() tag.name = "nicetag" tag.save() - + + # 创建文章测试数据 article = Article() article.title = "nicetitle" article.body = "nicecontent" article.author = user article.category = category - article.type = 'a' - article.status = 'p' - + article.type = 'a' # 类型为文章 + article.status = 'p' # 状态为已发布 article.save() - self.assertEqual(0, article.tags.count()) - article.tags.add(tag) + + # 测试文章标签关联 + self.assertEqual(0, article.tags.count()) # 初始无标签 + article.tags.add(tag) # 添加标签 article.save() - self.assertEqual(1, article.tags.count()) - + self.assertEqual(1, article.tags.count()) # 断言标签已添加 + + # 批量创建文章(用于测试分页) for i in range(20): article = Article() article.title = "nicetitle" + str(i) @@ -79,56 +107,73 @@ class ArticleTest(TestCase): article.save() article.tags.add(tag) article.save() + + # 测试Elasticsearch搜索功能(如果启用) from blog.documents import ELASTICSEARCH_ENABLED if ELASTICSEARCH_ENABLED: - call_command("build_index") - response = self.client.get('/search', {'q': 'nicetitle'}) - self.assertEqual(response.status_code, 200) - + call_command("build_index") # 调用命令构建索引 + response = self.client.get('/search', {'q': 'nicetitle'}) # 模拟搜索请求 + self.assertEqual(response.status_code, 200) # 断言搜索页面正常响应 + + # 测试文章详情页访问 response = self.client.get(article.get_absolute_url()) self.assertEqual(response.status_code, 200) + + # 测试蜘蛛通知功能 from djangoblog.spider_notify import SpiderNotify - SpiderNotify.notify(article.get_absolute_url()) + SpiderNotify.notify(article.get_absolute_url()) # 通知搜索引擎 + + # 测试标签页访问 response = self.client.get(tag.get_absolute_url()) self.assertEqual(response.status_code, 200) - + + # 测试分类页访问 response = self.client.get(category.get_absolute_url()) self.assertEqual(response.status_code, 200) - + + # 测试搜索功能 response = self.client.get('/search', {'q': 'django'}) self.assertEqual(response.status_code, 200) + + # 测试文章模板标签 s = load_articletags(article) - self.assertIsNotNone(s) - + self.assertIsNotNone(s) # 断言标签返回非空 + + # 用户登录 self.client.login(username='liangliangyy', password='liangliangyy') - + + # 测试归档页面访问 response = self.client.get(reverse('blog:archives')) self.assertEqual(response.status_code, 200) - + + # 测试不同类型的分页功能 p = Paginator(Article.objects.all(), settings.PAGINATE_BY) - self.check_pagination(p, '', '') - + self.check_pagination(p, '', '') # 全部文章分页 + p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY) - self.check_pagination(p, '分类标签归档', tag.slug) - + self.check_pagination(p, '分类标签归档', tag.slug) # 标签文章分页 + p = Paginator( Article.objects.filter( author__username='liangliangyy'), settings.PAGINATE_BY) - self.check_pagination(p, '作者文章归档', 'liangliangyy') - + self.check_pagination(p, '作者文章归档', 'liangliangyy') # 作者文章分页 + p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY) - self.check_pagination(p, '分类目录归档', category.slug) - + self.check_pagination(p, '分类目录归档', category.slug) # 分类文章分页 + + # 测试搜索表单 f = BlogSearchForm() - f.search() - # self.client.login(username='liangliangyy', password='liangliangyy') - from djangoblog.spider_notify import SpiderNotify + f.search() # 调用搜索方法 + + # 测试百度蜘蛛通知 SpiderNotify.baidu_notify([article.get_full_url()]) - + + # 测试头像相关模板标签 from blog.templatetags.blog_tags import gravatar_url, gravatar - u = gravatar_url('liangliangyy@gmail.com') - u = gravatar('liangliangyy@gmail.com') - + u = gravatar_url('liangliangyy@gmail.com') # 获取头像URL + u = gravatar('liangliangyy@gmail.com') # 生成头像HTML + + # 测试友情链接页面 link = Links( sequence=1, name="lylinux", @@ -136,57 +181,75 @@ class ArticleTest(TestCase): link.save() response = self.client.get('/links.html') self.assertEqual(response.status_code, 200) - + + # 测试RSS订阅和站点地图 response = self.client.get('/feed/') self.assertEqual(response.status_code, 200) - response = self.client.get('/sitemap.xml') self.assertEqual(response.status_code, 200) - + + # 测试admin操作(删除文章、访问日志) self.client.get("/admin/blog/article/1/delete/") self.client.get('/admin/servermanager/emailsendlog/') self.client.get('/admin/admin/logentry/') self.client.get('/admin/admin/logentry/1/change/') def check_pagination(self, p, type, value): + #ymq:测试分页功能的辅助方法 for page in range(1, p.num_pages + 1): + # 调用分页模板标签获取分页信息 s = load_pagination_info(p.page(page), type, value) - self.assertIsNotNone(s) + self.assertIsNotNone(s) # 断言分页信息非空 + # 测试上一页链接 if s['previous_url']: response = self.client.get(s['previous_url']) self.assertEqual(response.status_code, 200) + # 测试下一页链接 if s['next_url']: response = self.client.get(s['next_url']) self.assertEqual(response.status_code, 200) def test_image(self): + #ymq:测试图片上传功能 import requests + # 下载测试图片 rsp = requests.get( 'https://www.python.org/static/img/python-logo.png') - imagepath = os.path.join(settings.BASE_DIR, 'python.png') + imagepath = os.path.join(settings.BASE_DIR, 'python.png') # 保存路径 with open(imagepath, 'wb') as file: - file.write(rsp.content) + file.write(rsp.content) # 保存图片 + + # 测试未授权上传 rsp = self.client.post('/upload') - self.assertEqual(rsp.status_code, 403) + self.assertEqual(rsp.status_code, 403) # 断言被拒绝 + + # 生成上传签名(模拟授权) sign = get_sha256(get_sha256(settings.SECRET_KEY)) + # 模拟带签名的上传请求 with open(imagepath, 'rb') as file: imgfile = SimpleUploadedFile( 'python.png', file.read(), content_type='image/jpg') form_data = {'python.png': imgfile} rsp = self.client.post( '/upload?sign=' + sign, form_data, follow=True) - self.assertEqual(rsp.status_code, 200) - os.remove(imagepath) + self.assertEqual(rsp.status_code, 200) # 断言上传成功 + + os.remove(imagepath) # 清理测试文件 + + # 测试用户头像保存和邮件发送工具函数 from djangoblog.utils import save_user_avatar, send_email - send_email(['qq@qq.com'], 'testTitle', 'testContent') + send_email(['qq@qq.com'], 'testTitle', 'testContent') # 测试发送邮件 save_user_avatar( - 'https://www.python.org/static/img/python-logo.png') + 'https://www.python.org/static/img/python-logo.png') # 测试保存头像 def test_errorpage(self): - rsp = self.client.get('/eee') - self.assertEqual(rsp.status_code, 404) + #ymq:测试错误页面(404) + rsp = self.client.get('/eee') # 访问不存在的URL + self.assertEqual(rsp.status_code, 404) # 断言返回404 def test_commands(self): + #ymq:测试Django管理命令 + # 创建测试用户 user = BlogUser.objects.get_or_create( email="liangliangyy@gmail.com", username="liangliangyy")[0] @@ -194,13 +257,15 @@ class ArticleTest(TestCase): user.is_staff = True user.is_superuser = True user.save() - + + # 创建OAuth配置 c = OAuthConfig() c.type = 'qq' c.appkey = 'appkey' c.appsecret = 'appsecret' c.save() - + + # 创建OAuth用户关联 u = OAuthUser() u.type = 'qq' u.openid = 'openid' @@ -211,7 +276,8 @@ class ArticleTest(TestCase): "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" }''' u.save() - + + # 创建另一个OAuth用户 u = OAuthUser() u.type = 'qq' u.openid = 'openid1' @@ -221,12 +287,15 @@ class ArticleTest(TestCase): "figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30" }''' u.save() - + + # 测试Elasticsearch索引构建命令(如果启用) from blog.documents import ELASTICSEARCH_ENABLED if ELASTICSEARCH_ENABLED: call_command("build_index") - call_command("ping_baidu", "all") - call_command("create_testdata") - call_command("clear_cache") - call_command("sync_user_avatar") - call_command("build_search_words") + + # 测试其他管理命令 + call_command("ping_baidu", "all") # 百度ping通知 + call_command("create_testdata") # 创建测试数据 + call_command("clear_cache") # 清理缓存 + call_command("sync_user_avatar") # 同步用户头像 + call_command("build_search_words") # 构建搜索词 \ No newline at end of file diff --git a/src/django-master/blog/urls.py b/src/django-master/blog/urls.py index adf2703..966e0d4 100644 --- a/src/django-master/blog/urls.py +++ b/src/django-master/blog/urls.py @@ -1,62 +1,92 @@ from django.urls import path +#ymq:导入Django的path函数,用于定义URL路由 from django.views.decorators.cache import cache_page +#ymq:导入缓存装饰器,用于对视图进行缓存 from . import views +#ymq:从当前应用导入views模块,引用视图函数/类 app_name = "blog" +#ymq:定义应用命名空间,避免URL名称冲突 + urlpatterns = [ path( r'', views.IndexView.as_view(), name='index'), + #ymq:首页URL,映射到IndexView视图类,名称为'index' + path( r'page//', views.IndexView.as_view(), name='index_page'), + #ymq:分页首页URL,接收整数类型的page参数,名称为'index_page' + path( r'article////.html', views.ArticleDetailView.as_view(), name='detailbyid'), + #ymq:文章详情页URL,接收年、月、日、文章ID参数,名称为'detailbyid' + path( r'category/.html', views.CategoryDetailView.as_view(), name='category_detail'), + #ymq:分类详情页URL,接收slug类型的分类名称参数,名称为'category_detail' + path( r'category//.html', views.CategoryDetailView.as_view(), name='category_detail_page'), + #ymq:分类分页详情页URL,接收分类名称和页码参数,名称为'category_detail_page' + path( r'author/.html', views.AuthorDetailView.as_view(), name='author_detail'), + #ymq:作者详情页URL,接收作者名称参数,名称为'author_detail' + path( r'author//.html', views.AuthorDetailView.as_view(), name='author_detail_page'), + #ymq:作者分页详情页URL,接收作者名称和页码参数,名称为'author_detail_page' + path( r'tag/.html', views.TagDetailView.as_view(), name='tag_detail'), + #ymq:标签详情页URL,接收slug类型的标签名称参数,名称为'tag_detail' + path( r'tag//.html', views.TagDetailView.as_view(), name='tag_detail_page'), + #ymq:标签分页详情页URL,接收标签名称和页码参数,名称为'tag_detail_page' + path( 'archives.html', cache_page( 60 * 60)( views.ArchivesView.as_view()), name='archives'), + #ymq:归档页面URL,使用cache_page装饰器缓存1小时(60*60秒),名称为'archives' + path( 'links.html', views.LinkListView.as_view(), name='links'), + #ymq:友情链接页面URL,映射到LinkListView视图类,名称为'links' + path( r'upload', views.fileupload, name='upload'), + #ymq:文件上传URL,映射到fileupload视图函数,名称为'upload' + path( r'clean', views.clean_cache_view, name='clean'), -] + #ymq:清理缓存URL,映射到clean_cache_view视图函数,名称为'clean' +] \ No newline at end of file diff --git a/src/django-master/blog/views.py b/src/django-master/blog/views.py index d5dc7ec..01f69af 100644 --- a/src/django-master/blog/views.py +++ b/src/django-master/blog/views.py @@ -1,6 +1,7 @@ import logging import os import uuid +#ymq:导入日志、文件操作、UUID生成相关模块 from django.conf import settings from django.core.paginator import Paginator @@ -14,17 +15,24 @@ 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 +#ymq:导入Django核心组件、视图类、HTTP响应类等 from blog.models import Article, Category, LinkShowType, Links, Tag +#ymq:从blog应用导入模型类 from comments.forms import CommentForm +#ymq:从comments应用导入评论表单 from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME +#ymq:导入插件钩子相关模块,用于扩展文章功能 from djangoblog.utils import cache, get_blog_setting, get_sha256 +#ymq:导入工具函数,用于缓存、获取博客设置和加密 logger = logging.getLogger(__name__) +#ymq:创建当前模块的日志记录器实例 class ArticleListView(ListView): + #ymq:文章列表基础视图类,继承自Django的ListView # template_name属性用于指定使用哪个模板进行渲染 template_name = 'blog/article_index.html' @@ -33,15 +41,17 @@ class ArticleListView(ListView): # 页面类型,分类目录或标签列表等 page_type = '' - paginate_by = settings.PAGINATE_BY - page_kwarg = 'page' - link_type = LinkShowType.L + paginate_by = settings.PAGINATE_BY # 分页大小,从配置中获取 + page_kwarg = 'page' # 页码参数名 + link_type = LinkShowType.L # 链接展示类型 def get_view_cache_key(self): + #ymq:获取视图缓存键(未实际使用,预留方法) return self.request.get['pages'] @property def page_number(self): + #ymq:获取当前页码(从URL参数或默认值) page_kwarg = self.page_kwarg page = self.kwargs.get( page_kwarg) or self.request.GET.get(page_kwarg) or 1 @@ -51,13 +61,13 @@ class ArticleListView(ListView): """ 子类重写.获得queryset的缓存key """ - raise NotImplementedError() + raise NotImplementedError() # 强制子类实现该方法 def get_queryset_data(self): """ 子类重写.获取queryset的数据 """ - raise NotImplementedError() + raise NotImplementedError() # 强制子类实现该方法 def get_queryset_from_cache(self, cache_key): ''' @@ -70,8 +80,8 @@ class ArticleListView(ListView): 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) + 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 @@ -80,46 +90,53 @@ class ArticleListView(ListView): 重写默认,从缓存获取数据 :return: ''' - key = self.get_queryset_cache_key() - value = self.get_queryset_from_cache(key) + key = self.get_queryset_cache_key() # 获取缓存键 + value = self.get_queryset_from_cache(key) # 从缓存获取数据 return value def get_context_data(self, **kwargs): + #ymq:扩展上下文数据,添加链接类型 kwargs['linktype'] = self.link_type - return super(ArticleListView, self).get_context_data(**kwargs) + return super(ArticleListView, self).get_context_data(** kwargs) class IndexView(ArticleListView): ''' - 首页 + 首页视图 ''' - # 友情链接类型 + # 友情链接类型:首页展示 link_type = LinkShowType.I def get_queryset_data(self): + #ymq:获取首页文章列表(已发布的文章) article_list = Article.objects.filter(type='a', status='p') return article_list def get_queryset_cache_key(self): + #ymq:生成首页缓存键(包含页码) 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" + template_name = 'blog/article_detail.html' # 详情页模板 + model = Article # 关联模型 + pk_url_kwarg = 'article_id' # URL中主键参数名 + context_object_name = "article" # 模板中上下文变量名 def get_context_data(self, **kwargs): - comment_form = CommentForm() + #ymq:扩展文章详情页的上下文数据 + comment_form = CommentForm() # 初始化评论表单 + # 获取文章评论列表 article_comments = self.object.comment_list() - parent_comments = article_comments.filter(parent_comment=None) - blog_setting = get_blog_setting() + 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(): @@ -135,26 +152,32 @@ class ArticleDetailView(DetailView): 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) @@ -163,23 +186,27 @@ class ArticleDetailView(DetailView): class CategoryDetailView(ArticleListView): ''' - 分类目录列表 + 分类目录列表视图 ''' - page_type = "分类目录归档" + page_type = "分类目录归档" # 页面类型标识 def get_queryset_data(self): - slug = self.kwargs['category_name'] - category = get_object_or_404(Category, slug=slug) + #ymq:获取指定分类下的文章列表 + slug = self.kwargs['category_name'] # 从URL获取分类别名 + 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): + #ymq:生成分类列表缓存键 slug = self.kwargs['category_name'] category = get_object_or_404(Category, slug=slug) categoryname = category.name @@ -189,59 +216,65 @@ class CategoryDetailView(ArticleListView): return cache_key def get_context_data(self, **kwargs): - + #ymq:扩展分类页上下文数据 categoryname = self.categoryname try: - categoryname = categoryname.split('/')[-1] + 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) + return super(CategoryDetailView, self).get_context_data(** kwargs) class AuthorDetailView(ArticleListView): ''' - 作者详情页 + 作者详情页视图 ''' - page_type = '作者文章归档' + page_type = '作者文章归档' # 页面类型标识 def get_queryset_cache_key(self): + #ymq:生成作者文章列表缓存键 from uuslug import slugify - author_name = slugify(self.kwargs['author_name']) + author_name = slugify(self.kwargs['author_name']) # 作者名转slug cache_key = 'author_{author_name}_{page}'.format( author_name=author_name, page=self.page_number) return cache_key def get_queryset_data(self): + #ymq:获取指定作者的文章列表 author_name = self.kwargs['author_name'] article_list = Article.objects.filter( - author__username=author_name, type='a', status='p') + author__username=author_name, type='a', status='p') # 过滤已发布的文章 return article_list def get_context_data(self, **kwargs): + #ymq:扩展作者页上下文数据 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) + return super(AuthorDetailView, self).get_context_data(** kwargs) class TagDetailView(ArticleListView): ''' - 标签列表页面 + 标签列表页面视图 ''' - page_type = '分类标签归档' + page_type = '分类标签归档' # 页面类型标识 def get_queryset_data(self): - slug = self.kwargs['tag_name'] - tag = get_object_or_404(Tag, slug=slug) + #ymq:获取指定标签的文章列表 + slug = self.kwargs['tag_name'] # 从URL获取标签别名 + 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): + #ymq:生成标签文章列表缓存键 slug = self.kwargs['tag_name'] tag = get_object_or_404(Tag, slug=slug) tag_name = tag.name @@ -251,101 +284,118 @@ class TagDetailView(ArticleListView): return cache_key def get_context_data(self, **kwargs): - # tag_name = self.kwargs['tag_name'] + #ymq:扩展标签页上下文数据 tag_name = self.name kwargs['page_type'] = TagDetailView.page_type kwargs['tag_name'] = tag_name - return super(TagDetailView, self).get_context_data(**kwargs) + 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' + page_type = '文章归档' # 页面类型标识 + paginate_by = None # 不分页 + page_kwarg = None # 无页码参数 + template_name = 'blog/article_archives.html' # 归档页模板 def get_queryset_data(self): + #ymq:获取所有已发布文章(用于归档) return Article.objects.filter(status='p').all() def get_queryset_cache_key(self): + #ymq:生成归档页缓存键 cache_key = 'archives' return cache_key class LinkListView(ListView): - model = Links - template_name = 'blog/links_list.html' + #ymq:友情链接列表视图 + model = Links # 关联模型 + template_name = 'blog/links_list.html' # 链接列表模板 def get_queryset(self): + #ymq:只获取启用的友情链接 return Links.objects.filter(is_enable=True) class EsSearchView(SearchView): + #ymq:Elasticsearch搜索视图,继承自Haystack的SearchView def get_context(self): - paginator, page = self.build_page() + #ymq:构建搜索结果页面的上下文数据 + paginator, page = self.build_page() # 处理分页 context = { - "query": self.query, - "form": self.form, - "page": page, - "paginator": paginator, - "suggestion": None, + "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()) + context.update(self.extra_context()) # 添加额外上下文 return context -@csrf_exempt +@csrf_exempt # 禁用CSRF保护(用于外部调用) def fileupload(request): """ - 该方法需自己写调用端来上传图片,该方法仅提供图床功能 + 图片/文件上传接口,需验证签名 :param request: :return: """ if request.method == 'POST': - sign = request.GET.get('sign', None) + sign = request.GET.get('sign', None) # 获取签名参数 if not sign: - return HttpResponseForbidden() + return HttpResponseForbidden() # 无签名则拒绝 + # 验证签名(双重SHA256加密对比) if not sign == get_sha256(get_sha256(settings.SECRET_KEY)): - return HttpResponseForbidden() - response = [] + return HttpResponseForbidden() # 签名错误则拒绝 + + response = [] # 存储上传后的文件URL for filename in request.FILES: - timestr = timezone.now().strftime('%Y/%m/%d') - imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] + 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) + os.makedirs(base_dir) # 目录不存在则创建 + # 生成唯一文件名(UUID+原扩展名) 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) + image.save(savepath, quality=20, optimize=True) # 压缩质量为20 + # 生成文件访问URL url = static(savepath) response.append(url) - return HttpResponse(response) + return HttpResponse(response) # 返回所有上传文件的URL else: - return HttpResponse("only for post") + return HttpResponse("only for post") # 只允许POST方法 def page_not_found_view( request, exception, template_name='blog/error_page.html'): + #ymq:404错误处理视图 if exception: - logger.error(exception) + logger.error(exception) # 记录错误日志 url = request.get_full_path() return render(request, template_name, @@ -355,6 +405,7 @@ def page_not_found_view( def server_error_view(request, template_name='blog/error_page.html'): + #ymq:500错误处理视图 return render(request, template_name, {'message': _('Sorry, the server is busy, please click the home page to see other?'), @@ -366,8 +417,9 @@ def permission_denied_view( request, exception, template_name='blog/error_page.html'): + #ymq:403错误处理视图 if exception: - logger.error(exception) + logger.error(exception) # 记录错误日志 return render( request, template_name, { 'message': _('Sorry, you do not have permission to access this page?'), @@ -375,5 +427,6 @@ def permission_denied_view( def clean_cache_view(request): - cache.clear() - return HttpResponse('ok') + #ymq:清理缓存的视图 + cache.clear() # 清除所有缓存 + return HttpResponse('ok') # 返回成功响应 \ No newline at end of file From 30d8fb38e74875dae98269e12eb11e30d1d8ec7d Mon Sep 17 00:00:00 2001 From: lxy <2811172871@qq.com> Date: Sat, 8 Nov 2025 22:07:02 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E6=A0=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/accounts/admin.py | 36 ++++++++++++++-------------- src/django-master/accounts/apps.py | 6 ++--- src/django-master/accounts/forms.py | 18 +++++++------- src/django-master/accounts/models.py | 24 +++++++++---------- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/django-master/accounts/admin.py b/src/django-master/accounts/admin.py index a702223..3b6cd79 100644 --- a/src/django-master/accounts/admin.py +++ b/src/django-master/accounts/admin.py @@ -8,45 +8,45 @@ from django.utils.translation import gettext_lazy as _ from .models import BlogUser# 导入当前应用下的BlogUser模型(自定义用户模型) -class BlogUserCreationForm(forms.ModelForm): # 定义两个密码字段,使用PasswordInput小部件隐藏输入 +class BlogUserCreationForm(forms.ModelForm): # lxy定义两个密码字段,使用PasswordInput小部件隐藏输入 password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) class Meta: - model = BlogUser# 关联的模型是BlogUser - fields = ('email',) # 表单中显示的字段(仅邮箱,密码单独定义) + model = BlogUser# lxy关联的模型是BlogUser + fields = ('email',) # lxy表单中显示的字段(仅邮箱,密码单独定义) - def clean_password2(self):# 验证两个密码是否一致 + def clean_password2(self):# lxy验证两个密码是否一致 # Check that the two password entries match password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: - raise forms.ValidationError(_("passwords do not match"))# 密码不一致时抛出错误 + raise forms.ValidationError(_("passwords do not match"))# lxy密码不一致时抛出错误 return password2 - def save(self, commit=True):# 保存用户时,对密码进行哈希处理后存储 + def save(self, commit=True):# lxy保存用户时,对密码进行哈希处理后存储 # Save the provided password in hashed format - user = super().save(commit=False)# 先不提交到数据库 - user.set_password(self.cleaned_data["password1"]) # 哈希处理密码 + user = super().save(commit=False)# lxy先不提交到数据库 + user.set_password(self.cleaned_data["password1"]) # lxy哈希处理密码 if commit: - user.source = 'adminsite'# 标记用户来源为“后台管理” - user.save()# 提交到数据库 + user.source = 'adminsite'# lxy标记用户来源为“后台管理” + user.save()# lxy提交到数据库 return user class BlogUserChangeForm(UserChangeForm): class Meta: - model = BlogUser # 关联的模型是BlogUser - fields = '__all__'# 显示模型的所有字段 - field_classes = {'username': UsernameField}# 为用户名字段指定类(保持Django原生逻辑) + model = BlogUser # lxy关联的模型是BlogUser + fields = '__all__'# lxy显示模型的所有字段 + field_classes = {'username': UsernameField}# lxy为用户名字段指定类(保持Django原生逻辑) def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs)# 调用父类的初始化方法 + super().__init__(*args, **kwargs)#lxy调用父类的初始化方法 class BlogUserAdmin(UserAdmin): - form = BlogUserChangeForm# 指定修改用户时使用的表单 - add_form = BlogUserCreationForm# 指定创建用户时使用的表单 + form = BlogUserChangeForm#lxy指定修改用户时使用的表单 + add_form = BlogUserCreationForm# lxy指定创建用户时使用的表单 list_display = ( 'id', 'nickname', @@ -55,5 +55,5 @@ class BlogUserAdmin(UserAdmin): 'last_login', 'date_joined', 'source') - list_display_links = ('id', 'username')# 列表页中可点击跳转的字段 - ordering = ('-id',)# 列表页的排序方式(按ID倒序) + list_display_links = ('id', 'username')#lxy列表页中可点击跳转的字段 + ordering = ('-id',)#lxy列表页的排序方式(按ID倒序) diff --git a/src/django-master/accounts/apps.py b/src/django-master/accounts/apps.py index 0101a53..9549fc6 100644 --- a/src/django-master/accounts/apps.py +++ b/src/django-master/accounts/apps.py @@ -1,5 +1,5 @@ -from django.apps import AppConfig#导入 Django 框架中用于应用配置的 AppConfig 类,这是 Django 应用配置的核心类 +from django.apps import AppConfig#lxy导入 Django 框架中用于应用配置的 AppConfig 类,这是 Django 应用配置的核心类 -class AccountsConfig(AppConfig):#定义 AccountsConfig 类,继承自 AppConfig,用于对 accounts 应用进行自定义配置 - name = 'accounts'#指定该应用的名称为 accounts,Django 会通过这个名称来识别和管理该应用 +class AccountsConfig(AppConfig):#lxy定义 AccountsConfig 类,继承自 AppConfig,用于对 accounts 应用进行自定义配置 + name = 'accounts'#lxy指定该应用的名称为 accounts,Django 会通过这个名称来识别和管理该应用 diff --git a/src/django-master/accounts/forms.py b/src/django-master/accounts/forms.py index 9ce4052..df7c4d9 100644 --- a/src/django-master/accounts/forms.py +++ b/src/django-master/accounts/forms.py @@ -15,7 +15,7 @@ class LoginForm(AuthenticationForm): attrs={'placeholder': "username", "class": "form-control"}) self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) -#自定义登录表单,在__init__方法中设置username(文本输入,占位符、form-control样式)和password(密码输入,占位符、form-control样式)字段的前端显示样式。 +#lxy自定义登录表单,在__init__方法中设置username(文本输入,占位符、form-control样式)和password(密码输入,占位符、form-control样式)字段的前端显示样式。 class RegisterForm(UserCreationForm): def __init__(self, *args, **kwargs): @@ -29,17 +29,17 @@ class RegisterForm(UserCreationForm): attrs={'placeholder': "password", "class": "form-control"}) self.fields['password2'].widget = widgets.PasswordInput( attrs={'placeholder': "repeat password", "class": "form-control"}) -#__init__方法中设置username(文本输入)、email(邮箱输入)、password1和password2(密码输入)字段的占位符与form-control样式 +#lxy__init__方法中设置username(文本输入)、email(邮箱输入)、password1和password2(密码输入)字段的占位符与form-control样式 def clean_email(self): email = self.cleaned_data['email'] if get_user_model().objects.filter(email=email).exists(): raise ValidationError(_("email already exists")) return email -#clean_email方法验证邮箱是否已被注册,若存在则抛出“邮箱已存在”的验证错误 +#lxyclean_email方法验证邮箱是否已被注册,若存在则抛出“邮箱已存在”的验证错误 class Meta: model = get_user_model() fields = ("username", "email") -#Meta类指定关联模型为自定义用户模型,表单字段包含username和email +#lxy Meta类指定关联模型为自定义用户模型,表单字段包含username和email class ForgetPasswordForm(forms.Form): new_password1 = forms.CharField( @@ -81,7 +81,7 @@ class ForgetPasswordForm(forms.Form): } ), ) -#定义new_password1(新密码,密码输入)、new_password2(确认密码,密码输入)、email(邮箱,文本输入)、code(验证码,文本输入)字段,均设置form-control样式和占位符。 +#lxy定义new_password1(新密码,密码输入)、new_password2(确认密码,密码输入)、email(邮箱,文本输入)、code(验证码,文本输入)字段,均设置form-control样式和占位符。 def clean_new_password2(self): password1 = self.data.get("new_password1") password2 = self.data.get("new_password2") @@ -90,7 +90,7 @@ class ForgetPasswordForm(forms.Form): password_validation.validate_password(password2) return password2 -# clean_new_password2方法验证两次新密码是否一致,并对密码进行有效性校验 +#lxyclean_new_password2方法验证两次新密码是否一致,并对密码进行有效性校验 def clean_email(self): user_email = self.cleaned_data.get("email") if not BlogUser.objects.filter( @@ -100,7 +100,7 @@ class ForgetPasswordForm(forms.Form): raise ValidationError(_("email does not exist")) return user_email -# clean_email方法验证邮箱是否已注册(基于BlogUser模型),未注册则抛出“邮箱不存在”的验证错误 +#lxyclean_email方法验证邮箱是否已注册(基于BlogUser模型),未注册则抛出“邮箱不存在”的验证错误 def clean_code(self): code = self.cleaned_data.get("code") error = utils.verify( @@ -110,10 +110,10 @@ class ForgetPasswordForm(forms.Form): if error: raise ValidationError(error) return code -#clean_code方法调用工具方法utils.verify验证验证码有效性,无效则抛出错误 +#lxy clean_code方法调用工具方法utils.verify验证验证码有效性,无效则抛出错误 class ForgetPasswordCodeForm(forms.Form): email = forms.EmailField( label=_('Email'), ) -#仅包含email字段(邮箱输入),用于忘记密码流程中验证邮箱的步骤 \ No newline at end of file +#lxy仅包含email字段(邮箱输入),用于忘记密码流程中验证邮箱的步骤 \ No newline at end of file diff --git a/src/django-master/accounts/models.py b/src/django-master/accounts/models.py index 438173a..fbbd1e7 100644 --- a/src/django-master/accounts/models.py +++ b/src/django-master/accounts/models.py @@ -8,29 +8,29 @@ from djangoblog.utils import get_current_site # Create your models here. -class BlogUser(AbstractUser):#自定义用户模型BlogUser,继承自Django内置的AbstractUser(可扩展的用户抽象类)。 - nickname = models.CharField(_('nick name'), max_length=100, blank=True)#定义nickname字段,字符类型,支持国际化翻译,最大长度100,可为空。 - creation_time = models.DateTimeField(_('creation time'), default=now)#定义creation_time字段,日期时间类型,默认值为当前时间(now方法)。 - last_modify_time = models.DateTimeField(_('last modify time'), default=now)#定义last_modify_time字段,日期时间类型,默认值为当前时间。 - source = models.CharField(_('create source'), max_length=100, blank=True)#定义source字段,字符类型,记录用户创建来源,最大长度100,可为空。 +class BlogUser(AbstractUser):#lxy自定义用户模型BlogUser,继承自Django内置的AbstractUser(可扩展的用户抽象类)。 + nickname = models.CharField(_('nick name'), max_length=100, blank=True)#lxy定义nickname字段,字符类型,支持国际化翻译,最大长度100,可为空。 + creation_time = models.DateTimeField(_('creation time'), default=now)#lxy定义creation_time字段,日期时间类型,默认值为当前时间(now方法)。 + last_modify_time = models.DateTimeField(_('last modify time'), default=now)#lxy定义last_modify_time字段,日期时间类型,默认值为当前时间。 + source = models.CharField(_('create source'), max_length=100, blank=True)#lxy定义source字段,字符类型,记录用户创建来源,最大长度100,可为空。 def get_absolute_url(self): return reverse( 'blog:author_detail', kwargs={ - 'author_name': self.username})#定义获取用户详情页绝对URL的方法,通过reverse反向解析路由blog:author_detail,传递username参数。 + 'author_name': self.username})#lxy定义获取用户详情页绝对URL的方法,通过reverse反向解析路由blog:author_detail,传递username参数。 def __str__(self): - return self.email#定义对象的字符串表示方法,返回用户的email + return self.email#lxy定义对象的字符串表示方法,返回用户的email def get_full_url(self): site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) - return url#定义获取带域名的完整URL方法,结合当前站点域名和get_absolute_url生成完整链接 + return url#lxy定义获取带域名的完整URL方法,结合当前站点域名和get_absolute_url生成完整链接 class Meta: - ordering = ['-id']#查询结果按id倒序排列 - verbose_name = _('user')#模型的单数显示名称(支持国际化) - verbose_name_plural = verbose_name#模型的复数显示名称与单数一致。 - get_latest_by = 'id'#指定按id获取最新记录 + ordering = ['-id']#lxy查询结果按id倒序排列 + verbose_name = _('user')#lxy模型的单数显示名称(支持国际化) + verbose_name_plural = verbose_name#lxy模型的复数显示名称与单数一致。 + get_latest_by = 'id'#lxy指定按id获取最新记录 From a02180ded001b406ffa188a38f2ed9c8bbb9af02 Mon Sep 17 00:00:00 2001 From: lxy <2811172871@qq.com> Date: Sat, 8 Nov 2025 22:18:06 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E6=A0=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/accounts/tests.py | 10 +++---- .../accounts/user_login_backend.py | 6 ++-- src/django-master/accounts/utils.py | 4 +-- src/django-master/accounts/views.py | 28 +++++++++---------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/django-master/accounts/tests.py b/src/django-master/accounts/tests.py index 8cd5036..2fb71e8 100644 --- a/src/django-master/accounts/tests.py +++ b/src/django-master/accounts/tests.py @@ -53,7 +53,7 @@ class AccountTest(TestCase): article.save() response = self.client.get(article.get_admin_url()) - self.assertEqual(response.status_code, 200)#测试管理员账号登录后台功能:创建超级用户,验证登录状态和后台页面访问状态 + self.assertEqual(response.status_code, 200)#lxy测试管理员账号登录后台功能:创建超级用户,验证登录状态和后台页面访问状态 def test_validate_register(self): self.assertEquals( @@ -116,7 +116,7 @@ class AccountTest(TestCase): self.assertIn(response.status_code, [301, 302, 200]) response = self.client.get(article.get_admin_url()) - self.assertIn(response.status_code, [301, 302, 200])#测试用户注册流程:验证注册前后用户数量变化,邮箱验证链接的有效性,以及注册后用户权限、文章发布等功能 + self.assertIn(response.status_code, [301, 302, 200])#lxy测试用户注册流程:验证注册前后用户数量变化,邮箱验证链接的有效性,以及注册后用户权限、文章发布等功能 def test_verify_email_code(self): to_email = "admin@admin.com" @@ -128,7 +128,7 @@ class AccountTest(TestCase): self.assertEqual(err, None) err = utils.verify("admin@123.com", code) - self.assertEqual(type(err), str)#测试邮箱验证码功能:验证有效邮箱和无效邮箱的验证码校验结果 + self.assertEqual(type(err), str)#lxy测试邮箱验证码功能:验证有效邮箱和无效邮箱的验证码校验结果 def test_forget_password_email_code_success(self): resp = self.client.post( @@ -137,7 +137,7 @@ class AccountTest(TestCase): ) self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content.decode("utf-8"), "ok")#测试忘记密码的邮箱验证码发送:分别验证成功和失败场景(如邮箱错误)的接口响应 + self.assertEqual(resp.content.decode("utf-8"), "ok")#lxy测试忘记密码的邮箱验证码发送:分别验证成功和失败场景(如邮箱错误)的接口响应 def test_forget_password_email_code_fail(self): resp = self.client.post( @@ -203,5 +203,5 @@ class AccountTest(TestCase): data=data ) - self.assertEqual(resp.status_code, 200)#测试忘记密码流程:成功场景:验证密码修改后是否生效;失败场景:验证不存在用户、验证码错误时的接口响应 + self.assertEqual(resp.status_code, 200)#lxy测试忘记密码流程:成功场景:验证密码修改后是否生效;失败场景:验证不存在用户、验证码错误时的接口响应 diff --git a/src/django-master/accounts/user_login_backend.py b/src/django-master/accounts/user_login_backend.py index da55cc1..a9d9f72 100644 --- a/src/django-master/accounts/user_login_backend.py +++ b/src/django-master/accounts/user_login_backend.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend -class EmailOrUsernameModelBackend(ModelBackend):#自定义Django认证后端,支持用户名或邮箱两种方式登录。 +class EmailOrUsernameModelBackend(ModelBackend):#lxy自定义Django认证后端,支持用户名或邮箱两种方式登录。 """ 允许使用用户名或邮箱登录 """ @@ -18,10 +18,10 @@ class EmailOrUsernameModelBackend(ModelBackend):#自定义Django认证后端, return user except get_user_model().DoesNotExist: return None -#核心认证逻辑:判断输入是否为邮箱(含@),分别用邮箱或用户名查询用户,验证密码后返回用户对象;若用户不存在则返回None。 +#lxy核心认证逻辑:判断输入是否为邮箱(含@),分别用邮箱或用户名查询用户,验证密码后返回用户对象;若用户不存在则返回None。 def get_user(self, username): try: return get_user_model().objects.get(pk=username) except get_user_model().DoesNotExist: return None -#根据用户ID(主键)查询用户,不存在则返回None,用于Django认证系统的用户查询环节 \ No newline at end of file +#lxy根据用户ID(主键)查询用户,不存在则返回None,用于Django认证系统的用户查询环节 \ No newline at end of file diff --git a/src/django-master/accounts/utils.py b/src/django-master/accounts/utils.py index da3a40f..1a410b7 100644 --- a/src/django-master/accounts/utils.py +++ b/src/django-master/accounts/utils.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from djangoblog.utils import send_email -_code_ttl = timedelta(minutes=5)#验证码有效期,设置为5分钟。 +_code_ttl = timedelta(minutes=5)#lxy验证码有效期,设置为5分钟。 def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): @@ -44,6 +44,6 @@ def set_code(email: str, code: str): cache.set(email, code, _code_ttl.seconds) -def get_code(email: str) -> typing.Optional[str]:#从缓存中获取指定邮箱对应的验证码 +def get_code(email: str) -> typing.Optional[str]:#lxy从缓存中获取指定邮箱对应的验证码 """获取code""" return cache.get(email) diff --git a/src/django-master/accounts/views.py b/src/django-master/accounts/views.py index 2c69a88..c7b45d7 100644 --- a/src/django-master/accounts/views.py +++ b/src/django-master/accounts/views.py @@ -33,7 +33,7 @@ logger = logging.getLogger(__name__) class RegisterView(FormView): form_class = RegisterForm - template_name = 'account/registration_form.html'#处理用户注册逻辑,指定表单类RegisterForm和模板account/registration_form.html。 + template_name = 'account/registration_form.html'#lxy处理用户注册逻辑,指定表单类RegisterForm和模板account/registration_form.html。 @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): @@ -77,11 +77,11 @@ class RegisterView(FormView): else: return self.render_to_response({ 'form': form - })#form_valid方法中,保存用户并设置为非活跃状态,生成邮箱验证链接并发送验证邮件,最后重定向到结果页。 + })#lxyform_valid方法中,保存用户并设置为非活跃状态,生成邮箱验证链接并发送验证邮件,最后重定向到结果页。 class LogoutView(RedirectView): - url = '/login/'#处理用户登出,登出后重定向到/login/ + url = '/login/'#lxy处理用户登出,登出后重定向到/login/ @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): @@ -90,7 +90,7 @@ class LogoutView(RedirectView): def get(self, request, *args, **kwargs): logout(request) delete_sidebar_cache() - return super(LogoutView, self).get(request, *args, **kwargs)#get方法中调用logout登出用户,删除侧边栏缓存后完成重定向 + return super(LogoutView, self).get(request, *args, **kwargs)#lxyget方法中调用logout登出用户,删除侧边栏缓存后完成重定向 class LoginView(FormView): @@ -98,7 +98,7 @@ class LoginView(FormView): template_name = 'account/login.html' success_url = '/' redirect_field_name = REDIRECT_FIELD_NAME - login_ttl = 2626560 # 一个月的时间 + login_ttl = 2626560 #lxy 一个月的时间 @method_decorator(sensitive_post_parameters('password')) @method_decorator(csrf_protect) @@ -113,7 +113,7 @@ class LoginView(FormView): redirect_to = '/' kwargs['redirect_to'] = redirect_to - return super(LoginView, self).get_context_data(**kwargs)#处理用户登录逻辑,指定表单类LoginForm、模板account / login.html和成功后重定向地址 / + return super(LoginView, self).get_context_data(**kwargs)#lxy处理用户登录逻辑,指定表单类LoginForm、模板account / login.html和成功后重定向地址 / def form_valid(self, form): form = AuthenticationForm(data=self.request.POST, request=self.request) @@ -125,11 +125,11 @@ 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('/') + #lxyreturn HttpResponseRedirect('/') else: return self.render_to_response({ 'form': form - })#form_valid方法中验证表单,登录用户并根据“记住我”选项设置会话过期时间 + })#lxyform_valid方法中验证表单,登录用户并根据“记住我”选项设置会话过期时间 def get_success_url(self): @@ -138,7 +138,7 @@ class LoginView(FormView): url=redirect_to, allowed_hosts=[ self.request.get_host()]): redirect_to = self.success_url - return redirect_to#get_success_url方法处理登录后的重定向地址,确保其安全性 + return redirect_to#lxyget_success_url方法处理登录后的重定向地址,确保其安全性 def account_result(request): @@ -146,7 +146,7 @@ def account_result(request): id = request.GET.get('id') user = get_object_or_404(get_user_model(), id=id) - logger.info(type)#处理注册和邮箱验证的结果逻辑,根据type参数区分场景: + logger.info(type)#lxy处理注册和邮箱验证的结果逻辑,根据type参数区分场景: if user.is_active: return HttpResponseRedirect('/') if type and type in ['register', 'validation']: @@ -176,7 +176,7 @@ def account_result(request): class ForgetPasswordView(FormView): form_class = ForgetPasswordForm - template_name = 'account/forget_password.html'#处理忘记密码逻辑,指定表单类ForgetPasswordForm和模板account/forget_password.html + template_name = 'account/forget_password.html'#lxy处理忘记密码逻辑,指定表单类ForgetPasswordForm和模板account/forget_password.html def form_valid(self, form): if form.is_valid(): @@ -185,10 +185,10 @@ class ForgetPasswordView(FormView): blog_user.save() return HttpResponseRedirect('/login/') else: - return self.render_to_response({'form': form})#form_valid方法中验证表单后,重置用户密码并重定向到登录页 + return self.render_to_response({'form': form})#lxyform_valid方法中验证表单后,重置用户密码并重定向到登录页 -class ForgetPasswordEmailCode(View):# 处理忘记密码的邮箱验证码发送逻辑 +class ForgetPasswordEmailCode(View):#lxy处理忘记密码的邮箱验证码发送逻辑 def post(self, request: HttpRequest): form = ForgetPasswordCodeForm(request.POST) @@ -200,4 +200,4 @@ class ForgetPasswordEmailCode(View):# 处理忘记密码的邮箱验证码发送 utils.send_verify_email(to_email, code) utils.set_code(to_email, code) - return HttpResponse("ok")# post方法中验证邮箱表单,生成并发送验证码,将验证码存入缓存后返回成功标识 + return HttpResponse("ok")#lxypost方法中验证邮箱表单,生成并发送验证码,将验证码存入缓存后返回成功标识 From 01fb9fe3947078232290d1e553f11775ad1dddfe Mon Sep 17 00:00:00 2001 From: lxy <2811172871@qq.com> Date: Sat, 8 Nov 2025 22:41:02 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/blog/admin.py | 72 ++++++++++---------- src/django-master/blog/apps.py | 6 +- src/django-master/blog/context_processors.py | 54 +++++++-------- 3 files changed, 66 insertions(+), 66 deletions(-) diff --git a/src/django-master/blog/admin.py b/src/django-master/blog/admin.py index 46c3420..a401310 100644 --- a/src/django-master/blog/admin.py +++ b/src/django-master/blog/admin.py @@ -9,41 +9,41 @@ from django.utils.translation import gettext_lazy as _ from .models import Article -class ArticleForm(forms.ModelForm): - # body = forms.CharField(widget=AdminPagedownWidget()) +class ArticleForm(forms.ModelForm):#lxy 文章表单类 + # body = forms.CharField(widget=AdminPagedownWidget())#lxy 富文本组件 class Meta: - model = Article - fields = '__all__' + model = Article#lxy 关联Article模型 + fields = '__all__'#lxy 包含所有字段 -def makr_article_publish(modeladmin, request, queryset): +def makr_article_publish(modeladmin, request, queryset):#lxy 批量设为已发布 queryset.update(status='p') -def draft_article(modeladmin, request, queryset): +def draft_article(modeladmin, request, queryset):#lxy 批量设为草稿 queryset.update(status='d') -def close_article_commentstatus(modeladmin, request, queryset): +def close_article_commentstatus(modeladmin, request, queryset):#lxy 关闭评论 queryset.update(comment_status='c') -def open_article_commentstatus(modeladmin, request, queryset): +def open_article_commentstatus(modeladmin, request, queryset):#lxy 开启评论 queryset.update(comment_status='o') - +#lxy 操作描述 makr_article_publish.short_description = _('Publish selected articles') draft_article.short_description = _('Draft selected articles') close_article_commentstatus.short_description = _('Close article comments') open_article_commentstatus.short_description = _('Open article comments') -class ArticlelAdmin(admin.ModelAdmin): - list_per_page = 20 - search_fields = ('body', 'title') - form = ArticleForm - list_display = ( +class ArticlelAdmin(admin.ModelAdmin):#lxy 文章Admin配置 + list_per_page = 20#lxy 每页显示20条 + search_fields = ('body', 'title')#lxy 搜索字段 + form = ArticleForm#lxy 关联表单 + list_display = (#lxy 列表显示字段 'id', 'title', 'author', @@ -53,34 +53,34 @@ class ArticlelAdmin(admin.ModelAdmin): 'status', 'type', 'article_order') - list_display_links = ('id', 'title') - list_filter = ('status', 'type', 'category') - filter_horizontal = ('tags',) - exclude = ('creation_time', 'last_modify_time') - view_on_site = True - actions = [ + list_display_links = ('id', 'title') #lxy 排序字段 + list_filter = ('status', 'type', 'category') #lxy 可点击字段 + filter_horizontal = ('tags',)#lxy 标签选择器 + exclude = ('creation_time', 'last_modify_time')#lxy 隐藏字段 + view_on_site = True#lxy 允许查看站点 + actions = [#lxy 自定义操作 makr_article_publish, draft_article, close_article_commentstatus, open_article_commentstatus] - def link_to_category(self, obj): + def link_to_category(self, obj):#lxy 分类链接 info = (obj.category._meta.app_label, obj.category._meta.model_name) link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) return format_html(u'%s' % (link, obj.category.name)) - link_to_category.short_description = _('category') + link_to_category.short_description = _('category') #lxy 字段名称 - def get_form(self, request, obj=None, **kwargs): + def get_form(self, request, obj=None, **kwargs):#lxy 重写表单 form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) form.base_fields['author'].queryset = get_user_model( ).objects.filter(is_superuser=True) return form - def save_model(self, request, obj, form, change): + def save_model(self, request, obj, form, change):#lxy 重写保存 super(ArticlelAdmin, self).save_model(request, obj, form, change) - def get_view_on_site_url(self, obj=None): + def get_view_on_site_url(self, obj=None):#lxy 查看站点URL if obj: url = obj.get_full_url() return url @@ -90,23 +90,23 @@ class ArticlelAdmin(admin.ModelAdmin): return site -class TagAdmin(admin.ModelAdmin): - exclude = ('slug', 'last_mod_time', 'creation_time') +class TagAdmin(admin.ModelAdmin):#lxy 标签Admin配置 + exclude = ('slug', 'last_mod_time', 'creation_time')#lxy 隐藏字段 -class CategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'parent_category', 'index') - exclude = ('slug', 'last_mod_time', 'creation_time') +class CategoryAdmin(admin.ModelAdmin):#lxy 分类Admin配置 + list_display = ('name', 'parent_category', 'index') #lxy 列表显示 + exclude = ('slug', 'last_mod_time', 'creation_time')#lxy 隐藏字段 -class LinksAdmin(admin.ModelAdmin): - exclude = ('last_mod_time', 'creation_time') +class LinksAdmin(admin.ModelAdmin): #lxy 链接Admin配置 + exclude = ('last_mod_time', 'creation_time') #lxy 隐藏字段 -class SideBarAdmin(admin.ModelAdmin): - list_display = ('name', 'content', 'is_enable', 'sequence') - exclude = ('last_mod_time', 'creation_time') +class SideBarAdmin(admin.ModelAdmin): #lxy 侧边栏Admin配置 + list_display = ('name', 'content', 'is_enable', 'sequence')#lxy 列表显示 + exclude = ('last_mod_time', 'creation_time') #lxy 隐藏字段 -class BlogSettingsAdmin(admin.ModelAdmin): +class BlogSettingsAdmin(admin.ModelAdmin):#lxy 博客设置Admin配置 pass diff --git a/src/django-master/blog/apps.py b/src/django-master/blog/apps.py index 7930587..a70c932 100644 --- a/src/django-master/blog/apps.py +++ b/src/django-master/blog/apps.py @@ -1,5 +1,5 @@ -from django.apps import AppConfig +from django.apps import AppConfig#lxy 导入Django应用配置类 -class BlogConfig(AppConfig): - name = 'blog' +class BlogConfig(AppConfig):#lxy 博客应用的配置类 + name = 'blog'#lxy 应用名称(对应项目中的blog模块) diff --git a/src/django-master/blog/context_processors.py b/src/django-master/blog/context_processors.py index 73e3088..2133596 100644 --- a/src/django-master/blog/context_processors.py +++ b/src/django-master/blog/context_processors.py @@ -8,36 +8,36 @@ from .models import Category, Article logger = logging.getLogger(__name__) -def seo_processor(requests): - key = 'seo_processor' - value = cache.get(key) +def seo_processor(requests):#lxy SEO相关上下文处理器 + key = 'seo_processor'#lxy 缓存键名 + value = cache.get(key)#lxy 从缓存取数据 if value: - return value + return value#lxy 有缓存则直接返回 else: - logger.info('set processor cache.') - setting = get_blog_setting() + logger.info('set processor cache.')#lxy 记录缓存设置日志 + setting = get_blog_setting()#lxy 获取博客配置 value = { - 'SITE_NAME': setting.site_name, - 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, - 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, - 'SITE_SEO_DESCRIPTION': setting.site_seo_description, - 'SITE_DESCRIPTION': setting.site_description, - 'SITE_KEYWORDS': setting.site_keywords, - 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', - 'ARTICLE_SUB_LENGTH': setting.article_sub_length, - 'nav_category_list': Category.objects.all(), - 'nav_pages': Article.objects.filter( + 'SITE_NAME': setting.site_name,#lxy 站点名称 + 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,#lxy 是否显示谷歌广告 + #lxy 谷歌广告代码 + 'SITE_SEO_DESCRIPTION': setting.site_seo_description,#lxy 站点SEO描述 + 'SITE_DESCRIPTION': setting.site_description,#lxy 站点描述 + 'SITE_KEYWORDS': setting.site_keywords,#lxy 站点关键词 + 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',#lxy 站点基础URL + 'ARTICLE_SUB_LENGTH': setting.article_sub_length,#lxy 文章摘要长度 + 'nav_category_list': Category.objects.all(),#lxy 导航分类列表 + 'nav_pages': Article.objects.filter(#lxy 导航页面 type='p', status='p'), - 'OPEN_SITE_COMMENT': setting.open_site_comment, - 'BEIAN_CODE': setting.beian_code, - 'ANALYTICS_CODE': setting.analytics_code, - "BEIAN_CODE_GONGAN": setting.gongan_beiancode, - "SHOW_GONGAN_CODE": setting.show_gongan_code, - "CURRENT_YEAR": timezone.now().year, - "GLOBAL_HEADER": setting.global_header, - "GLOBAL_FOOTER": setting.global_footer, - "COMMENT_NEED_REVIEW": setting.comment_need_review, + 'OPEN_SITE_COMMENT': setting.open_site_comment,#lxy 是否开启站点评论 + 'BEIAN_CODE': setting.beian_code,#lxy 备案号 + 'ANALYTICS_CODE': setting.analytics_code,#lxy 统计代码 + "BEIAN_CODE_GONGAN": setting.gongan_beiancode,#lxy 公安备案号 + "SHOW_GONGAN_CODE": setting.show_gongan_code, #lxy 是否显示公安备案 + "CURRENT_YEAR": timezone.now().year,#lxy 当前年份 + "GLOBAL_HEADER": setting.global_header,#lxy 全局头部内容 + "GLOBAL_FOOTER": setting.global_footer,#lxy 全局底部内容 + "COMMENT_NEED_REVIEW": setting.comment_need_review,#lxy 评论是否需要审核 } - cache.set(key, value, 60 * 60 * 10) - return value + cache.set(key, value, 60 * 60 * 10)#lxy 设置缓存(有效期10小时) + return value#lxy 返回上下文数据 From 469bb816fd29e1c39e821d4cb8ca7546f8c943ae Mon Sep 17 00:00:00 2001 From: lxy <2811172871@qq.com> Date: Sat, 8 Nov 2025 23:21:11 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/django-master/blog/documents.py | 183 ++++----- src/django-master/blog/forms.py | 20 +- src/django-master/blog/middleware.py | 31 +- src/django-master/blog/models.py | 457 ++++++++++------------- src/django-master/blog/search_indexes.py | 14 +- src/django-master/blog/tests.py | 54 +-- src/django-master/blog/urls.py | 32 +- src/django-master/blog/views.py | 73 ++-- 8 files changed, 395 insertions(+), 469 deletions(-) diff --git a/src/django-master/blog/documents.py b/src/django-master/blog/documents.py index 0f1db7b..efa64dd 100644 --- a/src/django-master/blog/documents.py +++ b/src/django-master/blog/documents.py @@ -8,19 +8,19 @@ from elasticsearch_dsl.connections import connections from blog.models import Article ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') - +#lxy 判断是否启用ES if ELASTICSEARCH_ENABLED: - connections.create_connection( + connections.create_connection(#lxy 创建ES连接 hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) - from elasticsearch import Elasticsearch + from elasticsearch import Elasticsearch#lxy 导入ES客户端 - es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) - from elasticsearch.client import IngestClient + es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])#lxy 初始化ES客户端 + from elasticsearch.client import IngestClient#lxy 导入Ingest客户端 - c = IngestClient(es) + c = IngestClient(es) #lxy 初始化Ingest客户端 try: - c.get_pipeline('geoip') - except elasticsearch.exceptions.NotFoundError: + c.get_pipeline('geoip')#lxy 检查geoip管道是否存在 + except elasticsearch.exceptions.NotFoundError:#lxy 创建geoip管道(解析IP地理信息) c.put_pipeline('geoip', body='''{ "description" : "Add geoip info", "processors" : [ @@ -33,151 +33,134 @@ if ELASTICSEARCH_ENABLED: }''') -class GeoIp(InnerDoc): - continent_name = Keyword() - country_iso_code = Keyword() - country_name = Keyword() - location = GeoPoint() - - -class UserAgentBrowser(InnerDoc): - Family = Keyword() - Version = Keyword() - +class GeoIp(InnerDoc): #lxy IP地理信息嵌套文档 + continent_name = Keyword() #lxy 大洲名称 + country_iso_code = Keyword() #lxy 国家ISO代码 + country_name = Keyword() #lxy 国家名称 + location = GeoPoint() #lxy 地理位置坐标 -class UserAgentOS(UserAgentBrowser): - pass +class UserAgentBrowser(InnerDoc): #lxy 浏览器信息嵌套文档 + Family = Keyword() #lxy 浏览器类型 + Version = Keyword() #lxy 浏览器版本 -class UserAgentDevice(InnerDoc): - Family = Keyword() - Brand = Keyword() - Model = Keyword() +class UserAgentOS(UserAgentBrowser): #lxy 操作系统信息嵌套文档 + pass #lxy 继承浏览器文档结构 -class UserAgent(InnerDoc): - browser = Object(UserAgentBrowser, required=False) - os = Object(UserAgentOS, required=False) - device = Object(UserAgentDevice, required=False) - string = Text() - is_bot = Boolean() +class UserAgentDevice(InnerDoc): #lxy 设备信息嵌套文档 + Family = Keyword() #lxy 设备类型 + Brand = Keyword() #lxy 设备品牌 + Model = Keyword() #lxy 设备型号 +class UserAgent(InnerDoc): #lxy 用户代理信息嵌套文档 + browser = Object(UserAgentBrowser, required=False) #lxy 浏览器信息 + os = Object(UserAgentOS, required=False) #lxy 操作系统信息 + device = Object(UserAgentDevice, required=False) #lxy 设备信息 + string = Text() #lxy 用户代理原始字符串 + is_bot = Boolean() #lxy 是否为爬虫 -class ElapsedTimeDocument(Document): - url = Keyword() - time_taken = Long() - log_datetime = Date() - ip = Keyword() - geoip = Object(GeoIp, required=False) - useragent = Object(UserAgent, required=False) + class ElapsedTimeDocument(Document): # lxy 耗时统计ES文档类 + url = Keyword() # lxy 请求URL + time_taken = Long() # lxy 耗时(毫秒) + log_datetime = Date() # lxy 日志时间 + ip = Keyword() # lxy 请求IP + geoip = Object(GeoIp, required=False) # lxy IP地理信息 + useragent = Object(UserAgent, required=False) # lxy 用户代理信息 - class Index: - name = 'performance' + class Index: #lxy ES索引配置 + name = 'performance' #lxy 索引名称 settings = { - "number_of_shards": 1, - "number_of_replicas": 0 + "number_of_shards": 1, #lxy 分片数 + "number_of_replicas": 0 #lxy 副本数 } + class Meta: #lxy 文档元信息 + doc_type = 'ElapsedTime' #lxy 文档类型 - class Meta: - doc_type = 'ElapsedTime' - - -class ElaspedTimeDocumentManager: +class ElapsedTimeDocumentManager: #lxy 耗时文档管理类 @staticmethod - def build_index(): + def build_index(): #lxy 创建ES索引 from elasticsearch import Elasticsearch client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) - res = client.indices.exists(index="performance") + res = client.indices.exists(index="performance") #lxy 检查索引是否存在 if not res: - ElapsedTimeDocument.init() + ElapsedTimeDocument.init() #lxy 初始化索引 @staticmethod - def delete_index(): + def delete_index(): #lxy 删除ES索引 from elasticsearch import Elasticsearch es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) - es.indices.delete(index='performance', ignore=[400, 404]) + es.indices.delete(index='performance', ignore=[400, 404]) #lxy 忽略不存在错误 @staticmethod - def create(url, time_taken, log_datetime, useragent, ip): - ElaspedTimeDocumentManager.build_index() - ua = UserAgent() + def create(url, time_taken, log_datetime, useragent, ip): #lxy 创建耗时文档 + ElapsedTimeDocumentManager.build_index() + ua = UserAgent() #lxy 初始化用户代理对象 ua.browser = UserAgentBrowser() ua.browser.Family = useragent.browser.family ua.browser.Version = useragent.browser.version_string - ua.os = UserAgentOS() ua.os.Family = useragent.os.family ua.os.Version = useragent.os.version_string - ua.device = UserAgentDevice() ua.device.Family = useragent.device.family ua.device.Brand = useragent.device.brand ua.device.Model = useragent.device.model ua.string = useragent.ua_string ua.is_bot = useragent.is_bot - - doc = ElapsedTimeDocument( + doc = ElapsedTimeDocument( #lxy 构造耗时文档 meta={ - 'id': int( - round( - time.time() * - 1000)) + 'id': int(round(time.time() * 10000)) #lxy 生成唯一ID }, - url=url, - time_taken=time_taken, - log_datetime=log_datetime, + url=url, time_taken=time_taken, log_datetime=log_datetime, useragent=ua, ip=ip) - doc.save(pipeline="geoip") + doc.save(pipeline="geoip") #lxy 保存文档(用geoip管道解析IP) - -class ArticleDocument(Document): - body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') - title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') - author = Object(properties={ +class ArticleDocument(Document): #lxy 文章ES文档类 + body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') #lxy 文章内容(中文分词) + title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') #lxy 文章标题(中文分词) + author = Object(properties={ #lxy 作者信息 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'id': Integer() }) - category = Object(properties={ + category = Object(properties={ #lxy 分类信息 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'id': Integer() }) - tags = Object(properties={ + tags = Object(properties={ #lxy 标签信息 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), 'id': Integer() }) - - pub_time = Date() - status = Text() - comment_status = Text() - type = Text() - views = Integer() - article_order = Integer() - - class Index: - name = 'blog' + pub_time = Date() #lxy 发布时间 + status = Text() #lxy 文章状态 + comment_status = Text() #lxy 评论状态 + type = Text() #lxy 文章类型 + views = Integer() #lxy 阅读量 + article_order = Integer() #lxy 文章排序 + + class Index: #lxy 文章索引配置 + name = 'blog' #lxy 索引名称 settings = { - "number_of_shards": 1, - "number_of_replicas": 0 + "number_of_shards": 1, #lxy 分片数 + "number_of_replicas": 0 #lxy 副本数 } + class Meta: #lxy 文档元信息 + doc_type = 'Article' #lxy 文档类型 - class Meta: - doc_type = 'Article' - - -class ArticleDocumentManager(): +class ArticleDocumentManager():#lxy 文章文档管理类 - def __init__(self): + def __init__(self): #lxy 初始化方法 self.create_index() - def create_index(self): + def create_index(self):#lxy 创建文章索引 ArticleDocument.init() - def delete_index(self): + def delete_index(self): #lxy 删除文章索引 from elasticsearch import Elasticsearch es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es.indices.delete(index='blog', ignore=[400, 404]) - def convert_to_doc(self, articles): + def convert_to_doc(self, articles):#lxy 文章对象转ES文档 return [ ArticleDocument( meta={ @@ -201,13 +184,13 @@ class ArticleDocumentManager(): views=article.views, article_order=article.article_order) for article in articles] - def rebuild(self, articles=None): + def rebuild(self, articles=None):#lxy 重建文章索引 ArticleDocument.init() - articles = articles if articles else Article.objects.all() + articles = articles if articles else Article.objects.all()#lxy 获取所有文章 docs = self.convert_to_doc(articles) for doc in docs: - doc.save() + doc.save()#lxy 保存到ES - def update_docs(self, docs): + def update_docs(self, docs):#lxy 更新文章文档 for doc in docs: - doc.save() + doc.save() #lxy 保存更新 diff --git a/src/django-master/blog/forms.py b/src/django-master/blog/forms.py index 715be76..c66101c 100644 --- a/src/django-master/blog/forms.py +++ b/src/django-master/blog/forms.py @@ -3,17 +3,17 @@ import logging from django import forms from haystack.forms import SearchForm -logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__)#lxy 获取当前模块的日志记录器 -class BlogSearchForm(SearchForm): - querydata = forms.CharField(required=True) +class BlogSearchForm(SearchForm): #lxy 博客搜索表单类 + querydata = forms.CharField(required=True)#lxy 搜索关键词字段(必填) - def search(self): - datas = super(BlogSearchForm, self).search() - if not self.is_valid(): - return self.no_query_found() + def search(self):#lxy 搜索方法 + datas = super(BlogSearchForm, self).search()#lxy 调用父类搜索方法 + if not self.is_valid():#lxy 校验表单是否合法 + return self.no_query_found()#lxy 不合法则返回无结果 - if self.cleaned_data['querydata']: - logger.info(self.cleaned_data['querydata']) - return datas + if self.cleaned_data['querydata']:#lxy 若有搜索关键词 + logger.info(self.cleaned_data['querydata'])#lxy 记录搜索关键词日志 + return datas#lxy 返回搜索结果 diff --git a/src/django-master/blog/middleware.py b/src/django-master/blog/middleware.py index 94dd70c..edbbc03 100644 --- a/src/django-master/blog/middleware.py +++ b/src/django-master/blog/middleware.py @@ -9,34 +9,35 @@ from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager logger = logging.getLogger(__name__) -class OnlineMiddleware(object): - def __init__(self, get_response=None): +class OnlineMiddleware(object):#lxy 在线统计中间件 + def __init__(self, get_response=None):#lxy 初始化方法 self.get_response = get_response super().__init__() - def __call__(self, request): + def __call__(self, request):#lxy 中间件核心方法(处理请求) ''' 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: + http_user_agent = request.META.get('HTTP_USER_AGENT', '')#lxy 获取用户代理 + ip, _ = get_client_ip(request)#lxy 获取客户端IP + user_agent = parse(http_user_agent) #lxy 解析用户代理 + + if not response.streaming: #lxy 非流式响应时执行 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 + cast_time = time.time() - start_time#lxy 计算耗时 + if ELASTICSEARCH_ENABLED:#lxy 若启用ES + time_taken = round((cast_time) * 1000, 2)#lxy 耗时转毫秒 + url = request.path#lxy 请求路径 + from django.utils import timezone # 记录耗时到ES ElaspedTimeDocumentManager.create( url=url, time_taken=time_taken, log_datetime=timezone.now(), useragent=user_agent, - ip=ip) + ip=ip) # 替换页面中的加载时间标记 response.content = response.content.replace( b'', str.encode(str(cast_time)[:5])) except Exception as e: - logger.error("Error OnlineMiddleware: %s" % e) + logger.error("Error OnlineMiddleware: %s" % e)#lxy 捕获异常并日志 - return response + return response#lxy 返回响应 diff --git a/src/django-master/blog/models.py b/src/django-master/blog/models.py index 083788b..f226766 100644 --- a/src/django-master/blog/models.py +++ b/src/django-master/blog/models.py @@ -14,363 +14,298 @@ from uuslug import slugify from djangoblog.utils import cache_decorator, cache from djangoblog.utils import get_current_site -logger = logging.getLogger(__name__) - - -class LinkShowType(models.TextChoices): - I = ('i', _('index')) - L = ('l', _('list')) - P = ('p', _('post')) - A = ('a', _('all')) - S = ('s', _('slide')) - - -class BaseModel(models.Model): - id = models.AutoField(primary_key=True) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_modify_time = models.DateTimeField(_('modify time'), default=now) - - def save(self, *args, **kwargs): - is_update_views = isinstance( - self, - Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] +logger = logging.getLogger(__name__) #lxy 初始化日志记录器 + +class LinkShowType(models.TextChoices): #lxy 链接展示类型枚举 + I = ('i', _('index')) #lxy 首页展示 + L = ('l', _('list')) #lxy 列表页展示 + P = ('p', _('post')) #lxy 文章页展示 + A = ('a', _('all')) #lxy 所有页面展示 + S = ('s', _('side')) #lxy 侧边栏展示 + +class BaseModel(models.Model): #lxy 模型基类(公共字段) + id = models.AutoField(primary_key=True) #lxy 主键ID + creation_time = models.DateTimeField(_('creation time'), default=now) #lxy 创建时间 + last_modify_time = models.DateTimeField(_('modify time'), default=now) #lxy 修改时间 + + def save(self, *args, **kwargs): #lxy 重写保存方法 + # 判断是否是更新文章阅读量 + is_update_views = isinstance(self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] if is_update_views: - Article.objects.filter(pk=self.pk).update(views=self.views) + Article.objects.filter(pk=self.pk).update(views=self.views) #lxy 单独更新阅读量 else: - if 'slug' in self.__dict__: - slug = getattr( - self, 'title') if 'title' in self.__dict__ else getattr( - self, 'name') + if 'slug' in self.__dict__: #lxy 若有slug字段,自动生成 + slug = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name') setattr(self, 'slug', slugify(slug)) - super().save(*args, **kwargs) + super().save(*args, **kwargs) #lxy 调用父类保存 - def get_full_url(self): + def get_full_url(self): #lxy 获取完整URL site = get_current_site().domain - url = "https://{site}{path}".format(site=site, - path=self.get_absolute_url()) + url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url class Meta: - abstract = True + abstract = True #lxy 抽象基类(不生成表) @abstractmethod - def get_absolute_url(self): + def get_absolute_url(self): #lxy 抽象方法:获取对象URL pass - -class Article(BaseModel): - """文章""" +class Article(BaseModel): #lxy 文章模型 + # 文章状态枚举 STATUS_CHOICES = ( - ('d', _('Draft')), - ('p', _('Published')), + ('d', _('Draft')), #lxy 草稿 + ('p', _('Published')), #lxy 已发布 ) + # 评论状态枚举 COMMENT_STATUS = ( - ('o', _('Open')), - ('c', _('Close')), + ('o', _('open')), #lxy 开放评论 + ('c', _('close')), #lxy 关闭评论 ) + # 文章类型枚举 TYPE = ( - ('a', _('Article')), - ('p', _('Page')), + ('a', _('Article')), #lxy 文章 + ('p', _('Page')), #lxy 页面 ) - title = models.CharField(_('title'), max_length=200, unique=True) - body = MDTextField(_('body')) - pub_time = models.DateTimeField( - _('publish time'), blank=False, null=False, default=now) - status = models.CharField( - _('status'), - max_length=1, - choices=STATUS_CHOICES, - default='p') - comment_status = models.CharField( - _('comment status'), - max_length=1, - choices=COMMENT_STATUS, - default='o') - type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') - views = models.PositiveIntegerField(_('views'), default=0) - author = models.ForeignKey( - settings.AUTH_USER_MODEL, - verbose_name=_('author'), - blank=False, - null=False, - on_delete=models.CASCADE) - article_order = models.IntegerField( - _('order'), blank=False, null=False, default=0) - show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) - category = models.ForeignKey( - 'Category', - verbose_name=_('category'), - on_delete=models.CASCADE, - blank=False, - null=False) - tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) + title = models.CharField(_('title'), max_length=200, unique=True) #lxy 标题 + body = MDTextField(_('body')) #lxy 正文(Markdown) + pub_time = models.DateTimeField(_('publish time'), blank=False, null=False, default=now) #lxy 发布时间 + status = models.CharField(_('status'), max_length=1, choices=STATUS_CHOICES, default='p') #lxy 文章状态 + comment_status = models.CharField(_('comment status'), max_length=1, choices=COMMENT_STATUS, default='o') #lxy 评论状态 + type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') #lxy 文章类型 + views = models.PositiveIntegerField(_('views'), default=0) #lxy 阅读量 + author = models.ForeignKey( #lxy 关联作者 + settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=False, null=False, on_delete=models.CASCADE + ) + article_order = models.IntegerField(_('order'), blank=False, null=False, default=0) #lxy 排序序号 + show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) #lxy 是否显示目录 + category = models.ForeignKey( #lxy 关联分类 + 'Category', verbose_name=_('category'), on_delete=models.CASCADE, blank=False, null=False + ) + tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) #lxy 关联标签(多对多) - def body_to_string(self): + def body_to_string(self): #lxy 正文转字符串 return self.body - def __str__(self): + def __str__(self): #lxy 实例字符串表示 return self.title class Meta: - ordering = ['-article_order', '-pub_time'] + ordering = ['-article_order', '-pub_time'] #lxy 默认排序(倒序) verbose_name = _('article') verbose_name_plural = verbose_name get_latest_by = 'id' - def get_absolute_url(self): - return reverse('blog:detailbyid', kwargs={ + def get_absolute_url(self): #lxy 文章详情页URL + return reverse('blog:detail', kwargs={ 'article_id': self.id, 'year': self.creation_time.year, 'month': self.creation_time.month, 'day': self.creation_time.day }) - @cache_decorator(60 * 60 * 10) - def get_category_tree(self): + @cache_decorator(60 * 60 * 10) #lxy 缓存10小时 + def get_category_tree(self): #lxy 获取分类层级 tree = self.category.get_category_tree() names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) - return names - def save(self, *args, **kwargs): + def save(self, *args, **kwargs): #lxy 重写保存 super().save(*args, **kwargs) - def viewed(self): + def viewed(self): #lxy 阅读量+1 self.views += 1 - self.save(update_fields=['views']) + self.save(update_fields=['views']) #lxy 仅更新阅读量 - def comment_list(self): + def comment_list(self): #lxy 获取文章评论 cache_key = 'article_comments_{id}'.format(id=self.id) value = cache.get(cache_key) if value: logger.info('get article comments:{id}'.format(id=self.id)) return value else: - comments = self.comment_set.filter(is_enable=True).order_by('-id') - cache.set(cache_key, comments, 60 * 100) + comments = self.comment_set.filter(is_enable=True).order_by('-id') #lxy 过滤启用的评论 + cache.set(cache_key, comments, 60 * 100) #lxy 缓存评论 logger.info('set article comments:{id}'.format(id=self.id)) return comments - def get_admin_url(self): + def get_admin_url(self): #lxy 后台管理URL info = (self._meta.app_label, self._meta.model_name) return reverse('admin:%s_%s_change' % info, args=(self.pk,)) - @cache_decorator(expiration=60 * 100) - def next_article(self): - # 下一篇 - return Article.objects.filter( - id__gt=self.id, status='p').order_by('id').first() + @cache_decorator(expiration=60 * 100) #lxy 缓存 + def next_article(self): #lxy 获取下一篇文章 + return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first() - @cache_decorator(expiration=60 * 100) - def prev_article(self): - # 前一篇 + @cache_decorator(expiration=60 * 100) #lxy 缓存 + def prev_article(self): #lxy 获取上一篇文章 return Article.objects.filter(id__lt=self.id, status='p').first() - def get_first_image_url(self): - """ - Get the first image url from article.body. - :return: - """ - match = re.search(r'!\[.*?\]\((.+?)\)', self.body) + def get_first_image_url(self): #lxy 获取正文第一张图URL + match = re.search(pattern=r'!\[.*?]\((.*?)\)', self.body) if match: return match.group(1) return "" - -class Category(BaseModel): - """文章分类""" - name = models.CharField(_('category name'), max_length=30, unique=True) - parent_category = models.ForeignKey( - 'self', - verbose_name=_('parent category'), - blank=True, - null=True, - on_delete=models.CASCADE) - slug = models.SlugField(default='no-slug', max_length=60, blank=True) - index = models.IntegerField(default=0, verbose_name=_('index')) - - class Meta: - ordering = ['-index'] - verbose_name = _('category') - verbose_name_plural = verbose_name - - def get_absolute_url(self): - return reverse( - 'blog:category_detail', kwargs={ - 'category_name': self.slug}) - - def __str__(self): + class Category(BaseModel): # lxy 分类模型 + name = models.CharField(_('category name'), max_length=30, unique=True) # lxy 分类名称 + parent_category = models.ForeignKey( # lxy 父分类(自关联) + 'self', verbose_name=_('parent category'), blank=True, null=True, on_delete=models.CASCADE + ) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) # lxy 别名 + index = models.IntegerField(default=0, verbose_name=_('index')) # lxy 排序索引 + + class Meta: + ordering = ['-index'] # lxy 按索引倒序 + verbose_name = _('category') + verbose_name_plural = verbose_name + + def get_absolute_url(self): # lxy 分类详情页URL + return reverse('blog:category_detail', kwargs={'category_name': self.slug}) + + def __str__(self): # lxy 实例字符串表示 + return self.name + + def get_category_tree(self): # lxy 获取分类层级链 + categories = [] + + def parse(category): + categories.append(category) + if category.parent_category: + parse(category.parent_category) + + parse(self) + return categories + + @cache_decorator(60 * 60 * 10) # lxy 缓存 + def get_sub_categories(self): # lxy 获取所有子分类 + categories = [] + all_categories = Category.objects.all() + + def parse(category): + if category not in categories: + categories.append(category) + children = all_categories.filter(parent_category=category) + for child in children: + if category not in categories: + categories.append(child) + parse(child) + + parse(self) + return categories + +class Tag(BaseModel): #lxy 标签模型 + name = models.CharField(_('tag name'), max_length=30, unique=True) #lxy 标签名称 + slug = models.SlugField(default='no-slug', max_length=60, blank=True) #lxy 别名 + + def __str__(self): #lxy 实例字符串表示 return self.name - @cache_decorator(60 * 60 * 10) - def get_category_tree(self): - """ - 递归获得分类目录的父级 - :return: - """ - categorys = [] - - def parse(category): - categorys.append(category) - if category.parent_category: - parse(category.parent_category) - - parse(self) - return categorys - - @cache_decorator(60 * 60 * 10) - def get_sub_categorys(self): - """ - 获得当前分类目录所有子集 - :return: - """ - categorys = [] - all_categorys = Category.objects.all() - - def parse(category): - if category not in categorys: - categorys.append(category) - childs = all_categorys.filter(parent_category=category) - for child in childs: - if category not in categorys: - categorys.append(child) - parse(child) - - parse(self) - return categorys - - -class Tag(BaseModel): - """文章标签""" - name = models.CharField(_('tag name'), max_length=30, unique=True) - slug = models.SlugField(default='no-slug', max_length=60, blank=True) - - def __str__(self): - return self.name - - def get_absolute_url(self): + def get_absolute_url(self): #lxy 标签详情页URL return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) - @cache_decorator(60 * 60 * 10) - def get_article_count(self): + @cache_decorator(60 * 60 * 10) #lxy 缓存 + def get_article_count(self): #lxy 获取标签关联文章数 return Article.objects.filter(tags__name=self.name).distinct().count() class Meta: - ordering = ['name'] + ordering = ['name'] #lxy 按名称排序 verbose_name = _('tag') verbose_name_plural = verbose_name - -class Links(models.Model): - """友情链接""" - - name = models.CharField(_('link name'), max_length=30, unique=True) - link = models.URLField(_('link')) - sequence = models.IntegerField(_('order'), unique=True) - is_enable = models.BooleanField( - _('is show'), default=True, blank=False, null=False) - show_type = models.CharField( - _('show type'), - max_length=1, - choices=LinkShowType.choices, - default=LinkShowType.I) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_mod_time = models.DateTimeField(_('modify time'), default=now) +class Links(models.Model): #lxy 友链模型 + name = models.CharField(_('link name'), max_length=30, unique=True) #lxy 友链名称 + link = models.URLField(_('link')) #lxy 友链地址 + sequence = models.IntegerField(_('order'), unique=True) #lxy 排序序号 + is_enable = models.BooleanField(_('is show'), default=True, blank=False, null=False) #lxy 是否启用 + show_type = models.CharField( #lxy 展示类型 + _('show type'), max_length=1, choices=LinkShowType.choices, default=LinkShowType.I + ) + creation_time = models.DateTimeField(_('creation time'), default=now) #lxy 创建时间 + last_mod_time = models.DateTimeField(_('modify time'), default=now) #lxy 修改时间 class Meta: - ordering = ['sequence'] + ordering = ['sequence'] #lxy 按序号排序 verbose_name = _('link') verbose_name_plural = verbose_name - def __str__(self): + def __str__(self): #lxy 实例字符串表示 return self.name - -class SideBar(models.Model): - """侧边栏,可以展示一些html内容""" - name = models.CharField(_('title'), max_length=100) - content = models.TextField(_('content')) - sequence = models.IntegerField(_('order'), unique=True) - is_enable = models.BooleanField(_('is enable'), default=True) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_mod_time = models.DateTimeField(_('modify time'), default=now) +class SideBar(models.Model): #lxy 侧边栏模型 + name = models.CharField(_('title'), max_length=100) #lxy 侧边栏标题 + content = models.TextField(_('content')) #lxy 侧边栏内容(HTML) + sequence = models.IntegerField(_('order'), unique=True) #lxy 排序序号 + is_enable = models.BooleanField(_('is enable'), default=True) #lxy 是否启用 + creation_time = models.DateTimeField(_('creation time'), default=now) #lxy 创建时间 + last_mod_time = models.DateTimeField(_('modify time'), default=now) #lxy 修改时间 class Meta: - ordering = ['sequence'] + ordering = ['sequence'] #lxy 按序号排序 verbose_name = _('sidebar') verbose_name_plural = verbose_name - def __str__(self): + def __str__(self): #lxy 实例字符串表示 return self.name - -class BlogSettings(models.Model): - """blog的配置""" - site_name = models.CharField( - _('site name'), - max_length=200, - null=False, - blank=False, - default='') - site_description = models.TextField( - _('site description'), - max_length=1000, - null=False, - blank=False, - default='') - site_seo_description = models.TextField( - _('site seo description'), max_length=1000, null=False, blank=False, default='') - site_keywords = models.TextField( - _('site keywords'), - max_length=1000, - null=False, - blank=False, - default='') - article_sub_length = models.IntegerField(_('article sub length'), default=300) - sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) - sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) - article_comment_count = models.IntegerField(_('article comment count'), default=5) - show_google_adsense = models.BooleanField(_('show adsense'), default=False) - google_adsense_codes = models.TextField( - _('adsense code'), max_length=2000, null=True, blank=True, default='') - open_site_comment = models.BooleanField(_('open site comment'), default=True) - global_header = models.TextField("公共头部", null=True, blank=True, default='') - global_footer = models.TextField("公共尾部", null=True, blank=True, default='') - beian_code = models.CharField( - '备案号', - max_length=2000, - null=True, - blank=True, - default='') - analytics_code = models.TextField( + class BlogSettings(models.Model): # lxy 博客配置模型 + site_name = models.CharField(_('site name'), blank=False, default='') # lxy 站点名称 + site_description = models.TextField( # lxy 站点描述 + _('site description'), max_length=1000, null=False, blank=False, default='' + ) + site_seo_description = models.TextField( # lxy 站点SEO描述 + _('site seo description'), max_length=1000, null=False, blank=False, default='' + ) + site_keywords = models.TextField( # lxy 站点关键词 + _('site keywords'), max_length=1000, null=False, blank=False, default='' + ) + article_sub_length = models.IntegerField(_('article sub length'), default=300) # lxy 文章摘要长度 + sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # lxy 侧边栏文章数 + sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # lxy 侧边栏评论数 + show_google_adsense = models.BooleanField(_('show adsense'), default=False) # lxy 是否显示谷歌广告 + google_adsense_codes = models.TextField( # lxy 谷歌广告代码 + _('adsense code'), max_length=2000, null=True, blank=True, default='' + ) + open_site_comment = models.BooleanField(_('open site comment'), default=True) # lxy 是否开放站点评论 + global_header = models.TextField("公共头部", null=True, blank=True, default='') # lxy 公共头部HTML + global_footer = models.TextField("公共底部", null=True, blank=True, default='') # lxy 公共底部HTML + beian_code = models.CharField( # lxy 备案号 + '备案号', max_length=2000, null=True, blank=True, default='' + ) + + analytics_code = models.TextField( # lxy 网站统计代码 "网站统计代码", max_length=1000, null=False, blank=False, - default='') - show_gongan_code = models.BooleanField( - '是否显示公安备案号', default=False, null=False) - gongan_beiancode = models.TextField( - '公安备案号', + default="" + ) + show_gongan_code = models.BooleanField( # lxy 是否显示公安备案号 + "是否显示公安备案号", default=False, null=False + ) + gongan_beiancode = models.TextField( # lxy 公安备案号 + "公安备案号", max_length=2000, null=True, blank=True, - default='') - comment_need_review = models.BooleanField( - '评论是否需要审核', default=False, null=False) + default="" + ) + comment_need_review = models.BooleanField( # lxy 评论是否需要审核 + "评论是否需要审核", default=False, null=False + ) - class Meta: - verbose_name = _('Website configuration') - verbose_name_plural = verbose_name + class Meta: # lxy 模型元信息 + verbose_name = _('Website configuration') # lxy 单数名称 + verbose_name_plural = verbose_name # lxy 复数名称 - def __str__(self): + def __str__(self): # lxy 实例字符串表示 return self.site_name - def clean(self): + def clean(self): # lxy 数据校验(确保仅存在一个配置) if BlogSettings.objects.exclude(id=self.id).count(): - raise ValidationError(_('There can only be one configuration')) + raise ValidationError(_('There can only be one configuration')) # lxy 抛出唯一配置异常 - def save(self, *args, **kwargs): - super().save(*args, **kwargs) + def save(self, *args, **kwargs): # lxy 重写保存方法 + super().save(*args, **kwargs) # lxy 调用父类保存 from djangoblog.utils import cache - cache.clear() + cache.clear() # lxy 保存后清空缓存 \ No newline at end of file diff --git a/src/django-master/blog/search_indexes.py b/src/django-master/blog/search_indexes.py index 7f1dfac..f900e71 100644 --- a/src/django-master/blog/search_indexes.py +++ b/src/django-master/blog/search_indexes.py @@ -3,11 +3,13 @@ from haystack import indexes from blog.models import Article -class ArticleIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) +class ArticleIndex(indexes.SearchIndex, indexes.Indexable):#lxy 文章搜索索引类 + text = indexes.CharField(document=True, use_template=True)#lxy 搜索字段(关联模板) - def get_model(self): - return Article + def get_model(self):#lxy 指定关联的模型 + return Article#lxy 关联Article模型 - def index_queryset(self, using=None): - return self.get_model().objects.filter(status='p') + + def index_queryset(self, using=None):#lxy 指定要索引的数据集 + # 仅索引状态为“已发布(p)”的文章 + return self.get_model().objects.filter(status='p')#lxy 过滤已发布文章 diff --git a/src/django-master/blog/tests.py b/src/django-master/blog/tests.py index ee13505..c9d8b61 100644 --- a/src/django-master/blog/tests.py +++ b/src/django-master/blog/tests.py @@ -19,24 +19,27 @@ from oauth.models import OAuthUser, OAuthConfig # Create your tests here. -class ArticleTest(TestCase): - def setUp(self): - self.client = Client() - self.factory = RequestFactory() - - def test_validate_article(self): - site = get_current_site().domain - user = BlogUser.objects.get_or_create( - email="liangliangyy@gmail.com", - username="liangliangyy")[0] +class ArticleTest(TestCase): # lxy 文章相关测试类 + def setUp(self): # lxy 测试初始化 + self.client = Client() # lxy 测试客户端 + self.factory = RequestFactory() # lxy 请求工厂 + + def test_validate_article(self): # lxy 文章功能验证 + site = get_current_site().domain # lxy 获取站点域名 + # 创建测试用户 + user = BlogUser.objects.get_or_create(email="liangliangyy@gmail.com", username="liangliangyy")[0] user.set_password("liangliangyy") user.is_staff = True user.is_superuser = True user.save() + # 测试用户页面访问 response = self.client.get(user.get_absolute_url()) - self.assertEqual(response.status_code, 200) - response = self.client.get('/admin/servermanager/emailsendlog/') - response = self.client.get('admin/admin/logentry/') + self.assertEqual(response.status_code, 200) # lxy 断言状态码200 + # 访问后台页面(无权限,仅请求) + self.client.get('/admin/servermanager/emailsendlog/') + self.client.get('admin/admin/logentry/') + + # 创建测试侧边栏 s = SideBar() s.sequence = 1 s.name = 'test' @@ -44,6 +47,8 @@ class ArticleTest(TestCase): s.is_enable = True s.save() + # 创建测试分类 + category = Category() category.name = "category" category.creation_time = timezone.now() @@ -182,11 +187,11 @@ class ArticleTest(TestCase): save_user_avatar( 'https://www.python.org/static/img/python-logo.png') - def test_errorpage(self): - rsp = self.client.get('/eee') - self.assertEqual(rsp.status_code, 404) + def test_errorpage(self): # lxy 错误页面测试 + rsp = self.client.get('/eee') # lxy 访问不存在的路径 + self.assertEqual(rsp.status_code, 404) # lxy 断言404状态码 - def test_commands(self): + def test_commands(self):#lxy 命令行指令测试 user = BlogUser.objects.get_or_create( email="liangliangyy@gmail.com", username="liangliangyy")[0] @@ -223,10 +228,11 @@ class ArticleTest(TestCase): u.save() from blog.documents import ELASTICSEARCH_ENABLED - if ELASTICSEARCH_ENABLED: - call_command("build_index") - call_command("ping_baidu", "all") - call_command("create_testdata") - call_command("clear_cache") - call_command("sync_user_avatar") - call_command("build_search_words") + + if ELASTICSEARCH_ENABLED: + call_command("build_index") # lxy 构建ES索引 + call_command("ping_baidu", "all") # lxy ping百度 + call_command("create_testdata") # lxy 创建测试数据 + call_command("clear_cache") # lxy 清理缓存 + call_command("sync_user_avatar") # lxy 同步用户头像 + call_command("build_search_words") # lxy 构建搜索词 \ No newline at end of file diff --git a/src/django-master/blog/urls.py b/src/django-master/blog/urls.py index adf2703..cf45ece 100644 --- a/src/django-master/blog/urls.py +++ b/src/django-master/blog/urls.py @@ -3,60 +3,64 @@ from django.views.decorators.cache import cache_page from . import views -app_name = "blog" +app_name = "blog" #lxy 应用命名空间 urlpatterns = [ path( r'', views.IndexView.as_view(), - name='index'), + name='index'),#lxy 路由名称:首页 path( r'page//', views.IndexView.as_view(), - name='index_page'), + name='index_page'),#lxy 路由名称:首页分页 + # 文章详情路由(按日期+ID) path( r'article////.html', views.ArticleDetailView.as_view(), - name='detailbyid'), + name='detailbyid'),#lxy 路由名称:文章详情 + # 分类详情路由 path( r'category/.html', views.CategoryDetailView.as_view(), - name='category_detail'), + name='category_detail'), #lxy 路由名称:分类详情 + # 分类详情分页路由 path( r'category//.html', views.CategoryDetailView.as_view(), - name='category_detail_page'), + name='category_detail_page'),#lxy 路由名称:分类详情分页 + # 作者详情路由 path( r'author/.html', views.AuthorDetailView.as_view(), - name='author_detail'), + name='author_detail'),#lxy 路由名称:作者详情 path( r'author//.html', views.AuthorDetailView.as_view(), - name='author_detail_page'), + name='author_detail_page'),#lxy 路由名称:作者详情分页 path( r'tag/.html', views.TagDetailView.as_view(), - name='tag_detail'), + name='tag_detail'),#lxy 路由名称:标签详情 path( r'tag//.html', views.TagDetailView.as_view(), - name='tag_detail_page'), + name='tag_detail_page'),#lxy 路由名称:标签详情分页 path( 'archives.html', cache_page( 60 * 60)( views.ArchivesView.as_view()), - name='archives'), + name='archives'),#lxy 路由名称:归档页 path( 'links.html', views.LinkListView.as_view(), - name='links'), + name='links'),#lxy 路由名称:友链页 path( r'upload', views.fileupload, - name='upload'), + name='upload'),#lxy 路由名称:文件上传 path( r'clean', views.clean_cache_view, - name='clean'), + name='clean'),#lxy 路由名称:缓存清理 ] diff --git a/src/django-master/blog/views.py b/src/django-master/blog/views.py index d5dc7ec..f5c9961 100644 --- a/src/django-master/blog/views.py +++ b/src/django-master/blog/views.py @@ -24,42 +24,37 @@ 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): +class ArticleListView(ListView): #lxy 文章列表视图基类 + template_name = 'blog/article_index.html' #lxy 渲染模板 + context_object_name = 'article_list' #lxy 模板中数据变量名 + page_type = '' #lxy 页面类型标识 + paginate_by = settings.PAGINATE_BY #lxy 每页数量 + page_kwarg = 'page' #lxy 分页参数名 + link_type = LinkShowType.L #lxy 友链展示类型 + + def get_view_cache_key(self): #lxy 获取视图缓存键 return self.request.get['pages'] @property - def page_number(self): + def page_number(self):#lxy 获取当前页码 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): + def get_queryset_cache_key(self):#lxy 子类需重写:获取查询集缓存键 """ 子类重写.获得queryset的缓存key """ raise NotImplementedError() - def get_queryset_data(self): + def get_queryset_data(self):#lxy 子类需重写:获取查询集数据 """ 子类重写.获取queryset的数据 """ raise NotImplementedError() - def get_queryset_from_cache(self, cache_key): + def get_queryset_from_cache(self, cache_key):#lxy 从缓存取查询集 ''' 缓存页面数据 :param cache_key: 缓存key @@ -70,12 +65,12 @@ class ArticleListView(ListView): 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) + article_list = self.get_queryset_data()#lxy 从数据库取数据 + cache.set(cache_key, article_list)#lxy 写入缓存 logger.info('set view cache.key:{key}'.format(key=cache_key)) return article_list - def get_queryset(self): + def get_queryset(self):#lxy 获取查询集(优先缓存) ''' 重写默认,从缓存获取数据 :return: @@ -84,19 +79,19 @@ class ArticleListView(ListView): value = self.get_queryset_from_cache(key) return value - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs):#lxy 补充上下文数据 kwargs['linktype'] = self.link_type return super(ArticleListView, self).get_context_data(**kwargs) -class IndexView(ArticleListView): +class IndexView(ArticleListView):#lxy 首页视图 ''' 首页 ''' # 友情链接类型 - link_type = LinkShowType.I + link_type = LinkShowType.I#lxy 首页友链展示类型 - def get_queryset_data(self): + def get_queryset_data(self): #lxy 获取首页文章(已发布) article_list = Article.objects.filter(type='a', status='p') return article_list @@ -147,8 +142,8 @@ class ArticleDetailView(DetailView): 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 + kwargs['next_article'] = self.object.next_article # lxy 下一篇文章 + kwargs['prev_article'] = self.object.prev_article # lxy 上一篇文章 context = super(ArticleDetailView, self).get_context_data(**kwargs) article = self.object @@ -161,13 +156,13 @@ class ArticleDetailView(DetailView): return context -class CategoryDetailView(ArticleListView): +class CategoryDetailView(ArticleListView):#lxy 分类详情视图 ''' 分类目录列表 ''' - page_type = "分类目录归档" + page_type = "分类目录归档"#lxy 页面类型 - def get_queryset_data(self): + def get_queryset_data(self):#lxy 获取分类下文章 slug = self.kwargs['category_name'] category = get_object_or_404(Category, slug=slug) @@ -241,7 +236,7 @@ class TagDetailView(ArticleListView): tags__name=tag_name, type='a', status='p') return article_list - def get_queryset_cache_key(self): + def get_queryset_cache_key(self):#lxy 标签缓存键(含标签名、页码) slug = self.kwargs['tag_name'] tag = get_object_or_404(Tag, slug=slug) tag_name = tag.name @@ -258,7 +253,7 @@ class TagDetailView(ArticleListView): return super(TagDetailView, self).get_context_data(**kwargs) -class ArchivesView(ArticleListView): +class ArchivesView(ArticleListView):#lxy 文章归档视图 ''' 文章归档页面 ''' @@ -267,24 +262,24 @@ class ArchivesView(ArticleListView): page_kwarg = None template_name = 'blog/article_archives.html' - def get_queryset_data(self): + def get_queryset_data(self):#lxy 获取所有已发布文章 return Article.objects.filter(status='p').all() - def get_queryset_cache_key(self): + def get_queryset_cache_key(self):#lxy 归档缓存键 cache_key = 'archives' return cache_key -class LinkListView(ListView): +class LinkListView(ListView):#lxy 友链列表视图 model = Links - template_name = 'blog/links_list.html' + template_name = 'blog/links_list.html' #lxy 渲染模板 - def get_queryset(self): + def get_queryset(self):#lxy 获取启用的友链 return Links.objects.filter(is_enable=True) -class EsSearchView(SearchView): - def get_context(self): +class EsSearchView(SearchView):#lxy ES搜索视图 + def get_context(self):#lxy 补充搜索上下文 paginator, page = self.build_page() context = { "query": self.query,