diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py index c2f8e564f4f51..ede833f53014b 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 73012b3539cbb..0000000000000 --- 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 5caef9c7d45a9..2bfefa3e2da97 100644 --- a/django/contrib/comments/urls.py +++ b/django/contrib/comments/urls.py @@ -7,7 +7,6 @@ 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 3334b0927e9a3..d47fa8b4e713a 100644 --- a/django/contrib/comments/views/moderation.py +++ b/django/contrib/comments/views/moderation.py @@ -3,12 +3,10 @@ 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) - - try: - page = int(request.GET.get("page", 1)) - except ValueError: - raise Http404 - - 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)) - -moderation_queue = permission_required("comments.can_moderate")(moderation_queue) + 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, + ) + +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, + ) + + +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, + ) + + 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 b9eadd78b492c..61e90bc22025b 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 @@ def testApprovedView(self): response = self.client.get("/approved/", data={"c":pk}) self.assertTemplateUsed(response, "comments/approved.html") - -class ModerationQueueTests(CommentTestCase): - - def testModerationQueuePermissions(self): - """Only moderators can view the moderation queue""" +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() + + 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/") - - makeModerator("normaluser") - response = self.client.get("/moderate/") - self.assertEqual(response.status_code, 200) + response = self.client.get("/admin/comments/comment/") + self.assertEquals("approve_comments" in response.content, False) - 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 0000000000000..9e43d348651e5 --- /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)), +)