[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:
committed by
Thibault Delavallée
parent
f7c43b4343
commit
59d6bb23d7
@@ -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',
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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&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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 !')
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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>
|
||||
-104
@@ -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',
|
||||
});
|
||||
-13
@@ -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>
|
||||
-99
@@ -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',
|
||||
});
|
||||
-13
@@ -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 }),
|
||||
}
|
||||
)),
|
||||
|
||||
-46
@@ -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';
|
||||
|
||||
@@ -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`.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'})
|
||||
|
||||
Reference in New Issue
Block a user