Ticket #9282: 9282.diff

File 9282.diff, 18.7 KB (added by Thejaswi Puthraya, 16 years ago)

svn-patch against latest checkout

  • django/contrib/comments/moderators.py

     
     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 comment_utils.moderation 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.comments.models import Comment
     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`` 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        for model in Comment:
     379            signals.pre_save.connect(self.pre_save_moderation, sender=model)
     380            signals.post_save.connect(self.post_save_moderation, sender=model)
     381   
     382    def register(self, model_or_iterable, moderation_class):
     383        """
     384        Register a model or a list of models for comment moderation,
     385        using a particular moderation class.
     386       
     387        Raise ``AlreadyModerated`` if any of the models are already
     388        registered.
     389       
     390        """
     391        if isinstance(model_or_iterable, ModelBase):
     392            model_or_iterable = [model_or_iterable]
     393        for model in model_or_iterable:
     394            if model in self._registry:
     395                raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.module_name)
     396            self._registry[model] = moderation_class(model)
     397   
     398    def unregister(self, model_or_iterable):
     399        """
     400        Remove a model or a list of models from the list of models
     401        whose comments will be moderated.
     402       
     403        Raise ``NotModerated`` if any of the models are not currently
     404        registered for moderation.
     405       
     406        """
     407        if isinstance(model_or_iterable, ModelBase):
     408            model_or_iterable = [model_or_iterable]
     409        for model in model_or_iterable:
     410            if model not in self._registry:
     411                raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name)
     412            del self._registry[model]
     413   
     414    def pre_save_moderation(self, sender, instance, **kwargs):
     415        """
     416        Apply any necessary pre-save moderation steps to new
     417        comments.
     418       
     419        """
     420        model = instance.content_type.model_class()
     421        if instance.id or (model not in self._registry):
     422            return
     423        content_object = instance.get_content_object()
     424        moderation_class = self._registry[model]
     425        if not moderation_class.allow(instance, content_object): # Comment will get deleted in post-save hook.
     426            instance.moderation_disallowed = True
     427            return
     428        if moderation_class.moderate(instance, content_object):
     429            instance.is_public = False
     430   
     431    def post_save_moderation(self, sender, instance, **kwargs):
     432        """
     433        Apply any necessary post-save moderation steps to new
     434        comments.
     435       
     436        """
     437        model = instance.content_type.model_class()
     438        if model not in self._registry:
     439            return
     440        if hasattr(instance, 'moderation_disallowed'):
     441            instance.delete()
     442            return
     443        self._registry[model].email(instance, instance.get_content_object())
     444
     445    def comments_open(self, obj):
     446        """
     447        Return ``True`` if new comments are being accepted for
     448        ``obj``, ``False`` otherwise.
     449       
     450        If no moderation rules have been registered for the model of
     451        which ``obj`` is an instance, comments are assumed to be open
     452        for that object.
     453       
     454        """
     455        model = obj.__class__
     456        if model not in self._registry:
     457            return True
     458        return self._registry[model].comments_open(obj)
     459
     460    def comments_moderated(self, obj):
     461        """
     462        Return ``True`` if new comments for ``obj`` are being
     463        automatically sent to moderation, ``False`` otherwise.
     464       
     465        If no moderation rules have been registered for the model of
     466        which ``obj`` is an instance, comments for that object are
     467        assumed not to be moderated.
     468       
     469        """
     470        model = obj.__class__
     471        if model not in self._registry:
     472            return False
     473        return self._registry[model].comments_moderated(obj)
     474
     475
     476# Import this instance in your own code to use in registering
     477# your models for moderation.
     478moderator = Moderator()
Back to Top