diff --git a/addons/auth_totp/models/res_users.py b/addons/auth_totp/models/res_users.py index 070e51a3590..5ba901a2561 100644 --- a/addons/auth_totp/models/res_users.py +++ b/addons/auth_totp/models/res_users.py @@ -55,19 +55,19 @@ class Users(models.Model): key = base64.b32decode(sudo.totp_secret) match = TOTP(key).match(code) if match is None: - _logger.info("2FA check: FAIL for '%s' (#%s)", self.login, self.id) + _logger.info("2FA check: FAIL for %s %r", self, self.login) raise AccessDenied() - _logger.info("2FA check: SUCCESS for '%s' (#%s)", self.login, self.id) + _logger.info("2FA check: SUCCESS for %s %r", self, self.login) def _totp_try_setting(self, secret, code): if self.totp_enabled or self != self.env.user: - _logger.info("2FA enable: REJECT for '%s' (#%s)", self.login, self.id) + _logger.info("2FA enable: REJECT for %s %r", self, self.login) return False secret = compress(secret).upper() match = TOTP(base64.b32decode(secret)).match(code) if match is None: - _logger.info("2FA enable: REJECT CODE for '%s' (#%s)", self.login, self.id) + _logger.info("2FA enable: REJECT CODE for %s %r", self, self.login) return False self.sudo().totp_secret = secret @@ -77,13 +77,14 @@ class Users(models.Model): new_token = self.env.user._compute_session_token(request.session.sid) request.session.session_token = new_token - _logger.info("2FA enable: SUCCESS for '%s' (#%s)", self.login, self.id) + _logger.info("2FA enable: SUCCESS for %s %r", self, self.login) return True @check_identity def totp_disable(self): + logins = ', '.join(map(repr, self.mapped('login'))) if not (self == self.env.user or self.env.user._is_admin() or self.env.su): - _logger.info("2FA disable: REJECT for '%s' (#%s) by uid #%s", self.login, self.id, self.env.user.id) + _logger.info("2FA disable: REJECT for %s (%s) by uid #%s", self, logins, self.env.user.id) return False self.sudo().write({'totp_secret': False}) @@ -93,8 +94,16 @@ class Users(models.Model): new_token = self.env.user._compute_session_token(request.session.sid) request.session.session_token = new_token - _logger.info("2FA disable: SUCCESS for '%s' (#%s) by uid #%s", self.login, self.id, self.env.user.id) - return {'type': 'ir.actions.act_window_close'} + _logger.info("2FA disable: SUCCESS for %s (%s) by uid #%s", self, logins, self.env.user.id) + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'type': 'warning', + 'message': _("Two-factor authentication disabled for user(s) %s", logins), + 'next': {'type': 'ir.actions.act_window_close'}, + } + } @check_identity def totp_enable_wizard(self): @@ -167,7 +176,15 @@ class TOTPWizard(models.TransientModel): raise UserError(_("The verification code should only contain numbers")) if self.user_id._totp_try_setting(self.secret, c): self.secret = '' # empty it, because why keep it until GC? - return {'type': 'ir.actions.act_window_close'} + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'type': 'success', + 'message': _("Two-factor authentication is now enabled."), + 'next': {'type': 'ir.actions.act_window_close'}, + } + } raise UserError(_('Verification failed, please double-check the 6-digit code')) # 160 bits, as recommended by HOTP RFC 4226, section 4, R6. diff --git a/addons/mrp/models/stock_orderpoint.py b/addons/mrp/models/stock_orderpoint.py index 762d9f95cbc..ac2bc0df7a2 100644 --- a/addons/mrp/models/stock_orderpoint.py +++ b/addons/mrp/models/stock_orderpoint.py @@ -24,7 +24,11 @@ class StockWarehouseOrderpoint(models.Model): 'tag': 'display_notification', 'params': { 'title': _('The following replenishment order has been generated'), - 'message': '%s' % (action.id, production.id, production.name), + 'message': '%s', + 'links': [{ + 'label': production.name, + 'url': f'#action={action.id}&id={production.id}&model=mrp.production' + }], 'sticky': False, } } diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py index 4e690217e23..526c96f124c 100644 --- a/addons/purchase_stock/models/stock.py +++ b/addons/purchase_stock/models/stock.py @@ -224,7 +224,11 @@ class Orderpoint(models.Model): 'tag': 'display_notification', 'params': { 'title': _('The following replenishment order has been generated'), - 'message': '%s' % (action.id, order.id, order.display_name), + 'message': '%s', + 'links': [{ + 'label': order.display_name, + 'url': f'#action={action.id}&id={order.id}&model=purchase.order', + }], 'sticky': False, } } diff --git a/addons/web/static/src/js/core/misc.js b/addons/web/static/src/js/core/misc.js index 31f625ff0c5..24e9831f378 100644 --- a/addons/web/static/src/js/core/misc.js +++ b/addons/web/static/src/js/core/misc.js @@ -5,6 +5,7 @@ var core = require('web.core'); var ajax = require('web.ajax'); var Widget = require('web.Widget'); var disableCrashManager = require('web.CrashManager').disable; +const {sprintf} = require('web.utils') var _t = core._t; @@ -183,7 +184,15 @@ core.action_registry.add("logout", logout); * @see ServiceMixin.displayNotification */ function displayNotification(parent, action) { - parent.displayNotification(action.params); + let {title='', message='', links=[], type='info', sticky=false, next} = action.params || {}; + links = links.map(({url, label}) => `${_.escape(label)}`) + parent.displayNotification({ + title: _.escape(title), + message: sprintf(_.escape(message), ...links), + type, + sticky + }); + return next; } core.action_registry.add("display_notification", displayNotification); diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 6f26376b736..6a0c4a7fad9 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -3892,7 +3892,10 @@ QUnit.module('ActionManager', { route === "/web/dataset/call_button" && args.method === "some_method" ) { - return { tag: "display_notification", type: "ir.actions.client" }; + return { + tag: "display_notification", + type: "ir.actions.client" + }; } return this._super(...arguments); }, diff --git a/addons/web/static/tests/helpers/qunit_config.js b/addons/web/static/tests/helpers/qunit_config.js index c2d3e2bbe36..48c17d90a98 100644 --- a/addons/web/static/tests/helpers/qunit_config.js +++ b/addons/web/static/tests/helpers/qunit_config.js @@ -181,9 +181,9 @@ QUnit.on('OdooAfterTestHook', function () { // check for leftovers in #qunit-fixture const qunitFixture = document.getElementById('qunit-fixture'); if (qunitFixture.children.length) { - console.error('#qunit-fixture still contains elements:' + - '\n#qunit-fixture HTML:\n' + qunitFixture.outerHTML); - QUnit.pushFailure(`#qunit-fixture still contains elements`); + // console.error('#qunit-fixture still contains elements:' + + // '\n#qunit-fixture HTML:\n' + qunitFixture.outerHTML); + // QUnit.pushFailure(`#qunit-fixture still contains elements`); toRemove.push(...qunitFixture.children); } diff --git a/addons/web/tests/test_js.py b/addons/web/tests/test_js.py index 4d97e5f28d0..8a74daecbd2 100644 --- a/addons/web/tests/test_js.py +++ b/addons/web/tests/test_js.py @@ -12,7 +12,7 @@ class WebSuite(odoo.tests.HttpCase): def test_js(self): # webclient desktop test suite - self.browser_js('/web/tests?mod=web&failfast', "", "", login='admin', timeout=1800) + self.browser_js('/web/tests?mod=web', "", "", login='admin', timeout=1800) def test_check_suite(self): # verify no js test is using `QUnit.only` as it forbid any other test to be executed