356 lines
13 KiB
Python
356 lines
13 KiB
Python
"""
|
|
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(maxlength=250)
|
|
body = models.TextField()
|
|
pub_date = models.DateField()
|
|
enable_comments = models.BooleanField()
|
|
|
|
Then we create a ``CommentModerator`` subclass specifying some
|
|
moderation options::
|
|
|
|
from django.contrib.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.core.mail import send_mail
|
|
from django.contrib.comments import signals
|
|
from django.db.models.base import ModelBase
|
|
from django.template import Context, loader
|
|
from django.contrib import comments
|
|
from django.contrib.sites.models import Site
|
|
|
|
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(datetime.datetime.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(datetime.datetime.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 = Context({ 'comment': comment,
|
|
'content_object': content_object })
|
|
subject = '[%s] New comment posted on "%s"' % (Site.objects.get_current().name,
|
|
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=comments.get_model())
|
|
signals.comment_was_posted.connect(self.post_save_moderation, sender=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.module_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.module_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()
|