diff options
Diffstat (limited to 'app/django_comments/moderation.py')
-rw-r--r-- | app/django_comments/moderation.py | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/app/django_comments/moderation.py b/app/django_comments/moderation.py new file mode 100644 index 0000000..3e5c412 --- /dev/null +++ b/app/django_comments/moderation.py @@ -0,0 +1,369 @@ +""" +A generic comment-moderation system which allows configuration of +moderation options on a per-model basis. + +To use, do two things: + +1. Create or import a subclass of ``CommentModerator`` defining the + options you want. + +2. Import ``moderator`` from this module and register one or more + models, passing the models and the ``CommentModerator`` options + class you want to use. + + +Example +------- + +First, we define a simple model class which might represent entries in +a Weblog:: + + from django.db import models + + class Entry(models.Model): + title = models.CharField(max_length=250) + body = models.TextField() + pub_date = models.DateField() + enable_comments = models.BooleanField() + +Then we create a ``CommentModerator`` subclass specifying some +moderation options:: + + from django_comments.moderation import CommentModerator, moderator + + class EntryModerator(CommentModerator): + email_notification = True + enable_field = 'enable_comments' + +And finally register it for moderation:: + + moderator.register(Entry, EntryModerator) + +This sample class would apply two moderation steps to each new +comment submitted on an Entry: + +* If the entry's ``enable_comments`` field is set to ``False``, the + comment will be rejected (immediately deleted). + +* If the comment is successfully posted, an email notification of the + comment will be sent to site staff. + +For a full list of built-in moderation options and other +configurability, see the documentation for the ``CommentModerator`` +class. + +""" + +import datetime + +from django.conf import settings +from django.contrib.sites.shortcuts import get_current_site +from django.core.mail import send_mail +from django.db.models.base import ModelBase +from django.template import loader +from django.utils import timezone +from django.utils.translation import ugettext as _ + +import django_comments +from django_comments import signals + + +class AlreadyModerated(Exception): + """ + Raised when a model which is already registered for moderation is + attempting to be registered again. + + """ + pass + + +class NotModerated(Exception): + """ + Raised when a model which is not registered for moderation is + attempting to be unregistered. + + """ + pass + + +class CommentModerator(object): + """ + Encapsulates comment-moderation options for a given model. + + This class is not designed to be used directly, since it doesn't + enable any of the available moderation options. Instead, subclass + it and override attributes to enable different options:: + + ``auto_close_field`` + If this is set to the name of a ``DateField`` or + ``DateTimeField`` on the model for which comments are + being moderated, new comments for objects of that model + will be disallowed (immediately deleted) when a certain + number of days have passed after the date specified in + that field. Must be used in conjunction with + ``close_after``, which specifies the number of days past + which comments should be disallowed. Default value is + ``None``. + + ``auto_moderate_field`` + Like ``auto_close_field``, but instead of outright + deleting new comments when the requisite number of days + have elapsed, it will simply set the ``is_public`` field + of new comments to ``False`` before saving them. Must be + used in conjunction with ``moderate_after``, which + specifies the number of days past which comments should be + moderated. Default value is ``None``. + + ``close_after`` + If ``auto_close_field`` is used, this must specify the + number of days past the value of the field specified by + ``auto_close_field`` after which new comments for an + object should be disallowed. Default value is ``None``. + + ``email_notification`` + If ``True``, any new comment on an object of this model + which survives moderation will generate an email to site + staff. Default value is ``False``. + + ``enable_field`` + If this is set to the name of a ``BooleanField`` on the + model for which comments are being moderated, new comments + on objects of that model will be disallowed (immediately + deleted) whenever the value of that field is ``False`` on + the object the comment would be attached to. Default value + is ``None``. + + ``moderate_after`` + If ``auto_moderate_field`` is used, this must specify the number + of days past the value of the field specified by + ``auto_moderate_field`` after which new comments for an + object should be marked non-public. Default value is + ``None``. + + Most common moderation needs can be covered by changing these + attributes, but further customization can be obtained by + subclassing and overriding the following methods. Each method will + be called with three arguments: ``comment``, which is the comment + being submitted, ``content_object``, which is the object the + comment will be attached to, and ``request``, which is the + ``HttpRequest`` in which the comment is being submitted:: + + ``allow`` + Should return ``True`` if the comment should be allowed to + post on the content object, and ``False`` otherwise (in + which case the comment will be immediately deleted). + + ``email`` + If email notification of the new comment should be sent to + site staff or moderators, this method is responsible for + sending the email. + + ``moderate`` + Should return ``True`` if the comment should be moderated + (in which case its ``is_public`` field will be set to + ``False`` before saving), and ``False`` otherwise (in + which case the ``is_public`` field will not be changed). + + Subclasses which want to introspect the model for which comments + are being moderated can do so through the attribute ``_model``, + which will be the model class. + + """ + auto_close_field = None + auto_moderate_field = None + close_after = None + email_notification = False + enable_field = None + moderate_after = None + + def __init__(self, model): + self._model = model + + def _get_delta(self, now, then): + """ + Internal helper which will return a ``datetime.timedelta`` + representing the time between ``now`` and ``then``. Assumes + ``now`` is a ``datetime.date`` or ``datetime.datetime`` later + than ``then``. + + If ``now`` and ``then`` are not of the same type due to one of + them being a ``datetime.date`` and the other being a + ``datetime.datetime``, both will be coerced to + ``datetime.date`` before calculating the delta. + + """ + if now.__class__ is not then.__class__: + now = datetime.date(now.year, now.month, now.day) + then = datetime.date(then.year, then.month, then.day) + if now < then: + raise ValueError("Cannot determine moderation rules because date field is set to a value in the future") + return now - then + + def allow(self, comment, content_object, request): + """ + Determine whether a given comment is allowed to be posted on + a given object. + + Return ``True`` if the comment should be allowed, ``False + otherwise. + + """ + if self.enable_field: + if not getattr(content_object, self.enable_field): + return False + if self.auto_close_field and self.close_after is not None: + close_after_date = getattr(content_object, self.auto_close_field) + if close_after_date is not None and self._get_delta(timezone.now(), + close_after_date).days >= self.close_after: + return False + return True + + def moderate(self, comment, content_object, request): + """ + Determine whether a given comment on a given object should be + allowed to show up immediately, or should be marked non-public + and await approval. + + Return ``True`` if the comment should be moderated (marked + non-public), ``False`` otherwise. + + """ + if self.auto_moderate_field and self.moderate_after is not None: + moderate_after_date = getattr(content_object, self.auto_moderate_field) + if moderate_after_date is not None and self._get_delta(timezone.now(), + moderate_after_date).days >= self.moderate_after: + return True + return False + + def email(self, comment, content_object, request): + """ + Send email notification of a new comment to site staff when email + notifications have been requested. + + """ + if not self.email_notification: + return + recipient_list = [manager_tuple[1] for manager_tuple in settings.MANAGERS] + t = loader.get_template('comments/comment_notification_email.txt') + c = { + 'comment': comment, + 'content_object': content_object, + } + subject = _('[%(site)s] New comment posted on "%(object)s"') % { + 'site': get_current_site(request).name, + 'object': content_object, + } + message = t.render(c) + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True) + + +class Moderator(object): + """ + Handles moderation of a set of models. + + An instance of this class will maintain a list of one or more + models registered for comment moderation, and their associated + moderation classes, and apply moderation to all incoming comments. + + To register a model, obtain an instance of ``Moderator`` (this + module exports one as ``moderator``), and call its ``register`` + method, passing the model class and a moderation class (which + should be a subclass of ``CommentModerator``). Note that both of + these should be the actual classes, not instances of the classes. + + To cease moderation for a model, call the ``unregister`` method, + passing the model class. + + For convenience, both ``register`` and ``unregister`` can also + accept a list of model classes in place of a single model; this + allows easier registration of multiple models with the same + ``CommentModerator`` class. + + The actual moderation is applied in two phases: one prior to + saving a new comment, and the other immediately after saving. The + pre-save moderation may mark a comment as non-public or mark it to + be removed; the post-save moderation may delete a comment which + was disallowed (there is currently no way to prevent the comment + being saved once before removal) and, if the comment is still + around, will send any notification emails the comment generated. + + """ + + def __init__(self): + self._registry = {} + self.connect() + + def connect(self): + """ + Hook up the moderation methods to pre- and post-save signals + from the comment models. + + """ + signals.comment_will_be_posted.connect(self.pre_save_moderation, sender=django_comments.get_model()) + signals.comment_was_posted.connect(self.post_save_moderation, sender=django_comments.get_model()) + + def register(self, model_or_iterable, moderation_class): + """ + Register a model or a list of models for comment moderation, + using a particular moderation class. + + Raise ``AlreadyModerated`` if any of the models are already + registered. + + """ + if isinstance(model_or_iterable, ModelBase): + model_or_iterable = [model_or_iterable] + for model in model_or_iterable: + if model in self._registry: + raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.model_name) + self._registry[model] = moderation_class(model) + + def unregister(self, model_or_iterable): + """ + Remove a model or a list of models from the list of models + whose comments will be moderated. + + Raise ``NotModerated`` if any of the models are not currently + registered for moderation. + + """ + if isinstance(model_or_iterable, ModelBase): + model_or_iterable = [model_or_iterable] + for model in model_or_iterable: + if model not in self._registry: + raise NotModerated("The model '%s' is not currently being moderated" % model._meta.model_name) + del self._registry[model] + + def pre_save_moderation(self, sender, comment, request, **kwargs): + """ + Apply any necessary pre-save moderation steps to new + comments. + + """ + model = comment.content_type.model_class() + if model not in self._registry: + return + content_object = comment.content_object + moderation_class = self._registry[model] + + # Comment will be disallowed outright (HTTP 403 response) + if not moderation_class.allow(comment, content_object, request): + return False + + if moderation_class.moderate(comment, content_object, request): + comment.is_public = False + + def post_save_moderation(self, sender, comment, request, **kwargs): + """ + Apply any necessary post-save moderation steps to new + comments. + + """ + model = comment.content_type.model_class() + if model not in self._registry: + return + self._registry[model].email(comment, comment.content_object, request) + +# Import this instance in your own code to use in registering +# your models for moderation. +moderator = Moderator() |