From 9b905c8e95a70ef0cb017c306549a6142732c187 Mon Sep 17 00:00:00 2001 From: Shepilov Vladislav Date: Fri, 5 Apr 2019 13:39:14 +0300 Subject: [PATCH] [12.0] web_notify: improve popup UI (#1231) * [ADD]: all available bootstrap notifications (success/danger/warning/info/default) * [IMP] use black color for text for default notification. * [FIX] reverted require string for `bus.Longpolling` and rename `on_message_received` to `on_message` to prevent collisions. --- web_notify/README.rst | 10 +- web_notify/__init__.py | 1 + web_notify/__manifest__.py | 2 +- web_notify/models/__init__.py | 1 + web_notify/models/res_users.py | 99 ++++++++++++----- web_notify/readme/CONTRIBUTORS.rst | 3 +- web_notify/readme/DESCRIPTION.rst | 9 +- web_notify/readme/INSTALL.rst | 2 +- web_notify/readme/USAGE.rst | 26 ++++- web_notify/static/src/js/web_client.js | 85 ++++++++------- .../static/src/js/widgets/notification.js | 26 +++++ web_notify/static/src/scss/webclient.scss | 24 ++++ web_notify/tests/test_res_users.py | 103 ++++++++++++++---- web_notify/views/res_users_demo.xml | 22 +++- web_notify/views/web_notify.xml | 4 + 15 files changed, 310 insertions(+), 107 deletions(-) create mode 100644 web_notify/static/src/js/widgets/notification.js create mode 100644 web_notify/static/src/scss/webclient.scss diff --git a/web_notify/README.rst b/web_notify/README.rst index 53270a1b..6f8c52c2 100644 --- a/web_notify/README.rst +++ b/web_notify/README.rst @@ -23,11 +23,11 @@ Web Notify :target: https://runbot.odoo-community.org/runbot/162/12.0 :alt: Try me on Runbot -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| Send instant notification messages to the user in live. -This technical module allows you to send instant notification messages from the server to the user in live. +This technical module allows you to send instant notification messages from the server to the user in live. Two kinds of notification are supported. * Warning: Displayed in a red flying popup div @@ -50,13 +50,13 @@ Usage To send a notification to the user you just need to call one of the new methods defined on res.users: .. code-block:: python - + self.env.user.notify_info(message='My information message') -or +or .. code-block:: python - + self.env.user.notify_warning(message='My marning message') .. figure:: https://raw.githubusercontent.com/OCA/web/12.0/web_notify/static/description/notifications_screenshot.png diff --git a/web_notify/__init__.py b/web_notify/__init__.py index 31660d6a..3379be95 100644 --- a/web_notify/__init__.py +++ b/web_notify/__init__.py @@ -1,3 +1,4 @@ +# pylint: disable=missing-docstring # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from . import models diff --git a/web_notify/__manifest__.py b/web_notify/__manifest__.py index 0643b87d..cfee5407 100644 --- a/web_notify/__manifest__.py +++ b/web_notify/__manifest__.py @@ -1,3 +1,4 @@ +# pylint: disable=missing-docstring # Copyright 2016 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -6,7 +7,6 @@ 'summary': """ Send notification messages to user""", 'version': '12.0.1.0.0', - 'description': 'Web Notify', 'license': 'AGPL-3', 'author': 'ACSONE SA/NV,' 'AdaptiveCity,' diff --git a/web_notify/models/__init__.py b/web_notify/models/__init__.py index c39e40a1..15deefda 100644 --- a/web_notify/models/__init__.py +++ b/web_notify/models/__init__.py @@ -1,3 +1,4 @@ +# pylint: disable=missing-docstring # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from . import res_users diff --git a/web_notify/models/res_users.py b/web_notify/models/res_users.py index 9e1f2f04..76d80aee 100644 --- a/web_notify/models/res_users.py +++ b/web_notify/models/res_users.py @@ -1,46 +1,87 @@ +# pylint: disable=missing-docstring # Copyright 2016 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, exceptions, fields, models, _ +from odoo import _, api, exceptions, fields, models + +DEFAULT_MESSAGE = "Default message" + +SUCCESS = "success" +DANGER = "danger" +WARNING = "warning" +INFO = "info" +DEFAULT = "default" class ResUsers(models.Model): - _inherit = 'res.users' + _inherit = "res.users" - @api.depends('create_date') + @api.depends("create_date") def _compute_channel_names(self): for record in self: res_id = record.id - record.notify_info_channel_name = 'notify_info_%s' % res_id - record.notify_warning_channel_name = 'notify_warning_%s' % res_id + record.notify_success_channel_name = "notify_success_%s" % res_id + record.notify_danger_channel_name = "notify_danger_%s" % res_id + record.notify_warning_channel_name = "notify_warning_%s" % res_id + record.notify_info_channel_name = "notify_info_%s" % res_id + record.notify_default_channel_name = "notify_default_%s" % res_id + + notify_success_channel_name = fields.Char(compute="_compute_channel_names") + notify_danger_channel_name = fields.Char(compute="_compute_channel_names") + notify_warning_channel_name = fields.Char(compute="_compute_channel_names") + notify_info_channel_name = fields.Char(compute="_compute_channel_names") + notify_default_channel_name = fields.Char(compute="_compute_channel_names") + + def notify_success( + self, message="Default message", title=None, sticky=False + ): + title = title or _("Success") + self._notify_channel(SUCCESS, message, title, sticky) - notify_info_channel_name = fields.Char( - compute='_compute_channel_names') - notify_warning_channel_name = fields.Char( - compute='_compute_channel_names') + def notify_danger( + self, message="Default message", title=None, sticky=False + ): + title = title or _("Danger") + self._notify_channel(DANGER, message, title, sticky) + + def notify_warning( + self, message="Default message", title=None, sticky=False + ): + title = title or _("Warning") + self._notify_channel(WARNING, message, title, sticky) def notify_info(self, message="Default message", title=None, sticky=False): - title = title or _('Information') - self._notify_channel( - 'notify_info_channel_name', message, title, sticky) - - def notify_warning(self, message="Default message", - title=None, sticky=False): - title = title or _('Warning') - self._notify_channel( - 'notify_warning_channel_name', message, title, sticky) - - def _notify_channel(self, channel_name_field, message, title, sticky): - if (not self.env.user._is_admin() - and any(user.id != self.env.uid for user in self)): + title = title or _("Information") + self._notify_channel(INFO, message, title, sticky) + + def notify_default( + self, message="Default message", title=None, sticky=False + ): + title = title or _("Default") + self._notify_channel(DEFAULT, message, title, sticky) + + def _notify_channel( + self, + type_message=DEFAULT, + message=DEFAULT_MESSAGE, + title=None, + sticky=False, + ): + # pylint: disable=protected-access + if not self.env.user._is_admin() and any( + user.id != self.env.uid for user in self + ): raise exceptions.UserError( - _('Sending a notification to another user is forbidden.') + _("Sending a notification to another user is forbidden.") ) + channel_name_field = "notify_{}_channel_name".format(type_message) bus_message = { - 'message': message, - 'title': title, - 'sticky': sticky + "type": type_message, + "message": message, + "title": title, + "sticky": sticky, } - notifications = [(record[channel_name_field], bus_message) - for record in self] - self.env['bus.bus'].sendmany(notifications) + notifications = [ + (record[channel_name_field], bus_message) for record in self + ] + self.env["bus.bus"].sendmany(notifications) diff --git a/web_notify/readme/CONTRIBUTORS.rst b/web_notify/readme/CONTRIBUTORS.rst index 70f99b04..975ef2d3 100644 --- a/web_notify/readme/CONTRIBUTORS.rst +++ b/web_notify/readme/CONTRIBUTORS.rst @@ -1,3 +1,4 @@ * Laurent Mignon * Serpent Consulting Services Pvt. Ltd. -* Aitor Bouzas \ No newline at end of file +* Aitor Bouzas +* Shepilov Vladislav diff --git a/web_notify/readme/DESCRIPTION.rst b/web_notify/readme/DESCRIPTION.rst index 7d7528a3..2cd89780 100644 --- a/web_notify/readme/DESCRIPTION.rst +++ b/web_notify/readme/DESCRIPTION.rst @@ -1,7 +1,10 @@ Send instant notification messages to the user in live. -This technical module allows you to send instant notification messages from the server to the user in live. +This technical module allows you to send instant notification messages from the server to the user in live. Two kinds of notification are supported. -* Warning: Displayed in a red flying popup div -* Information: Displayed in a light yellow flying popup div \ No newline at end of file +* Success: Displayed in a `success` theme color flying popup div +* Danger: Displayed in a `danger` theme color flying popup div +* Warning: Displayed in a `warning` theme color flying popup div +* Information: Displayed in a `info` theme color flying popup div +* Default: Displayed in a `default` theme color flying popup div diff --git a/web_notify/readme/INSTALL.rst b/web_notify/readme/INSTALL.rst index a27ef894..b47aebb7 100644 --- a/web_notify/readme/INSTALL.rst +++ b/web_notify/readme/INSTALL.rst @@ -1 +1 @@ -This module is based on the Instant Messaging Bus. To work properly, the server must be launched in gevent mode. \ No newline at end of file +This module is based on the Instant Messaging Bus. To work properly, the server must be launched in gevent mode. diff --git a/web_notify/readme/USAGE.rst b/web_notify/readme/USAGE.rst index c6ebd48b..e25ef76e 100644 --- a/web_notify/readme/USAGE.rst +++ b/web_notify/readme/USAGE.rst @@ -2,14 +2,32 @@ To send a notification to the user you just need to call one of the new methods defined on res.users: .. code-block:: python - + + self.env.user.notify_success(message='My success message') + +or + +.. code-block:: python + + self.env.user.notify_danger(message='My danger message') + +or + +.. code-block:: python + + self.env.user.notify_warning(message='My warning message') + +or + +.. code-block:: python + self.env.user.notify_info(message='My information message') -or +or .. code-block:: python - - self.env.user.notify_warning(message='My marning message') + + self.env.user.notify_default(message='My default message') .. figure:: static/description/notifications_screenshot.png :scale: 80 % diff --git a/web_notify/static/src/js/web_client.js b/web_notify/static/src/js/web_client.js index c9b0139f..d1e1ff5a 100644 --- a/web_notify/static/src/js/web_client.js +++ b/web_notify/static/src/js/web_client.js @@ -1,44 +1,53 @@ odoo.define('web_notify.WebClient', function (require) { -"use strict"; + "use strict"; -var WebClient = require('web.WebClient'); -var base_bus = require('bus.Longpolling'); -var session = require('web.session'); -require('bus.BusService'); + var WebClient = require('web.WebClient'); + var base_bus = require('bus.Longpolling'); + var session = require('web.session'); + require('bus.BusService'); -WebClient.include({ - show_application: function() { - var res = this._super(); - this.start_polling(); - return res - }, - start_polling: function() { - this.channel_warning = 'notify_warning_' + session.uid; - this.channel_info = 'notify_info_' + session.uid; - this.call('bus_service', 'addChannel', this.channel_warning); - this.call('bus_service', 'addChannel', this.channel_info); - this.call('bus_service', 'on', 'notification', this, this.bus_notification); - this.call('bus_service', 'startPolling'); - }, - bus_notification: function(notifications) { - var self = this; - _.each(notifications, function (notification) { - var channel = notification[0]; - var message = notification[1]; - if (channel === self.channel_warning) { - self.on_message_warning(message); - } else if (channel === self.channel_info) { - self.on_message_info(message); - } - }); - }, - on_message_warning: function(message){ - this.do_warn(message.title, message.message, message.sticky); - }, - on_message_info: function(message){ - this.do_notify(message.title, message.message, message.sticky); - } -}); + WebClient.include({ + show_application: function () { + var res = this._super(); + this.start_polling(); + return res; + }, + start_polling: function () { + this.channel_success = 'notify_success_' + session.uid; + this.channel_danger = 'notify_danger_' + session.uid; + this.channel_warning = 'notify_warning_' + session.uid; + this.channel_info = 'notify_info_' + session.uid; + this.channel_default = 'notify_default_' + session.uid; + this.call('bus_service', 'addChannel', this.channel_success); + this.call('bus_service', 'addChannel', this.channel_danger); + this.call('bus_service', 'addChannel', this.channel_warning); + this.call('bus_service', 'addChannel', this.channel_info); + this.call('bus_service', 'addChannel', this.channel_default); + this.call( + 'bus_service', 'on', 'notification', + this, this.bus_notification); + this.call('bus_service', 'startPolling'); + }, + bus_notification: function (notifications) { + var self = this; + _.each(notifications, function (notification) { + // Not used: var channel = notification[0]; + var message = notification[1]; + self.on_message(message); + }); + }, + on_message: function (message) { + return this.call( + 'notification', 'notify', { + type: message.type, + title: message.title, + message: message.message, + sticky: message.sticky, + className: message.className, + } + ); + }, + }); }); diff --git a/web_notify/static/src/js/widgets/notification.js b/web_notify/static/src/js/widgets/notification.js new file mode 100644 index 00000000..ed797d20 --- /dev/null +++ b/web_notify/static/src/js/widgets/notification.js @@ -0,0 +1,26 @@ +odoo.define('web_notify.Notification', function (require) { + "use strict"; + + var Notification = require('web.Notification'); + + Notification.include({ + icon_mapping: { + 'success': 'fa-thumbs-up', + 'danger': 'fa-exclamation-triangle', + 'warning': 'fa-exclamation', + 'info': 'fa-info', + 'default': 'fa-lightbulb-o', + }, + init: function () { + this._super.apply(this, arguments); + // Delete default classes + this.className = this.className.replace(' o_error', ''); + // Add custom icon and custom class + this.icon = (this.type in this.icon_mapping) ? + this.icon_mapping[this.type] : + this.icon_mapping['default']; + this.className += ' o_' + this.type; + }, + }); + +}); diff --git a/web_notify/static/src/scss/webclient.scss b/web_notify/static/src/scss/webclient.scss new file mode 100644 index 00000000..1ce4cb41 --- /dev/null +++ b/web_notify/static/src/scss/webclient.scss @@ -0,0 +1,24 @@ +.o_notification_manager { + .o_notification { + &.o_success { + color: white; + background-color: theme-color('success'); + } + &.o_danger { + color: white; + background-color: theme-color('danger'); + } + &.o_warning { + color: white; + background-color: theme-color('warning'); + } + &.o_info { + color: white; + background-color: theme-color('info'); + } + &.o_default { + color: black; + background-color: theme-color('default'); + } + } +} diff --git a/web_notify/tests/test_res_users.py b/web_notify/tests/test_res_users.py index 80a1a937..36b00332 100644 --- a/web_notify/tests/test_res_users.py +++ b/web_notify/tests/test_res_users.py @@ -1,49 +1,106 @@ # Copyright 2016 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import exceptions -from odoo.tests import common -from odoo.addons.bus.models.bus import json_dump import json + import mock +from odoo import exceptions +from odoo.addons.bus.models.bus import json_dump +from ..models.res_users import SUCCESS, DANGER, WARNING, INFO, DEFAULT +from odoo.tests import common class TestResUsers(common.TransactionCase): + def test_notify_success(self): + bus_bus = self.env["bus.bus"] + domain = [ + ( + "channel", + "=", + json_dump(self.env.user.notify_success_channel_name), + ) + ] + existing = bus_bus.search(domain) + test_msg = {"message": "message", "title": "title", "sticky": True} + self.env.user.notify_success(**test_msg) + news = bus_bus.search(domain) - existing + self.assertEqual(1, len(news)) + test_msg.update({"type": SUCCESS}) + self.assertDictEqual(test_msg, json.loads(news.message)) - def test_notify_info(self): - bus_bus = self.env['bus.bus'] + def test_notify_danger(self): + bus_bus = self.env["bus.bus"] domain = [ - ('channel', '=', - json_dump(self.env.user.notify_info_channel_name)) + ( + "channel", + "=", + json_dump(self.env.user.notify_danger_channel_name), + ) ] existing = bus_bus.search(domain) - test_msg = {'message': 'message', 'title': 'title', 'sticky': True} - self.env.user.notify_info(**test_msg) + test_msg = {"message": "message", "title": "title", "sticky": True} + self.env.user.notify_danger(**test_msg) news = bus_bus.search(domain) - existing self.assertEqual(1, len(news)) - self.assertEqual(test_msg, json.loads(news.message)) + test_msg.update({"type": DANGER}) + self.assertDictEqual(test_msg, json.loads(news.message)) def test_notify_warning(self): - bus_bus = self.env['bus.bus'] + bus_bus = self.env["bus.bus"] domain = [ - ('channel', '=', - json_dump(self.env.user.notify_warning_channel_name)) + ( + "channel", + "=", + json_dump(self.env.user.notify_warning_channel_name), + ) ] existing = bus_bus.search(domain) - test_msg = {'message': 'message', 'title': 'title', 'sticky': True} + test_msg = {"message": "message", "title": "title", "sticky": True} self.env.user.notify_warning(**test_msg) news = bus_bus.search(domain) - existing self.assertEqual(1, len(news)) - self.assertEqual(test_msg, json.loads(news.message)) + test_msg.update({"type": WARNING}) + self.assertDictEqual(test_msg, json.loads(news.message)) + + def test_notify_info(self): + bus_bus = self.env["bus.bus"] + domain = [ + ("channel", "=", json_dump(self.env.user.notify_info_channel_name)) + ] + existing = bus_bus.search(domain) + test_msg = {"message": "message", "title": "title", "sticky": True} + self.env.user.notify_info(**test_msg) + news = bus_bus.search(domain) - existing + self.assertEqual(1, len(news)) + test_msg.update({"type": INFO}) + self.assertDictEqual(test_msg, json.loads(news.message)) + + def test_notify_default(self): + bus_bus = self.env["bus.bus"] + domain = [ + ( + "channel", + "=", + json_dump(self.env.user.notify_default_channel_name), + ) + ] + existing = bus_bus.search(domain) + test_msg = {"message": "message", "title": "title", "sticky": True} + self.env.user.notify_default(**test_msg) + news = bus_bus.search(domain) - existing + self.assertEqual(1, len(news)) + test_msg.update({"type": DEFAULT}) + self.assertDictEqual(test_msg, json.loads(news.message)) def test_notify_many(self): # check that the notification of a list of users is done with # a single call to the bus - with mock.patch('odoo.addons.bus.models.bus.ImBus.sendmany' - ) as mockedSendMany: + with mock.patch( + "odoo.addons.bus.models.bus.ImBus.sendmany" + ) as mockedSendMany: users = self.env.user.search([(1, "=", 1)]) self.assertTrue(len(users) > 1) - users.notify_warning(message='message') + users.notify_warning(message="message") self.assertEqual(1, mockedSendMany.call_count) @@ -58,11 +115,11 @@ class TestResUsers(common.TransactionCase): self.assertEqual(len(users), len(first_pos_call_args)) def test_notify_other_user(self): - other_user = self.env.ref('base.user_demo') - other_user_model = self.env['res.users'].sudo(other_user) + other_user = self.env.ref("base.user_demo") + other_user_model = self.env["res.users"].sudo(other_user) with self.assertRaises(exceptions.UserError): - other_user_model.browse(self.env.uid).notify_info(message='hello') + other_user_model.browse(self.env.uid).notify_info(message="hello") def test_notify_admin_allowed_other_user(self): - other_user = self.env.ref('base.user_demo') - other_user.notify_info(message='hello') + other_user = self.env.ref("base.user_demo") + other_user.notify_info(message="hello") diff --git a/web_notify/views/res_users_demo.xml b/web_notify/views/res_users_demo.xml index 4ca9f422..7e8c0d97 100644 --- a/web_notify/views/res_users_demo.xml +++ b/web_notify/views/res_users_demo.xml @@ -11,9 +11,15 @@ -