summaryrefslogtreecommitdiff
path: root/addons/mail/models
diff options
context:
space:
mode:
authorMathieu Duckaerts-Antoine <dam@odoo.com>2018-05-24 15:27:28 +0200
committerThibault Delavallée <tde@odoo.com>2018-06-06 16:01:33 +0200
commit299ebb2cdf63de2e3967da0d2f3e7effd239812b (patch)
treef2681c0fc1e283acdfd5942392db8f17ddc7801d /addons/mail/models
parenteeab18d753db21bca0636836c40df91268549390 (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/mail/models')
-rw-r--r--addons/mail/models/__init__.py1
-rw-r--r--addons/mail/models/mail_channel.py220
-rw-r--r--addons/mail/models/mail_message.py295
-rw-r--r--addons/mail/models/mail_thread.py24
-rw-r--r--addons/mail/models/res_company.py23
-rw-r--r--addons/mail/models/res_users.py35
6 files changed, 562 insertions, 36 deletions
diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py
index daf70f23ca4..a378f9e719d 100644
--- a/addons/mail/models/__init__.py
+++ b/addons/mail/models/__init__.py
@@ -14,6 +14,7 @@ from . import mail_template
from . import mail_shortcode
from . import res_partner
from . import res_users
+from . import res_company
from . import res_config_settings
from . import update
from . import ir_actions
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 9da0862376f..edb0eb0e53e 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -1,17 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import base64
-from email.utils import formataddr
+import base64
+import logging
import re
+
+from email.utils import formataddr
from uuid import uuid4
from odoo import _, api, fields, models, modules, tools
-from odoo.exceptions import UserError
+from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
-from odoo.tools import ormcache
+from odoo.tools import ormcache, pycompat
from odoo.tools.safe_eval import safe_eval
+MODERATION_FIELDS = ['moderation', 'moderator_ids', 'moderation_ids', 'moderation_notify', 'moderation_notify_msg', 'moderation_guidelines', 'moderation_guidelines_msg']
+_logger = logging.getLogger(__name__)
+
class ChannelPartner(models.Model):
_name = 'mail.channel.partner'
@@ -28,6 +33,22 @@ class ChannelPartner(models.Model):
is_pinned = fields.Boolean("Is pinned on the interface", default=True)
+class Moderation(models.Model):
+ _name = 'mail.moderation'
+ _description = 'Channel black/white list'
+
+ email = fields.Char(string="Email", index=True, required=True)
+ status = fields.Selection([
+ ('allow', 'Always Allow'),
+ ('ban', 'Permanent Ban')],
+ string="Status", required=True)
+ channel_id = fields.Many2one('mail.channel', string="Channel", index=True, required=True)
+
+ _sql_constraints = [
+ ('channel_email_uniq', 'unique (email,channel_id)', 'The email address must be unique per channel !')
+ ]
+
+
class Channel(models.Model):
""" A mail.channel is a discussion group that may behave like a listener
on documents. """
@@ -90,6 +111,20 @@ class Channel(models.Model):
"Use this field anywhere a small image is required.")
is_subscribed = fields.Boolean(
'Is Subscribed', compute='_compute_is_subscribed')
+ # moderation
+ moderation = fields.Boolean(string='Moderate this channel')
+ moderator_ids = fields.Many2many('res.users', 'mail_channel_moderator_rel', string='Moderators')
+ is_moderator = fields.Boolean(help="Current user is a moderator of the channel", string='Moderator', compute="_compute_is_moderator")
+ moderation_ids = fields.One2many(
+ 'mail.moderation', 'channel_id', string='Moderated Emails',
+ groups="base.group_user")
+ moderation_count = fields.Integer(
+ string='Moderated emails count', compute='_compute_moderation_count',
+ groups="base.group_user")
+ moderation_notify = fields.Boolean(string="Automatic notification", help="People receive an automatic notification about their message being waiting for moderation.")
+ moderation_notify_msg = fields.Text(string="Notification message")
+ moderation_guidelines = fields.Boolean(string="Send guidelines to new subscribers", help="Newcomers on this moderated channel will automatically receive the guidelines.")
+ moderation_guidelines_msg = fields.Text(string="Guidelines")
@api.one
@api.depends('channel_partner_ids')
@@ -97,6 +132,41 @@ class Channel(models.Model):
self.is_subscribed = self.env.user.partner_id in self.channel_partner_ids
@api.multi
+ @api.depends('moderator_ids')
+ def _compute_is_moderator(self):
+ for channel in self:
+ channel.is_moderator = self.env.user in channel.moderator_ids
+
+ @api.multi
+ @api.depends('moderation_ids')
+ def _compute_moderation_count(self):
+ read_group_res = self.env['mail.moderation'].read_group([('channel_id', 'in', self.ids)], ['channel_id'], 'channel_id')
+ data = dict((res['channel_id'][0], res['channel_id_count']) for res in read_group_res)
+ for channel in self:
+ channel.moderation_count = data.get(channel.id, 0)
+
+ @api.constrains('moderator_ids')
+ def _check_moderator_email(self):
+ if any(not moderator.email for channel in self for moderator in channel.moderator_ids):
+ raise ValidationError("Moderators must have an email address.")
+
+ @api.constrains('moderator_ids', 'channel_partner_ids', 'channel_last_seen_partner_ids')
+ def _check_moderator_is_member(self):
+ for channel in self:
+ if not (channel.mapped('moderator_ids.partner_id') <= channel.channel_partner_ids):
+ raise ValidationError("Moderators should be members of the channel they moderate.")
+
+ @api.constrains('moderation', 'email_send')
+ def _check_moderation_parameters(self):
+ if any(not channel.email_send and channel.moderation for channel in self):
+ raise ValidationError('Only mailing lists can be moderated.')
+
+ @api.constrains('moderator_ids')
+ def _check_moderator_existence(self):
+ if any(not channel.moderator_ids for channel in self if channel.moderation):
+ raise ValidationError('Moderated channels must have moderators.')
+
+ @api.multi
def _compute_is_member(self):
memberships = self.env['mail.channel.partner'].sudo().search([
('channel_id', 'in', self.ids),
@@ -113,6 +183,26 @@ class Channel(models.Model):
else:
self.alias_contact = 'followers'
+ @api.onchange('moderator_ids')
+ def _onchange_moderator_ids(self):
+ missing_partners = self.mapped('moderator_ids.partner_id') - self.channel_partner_ids
+ if missing_partners:
+ self.channel_partner_ids |= missing_partners
+
+ @api.onchange('email_send')
+ def _onchange_email_send(self):
+ if not self.email_send:
+ self.moderation = False
+
+ @api.onchange('moderation')
+ def _onchange_moderation(self):
+ if not self.moderation:
+ self.moderation_notify = False
+ self.moderation_guidelines = False
+ self.moderator_ids = False
+ else:
+ self.moderator_ids |= self.env.user
+
@api.model
def create(self, vals):
# ensure image at quick create
@@ -154,10 +244,25 @@ class Channel(models.Model):
@api.multi
def write(self, vals):
+ # First checks if user tries to modify moderation fields and has not the right to do it.
+ if any(key for key in MODERATION_FIELDS if vals.get(key)) and any(self.env.user not in channel.moderator_ids for channel in self if channel.moderation):
+ if not self.env.user.has_group('base.group_system'):
+ raise UserError("You do not possess the rights to modify fields related to moderation on one of the channels you are modifying.")
+
tools.image_resize_images(vals)
result = super(Channel, self).write(vals)
+
if vals.get('group_ids'):
self._subscribe_users()
+
+ # avoid keeping messages to moderate and accept them
+ if vals.get('moderation') is False:
+ self.env['mail.message'].search([
+ ('moderation_status', '=', 'pending_moderation'),
+ ('model', '=', 'mail.channel'),
+ ('res_id', 'in', self.ids)
+ ])._moderate_accept()
+
return result
def get_alias_model_name(self, vals):
@@ -240,12 +345,54 @@ class Channel(models.Model):
}
return super(Channel, self)._notify_email_recipients(message, recipient_ids)
+ def _extract_moderation_values(self, message_type, **kwargs):
+ """ This method is used to compute moderation status before the creation
+ of a message. For this operation the message's author email address is required.
+ This address is returned with status for other computations. """
+ moderation_status = 'accepted'
+ email = ''
+ if self.moderation and message_type in ['email', 'comment']:
+ author_id = kwargs.get('author_id')
+ if author_id and isinstance(author_id, pycompat.integer_types):
+ email = self.env['res.partner'].browse([author_id]).email
+ elif author_id:
+ email = author_id.email
+ elif kwargs.get('email_from'):
+ email = tools.email_split(kwargs['email_from'])[0]
+ else:
+ email = self.env.user.email
+ if email in self.mapped('moderator_ids.email'):
+ return moderation_status, email
+ status = self.env['mail.moderation'].sudo().search([('email', '=', email), ('channel_id', 'in', self.ids)]).mapped('status')
+ if status and status[0] == 'allow':
+ moderation_status = 'accepted'
+ elif status and status[0] == 'ban':
+ moderation_status = 'rejected'
+ else:
+ moderation_status = 'pending_moderation'
+ return moderation_status, email
+
@api.multi
- @api.returns('self', lambda value: value.id)
- def message_post(self, **kwargs):
- # auto pin 'direct_message' channel partner
+ @api.returns('mail.message', lambda value: value.id)
+ def message_post(self, message_type='notification', **kwargs):
+ moderation_status, email = self._extract_moderation_values(message_type, **kwargs)
+ if moderation_status == 'rejected':
+ return self.env['mail.message']
+
self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').write({'is_pinned': True})
- message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(**kwargs)
+
+ message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(message_type=message_type, moderation_status=moderation_status, **kwargs)
+
+ # Notifies the message author when his message is pending moderation if required on channel.
+ # The fields "email_from" and "reply_to" are filled in automatically by method create in model mail.message.
+ if self.moderation_notify and self.moderation_notify_msg and message_type == 'email' and moderation_status == 'pending_moderation':
+ self.env['mail.mail'].create({
+ 'body_html': self.moderation_notify_msg,
+ 'subject': 'Re: %s' % (kwargs.get('subject', '')),
+ 'email_to': email,
+ 'auto_delete': True,
+ 'state': 'outgoing'
+ })
return message
def _alias_check_contact(self, message, message_dict, alias):
@@ -264,6 +411,58 @@ class Channel(models.Model):
if not self._cr.fetchone():
self._cr.execute('CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)')
+ # --------------------------------------------------
+ # Moderation
+ # --------------------------------------------------
+
+ @api.multi
+ def send_guidelines(self):
+ """ Send guidelines to all channel members. """
+ if self.env.user in self.moderator_ids or self.env.user.has_group('base.group_system'):
+ success = self._send_guidelines(self.channel_partner_ids)
+ if not success:
+ raise UserError('Template "mail.mail_template_channel_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.')
+ else:
+ raise UserError("Only an administrator or a moderator can send guidelines to channel members!")
+
+ @api.multi
+ def _send_guidelines(self, partners):
+ """ Send guidelines of a given channel. Returns False if template used for guidelines
+ not found. Caller may have to handle this return value. """
+ self.ensure_one()
+ template = self.env.ref('mail.mail_template_channel_send_guidelines', raise_if_not_found=False)
+ if not template:
+ _logger.warning('Template "mail.mail_template_channel_send_guidelines" was not found.')
+ return False
+ banned_emails = self.env['mail.moderation'].sudo().search([
+ ('status', '=', 'ban'),
+ ('channel_id', 'in', self.ids)
+ ]).mapped('email')
+ for partner in partners.filtered(lambda p: p.email and not (p.email in banned_emails)):
+ # the sudo is needed because because send_mail will create a message (a mail). As the template is
+ # linked to res.partner model the current user could not have the rights to modify this model. It
+ # means it could prevent users to create the mail.message necessary for the mail.mail.
+ template.with_context(lang=partner.lang, channel=self).sudo().send_mail(partner.id)
+ return True
+
+ @api.multi
+ def _update_moderation_email(self, emails, status):
+ """ This method adds emails into either white or black of the channel list of emails
+ according to status. If an email in emails is already moderated, the method updates the email status.
+ :param emails: list of email addresses to put in white or black list of channel.
+ :param status: value is 'allow' or 'ban'. Emails are put in white list if 'allow', in black list if 'ban'.
+ """
+ self.ensure_one()
+ splitted_emails = [tools.email_split(email)[0] for email in emails if tools.email_split(email)]
+ moderated = self.env['mail.moderation'].sudo().search([
+ ('email', 'in', splitted_emails),
+ ('channel_id', 'in', self.ids)
+ ])
+ cmds = [(1, record.id, {'status': status}) for record in moderated]
+ not_moderated = [email for email in splitted_emails if email not in moderated.mapped('email')]
+ cmds += [(0, 0, {'email': email, 'status': status}) for email in not_moderated]
+ return self.write({'moderation_ids': cmds})
+
#------------------------------------------------------
# Instant Messaging API
#------------------------------------------------------
@@ -347,6 +546,8 @@ class Channel(models.Model):
'channel_type': channel.channel_type,
'public': channel.public,
'mass_mailing': channel.email_send,
+ 'moderation': channel.moderation,
+ 'is_moderator': self.env.uid in channel.moderator_ids.ids,
'group_based_subscription': bool(channel.group_ids),
}
if extra_info:
@@ -572,6 +773,9 @@ class Channel(models.Model):
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
self.action_follow()
+ if self.moderation_guidelines:
+ self._send_guidelines(self.env.user.partner_id)
+
channel_info = self.channel_info()[0]
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
return channel_info
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index b42de71abcf..d98c98fb1d9 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -107,6 +107,13 @@ class Message(models.Model):
message_id = fields.Char('Message-Id', help='Message unique identifier', index=True, readonly=1, copy=False)
reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.')
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
+ # moderation
+ moderation_status = fields.Selection([
+ ('pending_moderation', 'Pending Moderation'),
+ ('accepted', 'Accepted'),
+ ('rejected', 'Rejected')], string="Moderation Status", index=True)
+ moderator_id = fields.Many2one('res.users', string="Moderated By", index=True)
+ need_moderation = fields.Boolean('Need moderation', compute='_compute_need_moderation', search='_search_need_moderation')
@api.multi
def _get_needaction(self):
@@ -138,6 +145,23 @@ class Message(models.Model):
return [('starred_partner_ids', 'in', [self.env.user.partner_id.id])]
return [('starred_partner_ids', 'not in', [self.env.user.partner_id.id])]
+ @api.multi
+ def _compute_need_moderation(self):
+ for message in self:
+ self.need_moderation = False
+
+ @api.model
+ def _search_need_moderation(self, operator, operand):
+ if operator == '=' and operand:
+ return ['&', '&',
+ ('moderation_status', '=', 'pending_moderation'),
+ ('model', '=', 'mail.channel'),
+ ('res_id', 'in', self.env.user.moderation_channel_ids.ids)]
+ return ['|', '|',
+ ('moderation_status', '!=', 'pending_moderation'),
+ ('model', '!=', 'mail.channel'),
+ ('res_id', 'not in', self.env.user.moderation_channel_ids.ids)]
+
#------------------------------------------------------
# Notification API
#------------------------------------------------------
@@ -395,6 +419,7 @@ class Message(models.Model):
'is_note': True # only if the message is a note (subtype == note)
'is_discussion': False # only if the message is a discussion (subtype == discussion)
'is_notification': False # only if the message is a note but is a notification aka not linked to a document like assignation
+ 'moderation_status': 'pending_moderation'
}
"""
message_values = self.read([
@@ -403,6 +428,7 @@ class Message(models.Model):
'model', 'res_id', 'record_name', # document related
'channel_ids', 'partner_ids', # recipients
'starred_partner_ids', # list of partner ids for whom the message is starred
+ 'moderation_status',
])
message_tree = dict((m.id, m) for m in self.sudo())
self._message_read_dict_postprocess(message_values, message_tree)
@@ -474,8 +500,7 @@ class Message(models.Model):
- if author_id == pid, uid is the author, OR
- uid belongs to a notified channel, OR
- uid is in the specified recipients, OR
- - uid has a notification on the message, OR
- - uid have read access to the related document is model, res_id
+ - uid has a notification on the message
- otherwise: remove the id
"""
# Rules do not apply to administrator
@@ -517,6 +542,7 @@ class Message(models.Model):
ON channel.id = channel_rel.mail_channel_id
LEFT JOIN "mail_channel_partner" channel_partner
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
+
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=pid, ids=ids))
for id, rmod, rid, author_id, partner_id, channel_id in self._cr.fetchall():
if author_id == pid:
@@ -558,10 +584,12 @@ class Message(models.Model):
- write: if
- author_id == pid, uid is the author, OR
- uid is in the recipients (partner_ids) OR
- - uid has write or create access on the related document if model, res_id
+ - uid is moderator of the channel and moderation_status is pending_moderation OR
+ - uid has write or create access on the related document if model, res_id and moderation_status is not pending_moderation
- otherwise: raise
- unlink: if
- - uid has write or create access on the related document if model, res_id
+ - uid is moderator of the channel and moderation_status is pending_moderation OR
+ - uid has write or create access on the related document if model, res_id and moderation_status is not pending_moderation
- otherwise: raise
Specific case: non employee users see only messages with subtype (aka do
@@ -595,9 +623,9 @@ class Message(models.Model):
# Read mail_message.ids to have their values
message_values = dict((res_id, {}) for res_id in self.ids)
- if operation in ['read', 'write']:
+ if operation == 'read':
self._cr.execute("""
- SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id,
+ SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status,
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
channel_partner.channel_id as channel_id
FROM "%s" m
@@ -612,24 +640,84 @@ class Message(models.Model):
LEFT JOIN "mail_channel_partner" channel_partner
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, ids=self.ids))
- for mid, rmod, rid, author_id, parent_id, partner_id, channel_id in self._cr.fetchall():
+ for mid, rmod, rid, author_id, parent_id, partner_id, channel_id, moderation_status in self._cr.fetchall():
message_values[mid] = {
'model': rmod,
'res_id': rid,
'author_id': author_id,
'parent_id': parent_id,
+ 'moderation_status': moderation_status,
+ 'moderator_id': False,
'notified': any((message_values[mid].get('notified'), partner_id, channel_id))
}
- else:
- self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
- for mid, rmod, rid, author_id, parent_id in self._cr.fetchall():
- message_values[mid] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
+ elif operation == 'write':
+ self._cr.execute("""
+ SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status,
+ COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
+ channel_partner.channel_id as channel_id, channel_moderator_rel.res_users_id as moderator_id
+ FROM "%s" m
+ LEFT JOIN "mail_message_res_partner_rel" partner_rel
+ ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
+ LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel
+ ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s
+ LEFT JOIN "mail_message_mail_channel_rel" channel_rel
+ ON channel_rel.mail_message_id = m.id
+ LEFT JOIN "mail_channel" channel
+ ON channel.id = channel_rel.mail_channel_id
+ LEFT JOIN "mail_channel_partner" channel_partner
+ ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
+ LEFT JOIN "mail_channel" moderated_channel
+ ON m.moderation_status = 'pending_moderation' AND m.res_id = moderated_channel.id
+ LEFT JOIN "mail_channel_moderator_rel" channel_moderator_rel
+ ON channel_moderator_rel.mail_channel_id = moderated_channel.id AND channel_moderator_rel.res_users_id = %%(uid)s
+ WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, uid=self.env.user.id, ids=self.ids))
+ for mid, rmod, rid, author_id, parent_id, moderation_status, partner_id, channel_id, moderator_id in self._cr.fetchall():
+ message_values[mid] = {
+ 'model': rmod,
+ 'res_id': rid,
+ 'author_id': author_id,
+ 'parent_id': parent_id,
+ 'moderation_status': moderation_status,
+ 'moderator_id': moderator_id,
+ 'notified': any((message_values[mid].get('notified'), partner_id, channel_id))
+ }
+ elif operation == 'create':
+ self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id, moderation_status FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
+ for mid, rmod, rid, author_id, parent_id, moderation_status in self._cr.fetchall():
+ message_values[mid] = {
+ 'model': rmod,
+ 'res_id': rid,
+ 'author_id': author_id,
+ 'parent_id': parent_id,
+ 'moderation_status': moderation_status,
+ 'moderator_id': False
+ }
+ else: # unlink
+ self._cr.execute("""SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status, channel_moderator_rel.res_users_id as moderator_id
+ FROM "%s" m
+ LEFT JOIN "mail_channel" moderated_channel
+ ON m.moderation_status = 'pending_moderation' AND m.res_id = moderated_channel.id
+ LEFT JOIN "mail_channel_moderator_rel" channel_moderator_rel
+ ON channel_moderator_rel.mail_channel_id = moderated_channel.id AND channel_moderator_rel.res_users_id = (%%s)
+ WHERE m.id = ANY (%%s)""" % self._table, (self.env.user.id, self.ids,))
+ for mid, rmod, rid, author_id, parent_id, moderation_status, moderator_id in self._cr.fetchall():
+ message_values[mid] = {
+ 'model': rmod,
+ 'res_id': rid,
+ 'author_id': author_id,
+ 'parent_id': parent_id,
+ 'moderation_status': moderation_status,
+ 'moderator_id': moderator_id
+ }
# Author condition (READ, WRITE, CREATE (private))
author_ids = []
- if operation == 'read' or operation == 'write':
+ if operation == 'read':
author_ids = [mid for mid, message in message_values.items()
if message.get('author_id') and message.get('author_id') == self.env.user.partner_id.id]
+ elif operation == 'write':
+ author_ids = [mid for mid, message in message_values.items()
+ if message.get('moderation_status') != 'pending_moderation' and message.get('author_id') == self.env.user.partner_id.id]
elif operation == 'create':
author_ids = [mid for mid, message in message_values.items()
if not message.get('model') and not message.get('res_id')]
@@ -654,8 +742,13 @@ class Message(models.Model):
notified_ids += [mid for mid, message in message_values.items()
if message.get('parent_id') in not_parent_ids]
+ # Moderator condition: allow to WRITE, UNLINK if moderator of a pending message
+ moderator_ids = []
+ if operation in ['write', 'unlink']:
+ moderator_ids = [mid for mid, message in message_values.items() if message.get('moderator_id')]
+
# Recipients condition, for read and write (partner_ids) and create (message_follower_ids)
- other_ids = set(self.ids).difference(set(author_ids), set(notified_ids))
+ other_ids = set(self.ids).difference(set(author_ids), set(notified_ids), set(moderator_ids))
model_record_ids = _generate_model_record_ids(message_values, other_ids)
if operation in ['read', 'write']:
notified_ids = [mid for mid, message in message_values.items() if message.get('notified')]
@@ -681,8 +774,13 @@ class Message(models.Model):
DocumentModel.check_mail_message_access(mids.ids, operation) # ?? mids ?
else:
self.env['mail.thread'].check_mail_message_access(mids.ids, operation, model_name=model)
- document_related_ids += [mid for mid, message in message_values.items()
- if message.get('model') == model and message.get('res_id') in mids.ids]
+ if operation in ['write', 'unlink']:
+ document_related_ids += [mid for mid, message in message_values.items()
+ if message.get('model') == model and message.get('res_id') in mids.ids and
+ message.get('moderation_status') != 'pending_moderation']
+ else:
+ document_related_ids += [mid for mid, message in message_values.items()
+ if message.get('model') == model and message.get('res_id') in mids.ids]
# Calculate remaining ids: if not void, raise an error
other_ids = other_ids.difference(set(document_related_ids))
@@ -841,7 +939,6 @@ class Message(models.Model):
# remove author from notified partners
if not self._context.get('mail_notify_author', False) and self_sudo.author_id:
partners_sudo = partners_sudo - self_sudo.author_id
-
# update message, with maybe custom values
message_values = {}
if channels_sudo:
@@ -858,6 +955,7 @@ class Message(models.Model):
# have no access to partner model. Maybe propagating a real uid could be necessary.
email_channels = channels_sudo.filtered(lambda channel: channel.email_send)
notif_partners = partners_sudo.filtered(lambda partner: 'inbox' in partner.mapped('user_ids.notification_type'))
+
if email_channels or partners_sudo - notif_partners:
partners_sudo.search([
'|',
@@ -867,7 +965,170 @@ class Message(models.Model):
])._notify(self, layout=layout, force_send=force_send, send_after_commit=send_after_commit, values=values)
notif_partners._notify_by_chat(self)
-
channels_sudo._notify(self)
return True
+
+ # --------------------------------------------------
+ # Moderation
+ # --------------------------------------------------
+
+ @api.multi
+ def moderate(self, decision, **kwargs):
+ """ Moderate messages. A check is done on moderation status of the
+ current user to ensure we only moderate valid messages. """
+ moderated_channels = self.env.user.moderation_channel_ids
+ to_moderate = [message.id for message in self
+ if message.model == 'mail.channel' and
+ message.res_id in moderated_channels.ids and
+ message.moderation_status == 'pending_moderation']
+ if to_moderate:
+ self.browse(to_moderate)._moderate(decision, **kwargs)
+
+ @api.multi
+ def _moderate(self, decision, **kwargs):
+ """ :param decision
+ * accept - moderate message and broadcast that message to followers of relevant channels.
+ * reject - message will be deleted from the database without broadcast
+ an email sent to the author with an explanation that the moderators can edit.
+ * discard - message will be deleted from the database without broadcast.
+ * allow - add email address to white list people of specific channel,
+ so that next time if a message come from same email address on same channel,
+ it will be automatically broadcasted to relevant channels without any approval from moderator.
+ * ban - add email address to black list of emails for the specific channel.
+ From next time, a person sending a message using that email address will not need moderation.
+ message_post will not create messages with the corresponding expeditor.
+ """
+ if decision == 'accept':
+ self._moderate_accept()
+ elif decision == 'reject':
+ self._moderate_send_reject_email(kwargs.get('title'), kwargs.get('comment'))
+ self._moderate_discard()
+ elif decision == 'discard':
+ self._moderate_discard()
+ elif decision == 'allow':
+ channels = self.env['mail.channel'].browse(self.mapped('res_id'))
+ for channel in channels:
+ channel._update_moderation_email(
+ list({message.email_from for message in self if message.res_id == channel.id}),
+ 'allow'
+ )
+ self._search_from_same_authors()._moderate_accept()
+ elif decision == 'ban':
+ channels = self.env['mail.channel'].browse(self.mapped('res_id'))
+ for channel in channels:
+ channel._update_moderation_email(
+ list({message.email_from for message in self if message.res_id == channel.id}),
+ 'ban'
+ )
+ self._search_from_same_authors()._moderate_discard()
+
+ def _moderate_accept(self):
+ self.write({
+ 'moderation_status': 'accepted',
+ 'moderator_id': self.env.uid
+ })
+ # proceed with notification process to send notification emails and Inbox messages
+ for message in self:
+ message._notify()
+
+ @api.multi
+ def _moderate_send_reject_email(self, subject, comment):
+ for msg in self:
+ if not msg.email_from:
+ continue
+ if self.env.user.partner_id.email:
+ email_from = formataddr((self.env.user.partner_id.name, self.env.user.partner_id.email))
+ else:
+ email_from = self.env.user.company_id.catchall
+
+ body_html = tools.append_content_to_html('<div>%s</div>' % tools.ustr(comment), msg.body)
+ vals = {
+ 'subject': subject,
+ 'body_html': body_html,
+ 'email_from': email_from,
+ 'email_to': msg.email_from,
+ 'auto_delete': True,
+ 'state': 'outgoing'
+ }
+ self.env['mail.mail'].sudo().create(vals)
+
+ @api.multi
+ def _search_from_same_authors(self):
+ """ Returns all pending moderation messages that have same email_from and
+ same res_id as given recordset. """
+ messages = self.env['mail.message'].sudo()
+ for message in self:
+ messages |= messages.search([
+ ('moderation_status', '=', 'pending_moderation'),
+ ('email_from', '=', message.email_from),
+ ('model', '=', 'mail.channel'),
+ ('res_id', '=', message.res_id)
+ ])
+ return messages
+
+ @api.multi
+ def _moderate_discard(self):
+ """ Notify deletion of messages to their moderators and authors and then delete them.
+ """
+ channel_ids = self.mapped('res_id')
+ moderators = self.env['mail.channel'].browse(channel_ids).mapped('moderator_ids')
+ authors = self.mapped('author_id')
+ partner_to_pid = {}
+ for moderator in moderators:
+ partner_to_pid.setdefault(moderator.partner_id.id, set())
+ partner_to_pid[moderator.partner_id.id] |= set([message.id for message in self if message.res_id in moderator.moderation_channel_ids.ids])
+ for author in authors:
+ partner_to_pid.setdefault(author.id, set())
+ partner_to_pid[author.id] |= set([message.id for message in self if message.author_id == author])
+
+ notifications = []
+ for partner_id, message_ids in partner_to_pid.items():
+ notifications.append([
+ (self._cr.dbname, 'res.partner', partner_id),
+ {'type': 'deletion', 'message_ids': list(message_ids)}
+ ])
+ self.env['bus.bus'].sendmany(notifications)
+ self.unlink()
+
+ def _notify_pending_by_chat(self):
+ """ Generate the bus notifications for the given message and send them
+ to the appropriate moderators and the author (if the author has not been
+ elected moderator meanwhile). The author notification can be considered
+ as a feedback to the author.
+ """
+ self.ensure_one()
+ message = self.message_format()[0]
+ partners = self.env['mail.channel'].browse(self.res_id).mapped('moderator_ids.partner_id')
+ notifications = []
+ for partner in partners:
+ notifications.append([
+ (self._cr.dbname, 'res.partner', partner.id),
+ {'type': 'moderator', 'message': message}
+ ])
+ if self.author_id not in partners:
+ notifications.append([
+ (self._cr.dbname, 'res.partner', self.author_id.id),
+ {'type': 'author', 'message': message}
+ ])
+ self.env['bus.bus'].sendmany(notifications)
+
+ @api.model
+ def _notify_moderators(self):
+ """ Push a notification (Inbox/email) to moderators having messages
+ waiting for moderation. This method is called once a day by a cron.
+ """
+ channels = self.env['mail.channel'].browse(self.search([('moderation_status', '=', 'pending_moderation')]).mapped('res_id'))
+ moderators_to_notify = channels.mapped('moderator_ids')
+ template = self.env.ref('mail.mail_channel_notify_moderation', raise_if_not_found=False)
+ if not template:
+ _logger.warning('Template "mail.mail_channel_notify_moderation" was not found. Cannot send reminder notifications.')
+ return
+ MailThread = self.env['mail.thread'].with_context(mail_notify_author=True)
+ for moderator in moderators_to_notify:
+ MailThread.message_notify(
+ moderator.partner_id.ids,
+ subject=_('Message are pending moderation'), # tocheck: target language
+ body=template.render({'record': moderator.partner_id}, engine='ir.qweb', minimal_qcontext=True),
+ email_from=moderator.company_id.catchall or moderator.company_id.email,
+ )
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 5a7169ea9f1..4cde154e1cb 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -1294,7 +1294,7 @@ class MailThread(models.AbstractModel):
post_params['model'] = model
new_msg = thread.message_post(**post_params)
- if original_partner_ids:
+ if new_msg and original_partner_ids:
# postponed after message_post, because this is an external message and we don't want to create
# duplicate emails due to notifications
new_msg.write({'partner_ids': original_partner_ids})
@@ -1869,7 +1869,7 @@ class MailThread(models.AbstractModel):
return m2m_attachment_ids
@api.multi
- @api.returns('self', lambda value: value.id)
+ @api.returns('mail.message', lambda value: value.id)
def message_post(self, body='', subject=None,
message_type='notification', subtype=None,
parent_id=False, attachments=None,
@@ -1891,6 +1891,7 @@ class MailThread(models.AbstractModel):
to the related document. Should only be set by Chatter.
:return int: ID of newly created mail.message
"""
+
if attachments is None:
attachments = {}
if self.ids and not self.ensure_one():
@@ -1998,15 +1999,18 @@ class MailThread(models.AbstractModel):
message and computed value are given, to try to lessen query count by
using already-computed values instead of having to rebrowse things. """
# Notify recipients of the newly-created message (Inbox / Email + channels)
- message._notify(
- layout=notif_layout,
- force_send=self.env.context.get('mail_notify_force_send', True),
- values=notif_values,
- )
+ if values.get('moderation_status') != 'pending_moderation':
+ message._notify(
+ layout=notif_layout,
+ force_send=self.env.context.get('mail_notify_force_send', True),
+ values=notif_values,
+ )
- # Post-process: subscribe author
- if values['author_id'] and values['model'] and self.ids and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
- self._message_subscribe([values['author_id']])
+ # Post-process: subscribe author
+ if values['author_id'] and values['model'] and self.ids and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
+ self._message_subscribe([values['author_id']])
+ else:
+ message._notify_pending_by_chat()
@api.multi
def message_post_with_view(self, views_or_xmlid, **kwargs):
diff --git a/addons/mail/models/res_company.py b/addons/mail/models/res_company.py
new file mode 100644
index 00000000000..45011902e83
--- /dev/null
+++ b/addons/mail/models/res_company.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models, fields
+
+
+class Company(models.Model):
+ _name = 'res.company'
+ _inherit = 'res.company'
+
+ catchall = fields.Char(string="Catchall Email", compute="_compute_catchall")
+
+ @api.multi
+ def _compute_catchall(self):
+ ConfigParameter = self.env['ir.config_parameter'].sudo()
+ alias = ConfigParameter.get_param('mail.catchall.alias')
+ domain = ConfigParameter.get_param('mail.catchall.domain')
+ if alias and domain:
+ for company in self:
+ company.catchall = '%s@%s' % (alias, domain)
+ else:
+ for company in self:
+ company.catchall = ''
diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py
index f9ebd21eb1b..11d687cbca1 100644
--- a/addons/mail/models/res_users.py
+++ b/addons/mail/models/res_users.py
@@ -11,7 +11,7 @@ class Users(models.Model):
- make a new user follow itself
- add a welcome message
- add suggestion preference
- - if adding groups to an user, check mail.channels linked to this user
+ - if adding groups to a user, check mail.channels linked to this user
group, and the user. This is done by overriding the write method.
"""
_name = 'res.users'
@@ -31,6 +31,39 @@ class Users(models.Model):
help="Policy on how to handle Chatter notifications:\n"
"- Handle by Emails: notifications are sent to your email address\n"
"- Handle in Odoo: notifications appear in your Odoo Inbox")
+ # channel-specific: moderation
+ is_moderator = fields.Boolean(string='Is moderator', compute='_compute_is_moderator')
+ moderation_counter = fields.Integer(string='Moderation count', compute='_compute_moderation_counter')
+ moderation_channel_ids = fields.Many2many(
+ 'mail.channel', 'mail_channel_moderator_rel',
+ string='Moderated channels')
+
+ @api.depends('moderation_channel_ids.moderation', 'moderation_channel_ids.moderator_ids')
+ @api.multi
+ def _compute_is_moderator(self):
+ moderated = self.env['mail.channel'].search([
+ ('id', 'in', self.mapped('moderation_channel_ids').ids),
+ ('moderation', '=', True),
+ ('moderator_ids', 'in', self.ids)
+ ])
+ user_ids = moderated.mapped('moderator_ids')
+ for user in self:
+ user.is_moderator = user in user_ids
+
+ @api.multi
+ def _compute_moderation_counter(self):
+ self._cr.execute("""
+SELECT channel_moderator.res_users_id, COUNT(msg.id)
+FROM "mail_channel_moderator_rel" AS channel_moderator
+JOIN "mail_message" AS msg
+ON channel_moderator.mail_channel_id = msg.res_id
+ AND channel_moderator.res_users_id IN %s
+ AND msg.model = 'mail.channel'
+ AND msg.moderation_status = 'pending_moderation'
+GROUP BY channel_moderator.res_users_id""", [tuple(self.ids)])
+ result = dict(self._cr.fetchall())
+ for user in self:
+ user.moderation_counter = result.get(user.id, 0)
def __init__(self, pool, cr):
""" Override of __init__ to add access rights on notification_email_send