diff options
| author | Mathieu Duckaerts-Antoine <dam@odoo.com> | 2018-05-24 15:27:28 +0200 |
|---|---|---|
| committer | Thibault Delavallée <tde@odoo.com> | 2018-06-06 16:01:33 +0200 |
| commit | 299ebb2cdf63de2e3967da0d2f3e7effd239812b (patch) | |
| tree | f2681c0fc1e283acdfd5942392db8f17ddc7801d /addons/test_mail/tests | |
| parent | eeab18d753db21bca0636836c40df91268549390 (diff) | |
[IMP] mail: add moderation on channels
Purpose of this commit is to allow moderation on incoming messages in
discussion channels. On some channels on which moderation is required
messages should be in a pending moderation stage. Moderators can accept
or refuse messages as well as always allow or ban messages coming from
a given set of emails.
Channels now have an option to be moderated. Moderators can be added on
channels. They have access to a specific UI in Discuss to see and take
action on messages waiting for moderation.
Concerning mail.thread message that are pending moderation are not notified.
It means nobody receives a notification about them. Moderation process calls
the notification once the message is validated.
Various features included in this commit :
* a model is added to store the decision about emails, allow or ban;
* access rights are updated so that only moderators can modify moderation
fields on message;
* specific bus notifications are send to moderated people as well as to
moderators on incoming emails as well as when a decision is taken;
* options are added on channels to send explanations to moderated emails;
* options are added on channels to write and send guidelines explaining
why and how moderation is performed;
* a reminder is send daily to moderators with remaining messages to moderate;
* discuss UI is adapted and a new channel is added below Inbox and Starred
giving access to moderation tools;
* chanenl UI is adapted allowing to moderate directly inside channels;
This commit is linked to task ID 29521. Closes #21921.
Diffstat (limited to 'addons/test_mail/tests')
| -rw-r--r-- | addons/test_mail/tests/__init__.py | 3 | ||||
| -rw-r--r-- | addons/test_mail/tests/common.py | 58 | ||||
| -rw-r--r-- | addons/test_mail/tests/test_mail_channel.py | 104 | ||||
| -rw-r--r-- | addons/test_mail/tests/test_mail_message.py | 105 | ||||
| -rw-r--r-- | addons/test_mail/tests/test_res_users.py | 34 |
5 files changed, 299 insertions, 5 deletions
diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py index dc9978773c4..411d73d5694 100644 --- a/addons/test_mail/tests/__init__.py +++ b/addons/test_mail/tests/__init__.py @@ -13,4 +13,5 @@ from . import test_invite from . import test_ir_actions from . import test_update_notification from . import test_discuss -from . import test_performance
\ No newline at end of file +from . import test_performance +from . import test_res_users
\ No newline at end of file diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py index e536e15fa70..b526dce2906 100644 --- a/addons/test_mail/tests/common.py +++ b/addons/test_mail/tests/common.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from json import dumps, loads import json @@ -7,7 +8,7 @@ from email.utils import formataddr from odoo import api from odoo.addons.bus.models.bus import json_dump -from odoo.tests import common +from odoo.tests import common, tagged class BaseFunctionalTest(common.SavepointCase): @@ -244,6 +245,7 @@ class MockEmails(common.SingleTransactionCase): super(MockEmails, cls).tearDownClass() def _init_mock_build_email(self): + self.env['mail.mail'].search([]).unlink() self._mails_args[:] = [] self._mails[:] = [] @@ -260,3 +262,57 @@ class MockEmails(common.SingleTransactionCase): mail = self.format(template, to=to, subject=subject, cc=cc, extra=extra, email_from=email_from, msg_id=msg_id) self.env['mail.thread'].with_context(mail_channel_noautofollow=True).message_process(model, mail) return self.env[target_model].search([(target_field, '=', subject)]) + + +@tagged('moderation') +class Moderation(MockEmails, BaseFunctionalTest): + + @classmethod + def setUpClass(cls): + super(Moderation, cls).setUpClass() + Channel = cls.env['mail.channel'] + Users = cls.env['res.users'].with_context(cls._quick_create_user_ctx) + + cls.channel_moderation_1 = Channel.create({ + 'name': 'Moderation_1', + 'email_send': True, + 'moderation': True + }) + cls.channel_1 = cls.channel_moderation_1 + cls.channel_moderation_2 = Channel.create({ + 'name': 'Moderation_2', + 'email_send': True, + 'moderation': True + }) + cls.channel_2 = cls.channel_moderation_2 + + cls.user_employee.write({'moderation_channel_ids': [(6, 0, [cls.channel_1.id])]}) + + cls.user_employee_2 = Users.create({ + 'name': 'Roboute', + 'login': 'roboute', + 'email': 'roboute@guilliman.com', + 'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])], + 'moderation_channel_ids': [(6, 0, [cls.channel_2.id])] + }) + cls.partner_employee_2 = cls.user_employee_2.partner_id + + cls.channel_moderation_1.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': cls.partner_employee.id})]}) + cls.channel_moderation_2.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': cls.partner_employee_2.id})]}) + + def _create_new_message(self, channel_id, status='pending_moderation', author=None, body='', message_type="email"): + author = author if author else self.env.user.partner_id + message = self.env['mail.message'].create({ + 'model': 'mail.channel', + 'res_id': channel_id, + 'message_type': 'email', + 'body': body, + 'moderation_status': status, + 'author_id': author.id, + 'email_from': formataddr((author.name, author.email)), + 'subtype_id': self.env['mail.message.subtype'].search([('name', '=', 'Discussions')]).id + }) + return message + + def _clear_bus(self): + self.env['bus.bus'].search([]).unlink() diff --git a/addons/test_mail/tests/test_mail_channel.py b/addons/test_mail/tests/test_mail_channel.py index 73fa55ebbcc..395fbd7b318 100644 --- a/addons/test_mail/tests/test_mail_channel.py +++ b/addons/test_mail/tests/test_mail_channel.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- - from email.utils import formataddr +from odoo.tests import tagged from odoo.addons.test_mail.tests import common -from odoo.exceptions import AccessError, except_orm +from odoo.exceptions import AccessError, except_orm, ValidationError, UserError from odoo.tools import mute_logger @@ -195,3 +195,103 @@ class TestChannelFeatures(common.BaseFunctionalTest, common.MockEmails): self.assertIn( email['email_to'][0], [formataddr((self.user_employee.name, self.user_employee.email)), formataddr((self.test_partner.name, self.test_partner.email))]) + + +@tagged('moderation') +class TestChannelModeration(common.Moderation): + + @classmethod + def setUpClass(cls): + super(TestChannelModeration, cls).setUpClass() + + def test_moderator_consistency(self): + with self.assertRaises(ValidationError): + self.channel_1.write({'moderator_ids': [(4, self.user_employee_2.id)]}) + + self.channel_1.write({'channel_partner_ids': [(4, self.partner_employee_2.id)]}) + with self.assertRaises(ValidationError): + self.user_employee_2.write({'email': False}) + self.channel_1.write({'moderator_ids': [(4, self.user_employee_2.id)]}) + + def test_channel_moderation_parameters(self): + with self.assertRaises(ValidationError): + self.channel_1.write({'email_send': False}) + + with self.assertRaises(ValidationError): + self.channel_1.write({'moderator_ids': [(5, 0)]}) + + def test_moderation_count(self): + self.assertEqual(self.channel_1.moderation_count, 0) + self.channel_1.write({'moderation_ids': [ + (0, 0, {'email': 'test0@example.com', 'status': 'allow'}), + (0, 0, {'email': 'test1@example.com', 'status': 'ban'}) + ]}) + self.assertEqual(self.channel_1.moderation_count, 2) + + @mute_logger('odoo.addons.mail.models.mail_channel', 'odoo.models.unlink') + def test_send_guidelines(self): + self.channel_1.write({'channel_partner_ids': [(4, self.partner_employee_2.id), (4, self.partner_admin.id)]}) + self.channel_1._update_moderation_email([self.partner_admin.email], 'ban') + self._init_mock_build_email() + self.channel_1.sudo(self.user_employee).send_guidelines() + self.env['mail.mail'].process_email_queue() + self.assertEmails(False, self.partner_employee | self.partner_employee_2, email_from=self.env.user.company_id.catchall or self.env.user.company_id.email) + + def test_send_guidelines_crash(self): + with self.assertRaises(UserError): + self.channel_1.sudo(self.user_employee_2).send_guidelines() + + def test_update_moderation_email(self): + self.channel_1.write({'moderation_ids': [ + (0, 0, {'email': 'test0@example.com', 'status': 'allow'}), + (0, 0, {'email': 'test1@example.com', 'status': 'ban'}) + ]}) + self.channel_1._update_moderation_email(['test0@example.com', 'test3@example.com'], 'ban') + self.assertEqual(len(self.channel_1.moderation_ids), 3) + self.assertTrue(all(status == 'ban' for status in self.channel_1.moderation_ids.mapped('status'))) + + def test_moderation_reset(self): + self._create_new_message(self.channel_1.id) + self._create_new_message(self.channel_1.id, status='accepted') + self._create_new_message(self.channel_2.id) + self.channel_1.write({'moderation': False}) + self.assertEqual(self.env['mail.message'].search_count([ + ('moderation_status', '=', 'pending_moderation'), + ('model', '=', 'mail.channel'), + ('res_id', '=', self.channel_1.id) + ]), 0) + self.assertEqual(self.env['mail.message'].search_count([ + ('moderation_status', '=', 'pending_moderation'), + ('model', '=', 'mail.channel'), + ('res_id', '=', self.channel_2.id) + ]), 1) + self.channel_2.write({'moderation': False}) + self.assertEqual(self.env['mail.message'].search_count([ + ('moderation_status', '=', 'pending_moderation'), + ('model', '=', 'mail.channel'), + ('res_id', '=', self.channel_2.id) + ]), 0) + + @mute_logger('odoo.models.unlink') + def test_message_post(self): + email1 = 'test0@example.com' + email2 = 'test1@example.com' + + self.channel_1._update_moderation_email([email1], 'ban') + self.channel_1._update_moderation_email([email2], 'allow') + + msg_admin = self.channel_1.message_post(message_type='email', subtype='mt_comment', author_id=self.partner_admin.id) + msg_moderator = self.channel_1.message_post(message_type='comment', subtype='mt_comment', author_id=self.partner_employee.id) + msg_email1 = self.channel_1.message_post(message_type='comment', subtype='mt_comment', email_from=formataddr(("MyName", email1))) + msg_email2 = self.channel_1.message_post(message_type='email', subtype='mt_comment', email_from=email2) + msg_notif = self.channel_1.message_post() + + messages = self.env['mail.message'].search([('model', '=', 'mail.channel'), ('res_id', '=', self.channel_1.id)]) + pending_messages = messages.filtered(lambda m: m.moderation_status == 'pending_moderation') + accepted_messages = messages.filtered(lambda m: m.moderation_status == 'accepted') + + self.assertFalse(msg_email1) + self.assertEqual(msg_admin, pending_messages) + self.assertEqual(accepted_messages, msg_moderator | msg_email2 | msg_notif) + self.assertFalse(msg_admin.channel_ids) + self.assertEqual(msg_email2.channel_ids, self.channel_1) diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py index 5d747e0a207..94540272b4e 100644 --- a/addons/test_mail/tests/test_mail_message.py +++ b/addons/test_mail/tests/test_mail_message.py @@ -5,6 +5,7 @@ import base64 from odoo.addons.test_mail.tests import common from odoo.exceptions import AccessError, except_orm from odoo.tools import mute_logger +from odoo.tests import tagged class TestMessageValues(common.BaseFunctionalTest, common.MockEmails): @@ -213,6 +214,10 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails): messages = self.env['mail.message'].sudo(self.user_portal).search([('subject', 'like', '_ZTest')]) self.assertEqual(messages, msg4 | msg5) + # -------------------------------------------------- + # READ + # -------------------------------------------------- + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') def test_mail_message_access_read_crash(self): # TODO: Change the except_orm to Warning ( Because here it's call check_access_rule @@ -251,6 +256,16 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails): # Test: Bert reads the message, ok because linked to a doc he is allowed to read self.message.sudo(self.user_employee).read() + def test_mail_message_access_read_crash_moderation(self): + # with self.assertRaises(AccessError): + self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'}) + # Test: Bert reads the message, ok because linked to a doc he is allowed to read + self.message.sudo(self.user_employee).read() + + # -------------------------------------------------- + # CREATE + # -------------------------------------------------- + @mute_logger('odoo.addons.base.models.ir_model') def test_mail_message_access_create_crash_public(self): # Do: Bert creates a message on Pigs -> ko, no creation rights @@ -284,6 +299,25 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails): self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]}) self.env['mail.message'].sudo(self.user_employee).create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test', 'parent_id': self.message.id}) + # -------------------------------------------------- + # WRITE + # -------------------------------------------------- + + def test_mail_message_access_write_moderation(self): + """ Only moderators can modify pending messages """ + self.group_public.write({ + 'email_send': True, + 'moderation': True, + 'channel_partner_ids': [(4, self.partner_employee.id)], + 'moderator_ids': [(4, self.user_employee.id)], + }) + self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'}) + self.message.sudo(self.user_employee).write({'moderation_status': 'accepted'}) + + def test_mail_message_access_write_crash_moderation(self): + self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'}) + with self.assertRaises(AccessError): + self.message.sudo(self.user_employee).write({'moderation_status': 'accepted'}) @mute_logger('openerp.addons.mail.models.mail_mail') def test_mark_all_as_read(self): @@ -350,4 +384,73 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails): portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[]) na_count = portal_partner.get_needaction_count() - self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
\ No newline at end of file + self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones") + + +@tagged('moderation') +class TestMessageModeration(common.Moderation): + + @classmethod + def setUpClass(cls): + super(TestMessageModeration, cls).setUpClass() + + cls.msg_admin_pending_c1 = cls._create_new_message(cls, cls.channel_1.id, status='pending_moderation', author=cls.partner_admin) + cls.msg_admin_pending_c1_2 = cls._create_new_message(cls, cls.channel_1.id, status='pending_moderation', author=cls.partner_admin) + cls.msg_emp2_pending_c1 = cls._create_new_message(cls, cls.channel_1.id, status='pending_moderation', author=cls.partner_employee_2) + + @mute_logger('odoo.models.unlink') + def test_moderate_accept(self): + self._clear_bus() + # A pending moderation message needs to have field channel_ids empty. Moderators + # need to be able to notify a pending moderation message (in a channel they moderate). + self.assertFalse(self.msg_admin_pending_c1.channel_ids) + self.msg_admin_pending_c1.sudo(self.user_employee)._moderate('accept') + self.assertEqual(self.msg_admin_pending_c1.channel_ids, self.channel_1) + self.assertEqual(self.msg_admin_pending_c1.moderation_status, 'accepted') + self.assertEqual(self.msg_admin_pending_c1_2.moderation_status, 'pending_moderation') + self.assertBusNotification([(self.cr.dbname, 'mail.channel', self.channel_1.id)]) + + @mute_logger('odoo.models.unlink') + def test_moderate_allow(self): + self._clear_bus() + # A pending moderation message needs to have field channel_ids empty. Moderators + # need to be able to notify a pending moderation message (in a channel they moderate). + self.assertFalse(self.msg_admin_pending_c1.channel_ids) + self.assertFalse(self.msg_admin_pending_c1_2.channel_ids) + self.msg_admin_pending_c1.sudo(self.user_employee)._moderate('allow') + self.assertEqual(self.msg_admin_pending_c1.channel_ids, self.channel_1) + self.assertEqual(self.msg_admin_pending_c1_2.channel_ids, self.channel_1) + self.assertEqual(self.msg_admin_pending_c1.moderation_status, 'accepted') + self.assertEqual(self.msg_admin_pending_c1_2.moderation_status, 'accepted') + self.assertBusNotification([ + (self.cr.dbname, 'mail.channel', self.channel_1.id), + (self.cr.dbname, 'mail.channel', self.channel_1.id)]) + + @mute_logger('odoo.models.unlink') + def test_moderate_reject(self): + self._init_mock_build_email() + (self.msg_admin_pending_c1 | self.msg_emp2_pending_c1).sudo(self.user_employee)._moderate_send_reject_email('Title', 'Message to author') + self.env['mail.mail'].process_email_queue() + self.assertEmails(self.partner_employee, self.partner_employee_2 | self.partner_admin, subject='Title', body_content='Message to author') + + def test_moderate_discard(self): + self._clear_bus() + id1, id2 = self.msg_admin_pending_c1.id, self.msg_emp2_pending_c1.id # save ids because unlink will discard them + (self.msg_admin_pending_c1 | self.msg_emp2_pending_c1).sudo(self.user_employee)._moderate_discard() + + self.assertBusNotification( + [(self.cr.dbname, 'res.partner', self.partner_admin.id), + (self.cr.dbname, 'res.partner', self.partner_employee_2.id), + (self.cr.dbname, 'res.partner', self.partner_employee.id)], + [{'type': 'deletion', 'message_ids': [id1]}, # admin: one message deleted because discarded + {'type': 'deletion', 'message_ids': [id2]}, # employee_2: one message delete because discarded + {'type': 'deletion', 'message_ids': [id1, id2]}] # employee: two messages deleted because moderation done + ) + + @mute_logger('odoo.models.unlink') + def test_notify_moderators(self): + # create pending messages in another channel to have two notification to push + msg_emp_pending_c2 = self._create_new_message(self.channel_2.id, status='pending_moderation', author=self.partner_employee) + + self.env['mail.message']._notify_moderators() + self.assertEmails(False, self.partner_employee | self.partner_employee_2, subject='Message are pending moderation', email_from=self.env.user.company_id.catchall or self.env.user.company_id.email) diff --git a/addons/test_mail/tests/test_res_users.py b/addons/test_mail/tests/test_res_users.py new file mode 100644 index 00000000000..4ae3ca00265 --- /dev/null +++ b/addons/test_mail/tests/test_res_users.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.test_mail.tests import common +from odoo.tests import tagged + + +@tagged('moderation') +class TestMessageModeration(common.Moderation): + + @classmethod + def setUpClass(cls): + super(TestMessageModeration, cls).setUpClass() + + def test_is_moderator(self): + self.assertTrue(self.user_employee.is_moderator) + self.assertFalse(self.user_admin.is_moderator) + self.assertTrue(self.user_employee_2.is_moderator) + + def test_moderation_counter(self): + self._create_new_message(self.channel_1.id, status='pending_moderation', author=self.partner_admin) + self._create_new_message(self.channel_1.id, status='accepted', author=self.partner_admin) + self._create_new_message(self.channel_1.id, status='accepted', author=self.partner_employee) + self._create_new_message(self.channel_1.id, status='pending_moderation', author=self.partner_employee) + self._create_new_message(self.channel_1.id, status='accepted', author=self.partner_employee_2) + + self.assertEqual(self.user_employee.moderation_counter, 2) + self.assertEqual(self.user_employee_2.moderation_counter, 0) + self.assertEqual(self.user_admin.moderation_counter, 0) + + self.channel_1.write({'channel_partner_ids': [(4, self.partner_employee_2.id)], 'moderator_ids': [(4, self.user_employee_2.id)]}) + self.assertEqual(self.user_employee.moderation_counter, 2) + self.assertEqual(self.user_employee_2.moderation_counter, 0) + self.assertEqual(self.user_admin.moderation_counter, 0) |
