PageRenderTime 176ms CodeModel.GetById 81ms app.highlight 14ms RepoModel.GetById 76ms app.codeStats 1ms

/django/contrib/comments/moderation.py

https://code.google.com/p/mango-py/
Python | 355 lines | 290 code | 6 blank | 59 comment | 7 complexity | 1f197d835e74c5650f0c663649b92d86 MD5 | raw file
  1"""
  2A generic comment-moderation system which allows configuration of
  3moderation options on a per-model basis.
  4
  5To use, do two things:
  6
  71. Create or import a subclass of ``CommentModerator`` defining the
  8   options you want.
  9
 102. 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
 15Example
 16-------
 17
 18First, we define a simple model class which might represent entries in
 19a 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
 29Then we create a ``CommentModerator`` subclass specifying some
 30moderation 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
 38And finally register it for moderation::
 39
 40    moderator.register(Entry, EntryModerator)
 41
 42This sample class would apply two moderation steps to each new
 43comment 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
 51For a full list of built-in moderation options and other
 52configurability, see the documentation for the ``CommentModerator``
 53class.
 54
 55"""
 56
 57import datetime
 58
 59from django.conf import settings
 60from django.core.mail import send_mail
 61from django.contrib.comments import signals
 62from django.db.models.base import ModelBase
 63from django.template import Context, loader
 64from django.contrib import comments
 65from django.contrib.sites.models import Site
 66
 67class 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
 75class 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
 83class 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 is not None:
209            close_after_date = getattr(content_object, self.auto_close_field)
210            if close_after_date is not None and self._get_delta(datetime.datetime.now(), close_after_date).days >= self.close_after:
211                return False
212        return True
213
214    def moderate(self, comment, content_object, request):
215        """
216        Determine whether a given comment on a given object should be
217        allowed to show up immediately, or should be marked non-public
218        and await approval.
219
220        Return ``True`` if the comment should be moderated (marked
221        non-public), ``False`` otherwise.
222
223        """
224        if self.auto_moderate_field and self.moderate_after is not None:
225            moderate_after_date = getattr(content_object, self.auto_moderate_field)
226            if moderate_after_date is not None and self._get_delta(datetime.datetime.now(), moderate_after_date).days >= self.moderate_after:
227                return True
228        return False
229
230    def email(self, comment, content_object, request):
231        """
232        Send email notification of a new comment to site staff when email
233        notifications have been requested.
234
235        """
236        if not self.email_notification:
237            return
238        recipient_list = [manager_tuple[1] for manager_tuple in settings.MANAGERS]
239        t = loader.get_template('comments/comment_notification_email.txt')
240        c = Context({ 'comment': comment,
241                      'content_object': content_object })
242        subject = '[%s] New comment posted on "%s"' % (Site.objects.get_current().name,
243                                                          content_object)
244        message = t.render(c)
245        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True)
246
247class Moderator(object):
248    """
249    Handles moderation of a set of models.
250
251    An instance of this class will maintain a list of one or more
252    models registered for comment moderation, and their associated
253    moderation classes, and apply moderation to all incoming comments.
254
255    To register a model, obtain an instance of ``Moderator`` (this
256    module exports one as ``moderator``), and call its ``register``
257    method, passing the model class and a moderation class (which
258    should be a subclass of ``CommentModerator``). Note that both of
259    these should be the actual classes, not instances of the classes.
260
261    To cease moderation for a model, call the ``unregister`` method,
262    passing the model class.
263
264    For convenience, both ``register`` and ``unregister`` can also
265    accept a list of model classes in place of a single model; this
266    allows easier registration of multiple models with the same
267    ``CommentModerator`` class.
268
269    The actual moderation is applied in two phases: one prior to
270    saving a new comment, and the other immediately after saving. The
271    pre-save moderation may mark a comment as non-public or mark it to
272    be removed; the post-save moderation may delete a comment which
273    was disallowed (there is currently no way to prevent the comment
274    being saved once before removal) and, if the comment is still
275    around, will send any notification emails the comment generated.
276
277    """
278    def __init__(self):
279        self._registry = {}
280        self.connect()
281
282    def connect(self):
283        """
284        Hook up the moderation methods to pre- and post-save signals
285        from the comment models.
286
287        """
288        signals.comment_will_be_posted.connect(self.pre_save_moderation, sender=comments.get_model())
289        signals.comment_was_posted.connect(self.post_save_moderation, sender=comments.get_model())
290
291    def register(self, model_or_iterable, moderation_class):
292        """
293        Register a model or a list of models for comment moderation,
294        using a particular moderation class.
295
296        Raise ``AlreadyModerated`` if any of the models are already
297        registered.
298
299        """
300        if isinstance(model_or_iterable, ModelBase):
301            model_or_iterable = [model_or_iterable]
302        for model in model_or_iterable:
303            if model in self._registry:
304                raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.module_name)
305            self._registry[model] = moderation_class(model)
306
307    def unregister(self, model_or_iterable):
308        """
309        Remove a model or a list of models from the list of models
310        whose comments will be moderated.
311
312        Raise ``NotModerated`` if any of the models are not currently
313        registered for moderation.
314
315        """
316        if isinstance(model_or_iterable, ModelBase):
317            model_or_iterable = [model_or_iterable]
318        for model in model_or_iterable:
319            if model not in self._registry:
320                raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name)
321            del self._registry[model]
322
323    def pre_save_moderation(self, sender, comment, request, **kwargs):
324        """
325        Apply any necessary pre-save moderation steps to new
326        comments.
327
328        """
329        model = comment.content_type.model_class()
330        if model not in self._registry:
331            return
332        content_object = comment.content_object
333        moderation_class = self._registry[model]
334
335        # Comment will be disallowed outright (HTTP 403 response)
336        if not moderation_class.allow(comment, content_object, request): 
337            return False
338
339        if moderation_class.moderate(comment, content_object, request):
340            comment.is_public = False
341
342    def post_save_moderation(self, sender, comment, request, **kwargs):
343        """
344        Apply any necessary post-save moderation steps to new
345        comments.
346
347        """
348        model = comment.content_type.model_class()
349        if model not in self._registry:
350            return
351        self._registry[model].email(comment, comment.content_object, request)
352
353# Import this instance in your own code to use in registering
354# your models for moderation.
355moderator = Moderator()