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

Fixed #11113: fixed a couple of issues that slipped through the cracks when...

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
parent 3da37162
Loading
Loading
Loading
Loading
+25 −113
Original line number Diff line number Diff line
@@ -2,8 +2,6 @@
A generic comment-moderation system which allows configuration of
moderation options on a per-model basis.

Originally part of django-comment-utils, by James Bennett.

To use, do two things:

1. Create or import a subclass of ``CommentModerator`` defining the
@@ -41,7 +39,7 @@ And finally register it for moderation::

    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:

* 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``
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

from django.conf import settings
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.template import Context, loader
from django.contrib import comments
@@ -145,9 +137,10 @@ class CommentModerator(object):
    Most common moderation needs can be covered by changing these
    attributes, but further customization can be obtained by
    subclassing and overriding the following methods. Each method will
    be called with two arguments: ``comment``, which is the comment
    being submitted, and ``content_object``, which is the object the
    comment will be attached to::
    be called with three arguments: ``comment``, which is the comment
    being submitted, ``content_object``, which is the object the
    comment will be attached to, and ``request``, which is the
    ``HttpRequest`` in which the comment is being submitted::

    ``allow``
        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")
        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
        a given object.
@@ -217,7 +210,7 @@ class CommentModerator(object):
                return False
        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
        allowed to show up immediately, or should be marked non-public
@@ -232,57 +225,7 @@ class CommentModerator(object):
                return True
        return False

    def comments_open(self, obj):
        """
        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):
    def email(self, comment, content_object, request):
        """
        Send email notification of a new comment to site staff when email
        notifications have been requested.
@@ -341,8 +284,8 @@ class Moderator(object):
        from the comment models.

        """
        signals.pre_save.connect(self.pre_save_moderation, sender=comments.get_model())
        signals.post_save.connect(self.post_save_moderation, sender=comments.get_model())
        signals.comment_will_be_posted.connect(self.pre_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):
        """
@@ -376,66 +319,35 @@ class Moderator(object):
                raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name)
            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
        comments.

        """
        model = instance.content_type.model_class()
        if instance.id or (model not in self._registry):
        model = comment.content_type.model_class()
        if model not in self._registry:
            return
        content_object = instance.content_object
        content_object = comment.content_object
        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
        comments.

        """
        model = instance.content_type.model_class()
        model = comment.content_type.model_class()
        if model not in self._registry:
            return
        if hasattr(instance, 'moderation_disallowed'):
            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)
        self._registry[model].email(comment, comment.content_object, request)

# Import this instance in your own code to use in registering
# your models for moderation.
+19 −22
Original line number Diff line number Diff line
@@ -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
moderation system in place for any application which makes use of
comments. To make this easier to handle in a consistent fashion,
``django.contrib.comments.moderation`` (based on `comment_utils`_)
provides a generic, extensible comment-moderation system which can
be applied to any model or set of models which want to make use of
Django's comment system.
``django.contrib.comments.moderation`` provides a generic, extensible
comment-moderation system which can be applied to any model or set of
models which want to make use of Django's comment system.

.. _`comment_utils`: http://code.google.com/p/django-comment-utils/

Overview
========
@@ -140,29 +138,28 @@ Adding custom moderation methods
--------------------------------

For situations where the built-in options listed above are not
sufficient, subclasses of
:class:`CommentModerator` can also
override the methods which actually perform the moderation, and apply any
logic they desire.
:class:`CommentModerator` defines three
methods which determine how moderation will take place; each method will be
called by the moderation system and passed two arguments: ``comment``, which
is the new comment being posted, and ``content_object``, which is the
object the comment will be attached to:

.. method:: CommentModerator.allow(comment, content_object)
sufficient, subclasses of :class:`CommentModerator` can also override
the methods which actually perform the moderation, and apply any logic
they desire.  :class:`CommentModerator` defines three methods which
determine how moderation will take place; each method will be called
by the moderation system and passed two arguments: ``comment``, which
is the new comment being posted, ``content_object``, which is the
object the comment will be attached to, and ``request``, which is the
``HttpRequest`` in which the comment is being submitted:

.. method:: CommentModerator.allow(comment, content_object, request)

    Should return ``True`` if the comment should be allowed to
    post on the content object, and ``False`` otherwise (in which
    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
    site staff or moderators, this method is responsible for
    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
    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
        implementation in
        :class:`Moderator` does this by
        attaching listeners to the :data:`~django.db.models.signals.pre_save`
        and :data:`~django.db.models.signals.post_save` signals from the
        attaching listeners to the :data:`~django.contrib.comments.signals.comment_will_be_posted`
        and :data:`~django.contrib.comments.signals.comment_was_posted` signals from the
        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
        steps (such as determining whether the comment needs to be
        deleted, or whether it needs to be marked as non-public or
        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
        steps (currently this consists entirely of deleting comments
+24 −21
Original line number Diff line number Diff line
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.moderation import moderator, CommentModerator, AlreadyModerated
from regressiontests.comment_tests.models import Entry
@@ -22,24 +23,26 @@ class CommentUtilsModeratorTests(CommentTestCase):
    fixtures = ["comment_utils.xml"]

    def createSomeComments(self):
        c1 = Comment.objects.create(
            content_type = CT(Entry),
            object_pk = "1",
            user_name = "Joe Somebody",
            user_email = "jsomebody@example.com",
            user_url = "http://example.com/~joe/",
            comment = "First!",
            site = Site.objects.get_current(),
        )
        c2 = Comment.objects.create(
            content_type = CT(Entry),
            object_pk = "2",
            user_name = "Joe the Plumber",
            user_email = "joetheplumber@whitehouse.gov",
            user_url = "http://example.com/~joe/",
            comment = "Second!",
            site = Site.objects.get_current(),
        )
        # Tests for the moderation signals must actually post data
        # through the comment views, because only the comment views
        # emit the custom signals moderation listens for.
        e = Entry.objects.get(pk=1)
        data = self.getValidData(e)
        self.client.post("/post/", data, REMOTE_ADDR="1.2.3.4")
        self.client.post("/post/", data, REMOTE_ADDR="1.2.3.4")

        # We explicitly do a try/except to get the comment we've just
        # posted because moderation may have disallowed it, in which
        # case we can just return it as None.
        try:
            c1 = Comment.objects.all()[0]
        except IndexError:
            c1 = None

        try:
            c2 = Comment.objects.all()[0]
        except IndexError:
            c2 = None
        return c1, c2

    def tearDown(self):
@@ -51,17 +54,17 @@ class CommentUtilsModeratorTests(CommentTestCase):

    def testEmailNotification(self):
        moderator.register(Entry, EntryModerator1)
        c1, c2 = self.createSomeComments()
        self.createSomeComments()
        self.assertEquals(len(mail.outbox), 2)

    def testCommentsEnabled(self):
        moderator.register(Entry, EntryModerator2)
        c1, c2 = self.createSomeComments()
        self.createSomeComments()
        self.assertEquals(Comment.objects.all().count(), 1)

    def testAutoCloseField(self):
        moderator.register(Entry, EntryModerator3)
        c1, c2 = self.createSomeComments()
        self.createSomeComments()
        self.assertEquals(Comment.objects.all().count(), 0)

    def testAutoModerateField(self):