From 8be1bb2268b09e5ea33ccf07d602feed8b075146 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Fri, 23 Oct 2009 19:22:31 +0000 Subject: [PATCH] Fixed #11625: added comment moderation via admin actions. This is BACKWARDS INCOMPATIBLE if you were using the completely undocumented moderation view from 1.1. That view's been removed in favor of the admin actions. Thanks, Thejaswi Puthraya. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11639 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/comments/admin.py | 41 ++++- .../templates/comments/moderation_queue.html | 75 -------- django/contrib/comments/urls.py | 1 - django/contrib/comments/views/moderation.py | 163 ++++++------------ .../tests/moderation_view_tests.py | 44 +++-- .../comment_tests/urls_admin.py | 8 + 6 files changed, 126 insertions(+), 206 deletions(-) delete mode 100644 django/contrib/comments/templates/comments/moderation_queue.html create mode 100644 tests/regressiontests/comment_tests/urls_admin.py diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py index c2f8e564f4f..ede833f5301 100644 --- a/django/contrib/comments/admin.py +++ b/django/contrib/comments/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin from django.contrib.comments.models import Comment -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ungettext from django.contrib.comments import get_model +from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete class CommentsAdmin(admin.ModelAdmin): fieldsets = ( @@ -22,6 +23,44 @@ class CommentsAdmin(admin.ModelAdmin): ordering = ('-submit_date',) raw_id_fields = ('user',) search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address') + actions = ["flag_comments", "approve_comments", "remove_comments"] + + def get_actions(self, request): + actions = super(CommentsAdmin, self).get_actions(request) + # Only superusers should be able to delete the comments from the DB. + if not request.user.is_superuser: + actions.pop('delete_selected') + if not request.user.has_perm('comments.can_moderate'): + actions.pop('approve_comments') + actions.pop('remove_comments') + return actions + + def flag_comments(self, request, queryset): + self._bulk_flag(request, queryset, perform_flag, _("flagged")) + flag_comments.short_description = _("Flag selected comments") + + def approve_comments(self, request, queryset): + self._bulk_flag(request, queryset, perform_approve, _('approved')) + approve_comments.short_description = _("Approve selected comments") + + def remove_comments(self, request, queryset): + self._bulk_flag(request, queryset, perform_delete, _('removed')) + remove_comments.short_description = _("Remove selected comments") + + def _bulk_flag(self, request, queryset, action, description): + """ + Flag, approve, or remove some comments from an admin action. Actually + calls the `action` argument to perform the heavy lifting. + """ + n_comments = 0 + for comment in queryset: + action(request, comment) + n_comments += 1 + + msg = ungettext(u'1 comment was successfully %(action)s.', + u'%(count)s comments were successfully %(action)s.', + n_comments) + self.message_user(request, msg % {'count': n_comments, 'action': description}) # Only register the default admin if the model is the built-in comment model # (this won't be true if there's a custom comment app). diff --git a/django/contrib/comments/templates/comments/moderation_queue.html b/django/contrib/comments/templates/comments/moderation_queue.html deleted file mode 100644 index 73012b3539c..00000000000 --- a/django/contrib/comments/templates/comments/moderation_queue.html +++ /dev/null @@ -1,75 +0,0 @@ -{% extends "admin/change_list.html" %} -{% load adminmedia i18n %} - -{% block title %}{% trans "Comment moderation queue" %}{% endblock %} - -{% block extrahead %} - {{ block.super }} - -{% endblock %} - -{% block branding %} -

{% trans "Comment moderation queue" %}

-{% endblock %} - -{% block breadcrumbs %}{% endblock %} - -{% block content %} -{% if empty %} -

{% trans "No comments to moderate" %}.

-{% else %} -
-
- - - - - - - - - - - - - - - {% for comment in comments %} - - - - - - - - - - - {% endfor %} - -
{% trans "Action" %}{% trans "Name" %}{% trans "Comment" %}{% trans "Email" %}{% trans "URL" %}{% trans "Authenticated?" %}{% trans "IP Address" %}{% trans "Date posted" %}
-
- - -
-
- - -
-
{{ comment.name }}{{ comment.comment|truncatewords:"50" }}{{ comment.email }}{{ comment.url }} - {% if comment.user %}{% trans - {{ comment.ip_address }}{{ comment.submit_date|date:"F j, P" }}
-
-
-{% endif %} -{% endblock %} diff --git a/django/contrib/comments/urls.py b/django/contrib/comments/urls.py index 5caef9c7d45..2bfefa3e2da 100644 --- a/django/contrib/comments/urls.py +++ b/django/contrib/comments/urls.py @@ -7,7 +7,6 @@ urlpatterns = patterns('django.contrib.comments.views', url(r'^flagged/$', 'moderation.flag_done', name='comments-flag-done'), url(r'^delete/(\d+)/$', 'moderation.delete', name='comments-delete'), url(r'^deleted/$', 'moderation.delete_done', name='comments-delete-done'), - url(r'^moderate/$', 'moderation.moderation_queue', name='comments-moderation-queue'), url(r'^approve/(\d+)/$', 'moderation.approve', name='comments-approve'), url(r'^approved/$', 'moderation.approve_done', name='comments-approve-done'), ) diff --git a/django/contrib/comments/views/moderation.py b/django/contrib/comments/views/moderation.py index 3334b0927e9..d47fa8b4e71 100644 --- a/django/contrib/comments/views/moderation.py +++ b/django/contrib/comments/views/moderation.py @@ -3,12 +3,10 @@ from django.conf import settings from django.shortcuts import get_object_or_404, render_to_response from django.contrib.auth.decorators import login_required, permission_required from utils import next_redirect, confirmation_view -from django.core.paginator import Paginator, InvalidPage -from django.http import Http404 from django.contrib import comments from django.contrib.comments import signals -#@login_required +@login_required def flag(request, comment_id, next=None): """ Flags a comment. Confirmation on GET, action on POST. @@ -22,18 +20,7 @@ def flag(request, comment_id, next=None): # Flag on POST if request.method == 'POST': - flag, created = comments.models.CommentFlag.objects.get_or_create( - comment = comment, - user = request.user, - flag = comments.models.CommentFlag.SUGGEST_REMOVAL - ) - signals.comment_was_flagged.send( - sender = comment.__class__, - comment = comment, - flag = flag, - created = created, - request = request, - ) + perform_flag(request, comment) return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk) # Render a form on GET @@ -42,9 +29,8 @@ def flag(request, comment_id, next=None): {'comment': comment, "next": next}, template.RequestContext(request) ) -flag = login_required(flag) -#@permission_required("comments.delete_comment") +@permission_required("comments.can_moderate") def delete(request, comment_id, next=None): """ Deletes a comment. Confirmation on GET, action on POST. Requires the "can @@ -60,20 +46,7 @@ def delete(request, comment_id, next=None): # Delete on POST if request.method == 'POST': # Flag the comment as deleted instead of actually deleting it. - flag, created = comments.models.CommentFlag.objects.get_or_create( - comment = comment, - user = request.user, - flag = comments.models.CommentFlag.MODERATOR_DELETION - ) - comment.is_removed = True - comment.save() - signals.comment_was_flagged.send( - sender = comment.__class__, - comment = comment, - flag = flag, - created = created, - request = request, - ) + perform_delete(request, comment) return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk) # Render a form on GET @@ -82,9 +55,8 @@ def delete(request, comment_id, next=None): {'comment': comment, "next": next}, template.RequestContext(request) ) -delete = permission_required("comments.can_moderate")(delete) -#@permission_required("comments.can_moderate") +@permission_required("comments.can_moderate") def approve(request, comment_id, next=None): """ Approve a comment (that is, mark it as public and non-removed). Confirmation @@ -100,23 +72,7 @@ def approve(request, comment_id, next=None): # Delete on POST if request.method == 'POST': # Flag the comment as approved. - flag, created = comments.models.CommentFlag.objects.get_or_create( - comment = comment, - user = request.user, - flag = comments.models.CommentFlag.MODERATOR_APPROVAL, - ) - - comment.is_removed = False - comment.is_public = True - comment.save() - - signals.comment_was_flagged.send( - sender = comment.__class__, - comment = comment, - flag = flag, - created = created, - request = request, - ) + perform_approve(request, comment) return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk) # Render a form on GET @@ -126,69 +82,64 @@ def approve(request, comment_id, next=None): template.RequestContext(request) ) -approve = permission_required("comments.can_moderate")(approve) +# The following functions actually perform the various flag/aprove/delete +# actions. They've been broken out into seperate functions to that they +# may be called from admin actions. - -#@permission_required("comments.can_moderate") -def moderation_queue(request): +def perform_flag(request, comment): """ - Displays a list of unapproved comments to be approved. - - Templates: `comments/moderation_queue.html` - Context: - comments - Comments to be approved (paginated). - empty - Is the comment list empty? - is_paginated - Is there more than one page? - results_per_page - Number of comments per page - has_next - Is there a next page? - has_previous - Is there a previous page? - page - The current page number - next - The next page number - pages - Number of pages - hits - Total number of comments - page_range - Range of page numbers - + Actually perform the flagging of a comment from a request. """ - qs = comments.get_model().objects.filter(is_public=False, is_removed=False) - paginator = Paginator(qs, 100) + flag, created = comments.models.CommentFlag.objects.get_or_create( + comment = comment, + user = request.user, + flag = comments.models.CommentFlag.SUGGEST_REMOVAL + ) + signals.comment_was_flagged.send( + sender = comment.__class__, + comment = comment, + flag = flag, + created = created, + request = request, + ) - try: - page = int(request.GET.get("page", 1)) - except ValueError: - raise Http404 +def perform_delete(request, comment): + flag, created = comments.models.CommentFlag.objects.get_or_create( + comment = comment, + user = request.user, + flag = comments.models.CommentFlag.MODERATOR_DELETION + ) + comment.is_removed = True + comment.save() + signals.comment_was_flagged.send( + sender = comment.__class__, + comment = comment, + flag = flag, + created = created, + request = request, + ) - try: - comments_per_page = paginator.page(page) - except InvalidPage: - raise Http404 - return render_to_response("comments/moderation_queue.html", { - 'comments' : comments_per_page.object_list, - 'empty' : page == 1 and paginator.count == 0, - 'is_paginated': paginator.num_pages > 1, - 'results_per_page': 100, - 'has_next': comments_per_page.has_next(), - 'has_previous': comments_per_page.has_previous(), - 'page': page, - 'next': page + 1, - 'previous': page - 1, - 'pages': paginator.num_pages, - 'hits' : paginator.count, - 'page_range' : paginator.page_range - }, context_instance=template.RequestContext(request)) +def perform_approve(request, comment): + flag, created = comments.models.CommentFlag.objects.get_or_create( + comment = comment, + user = request.user, + flag = comments.models.CommentFlag.MODERATOR_APPROVAL, + ) -moderation_queue = permission_required("comments.can_moderate")(moderation_queue) + comment.is_removed = False + comment.is_public = True + comment.save() + + signals.comment_was_flagged.send( + sender = comment.__class__, + comment = comment, + flag = flag, + created = created, + request = request, + ) + +# Confirmation views. flag_done = confirmation_view( template = "comments/flagged.html", diff --git a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py index b9eadd78b49..61e90bc2202 100644 --- a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py +++ b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py @@ -159,31 +159,29 @@ class ApproveViewTests(CommentTestCase): response = self.client.get("/approved/", data={"c":pk}) self.assertTemplateUsed(response, "comments/approved.html") +class AdminActionsTests(CommentTestCase): + urls = "regressiontests.comment_tests.urls_admin" + + def setUp(self): + super(AdminActionsTests, self).setUp() + + # Make "normaluser" a moderator + u = User.objects.get(username="normaluser") + u.is_staff = True + u.user_permissions.add(Permission.objects.get(codename='add_comment')) + u.user_permissions.add(Permission.objects.get(codename='change_comment')) + u.user_permissions.add(Permission.objects.get(codename='delete_comment')) + u.save() -class ModerationQueueTests(CommentTestCase): - - def testModerationQueuePermissions(self): - """Only moderators can view the moderation queue""" + def testActionsNonModerator(self): + comments = self.createSomeComments() self.client.login(username="normaluser", password="normaluser") - response = self.client.get("/moderate/") - self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/moderate/") + response = self.client.get("/admin/comments/comment/") + self.assertEquals("approve_comments" in response.content, False) - makeModerator("normaluser") - response = self.client.get("/moderate/") - self.assertEqual(response.status_code, 200) - - def testModerationQueueContents(self): - """Moderation queue should display non-public, non-removed comments.""" - c1, c2, c3, c4 = self.createSomeComments() + def testActionsModerator(self): + comments = self.createSomeComments() makeModerator("normaluser") self.client.login(username="normaluser", password="normaluser") - - c1.is_public = c2.is_public = False - c1.save(); c2.save() - response = self.client.get("/moderate/") - self.assertEqual(list(response.context[0]["comments"]), [c1, c2]) - - c2.is_removed = True - c2.save() - response = self.client.get("/moderate/") - self.assertEqual(list(response.context[0]["comments"]), [c1]) + response = self.client.get("/admin/comments/comment/") + self.assertEquals("approve_comments" in response.content, True) diff --git a/tests/regressiontests/comment_tests/urls_admin.py b/tests/regressiontests/comment_tests/urls_admin.py new file mode 100644 index 00000000000..9e43d348651 --- /dev/null +++ b/tests/regressiontests/comment_tests/urls_admin.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import * +from django.contrib import admin + +admin.autodiscover() + +urlpatterns = patterns('', + (r'^admin/', include(admin.site.urls)), +)