[IMP] web, *: display warning/error in frontend

* = account, iap, point_of_sale

Removing the CrashManager templates from base.xml and moving
it to a new file (crash_manager.xml) to allow lazy loading the required
xml only when a crash occurs (necessary for the frontend as no xml is
pre-loaded).

All rpc errors/warnings are now displayed using the same modal system as
the backend (crash_manager.js).

CrashManager will now return the WarningDialog and ErrorDialog
in addition to the CrashManager itself.
In some cases an instance of CrashManager was created just to
display a warning/error dialog.

In the backend all rpc errors but sessionExpired will be logged.
If the exception is a warning, NotFound (403) or except_orm, it will be
a warning log. The rest will be exception log.

In the frontend all rpc errors will be logged using console.debug
instead of console.warn and console.error. This way we can trigger
rpc errors in tests without failing them. If a real error happend
during a test it will fall back on the backend logger to fail the test.

Part of https://github.com/odoo/odoo/pull/32132
task-1894820
This commit is contained in:
fja-odoo
2019-06-28 08:53:52 +00:00
parent e6d4ab84da
commit f8fe314e78
10 changed files with 140 additions and 80 deletions
@@ -5,7 +5,7 @@ var BasicModel = require('web.BasicModel');
var field_utils = require('web.field_utils'); var field_utils = require('web.field_utils');
var utils = require('web.utils'); var utils = require('web.utils');
var session = require('web.session'); var session = require('web.session');
var CrashManager = require('web.CrashManager'); var WarningDialog = require('web.CrashManager').WarningDialog;
var core = require('web.core'); var core = require('web.core');
var _t = core._t; var _t = core._t;
@@ -636,7 +636,7 @@ var StatementModel = BasicModel.extend({
}); });
break; break;
} }
} }
else if (fieldName === 'tax_ids') { else if (fieldName === 'tax_ids') {
switch(value.operation) { switch(value.operation) {
case "ADD_M2M": case "ADD_M2M":
+1 -1
View File
@@ -3,7 +3,7 @@ odoo.define('iap.CrashManager', function (require) {
var ajax = require('web.ajax'); var ajax = require('web.ajax');
var core = require('web.core'); var core = require('web.core');
var CrashManager = require('web.CrashManager'); var CrashManager = require('web.CrashManager').CrashManager;
var Dialog = require('web.Dialog'); var Dialog = require('web.Dialog');
var _t = core._t; var _t = core._t;
+1 -1
View File
@@ -8,7 +8,7 @@ var models = require('point_of_sale.models');
var AbstractAction = require('web.AbstractAction'); var AbstractAction = require('web.AbstractAction');
var core = require('web.core'); var core = require('web.core');
var ajax = require('web.ajax'); var ajax = require('web.ajax');
var CrashManager = require('web.CrashManager'); var CrashManager = require('web.CrashManager').CrashManager;
var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents; var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents;
@@ -15,9 +15,8 @@ var ActionManager = require('web.ActionManager');
var concurrency = require('web.concurrency'); var concurrency = require('web.concurrency');
var core = require('web.core'); var core = require('web.core');
var config = require('web.config'); var config = require('web.config');
var crash_manager = require('web.crash_manager'); var WarningDialog = require('web.CrashManager').WarningDialog;
var data_manager = require('web.data_manager'); var data_manager = require('web.data_manager');
var Dialog = require('web.Dialog');
var dom = require('web.dom'); var dom = require('web.dom');
var KeyboardNavigationMixin = require('web.KeyboardNavigationMixin'); var KeyboardNavigationMixin = require('web.KeyboardNavigationMixin');
var Loading = require('web.Loading'); var Loading = require('web.Loading');
@@ -29,7 +28,6 @@ var utils = require('web.utils');
var Widget = require('web.Widget'); var Widget = require('web.Widget');
var _t = core._t; var _t = core._t;
var qweb = core.qweb;
var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMixin, { var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMixin, {
dependencies: ['notification'], dependencies: ['notification'],
@@ -393,11 +391,9 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
_onDisplayWarning: function (e) { _onDisplayWarning: function (e) {
var data = e.data; var data = e.data;
if (data.type === 'dialog') { if (data.type === 'dialog') {
new Dialog(this, { new WarningDialog(this, {
size: 'medium',
title: data.title, title: data.title,
$content: qweb.render("CrashManager.warning", data), }, data).open();
}).open({shouldFocusButtons: true});
} else { } else {
this.call('notification', 'notify', e.data); this.call('notification', 'notify', e.data);
} }
+1 -5
View File
@@ -27,11 +27,7 @@ function _genericJsonRpc (fct_name, params, settings, fct) {
core.bus.trigger('rpc:result', data, result); core.bus.trigger('rpc:result', data, result);
if (result.error !== undefined) { if (result.error !== undefined) {
if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") { if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") {
var func = console.error; console.debug(
if (result.error.data.exception_type === "user_error") {
func = console.log;
}
func(
"Server application error\n", "Server application error\n",
"Error code:", result.error.code, "\n", "Error code:", result.error.code, "\n",
"Error message:", result.error.message, "\n", "Error message:", result.error.message, "\n",
@@ -4,21 +4,76 @@ odoo.define('web.CrashManager', function (require) {
var ajax = require('web.ajax'); var ajax = require('web.ajax');
var core = require('web.core'); var core = require('web.core');
var Dialog = require('web.Dialog'); var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t; var _t = core._t;
var _lt = core._lt; var _lt = core._lt;
var map_title ={ var map_title ={
user_error: _lt('Warning'), user_error: _lt("User Error"),
warning: _lt('Warning'), warning: _lt("Warning"),
access_error: _lt('Access Error'), access_error: _lt("Access Error"),
missing_error: _lt('Missing Record'), missing_error: _lt("Missing Record"),
validation_error: _lt('Validation Error'), validation_error: _lt("Validation Error"),
except_orm: _lt('Global Business Error'), except_orm: _lt("Global Business Error"),
access_denied: _lt('Access Denied'), access_denied: _lt("Access Denied"),
}; };
/**
* An extension of Dialog Widget to render the warnings and errors on the website.
* Extend it with your template of choice like ErrorDialog/WarningDialog
*/
var CrashManagerDialog = Dialog.extend({
xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat(
['/web/static/src/xml/crash_manager.xml']
),
/**
* @param {Object} error
* @param {string} error.message the message in Warning/Error Dialog
* @param {string} error.traceback the traceback in ErrorDialog
*
* @constructor
*/
init: function (parent, options, error) {
this._super.apply(this, [parent, options]);
this.message = error.message;
this.traceback = error.traceback;
},
});
var ErrorDialog = CrashManagerDialog.extend({
template: 'CrashManager.error',
});
var WarningDialog = CrashManagerDialog.extend({
template: 'CrashManager.warning',
/**
* Sets size to medium by default.
*
* @override
*/
init: function (parent, options, error) {
this._super(parent, _.extend({
size: 'medium',
}, options), error);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Focuses the ok button.
*
* @override
*/
open: function () {
this._super({shouldFocusButtons: true});
},
});
var CrashManager = core.Class.extend({ var CrashManager = core.Class.extend({
init: function () { init: function () {
var self = this; var self = this;
@@ -129,8 +184,8 @@ var CrashManager = core.Class.extend({
return; return;
} }
if (_.has(map_title, error.data.exception_type)) { if (_.has(map_title, error.data.exception_type)) {
if(error.data.exception_type === 'except_orm'){ if (error.data.exception_type === 'except_orm') {
if(error.data.arguments[1]) { if (error.data.arguments[1]) {
error = _.extend({}, error, error = _.extend({}, error,
{ {
data: _.extend({}, error.data, data: _.extend({}, error.data,
@@ -169,26 +224,30 @@ var CrashManager = core.Class.extend({
this.show_error(error); this.show_error(error);
} }
}, },
show_warning: function(error) { show_warning: function (error) {
if (!this.active) { if (!this.active) {
return; return;
} }
return new Dialog(this, { var message = error.data ? error.data.message : error.message;
size: 'medium', return new WarningDialog(this, {
title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"), title: _.str.capitalize(error.type) || _t("Odoo Warning"),
subtitle: error.data.title, subtitle: error.data.title,
$content: $(QWeb.render('CrashManager.warning', {error: error})) }, {
}).open({shouldFocusButtons:true}); message: message,
}).open();
}, },
show_error: function(error) { show_error: function (error) {
if (!this.active) { if (!this.active) {
return; return;
} }
var dialog = new Dialog(this, { var dialog = new ErrorDialog(this, {
title: _.str.capitalize(error.type || error.message) || _t("Odoo Error"), title: _.str.capitalize(error.type) || _t("Odoo Error"),
$content: $(QWeb.render('CrashManager.error', {error: error})) }, {
message: error.message,
traceback: error.data.debug,
}); });
// When the dialog opens, initialize the copy feature and destroy it when the dialog is closed // When the dialog opens, initialize the copy feature and destroy it when the dialog is closed
var $clipboardBtn; var $clipboardBtn;
var clipboard; var clipboard;
@@ -256,7 +315,7 @@ var ExceptionHandler = {
* Handle redirection warnings, which behave more or less like a regular * Handle redirection warnings, which behave more or less like a regular
* warning, with an additional redirection button. * warning, with an additional redirection button.
*/ */
var RedirectWarningHandler = Dialog.extend(ExceptionHandler, { var RedirectWarningHandler = Widget.extend(ExceptionHandler, {
init: function(parent, error) { init: function(parent, error) {
this._super(parent); this._super(parent);
this.error = error; this.error = error;
@@ -264,10 +323,8 @@ var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
display: function() { display: function() {
var self = this; var self = this;
var error = this.error; var error = this.error;
error.data.message = error.data.arguments[0];
new Dialog(this, { new WarningDialog(this, {
size: 'medium',
title: _.str.capitalize(error.type) || _t("Odoo Warning"), title: _.str.capitalize(error.type) || _t("Odoo Warning"),
buttons: [ buttons: [
{text: error.data.arguments[2], classes : "btn-primary", click: function() { {text: error.data.arguments[2], classes : "btn-primary", click: function() {
@@ -279,8 +336,9 @@ var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
location.reload(); location.reload();
}}, }},
{text: _t("Cancel"), click: function() { self.destroy(); }, close: true} {text: _t("Cancel"), click: function() { self.destroy(); }, close: true}
], ]
$content: QWeb.render('CrashManager.warning', {error: error}), }, {
message: error.data.arguments[0],
}).open(); }).open();
} }
}); });
@@ -290,9 +348,9 @@ core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandle
function session_expired(cm) { function session_expired(cm) {
return { return {
display: function () { display: function () {
cm.show_warning({type: _t("Odoo Session Expired"), data: {message: _t("Your Odoo session expired. Please refresh the current web page.")}}); cm.show_warning({type: _t("Odoo Session Expired"), message: _t("Your Odoo session expired. Please refresh the current web page.")});
} }
} };
} }
core.crash_registry.add('odoo.http.SessionExpiredException', session_expired); core.crash_registry.add('odoo.http.SessionExpiredException', session_expired);
core.crash_registry.add('werkzeug.exceptions.Forbidden', session_expired); core.crash_registry.add('werkzeug.exceptions.Forbidden', session_expired);
@@ -302,18 +360,22 @@ core.crash_registry.add('504', function (cm) {
display: function () { display: function () {
cm.show_warning({ cm.show_warning({
type: _t("Request timeout"), type: _t("Request timeout"),
data: {message: _t("The operation was interrupted. This usually means that the current operation is taking too much time.")}}); message: _t("The operation was interrupted. This usually means that the current operation is taking too much time.")});
} }
} };
}); });
return CrashManager; return {
CrashManager: CrashManager,
ErrorDialog: ErrorDialog,
WarningDialog: WarningDialog,
};
}); });
odoo.define('web.crash_manager', function (require) { odoo.define('web.crash_manager', function (require) {
"use strict"; "use strict";
var CrashManager = require('web.CrashManager'); var CrashManager = require('web.CrashManager').CrashManager;
return new CrashManager(); return new CrashManager();
}); });
-24
View File
@@ -120,30 +120,6 @@
</ul> </ul>
</t> </t>
<div t-name="CrashManager.warning" class="o_dialog_warning" role="alert">
<t t-js="d">
var message = (d.message !== undefined) ? d.message : d.error.data.message;
d.html_error = context.engine.tools.html_escape(message).replace(/\n/g, '<br/>');
</t>
<t t-raw="html_error"/>
</div>
<div t-name="CrashManager.error" class="o_dialog_error">
<div class="alert alert-warning clearfix" role="alert">
<button class="btn btn-primary float-right ml8 o_clipboard_button">
<i class="fa fa-clipboard mr8"/>Copy the full error to clipboard
</button>
<p><b>An error occurred</b></p>
<p>Please use the copy button to report the error to your support service.</p>
</div>
<t t-set="errUID" t-value="_.uniqueId()"/>
<button class="btn btn-link" t-att-data-target="'#o_error_detail' + errUID" data-toggle="collapse">See details</button>
<div t-att-id="'o_error_detail' + errUID" class="collapse alert alert-danger o_error_detail" role="alert">
<pre><t t-esc="error.message"/></pre>
<pre><t t-esc="error.data.debug"/></pre>
</div>
</div>
<form t-name="ChangePassword" name="change_password_form" method="POST" aria-atomic="true"> <form t-name="ChangePassword" name="change_password_form" method="POST" aria-atomic="true">
<div class="o_form_view"> <div class="o_form_view">
<table class="o_group o_inner_group o_label_nowrap"> <table class="o_group o_inner_group o_label_nowrap">
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="CrashManager.warning" class="o_dialog_warning" role="alert">
<pre t-esc="widget.message"/>
</div>
<div t-name="CrashManager.error" class="o_dialog_error">
<div class="alert alert-warning clearfix" role="alert">
<button class="btn btn-primary float-right ml8 o_clipboard_button">
<i class="fa fa-clipboard mr8"/>Copy the full error to clipboard
</button>
<p><b>An error occurred</b></p>
<p>Please use the copy button to report the error to your support service.</p>
</div>
<t t-set="errUID" t-value="_.uniqueId()"/>
<button class="btn btn-link" t-att-data-target="'#o_error_detail' + errUID" data-toggle="collapse">See details</button>
<div t-att-id="'o_error_detail' + errUID" class="collapse alert alert-danger o_error_detail" role="alert">
<pre t-esc="widget.message"/>
<pre t-esc="widget.traceback"/>
</div>
</div>
</templates>
+1 -1
View File
@@ -153,6 +153,7 @@
<script type="text/javascript" src="/web/static/src/js/services/core.js"></script> <script type="text/javascript" src="/web/static/src/js/services/core.js"></script>
<script type="text/javascript" src="/web/static/src/js/services/local_storage_service.js"></script> <script type="text/javascript" src="/web/static/src/js/services/local_storage_service.js"></script>
<script type="text/javascript" src="/web/static/src/js/services/notification_service.js"></script> <script type="text/javascript" src="/web/static/src/js/services/notification_service.js"></script>
<script type="text/javascript" src="/web/static/src/js/services/crash_manager.js"></script>
<script type="text/javascript" src="/web/static/src/js/services/session_storage_service.js"></script> <script type="text/javascript" src="/web/static/src/js/services/session_storage_service.js"></script>
<script type="text/javascript" src="/web/static/src/js/tools/debug_manager.js"></script> <script type="text/javascript" src="/web/static/src/js/tools/debug_manager.js"></script>
@@ -247,7 +248,6 @@
<script type="text/javascript" src="/web/static/src/js/core/context.js"></script> <script type="text/javascript" src="/web/static/src/js/core/context.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/data_comparison_utils.js"></script> <script type="text/javascript" src="/web/static/src/js/core/data_comparison_utils.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/misc.js"></script> <script type="text/javascript" src="/web/static/src/js/core/misc.js"></script>
<script type="text/javascript" src="/web/static/src/js/services/crash_manager.js"></script>
<script type="text/javascript" src="/web/static/src/js/services/data_manager.js"></script> <script type="text/javascript" src="/web/static/src/js/services/data_manager.js"></script>
<script type="text/javascript" src="/web/static/src/js/services/report_service.js"></script> <script type="text/javascript" src="/web/static/src/js/services/report_service.js"></script>
<script type="text/javascript" src="/web/static/src/js/services/session.js"></script> <script type="text/javascript" src="/web/static/src/js/services/session.js"></script>
+12 -7
View File
@@ -630,13 +630,18 @@ class JsonRequest(WebRequest):
try: try:
return super(JsonRequest, self)._handle_exception(exception) return super(JsonRequest, self)._handle_exception(exception)
except Exception: except Exception:
if not isinstance(exception, (odoo.exceptions.Warning, SessionExpiredException, if not isinstance(exception, SessionExpiredException):
odoo.exceptions.except_orm, werkzeug.exceptions.NotFound)): if exception.args and exception.args[0] == "bus.Bus not available in test mode":
_logger.exception("Exception during JSON request handling.") _logger.info(exception)
elif isinstance(exception, (odoo.exceptions.Warning, odoo.exceptions.except_orm,
werkzeug.exceptions.NotFound)):
_logger.warning(exception)
else:
_logger.exception("Exception during JSON request handling.")
error = { error = {
'code': 200, 'code': 200,
'message': "Odoo Server Error", 'message': "Odoo Server Error",
'data': serialize_exception(exception) 'data': serialize_exception(exception),
} }
if isinstance(exception, werkzeug.exceptions.NotFound): if isinstance(exception, werkzeug.exceptions.NotFound):
error['http_status'] = 404 error['http_status'] = 404
@@ -1414,7 +1419,7 @@ class Root(object):
httprequest = werkzeug.wrappers.Request(environ) httprequest = werkzeug.wrappers.Request(environ)
httprequest.app = self httprequest.app = self
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableOrderedMultiDict httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableOrderedMultiDict
current_thread = threading.current_thread() current_thread = threading.current_thread()
current_thread.url = httprequest.url current_thread.url = httprequest.url
current_thread.query_count = 0 current_thread.query_count = 0