diff options
| author | Mathieu Duckaerts-Antoine <dam@odoo.com> | 2018-05-24 15:27:28 +0200 |
|---|---|---|
| committer | Thibault Delavallée <tde@odoo.com> | 2018-06-06 16:01:33 +0200 |
| commit | 299ebb2cdf63de2e3967da0d2f3e7effd239812b (patch) | |
| tree | f2681c0fc1e283acdfd5942392db8f17ddc7801d /addons/mail/static | |
| parent | eeab18d753db21bca0636836c40df91268549390 (diff) | |
[IMP] mail: add moderation on channels
Purpose of this commit is to allow moderation on incoming messages in
discussion channels. On some channels on which moderation is required
messages should be in a pending moderation stage. Moderators can accept
or refuse messages as well as always allow or ban messages coming from
a given set of emails.
Channels now have an option to be moderated. Moderators can be added on
channels. They have access to a specific UI in Discuss to see and take
action on messages waiting for moderation.
Concerning mail.thread message that are pending moderation are not notified.
It means nobody receives a notification about them. Moderation process calls
the notification once the message is validated.
Various features included in this commit :
* a model is added to store the decision about emails, allow or ban;
* access rights are updated so that only moderators can modify moderation
fields on message;
* specific bus notifications are send to moderated people as well as to
moderators on incoming emails as well as when a decision is taken;
* options are added on channels to send explanations to moderated emails;
* options are added on channels to write and send guidelines explaining
why and how moderation is performed;
* a reminder is send daily to moderators with remaining messages to moderate;
* discuss UI is adapted and a new channel is added below Inbox and Starred
giving access to moderation tools;
* chanenl UI is adapted allowing to moderate directly inside channels;
This commit is linked to task ID 29521. Closes #21921.
Diffstat (limited to 'addons/mail/static')
| -rw-r--r-- | addons/mail/static/src/js/discuss.js | 359 | ||||
| -rw-r--r-- | addons/mail/static/src/js/services/chat_manager.js | 118 | ||||
| -rw-r--r-- | addons/mail/static/src/js/thread.js | 29 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/discuss.xml | 15 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/thread.xml | 36 | ||||
| -rw-r--r-- | addons/mail/static/tests/discuss_moderation_tests.js | 822 | ||||
| -rw-r--r-- | addons/mail/static/tests/discuss_tests.js | 22 |
7 files changed, 1354 insertions, 47 deletions
diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js index 3f37a620c80..def90b01595 100644 --- a/addons/mail/static/src/js/discuss.js +++ b/addons/mail/static/src/js/discuss.js @@ -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', @@ -240,6 +303,49 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { //-------------------------------------------------------------------------- /** + * 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,12 +384,36 @@ 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 * loaded when scrolling to the top, so they can't be loaded if there is no @@ -326,6 +456,33 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { }); }, /** + * 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. * @@ -394,6 +551,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 */ _renderButtons: function () { @@ -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 @@ -781,6 +1019,12 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { /** * @private */ + _onCloseNotificationBar: function () { + this.$(".o_mail_annoying_notification_bar").slideUp(); + }, + /** + * @private + */ _onComposerFocused: function () { var composer = this.channel.mass_mailing ? this.extendedComposer : this.basicComposer; var commands = this.call('chat_manager', 'getCommands', this.channel); @@ -791,6 +1035,22 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { /** * @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 + */ _onMarkAllReadClicked: function () { this.call('chat_manager', 'markAllAsRead', this.channel, this.domain); }, @@ -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,12 +1075,36 @@ 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 */ _onNewChannel: function (channel) { @@ -872,7 +1157,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { } }) .fail(function () { - // todo: display notification + // todo: display notifications }); }, /** @@ -891,28 +1176,6 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { }, /** * @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 */ _onRequestNotificationPermission: function (event) { @@ -951,6 +1214,17 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { this._fetchAndRenderThread(); } }, + /** + * @private + * @param {MouseEvent} ev + */ + _onSelectAllClicked: function (ev) { + var $button = $(ev.target); + if (!$button.hasClass('disabled')) { + this.thread.toggleModerationCheckboxes(true); + this._updateModerationButtons(); + } + }, /** * @private * @param {MouseEvent} event @@ -977,10 +1251,31 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { }, /** * @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); diff --git a/addons/mail/static/src/js/services/chat_manager.js b/addons/mail/static/src/js/services/chat_manager.js index 4bb6e9be6aa..eaedb9cd11e 100644 --- a/addons/mail/static/src/js/services/chat_manager.js +++ b/addons/mail/static/src/js/services/chat_manager.js @@ -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 = []; @@ -522,6 +524,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. * @@ -557,6 +567,12 @@ var ChatManager = AbstractService.extend({ return this._getChannelCache(channel, domain).all_history_loaded; }, /** + * @return {boolean} + */ + isModerator: function () { + return this._isModerator; + }, + /** * @return {$.Promise} */ isReady: function () { @@ -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) { @@ -1469,6 +1531,13 @@ var ChatManager = AbstractService.extend({ }, /** * @private + * @param {Object} data + */ + _manageAuthorNotification: function (data) { + this._addMessage(data.message); + }, + /** + * @private * @param {Object} message * @param {Array} message.channel_ids list of integers and strings, * where strings for static channels, e.g. 'channel_inbox'. @@ -1546,6 +1615,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. * * @private @@ -1581,6 +1671,16 @@ var ChatManager = AbstractService.extend({ }, /** * @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 * @param {integer[]} message.channel_ids */ @@ -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') { diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index f34823ff9fc..39d179a715a 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -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; diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml index b0a3bfe1ba9..0bfec270eea 100644 --- a/addons/mail/static/src/xml/discuss.xml +++ b/addons/mail/static/src/xml/discuss.xml @@ -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"> diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index 5ef23d46c43..20049e89cf6 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -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> diff --git a/addons/mail/static/tests/discuss_moderation_tests.js b/addons/mail/static/tests/discuss_moderation_tests.js new file mode 100644 index 00000000000..da8e8b1c68d --- /dev/null +++ b/addons/mail/static/tests/discuss_moderation_tests.js @@ -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(); + }); +}); + +}); +}); diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js index 8b7c72fd21e..a56a7c64855 100644 --- a/addons/mail/static/tests/discuss_tests.js +++ b/addons/mail/static/tests/discuss_tests.js @@ -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', + }, }, }, }; |
