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
This commit is contained in:
parent
162fade2b7
commit
8be1bb2268
|
@ -1,7 +1,8 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.comments.models import Comment
|
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 import get_model
|
||||||
|
from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete
|
||||||
|
|
||||||
class CommentsAdmin(admin.ModelAdmin):
|
class CommentsAdmin(admin.ModelAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
|
@ -22,6 +23,44 @@ class CommentsAdmin(admin.ModelAdmin):
|
||||||
ordering = ('-submit_date',)
|
ordering = ('-submit_date',)
|
||||||
raw_id_fields = ('user',)
|
raw_id_fields = ('user',)
|
||||||
search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address')
|
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
|
# 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).
|
# (this won't be true if there's a custom comment app).
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
{% 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 %}
|
|
|
@ -7,7 +7,6 @@ urlpatterns = patterns('django.contrib.comments.views',
|
||||||
url(r'^flagged/$', 'moderation.flag_done', name='comments-flag-done'),
|
url(r'^flagged/$', 'moderation.flag_done', name='comments-flag-done'),
|
||||||
url(r'^delete/(\d+)/$', 'moderation.delete', name='comments-delete'),
|
url(r'^delete/(\d+)/$', 'moderation.delete', name='comments-delete'),
|
||||||
url(r'^deleted/$', 'moderation.delete_done', name='comments-delete-done'),
|
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'^approve/(\d+)/$', 'moderation.approve', name='comments-approve'),
|
||||||
url(r'^approved/$', 'moderation.approve_done', name='comments-approve-done'),
|
url(r'^approved/$', 'moderation.approve_done', name='comments-approve-done'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,12 +3,10 @@ from django.conf import settings
|
||||||
from django.shortcuts import get_object_or_404, render_to_response
|
from django.shortcuts import get_object_or_404, render_to_response
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from utils import next_redirect, confirmation_view
|
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 import comments
|
||||||
from django.contrib.comments import signals
|
from django.contrib.comments import signals
|
||||||
|
|
||||||
#@login_required
|
@login_required
|
||||||
def flag(request, comment_id, next=None):
|
def flag(request, comment_id, next=None):
|
||||||
"""
|
"""
|
||||||
Flags a comment. Confirmation on GET, action on POST.
|
Flags a comment. Confirmation on GET, action on POST.
|
||||||
|
@ -22,18 +20,7 @@ def flag(request, comment_id, next=None):
|
||||||
|
|
||||||
# Flag on POST
|
# Flag on POST
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
flag, created = comments.models.CommentFlag.objects.get_or_create(
|
perform_flag(request, comment)
|
||||||
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,
|
|
||||||
)
|
|
||||||
return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk)
|
return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk)
|
||||||
|
|
||||||
# Render a form on GET
|
# Render a form on GET
|
||||||
|
@ -42,9 +29,8 @@ def flag(request, comment_id, next=None):
|
||||||
{'comment': comment, "next": next},
|
{'comment': comment, "next": next},
|
||||||
template.RequestContext(request)
|
template.RequestContext(request)
|
||||||
)
|
)
|
||||||
flag = login_required(flag)
|
|
||||||
|
|
||||||
#@permission_required("comments.delete_comment")
|
@permission_required("comments.can_moderate")
|
||||||
def delete(request, comment_id, next=None):
|
def delete(request, comment_id, next=None):
|
||||||
"""
|
"""
|
||||||
Deletes a comment. Confirmation on GET, action on POST. Requires the "can
|
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
|
# Delete on POST
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# Flag the comment as deleted instead of actually deleting it.
|
# Flag the comment as deleted instead of actually deleting it.
|
||||||
flag, created = comments.models.CommentFlag.objects.get_or_create(
|
perform_delete(request, comment)
|
||||||
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,
|
|
||||||
)
|
|
||||||
return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk)
|
return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk)
|
||||||
|
|
||||||
# Render a form on GET
|
# Render a form on GET
|
||||||
|
@ -82,9 +55,8 @@ def delete(request, comment_id, next=None):
|
||||||
{'comment': comment, "next": next},
|
{'comment': comment, "next": next},
|
||||||
template.RequestContext(request)
|
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):
|
def approve(request, comment_id, next=None):
|
||||||
"""
|
"""
|
||||||
Approve a comment (that is, mark it as public and non-removed). Confirmation
|
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
|
# Delete on POST
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# Flag the comment as approved.
|
# Flag the comment as approved.
|
||||||
flag, created = comments.models.CommentFlag.objects.get_or_create(
|
perform_approve(request, comment)
|
||||||
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,
|
|
||||||
)
|
|
||||||
return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)
|
return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)
|
||||||
|
|
||||||
# Render a form on GET
|
# Render a form on GET
|
||||||
|
@ -126,69 +82,64 @@ def approve(request, comment_id, next=None):
|
||||||
template.RequestContext(request)
|
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.
|
||||||
|
|
||||||
|
def perform_flag(request, comment):
|
||||||
#@permission_required("comments.can_moderate")
|
|
||||||
def moderation_queue(request):
|
|
||||||
"""
|
"""
|
||||||
Displays a list of unapproved comments to be approved.
|
Actually perform the flagging of a comment from a request.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
qs = comments.get_model().objects.filter(is_public=False, is_removed=False)
|
flag, created = comments.models.CommentFlag.objects.get_or_create(
|
||||||
paginator = Paginator(qs, 100)
|
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:
|
def perform_delete(request, comment):
|
||||||
page = int(request.GET.get("page", 1))
|
flag, created = comments.models.CommentFlag.objects.get_or_create(
|
||||||
except ValueError:
|
comment = comment,
|
||||||
raise Http404
|
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", {
|
def perform_approve(request, comment):
|
||||||
'comments' : comments_per_page.object_list,
|
flag, created = comments.models.CommentFlag.objects.get_or_create(
|
||||||
'empty' : page == 1 and paginator.count == 0,
|
comment = comment,
|
||||||
'is_paginated': paginator.num_pages > 1,
|
user = request.user,
|
||||||
'results_per_page': 100,
|
flag = comments.models.CommentFlag.MODERATOR_APPROVAL,
|
||||||
'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)
|
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(
|
flag_done = confirmation_view(
|
||||||
template = "comments/flagged.html",
|
template = "comments/flagged.html",
|
||||||
|
|
|
@ -159,31 +159,29 @@ class ApproveViewTests(CommentTestCase):
|
||||||
response = self.client.get("/approved/", data={"c":pk})
|
response = self.client.get("/approved/", data={"c":pk})
|
||||||
self.assertTemplateUsed(response, "comments/approved.html")
|
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 testActionsNonModerator(self):
|
||||||
|
comments = self.createSomeComments()
|
||||||
def testModerationQueuePermissions(self):
|
|
||||||
"""Only moderators can view the moderation queue"""
|
|
||||||
self.client.login(username="normaluser", password="normaluser")
|
self.client.login(username="normaluser", password="normaluser")
|
||||||
response = self.client.get("/moderate/")
|
response = self.client.get("/admin/comments/comment/")
|
||||||
self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/moderate/")
|
self.assertEquals("approve_comments" in response.content, False)
|
||||||
|
|
||||||
makeModerator("normaluser")
|
def testActionsModerator(self):
|
||||||
response = self.client.get("/moderate/")
|
comments = self.createSomeComments()
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def testModerationQueueContents(self):
|
|
||||||
"""Moderation queue should display non-public, non-removed comments."""
|
|
||||||
c1, c2, c3, c4 = self.createSomeComments()
|
|
||||||
makeModerator("normaluser")
|
makeModerator("normaluser")
|
||||||
self.client.login(username="normaluser", password="normaluser")
|
self.client.login(username="normaluser", password="normaluser")
|
||||||
|
response = self.client.get("/admin/comments/comment/")
|
||||||
c1.is_public = c2.is_public = False
|
self.assertEquals("approve_comments" in response.content, True)
|
||||||
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])
|
|
||||||
|
|
|
@ -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)),
|
||||||
|
)
|
Loading…
Reference in New Issue