summaryrefslogtreecommitdiff
path: root/addons/mail/static
diff options
context:
space:
mode:
authorMathieu Duckaerts-Antoine <dam@odoo.com>2018-05-24 15:27:28 +0200
committerThibault Delavallée <tde@odoo.com>2018-06-06 16:01:33 +0200
commit299ebb2cdf63de2e3967da0d2f3e7effd239812b (patch)
treef2681c0fc1e283acdfd5942392db8f17ddc7801d /addons/mail/static
parenteeab18d753db21bca0636836c40df91268549390 (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.js359
-rw-r--r--addons/mail/static/src/js/services/chat_manager.js118
-rw-r--r--addons/mail/static/src/js/thread.js29
-rw-r--r--addons/mail/static/src/xml/discuss.xml15
-rw-r--r--addons/mail/static/src/xml/thread.xml36
-rw-r--r--addons/mail/static/tests/discuss_moderation_tests.js822
-rw-r--r--addons/mail/static/tests/discuss_tests.js22
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 &amp;&amp; !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 &amp;&amp; message.model != 'mail.channel' &amp;&amp; options.display_reply_icon"
@@ -194,6 +199,15 @@
<i t-if="message.is_needaction &amp;&amp; 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',
+ },
},
},
};