web/lib/django/contrib/comments/moderation.py
changeset 38 77b6da96e6f1
equal deleted inserted replaced
37:8d941af65caf 38:77b6da96e6f1
       
     1 """
       
     2 A generic comment-moderation system which allows configuration of
       
     3 moderation options on a per-model basis.
       
     4 
       
     5 To use, do two things:
       
     6 
       
     7 1. Create or import a subclass of ``CommentModerator`` defining the
       
     8    options you want.
       
     9 
       
    10 2. Import ``moderator`` from this module and register one or more
       
    11    models, passing the models and the ``CommentModerator`` options
       
    12    class you want to use.
       
    13 
       
    14 
       
    15 Example
       
    16 -------
       
    17 
       
    18 First, we define a simple model class which might represent entries in
       
    19 a weblog::
       
    20 
       
    21     from django.db import models
       
    22 
       
    23     class Entry(models.Model):
       
    24         title = models.CharField(maxlength=250)
       
    25         body = models.TextField()
       
    26         pub_date = models.DateField()
       
    27         enable_comments = models.BooleanField()
       
    28 
       
    29 Then we create a ``CommentModerator`` subclass specifying some
       
    30 moderation options::
       
    31 
       
    32     from django.contrib.comments.moderation import CommentModerator, moderator
       
    33 
       
    34     class EntryModerator(CommentModerator):
       
    35         email_notification = True
       
    36         enable_field = 'enable_comments'
       
    37 
       
    38 And finally register it for moderation::
       
    39 
       
    40     moderator.register(Entry, EntryModerator)
       
    41 
       
    42 This sample class would apply two moderation steps to each new
       
    43 comment submitted on an Entry:
       
    44 
       
    45 * If the entry's ``enable_comments`` field is set to ``False``, the
       
    46   comment will be rejected (immediately deleted).
       
    47 
       
    48 * If the comment is successfully posted, an email notification of the
       
    49   comment will be sent to site staff.
       
    50 
       
    51 For a full list of built-in moderation options and other
       
    52 configurability, see the documentation for the ``CommentModerator``
       
    53 class.
       
    54 
       
    55 """
       
    56 
       
    57 import datetime
       
    58 
       
    59 from django.conf import settings
       
    60 from django.core.mail import send_mail
       
    61 from django.contrib.comments import signals
       
    62 from django.db.models.base import ModelBase
       
    63 from django.template import Context, loader
       
    64 from django.contrib import comments
       
    65 from django.contrib.sites.models import Site
       
    66 
       
    67 class AlreadyModerated(Exception):
       
    68     """
       
    69     Raised when a model which is already registered for moderation is
       
    70     attempting to be registered again.
       
    71 
       
    72     """
       
    73     pass
       
    74 
       
    75 class NotModerated(Exception):
       
    76     """
       
    77     Raised when a model which is not registered for moderation is
       
    78     attempting to be unregistered.
       
    79 
       
    80     """
       
    81     pass
       
    82 
       
    83 class CommentModerator(object):
       
    84     """
       
    85     Encapsulates comment-moderation options for a given model.
       
    86 
       
    87     This class is not designed to be used directly, since it doesn't
       
    88     enable any of the available moderation options. Instead, subclass
       
    89     it and override attributes to enable different options::
       
    90 
       
    91     ``auto_close_field``
       
    92         If this is set to the name of a ``DateField`` or
       
    93         ``DateTimeField`` on the model for which comments are
       
    94         being moderated, new comments for objects of that model
       
    95         will be disallowed (immediately deleted) when a certain
       
    96         number of days have passed after the date specified in
       
    97         that field. Must be used in conjunction with
       
    98         ``close_after``, which specifies the number of days past
       
    99         which comments should be disallowed. Default value is
       
   100         ``None``.
       
   101 
       
   102     ``auto_moderate_field``
       
   103         Like ``auto_close_field``, but instead of outright
       
   104         deleting new comments when the requisite number of days
       
   105         have elapsed, it will simply set the ``is_public`` field
       
   106         of new comments to ``False`` before saving them. Must be
       
   107         used in conjunction with ``moderate_after``, which
       
   108         specifies the number of days past which comments should be
       
   109         moderated. Default value is ``None``.
       
   110 
       
   111     ``close_after``
       
   112         If ``auto_close_field`` is used, this must specify the
       
   113         number of days past the value of the field specified by
       
   114         ``auto_close_field`` after which new comments for an
       
   115         object should be disallowed. Default value is ``None``.
       
   116 
       
   117     ``email_notification``
       
   118         If ``True``, any new comment on an object of this model
       
   119         which survives moderation will generate an email to site
       
   120         staff. Default value is ``False``.
       
   121 
       
   122     ``enable_field``
       
   123         If this is set to the name of a ``BooleanField`` on the
       
   124         model for which comments are being moderated, new comments
       
   125         on objects of that model will be disallowed (immediately
       
   126         deleted) whenever the value of that field is ``False`` on
       
   127         the object the comment would be attached to. Default value
       
   128         is ``None``.
       
   129 
       
   130     ``moderate_after``
       
   131         If ``auto_moderate_field`` is used, this must specify the number
       
   132         of days past the value of the field specified by
       
   133         ``auto_moderate_field`` after which new comments for an
       
   134         object should be marked non-public. Default value is
       
   135         ``None``.
       
   136 
       
   137     Most common moderation needs can be covered by changing these
       
   138     attributes, but further customization can be obtained by
       
   139     subclassing and overriding the following methods. Each method will
       
   140     be called with three arguments: ``comment``, which is the comment
       
   141     being submitted, ``content_object``, which is the object the
       
   142     comment will be attached to, and ``request``, which is the
       
   143     ``HttpRequest`` in which the comment is being submitted::
       
   144 
       
   145     ``allow``
       
   146         Should return ``True`` if the comment should be allowed to
       
   147         post on the content object, and ``False`` otherwise (in
       
   148         which case the comment will be immediately deleted).
       
   149 
       
   150     ``email``
       
   151         If email notification of the new comment should be sent to
       
   152         site staff or moderators, this method is responsible for
       
   153         sending the email.
       
   154 
       
   155     ``moderate``
       
   156         Should return ``True`` if the comment should be moderated
       
   157         (in which case its ``is_public`` field will be set to
       
   158         ``False`` before saving), and ``False`` otherwise (in
       
   159         which case the ``is_public`` field will not be changed).
       
   160 
       
   161     Subclasses which want to introspect the model for which comments
       
   162     are being moderated can do so through the attribute ``_model``,
       
   163     which will be the model class.
       
   164 
       
   165     """
       
   166     auto_close_field = None
       
   167     auto_moderate_field = None
       
   168     close_after = None
       
   169     email_notification = False
       
   170     enable_field = None
       
   171     moderate_after = None
       
   172 
       
   173     def __init__(self, model):
       
   174         self._model = model
       
   175 
       
   176     def _get_delta(self, now, then):
       
   177         """
       
   178         Internal helper which will return a ``datetime.timedelta``
       
   179         representing the time between ``now`` and ``then``. Assumes
       
   180         ``now`` is a ``datetime.date`` or ``datetime.datetime`` later
       
   181         than ``then``.
       
   182 
       
   183         If ``now`` and ``then`` are not of the same type due to one of
       
   184         them being a ``datetime.date`` and the other being a
       
   185         ``datetime.datetime``, both will be coerced to
       
   186         ``datetime.date`` before calculating the delta.
       
   187 
       
   188         """
       
   189         if now.__class__ is not then.__class__:
       
   190             now = datetime.date(now.year, now.month, now.day)
       
   191             then = datetime.date(then.year, then.month, then.day)
       
   192         if now < then:
       
   193             raise ValueError("Cannot determine moderation rules because date field is set to a value in the future")
       
   194         return now - then
       
   195 
       
   196     def allow(self, comment, content_object, request):
       
   197         """
       
   198         Determine whether a given comment is allowed to be posted on
       
   199         a given object.
       
   200 
       
   201         Return ``True`` if the comment should be allowed, ``False
       
   202         otherwise.
       
   203 
       
   204         """
       
   205         if self.enable_field:
       
   206             if not getattr(content_object, self.enable_field):
       
   207                 return False
       
   208         if self.auto_close_field and self.close_after:
       
   209             if self._get_delta(datetime.datetime.now(), getattr(content_object, self.auto_close_field)).days >= self.close_after:
       
   210                 return False
       
   211         return True
       
   212 
       
   213     def moderate(self, comment, content_object, request):
       
   214         """
       
   215         Determine whether a given comment on a given object should be
       
   216         allowed to show up immediately, or should be marked non-public
       
   217         and await approval.
       
   218 
       
   219         Return ``True`` if the comment should be moderated (marked
       
   220         non-public), ``False`` otherwise.
       
   221 
       
   222         """
       
   223         if self.auto_moderate_field and self.moderate_after:
       
   224             if self._get_delta(datetime.datetime.now(), getattr(content_object, self.auto_moderate_field)).days >= self.moderate_after:
       
   225                 return True
       
   226         return False
       
   227 
       
   228     def email(self, comment, content_object, request):
       
   229         """
       
   230         Send email notification of a new comment to site staff when email
       
   231         notifications have been requested.
       
   232 
       
   233         """
       
   234         if not self.email_notification:
       
   235             return
       
   236         recipient_list = [manager_tuple[1] for manager_tuple in settings.MANAGERS]
       
   237         t = loader.get_template('comments/comment_notification_email.txt')
       
   238         c = Context({ 'comment': comment,
       
   239                       'content_object': content_object })
       
   240         subject = '[%s] New comment posted on "%s"' % (Site.objects.get_current().name,
       
   241                                                           content_object)
       
   242         message = t.render(c)
       
   243         send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True)
       
   244 
       
   245 class Moderator(object):
       
   246     """
       
   247     Handles moderation of a set of models.
       
   248 
       
   249     An instance of this class will maintain a list of one or more
       
   250     models registered for comment moderation, and their associated
       
   251     moderation classes, and apply moderation to all incoming comments.
       
   252 
       
   253     To register a model, obtain an instance of ``Moderator`` (this
       
   254     module exports one as ``moderator``), and call its ``register``
       
   255     method, passing the model class and a moderation class (which
       
   256     should be a subclass of ``CommentModerator``). Note that both of
       
   257     these should be the actual classes, not instances of the classes.
       
   258 
       
   259     To cease moderation for a model, call the ``unregister`` method,
       
   260     passing the model class.
       
   261 
       
   262     For convenience, both ``register`` and ``unregister`` can also
       
   263     accept a list of model classes in place of a single model; this
       
   264     allows easier registration of multiple models with the same
       
   265     ``CommentModerator`` class.
       
   266 
       
   267     The actual moderation is applied in two phases: one prior to
       
   268     saving a new comment, and the other immediately after saving. The
       
   269     pre-save moderation may mark a comment as non-public or mark it to
       
   270     be removed; the post-save moderation may delete a comment which
       
   271     was disallowed (there is currently no way to prevent the comment
       
   272     being saved once before removal) and, if the comment is still
       
   273     around, will send any notification emails the comment generated.
       
   274 
       
   275     """
       
   276     def __init__(self):
       
   277         self._registry = {}
       
   278         self.connect()
       
   279 
       
   280     def connect(self):
       
   281         """
       
   282         Hook up the moderation methods to pre- and post-save signals
       
   283         from the comment models.
       
   284 
       
   285         """
       
   286         signals.comment_will_be_posted.connect(self.pre_save_moderation, sender=comments.get_model())
       
   287         signals.comment_was_posted.connect(self.post_save_moderation, sender=comments.get_model())
       
   288 
       
   289     def register(self, model_or_iterable, moderation_class):
       
   290         """
       
   291         Register a model or a list of models for comment moderation,
       
   292         using a particular moderation class.
       
   293 
       
   294         Raise ``AlreadyModerated`` if any of the models are already
       
   295         registered.
       
   296 
       
   297         """
       
   298         if isinstance(model_or_iterable, ModelBase):
       
   299             model_or_iterable = [model_or_iterable]
       
   300         for model in model_or_iterable:
       
   301             if model in self._registry:
       
   302                 raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.module_name)
       
   303             self._registry[model] = moderation_class(model)
       
   304 
       
   305     def unregister(self, model_or_iterable):
       
   306         """
       
   307         Remove a model or a list of models from the list of models
       
   308         whose comments will be moderated.
       
   309 
       
   310         Raise ``NotModerated`` if any of the models are not currently
       
   311         registered for moderation.
       
   312 
       
   313         """
       
   314         if isinstance(model_or_iterable, ModelBase):
       
   315             model_or_iterable = [model_or_iterable]
       
   316         for model in model_or_iterable:
       
   317             if model not in self._registry:
       
   318                 raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name)
       
   319             del self._registry[model]
       
   320 
       
   321     def pre_save_moderation(self, sender, comment, request, **kwargs):
       
   322         """
       
   323         Apply any necessary pre-save moderation steps to new
       
   324         comments.
       
   325 
       
   326         """
       
   327         model = comment.content_type.model_class()
       
   328         if model not in self._registry:
       
   329             return
       
   330         content_object = comment.content_object
       
   331         moderation_class = self._registry[model]
       
   332 
       
   333         # Comment will be disallowed outright (HTTP 403 response)
       
   334         if not moderation_class.allow(comment, content_object, request): 
       
   335             return False
       
   336 
       
   337         if moderation_class.moderate(comment, content_object, request):
       
   338             comment.is_public = False
       
   339 
       
   340     def post_save_moderation(self, sender, comment, request, **kwargs):
       
   341         """
       
   342         Apply any necessary post-save moderation steps to new
       
   343         comments.
       
   344 
       
   345         """
       
   346         model = comment.content_type.model_class()
       
   347         if model not in self._registry:
       
   348             return
       
   349         self._registry[model].email(comment, comment.content_object, request)
       
   350 
       
   351 # Import this instance in your own code to use in registering
       
   352 # your models for moderation.
       
   353 moderator = Moderator()