[IMP] mail: add moderation on channels
Purpose of this commit is to allow moderation on incoming messages in discussion channels. On some channels on which moderation is required messages should be in a pending moderation stage. Moderators can accept or refuse messages as well as always allow or ban messages coming from a given set of emails. Channels now have an option to be moderated. Moderators can be added on channels. They have access to a specific UI in Discuss to see and take action on messages waiting for moderation. Concerning mail.thread message that are pending moderation are not notified. It means nobody receives a notification about them. Moderation process calls the notification once the message is validated. Various features included in this commit : * a model is added to store the decision about emails, allow or ban; * access rights are updated so that only moderators can modify moderation fields on message; * specific bus notifications are send to moderated people as well as to moderators on incoming emails as well as when a decision is taken; * options are added on channels to send explanations to moderated emails; * options are added on channels to write and send guidelines explaining why and how moderation is performed; * a reminder is send daily to moderators with remaining messages to moderate; * discuss UI is adapted and a new channel is added below Inbox and Starred giving access to moderation tools; * chanenl UI is adapted allowing to moderate directly inside channels; This commit is linked to task ID 29521. Closes #21921.
This commit is contained in:
committed by
Thibault Delavallée
parent
eeab18d753
commit
299ebb2cdf
@@ -32,6 +32,7 @@
|
||||
'views/mail_templates.xml',
|
||||
'wizard/email_template_preview_view.xml',
|
||||
'views/mail_template_views.xml',
|
||||
'views/mail_moderation_views.xml',
|
||||
'views/ir_actions_views.xml',
|
||||
'views/ir_model_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
|
||||
@@ -228,5 +228,8 @@ 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.mail_channel_menu_root_chat'),
|
||||
'is_moderator': request.env.user.is_moderator,
|
||||
'moderation_counter': request.env.user.moderation_counter,
|
||||
'moderation_channel_ids': request.env.user.moderation_channel_ids.ids,
|
||||
}
|
||||
return values
|
||||
|
||||
@@ -26,5 +26,18 @@
|
||||
<field eval="False" name="doall" />
|
||||
<field name="priority">1000</field>
|
||||
</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>
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
||||
@@ -226,5 +226,37 @@
|
||||
</td></tr>
|
||||
</table>
|
||||
</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.mail_channel_action_client_chat&active_id=channel_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>
|
||||
|
||||
<record id="mail_template_channel_send_guidelines" model="mail.template">
|
||||
<field name="name">Partner: Send channel guidelines</field>
|
||||
<field name="model_id" ref="base.model_res_partner"/>
|
||||
<field name="email_from">${object.company_id.catchall or object.company_id.email|safe}</field>
|
||||
<field name="partner_to">${object.id}</field>
|
||||
<field name="subject">Guidelines of channel ${ctx['channel'].name}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<div>
|
||||
<p>Hello ${object.name or ''},</p>
|
||||
<p>Please find below the guidelines of the ${ctx['channel'].name} channel.</p>
|
||||
<p>
|
||||
${ctx['channel'].moderation_guidelines_msg}
|
||||
</p>
|
||||
<p></p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="user_signature" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -14,6 +14,7 @@ from . import mail_template
|
||||
from . import mail_shortcode
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import update
|
||||
from . import ir_actions
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import base64
|
||||
from email.utils import formataddr
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import re
|
||||
|
||||
from email.utils import formataddr
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo import _, api, fields, models, modules, tools
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import ormcache
|
||||
from odoo.tools import ormcache, pycompat
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
MODERATION_FIELDS = ['moderation', 'moderator_ids', 'moderation_ids', 'moderation_notify', 'moderation_notify_msg', 'moderation_guidelines', 'moderation_guidelines_msg']
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChannelPartner(models.Model):
|
||||
_name = 'mail.channel.partner'
|
||||
@@ -28,6 +33,22 @@ class ChannelPartner(models.Model):
|
||||
is_pinned = fields.Boolean("Is pinned on the interface", default=True)
|
||||
|
||||
|
||||
class Moderation(models.Model):
|
||||
_name = 'mail.moderation'
|
||||
_description = 'Channel black/white list'
|
||||
|
||||
email = fields.Char(string="Email", index=True, required=True)
|
||||
status = fields.Selection([
|
||||
('allow', 'Always Allow'),
|
||||
('ban', 'Permanent Ban')],
|
||||
string="Status", required=True)
|
||||
channel_id = fields.Many2one('mail.channel', string="Channel", index=True, required=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('channel_email_uniq', 'unique (email,channel_id)', 'The email address must be unique per channel !')
|
||||
]
|
||||
|
||||
|
||||
class Channel(models.Model):
|
||||
""" A mail.channel is a discussion group that may behave like a listener
|
||||
on documents. """
|
||||
@@ -90,12 +111,61 @@ class Channel(models.Model):
|
||||
"Use this field anywhere a small image is required.")
|
||||
is_subscribed = fields.Boolean(
|
||||
'Is Subscribed', compute='_compute_is_subscribed')
|
||||
# moderation
|
||||
moderation = fields.Boolean(string='Moderate this channel')
|
||||
moderator_ids = fields.Many2many('res.users', 'mail_channel_moderator_rel', string='Moderators')
|
||||
is_moderator = fields.Boolean(help="Current user is a moderator of the channel", string='Moderator', compute="_compute_is_moderator")
|
||||
moderation_ids = fields.One2many(
|
||||
'mail.moderation', 'channel_id', string='Moderated Emails',
|
||||
groups="base.group_user")
|
||||
moderation_count = fields.Integer(
|
||||
string='Moderated emails count', compute='_compute_moderation_count',
|
||||
groups="base.group_user")
|
||||
moderation_notify = fields.Boolean(string="Automatic notification", help="People receive an automatic notification about their message being waiting for moderation.")
|
||||
moderation_notify_msg = fields.Text(string="Notification message")
|
||||
moderation_guidelines = fields.Boolean(string="Send guidelines to new subscribers", help="Newcomers on this moderated channel will automatically receive the guidelines.")
|
||||
moderation_guidelines_msg = fields.Text(string="Guidelines")
|
||||
|
||||
@api.one
|
||||
@api.depends('channel_partner_ids')
|
||||
def _compute_is_subscribed(self):
|
||||
self.is_subscribed = self.env.user.partner_id in self.channel_partner_ids
|
||||
|
||||
@api.multi
|
||||
@api.depends('moderator_ids')
|
||||
def _compute_is_moderator(self):
|
||||
for channel in self:
|
||||
channel.is_moderator = self.env.user in channel.moderator_ids
|
||||
|
||||
@api.multi
|
||||
@api.depends('moderation_ids')
|
||||
def _compute_moderation_count(self):
|
||||
read_group_res = self.env['mail.moderation'].read_group([('channel_id', 'in', self.ids)], ['channel_id'], 'channel_id')
|
||||
data = dict((res['channel_id'][0], res['channel_id_count']) for res in read_group_res)
|
||||
for channel in self:
|
||||
channel.moderation_count = data.get(channel.id, 0)
|
||||
|
||||
@api.constrains('moderator_ids')
|
||||
def _check_moderator_email(self):
|
||||
if any(not moderator.email for channel in self for moderator in channel.moderator_ids):
|
||||
raise ValidationError("Moderators must have an email address.")
|
||||
|
||||
@api.constrains('moderator_ids', 'channel_partner_ids', 'channel_last_seen_partner_ids')
|
||||
def _check_moderator_is_member(self):
|
||||
for channel in self:
|
||||
if not (channel.mapped('moderator_ids.partner_id') <= channel.channel_partner_ids):
|
||||
raise ValidationError("Moderators should be members of the channel they moderate.")
|
||||
|
||||
@api.constrains('moderation', 'email_send')
|
||||
def _check_moderation_parameters(self):
|
||||
if any(not channel.email_send and channel.moderation for channel in self):
|
||||
raise ValidationError('Only mailing lists can be moderated.')
|
||||
|
||||
@api.constrains('moderator_ids')
|
||||
def _check_moderator_existence(self):
|
||||
if any(not channel.moderator_ids for channel in self if channel.moderation):
|
||||
raise ValidationError('Moderated channels must have moderators.')
|
||||
|
||||
@api.multi
|
||||
def _compute_is_member(self):
|
||||
memberships = self.env['mail.channel.partner'].sudo().search([
|
||||
@@ -113,6 +183,26 @@ class Channel(models.Model):
|
||||
else:
|
||||
self.alias_contact = 'followers'
|
||||
|
||||
@api.onchange('moderator_ids')
|
||||
def _onchange_moderator_ids(self):
|
||||
missing_partners = self.mapped('moderator_ids.partner_id') - self.channel_partner_ids
|
||||
if missing_partners:
|
||||
self.channel_partner_ids |= missing_partners
|
||||
|
||||
@api.onchange('email_send')
|
||||
def _onchange_email_send(self):
|
||||
if not self.email_send:
|
||||
self.moderation = False
|
||||
|
||||
@api.onchange('moderation')
|
||||
def _onchange_moderation(self):
|
||||
if not self.moderation:
|
||||
self.moderation_notify = False
|
||||
self.moderation_guidelines = False
|
||||
self.moderator_ids = False
|
||||
else:
|
||||
self.moderator_ids |= self.env.user
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
# ensure image at quick create
|
||||
@@ -154,10 +244,25 @@ class Channel(models.Model):
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
# First checks if user tries to modify moderation fields and has not the right to do it.
|
||||
if any(key for key in MODERATION_FIELDS if vals.get(key)) and any(self.env.user not in channel.moderator_ids for channel in self if channel.moderation):
|
||||
if not self.env.user.has_group('base.group_system'):
|
||||
raise UserError("You do not possess the rights to modify fields related to moderation on one of the channels you are modifying.")
|
||||
|
||||
tools.image_resize_images(vals)
|
||||
result = super(Channel, self).write(vals)
|
||||
|
||||
if vals.get('group_ids'):
|
||||
self._subscribe_users()
|
||||
|
||||
# avoid keeping messages to moderate and accept them
|
||||
if vals.get('moderation') is False:
|
||||
self.env['mail.message'].search([
|
||||
('moderation_status', '=', 'pending_moderation'),
|
||||
('model', '=', 'mail.channel'),
|
||||
('res_id', 'in', self.ids)
|
||||
])._moderate_accept()
|
||||
|
||||
return result
|
||||
|
||||
def get_alias_model_name(self, vals):
|
||||
@@ -240,12 +345,54 @@ class Channel(models.Model):
|
||||
}
|
||||
return super(Channel, self)._notify_email_recipients(message, recipient_ids)
|
||||
|
||||
def _extract_moderation_values(self, message_type, **kwargs):
|
||||
""" This method is used to compute moderation status before the creation
|
||||
of a message. For this operation the message's author email address is required.
|
||||
This address is returned with status for other computations. """
|
||||
moderation_status = 'accepted'
|
||||
email = ''
|
||||
if self.moderation and message_type in ['email', 'comment']:
|
||||
author_id = kwargs.get('author_id')
|
||||
if author_id and isinstance(author_id, pycompat.integer_types):
|
||||
email = self.env['res.partner'].browse([author_id]).email
|
||||
elif author_id:
|
||||
email = author_id.email
|
||||
elif kwargs.get('email_from'):
|
||||
email = tools.email_split(kwargs['email_from'])[0]
|
||||
else:
|
||||
email = self.env.user.email
|
||||
if email in self.mapped('moderator_ids.email'):
|
||||
return moderation_status, email
|
||||
status = self.env['mail.moderation'].sudo().search([('email', '=', email), ('channel_id', 'in', self.ids)]).mapped('status')
|
||||
if status and status[0] == 'allow':
|
||||
moderation_status = 'accepted'
|
||||
elif status and status[0] == 'ban':
|
||||
moderation_status = 'rejected'
|
||||
else:
|
||||
moderation_status = 'pending_moderation'
|
||||
return moderation_status, email
|
||||
|
||||
@api.multi
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def message_post(self, **kwargs):
|
||||
# auto pin 'direct_message' channel partner
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, message_type='notification', **kwargs):
|
||||
moderation_status, email = self._extract_moderation_values(message_type, **kwargs)
|
||||
if moderation_status == 'rejected':
|
||||
return self.env['mail.message']
|
||||
|
||||
self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').write({'is_pinned': True})
|
||||
message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(**kwargs)
|
||||
|
||||
message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(message_type=message_type, moderation_status=moderation_status, **kwargs)
|
||||
|
||||
# Notifies the message author when his message is pending moderation if required on channel.
|
||||
# The fields "email_from" and "reply_to" are filled in automatically by method create in model mail.message.
|
||||
if self.moderation_notify and self.moderation_notify_msg and message_type == 'email' and moderation_status == 'pending_moderation':
|
||||
self.env['mail.mail'].create({
|
||||
'body_html': self.moderation_notify_msg,
|
||||
'subject': 'Re: %s' % (kwargs.get('subject', '')),
|
||||
'email_to': email,
|
||||
'auto_delete': True,
|
||||
'state': 'outgoing'
|
||||
})
|
||||
return message
|
||||
|
||||
def _alias_check_contact(self, message, message_dict, alias):
|
||||
@@ -264,6 +411,58 @@ class Channel(models.Model):
|
||||
if not self._cr.fetchone():
|
||||
self._cr.execute('CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)')
|
||||
|
||||
# --------------------------------------------------
|
||||
# Moderation
|
||||
# --------------------------------------------------
|
||||
|
||||
@api.multi
|
||||
def send_guidelines(self):
|
||||
""" Send guidelines to all channel members. """
|
||||
if self.env.user in self.moderator_ids or self.env.user.has_group('base.group_system'):
|
||||
success = self._send_guidelines(self.channel_partner_ids)
|
||||
if not success:
|
||||
raise UserError('Template "mail.mail_template_channel_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.')
|
||||
else:
|
||||
raise UserError("Only an administrator or a moderator can send guidelines to channel members!")
|
||||
|
||||
@api.multi
|
||||
def _send_guidelines(self, partners):
|
||||
""" Send guidelines of a given channel. Returns False if template used for guidelines
|
||||
not found. Caller may have to handle this return value. """
|
||||
self.ensure_one()
|
||||
template = self.env.ref('mail.mail_template_channel_send_guidelines', raise_if_not_found=False)
|
||||
if not template:
|
||||
_logger.warning('Template "mail.mail_template_channel_send_guidelines" was not found.')
|
||||
return False
|
||||
banned_emails = self.env['mail.moderation'].sudo().search([
|
||||
('status', '=', 'ban'),
|
||||
('channel_id', 'in', self.ids)
|
||||
]).mapped('email')
|
||||
for partner in partners.filtered(lambda p: p.email and not (p.email in banned_emails)):
|
||||
# the sudo is needed because because send_mail will create a message (a mail). As the template is
|
||||
# linked to res.partner model the current user could not have the rights to modify this model. It
|
||||
# means it could prevent users to create the mail.message necessary for the mail.mail.
|
||||
template.with_context(lang=partner.lang, channel=self).sudo().send_mail(partner.id)
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def _update_moderation_email(self, emails, status):
|
||||
""" This method adds emails into either white or black of the channel list of emails
|
||||
according to status. If an email in emails is already moderated, the method updates the email status.
|
||||
:param emails: list of email addresses to put in white or black list of channel.
|
||||
:param status: value is 'allow' or 'ban'. Emails are put in white list if 'allow', in black list if 'ban'.
|
||||
"""
|
||||
self.ensure_one()
|
||||
splitted_emails = [tools.email_split(email)[0] for email in emails if tools.email_split(email)]
|
||||
moderated = self.env['mail.moderation'].sudo().search([
|
||||
('email', 'in', splitted_emails),
|
||||
('channel_id', 'in', self.ids)
|
||||
])
|
||||
cmds = [(1, record.id, {'status': status}) for record in moderated]
|
||||
not_moderated = [email for email in splitted_emails if email not in moderated.mapped('email')]
|
||||
cmds += [(0, 0, {'email': email, 'status': status}) for email in not_moderated]
|
||||
return self.write({'moderation_ids': cmds})
|
||||
|
||||
#------------------------------------------------------
|
||||
# Instant Messaging API
|
||||
#------------------------------------------------------
|
||||
@@ -347,6 +546,8 @@ class Channel(models.Model):
|
||||
'channel_type': channel.channel_type,
|
||||
'public': channel.public,
|
||||
'mass_mailing': channel.email_send,
|
||||
'moderation': channel.moderation,
|
||||
'is_moderator': self.env.uid in channel.moderator_ids.ids,
|
||||
'group_based_subscription': bool(channel.group_ids),
|
||||
}
|
||||
if extra_info:
|
||||
@@ -572,6 +773,9 @@ class Channel(models.Model):
|
||||
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
|
||||
self.action_follow()
|
||||
|
||||
if self.moderation_guidelines:
|
||||
self._send_guidelines(self.env.user.partner_id)
|
||||
|
||||
channel_info = self.channel_info()[0]
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
|
||||
return channel_info
|
||||
|
||||
@@ -107,6 +107,13 @@ class Message(models.Model):
|
||||
message_id = fields.Char('Message-Id', help='Message unique identifier', index=True, readonly=1, copy=False)
|
||||
reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.')
|
||||
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
|
||||
# moderation
|
||||
moderation_status = fields.Selection([
|
||||
('pending_moderation', 'Pending Moderation'),
|
||||
('accepted', 'Accepted'),
|
||||
('rejected', 'Rejected')], string="Moderation Status", index=True)
|
||||
moderator_id = fields.Many2one('res.users', string="Moderated By", index=True)
|
||||
need_moderation = fields.Boolean('Need moderation', compute='_compute_need_moderation', search='_search_need_moderation')
|
||||
|
||||
@api.multi
|
||||
def _get_needaction(self):
|
||||
@@ -138,6 +145,23 @@ class Message(models.Model):
|
||||
return [('starred_partner_ids', 'in', [self.env.user.partner_id.id])]
|
||||
return [('starred_partner_ids', 'not in', [self.env.user.partner_id.id])]
|
||||
|
||||
@api.multi
|
||||
def _compute_need_moderation(self):
|
||||
for message in self:
|
||||
self.need_moderation = False
|
||||
|
||||
@api.model
|
||||
def _search_need_moderation(self, operator, operand):
|
||||
if operator == '=' and operand:
|
||||
return ['&', '&',
|
||||
('moderation_status', '=', 'pending_moderation'),
|
||||
('model', '=', 'mail.channel'),
|
||||
('res_id', 'in', self.env.user.moderation_channel_ids.ids)]
|
||||
return ['|', '|',
|
||||
('moderation_status', '!=', 'pending_moderation'),
|
||||
('model', '!=', 'mail.channel'),
|
||||
('res_id', 'not in', self.env.user.moderation_channel_ids.ids)]
|
||||
|
||||
#------------------------------------------------------
|
||||
# Notification API
|
||||
#------------------------------------------------------
|
||||
@@ -395,6 +419,7 @@ class Message(models.Model):
|
||||
'is_note': True # only if the message is a note (subtype == note)
|
||||
'is_discussion': False # only if the message is a discussion (subtype == discussion)
|
||||
'is_notification': False # only if the message is a note but is a notification aka not linked to a document like assignation
|
||||
'moderation_status': 'pending_moderation'
|
||||
}
|
||||
"""
|
||||
message_values = self.read([
|
||||
@@ -403,6 +428,7 @@ class Message(models.Model):
|
||||
'model', 'res_id', 'record_name', # document related
|
||||
'channel_ids', 'partner_ids', # recipients
|
||||
'starred_partner_ids', # list of partner ids for whom the message is starred
|
||||
'moderation_status',
|
||||
])
|
||||
message_tree = dict((m.id, m) for m in self.sudo())
|
||||
self._message_read_dict_postprocess(message_values, message_tree)
|
||||
@@ -474,8 +500,7 @@ class Message(models.Model):
|
||||
- if author_id == pid, uid is the author, OR
|
||||
- uid belongs to a notified channel, OR
|
||||
- uid is in the specified recipients, OR
|
||||
- uid has a notification on the message, OR
|
||||
- uid have read access to the related document is model, res_id
|
||||
- uid has a notification on the message
|
||||
- otherwise: remove the id
|
||||
"""
|
||||
# Rules do not apply to administrator
|
||||
@@ -517,6 +542,7 @@ class Message(models.Model):
|
||||
ON channel.id = channel_rel.mail_channel_id
|
||||
LEFT JOIN "mail_channel_partner" channel_partner
|
||||
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
|
||||
|
||||
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=pid, ids=ids))
|
||||
for id, rmod, rid, author_id, partner_id, channel_id in self._cr.fetchall():
|
||||
if author_id == pid:
|
||||
@@ -558,10 +584,12 @@ class Message(models.Model):
|
||||
- write: if
|
||||
- author_id == pid, uid is the author, OR
|
||||
- uid is in the recipients (partner_ids) OR
|
||||
- uid has write or create access on the related document if model, res_id
|
||||
- uid is moderator of the channel and moderation_status is pending_moderation OR
|
||||
- uid has write or create access on the related document if model, res_id and moderation_status is not pending_moderation
|
||||
- otherwise: raise
|
||||
- unlink: if
|
||||
- uid has write or create access on the related document if model, res_id
|
||||
- uid is moderator of the channel and moderation_status is pending_moderation OR
|
||||
- uid has write or create access on the related document if model, res_id and moderation_status is not pending_moderation
|
||||
- otherwise: raise
|
||||
|
||||
Specific case: non employee users see only messages with subtype (aka do
|
||||
@@ -595,9 +623,9 @@ class Message(models.Model):
|
||||
# Read mail_message.ids to have their values
|
||||
message_values = dict((res_id, {}) for res_id in self.ids)
|
||||
|
||||
if operation in ['read', 'write']:
|
||||
if operation == 'read':
|
||||
self._cr.execute("""
|
||||
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id,
|
||||
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status,
|
||||
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
|
||||
channel_partner.channel_id as channel_id
|
||||
FROM "%s" m
|
||||
@@ -612,24 +640,84 @@ class Message(models.Model):
|
||||
LEFT JOIN "mail_channel_partner" channel_partner
|
||||
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
|
||||
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, ids=self.ids))
|
||||
for mid, rmod, rid, author_id, parent_id, partner_id, channel_id in self._cr.fetchall():
|
||||
for mid, rmod, rid, author_id, parent_id, partner_id, channel_id, moderation_status in self._cr.fetchall():
|
||||
message_values[mid] = {
|
||||
'model': rmod,
|
||||
'res_id': rid,
|
||||
'author_id': author_id,
|
||||
'parent_id': parent_id,
|
||||
'moderation_status': moderation_status,
|
||||
'moderator_id': False,
|
||||
'notified': any((message_values[mid].get('notified'), partner_id, channel_id))
|
||||
}
|
||||
else:
|
||||
self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
|
||||
for mid, rmod, rid, author_id, parent_id in self._cr.fetchall():
|
||||
message_values[mid] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
|
||||
elif operation == 'write':
|
||||
self._cr.execute("""
|
||||
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status,
|
||||
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
|
||||
channel_partner.channel_id as channel_id, channel_moderator_rel.res_users_id as moderator_id
|
||||
FROM "%s" m
|
||||
LEFT JOIN "mail_message_res_partner_rel" partner_rel
|
||||
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
|
||||
LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel
|
||||
ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s
|
||||
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
|
||||
ON channel_rel.mail_message_id = m.id
|
||||
LEFT JOIN "mail_channel" channel
|
||||
ON channel.id = channel_rel.mail_channel_id
|
||||
LEFT JOIN "mail_channel_partner" channel_partner
|
||||
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
|
||||
LEFT JOIN "mail_channel" moderated_channel
|
||||
ON m.moderation_status = 'pending_moderation' AND m.res_id = moderated_channel.id
|
||||
LEFT JOIN "mail_channel_moderator_rel" channel_moderator_rel
|
||||
ON channel_moderator_rel.mail_channel_id = moderated_channel.id AND channel_moderator_rel.res_users_id = %%(uid)s
|
||||
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, uid=self.env.user.id, ids=self.ids))
|
||||
for mid, rmod, rid, author_id, parent_id, moderation_status, partner_id, channel_id, moderator_id in self._cr.fetchall():
|
||||
message_values[mid] = {
|
||||
'model': rmod,
|
||||
'res_id': rid,
|
||||
'author_id': author_id,
|
||||
'parent_id': parent_id,
|
||||
'moderation_status': moderation_status,
|
||||
'moderator_id': moderator_id,
|
||||
'notified': any((message_values[mid].get('notified'), partner_id, channel_id))
|
||||
}
|
||||
elif operation == 'create':
|
||||
self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id, moderation_status FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,))
|
||||
for mid, rmod, rid, author_id, parent_id, moderation_status in self._cr.fetchall():
|
||||
message_values[mid] = {
|
||||
'model': rmod,
|
||||
'res_id': rid,
|
||||
'author_id': author_id,
|
||||
'parent_id': parent_id,
|
||||
'moderation_status': moderation_status,
|
||||
'moderator_id': False
|
||||
}
|
||||
else: # unlink
|
||||
self._cr.execute("""SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, m.moderation_status, channel_moderator_rel.res_users_id as moderator_id
|
||||
FROM "%s" m
|
||||
LEFT JOIN "mail_channel" moderated_channel
|
||||
ON m.moderation_status = 'pending_moderation' AND m.res_id = moderated_channel.id
|
||||
LEFT JOIN "mail_channel_moderator_rel" channel_moderator_rel
|
||||
ON channel_moderator_rel.mail_channel_id = moderated_channel.id AND channel_moderator_rel.res_users_id = (%%s)
|
||||
WHERE m.id = ANY (%%s)""" % self._table, (self.env.user.id, self.ids,))
|
||||
for mid, rmod, rid, author_id, parent_id, moderation_status, moderator_id in self._cr.fetchall():
|
||||
message_values[mid] = {
|
||||
'model': rmod,
|
||||
'res_id': rid,
|
||||
'author_id': author_id,
|
||||
'parent_id': parent_id,
|
||||
'moderation_status': moderation_status,
|
||||
'moderator_id': moderator_id
|
||||
}
|
||||
|
||||
# Author condition (READ, WRITE, CREATE (private))
|
||||
author_ids = []
|
||||
if operation == 'read' or operation == 'write':
|
||||
if operation == 'read':
|
||||
author_ids = [mid for mid, message in message_values.items()
|
||||
if message.get('author_id') and message.get('author_id') == self.env.user.partner_id.id]
|
||||
elif operation == 'write':
|
||||
author_ids = [mid for mid, message in message_values.items()
|
||||
if message.get('moderation_status') != 'pending_moderation' and message.get('author_id') == self.env.user.partner_id.id]
|
||||
elif operation == 'create':
|
||||
author_ids = [mid for mid, message in message_values.items()
|
||||
if not message.get('model') and not message.get('res_id')]
|
||||
@@ -654,8 +742,13 @@ class Message(models.Model):
|
||||
notified_ids += [mid for mid, message in message_values.items()
|
||||
if message.get('parent_id') in not_parent_ids]
|
||||
|
||||
# Moderator condition: allow to WRITE, UNLINK if moderator of a pending message
|
||||
moderator_ids = []
|
||||
if operation in ['write', 'unlink']:
|
||||
moderator_ids = [mid for mid, message in message_values.items() if message.get('moderator_id')]
|
||||
|
||||
# Recipients condition, for read and write (partner_ids) and create (message_follower_ids)
|
||||
other_ids = set(self.ids).difference(set(author_ids), set(notified_ids))
|
||||
other_ids = set(self.ids).difference(set(author_ids), set(notified_ids), set(moderator_ids))
|
||||
model_record_ids = _generate_model_record_ids(message_values, other_ids)
|
||||
if operation in ['read', 'write']:
|
||||
notified_ids = [mid for mid, message in message_values.items() if message.get('notified')]
|
||||
@@ -681,8 +774,13 @@ class Message(models.Model):
|
||||
DocumentModel.check_mail_message_access(mids.ids, operation) # ?? mids ?
|
||||
else:
|
||||
self.env['mail.thread'].check_mail_message_access(mids.ids, operation, model_name=model)
|
||||
document_related_ids += [mid for mid, message in message_values.items()
|
||||
if message.get('model') == model and message.get('res_id') in mids.ids]
|
||||
if operation in ['write', 'unlink']:
|
||||
document_related_ids += [mid for mid, message in message_values.items()
|
||||
if message.get('model') == model and message.get('res_id') in mids.ids and
|
||||
message.get('moderation_status') != 'pending_moderation']
|
||||
else:
|
||||
document_related_ids += [mid for mid, message in message_values.items()
|
||||
if message.get('model') == model and message.get('res_id') in mids.ids]
|
||||
|
||||
# Calculate remaining ids: if not void, raise an error
|
||||
other_ids = other_ids.difference(set(document_related_ids))
|
||||
@@ -841,7 +939,6 @@ class Message(models.Model):
|
||||
# remove author from notified partners
|
||||
if not self._context.get('mail_notify_author', False) and self_sudo.author_id:
|
||||
partners_sudo = partners_sudo - self_sudo.author_id
|
||||
|
||||
# update message, with maybe custom values
|
||||
message_values = {}
|
||||
if channels_sudo:
|
||||
@@ -858,6 +955,7 @@ class Message(models.Model):
|
||||
# have no access to partner model. Maybe propagating a real uid could be necessary.
|
||||
email_channels = channels_sudo.filtered(lambda channel: channel.email_send)
|
||||
notif_partners = partners_sudo.filtered(lambda partner: 'inbox' in partner.mapped('user_ids.notification_type'))
|
||||
|
||||
if email_channels or partners_sudo - notif_partners:
|
||||
partners_sudo.search([
|
||||
'|',
|
||||
@@ -867,7 +965,170 @@ class Message(models.Model):
|
||||
])._notify(self, layout=layout, force_send=force_send, send_after_commit=send_after_commit, values=values)
|
||||
|
||||
notif_partners._notify_by_chat(self)
|
||||
|
||||
channels_sudo._notify(self)
|
||||
|
||||
return True
|
||||
|
||||
# --------------------------------------------------
|
||||
# Moderation
|
||||
# --------------------------------------------------
|
||||
|
||||
@api.multi
|
||||
def moderate(self, decision, **kwargs):
|
||||
""" Moderate messages. A check is done on moderation status of the
|
||||
current user to ensure we only moderate valid messages. """
|
||||
moderated_channels = self.env.user.moderation_channel_ids
|
||||
to_moderate = [message.id for message in self
|
||||
if message.model == 'mail.channel' and
|
||||
message.res_id in moderated_channels.ids and
|
||||
message.moderation_status == 'pending_moderation']
|
||||
if to_moderate:
|
||||
self.browse(to_moderate)._moderate(decision, **kwargs)
|
||||
|
||||
@api.multi
|
||||
def _moderate(self, decision, **kwargs):
|
||||
""" :param decision
|
||||
* accept - moderate message and broadcast that message to followers of relevant channels.
|
||||
* reject - message will be deleted from the database without broadcast
|
||||
an email sent to the author with an explanation that the moderators can edit.
|
||||
* discard - message will be deleted from the database without broadcast.
|
||||
* allow - add email address to white list people of specific channel,
|
||||
so that next time if a message come from same email address on same channel,
|
||||
it will be automatically broadcasted to relevant channels without any approval from moderator.
|
||||
* ban - add email address to black list of emails for the specific channel.
|
||||
From next time, a person sending a message using that email address will not need moderation.
|
||||
message_post will not create messages with the corresponding expeditor.
|
||||
"""
|
||||
if decision == 'accept':
|
||||
self._moderate_accept()
|
||||
elif decision == 'reject':
|
||||
self._moderate_send_reject_email(kwargs.get('title'), kwargs.get('comment'))
|
||||
self._moderate_discard()
|
||||
elif decision == 'discard':
|
||||
self._moderate_discard()
|
||||
elif decision == 'allow':
|
||||
channels = self.env['mail.channel'].browse(self.mapped('res_id'))
|
||||
for channel in channels:
|
||||
channel._update_moderation_email(
|
||||
list({message.email_from for message in self if message.res_id == channel.id}),
|
||||
'allow'
|
||||
)
|
||||
self._search_from_same_authors()._moderate_accept()
|
||||
elif decision == 'ban':
|
||||
channels = self.env['mail.channel'].browse(self.mapped('res_id'))
|
||||
for channel in channels:
|
||||
channel._update_moderation_email(
|
||||
list({message.email_from for message in self if message.res_id == channel.id}),
|
||||
'ban'
|
||||
)
|
||||
self._search_from_same_authors()._moderate_discard()
|
||||
|
||||
def _moderate_accept(self):
|
||||
self.write({
|
||||
'moderation_status': 'accepted',
|
||||
'moderator_id': self.env.uid
|
||||
})
|
||||
# proceed with notification process to send notification emails and Inbox messages
|
||||
for message in self:
|
||||
message._notify()
|
||||
|
||||
@api.multi
|
||||
def _moderate_send_reject_email(self, subject, comment):
|
||||
for msg in self:
|
||||
if not msg.email_from:
|
||||
continue
|
||||
if self.env.user.partner_id.email:
|
||||
email_from = formataddr((self.env.user.partner_id.name, self.env.user.partner_id.email))
|
||||
else:
|
||||
email_from = self.env.user.company_id.catchall
|
||||
|
||||
body_html = tools.append_content_to_html('<div>%s</div>' % tools.ustr(comment), msg.body)
|
||||
vals = {
|
||||
'subject': subject,
|
||||
'body_html': body_html,
|
||||
'email_from': email_from,
|
||||
'email_to': msg.email_from,
|
||||
'auto_delete': True,
|
||||
'state': 'outgoing'
|
||||
}
|
||||
self.env['mail.mail'].sudo().create(vals)
|
||||
|
||||
@api.multi
|
||||
def _search_from_same_authors(self):
|
||||
""" Returns all pending moderation messages that have same email_from and
|
||||
same res_id as given recordset. """
|
||||
messages = self.env['mail.message'].sudo()
|
||||
for message in self:
|
||||
messages |= messages.search([
|
||||
('moderation_status', '=', 'pending_moderation'),
|
||||
('email_from', '=', message.email_from),
|
||||
('model', '=', 'mail.channel'),
|
||||
('res_id', '=', message.res_id)
|
||||
])
|
||||
return messages
|
||||
|
||||
@api.multi
|
||||
def _moderate_discard(self):
|
||||
""" Notify deletion of messages to their moderators and authors and then delete them.
|
||||
"""
|
||||
channel_ids = self.mapped('res_id')
|
||||
moderators = self.env['mail.channel'].browse(channel_ids).mapped('moderator_ids')
|
||||
authors = self.mapped('author_id')
|
||||
partner_to_pid = {}
|
||||
for moderator in moderators:
|
||||
partner_to_pid.setdefault(moderator.partner_id.id, set())
|
||||
partner_to_pid[moderator.partner_id.id] |= set([message.id for message in self if message.res_id in moderator.moderation_channel_ids.ids])
|
||||
for author in authors:
|
||||
partner_to_pid.setdefault(author.id, set())
|
||||
partner_to_pid[author.id] |= set([message.id for message in self if message.author_id == author])
|
||||
|
||||
notifications = []
|
||||
for partner_id, message_ids in partner_to_pid.items():
|
||||
notifications.append([
|
||||
(self._cr.dbname, 'res.partner', partner_id),
|
||||
{'type': 'deletion', 'message_ids': list(message_ids)}
|
||||
])
|
||||
self.env['bus.bus'].sendmany(notifications)
|
||||
self.unlink()
|
||||
|
||||
def _notify_pending_by_chat(self):
|
||||
""" Generate the bus notifications for the given message and send them
|
||||
to the appropriate moderators and the author (if the author has not been
|
||||
elected moderator meanwhile). The author notification can be considered
|
||||
as a feedback to the author.
|
||||
"""
|
||||
self.ensure_one()
|
||||
message = self.message_format()[0]
|
||||
partners = self.env['mail.channel'].browse(self.res_id).mapped('moderator_ids.partner_id')
|
||||
notifications = []
|
||||
for partner in partners:
|
||||
notifications.append([
|
||||
(self._cr.dbname, 'res.partner', partner.id),
|
||||
{'type': 'moderator', 'message': message}
|
||||
])
|
||||
if self.author_id not in partners:
|
||||
notifications.append([
|
||||
(self._cr.dbname, 'res.partner', self.author_id.id),
|
||||
{'type': 'author', 'message': message}
|
||||
])
|
||||
self.env['bus.bus'].sendmany(notifications)
|
||||
|
||||
@api.model
|
||||
def _notify_moderators(self):
|
||||
""" Push a notification (Inbox/email) to moderators having messages
|
||||
waiting for moderation. This method is called once a day by a cron.
|
||||
"""
|
||||
channels = self.env['mail.channel'].browse(self.search([('moderation_status', '=', 'pending_moderation')]).mapped('res_id'))
|
||||
moderators_to_notify = channels.mapped('moderator_ids')
|
||||
template = self.env.ref('mail.mail_channel_notify_moderation', raise_if_not_found=False)
|
||||
if not template:
|
||||
_logger.warning('Template "mail.mail_channel_notify_moderation" was not found. Cannot send reminder notifications.')
|
||||
return
|
||||
MailThread = self.env['mail.thread'].with_context(mail_notify_author=True)
|
||||
for moderator in moderators_to_notify:
|
||||
MailThread.message_notify(
|
||||
moderator.partner_id.ids,
|
||||
subject=_('Message are pending moderation'), # tocheck: target language
|
||||
body=template.render({'record': moderator.partner_id}, engine='ir.qweb', minimal_qcontext=True),
|
||||
email_from=moderator.company_id.catchall or moderator.company_id.email,
|
||||
)
|
||||
|
||||
@@ -1294,7 +1294,7 @@ class MailThread(models.AbstractModel):
|
||||
post_params['model'] = model
|
||||
new_msg = thread.message_post(**post_params)
|
||||
|
||||
if original_partner_ids:
|
||||
if new_msg and original_partner_ids:
|
||||
# postponed after message_post, because this is an external message and we don't want to create
|
||||
# duplicate emails due to notifications
|
||||
new_msg.write({'partner_ids': original_partner_ids})
|
||||
@@ -1869,7 +1869,7 @@ class MailThread(models.AbstractModel):
|
||||
return m2m_attachment_ids
|
||||
|
||||
@api.multi
|
||||
@api.returns('self', lambda value: value.id)
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, body='', subject=None,
|
||||
message_type='notification', subtype=None,
|
||||
parent_id=False, attachments=None,
|
||||
@@ -1891,6 +1891,7 @@ class MailThread(models.AbstractModel):
|
||||
to the related document. Should only be set by Chatter.
|
||||
:return int: ID of newly created mail.message
|
||||
"""
|
||||
|
||||
if attachments is None:
|
||||
attachments = {}
|
||||
if self.ids and not self.ensure_one():
|
||||
@@ -1998,15 +1999,18 @@ class MailThread(models.AbstractModel):
|
||||
message and computed value are given, to try to lessen query count by
|
||||
using already-computed values instead of having to rebrowse things. """
|
||||
# Notify recipients of the newly-created message (Inbox / Email + channels)
|
||||
message._notify(
|
||||
layout=notif_layout,
|
||||
force_send=self.env.context.get('mail_notify_force_send', True),
|
||||
values=notif_values,
|
||||
)
|
||||
if values.get('moderation_status') != 'pending_moderation':
|
||||
message._notify(
|
||||
layout=notif_layout,
|
||||
force_send=self.env.context.get('mail_notify_force_send', True),
|
||||
values=notif_values,
|
||||
)
|
||||
|
||||
# Post-process: subscribe author
|
||||
if values['author_id'] and values['model'] and self.ids and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
|
||||
self._message_subscribe([values['author_id']])
|
||||
# Post-process: subscribe author
|
||||
if values['author_id'] and values['model'] and self.ids and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
|
||||
self._message_subscribe([values['author_id']])
|
||||
else:
|
||||
message._notify_pending_by_chat()
|
||||
|
||||
@api.multi
|
||||
def message_post_with_view(self, views_or_xmlid, **kwargs):
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
_name = 'res.company'
|
||||
_inherit = 'res.company'
|
||||
|
||||
catchall = fields.Char(string="Catchall Email", compute="_compute_catchall")
|
||||
|
||||
@api.multi
|
||||
def _compute_catchall(self):
|
||||
ConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
alias = ConfigParameter.get_param('mail.catchall.alias')
|
||||
domain = ConfigParameter.get_param('mail.catchall.domain')
|
||||
if alias and domain:
|
||||
for company in self:
|
||||
company.catchall = '%s@%s' % (alias, domain)
|
||||
else:
|
||||
for company in self:
|
||||
company.catchall = ''
|
||||
@@ -11,7 +11,7 @@ class Users(models.Model):
|
||||
- make a new user follow itself
|
||||
- add a welcome message
|
||||
- add suggestion preference
|
||||
- if adding groups to an user, check mail.channels linked to this user
|
||||
- if adding groups to a user, check mail.channels linked to this user
|
||||
group, and the user. This is done by overriding the write method.
|
||||
"""
|
||||
_name = 'res.users'
|
||||
@@ -31,6 +31,39 @@ class Users(models.Model):
|
||||
help="Policy on how to handle Chatter notifications:\n"
|
||||
"- Handle by Emails: notifications are sent to your email address\n"
|
||||
"- Handle in Odoo: notifications appear in your Odoo Inbox")
|
||||
# channel-specific: moderation
|
||||
is_moderator = fields.Boolean(string='Is moderator', compute='_compute_is_moderator')
|
||||
moderation_counter = fields.Integer(string='Moderation count', compute='_compute_moderation_counter')
|
||||
moderation_channel_ids = fields.Many2many(
|
||||
'mail.channel', 'mail_channel_moderator_rel',
|
||||
string='Moderated channels')
|
||||
|
||||
@api.depends('moderation_channel_ids.moderation', 'moderation_channel_ids.moderator_ids')
|
||||
@api.multi
|
||||
def _compute_is_moderator(self):
|
||||
moderated = self.env['mail.channel'].search([
|
||||
('id', 'in', self.mapped('moderation_channel_ids').ids),
|
||||
('moderation', '=', True),
|
||||
('moderator_ids', 'in', self.ids)
|
||||
])
|
||||
user_ids = moderated.mapped('moderator_ids')
|
||||
for user in self:
|
||||
user.is_moderator = user in user_ids
|
||||
|
||||
@api.multi
|
||||
def _compute_moderation_counter(self):
|
||||
self._cr.execute("""
|
||||
SELECT channel_moderator.res_users_id, COUNT(msg.id)
|
||||
FROM "mail_channel_moderator_rel" AS channel_moderator
|
||||
JOIN "mail_message" AS msg
|
||||
ON channel_moderator.mail_channel_id = msg.res_id
|
||||
AND channel_moderator.res_users_id IN %s
|
||||
AND msg.model = 'mail.channel'
|
||||
AND msg.moderation_status = 'pending_moderation'
|
||||
GROUP BY channel_moderator.res_users_id""", [tuple(self.ids)])
|
||||
result = dict(self._cr.fetchall())
|
||||
for user in self:
|
||||
user.moderation_counter = result.get(user.id, 0)
|
||||
|
||||
def __init__(self, pool, cr):
|
||||
""" Override of __init__ to add access rights on notification_email_send
|
||||
|
||||
@@ -18,6 +18,7 @@ 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,1,1,1
|
||||
access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1,1,1,1
|
||||
|
||||
|
@@ -51,5 +51,10 @@
|
||||
<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>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -106,10 +106,73 @@ var PartnerInviteDialog = Dialog.extend({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Widget : Moderator reject message dialog
|
||||
*
|
||||
* Popup containing message title and reject message body.
|
||||
* This let the moderator provide a reason for rejecting the messages.
|
||||
*/
|
||||
var ModeratorRejectMessageDialog = Dialog.extend({
|
||||
template: "mail.ModeratorRejectMessageDialog",
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {web.Widget} parent
|
||||
* @param {Object} params
|
||||
* @param {integer[]} params.messageIDs list of message IDs to send
|
||||
* 'reject' decision reason
|
||||
* @param {function} params.proceedReject a function to call when the
|
||||
* moderator confirms the reason for rejecting the messages. This
|
||||
* function passes an object as the reason for reject, which is
|
||||
* structured as follow:
|
||||
*
|
||||
* {
|
||||
* title: <string>,
|
||||
* comment: <string>,
|
||||
* }
|
||||
*/
|
||||
init: function (parent, params) {
|
||||
this._messageIDs = params.messageIDs;
|
||||
this._proceedReject = params.proceedReject;
|
||||
this._super(parent, {
|
||||
title: _t('Send explanation to author'),
|
||||
size: "medium",
|
||||
buttons: [{
|
||||
text: _t("Send"),
|
||||
close: true,
|
||||
classes: "btn-primary",
|
||||
click: _.bind(this._onSendClicked, this),
|
||||
}],
|
||||
});
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when the moderator would like to submit reason for rejecting the
|
||||
* messages.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onSendClicked: function () {
|
||||
var title = this.$('#message_title').val();
|
||||
var comment = this.$('#reject_message').val();
|
||||
if (title && comment) {
|
||||
this._proceedReject({
|
||||
title: title,
|
||||
comment: comment
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
template: 'mail.discuss',
|
||||
custom_events: {
|
||||
message_moderation: '_onMessageModeration',
|
||||
search: '_onSearch',
|
||||
update_moderation_buttons: '_onUpdateModerationButtons',
|
||||
},
|
||||
events: {
|
||||
'blur .o_mail_add_channel input': '_onAddChannelBlur',
|
||||
@@ -239,6 +302,49 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Ban the authors of the messages with ID in `messageIDs`
|
||||
* Show a confirmation dialog to the moderator.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} messageIDs IDs of messages for which we should ban authors
|
||||
*/
|
||||
_banAuthorsFromMessageIDs: function (messageIDs) {
|
||||
var self = this;
|
||||
var emailList = _.map(messageIDs, function (messageID) {
|
||||
return self.call('chat_manager', 'getMessage', messageID).email_from;
|
||||
}).join(", ");
|
||||
var text = _.str.sprintf(_t("You are going to ban: %s. Do you confirm the action?"), emailList);
|
||||
var options = {
|
||||
confirm_callback: function () {
|
||||
self._moderateMessages(messageIDs, 'ban');
|
||||
}
|
||||
};
|
||||
Dialog.confirm(this, text, options);
|
||||
},
|
||||
/**
|
||||
* Discard the messages with ID in `messageIDs`
|
||||
* Show a confirmation dialog to the moderator.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]]} messageIDs list of message IDs to discard
|
||||
*/
|
||||
_discardMessages: function (messageIDs) {
|
||||
var self = this;
|
||||
var num = messageIDs.length;
|
||||
var text;
|
||||
if (num > 1) {
|
||||
text = _.str.sprintf(_t("You are going to discard %s messages. Do you confirm the action?"), num);
|
||||
} else if (num === 1) {
|
||||
text = _t("You are going to discard 1 message. Do you confirm the action?");
|
||||
}
|
||||
var options = {
|
||||
confirm_callback: function () {
|
||||
self._moderateMessages(messageIDs, 'discard');
|
||||
}
|
||||
};
|
||||
Dialog.confirm(this, text, options);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {$.Promise}
|
||||
@@ -278,11 +384,35 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
squash_close_messages: this.channel.type !== 'static' && !this.channel.mass_mailing,
|
||||
display_empty_channel: !messages.length && !this.domain.length,
|
||||
display_no_match: !messages.length && this.domain.length,
|
||||
display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox",
|
||||
display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox" || this.channel.id === "channel_moderation",
|
||||
display_email_icon: false,
|
||||
display_reply_icon: true,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Determine the action to apply on messages with ID in `messageIDs`
|
||||
* based on the moderation decision `decision`.
|
||||
*
|
||||
* @private
|
||||
* @param {number[]} messageIDs list of message ids that are moderated
|
||||
* @param {string} decision of the moderator, could be either 'reject',
|
||||
* 'discard', 'ban', 'accept', 'allow'.
|
||||
*/
|
||||
_handleModerationDecision: function (messageIDs, decision) {
|
||||
if (messageIDs) {
|
||||
if (decision === 'reject') {
|
||||
this._rejectMessages(messageIDs);
|
||||
} else if (decision === 'discard') {
|
||||
this._discardMessages(messageIDs);
|
||||
} else if (decision === 'ban') {
|
||||
this._banAuthorsFromMessageIDs(messageIDs);
|
||||
} else {
|
||||
// other decisions do not need more information,
|
||||
// confirmation dialog, etc.
|
||||
this._moderateMessages(messageIDs, decision);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Ensures that enough messages have been loaded to fill the entire screen
|
||||
* (this is particularily important because remaining messages can only be
|
||||
@@ -325,6 +455,33 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
self.thread.scroll_to({offset: offset});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Apply the moderation decision `decision` on the messages with ID in
|
||||
* `messageIDs`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} messageIDs list of message IDs to apply the
|
||||
* moderation decision.
|
||||
* @param {string} decision the moderation decision to apply on the
|
||||
* messages. Could be either 'reject', 'discard', 'ban', 'accept',
|
||||
* or 'allow'.
|
||||
* @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]
|
||||
* @return {undefined|$.Promise}
|
||||
*/
|
||||
_moderateMessages: function (messageIDs, decision, kwargs) {
|
||||
if (messageIDs.length && decision) {
|
||||
return this._rpc({
|
||||
model: 'mail.message',
|
||||
method: 'moderate',
|
||||
args: [messageIDs, decision],
|
||||
kwargs: kwargs
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Binds handlers on the given $input to make them autocomplete and/or
|
||||
* create channels.
|
||||
@@ -393,6 +550,24 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Reject the messages
|
||||
*
|
||||
* The moderator must provide a reason for reject, and may also
|
||||
* cancel his action.
|
||||
*
|
||||
* @private
|
||||
* @param {number[]]} messageIDs list of message IDs to reject
|
||||
*/
|
||||
_rejectMessages: function (messageIDs) {
|
||||
var self = this;
|
||||
new ModeratorRejectMessageDialog(this, {
|
||||
messageIDs: messageIDs,
|
||||
proceedReject: function (reason) {
|
||||
self._moderateMessages(messageIDs, 'reject', reason);
|
||||
}
|
||||
}).open();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -402,6 +577,9 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
this.$buttons.on('click', '.o_mail_chat_button_invite', this._onInviteButtonClicked.bind(this));
|
||||
this.$buttons.on('click', '.o_mail_chat_button_mark_read', this._onMarkAllReadClicked.bind(this));
|
||||
this.$buttons.on('click', '.o_mail_chat_button_unstar_all', this._onUnstarAllClicked.bind(this));
|
||||
this.$buttons.on('click', '.o_mail_chat_button_moderate_all', this._onModerateAllClicked.bind(this));
|
||||
this.$buttons.on('click', '.o_mail_chat_button_select_all', this._onSelectAllClicked.bind(this));
|
||||
this.$buttons.on('click', '.o_mail_chat_button_unselect_all', this._onUnselectAllClicked.bind(this));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -454,25 +632,23 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_renderThread: function () {
|
||||
var self = this;
|
||||
this.thread = new ChatThread(this, {display_help: true, loadMoreOnScroll: true});
|
||||
|
||||
this.thread.on('redirect', this, function (resModel, resID) {
|
||||
self.call('chat_manager', 'redirect', resModel, resID, self._setChannel.bind(self));
|
||||
this.call('chat_manager', 'redirect', resModel, resID, this._setChannel.bind(this));
|
||||
});
|
||||
this.thread.on('redirect_to_channel', this, function (channelID) {
|
||||
self.call('chat_manager', 'joinChannel', channelID).then(this._setChannel.bind(this));
|
||||
this.call('chat_manager', 'joinChannel', channelID).then(this._setChannel.bind(this));
|
||||
});
|
||||
this.thread.on('load_more_messages', this, this._loadMoreMessages);
|
||||
this.thread.on('mark_as_read', this, function (messageID) {
|
||||
self.call('chat_manager', 'markAsRead', [messageID]);
|
||||
this.call('chat_manager', 'markAsRead', [messageID]);
|
||||
});
|
||||
this.thread.on('toggle_star_status', this, function (messageID) {
|
||||
self.call('chat_manager', 'toggleStarStatus', messageID);
|
||||
this.call('chat_manager', 'toggleStarStatus', messageID);
|
||||
});
|
||||
this.thread.on('select_message', this, this._selectMessage);
|
||||
this.thread.on('unselect_message', this, this._unselectMessage);
|
||||
|
||||
return this.thread.appendTo(this.$('.o_mail_chat_content'));
|
||||
},
|
||||
/**
|
||||
@@ -612,6 +788,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
chatBus.on('update_channel_unread_counter', this, this.throttledUpdateChannels);
|
||||
chatBus.on('update_dm_presence', this, this.throttledUpdateChannels);
|
||||
chatBus.on('activity_updated', this, this.throttledUpdateChannels);
|
||||
chatBus.on('update_moderation_counter', this, this.throttledUpdateChannels);
|
||||
},
|
||||
/**
|
||||
* Stores the scroll position and composer state of the current channel
|
||||
@@ -663,6 +840,8 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
channels: this.call('chat_manager', 'getChannels'),
|
||||
needaction_counter: this.call('chat_manager', 'getNeedactionCounter'),
|
||||
starred_counter: this.call('chat_manager', 'getStarredCounter'),
|
||||
moderationCounter: this.call('chat_manager', 'getModerationCounter'),
|
||||
isModerator: this.call('chat_manager', 'isModerator'),
|
||||
});
|
||||
this.$(".o_mail_chat_sidebar").html($sidebar.contents());
|
||||
_.each(['dm', 'public', 'private'], function (type) {
|
||||
@@ -694,6 +873,9 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
.find('.o_mail_chat_button_unstar_all')
|
||||
.toggleClass('disabled', disabled);
|
||||
}
|
||||
if ((this.channel.isModerated && this.channel.isModerator) || this.channel.id === "channel_moderation") {
|
||||
this._updateModerationButtons();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -727,13 +909,69 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
.find('.o_mail_chat_button_unstar_all')
|
||||
.toggle(channel.id === "channel_starred")
|
||||
.removeClass("o_hidden");
|
||||
|
||||
this.$buttons
|
||||
.find('.o_mail_chat_button_select_all')
|
||||
.toggle((channel.isModerated && channel.isModerator) || channel.id === "channel_moderation")
|
||||
.removeClass("o_hidden");
|
||||
this.$buttons
|
||||
.find('.o_mail_chat_button_unselect_all')
|
||||
.toggle((channel.isModerated && channel.isModerator) || channel.id === "channel_moderation")
|
||||
.removeClass("o_hidden");
|
||||
this.$buttons.find('.o_mail_chat_button_moderate_all').hide();
|
||||
this.$('.o_mail_chat_channel_item')
|
||||
.removeClass('o_active')
|
||||
.filter('[data-channel-id=' + channel.id + ']')
|
||||
.removeClass('o_unread_message')
|
||||
.addClass('o_active');
|
||||
},
|
||||
/**
|
||||
* Update the moderation buttons.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_updateModerationButtons: function () {
|
||||
this._updateSelectUnselectAllButtons();
|
||||
this._updateModerationDecisionButton();
|
||||
},
|
||||
/**
|
||||
* Display/hide the "moderate all" button based on whether
|
||||
* some moderation checkboxes are checked or not.
|
||||
* If some checkboxes are checked, display this button,
|
||||
* otherwise hide it.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_updateModerationDecisionButton: function () {
|
||||
if (this.thread.$('.moderation_checkbox:checked').length) {
|
||||
this.$buttons.find('.o_mail_chat_button_moderate_all').show();
|
||||
} else {
|
||||
this.$buttons.find('.o_mail_chat_button_moderate_all').hide();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_updateSelectUnselectAllButtons: function () {
|
||||
var buttonSelect = this.$buttons.find('.o_mail_chat_button_select_all');
|
||||
var buttonUnselect = this.$buttons.find('.o_mail_chat_button_unselect_all');
|
||||
var numCheckboxes = this.thread.$('.moderation_checkbox').length;
|
||||
var numCheckboxesChecked = this.thread.$('.moderation_checkbox:checked').length;
|
||||
if (numCheckboxes) {
|
||||
if (numCheckboxesChecked === numCheckboxes) {
|
||||
buttonSelect.toggleClass('disabled', true);
|
||||
buttonUnselect.toggleClass('disabled', false);
|
||||
} else if (numCheckboxesChecked === 0) {
|
||||
buttonSelect.toggleClass('disabled', false);
|
||||
buttonUnselect.toggleClass('disabled', true);
|
||||
} else {
|
||||
buttonSelect.toggleClass('disabled', false);
|
||||
buttonUnselect.toggleClass('disabled', false);
|
||||
}
|
||||
} else {
|
||||
buttonSelect.toggleClass('disabled', true);
|
||||
buttonUnselect.toggleClass('disabled', true);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
@@ -778,6 +1016,12 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
this._updateChannels();
|
||||
delete this.channelsScrolltop[channelID];
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onCloseNotificationBar: function () {
|
||||
this.$(".o_mail_annoying_notification_bar").slideUp();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -788,6 +1032,22 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
composer.mention_set_enabled_commands(commands);
|
||||
composer.mention_set_prefetched_partners(partners);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onInviteButtonClicked: function () {
|
||||
var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name);
|
||||
new PartnerInviteDialog(this, title, this.channel.id).open();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyEvent} event
|
||||
*/
|
||||
_onKeydown: function (event) {
|
||||
if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) {
|
||||
this._unselectMessage();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -803,6 +1063,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
var self = this;
|
||||
var currentChannelID = this.channel.id;
|
||||
if ((currentChannelID === "channel_starred" && !message.is_starred) ||
|
||||
(currentChannelID === "channel_moderation" && !message.needsModeration) ||
|
||||
(currentChannelID === "channel_inbox" && !message.is_needaction)) {
|
||||
this.call('chat_manager', 'getMessages', {
|
||||
channelID: this.channel.id,
|
||||
@@ -814,10 +1075,34 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
self._updateButtonStatus(messages.length === 0, type);
|
||||
});
|
||||
});
|
||||
} else if (_.contains(message.channel_ids, currentChannelID)) {
|
||||
} else if (_.contains(message.channel_ids, currentChannelID) || (message.res_id === currentChannelID)) {
|
||||
this._fetchAndRenderThread();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onModerateAllClicked: function (ev) {
|
||||
var decision = $(ev.target).data('decision');
|
||||
var messageIDs = this.thread.$('.moderation_checkbox:checked')
|
||||
.map(function () {
|
||||
return $(this).data('message-id');
|
||||
})
|
||||
.get();
|
||||
this._handleModerationDecision(messageIDs, decision);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {integer} ev.data.messageID ID of the moderated message
|
||||
* @param {string} ev.data.decision can be 'reject', 'discard', 'ban', 'accept', 'allow'.
|
||||
*/
|
||||
_onMessageModeration: function (ev) {
|
||||
var messageIDs = [ev.data.messageID];
|
||||
var decision = ev.data.decision;
|
||||
this._handleModerationDecision(messageIDs, decision);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} channel
|
||||
@@ -872,7 +1157,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
// todo: display notification
|
||||
// todo: display notifications
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -889,28 +1174,6 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
on_reverse_breadcrumb: this.on_reverse_breadcrumb,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onCloseNotificationBar: function () {
|
||||
this.$(".o_mail_annoying_notification_bar").slideUp();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onInviteButtonClicked: function () {
|
||||
var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name);
|
||||
new PartnerInviteDialog(this, title, this.channel.id).open();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyEvent} event
|
||||
*/
|
||||
_onKeydown: function (event) {
|
||||
if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) {
|
||||
this._unselectMessage();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
@@ -950,6 +1213,17 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
// this will be done as soon as the default channel is set
|
||||
this._fetchAndRenderThread();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onSelectAllClicked: function (ev) {
|
||||
var $button = $(ev.target);
|
||||
if (!$button.hasClass('disabled')) {
|
||||
this.thread.toggleModerationCheckboxes(true);
|
||||
this._updateModerationButtons();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -975,12 +1249,33 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
var channel = this.call('chat_manager', 'getChannel', channelID);
|
||||
this.call('chat_manager', 'unsubscribe', channel);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onUnselectAllClicked: function (ev) {
|
||||
var $button = $(ev.target);
|
||||
if (!$button.hasClass('disabled')) {
|
||||
this.thread.toggleModerationCheckboxes(false);
|
||||
this._updateModerationButtons();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onUnstarAllClicked: function () {
|
||||
this.call('chat_manager', 'unstarAll');
|
||||
},
|
||||
/**
|
||||
* Update the moderation buttons.
|
||||
* This is triggered when a moderation checkbox
|
||||
* has its checked property changed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onUpdateModerationButtons: function () {
|
||||
this._updateModerationButtons();
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('mail.chat.instant_messaging', Discuss);
|
||||
|
||||
@@ -125,6 +125,8 @@ var ChatManager = AbstractService.extend({
|
||||
this.emojis = [];
|
||||
this.needactionCounter = 0;
|
||||
this.starredCounter = 0;
|
||||
this.moderationCounter = 0;
|
||||
this.moderatedChannelIDs = [];
|
||||
this.mentionPartnerSuggestions = [];
|
||||
this.cannedResponses = [];
|
||||
this.commands = [];
|
||||
@@ -521,6 +523,14 @@ var ChatManager = AbstractService.extend({
|
||||
}
|
||||
return $.when([]);
|
||||
},
|
||||
/**
|
||||
* Returns the number of messages that the user needs to moderate
|
||||
*
|
||||
* @return {integer}
|
||||
*/
|
||||
getModerationCounter: function () {
|
||||
return this.moderationCounter;
|
||||
},
|
||||
/**
|
||||
* Returns the number of messages received from followed channels
|
||||
* + all messages where the current user is notified.
|
||||
@@ -556,6 +566,12 @@ var ChatManager = AbstractService.extend({
|
||||
isAllHistoryLoaded: function (channel, domain) {
|
||||
return this._getChannelCache(channel, domain).all_history_loaded;
|
||||
},
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isModerator: function () {
|
||||
return this._isModerator;
|
||||
},
|
||||
/**
|
||||
* @return {$.Promise}
|
||||
*/
|
||||
@@ -733,7 +749,7 @@ var ChatManager = AbstractService.extend({
|
||||
subtype: 'mail.mt_comment',
|
||||
command: data.command,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
if ('model' in options && 'res_id' in options) {
|
||||
// post a message in a chatter
|
||||
@@ -1034,6 +1050,15 @@ var ChatManager = AbstractService.extend({
|
||||
}
|
||||
} else if (options.domain && options.domain !== []) {
|
||||
this._addToCache(msg, options.domain);
|
||||
} else if (data.moderation_status === 'accepted') {
|
||||
msg.channel_ids = data.channel_ids;
|
||||
msg.needsModeration = false;
|
||||
if (msg.isModerator) {
|
||||
this.moderationCounter--;
|
||||
this._removeMessageFromChannel("channel_moderation", msg);
|
||||
this.chatBus.trigger('update_moderation_counter');
|
||||
}
|
||||
this.chatBus.trigger('update_message', msg);
|
||||
}
|
||||
return msg;
|
||||
},
|
||||
@@ -1110,9 +1135,16 @@ var ChatManager = AbstractService.extend({
|
||||
var self = this;
|
||||
options = options || {};
|
||||
var domain =
|
||||
(channel.id === "channel_inbox") ? [['needaction', '=', true]] :
|
||||
(channel.id === "channel_starred") ? [['starred', '=', true]] :
|
||||
[['channel_ids', 'in', channel.id]];
|
||||
(channel.id === 'channel_inbox') ? [['needaction', '=', true]] :
|
||||
(channel.id === 'channel_starred') ? [['starred', '=', true]] :
|
||||
(channel.id === 'channel_moderation') ? [['need_moderation', '=', true]] :
|
||||
['|',
|
||||
'&', '&',
|
||||
['model', '=', 'mail.channel'],
|
||||
['res_id', 'in', [channel.id]],
|
||||
['need_moderation', '=', true],
|
||||
['channel_ids', 'in', [channel.id]]
|
||||
];
|
||||
var cache = this._getChannelCache(channel, options.domain);
|
||||
|
||||
if (options.domain) {
|
||||
@@ -1185,6 +1217,18 @@ var ChatManager = AbstractService.extend({
|
||||
});
|
||||
self.needactionCounter = result.needaction_inbox_counter || 0;
|
||||
self.starredCounter = result.starred_counter || 0;
|
||||
self.moderationCounter = result.moderation_counter;
|
||||
self.moderatedChannelIDs = result.moderation_channel_ids;
|
||||
self._isModerator = result.is_moderator;
|
||||
|
||||
//if user is moderator then add moderation channel
|
||||
if (self._isModerator) {
|
||||
self._addChannel({
|
||||
id: "channel_moderation",
|
||||
name: _lt("Moderate Messages"),
|
||||
type: "static"
|
||||
});
|
||||
}
|
||||
self.commands = _.map(result.commands, function (command) {
|
||||
return _.extend({ id: command.name }, command);
|
||||
});
|
||||
@@ -1233,17 +1277,19 @@ var ChatManager = AbstractService.extend({
|
||||
* @param {Object[]} [data.direct_partner]
|
||||
* @param {boolean} [data.group_based_subscription]
|
||||
* @param {integer|string} data.id
|
||||
* @param {boolean} data.is_moderator
|
||||
* @param {boolean} data.is_minimized
|
||||
* @param {string} [data.last_message_date]
|
||||
* @param {boolean} data.mass_mailing
|
||||
* @param {integer} [data.message_needaction_counter]
|
||||
* @param {integer} [data.message_unread_counter]
|
||||
* @param {boolean} data.moderation whether this channel is moderated or not
|
||||
* @param {string} data.name
|
||||
* @param {string} [data.public]
|
||||
* @param {integer} data.seen_message_id
|
||||
* @param {string} [data.state]
|
||||
* @param {string} [data.type]
|
||||
* @param {string} channel.uuid
|
||||
* @param {string} data.uuid
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.autoswitch]
|
||||
* @param {boolean} options.displayNeedactions
|
||||
@@ -1264,6 +1310,8 @@ var ChatManager = AbstractService.extend({
|
||||
hidden: options.hidden,
|
||||
display_needactions: options.displayNeedactions,
|
||||
mass_mailing: data.mass_mailing,
|
||||
isModerated: data.moderation,
|
||||
isModerator: data.is_moderator,
|
||||
group_based_subscription: data.group_based_subscription,
|
||||
needaction_counter: data.message_needaction_counter || 0,
|
||||
unread_counter: 0,
|
||||
@@ -1314,6 +1362,7 @@ var ChatManager = AbstractService.extend({
|
||||
* @param {boolean} data.is_note
|
||||
* @param {boolean} data.is_notification
|
||||
* @param {string} data.message_type
|
||||
* @param {string} [data.moderation_status]
|
||||
* @param {string} [data.model]
|
||||
* @param {boolean} data.module_icon src url of the module icon
|
||||
* @param {string} data.record_name
|
||||
@@ -1333,6 +1382,9 @@ var ChatManager = AbstractService.extend({
|
||||
message_type: data.message_type,
|
||||
subtype_description: data.subtype_description,
|
||||
is_author: data.author_id && data.author_id[0] === session.partner_id,
|
||||
isModerator: data.model === 'mail.channel' &&
|
||||
_.contains(this.moderatedChannelIDs, data.res_id) &&
|
||||
data.moderation_status === 'pending_moderation',
|
||||
is_note: data.is_note,
|
||||
is_discussion: data.is_discussion,
|
||||
is_notification: data.is_notification,
|
||||
@@ -1384,6 +1436,7 @@ var ChatManager = AbstractService.extend({
|
||||
Object.defineProperties(msg, {
|
||||
is_starred: propertyDescr("channel_starred"),
|
||||
is_needaction: propertyDescr("channel_inbox"),
|
||||
needsModerationByUser: propertyDescr("channel_moderation"),
|
||||
});
|
||||
|
||||
if (_.contains(data.needaction_partner_ids, session.partner_id)) {
|
||||
@@ -1392,8 +1445,17 @@ var ChatManager = AbstractService.extend({
|
||||
if (_.contains(data.starred_partner_ids, session.partner_id)) {
|
||||
msg.is_starred = true;
|
||||
}
|
||||
if (data.moderation_status === 'pending_moderation') {
|
||||
msg.needsModeration = true;
|
||||
msg.needsModerationByUser = msg.isModerator;
|
||||
// the message is not linked to the moderated channel on the
|
||||
// server, therefore this message has not this channel in
|
||||
// channel_ids. Here, just to show this message in the channel
|
||||
//visually, it links this message to the channel
|
||||
msg.channel_ids.push(msg.res_id);
|
||||
}
|
||||
if (msg.model === 'mail.channel') {
|
||||
var realChannels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred');
|
||||
var realChannels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred', 'channel_moderation');
|
||||
var origin = realChannels.length === 1 ? realChannels[0] : undefined;
|
||||
var channel = origin && this.getChannel(origin);
|
||||
if (channel) {
|
||||
@@ -1467,6 +1529,13 @@ var ChatManager = AbstractService.extend({
|
||||
_manageActivityUpdateNotification: function (data) {
|
||||
this.chatBus.trigger('activity_updated', data);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} data
|
||||
*/
|
||||
_manageAuthorNotification: function (data) {
|
||||
this._addMessage(data.message);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} message
|
||||
@@ -1545,6 +1614,27 @@ var ChatManager = AbstractService.extend({
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Object[]} [data.message_ids]
|
||||
*/
|
||||
_manageDeletionNotification: function (data) {
|
||||
var self = this;
|
||||
_.each(data.message_ids, function (msgID) {
|
||||
var message = _.findWhere(self.messages, { id: msgID });
|
||||
if (message) {
|
||||
if (message.isModerator) {
|
||||
self._removeMessageFromChannel("channel_moderation", message);
|
||||
self.moderationCounter--;
|
||||
}
|
||||
message.needsModeration = false;
|
||||
self._removeMessageFromChannel(message.res_id, message);
|
||||
self.chatBus.trigger('update_message', message);
|
||||
}
|
||||
});
|
||||
this.chatBus.trigger('update_moderation_counter');
|
||||
},
|
||||
/**
|
||||
* Updates channel_inbox when a message has marked as read.
|
||||
*
|
||||
@@ -1579,6 +1669,16 @@ var ChatManager = AbstractService.extend({
|
||||
this.needactionCounter = Math.max(this.needactionCounter - data.message_ids.length, 0);
|
||||
this.chatBus.trigger('update_needaction', this.needactionCounter);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} data notification data
|
||||
* @param {Object} data.message
|
||||
*/
|
||||
_manageModeratorNotification: function (data) {
|
||||
this.moderationCounter++;
|
||||
this._addMessage(data.message);
|
||||
this.chatBus.trigger('update_moderation_counter');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} message
|
||||
@@ -1626,6 +1726,12 @@ var ChatManager = AbstractService.extend({
|
||||
this._manageToggleStarNotification(data);
|
||||
} else if (data.type === 'mark_as_read') {
|
||||
this._manageMarkAsReadNotification(data);
|
||||
} else if (data.type === 'moderator') {
|
||||
this._manageModeratorNotification(data);
|
||||
} else if (data.type === 'author') {
|
||||
this._manageAuthorNotification(data);
|
||||
} else if (data.type === 'deletion') {
|
||||
this._manageDeletionNotification(data);
|
||||
} else if (data.info === 'channel_seen') {
|
||||
this._manageChannelSeenNotification(data);
|
||||
} else if (data.info === 'transient_message') {
|
||||
|
||||
@@ -38,6 +38,8 @@ var Thread = Widget.extend({
|
||||
var message_id = $(event.currentTarget).data('message-id');
|
||||
this.trigger("mark_as_read", message_id);
|
||||
},
|
||||
"click .o_thread_message_moderation": "_onClickMessageModeration",
|
||||
"change .moderation_checkbox": "_onChangeModerationCheckbox",
|
||||
"click .o_thread_message_star": function (event) {
|
||||
var message_id = $(event.currentTarget).data('message-id');
|
||||
this.trigger("toggle_star_status", message_id);
|
||||
@@ -141,7 +143,6 @@ var Thread = Widget.extend({
|
||||
}
|
||||
prev_msg = msg;
|
||||
});
|
||||
|
||||
this.$el.html(QWeb.render('mail.ChatThread', {
|
||||
messages: msgs,
|
||||
options: options,
|
||||
@@ -313,6 +314,15 @@ var Thread = Widget.extend({
|
||||
destroy: function () {
|
||||
clearInterval(this.update_timestamps_interval);
|
||||
},
|
||||
/**
|
||||
* Toggle all the moderation checkboxes in the thread
|
||||
*
|
||||
* @param {boolean} checked if true, check the boxes,
|
||||
* otherwise uncheck them.
|
||||
*/
|
||||
toggleModerationCheckboxes: function (checked) {
|
||||
this.$('.moderation_checkbox').prop('checked', checked);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -387,6 +397,23 @@ var Thread = Widget.extend({
|
||||
attachmentViewer.appendTo($('body'));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onChangeModerationCheckbox: function (ev) {
|
||||
this.trigger_up('update_moderation_buttons');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickMessageModeration: function (ev) {
|
||||
var $button = $(ev.currentTarget);
|
||||
var messageID = $button.data('message-id');
|
||||
var decision = $button.data('decision');
|
||||
this.trigger_up('message_moderation', { messageID: messageID, decision: decision });
|
||||
},
|
||||
});
|
||||
|
||||
Thread.ORDER = ORDER;
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
<t t-set="counter" t-value="starred_counter"/>
|
||||
<t t-call="mail.chat.SidebarNeedaction"/>
|
||||
</div>
|
||||
<div t-if="isModerator" t-attf-class="o_mail_chat_title_main o_mail_chat_channel_item #{(active_channel_id == 'channel_moderation') ? 'o_active': ''}"
|
||||
data-channel-id="channel_moderation">
|
||||
<span class="o_channel_name"> <i class="fa fa-envelope mr8"/>Moderation Queue</span>
|
||||
<t t-set="counter" t-value="moderationCounter"/>
|
||||
<t t-call="mail.chat.SidebarNeedaction"/>
|
||||
</div>
|
||||
<hr class="mb8"/>
|
||||
|
||||
<t t-set="channel_type" t-value="'public'"/>
|
||||
@@ -137,6 +143,11 @@
|
||||
<button type="button" class="btn btn-default btn-sm o_mail_chat_button_unstar_all" title="Unstar all messages">Unstar all</button>
|
||||
<button type="button" class="btn btn-default btn-sm o_mail_chat_button_dm visible-xs" title="New Message">New Message</button>
|
||||
<button t-if="!disable_add_channel" type="button" class="btn btn-default btn-sm o_mail_chat_button_public o_mail_chat_button_private visible-xs" title="New Channel">New Channel</button>
|
||||
<button type="button" class="btn btn-default btn-sm o_hidden o_mail_chat_button_select_all" title="Select all messages to moderate">Select All</button>
|
||||
<button type="button" class="btn btn-default btn-sm o_hidden o_mail_chat_button_unselect_all" title="Unselect all messages to moderate">Unselect All</button>
|
||||
<button type="button" data-decision="accept" class="btn btn-default btn-sm o_mail_chat_button_moderate_all" title="Accept selected messages">Accept</button>
|
||||
<button type="button" data-decision="reject" class="btn btn-default btn-sm o_mail_chat_button_moderate_all" title="Reject selected messages">Reject</button>
|
||||
<button type="button" data-decision="discard" class="btn btn-default btn-sm o_mail_chat_button_moderate_all" title="Discard selected messages">Discard</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -145,6 +156,10 @@
|
||||
<input type="text" class="o_input o_mail_chat_partner_invite_input" id="mail_search_partners"/>
|
||||
</div>
|
||||
|
||||
<div t-name="mail.ModeratorRejectMessageDialog">
|
||||
<input class="form-control" type="text" id="message_title" placeholder="Subject" value="Message Rejected"/>
|
||||
<textarea class="form-control mt16" id="reject_message" placeholder="Mail Body">Your message was rejected by moderator.</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Mobile templates -->
|
||||
<t t-name="mail.discuss_mobile">
|
||||
|
||||
@@ -103,6 +103,10 @@
|
||||
<div class="o_thread_title">No starred message</div>
|
||||
<div>You can mark any message as 'starred', and it shows up in this channel.</div>
|
||||
</t>
|
||||
<t t-if="options.channel_id==='channel_moderation'">
|
||||
<div class="o_thread_title">You have no message to moderate</div>
|
||||
<div>Pending moderation messages appear here.</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<t t-name="mail.ChatThread.Content">
|
||||
@@ -144,6 +148,7 @@
|
||||
</div>
|
||||
<div class="o_thread_message_core">
|
||||
<p t-if="message.display_author" class="o_mail_info text-muted">
|
||||
<input t-if="message.needsModeration and message.isModerator" type="checkbox" class="moderation_checkbox" t-att-data-message-id="message.id"/>
|
||||
<t t-if="message.is_note">
|
||||
Note by
|
||||
</t>
|
||||
@@ -185,7 +190,7 @@
|
||||
</span>
|
||||
</span>
|
||||
<span t-attf-class="o_thread_icons">
|
||||
<i t-if="options.display_stars && !message.is_system_notification"
|
||||
<i t-if="options.display_stars and !message.is_system_notification and !message.needsModeration"
|
||||
t-att-class="'fa fa-lg o_thread_icon o_thread_message_star ' + (message.is_starred ? 'fa-star' : 'fa-star-o')"
|
||||
t-att-data-message-id="message.id" title="Mark as Todo"/>
|
||||
<i t-if="message.record_name && message.model != 'mail.channel' && options.display_reply_icon"
|
||||
@@ -194,6 +199,15 @@
|
||||
<i t-if="message.is_needaction && options.display_needactions"
|
||||
class="fa fa-check o_thread_icon o_thread_message_needaction"
|
||||
t-att-data-message-id="message.id" title="Mark as Read"/>
|
||||
|
||||
<t t-if="message.needsModeration and message.model === 'mail.channel'">
|
||||
<t t-if="message.isModerator">
|
||||
<t t-call="mail.MessageModerationDecision"/>
|
||||
</t>
|
||||
<t t-elif="message.is_author">
|
||||
<t t-call="mail.MessageModerationFeedback"/>
|
||||
</t>
|
||||
</t>
|
||||
</span>
|
||||
</p>
|
||||
<div class="o_thread_message_content">
|
||||
@@ -227,6 +241,26 @@
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.MessageModerationDecision">
|
||||
<i class="o_thread_icon ml4 o_thread_message_moderation text-success"
|
||||
t-att-data-message-id="message.id" data-decision="accept" title="Accept"><b>Accept |</b></i>
|
||||
<i class="o_thread_icon ml4 o_thread_message_moderation text-danger"
|
||||
t-att-data-message-id="message.id" data-decision="reject" title="Remove message with explanation"><b>Reject |</b></i>
|
||||
<i class="o_thread_icon ml4 o_thread_message_moderation text-danger"
|
||||
t-att-data-message-id="message.id" data-decision="discard" title="Remove message without explanation"><b>Discard |</b></i>
|
||||
<i class="o_thread_icon ml4 o_thread_message_moderation text-success"
|
||||
t-att-data-message-id="message.id" data-decision="allow" title="Add this email address to white list of people"><b>Always Allow |</b></i>
|
||||
<i class="o_thread_icon ml4 o_thread_message_moderation text-danger"
|
||||
t-att-data-message-id="message.id" data-decision="ban" title="Ban this email address"><b>Ban</b></i>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.MessageModerationFeedback">
|
||||
<i class="text-danger" title="Your message is pending moderation"><b>Pending moderation</b></i>
|
||||
</t>
|
||||
|
||||
|
||||
|
||||
|
||||
<t t-name="mail.MessagesSeparator">
|
||||
<div class="o_thread_new_messages_separator">
|
||||
<span class="o_thread_separator_label">New messages</span>
|
||||
|
||||
@@ -0,0 +1,822 @@
|
||||
odoo.define('mail.discuss_moderation_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ChatManager = require('mail.ChatManager');
|
||||
var mailTestUtils = require('mail.testUtils');
|
||||
|
||||
var Bus = require('web.Bus');
|
||||
|
||||
var createBusService = mailTestUtils.createBusService;
|
||||
var createDiscuss = mailTestUtils.createDiscuss;
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
|
||||
QUnit.module('Discuss moderation', {
|
||||
beforeEach: function () {
|
||||
// patch _.debounce and _.throttle to be fast and synchronous
|
||||
this.underscoreDebounce = _.debounce;
|
||||
this.underscoreThrottle = _.throttle;
|
||||
_.debounce = _.identity;
|
||||
_.throttle = _.identity;
|
||||
|
||||
this.data = {
|
||||
'mail.message': {
|
||||
fields: {
|
||||
body: {
|
||||
string: "Contents",
|
||||
type: 'html',
|
||||
},
|
||||
author_id: {
|
||||
string: "Author",
|
||||
relation: 'res.partner',
|
||||
},
|
||||
channel_ids: {
|
||||
string: "Channels",
|
||||
type: 'many2many',
|
||||
relation: 'mail.channel',
|
||||
},
|
||||
starred: {
|
||||
string: "Starred",
|
||||
type: 'boolean',
|
||||
},
|
||||
needaction: {
|
||||
string: "Need Action",
|
||||
type: 'boolean',
|
||||
},
|
||||
starred_partner_ids: {
|
||||
string: "partner ids",
|
||||
type: 'integer',
|
||||
},
|
||||
model: {
|
||||
string: "Related Document model",
|
||||
type: 'char',
|
||||
},
|
||||
res_id: {
|
||||
string: "Related Document ID",
|
||||
type: 'integer',
|
||||
},
|
||||
need_moderation: {
|
||||
string: "Need moderation",
|
||||
type: 'boolean',
|
||||
},
|
||||
moderation_status: {
|
||||
string: "Moderation Status",
|
||||
type: 'integer',
|
||||
selection: [
|
||||
['pending_moderation', 'Pending Moderation'],
|
||||
['accepted', 'Accepted'],
|
||||
['rejected', 'Rejected']
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
this.services = [ChatManager, createBusService()];
|
||||
},
|
||||
afterEach: function () {
|
||||
// unpatch _.debounce and _.throttle
|
||||
_.debounce = this.underscoreDebounce;
|
||||
_.throttle = this.underscoreThrottle;
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('moderator: display moderation box', function (assert) {
|
||||
assert.expect(1);
|
||||
var done = assert.async();
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
moderation: true,
|
||||
}],
|
||||
},
|
||||
is_moderator: true,
|
||||
};
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
})
|
||||
.then(function (discuss) {
|
||||
var moderationBoxSelector = '.o_mail_chat_channel_item' +
|
||||
'[data-channel-id="channel_moderation"]';
|
||||
assert.strictEqual(discuss.$(moderationBoxSelector).length, 1,
|
||||
"there should be a moderation mailbox");
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('moderator: moderated channel with pending moderation message', function (assert) {
|
||||
assert.expect(33);
|
||||
var done = assert.async();
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
moderation: true,
|
||||
}],
|
||||
},
|
||||
is_moderator: true,
|
||||
moderation_counter: 1,
|
||||
moderation_channel_ids: [1],
|
||||
};
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [2, "Someone"],
|
||||
body: "<p>test</p>",
|
||||
id: 100,
|
||||
model: 'mail.channel',
|
||||
moderation_status: 'pending_moderation',
|
||||
need_moderation: true,
|
||||
res_id: 1,
|
||||
}];
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
})
|
||||
.then(function (discuss) {
|
||||
var $moderationBox = discuss.$(
|
||||
'.o_mail_chat_channel_item' +
|
||||
'[data-channel-id="channel_moderation"]');
|
||||
var $mailboxCounter = $moderationBox.find('.o_mail_sidebar_needaction.badge');
|
||||
assert.strictEqual($mailboxCounter.length, 1,
|
||||
"there should be a counter next to the moderation mailbox in the sidebar");
|
||||
assert.strictEqual($mailboxCounter.text().trim(), "1",
|
||||
"the mailbox counter of the moderation mailbox should display '1'");
|
||||
|
||||
// 1. go to moderation mailbox
|
||||
$moderationBox.click();
|
||||
var $message = discuss.$('.o_thread_message');
|
||||
// check message
|
||||
assert.strictEqual($message.length, 1,
|
||||
"there should be one message in the moderation box");
|
||||
assert.strictEqual($message.data('message-id'), 100,
|
||||
"this message pending moderation should have correct ID");
|
||||
assert.strictEqual($message.find('a[data-oe-id="1"]').text(), "#general",
|
||||
"the message pending moderation should have correct origin as its linked document");
|
||||
assert.strictEqual($message.find('.moderation_checkbox').length, 1,
|
||||
"there should be a moderation checkbox next to the message");
|
||||
assert.notOk($message.find('.moderation_checkbox').prop('checked'), false,
|
||||
"the moderation checkbox should be unchecked by default");
|
||||
// check moderation actions next to message
|
||||
assert.strictEqual(discuss.$('.o_thread_message_moderation').length, 5,
|
||||
"there should be 5 contextual moderation decisions next to the message");
|
||||
assert.strictEqual(discuss.$('.o_thread_message_moderation[data-decision="accept"]').length, 1,
|
||||
"there should be a contextual moderation decision to accept the message");
|
||||
assert.strictEqual(discuss.$('.o_thread_message_moderation[data-decision="reject"]').length, 1,
|
||||
"there should be a contextual moderation decision to reject the message");
|
||||
assert.strictEqual(discuss.$('.o_thread_message_moderation[data-decision="discard"]').length, 1,
|
||||
"there should be a contextual moderation decision to discard the message");
|
||||
assert.strictEqual(discuss.$('.o_thread_message_moderation[data-decision="allow"]').length, 1,
|
||||
"there should be a contextual moderation decision to allow the user of the message)");
|
||||
assert.strictEqual(discuss.$('.o_thread_message_moderation[data-decision="ban"]').length, 1,
|
||||
"there should be a contextual moderation decision to ban the user of the message");
|
||||
|
||||
// check select all (enabled) / unselect all (disabled) buttons
|
||||
assert.strictEqual($('.o_mail_chat_button_select_all').length, 1,
|
||||
"there should be a 'Select All' button in the control panel");
|
||||
assert.notOk($('.o_mail_chat_button_select_all').hasClass('disabled'),
|
||||
"the 'Select All' button should not be disabled");
|
||||
assert.strictEqual($('.o_mail_chat_button_unselect_all').length, 1,
|
||||
"there should be a 'Unselect All' button in the control panel");
|
||||
assert.ok($('.o_mail_chat_button_unselect_all').hasClass('disabled'),
|
||||
"the 'Unselect All' button should be disabled");
|
||||
// check moderate all buttons (invisible)
|
||||
var moderateAllSelector = '.o_mail_chat_button_moderate_all';
|
||||
assert.strictEqual($(moderateAllSelector).length, 3,
|
||||
"there should be 3 buttons to moderate selected messages in the control panel");
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="accept"]').length, 1,
|
||||
"there should one moderate button to accept messages pending moderation");
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="accept"]').attr('style'),
|
||||
'display: none;', 'the moderate button "Accept" should be invisible by default');
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="reject"]').length, 1,
|
||||
"there should one moderate button to reject messages pending moderation");
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="reject"]').attr('style'),
|
||||
'display: none;', 'the moderate button "Reject" should be invisible by default');
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="discard"]').length, 1,
|
||||
"there should one moderate button to discard messages pending moderation");
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="discard"]').attr('style'),
|
||||
'display: none;', 'the moderate button "Discard" should be invisible by default');
|
||||
|
||||
// click on message moderation checkbox
|
||||
$message.find('.moderation_checkbox').click();
|
||||
assert.ok($message.find('.moderation_checkbox').prop('checked'),
|
||||
"the moderation checkbox should become checked after click");
|
||||
// check select all (disabled) / unselect all buttons (enabled)
|
||||
assert.ok($('.o_mail_chat_button_select_all').hasClass('disabled'),
|
||||
"the 'Select All' button should be disabled");
|
||||
assert.notOk($('.o_mail_chat_button_unselect_all').hasClass('disabled'),
|
||||
"the 'Unselect All' button should not be disabled");
|
||||
// check moderate all buttons updated (visible)
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="accept"]').attr('style'),
|
||||
'display: inline-block;', 'the moderate button "Accept" should become visible');
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="reject"]').attr('style'),
|
||||
'display: inline-block;', 'the moderate button "Reject" should become visible');
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="discard"]').attr('style'),
|
||||
'display: inline-block;', 'the moderate button "Discard" should become visible');
|
||||
|
||||
// 2. go to channel 'general'
|
||||
discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
|
||||
$message = discuss.$('.o_thread_message');
|
||||
// check correct message
|
||||
assert.strictEqual($message.length, 1,
|
||||
"there should be one message in the general channel");
|
||||
assert.strictEqual($message.data('message-id'), 100,
|
||||
"this message should have correct ID");
|
||||
assert.notOk($message.find('.moderation_checkbox').prop('checked'),
|
||||
"the moderation checkbox should be unchecked by default");
|
||||
|
||||
// don't check moderation actions visibility, since it is similar to moderation box.
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('moderator: accept pending moderation message', function (assert) {
|
||||
assert.expect(12);
|
||||
var done = assert.async();
|
||||
|
||||
var bus = new Bus();
|
||||
var BusService = createBusService(bus);
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
moderation: true,
|
||||
}],
|
||||
},
|
||||
is_moderator: true,
|
||||
moderation_counter: 1,
|
||||
moderation_channel_ids: [1],
|
||||
};
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [2, "Someone"],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [],
|
||||
id: 100,
|
||||
model: 'mail.channel',
|
||||
moderation_status: 'pending_moderation',
|
||||
need_moderation: true,
|
||||
res_id: 1,
|
||||
}];
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: [ChatManager, BusService],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'moderate') {
|
||||
assert.step('moderate');
|
||||
var messageIDs = args.args[0];
|
||||
var decision = args.args[1];
|
||||
assert.strictEqual(messageIDs.length, 1, "should moderate one message");
|
||||
assert.strictEqual(messageIDs[0], 100, "should moderate message with ID 100");
|
||||
assert.strictEqual(decision, 'accept', "should accept the message");
|
||||
|
||||
// simulate notification back (new accepted message in channel)
|
||||
var dbName = undefined; // useless for tests
|
||||
var messageData = {
|
||||
author_id: [2, "Someone"],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [],
|
||||
id: 100,
|
||||
model: 'mail.channel',
|
||||
moderation_status: 'accepted'
|
||||
};
|
||||
var metaData = [dbName, 'mail.channel'];
|
||||
var notification = [metaData, messageData];
|
||||
bus.trigger('notification', [notification]);
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
})
|
||||
.then(function (discuss) {
|
||||
// 1. go to moderation box
|
||||
var $moderationBox = discuss.$(
|
||||
'.o_mail_chat_channel_item' +
|
||||
'[data-channel-id="channel_moderation"]');
|
||||
$moderationBox.click();
|
||||
// check there is a message to moderate
|
||||
var $message = discuss.$('.o_thread_message');
|
||||
assert.strictEqual($message.length, 1,
|
||||
"there should be one message in the moderation box");
|
||||
assert.strictEqual($message.data('message-id'), 100,
|
||||
"this message should have correct ID");
|
||||
assert.strictEqual($message.find('.moderation_checkbox').length, 1,
|
||||
"the message should have a moderation checkbox");
|
||||
// accept the message pending moderation
|
||||
discuss.$('.o_thread_message_moderation[data-decision="accept"]').click();
|
||||
assert.verifySteps(['moderate']);
|
||||
|
||||
// stop the fadeout animation and immediately remove the element
|
||||
discuss.$('.o_thread_message').stop(false, true);
|
||||
assert.strictEqual(discuss.$('.o_thread_message').length, 0,
|
||||
"should now have no message displayed in moderation box");
|
||||
|
||||
// 2. go to channel 'general'
|
||||
discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
|
||||
$message = discuss.$('.o_thread_message');
|
||||
// check message is there and has no moderate checkbox
|
||||
assert.strictEqual($message.length, 1,
|
||||
"there should be one message in the general channel");
|
||||
assert.strictEqual($message.data('message-id'), 100,
|
||||
"this message should have correct ID");
|
||||
assert.strictEqual($message.find('.moderation_checkbox').length, 0,
|
||||
"the message should not have any moderation checkbox");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('moderator: reject pending moderation message (reject with explanation)', function (assert) {
|
||||
assert.expect(21);
|
||||
var done = assert.async();
|
||||
|
||||
var bus = new Bus();
|
||||
var BusService = createBusService(bus);
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
moderation: true,
|
||||
}],
|
||||
},
|
||||
is_moderator: true,
|
||||
moderation_counter: 1,
|
||||
moderation_channel_ids: [1],
|
||||
};
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [2, "Someone"],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [],
|
||||
id: 100,
|
||||
model: 'mail.channel',
|
||||
moderation_status: 'pending_moderation',
|
||||
need_moderation: true,
|
||||
res_id: 1,
|
||||
}];
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: [ChatManager, BusService],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'moderate') {
|
||||
assert.step('moderate');
|
||||
var messageIDs = args.args[0];
|
||||
var decision = args.args[1];
|
||||
var kwargs = args.kwargs;
|
||||
assert.strictEqual(messageIDs.length, 1, "should moderate one message");
|
||||
assert.strictEqual(messageIDs[0], 100, "should moderate message with ID 100");
|
||||
assert.strictEqual(decision, 'reject', "should reject the message");
|
||||
assert.strictEqual(kwargs.title, "Message Rejected",
|
||||
"should have correct reject message title");
|
||||
assert.strictEqual(kwargs.comment, "Your message was rejected by moderator.",
|
||||
"should have correct reject message body / comment");
|
||||
|
||||
// simulate notification back (deletion of rejected message in channel)
|
||||
var dbName = undefined; // useless for tests
|
||||
var notifData = {
|
||||
message_ids: messageIDs,
|
||||
type: "deletion",
|
||||
};
|
||||
var metaData = [dbName, 'res.partner'];
|
||||
var notification = [metaData, notifData];
|
||||
bus.trigger('notification', [notification]);
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
})
|
||||
.then(function (discuss) {
|
||||
// 1. go to moderation box
|
||||
var $moderationBox = discuss.$(
|
||||
'.o_mail_chat_channel_item' +
|
||||
'[data-channel-id="channel_moderation"]');
|
||||
$moderationBox.click();
|
||||
// check there is a message to moderate
|
||||
var $message = discuss.$('.o_thread_message');
|
||||
assert.strictEqual($message.length, 1,
|
||||
"there should be one message in the moderation box");
|
||||
assert.strictEqual($message.data('message-id'), 100,
|
||||
"this message should have correct ID");
|
||||
assert.strictEqual($message.find('.moderation_checkbox').length, 1,
|
||||
"the message should have a moderation checkbox");
|
||||
// reject the message pending moderation
|
||||
discuss.$('.o_thread_message_moderation[data-decision="reject"]').click();
|
||||
|
||||
// check reject dialog prompt
|
||||
assert.strictEqual($('.modal-dialog').length, 1,
|
||||
"a dialog should be prompt to the moderator on click reject");
|
||||
assert.strictEqual($('.modal-title').text(), "Send explanation to author",
|
||||
"dialog should have correct title");
|
||||
var $messageTitle = $('.modal-body input[id="message_title"]');
|
||||
assert.strictEqual($messageTitle.length, 1,
|
||||
"should have a title of message for rejecting the message");
|
||||
assert.strictEqual($messageTitle.attr('placeholder'), "Subject",
|
||||
"message title for reject reason should have correct placeholder");
|
||||
assert.strictEqual($messageTitle.val(), "Message Rejected",
|
||||
"message title for reject reason should have correct default value");
|
||||
var $messageBody = $('.modal-body textarea[id="reject_message"]');
|
||||
assert.strictEqual($messageBody.length, 1,
|
||||
"should have a body of message for rejecting the message");
|
||||
assert.strictEqual($messageBody.attr('placeholder'), "Mail Body",
|
||||
"message body for reject reason should have correct placeholder");
|
||||
assert.strictEqual($messageBody.text(), "Your message was rejected by moderator.",
|
||||
"message body for reject reason should have correct default text content");
|
||||
assert.strictEqual($('.modal-footer button').text(), "Send",
|
||||
"should have a send button on the reject dialog");
|
||||
|
||||
// send mesage
|
||||
$('.modal-footer button').click();
|
||||
assert.verifySteps(['moderate']);
|
||||
|
||||
// stop the fadeout animation and immediately remove the element
|
||||
discuss.$('.o_thread_message').stop(false, true);
|
||||
assert.strictEqual(discuss.$('.o_thread_message').length, 0,
|
||||
"should now have no message displayed in moderation box");
|
||||
|
||||
// 2. go to channel 'general'
|
||||
discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
|
||||
assert.strictEqual(discuss.$('.o_thread_message').length, 0,
|
||||
"should now have no message displayed in moderated channel");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('moderator: discard pending moderation message (reject without explanation)', function (assert) {
|
||||
assert.expect(15);
|
||||
var done = assert.async();
|
||||
|
||||
var bus = new Bus();
|
||||
var BusService = createBusService(bus);
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
moderation: true,
|
||||
}],
|
||||
},
|
||||
is_moderator: true,
|
||||
moderation_counter: 1,
|
||||
moderation_channel_ids: [1],
|
||||
};
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [2, "Someone"],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [],
|
||||
id: 100,
|
||||
model: 'mail.channel',
|
||||
moderation_status: 'pending_moderation',
|
||||
need_moderation: true,
|
||||
res_id: 1,
|
||||
}];
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: [ChatManager, BusService],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'moderate') {
|
||||
assert.step('moderate');
|
||||
var messageIDs = args.args[0];
|
||||
var decision = args.args[1];
|
||||
assert.strictEqual(messageIDs.length, 1, "should moderate one message");
|
||||
assert.strictEqual(messageIDs[0], 100, "should moderate message with ID 100");
|
||||
assert.strictEqual(decision, 'discard', "should discard the message");
|
||||
|
||||
// simulate notification back (deletion of rejected message in channel)
|
||||
var dbName = undefined; // useless for tests
|
||||
var notifData = {
|
||||
message_ids: messageIDs,
|
||||
type: 'deletion',
|
||||
};
|
||||
var metaData = [dbName, 'res.partner'];
|
||||
var notification = [metaData, notifData];
|
||||
bus.trigger('notification', [notification]);
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
})
|
||||
.then(function (discuss) {
|
||||
// 1. go to moderation box
|
||||
var $moderationBox = discuss.$(
|
||||
'.o_mail_chat_channel_item' +
|
||||
'[data-channel-id="channel_moderation"]');
|
||||
$moderationBox.click();
|
||||
// check there is a message to moderate
|
||||
var $message = discuss.$('.o_thread_message');
|
||||
assert.strictEqual($message.length, 1,
|
||||
"there should be one message in the moderation box");
|
||||
assert.strictEqual($message.data('message-id'), 100,
|
||||
"this message should have correct ID");
|
||||
assert.strictEqual($message.find('.moderation_checkbox').length, 1,
|
||||
"the message should have a moderation checkbox");
|
||||
// discard the message pending moderation
|
||||
discuss.$('.o_thread_message_moderation[data-decision="discard"]').click();
|
||||
|
||||
// check discard dialog prompt
|
||||
assert.strictEqual($('.modal-dialog').length, 1,
|
||||
"a dialog should be prompt to the moderator on click discard");
|
||||
assert.strictEqual($('.modal-body').text(),
|
||||
"You are going to discard 1 message. Do you confirm the action?",
|
||||
"should warn the user on discard action");
|
||||
assert.strictEqual($('.modal-footer button').length, 2,
|
||||
"should have two buttons in the footer of the dialog");
|
||||
assert.strictEqual($('.modal-footer button.btn-primary').text(), "Ok",
|
||||
"should have a confirm button in the dialog for discard");
|
||||
assert.strictEqual($('.modal-footer button.btn-default').text(), "Cancel",
|
||||
"should have a cancel button in the dialog for discard");
|
||||
|
||||
// discard mesage
|
||||
$('.modal-footer button.btn-primary').click();
|
||||
assert.verifySteps(['moderate']);
|
||||
|
||||
// stop the fadeout animation and immediately remove the element
|
||||
discuss.$('.o_thread_message').stop(false, true);
|
||||
assert.strictEqual(discuss.$('.o_thread_message').length, 0,
|
||||
"should now have no message displayed in moderation box");
|
||||
|
||||
// 2. go to channel 'general'
|
||||
discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
|
||||
assert.strictEqual(discuss.$('.o_thread_message').length, 0,
|
||||
"should now have no message displayed in moderated channel");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('author: send message in moderated channel', function (assert) {
|
||||
assert.expect(4);
|
||||
var done = assert.async();
|
||||
|
||||
var bus = new Bus();
|
||||
var BusService = createBusService(bus);
|
||||
|
||||
var messagePostDef = $.Deferred();
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
moderation: true,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: [ChatManager, BusService],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'message_post') {
|
||||
var message = {
|
||||
id: 100,
|
||||
author_id: [2, 'Someone'],
|
||||
body: args.kwargs.body,
|
||||
message_type: args.kwargs.message_type,
|
||||
model: 'mail.channel',
|
||||
moderation_status: 'pending_moderation',
|
||||
res_id: 1,
|
||||
};
|
||||
var metaData = [undefined, 'res.partner'];
|
||||
var notifData = {
|
||||
type: 'author',
|
||||
message: message,
|
||||
};
|
||||
var notification = [metaData, notifData];
|
||||
bus.trigger('notification', [notification]);
|
||||
|
||||
messagePostDef.resolve();
|
||||
return $.when(message.id);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
session: {
|
||||
partner_id: 2,
|
||||
},
|
||||
})
|
||||
.then(function (discuss) {
|
||||
|
||||
// go to channel 'general'
|
||||
discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
|
||||
// post a message
|
||||
discuss.$('.o_composer_input textarea').first().val("some text");
|
||||
discuss.$('.o_composer_send button').click();
|
||||
|
||||
messagePostDef
|
||||
.then(function () {
|
||||
|
||||
var $message = discuss.$('.o_thread_message');
|
||||
assert.strictEqual($message.length, 1,
|
||||
"should have a message in the thread");
|
||||
assert.strictEqual($message.data('message-id'), 100,
|
||||
"message should have ID returned from 'message_post'");
|
||||
assert.strictEqual($message.find('.o_thread_author').text().trim(),
|
||||
"Someone", "message should have correct author displayed name");
|
||||
assert.strictEqual(discuss.$('.o_thread_icons i.text-danger').text(),
|
||||
"Pending moderation", "the message should be pending moderation");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('author: sent message accepted in moderated channel', function (assert) {
|
||||
assert.expect(8);
|
||||
var done = assert.async();
|
||||
|
||||
var bus = new Bus();
|
||||
var BusService = createBusService(bus);
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
moderation: true,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [2, "Someone"],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [],
|
||||
id: 100,
|
||||
model: 'mail.channel',
|
||||
moderation_status: 'pending_moderation',
|
||||
need_moderation: true,
|
||||
res_id: 1,
|
||||
}];
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: [ChatManager, BusService],
|
||||
session: {
|
||||
partner_id: 2,
|
||||
},
|
||||
})
|
||||
.then(function (discuss) {
|
||||
|
||||
// go to channel 'general'
|
||||
discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
|
||||
// check message is pending
|
||||
var $message = discuss.$('.o_thread_message');
|
||||
assert.strictEqual($message.length, 1,
|
||||
"should have a message in the thread");
|
||||
assert.strictEqual($message.data('message-id'), 100,
|
||||
"message should have ID returned from 'message_post'");
|
||||
assert.strictEqual($message.find('.o_thread_author').text().trim(),
|
||||
"Someone", "message should have correct author displayed name");
|
||||
assert.strictEqual(discuss.$('.o_thread_icons i.text-danger').text(),
|
||||
"Pending moderation", "the message should be pending moderation");
|
||||
|
||||
// simulate accepted message
|
||||
var dbName = undefined; // useless for tests
|
||||
var messageData = {
|
||||
author_id: [2, "Someone"],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [],
|
||||
id: 100,
|
||||
model: 'mail.channel',
|
||||
moderation_status: 'accepted'
|
||||
};
|
||||
var metaData = [dbName, 'mail.channel'];
|
||||
var notification = [metaData, messageData];
|
||||
bus.trigger('notification', [notification]);
|
||||
|
||||
// // check message is accepted
|
||||
$message = discuss.$('.o_thread_message');
|
||||
assert.strictEqual($message.length, 1,
|
||||
"should still have a message in the thread");
|
||||
assert.strictEqual($message.data('message-id'), 100,
|
||||
"message should still have ID returned from 'message_post'");
|
||||
assert.strictEqual($message.find('.o_thread_author').text().trim(),
|
||||
"Someone", "message should still have correct author displayed name");
|
||||
assert.strictEqual(discuss.$('.o_thread_icons i.text-danger').length, 0,
|
||||
"the message should not be in pending moderation anymore");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('author: sent message rejected in moderated channel', function (assert) {
|
||||
assert.expect(5);
|
||||
var done = assert.async();
|
||||
|
||||
var bus = new Bus();
|
||||
var BusService = createBusService(bus);
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "general",
|
||||
moderation: true,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
this.data['mail.message'].records = [{
|
||||
author_id: [2, "Someone"],
|
||||
body: "<p>test</p>",
|
||||
channel_ids: [],
|
||||
id: 100,
|
||||
model: 'mail.channel',
|
||||
moderation_status: 'pending_moderation',
|
||||
need_moderation: true,
|
||||
res_id: 1,
|
||||
}];
|
||||
|
||||
createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: [ChatManager, BusService],
|
||||
session: {
|
||||
partner_id: 2,
|
||||
},
|
||||
})
|
||||
.then(function (discuss) {
|
||||
|
||||
// go to channel 'general'
|
||||
discuss.$('.o_mail_chat_channel_item[data-channel-id="1"]').click();
|
||||
// check message is pending
|
||||
var $message = discuss.$('.o_thread_message');
|
||||
assert.strictEqual($message.length, 1,
|
||||
"should have a message in the thread");
|
||||
assert.strictEqual($message.data('message-id'), 100,
|
||||
"message should have ID returned from 'message_post'");
|
||||
assert.strictEqual($message.find('.o_thread_author').text().trim(),
|
||||
"Someone", "message should have correct author displayed name");
|
||||
assert.strictEqual(discuss.$('.o_thread_icons i.text-danger').text(),
|
||||
"Pending moderation", "the message should be pending moderation");
|
||||
|
||||
// simulate reject from moderator
|
||||
var dbName = undefined; // useless for tests
|
||||
var notifData = {
|
||||
type: 'deletion',
|
||||
message_ids: [100],
|
||||
};
|
||||
var metaData = [dbName, 'res.partner'];
|
||||
var notification = [metaData, notifData];
|
||||
bus.trigger('notification', [notification]);
|
||||
|
||||
// // check no message
|
||||
assert.strictEqual(discuss.$('.o_thread_message').length, 0,
|
||||
"message should be removed from channel after reject");
|
||||
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -44,13 +44,21 @@ QUnit.module('Discuss client action', {
|
||||
type: 'boolean',
|
||||
},
|
||||
needaction: {
|
||||
string: "Need Action",
|
||||
type: 'boolean',
|
||||
},
|
||||
starred_partner_ids: {
|
||||
string: "partner ids",
|
||||
type: 'integer',
|
||||
}
|
||||
string: "Need Action",
|
||||
type: 'boolean',
|
||||
},
|
||||
starred_partner_ids: {
|
||||
string: "partner ids",
|
||||
type: 'integer',
|
||||
},
|
||||
model: {
|
||||
string: "Related Document model",
|
||||
type: 'char',
|
||||
},
|
||||
res_id: {
|
||||
string: "Related Document ID",
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -83,7 +83,16 @@
|
||||
<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 btn-sm 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="Ban List" name="moderation_count" widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_button_box" name="button_box"/>
|
||||
<field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium', 'size': [90, 90]}"/>
|
||||
<div class="oe_title">
|
||||
@@ -96,6 +105,7 @@
|
||||
</div>
|
||||
<group class="o_label_nowrap">
|
||||
<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)]}">
|
||||
@@ -123,10 +133,19 @@
|
||||
<field name="channel_last_seen_partner_ids" mode="tree" context="{'active_test': False}">
|
||||
<tree string="Members" editable="bottom">
|
||||
<field name="partner_id"/>
|
||||
<field name="partner_email"/>
|
||||
<field name="partner_email" readonly="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="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="0" name="mail_channel_integrations"></page>
|
||||
</notebook>
|
||||
<div class="oe_chatter" groups="base.group_no_one">
|
||||
@@ -136,7 +155,7 @@
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="mail_channel_view_tree" model="ir.ui.view">
|
||||
<field name="name">mail.channel.tree</field>
|
||||
<field name="model">mail.channel</field>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<field name="email_from"/>
|
||||
<field name="author_id"/>
|
||||
<field name="record_name"/>
|
||||
<field name="moderator_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="parent_id"/>
|
||||
@@ -39,6 +40,7 @@
|
||||
<field name="res_id"/>
|
||||
<field name="message_type"/>
|
||||
<field name="subtype_id"/>
|
||||
<field name="moderation_status"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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_type">form</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>
|
||||
|
||||
<!-- Add menu entry in Settings/Email -->
|
||||
<menuitem name="White List / Black List"
|
||||
id="mail_moderation_menu"
|
||||
parent="base.menu_email"
|
||||
action="mail_moderation_action"/>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -53,6 +53,7 @@
|
||||
<script type="text/javascript" src="/mail/static/tests/mail_utils_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/discuss_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/systray_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/discuss_moderation_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/helpers/mock_server.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/helpers/test_utils.js"></script>
|
||||
</xpath>
|
||||
|
||||
@@ -13,4 +13,5 @@ from . import test_invite
|
||||
from . import test_ir_actions
|
||||
from . import test_update_notification
|
||||
from . import test_discuss
|
||||
from . import test_performance
|
||||
from . import test_performance
|
||||
from . import test_res_users
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from json import dumps, loads
|
||||
|
||||
import json
|
||||
|
||||
@@ -7,7 +8,7 @@ from email.utils import formataddr
|
||||
|
||||
from odoo import api
|
||||
from odoo.addons.bus.models.bus import json_dump
|
||||
from odoo.tests import common
|
||||
from odoo.tests import common, tagged
|
||||
|
||||
|
||||
class BaseFunctionalTest(common.SavepointCase):
|
||||
@@ -244,6 +245,7 @@ class MockEmails(common.SingleTransactionCase):
|
||||
super(MockEmails, cls).tearDownClass()
|
||||
|
||||
def _init_mock_build_email(self):
|
||||
self.env['mail.mail'].search([]).unlink()
|
||||
self._mails_args[:] = []
|
||||
self._mails[:] = []
|
||||
|
||||
@@ -260,3 +262,57 @@ class MockEmails(common.SingleTransactionCase):
|
||||
mail = self.format(template, to=to, subject=subject, cc=cc, extra=extra, email_from=email_from, msg_id=msg_id)
|
||||
self.env['mail.thread'].with_context(mail_channel_noautofollow=True).message_process(model, mail)
|
||||
return self.env[target_model].search([(target_field, '=', subject)])
|
||||
|
||||
|
||||
@tagged('moderation')
|
||||
class Moderation(MockEmails, BaseFunctionalTest):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(Moderation, cls).setUpClass()
|
||||
Channel = cls.env['mail.channel']
|
||||
Users = cls.env['res.users'].with_context(cls._quick_create_user_ctx)
|
||||
|
||||
cls.channel_moderation_1 = Channel.create({
|
||||
'name': 'Moderation_1',
|
||||
'email_send': True,
|
||||
'moderation': True
|
||||
})
|
||||
cls.channel_1 = cls.channel_moderation_1
|
||||
cls.channel_moderation_2 = Channel.create({
|
||||
'name': 'Moderation_2',
|
||||
'email_send': True,
|
||||
'moderation': True
|
||||
})
|
||||
cls.channel_2 = cls.channel_moderation_2
|
||||
|
||||
cls.user_employee.write({'moderation_channel_ids': [(6, 0, [cls.channel_1.id])]})
|
||||
|
||||
cls.user_employee_2 = Users.create({
|
||||
'name': 'Roboute',
|
||||
'login': 'roboute',
|
||||
'email': 'roboute@guilliman.com',
|
||||
'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])],
|
||||
'moderation_channel_ids': [(6, 0, [cls.channel_2.id])]
|
||||
})
|
||||
cls.partner_employee_2 = cls.user_employee_2.partner_id
|
||||
|
||||
cls.channel_moderation_1.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': cls.partner_employee.id})]})
|
||||
cls.channel_moderation_2.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': cls.partner_employee_2.id})]})
|
||||
|
||||
def _create_new_message(self, channel_id, status='pending_moderation', author=None, body='', message_type="email"):
|
||||
author = author if author else self.env.user.partner_id
|
||||
message = self.env['mail.message'].create({
|
||||
'model': 'mail.channel',
|
||||
'res_id': channel_id,
|
||||
'message_type': 'email',
|
||||
'body': body,
|
||||
'moderation_status': status,
|
||||
'author_id': author.id,
|
||||
'email_from': formataddr((author.name, author.email)),
|
||||
'subtype_id': self.env['mail.message.subtype'].search([('name', '=', 'Discussions')]).id
|
||||
})
|
||||
return message
|
||||
|
||||
def _clear_bus(self):
|
||||
self.env['bus.bus'].search([]).unlink()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from email.utils import formataddr
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.addons.test_mail.tests import common
|
||||
from odoo.exceptions import AccessError, except_orm
|
||||
from odoo.exceptions import AccessError, except_orm, ValidationError, UserError
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@@ -195,3 +195,103 @@ class TestChannelFeatures(common.BaseFunctionalTest, common.MockEmails):
|
||||
self.assertIn(
|
||||
email['email_to'][0],
|
||||
[formataddr((self.user_employee.name, self.user_employee.email)), formataddr((self.test_partner.name, self.test_partner.email))])
|
||||
|
||||
|
||||
@tagged('moderation')
|
||||
class TestChannelModeration(common.Moderation):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestChannelModeration, cls).setUpClass()
|
||||
|
||||
def test_moderator_consistency(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.channel_1.write({'moderator_ids': [(4, self.user_employee_2.id)]})
|
||||
|
||||
self.channel_1.write({'channel_partner_ids': [(4, self.partner_employee_2.id)]})
|
||||
with self.assertRaises(ValidationError):
|
||||
self.user_employee_2.write({'email': False})
|
||||
self.channel_1.write({'moderator_ids': [(4, self.user_employee_2.id)]})
|
||||
|
||||
def test_channel_moderation_parameters(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.channel_1.write({'email_send': False})
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
self.channel_1.write({'moderator_ids': [(5, 0)]})
|
||||
|
||||
def test_moderation_count(self):
|
||||
self.assertEqual(self.channel_1.moderation_count, 0)
|
||||
self.channel_1.write({'moderation_ids': [
|
||||
(0, 0, {'email': 'test0@example.com', 'status': 'allow'}),
|
||||
(0, 0, {'email': 'test1@example.com', 'status': 'ban'})
|
||||
]})
|
||||
self.assertEqual(self.channel_1.moderation_count, 2)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_channel', 'odoo.models.unlink')
|
||||
def test_send_guidelines(self):
|
||||
self.channel_1.write({'channel_partner_ids': [(4, self.partner_employee_2.id), (4, self.partner_admin.id)]})
|
||||
self.channel_1._update_moderation_email([self.partner_admin.email], 'ban')
|
||||
self._init_mock_build_email()
|
||||
self.channel_1.sudo(self.user_employee).send_guidelines()
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
self.assertEmails(False, self.partner_employee | self.partner_employee_2, email_from=self.env.user.company_id.catchall or self.env.user.company_id.email)
|
||||
|
||||
def test_send_guidelines_crash(self):
|
||||
with self.assertRaises(UserError):
|
||||
self.channel_1.sudo(self.user_employee_2).send_guidelines()
|
||||
|
||||
def test_update_moderation_email(self):
|
||||
self.channel_1.write({'moderation_ids': [
|
||||
(0, 0, {'email': 'test0@example.com', 'status': 'allow'}),
|
||||
(0, 0, {'email': 'test1@example.com', 'status': 'ban'})
|
||||
]})
|
||||
self.channel_1._update_moderation_email(['test0@example.com', 'test3@example.com'], 'ban')
|
||||
self.assertEqual(len(self.channel_1.moderation_ids), 3)
|
||||
self.assertTrue(all(status == 'ban' for status in self.channel_1.moderation_ids.mapped('status')))
|
||||
|
||||
def test_moderation_reset(self):
|
||||
self._create_new_message(self.channel_1.id)
|
||||
self._create_new_message(self.channel_1.id, status='accepted')
|
||||
self._create_new_message(self.channel_2.id)
|
||||
self.channel_1.write({'moderation': False})
|
||||
self.assertEqual(self.env['mail.message'].search_count([
|
||||
('moderation_status', '=', 'pending_moderation'),
|
||||
('model', '=', 'mail.channel'),
|
||||
('res_id', '=', self.channel_1.id)
|
||||
]), 0)
|
||||
self.assertEqual(self.env['mail.message'].search_count([
|
||||
('moderation_status', '=', 'pending_moderation'),
|
||||
('model', '=', 'mail.channel'),
|
||||
('res_id', '=', self.channel_2.id)
|
||||
]), 1)
|
||||
self.channel_2.write({'moderation': False})
|
||||
self.assertEqual(self.env['mail.message'].search_count([
|
||||
('moderation_status', '=', 'pending_moderation'),
|
||||
('model', '=', 'mail.channel'),
|
||||
('res_id', '=', self.channel_2.id)
|
||||
]), 0)
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_message_post(self):
|
||||
email1 = 'test0@example.com'
|
||||
email2 = 'test1@example.com'
|
||||
|
||||
self.channel_1._update_moderation_email([email1], 'ban')
|
||||
self.channel_1._update_moderation_email([email2], 'allow')
|
||||
|
||||
msg_admin = self.channel_1.message_post(message_type='email', subtype='mt_comment', author_id=self.partner_admin.id)
|
||||
msg_moderator = self.channel_1.message_post(message_type='comment', subtype='mt_comment', author_id=self.partner_employee.id)
|
||||
msg_email1 = self.channel_1.message_post(message_type='comment', subtype='mt_comment', email_from=formataddr(("MyName", email1)))
|
||||
msg_email2 = self.channel_1.message_post(message_type='email', subtype='mt_comment', email_from=email2)
|
||||
msg_notif = self.channel_1.message_post()
|
||||
|
||||
messages = self.env['mail.message'].search([('model', '=', 'mail.channel'), ('res_id', '=', self.channel_1.id)])
|
||||
pending_messages = messages.filtered(lambda m: m.moderation_status == 'pending_moderation')
|
||||
accepted_messages = messages.filtered(lambda m: m.moderation_status == 'accepted')
|
||||
|
||||
self.assertFalse(msg_email1)
|
||||
self.assertEqual(msg_admin, pending_messages)
|
||||
self.assertEqual(accepted_messages, msg_moderator | msg_email2 | msg_notif)
|
||||
self.assertFalse(msg_admin.channel_ids)
|
||||
self.assertEqual(msg_email2.channel_ids, self.channel_1)
|
||||
|
||||
@@ -5,6 +5,7 @@ import base64
|
||||
from odoo.addons.test_mail.tests import common
|
||||
from odoo.exceptions import AccessError, except_orm
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
|
||||
@@ -213,6 +214,10 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
|
||||
messages = self.env['mail.message'].sudo(self.user_portal).search([('subject', 'like', '_ZTest')])
|
||||
self.assertEqual(messages, msg4 | msg5)
|
||||
|
||||
# --------------------------------------------------
|
||||
# READ
|
||||
# --------------------------------------------------
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
|
||||
def test_mail_message_access_read_crash(self):
|
||||
# TODO: Change the except_orm to Warning ( Because here it's call check_access_rule
|
||||
@@ -251,6 +256,16 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
|
||||
# Test: Bert reads the message, ok because linked to a doc he is allowed to read
|
||||
self.message.sudo(self.user_employee).read()
|
||||
|
||||
def test_mail_message_access_read_crash_moderation(self):
|
||||
# with self.assertRaises(AccessError):
|
||||
self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'})
|
||||
# Test: Bert reads the message, ok because linked to a doc he is allowed to read
|
||||
self.message.sudo(self.user_employee).read()
|
||||
|
||||
# --------------------------------------------------
|
||||
# CREATE
|
||||
# --------------------------------------------------
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_model')
|
||||
def test_mail_message_access_create_crash_public(self):
|
||||
# Do: Bert creates a message on Pigs -> ko, no creation rights
|
||||
@@ -284,6 +299,25 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
|
||||
self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
|
||||
self.env['mail.message'].sudo(self.user_employee).create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test', 'parent_id': self.message.id})
|
||||
|
||||
# --------------------------------------------------
|
||||
# WRITE
|
||||
# --------------------------------------------------
|
||||
|
||||
def test_mail_message_access_write_moderation(self):
|
||||
""" Only moderators can modify pending messages """
|
||||
self.group_public.write({
|
||||
'email_send': True,
|
||||
'moderation': True,
|
||||
'channel_partner_ids': [(4, self.partner_employee.id)],
|
||||
'moderator_ids': [(4, self.user_employee.id)],
|
||||
})
|
||||
self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'})
|
||||
self.message.sudo(self.user_employee).write({'moderation_status': 'accepted'})
|
||||
|
||||
def test_mail_message_access_write_crash_moderation(self):
|
||||
self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'})
|
||||
with self.assertRaises(AccessError):
|
||||
self.message.sudo(self.user_employee).write({'moderation_status': 'accepted'})
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_mail')
|
||||
def test_mark_all_as_read(self):
|
||||
@@ -350,4 +384,73 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
|
||||
|
||||
portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
|
||||
na_count = portal_partner.get_needaction_count()
|
||||
self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
|
||||
self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
|
||||
|
||||
|
||||
@tagged('moderation')
|
||||
class TestMessageModeration(common.Moderation):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMessageModeration, cls).setUpClass()
|
||||
|
||||
cls.msg_admin_pending_c1 = cls._create_new_message(cls, cls.channel_1.id, status='pending_moderation', author=cls.partner_admin)
|
||||
cls.msg_admin_pending_c1_2 = cls._create_new_message(cls, cls.channel_1.id, status='pending_moderation', author=cls.partner_admin)
|
||||
cls.msg_emp2_pending_c1 = cls._create_new_message(cls, cls.channel_1.id, status='pending_moderation', author=cls.partner_employee_2)
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_moderate_accept(self):
|
||||
self._clear_bus()
|
||||
# A pending moderation message needs to have field channel_ids empty. Moderators
|
||||
# need to be able to notify a pending moderation message (in a channel they moderate).
|
||||
self.assertFalse(self.msg_admin_pending_c1.channel_ids)
|
||||
self.msg_admin_pending_c1.sudo(self.user_employee)._moderate('accept')
|
||||
self.assertEqual(self.msg_admin_pending_c1.channel_ids, self.channel_1)
|
||||
self.assertEqual(self.msg_admin_pending_c1.moderation_status, 'accepted')
|
||||
self.assertEqual(self.msg_admin_pending_c1_2.moderation_status, 'pending_moderation')
|
||||
self.assertBusNotification([(self.cr.dbname, 'mail.channel', self.channel_1.id)])
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_moderate_allow(self):
|
||||
self._clear_bus()
|
||||
# A pending moderation message needs to have field channel_ids empty. Moderators
|
||||
# need to be able to notify a pending moderation message (in a channel they moderate).
|
||||
self.assertFalse(self.msg_admin_pending_c1.channel_ids)
|
||||
self.assertFalse(self.msg_admin_pending_c1_2.channel_ids)
|
||||
self.msg_admin_pending_c1.sudo(self.user_employee)._moderate('allow')
|
||||
self.assertEqual(self.msg_admin_pending_c1.channel_ids, self.channel_1)
|
||||
self.assertEqual(self.msg_admin_pending_c1_2.channel_ids, self.channel_1)
|
||||
self.assertEqual(self.msg_admin_pending_c1.moderation_status, 'accepted')
|
||||
self.assertEqual(self.msg_admin_pending_c1_2.moderation_status, 'accepted')
|
||||
self.assertBusNotification([
|
||||
(self.cr.dbname, 'mail.channel', self.channel_1.id),
|
||||
(self.cr.dbname, 'mail.channel', self.channel_1.id)])
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_moderate_reject(self):
|
||||
self._init_mock_build_email()
|
||||
(self.msg_admin_pending_c1 | self.msg_emp2_pending_c1).sudo(self.user_employee)._moderate_send_reject_email('Title', 'Message to author')
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
self.assertEmails(self.partner_employee, self.partner_employee_2 | self.partner_admin, subject='Title', body_content='Message to author')
|
||||
|
||||
def test_moderate_discard(self):
|
||||
self._clear_bus()
|
||||
id1, id2 = self.msg_admin_pending_c1.id, self.msg_emp2_pending_c1.id # save ids because unlink will discard them
|
||||
(self.msg_admin_pending_c1 | self.msg_emp2_pending_c1).sudo(self.user_employee)._moderate_discard()
|
||||
|
||||
self.assertBusNotification(
|
||||
[(self.cr.dbname, 'res.partner', self.partner_admin.id),
|
||||
(self.cr.dbname, 'res.partner', self.partner_employee_2.id),
|
||||
(self.cr.dbname, 'res.partner', self.partner_employee.id)],
|
||||
[{'type': 'deletion', 'message_ids': [id1]}, # admin: one message deleted because discarded
|
||||
{'type': 'deletion', 'message_ids': [id2]}, # employee_2: one message delete because discarded
|
||||
{'type': 'deletion', 'message_ids': [id1, id2]}] # employee: two messages deleted because moderation done
|
||||
)
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_notify_moderators(self):
|
||||
# create pending messages in another channel to have two notification to push
|
||||
msg_emp_pending_c2 = self._create_new_message(self.channel_2.id, status='pending_moderation', author=self.partner_employee)
|
||||
|
||||
self.env['mail.message']._notify_moderators()
|
||||
self.assertEmails(False, self.partner_employee | self.partner_employee_2, subject='Message are pending moderation', email_from=self.env.user.company_id.catchall or self.env.user.company_id.email)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.test_mail.tests import common
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('moderation')
|
||||
class TestMessageModeration(common.Moderation):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMessageModeration, cls).setUpClass()
|
||||
|
||||
def test_is_moderator(self):
|
||||
self.assertTrue(self.user_employee.is_moderator)
|
||||
self.assertFalse(self.user_admin.is_moderator)
|
||||
self.assertTrue(self.user_employee_2.is_moderator)
|
||||
|
||||
def test_moderation_counter(self):
|
||||
self._create_new_message(self.channel_1.id, status='pending_moderation', author=self.partner_admin)
|
||||
self._create_new_message(self.channel_1.id, status='accepted', author=self.partner_admin)
|
||||
self._create_new_message(self.channel_1.id, status='accepted', author=self.partner_employee)
|
||||
self._create_new_message(self.channel_1.id, status='pending_moderation', author=self.partner_employee)
|
||||
self._create_new_message(self.channel_1.id, status='accepted', author=self.partner_employee_2)
|
||||
|
||||
self.assertEqual(self.user_employee.moderation_counter, 2)
|
||||
self.assertEqual(self.user_employee_2.moderation_counter, 0)
|
||||
self.assertEqual(self.user_admin.moderation_counter, 0)
|
||||
|
||||
self.channel_1.write({'channel_partner_ids': [(4, self.partner_employee_2.id)], 'moderator_ids': [(4, self.user_employee_2.id)]})
|
||||
self.assertEqual(self.user_employee.moderation_counter, 2)
|
||||
self.assertEqual(self.user_employee_2.moderation_counter, 0)
|
||||
self.assertEqual(self.user_admin.moderation_counter, 0)
|
||||
Reference in New Issue
Block a user