From 423cf93eb32df363c16ccabab2fddaea384ceac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 20 Jun 2019 09:55:30 +0000 Subject: [PATCH] [IMP] sms: add UX elements to use SMS notifications Purpose of this commit is to allow users to have a feedback on SMS notifications status in chatter. This commit contains notably * an update of systray widget that now handles SMS failures. Those are displayed in another item of the systray and redirect to documents having an SMS failure; * an update of chatter widgets to support SMS notifications in messages. Failed notifications appear in red and allow to use the SMS resend wizard; Mail message model gains a new has_sms_error computed and searchable field allowing to filter on messages having failed SMS notifications. Mail thread model gains a new message_has_sms_error computed and searchable field, based on the one on mail.message. It allows to filter on records with failed SMS messages directly from the systray. That way clicking on the systray entry redirects to the right records. Related to task 1922163 Linked to PR #33510 Co-Authored-By: Thibault Delavallee Co-Authored-By: Pierre Rousseau --- addons/sms/__manifest__.py | 2 +- addons/sms/models/mail_message.py | 28 +++++- addons/sms/models/mail_thread.py | 25 +++++- addons/sms/static/src/js/mail_failure.js | 29 ++++++ addons/sms/static/src/js/message.js | 73 +++++++++++++++ .../static/src/js/sms_notification_manager.js | 64 +++++++++++++ .../static/src/js/systray_messaging_menu.js | 80 +++++++++++++++++ addons/sms/static/src/js/thread_widget.js | 89 +++++++++++++++++++ addons/sms/static/src/scss/thread.scss | 5 ++ addons/sms/static/src/xml/sms_widget.xml | 6 -- addons/sms/static/src/xml/thread.xml | 26 ++++++ addons/sms/views/assets.xml | 6 ++ addons/test_mail_full/tests/test_sms_post.py | 4 + 13 files changed, 425 insertions(+), 12 deletions(-) create mode 100644 addons/sms/static/src/js/mail_failure.js create mode 100644 addons/sms/static/src/js/message.js create mode 100644 addons/sms/static/src/js/sms_notification_manager.js create mode 100644 addons/sms/static/src/js/systray_messaging_menu.js create mode 100644 addons/sms/static/src/js/thread_widget.js create mode 100644 addons/sms/static/src/scss/thread.scss delete mode 100644 addons/sms/static/src/xml/sms_widget.xml create mode 100644 addons/sms/static/src/xml/thread.xml diff --git a/addons/sms/__manifest__.py b/addons/sms/__manifest__.py index 820b8b5a711..9cf60447885 100644 --- a/addons/sms/__manifest__.py +++ b/addons/sms/__manifest__.py @@ -24,7 +24,7 @@ The service is provided by the In App Purchase Odoo platform. 'security/ir.model.access.csv', ], 'qweb': [ - 'static/src/xml/sms_widget.xml', + 'static/src/xml/thread.xml', ], 'installable': True, 'auto_install': True, diff --git a/addons/sms/models/mail_message.py b/addons/sms/models/mail_message.py index 0581b9248c6..92fda5e8a83 100644 --- a/addons/sms/models/mail_message.py +++ b/addons/sms/models/mail_message.py @@ -15,6 +15,24 @@ class MailMessage(models.Model): _inherit = 'mail.message' message_type = fields.Selection(selection_add=[('sms', 'SMS')]) + has_sms_error = fields.Boolean( + 'Has SMS error', compute='_compute_has_sms_error', search='_search_has_sms_error', + help='Has error') + + @api.multi + def _compute_has_sms_error(self): + sms_error_from_notification = self.env['mail.notification'].sudo().search([ + ('notification_type', '=', 'sms'), + ('mail_message_id', 'in', self.ids), + ('notification_status', '=', 'exception')]).mapped('mail_message_id') + for message in self: + message.has_error = message in sms_error_from_notification + + @api.multi + def _search_has_sms_error(self, operator, operand): + if operator == '=' and operand: + return ['&', ('notification_ids.notification_status', '=', 'exception'), ('notification_ids.notification_type', '=', True)] + raise NotImplementedError() @api.multi def _format_mail_failures(self): @@ -46,11 +64,13 @@ class MailMessage(models.Model): @api.multi def _notify_sms_update(self): + """ Send bus notifications to update status of notifications in chatter. + Purpose is to send the updated status per author. + + TDE FIXME: author_id strategy seems curious, check with JS """ messages = self.env['mail.message'] for message in self: - # Check if user has access to the record before displaying a notification about it. - # In case the user switches from one company to another, it might happen that he doesn't - # have access to the record related to the notification. In this case, we skip it. + # YTI FIXME: check allowed_company_ids if necessary if message.model and message.res_id: record = self.env[message.model].browse(message.res_id) try: @@ -85,6 +105,6 @@ class MailMessage(models.Model): msgid_to_notif[notif.mail_message_id.id] += notif for message in message_values: - customer_sms_data = [(notif.id, notif.res_partner_id.display_name, notif.notification_status) for notif in msgid_to_notif.get(message['id'], [])] + customer_sms_data = [(notif.id, notif.res_partner_id.display_name or notif.sms_number, notif.notification_status) for notif in msgid_to_notif.get(message['id'], [])] message['sms_ids'] = customer_sms_data return message_values diff --git a/addons/sms/models/mail_thread.py b/addons/sms/models/mail_thread.py index b088309cbfe..a432d29a2d3 100644 --- a/addons/sms/models/mail_thread.py +++ b/addons/sms/models/mail_thread.py @@ -3,7 +3,7 @@ import logging -from odoo import api, models +from odoo import api, models, fields from odoo.addons.phone_validation.tools import phone_validation from odoo.tools import html2plaintext @@ -13,6 +13,29 @@ _logger = logging.getLogger(__name__) class MailThread(models.AbstractModel): _inherit = 'mail.thread' + message_has_sms_error = fields.Boolean( + 'SMS Delivery error', compute='_compute_message_has_sms_error', search='_search_message_has_sms_error', + help="If checked, some messages have a delivery error.") + + @api.multi + def _compute_message_has_sms_error(self): + res = {} + if self.ids: + self._cr.execute(""" SELECT msg.res_id, COUNT(msg.res_id) FROM mail_message msg + RIGHT JOIN mail_message_res_partner_needaction_rel rel + ON rel.mail_message_id = msg.id AND rel.notification_type = 'sms' AND rel.notification_status in ('exception') + WHERE msg.author_id = %s AND msg.model = %s AND msg.res_id in %s AND msg.message_type != 'user_notification' + GROUP BY msg.res_id""", + (self.env.user.partner_id.id, self._name, tuple(self.ids),)) + res.update(self._cr.fetchall()) + + for record in self: + record.message_has_sms_error = bool(res.get(record._origin.id, 0)) + + @api.model + def _search_message_has_sms_error(self, operator, operand): + return ['&', ('message_ids.has_sms_error', operator, operand), ('message_ids.author_id', '=', self.env.user.partner_id.id)] + def _sms_get_default_partners(self): """ This method will likely need to be overriden by inherited models. :returns partners: recordset of res.partner diff --git a/addons/sms/static/src/js/mail_failure.js b/addons/sms/static/src/js/mail_failure.js new file mode 100644 index 00000000000..0acb8cad76a --- /dev/null +++ b/addons/sms/static/src/js/mail_failure.js @@ -0,0 +1,29 @@ +odoo.define('sms.model.MailFailure', function (require) { +'use strict'; + +var MailFailure = require('mail.model.MailFailure'); +var core = require('web.core'); +var _t = core._t; + +MailFailure.include({ + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + getPreview: function () { + var preview = this._super.apply(this, arguments); + if (this._failureType === 'sms') { + _.extend(preview, { + body: _t('An error occurred when sending SMS'), + id: 'sms_failure', + }); + } + return preview; + }, +}); + +}); diff --git a/addons/sms/static/src/js/message.js b/addons/sms/static/src/js/message.js new file mode 100644 index 00000000000..f800563fd72 --- /dev/null +++ b/addons/sms/static/src/js/message.js @@ -0,0 +1,73 @@ +odoo.define('sms.model.Message', function (require) { +"use strict"; + +var Message = require('mail.model.Message'); + +Message.include({ + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Retrieves the list of SMS + * + * @returns {Array} Array of Array(smsID, recipientName, smsStatus) + */ + getSmsIds: function () { + return this._smsIds; + }, + /** + * Retrieves the SMS status + * + * @return {string} + */ + getSmsStatus: function () { + var self = this; + this._smsStatus = 'sent'; + _.each(this._smsIds, function (sms) { + if (sms[2] === 'bounce' || sms[2] === 'exception') { + self._smsStatus = 'error'; + } + }); + return this._smsStatus; + }, + /** + * Does the message contains at least one SMS failure + * + * @returns {boolean} + */ + isError: function () { + return this.getSmsStatus() === 'error'; + }, + /** + * Update the status of a SMS + * + * @param {integer} smsID ID of the SMS to update + * @param {string} smsStatus New status of the SMS + */ + setSmsStatus: function (smsID, smsStatus) { + var self = this; + var sms = _.find(self._smsIds, function (sms) { + return sms[0] === smsID; + }); + sms[2] = smsStatus; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _setInitialData: function (data) { + this._super.apply(this, arguments); + this._smsIds = data.sms_ids; + if (this._smsStatus === false) { + this._smsStatus = 'sent'; + } + }, +}); +}); diff --git a/addons/sms/static/src/js/sms_notification_manager.js b/addons/sms/static/src/js/sms_notification_manager.js new file mode 100644 index 00000000000..7d93d96fe51 --- /dev/null +++ b/addons/sms/static/src/js/sms_notification_manager.js @@ -0,0 +1,64 @@ +odoo.define('sms.NotificationManager', function (require) { +"use strict"; + +var MailManager = require('mail.Manager'); +var MailFailure = require('mail.model.MailFailure'); + +MailManager.include({ + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + * @param {Object} data structure depending on the type + * @param {integer} data.id + */ + _handlePartnerNotification: function (data) { + if (data.type === 'sms_update') { + this._handleSMSUpdateNotification(data); + } else { + this._super.apply(this, arguments); + } + }, + /** + * Updates message in thread when there's an update in a SMS letter + * + * @private + * @param {Object} datas + * @param {Object[]} datas.elements list of SMS failure data + * @param {string} datas.elements[].message_id ID of related message that + * has a sms failure. + */ + _handleSMSUpdateNotification: function (datas) { + var self = this; + _.each(datas.elements, function (data) { + var isNewFailure = data.sms_status === 'error'; + var matchedFailure = _.find(self._mailFailures, function (failure) { + return failure.getMessageID() === data.message_id; + }); + + if (matchedFailure) { + var index = _.findIndex(self._mailFailures, matchedFailure); + if (isNewFailure) { + self._mailFailures[index] = new MailFailure(self, data); + } else { + self._mailFailures.splice(index, 1); + } + } else if (isNewFailure) { + self._mailFailures.push(new MailFailure(self, data)); + } + var message = _.find(self._messages, function (msg) { + return msg.getID() === data.message_id; + }); + if (message) { + message.setSmsStatus(data.sms_id, data.sms_status); + self._mailBus.trigger('update_message', message); + } + }); + this._mailBus.trigger('update_needaction', this.needactionCounter); + }, +}); + +}); diff --git a/addons/sms/static/src/js/systray_messaging_menu.js b/addons/sms/static/src/js/systray_messaging_menu.js new file mode 100644 index 00000000000..80987af37e6 --- /dev/null +++ b/addons/sms/static/src/js/systray_messaging_menu.js @@ -0,0 +1,80 @@ +odoo.define('sms.systray.MessagingMenu', function (require) { +"use strict"; + +var core = require('web.core'); +var MessagingMenu = require('mail.systray.MessagingMenu'); + +var _t = core._t; + +MessagingMenu.include({ + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Called when clicking on a preview related to a snailmail failure + * + * @private + * @param {$.Element} $target DOM of preview element clicked + */ + _clickSMSFailurePreview: function ($target) { + var documentID = $target.data('document-id'); + var documentModel = $target.data('document-model'); + if (documentModel && documentID) { + this._openDocument(documentModel, documentID); + } else if (documentModel !== 'mail.channel') { + // preview of SMS failures grouped to different document of same model + this.do_action({ + name: _t('SMS Failures'), + type: 'ir.actions.act_window', + view_mode: 'kanban,list,form', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], + target: 'current', + res_model: documentModel, + domain: [['message_has_sms_error', '=', true]], + }); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @override + */ + _onClickPreview: function (ev) { + var $target = $(ev.currentTarget); + var previewID = $target.data('preview-id'); + if (previewID === 'sms_failure') { + this._clickSMSFailurePreview($target); + } else { + this._super.apply(this, arguments); + } + }, + /** + * @private + * @override + */ + _onClickPreviewMarkAsRead: function (ev) { + ev.stopPropagation(); + var $preview = $(ev.currentTarget).closest('.o_mail_preview'); + var previewID = $preview.data('preview-id'); + if (previewID === 'sms_failure') { + var documentModel = $preview.data('document-model'); + var unreadCounter = $preview.data('unread-counter'); + this.do_action('sms.sms_cancel_action', { + additional_context: { + default_model: documentModel, + unread_counter: unreadCounter + } + }); + } else { + this._super.apply(this, arguments); + } + }, +}); + +}); diff --git a/addons/sms/static/src/js/thread_widget.js b/addons/sms/static/src/js/thread_widget.js new file mode 100644 index 00000000000..4655732b34d --- /dev/null +++ b/addons/sms/static/src/js/thread_widget.js @@ -0,0 +1,89 @@ +odoo.define('sms.widget.Thread', function (require) { +"use strict"; + +var ThreadWidget = require('mail.widget.Thread'); +var core = require('web.core'); + +var QWeb = core.qweb; + +ThreadWidget.include({ + events: _.extend({}, ThreadWidget.prototype.events, { + 'click .o_thread_message_sms_error': '_onClickSMSError' + }), + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this._enabledOptions = _.defaults(this._enabledOptions, { + displaySmsIcons: true, + }); + this._disabledOptions = _.defaults(this._disabledOptions, { + displaySmsIcons: false, + }); + }, + /** + * @override + */ + render: function (thread, options) { + this._super.apply(this, arguments); + var messages = _.clone(thread.getMessages({domain: options.domain || []})); + this._renderMessageSmsPopover(messages); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Render the popover when mouse-hovering on the mail icon of a message + * in the thread. There is at most one such popover at any given time. + * + * @private + * @param {mail.model.AbstractMessage[]} messages list of messages in the + * rendered thread, for which popover on mouseover interaction is + * permitted. + */ + _renderMessageSmsPopover: function (messages) { + if (this._messageSmsPopover) { + this._messageSmsPopover.popover('hide'); + } + if (!this.$('.o_thread_sms_tooltip').length) { + return; + } + this._messageSmsPopover = this.$('.o_thread_sms_tooltip').popover({ + html: true, + boundary: 'viewport', + placement: 'auto', + trigger: 'hover', + offset: '0, 1', + content: function () { + var messageID = $(this).data('message-id'); + var message = _.find(messages, function (message) { + return message.getID() === messageID; + }); + return QWeb.render('sms.widget.Thread.Message.SmsTooltip', { + data: message.getSmsIds() + }); + }, + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickSMSError: function (ev) { + var messageID = $(ev.currentTarget).data('message-id'); + this.do_action('sms.sms_resend_action', { + additional_context: { + default_mail_message_id: messageID + } + }); + }, +}); +}); diff --git a/addons/sms/static/src/scss/thread.scss b/addons/sms/static/src/scss/thread.scss new file mode 100644 index 00000000000..ba4e54386c7 --- /dev/null +++ b/addons/sms/static/src/scss/thread.scss @@ -0,0 +1,5 @@ +i.o_thread_message_sms.o_thread_message_sms_error { + color: red; + opacity: 1; + cursor: pointer; +} diff --git a/addons/sms/static/src/xml/sms_widget.xml b/addons/sms/static/src/xml/sms_widget.xml deleted file mode 100644 index 62d356e3a70..00000000000 --- a/addons/sms/static/src/xml/sms_widget.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/addons/sms/static/src/xml/thread.xml b/addons/sms/static/src/xml/thread.xml new file mode 100644 index 00000000000..b01524ba609 --- /dev/null +++ b/addons/sms/static/src/xml/thread.xml @@ -0,0 +1,26 @@ + + diff --git a/addons/sms/views/assets.xml b/addons/sms/views/assets.xml index b3fdf38d74a..ba7f5a5914f 100644 --- a/addons/sms/views/assets.xml +++ b/addons/sms/views/assets.xml @@ -5,6 +5,12 @@ + + + + + + diff --git a/addons/test_mail_full/tests/test_sms_post.py b/addons/test_mail_full/tests/test_sms_post.py index 686ed941415..489250fde1c 100644 --- a/addons/test_mail_full/tests/test_sms_post.py +++ b/addons/test_mail_full/tests/test_sms_post.py @@ -8,6 +8,10 @@ from odoo.addons.test_mail_full.tests import common as test_mail_full_common class TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients): + """ TODO + + * add tests for new mail.message and mail.thread fields; + """ @classmethod def setUpClass(cls):