Ticket #9282: comment-utils.r10029.diff

File comment-utils.r10029.diff, 34.7 KB (added by Jannis Leidel, 15 years ago)

Updated patch with sphinxified documentation

  • new file django/contrib/comments/moderators.py

    diff --git a/django/contrib/comments/moderators.py b/django/contrib/comments/moderators.py
    new file mode 100644
    index 0000000..d9e7d98
    - +  
     1"""
     2Copyright (c) 2007, James Bennett
     3All rights reserved.
     4
     5Redistribution and use in source and binary forms, with or without
     6modification, are permitted provided that the following conditions are
     7met:
     8
     9    * Redistributions of source code must retain the above copyright
     10      notice, this list of conditions and the following disclaimer.
     11    * Redistributions in binary form must reproduce the above
     12      copyright notice, this list of conditions and the following
     13      disclaimer in the documentation and/or other materials provided
     14      with the distribution.
     15    * Neither the name of the author nor the names of other
     16      contributors may be used to endorse or promote products derived
     17      from this software without specific prior written permission.
     18
     19THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     20"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     21LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     22A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     23OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     24SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     25LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     26DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     27THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     28(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     29OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     30"""
     31
     32"""
     33A generic comment-moderation system which allows configuration of
     34moderation options on a per-model basis.
     35
     36To use, do two things:
     37
     381. Create or import a subclass of ``CommentModerator`` defining the
     39   options you want.
     40
     412. Import ``moderator`` from this module and register one or more
     42   models, passing the models and the ``CommentModerator`` options
     43   class you want to use.
     44
     45
     46Example
     47-------
     48
     49First, we define a simple model class which might represent entries in
     50a weblog::
     51
     52    from django.db import models
     53
     54    class Entry(models.Model):
     55        title = models.CharField(maxlength=250)
     56        body = models.TextField()
     57        pub_date = models.DateField()
     58        enable_comments = models.BooleanField()
     59
     60Then we create a ``CommentModerator`` subclass specifying some
     61moderation options::
     62
     63    from django.contrib.comments.moderators import CommentModerator, moderator
     64
     65    class EntryModerator(CommentModerator):
     66        email_notification = True
     67        enable_field = 'enable_comments'
     68
     69And finally register it for moderation::
     70
     71    moderator.register(Entry, EntryModerator)
     72
     73This sample class would apply several moderation steps to each new
     74comment submitted on an Entry:
     75
     76* If the entry's ``enable_comments`` field is set to ``False``, the
     77  comment will be rejected (immediately deleted).
     78
     79* If the comment is successfully posted, an email notification of the
     80  comment will be sent to site staff.
     81
     82For a full list of built-in moderation options and other
     83configurability, see the documentation for the ``CommentModerator``
     84class.
     85
     86Several example subclasses of ``CommentModerator`` are provided in
     87`django-comment-utils`_, both to provide common moderation options and to
     88demonstrate some of the ways subclasses can customize moderation
     89behavior.
     90
     91.. _`django-comment-utils`: http://code.google.com/p/django-comment-utils/
     92"""
     93
     94
     95import datetime
     96
     97from django.conf import settings
     98from django.core.mail import send_mail
     99from django.db.models import signals
     100from django.db.models.base import ModelBase
     101from django.template import Context, loader
     102from django.contrib import comments
     103from django.contrib.sites.models import Site
     104
     105
     106class AlreadyModerated(Exception):
     107    """
     108    Raised when a model which is already registered for moderation is
     109    attempting to be registered again.
     110
     111    """
     112    pass
     113
     114
     115class NotModerated(Exception):
     116    """
     117    Raised when a model which is not registered for moderation is
     118    attempting to be unregistered.
     119
     120    """
     121    pass
     122
     123
     124class CommentModerator(object):
     125    """
     126    Encapsulates comment-moderation options for a given model.
     127
     128    This class is not designed to be used directly, since it doesn't
     129    enable any of the available moderation options. Instead, subclass
     130    it and override attributes to enable different options::
     131
     132    ``auto_close_field``
     133        If this is set to the name of a ``DateField`` or
     134        ``DateTimeField`` on the model for which comments are
     135        being moderated, new comments for objects of that model
     136        will be disallowed (immediately deleted) when a certain
     137        number of days have passed after the date specified in
     138        that field. Must be used in conjunction with
     139        ``close_after``, which specifies the number of days past
     140        which comments should be disallowed. Default value is
     141        ``None``.
     142
     143    ``auto_moderate_field``
     144        Like ``auto_close_field``, but instead of outright
     145        deleting new comments when the requisite number of days
     146        have elapsed, it will simply set the ``is_public`` field
     147        of new comments to ``False`` before saving them. Must be
     148        used in conjunction with ``moderate_after``, which
     149        specifies the number of days past which comments should be
     150        moderated. Default value is ``None``.
     151
     152    ``close_after``
     153        If ``auto_close_field`` is used, this must specify the
     154        number of days past the value of the field specified by
     155        ``auto_close_field`` after which new comments for an
     156        object should be disallowed. Default value is ``None``.
     157
     158    ``email_notification``
     159        If ``True``, any new comment on an object of this model
     160        which survives moderation will generate an email to site
     161        staff. Default value is ``False``.
     162
     163    ``enable_field``
     164        If this is set to the name of a ``BooleanField`` on the
     165        model for which comments are being moderated, new comments
     166        on objects of that model will be disallowed (immediately
     167        deleted) whenever the value of that field is ``False`` on
     168        the object the comment would be attached to. Default value
     169        is ``None``.
     170
     171    ``moderate_after``
     172        If ``auto_moderate_field`` is used, this must specify the number
     173        of days past the value of the field specified by
     174        ``auto_moderate_field`` after which new comments for an
     175        object should be marked non-public. Default value is
     176        ``None``.
     177
     178    Most common moderation needs can be covered by changing these
     179    attributes, but further customization can be obtained by
     180    subclassing and overriding the following methods. Each method will
     181    be called with two arguments: ``comment``, which is the comment
     182    being submitted, and ``content_object``, which is the object the
     183    comment will be attached to::
     184
     185    ``allow``
     186        Should return ``True`` if the comment should be allowed to
     187        post on the content object, and ``False`` otherwise (in
     188        which case the comment will be immediately deleted).
     189
     190    ``email``
     191        If email notification of the new comment should be sent to
     192        site staff or moderators, this method is responsible for
     193        sending the email.
     194
     195    ``moderate``
     196        Should return ``True`` if the comment should be moderated
     197        (in which case its ``is_public`` field will be set to
     198        ``False`` before saving), and ``False`` otherwise (in
     199        which case the ``is_public`` field will not be changed).
     200
     201    Subclasses which want to introspect the model for which comments
     202    are being moderated can do so through the attribute ``_model``,
     203    which will be the model class.
     204
     205    """
     206    auto_close_field = None
     207    auto_moderate_field = None
     208    close_after = None
     209    email_notification = False
     210    enable_field = None
     211    moderate_after = None
     212
     213    def __init__(self, model):
     214        self._model = model
     215
     216    def _get_delta(self, now, then):
     217        """
     218        Internal helper which will return a ``datetime.timedelta``
     219        representing the time between ``now`` and ``then``. Assumes
     220        ``now`` is a ``datetime.date`` or ``datetime.datetime`` later
     221        than ``then``.
     222
     223        If ``now`` and ``then`` are not of the same type due to one of
     224        them being a ``datetime.date`` and the other being a
     225        ``datetime.datetime``, both will be coerced to
     226        ``datetime.date`` before calculating the delta.
     227
     228        """
     229        if now.__class__ is not then.__class__:
     230            now = datetime.date(now.year, now.month, now.day)
     231            then = datetime.date(then.year, then.month, then.day)
     232        if now < then:
     233            raise ValueError("Cannot determine moderation rules because date field is set to a value in the future")
     234        return now - then
     235
     236    def allow(self, comment, content_object):
     237        """
     238        Determine whether a given comment is allowed to be posted on
     239        a given object.
     240
     241        Return ``True`` if the comment should be allowed, ``False
     242        otherwise.
     243
     244        """
     245        if self.enable_field:
     246            if not getattr(content_object, self.enable_field):
     247                return False
     248        if self.auto_close_field and self.close_after:
     249            if self._get_delta(datetime.datetime.now(), getattr(content_object, self.auto_close_field)).days >= self.close_after:
     250                return False
     251        return True
     252
     253    def moderate(self, comment, content_object):
     254        """
     255        Determine whether a given comment on a given object should be
     256        allowed to show up immediately, or should be marked non-public
     257        and await approval.
     258
     259        Return ``True`` if the comment should be moderated (marked
     260        non-public), ``False`` otherwise.
     261
     262        """
     263        if self.auto_moderate_field and self.moderate_after:
     264            if self._get_delta(datetime.datetime.now(), getattr(content_object, self.auto_moderate_field)).days >= self.moderate_after:
     265                return True
     266        return False
     267
     268    def comments_open(self, obj):
     269        """
     270        Return ``True`` if new comments are being accepted for
     271        ``obj``, ``False`` otherwise.
     272
     273        The algorithm for determining this is as follows:
     274
     275        1. If ``enable_field`` is set and the relevant field on
     276           ``obj`` contains a false value, comments are not open.
     277
     278        2. If ``close_after`` is set and the relevant date field on
     279           ``obj`` is far enough in the past, comments are not open.
     280
     281        3. If neither of the above checks determined that comments are
     282           not open, comments are open.
     283
     284        """
     285        if self.enable_field:
     286            if not getattr(obj, self.enable_field):
     287                return False
     288        if self.auto_close_field and self.close_after:
     289            if self._get_delta(datetime.datetime.now(), getattr(obj, self.auto_close_field)).days >= self.close_after:
     290                return False
     291        return True
     292
     293    def comments_moderated(self, obj):
     294        """
     295        Return ``True`` if new comments for ``obj`` are being
     296        automatically sent to moderation, ``False`` otherwise.
     297
     298        The algorithm for determining this is as follows:
     299
     300        1. If ``moderate_field`` is set and the relevant field on
     301           ``obj`` contains a true value, comments are moderated.
     302
     303        2. If ``moderate_after`` is set and the relevant date field on
     304           ``obj`` is far enough in the past, comments are moderated.
     305
     306        3. If neither of the above checks decided that comments are
     307           moderated, comments are not moderated.
     308
     309        """
     310        if self.moderate_field:
     311            if getattr(obj, self.moderate_field):
     312                return True
     313        if self.auto_moderate_field and self.moderate_after:
     314            if self._get_delta(datetime.datetime.now(), getattr(obj, self.auto_moderate_field)).days >= self.moderate_after:
     315                return True
     316        return False
     317
     318    def email(self, comment, content_object):
     319        """
     320        Send email notification of a new comment to site staff when email
     321        notifications have been requested.
     322
     323        """
     324        if not self.email_notification:
     325            return
     326        recipient_list = [manager_tuple[1] for manager_tuple in settings.MANAGERS]
     327        t = loader.get_template('comments/comment_notification_email.txt')
     328        c = Context({ 'comment': comment,
     329                      'content_object': content_object })
     330        subject = '[%s] New comment posted on "%s"' % (Site.objects.get_current().name,
     331                                                          content_object)
     332        message = t.render(c)
     333        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True)
     334
     335
     336class Moderator(object):
     337    """
     338    Handles moderation of a set of models.
     339
     340    An instance of this class will maintain a list of one or more
     341    models registered for comment moderation, and their associated
     342    moderation classes, and apply moderation to all incoming comments.
     343
     344    To register a model, obtain an instance of ``CommentModerator``
     345    (this module exports one as ``moderator``), and call its
     346    ``register`` method, passing the model class and a moderation
     347    class (which should be a subclass of ``CommentModerator``). Note
     348    that both of these should be the actual classes, not instances of
     349    the classes.
     350
     351    To cease moderation for a model, call the ``unregister`` method,
     352    passing the model class.
     353
     354    For convenience, both ``register`` and ``unregister`` can also
     355    accept a list of model classes in place of a single model; this
     356    allows easier registration of multiple models with the same
     357    ``CommentModerator`` class.
     358
     359    The actual moderation is applied in two phases: one prior to
     360    saving a new comment, and the other immediately after saving. The
     361    pre-save moderation may mark a comment as non-public or mark it to
     362    be removed; the post-save moderation may delete a comment which
     363    was disallowed (there is currently no way to prevent the comment
     364    being saved once before removal) and, if the comment is still
     365    around, will send any notification emails the comment generated.
     366
     367    """
     368    def __init__(self):
     369        self._registry = {}
     370        self.connect()
     371
     372    def connect(self):
     373        """
     374        Hook up the moderation methods to pre- and post-save signals
     375        from the comment models.
     376
     377        """
     378        signals.pre_save.connect(self.pre_save_moderation, sender=comments.get_model())
     379        signals.post_save.connect(self.post_save_moderation, sender=comments.get_model())
     380
     381    def register(self, model_or_iterable, moderation_class):
     382        """
     383        Register a model or a list of models for comment moderation,
     384        using a particular moderation class.
     385
     386        Raise ``AlreadyModerated`` if any of the models are already
     387        registered.
     388
     389        """
     390        if isinstance(model_or_iterable, ModelBase):
     391            model_or_iterable = [model_or_iterable]
     392        for model in model_or_iterable:
     393            if model in self._registry:
     394                raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.module_name)
     395            self._registry[model] = moderation_class(model)
     396
     397    def unregister(self, model_or_iterable):
     398        """
     399        Remove a model or a list of models from the list of models
     400        whose comments will be moderated.
     401
     402        Raise ``NotModerated`` if any of the models are not currently
     403        registered for moderation.
     404
     405        """
     406        if isinstance(model_or_iterable, ModelBase):
     407            model_or_iterable = [model_or_iterable]
     408        for model in model_or_iterable:
     409            if model not in self._registry:
     410                raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name)
     411            del self._registry[model]
     412
     413    def pre_save_moderation(self, sender, instance, **kwargs):
     414        """
     415        Apply any necessary pre-save moderation steps to new
     416        comments.
     417
     418        """
     419        model = instance.content_type.model_class()
     420        if instance.id or (model not in self._registry):
     421            return
     422        content_object = instance.content_object
     423        moderation_class = self._registry[model]
     424        if not moderation_class.allow(instance, content_object): # Comment will get deleted in post-save hook.
     425            instance.moderation_disallowed = True
     426            return
     427        if moderation_class.moderate(instance, content_object):
     428            instance.is_public = False
     429
     430    def post_save_moderation(self, sender, instance, **kwargs):
     431        """
     432        Apply any necessary post-save moderation steps to new
     433        comments.
     434
     435        """
     436        model = instance.content_type.model_class()
     437        if model not in self._registry:
     438            return
     439        if hasattr(instance, 'moderation_disallowed'):
     440            instance.delete()
     441            return
     442        self._registry[model].email(instance, instance.content_object)
     443
     444    def comments_open(self, obj):
     445        """
     446        Return ``True`` if new comments are being accepted for
     447        ``obj``, ``False`` otherwise.
     448
     449        If no moderation rules have been registered for the model of
     450        which ``obj`` is an instance, comments are assumed to be open
     451        for that object.
     452
     453        """
     454        model = obj.__class__
     455        if model not in self._registry:
     456            return True
     457        return self._registry[model].comments_open(obj)
     458
     459    def comments_moderated(self, obj):
     460        """
     461        Return ``True`` if new comments for ``obj`` are being
     462        automatically sent to moderation, ``False`` otherwise.
     463
     464        If no moderation rules have been registered for the model of
     465        which ``obj`` is an instance, comments for that object are
     466        assumed not to be moderated.
     467
     468        """
     469        model = obj.__class__
     470        if model not in self._registry:
     471            return False
     472        return self._registry[model].comments_moderated(obj)
     473
     474
     475# Import this instance in your own code to use in registering
     476# your models for moderation.
     477moderator = Moderator()
  • docs/ref/contrib/comments/index.txt

    diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt
    index b78ac4f..ab5a7ec 100644
    a b More information  
    215215   signals
    216216   upgrade
    217217   custom
     218   moderators
  • new file docs/ref/contrib/comments/moderators.txt

    diff --git a/docs/ref/contrib/comments/moderators.txt b/docs/ref/contrib/comments/moderators.txt
    new file mode 100644
    index 0000000..d66bb32
    - +  
     1.. _ref-contrib-comments-moderators:
     2
     3==========================
     4Generic comment moderation
     5==========================
     6
     7.. currentmodule:: django.contrib.comments.moderators
     8
     9Django's bundled comments application is extremely useful on its own,
     10but the amount of comment spam circulating on the Web today
     11essentially makes it necessary to have some sort of automatic
     12moderation system in place for any application which makes use of
     13comments. To make this easier to handle in a consistent fashion,
     14``django.contrib.comments.moderators`` (based on `comment_utils`_)
     15provides a generic, extensible comment-moderation system which can
     16be applied to any model or set of models which want to make use of
     17Django's comment system.
     18
     19.. _`comment_utils`: http://code.google.com/p/django-comment-utils/
     20
     21
     22Overview
     23========
     24
     25The entire system is contained within ``django.contrib.comments.moderators``,
     26and uses a two-step process to enable moderation for any given model:
     27
     281. A subclass of :class:`django.contrib.comments.moderators.CommentModerator`
     29   is defined which specifies the moderation options the model wants to
     30   enable.
     31
     322. The model is registered with the moderation system, passing in the
     33   model class and the class which specifies its moderation options.
     34
     35A simple example is the best illustration of this. Suppose we have the
     36following model, which would represent entries in a weblog::
     37
     38    from django.db import models
     39   
     40    class Entry(models.Model):
     41        title = models.CharField(maxlength=250)
     42        body = models.TextField()
     43        pub_date = models.DateTimeField()
     44        enable_comments = models.BooleanField()
     45
     46Now, suppose that we want the following steps to be applied whenever a
     47new comment is posted on an ``Entry``:
     48
     491. If the ``Entry``'s ``enable_comments`` field is ``False``, the
     50   comment will simply be disallowed (i.e., immediately deleted).
     51
     522. If the ``enable_comments`` field is ``True``, the comment will be
     53   allowed to save.
     54
     553. Once the comment is saved, an email should be sent to site staff
     56   notifying them of the new comment.
     57
     58Accomplishing this is fairly straightforward and requires very little
     59code::
     60
     61    from django.contrib.comments.moderators import CommentModerator, moderator
     62   
     63    class EntryModerator(CommentModerator):
     64        email_notification = True
     65        enable_field = 'enable_comments'
     66   
     67    moderator.register(Entry, EntryModerator)
     68
     69The :class:`django.contrib.comments.moderators.CommentModerator` class
     70pre-defines a number of useful moderation options which subclasses can enable
     71or disable as desired, and ``moderator`` knows how to work with them to
     72determine whether to allow a comment, whether to moderate a comment which
     73will be allowed to post, and whether to email notifications of new comments.
     74
     75
     76Built-in moderation options
     77---------------------------
     78
     79.. class:: CommentModerator
     80
     81    Most common comment-moderation needs can be handled by subclassing
     82    :class:`~django.contrib.comments.moderators.CommentModerator` and
     83    changing the values of pre-defined attributes; the full range of built-in
     84    options is as follows.
     85
     86    .. attribute:: auto_close_field
     87
     88        If this is set to the name of a
     89        :class:`~django.db.models.fields.DateField` or
     90        :class:`~django.db.models.fields.DateTimeField` on the model for which
     91        comments are being moderated, new comments for objects of that model
     92        will be disallowed (immediately deleted) when a certain number of days
     93        have passed after the date specified in that field. Must be
     94        used in conjunction with :attr:`close_after`, which specifies the
     95        number of days past which comments should be
     96        disallowed. Default value is ``None``.
     97
     98    .. attribute:: auto_moderate_field
     99
     100        Like :attr:`auto_close_field`, but instead of outright deleting
     101        new comments when the requisite number of days have elapsed,
     102        it will simply set the ``is_public`` field of new comments to
     103        ``False`` before saving them. Must be used in conjunction with
     104        :attr:`moderate_after`, which specifies the number of days past
     105        which comments should be moderated. Default value is ``None``.
     106
     107    .. attribute:: close_after
     108
     109        If :attr:`auto_close_field` is used, this must specify the number
     110        of days past the value of the field specified by
     111        :attr:`auto_close_field` after which new comments for an object
     112        should be disallowed. Default value is ``None``.
     113
     114    .. attribute:: email_notification
     115
     116        If ``True``, any new comment on an object of this model which
     117        survives moderation (i.e., is not deleted) will generate an
     118        email to site staff. Default value is ``False``.
     119
     120    .. attribute:: enable_field
     121
     122        If this is set to the name of a
     123        :class:`~django.db.models.fields.BooleanField` on the model
     124        for which comments are being moderated, new comments on
     125        objects of that model will be disallowed (immediately deleted)
     126        whenever the value of that field is ``False`` on the object
     127        the comment would be attached to. Default value is ``None``.
     128
     129    .. attribute:: moderate_after
     130
     131        If :attr:`auto_moderate_field` is used, this must specify the number
     132        of days past the value of the field specified by
     133        :attr:`auto_moderate_field` after which new comments for an object
     134        should be marked non-public. Default value is ``None``.
     135
     136Simply subclassing
     137:class:`~django.contrib.comments.moderators.CommentModerator` and changing
     138the values of these options will automatically enable the various moderation
     139methods for any models registered using the subclass.
     140
     141Adding custom moderation methods
     142--------------------------------
     143
     144For situations where the built-in options listed above are not
     145sufficient, subclasses of
     146:class:`~django.contrib.comments.moderators.CommentModerator` can also
     147override the methods which actually perform the moderation, and apply any
     148logic they desire.
     149:class:`~django.contrib.comments.moderators.CommentModerator` defines three
     150methods which determine how moderation will take place; each method will be
     151called by the moderation system and passed two arguments: ``comment``, which
     152is the new comment being posted, and ``content_object``, which is the
     153object the comment will be attached to:
     154
     155.. method:: CommentModerator.allow(comment, content_object)
     156
     157    Should return ``True`` if the comment should be allowed to
     158    post on the content object, and ``False`` otherwise (in which
     159    case the comment will be immediately deleted).
     160
     161.. method:: CommentModerator.email(comment, content_object)
     162
     163    If email notification of the new comment should be sent to
     164    site staff or moderators, this method is responsible for
     165    sending the email.
     166
     167.. method:: CommentModerator.moderate(comment, content_object)
     168
     169    Should return ``True`` if the comment should be moderated (in
     170    which case its ``is_public`` field will be set to ``False``
     171    before saving), and ``False`` otherwise (in which case the
     172    ``is_public`` field will not be changed).
     173
     174
     175Registering models for moderation
     176---------------------------------
     177
     178The moderation system, represented by
     179``django.contrib.comments.moderators.moderator`` is an instance of the class
     180:class:`django.contrib.comments.moderators.Moderator`, which allows
     181registration and "unregistration" of models via two methods:
     182
     183.. function:: moderator.register(model_or_iterable, moderation_class)
     184
     185    Takes two arguments: the first should be either a model class
     186    or list of model classes, and the second should be a subclass
     187    of ``CommentModerator``, and register the model or models to
     188    be moderated using the options defined in the
     189    ``CommentModerator`` subclass. If any of the models are
     190    already registered for moderation, the exception
     191    ``django.contrib.comments.moderators.AlreadyModerated`` will be raised.
     192
     193.. function:: moderator.unregister(model_or_iterable)
     194
     195    Takes one argument: a model class or list of model classes,
     196    and removes the model or models from the set of models which
     197    are being moderated. If any of the models are not currently
     198    being moderated, the exception
     199    :exc:`django.contrib.comments.moderators.NotModerated` will be raised.
     200
     201
     202Customizing the moderation system
     203---------------------------------
     204
     205Most use cases will work easily with simple subclassing of
     206:class:`~django.contrib.comments.moderators.CommentModerator` and registration
     207with the provided :class:`~django.contrib.comments.moderators.moderator`
     208instance, but customization of global moderation behavior can be
     209achieved by subclassing :class:`~django.contrib.comments.moderators.Moderator`
     210and instead registering models with an instance of the subclass.
     211
     212
     213.. class:: Moderator
     214
     215    In addition to the
     216    :func:`~django.contrib.comments.moderators.moderator.register` and
     217    :func:`~django.contrib.comments.moderators.moderator.unregister` methods
     218    detailed above, the following methods on
     219    :class:`~django.contrib.comments.moderators.Moderator` can be overridden
     220    to achieve customized behavior:
     221
     222    .. method:: connect
     223
     224        Determines how moderation is set up globally. The base
     225        implementation in
     226        :class:`~django.contrib.comments.moderators.Moderator` does this by
     227        attaching listeners to the :data:`~django.db.models.signals.pre_save`
     228        and :data:`~django.db.models.signals.post_save` signals from the
     229        comment models.
     230
     231    .. method:: pre_save_moderation(sender, instance, **kwargs)
     232
     233        In the base implementation, applies all pre-save moderation
     234        steps (such as determining whether the comment needs to be
     235        deleted, or whether it needs to be marked as non-public or
     236        generate an email).
     237
     238    .. method:: post_save_moderation(sender, instance, **kwargs)
     239
     240        In the base implementation, applies all post-save moderation
     241        steps (currently this consists entirely of deleting comments
     242        which were disallowed).
  • new file tests/regressiontests/comment_tests/fixtures/comment_utils.xml

    diff --git a/tests/regressiontests/comment_tests/fixtures/comment_utils.xml b/tests/regressiontests/comment_tests/fixtures/comment_utils.xml
    new file mode 100644
    index 0000000..a39bbf6
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<django-objects version="1.0">
     3  <object pk="1" model="comment_tests.entry">
     4      <field type="CharField" name="title">ABC</field>
     5      <field type="TextField" name="body">This is the body</field>
     6      <field type="DateField" name="pub_date">2008-01-01</field>
     7      <field type="BooleanField" name="enable_comments">True</field>
     8  </object>
     9  <object pk="2" model="comment_tests.entry">
     10      <field type="CharField" name="title">XYZ</field>
     11      <field type="TextField" name="body">Text here</field>
     12      <field type="DateField" name="pub_date">2008-01-02</field>
     13      <field type="BooleanField" name="enable_comments">False</field>
     14  </object>
     15</django-objects>
  • tests/regressiontests/comment_tests/models.py

    diff --git a/tests/regressiontests/comment_tests/models.py b/tests/regressiontests/comment_tests/models.py
    index 28022e2..62f4168 100644
    a b class Article(models.Model):  
    2020    def __str__(self):
    2121        return self.headline
    2222
     23class Entry(models.Model):
     24    title = models.CharField(max_length=250)
     25    body = models.TextField()
     26    pub_date = models.DateField()
     27    enable_comments = models.BooleanField()
     28
     29    def __str__(self):
     30        return self.title
  • tests/regressiontests/comment_tests/tests/__init__.py

    diff --git a/tests/regressiontests/comment_tests/tests/__init__.py b/tests/regressiontests/comment_tests/tests/__init__.py
    index 09026aa..449fea4 100644
    a b from regressiontests.comment_tests.tests.comment_form_tests import *  
    8686from regressiontests.comment_tests.tests.templatetag_tests import *
    8787from regressiontests.comment_tests.tests.comment_view_tests import *
    8888from regressiontests.comment_tests.tests.moderation_view_tests import *
     89from regressiontests.comment_tests.tests.comment_utils_moderators_tests import *
  • new file tests/regressiontests/comment_tests/tests/comment_utils_moderators_tests.py

    diff --git a/tests/regressiontests/comment_tests/tests/comment_utils_moderators_tests.py b/tests/regressiontests/comment_tests/tests/comment_utils_moderators_tests.py
    new file mode 100644
    index 0000000..1052636
    - +  
     1from regressiontests.comment_tests.tests import CommentTestCase, CT, Site
     2from django.contrib.comments.models import Comment
     3from django.contrib.comments.moderators import moderator, CommentModerator, AlreadyModerated
     4from regressiontests.comment_tests.models import Entry
     5from django.core import mail
     6
     7class EntryModerator1(CommentModerator):
     8    email_notification = True
     9
     10class EntryModerator2(CommentModerator):
     11    enable_field = 'enable_comments'
     12
     13class EntryModerator3(CommentModerator):
     14    auto_close_field = 'pub_date'
     15    close_after = 7
     16
     17class EntryModerator4(CommentModerator):
     18    auto_moderate_field = 'pub_date'
     19    moderate_after = 7
     20
     21class CommentUtilsModeratorTests(CommentTestCase):
     22    fixtures = ["comment_utils.xml"]
     23
     24    def createSomeComments(self):
     25        c1 = Comment.objects.create(
     26            content_type = CT(Entry),
     27            object_pk = "1",
     28            user_name = "Joe Somebody",
     29            user_email = "jsomebody@example.com",
     30            user_url = "http://example.com/~joe/",
     31            comment = "First!",
     32            site = Site.objects.get_current(),
     33        )
     34        c2 = Comment.objects.create(
     35            content_type = CT(Entry),
     36            object_pk = "2",
     37            user_name = "Joe the Plumber",
     38            user_email = "joetheplumber@whitehouse.gov",
     39            user_url = "http://example.com/~joe/",
     40            comment = "Second!",
     41            site = Site.objects.get_current(),
     42        )
     43        return c1, c2
     44
     45    def tearDown(self):
     46        moderator.unregister(Entry)
     47
     48    def testRegisterExistingModel(self):
     49        moderator.register(Entry, EntryModerator1)
     50        self.assertRaises(AlreadyModerated, moderator.register, Entry, EntryModerator1)
     51
     52    def testEmailNotification(self):
     53        moderator.register(Entry, EntryModerator1)
     54        c1, c2 = self.createSomeComments()
     55        self.assertEquals(len(mail.outbox), 2)
     56
     57    def testCommentsEnabled(self):
     58        moderator.register(Entry, EntryModerator2)
     59        c1, c2 = self.createSomeComments()
     60        self.assertEquals(Comment.objects.all().count(), 1)
     61
     62    def testAutoCloseField(self):
     63        moderator.register(Entry, EntryModerator3)
     64        c1, c2 = self.createSomeComments()
     65        self.assertEquals(Comment.objects.all().count(), 0)
     66
     67    def testAutoModerateField(self):
     68        moderator.register(Entry, EntryModerator4)
     69        c1, c2 = self.createSomeComments()
     70        self.assertEquals(c2.is_public, False)
  • tests/runtests.py

    diff --git a/tests/runtests.py b/tests/runtests.py
    index bd7f59b..f556246 100755
    a b def django_tests(verbosity, interactive, test_labels):  
    110110        'django.middleware.common.CommonMiddleware',
    111111    )
    112112    settings.SITE_ID = 1
     113    # For testing comment-utils, we require the MANAGERS attribute
     114    # to be set, so that a test email is sent out which we catch
     115    # in our tests.
     116    settings.MANAGERS = ("admin@djangoproject.com",)
    113117
    114118    # Load all the ALWAYS_INSTALLED_APPS.
    115119    # (This import statement is intentionally delayed until after we
  • new file tests/templates/comments/comment_notification_email.txt

    diff --git a/tests/templates/comments/comment_notification_email.txt b/tests/templates/comments/comment_notification_email.txt
    new file mode 100644
    index 0000000..63f1493
    - +  
     1A comment has been posted on {{ content_object }}.
     2The comment reads as follows:
     3{{ comment }}
Back to Top