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 article.type == 'a' %} // 检查文章类型是否为文章 - {% if not isindex %} // 如果当前不是索引页(即文章详情页) - {% cache 36000 breadcrumb article.pk %}// 缓存面包屑导航,有效期10小时(36000秒),以文章主键作为缓存键 - {% load_breadcrumb article %}// 调用自定义标签加载文章的面包屑导航 + + {% if article.type == 'a' %} + {% if not isindex %} + + {% cache 36000 breadcrumb article.pk %} + {% load_breadcrumb article %} {% endcache %} {% endif %} {% endif %} -
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 %}
- - {% for name,url in names %} - + {% for name,url in names %} +-
-
-
+
-
- {{ name }}
-
-
-
+ {{ name }}
+
-
+
- {% endfor %}
+ {% endfor %}
-
+
-
- {{ title }}
-
+ {{ title }}
+
-
-
+ 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 %} + +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