From 299ebb2cdf63de2e3967da0d2f3e7effd239812b Mon Sep 17 00:00:00 2001 From: Mathieu Duckaerts-Antoine Date: Thu, 24 May 2018 15:27:28 +0200 Subject: [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. --- addons/mail/static/src/js/discuss.js | 359 +++++++++++++++++++-- addons/mail/static/src/js/services/chat_manager.js | 118 ++++++- addons/mail/static/src/js/thread.js | 29 +- addons/mail/static/src/xml/discuss.xml | 15 + addons/mail/static/src/xml/thread.xml | 36 ++- 5 files changed, 517 insertions(+), 40 deletions(-) (limited to 'addons/mail/static/src') 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: , + * comment: , + * } + */ + 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); 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 = []; @@ -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') { 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 @@ +
+ Moderation Queue + + +

@@ -137,6 +143,11 @@ + + + + + @@ -145,6 +156,10 @@ +
+ + +
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 @@
No starred message
You can mark any message as 'starred', and it shows up in this channel.
+ +
You have no message to moderate
+
Pending moderation messages appear here.
+
@@ -144,6 +148,7 @@

+ Note by @@ -185,7 +190,7 @@ - + + + + + + + + +

@@ -227,6 +241,26 @@ + + Accept | + Reject | + Discard | + Always Allow | + Ban + + + + Pending moderation + + + + +
New messages -- cgit v1.2.3