Code

Ticket #9282: 9282.2.diff

File 9282.2.diff, 33.9 KB (added by thejaswi_puthraya, 5 years ago)

git-patch against the latest checkout. Contains tests and docs. Thanks ubernostrum

Line 
1diff --git a/django/contrib/comments/moderators.py b/django/contrib/comments/moderators.py
2new file mode 100644
3index 0000000..13c5064
4--- /dev/null
5+++ b/django/contrib/comments/moderators.py
6@@ -0,0 +1,477 @@
7+"""
8+Copyright (c) 2007, James Bennett
9+All rights reserved.
10+
11+Redistribution and use in source and binary forms, with or without
12+modification, are permitted provided that the following conditions are
13+met:
14+
15+    * Redistributions of source code must retain the above copyright
16+      notice, this list of conditions and the following disclaimer.
17+    * Redistributions in binary form must reproduce the above
18+      copyright notice, this list of conditions and the following
19+      disclaimer in the documentation and/or other materials provided
20+      with the distribution.
21+    * Neither the name of the author nor the names of other
22+      contributors may be used to endorse or promote products derived
23+      from this software without specific prior written permission.
24+
25+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
26+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
27+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
28+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
29+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
30+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
31+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
32+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
33+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
35+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36+"""
37+
38+"""
39+A generic comment-moderation system which allows configuration of
40+moderation options on a per-model basis.
41+
42+To use, do two things:
43+
44+1. Create or import a subclass of ``CommentModerator`` defining the
45+   options you want.
46+
47+2. Import ``moderator`` from this module and register one or more
48+   models, passing the models and the ``CommentModerator`` options
49+   class you want to use.
50+
51+
52+Example
53+-------
54+
55+First, we define a simple model class which might represent entries in
56+a weblog::
57+   
58+    from django.db import models
59+   
60+    class Entry(models.Model):
61+        title = models.CharField(maxlength=250)
62+        body = models.TextField()
63+        pub_date = models.DateField()
64+        enable_comments = models.BooleanField()
65+
66+Then we create a ``CommentModerator`` subclass specifying some
67+moderation options::
68+   
69+    from django.contrib.comments.moderators import CommentModerator, moderator
70+   
71+    class EntryModerator(CommentModerator):
72+        email_notification = True
73+        enable_field = 'enable_comments'
74+
75+And finally register it for moderation::
76+   
77+    moderator.register(Entry, EntryModerator)
78+
79+This sample class would apply several moderation steps to each new
80+comment submitted on an Entry:
81+
82+* If the entry's ``enable_comments`` field is set to ``False``, the
83+  comment will be rejected (immediately deleted).
84+
85+* If the comment is successfully posted, an email notification of the
86+  comment will be sent to site staff.
87+
88+For a full list of built-in moderation options and other
89+configurability, see the documentation for the ``CommentModerator``
90+class.
91+
92+Several example subclasses of ``CommentModerator`` are provided in
93+`django-comment-utils`_, both to provide common moderation options and to
94+demonstrate some of the ways subclasses can customize moderation
95+behavior.
96+
97+.. _`django-comment-utils`: http://code.google.com/p/django-comment-utils/
98+"""
99+
100+
101+import datetime
102+
103+from django.conf import settings
104+from django.core.mail import send_mail
105+from django.db.models import signals
106+from django.db.models.base import ModelBase
107+from django.template import Context, loader
108+from django.contrib import comments
109+from django.contrib.sites.models import Site
110+
111+
112+class AlreadyModerated(Exception):
113+    """
114+    Raised when a model which is already registered for moderation is
115+    attempting to be registered again.
116+   
117+    """
118+    pass
119+
120+
121+class NotModerated(Exception):
122+    """
123+    Raised when a model which is not registered for moderation is
124+    attempting to be unregistered.
125+   
126+    """
127+    pass
128+
129+
130+class CommentModerator(object):
131+    """
132+    Encapsulates comment-moderation options for a given model.
133+   
134+    This class is not designed to be used directly, since it doesn't
135+    enable any of the available moderation options. Instead, subclass
136+    it and override attributes to enable different options::
137+   
138+    ``auto_close_field``
139+        If this is set to the name of a ``DateField`` or
140+        ``DateTimeField`` on the model for which comments are
141+        being moderated, new comments for objects of that model
142+        will be disallowed (immediately deleted) when a certain
143+        number of days have passed after the date specified in
144+        that field. Must be used in conjunction with
145+        ``close_after``, which specifies the number of days past
146+        which comments should be disallowed. Default value is
147+        ``None``.
148+   
149+    ``auto_moderate_field``
150+        Like ``auto_close_field``, but instead of outright
151+        deleting new comments when the requisite number of days
152+        have elapsed, it will simply set the ``is_public`` field
153+        of new comments to ``False`` before saving them. Must be
154+        used in conjunction with ``moderate_after``, which
155+        specifies the number of days past which comments should be
156+        moderated. Default value is ``None``.
157+   
158+    ``close_after``
159+        If ``auto_close_field`` is used, this must specify the
160+        number of days past the value of the field specified by
161+        ``auto_close_field`` after which new comments for an
162+        object should be disallowed. Default value is ``None``.
163+   
164+    ``email_notification``
165+        If ``True``, any new comment on an object of this model
166+        which survives moderation will generate an email to site
167+        staff. Default value is ``False``.
168+   
169+    ``enable_field``
170+        If this is set to the name of a ``BooleanField`` on the
171+        model for which comments are being moderated, new comments
172+        on objects of that model will be disallowed (immediately
173+        deleted) whenever the value of that field is ``False`` on
174+        the object the comment would be attached to. Default value
175+        is ``None``.
176+   
177+    ``moderate_after``
178+        If ``auto_moderate`` is used, this must specify the number
179+        of days past the value of the field specified by
180+        ``auto_moderate_field`` after which new comments for an
181+        object should be marked non-public. Default value is
182+        ``None``.
183+   
184+    Most common moderation needs can be covered by changing these
185+    attributes, but further customization can be obtained by
186+    subclassing and overriding the following methods. Each method will
187+    be called with two arguments: ``comment``, which is the comment
188+    being submitted, and ``content_object``, which is the object the
189+    comment will be attached to::
190+   
191+    ``allow``
192+        Should return ``True`` if the comment should be allowed to
193+        post on the content object, and ``False`` otherwise (in
194+        which case the comment will be immediately deleted).
195+   
196+    ``email``
197+        If email notification of the new comment should be sent to
198+        site staff or moderators, this method is responsible for
199+        sending the email.
200+   
201+    ``moderate``
202+        Should return ``True`` if the comment should be moderated
203+        (in which case its ``is_public`` field will be set to
204+        ``False`` before saving), and ``False`` otherwise (in
205+        which case the ``is_public`` field will not be changed).
206+   
207+    Subclasses which want to introspect the model for which comments
208+    are being moderated can do so through the attribute ``_model``,
209+    which will be the model class.
210+   
211+    """
212+    auto_close_field = None
213+    auto_moderate_field = None
214+    close_after = None
215+    email_notification = False
216+    enable_field = None
217+    moderate_after = None
218+   
219+    def __init__(self, model):
220+        self._model = model
221+   
222+    def _get_delta(self, now, then):
223+        """
224+        Internal helper which will return a ``datetime.timedelta``
225+        representing the time between ``now`` and ``then``. Assumes
226+        ``now`` is a ``datetime.date`` or ``datetime.datetime`` later
227+        than ``then``.
228+       
229+        If ``now`` and ``then`` are not of the same type due to one of
230+        them being a ``datetime.date`` and the other being a
231+        ``datetime.datetime``, both will be coerced to
232+        ``datetime.date`` before calculating the delta.
233+       
234+        """
235+        if now.__class__ is not then.__class__:
236+            now = datetime.date(now.year, now.month, now.day)
237+            then = datetime.date(then.year, then.month, then.day)
238+        if now < then:
239+            raise ValueError("Cannot determine moderation rules because date field is set to a value in the future")
240+        return now - then
241+       
242+    def allow(self, comment, content_object):
243+        """
244+        Determine whether a given comment is allowed to be posted on
245+        a given object.
246+       
247+        Return ``True`` if the comment should be allowed, ``False
248+        otherwise.
249+       
250+        """
251+        if self.enable_field:
252+            if not getattr(content_object, self.enable_field):
253+                return False
254+        if self.auto_close_field and self.close_after:
255+            if self._get_delta(datetime.datetime.now(), getattr(content_object, self.auto_close_field)).days >= self.close_after:
256+                return False
257+        return True
258+   
259+    def moderate(self, comment, content_object):
260+        """
261+        Determine whether a given comment on a given object should be
262+        allowed to show up immediately, or should be marked non-public
263+        and await approval.
264+       
265+        Return ``True`` if the comment should be moderated (marked
266+        non-public), ``False`` otherwise.
267+       
268+        """
269+        if self.auto_moderate_field and self.moderate_after:
270+            if self._get_delta(datetime.datetime.now(), getattr(content_object, self.auto_moderate_field)).days >= self.moderate_after:
271+                return True
272+        return False
273+   
274+    def comments_open(self, obj):
275+        """
276+        Return ``True`` if new comments are being accepted for
277+        ``obj``, ``False`` otherwise.
278+       
279+        The algorithm for determining this is as follows:
280+       
281+        1. If ``enable_field`` is set and the relevant field on
282+           ``obj`` contains a false value, comments are not open.
283+       
284+        2. If ``close_after`` is set and the relevant date field on
285+           ``obj`` is far enough in the past, comments are not open.
286+       
287+        3. If neither of the above checks determined that comments are
288+           not open, comments are open.
289+       
290+        """
291+        if self.enable_field:
292+            if not getattr(obj, self.enable_field):
293+                return False
294+        if self.auto_close_field and self.close_after:
295+            if self._get_delta(datetime.datetime.now(), getattr(obj, self.auto_close_field)).days >= self.close_after:
296+                return False
297+        return True
298+   
299+    def comments_moderated(self, obj):
300+        """
301+        Return ``True`` if new comments for ``obj`` are being
302+        automatically sent to moderation, ``False`` otherwise.
303+       
304+        The algorithm for determining this is as follows:
305+       
306+        1. If ``moderate_field`` is set and the relevant field on
307+           ``obj`` contains a true value, comments are moderated.
308+       
309+        2. If ``moderate_after`` is set and the relevant date field on
310+           ``obj`` is far enough in the past, comments are moderated.
311+       
312+        3. If neither of the above checks decided that comments are
313+           moderated, comments are not moderated.
314+       
315+        """
316+        if self.moderate_field:
317+            if getattr(obj, self.moderate_field):
318+                return True
319+        if self.auto_moderate_field and self.moderate_after:
320+            if self._get_delta(datetime.datetime.now(), getattr(obj, self.auto_moderate_field)).days >= self.moderate_after:
321+                return True
322+        return False
323+   
324+    def email(self, comment, content_object):
325+        """
326+        Send email notification of a new comment to site staff when email
327+        notifications have been requested.
328+       
329+        """
330+        if not self.email_notification:
331+            return
332+        recipient_list = [manager_tuple[1] for manager_tuple in settings.MANAGERS]
333+        t = loader.get_template('comments/comment_notification_email.txt')
334+        c = Context({ 'comment': comment,
335+                      'content_object': content_object })
336+        subject = '[%s] New comment posted on "%s"' % (Site.objects.get_current().name,
337+                                                          content_object)
338+        message = t.render(c)
339+        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True)
340+
341+
342+class Moderator(object):
343+    """
344+    Handles moderation of a set of models.
345+   
346+    An instance of this class will maintain a list of one or more
347+    models registered for comment moderation, and their associated
348+    moderation classes, and apply moderation to all incoming comments.
349+   
350+    To register a model, obtain an instance of ``CommentModerator``
351+    (this module exports one as ``moderator``), and call its
352+    ``register`` method, passing the model class and a moderation
353+    class (which should be a subclass of ``CommentModerator``). Note
354+    that both of these should be the actual classes, not instances of
355+    the classes.
356+   
357+    To cease moderation for a model, call the ``unregister`` method,
358+    passing the model class.
359+   
360+    For convenience, both ``register`` and ``unregister`` can also
361+    accept a list of model classes in place of a single model; this
362+    allows easier registration of multiple models with the same
363+    ``CommentModerator`` class.
364+   
365+    The actual moderation is applied in two phases: one prior to
366+    saving a new comment, and the other immediately after saving. The
367+    pre-save moderation may mark a comment as non-public or mark it to
368+    be removed; the post-save moderation may delete a comment which
369+    was disallowed (there is currently no way to prevent the comment
370+    being saved once before removal) and, if the comment is still
371+    around, will send any notification emails the comment generated.
372+   
373+    """
374+    def __init__(self):
375+        self._registry = {}
376+        self.connect()
377+   
378+    def connect(self):
379+        """
380+        Hook up the moderation methods to pre- and post-save signals
381+        from the comment models.
382+       
383+        """
384+        signals.pre_save.connect(self.pre_save_moderation, sender=comments.get_model())
385+        signals.post_save.connect(self.post_save_moderation, sender=comments.get_model())
386+   
387+    def register(self, model_or_iterable, moderation_class):
388+        """
389+        Register a model or a list of models for comment moderation,
390+        using a particular moderation class.
391+       
392+        Raise ``AlreadyModerated`` if any of the models are already
393+        registered.
394+       
395+        """
396+        if isinstance(model_or_iterable, ModelBase):
397+            model_or_iterable = [model_or_iterable]
398+        for model in model_or_iterable:
399+            if model in self._registry:
400+                raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.module_name)
401+            self._registry[model] = moderation_class(model)
402+   
403+    def unregister(self, model_or_iterable):
404+        """
405+        Remove a model or a list of models from the list of models
406+        whose comments will be moderated.
407+       
408+        Raise ``NotModerated`` if any of the models are not currently
409+        registered for moderation.
410+       
411+        """
412+        if isinstance(model_or_iterable, ModelBase):
413+            model_or_iterable = [model_or_iterable]
414+        for model in model_or_iterable:
415+            if model not in self._registry:
416+                raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name)
417+            del self._registry[model]
418+   
419+    def pre_save_moderation(self, sender, instance, **kwargs):
420+        """
421+        Apply any necessary pre-save moderation steps to new
422+        comments.
423+       
424+        """
425+        model = instance.content_type.model_class()
426+        if instance.id or (model not in self._registry):
427+            return
428+        content_object = instance.content_object
429+        moderation_class = self._registry[model]
430+        if not moderation_class.allow(instance, content_object): # Comment will get deleted in post-save hook.
431+            instance.moderation_disallowed = True
432+            return
433+        if moderation_class.moderate(instance, content_object):
434+            instance.is_public = False
435+   
436+    def post_save_moderation(self, sender, instance, **kwargs):
437+        """
438+        Apply any necessary post-save moderation steps to new
439+        comments.
440+       
441+        """
442+        model = instance.content_type.model_class()
443+        if model not in self._registry:
444+            return
445+        if hasattr(instance, 'moderation_disallowed'):
446+            instance.delete()
447+            return
448+        self._registry[model].email(instance, instance.content_object)
449+
450+    def comments_open(self, obj):
451+        """
452+        Return ``True`` if new comments are being accepted for
453+        ``obj``, ``False`` otherwise.
454+       
455+        If no moderation rules have been registered for the model of
456+        which ``obj`` is an instance, comments are assumed to be open
457+        for that object.
458+       
459+        """
460+        model = obj.__class__
461+        if model not in self._registry:
462+            return True
463+        return self._registry[model].comments_open(obj)
464+
465+    def comments_moderated(self, obj):
466+        """
467+        Return ``True`` if new comments for ``obj`` are being
468+        automatically sent to moderation, ``False`` otherwise.
469+       
470+        If no moderation rules have been registered for the model of
471+        which ``obj`` is an instance, comments for that object are
472+        assumed not to be moderated.
473+       
474+        """
475+        model = obj.__class__
476+        if model not in self._registry:
477+            return False
478+        return self._registry[model].comments_moderated(obj)
479+
480+
481+# Import this instance in your own code to use in registering
482+# your models for moderation.
483+moderator = Moderator()
484diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt
485index 5aeebe3..2bca94e 100644
486--- a/docs/ref/contrib/comments/index.txt
487+++ b/docs/ref/contrib/comments/index.txt
488@@ -212,4 +212,5 @@ More information
489    settings
490    signals
491    upgrade
492+   moderators
493 
494diff --git a/docs/ref/contrib/comments/moderators.txt b/docs/ref/contrib/comments/moderators.txt
495new file mode 100644
496index 0000000..8c1713e
497--- /dev/null
498+++ b/docs/ref/contrib/comments/moderators.txt
499@@ -0,0 +1,208 @@
500+==========================
501+Generic comment moderation
502+==========================
503+
504+Django's bundled comments application is extremely useful on its own,
505+but the amount of comment spam circulating on the Web today
506+essentially makes it necessary to have some sort of automatic
507+moderation system in place for any application which makes use of
508+comments. To make this easier to handle in a consistent fashion,
509+``django.contrib.comments.moderators`` (based on `comment_utils`_)
510+provides a generic, extensible comment-moderation system which can
511+be applied to any model or set of models which want to make use of
512+Django's comment system.
513+
514+.. _`comment_utils`: http://code.google.com/p/django-comment-utils/
515+
516+
517+Overview
518+========
519+
520+The entire system is contained within ``django.contrib.comments.moderators``,
521+and uses a two-step process to enable moderation for any given model:
522+
523+1. A subclass of ``django.contrib.comments.moderators.CommentModerator`` is
524+   defined which specifies the moderation options the model wants to
525+   enable.
526+
527+2. The model is registered with the moderation system, passing in the
528+   model class and the class which specifies its moderation options.
529+
530+A simple example is the best illustration of this. Suppose we have the
531+following model, which would represent entries in a weblog::
532+
533+    from django.db import models
534+   
535+    class Entry(models.Model):
536+        title = models.CharField(maxlength=250)
537+        body = models.TextField()
538+        pub_date = models.DateTimeField()
539+        enable_comments = models.BooleanField()
540+
541+Now, suppose that we want the following steps to be applied whenever a
542+new comment is posted on an ``Entry``:
543+
544+1. If the ``Entry``'s ``enable_comments`` field is ``False``, the
545+   comment will simply be disallowed (i.e., immediately deleted).
546+
547+2. If the ``enable_comments`` field is ``True``, the comment will be
548+   allowed to save.
549+
550+3. Once the comment is saved, an email should be sent to site staff
551+   notifying them of the new comment.
552+
553+Accomplishing this is fairly straightforward and requires very little
554+code::
555+
556+    from django.contrib.comments.moderators import CommentModerator, moderator
557+   
558+    class EntryModerator(CommentModerator):
559+        email_notification = True
560+        enable_field = 'enable_comments'
561+   
562+    moderator.register(Entry, EntryModerator)
563+
564+The ``CommentModerator`` class pre-defines a number of useful
565+moderation options which subclasses can enable or disable as desired,
566+and ``moderator`` knows how to work with them to determine whether to
567+allow a comment, whether to moderate a comment which will be allowed
568+to post, and whether to email notifications of new comments.
569+
570+
571+Built-in moderation options
572+---------------------------
573+
574+Most common comment-moderation needs can be handled by subclassing
575+``CommentModerator`` and changing the values of pre-defined
576+attributes; the full range of built-in options is as follows:
577+
578+    ``auto_close_field``
579+        If this is set to the name of a ``DateField`` or
580+        ``DateTimeField`` on the model for which comments are being
581+        moderated, new comments for objects of that model will be
582+        disallowed (immediately deleted) when a certain number of days
583+        have passed after the date specified in that field. Must be
584+        used in conjunction with ``close_after``, which specifies the
585+        number of days past which comments should be
586+        disallowed. Default value is ``None``.
587+   
588+    ``auto_moderate_field``
589+        Like ``auto_close_field``, but instead of outright deleting
590+        new comments when the requisite number of days have elapsed,
591+        it will simply set the ``is_public`` field of new comments to
592+        ``False`` before saving them. Must be used in conjunction with
593+        ``moderate_after``, which specifies the number of days past
594+        which comments should be moderated. Default value is ``None``.
595+   
596+    ``close_after``
597+        If ``auto_close_field`` is used, this must specify the number
598+        of days past the value of the field specified by
599+        ``auto_close_field`` after which new comments for an object
600+        should be disallowed. Default value is ``None``.
601+   
602+    ``email_notification``
603+        If ``True``, any new comment on an object of this model which
604+        survives moderation (i.e., is not deleted) will generate an
605+        email to site staff. Default value is ``False``.
606+   
607+    ``enable_field``
608+        If this is set to the name of a ``BooleanField`` on the model
609+        for which comments are being moderated, new comments on
610+        objects of that model will be disallowed (immediately deleted)
611+        whenever the value of that field is ``False`` on the object
612+        the comment would be attached to. Default value is ``None``.
613+   
614+    ``moderate_after``
615+        If ``auto_moderate`` is used, this must specify the number of
616+        days past the value of the field specified by
617+        ``auto_moderate_field`` after which new comments for an object
618+        should be marked non-public. Default value is ``None``.
619+
620+Simply subclassing ``CommentModerator`` and changing the values of
621+these options will automatically enable the various moderation methods
622+for any models registered using the subclass.
623+
624+
625+Adding custom moderation methods
626+--------------------------------
627+
628+For situations where the built-in options listed above are not
629+sufficient, subclasses of ``CommentModerator`` can also override the
630+methods which actually perform the moderation, and apply any logic
631+they desire. ``CommentModerator`` defines three methods which
632+determine how moderation will take place; each method will be called
633+by the moderation system and passed two arguments: ``comment``, which
634+is the new comment being posted, and ``content_object``, which is the
635+object the comment will be attached to:
636+
637+    ``allow``
638+        Should return ``True`` if the comment should be allowed to
639+        post on the content object, and ``False`` otherwise (in which
640+        case the comment will be immediately deleted).
641+   
642+    ``email``
643+        If email notification of the new comment should be sent to
644+        site staff or moderators, this method is responsible for
645+        sending the email.
646+   
647+    ``moderate``
648+        Should return ``True`` if the comment should be moderated (in
649+        which case its ``is_public`` field will be set to ``False``
650+        before saving), and ``False`` otherwise (in which case the
651+        ``is_public`` field will not be changed).
652+
653+
654+Registering models for moderation
655+---------------------------------
656+
657+The moderation system, represented by
658+``django.contrib.comments.moderators.moderator`` is an instance of the class
659+``django.contrib.comments.moderators.Moderator``, which allows registration and
660+"unregistration" of models via two methods:
661+
662+    ``register``
663+        Takes two arguments: the first should be either a model class
664+        or list of model classes, and the second should be a subclass
665+        of ``CommentModerator``, and register the model or models to
666+        be moderated using the options defined in the
667+        ``CommentModerator`` subclass. If any of the models are
668+        already registered for moderation, the exception
669+        ``django.contrib.comments.moderators.AlreadyModerated`` will be raised.
670+
671+    ``unregister``
672+        Takes one argument: a model class or list of model classes,
673+        and removes the model or models from the set of models which
674+        are being moderated. If any of the models are not currently
675+        being moderated, the exception
676+        ``django.contrib.comments.moderators.NotModerated`` will be raised.
677+
678+
679+Customizing the moderation system
680+---------------------------------
681+
682+Most use cases will work easily with simple subclassing of
683+``CommentModerator`` and registration with the provided ``moderator``
684+instance, but customization of global moderation behavior can be
685+achieved by subclassing ``Moderator`` and instead registering models
686+with an instance of the subclass.
687+
688+In addition to the ``register`` and ``unregister`` methods detailed
689+above, the following methods on ``Moderator`` can be overridden to
690+achieve customized behavior:
691+
692+    ``connect``
693+        Determines how moderation is set up globally. The base
694+        implementation in ``Moderator`` does this by attaching
695+        listeners to the ``pre_save`` and ``post_save`` signals from
696+        the comment models.
697+
698+    ``pre_save_moderation``
699+        In the base implementation, applies all pre-save moderation
700+        steps (such as determining whether the comment needs to be
701+        deleted, or whether it needs to be marked as non-public or
702+        generate an email).
703+
704+    ``post_save_moderation``
705+        In the base implementation, applies all post-save moderation
706+        steps (currently this consists entirely of deleting comments
707+        which were disallowed).
708diff --git a/tests/regressiontests/comment_tests/fixtures/comment_utils.xml b/tests/regressiontests/comment_tests/fixtures/comment_utils.xml
709new file mode 100644
710index 0000000..a39bbf6
711--- /dev/null
712+++ b/tests/regressiontests/comment_tests/fixtures/comment_utils.xml
713@@ -0,0 +1,15 @@
714+<?xml version="1.0" encoding="utf-8"?>
715+<django-objects version="1.0">
716+  <object pk="1" model="comment_tests.entry">
717+      <field type="CharField" name="title">ABC</field>
718+      <field type="TextField" name="body">This is the body</field>
719+      <field type="DateField" name="pub_date">2008-01-01</field>
720+      <field type="BooleanField" name="enable_comments">True</field>
721+  </object>
722+  <object pk="2" model="comment_tests.entry">
723+      <field type="CharField" name="title">XYZ</field>
724+      <field type="TextField" name="body">Text here</field>
725+      <field type="DateField" name="pub_date">2008-01-02</field>
726+      <field type="BooleanField" name="enable_comments">False</field>
727+  </object>
728+</django-objects>
729diff --git a/tests/regressiontests/comment_tests/models.py b/tests/regressiontests/comment_tests/models.py
730index 28022e2..62f4168 100644
731--- a/tests/regressiontests/comment_tests/models.py
732+++ b/tests/regressiontests/comment_tests/models.py
733@@ -20,3 +20,11 @@ class Article(models.Model):
734     def __str__(self):
735         return self.headline
736 
737+class Entry(models.Model):
738+    title = models.CharField(max_length=250)
739+    body = models.TextField()
740+    pub_date = models.DateField()
741+    enable_comments = models.BooleanField()
742+
743+    def __str__(self):
744+        return self.title
745diff --git a/tests/regressiontests/comment_tests/tests/__init__.py b/tests/regressiontests/comment_tests/tests/__init__.py
746index 09026aa..449fea4 100644
747--- a/tests/regressiontests/comment_tests/tests/__init__.py
748+++ b/tests/regressiontests/comment_tests/tests/__init__.py
749@@ -86,3 +86,4 @@ from regressiontests.comment_tests.tests.comment_form_tests import *
750 from regressiontests.comment_tests.tests.templatetag_tests import *
751 from regressiontests.comment_tests.tests.comment_view_tests import *
752 from regressiontests.comment_tests.tests.moderation_view_tests import *
753+from regressiontests.comment_tests.tests.comment_utils_moderators_tests import *
754diff --git a/tests/regressiontests/comment_tests/tests/comment_utils_moderators_tests.py b/tests/regressiontests/comment_tests/tests/comment_utils_moderators_tests.py
755new file mode 100644
756index 0000000..4e6a03d
757--- /dev/null
758+++ b/tests/regressiontests/comment_tests/tests/comment_utils_moderators_tests.py
759@@ -0,0 +1,70 @@
760+from regressiontests.comment_tests.tests import CommentTestCase, CT, Site
761+from django.contrib.comments.models import Comment
762+from django.contrib.comments.moderators import moderator, CommentModerator, AlreadyModerated
763+from regressiontests.comment_tests.models import Entry
764+from django.core import mail
765+
766+class EntryModerator1(CommentModerator):
767+    email_notification = True
768+
769+class EntryModerator2(CommentModerator):
770+    enable_field = 'enable_comments'
771+
772+class EntryModerator3(CommentModerator):
773+    auto_close_field = 'pub_date'
774+    close_after = 7
775+
776+class EntryModerator4(CommentModerator):
777+    auto_moderate_field = 'pub_date'
778+    moderate_after = 7
779+
780+class CommentUtilsModeratorTests(CommentTestCase):
781+    fixtures = ["comment_utils.xml"]
782+
783+    def createSomeComments(self):
784+        c1 = Comment.objects.create(
785+            content_type = CT(Entry),
786+            object_pk = "1",
787+            user_name = "Joe Somebody",
788+            user_email = "jsomebody@example.com",
789+            user_url = "http://example.com/~joe/",
790+            comment = "First!",
791+            site = Site.objects.get_current(),
792+        )
793+        c2 = Comment.objects.create(
794+            content_type = CT(Entry),
795+            object_pk = "2",
796+            user_name = "Joe the Plumber",
797+            user_email = "joetheplumber@whitehouse.gov",
798+            user_url = "http://example.com/~joe/",
799+            comment = "Second!",
800+            site = Site.objects.get_current(),
801+        )
802+        return c1, c2
803+
804+    def tearDown(self):
805+        moderator.unregister(Entry)
806+
807+    def testRegisterExistingModel(self):
808+        moderator.register(Entry, EntryModerator1)
809+        self.assertRaises(AlreadyModerated, moderator.register, Entry, EntryModerator1)
810+
811+    def testEmailNotification(self):
812+        moderator.register(Entry, EntryModerator1)
813+        c1, c2 = self.createSomeComments()
814+        self.assertEquals(len(mail.outbox), 2)
815+
816+    def testCommentsEnabled(self):
817+        moderator.register(Entry, EntryModerator2)
818+        c1, c2 = self.createSomeComments()
819+        self.assertEquals(Comment.objects.all().count(), 1)
820+
821+    def testAutoCloseField(self):
822+        moderator.register(Entry, EntryModerator3)
823+        c1, c2 = self.createSomeComments()
824+        self.assertEquals(Comment.objects.all().count(), 0)       
825+
826+    def testAutoModerateField(self):
827+        moderator.register(Entry, EntryModerator4)
828+        c1, c2 = self.createSomeComments()
829+        self.assertEquals(c2.is_public, False)       
830diff --git a/tests/runtests.py b/tests/runtests.py
831index cc9594b..656d669 100755
832--- a/tests/runtests.py
833+++ b/tests/runtests.py
834@@ -110,6 +110,10 @@ def django_tests(verbosity, interactive, test_labels):
835         'django.middleware.common.CommonMiddleware',
836     )
837     settings.SITE_ID = 1
838+    # For testing comment-utils, we require the MANAGERS attribute
839+    # to be set, so that a test email is sent out which we catch
840+    # in our tests.
841+    settings.MANAGERS = ("admin@djangoproject.com",)
842 
843     # Load all the ALWAYS_INSTALLED_APPS.
844     # (This import statement is intentionally delayed until after we
845diff --git a/tests/templates/comments/comment_notification_email.txt b/tests/templates/comments/comment_notification_email.txt
846new file mode 100644
847index 0000000..63f1493
848--- /dev/null
849+++ b/tests/templates/comments/comment_notification_email.txt
850@@ -0,0 +1,3 @@
851+A comment has been posted on {{ content_object }}.
852+The comment reads as follows:
853+{{ comment }}