[MERGE] website, *: handle all website errors with beautiful design
* web, base, website_forum, test_website, account, gamification, iap, mail, partner_autocomplete, point_of_sale, stock, website_sale_comparison All rpc errors are now displayed using the notification system recently updated for website. For the redirections triggering an error that are currently displayed as a 500 Error with an ugly white page, the error page will be similar to 404/403 error but with code 400. All exception_orm are now handled as a 400 except for the AccessError which is a 403. The error card on all the Error pages is now loaded on an open state displaying clearly the reason of the error without clicking anything. If there is no error message the error card is not displayed and the traceback is open. The traceback is only displayed if we are in debug mode. These changes are also effective for the 500, 404 and 403 error pages. KarmaError is now an except_orm exception. Karma error message now contains the required karma. All the alerts in website_forum will use the toast notifications instead of alerts. A test tour is located in test_website, visit /test_error_view to trigger some errors manually and look at the result. task-1894820 closes odoo/odoo#32132 Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
@@ -5,7 +5,7 @@ var BasicModel = require('web.BasicModel');
|
||||
var field_utils = require('web.field_utils');
|
||||
var utils = require('web.utils');
|
||||
var session = require('web.session');
|
||||
var CrashManager = require('web.CrashManager');
|
||||
var WarningDialog = require('web.CrashManager').WarningDialog;
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
@@ -636,7 +636,7 @@ var StatementModel = BasicModel.extend({
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (fieldName === 'tax_ids') {
|
||||
switch(value.operation) {
|
||||
case "ADD_M2M":
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from odoo import api, tools, fields, models
|
||||
from odoo import api, tools, fields, models, exceptions, http
|
||||
from odoo.tools.translate import html_translate
|
||||
|
||||
|
||||
class KarmaError(Forbidden):
|
||||
class KarmaError(exceptions.except_orm):
|
||||
""" Karma-related error, used for forum and posts. """
|
||||
pass
|
||||
def __init__(self, msg):
|
||||
super(KarmaError, self).__init__(msg)
|
||||
|
||||
|
||||
class Http(models.AbstractModel):
|
||||
_inherit = 'ir.http'
|
||||
|
||||
@classmethod
|
||||
def serialize_exception(self, e):
|
||||
res = super(Http, self).serialize_exception(e)
|
||||
if isinstance(e, KarmaError):
|
||||
res["exception_type"] = "karma_error"
|
||||
return res
|
||||
|
||||
|
||||
class KarmaRank(models.Model):
|
||||
|
||||
@@ -3,7 +3,7 @@ odoo.define('iap.CrashManager', function (require) {
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var CrashManager = require('web.CrashManager');
|
||||
var CrashManager = require('web.CrashManager').CrashManager;
|
||||
var Dialog = require('web.Dialog');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
<div id="o_chatter_planned_activities" class="collapse in show">
|
||||
<div id="o_chatter_planned_activities" class="collapse show">
|
||||
<t t-foreach="activities" t-as="activity">
|
||||
<div class="o_thread_message" style="margin-bottom: 10px">
|
||||
<div class="o_thread_message_sidebar">
|
||||
|
||||
@@ -81,7 +81,13 @@ odoo.define('partner_autocomplete.tests', function (require) {
|
||||
return this._getOdooSuggestions(value);
|
||||
},
|
||||
do_notify: function (title, message, sticky, className) {
|
||||
return this.displayNotification(title, message, 'warning', true, 'o_partner_autocomplete_test_notify');
|
||||
return this.displayNotification({
|
||||
type: 'warning',
|
||||
title: title,
|
||||
message: message,
|
||||
sticky: sticky,
|
||||
className: 'o_partner_autocomplete_test_notify'
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ var models = require('point_of_sale.models');
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var core = require('web.core');
|
||||
var ajax = require('web.ajax');
|
||||
var CrashManager = require('web.CrashManager');
|
||||
var CrashManager = require('web.CrashManager').CrashManager;
|
||||
var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents;
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.web.controllers.main import _serialize_exception
|
||||
from odoo.tools import html_escape
|
||||
|
||||
import json
|
||||
@@ -37,7 +36,7 @@ class StockReportController(http.Controller):
|
||||
response.set_cookie('fileToken', token)
|
||||
return response
|
||||
except Exception as e:
|
||||
se = _serialize_exception(e)
|
||||
se = request.env['ir.http'].serialize_exception(e)
|
||||
error = {
|
||||
'code': 200,
|
||||
'message': 'Odoo Server Error',
|
||||
|
||||
@@ -14,6 +14,7 @@ dependencies. Neither we want to add in website module some routes, views and
|
||||
models which only purpose is to run tests.""",
|
||||
'depends': [
|
||||
'website',
|
||||
'gamification',
|
||||
],
|
||||
'data': [
|
||||
'views/templates.xml',
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import werkzeug
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.web import Home
|
||||
import json
|
||||
from odoo.exceptions import UserError, ValidationError, AccessError, MissingError, AccessError, AccessDenied
|
||||
from odoo.addons.gamification.models.gamification_karma_rank import KarmaError
|
||||
|
||||
|
||||
class WebsiteTest(Home):
|
||||
|
||||
@http.route('/test_view', type='http', auth="public", website=True)
|
||||
def test_view(self, **kw):
|
||||
@http.route('/test_view', type='http', auth='public', website=True)
|
||||
def test_view(self, **kwargs):
|
||||
return request.render('test_website.test_view')
|
||||
|
||||
@http.route('/ignore_args/converteronly/<string:a>/', type='http', auth="public", website=True)
|
||||
@@ -40,3 +44,63 @@ class WebsiteTest(Home):
|
||||
@http.route('/multi_company_website', type='http', auth="public", website=True)
|
||||
def test_company_context(self):
|
||||
return request.make_response(json.dumps(request.context.get('allowed_company_ids')))
|
||||
|
||||
@http.route('/test_error_view', type='http', auth='public', website=True)
|
||||
def test_error_view(self, **kwargs):
|
||||
return request.render('test_website.test_error_view')
|
||||
|
||||
@http.route('/test_user_error_http', type='http', auth='public', website=True)
|
||||
def test_user_error_http(self, **kwargs):
|
||||
raise UserError("This is a user http test")
|
||||
|
||||
@http.route('/test_user_error_json', type='json', auth='public', website=True)
|
||||
def test_user_error_json(self, **kwargs):
|
||||
raise UserError("This is a user rpc test")
|
||||
|
||||
@http.route('/test_validation_error_http', type='http', auth='public', website=True)
|
||||
def test_validation_error_http(self, **kwargs):
|
||||
raise ValidationError("This is a validation http test")
|
||||
|
||||
@http.route('/test_validation_error_json', type='json', auth='public', website=True)
|
||||
def test_validation_error_json(self, **kwargs):
|
||||
raise ValidationError("This is a validation rpc test")
|
||||
|
||||
@http.route('/test_access_error_json', type='json', auth='public', website=True)
|
||||
def test_access_error_json(self, **kwargs):
|
||||
raise AccessError("This is an access rpc test")
|
||||
|
||||
@http.route('/test_access_error_http', type='http', auth='public', website=True)
|
||||
def test_access_error_http(self, **kwargs):
|
||||
raise AccessError("This is an access http test")
|
||||
|
||||
@http.route('/test_missing_error_json', type='json', auth='public', website=True)
|
||||
def test_missing_error_json(self, **kwargs):
|
||||
raise MissingError("This is a missing rpc test")
|
||||
|
||||
@http.route('/test_missing_error_http', type='http', auth='public', website=True)
|
||||
def test_missing_error_http(self, **kwargs):
|
||||
raise MissingError("This is a missing http test")
|
||||
|
||||
@http.route('/test_karma_error_json', type='json', auth='public', website=True)
|
||||
def test_karma_error_rpc(self, **kwargs):
|
||||
raise KarmaError("This is a karma rpc test")
|
||||
|
||||
@http.route('/test_karma_error_http', type='http', auth='public', website=True)
|
||||
def test_karma_error_http(self, **kwargs):
|
||||
raise KarmaError("This is a karma http test")
|
||||
|
||||
@http.route('/test_internal_error_json', type='json', auth='public', website=True)
|
||||
def test_internal_error_json(self, **kwargs):
|
||||
raise werkzeug.exceptions.InternalServerError()
|
||||
|
||||
@http.route('/test_internal_error_http', type='http', auth='public', website=True)
|
||||
def test_internal_error_http(self, **kwargs):
|
||||
raise werkzeug.exceptions.InternalServerError()
|
||||
|
||||
@http.route('/test_access_denied_json', type='json', auth='public', website=True)
|
||||
def test_denied_error_json(self, **kwargs):
|
||||
raise AccessDenied("This is an access denied rpc test")
|
||||
|
||||
@http.route('/test_access_denied_http', type='http', auth='public', website=True)
|
||||
def test_denied_error_http(self, **kwargs):
|
||||
raise AccessDenied("This is an access denied http test")
|
||||
|
||||
@@ -30,6 +30,44 @@
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="test_error_view" model="ir.ui.view">
|
||||
<field name="name">Test Error View</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">test_website.test_error_view</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Test Error View" t-name="test_website.test_error_view">
|
||||
<t t-call="website.layout">
|
||||
<div class="container">
|
||||
<h1>Test Error View</h1>
|
||||
<div class="row">
|
||||
<ul class="list-group http_error col-6">
|
||||
<li class="list-group-item list-group-item-primary"><h2>http Errors</h2></li>
|
||||
<li class="list-group-item"><a href="/test_user_error_http">http UserError (400)</a></li>
|
||||
<li class="list-group-item"><a href="/test_validation_error_http">http ValidationError (400)</a></li>
|
||||
<li class="list-group-item"><a href="/test_missing_error_http">http MissingError (400)</a></li>
|
||||
<li class="list-group-item"><a href="/test_karma_error_http">http KarmaError (400)</a></li>
|
||||
<li class="list-group-item"><a href="/test_access_error_http">http AccessError (403)</a></li>
|
||||
<li class="list-group-item"><a href="/test_access_denied_http">http AccessDenied (403)</a></li>
|
||||
<li class="list-group-item"><a href="/test_internal_error_http">http InternalServerError (500)</a></li>
|
||||
<li class="list-group-item"><a href="/test_not_found_http">http NotFound (404)</a></li>
|
||||
</ul>
|
||||
<ul class="list-group rpc_error col-6">
|
||||
<li class="list-group-item list-group-item-primary"><h2>rpc Warnings</h2></li>
|
||||
<li class="list-group-item"><a href="/test_user_error_json">rpc UserError</a></li>
|
||||
<li class="list-group-item"><a href="/test_validation_error_json">rpc ValidationError</a></li>
|
||||
<li class="list-group-item"><a href="/test_missing_error_json">rpc MissingError</a></li>
|
||||
<li class="list-group-item"><a href="/test_karma_error_json">rpc KarmaError</a></li>
|
||||
<li class="list-group-item"><a href="/test_access_error_json">rpc AccessError</a></li>
|
||||
<li class="list-group-item"><a href="/test_access_denied_json">rpc AccessDenied</a></li>
|
||||
<li class="list-group-item list-group-item-primary"><h2>rpc Errors</h2></li>
|
||||
<li class="list-group-item"><a href="/test_internal_error_json">rpc InternalServerError</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="test_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/test_page_view</field>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
odoo.define('website_forum.test_error', function (require) {
|
||||
'use strict';
|
||||
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
publicWidget.registry.testError = publicWidget.Widget.extend({
|
||||
selector: '.rpc_error',
|
||||
events: {
|
||||
'click a': '_onRpcErrorClick',
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Handlers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* make a rpc call with the href of the DOM element clicked
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onRpcErrorClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
var $link = $(ev.currentTarget);
|
||||
return this._rpc({
|
||||
route: $link.attr('href'),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
odoo.define('test_website.error_views', function (require) {
|
||||
'use strict';
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('test_error_website', {
|
||||
test: true,
|
||||
url: '/test_error_view',
|
||||
},
|
||||
[
|
||||
// RPC ERROR
|
||||
{
|
||||
content: "trigger rpc user error",
|
||||
trigger: 'a[href="/test_user_error_json"]',
|
||||
}, {
|
||||
content: "rpc user error modal has message",
|
||||
extra_trigger: 'div.toast-body:contains("This is a user rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger rpc access error",
|
||||
trigger: 'a[href="/test_access_error_json"]',
|
||||
}, {
|
||||
content: "rpc access error modal has message",
|
||||
extra_trigger: 'div.toast-body:contains("This is an access rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger validation rpc error",
|
||||
trigger: 'a[href="/test_validation_error_json"]',
|
||||
}, {
|
||||
content: "rpc validation error modal has message",
|
||||
extra_trigger: 'div.toast-body:contains("This is a validation rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger rpc missing error",
|
||||
trigger: 'a[href="/test_missing_error_json"]',
|
||||
}, {
|
||||
content: "rpc missing error modal has message",
|
||||
extra_trigger: 'div.toast-body:contains("This is a missing rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger rpc error 403",
|
||||
trigger: 'a[href="/test_access_denied_json"]',
|
||||
}, {
|
||||
content: "rpc error 403 modal has message",
|
||||
extra_trigger: 'div.toast-body:contains("This is an access denied rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger rpc missing error",
|
||||
trigger: 'a[href="/test_karma_error_json"]',
|
||||
}, {
|
||||
content: "rpc missing error modal has message",
|
||||
extra_trigger: 'div.toast-body:contains("This is a karma rpc test")',
|
||||
trigger: 'button.o_notification_close',
|
||||
}, {
|
||||
content: "trigger rpc error 500",
|
||||
trigger: 'a[href="/test_internal_error_json"]',
|
||||
}, {
|
||||
content: "rpc error 500 modal is an ErrorDialog",
|
||||
extra_trigger: 'div.o_dialog_error.modal-body div.alert.alert-warning',
|
||||
trigger: 'button.btn.btn-primary[type="button"]',
|
||||
},
|
||||
// HTTP ERROR
|
||||
{
|
||||
content: "trigger http user error",
|
||||
trigger: 'body',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_user_error_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http user error page has title and message",
|
||||
extra_trigger: 'h1:contains("Oops! Something went wrong.")',
|
||||
trigger: 'div.container pre:contains("This is a user http test")',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_user_error_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http user error page debug has title and message open",
|
||||
extra_trigger: 'h1:contains("Oops! Something went wrong.")',
|
||||
trigger: 'div#error_main.collapse.show pre:contains("This is a user http test")',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http user error page debug has traceback closed",
|
||||
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_validation_error_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http validation error page has title and message",
|
||||
extra_trigger: 'h1:contains("Oops! Something went wrong.")',
|
||||
trigger: 'div.container pre:contains("This is a validation http test")',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_validation_error_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http validation error page debug has title and message open",
|
||||
extra_trigger: 'h1:contains("Oops! Something went wrong.")',
|
||||
trigger: 'div#error_main.collapse.show pre:contains("This is a validation http test")',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http validation error page debug has traceback closed",
|
||||
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_access_error_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http access error page has title and message",
|
||||
extra_trigger: 'h1:contains("403: Forbidden")',
|
||||
trigger: 'div.container pre:contains("This is an access http test")',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_access_error_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http access error page debug has title and message open",
|
||||
extra_trigger: 'h1:contains("403: Forbidden")',
|
||||
trigger: 'div#error_main.collapse.show pre:contains("This is an access http test")',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http access error page debug has traceback closed",
|
||||
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_missing_error_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http missing error page has title and message",
|
||||
extra_trigger: 'h1:contains("Oops! Something went wrong.")',
|
||||
trigger: 'div.container pre:contains("This is a missing http test")',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_missing_error_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http missing error page debug has title and message open",
|
||||
extra_trigger: 'h1:contains("Oops! Something went wrong.")',
|
||||
trigger: 'div#error_main.collapse.show pre:contains("This is a missing http test")',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http missing error page debug has traceback closed",
|
||||
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_karma_error_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http karma error page has title and message",
|
||||
extra_trigger: 'h1:contains("Oops! Something went wrong.")',
|
||||
trigger: 'div.container pre:contains("This is a karma http test")',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_karma_error_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http karma error page debug has title and message open",
|
||||
extra_trigger: 'h1:contains("Oops! Something went wrong.")',
|
||||
trigger: 'div#error_main.collapse.show pre:contains("This is a karma http test")',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http karma error page debug has traceback closed",
|
||||
trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_access_denied_http?debug=0';
|
||||
},
|
||||
}, {
|
||||
content: "http error 403 page has title but no message",
|
||||
extra_trigger: 'h1:contains("403: Forbidden")',
|
||||
trigger: 'div#wrap:not(:has(pre:contains("This is an access denied http test"))', //See ir_http.py handle_exception, the exception is replaced so there is no message !
|
||||
run: function () {
|
||||
window.location.href = window.location.origin + '/test_access_denied_http?debug=1';
|
||||
},
|
||||
}, {
|
||||
content: "http 403 error page debug has title but no message",
|
||||
extra_trigger: 'h1:contains("403: Forbidden")',
|
||||
trigger: 'div#debug_infos:not(:has(#error_main))',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "http 403 error page debug has traceback open",
|
||||
trigger: 'body:has(div#error_traceback.collapse.show pre#exception_traceback)',
|
||||
run: function () {},
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -3,5 +3,6 @@
|
||||
|
||||
from . import test_controller_args
|
||||
from . import test_reset_views
|
||||
from . import test_error
|
||||
from . import test_views_during_module_operation
|
||||
from . import test_multi_company
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import odoo.tests
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
class TestWebsiteError(odoo.tests.HttpCase):
|
||||
|
||||
@mute_logger('odoo.addons.website.models.ir_http', 'odoo.http')
|
||||
def test_01_run_test(self):
|
||||
self.start_tour("/test_error_view", 'test_error_website')
|
||||
@@ -1,8 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_frontend" inherit_id="website.assets_frontend">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/test_website/static/src/js/test_error.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_tests" name="Test Website Assets Tests" inherit_id="web.assets_tests">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/test_website/static/tests/tours/reset_views.js"></script>
|
||||
<script type="text/javascript" src="/test_website/static/tests/tours/reset_views.js"></script>
|
||||
<script type="text/javascript" src="/test_website/static/tests/tours/error_views.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -42,8 +42,7 @@ from odoo.tools.translate import _
|
||||
from odoo.tools.misc import str2bool, xlwt, file_open
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
from odoo import http, tools
|
||||
from odoo.http import content_disposition, dispatch_rpc, request, \
|
||||
serialize_exception as _serialize_exception, Response
|
||||
from odoo.http import content_disposition, dispatch_rpc, request, Response
|
||||
from odoo.exceptions import AccessError, UserError, AccessDenied
|
||||
from odoo.models import check_method_name
|
||||
from odoo.service import db, security
|
||||
@@ -83,7 +82,7 @@ def serialize_exception(f):
|
||||
return f(*args, **kwargs)
|
||||
except Exception as e:
|
||||
_logger.exception("An exception occured during an http request")
|
||||
se = _serialize_exception(e)
|
||||
se = request.registry['ir.http'].serialize_exception(e)
|
||||
error = {
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
@@ -1694,7 +1693,7 @@ class ReportController(http.Controller):
|
||||
else:
|
||||
return
|
||||
except Exception as e:
|
||||
se = _serialize_exception(e)
|
||||
se = request.env['ir.http'].serialize_exception(e)
|
||||
error = {
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
|
||||
@@ -15,9 +15,8 @@ var ActionManager = require('web.ActionManager');
|
||||
var concurrency = require('web.concurrency');
|
||||
var core = require('web.core');
|
||||
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 Dialog = require('web.Dialog');
|
||||
var dom = require('web.dom');
|
||||
var KeyboardNavigationMixin = require('web.KeyboardNavigationMixin');
|
||||
var Loading = require('web.Loading');
|
||||
@@ -29,7 +28,6 @@ var utils = require('web.utils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var _t = core._t;
|
||||
var qweb = core.qweb;
|
||||
|
||||
var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMixin, {
|
||||
dependencies: ['notification'],
|
||||
@@ -202,46 +200,6 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
|
||||
});
|
||||
core.bus.on('connection_lost', this, this._onConnectionLost);
|
||||
core.bus.on('connection_restored', this, this._onConnectionRestored);
|
||||
|
||||
// crash manager integration
|
||||
core.bus.on('rpc_error', crash_manager, crash_manager.rpc_error);
|
||||
window.onerror = function (message, file, line, col, error) {
|
||||
// Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror.
|
||||
// The browser will just give you a 'Script error.' as message and nothing else for security issue.
|
||||
// To enable onerror to work properly with CORS file, you should:
|
||||
// 1. add crossorigin="anonymous" to your <script> tag loading the file
|
||||
// 2. enabling 'Access-Control-Allow-Origin' on the server serving the file.
|
||||
// Since in some case it wont be possible to to this, this handle should have the possibility to be
|
||||
// handled by the script manipulating the injected file. For this, you will use window.onOriginError
|
||||
// If it is not handled, we should display something clearer than the common crash_manager error dialog
|
||||
// since it won't show anything except "Script error."
|
||||
// This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
|
||||
if (!file && !line && !col) {
|
||||
// Chrome and Opera set "Script error." on the `message` and hide the `error`
|
||||
// Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error`
|
||||
if (window.onOriginError) {
|
||||
window.onOriginError();
|
||||
delete window.onOriginError;
|
||||
} else {
|
||||
crash_manager.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: _t("Unknown CORS error"),
|
||||
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// ignore Chrome video internal error: https://crbug.com/809574
|
||||
if (!error && message === 'ResizeObserver loop limit exceeded') {
|
||||
return;
|
||||
}
|
||||
var traceback = error ? error.stack : '';
|
||||
crash_manager.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: message,
|
||||
data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback},
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
set_action_manager: function () {
|
||||
var self = this;
|
||||
@@ -373,7 +331,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
|
||||
* @private
|
||||
*/
|
||||
_onConnectionLost: function () {
|
||||
this.connectionNotificationID = this.call('notification', 'notify', {
|
||||
this.connectionNotificationID = this.displayNotification({
|
||||
title: _t('Connection lost'),
|
||||
message: _t('Trying to reconnect...'),
|
||||
sticky: true
|
||||
@@ -387,7 +345,8 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
|
||||
_onConnectionRestored: function () {
|
||||
if (this.connectionNotificationID) {
|
||||
this.call('notification', 'close', this.connectionNotificationID);
|
||||
this.call('notification', 'notify', {
|
||||
this.displayNotification({
|
||||
type: 'info',
|
||||
title: _t('Connection restored'),
|
||||
message: _t('You are back online'),
|
||||
sticky: false
|
||||
@@ -433,11 +392,9 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
|
||||
_onDisplayWarning: function (e) {
|
||||
var data = e.data;
|
||||
if (data.type === 'dialog') {
|
||||
new Dialog(this, {
|
||||
size: 'medium',
|
||||
new WarningDialog(this, {
|
||||
title: data.title,
|
||||
$content: qweb.render("CrashManager.warning", data),
|
||||
}).open({shouldFocusButtons: true});
|
||||
}, data).open();
|
||||
} else {
|
||||
this.call('notification', 'notify', e.data);
|
||||
}
|
||||
|
||||
@@ -27,11 +27,7 @@ function _genericJsonRpc (fct_name, params, settings, fct) {
|
||||
core.bus.trigger('rpc:result', data, result);
|
||||
if (result.error !== undefined) {
|
||||
if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") {
|
||||
var func = console.error;
|
||||
if (result.error.data.exception_type === "user_error") {
|
||||
func = console.log;
|
||||
}
|
||||
func(
|
||||
console.debug(
|
||||
"Server application error\n",
|
||||
"Error code:", result.error.code, "\n",
|
||||
"Error message:", result.error.message, "\n",
|
||||
|
||||
@@ -225,34 +225,42 @@ var ServicesMixin = {
|
||||
/**
|
||||
* Displays a notification.
|
||||
*
|
||||
* @param {string} title
|
||||
* @param {string} [message]
|
||||
* @param {string} [type='warning'] 'info', 'success', 'warning', 'danger' or ''
|
||||
* @param {boolean} [sticky=false]
|
||||
* @param {string} [className]
|
||||
* @param {Object} options
|
||||
* @param {string} options.title
|
||||
* @param {string} [options.subtitle]
|
||||
* @param {string} [options.message]
|
||||
* @param {string} [options.type='warning'] 'info', 'success', 'warning', 'danger' or ''
|
||||
* @param {boolean} [options.sticky=false]
|
||||
* @param {string} [options.className]
|
||||
*/
|
||||
displayNotification: function (title, message, type, sticky, className) {
|
||||
return this.call('notification', 'notify', {
|
||||
type: type,
|
||||
displayNotification: function (options) {
|
||||
return this.call('notification', 'notify', options);
|
||||
},
|
||||
/**
|
||||
* @deprecated will be removed as soon as the notification system is reviewed
|
||||
* @see displayNotification
|
||||
*/
|
||||
do_notify: function (title, message, sticky, className) {
|
||||
return this.displayNotification({
|
||||
type: 'warning',
|
||||
title: title,
|
||||
message: message,
|
||||
sticky: sticky,
|
||||
className: className,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @deprecated will be removed as soon as the notification system is reviewed
|
||||
* @see displayNotification
|
||||
*/
|
||||
do_notify: function (title, message, sticky, className) {
|
||||
return this.displayNotification(title, message, 'warning', sticky, className);
|
||||
},
|
||||
/**
|
||||
* @deprecated will be removed as soon as the notification system is reviewed
|
||||
* @see displayNotification
|
||||
*/
|
||||
do_warn: function (title, message, sticky, className) {
|
||||
return this.displayNotification(title, message, 'danger', sticky, className);
|
||||
return this.displayNotification({
|
||||
type: 'danger',
|
||||
title: title,
|
||||
message: message,
|
||||
sticky: sticky,
|
||||
className: className,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,27 +4,128 @@ odoo.define('web.CrashManager', function (require) {
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var mixins = require('web.mixins');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
var ServiceProviderMixin = require('web.ServiceProviderMixin');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
|
||||
var map_title ={
|
||||
user_error: _lt('Warning'),
|
||||
warning: _lt('Warning'),
|
||||
access_error: _lt('Access Error'),
|
||||
missing_error: _lt('Missing Record'),
|
||||
validation_error: _lt('Validation Error'),
|
||||
except_orm: _lt('Global Business Error'),
|
||||
access_denied: _lt('Access Denied'),
|
||||
user_error: _lt("User Error"),
|
||||
warning: _lt("Warning"),
|
||||
access_error: _lt("Access Error"),
|
||||
missing_error: _lt("Missing Record"),
|
||||
validation_error: _lt("Validation Error"),
|
||||
except_orm: _lt("Global Business Error"),
|
||||
access_denied: _lt("Access Denied"),
|
||||
karma_error: _lt("Karma Error"),
|
||||
};
|
||||
|
||||
var CrashManager = core.Class.extend({
|
||||
/**
|
||||
* 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(mixins.EventDispatcherMixin, ServicesMixin, ServiceProviderMixin,{
|
||||
init: function () {
|
||||
var self = this;
|
||||
this.active = true;
|
||||
this.isConnected = true;
|
||||
|
||||
mixins.EventDispatcherMixin.init.call(this);
|
||||
|
||||
// crash manager integration
|
||||
core.bus.on('rpc_error', this, this.rpc_error);
|
||||
window.onerror = function (message, file, line, col, error) {
|
||||
// Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror.
|
||||
// The browser will just give you a 'Script error.' as message and nothing else for security issue.
|
||||
// To enable onerror to work properly with CORS file, you should:
|
||||
// 1. add crossorigin="anonymous" to your <script> tag loading the file
|
||||
// 2. enabling 'Access-Control-Allow-Origin' on the server serving the file.
|
||||
// Since in some case it wont be possible to to this, this handle should have the possibility to be
|
||||
// handled by the script manipulating the injected file. For this, you will use window.onOriginError
|
||||
// If it is not handled, we should display something clearer than the common crash_manager error dialog
|
||||
// since it won't show anything except "Script error."
|
||||
// This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
|
||||
if (!file && !line && !col) {
|
||||
// Chrome and Opera set "Script error." on the `message` and hide the `error`
|
||||
// Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error`
|
||||
if (window.onOriginError) {
|
||||
window.onOriginError();
|
||||
delete window.onOriginError;
|
||||
} else {
|
||||
self.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: _t("Unknown CORS error"),
|
||||
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// ignore Chrome video internal error: https://crbug.com/809574
|
||||
if (!error && message === 'ResizeObserver loop limit exceeded') {
|
||||
return;
|
||||
}
|
||||
var traceback = error ? error.stack : '';
|
||||
self.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: message,
|
||||
data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// listen to unhandled rejected promises, and throw an error when the
|
||||
// promise has been rejected due to a crash
|
||||
window.addEventListener('unhandledrejection', function (ev) {
|
||||
@@ -43,6 +144,7 @@ var CrashManager = core.Class.extend({
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
ServiceProviderMixin.init.call(this);
|
||||
},
|
||||
enable: function () {
|
||||
this.active = true;
|
||||
@@ -89,14 +191,14 @@ var CrashManager = core.Class.extend({
|
||||
return;
|
||||
}
|
||||
if (_.has(map_title, error.data.exception_type)) {
|
||||
if(error.data.exception_type === 'except_orm'){
|
||||
if(error.data.arguments[1]) {
|
||||
if (error.data.exception_type === 'except_orm') {
|
||||
if (error.data.arguments[1]) {
|
||||
error = _.extend({}, error,
|
||||
{
|
||||
data: _.extend({}, error.data,
|
||||
{
|
||||
message: error.data.arguments[1],
|
||||
title: error.data.arguments[0] !== 'Warning' ? (" - " + error.data.arguments[0]) : '',
|
||||
title: error.data.arguments[0] !== 'Warning' ? error.data.arguments[0] : '',
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -117,38 +219,41 @@ var CrashManager = core.Class.extend({
|
||||
data: _.extend({}, error.data,
|
||||
{
|
||||
message: error.data.arguments[0],
|
||||
title: map_title[error.data.exception_type] !== 'Warning' ? (" - " + map_title[error.data.exception_type]) : '',
|
||||
title: map_title[error.data.exception_type] !== 'Warning' ? map_title[error.data.exception_type] : '',
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
this.show_warning(error);
|
||||
//InternalError
|
||||
|
||||
} else {
|
||||
this.show_error(error);
|
||||
}
|
||||
},
|
||||
show_warning: function(error) {
|
||||
show_warning: function (error, options) {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
return new Dialog(this, {
|
||||
size: 'medium',
|
||||
title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"),
|
||||
subtitle: error.data.title,
|
||||
$content: $(QWeb.render('CrashManager.warning', {error: error}))
|
||||
}).open({shouldFocusButtons:true});
|
||||
var message = error.data ? error.data.message : error.message;
|
||||
var title = _.str.capitalize(error.type) || _t("Oops Something went wrong !");
|
||||
var subtitle = error.data ? error.data.title : error.title;
|
||||
this.displayNotification(_.extend({
|
||||
title: title,
|
||||
message: message,
|
||||
subtitle: subtitle,
|
||||
sticky: true,
|
||||
}, options));
|
||||
},
|
||||
show_error: function(error) {
|
||||
show_error: function (error) {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
var dialog = new Dialog(this, {
|
||||
title: _.str.capitalize(error.type || error.message) || _t("Odoo Error"),
|
||||
$content: $(QWeb.render('CrashManager.error', {error: error}))
|
||||
var dialog = new ErrorDialog(this, {
|
||||
title: _.str.capitalize(error.type) || _t("Odoo Error"),
|
||||
}, {
|
||||
message: error.message,
|
||||
traceback: error.data.debug,
|
||||
});
|
||||
|
||||
|
||||
// When the dialog opens, initialize the copy feature and destroy it when the dialog is closed
|
||||
var $clipboardBtn;
|
||||
var clipboard;
|
||||
@@ -216,7 +321,7 @@ var ExceptionHandler = {
|
||||
* Handle redirection warnings, which behave more or less like a regular
|
||||
* warning, with an additional redirection button.
|
||||
*/
|
||||
var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
|
||||
var RedirectWarningHandler = Widget.extend(ExceptionHandler, {
|
||||
init: function(parent, error) {
|
||||
this._super(parent);
|
||||
this.error = error;
|
||||
@@ -224,10 +329,8 @@ var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
|
||||
display: function() {
|
||||
var self = this;
|
||||
var error = this.error;
|
||||
error.data.message = error.data.arguments[0];
|
||||
|
||||
new Dialog(this, {
|
||||
size: 'medium',
|
||||
new WarningDialog(this, {
|
||||
title: _.str.capitalize(error.type) || _t("Odoo Warning"),
|
||||
buttons: [
|
||||
{text: error.data.arguments[2], classes : "btn-primary", click: function() {
|
||||
@@ -239,8 +342,9 @@ var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
|
||||
location.reload();
|
||||
}},
|
||||
{text: _t("Cancel"), click: function() { self.destroy(); }, close: true}
|
||||
],
|
||||
$content: QWeb.render('CrashManager.warning', {error: error}),
|
||||
]
|
||||
}, {
|
||||
message: error.data.arguments[0],
|
||||
}).open();
|
||||
}
|
||||
});
|
||||
@@ -250,9 +354,9 @@ core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandle
|
||||
function session_expired(cm) {
|
||||
return {
|
||||
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('werkzeug.exceptions.Forbidden', session_expired);
|
||||
@@ -262,18 +366,22 @@ core.crash_registry.add('504', function (cm) {
|
||||
display: function () {
|
||||
cm.show_warning({
|
||||
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) {
|
||||
"use strict";
|
||||
|
||||
var CrashManager = require('web.CrashManager');
|
||||
var CrashManager = require('web.CrashManager').CrashManager;
|
||||
return new CrashManager();
|
||||
|
||||
});
|
||||
|
||||
@@ -60,6 +60,7 @@ var NotificationService = AbstractService.extend({
|
||||
* @param {function} [params.Notification] javascript class of a notification
|
||||
* to instantiate by default use 'web.Notification'
|
||||
* @param {string} params.title notification title
|
||||
* @param {string} params.subtitle notification subtitle
|
||||
* @param {string} params.message notification main message
|
||||
* @param {string} params.type 'notification' or 'warning'
|
||||
* @param {boolean} [params.sticky=false] if true, the notification will stay
|
||||
|
||||
@@ -25,6 +25,7 @@ var Notification = Widget.extend({
|
||||
* @param {Widget} parent
|
||||
* @param {Object} params
|
||||
* @param {string} params.title
|
||||
* @param {string} params.subtitle
|
||||
* @param {string} [params.message]
|
||||
* @param {string} [params.type='warning'] 'info', 'success', 'warning', 'danger' or ''
|
||||
* @param {boolean} [params.sticky=false] if true, the notification will
|
||||
@@ -41,6 +42,7 @@ var Notification = Widget.extend({
|
||||
init: function (parent, params) {
|
||||
this._super.apply(this, arguments);
|
||||
this.title = params.title;
|
||||
this.subtitle = params.subtitle;
|
||||
this.message = params.message;
|
||||
this.buttons = params.buttons || [];
|
||||
this.sticky = !!this.buttons.length || !!params.sticky;
|
||||
|
||||
@@ -120,30 +120,6 @@
|
||||
</ul>
|
||||
</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">
|
||||
<div class="o_form_view">
|
||||
<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>
|
||||
@@ -16,7 +16,9 @@
|
||||
<span class="d-inline" aria-hidden="true">&times;</span>
|
||||
</button>
|
||||
</div>
|
||||
<div t-if="widget.message or widget.buttons.length" class="toast-body">
|
||||
<div t-if="widget.message or widget.subtitle or widget.buttons.length" class="toast-body">
|
||||
<strong t-if="widget.subtitle" t-raw="widget.subtitle"
|
||||
class="o_notification_subtitle"/>
|
||||
<div t-if="widget.message" t-raw="widget.message"
|
||||
class="o_notification_content"/>
|
||||
<div t-if="widget.buttons.length" class="mt-2 o_notification_buttons">
|
||||
|
||||
@@ -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/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/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/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/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/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/report_service.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/services/session.js"></script>
|
||||
|
||||
@@ -214,6 +214,11 @@ class Http(models.AbstractModel):
|
||||
traceback=traceback.format_exc(),
|
||||
)
|
||||
|
||||
# only except_orm exceptions contain a message
|
||||
if isinstance(exception, odoo.exceptions.except_orm):
|
||||
values['error_message'] = exception.name
|
||||
code = 400
|
||||
|
||||
if isinstance(exception, werkzeug.exceptions.HTTPException):
|
||||
if exception.code is None:
|
||||
# Hand-crafted HTTPException likely coming from abort(),
|
||||
@@ -279,6 +284,8 @@ class Http(models.AbstractModel):
|
||||
values['editable'] = request.uid and request.website.is_publisher()
|
||||
elif code == 403:
|
||||
logger.warn("403 Forbidden:\n\n%s", values['traceback'])
|
||||
elif code == 400:
|
||||
logger.warn("400 Bad Request:\n\n%s", values['traceback'])
|
||||
try:
|
||||
html = env['ir.ui.view'].render_template('website.%s' % view_id, values)
|
||||
except Exception:
|
||||
|
||||
@@ -911,19 +911,22 @@
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="error_message">
|
||||
<p t-if="error_message">
|
||||
<strong>Error message:</strong>
|
||||
<pre t-esc="error_message"/>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template id="http_error_debug">
|
||||
<div class="container accordion mb32 mt32" id="debug_infos">
|
||||
<div class="card" t-if="exception">
|
||||
<div class="card" t-if="error_message">
|
||||
<h4 class="card-header">
|
||||
<a data-toggle="collapse" href="#error_main">Error</a>
|
||||
</h4>
|
||||
<div id="error_main" class="collapse in">
|
||||
<div id="error_main" class="collapse show">
|
||||
<div class="card-body">
|
||||
<p t-if="website_controller">The following error was raised in the website controller <code t-esc="website_controller"/></p>
|
||||
<p>
|
||||
<strong>Error message:</strong>
|
||||
<pre t-esc="exception"/>
|
||||
</p>
|
||||
<t t-call="website.error_message"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -931,7 +934,7 @@
|
||||
<h4 class="card-header">
|
||||
<a data-toggle="collapse" href="#error_qweb">QWeb</a>
|
||||
</h4>
|
||||
<div id="error_qweb" class="collapse">
|
||||
<div id="error_qweb" class="collapse show">
|
||||
<div class="card-body">
|
||||
<p t-if="exception.message">
|
||||
<strong>Error message:</strong>
|
||||
@@ -948,7 +951,7 @@
|
||||
<h4 class="card-header">
|
||||
<a data-toggle="collapse" href="#error_traceback">Traceback</a>
|
||||
</h4>
|
||||
<div id="error_traceback" class="collapse">
|
||||
<div id="error_traceback" t-attf-class="collapse #{not error_message and not qweb_exception and 'show'}">
|
||||
<div class="card-body">
|
||||
<pre id="exception_traceback" t-esc="traceback"/>
|
||||
</div>
|
||||
@@ -957,21 +960,40 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="400">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="container">
|
||||
<h1 class="mt-5">Oops! Something went wrong.</h1>
|
||||
<p>Take a look at the error message below.</p>
|
||||
</div>
|
||||
<t t-if="editable or request.session.debug">
|
||||
<t t-call="website.http_error_debug"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="container">
|
||||
<t t-call="website.error_message"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="403">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="container">
|
||||
<h1 class="mt32">403: Forbidden</h1>
|
||||
<h1 class="mt-5">403: Forbidden</h1>
|
||||
<p>The page you were looking for could not be authorized.</p>
|
||||
<p>Maybe you were looking for one of these popular pages ?</p>
|
||||
<ul>
|
||||
<li><a href="/">Homepage</a></li>
|
||||
<li><a href="/contactus">Contact Us</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<t t-if="editable or debug">
|
||||
<t t-call="website.http_error_debug"/>
|
||||
</t>
|
||||
<t t-if="editable or request.session.debug">
|
||||
<t t-call="website.http_error_debug"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="container">
|
||||
<t t-call="website.error_message"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
@@ -982,7 +1004,7 @@
|
||||
<t t-raw="0"/>
|
||||
<div class="oe_structure oe_empty">
|
||||
<div class="container">
|
||||
<h1 class="mt32">404: Page not found!</h1>
|
||||
<h1 class="mt-5">404: Page not found!</h1>
|
||||
<p>
|
||||
The page you were looking for could not be found; it is possible you have
|
||||
typed the address incorrectly, but it has most probably been removed due
|
||||
@@ -995,10 +1017,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-if="debug">
|
||||
<t t-call="website.http_error_debug"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
@@ -59,12 +59,6 @@ class WebsiteForum(WebsiteProfile):
|
||||
})
|
||||
return "/forum/%s" % slug(forum_id)
|
||||
|
||||
@http.route('/forum/notification_read', type='json', auth="user", methods=['POST'], website=True)
|
||||
def notification_read(self, **kwargs):
|
||||
if kwargs.get('notification_id'):
|
||||
request.env['mail.message'].browse([int(kwargs.get('notification_id'))]).set_message_done()
|
||||
return True
|
||||
|
||||
def sitemap_forum(env, rule, qs):
|
||||
Forum = env['forum.forum']
|
||||
dom = sitemap_qs2dom(qs, '/forum', Forum._rec_name)
|
||||
|
||||
@@ -384,7 +384,7 @@ class Post(models.Model):
|
||||
filter_regexp = r'(<img.*?>)|(<a[^>]*?href[^>]*?>)|(<[a-z|A-Z]+[^>]*style\s*=\s*[\'"][^\'"]*\s*background[^:]*:[^url;]*url)'
|
||||
content_match = re.search(filter_regexp, content, re.I)
|
||||
if content_match:
|
||||
raise KarmaError('User karma not sufficient to post an image or link.')
|
||||
raise KarmaError(_('%d karma required to post an image or link.') % forum.karma_editor)
|
||||
return content
|
||||
|
||||
def _default_website_meta(self):
|
||||
@@ -412,9 +412,9 @@ class Post(models.Model):
|
||||
raise UserError(_('Posting answer on a [Deleted] or [Closed] question is not possible.'))
|
||||
# karma-based access
|
||||
if not post.parent_id and not post.can_ask:
|
||||
raise KarmaError('You don\'t have enough karma to create a new question.')
|
||||
raise KarmaError(_('%d karma required to create a new question.') % post.forum_id.karma_ask)
|
||||
elif post.parent_id and not post.can_answer:
|
||||
raise KarmaError('You don\'t have enough karma to answer a question.')
|
||||
raise KarmaError(_('%d karma required to answer a question.') % post.forum_id.karma_answer)
|
||||
if not post.parent_id and not post.can_post:
|
||||
post.sudo().state = 'pending'
|
||||
|
||||
@@ -429,8 +429,9 @@ class Post(models.Model):
|
||||
# XDO FIXME: to be correctly fixed with new get_mail_message_access and filter access rule
|
||||
if operation in ('write', 'unlink') and (not model_name or model_name == 'forum.post'):
|
||||
# Make sure only author or moderator can edit/delete messages
|
||||
if any(not post.can_edit for post in self.browse(res_ids)):
|
||||
raise KarmaError('Not enough karma to edit a post.')
|
||||
for post in self.browse(res_ids):
|
||||
if not post.can_edit:
|
||||
raise KarmaError(_('%d karma required to edit a post.') % post.karma_edit)
|
||||
return super(Post, self).get_mail_message_access(res_ids, operation, model_name=model_name)
|
||||
|
||||
@api.multi
|
||||
@@ -438,33 +439,37 @@ class Post(models.Model):
|
||||
trusted_keys = ['active', 'is_correct', 'tag_ids'] # fields where security is checked manually
|
||||
if 'content' in vals:
|
||||
vals['content'] = self._update_content(vals['content'], self.forum_id.id)
|
||||
if 'state' in vals:
|
||||
if vals['state'] in ['active', 'close']:
|
||||
if any(not post.can_close for post in self):
|
||||
raise KarmaError('Not enough karma to close or reopen a post.')
|
||||
trusted_keys += ['state', 'closed_uid', 'closed_date', 'closed_reason_id']
|
||||
elif vals['state'] == 'flagged':
|
||||
if any(not post.can_flag for post in self):
|
||||
raise KarmaError('Not enough karma to flag a post.')
|
||||
trusted_keys += ['state', 'flag_user_id']
|
||||
if 'active' in vals:
|
||||
if any(not post.can_unlink for post in self):
|
||||
raise KarmaError('Not enough karma to delete or reactivate a post')
|
||||
if 'is_correct' in vals:
|
||||
if any(not post.can_accept for post in self):
|
||||
raise KarmaError('Not enough karma to accept or refuse an answer')
|
||||
# update karma except for self-acceptance
|
||||
mult = 1 if vals['is_correct'] else -1
|
||||
for post in self:
|
||||
|
||||
tag_ids = False
|
||||
if 'tag_ids' in vals:
|
||||
tag_ids = set(tag.get('id') for tag in self.resolve_2many_commands('tag_ids', vals['tag_ids']))
|
||||
|
||||
for post in self:
|
||||
if 'state' in vals:
|
||||
if vals['state'] in ['active', 'close']:
|
||||
if not post.can_close:
|
||||
raise KarmaError(_('%d karma required to close or reopen a post.') % post.karma_close)
|
||||
trusted_keys += ['state', 'closed_uid', 'closed_date', 'closed_reason_id']
|
||||
elif vals['state'] == 'flagged':
|
||||
if not post.can_flag:
|
||||
raise KarmaError(_('%d karma required to flag a post.') % post.forum_id.karma_flag)
|
||||
trusted_keys += ['state', 'flag_user_id']
|
||||
if 'active' in vals:
|
||||
if not post.can_unlink:
|
||||
raise KarmaError(_('%d karma required to delete or reactivate a post.') % post.karma_unlink)
|
||||
if 'is_correct' in vals:
|
||||
if not post.can_accept:
|
||||
raise KarmaError(_('%d karma required to accept or refuse an answer.') % post.karma_accept)
|
||||
# update karma except for self-acceptance
|
||||
mult = 1 if vals['is_correct'] else -1
|
||||
if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid:
|
||||
post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * mult)
|
||||
self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accept * mult)
|
||||
if 'tag_ids' in vals:
|
||||
tag_ids = set(tag.get('id') for tag in self.resolve_2many_commands('tag_ids', vals['tag_ids']))
|
||||
if any(set(post.tag_ids.ids) != tag_ids for post in self) and any(self.env.user.karma < post.forum_id.karma_edit_retag for post in self):
|
||||
raise KarmaError(_('Not enough karma to retag.'))
|
||||
if any(key not in trusted_keys for key in vals) and any(not post.can_edit for post in self):
|
||||
raise KarmaError('Not enough karma to edit a post.')
|
||||
if tag_ids:
|
||||
if set(post.tag_ids.ids) != tag_ids and self.env.user.karma < post.forum_id.karma_edit_retag:
|
||||
raise KarmaError(_('%d karma required to retag.') % post.forum_id.karma_edit_retag)
|
||||
if any(key not in trusted_keys for key in vals) and not post.can_edit:
|
||||
raise KarmaError(_('%d karma required to edit a post.') % post.karma_edit)
|
||||
|
||||
res = super(Post, self).write(vals)
|
||||
|
||||
@@ -566,7 +571,7 @@ class Post(models.Model):
|
||||
@api.one
|
||||
def validate(self):
|
||||
if not self.can_moderate:
|
||||
raise KarmaError('Not enough karma to validate a post')
|
||||
raise KarmaError(_('%d karma required to validate a post.') % self.forum_id.karma_moderate)
|
||||
|
||||
# if state == pending, no karma previously added for the new question
|
||||
if self.state == 'pending':
|
||||
@@ -583,7 +588,7 @@ class Post(models.Model):
|
||||
@api.one
|
||||
def refuse(self):
|
||||
if not self.can_moderate:
|
||||
raise KarmaError(_('Not enough karma to refuse a post'))
|
||||
raise KarmaError(_('%d karma required to refuse a post.') % self.forum_id.karma_moderate)
|
||||
|
||||
self.moderator_id = self.env.user
|
||||
return True
|
||||
@@ -591,7 +596,7 @@ class Post(models.Model):
|
||||
@api.one
|
||||
def flag(self):
|
||||
if not self.can_flag:
|
||||
raise KarmaError('Not enough karma to flag a post')
|
||||
raise KarmaError(_('%d karma required to flag a post.') % self.forum_id.karma_flag)
|
||||
|
||||
if(self.state == 'flagged'):
|
||||
return {'error': 'post_already_flagged'}
|
||||
@@ -607,7 +612,7 @@ class Post(models.Model):
|
||||
@api.one
|
||||
def mark_as_offensive(self, reason_id):
|
||||
if not self.can_moderate:
|
||||
raise KarmaError('Not enough karma to mark a post as offensive')
|
||||
raise KarmaError(_('%d karma required to mark a post as offensive.') % self.forum_id.karma_moderate)
|
||||
|
||||
# remove some karma
|
||||
_logger.info('Downvoting user <%s> for posting spam/offensive contents', self.create_uid)
|
||||
@@ -638,8 +643,9 @@ class Post(models.Model):
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
if any(not post.can_unlink for post in self):
|
||||
raise KarmaError('Not enough karma to unlink a post')
|
||||
for post in self:
|
||||
if not post.can_unlink:
|
||||
raise KarmaError(_('%d karma required to unlink a post.') % post.karma_unlink)
|
||||
# if unlinking an answer with accepted answer: remove provided karma
|
||||
for post in self:
|
||||
if post.is_correct:
|
||||
@@ -687,7 +693,7 @@ class Post(models.Model):
|
||||
|
||||
# karma-based action check: use the post field that computed own/all value
|
||||
if not self.can_comment_convert:
|
||||
raise KarmaError('Not enough karma to convert an answer to a comment')
|
||||
raise KarmaError(_('%d karma required to convert an answer to a comment.') % self.karma_comment_convert)
|
||||
|
||||
# post the message
|
||||
question = self.parent_id
|
||||
@@ -719,10 +725,16 @@ class Post(models.Model):
|
||||
return False
|
||||
|
||||
# karma-based action check: must check the message's author to know if own / all
|
||||
karma_convert = comment.author_id.id == self.env.user.partner_id.id and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all
|
||||
is_author = comment.author_id.id == self.env.user.partner_id.id
|
||||
karma_own = post.forum_id.karma_comment_convert_own
|
||||
karma_all = post.forum_id.karma_comment_convert_all
|
||||
karma_convert = is_author and karma_own or karma_all
|
||||
can_convert = self.env.user.karma >= karma_convert
|
||||
if not can_convert:
|
||||
raise KarmaError('Not enough karma to convert a comment to an answer')
|
||||
if is_author and karma_own < karma_all:
|
||||
raise KarmaError(_('%d karma required to convert your comment to an answer.') % karma_own)
|
||||
else:
|
||||
raise KarmaError(_('%d karma required to convert a comment to an answer.') % karma_all)
|
||||
|
||||
# check the message's author has not already an answer
|
||||
question = post.parent_id if post.parent_id else post
|
||||
@@ -754,7 +766,7 @@ class Post(models.Model):
|
||||
karma_unlink = comment.author_id.id == user.partner_id.id and self.forum_id.karma_comment_unlink_own or self.forum_id.karma_comment_unlink_all
|
||||
can_unlink = user.karma >= karma_unlink
|
||||
if not can_unlink:
|
||||
raise KarmaError('Not enough karma to unlink a comment')
|
||||
raise KarmaError(_('%d karma required to unlink a comment.') % karma_unlink)
|
||||
return comment.unlink()
|
||||
|
||||
@api.multi
|
||||
@@ -803,7 +815,7 @@ class Post(models.Model):
|
||||
|
||||
self.ensure_one()
|
||||
if not self.can_comment:
|
||||
raise KarmaError(_('Not enough karma to comment'))
|
||||
raise KarmaError(_('%d karma required to comment.') % self.karma_comment)
|
||||
if not kwargs.get('record_name') and self.parent_id:
|
||||
kwargs['record_name'] = self.parent_id.name
|
||||
return super(Post, self).message_post(message_type=message_type, **kwargs)
|
||||
@@ -903,9 +915,9 @@ class Vote(models.Model):
|
||||
def _check_karma_rights(self, upvote=None):
|
||||
# karma check
|
||||
if upvote and not self.post_id.can_upvote:
|
||||
raise KarmaError('You don\'t have enough karma to upvote.')
|
||||
raise KarmaError(_('%d karma required to upvote.') % self.post_id.forum_id.karma_upvote)
|
||||
elif not upvote and not self.post_id.can_downvote:
|
||||
raise KarmaError('You don\'t have enough karma to downvote.')
|
||||
raise KarmaError(_('%d karma required to downvote.') % self.post_id.forum_id.karma_downvote)
|
||||
|
||||
def _vote_update_karma(self, old_vote, new_vote):
|
||||
if self.post_id.parent_id:
|
||||
@@ -941,5 +953,5 @@ class Tags(models.Model):
|
||||
def create(self, vals):
|
||||
forum = self.env['forum.forum'].browse(vals.get('forum_id'))
|
||||
if self.env.user.karma < forum.karma_tag_create:
|
||||
raise KarmaError(_('You don\'t have enough karma to create a new Tag.'))
|
||||
raise KarmaError(_('%d karma required to create a new Tag.') % forum.karma_tag_create)
|
||||
return super(Tags, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(vals)
|
||||
|
||||
@@ -2,12 +2,12 @@ odoo.define('website_forum.website_forum', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var crash_manager = require('web.crash_manager');
|
||||
var Wysiwyg = require('web_editor.wysiwyg.root');
|
||||
var publicWidget = require('web.public.widget');
|
||||
var session = require('web.session');
|
||||
var utils = require('web.utils');
|
||||
var qweb = core.qweb;
|
||||
var WebsiteProfile = require('website_profile.website_profile');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
@@ -30,7 +30,6 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
|
||||
'click .accept_answer:not(.karma_required)': '_onAcceptAnswerClick',
|
||||
'click .favourite_question': '_onFavoriteQuestionClick',
|
||||
'click .comment_delete': '_onDeleteCommentClick',
|
||||
'click .notification_close': '_onCloseNotificationClick',
|
||||
'click .js_close_intro': '_onCloseIntroClick',
|
||||
},
|
||||
|
||||
@@ -186,20 +185,20 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
|
||||
}
|
||||
ev.preventDefault();
|
||||
var msg = karma + ' ' + _t("karma is required to perform this action. ");
|
||||
var title = _t("Karma Error");
|
||||
if (forum_id) {
|
||||
msg += '<a class="alert-link" href="/forum/' + forum_id + '/faq">' + _t("Read the guidelines to know how to gain karma.") + '</a>';
|
||||
}
|
||||
if (session.is_website_user) {
|
||||
msg = _t("Sorry you must be logged in to perform this action");
|
||||
title = _t("Access Denied");
|
||||
}
|
||||
var $warning = $('<div class="alert alert-danger alert-dismissible oe_forum_alert" id="karma_alert">' +
|
||||
'<button type="button" class="close notification_close" data-dismiss="alert">×</button>' +
|
||||
msg + '</div>');
|
||||
var $voteAlert = $('#karma_alert');
|
||||
if ($voteAlert.length) {
|
||||
$voteAlert.remove();
|
||||
}
|
||||
$karma.after($warning);
|
||||
crash_manager.show_warning({
|
||||
message: msg,
|
||||
title: title,
|
||||
}, {
|
||||
sticky: false,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -282,35 +281,20 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
|
||||
route: $link.data('href') || ($link.attr('href') !== '#' && $link.attr('href')) || $link.closest('form').attr('action'),
|
||||
}).then(function (data) {
|
||||
if (data.error) {
|
||||
var $warning;
|
||||
var message;
|
||||
if (data.error === 'anonymous_user') {
|
||||
$warning = $(
|
||||
'<div class="alert alert-danger alert-dismissable oe_forum_alert" id="flag_alert">' +
|
||||
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">×</button>' +
|
||||
_t("Sorry you must be logged to flag a post") +
|
||||
'</div>'
|
||||
);
|
||||
message = _t("Sorry you must be logged to flag a post");
|
||||
} else if (data.error === 'post_already_flagged') {
|
||||
$warning = $(
|
||||
'<div class="alert alert-danger alert-dismissable oe_forum_alert" id="flag_alert">' +
|
||||
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">×</button>' +
|
||||
_t("This post is already flagged") +
|
||||
'<button type="button" class="close notification_close" t-att-id="notification.id" data-dismiss="alert" aria-label="Close">×</button>' +
|
||||
'</div>'
|
||||
);
|
||||
message = _t("This post is already flagged");
|
||||
} else if (data.error === 'post_non_flaggable') {
|
||||
$warning = $(
|
||||
'<div class="alert alert-danger alert-dismissable oe_forum_alert" id="flag_alert">' +
|
||||
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">×</button>' +
|
||||
_t("This post can not be flagged") +
|
||||
'<button type="button" class="close notification_close" t-att-id="notification.id" data-dismiss="alert" aria-label="Close">×</button>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
var $flagAlert = $link.parent().find('#flag_alert');
|
||||
if ($flagAlert.length === 0) {
|
||||
$link.parent().append($warning);
|
||||
message = _t("This post can not be flagged");
|
||||
}
|
||||
crash_manager.show_warning({
|
||||
message: message,
|
||||
title: _t("Access Denied"),
|
||||
}, {
|
||||
sticky: false,
|
||||
});
|
||||
} else if (data.success) {
|
||||
var elem = $link;
|
||||
if (data.success === 'post_flagged_moderator') {
|
||||
@@ -338,22 +322,18 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
|
||||
route: $link.data('href'),
|
||||
}).then(function (data) {
|
||||
if (data.error) {
|
||||
var $warning;
|
||||
var message;
|
||||
if (data.error === 'own_post') {
|
||||
$warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="vote_alert">' +
|
||||
'<button type="button" class="close notification_close" data-dismiss="alert">×</button>' +
|
||||
_t('Sorry, you cannot vote for your own posts') +
|
||||
'</div>');
|
||||
message = _t('Sorry, you cannot vote for your own posts');
|
||||
} else if (data.error === 'anonymous_user') {
|
||||
$warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="vote_alert">' +
|
||||
'<button type="button" class="close notification_close" data-dismiss="alert">×</button>' +
|
||||
_t('Sorry you must be logged to vote') +
|
||||
'</div>');
|
||||
}
|
||||
var $voteAlert = $link.parent().find('#vote_alert');
|
||||
if ($voteAlert.length === 0) {
|
||||
$link.parent().append($warning);
|
||||
message = _t('Sorry you must be logged to vote');
|
||||
}
|
||||
crash_manager.show_warning({
|
||||
message: message,
|
||||
title: _t("Access Denied"),
|
||||
}, {
|
||||
sticky: false,
|
||||
});
|
||||
} else {
|
||||
$link.parent().find('.vote_count').html(data.vote_count);
|
||||
if (data.user_vote === 0) {
|
||||
@@ -399,17 +379,14 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
|
||||
}).then(function (data) {
|
||||
if (data.error) {
|
||||
if (data.error === 'anonymous_user') {
|
||||
var $warning = $(
|
||||
'<div class="alert alert-danger alert-dismissable" id="correct_answer_alert" style="position:absolute; margin-top: -30px; margin-left: 90px;">' +
|
||||
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">×</button>' +
|
||||
_t("Sorry, anonymous users cannot choose correct answer.") +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
var $correctAnswerAlert = $link.parent().find('#correct_answer_alert');
|
||||
if ($correctAnswerAlert.length === 0) {
|
||||
$link.parent().append($warning);
|
||||
var message = _t("Sorry, anonymous users cannot choose correct answer.");
|
||||
}
|
||||
crash_manager.show_warning({
|
||||
message: message,
|
||||
title: _t("Access Denied"),
|
||||
}, {
|
||||
sticky: false,
|
||||
});
|
||||
} else {
|
||||
$link.toggleClass('oe_answer_true', !!data)
|
||||
.toggleClass('oe_answer_false', !data);
|
||||
@@ -451,22 +428,6 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
|
||||
$link.parents('.comment').first().remove();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onCloseNotificationClick: function (ev) {
|
||||
if (!session.is_website_user) {
|
||||
ev.preventDefault();
|
||||
var $link = $(ev.currentTarget);
|
||||
this._rpc({
|
||||
route: '/forum/notification_read',
|
||||
params: {
|
||||
notification_id: $link.attr('id'),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
|
||||
@@ -135,12 +135,6 @@ $bronze: #eea91e;
|
||||
}
|
||||
}
|
||||
|
||||
.oe_forum_alert {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.oe_forum_email_required {
|
||||
position: absolute;
|
||||
margin-top: 155px;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<th class="text-left" t-att-colspan="2"><t t-esc="spec"/></th>
|
||||
</tr>
|
||||
</t>
|
||||
<tr t-foreach="atts[spec]" t-as="att" t-att-class="'collapse in show ' + spec.split()[0]">
|
||||
<tr t-foreach="atts[spec]" t-as="att" t-att-class="'collapse show ' + spec.split()[0]">
|
||||
<td t-esc="att.attribute_id.name"/>
|
||||
<td>
|
||||
<t t-set='or_separator'>or</t>
|
||||
@@ -126,7 +126,7 @@
|
||||
<th class="text-left" t-att-colspan="len(products) + 1"><i class="fa fa-chevron-circle-down o_product_comparison_collpase" role="img" aria-label="Collapse" title="Collapse"></i><t t-esc="spec"/></th>
|
||||
</tr>
|
||||
</t>
|
||||
<tr t-foreach="specs[spec].keys()" t-as="att" t-att-class="'collapse in show ' + spec.split()[0]">
|
||||
<tr t-foreach="specs[spec].keys()" t-as="att" t-att-class="'collapse show ' + spec.split()[0]">
|
||||
<td t-esc="att"/>
|
||||
<td t-foreach="specs[spec][att]" t-as="val"><t t-esc="val"/></td>
|
||||
</tr>
|
||||
|
||||
@@ -9,6 +9,7 @@ import mimetypes
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import werkzeug
|
||||
import werkzeug.exceptions
|
||||
@@ -20,7 +21,7 @@ import odoo
|
||||
from odoo import api, http, models, tools, SUPERUSER_ID
|
||||
from odoo.exceptions import AccessDenied, AccessError
|
||||
from odoo.http import request, STATIC_CACHE, content_disposition
|
||||
from odoo.tools import pycompat, consteq
|
||||
from odoo.tools import consteq, pycompat, ustr
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
from odoo.modules.module import get_resource_path, get_module_path
|
||||
|
||||
@@ -178,6 +179,33 @@ class IrHttp(models.AbstractModel):
|
||||
return attach
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def serialize_exception(self, e):
|
||||
tmp = {
|
||||
"name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
|
||||
"debug": traceback.format_exc(),
|
||||
"message": ustr(e),
|
||||
"arguments": e.args,
|
||||
"exception_type": "internal_error"
|
||||
}
|
||||
if isinstance(e, odoo.exceptions.UserError):
|
||||
tmp["exception_type"] = "user_error"
|
||||
elif isinstance(e, odoo.exceptions.Warning):
|
||||
tmp["exception_type"] = "warning"
|
||||
elif isinstance(e, odoo.exceptions.RedirectWarning):
|
||||
tmp["exception_type"] = "warning"
|
||||
elif isinstance(e, odoo.exceptions.AccessError):
|
||||
tmp["exception_type"] = "access_error"
|
||||
elif isinstance(e, odoo.exceptions.MissingError):
|
||||
tmp["exception_type"] = "missing_error"
|
||||
elif isinstance(e, odoo.exceptions.AccessDenied):
|
||||
tmp["exception_type"] = "access_denied"
|
||||
elif isinstance(e, odoo.exceptions.ValidationError):
|
||||
tmp["exception_type"] = "validation_error"
|
||||
elif isinstance(e, odoo.exceptions.except_orm):
|
||||
tmp["exception_type"] = "except_orm"
|
||||
return tmp
|
||||
|
||||
@classmethod
|
||||
def _handle_exception(cls, exception):
|
||||
# in case of Exception, e.g. 404, we don't step into _dispatch
|
||||
|
||||
+17
-37
@@ -98,16 +98,16 @@ def replace_request_password(args):
|
||||
args[2] = '*'
|
||||
return tuple(args)
|
||||
|
||||
|
||||
# don't trigger debugger for those exceptions, they carry user-facing warnings
|
||||
# and indications, they're not necessarily indicative of anything being
|
||||
# *broken*
|
||||
NO_POSTMORTEM = (odoo.osv.orm.except_orm,
|
||||
odoo.exceptions.AccessError,
|
||||
odoo.exceptions.ValidationError,
|
||||
odoo.exceptions.MissingError,
|
||||
NO_POSTMORTEM = (odoo.exceptions.except_orm,
|
||||
odoo.exceptions.AccessDenied,
|
||||
odoo.exceptions.Warning,
|
||||
odoo.exceptions.RedirectWarning)
|
||||
|
||||
|
||||
def dispatch_rpc(service_name, method, params):
|
||||
""" Handle a RPC call.
|
||||
|
||||
@@ -316,7 +316,7 @@ class WebRequest(object):
|
||||
"""Called within an except block to allow converting exceptions
|
||||
to abitrary responses. Anything returned (except None) will
|
||||
be used as response."""
|
||||
self._failed = exception # prevent tx commit
|
||||
self._failed = exception # prevent tx commit
|
||||
if not isinstance(exception, NO_POSTMORTEM) \
|
||||
and not isinstance(exception, werkzeug.exceptions.HTTPException):
|
||||
odoo.tools.debugger.post_mortem(
|
||||
@@ -630,13 +630,18 @@ class JsonRequest(WebRequest):
|
||||
try:
|
||||
return super(JsonRequest, self)._handle_exception(exception)
|
||||
except Exception:
|
||||
if not isinstance(exception, (odoo.exceptions.Warning, SessionExpiredException,
|
||||
odoo.exceptions.except_orm, werkzeug.exceptions.NotFound)):
|
||||
_logger.exception("Exception during JSON request handling.")
|
||||
if not isinstance(exception, SessionExpiredException):
|
||||
if exception.args and exception.args[0] == "bus.Bus not available in test mode":
|
||||
_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 = {
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
'data': serialize_exception(exception)
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
'data': request.registry['ir.http'].serialize_exception(exception),
|
||||
}
|
||||
if isinstance(exception, werkzeug.exceptions.NotFound):
|
||||
error['http_status'] = 404
|
||||
@@ -686,31 +691,6 @@ class JsonRequest(WebRequest):
|
||||
except Exception as e:
|
||||
return self._handle_exception(e)
|
||||
|
||||
def serialize_exception(e):
|
||||
tmp = {
|
||||
"name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__,
|
||||
"debug": traceback.format_exc(),
|
||||
"message": ustr(e),
|
||||
"arguments": e.args,
|
||||
"exception_type": "internal_error"
|
||||
}
|
||||
if isinstance(e, odoo.exceptions.UserError):
|
||||
tmp["exception_type"] = "user_error"
|
||||
elif isinstance(e, odoo.exceptions.Warning):
|
||||
tmp["exception_type"] = "warning"
|
||||
elif isinstance(e, odoo.exceptions.RedirectWarning):
|
||||
tmp["exception_type"] = "warning"
|
||||
elif isinstance(e, odoo.exceptions.AccessError):
|
||||
tmp["exception_type"] = "access_error"
|
||||
elif isinstance(e, odoo.exceptions.MissingError):
|
||||
tmp["exception_type"] = "missing_error"
|
||||
elif isinstance(e, odoo.exceptions.AccessDenied):
|
||||
tmp["exception_type"] = "access_denied"
|
||||
elif isinstance(e, odoo.exceptions.ValidationError):
|
||||
tmp["exception_type"] = "validation_error"
|
||||
elif isinstance(e, odoo.exceptions.except_orm):
|
||||
tmp["exception_type"] = "except_orm"
|
||||
return tmp
|
||||
|
||||
class HttpRequest(WebRequest):
|
||||
""" Handler for the ``http`` request type.
|
||||
@@ -1414,7 +1394,7 @@ class Root(object):
|
||||
httprequest = werkzeug.wrappers.Request(environ)
|
||||
httprequest.app = self
|
||||
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableOrderedMultiDict
|
||||
|
||||
|
||||
current_thread = threading.current_thread()
|
||||
current_thread.url = httprequest.url
|
||||
current_thread.query_count = 0
|
||||
|
||||
Reference in New Issue
Block a user