diff --git a/doc/12组软工4班9-10周实践任务.docx b/doc/12组软工4班9-10周实践任务.docx new file mode 100644 index 0000000..44fc6e4 Binary files /dev/null and b/doc/12组软工4班9-10周实践任务.docx differ diff --git a/src/DjangoBlog-master/comments/admin.py b/src/DjangoBlog-master/comments/admin.py index 48183c0..032069a 100644 --- a/src/DjangoBlog-master/comments/admin.py +++ b/src/DjangoBlog-master/comments/admin.py @@ -1,7 +1,9 @@ +# admin.py from django.contrib import admin from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ +from comments.models import Comment, CommentLike, CommentReport def disable_commentstatus(modeladmin, request, queryset): @@ -45,3 +47,48 @@ class CommentAdmin(admin.ModelAdmin): link_to_userinfo.short_description = _('User') # 杨智鑫:用户 link_to_article.short_description = _('Article') # 杨智鑫:文章 + + + +class CommentLikeAdmin(admin.ModelAdmin): + """评论点赞管理界面""" + list_display = ('id', 'user', 'comment', 'created_time') + list_filter = ('created_time',) + search_fields = ('user__username', 'comment__body') + readonly_fields = ('created_time',) + list_per_page = 20 + + def get_queryset(self, request): + return super().get_queryset(request).select_related('user', 'comment') + + +class CommentReportAdmin(admin.ModelAdmin): + """评论举报管理界面""" + list_display = ('id', 'user', 'comment', 'reason', 'is_handled', 'created_time') + list_filter = ('reason', 'is_handled', 'created_time') + search_fields = ('user__username', 'comment__body', 'description') + readonly_fields = ('created_time',) + list_per_page = 20 + actions = ['mark_as_handled', 'mark_as_pending'] + + def mark_as_handled(self, request, queryset): + """标记为已处理""" + updated_count = queryset.update(is_handled=True) + self.message_user(request, f'成功标记 {updated_count} 个举报为已处理') + + mark_as_handled.short_description = '标记选中的举报为已处理' + + def mark_as_pending(self, request, queryset): + """标记为待处理""" + updated_count = queryset.update(is_handled=False) + self.message_user(request, f'成功标记 {updated_count} 个举报为待处理') + + mark_as_pending.short_description = '标记选中的举报为待处理' + + def get_queryset(self, request): + return super().get_queryset(request).select_related('user', 'comment') + + +# 注册新模型到admin +admin.site.register(CommentLike, CommentLikeAdmin) +admin.site.register(CommentReport, CommentReportAdmin) \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/apps.py b/src/DjangoBlog-master/comments/apps.py index 3795ad4..da3cab8 100644 --- a/src/DjangoBlog-master/comments/apps.py +++ b/src/DjangoBlog-master/comments/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class CommentsConfig(AppConfig): - name = 'comments' # 杨智鑫:应用名称 + name = 'comments' # 杨智鑫:应用名称 \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/forms.py b/src/DjangoBlog-master/comments/forms.py index 75e9532..e287dd6 100644 --- a/src/DjangoBlog-master/comments/forms.py +++ b/src/DjangoBlog-master/comments/forms.py @@ -1,7 +1,8 @@ +# forms.py from django import forms from django.forms import ModelForm -from .models import Comment +from .models import Comment,CommentReport class CommentForm(ModelForm): @@ -11,3 +12,24 @@ class CommentForm(ModelForm): class Meta: model = Comment # 杨智鑫:指定表单关联的模型 fields = ['body'] # 杨智鑫:表单只包含评论内容字段 +# 新增评论举报表单 +class CommentReportForm(forms.ModelForm): + """评论举报表单""" + class Meta: + model = CommentReport + fields = ['reason', 'description'] + widgets = { + 'reason': forms.Select(attrs={ + 'class': 'form-control', + 'placeholder': '请选择举报原因' + }), + 'description': forms.Textarea(attrs={ + 'rows': 3, + 'class': 'form-control', + 'placeholder': '请详细描述举报原因(可选)' + }) + } + labels = { + 'reason': '举报原因', + 'description': '详细说明' + } \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/models.py b/src/DjangoBlog-master/comments/models.py index fcaf118..076a0d7 100644 --- a/src/DjangoBlog-master/comments/models.py +++ b/src/DjangoBlog-master/comments/models.py @@ -1,3 +1,4 @@ +# models.py from django.conf import settings from django.db import models from django.utils.timezone import now @@ -37,3 +38,63 @@ class Comment(models.Model): def __str__(self): return self.body # 杨智鑫:返回评论内容 + + +# 在 Comment 类后面添加以下代码 + +class CommentLike(models.Model): + """评论点赞模型""" + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=_('user'), + on_delete=models.CASCADE + ) + comment = models.ForeignKey( + Comment, + verbose_name=_('comment'), + on_delete=models.CASCADE + ) + created_time = models.DateTimeField(_('creation time'), default=now) + + class Meta: + unique_together = ('user', 'comment') # 防止重复点赞 + verbose_name = _('comment like') + verbose_name_plural = _('comment likes') + ordering = ['-created_time'] + + def __str__(self): + return f"{self.user.username} likes {self.comment.id}" + + +class CommentReport(models.Model): + """评论举报模型""" + REPORT_REASONS = [ + ('spam', '垃圾广告'), + ('abuse', '辱骂攻击'), + ('illegal', '违法信息'), + ('other', '其他原因') + ] + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=_('user'), + on_delete=models.CASCADE + ) + comment = models.ForeignKey( + Comment, + verbose_name=_('comment'), + on_delete=models.CASCADE + ) + reason = models.CharField(_('report reason'), max_length=20, choices=REPORT_REASONS) + description = models.TextField(_('description'), max_length=500, blank=True) + created_time = models.DateTimeField(_('creation time'), default=now) + is_handled = models.BooleanField(_('is handled'), default=False) + + class Meta: + unique_together = ('user', 'comment') # 防止重复举报 + verbose_name = _('comment report') + verbose_name_plural = _('comment reports') + ordering = ['-created_time'] + + def __str__(self): + return f"Report {self.comment.id} by {self.user.username}" \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/tests.py b/src/DjangoBlog-master/comments/tests.py index 787f820..3590c0d 100644 --- a/src/DjangoBlog-master/comments/tests.py +++ b/src/DjangoBlog-master/comments/tests.py @@ -1,3 +1,4 @@ +# tests.py from django.test import Client, RequestFactory, TransactionTestCase from django.urls import reverse @@ -106,4 +107,4 @@ class CommentsTest(TransactionTestCase): self.assertIsNotNone(s) # 杨智鑫:判断数据是否为空 from comments.utils import send_comment_email - send_comment_email(comment) # 杨智鑫:发送邮件 + send_comment_email(comment) # 杨智鑫:发送邮件 \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/urls.py b/src/DjangoBlog-master/comments/urls.py index 9bd97a0..a5360d7 100644 --- a/src/DjangoBlog-master/comments/urls.py +++ b/src/DjangoBlog-master/comments/urls.py @@ -1,11 +1,24 @@ from django.urls import path - from . import views app_name = "comments" # 杨智鑫:定义应用命名空间 + urlpatterns = [ path( 'article//postcomment', views.CommentPostView.as_view(), # 杨智鑫:定义路由 - name='postcomment'), # 杨智鑫:定义路由名称 -] + name='postcomment' # 杨智鑫:定义路由名称 + ), + # 杨智鑫:添加点赞评论路由 + path( + 'comment//like/', # 修改路径,添加comment前缀 + views.like_comment, # 杨智鑫:点赞视图 + name='like_comment' # 杨智鑫:路由名称 + ), + # 杨智鑫:添加举报评论路由 + path( + 'comment//report/', # 修改路径,添加comment前缀 + views.report_comment, # 杨智鑫:举报视图 + name='report_comment' # 杨智鑫:路由名称 + ), +] \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/utils.py b/src/DjangoBlog-master/comments/utils.py index 0e79dbb..4a4c32f 100644 --- a/src/DjangoBlog-master/comments/utils.py +++ b/src/DjangoBlog-master/comments/utils.py @@ -1,3 +1,4 @@ +# utils.py import logging from django.utils.translation import gettext_lazy as _ diff --git a/src/DjangoBlog-master/comments/views.py b/src/DjangoBlog-master/comments/views.py index 03658bf..2b25202 100644 --- a/src/DjangoBlog-master/comments/views.py +++ b/src/DjangoBlog-master/comments/views.py @@ -1,20 +1,23 @@ # Create your views here. from django.core.exceptions import ValidationError -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_protect +from django.views.decorators.csrf import csrf_protect, csrf_exempt +from django.views.decorators.http import require_http_methods from django.views.generic.edit import FormView +from django.contrib.auth.decorators import login_required +import json from accounts.models import BlogUser from blog.models import Article from .forms import CommentForm -from .models import Comment +from .models import Comment, CommentLike, CommentReport class CommentPostView(FormView): form_class = CommentForm # 杨智鑫:指定使用的表单类 - template_name = 'blog/article_detail.html' # 杨智鑫:指定使用的模板 + template_name = 'blog/article_detail.html' # 杨智鑫:指定使用的模板 @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): # 杨智鑫:添加csrf_protect装饰器 @@ -43,21 +46,116 @@ class CommentPostView(FormView): article = get_object_or_404(Article, pk=article_id) # 杨智鑫:获取文章对象 if article.comment_status == 'c' or article.status == 'c': - raise ValidationError("该文章评论已关闭.") # 杨智鑫:抛出异常 - comment = form.save(False) # 杨智鑫:保存评论 + raise ValidationError("该文章评论已关闭.") # 杨智鑫:抛出异常 + comment = form.save(False) # 杨智鑫:保存评论 comment.article = article # 杨智鑫:设置评论所属文章 from djangoblog.utils import get_blog_setting - settings = get_blog_setting() # 杨智鑫:获取博客设置 + settings = get_blog_setting() # 杨智鑫:获取博客设置 if not settings.comment_need_review: comment.is_enable = True comment.author = author # 杨智鑫:设置评论作者 if form.cleaned_data['parent_comment_id']: # 杨智鑫:判断是否有父级评论 parent_comment = Comment.objects.get( - pk=form.cleaned_data['parent_comment_id']) # 杨智鑫:获取父级评论对象 + pk=form.cleaned_data['parent_comment_id']) # 杨智鑫:获取父级评论对象 comment.parent_comment = parent_comment # 杨智鑫:设置父级评论 - comment.save(True) # 杨智鑫:保存评论 + comment.save(True) # 杨智鑫:保存评论 return HttpResponseRedirect( "%s#div-comment-%d" % (article.get_absolute_url(), comment.pk)) # 杨智鑫:跳转到评论区 + + +# 杨智鑫:点赞评论视图 +@login_required +@require_http_methods(["POST"]) +@csrf_exempt +def like_comment(request, comment_id): + """点赞/取消点赞评论""" + try: + comment = Comment.objects.get(pk=comment_id) + user = request.user + + # 检查是否已经点赞 + like, created = CommentLike.objects.get_or_create( + user=user, + comment=comment + ) + + if not created: + # 如果已经点赞,则取消点赞(删除记录) + like.delete() + liked = False + message = "取消点赞成功" + else: + # 新点赞 + liked = True + message = "点赞成功" + + # 获取当前点赞数 + like_count = CommentLike.objects.filter(comment=comment).count() + + return JsonResponse({ + 'success': True, + 'liked': liked, + 'like_count': like_count, + 'message': message + }) + + except Comment.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': '评论不存在' + }, status=404) + except Exception as e: + return JsonResponse({ + 'success': False, + 'message': f'操作失败: {str(e)}' + }, status=500) + + +# 杨智鑫:举报评论视图 +@login_required +@require_http_methods(["POST"]) +@csrf_exempt +def report_comment(request, comment_id): + """举报评论""" + try: + comment = Comment.objects.get(pk=comment_id) + user = request.user + + # 解析请求数据 + data = json.loads(request.body) + reason = data.get('reason') + description = data.get('description', '') + + # 检查是否已经举报过 + if CommentReport.objects.filter(user=user, comment=comment).exists(): + return JsonResponse({ + 'success': False, + 'message': '您已经举报过该评论' + }) + + # 创建举报记录 + report = CommentReport.objects.create( + user=user, + comment=comment, + reason=reason, + description=description + ) + + return JsonResponse({ + 'success': True, + 'message': '举报成功,我们会尽快处理' + }) + + except Comment.DoesNotExist: + return JsonResponse({ + 'success': False, + 'message': '评论不存在' + }, status=404) + except Exception as e: + return JsonResponse({ + 'success': False, + 'message': f'举报失败: {str(e)}' + }, status=500) \ No newline at end of file diff --git a/src/DjangoBlog-master/templates/comments/tags/comment_item_tree.html b/src/DjangoBlog-master/templates/comments/tags/comment_item_tree.html index a9decd1..b8120a8 100644 --- a/src/DjangoBlog-master/templates/comments/tags/comment_item_tree.html +++ b/src/DjangoBlog-master/templates/comments/tags/comment_item_tree.html @@ -18,7 +18,6 @@ class="url">{{ comment_item.author.username }} - + +
+ + + + + + + - + + + 回复 + +
+ + {% query article_comments parent_comment=comment_item as cc_comments %} {% for cc in cc_comments %} {% with comment_item=cc template_name="comments/tags/comment_item_tree.html" %} diff --git a/src/DjangoBlog-master/templates/comments/tags/comment_list.html b/src/DjangoBlog-master/templates/comments/tags/comment_list.html index 4092161..af7310a 100644 --- a/src/DjangoBlog-master/templates/comments/tags/comment_list.html +++ b/src/DjangoBlog-master/templates/comments/tags/comment_list.html @@ -1,4 +1,7 @@ - + + + +
{% load blog_tags %} {% load comments_tags %} @@ -7,39 +10,317 @@ + {% if article_comments %}
    - {# {% query article_comments parent_comment=None as parent_comments %}#} - {% for comment_item in p_comments %} - + {# 杨智鑫:确保这里使用正确的变量名 #} + {% for comment_item in article_comments %} {% with 0 as depth %} {% include "comments/tags/comment_item_tree.html" %} {% endwith %} {% endfor %} -
+
+ {% else %} +
+

暂无评论

+
{% endif %}
+
+ + + + + + \ No newline at end of file