web/lib/django/contrib/comments/moderation.py
changeset 0 0d40e90630ef
child 29 cc9b7e14412b
equal deleted inserted replaced
-1:000000000000 0:0d40e90630ef
       
     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 ``CommentModerator``
       
   254     (this module exports one as ``moderator``), and call its
       
   255     ``register`` method, passing the model class and a moderation
       
   256     class (which should be a subclass of ``CommentModerator``). Note
       
   257     that both of these should be the actual classes, not instances of
       
   258     the classes.
       
   259 
       
   260     To cease moderation for a model, call the ``unregister`` method,
       
   261     passing the model class.
       
   262 
       
   263     For convenience, both ``register`` and ``unregister`` can also
       
   264     accept a list of model classes in place of a single model; this
       
   265     allows easier registration of multiple models with the same
       
   266     ``CommentModerator`` class.
       
   267 
       
   268     The actual moderation is applied in two phases: one prior to
       
   269     saving a new comment, and the other immediately after saving. The
       
   270     pre-save moderation may mark a comment as non-public or mark it to
       
   271     be removed; the post-save moderation may delete a comment which
       
   272     was disallowed (there is currently no way to prevent the comment
       
   273     being saved once before removal) and, if the comment is still
       
   274     around, will send any notification emails the comment generated.
       
   275 
       
   276     """
       
   277     def __init__(self):
       
   278         self._registry = {}
       
   279         self.connect()
       
   280 
       
   281     def connect(self):
       
   282         """
       
   283         Hook up the moderation methods to pre- and post-save signals
       
   284         from the comment models.
       
   285 
       
   286         """
       
   287         signals.comment_will_be_posted.connect(self.pre_save_moderation, sender=comments.get_model())
       
   288         signals.comment_was_posted.connect(self.post_save_moderation, sender=comments.get_model())
       
   289 
       
   290     def register(self, model_or_iterable, moderation_class):
       
   291         """
       
   292         Register a model or a list of models for comment moderation,
       
   293         using a particular moderation class.
       
   294 
       
   295         Raise ``AlreadyModerated`` if any of the models are already
       
   296         registered.
       
   297 
       
   298         """
       
   299         if isinstance(model_or_iterable, ModelBase):
       
   300             model_or_iterable = [model_or_iterable]
       
   301         for model in model_or_iterable:
       
   302             if model in self._registry:
       
   303                 raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.module_name)
       
   304             self._registry[model] = moderation_class(model)
       
   305 
       
   306     def unregister(self, model_or_iterable):
       
   307         """
       
   308         Remove a model or a list of models from the list of models
       
   309         whose comments will be moderated.
       
   310 
       
   311         Raise ``NotModerated`` if any of the models are not currently
       
   312         registered for moderation.
       
   313 
       
   314         """
       
   315         if isinstance(model_or_iterable, ModelBase):
       
   316             model_or_iterable = [model_or_iterable]
       
   317         for model in model_or_iterable:
       
   318             if model not in self._registry:
       
   319                 raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name)
       
   320             del self._registry[model]
       
   321 
       
   322     def pre_save_moderation(self, sender, comment, request, **kwargs):
       
   323         """
       
   324         Apply any necessary pre-save moderation steps to new
       
   325         comments.
       
   326 
       
   327         """
       
   328         model = comment.content_type.model_class()
       
   329         if model not in self._registry:
       
   330             return
       
   331         content_object = comment.content_object
       
   332         moderation_class = self._registry[model]
       
   333 
       
   334         # Comment will be disallowed outright (HTTP 403 response)
       
   335         if not moderation_class.allow(comment, content_object, request): 
       
   336             return False
       
   337 
       
   338         if moderation_class.moderate(comment, content_object, request):
       
   339             comment.is_public = False
       
   340 
       
   341     def post_save_moderation(self, sender, comment, request, **kwargs):
       
   342         """
       
   343         Apply any necessary post-save moderation steps to new
       
   344         comments.
       
   345 
       
   346         """
       
   347         model = comment.content_type.model_class()
       
   348         if model not in self._registry:
       
   349             return
       
   350         self._registry[model].email(comment, comment.content_object, request)
       
   351 
       
   352 # Import this instance in your own code to use in registering
       
   353 # your models for moderation.
       
   354 moderator = Moderator()