/django/contrib/comments/moderation.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()