[REM] mail: remove mail channel moderation

Rationale
=========

Currently, mail channels have 2 modes
- They can be used like Discuss channel (chat, livechat, group)
- They can be used like a Mailing List (with the "email_send" field)

The mix of both feature in the same model causes some code complexity.
Several fields are not used in both cases (moderation related field)
and the future "Discord like" Discuss will even push the mail channel
further than the usage of the mailing list.

Because of all those reasons, we want to split the 2 mains features of
the mail channels into 2 different modules and models.

Purpose
=======

This commit remove the moderation feature of the mail channel
(email_send=True). This will be re-implemented in a separate module
in the next commit.

Do not be able to check messages in Discuss anymore because this
feature is only used to moderate the message and this feature will be
spitted in a new module "mail_group".

Links
=====

Task-2510267
See odoo/odoo/pull/71599
See odoo/enterprise/pull/19296
See odoo/upgrade/pull/2600
This commit is contained in:
std-odoo
2021-07-09 11:16:38 +00:00
committed by Thibault Delavallée
parent f7c43b4343
commit 59d6bb23d7
52 changed files with 37 additions and 3340 deletions
+1 -9
View File
@@ -2,7 +2,7 @@
{
'name': 'Discuss',
'version': '1.4',
'version': '1.5',
'category': 'Productivity/Discuss',
'sequence': 145,
'summary': 'Chat, mail gateway and private channels',
@@ -22,7 +22,6 @@
'views/mail_message_views.xml',
'views/mail_mail_views.xml',
'views/mail_followers_views.xml',
'views/mail_moderation_views.xml',
'views/mail_channel_partner_views.xml',
'views/mail_channel_views.xml',
'views/mail_shortcode_views.xml',
@@ -139,9 +138,6 @@
'mail/static/src/components/message_seen_indicator/message_seen_indicator.js',
'mail/static/src/components/messaging_menu/messaging_menu.js',
'mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.js',
'mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.js',
'mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js',
'mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js',
'mail/static/src/components/notification_alert/notification_alert.js',
'mail/static/src/components/notification_group/notification_group.js',
'mail/static/src/components/notification_list/notification_list.js',
@@ -301,7 +297,6 @@
'mail/static/src/components/dialog_manager/dialog_manager_tests.js',
'mail/static/src/components/discuss/tests/discuss_domain_tests.js',
'mail/static/src/components/discuss/tests/discuss_inbox_tests.js',
'mail/static/src/components/discuss/tests/discuss_moderation_tests.js',
'mail/static/src/components/discuss/tests/discuss_pinned_tests.js',
'mail/static/src/components/discuss/tests/discuss_sidebar_tests.js',
'mail/static/src/components/discuss/tests/discuss_tests.js',
@@ -396,9 +391,6 @@
'mail/static/src/components/message_seen_indicator/message_seen_indicator.xml',
'mail/static/src/components/messaging_menu/messaging_menu.xml',
'mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.xml',
'mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.xml',
'mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.xml',
'mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.xml',
'mail/static/src/components/notification_alert/notification_alert.xml',
'mail/static/src/components/notification_group/notification_group.xml',
'mail/static/src/components/notification_list/notification_list.xml',
-2
View File
@@ -264,8 +264,6 @@ class MailController(http.Controller):
'mention_partner_suggestions': request.env['res.partner'].get_static_mention_suggestions(),
'shortcodes': request.env['mail.shortcode'].sudo().search_read([], ['source', 'substitution', 'description']),
'menu_id': request.env['ir.model.data'].xmlid_to_res_id('mail.menu_root_discuss'),
'moderation_counter': request.env.user.moderation_counter,
'moderation_channel_ids': request.env.user.moderation_channel_ids.ids,
'partner_root': request.env.ref('base.partner_root').sudo().mail_partner_format(),
'public_partners': [partner.mail_partner_format() for partner in request.env.ref('base.group_public').sudo().with_context(active_test=False).users.partner_id],
'current_partner': request.env.user.partner_id.mail_partner_format(),
-13
View File
@@ -31,19 +31,6 @@
<field name="domain" eval="[('id','!=', ref('mail.ir_cron_module_update_notification'))]"/>
</record>
<record id="ir_cron_mail_notify_channel_moderators" model="ir.cron">
<field name="name">Mail: Notify channel moderators</field>
<field name="model_id" ref="model_mail_message"/>
<field name="state">code</field>
<field name="code">model._notify_moderators</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall" />
<field name="priority">1000</field>
</record>
<record id="ir_cron_delete_notification" model="ir.cron">
<field name="name">Notification: Delete Notifications older than 6 Month</field>
<field name="interval_number">1</field>
-24
View File
@@ -341,29 +341,5 @@
<div><t t-out="body"/></div>
<blockquote><t t-out="message['body']"/></blockquote>
</template>
<!-- Channel and moderation related data -->
<template id="mail_channel_notify_moderation">
<div>
<p>Hello <t t-esc='record.name'/></p>
<p>You have messages to moderate, please go for the proceedings.</p><br/><br/>
<div style="text-align: center;">
<a href="/web#action=mail.action_discuss&amp;active_id=mail.box_moderation" style="background-color: #1abc9c; padding: 20px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 16px;" class="o_default_snippet_text">Moderate Messages</a>
<br/><br/><br/>
</div>
<p>Thank you!</p>
</div>
</template>
<template id="mail_channel_send_guidelines">
<div>
<p>Hello <t t-esc="partner.name"/>,</p>
<p>Please find below the guidelines of the <t t-esc="channel.name"/> channel.</p>
<p>
<t t-esc="channel.moderation_guidelines_msg"/>
</p>
<p></p>
</div>
</template>
</data>
</odoo>
-1
View File
@@ -17,7 +17,6 @@ from . import mail_thread
from . import mail_thread_blacklist
from . import mail_thread_cc
from . import mail_blacklist
from . import mail_moderation
from . import mail_channel_partner
from . import mail_channel
from . import mail_template
+6 -213
View File
@@ -11,7 +11,6 @@ from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
from odoo.tools import ormcache, formataddr
MODERATION_FIELDS = ['moderation', 'moderator_ids', 'moderation_ids', 'moderation_notify', 'moderation_notify_msg', 'moderation_guidelines', 'moderation_guidelines_msg']
_logger = logging.getLogger(__name__)
@@ -48,7 +47,6 @@ class Channel(models.Model):
description = fields.Text('Description')
email_send = fields.Boolean('Send messages by email', default=False)
image_128 = fields.Image("Image", max_width=128, max_height=128, default=_get_default_image)
# members (depends=['...'] is for `test_mail/tests/common.py`, class Moderation, `setUpClass`)
channel_partner_ids = fields.Many2many(
'res.partner', string='Members',
compute='_compute_channel_partner_ids', inverse='_inverse_channel_partner_ids',
@@ -73,21 +71,6 @@ class Channel(models.Model):
help='This group is visible by non members. Invisible groups can add members through the invite button.')
group_public_id = fields.Many2one('res.groups', string='Authorized Group',
default=lambda self: self.env.ref('base.group_user'))
# 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")
# COMPUTE / INVERSE
@api.depends('channel_type')
@@ -133,46 +116,6 @@ class Channel(models.Model):
for channel in self:
channel.is_member = self.env.user.partner_id in channel.channel_partner_ids
@api.depends('moderator_ids')
@api.depends_context('uid')
def _compute_is_moderator(self):
for channel in self:
channel.is_moderator = self.env.user in channel.moderator_ids
@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)
# CONSTRAINTS
@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')
def _check_moderator_is_member(self):
for channel in self:
if channel.moderator_ids.partner_id > channel.channel_partner_ids:
missing = channel.mapped('moderator_ids.partner_id') - channel.channel_partner_ids
raise ValidationError(
_("Moderators should be members of the channel they moderate (missing %(user_names)s).",
user_names=', '.join(missing.mapped('name')))
)
@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.'))
# ONCHANGE
@api.onchange('public')
@@ -180,29 +123,6 @@ class Channel(models.Model):
if self.public != 'public' and self.alias_contact == 'everyone':
self.alias_contact = 'followers'
@api.onchange('moderator_ids')
def _onchange_moderator_ids(self):
missing_partner_ids = set(self.mapped('moderator_ids.partner_id').ids) - set(self.mapped('channel_last_seen_partner_ids.partner_id').ids)
if missing_partner_ids:
self.channel_last_seen_partner_ids = [
Command.create({'partner_id': partner_id})
for partner_id in missing_partner_ids
]
@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
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@@ -271,27 +191,11 @@ class Channel(models.Model):
raise UserError(_('You cannot delete those groups, as the Whole Company group is required by other modules.'))
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 not self.env.user.has_group('base.group_system'):
not_moderator = self.filtered(lambda channel: channel.moderation and self.env.user not in channel.moderator_ids)
if not_moderator:
raise UserError(_("You do not have rights to modify moderation configuration on %(channel_names)s",
channel_names=', '.join(not_moderator.mapped('name'))
))
result = super(Channel, self).write(vals)
if vals.get('group_ids'):
self._subscribe_users_automatically()
# 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 init(self):
@@ -568,19 +472,12 @@ class Channel(models.Model):
return super(Channel, self)._notify_email_recipient_values(whitelist.ids)
def _notify_thread(self, message, msg_vals=False, **kwargs):
# When posting a message on a mail channel, manage moderation and postpone notify users
if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation':
# link message to channel
rdata = super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
message_format_values = message.message_format()[0]
bus_notifications = self._channel_message_notifications(message, message_format_values)
self.env['bus.bus'].sudo().sendmany(bus_notifications)
else:
message._notify_pending_by_chat()
rdata = False
# link message to channel
rdata = super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
message_format_values = message.message_format()[0]
bus_notifications = self._channel_message_notifications(message, message_format_values)
self.env['bus.bus'].sudo().sendmany(bus_notifications)
return rdata
def _message_receive_bounce(self, email, partner):
@@ -592,10 +489,6 @@ class Channel(models.Model):
@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.is_chat).mapped('channel_last_seen_partner_ids').sudo().write({'is_pinned': True})
# mail_post_autofollow=False is necessary to prevent adding followers
@@ -605,21 +498,7 @@ class Channel(models.Model):
# The current client code might be setting the key to True on sending
# message but it is only useful when targeting customers in chatter.
# This value should simply be set to False in channels no matter what.
message = super(Channel, self.with_context(mail_create_nosubscribe=True, mail_post_autofollow=False)).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 in ['email','comment'] and moderation_status == 'pending_moderation':
self.env['mail.mail'].sudo().create({
'author_id': self.env.user.partner_id.id,
'email_from': self.env.user.company_id.catchall_formatted or self.env.user.company_id.email_formatted,
'body_html': self.moderation_notify_msg,
'subject': 'Re: %s' % (kwargs.get('subject', '')),
'email_to': email,
'auto_delete': True,
'state': 'outgoing'
})
return message
return super(Channel, self.with_context(mail_create_nosubscribe=True, mail_post_autofollow=False)).message_post(message_type=message_type, **kwargs)
def _message_post_after_hook(self, message, msg_vals):
"""
@@ -628,92 +507,11 @@ class Channel(models.Model):
self._set_last_seen_message(message)
return super()._message_post_after_hook(message=message, msg_vals=msg_vals)
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, int):
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
def _message_subscribe(self, partner_ids=None, subtype_ids=None, customer_ids=None):
""" Do not allow follower subscription on channels. Only members are
considered. """
raise UserError(_('Adding followers on channels is not possible. Consider adding members instead.'))
# ------------------------------------------------------------
# MODERATION
# ------------------------------------------------------------
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(_('View "mail.mail_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!"))
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()
view = self.env.ref('mail.mail_channel_send_guidelines', raise_if_not_found=False)
if not view:
_logger.warning('View "mail.mail_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)):
company = partner.company_id or self.env.company
create_values = {
'email_from': company.catchall_formatted or company.email_formatted,
'author_id': self.env.user.partner_id.id,
'body_html': view._render({'channel': self, 'partner': partner}, engine='ir.qweb', minimal_qcontext=True),
'subject': _("Guidelines of channel %s", self.name),
'recipient_ids': [Command.link(partner.id)]
}
mail = self.env['mail.mail'].sudo().create(create_values)
return True
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 = [Command.update(record.id, {'status': status}) for record in moderated]
not_moderated = [email for email in splitted_emails if email not in moderated.mapped('email')]
cmds += [Command.create({'email': email, 'status': status}) for email in not_moderated]
return self.write({'moderation_ids': cmds})
# ------------------------------------------------------------
# BROADCAST
# ------------------------------------------------------------
@@ -813,8 +611,6 @@ 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),
'create_uid': channel.create_uid.id,
}
@@ -1126,9 +922,6 @@ class Channel(models.Model):
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>', self.id, self.name)
self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment")
if added and self.moderation_guidelines:
self._send_guidelines(self.env.user.partner_id)
channel_info = self.channel_info('join')[0]
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
return channel_info
+25 -260
View File
@@ -162,13 +162,6 @@ 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')
# keep notification layout informations to be able to generate mail again
email_layout_xmlid = fields.Char('Layout', copy=False) # xml id of layout
add_sign = fields.Boolean(default=True)
@@ -232,21 +225,6 @@ 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])]
def _compute_need_moderation(self):
for message in self:
message.need_moderation = False
@api.model
def _search_need_moderation(self, operator, operand):
if operator == '=' and operand is True:
return ['&', '&',
('moderation_status', '=', 'pending_moderation'),
('model', '=', 'mail.channel'),
('res_id', 'in', self.env.user.moderation_channel_ids.ids)]
# no support for other operators
raise UserError(_('Unsupported search filter on moderation status'))
# ------------------------------------------------------
# CRUD / ORM
# ------------------------------------------------------
@@ -360,12 +338,10 @@ class Message(models.Model):
- write: if
- author_id == pid, uid is the author, OR
- uid is in the recipients (partner_ids) OR
- 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
- uid has write or create access on the related document if model, res_id
- otherwise: raise
- unlink: if
- 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
- uid has write or create access on the related document
- otherwise: raise
Specific case: non employee users see only messages with subtype (aka do
@@ -402,16 +378,13 @@ class Message(models.Model):
# Read mail_message.ids to have their values
message_values = dict((message_id, {}) for message_id in self.ids)
self.flush(['model', 'res_id', 'author_id', 'parent_id', 'moderation_status', 'message_type', 'partner_ids'])
self.flush(['model', 'res_id', 'author_id', 'parent_id', 'message_type', 'partner_ids'])
self.env['mail.notification'].flush(['mail_message_id', 'res_partner_id'])
self.env['mail.channel'].flush(['moderator_ids'])
self.env['res.users'].flush(['moderation_channel_ids'])
if operation == 'read':
self._cr.execute("""
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id,
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
m.moderation_status,
m.message_type as message_type
FROM "%s" m
LEFT JOIN "mail_message_res_partner_rel" partner_rel
@@ -419,74 +392,47 @@ class Message(models.Model):
LEFT JOIN "mail_notification" needaction_rel
ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_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, moderation_status, message_type in self._cr.fetchall():
for mid, rmod, rid, author_id, parent_id, partner_id, message_type 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)),
'message_type': message_type,
}
elif operation == 'write':
self._cr.execute("""
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status,
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id,
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
channel_moderator_rel.res_users_id as moderator_id,
m.message_type as message_type
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_notification" needaction_rel
ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_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, moderator_id, message_type in self._cr.fetchall():
for mid, rmod, rid, author_id, parent_id, partner_id, message_type 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)),
'message_type': message_type,
}
elif operation == 'create':
self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id, moderation_status, message_type FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
for mid, rmod, rid, author_id, parent_id, moderation_status, message_type in self._cr.fetchall():
elif operation in ('create', 'unlink'):
self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id, message_type FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
for mid, rmod, rid, author_id, parent_id, message_type 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,
'message_type': message_type,
}
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, m.message_type as message_type
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, message_type 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,
'message_type': message_type,
}
else:
raise ValueError(_('Wrong operation name (%s)', operation))
# Author condition (READ, WRITE, CREATE (private))
author_ids = []
@@ -494,18 +440,13 @@ class Message(models.Model):
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]
author_ids = [mid for mid, message in message_values.items() if 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 self.is_thread_message(message)]
# 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')]
messages_to_check = self.ids
messages_to_check = set(messages_to_check).difference(set(author_ids), set(moderator_ids))
messages_to_check = set(messages_to_check).difference(set(author_ids))
if not messages_to_check:
return
@@ -522,10 +463,11 @@ class Message(models.Model):
# CRUD: Access rights related to the document
document_related_ids = []
document_related_candidate_ids = [mid for mid, message in message_values.items()
if (message.get('model') and message.get('res_id') and
message.get('message_type') != 'user_notification' and
(message.get('moderation_status') != 'pending_moderation' or operation not in ['write', 'unlink']))]
document_related_candidate_ids = [
mid for mid, message in message_values.items()
if (message.get('model') and message.get('res_id') and
message.get('message_type') != 'user_notification')
]
model_record_ids = _generate_model_record_ids(message_values, document_related_candidate_ids)
for model, doc_ids in model_record_ids.items():
DocumentModel = self.env[model]
@@ -538,11 +480,12 @@ class Message(models.Model):
mids = records.browse(doc_ids)._filter_access_rules(check_operation)
document_related_ids += [
mid for mid, message in message_values.items()
if (message.get('model') == model and
if (
message.get('model') == model and
message.get('res_id') in mids.ids and
message.get('message_type') != 'user_notification' and
(message.get('moderation_status') != 'pending_moderation' or
operation not in ['write', 'unlink']))]
message.get('message_type') != 'user_notification'
)
]
messages_to_check = messages_to_check.difference(set(document_related_ids))
@@ -794,163 +737,6 @@ class Message(models.Model):
notification = {'type': 'toggle_star', 'message_ids': [self.id], 'starred': starred}
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification)
# --------------------------------------------------
# MODERATION API
# --------------------------------------------------
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)
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:
if message.is_thread_message(): # note, since we will only intercept _notify_thread for message posted on channel,
# message will always be a thread_message. This check should always be true.
self.env[message.model].browse(message.res_id)._notify_thread(message)
def _moderate_send_reject_email(self, subject, comment):
for msg in self:
if not msg.email_from:
continue
body_html = tools.append_content_to_html('<div>%s</div>' % tools.ustr(comment), msg.body, plaintext=False)
vals = {
'subject': subject,
'body_html': body_html,
'author_id': self.env.user.partner_id.id,
'email_from': self.env.user.email_formatted or self.env.company.catchall_formatted,
'email_to': msg.email_from,
'auto_delete': True,
'state': 'outgoing'
}
self.env['mail.mail'].sudo().create(vals)
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
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': sorted(list(message_ids))} # sorted to make deterministic for tests
])
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(
partner_ids=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_formatted or moderator.company_id.email_formatted,
)
# ------------------------------------------------------
# MESSAGE READ / FETCH / FAILURE API
# ------------------------------------------------------
@@ -1027,32 +813,13 @@ class Message(models.Model):
return messages._message_notification_format()
@api.model
def message_fetch(self, domain, limit=20, moderated_channel_ids=None):
def message_fetch(self, domain, limit=20):
""" Get a limited amount of formatted messages with provided domain.
:param domain: the domain to filter messages;
:param limit: the maximum amount of messages to get;
:param list(int) moderated_channel_ids: if set, it contains the ID
of a moderated channel. Fetched messages should include pending
moderation messages for moderators. If the current user is not
moderator, it should still get self-authored messages that are
pending moderation;
:returns list(dict).
"""
messages = self.search(domain, limit=limit)
if moderated_channel_ids:
# Split load moderated and regular messages, as the ORed domain can
# cause performance issues on large databases.
moderated_messages_dom = [
('model', '=', 'mail.channel'),
('res_id', 'in', moderated_channel_ids),
'|',
('author_id', '=', self.env.user.partner_id.id),
('moderation_status', '=', 'pending_moderation'),
]
messages |= self.search(moderated_messages_dom, limit=limit)
# Truncate the results to `limit`
messages = messages.sorted(key='id', reverse=True)[:limit]
return messages.message_format()
return self.search(domain, limit=limit).message_format()
def message_format(self):
""" Get the message values in the format for web client. Since message values can be broadcasted,
@@ -1092,7 +859,6 @@ 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'
}
"""
vals_list = self._message_format(self._get_message_format_fields())
@@ -1122,7 +888,6 @@ class Message(models.Model):
'model', 'res_id', 'record_name', # document related
'partner_ids', # recipients
'starred_partner_ids', # list of partner ids for whom the message is starred
'moderation_status',
]
def _message_notification_format(self):
-20
View File
@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
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 !')
]
-3
View File
@@ -2106,9 +2106,6 @@ class MailThread(models.AbstractModel):
* performs the notification process by calling the various notification
methods implemented;
This method cnn be overridden to intercept and postpone notification
mechanism like mail.channel moderation.
:param message: mail.message record to notify;
:param msg_vals: dictionary of values used to create the message. If given
it is used instead of accessing ``self`` to lessen query count in some
-31
View File
@@ -25,37 +25,6 @@ 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')
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
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)
@property
def SELF_READABLE_FIELDS(self):
-1
View File
@@ -17,7 +17,6 @@ access_mail_channel_user,mail.group.user,model_mail_channel,base.group_user,1,1,
access_mail_channel_partner_public,mail.channel.partner.public,model_mail_channel_partner,base.group_public,1,0,0,0
access_mail_channel_partner_portal,mail.channel.partner.portal,model_mail_channel_partner,base.group_portal,1,1,1,1
access_mail_channel_partner_user,mail.channel.partner.user,model_mail_channel_partner,base.group_user,1,1,1,1
access_mail_moderation_user,mail.moderation.user,model_mail_moderation,base.group_user,1,1,1,1
access_mail_alias_all,mail.alias.all,model_mail_alias,,1,0,0,0
access_mail_alias_user,mail.alias.user,model_mail_alias,base.group_user,1,0,0,0
access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
17 access_mail_channel_partner_public mail.channel.partner.public model_mail_channel_partner base.group_public 1 0 0 0
18 access_mail_channel_partner_portal mail.channel.partner.portal model_mail_channel_partner base.group_portal 1 1 1 1
19 access_mail_channel_partner_user mail.channel.partner.user model_mail_channel_partner base.group_user 1 1 1 1
access_mail_moderation_user mail.moderation.user model_mail_moderation base.group_user 1 1 1 1
20 access_mail_alias_all mail.alias.all model_mail_alias 1 0 0 0
21 access_mail_alias_user mail.alias.user model_mail_alias base.group_user 1 0 0 0
22 access_mail_alias_system mail.alias.system model_mail_alias base.group_system 1 1 1 1
-6
View File
@@ -55,12 +55,6 @@
<field name="perm_unlink" eval="True"/>
</record>
<record id="mail_moderation_rule_user" model="ir.rule">
<field name="name">White/Black List: moderators: moderated channels only</field>
<field name="model_id" ref="model_mail_moderation"/>
<field name="domain_force">[('channel_id.moderator_ids', 'in', user.id)]</field>
</record>
<record id="mail_compose_message_rule" model="ir.rule">
<field name="name">Mail Compose Message Rule</field>
<field name="model_id" ref="model_mail_compose_message"/>
@@ -7,8 +7,6 @@ import { Composer } from '@mail/components/composer/composer';
import { DiscussMobileMailboxSelection } from '@mail/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection';
import { DiscussSidebar } from '@mail/components/discuss_sidebar/discuss_sidebar';
import { MobileMessagingNavbar } from '@mail/components/mobile_messaging_navbar/mobile_messaging_navbar';
import { ModerationDiscardDialog } from '@mail/components/moderation_discard_dialog/moderation_discard_dialog';
import { ModerationRejectDialog } from '@mail/components/moderation_reject_dialog/moderation_reject_dialog';
import { NotificationList } from '@mail/components/notification_list/notification_list';
import { ThreadView } from '@mail/components/thread_view/thread_view';
@@ -21,8 +19,6 @@ const components = {
DiscussMobileMailboxSelection,
DiscussSidebar,
MobileMessagingNavbar,
ModerationDiscardDialog,
ModerationRejectDialog,
NotificationList,
ThreadView,
};
@@ -34,12 +30,7 @@ export class Discuss extends Component {
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore((...args) => this._useStoreSelector(...args), {
compareDepth: {
checkedMessages: 1,
uncheckedMessages: 1,
},
});
useStore((...args) => this._useStoreSelector(...args), {});
this._updateLocalStoreProps();
/**
* Reference of the composer. Useful to focus it.
@@ -173,15 +164,10 @@ export class Discuss extends Component {
const threadView = discuss && discuss.threadView;
const replyingToMessage = discuss && discuss.replyingToMessage;
const replyingToMessageOriginThread = replyingToMessage && replyingToMessage.originThread;
const checkedMessages = threadView ? threadView.checkedMessages : [];
return {
checkedMessages,
checkedMessagesIsModeratedByCurrentPartner: checkedMessages && checkedMessages.some(message => message.isModeratedByCurrentPartner), // for widget
discuss,
discussActiveId: discuss && discuss.activeId, // for widget
discussActiveMobileNavbarTabId: discuss && discuss.activeMobileNavbarTabId,
discussHasModerationDiscardDialog: discuss && discuss.hasModerationDiscardDialog,
discussHasModerationRejectDialog: discuss && discuss.hasModerationRejectDialog,
discussIsAddingChannel: discuss && discuss.isAddingChannel,
discussIsAddingChat: discuss && discuss.isAddingChat,
discussIsDoFocus: discuss && discuss.isDoFocus,
@@ -200,7 +186,6 @@ export class Discuss extends Component {
threadPublic: thread && thread.public, // for widget
threadView,
threadViewMessagesLength: threadView && threadView.messages.length, // for widget
uncheckedMessages: threadView ? threadView.uncheckedMessages : [], // for widget
};
}
@@ -208,20 +193,6 @@ export class Discuss extends Component {
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onDialogClosedModerationDiscard() {
this.discuss.update({ hasModerationDiscardDialog: false });
}
/**
* @private
*/
_onDialogClosedModerationReject() {
this.discuss.update({ hasModerationRejectDialog: false });
}
/**
* @private
* @param {CustomEvent} ev
@@ -21,12 +21,6 @@
<t t-call="mail.Discuss.content"/>
</div>
</t>
<t t-if="discuss.hasModerationDiscardDialog">
<ModerationDiscardDialog messageLocalIds="discuss.threadView.checkedMessages.map(message => message.localId)" t-on-dialog-closed="_onDialogClosedModerationDiscard"/>
</t>
<t t-if="discuss.hasModerationRejectDialog">
<ModerationRejectDialog messageLocalIds="discuss.threadView.checkedMessages.map(message => message.localId)" t-on-dialog-closed="_onDialogClosedModerationReject"/>
</t>
</t>
</div>
</t>
@@ -56,7 +50,6 @@
hasComposer="discuss.thread.model !== 'mail.box'"
hasComposerCurrentPartnerAvatar="!env.messaging.device.isMobile"
hasComposerThreadTyping="true"
hasMessageCheckbox="true"
hasSquashCloseMessages="discuss.thread.model !== 'mail.box'"
haveMessagesMarkAsReadIcon="discuss.thread === env.messaging.inbox"
haveMessagesReplyIcon="discuss.thread === env.messaging.inbox"
@@ -330,78 +330,6 @@ QUnit.test('discuss should refresh filtered thread on changing thread', async fu
);
});
QUnit.test('select all and unselect all buttons should work on filtered thread', async function (assert) {
assert.expect(4);
this.data['mail.channel'].records.push({
id: 20,
is_moderator: true,
moderation: true,
name: "general",
});
this.data['mail.message'].records.push({
body: "<p>test</p>",
model: 'mail.channel',
moderation_status: 'pending_moderation',
res_id: 20,
});
await this.start();
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => {
document.querySelector(`
.o_DiscussSidebar_item[data-thread-local-id="${this.env.messaging.moderation.localId}"]
`).click();
},
message: "should wait until moderation box is loaded after clicking on it",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread.model === 'mail.box' &&
threadViewer.thread.id === 'moderation'
);
},
});
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => {
// simulate control panel search
this.env.messaging.discuss.update({
stringifiedDomain: JSON.stringify([['body', 'ilike', 'test']]),
});
},
message: "should wait until search filter is applied",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread.model === 'mail.box' &&
threadViewer.thread.id === 'moderation'
);
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should only have the 1 message containing 'test' in moderation box"
);
assert.notOk(
document.querySelector('.o_Message_checkbox').checked,
"the moderation checkbox should not be checked initially"
);
await afterNextRender(() => document.querySelector('.o_widget_Discuss_controlPanelButtonSelectAll').click());
assert.ok(
document.querySelector('.o_Message_checkbox').checked,
"the moderation checkbox should be checked after clicking on 'select all'"
);
await afterNextRender(() => document.querySelector('.o_widget_Discuss_controlPanelButtonUnselectAll').click());
assert.notOk(
document.querySelector('.o_Message_checkbox').checked,
"the moderation checkbox should be unchecked after clicking on 'unselect all'"
);
});
});
});
});
File diff suppressed because it is too large Load Diff
@@ -5,9 +5,6 @@ import { useStore } from '@mail/component_hooks/use_store/use_store';
import { useUpdate } from '@mail/component_hooks/use_update/use_update';
import { AttachmentList } from '@mail/components/attachment_list/attachment_list';
import { MessageSeenIndicator } from '@mail/components/message_seen_indicator/message_seen_indicator';
import { ModerationBanDialog } from '@mail/components/moderation_ban_dialog/moderation_ban_dialog';
import { ModerationDiscardDialog } from '@mail/components/moderation_discard_dialog/moderation_discard_dialog';
import { ModerationRejectDialog } from '@mail/components/moderation_reject_dialog/moderation_reject_dialog';
import { NotificationPopover } from '@mail/components/notification_popover/notification_popover';
import { PartnerImStatusIcon } from '@mail/components/partner_im_status_icon/partner_im_status_icon';
import { isEventHandled, markEventHandled } from '@mail/utils/utils';
@@ -24,9 +21,6 @@ const READ_LESS = _lt("read less");
const components = {
AttachmentList,
MessageSeenIndicator,
ModerationBanDialog,
ModerationDiscardDialog,
ModerationRejectDialog,
NotificationPopover,
PartnerImStatusIcon,
};
@@ -39,12 +33,6 @@ export class Message extends Component {
constructor(...args) {
super(...args);
this.state = useState({
// Determine if the moderation ban dialog is displayed.
hasModerationBanDialog: false,
// Determine if the moderation discard dialog is displayed.
hasModerationDiscardDialog: false,
// Determine if the moderation reject dialog is displayed.
hasModerationRejectDialog: false,
/**
* Determine whether the message is clicked. When message is in
* clicked state, it keeps displaying the commands.
@@ -68,11 +56,7 @@ export class Message extends Component {
authorImStatus: author && author.im_status,
authorNameOrDisplayName: author && author.nameOrDisplayName,
correspondent: thread && thread.correspondent,
hasMessageCheckbox: message ? message.hasCheckbox : false,
isDeviceMobile: this.env.messaging.device.isMobile,
isMessageChecked: message && threadView
? message.isChecked(thread, threadView.stringifiedDomain)
: false,
isMessageSelected: message && threadView && threadView.threadViewer
? threadView.threadViewer.selectedMessage === message
: false,
@@ -486,13 +470,6 @@ export class Message extends Component {
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onChangeCheckbox() {
this.message.toggleCheck(this.threadView.thread, this.threadView.stringifiedDomain);
}
/**
* @private
* @param {MouseEvent} ev
@@ -560,51 +537,6 @@ export class Message extends Component {
this.message.openResendAction();
}
/**
* @private
* @param {MouseEvent} ev
*/
_onClickModerationAccept(ev) {
ev.preventDefault();
this.message.moderate('accept');
}
/**
* @private
* @param {MouseEvent} ev
*/
_onClickModerationAllow(ev) {
ev.preventDefault();
this.message.moderate('allow');
}
/**
* @private
* @param {MouseEvent} ev
*/
_onClickModerationBan(ev) {
ev.preventDefault();
this.state.hasModerationBanDialog = true;
}
/**
* @private
* @param {MouseEvent} ev
*/
_onClickModerationDiscard(ev) {
ev.preventDefault();
this.state.hasModerationDiscardDialog = true;
}
/**
* @private
* @param {MouseEvent} ev
*/
_onClickModerationReject(ev) {
ev.preventDefault();
this.state.hasModerationRejectDialog = true;
}
/**
* @private
* @param {MouseEvent} ev
@@ -648,34 +580,11 @@ export class Message extends Component {
this.message.replyTo();
}
}
/**
* @private
*/
_onDialogClosedModerationBan() {
this.state.hasModerationBanDialog = false;
}
/**
* @private
*/
_onDialogClosedModerationDiscard() {
this.state.hasModerationDiscardDialog = false;
}
/**
* @private
*/
_onDialogClosedModerationReject() {
this.state.hasModerationRejectDialog = false;
}
}
Object.assign(Message, {
components,
defaultProps: {
hasCheckbox: false,
hasMarkAsReadIcon: false,
hasReplyIcon: false,
isSquashed: false,
@@ -686,7 +595,6 @@ Object.assign(Message, {
optional: true,
validate: prop => ['auto', 'card', 'hover', 'none'].includes(prop),
},
hasCheckbox: Boolean,
hasMarkAsReadIcon: Boolean,
hasReplyIcon: Boolean,
isSquashed: Boolean,
@@ -28,10 +28,6 @@
margin-inline-end: map-get($spacers, 2);
}
.o_Message_checkbox {
margin-inline-end: map-get($spacers, 2);
}
.o_Message_commandStar {
font-size: 1.3em;
}
@@ -116,20 +112,6 @@
width: $o-mail-discuss-message-highlight-indicator-width;
}
.o_Message_moderationAction {
margin-inline-end: map-get($spacers, 3);
}
.o_Message_moderationPending {
margin-inline-end: map-get($spacers, 3);
}
.o_Message_moderationSubHeader {
display: flex;
flex-flow: row wrap;
align-items: center;
}
.o_Message_originThread {
margin-inline-end: map-get($spacers, 2);
}
@@ -344,37 +326,6 @@
font-size: x-small;
}
.o_Message_moderationAction {
font-weight: $font-weight-bold;
font-style: italic;
&.o-accept,
&.o-allow {
color: $o-mail-moderation-accept-color;
@include hover-focus {
color: darken($o-mail-moderation-accept-color, $emphasized-link-hover-darken-percentage);
}
}
&.o-ban,
&.o-discard,
&.o-reject {
color: $o-mail-moderation-reject-color;
@include hover-focus {
color: darken($o-mail-moderation-reject-color, $emphasized-link-hover-darken-percentage);
}
}
}
.o_Message_moderationPending {
font-style: italic;
&.o-author {
color: o-text-color('danger');
font-weight: $font-weight-bold;
}
}
.o_Message_notificationIconClickable {
color: gray('600');
cursor: pointer;
@@ -96,9 +96,6 @@
</t>
</div>
</t>
<t t-if="message.moderation_status === 'pending_moderation' and !message.isModeratedByCurrentPartner">
<span class="o_Message_moderationPending o-author" title="Your message is pending moderation.">Pending moderation</span>
</t>
<t t-if="threadView and message.originThread and message.originThread === threadView.thread and message.notifications.length > 0">
<t t-if="message.failureNotifications.length > 0">
<span class="o_Message_notificationIconClickable o-error" t-on-click="_onClickFailure">
@@ -119,7 +116,7 @@
</t>
</t>
<div class="o_Message_commands o_Message_headerCommands" t-att-class="{ 'o-mobile': env.messaging.device.isMobile }">
<t t-if="!message.isTemporary and ((message.message_type !== 'notification' and message.originThread and message.originThread.model === 'mail.channel') or !message.isTransient) and message.moderation_status !== 'pending_moderation'">
<t t-if="!message.isTemporary and ((message.message_type !== 'notification' and message.originThread and message.originThread.model === 'mail.channel') or !message.isTransient)">
<span class="o_Message_command o_Message_commandStar o_Message_headerCommand fa"
t-att-class="{
'fa-star': message.isStarred,
@@ -148,19 +145,6 @@
</t>
</div>
</div>
<t t-if="message.isModeratedByCurrentPartner">
<div class="o_Message_moderationSubHeader">
<t t-if="threadView and props.hasCheckbox and message.hasCheckbox">
<input class="o_Message_checkbox" type="checkbox" t-att-checked="message.isChecked(threadView.thread, threadView.stringifiedDomain) ? 'checked': ''" t-on-change="_onChangeCheckbox" t-ref="checkbox"/>
</t>
<span class="o_Message_moderationPending">Pending moderation:</span>
<a class="o_Message_moderationAction o-accept" href="#" title="Accept" t-on-click="_onClickModerationAccept">Accept</a>
<a class="o_Message_moderationAction o-reject" href="#" title="Remove message with explanation" t-on-click="_onClickModerationReject">Reject</a>
<a class="o_Message_moderationAction o-discard" href="#" title="Remove message without explanation" t-on-click="_onClickModerationDiscard">Discard</a>
<a class="o_Message_moderationAction o-allow" href="#" title="Add this email address to white list of people" t-on-click="_onClickModerationAllow">Always Allow</a>
<a class="o_Message_moderationAction o-ban" href="#" title="Ban this email address" t-on-click="_onClickModerationBan">Ban</a>
</div>
</t>
</t>
<div class="o_Message_content" t-ref="content">
<div class="o_Message_prettyBody" t-ref="prettyBody"/><!-- message.prettyBody is inserted here from _update() -->
@@ -201,15 +185,6 @@
</div>
</t>
</div>
<t t-if="state.hasModerationBanDialog">
<ModerationBanDialog messageLocalIds="[message.localId]" t-on-dialog-closed="_onDialogClosedModerationBan"/>
</t>
<t t-if="state.hasModerationDiscardDialog">
<ModerationDiscardDialog messageLocalIds="[message.localId]" t-on-dialog-closed="_onDialogClosedModerationDiscard"/>
</t>
<t t-if="state.hasModerationRejectDialog">
<ModerationRejectDialog messageLocalIds="[message.localId]" t-on-dialog-closed="_onDialogClosedModerationReject"/>
</t>
</t>
</div>
</t>
@@ -123,80 +123,6 @@ QUnit.test('basic rendering', async function (assert) {
);
});
QUnit.test('moderation: as author, moderated channel with pending moderation message', async function (assert) {
assert.expect(1);
this.data['mail.channel'].records.push({ id: 20 });
await this.start();
const thread = this.env.models['mail.thread'].findFromIdentifyingData({
id: 20,
model: 'mail.channel',
});
const message = this.env.models['mail.message'].create({
author: insert({ id: 1, display_name: "Admin" }),
body: "<p>Test</p>",
id: 100,
moderation_status: 'pending_moderation',
originThread: link(thread),
});
await this.createMessageComponent(message);
assert.strictEqual(
document.querySelectorAll(`.o_Message_moderationPending.o-author`).length,
1,
"should have the message pending moderation"
);
});
QUnit.test('moderation: as moderator, moderated channel with pending moderation message', async function (assert) {
assert.expect(9);
this.data['mail.channel'].records.push({
id: 20,
is_moderator: true,
});
await this.start();
const thread = this.env.models['mail.thread'].findFromIdentifyingData({
id: 20,
model: 'mail.channel',
});
const message = this.env.models['mail.message'].create({
author: insert({ id: 7, display_name: "Demo User" }),
body: "<p>Test</p>",
id: 100,
moderation_status: 'pending_moderation',
originThread: link(thread),
});
await this.createMessageComponent(message);
const messageEl = document.querySelector('.o_Message');
assert.ok(messageEl, "should display a message");
assert.containsOnce(messageEl, `.o_Message_moderationSubHeader`,
"should have the message pending moderation"
);
assert.containsNone(messageEl, `.o_Message_checkbox`,
"should not have the moderation checkbox by default"
);
assert.containsN(messageEl, '.o_Message_moderationAction', 5,
"there should be 5 contextual moderation decisions next to the message"
);
assert.containsOnce(messageEl, '.o_Message_moderationAction.o-accept',
"there should be a contextual moderation decision to accept the message"
);
assert.containsOnce(messageEl, '.o_Message_moderationAction.o-reject',
"there should be a contextual moderation decision to reject the message"
);
assert.containsOnce(messageEl, '.o_Message_moderationAction.o-discard',
"there should be a contextual moderation decision to discard the message"
);
assert.containsOnce(messageEl, '.o_Message_moderationAction.o-allow',
"there should be a contextual moderation decision to allow the user of the message)"
);
assert.containsOnce(messageEl, '.o_Message_moderationAction.o-ban',
"there should be a contextual moderation decision to ban the user of the message"
);
// The actions are tested as part of discuss tests.
});
QUnit.test('Notification Sent', async function (assert) {
assert.expect(9);
@@ -315,12 +315,6 @@ export class MessageList extends Component {
if (prevMessage.originThread !== message.originThread) {
return false;
}
if (
prevMessage.moderation_status === 'pending_moderation' ||
message.moderation_status === 'pending_moderation'
) {
return false;
}
if (
prevMessage.notifications.length > 0 ||
message.notifications.length > 0
@@ -583,7 +577,6 @@ export class MessageList extends Component {
Object.assign(MessageList, {
components,
defaultProps: {
hasMessageCheckbox: false,
hasScrollAdjust: true,
hasSquashCloseMessages: false,
haveMessagesMarkAsReadIcon: false,
@@ -599,7 +592,6 @@ Object.assign(MessageList, {
type: Function,
optional: true,
},
hasMessageCheckbox: Boolean,
hasSquashCloseMessages: Boolean,
haveMessagesMarkAsReadIcon: Boolean,
haveMessagesReplyIcon: Boolean,
@@ -25,12 +25,6 @@
</div>
Messages marked as read will appear in the history.
</t>
<t t-elif="threadView.thread === env.messaging.moderation">
<div class="o_MessageList_emptyTitle">
You have no messages to moderate.
</div>
Messages pending moderation appear here.
</t>
<t t-else="">
There are no messages in this conversation.
</t>
@@ -74,7 +68,6 @@
<Message
class="o_MessageList_item o_MessageList_message"
hasMarkAsReadIcon="props.haveMessagesMarkAsReadIcon"
hasCheckbox="props.hasMessageCheckbox"
hasReplyIcon="props.haveMessagesReplyIcon"
isSquashed="isMessageSquashed"
messageLocalId="message.localId"
@@ -1,89 +0,0 @@
/** @odoo-module **/
import { useShouldUpdateBasedOnProps } from '@mail/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props';
import { useStore } from '@mail/component_hooks/use_store/use_store';
import Dialog from 'web.OwlDialog';
const { Component } = owl;
const { useRef } = owl.hooks;
const components = { Dialog };
export class ModerationBanDialog extends Component {
/**
* @override
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
messageLocalIds: 1,
},
});
useStore(props => {
const messages = props.messageLocalIds.map(localId =>
this.env.models['mail.message'].get(localId)
);
return {
messages: messages.map(message => message ? message.__state : undefined),
};
}, {
compareDepth: {
messages: 1,
},
});
// to manually trigger the dialog close event
this._dialogRef = useRef('dialog');
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {mail.message[]}
*/
get messages() {
return this.props.messageLocalIds.map(localId => this.env.models['mail.message'].get(localId));
}
/**
* @returns {string}
*/
get CONFIRMATION() {
return this.env._t("Confirmation");
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onClickBan() {
this._dialogRef.comp._close();
this.env.models['mail.message'].moderate(this.messages, 'ban');
}
/**
* @private
*/
_onClickCancel() {
this._dialogRef.comp._close();
}
}
Object.assign(ModerationBanDialog, {
components,
props: {
messageLocalIds: {
type: Array,
element: String,
},
},
template: 'mail.ModerationBanDialog',
});
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ModerationBanDialog" owl="1">
<Dialog contentClass="'o_ModerationBanDialog'" title="CONFIRMATION" size="'medium'" t-ref="dialog">
<t t-if="messages.length === 1">
<p>You are going to ban the following user:</p>
</t>
<t t-else="">
<p>You are going to ban the following users:</p>
</t>
<ul class="my-5">
<t t-foreach="messages" t-as="message" t-key="message.localId">
<li t-esc="message.email_from"/>
</t>
</ul>
<p>Do you confirm the action?</p>
<t t-set-slot="buttons">
<button class="o-ban btn btn-primary" t-on-click="_onClickBan">Ban</button>
<button class="o-cancel btn btn-secondary" t-on-click="_onClickCancel">Cancel</button>
</t>
</Dialog>
</t>
</templates>
@@ -1,104 +0,0 @@
/** @odoo-module **/
import { useShouldUpdateBasedOnProps } from '@mail/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props';
import { useStore } from '@mail/component_hooks/use_store/use_store';
import Dialog from 'web.OwlDialog';
const { Component } = owl;
const { useRef } = owl.hooks;
const components = { Dialog };
export class ModerationDiscardDialog extends Component {
/**
* @override
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
messageLocalIds: 1,
},
});
useStore(props => {
const messages = props.messageLocalIds.map(localId =>
this.env.models['mail.message'].get(localId)
);
return {
messages: messages.map(message => message ? message.__state : undefined),
};
}, {
compareDepth: {
messages: 1,
},
});
// to manually trigger the dialog close event
this._dialogRef = useRef('dialog');
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {string}
*/
getBody() {
if (this.messages.length === 1) {
return this.env._t("You are going to discard 1 message.");
}
return _.str.sprintf(
this.env._t("You are going to discard %s messages."),
this.messages.length
);
}
/**
* @returns {mail.message[]}
*/
get messages() {
return this.props.messageLocalIds.map(localId =>
this.env.models['mail.message'].get(localId)
);
}
/**
* @returns {string}
*/
getTitle() {
return this.env._t("Confirmation");
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onClickCancel() {
this._dialogRef.comp._close();
}
/**
* @private
*/
_onClickDiscard() {
this._dialogRef.comp._close();
this.env.models['mail.message'].moderate(this.messages, 'discard');
}
}
Object.assign(ModerationDiscardDialog, {
components,
props: {
messageLocalIds: {
type: Array,
element: String,
},
},
template: 'mail.ModerationDiscardDialog',
});
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ModerationDiscardDialog" owl="1">
<Dialog contentClass="'o_ModerationDiscardDialog'" title="getTitle()" size="'medium'" t-ref="dialog">
<p t-esc="getBody()"/>
<p>Do you confirm the action?</p>
<t t-set-slot="buttons">
<button class="o-discard btn btn-primary" t-on-click="_onClickDiscard">Discard</button>
<button class="o-cancel btn btn-secondary" t-on-click="_onClickCancel">Cancel</button>
</t>
</Dialog>
</t>
</templates>
@@ -1,99 +0,0 @@
/** @odoo-module **/
import { useShouldUpdateBasedOnProps } from '@mail/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props';
import { useStore } from '@mail/component_hooks/use_store/use_store';
import Dialog from 'web.OwlDialog';
const { Component, useState } = owl;
const { useRef } = owl.hooks;
const components = { Dialog };
export class ModerationRejectDialog extends Component {
/**
* @override
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
messageLocalIds: 1,
},
});
this.state = useState({
title: this.env._t("Message Rejected"),
comment: this.env._t("Your message was rejected by moderator."),
});
useStore(props => {
const messages = props.messageLocalIds.map(localId =>
this.env.models['mail.message'].get(localId)
);
return {
messages: messages.map(message => message ? message.__state : undefined),
};
}, {
compareDepth: {
messages: 1,
},
});
// to manually trigger the dialog close event
this._dialogRef = useRef('dialog');
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {mail.message[]}
*/
get messages() {
return this.props.messageLocalIds.map(localId =>
this.env.models['mail.message'].get(localId)
);
}
/**
* @returns {string}
*/
get SEND_EXPLANATION_TO_AUTHOR() {
return this.env._t("Send explanation to author");
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onClickCancel() {
this._dialogRef.comp._close();
}
/**
* @private
*/
_onClickReject() {
this._dialogRef.comp._close();
const kwargs = {
title: this.state.title,
comment: this.state.comment,
};
this.env.models['mail.message'].moderate(this.messages, 'reject', kwargs);
}
}
Object.assign(ModerationRejectDialog, {
components,
props: {
messageLocalIds: {
type: Array,
element: String,
},
},
template: 'mail.ModerationRejectDialog',
});
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ModerationRejectDialog" owl="1">
<Dialog contentClass="'o_ModerationRejectDialog'" title="SEND_EXPLANATION_TO_AUTHOR" size="'medium'" t-ref="dialog">
<input class="o_ModerationRejectDialog_title form-control" type="text" placeholder="Subject" autofocus="autofocus" t-model="state.title"/>
<textarea class="o_ModerationRejectDialog_comment form-control mt16" placeholder="Mail Body" t-model="state.comment"/>
<t t-set-slot="buttons">
<button class="o-reject btn btn-primary" t-on-click="_onClickReject">Reject</button>
<button class="o-cancel btn btn-secondary" t-on-click="_onClickCancel">Cancel</button>
</t>
</Dialog>
</t>
</templates>
@@ -24,7 +24,6 @@ export class ThreadIcon extends Component {
correspondentImStatus: correspondent && correspondent.im_status,
history: this.env.messaging.history,
inbox: this.env.messaging.inbox,
moderation: this.env.messaging.moderation,
partnerRoot: this.env.messaging.partnerRoot,
starred: this.env.messaging.starred,
thread,
@@ -47,9 +47,6 @@
<t t-elif="thread === env.messaging.history">
<div class="o_ThreadIcon_mailboxHistory fa fa-history"/>
</t>
<t t-elif="thread === env.messaging.moderation">
<div class="o_ThreadIcon_mailboxModeration fa fa-envelope"/>
</t>
</t>
</t>
</div>
@@ -135,7 +135,6 @@ export class ThreadView extends Component {
threadCacheIsLoaded: threadCache && threadCache.isLoaded,
threadCacheHasLoadingFailed: threadCache && threadCache.hasLoadingFailed,
threadIsTemporary: thread && thread.isTemporary,
threadMassMailing: thread && thread.mass_mailing,
threadModel: thread && thread.model,
threadTextInputSendShortcuts: thread && thread.textInputSendShortcuts || [],
threadView,
@@ -167,7 +166,6 @@ Object.assign(ThreadView, {
defaultProps: {
composerAttachmentsDetailsMode: 'auto',
hasComposer: false,
hasMessageCheckbox: false,
hasSquashCloseMessages: false,
haveMessagesMarkAsReadIcon: false,
haveMessagesReplyIcon: false,
@@ -207,7 +205,6 @@ Object.assign(ThreadView, {
type: Boolean,
optional: true,
},
hasMessageCheckbox: Boolean,
hasScrollAdjust: {
type: Boolean,
optional: true,
@@ -13,7 +13,6 @@
<MessageList
class="o_ThreadView_messageList"
getScrollableElement= "props.getScrollableElement"
hasMessageCheckbox="props.hasMessageCheckbox"
hasScrollAdjust="props.hasScrollAdjust"
hasSquashCloseMessages="props.hasSquashCloseMessages"
haveMessagesMarkAsReadIcon="props.haveMessagesMarkAsReadIcon"
@@ -406,18 +406,6 @@ function factory(dependencies) {
deviceIsMobile: attr({
related: 'device.isMobile',
}),
/**
* Determine if the moderation discard dialog is displayed.
*/
hasModerationDiscardDialog: attr({
default: false,
}),
/**
* Determine if the moderation reject dialog is displayed.
*/
hasModerationRejectDialog: attr({
default: false,
}),
/**
* Determines whether `this.thread` should be displayed.
*/
@@ -16,16 +16,6 @@ function factory(dependencies) {
// Public
//----------------------------------------------------------------------
/**
* @static
* @param {mail.thread} thread
* @param {string} threadStringifiedDomain
*/
static checkAll(thread, threadStringifiedDomain) {
const threadCache = thread.cache(threadStringifiedDomain);
threadCache.update({ checkedMessages: link(threadCache.messages) });
}
/**
* @static
* @param {Object} data
@@ -98,9 +88,6 @@ function factory(dependencies) {
}
data2.originThread = insert(originThreadData);
}
if ('moderation_status' in data) {
data2.moderation_status = data.moderation_status;
}
if ('needaction_partner_ids' in data) {
data2.isNeedaction = data.needaction_partner_ids.includes(this.env.messaging.currentPartner.id);
}
@@ -165,38 +152,16 @@ function factory(dependencies) {
});
}
/**
* Applies the moderation `decision` on the provided messages.
*
* @static
* @param {mail.message[]} messages
* @param {string} decision: 'accept', 'allow', ban', 'discard', or 'reject'
* @param {Object|undefined} [kwargs] optional data to pass on
* message moderation. This is provided when rejecting the messages
* for which title and comment give reason(s) for reject.
* @param {string} [kwargs.title]
* @param {string} [kwargs.comment]
*/
static async moderate(messages, decision, kwargs) {
const messageIds = messages.map(message => message.id);
await this.env.services.rpc({
model: 'mail.message',
method: 'moderate',
args: [messageIds, decision],
kwargs: kwargs,
});
}
/**
* Performs the `message_fetch` RPC on `mail.message`.
*
* @static
* @param {Array[]} domain
* @param {integer} [limit]
* @param {integer[]} [moderated_channel_ids]
* @param {Object} [context]
* @returns {mail.message[]}
*/
static async performRpcMessageFetch(domain, limit, moderated_channel_ids, context) {
static async performRpcMessageFetch(domain, limit, context) {
const messagesData = await this.env.services.rpc({
model: 'mail.message',
method: 'message_fetch',
@@ -204,7 +169,6 @@ function factory(dependencies) {
context,
domain,
limit,
moderated_channel_ids,
},
}, { shadow: true });
const messages = this.env.models['mail.message'].insert(messagesData.map(
@@ -227,16 +191,6 @@ function factory(dependencies) {
return messages;
}
/**
* @static
* @param {mail.thread} thread
* @param {string} threadStringifiedDomain
*/
static uncheckAll(thread, threadStringifiedDomain) {
const threadCache = thread.cache(threadStringifiedDomain);
threadCache.update({ checkedMessages: unlink(threadCache.messages) });
}
/**
* Unstar all starred messages of current user.
*/
@@ -247,22 +201,6 @@ function factory(dependencies) {
});
}
/**
* @param {mail.thread} thread
* @param {string} threadStringifiedDomain
* @returns {boolean}
*/
isChecked(thread, threadStringifiedDomain) {
// aku todo
const relatedCheckedThreadCache = this.checkedThreadCaches.find(
threadCache => (
threadCache.thread === thread &&
threadCache.stringifiedDomain === threadStringifiedDomain
)
);
return !!relatedCheckedThreadCache;
}
/**
* Mark this message as read, so that it no longer appears in current
* partner Inbox.
@@ -275,20 +213,6 @@ function factory(dependencies) {
}));
}
/**
* Applies the moderation `decision` on this message.
*
* @param {string} decision: 'accept', 'allow', ban', 'discard', or 'reject'
* @param {Object|undefined} [kwargs] optional data to pass on
* message moderation. This is provided when rejecting the messages
* for which title and comment give reason(s) for reject.
* @param {string} [kwargs.title]
* @param {string} [kwargs.comment]
*/
async moderate(decision, kwargs) {
await this.async(() => this.constructor.moderate([this], decision, kwargs));
}
/**
* Opens the view that allows to resend the message in case of failure.
*/
@@ -318,22 +242,6 @@ function factory(dependencies) {
this.env.messaging.discuss.replyToMessage(this);
}
/**
* Toggle check state of this message in the context of the provided
* thread and its stringifiedDomain.
*
* @param {mail.thread} thread
* @param {string} threadStringifiedDomain
*/
toggleCheck(thread, threadStringifiedDomain) {
const threadCache = thread.cache(threadStringifiedDomain);
if (threadCache.checkedMessages.includes(this)) {
threadCache.update({ checkedMessages: unlink(this) });
} else {
threadCache.update({ checkedMessages: link(this) });
}
}
/**
* Toggle the starred status of the provided message.
*/
@@ -375,14 +283,6 @@ function factory(dependencies) {
));
}
/**
* @private
* @returns {boolean}
*/
_computeHasCheckbox() {
return this.isModeratedByCurrentPartner;
}
/**
* @private
* @returns {boolean}
@@ -454,17 +354,6 @@ function factory(dependencies) {
);
}
/**
* @private
* @returns {boolean}
*/
_computeIsModeratedByCurrentPartner() {
return (
this.moderation_status === 'pending_moderation' &&
this.originThread &&
this.originThread.isModeratedByCurrentPartner
);
}
/**
* @private
* @returns {boolean}
@@ -555,9 +444,6 @@ function factory(dependencies) {
if (this.isStarred) {
threads.push(this.env.messaging.starred);
}
if (this.env.messaging.moderation && this.isModeratedByCurrentPartner) {
threads.push(this.env.messaging.moderation);
}
if (this.originThread) {
threads.push(this.originThread);
}
@@ -582,10 +468,6 @@ function factory(dependencies) {
body: attr({
default: "",
}),
checkedThreadCaches: many2many('mail.thread_cache', {
inverse: 'checkedMessages',
readonly: true,
}),
/**
* Determines the date of the message as a moment object.
*/
@@ -604,11 +486,6 @@ function factory(dependencies) {
compute: '_computeFailureNotifications',
dependencies: ['notificationsStatus'],
}),
hasCheckbox: attr({
compute: '_computeHasCheckbox',
default: false,
dependencies: ['isModeratedByCurrentPartner'],
}),
id: attr({
required: true,
}),
@@ -662,15 +539,6 @@ function factory(dependencies) {
'tracking_value_ids',
],
}),
isModeratedByCurrentPartner: attr({
compute: '_computeIsModeratedByCurrentPartner',
default: false,
dependencies: [
'moderation_status',
'originThread',
'originThreadIsModeratedByCurrentPartner',
],
}),
/**
* States whether `originThread.name` and `subject` contain similar
* values except it contains the extra prefix at the start
@@ -753,13 +621,9 @@ function factory(dependencies) {
messagingInbox: many2one('mail.thread', {
related: 'messaging.inbox',
}),
messagingModeration: many2one('mail.thread', {
related: 'messaging.moderation',
}),
messagingStarred: many2one('mail.thread', {
related: 'messaging.starred',
}),
moderation_status: attr(),
notifications: one2many('mail.notification', {
inverse: 'message',
isCausal: true,
@@ -774,10 +638,6 @@ function factory(dependencies) {
originThread: many2one('mail.thread', {
inverse: 'messagesAsOriginThread',
}),
originThreadIsModeratedByCurrentPartner: attr({
default: false,
related: 'originThread.isModeratedByCurrentPartner',
}),
/**
* Serves as compute dependency.
*/
@@ -810,12 +670,10 @@ function factory(dependencies) {
compute: '_computeThreads',
dependencies: [
'isHistory',
'isModeratedByCurrentPartner',
'isNeedaction',
'isStarred',
'messagingHistory',
'messagingInbox',
'messagingModeration',
'messagingStarred',
'originThread',
],
@@ -286,10 +286,6 @@ function factory(dependencies) {
isCausal: true,
readonly: true,
}),
/**
* Mailbox Moderation.
*/
moderation: one2one('mail.thread'),
notificationGroupManager: one2one('mail.notification_group_manager', {
default: create(),
isCausal: true,
@@ -32,11 +32,6 @@ function factory(dependencies) {
model: 'mail.box',
name: this.env._t("Inbox"),
}),
moderation: create({
id: 'moderation',
model: 'mail.box',
name: this.env._t("Moderation"),
}),
starred: create({
id: 'starred',
isServerPinned: true,
@@ -76,8 +71,6 @@ function factory(dependencies) {
* @param {integer} param0.current_user_id
* @param {Object} [param0.mail_failures={}]
* @param {Object[]} [param0.mention_partner_suggestions=[]]
* @param {Object[]} [param0.moderation_channel_ids=[]]
* @param {integer} [param0.moderation_counter=0]
* @param {integer} [param0.needaction_inbox_counter=0]
* @param {Object} param0.partner_root
* @param {Object[]} param0.public_partners
@@ -92,8 +85,6 @@ function factory(dependencies) {
mail_failures = {},
mention_partner_suggestions = [],
menu_id,
moderation_channel_ids = [],
moderation_counter = 0,
needaction_inbox_counter = 0,
partner_root,
public_partners,
@@ -105,15 +96,12 @@ function factory(dependencies) {
this._initPartners({
current_partner,
current_user_id,
moderation_channel_ids,
partner_root,
public_partners,
});
// mailboxes after partners and before other initializers that might
// manipulate threads or messages
this._initMailboxes({
moderation_channel_ids,
moderation_counter,
needaction_inbox_counter,
starred_counter,
});
@@ -186,25 +174,15 @@ function factory(dependencies) {
/**
* @private
* @param {Object} param0
* @param {Object[]} [param0.moderation_channel_ids=[]]
* @param {integer} param0.moderation_counter
* @param {integer} param0.needaction_inbox_counter
* @param {integer} param0.starred_counter
*/
_initMailboxes({
moderation_channel_ids,
moderation_counter,
needaction_inbox_counter,
starred_counter,
}) {
this.env.messaging.inbox.update({ counter: needaction_inbox_counter });
this.env.messaging.starred.update({ counter: starred_counter });
if (moderation_channel_ids.length > 0) {
this.messaging.moderation.update({
counter: moderation_counter,
isServerPinned: true,
});
}
}
/**
@@ -239,14 +217,12 @@ function factory(dependencies) {
* @private
* @param {Object} current_partner
* @param {integer} current_user_id
* @param {integer[]} moderation_channel_ids
* @param {Object} partner_root
* @param {Object[]} [public_partners=[]]
*/
_initPartners({
current_partner,
current_user_id: currentUserId,
moderation_channel_ids = [],
partner_root,
public_partners = [],
}) {
@@ -254,12 +230,6 @@ function factory(dependencies) {
currentPartner: insert(Object.assign(
this.env.models['mail.partner'].convertData(current_partner),
{
moderatedChannels: insert(moderation_channel_ids.map(id => {
return {
id,
model: 'mail.channel',
};
})),
user: insert({ id: currentUserId }),
}
)),
@@ -182,13 +182,6 @@ function factory(dependencies) {
const wasChannelExisting = !!channel;
const convertedData = this.env.models['mail.message'].convertData(messageData);
const oldMessage = this.env.models['mail.message'].findFromIdentifyingData(convertedData);
// locally save old values, as insert would overwrite them
const oldMessageModerationStatus = (
oldMessage && oldMessage.moderation_status
);
const oldMessageWasModeratedByCurrentPartner = (
oldMessage && oldMessage.isModeratedByCurrentPartner
);
// Fetch missing info from channel before going further. Inserting
// a channel with incomplete info can lead to issues. This is in
@@ -207,21 +200,6 @@ function factory(dependencies) {
const message = this.env.models['mail.message'].insert(convertedData);
this._notifyThreadViewsMessageReceived(message);
// If the message was already known: nothing else should be done,
// except if it was pending moderation by the current partner, then
// decrement the moderation counter.
if (oldMessage) {
if (
oldMessageModerationStatus === 'pending_moderation' &&
message.moderation_status !== 'pending_moderation' &&
oldMessageWasModeratedByCurrentPartner
) {
const moderation = this.env.messaging.moderation;
moderation.update({ counter: decrement() });
}
return;
}
// If the current partner is author, do nothing else.
if (message.author === this.env.messaging.currentPartner) {
return;
@@ -407,8 +385,6 @@ function factory(dependencies) {
return this._handleNotificationPartnerMessageNotificationUpdate(data.elements);
} else if (type === 'mark_as_read') {
return this._handleNotificationPartnerMarkAsRead(data);
} else if (type === 'moderator') {
return this._handleNotificationPartnerModerator(data);
} else if (type === 'simple_notification') {
this.env.services['notification'].notify({
message: data.message,
@@ -492,16 +468,9 @@ function factory(dependencies) {
* @param {integer[]} param0.messag_ids
*/
_handleNotificationPartnerDeletion({ message_ids }) {
const moderationMailbox = this.env.messaging.moderation;
for (const id of message_ids) {
const message = this.env.models['mail.message'].findFromIdentifyingData({ id });
if (message) {
if (
message.moderation_status === 'pending_moderation' &&
message.originThread.isModeratedByCurrentPartner
) {
moderationMailbox.update({ counter: decrement() });
}
message.delete();
}
}
@@ -570,21 +539,6 @@ function factory(dependencies) {
}
}
/**
* @private
* @param {Object} param0
* @param {Object} param0.message
*/
_handleNotificationPartnerModerator({ message: data }) {
this.env.models['mail.message'].insert(
this.env.models['mail.message'].convertData(data)
);
const moderationMailbox = this.env.messaging.moderation;
if (moderationMailbox) {
moderationMailbox.update({ counter: increment() });
}
}
/**
* @private
* @param {Object} param0
@@ -500,12 +500,6 @@ function factory(dependencies) {
model: attr({
default: 'res.partner',
}),
/**
* Channels that are moderated by this partner.
*/
moderatedChannels: many2many('mail.thread', {
inverse: 'moderators',
}),
name: attr(),
nameOrDisplayName: attr({
compute: '_computeNameOrDisplayName',
@@ -162,9 +162,6 @@ function factory(dependencies) {
if ('is_minimized' in data && 'state' in data) {
data2.serverFoldState = data.is_minimized ? data.state : 'closed';
}
if ('is_moderator' in data) {
data2.is_moderator = data.is_moderator;
}
if ('is_pinned' in data) {
data2.isServerPinned = data.is_pinned;
}
@@ -184,12 +181,6 @@ function factory(dependencies) {
});
data2.serverLastMessage = insert(messageData);
}
if ('mass_mailing' in data) {
data2.mass_mailing = data.mass_mailing;
}
if ('moderation' in data) {
data2.moderation = data.moderation;
}
if ('message_needaction_counter' in data) {
data2.message_needaction_counter = data.message_needaction_counter;
}
@@ -1201,20 +1192,6 @@ function factory(dependencies) {
);
}
/**
* @private
* @returns {boolean}
*/
_computeIsModeratedByCurrentPartner() {
if (!this.messaging) {
return false;
}
if (!this.messaging.currentPartner) {
return false;
}
return this.moderators.includes(this.env.messaging.currentPartner);
}
/**
* @private
* @returns {boolean}
@@ -1854,13 +1831,6 @@ function factory(dependencies) {
isLoadingAttachments: attr({
default: false,
}),
isModeratedByCurrentPartner: attr({
compute: '_computeIsModeratedByCurrentPartner',
dependencies: [
'messagingCurrentPartner',
'moderators',
],
}),
/**
* Determine if there is a pending pin state change, which is a change
* of pin state requested by the client but not yet confirmed by the
@@ -1896,9 +1866,6 @@ function factory(dependencies) {
isTemporary: attr({
default: false,
}),
is_moderator: attr({
default: false,
}),
lastCurrentPartnerMessageSeenByEveryone: many2one('mail.message', {
compute: '_computeLastCurrentPartnerMessageSeenByEveryone',
dependencies: [
@@ -2032,15 +1999,6 @@ function factory(dependencies) {
required: true,
}),
model_name: attr(),
moderation: attr({
default: false,
}),
/**
* Partners that are moderating this thread (only applies to channels).
*/
moderators: many2many('mail.partner', {
inverse: 'moderatedChannels',
}),
moduleIcon: attr(),
name: attr(),
/**
@@ -85,16 +85,6 @@ function factory(dependencies) {
return `${this.modelName}_[${thread.localId}]_<${stringifiedDomain}>`;
}
/**
* @private
*/
_computeCheckedMessages() {
const messagesWithoutCheckbox = this.checkedMessages.filter(
message => !message.hasCheckbox
);
return unlink(messagesWithoutCheckbox);
}
/**
* @private
* @returns {mail.message[]}
@@ -238,16 +228,6 @@ function factory(dependencies) {
return res;
}
/**
* @private
* @returns {mail.message[]}
*/
_computeUncheckedMessages() {
return replace(this.messages.filter(
message => message.hasCheckbox && !this.checkedMessages.includes(message)
));
}
/**
* @private
* @param {Array} domain
@@ -263,8 +243,6 @@ function factory(dependencies) {
]);
} else if (thread === this.env.messaging.history) {
return domain.concat([['needaction', '=', false]]);
} else if (thread === this.env.messaging.moderation) {
return domain.concat([['moderation_status', '=', 'pending_moderation']]);
} else {
// Avoid to load user_notification as these messages are not
// meant to be shown on chatters.
@@ -293,16 +271,12 @@ function factory(dependencies) {
domain = extraDomain.concat(domain);
}
const context = this.env.session.user_context;
const moderated_channel_ids = this.thread.moderation
? [this.thread.id]
: undefined;
let messages;
try {
messages = await this.async(() =>
this.env.models['mail.message'].performRpcMessageFetch(
domain,
limit,
moderated_channel_ids,
context,
)
);
@@ -426,14 +400,6 @@ function factory(dependencies) {
}
ThreadCache.fields = {
checkedMessages: many2many('mail.message', {
compute: '_computeCheckedMessages',
dependencies: [
'checkedMessages',
'messagesCheckboxes',
],
inverse: 'checkedThreadCaches',
}),
/**
* List of messages that have been fetched by this cache.
*
@@ -513,9 +479,6 @@ function factory(dependencies) {
compute: '_computeLastMessage',
dependencies: ['orderedMessages'],
}),
messagesCheckboxes: attr({
related: 'messages.hasCheckbox',
}),
/**
* List of messages linked to this cache.
*/
@@ -654,14 +617,6 @@ function factory(dependencies) {
threadViews: one2many('mail.thread_view', {
inverse: 'threadCache',
}),
uncheckedMessages: many2many('mail.message', {
compute: '_computeUncheckedMessages',
dependencies: [
'checkedMessages',
'messagesCheckboxes',
'messages',
],
}),
};
ThreadCache.modelName = 'mail.thread_cache';
@@ -198,9 +198,6 @@ function factory(dependencies) {
}
ThreadView.fields = {
checkedMessages: many2many('mail.message', {
related: 'threadCache.checkedMessages',
}),
/**
* List of component hints. Hints contain information that help
* components make UI/UX decisions based on their UI state.
@@ -431,9 +428,6 @@ function factory(dependencies) {
inverse: 'threadView',
readonly: true,
}),
uncheckedMessages: many2many('mail.message', {
related: 'threadCache.uncheckedMessages',
}),
};
ThreadView.modelName = 'mail.thread_view';
-3
View File
@@ -12,6 +12,3 @@ $o-mail-discuss-sidebar-active-indicator-width: 3px !default;
$o-mail-discuss-sidebar-scrollbar-width: 15px !default;
$o-mail-message-sidebar-width: 50px;
$o-mail-moderation-accept-color: theme-color('success') !default;
$o-mail-moderation-reject-color: theme-color('danger') !default;
@@ -39,11 +39,6 @@ const DiscussWidget = AbstractAction.extend({
this.$buttons.on('click', '.o_mobile_new_channel', ev => this._onClickMobileNewChannel(ev));
this.$buttons.on('click', '.o_mobile_new_message', ev => this._onClickMobileNewMessage(ev));
this.$buttons.on('click', '.o_unstar_all', ev => this._onClickUnstarAll(ev));
this.$buttons.on('click', '.o_widget_Discuss_controlPanelButtonSelectAll', ev => this._onClickSelectAll(ev));
this.$buttons.on('click', '.o_widget_Discuss_controlPanelButtonUnselectAll', ev => this._onClickUnselectAll(ev));
this.$buttons.on('click', '.o_widget_Discuss_controlPanelButtonModeration.o-accept', ev => this._onClickModerationAccept(ev));
this.$buttons.on('click', '.o_widget_Discuss_controlPanelButtonModeration.o-discard', ev => this._onClickModerationDiscard(ev));
this.$buttons.on('click', '.o_widget_Discuss_controlPanelButtonModeration.o-reject', ev => this._onClickModerationReject(ev));
// control panel attributes
this.action = action;
@@ -246,41 +241,6 @@ const DiscussWidget = AbstractAction.extend({
.find('.o_mobile_new_message')
.addClass('o_hidden');
}
// Select All & Unselect All
const $selectAll = this.$buttons.find('.o_widget_Discuss_controlPanelButtonSelectAll');
const $unselectAll = this.$buttons.find('.o_widget_Discuss_controlPanelButtonUnselectAll');
if (
this.discuss.threadView &&
(
this.discuss.threadView.checkedMessages.length > 0 ||
this.discuss.threadView.uncheckedMessages.length > 0
)
) {
$selectAll.removeClass('o_hidden');
$selectAll.toggleClass('disabled', this.discuss.threadView.uncheckedMessages.length === 0);
$unselectAll.removeClass('o_hidden');
$unselectAll.toggleClass('disabled', this.discuss.threadView.checkedMessages.length === 0);
} else {
$selectAll.addClass('o_hidden');
$selectAll.addClass('disabled');
$unselectAll.addClass('o_hidden');
$unselectAll.addClass('disabled');
}
// Moderation Actions
const $moderationButtons = this.$buttons.find('.o_widget_Discuss_controlPanelButtonModeration');
if (
this.discuss.threadView &&
this.discuss.threadView.checkedMessages.length > 0 &&
this.discuss.threadView.checkedMessages.filter(
message => !message.isModeratedByCurrentPartner
).length === 0
) {
$moderationButtons.removeClass('o_hidden');
} else {
$moderationButtons.addClass('o_hidden');
}
let title;
if (this.env.messaging.device.isMobile || !this.discuss.thread) {
@@ -333,48 +293,6 @@ const DiscussWidget = AbstractAction.extend({
_onClickMobileNewMessage() {
this.discuss.update({ isAddingChat: true });
},
/**
* @private
*/
_onClickModerationAccept() {
this.env.models['mail.message'].moderate(
this.discuss.threadView.checkedMessages,
'accept'
);
},
/**
* @private
*/
_onClickModerationDiscard() {
this.discuss.update({ hasModerationDiscardDialog: true });
},
/**
* @private
*/
_onClickModerationReject() {
this.discuss.update({ hasModerationRejectDialog: true });
},
/**
* @private
*/
_onClickSelectAll() {
this.env.models['mail.message'].checkAll(
this.discuss.thread,
this.discuss.stringifiedDomain
);
},
/**
* @private
*/
_onClickUnselectAll() {
this.env.models['mail.message'].uncheckAll(
this.discuss.thread,
this.discuss.stringifiedDomain
);
},
/**
* @private
*/
_onClickUnstarAll() {
this.env.models['mail.message'].unstarAll();
},
@@ -16,11 +16,6 @@
<button class="o_widget_Discuss_controlPanelButtonUnstarAll o_unstar_all o_hidden btn btn-secondary" type="button" title="Unstar all messages">Unstar all</button>
<button class="o_widget_Discuss_controlPanelButtonMobileNewMessage o_mobile_new_message o_hidden btn btn-secondary" type="button" title="New Message">New Message</button>
<button class="o_widget_Discuss_controlPanelButtonMobileNewChannel o_mobile_new_channel o_hidden btn btn-secondary" title="New Channel" type="button">New Channel</button>
<button class="o_widget_Discuss_controlPanelButtonSelectAll btn btn-secondary o_hidden" title="Select all messages">Select All</button>
<button class="o_widget_Discuss_controlPanelButtonUnselectAll btn btn-secondary o_hidden" title="Unselect all messages">Unselect All</button>
<button class="o_widget_Discuss_controlPanelButtonModeration btn btn-secondary o_hidden o-accept" title="Accept selected messages">Accept</button>
<button class="o_widget_Discuss_controlPanelButtonModeration btn btn-secondary o_hidden o-reject" title="Reject selected messages">Reject</button>
<button class="o_widget_Discuss_controlPanelButtonModeration btn btn-secondary o_hidden o-discard" title="Discard selected messages">Discard</button>
</div>
</t>
@@ -76,15 +76,12 @@ export class MockModels {
id: { string: "Id", type: 'integer' },
// In python this belongs to mail.channel.partner. Here for simplicity.
is_minimized: { string: "isMinimized", type: "boolean", default: false },
// In python it is moderator_ids. Here for simplicity.
is_moderator: { string: "Is current partner moderator?", type: "boolean", default: false },
// In python this belongs to mail.channel.partner. Here for simplicity.
is_pinned: { string: "isPinned", type: "boolean", default: true },
// In python: email_send.
mass_mailing: { string: "Send messages by email", type: "boolean", default: false },
members: { string: "Members", type: 'many2many', relation: 'res.partner', default() { return [this.currentPartnerId]; } },
message_unread_counter: { string: "# unread messages", type: 'integer' },
moderation: { string: "Moderation", type: 'boolean', default: false },
name: { string: "Name", type: "char", required: true },
public: { string: "Public", type: "boolean", default: 'groups' },
seen_message_id: { string: "Last Seen", type: 'many2one', relation: 'mail.message' },
@@ -135,7 +132,6 @@ export class MockModels {
model: { string: "Related Document model", type: 'char' },
needaction: { string: "Need Action", type: 'boolean' },
needaction_partner_ids: { string: "Partners with Need Action", type: 'many2many', relation: 'res.partner' },
moderation_status: { string: "Moderation status", type: 'selection', selection: [['pending_moderation', "Pending Moderation"], ['accepted', "Accepted"], ['rejected', "Rejected"]], default: false },
notification_ids: { string: "Notifications", type: 'one2many', relation: 'mail.notification' },
partner_ids: { string: "Recipients", type: 'many2many', relation: 'res.partner' },
record_name: { string: "Name", type: 'char' },
@@ -211,16 +211,12 @@ MockServer.include({
await nextAnimationFrame();
const domain = args.args[0] || args.kwargs.domain;
const limit = args.args[1] || args.kwargs.limit;
const moderated_channel_ids = args.args[2] || args.kwargs.moderated_channel_ids;
return this._mockMailMessageMessageFetch(domain, limit, moderated_channel_ids);
return this._mockMailMessageMessageFetch(domain, limit);
}
if (args.model === 'mail.message' && args.method === 'message_format') {
const ids = args.args[0];
return this._mockMailMessageMessageFormat(ids);
}
if (args.model === 'mail.message' && args.method === 'moderate') {
return this._mockMailMessageModerate(args);
}
if (args.model === 'mail.message' && args.method === 'set_message_done') {
const ids = args.args[0];
return this._mockMailMessageSetMessageDone(ids);
@@ -363,13 +359,6 @@ MockServer.include({
]);
const privateGroupInfos = this._mockMailChannelChannelInfo(privateGroups.map(channel => channel.id));
const moderation_channel_ids = this._getRecords('mail.channel', [['is_moderator', '=', true]]).map(channel => channel.id);
const moderation_counter = this._getRecords('mail.message', [
['model', '=', 'mail.channel'],
['res_id', 'in', moderation_channel_ids],
['moderation_status', '=', 'pending_moderation'],
]).length;
const partnerRoot = this._getRecords(
'res.partner',
[['id', '=', this.partnerRootId]],
@@ -411,8 +400,6 @@ MockServer.include({
mail_failures: mailFailures,
mention_partner_suggestions: [],
menu_id: false,
moderation_channel_ids,
moderation_counter,
needaction_inbox_counter,
partner_root: partnerRootFormat,
public_partners: [publicPartnerFormat],
@@ -789,7 +776,6 @@ MockServer.include({
{ body, message_type, subtype_xmlid },
);
}
// moderation_guidelines not handled here for simplicity
const channelInfo = this._mockMailChannelChannelInfo([channel.id], 'join')[0];
const notification = [[false, 'res.partner', this.currentPartnerId], channelInfo];
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
@@ -951,10 +937,6 @@ MockServer.include({
/**
* Simulates `message_post` on `mail.channel`.
*
* For simplicity this mock handles a simple case in regard to moderation:
* - messages from JS are assumed to be always sent by the current partner,
* - moderation white list and black list are not checked.
*
* @private
* @param {integer} id
* @param {Object} kwargs
@@ -971,18 +953,11 @@ MockServer.include({
{ is_pinned: true },
]);
}
let moderation_status = 'accepted';
if (channel.moderation && ['email', 'comment'].includes(message_type)) {
if (!channel.is_moderator) {
moderation_status = 'pending_moderation';
}
}
const messageId = this._mockMailThreadMessagePost(
'mail.channel',
[id],
Object.assign(kwargs, {
message_type,
moderation_status,
}),
context,
);
@@ -1120,21 +1095,10 @@ MockServer.include({
* @private
* @param {Array[]} domain
* @param {string} [limit=20]
* @param {Object} [moderated_channel_ids]
* @returns {Object[]}
*/
_mockMailMessageMessageFetch(domain, limit = 20, moderated_channel_ids) {
_mockMailMessageMessageFetch(domain, limit = 20) {
let messages = this._getRecords('mail.message', domain);
if (moderated_channel_ids) {
const mod_messages = this._getRecords('mail.message', [
['model', '=', 'mail.channel'],
['res_id', 'in', moderated_channel_ids],
'|',
['author_id', '=', this.currentPartnerId],
['moderation_status', '=', 'pending_moderation'],
]);
messages = [...new Set([...messages, ...mod_messages])];
}
// sorted from highest ID to lowest ID (i.e. from youngest to oldest)
messages.sort(function (m1, m2) {
return m1.id < m2.id ? 1 : -1;
@@ -1245,40 +1209,6 @@ MockServer.include({
return response;
});
},
/**
* Simulates `moderate` on `mail.message`.
*
* @private
*/
_mockMailMessageModerate(args) {
const messageIDs = args.args[0];
const decision = args.args[1];
const model = this.data['mail.message'];
if (decision === 'reject' || decision === 'discard') {
model.records = _.reject(model.records, function (rec) {
return _.contains(messageIDs, rec.id);
});
// simulate notification back (deletion of rejected/discarded
// message in channel)
const dbName = undefined; // useless for tests
const notifData = {
message_ids: messageIDs,
type: "deletion",
};
const metaData = [dbName, 'res.partner', this.currentPartnerId];
const notification = [metaData, notifData];
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
} else if (decision === 'accept') {
// simulate notification back (new accepted message in channel)
const messages = this._getRecords('mail.message', [['id', 'in', messageIDs]]);
for (const message of messages) {
this._mockWrite('mail.message', [[message.id], {
moderation_status: 'accepted',
}]);
this._mockMailThread_NotifyThread(model, [message.res_id], message.id);
}
}
},
/**
* Simulates `_message_notification_format` on `mail.message`.
*
-270
View File
@@ -502,273 +502,3 @@ class TestChannelInternals(MailCommon):
self.assertFalse(notification_seen_user_2, 'Should not have sent a notification as user 2')
notification_seen_user_4 = self.env['bus.bus'].search([('create_uid', '=', user_4.id)])
self.assertFalse(notification_seen_user_4, 'Should not have sent a notification as user 4')
@tagged('moderation', 'mail_channel')
class TestChannelModeration(MailCommon):
@classmethod
def setUpClass(cls):
super(TestChannelModeration, cls).setUpClass()
cls.channel = cls.env['mail.channel'].create({
'name': 'Moderation_1',
'channel_type': 'channel',
'email_send': True,
'moderation': True,
'channel_partner_ids': [(4, cls.partner_employee.id)],
'moderator_ids': [(4, cls.user_employee.id)],
'moderation_ids': [
(0, 0, {'email': 'allow@example.com', 'status': 'allow'}),
(0, 0, {'email': 'ban@example.com', 'status': 'ban'})
],
})
cls.user_employee_2 = mail_new_test_user(
cls.env, login='employee2', groups='base.group_user', company_id=cls.company_admin.id,
name='Enguerrand Employee2', notification_type='inbox', signature='--\nEnguerrand'
)
cls.partner_employee_2 = cls.user_employee_2.partner_id
cls.user_portal = cls._create_portal_user()
@users('employee')
def test_initial_values(self):
channel = self.env['mail.channel'].browse(self.channel.ids)
self.assertEqual(channel.channel_partner_ids, self.partner_employee)
self.assertEqual(channel.channel_last_seen_partner_ids.partner_id, self.partner_employee | self.partner_root)
self.assertEqual(channel.moderator_ids, self.user_employee)
@mute_logger('odoo.models.unlink')
def test_message_moderate_ok(self):
msg_c1_admin1 = self._add_messages(self.channel, 'Body11', author=self.partner_admin, moderation_status='pending_moderation')
msg_c1_admin2 = self._add_messages(self.channel, 'Body12', author=self.partner_admin, moderation_status='pending_moderation')
msg_c1_emplo2 = self._add_messages(self.channel, 'Body21', author=self.partner_employee_2, moderation_status='pending_moderation')
self._reset_bus()
# accept
with self.assertBus([(self.cr.dbname, 'mail.channel', self.channel.id)]):
msg_c1_admin1.with_user(self.user_employee)._moderate('accept')
self.assertEqual(msg_c1_admin1.moderation_status, 'accepted')
self.assertEqual(msg_c1_admin2.moderation_status, 'pending_moderation')
# allow
self._reset_bus()
with self.assertBus([
(self.cr.dbname, 'mail.channel', self.channel.id),
(self.cr.dbname, 'mail.channel', self.channel.id)]):
(msg_c1_admin1 | msg_c1_emplo2).with_user(self.user_employee)._moderate('allow')
self.assertEqual(msg_c1_admin1.moderation_status, 'accepted')
self.assertEqual(msg_c1_admin2.moderation_status, 'accepted')
self.assertEqual(msg_c1_emplo2.moderation_status, 'accepted')
@mute_logger('odoo.models.unlink')
def test_message_moderate_reject(self):
msg_c1_admin1 = self._add_messages(self.channel, '<p>Body11</p>', author=self.partner_admin, moderation_status='pending_moderation')
msg_c1_admin2 = self._add_messages(self.channel, '<p>Body12</p>', author=self.partner_admin, moderation_status='pending_moderation')
msg_c1_emplo2 = self._add_messages(self.channel, '<p>Body21</p>', author=self.partner_employee_2, moderation_status='pending_moderation')
msg_c1_portal = self._add_messages(self.channel, '<p>Body12</p>', author=self.partner_portal, moderation_status='pending_moderation')
id2, id4 = msg_c1_admin2.id, msg_c1_portal.id # save ids because unlink will discard them
# test reject: should also send a rejection email
with self.mock_mail_gateway():
(msg_c1_admin1 | msg_c1_emplo2).with_user(self.user_employee)._moderate('reject', title='RejectTitle', comment='RejectComment')
self.assertEqual(len(self._new_mails), 2)
self.assertMailMailWEmails(
[self.partner_admin.email_formatted, self.partner_employee_2.email_formatted], 'outgoing',
content='RejectComment',
fields_values={'subject': 'RejectTitle', 'author_id': self.partner_employee}
)
self.assertTrue(all('RejectComment' in body for body in self._new_mails.mapped('body_html')))
self.assertTrue(all('Body' in body for body in self._new_mails.mapped('body_html')))
# test discard: silently remove
self._reset_bus()
with self.assertBus(
[(self.cr.dbname, 'res.partner', self.partner_admin.id),
(self.cr.dbname, 'res.partner', self.partner_employee.id),
(self.cr.dbname, 'res.partner', self.partner_portal.id)],
message_items=[
{'type': 'deletion', 'message_ids': [id2]}, # author of 1 message
{'type': 'deletion', 'message_ids': [id2, id4]}, # moderator
{'type': 'deletion', 'message_ids': [id4]} # author of 1 message
]):
(msg_c1_admin2 + msg_c1_portal).with_user(self.user_employee)._moderate_discard()
@mute_logger('odoo.models.unlink')
def test_message_notify_moderators(self):
# create pending messages in another channel to have two notification to push
msg_c1_admin1 = self._add_messages(self.channel, '<p>Body11</p>', author=self.partner_admin, moderation_status='pending_moderation')
msg_c1_admin2 = self._add_messages(self.channel, '<p>Body12</p>', author=self.partner_admin, moderation_status='pending_moderation')
msg_c1_portal = self._add_messages(self.channel, '<p>Body21</p>', author=self.partner_employee_2, moderation_status='pending_moderation')
channel_2 = self.env['mail.channel'].create({
'name': 'Moderation_1',
'email_send': True,
'moderation': True,
'channel_partner_ids': [(4, self.partner_admin.id)],
'moderator_ids': [(4, self.user_admin.id)],
})
msg_c2_portal = self._add_messages(channel_2, 'Body31', author=self.partner_employee_2, moderation_status='pending_moderation')
# one notification for each moderator: employee (channel1), admin (channel2)
with self.assertPostNotifications([{
'content': 'Hello %s' % self.partner_employee.name,
'message_type': 'user_notification', 'subtype': 'mail.mt_note',
'notif': [{
'partner': self.partner_employee,
'type': 'inbox'}]
}, {
'content': 'Hello %s' % self.partner_admin.name,
'message_type': 'user_notification', 'subtype': 'mail.mt_note',
'notif': [{
'partner': self.partner_admin,
'type': 'inbox'}]
}]):
self.env['mail.message']._notify_moderators()
@users('employee')
def test_moderation_fields(self):
channel = self.env['mail.channel'].browse(self.channel.ids)
# moderators should be channel members
with self.assertRaises(ValidationError):
channel.write({'moderator_ids': [(4, self.user_admin.id)]})
channel.write({'channel_partner_ids': [(4, self.partner_admin.id)]})
channel.write({'moderator_ids': [(4, self.user_admin.id)]})
# member -> moderator ok (even if updated at same time, constraint should fire after updating all fields)
channel.write({
'channel_partner_ids': [(4, self.partner_employee_2.id)],
'moderator_ids': [(4, self.user_employee_2.id)]
})
# test moderation_count
self.assertEqual(channel.moderation_count, 2)
channel.write({'moderation_ids': [(0, 0, {'email': 'ban2@example.com', 'status': 'ban'})]})
self.assertEqual(channel.moderation_count, 3)
# test _update_moderation_email
channel._update_moderation_email(['allow@example.com', 'ban3@example.com'], 'ban')
self.assertEqual(len(channel.moderation_ids), 4)
self.assertTrue(all(status == 'ban' for status in channel.moderation_ids.mapped('status')))
# test user moderation fields
channel.write({'moderator_ids': [(3, self.user_employee_2.id)]})
(self.user_employee | self.user_employee_2).invalidate_cache(fnames=['moderation_counter'])
self.assertTrue(self.user_employee.is_moderator)
self.assertFalse(self.user_employee_2.is_moderator)
self.assertEqual(self.user_employee.moderation_counter, 0)
self.assertEqual(self.user_employee_2.moderation_counter, 0)
self._add_messages(channel, 'B', moderation_status='pending_moderation', author=self.partner_employee_2)
self._add_messages(channel, 'B', moderation_status='accepted', author=self.partner_employee_2)
self._add_messages(channel, 'B', moderation_status='accepted', author=self.partner_employee)
self._add_messages(channel, 'B', moderation_status='pending_moderation', author=self.partner_employee)
self._add_messages(channel, 'B', moderation_status='accepted', author=self.partner_employee)
(self.user_employee | self.user_employee_2).invalidate_cache(fnames=['moderation_counter'])
self.assertEqual(self.user_employee.moderation_counter, 2)
self.assertEqual(self.user_employee_2.moderation_counter, 0)
channel.write({
'channel_partner_ids': [(4, self.partner_employee_2.id)],
'moderator_ids': [(4, self.user_employee_2.id)],
})
self.user_employee_2.invalidate_cache(fnames=['moderation_counter'])
self.assertTrue(self.user_employee_2.is_moderator)
self.assertEqual(self.user_employee_2.moderation_counter, 2)
# member -> moderator ko if no email
channel.write({'moderator_ids': [(3, self.partner_admin.id)]})
self.user_admin.write({'email': False})
with self.assertRaises(ValidationError):
channel.write({'moderator_ids': [(4, self.user_admin.id)]})
# moderation enabled channels are restricted to mailing lists
with self.assertRaises(ValidationError):
channel.write({'email_send': False})
# moderation enabled channels should always have moderators
with self.assertRaises(ValidationError):
channel.write({'moderator_ids': [(5, 0)]})
@users('employee')
def test_moderation_reset(self):
channel_2 = self.env['mail.channel'].create({
'name': 'Moderation_1',
'email_send': True,
'moderation': True,
'channel_partner_ids': [(4, self.partner_employee.id)],
'moderator_ids': [(4, self.user_employee.id)],
})
msg_c1_1 = self._add_messages(self.channel, 'Body11', author=self.partner_admin, moderation_status='accepted')
msg_c1_2 = self._add_messages(self.channel, 'Body12', author=self.partner_admin, moderation_status='pending_moderation')
msg_c2_1 = self._add_messages(channel_2, 'Body21', author=self.partner_admin, moderation_status='pending_moderation')
self.assertEqual(self.env['mail.message'].search([
('moderation_status', '=', 'pending_moderation'),
('model', '=', 'mail.channel'), ('res_id', '=', self.channel.id)
]), msg_c1_2)
self.channel.write({'moderation': False})
self.assertEqual(self.env['mail.message'].search([
('moderation_status', '=', 'pending_moderation'),
('model', '=', 'mail.channel'), ('res_id', '=', self.channel.id)
]), self.env['mail.message'])
self.assertEqual(self.env['mail.message'].search([
('moderation_status', '=', 'pending_moderation'),
('model', '=', 'mail.channel'), ('res_id', '=', channel_2.id)
]), msg_c2_1)
channel_2.write({'moderation': False})
self.assertEqual(self.env['mail.message'].search([
('moderation_status', '=', 'pending_moderation'),
('model', '=', 'mail.channel'), ('res_id', '=', self.channel.id)
]), self.env['mail.message'])
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_channel', 'odoo.models.unlink')
def test_send_guidelines(self):
channel = self.env['mail.channel'].browse(self.channel.ids)
channel._action_add_members(self.partner_admin + self.partner_portal)
channel._update_moderation_email([self.partner_admin.email], 'ban')
self.assertEqual(channel.channel_partner_ids, self.partner_employee + self.partner_admin + self.partner_portal)
with self.mock_mail_gateway():
channel.send_guidelines()
for partner in self.partner_employee + self.partner_portal:
self.assertMailMail(partner, 'outgoing', author=self.partner_employee, fields_values={
'subject': 'Guidelines of channel %s' % self.channel.name,
'email_from': self.user_employee.company_id.catchall_formatted
})
# admin being banned, should not received any email
self.assertNoMail(self.partner_employee, author=self.partner_admin)
# only moderators can send guidelines
channel.write({
'moderator_ids': [(4, self.user_admin.id), (3, self.user_employee.id)]
})
with self.assertRaises(UserError):
channel.send_guidelines()
@users('employee')
@mute_logger('odoo.models.unlink')
def test_post_flow(self):
channel = self.env['mail.channel'].browse(self.channel.ids)
msg_admin = channel.message_post(message_type='email', subtype_xmlid='mail.mt_comment', author_id=self.partner_admin.id)
msg_moderator = channel.message_post(message_type='comment', subtype_xmlid='mail.mt_comment', author_id=self.partner_employee.id)
msg_email1 = channel.message_post(message_type='comment', subtype_xmlid='mail.mt_comment', email_from=formataddr(("MyName", "ban@example.com")))
msg_email2 = channel.message_post(message_type='email', subtype_xmlid='mail.mt_comment', email_from="allow@example.com")
msg_notif = channel.message_post()
messages = self.env['mail.message'].search([('model', '=', 'mail.channel'), ('res_id', '=', channel.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)
-19
View File
@@ -43,16 +43,7 @@
<field name="priority" eval="10"/>
<field name="arch" type="xml">
<form string="Mail Channel Form">
<header>
<button name="send_guidelines" type="object" string="Send guidelines" confirm="You are going to send the guidelines to all the subscribers. Do you confirm the action?" attrs="{'invisible':['|',('moderation_guidelines','=',False), ('is_moderator', '=', False)]}"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<field name="is_moderator" invisible="1"/>
<button class="btn oe_stat_button" name="mail.mail_moderation_action" type="action" attrs="{'invisible':[('is_moderator', '=', False)]}" icon="fa-bars" context="{'search_default_channel_id': active_id}">
<field string="Moderation List" name="moderation_count" widget="statinfo"/>
</button>
</div>
<div class="oe_button_box" name="button_box"/>
<field name="image_128" widget="image" class="oe_avatar" options="{'size': [90, 90]}"/>
<div class="oe_title">
@@ -64,7 +55,6 @@
<group class="o_label_nowrap">
<field name="active" invisible="1"/>
<field name="email_send"/>
<field name="moderation" attrs="{'invisible': [('email_send', '=', False)]}"/>
<field name="description" placeholder="Topics discussed in this group..."/>
</group>
<group name="group_alias" attrs="{'invisible': [('alias_domain', '=', False)]}">
@@ -97,15 +87,6 @@
</tree>
</field>
</page>
<page string="Moderation" name="moderation" attrs="{'invisible': [('moderation', '=', False)]}">
<group>
<field name="moderator_ids" widget="many2many_tags"/>
<field name="moderation_notify"/>
<field name="moderation_notify_msg" attrs="{'invisible': [('moderation_notify', '=', False)]}"/>
<field name="moderation_guidelines"/>
<field name="moderation_guidelines_msg" attrs="{'invisible':[('moderation_guidelines', '=', False)]}"/>
</group>
</page>
<page string="Integrations" invisible="1" name="mail_channel_integrations"></page>
</notebook>
</sheet>
-5
View File
@@ -39,11 +39,6 @@
action="mail_channel_partner_action"
sequence="21"
groups="base.group_no_one"/>
<menuitem name="Channel Moderation"
id="mail_moderation_menu"
parent="base.menu_email"
sequence="22"
action="mail_moderation_action"/>
<!-- Under Technical/Discuss -->
<menuitem name="Discuss"
-2
View File
@@ -40,8 +40,6 @@
<field name="res_id" widget="integer"/>
<field name="record_name"/>
<field name="parent_id"/>
<field name="moderation_status"/>
<field name="moderator_id"/>
</group>
</group>
<notebook>
@@ -1,46 +0,0 @@
<?xml version="1.0"?>
<odoo>
<data>
<record id="mail_moderation_view_tree" model="ir.ui.view">
<field name="name">mail.moderation.view.tree</field>
<field name="model">mail.moderation</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<tree string="Moderation Lists" editable="bottom">
<field name="channel_id"/>
<field name="email"/>
<field name="status"/>
</tree>
</field>
</record>
<record id="mail_moderation_view_search" model="ir.ui.view">
<field name="name">mail.moderation.view.search</field>
<field name="model">mail.moderation</field>
<field name="priority">25</field>
<field name="arch" type="xml">
<search string="Search Moderation List">
<field name="channel_id"/>
<field name="email"/>
<field name="status"/>
<filter string="Is Banned"
name="status_ban" help="Banned Emails"
domain="[('status', '=', 'ban')]"/>
<separator/>
<filter string="Is Allowed"
name="status_allow" help="Allowed Emails"
domain="[('status', '=', 'allow')]"/>
</search>
</field>
</record>
<record id="mail_moderation_action" model="ir.actions.act_window">
<field name="name">Moderation</field>
<field name="res_model">mail.moderation</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="mail_moderation_view_search"/>
<field name="context">{'search_default_status_ban': 1}</field>
</record>
</data>
</odoo>
@@ -310,12 +310,6 @@ class TestMessageAccess(TestMailCommon):
# Test: Bert reads the message, ok because linked to a doc he is allowed to read
self.message.with_user(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.with_user(self.user_employee).read()
# --------------------------------------------------
# CREATE
# --------------------------------------------------
@@ -392,23 +386,3 @@ class TestMessageAccess(TestMailCommon):
self.assertTrue(new_mail)
self.assertEqual(new_msg.parent_id, message)
# --------------------------------------------------
# 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.with_user(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.with_user(self.user_employee).write({'moderation_status': 'accepted'})