diff --git a/src/admin.py b/src/admin.py index 32e483c..ffc2646 100644 --- a/src/admin.py +++ b/src/admin.py @@ -1,3 +1,4 @@ +<<<<<<< HEAD from django import forms from django.contrib.auth.admin import UserAdmin from django.contrib.auth.forms import UserChangeForm @@ -57,3 +58,70 @@ class BlogUserAdmin(UserAdmin): 'source') list_display_links = ('id', 'username') ordering = ('-id',) +======= +from django.contrib import admin +from django.urls import reverse +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') +>>>>>>> zh_branch diff --git a/src/apps.py b/src/apps.py index 1ee96c1..a6b70f2 100644 --- a/src/apps.py +++ b/src/apps.py @@ -1,4 +1,5 @@ <<<<<<< HEAD +<<<<<<< HEAD # 导入Django的AppConfig类,用于配置Django应用的生命周期和元数据 from django.apps import AppConfig @@ -49,3 +50,12 @@ from django.apps import AppConfig class AccountsConfig(AppConfig): name = 'accounts' +======= +from django.apps import AppConfig + + +class CommentsConfig(AppConfig): + # 配置应用的名称,对应项目中该应用的目录名) + # Django通过这个名称识别和管理该应用 + name = 'comments' +>>>>>>> zh_branch 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 index fce4137..a7b6b65 100644 --- a/src/forms.py +++ b/src/forms.py @@ -1,4 +1,5 @@ from django import forms +<<<<<<< HEAD from django.contrib.auth import get_user_model, password_validation from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.core.exceptions import ValidationError @@ -115,3 +116,19 @@ class ForgetPasswordCodeForm(forms.Form): email = forms.EmailField( label=_('Email'), ) +======= +from django.forms import ModelForm + +from .models import 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 +>>>>>>> zh_branch diff --git a/src/models.py b/src/models.py index 3baddbb..0e2da26 100644 --- a/src/models.py +++ b/src/models.py @@ -1,3 +1,4 @@ +<<<<<<< HEAD from django.contrib.auth.models import AbstractUser from django.db import models from django.urls import reverse @@ -33,3 +34,43 @@ class BlogUser(AbstractUser): verbose_name = _('user') verbose_name_plural = verbose_name get_latest_by = 'id' +======= +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 +>>>>>>> zh_branch 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 index b8a960c..4a56c76 100644 --- a/src/tests.py +++ b/src/tests.py @@ -1,4 +1,5 @@ <<<<<<< HEAD +<<<<<<< HEAD from django.test import TestCase from djangoblog.utils import * @@ -90,20 +91,118 @@ class AccountTest(TestCase): self.assertEqual(response.status_code, 200) def test_validate_register(self): +======= + +from django.test import Client, RequestFactory, TestCase + +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) +>>>>>>> zh_branch self.assertEquals( 0, len( BlogUser.objects.filter( email='user123@user.com'))) +<<<<<<< HEAD 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', }) +======= + # 模拟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) +>>>>>>> zh_branch self.assertEquals( 1, len( BlogUser.objects.filter( email='user123@user.com'))) +<<<<<<< HEAD user = BlogUser.objects.filter(email='user123@user.com')[0] sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) path = reverse('accounts:result') @@ -215,11 +314,174 @@ class AccountTest(TestCase): email="123@123.com", code="123456", ) +======= + # 从数据库中查询刚注册的用户 + 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请求提交数据,访问重置密码接口 +>>>>>>> zh_branch resp = self.client.post( path=reverse("account:forget_password"), data=data ) +<<<<<<< HEAD self.assertEqual(resp.status_code, 200) @@ -232,11 +494,35 @@ class AccountTest(TestCase): email=self.blog_user.email, code="111111", ) +======= + # 断言:响应状态码为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请求提交数据,访问重置密码接口 +>>>>>>> zh_branch resp = self.client.post( path=reverse("account:forget_password"), data=data ) +<<<<<<< HEAD self.assertEqual(resp.status_code, 200) +======= + # 断言:响应状态码为200(请求处理完成,但验证码错误,返回表单页) + self.assertEqual(resp.status_code, 200) +>>>>>>> zh_branch diff --git a/src/urls.py b/src/urls.py index 2e04bfd..aa2b4bd 100644 --- a/src/urls.py +++ b/src/urls.py @@ -1,4 +1,5 @@ <<<<<<< HEAD +<<<<<<< HEAD """djangoblog URL Configuration 项目URL路由总配置文件:定义所有URL与视图/应用的映射关系 核心作用是将用户访问的URL地址,分发到对应的应用或视图函数处理 @@ -127,3 +128,22 @@ urlpatterns = [re_path(r'^login/$', name='forget_password_code'), ] >>>>>>> sh_branch +======= + +from django.urls import path + +# 导入当前应用(comments)的views模块,用于关联视图函数/类 +from . import views + +# 定义当前应用的命名空间为"comments",避免URL名称冲突 +app_name = "comments" + +# 定义URL路由列表,存储URL规则与视图的映射关系 +urlpatterns = [ + + path( + 'article//postcomment', # URL路径:包含文章ID(整数类型)的动态路径 + views.CommentPostView.as_view(), # 关联的视图类:调用CommentPostView的as_view()方法生成视图函数 + name='postcomment'), # 给该URL命名为"postcomment",用于反向解析 +] +>>>>>>> zh_branch diff --git a/src/utils.py b/src/utils.py index f263a9e..3b4a8bf 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,4 +1,5 @@ <<<<<<< HEAD +<<<<<<< HEAD #!/usr/bin/env python # encoding: utf-8 @@ -404,3 +405,64 @@ def get_code(email: str) -> typing.Optional[str]: """获取code""" return cache.get(email) >>>>>>> sh_branch +======= +# 导入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: + + logger.error(e) +>>>>>>> zh_branch diff --git a/src/views.py b/src/views.py index ae67aec..9a88437 100644 --- a/src/views.py +++ b/src/views.py @@ -1,3 +1,4 @@ +<<<<<<< HEAD import logging from django.utils.translation import gettext_lazy as _ from django.conf import settings @@ -202,3 +203,103 @@ class ForgetPasswordEmailCode(View): utils.set_code(to_email, code) return HttpResponse("ok") +======= +from django.core.exceptions import ValidationError + +from django.http import HttpResponseRedirect +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)) +>>>>>>> zh_branch