From 9d12f4621f053406d7c582b1926146dfe145542c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=85=A7?= <1096877868@qq.com> Date: Sat, 18 Oct 2025 20:38:12 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E5=BC=A0=E6=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/.idea/.gitignore | 5 + .../inspectionProfiles/profiles_settings.xml | 6 + src/.idea/misc.xml | 7 + src/.idea/modules.xml | 8 + src/.idea/src.iml | 8 + src/.idea/vcs.xml | 6 + src/__init__.py | 0 src/admin.py | 65 ++++ src/apps.py | 7 + src/article_info.html | 92 +++--- src/article_meta_info.html | 95 +++--- src/article_pagination.html | 29 +- src/article_tag_list.html | 45 +-- src/breadcrumb.html | 32 +- src/forms.py | 15 + src/models.py | 38 +++ src/sidebar.html | 199 +++++------- src/tests.py | 289 ++++++++++++++++++ src/urls.py | 17 ++ src/utils.py | 60 ++++ src/views.py | 100 ++++++ 21 files changed, 855 insertions(+), 268 deletions(-) create mode 100644 src/.idea/.gitignore create mode 100644 src/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 src/.idea/misc.xml create mode 100644 src/.idea/modules.xml create mode 100644 src/.idea/src.iml create mode 100644 src/.idea/vcs.xml create mode 100644 src/__init__.py create mode 100644 src/admin.py create mode 100644 src/apps.py create mode 100644 src/forms.py create mode 100644 src/models.py create mode 100644 src/tests.py create mode 100644 src/urls.py create mode 100644 src/utils.py create mode 100644 src/views.py diff --git a/src/.idea/.gitignore b/src/.idea/.gitignore new file mode 100644 index 0000000..10b731c --- /dev/null +++ b/src/.idea/.gitignore @@ -0,0 +1,5 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/src/.idea/inspectionProfiles/profiles_settings.xml b/src/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/src/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/.idea/misc.xml b/src/.idea/misc.xml new file mode 100644 index 0000000..db8786c --- /dev/null +++ b/src/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/.idea/modules.xml b/src/.idea/modules.xml new file mode 100644 index 0000000..f669a0e --- /dev/null +++ b/src/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/.idea/src.iml b/src/.idea/src.iml new file mode 100644 index 0000000..f571432 --- /dev/null +++ b/src/.idea/src.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/.idea/vcs.xml b/src/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/src/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/admin.py b/src/admin.py new file mode 100644 index 0000000..1ae5ac1 --- /dev/null +++ b/src/admin.py @@ -0,0 +1,65 @@ +from django.contrib import admin +from django.urls import reverse # 用于生成管理员界面的URL反向解析 +from django.utils.html import format_html # 用于安全地生成HTML内容 +from django.utils.translation import gettext_lazy as _ # 用于国际化翻译 + + +# 自定义批量操作:禁用评论状态 +def disable_commentstatus(modeladmin, request, queryset): + queryset.update(is_enable=False) # 将选中的评论记录is_enable字段设为False + + +# 自定义批量操作:启用评论状态 +def enable_commentstatus(modeladmin, request, queryset): + queryset.update(is_enable=True) # 将选中的评论记录is_enable字段设为True + + +# 为批量操作设置显示名称(支持国际化) +disable_commentstatus.short_description = _('Disable comments') +enable_commentstatus.short_description = _('Enable comments') + + +class CommentAdmin(admin.ModelAdmin): + list_per_page = 20 # 每页显示20条记录 + # 列表页显示的字段 + list_display = ( + 'id', + 'body', # 评论内容 + 'link_to_userinfo', # 自定义字段:链接到用户信息 + 'link_to_article', # 自定义字段:链接到文章 + 'is_enable', # 是否启用 + 'creation_time' # 创建时间 + ) + # 列表页中可点击跳转编辑页的字段 + list_display_links = ('id', 'body', 'is_enable') + # 可筛选的字段(右侧过滤器) + list_filter = ('is_enable',) + # 编辑页排除的字段(不允许编辑,如自动生成的时间) + exclude = ('creation_time', 'last_modify_time') + # 注册自定义批量操作 + actions = [disable_commentstatus, enable_commentstatus] + + # 自定义列表字段:生成用户信息的编辑链接 + def link_to_userinfo(self, obj): + # 获取用户模型的app标签和模型名称 + info = (obj.author._meta.app_label, obj.author._meta.model_name) + # 生成用户编辑页的URL + link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + # 返回带链接的HTML(优先显示昵称,无昵称则显示邮箱) + return format_html( + u'%s' % + (link, obj.author.nickname if obj.author.nickname else obj.author.email)) + + # 自定义列表字段:生成文章的编辑链接 + def link_to_article(self, obj): + # 获取文章模型的app标签和模型名称 + info = (obj.article._meta.app_label, obj.article._meta.model_name) + # 生成文章编辑页的URL + link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) + # 返回带链接的HTML(显示文章标题) + return format_html( + u'%s' % (link, obj.article.title)) + + # 自定义字段的显示名称(支持国际化) + link_to_userinfo.short_description = _('User') + link_to_article.short_description = _('Article') \ No newline at end of file diff --git a/src/apps.py b/src/apps.py new file mode 100644 index 0000000..c8106f9 --- /dev/null +++ b/src/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig # 导入Django的应用配置基类 + + +class CommentsConfig(AppConfig): + # 配置应用的名称,对应项目中该应用的目录名(此处为'comments') + # Django通过这个名称识别和管理该应用 + name = 'comments' \ No newline at end of file diff --git a/src/article_info.html b/src/article_info.html index c6f793b..f442dd1 100644 --- a/src/article_info.html +++ b/src/article_info.html @@ -1,79 +1,83 @@ -{% load blog_tags %} // 加载自定义博客相关的自定义模板标签库 -{% load cache %} // 加载缓存功能相关的模板标签库 -{% load i18n %} // 加载国际化相关的模板标签库 +{% load blog_tags %} +{% load cache %} +{% load i18n %} +
// 加载国际化相关的模板标签库 + class="post-{{ article.pk }} post type-post status-publish format-standard hentry"> +
-
// 文章头部区域,包含标题等信息 - - -

// 文章标题元素 - {% if isindex %} // 判断当前是否为索引页(文章列表页) - {% if article.article_order > 0 %} // 若为索引页,判断文章是否有置顶顺序(大于0表示置顶) +

+ {% if isindex %} + {% if article.article_order > 0 %} + 【{% trans 'pin to top' %}】{{ article.title }}// 显示带"置顶"标识的文章标题 + rel="bookmark">【{% trans 'pin to top' %}】{{ article.title }} {% else %} + {{ article.title }}// 显示普通文章标题 + rel="bookmark">{{ article.title }} {% endif %} - {% else %} + {{ article.title }} {% endif %}

-
-
- {% if isindex %} - {{ article.body|custom_markdown|escape|truncatechars_content }}// 显示经过markdown处理、转义并截断的文章内容 - +
+ {% if isindex %} + + {{ article.body|custom_markdown|escape|truncatechars_content }} +

Read more

- {% else %} - - {% if article.show_toc %}// 检查文章是否设置显示目录 - {% get_markdown_toc article.body as toc %}// 调用自定义标签获取文章内容的markdown目录,并赋值给toc变量 - {% trans 'toc' %}: - {{ toc|safe }}// 安全地显示目录内容(允许HTML渲染) - - -
+ {% else %} + {% if article.show_toc %} + + {% get_markdown_toc article.body as toc %} + {% trans 'toc' %}: + {{ toc|safe }} +
{% endif %}
- - {{ article.body|custom_markdown|escape }}// 显示经过markdown处理和转义的完整文章内容 - + + {{ article.body|custom_markdown|escape }}
{% endif %} -
- {% load_article_metas article user %}// 调用自定义标签加载文章的元数据(如作者、发布时间等),传入文章对象和用户对象 + + {% load_article_metas article user %}
\ No newline at end of file diff --git a/src/article_meta_info.html b/src/article_meta_info.html index 0fc1df1..fb6d147 100644 --- a/src/article_meta_info.html +++ b/src/article_meta_info.html @@ -2,57 +2,62 @@ {% load blog_tags %} - \ No newline at end of file diff --git a/src/article_pagination.html b/src/article_pagination.html index ab821b8..d406df7 100644 --- a/src/article_pagination.html +++ b/src/article_pagination.html @@ -1,21 +1,28 @@ {% load i18n %} - \ No newline at end of file diff --git a/src/article_tag_list.html b/src/article_tag_list.html index 6f0ac5e..98b0a7a 100644 --- a/src/article_tag_list.html +++ b/src/article_tag_list.html @@ -1,33 +1,20 @@ -{% load i18n %} - - -{% if article_tags_list %} - -
- -
- {% trans 'tags' %} +{% load i18n %} +{% if article_tags_list %} +
+
+ {% trans 'tags' %}
+
- -
- - {% for url, count, tag, color in article_tags_list %} - - + {% for url,count,tag,color in article_tags_list %} + + {{ tag.name }} - {{ count }} + {{ count }} - {% endfor %} -
-
-{% endif %} \ No newline at end of file + {% endfor %} + +
+
+{% endif %} \ No newline at end of file diff --git a/src/breadcrumb.html b/src/breadcrumb.html index cf99ea5..e494701 100644 --- a/src/breadcrumb.html +++ b/src/breadcrumb.html @@ -1,33 +1,25 @@ + + diff --git a/src/forms.py b/src/forms.py new file mode 100644 index 0000000..440cc02 --- /dev/null +++ b/src/forms.py @@ -0,0 +1,15 @@ +from django import forms # 导入Django表单基础模块 +from django.forms import ModelForm # 导入模型表单类,用于快速生成与模型对应的表单 + +from .models import Comment # 从当前应用的models中导入Comment模型 + + +class CommentForm(ModelForm): # 定义评论表单类,继承自ModelForm + # 添加父评论ID字段,用于实现评论回复功能 + # 使用HiddenInput控件隐藏显示,且非必填(顶级评论无需父ID) + parent_comment_id = forms.IntegerField( + widget=forms.HiddenInput, required=False) + + class Meta: # Meta类用于配置表单与模型的关联信息 + model = Comment # 指定表单对应的模型为Comment + fields = ['body'] # 表单需要包含的模型字段,这里只包含评论内容body diff --git a/src/models.py b/src/models.py new file mode 100644 index 0000000..cd2b52c --- /dev/null +++ b/src/models.py @@ -0,0 +1,38 @@ +from django.conf import settings +from django.db import models +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ # 用于国际化翻译,将文本标记为可翻译 + +from blog.models import Article # 导入文章模型,用于建立评论与文章的关联 + + +# 评论模型,存储用户对文章的评论及评论间的嵌套关系 +class Comment(models.Model): + body = models.TextField('正文', max_length=300) # 评论内容,限制最大长度300字符 + creation_time = models.DateTimeField(_('creation time'), default=now) # 评论创建时间,默认当前时间 + last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 评论最后修改时间,默认当前时间 + author = models.ForeignKey( + settings.AUTH_USER_MODEL, # 关联Django内置用户模型,便于扩展用户系统 + verbose_name=_('author'), + on_delete=models.CASCADE) # 级联删除:用户删除时,其评论也会被删除 + article = models.ForeignKey( + Article, + verbose_name=_('article'), + on_delete=models.CASCADE) # 级联删除:文章删除时,其下所有评论也会被删除 + parent_comment = models.ForeignKey( + 'self', # 自关联,实现评论嵌套回复功能 + verbose_name=_('parent comment'), + blank=True, + null=True, # 允许为空,表示该评论是顶级评论(不是回复) + on_delete=models.CASCADE) # 级联删除:父评论删除时,其所有子评论也会被删除 + is_enable = models.BooleanField(_('enable'), + default=False, blank=False, null=False) # 评论是否启用(可用于审核功能) + + class Meta: + ordering = ['-id'] # 默认按ID降序排列,最新评论显示在前面 + verbose_name = _('comment') + verbose_name_plural = verbose_name + get_latest_by = 'id' # 指定通过id字段获取最新记录 + + def __str__(self): + return self.body # 模型实例的字符串表示,返回评论内容 \ No newline at end of file diff --git a/src/sidebar.html b/src/sidebar.html index d8d7604..94df08a 100644 --- a/src/sidebar.html +++ b/src/sidebar.html @@ -1,223 +1,184 @@ -{% load blog_tags %} -{% load i18n %} - +{% load blog_tags %} +{% load i18n %} + + \ No newline at end of file +
\ No newline at end of file diff --git a/src/tests.py b/src/tests.py new file mode 100644 index 0000000..faebbcf --- /dev/null +++ b/src/tests.py @@ -0,0 +1,289 @@ +# 导入Django测试相关类:客户端模拟、请求工厂、基础测试用例 +from django.test import Client, RequestFactory, TestCase +# 导入URL反向解析函数,用于生成测试URL +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 * +# 导入当前应用(accounts)的工具函数,用于测试账号相关工具功能 +from . import utils + + +# 定义账号功能测试类,继承TestCase(基础测试用例类) +class AccountTest(TestCase): + # 测试前初始化方法,每个测试方法执行前自动运行 + def setUp(self): + # 初始化测试客户端,用于模拟用户发起HTTP请求 + self.client = Client() + # 初始化请求工厂,用于构造自定义请求对象 + self.factory = RequestFactory() + # 创建普通测试用户,存入测试数据库 + self.blog_user = BlogUser.objects.create_user( + username="test", # 用户名 + email="admin@admin.com", # 邮箱 + password="12345678" # 密码 + ) + # 定义测试用的新密码字符串,用于后续密码修改测试 + self.new_test = "xxx123--=" + + # 测试账号验证功能(登录、管理员权限、文章管理) + def test_validate_account(self): + # 获取当前站点域名(用于测试环境下的域名相关逻辑) + site = get_current_site().domain + # 创建超级用户,用于测试管理员权限 + user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", # 超级用户邮箱 + username="liangliangyy1", # 超级用户名 + password="qwer!@#$ggg") # 超级用户密码 + # 从数据库中查询刚创建的超级用户,用于后续验证 + testuser = BlogUser.objects.get(username='liangliangyy1') + + # 模拟超级用户登录,返回登录结果(布尔值) + loginresult = self.client.login( + username='liangliangyy1', # 登录用户名 + password='qwer!@#$ggg') # 登录密码 + # 断言:登录结果应为True(登录成功) + self.assertEqual(loginresult, True) + # 模拟超级用户访问管理员后台首页 + response = self.client.get('/admin/') + # 断言:响应状态码应为200(访问成功) + self.assertEqual(response.status_code, 200) + + # 创建测试分类,用于后续文章关联 + category = Category() + category.name = "categoryaaa" # 分类名称 + category.creation_time = timezone.now() # 分类创建时间(当前时间) + category.last_modify_time = timezone.now() # 分类最后修改时间(当前时间) + category.save() # 保存分类到测试数据库 + + # 创建测试文章,关联上述分类和超级用户 + article = Article() + article.title = "nicetitleaaa" # 文章标题 + article.body = "nicecontentaaa" # 文章内容 + article.author = user # 文章作者(超级用户) + article.category = category # 文章所属分类 + article.type = 'a' # 文章类型(假设'a'代表普通文章) + article.status = 'p' # 文章状态(假设'p'代表已发布) + article.save() # 保存文章到测试数据库 + + # 模拟访问该文章的管理员编辑页(通过文章模型的自定义方法获取URL) + response = self.client.get(article.get_admin_url()) + # 断言:响应状态码应为200(管理员有权限访问,访问成功) + self.assertEqual(response.status_code, 200) + + # 测试账号注册功能(注册、邮箱验证、登录、权限提升、文章管理、登出) + def test_validate_register(self): + # 断言:数据库中初始不存在邮箱为'user123@user.com'的用户(计数为0) + self.assertEquals( + 0, len( + BlogUser.objects.filter( + email='user123@user.com'))) + # 模拟POST请求提交注册表单,访问注册接口 + response = self.client.post(reverse('account:register'), { + 'username': 'user1233', # 注册用户名 + 'email': 'user123@user.com', # 注册邮箱 + 'password1': 'password123!q@wE#R$T', # 注册密码 + 'password2': 'password123!q@wE#R$T', # 密码确认(与密码一致) + }) + # 断言:注册后数据库中应存在该邮箱用户(计数为1) + self.assertEquals( + 1, len( + BlogUser.objects.filter( + email='user123@user.com'))) + # 从数据库中查询刚注册的用户 + user = BlogUser.objects.filter(email='user123@user.com')[0] + # 生成用户邮箱验证的签名(双重SHA256加密,结合密钥和用户ID) + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + # 反向解析验证结果页的URL + path = reverse('accounts:result') + # 拼接完整的邮箱验证URL(包含用户ID和签名) + url = '{path}?type=validation&id={id}&sign={sign}'.format( + path=path, id=user.id, sign=sign) + # 模拟访问邮箱验证URL,完成验证 + response = self.client.get(url) + # 断言:验证页面访问成功(状态码200) + self.assertEqual(response.status_code, 200) + + # 模拟刚注册的用户登录 + self.client.login(username='user1233', password='password123!q@wE#R$T') + # 重新查询该用户,准备提升权限 + user = BlogUser.objects.filter(email='user123@user.com')[0] + user.is_superuser = True # 设置为超级用户 + user.is_staff = True # 设置为管理员(有权访问admin后台) + user.save() # 保存权限修改 + # 调用工具函数删除侧边栏缓存(避免缓存影响测试结果) + delete_sidebar_cache() + # 创建测试分类(用于后续文章关联) + category = Category() + category.name = "categoryaaa" # 分类名称 + category.creation_time = timezone.now() # 创建时间 + category.last_modify_time = timezone.now() # 最后修改时间 + category.save() # 保存分类 + + # 创建测试文章(关联上述分类和提升权限后的用户) + article = Article() + article.category = category # 所属分类 + article.title = "nicetitle333" # 文章标题 + article.body = "nicecontentttt" # 文章内容 + article.author = user # 文章作者(提升权限后的用户) + article.type = 'a' # 文章类型 + article.status = 'p' # 文章状态(已发布) + article.save() # 保存文章 + + # 模拟访问该文章的管理员编辑页 + response = self.client.get(article.get_admin_url()) + # 断言:访问成功(状态码200,因用户已提升为管理员) + self.assertEqual(response.status_code, 200) + + # 模拟用户登出(访问登出接口) + response = self.client.get(reverse('account:logout')) + # 断言:登出响应状态码在[301,302,200]内(重定向或成功) + self.assertIn(response.status_code, [301, 302, 200]) + + # 登出后再次访问文章管理员编辑页(应无权限) + response = self.client.get(article.get_admin_url()) + # 断言:响应状态码在[301,302,200]内(可能重定向到登录页) + self.assertIn(response.status_code, [301, 302, 200]) + + # 模拟使用错误密码登录(密码不匹配) + response = self.client.post(reverse('account:login'), { + 'username': 'user1233', # 正确用户名 + 'password': 'password123' # 错误密码 + }) + # 断言:登录响应状态码在[301,302,200]内(登录失败可能重定向或返回表单) + self.assertIn(response.status_code, [301, 302, 200]) + + # 错误登录后访问文章管理员编辑页(仍无权限) + response = self.client.get(article.get_admin_url()) + # 断言:响应状态码在[301,302,200]内(可能重定向到登录页) + 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) + + # 验证:使用正确邮箱和正确验证码 + err = utils.verify("admin@admin.com", code) + # 断言:验证无错误(返回None) + self.assertEqual(err, None) + + # 验证:使用错误邮箱和正确验证码 + err = utils.verify("admin@123.com", code) + # 断言:验证错误,错误类型为字符串(返回错误信息) + self.assertEqual(type(err), str) + + # 测试“忘记密码-发送验证码”功能的成功场景 + def test_forget_password_email_code_success(self): + # 模拟POST请求提交邮箱,访问“发送忘记密码验证码”接口 + resp = self.client.post( + path=reverse("account:forget_password_code"), # 反向解析接口URL + data=dict(email="admin@admin.com") # 提交已存在的测试邮箱 + ) + + # 断言:响应状态码为200(请求处理成功) + self.assertEqual(resp.status_code, 200) + # 断言:响应内容为"ok"(表示验证码发送成功) + self.assertEqual(resp.content.decode("utf-8"), "ok") + + # 测试“忘记密码-发送验证码”功能的失败场景 + def test_forget_password_email_code_fail(self): + # 模拟POST请求:不提交邮箱(空数据) + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict() # 空数据 + ) + # 断言:响应内容为“错误的邮箱”(无邮箱参数,请求失败) + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + + # 模拟POST请求:提交格式错误的邮箱(无效邮箱) + resp = self.client.post( + path=reverse("account:forget_password_code"), + data=dict(email="admin@com") # 格式错误的邮箱 + ) + # 断言:响应内容为“错误的邮箱”(邮箱格式无效,请求失败) + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") + + # 测试“忘记密码-重置密码”功能的成功场景 + def test_forget_password_email_success(self): + # 生成随机验证码 + code = generate_code() + # 存储验证码(关联测试用户的邮箱) + utils.set_code(self.blog_user.email, code) + # 构造重置密码的请求数据 + data = dict( + new_password1=self.new_test, # 新密码 + new_password2=self.new_test, # 新密码确认(与新密码一致) + email=self.blog_user.email, # 测试用户邮箱 + code=code, # 正确的验证码 + ) + # 模拟POST请求提交重置密码数据,访问重置密码接口 + resp = self.client.post( + path=reverse("account:forget_password"), # 反向解析接口URL + data=data + ) + # 断言:响应状态码为302(重置成功,重定向到登录页或结果页) + self.assertEqual(resp.status_code, 302) + + # 验证:数据库中用户密码是否已更新 + blog_user = BlogUser.objects.filter( + email=self.blog_user.email, # 按邮箱查询测试用户 + ).first() # 获取查询结果的第一个(唯一用户) + # 断言:查询到用户(用户存在) + self.assertNotEqual(blog_user, None) + # 断言:用户密码与新密码匹配(check_password方法验证哈希密码) + self.assertEqual(blog_user.check_password(data["new_password1"]), True) + + # 测试“忘记密码-重置密码”功能:邮箱不存在的失败场景 + def test_forget_password_email_not_user(self): + # 构造重置密码请求数据(使用不存在的邮箱) + data = dict( + new_password1=self.new_test, # 新密码 + new_password2=self.new_test, # 新密码确认 + email="123@123.com", # 不存在的邮箱 + code="123456", # 任意验证码 + ) + # 模拟POST请求提交数据,访问重置密码接口 + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + + # 断言:响应状态码为200(请求处理完成,但重置失败,返回表单页) + self.assertEqual(resp.status_code, 200) + + + # 测试“忘记密码-重置密码”功能:验证码错误的失败场景 + def test_forget_password_email_code_error(self): + # 生成正确的验证码并存储(关联测试用户邮箱) + code = generate_code() + utils.set_code(self.blog_user.email, code) + # 构造重置密码请求数据(使用错误的验证码) + data = dict( + new_password1=self.new_test, # 新密码 + new_password2=self.new_test, # 新密码确认 + email=self.blog_user.email, # 正确的测试用户邮箱 + code="111111", # 错误的验证码 + ) + # 模拟POST请求提交数据,访问重置密码接口 + resp = self.client.post( + path=reverse("account:forget_password"), + data=data + ) + + # 断言:响应状态码为200(请求处理完成,但验证码错误,返回表单页) + self.assertEqual(resp.status_code, 200) \ No newline at end of file diff --git a/src/urls.py b/src/urls.py new file mode 100644 index 0000000..f2f54d3 --- /dev/null +++ b/src/urls.py @@ -0,0 +1,17 @@ +# 导入Django的path函数,用于定义URL路由规则 +from django.urls import path + +# 导入当前应用(comments)的views模块,用于关联视图函数/类 +from . import views + +# 定义当前应用的命名空间为"comments",避免URL名称冲突 +app_name = "comments" + +# 定义URL路由列表,存储URL规则与视图的映射关系 +urlpatterns = [ + # 定义评论提交的URL规则 + path( + 'article//postcomment', # URL路径:包含文章ID(整数类型)的动态路径 + views.CommentPostView.as_view(), # 关联的视图类:调用CommentPostView的as_view()方法生成视图函数 + name='postcomment'), # 给该URL命名为"postcomment",用于反向解析 +] \ No newline at end of file diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..a413b40 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,60 @@ +# 导入logging模块,用于记录日志(如异常信息) +import logging + +# 导入国际化翻译函数,用于生成多语言邮件内容 +from django.utils.translation import gettext_lazy as _ + +# 导入项目工具函数,获取当前站点域名 +from djangoblog.utils import get_current_site +# 导入项目工具函数,用于发送邮件 +from djangoblog.utils import send_email + +# 创建当前模块的日志记录器,用于记录该函数的运行日志 +logger = logging.getLogger(__name__) + + +# 定义发送评论相关邮件的函数,接收评论对象作为参数 +def send_comment_email(comment): + # 获取当前站点的域名(用于拼接文章链接) + site = get_current_site().domain + # 定义邮件主题(多语言翻译,如中文为“感谢您的评论”) + subject = _('Thanks for your comment') + # 拼接评论所属文章的完整URL(HTTPS协议 + 域名 + 文章相对路径) + article_url = f"https://{site}{comment.article.get_absolute_url()}" + # 定义邮件HTML内容(多语言模板,包含感谢语、文章链接、链接提示) + # 使用字符串格式化,替换{article_url}和{article_title}为实际值 + html_content = _("""

Thank you very much for your comments on this site

+ You can visit %(article_title)s + to review your comments, + Thank you again! +
+ If the link above cannot be opened, please copy this link to your browser. + %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} + # 获取评论作者的邮箱(收件人邮箱) + tomail = comment.author.email + # 调用send_email函数发送邮件:收件人列表、主题、HTML内容 + send_email([tomail], subject, html_content) + + # 尝试给父评论作者发送“评论被回复”的邮件(若当前评论是回复) + try: + # 判断当前评论是否有父评论(即是否为回复) + if comment.parent_comment: + # 定义回复通知的HTML邮件内容(多语言模板) + # 包含父评论所属文章链接、父评论内容、查看提示 + html_content = _("""Your comment on %(article_title)s
has + received a reply.
%(comment_body)s +
+ go check it out! +
+ If the link above cannot be opened, please copy this link to your browser. + %(article_url)s + """) % {'article_url': article_url, 'article_title': comment.article.title, + 'comment_body': comment.parent_comment.body} + # 获取父评论作者的邮箱(回复通知的收件人) + tomail = comment.parent_comment.author.email + # 发送回复通知邮件 + send_email([tomail], subject, html_content) + # 捕获所有异常,避免发送失败影响主流程 + except Exception as e: + # 将异常信息记录到日志(级别为ERROR) + logger.error(e) \ No newline at end of file diff --git a/src/views.py b/src/views.py new file mode 100644 index 0000000..155c322 --- /dev/null +++ b/src/views.py @@ -0,0 +1,100 @@ +# 导入Django的验证错误类,用于抛出表单验证异常 +from django.core.exceptions import ValidationError +# 导入HTTP重定向响应类,用于页面跳转 +from django.http import HttpResponseRedirect +# 导入快捷函数,用于获取对象或返回404错误 +from django.shortcuts import get_object_or_404 +# 导入方法装饰器工具,用于装饰类中的方法 +from django.utils.decorators import method_decorator +# 导入CSRF保护装饰器,防止跨站请求伪造 +from django.views.decorators.csrf import csrf_protect +# 导入表单视图基类,用于处理表单提交逻辑 +from django.views.generic.edit import FormView + +# 导入用户模型,用于获取评论作者信息 +from accounts.models import BlogUser +# 导入文章模型,用于关联评论所属文章 +from blog.models import Article +# 导入评论表单类,用于处理评论提交数据 +from .forms import CommentForm +# 导入评论模型,用于创建和保存评论 +from .models import Comment + + +# 定义评论提交视图类,继承自FormView(表单处理基类) +class CommentPostView(FormView): + form_class = CommentForm # 指定使用的表单类为CommentForm + template_name = 'blog/article_detail.html' # 指定表单验证失败时渲染的模板 + + # 使用CSRF保护装饰器装饰dispatch方法,确保表单提交安全 + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + # 调用父类的dispatch方法,处理请求分发 + return super(CommentPostView, self).dispatch(*args, **kwargs) + + # 处理GET请求:重定向到文章详情页的评论区 + def get(self, request, *args, **kwargs): + # 从URL参数中获取文章ID + article_id = self.kwargs['article_id'] + # 获取对应的文章对象,不存在则返回404 + article = get_object_or_404(Article, pk=article_id) + # 获取文章详情页的URL + url = article.get_absolute_url() + # 重定向到文章详情页的评论区(通过锚点#comments定位) + return HttpResponseRedirect(url + "#comments") + + # 处理表单验证失败的逻辑 + def form_invalid(self, form): + # 从URL参数中获取文章ID + article_id = self.kwargs['article_id'] + # 获取对应的文章对象 + article = get_object_or_404(Article, pk=article_id) + + # 渲染文章详情页模板,传递错误的表单和文章对象(用于显示错误信息) + return self.render_to_response({ + 'form': form, + 'article': article + }) + + # 处理表单验证成功后的逻辑 + def form_valid(self, form): + """提交的数据验证合法后的逻辑""" + # 获取当前登录用户 + user = self.request.user + # 根据用户ID获取对应的用户对象(评论作者) + author = BlogUser.objects.get(pk=user.pk) + # 从URL参数中获取文章ID + article_id = self.kwargs['article_id'] + # 获取对应的文章对象 + article = get_object_or_404(Article, pk=article_id) + + # 检查文章是否允许评论:若文章评论状态为关闭或文章状态为草稿,则抛出验证错误 + if article.comment_status == 'c' or article.status == 'c': + raise ValidationError("该文章评论已关闭.") + # 保存表单数据但不提交到数据库(获取评论对象) + comment = form.save(False) + # 关联评论到对应的文章 + comment.article = article + # 导入工具函数,获取博客设置 + from djangoblog.utils import get_blog_setting + settings = get_blog_setting() + # 若博客设置为评论无需审核,则直接启用评论 + if not settings.comment_need_review: + comment.is_enable = True + # 设置评论的作者 + comment.author = author + + # 处理回复功能:若存在父评论ID,则关联到父评论 + if form.cleaned_data['parent_comment_id']: + # 根据父评论ID获取父评论对象 + parent_comment = Comment.objects.get( + pk=form.cleaned_data['parent_comment_id']) + # 设置当前评论的父评论 + comment.parent_comment = parent_comment + + # 保存评论到数据库(执行真正的保存操作) + comment.save(True) + # 重定向到文章详情页的当前评论位置(通过锚点#div-comment-{评论ID}定位) + return HttpResponseRedirect( + "%s#div-comment-%d" % + (article.get_absolute_url(), comment.pk)) From 159ba0643c394cc88f5be18dd79c5f6d3ce5820f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=85=A7?= <1096877868@qq.com> Date: Sat, 18 Oct 2025 20:43:21 +0800 Subject: [PATCH 2/6] zh --- src/.idea/.name | 1 + src/__init__.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 src/.idea/.name diff --git a/src/.idea/.name b/src/.idea/.name new file mode 100644 index 0000000..93f5256 --- /dev/null +++ b/src/.idea/.name @@ -0,0 +1 @@ +__init__.py \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py index e69de29..4287ca8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -0,0 +1 @@ +# \ No newline at end of file From 40f99c72d0d8c5ee87653b8b60618da675e65afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=85=A7?= <1096877868@qq.com> Date: Sat, 18 Oct 2025 20:45:49 +0800 Subject: [PATCH 3/6] zh --- src/admin.py | 2 +- src/apps.py | 2 +- src/forms.py | 4 ++-- src/models.py | 4 ++-- src/tests.py | 2 +- src/urls.py | 2 +- src/utils.py | 1 - src/views.py | 1 - 8 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/admin.py b/src/admin.py index 1ae5ac1..cd0504b 100644 --- a/src/admin.py +++ b/src/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from django.urls import reverse # 用于生成管理员界面的URL反向解析 +from django.urls import reverse from django.utils.html import format_html # 用于安全地生成HTML内容 from django.utils.translation import gettext_lazy as _ # 用于国际化翻译 diff --git a/src/apps.py b/src/apps.py index c8106f9..c7e4666 100644 --- a/src/apps.py +++ b/src/apps.py @@ -1,4 +1,4 @@ -from django.apps import AppConfig # 导入Django的应用配置基类 +from django.apps import AppConfig class CommentsConfig(AppConfig): diff --git a/src/forms.py b/src/forms.py index 440cc02..f4529ee 100644 --- a/src/forms.py +++ b/src/forms.py @@ -1,5 +1,5 @@ -from django import forms # 导入Django表单基础模块 -from django.forms import ModelForm # 导入模型表单类,用于快速生成与模型对应的表单 +from django import forms +from django.forms import ModelForm from .models import Comment # 从当前应用的models中导入Comment模型 diff --git a/src/models.py b/src/models.py index cd2b52c..7e1d094 100644 --- a/src/models.py +++ b/src/models.py @@ -1,9 +1,9 @@ from django.conf import settings from django.db import models from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _ # 用于国际化翻译,将文本标记为可翻译 +from django.utils.translation import gettext_lazy as _ -from blog.models import Article # 导入文章模型,用于建立评论与文章的关联 +from blog.models import Article # 评论模型,存储用户对文章的评论及评论间的嵌套关系 diff --git a/src/tests.py b/src/tests.py index faebbcf..b67fd77 100644 --- a/src/tests.py +++ b/src/tests.py @@ -1,4 +1,4 @@ -# 导入Django测试相关类:客户端模拟、请求工厂、基础测试用例 + from django.test import Client, RequestFactory, TestCase # 导入URL反向解析函数,用于生成测试URL from django.urls import reverse diff --git a/src/urls.py b/src/urls.py index f2f54d3..62766d2 100644 --- a/src/urls.py +++ b/src/urls.py @@ -1,4 +1,4 @@ -# 导入Django的path函数,用于定义URL路由规则 + from django.urls import path # 导入当前应用(comments)的views模块,用于关联视图函数/类 diff --git a/src/utils.py b/src/utils.py index a413b40..c8eb74f 100644 --- a/src/utils.py +++ b/src/utils.py @@ -30,7 +30,6 @@ def send_comment_email(comment):
If the link above cannot be opened, please copy this link to your browser. %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} - # 获取评论作者的邮箱(收件人邮箱) tomail = comment.author.email # 调用send_email函数发送邮件:收件人列表、主题、HTML内容 send_email([tomail], subject, html_content) diff --git a/src/views.py b/src/views.py index 155c322..601ca2c 100644 --- a/src/views.py +++ b/src/views.py @@ -1,4 +1,3 @@ -# 导入Django的验证错误类,用于抛出表单验证异常 from django.core.exceptions import ValidationError # 导入HTTP重定向响应类,用于页面跳转 from django.http import HttpResponseRedirect From 53f78195fd156b08b4a6cc60308b4784c31ddc37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=85=A7?= <1096877868@qq.com> Date: Sat, 18 Oct 2025 22:30:15 +0800 Subject: [PATCH 4/6] =?UTF-8?q?zh=E7=AC=AC=E4=BA=94=E5=91=A8=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__init__.py | 1 - src/apps.py | 2 +- src/forms.py | 2 +- src/models.py | 2 +- src/tests.py | 2 +- src/urls.py | 2 +- src/utils.py | 2 +- src/views.py | 3 +-- 8 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 4287ca8..e69de29 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1 +0,0 @@ -# \ No newline at end of file diff --git a/src/apps.py b/src/apps.py index c7e4666..d932e89 100644 --- a/src/apps.py +++ b/src/apps.py @@ -2,6 +2,6 @@ from django.apps import AppConfig class CommentsConfig(AppConfig): - # 配置应用的名称,对应项目中该应用的目录名(此处为'comments') + # 配置应用的名称,对应项目中该应用的目录名) # Django通过这个名称识别和管理该应用 name = 'comments' \ No newline at end of file diff --git a/src/forms.py b/src/forms.py index f4529ee..3d4331d 100644 --- a/src/forms.py +++ b/src/forms.py @@ -1,7 +1,7 @@ from django import forms from django.forms import ModelForm -from .models import Comment # 从当前应用的models中导入Comment模型 +from .models import Comment class CommentForm(ModelForm): # 定义评论表单类,继承自ModelForm diff --git a/src/models.py b/src/models.py index 7e1d094..345c463 100644 --- a/src/models.py +++ b/src/models.py @@ -35,4 +35,4 @@ class Comment(models.Model): get_latest_by = 'id' # 指定通过id字段获取最新记录 def __str__(self): - return self.body # 模型实例的字符串表示,返回评论内容 \ No newline at end of file + return self.body \ No newline at end of file diff --git a/src/tests.py b/src/tests.py index b67fd77..eb3263e 100644 --- a/src/tests.py +++ b/src/tests.py @@ -1,6 +1,6 @@ from django.test import Client, RequestFactory, TestCase -# 导入URL反向解析函数,用于生成测试URL + from django.urls import reverse # 导入时区处理模块,用于处理时间相关数据 from django.utils import timezone diff --git a/src/urls.py b/src/urls.py index 62766d2..7bb9b5e 100644 --- a/src/urls.py +++ b/src/urls.py @@ -9,7 +9,7 @@ app_name = "comments" # 定义URL路由列表,存储URL规则与视图的映射关系 urlpatterns = [ - # 定义评论提交的URL规则 + path( 'article//postcomment', # URL路径:包含文章ID(整数类型)的动态路径 views.CommentPostView.as_view(), # 关联的视图类:调用CommentPostView的as_view()方法生成视图函数 diff --git a/src/utils.py b/src/utils.py index c8eb74f..35d61e7 100644 --- a/src/utils.py +++ b/src/utils.py @@ -55,5 +55,5 @@ def send_comment_email(comment): send_email([tomail], subject, html_content) # 捕获所有异常,避免发送失败影响主流程 except Exception as e: - # 将异常信息记录到日志(级别为ERROR) + logger.error(e) \ No newline at end of file diff --git a/src/views.py b/src/views.py index 601ca2c..20487ed 100644 --- a/src/views.py +++ b/src/views.py @@ -1,7 +1,6 @@ from django.core.exceptions import ValidationError -# 导入HTTP重定向响应类,用于页面跳转 + from django.http import HttpResponseRedirect -# 导入快捷函数,用于获取对象或返回404错误 from django.shortcuts import get_object_or_404 # 导入方法装饰器工具,用于装饰类中的方法 from django.utils.decorators import method_decorator From 349168fe7264b5b5b21900abacbca0193be6ffb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=85=A7?= <1096877868@qq.com> Date: Sat, 18 Oct 2025 22:58:37 +0800 Subject: [PATCH 5/6] =?UTF-8?q?zh=E7=AC=AC=E4=BA=94=E5=91=A8=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/.idea/.name | 2 +- src/admin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/.idea/.name b/src/.idea/.name index 93f5256..35ff20d 100644 --- a/src/.idea/.name +++ b/src/.idea/.name @@ -1 +1 @@ -__init__.py \ No newline at end of file +admin.py \ No newline at end of file diff --git a/src/admin.py b/src/admin.py index cd0504b..2d0166a 100644 --- a/src/admin.py +++ b/src/admin.py @@ -21,7 +21,7 @@ enable_commentstatus.short_description = _('Enable comments') class CommentAdmin(admin.ModelAdmin): list_per_page = 20 # 每页显示20条记录 - # 列表页显示的字段 + list_display = ( 'id', 'body', # 评论内容 From 4ccff4e443f905d1c53eef04edfe288c6a380202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=85=A7?= <1096877868@qq.com> Date: Sat, 18 Oct 2025 22:59:51 +0800 Subject: [PATCH 6/6] =?UTF-8?q?zh=E7=AC=AC=E4=BA=94=E5=91=A8=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/.idea/.gitignore | 5 ----- src/.idea/.name | 1 - src/.idea/inspectionProfiles/profiles_settings.xml | 6 ------ src/.idea/misc.xml | 7 ------- src/.idea/modules.xml | 8 -------- src/.idea/src.iml | 8 -------- src/.idea/vcs.xml | 6 ------ 7 files changed, 41 deletions(-) delete mode 100644 src/.idea/.gitignore delete mode 100644 src/.idea/.name delete mode 100644 src/.idea/inspectionProfiles/profiles_settings.xml delete mode 100644 src/.idea/misc.xml delete mode 100644 src/.idea/modules.xml delete mode 100644 src/.idea/src.iml delete mode 100644 src/.idea/vcs.xml diff --git a/src/.idea/.gitignore b/src/.idea/.gitignore deleted file mode 100644 index 10b731c..0000000 --- a/src/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ diff --git a/src/.idea/.name b/src/.idea/.name deleted file mode 100644 index 35ff20d..0000000 --- a/src/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -admin.py \ No newline at end of file diff --git a/src/.idea/inspectionProfiles/profiles_settings.xml b/src/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/src/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/.idea/misc.xml b/src/.idea/misc.xml deleted file mode 100644 index db8786c..0000000 --- a/src/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/.idea/modules.xml b/src/.idea/modules.xml deleted file mode 100644 index f669a0e..0000000 --- a/src/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/.idea/src.iml b/src/.idea/src.iml deleted file mode 100644 index f571432..0000000 --- a/src/.idea/src.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/.idea/vcs.xml b/src/.idea/vcs.xml deleted file mode 100644 index 6c0b863..0000000 --- a/src/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file