Code

Ticket #9282: 9282.diff

File 9282.diff, 18.7 KB (added by thejaswi_puthraya, 6 years ago)

svn-patch against latest checkout

Line 
1Index: django/contrib/comments/moderators.py
2===================================================================
3--- django/contrib/comments/moderators.py       (revision 0)
4+++ django/contrib/comments/moderators.py       (revision 0)
5@@ -0,0 +1,478 @@
6+"""
7+Copyright (c) 2007, James Bennett
8+All rights reserved.
9+
10+Redistribution and use in source and binary forms, with or without
11+modification, are permitted provided that the following conditions are
12+met:
13+
14+    * Redistributions of source code must retain the above copyright
15+      notice, this list of conditions and the following disclaimer.
16+    * Redistributions in binary form must reproduce the above
17+      copyright notice, this list of conditions and the following
18+      disclaimer in the documentation and/or other materials provided
19+      with the distribution.
20+    * Neither the name of the author nor the names of other
21+      contributors may be used to endorse or promote products derived
22+      from this software without specific prior written permission.
23+
24+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35+"""
36+
37+"""
38+A generic comment-moderation system which allows configuration of
39+moderation options on a per-model basis.
40+
41+To use, do two things:
42+
43+1. Create or import a subclass of ``CommentModerator`` defining the
44+   options you want.
45+
46+2. Import ``moderator`` from this module and register one or more
47+   models, passing the models and the ``CommentModerator`` options
48+   class you want to use.
49+
50+
51+Example
52+-------
53+
54+First, we define a simple model class which might represent entries in
55+a weblog::
56+   
57+    from django.db import models
58+   
59+    class Entry(models.Model):
60+        title = models.CharField(maxlength=250)
61+        body = models.TextField()
62+        pub_date = models.DateField()
63+        enable_comments = models.BooleanField()
64+
65+Then we create a ``CommentModerator`` subclass specifying some
66+moderation options::
67+   
68+    from comment_utils.moderation import CommentModerator, moderator
69+   
70+    class EntryModerator(CommentModerator):
71+        email_notification = True
72+        enable_field = 'enable_comments'
73+
74+And finally register it for moderation::
75+   
76+    moderator.register(Entry, EntryModerator)
77+
78+This sample class would apply several moderation steps to each new
79+comment submitted on an Entry:
80+
81+* If the entry's ``enable_comments`` field is set to ``False``, the
82+  comment will be rejected (immediately deleted).
83+
84+* If the comment is successfully posted, an email notification of the
85+  comment will be sent to site staff.
86+
87+For a full list of built-in moderation options and other
88+configurability, see the documentation for the ``CommentModerator``
89+class.
90+
91+Several example subclasses of ``CommentModerator`` are provided in
92+`django-comment-utils`_, both to provide common moderation options and to
93+demonstrate some of the ways subclasses can customize moderation
94+behavior.
95+
96+.. _`django-comment-utils`: http://code.google.com/p/django-comment-utils/
97+"""
98+
99+
100+import datetime
101+
102+from django.conf import settings
103+from django.core.mail import send_mail
104+from django.db.models import signals
105+from django.db.models.base import ModelBase
106+from django.template import Context, loader
107+from django.contrib.comments.models import Comment
108+from django.contrib.sites.models import Site
109+
110+
111+class AlreadyModerated(Exception):
112+    """
113+    Raised when a model which is already registered for moderation is
114+    attempting to be registered again.
115+   
116+    """
117+    pass
118+
119+
120+class NotModerated(Exception):
121+    """
122+    Raised when a model which is not registered for moderation is
123+    attempting to be unregistered.
124+   
125+    """
126+    pass
127+
128+
129+class CommentModerator(object):
130+    """
131+    Encapsulates comment-moderation options for a given model.
132+   
133+    This class is not designed to be used directly, since it doesn't
134+    enable any of the available moderation options. Instead, subclass
135+    it and override attributes to enable different options::
136+   
137+    ``auto_close_field``
138+        If this is set to the name of a ``DateField`` or
139+        ``DateTimeField`` on the model for which comments are
140+        being moderated, new comments for objects of that model
141+        will be disallowed (immediately deleted) when a certain
142+        number of days have passed after the date specified in
143+        that field. Must be used in conjunction with
144+        ``close_after``, which specifies the number of days past
145+        which comments should be disallowed. Default value is
146+        ``None``.
147+   
148+    ``auto_moderate_field``
149+        Like ``auto_close_field``, but instead of outright
150+        deleting new comments when the requisite number of days
151+        have elapsed, it will simply set the ``is_public`` field
152+        of new comments to ``False`` before saving them. Must be
153+        used in conjunction with ``moderate_after``, which
154+        specifies the number of days past which comments should be
155+        moderated. Default value is ``None``.
156+   
157+    ``close_after``
158+        If ``auto_close_field`` is used, this must specify the
159+        number of days past the value of the field specified by
160+        ``auto_close_field`` after which new comments for an
161+        object should be disallowed. Default value is ``None``.
162+   
163+    ``email_notification``
164+        If ``True``, any new comment on an object of this model
165+        which survives moderation will generate an email to site
166+        staff. Default value is ``False``.
167+   
168+    ``enable_field``
169+        If this is set to the name of a ``BooleanField`` on the
170+        model for which comments are being moderated, new comments
171+        on objects of that model will be disallowed (immediately
172+        deleted) whenever the value of that field is ``False`` on
173+        the object the comment would be attached to. Default value
174+        is ``None``.
175+   
176+    ``moderate_after``
177+        If ``auto_moderate`` is used, this must specify the number
178+        of days past the value of the field specified by
179+        ``auto_moderate_field`` after which new comments for an
180+        object should be marked non-public. Default value is
181+        ``None``.
182+   
183+    Most common moderation needs can be covered by changing these
184+    attributes, but further customization can be obtained by
185+    subclassing and overriding the following methods. Each method will
186+    be called with two arguments: ``comment``, which is the comment
187+    being submitted, and ``content_object``, which is the object the
188+    comment will be attached to::
189+   
190+    ``allow``
191+        Should return ``True`` if the comment should be allowed to
192+        post on the content object, and ``False`` otherwise (in
193+        which case the comment will be immediately deleted).
194+   
195+    ``email``
196+        If email notification of the new comment should be sent to
197+        site staff or moderators, this method is responsible for
198+        sending the email.
199+   
200+    ``moderate``
201+        Should return ``True`` if the comment should be moderated
202+        (in which case its ``is_public`` field will be set to
203+        ``False`` before saving), and ``False`` otherwise (in
204+        which case the ``is_public`` field will not be changed).
205+   
206+    Subclasses which want to introspect the model for which comments
207+    are being moderated can do so through the attribute ``_model``,
208+    which will be the model class.
209+   
210+    """
211+    auto_close_field = None
212+    auto_moderate_field = None
213+    close_after = None
214+    email_notification = False
215+    enable_field = None
216+    moderate_after = None
217+   
218+    def __init__(self, model):
219+        self._model = model
220+   
221+    def _get_delta(self, now, then):
222+        """
223+        Internal helper which will return a ``datetime.timedelta``
224+        representing the time between ``now`` and ``then``. Assumes
225+        ``now`` is a ``datetime.date`` or ``datetime.datetime`` later
226+        than ``then``.
227+       
228+        If ``now`` and ``then`` are not of the same type due to one of
229+        them being a ``datetime.date`` and the other being a
230+        ``datetime.datetime``, both will be coerced to
231+        ``datetime.date`` before calculating the delta.
232+       
233+        """
234+        if now.__class__ is not then.__class__:
235+            now = datetime.date(now.year, now.month, now.day)
236+            then = datetime.date(then.year, then.month, then.day)
237+        if now < then:
238+            raise ValueError("Cannot determine moderation rules because date field is set to a value in the future")
239+        return now - then
240+       
241+    def allow(self, comment, content_object):
242+        """
243+        Determine whether a given comment is allowed to be posted on
244+        a given object.
245+       
246+        Return ``True`` if the comment should be allowed, ``False
247+        otherwise.
248+       
249+        """
250+        if self.enable_field:
251+            if not getattr(content_object, self.enable_field):
252+                return False
253+        if self.auto_close_field and self.close_after:
254+            if self._get_delta(datetime.datetime.now(), getattr(content_object, self.auto_close_field)).days >= self.close_after:
255+                return False
256+        return True
257+   
258+    def moderate(self, comment, content_object):
259+        """
260+        Determine whether a given comment on a given object should be
261+        allowed to show up immediately, or should be marked non-public
262+        and await approval.
263+       
264+        Return ``True`` if the comment should be moderated (marked
265+        non-public), ``False`` otherwise.
266+       
267+        """
268+        if self.auto_moderate_field and self.moderate_after:
269+            if self._get_delta(datetime.datetime.now(), getattr(content_object, self.auto_moderate_field)).days >= self.moderate_after:
270+                return True
271+        return False
272+   
273+    def comments_open(self, obj):
274+        """
275+        Return ``True`` if new comments are being accepted for
276+        ``obj``, ``False`` otherwise.
277+       
278+        The algorithm for determining this is as follows:
279+       
280+        1. If ``enable_field`` is set and the relevant field on
281+           ``obj`` contains a false value, comments are not open.
282+       
283+        2. If ``close_after`` is set and the relevant date field on
284+           ``obj`` is far enough in the past, comments are not open.
285+       
286+        3. If neither of the above checks determined that comments are
287+           not open, comments are open.
288+       
289+        """
290+        if self.enable_field:
291+            if not getattr(obj, self.enable_field):
292+                return False
293+        if self.auto_close_field and self.close_after:
294+            if self._get_delta(datetime.datetime.now(), getattr(obj, self.auto_close_field)).days >= self.close_after:
295+                return False
296+        return True
297+   
298+    def comments_moderated(self, obj):
299+        """
300+        Return ``True`` if new comments for ``obj`` are being
301+        automatically sent to moderation, ``False`` otherwise.
302+       
303+        The algorithm for determining this is as follows:
304+       
305+        1. If ``moderate_field`` is set and the relevant field on
306+           ``obj`` contains a true value, comments are moderated.
307+       
308+        2. If ``moderate_after`` is set and the relevant date field on
309+           ``obj`` is far enough in the past, comments are moderated.
310+       
311+        3. If neither of the above checks decided that comments are
312+           moderated, comments are not moderated.
313+       
314+        """
315+        if self.moderate_field:
316+            if getattr(obj, self.moderate_field):
317+                return True
318+        if self.auto_moderate_field and self.moderate_after:
319+            if self._get_delta(datetime.datetime.now(), getattr(obj, self.auto_moderate_field)).days >= self.moderate_after:
320+                return True
321+        return False
322+   
323+    def email(self, comment, content_object):
324+        """
325+        Send email notification of a new comment to site staff when email
326+        notifications have been requested.
327+       
328+        """
329+        if not self.email_notification:
330+            return
331+        recipient_list = [manager_tuple[1] for manager_tuple in settings.MANAGERS]
332+        t = loader.get_template('comments/comment_notification_email.txt')
333+        c = Context({ 'comment': comment,
334+                      'content_object': content_object })
335+        subject = '[%s] New comment posted on "%s"' % (Site.objects.get_current().name,
336+                                                          content_object)
337+        message = t.render(c)
338+        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True)
339+
340+
341+class Moderator(object):
342+    """
343+    Handles moderation of a set of models.
344+   
345+    An instance of this class will maintain a list of one or more
346+    models registered for comment moderation, and their associated
347+    moderation classes, and apply moderation to all incoming comments.
348+   
349+    To register a model, obtain an instance of ``CommentModerator``
350+    (this module exports one as ``moderator``), and call its
351+    ``register`` method, passing the model class and a moderation
352+    class (which should be a subclass of ``CommentModerator``). Note
353+    that both of these should be the actual classes, not instances of
354+    the classes.
355+   
356+    To cease moderation for a model, call the ``unregister`` method,
357+    passing the model class.
358+   
359+    For convenience, both ``register`` and ``unregister`` can also
360+    accept a list of model classes in place of a single model; this
361+    allows easier registration of multiple models with the same
362+    ``CommentModerator`` class.
363+   
364+    The actual moderation is applied in two phases: one prior to
365+    saving a new comment, and the other immediately after saving. The
366+    pre-save moderation may mark a comment as non-public or mark it to
367+    be removed; the post-save moderation may delete a comment which
368+    was disallowed (there is currently no way to prevent the comment
369+    being saved once before removal) and, if the comment is still
370+    around, will send any notification emails the comment generated.
371+   
372+    """
373+    def __init__(self):
374+        self._registry = {}
375+        self.connect()
376+   
377+    def connect(self):
378+        """
379+        Hook up the moderation methods to pre- and post-save signals
380+        from the comment models.
381+       
382+        """
383+        for model in Comment:
384+            signals.pre_save.connect(self.pre_save_moderation, sender=model)
385+            signals.post_save.connect(self.post_save_moderation, sender=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.get_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.get_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()