Commits

Patrick Samson committed 0151da8

Add an optional auto_moderators parameter to the pm_write() API function

Comments (0)

Files changed (3)

 Broadcast a message to multiple Users.
 
 For an easier cleanup, all these messages are directly marked as archived and deleted on the sender side.
+The message is expected to be issued from a trusted application, so moderation
+is not necessary and the status is automatically set to 'accepted'.
 
 Arguments: (sender, recipients, subject, body='', skip_notification=False)
 
 Write a message to a User.
 
 Contrary to pm_broadcast(), the message is archived and/or deleted on the sender side only if requested.
+The message may come from an untrusted application, a gateway for example,
+so it may be useful to involve some auto moderators in the processing.
 
-Arguments: (sender, recipient, subject, body='', skip_notification=False, auto_archive=False, auto_delete=False)
+Arguments: (sender, recipient, subject, body='', skip_notification=False,
+auto_archive=False, auto_delete=False, auto_moderators=[])
 
 Arguments
 ---------
 * ``auto_archive``: to mark the message as archived on the sender side
 * ``auto_delete``: to mark the message as deleted on the sender side
+* ``auto_moderators``: a list of auto-moderation functions
 * ``body``: the contents of the message
 * ``recipient``: a User instance
 * ``recipients``: a list or tuple of User instances, or a single User instance
 def pm_broadcast(sender, recipients, subject, body='', skip_notification=False):
     """
     Broadcast a message to multiple Users.
-    For an easier cleanup, all these messages are directly marked as archived and deleted on the sender side.
+    For an easier cleanup, all these messages are directly marked as archived
+    and deleted on the sender side.
+    The message is expected to be issued from a trusted application, so moderation
+    is not necessary and the status is automatically set to 'accepted'.
 
     Optional argument:
         ``skip_notification``: if the normal notification event is not wished
         if not skip_notification:
             message.notify_users(STATUS_PENDING)
 
-def pm_write(sender, recipient, subject, body='', skip_notification=False, auto_archive=False, auto_delete=False):
+def pm_write(sender, recipient, subject, body='', skip_notification=False,
+        auto_archive=False, auto_delete=False, auto_moderators=None):
     """
     Write a message to a User.
-    Contrary to pm_broadcast(), the message is archived and/or deleted on the sender side only if requested.
+    Contrary to pm_broadcast(), the message is archived and/or deleted on
+    the sender side only if requested.
+    The message may come from an untrusted application, a gateway for example,
+    so it may be useful to involve some auto moderators in the processing.
 
     Optional arguments:
         ``skip_notification``: if the normal notification event is not wished
         ``auto_archive``: to mark the message as archived on the sender side
         ``auto_delete``: to mark the message as deleted on the sender side
+        ``auto_moderators``: a list of auto-moderation functions
     """
-    message = Message(subject=subject, body=body, sender=sender, recipient=recipient,
-        moderation_status=STATUS_ACCEPTED, moderation_date=now())
+    message = Message(subject=subject, body=body, sender=sender, recipient=recipient)
+    initial_status = message.moderation_status
+    if auto_moderators:
+        message.auto_moderate(auto_moderators)
+    else:
+        message.moderation_status = STATUS_ACCEPTED
+    message.clean_moderation(initial_status)
     if auto_archive:
         message.sender_archived = True
     if auto_delete:
         message.sender_deleted_at = now()
     message.save()
     if not skip_notification:
-        message.notify_users(STATUS_PENDING)
+        message.notify_users(initial_status)
 
     def setUp(self):
         deactivate()    # necessary for 1.4 to consider a new settings.LANGUAGE_CODE; 1.3 is fine with or without
-        settings.LANGUAGE_CODE = 'en' # do not bother about translation
+        settings.LANGUAGE_CODE = 'en'  # do not bother about translation
         for a in (
             'POSTMAN_DISALLOW_ANONYMOUS',
             'POSTMAN_DISALLOW_MULTIRECIPIENTS',
                 delattr(settings, a)
         settings.POSTMAN_MAILER_APP = None
         settings.POSTMAN_AUTOCOMPLETER_APP = {
-            'arg_default': 'postman_single_as1-1', # no default, mandatory to enable the feature
+            'arg_default': 'postman_single_as1-1',  # no default, mandatory to enable the feature
         }
         self.reload_modules()
 
         "Check that a date is now. Well... almost."
         delta = dt - now()
         seconds = delta.days * (24*60*60) + delta.seconds
-        self.assert_(-2 <= seconds <= 1) # -1 is not enough for Mysql
+        self.assert_(-2 <= seconds <= 1)  # -1 is not enough for Mysql
 
     def check_status(self, m, status=STATUS_PENDING, is_new=True, is_replied=False, parent=None, thread=None,
         moderation_date=False, moderation_by=None, moderation_reason='',
             reload(sys.modules['postman.forms'])
             reload(sys.modules['postman.views'])
             reload(sys.modules['postman.urls'])
-        except KeyError: # happens once at the setUp
+        except KeyError:  # happens once at the setUp
             pass
         reload(get_resolver(get_urlconf()).urlconf_module)
     
         # anonymous
         response = self.client.get(url)
         f = response.context['form'].fields['recipients']
-        if hasattr(f, 'channel'): # app may not be in INSTALLED_APPS
+        if hasattr(f, 'channel'):  # app may not be in INSTALLED_APPS
             self.assertEqual(f.channel, 'postman_single_as1-1')
         # authenticated
         self.assert_(self.client.login(username='foo', password='pass'))
         # invalid message id
         self.check_view_404(1000)
         # existent message but not yours
-        self.check_view_404(Message.objects.get(pk=self.c23().pk).pk) # create & verify really there
+        self.check_view_404(Message.objects.get(pk=self.c23().pk).pk)  # create & verify really there
         # existent message but not yet visible to you
         self.check_view_404(Message.objects.get(pk=self.create(sender=self.user2, recipient=self.user1).pk).pk)
 
         self.check_status(Message.objects.get(pk=pk+2), status=STATUS_ACCEPTED, **{sender_kw: field_value})
         self.check_status(Message.objects.get(pk=pk+3), status=STATUS_ACCEPTED)
         # fallback redirect is to inbox
-        response = self.client.post(url, data) # doesn't hurt if already archived|deleted|undeleted
+        response = self.client.post(url, data)  # doesn't hurt if already archived|deleted|undeleted
         self.assertRedirects(response, reverse('postman_inbox'))
         # redirect url may be superseded
         response = self.client.post(url_with_success_url, data, HTTP_REFERER=redirect_url)
         # pending -> rejected
         m = copy.copy(msg)
         m.moderation_status = STATUS_REJECTED
-        m.clean_moderation(STATUS_PENDING, self.user1) # one try with moderator
+        m.clean_moderation(STATUS_PENDING, self.user1)  # one try with moderator
         self.check_status(m, status=STATUS_REJECTED,
             moderation_date=True, moderation_by=self.user1, recipient_deleted_at=True)
         self.check_now(m.moderation_date)
         # pending -> accepted
         m = copy.copy(msg)
         m.moderation_status = STATUS_ACCEPTED
-        m.clean_moderation(STATUS_PENDING) # one try without moderator
+        m.clean_moderation(STATUS_PENDING)  # one try without moderator
         self.check_status(m, status=STATUS_ACCEPTED, moderation_date=True)
         self.check_now(m.moderation_date)
 
     def test_moderation_from_rejected(self):
         "Test moderation management when leaving 'rejected' status."
-        date_in_past = now() - timedelta(days=2) # any value, just to avoid now()
+        date_in_past = now() - timedelta(days=2)  # any value, just to avoid now()
         reason = 'some good reason'
         msg = Message.objects.create(subject='s', moderation_status=STATUS_REJECTED,
             moderation_date=date_in_past, moderation_by=self.user1, moderation_reason=reason,
         # rejected -> pending
         m = copy.copy(msg)
         m.moderation_status = STATUS_PENDING
-        m.clean_moderation(STATUS_REJECTED) # one try without moderator
+        m.clean_moderation(STATUS_REJECTED)  # one try without moderator
         self.check_status(m, status=STATUS_PENDING,
             moderation_date=True, moderation_reason=reason, recipient_deleted_at=False)
         self.check_now(m.moderation_date)
         # rejected -> accepted
         m = copy.copy(msg)
         m.moderation_status = STATUS_ACCEPTED
-        m.clean_moderation(STATUS_REJECTED, self.user2) # one try with moderator
+        m.clean_moderation(STATUS_REJECTED, self.user2)  # one try with moderator
         self.check_status(m, status=STATUS_ACCEPTED,
             moderation_date=True, moderation_by=self.user2, moderation_reason=reason,
             recipient_deleted_at=False)
 
     def test_moderation_from_accepted(self):
         "Test moderation management when leaving 'accepted' status."
-        date_in_past = now() - timedelta(days=2) # any value, just to avoid now()
+        date_in_past = now() - timedelta(days=2)  # any value, just to avoid now()
         msg = Message.objects.create(subject='s', moderation_status=STATUS_ACCEPTED,
             moderation_date=date_in_past, moderation_by=self.user1, recipient_deleted_at=date_in_past)
         # accepted -> accepted: nothing changes
         # accepted -> pending
         m = copy.copy(msg)
         m.moderation_status = STATUS_PENDING
-        m.clean_moderation(STATUS_ACCEPTED, self.user2) # one try with moderator
+        m.clean_moderation(STATUS_ACCEPTED, self.user2)  # one try with moderator
         self.check_status(m, status=STATUS_PENDING,
             moderation_date=True, moderation_by=self.user2, recipient_deleted_at=date_in_past)
         self.check_now(m.moderation_date)
         # accepted -> rejected
         m = copy.copy(msg)
         m.moderation_status = STATUS_REJECTED
-        m.clean_moderation(STATUS_ACCEPTED) # one try without moderator
+        m.clean_moderation(STATUS_ACCEPTED)  # one try without moderator
         self.check_status(m, status=STATUS_REJECTED, moderation_date=True, recipient_deleted_at=True)
         self.check_now(m.moderation_date)
         self.check_now(m.recipient_deleted_at)
 
     def test_visitor(self):
         "Test clean_for_visitor()."
-        date_in_past = now() - timedelta(days=2) # any value, just to avoid now()
+        date_in_past = now() - timedelta(days=2)  # any value, just to avoid now()
         # as the sender
         m = Message.objects.create(subject='s', recipient=self.user1)
         m.clean_for_visitor()
         self.check_status(r.parent, status=STATUS_ACCEPTED, thread=parent)
         # pending -> accepted: parent is replied
         r.update_parent(STATUS_PENDING)
-        p = Message.objects.get(pk=parent.pk) # better to ask the DB to check the save()
+        p = Message.objects.get(pk=parent.pk)  # better to ask the DB to check the save()
         self.check_status(p, status=STATUS_ACCEPTED, thread=parent, is_replied=True)
-        self.assertEqual(p.replied_at.timetuple(), r.sent_at.timetuple()) # mysql doesn't store microseconds
+        self.assertEqual(p.replied_at.timetuple(), r.sent_at.timetuple())  # mysql doesn't store microseconds
         # rejected -> accepted: same as pending -> accepted
         # so check here the acceptance of an anterior date
         # note: use again the some object for convenience but another reply is more realistic
         # a reply is withdrawn and no other reply
         r = copy.deepcopy(reply)
         r.parent.replied_at = r.sent_at
-        r.moderation_status = STATUS_REJECTED # could be STATUS_PENDING
+        r.moderation_status = STATUS_REJECTED  # could be STATUS_PENDING
         # rejected -> rejected: no change. In real case, parent.replied_at would be already empty
         r.update_parent(STATUS_REJECTED)
         self.check_status(r.parent, status=STATUS_ACCEPTED, thread=parent, is_replied=True)
             parent=parent, thread=parent.thread, moderation_status=STATUS_ACCEPTED)
         r = copy.deepcopy(reply)
         r.parent.replied_at = r.sent_at
-        r.moderation_status = STATUS_PENDING # could be STATUS_REJECTED
+        r.moderation_status = STATUS_PENDING  # could be STATUS_REJECTED
         # pending -> pending: no change. In real case, parent.replied_at would be from another reply object
         r.update_parent(STATUS_PENDING)
         self.check_status(r.parent, status=STATUS_ACCEPTED, thread=parent, is_replied=True)
             else:
                 changes['status'] = STATUS_REJECTED
                 changes['moderation_reason'] = result
-            m.sent_at = now() # refresh, as we recycle the same base message
+            m.sent_at = now()  # refresh, as we recycle the same base message
             self.check_status(m, **changes)
 
     def test_auto_moderation(self):
         self.check_sub("'X'", '2', 'X')
 
     def check_or_me(self, x, value, user=None):
-        t = Template("{% load postman_tags %}{{ "+x+"|or_me:user }}") # do not load i18n to be able to check the untranslated pattern
+        t = Template("{% load postman_tags %}{{ "+x+"|or_me:user }}")  # do not load i18n to be able to check the untranslated pattern
         self.assertEqual(t.render(Context({'user': user or AnonymousUser()})), value)
 
     def test_or_me(self):
         "Test '|compact_date'."
         dt = now()
         try:
-            from django.utils.timezone import localtime # Django 1.4 aware datetimes
+            from django.utils.timezone import localtime  # Django 1.4 aware datetimes
             # (1.4) template/base.py/_render_value_in_context()
             dt = localtime(dt)
         except ImportError:
         self.check_compact_date(dt, default, format='one')
         self.check_compact_date(dt, default, format='one,two')
         self.check_compact_date(dt, dt.strftime('%H:%M'))
-        dt = now() - timedelta(days=1) # little fail: do not work on Jan, 1st, because the year changes as well
-        self.check_compact_date(dt, dt.strftime('%d %b').lower()) # filter's 'b' is lowercase
+        dt = now() - timedelta(days=1)  # little fail: do not work on Jan, 1st, because the year changes as well
+        self.check_compact_date(dt, dt.strftime('%d %b').lower())  # filter's 'b' is lowercase
         dt = now() - timedelta(days=365)
         self.check_compact_date(dt, dt.strftime('%d/%m/%y'))
 
         self.check_status(m, status=STATUS_ACCEPTED, moderation_date=True)
         self.check_now(m.moderation_date)
         self.check_message(m)
-        self.assertEqual(len(mail.outbox), 1)
+        self.assertEqual(len(mail.outbox), 1)  # notify the recipient
 
     def test_pm_write_skip_notification(self):
         "Test the notification skipping."
         m = Message.objects.get()
         self.check_status(m, status=STATUS_ACCEPTED, moderation_date=True, sender_deleted_at=True)
         self.check_now(m.sender_deleted_at)
+
+    def test_pm_write_auto_moderators_accepted(self):
+        "Test the auto_moderators parameter, moderate as accepted."
+        pm_write(sender=self.user1, recipient=self.user2, subject='s', auto_moderators=lambda m: True)
+        m = Message.objects.get()
+        self.check_status(m, status=STATUS_ACCEPTED, moderation_date=True)
+
+    def test_pm_write_auto_moderators_pending(self):
+        "Test the auto_moderators parameter, no moderation decision is taken. Test the parameter as a list."
+        pm_write(sender=self.user1, recipient=self.user2, subject='s', auto_moderators=[lambda m: None])
+        m = Message.objects.get()
+        self.check_status(m)
+        self.assertEqual(len(mail.outbox), 0)  # no one to notify
+
+    def test_pm_write_auto_moderators_rejected(self):
+        "Test the auto_moderators parameter, moderate as rejected. Test the parameter as a tuple."
+        pm_write(sender=self.user1, recipient=self.user2, subject='s', auto_moderators=(lambda m: False, ))
+        m = Message.objects.get()
+        self.check_status(m, status=STATUS_REJECTED, moderation_date=True, recipient_deleted_at=True)
+        self.check_now(m.moderation_date)
+        self.check_now(m.recipient_deleted_at)
+        self.assertEqual(len(mail.outbox), 0)  # sender is not notified in the case of auto moderation