Commit 8be1bb22 authored by Jacob Kaplan-Moss's avatar Jacob Kaplan-Moss
Browse files

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
parent 162fade2
Loading
Loading
Loading
Loading
+40 −1
Original line number Diff line number Diff line
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).
+0 −75
Original line number Diff line number Diff line
{% extends "admin/change_list.html" %}
{% load adminmedia i18n %}

{% block title %}{% trans "Comment moderation queue" %}{% endblock %}

{% block extrahead %}
  {{ block.super }}
  <style type="text/css" media="screen">
    p#nocomments { font-size: 200%; text-align: center; border: 1px #ccc dashed; padding: 4em; }
    td.actions { width: 11em; }
    td.actions form { display: inline; }
    td.actions form input.submit { width: 5em; padding: 2px 4px; margin-right: 4px;}
    td.actions form input.approve { background: green; color: white; }
    td.actions form input.remove { background: red; color: white; }
  </style>
{% endblock %}

{% block branding %}
<h1 id="site-name">{% trans "Comment moderation queue" %}</h1>
{% endblock %}

{% block breadcrumbs %}{% endblock %}

{% block content %}
{% if empty %}
<p id="nocomments">{% trans "No comments to moderate" %}.</p>
{% else %}
<div id="content-main">
  <div class="module" id="changelist">
    <table cellspacing="0">
      <thead>
        <tr>
          <th>{% trans "Action" %}</th>
          <th>{% trans "Name" %}</th>
          <th>{% trans "Comment" %}</th>
          <th>{% trans "Email" %}</th>
          <th>{% trans "URL" %}</th>
          <th>{% trans "Authenticated?" %}</th>
          <th>{% trans "IP Address" %}</th>
          <th class="sorted desc">{% trans "Date posted" %}</th>
        </tr>
    </thead>
    <tbody>
      {% for comment in comments %}
        <tr class="{% cycle 'row1' 'row2' %}">
          <td class="actions">
            <form action="{% url comments-approve comment.pk %}" method="post">
              <input type="hidden" name="next" value="{% url comments-moderation-queue %}" />
              <input class="approve submit" type="submit" name="submit" value="{% trans "Approve" %}" />
            </form>
            <form action="{% url comments-delete comment.pk %}" method="post">
              <input type="hidden" name="next" value="{% url comments-moderation-queue %}" />
              <input class="remove submit" type="submit" name="submit" value="{% trans "Remove" %}" />
            </form>
          </td>
          <td>{{ comment.name }}</td>
          <td>{{ comment.comment|truncatewords:"50" }}</td>
          <td>{{ comment.email }}</td>
          <td>{{ comment.url }}</td>
          <td>
            <img
              src="{% admin_media_prefix %}img/admin/icon-{% if comment.user %}yes{% else %}no{% endif %}.gif"
              alt="{% if comment.user %}{% trans "yes" %}{% else %}{% trans "no" %}{% endif %}"
            />
          </td>
          <td>{{ comment.ip_address }}</td>
          <td>{{ comment.submit_date|date:"F j, P" }}</td>
        </tr>
      {% endfor %}
    </tbody>
    </table>
  </div>
</div>
{% endif %}
{% endblock %}
+0 −1
Original line number Diff line number Diff line
@@ -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'),
)
+61 −110
Original line number Diff line number Diff line
@@ -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,16 +72,29 @@ def approve(request, comment_id, next=None):
    # Delete on POST
    if request.method == 'POST':
        # Flag the comment as approved.
        perform_approve(request, comment)
        return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)

    # Render a form on GET
    else:
        return render_to_response('comments/approve.html',
            {'comment': comment, "next": next},
            template.RequestContext(request)
        )

# 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.

def perform_flag(request, comment):
    """
    Actually perform the flagging of a comment from a request.
    """
    flag, created = comments.models.CommentFlag.objects.get_or_create(
        comment = comment,
        user    = request.user,
            flag    = comments.models.CommentFlag.MODERATOR_APPROVAL,
        flag    = comments.models.CommentFlag.SUGGEST_REMOVAL
    )

        comment.is_removed = False
        comment.is_public = True
        comment.save()

    signals.comment_was_flagged.send(
        sender  = comment.__class__,
        comment = comment,
@@ -117,78 +102,44 @@ def approve(request, comment_id, next=None):
        created = created,
        request = request,
    )
        return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)

    # Render a form on GET
    else:
        return render_to_response('comments/approve.html',
            {'comment': comment, "next": next},
            template.RequestContext(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,
    )

approve = permission_required("comments.can_moderate")(approve)

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,
    )

#@permission_required("comments.can_moderate")
def moderation_queue(request):
    """
    Displays a list of unapproved comments to be approved.
    comment.is_removed = False
    comment.is_public = True
    comment.save()

    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
    signals.comment_was_flagged.send(
        sender  = comment.__class__,
        comment = comment,
        flag    = flag,
        created = created,
        request = 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)
# Confirmation views.

flag_done = confirmation_view(
    template = "comments/flagged.html",
+22 −24
Original line number Diff line number Diff line
@@ -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"
    
class ModerationQueueTests(CommentTestCase):
    def setUp(self):
        super(AdminActionsTests, self).setUp()
        
    def testModerationQueuePermissions(self):
        """Only moderators can view the moderation queue"""
        self.client.login(username="normaluser", password="normaluser")
        response = self.client.get("/moderate/")
        self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/moderate/")
        # 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()

        makeModerator("normaluser")
        response = self.client.get("/moderate/")
        self.assertEqual(response.status_code, 200)
    def testActionsNonModerator(self):
        comments = self.createSomeComments()
        self.client.login(username="normaluser", password="normaluser")
        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)
Loading