Fixed #11113: fixed a couple of issues that slipped through the cracks when comment moderation was added to `django.contrib.comments`.
The is a potentially backwards-incompatible change for users already relying on the internals of comment moderaration. To wit: * The moderation system now listens to the new `comment_will_be_posted`/`comment_was_posted` signals instead of `pre/post_save`. This means that import request-based information is available to moderation as it should be. * Some experimental code from `django.contrib.comments.moderation` has been removed. It was never intended to be merged into Django, and was completely untested and likely buggy. git-svn-id: http://code.djangoproject.com/svn/django/trunk@10784 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
3da3716252
commit
d246401552
|
@ -2,8 +2,6 @@
|
||||||
A generic comment-moderation system which allows configuration of
|
A generic comment-moderation system which allows configuration of
|
||||||
moderation options on a per-model basis.
|
moderation options on a per-model basis.
|
||||||
|
|
||||||
Originally part of django-comment-utils, by James Bennett.
|
|
||||||
|
|
||||||
To use, do two things:
|
To use, do two things:
|
||||||
|
|
||||||
1. Create or import a subclass of ``CommentModerator`` defining the
|
1. Create or import a subclass of ``CommentModerator`` defining the
|
||||||
|
@ -41,7 +39,7 @@ And finally register it for moderation::
|
||||||
|
|
||||||
moderator.register(Entry, EntryModerator)
|
moderator.register(Entry, EntryModerator)
|
||||||
|
|
||||||
This sample class would apply several moderation steps to each new
|
This sample class would apply two moderation steps to each new
|
||||||
comment submitted on an Entry:
|
comment submitted on an Entry:
|
||||||
|
|
||||||
* If the entry's ``enable_comments`` field is set to ``False``, the
|
* If the entry's ``enable_comments`` field is set to ``False``, the
|
||||||
|
@ -54,19 +52,13 @@ For a full list of built-in moderation options and other
|
||||||
configurability, see the documentation for the ``CommentModerator``
|
configurability, see the documentation for the ``CommentModerator``
|
||||||
class.
|
class.
|
||||||
|
|
||||||
Several example subclasses of ``CommentModerator`` are provided in
|
|
||||||
`django-comment-utils`_, both to provide common moderation options and to
|
|
||||||
demonstrate some of the ways subclasses can customize moderation
|
|
||||||
behavior.
|
|
||||||
|
|
||||||
.. _`django-comment-utils`: http://code.google.com/p/django-comment-utils/
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.db.models import signals
|
from django.contrib.comments import signals
|
||||||
from django.db.models.base import ModelBase
|
from django.db.models.base import ModelBase
|
||||||
from django.template import Context, loader
|
from django.template import Context, loader
|
||||||
from django.contrib import comments
|
from django.contrib import comments
|
||||||
|
@ -145,9 +137,10 @@ class CommentModerator(object):
|
||||||
Most common moderation needs can be covered by changing these
|
Most common moderation needs can be covered by changing these
|
||||||
attributes, but further customization can be obtained by
|
attributes, but further customization can be obtained by
|
||||||
subclassing and overriding the following methods. Each method will
|
subclassing and overriding the following methods. Each method will
|
||||||
be called with two arguments: ``comment``, which is the comment
|
be called with three arguments: ``comment``, which is the comment
|
||||||
being submitted, and ``content_object``, which is the object the
|
being submitted, ``content_object``, which is the object the
|
||||||
comment will be attached to::
|
comment will be attached to, and ``request``, which is the
|
||||||
|
``HttpRequest`` in which the comment is being submitted::
|
||||||
|
|
||||||
``allow``
|
``allow``
|
||||||
Should return ``True`` if the comment should be allowed to
|
Should return ``True`` if the comment should be allowed to
|
||||||
|
@ -200,7 +193,7 @@ class CommentModerator(object):
|
||||||
raise ValueError("Cannot determine moderation rules because date field is set to a value in the future")
|
raise ValueError("Cannot determine moderation rules because date field is set to a value in the future")
|
||||||
return now - then
|
return now - then
|
||||||
|
|
||||||
def allow(self, comment, content_object):
|
def allow(self, comment, content_object, request):
|
||||||
"""
|
"""
|
||||||
Determine whether a given comment is allowed to be posted on
|
Determine whether a given comment is allowed to be posted on
|
||||||
a given object.
|
a given object.
|
||||||
|
@ -217,7 +210,7 @@ class CommentModerator(object):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def moderate(self, comment, content_object):
|
def moderate(self, comment, content_object, request):
|
||||||
"""
|
"""
|
||||||
Determine whether a given comment on a given object should be
|
Determine whether a given comment on a given object should be
|
||||||
allowed to show up immediately, or should be marked non-public
|
allowed to show up immediately, or should be marked non-public
|
||||||
|
@ -232,57 +225,7 @@ class CommentModerator(object):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def comments_open(self, obj):
|
def email(self, comment, content_object, request):
|
||||||
"""
|
|
||||||
Return ``True`` if new comments are being accepted for
|
|
||||||
``obj``, ``False`` otherwise.
|
|
||||||
|
|
||||||
The algorithm for determining this is as follows:
|
|
||||||
|
|
||||||
1. If ``enable_field`` is set and the relevant field on
|
|
||||||
``obj`` contains a false value, comments are not open.
|
|
||||||
|
|
||||||
2. If ``close_after`` is set and the relevant date field on
|
|
||||||
``obj`` is far enough in the past, comments are not open.
|
|
||||||
|
|
||||||
3. If neither of the above checks determined that comments are
|
|
||||||
not open, comments are open.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self.enable_field:
|
|
||||||
if not getattr(obj, self.enable_field):
|
|
||||||
return False
|
|
||||||
if self.auto_close_field and self.close_after:
|
|
||||||
if self._get_delta(datetime.datetime.now(), getattr(obj, self.auto_close_field)).days >= self.close_after:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def comments_moderated(self, obj):
|
|
||||||
"""
|
|
||||||
Return ``True`` if new comments for ``obj`` are being
|
|
||||||
automatically sent to moderation, ``False`` otherwise.
|
|
||||||
|
|
||||||
The algorithm for determining this is as follows:
|
|
||||||
|
|
||||||
1. If ``moderate_field`` is set and the relevant field on
|
|
||||||
``obj`` contains a true value, comments are moderated.
|
|
||||||
|
|
||||||
2. If ``moderate_after`` is set and the relevant date field on
|
|
||||||
``obj`` is far enough in the past, comments are moderated.
|
|
||||||
|
|
||||||
3. If neither of the above checks decided that comments are
|
|
||||||
moderated, comments are not moderated.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self.moderate_field:
|
|
||||||
if getattr(obj, self.moderate_field):
|
|
||||||
return True
|
|
||||||
if self.auto_moderate_field and self.moderate_after:
|
|
||||||
if self._get_delta(datetime.datetime.now(), getattr(obj, self.auto_moderate_field)).days >= self.moderate_after:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def email(self, comment, content_object):
|
|
||||||
"""
|
"""
|
||||||
Send email notification of a new comment to site staff when email
|
Send email notification of a new comment to site staff when email
|
||||||
notifications have been requested.
|
notifications have been requested.
|
||||||
|
@ -341,8 +284,8 @@ class Moderator(object):
|
||||||
from the comment models.
|
from the comment models.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
signals.pre_save.connect(self.pre_save_moderation, sender=comments.get_model())
|
signals.comment_will_be_posted.connect(self.pre_save_moderation, sender=comments.get_model())
|
||||||
signals.post_save.connect(self.post_save_moderation, sender=comments.get_model())
|
signals.comment_was_posted.connect(self.post_save_moderation, sender=comments.get_model())
|
||||||
|
|
||||||
def register(self, model_or_iterable, moderation_class):
|
def register(self, model_or_iterable, moderation_class):
|
||||||
"""
|
"""
|
||||||
|
@ -376,66 +319,35 @@ class Moderator(object):
|
||||||
raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name)
|
raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name)
|
||||||
del self._registry[model]
|
del self._registry[model]
|
||||||
|
|
||||||
def pre_save_moderation(self, sender, instance, **kwargs):
|
def pre_save_moderation(self, sender, comment, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Apply any necessary pre-save moderation steps to new
|
Apply any necessary pre-save moderation steps to new
|
||||||
comments.
|
comments.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
model = instance.content_type.model_class()
|
model = comment.content_type.model_class()
|
||||||
if instance.id or (model not in self._registry):
|
if model not in self._registry:
|
||||||
return
|
return
|
||||||
content_object = instance.content_object
|
content_object = comment.content_object
|
||||||
moderation_class = self._registry[model]
|
moderation_class = self._registry[model]
|
||||||
if not moderation_class.allow(instance, content_object): # Comment will get deleted in post-save hook.
|
|
||||||
instance.moderation_disallowed = True
|
|
||||||
return
|
|
||||||
if moderation_class.moderate(instance, content_object):
|
|
||||||
instance.is_public = False
|
|
||||||
|
|
||||||
def post_save_moderation(self, sender, instance, **kwargs):
|
# Comment will be disallowed outright (HTTP 403 response)
|
||||||
|
if not moderation_class.allow(comment, content_object, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if moderation_class.moderate(comment, content_object, request):
|
||||||
|
comment.is_public = False
|
||||||
|
|
||||||
|
def post_save_moderation(self, sender, comment, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Apply any necessary post-save moderation steps to new
|
Apply any necessary post-save moderation steps to new
|
||||||
comments.
|
comments.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
model = instance.content_type.model_class()
|
model = comment.content_type.model_class()
|
||||||
if model not in self._registry:
|
if model not in self._registry:
|
||||||
return
|
return
|
||||||
if hasattr(instance, 'moderation_disallowed'):
|
self._registry[model].email(comment, comment.content_object, request)
|
||||||
instance.delete()
|
|
||||||
return
|
|
||||||
self._registry[model].email(instance, instance.content_object)
|
|
||||||
|
|
||||||
def comments_open(self, obj):
|
|
||||||
"""
|
|
||||||
Return ``True`` if new comments are being accepted for
|
|
||||||
``obj``, ``False`` otherwise.
|
|
||||||
|
|
||||||
If no moderation rules have been registered for the model of
|
|
||||||
which ``obj`` is an instance, comments are assumed to be open
|
|
||||||
for that object.
|
|
||||||
|
|
||||||
"""
|
|
||||||
model = obj.__class__
|
|
||||||
if model not in self._registry:
|
|
||||||
return True
|
|
||||||
return self._registry[model].comments_open(obj)
|
|
||||||
|
|
||||||
def comments_moderated(self, obj):
|
|
||||||
"""
|
|
||||||
Return ``True`` if new comments for ``obj`` are being
|
|
||||||
automatically sent to moderation, ``False`` otherwise.
|
|
||||||
|
|
||||||
If no moderation rules have been registered for the model of
|
|
||||||
which ``obj`` is an instance, comments for that object are
|
|
||||||
assumed not to be moderated.
|
|
||||||
|
|
||||||
"""
|
|
||||||
model = obj.__class__
|
|
||||||
if model not in self._registry:
|
|
||||||
return False
|
|
||||||
return self._registry[model].comments_moderated(obj)
|
|
||||||
|
|
||||||
# Import this instance in your own code to use in registering
|
# Import this instance in your own code to use in registering
|
||||||
# your models for moderation.
|
# your models for moderation.
|
||||||
|
|
|
@ -12,12 +12,10 @@ but the amount of comment spam circulating on the Web today
|
||||||
essentially makes it necessary to have some sort of automatic
|
essentially makes it necessary to have some sort of automatic
|
||||||
moderation system in place for any application which makes use of
|
moderation system in place for any application which makes use of
|
||||||
comments. To make this easier to handle in a consistent fashion,
|
comments. To make this easier to handle in a consistent fashion,
|
||||||
``django.contrib.comments.moderation`` (based on `comment_utils`_)
|
``django.contrib.comments.moderation`` provides a generic, extensible
|
||||||
provides a generic, extensible comment-moderation system which can
|
comment-moderation system which can be applied to any model or set of
|
||||||
be applied to any model or set of models which want to make use of
|
models which want to make use of Django's comment system.
|
||||||
Django's comment system.
|
|
||||||
|
|
||||||
.. _`comment_utils`: http://code.google.com/p/django-comment-utils/
|
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
========
|
========
|
||||||
|
@ -140,29 +138,28 @@ Adding custom moderation methods
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
For situations where the built-in options listed above are not
|
For situations where the built-in options listed above are not
|
||||||
sufficient, subclasses of
|
sufficient, subclasses of :class:`CommentModerator` can also override
|
||||||
:class:`CommentModerator` can also
|
the methods which actually perform the moderation, and apply any logic
|
||||||
override the methods which actually perform the moderation, and apply any
|
they desire. :class:`CommentModerator` defines three methods which
|
||||||
logic they desire.
|
determine how moderation will take place; each method will be called
|
||||||
:class:`CommentModerator` defines three
|
by the moderation system and passed two arguments: ``comment``, which
|
||||||
methods which determine how moderation will take place; each method will be
|
is the new comment being posted, ``content_object``, which is the
|
||||||
called by the moderation system and passed two arguments: ``comment``, which
|
object the comment will be attached to, and ``request``, which is the
|
||||||
is the new comment being posted, and ``content_object``, which is the
|
``HttpRequest`` in which the comment is being submitted:
|
||||||
object the comment will be attached to:
|
|
||||||
|
|
||||||
.. method:: CommentModerator.allow(comment, content_object)
|
.. method:: CommentModerator.allow(comment, content_object, request)
|
||||||
|
|
||||||
Should return ``True`` if the comment should be allowed to
|
Should return ``True`` if the comment should be allowed to
|
||||||
post on the content object, and ``False`` otherwise (in which
|
post on the content object, and ``False`` otherwise (in which
|
||||||
case the comment will be immediately deleted).
|
case the comment will be immediately deleted).
|
||||||
|
|
||||||
.. method:: CommentModerator.email(comment, content_object)
|
.. method:: CommentModerator.email(comment, content_object, request)
|
||||||
|
|
||||||
If email notification of the new comment should be sent to
|
If email notification of the new comment should be sent to
|
||||||
site staff or moderators, this method is responsible for
|
site staff or moderators, this method is responsible for
|
||||||
sending the email.
|
sending the email.
|
||||||
|
|
||||||
.. method:: CommentModerator.moderate(comment, content_object)
|
.. method:: CommentModerator.moderate(comment, content_object, request)
|
||||||
|
|
||||||
Should return ``True`` if the comment should be moderated (in
|
Should return ``True`` if the comment should be moderated (in
|
||||||
which case its ``is_public`` field will be set to ``False``
|
which case its ``is_public`` field will be set to ``False``
|
||||||
|
@ -217,18 +214,18 @@ models with an instance of the subclass.
|
||||||
Determines how moderation is set up globally. The base
|
Determines how moderation is set up globally. The base
|
||||||
implementation in
|
implementation in
|
||||||
:class:`Moderator` does this by
|
:class:`Moderator` does this by
|
||||||
attaching listeners to the :data:`~django.db.models.signals.pre_save`
|
attaching listeners to the :data:`~django.contrib.comments.signals.comment_will_be_posted`
|
||||||
and :data:`~django.db.models.signals.post_save` signals from the
|
and :data:`~django.contrib.comments.signals.comment_was_posted` signals from the
|
||||||
comment models.
|
comment models.
|
||||||
|
|
||||||
.. method:: pre_save_moderation(sender, instance, **kwargs)
|
.. method:: pre_save_moderation(sender, comment, request, **kwargs)
|
||||||
|
|
||||||
In the base implementation, applies all pre-save moderation
|
In the base implementation, applies all pre-save moderation
|
||||||
steps (such as determining whether the comment needs to be
|
steps (such as determining whether the comment needs to be
|
||||||
deleted, or whether it needs to be marked as non-public or
|
deleted, or whether it needs to be marked as non-public or
|
||||||
generate an email).
|
generate an email).
|
||||||
|
|
||||||
.. method:: post_save_moderation(sender, instance, **kwargs)
|
.. method:: post_save_moderation(sender, comment, request, **kwargs)
|
||||||
|
|
||||||
In the base implementation, applies all post-save moderation
|
In the base implementation, applies all post-save moderation
|
||||||
steps (currently this consists entirely of deleting comments
|
steps (currently this consists entirely of deleting comments
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from regressiontests.comment_tests.tests import CommentTestCase, CT, Site
|
from regressiontests.comment_tests.tests import CommentTestCase, CT, Site
|
||||||
|
from django.contrib.comments.forms import CommentForm
|
||||||
from django.contrib.comments.models import Comment
|
from django.contrib.comments.models import Comment
|
||||||
from django.contrib.comments.moderation import moderator, CommentModerator, AlreadyModerated
|
from django.contrib.comments.moderation import moderator, CommentModerator, AlreadyModerated
|
||||||
from regressiontests.comment_tests.models import Entry
|
from regressiontests.comment_tests.models import Entry
|
||||||
|
@ -22,24 +23,26 @@ class CommentUtilsModeratorTests(CommentTestCase):
|
||||||
fixtures = ["comment_utils.xml"]
|
fixtures = ["comment_utils.xml"]
|
||||||
|
|
||||||
def createSomeComments(self):
|
def createSomeComments(self):
|
||||||
c1 = Comment.objects.create(
|
# Tests for the moderation signals must actually post data
|
||||||
content_type = CT(Entry),
|
# through the comment views, because only the comment views
|
||||||
object_pk = "1",
|
# emit the custom signals moderation listens for.
|
||||||
user_name = "Joe Somebody",
|
e = Entry.objects.get(pk=1)
|
||||||
user_email = "jsomebody@example.com",
|
data = self.getValidData(e)
|
||||||
user_url = "http://example.com/~joe/",
|
self.client.post("/post/", data, REMOTE_ADDR="1.2.3.4")
|
||||||
comment = "First!",
|
self.client.post("/post/", data, REMOTE_ADDR="1.2.3.4")
|
||||||
site = Site.objects.get_current(),
|
|
||||||
)
|
# We explicitly do a try/except to get the comment we've just
|
||||||
c2 = Comment.objects.create(
|
# posted because moderation may have disallowed it, in which
|
||||||
content_type = CT(Entry),
|
# case we can just return it as None.
|
||||||
object_pk = "2",
|
try:
|
||||||
user_name = "Joe the Plumber",
|
c1 = Comment.objects.all()[0]
|
||||||
user_email = "joetheplumber@whitehouse.gov",
|
except IndexError:
|
||||||
user_url = "http://example.com/~joe/",
|
c1 = None
|
||||||
comment = "Second!",
|
|
||||||
site = Site.objects.get_current(),
|
try:
|
||||||
)
|
c2 = Comment.objects.all()[0]
|
||||||
|
except IndexError:
|
||||||
|
c2 = None
|
||||||
return c1, c2
|
return c1, c2
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
@ -51,17 +54,17 @@ class CommentUtilsModeratorTests(CommentTestCase):
|
||||||
|
|
||||||
def testEmailNotification(self):
|
def testEmailNotification(self):
|
||||||
moderator.register(Entry, EntryModerator1)
|
moderator.register(Entry, EntryModerator1)
|
||||||
c1, c2 = self.createSomeComments()
|
self.createSomeComments()
|
||||||
self.assertEquals(len(mail.outbox), 2)
|
self.assertEquals(len(mail.outbox), 2)
|
||||||
|
|
||||||
def testCommentsEnabled(self):
|
def testCommentsEnabled(self):
|
||||||
moderator.register(Entry, EntryModerator2)
|
moderator.register(Entry, EntryModerator2)
|
||||||
c1, c2 = self.createSomeComments()
|
self.createSomeComments()
|
||||||
self.assertEquals(Comment.objects.all().count(), 1)
|
self.assertEquals(Comment.objects.all().count(), 1)
|
||||||
|
|
||||||
def testAutoCloseField(self):
|
def testAutoCloseField(self):
|
||||||
moderator.register(Entry, EntryModerator3)
|
moderator.register(Entry, EntryModerator3)
|
||||||
c1, c2 = self.createSomeComments()
|
self.createSomeComments()
|
||||||
self.assertEquals(Comment.objects.all().count(), 0)
|
self.assertEquals(Comment.objects.all().count(), 0)
|
||||||
|
|
||||||
def testAutoModerateField(self):
|
def testAutoModerateField(self):
|
||||||
|
|
Loading…
Reference in New Issue