Browse Source

[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.
pull/1198/head
Shepilov Vladislav 5 years ago
committed by Simone Orsi
parent
commit
9b905c8e95
  1. 10
      web_notify/README.rst
  2. 1
      web_notify/__init__.py
  3. 2
      web_notify/__manifest__.py
  4. 1
      web_notify/models/__init__.py
  5. 99
      web_notify/models/res_users.py
  6. 3
      web_notify/readme/CONTRIBUTORS.rst
  7. 9
      web_notify/readme/DESCRIPTION.rst
  8. 2
      web_notify/readme/INSTALL.rst
  9. 26
      web_notify/readme/USAGE.rst
  10. 85
      web_notify/static/src/js/web_client.js
  11. 26
      web_notify/static/src/js/widgets/notification.js
  12. 24
      web_notify/static/src/scss/webclient.scss
  13. 103
      web_notify/tests/test_res_users.py
  14. 22
      web_notify/views/res_users_demo.xml
  15. 4
      web_notify/views/web_notify.xml

10
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

1
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

2
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,'

1
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

99
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)

3
web_notify/readme/CONTRIBUTORS.rst

@ -1,3 +1,4 @@
* Laurent Mignon <laurent.mignon@acsone.eu>
* Serpent Consulting Services Pvt. Ltd.<jay.vora@serpentcs.com>
* Aitor Bouzas <aitor.bouzas@adaptivecity.com>
* Aitor Bouzas <aitor.bouzas@adaptivecity.com>
* Shepilov Vladislav <shepilov.v@protonmail.com>

9
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
* 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

2
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.
This module is based on the Instant Messaging Bus. To work properly, the server must be launched in gevent mode.

26
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 %

85
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,
}
);
},
});
});

26
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;
},
});
});

24
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');
}
}
}

103
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")

22
web_notify/views/res_users_demo.xml

@ -11,9 +11,15 @@
<page string="Test web notify" name="test_web_notify">
<group>
<group>
<button name="notify_info"
<button name="notify_success"
type="object"
string="Test info notification"
string="Test success notification"
class="oe_highlight"/>
</group>
<group>
<button name="notify_danger"
type="object"
string="Test danger notification"
class="oe_highlight"/>
</group>
<group>
@ -22,6 +28,18 @@
string="Test warning notification"
class="oe_highlight"/>
</group>
<group>
<button name="notify_info"
type="object"
string="Test info notification"
class="oe_highlight"/>
</group>
<group>
<button name="notify_default"
type="object"
string="Test default notification"
class="oe_highlight"/>
</group>
</group>
</page>
</xpath>

4
web_notify/views/web_notify.xml

@ -1,8 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend" name="web_notify assets" inherit_id="web.assets_backend">
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/webclient.scss" position="after">
<link rel="stylesheet" type="text/scss" href="/web_notify/static/src/scss/webclient.scss"/>
</link>
<xpath expr="." position="inside">
<script type="text/javascript" src="/web_notify/static/src/js/web_client.js"/>
<script type="text/javascript" src="/web_notify/static/src/js/widgets/notification.js"/>
</xpath>
</template>
</odoo>
Loading…
Cancel
Save