Merge "Refactor error messages."
This commit is contained in:
commit
5943532672
@ -260,8 +260,13 @@ HANDLE_EXC_METHODS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _append_detail(message, details):
|
||||||
|
return encoding.force_text(message) + '\u2026' + \
|
||||||
|
encoding.force_text(details)
|
||||||
|
|
||||||
|
|
||||||
def handle(request, message=None, redirect=None, ignore=False,
|
def handle(request, message=None, redirect=None, ignore=False,
|
||||||
escalate=False, log_level=None, force_log=None):
|
escalate=False, log_level=None, force_log=None, details=None):
|
||||||
"""Centralized error handling for Horizon.
|
"""Centralized error handling for Horizon.
|
||||||
|
|
||||||
Because Horizon consumes so many different APIs with completely
|
Because Horizon consumes so many different APIs with completely
|
||||||
@ -288,6 +293,9 @@ def handle(request, message=None, redirect=None, ignore=False,
|
|||||||
If the exception is not re-raised, an appropriate wrapper exception
|
If the exception is not re-raised, an appropriate wrapper exception
|
||||||
class indicating the type of exception that was encountered will be
|
class indicating the type of exception that was encountered will be
|
||||||
returned.
|
returned.
|
||||||
|
If details is None (default), take it from exception sys.exc_info.
|
||||||
|
If details is other string, then use that string explicitly or if details
|
||||||
|
is empty then suppress it.
|
||||||
"""
|
"""
|
||||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||||
log_method = getattr(LOG, log_level or "exception")
|
log_method = getattr(LOG, log_level or "exception")
|
||||||
@ -316,6 +324,10 @@ def handle(request, message=None, redirect=None, ignore=False,
|
|||||||
user_message = encoding.force_text(message) % {"exc": log_entry}
|
user_message = encoding.force_text(message) % {"exc": log_entry}
|
||||||
elif message:
|
elif message:
|
||||||
user_message = encoding.force_text(message)
|
user_message = encoding.force_text(message)
|
||||||
|
if details is None:
|
||||||
|
user_message = _append_detail(user_message, exc_value)
|
||||||
|
elif details:
|
||||||
|
user_message = _append_detail(user_message, details)
|
||||||
|
|
||||||
for exc_handler in HANDLE_EXC_METHODS:
|
for exc_handler in HANDLE_EXC_METHODS:
|
||||||
if issubclass(exc_type, exc_handler['exc']):
|
if issubclass(exc_type, exc_handler['exc']):
|
||||||
|
@ -12,8 +12,11 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
horizon.alert = function (type, message, extra_tags) {
|
horizon.alert = function (type, message, extra_tags, details) {
|
||||||
var safe = false;
|
var safe = false;
|
||||||
|
var arr = extractDetail(message);
|
||||||
|
var message = arr[0];
|
||||||
|
var details = arr[1];
|
||||||
// Check if the message is tagged as safe.
|
// Check if the message is tagged as safe.
|
||||||
if (typeof(extra_tags) !== "undefined" && $.inArray('safe', extra_tags.split(' ')) !== -1) {
|
if (typeof(extra_tags) !== "undefined" && $.inArray('safe', extra_tags.split(' ')) !== -1) {
|
||||||
safe = true;
|
safe = true;
|
||||||
@ -32,12 +35,17 @@ horizon.alert = function (type, message, extra_tags) {
|
|||||||
type = 'danger';
|
type = 'danger';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractDetail(str) {
|
||||||
|
return str.split('\u2026');
|
||||||
|
}
|
||||||
|
|
||||||
var template = horizon.templates.compiled_templates["#alert_message_template"],
|
var template = horizon.templates.compiled_templates["#alert_message_template"],
|
||||||
params = {
|
params = {
|
||||||
"type": type || 'default',
|
"type": type || 'default',
|
||||||
"type_display": type_display,
|
"type_display": type_display,
|
||||||
"message": message,
|
"message": message,
|
||||||
"safe": safe
|
"safe": safe,
|
||||||
|
"details": details
|
||||||
};
|
};
|
||||||
var this_alert = $(template.render(params)).hide().prependTo("#main_content .messages").fadeIn(100);
|
var this_alert = $(template.render(params)).hide().prependTo("#main_content .messages").fadeIn(100);
|
||||||
horizon.autoDismissAlert(this_alert);
|
horizon.autoDismissAlert(this_alert);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% load i18n %}
|
{% load i18n splitfilter %}
|
||||||
<div class="messages">
|
<div class="messages">
|
||||||
<toast></toast>
|
<toast></toast>
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
@ -31,8 +31,19 @@
|
|||||||
<a class="close" data-dismiss="alert" href="#">
|
<a class="close" data-dismiss="alert" href="#">
|
||||||
<span class="fa fa-times"></span>
|
<span class="fa fa-times"></span>
|
||||||
</a>
|
</a>
|
||||||
<p><strong>{% trans "Error: " %}</strong>{{ message }}</p>
|
<p>
|
||||||
|
{% with message.message|split_message as messages %}
|
||||||
|
<strong>{% trans "Error: " %}</strong>{{ messages|first }}
|
||||||
|
{% if messages|length > 1 %}
|
||||||
|
<a href="#message_details" data-toggle="collapse"
|
||||||
|
data-target="#message_details">Details</a>
|
||||||
|
<div id="message_details" class="collapse">
|
||||||
|
{{ messages|last }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
@ -15,6 +15,10 @@
|
|||||||
[[/safe]]
|
[[/safe]]
|
||||||
[[^safe]]
|
[[^safe]]
|
||||||
[[message]]
|
[[message]]
|
||||||
|
<a href="#message_details" data-toggle="collapse" data-target="#message_details">Details</a>
|
||||||
|
<div id="message_details" class="collapse">
|
||||||
|
[[details]]
|
||||||
|
</div>
|
||||||
[[/safe]]
|
[[/safe]]
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
20
horizon/templatetags/splitfilter.py
Normal file
20
horizon/templatetags/splitfilter.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='split_message')
|
||||||
|
def split_message(value):
|
||||||
|
return value.split('\u2026')
|
@ -25,7 +25,8 @@ class HandleTests(test.TestCase):
|
|||||||
# Japanese translation of:
|
# Japanese translation of:
|
||||||
# 'Because the container is not empty, it can not be deleted.'
|
# 'Because the container is not empty, it can not be deleted.'
|
||||||
|
|
||||||
expected = ['error', force_text(translated_unicode), '']
|
expected = ['error', force_text(translated_unicode +
|
||||||
|
'\u2026' + translated_unicode), '']
|
||||||
|
|
||||||
req = self.request
|
req = self.request
|
||||||
req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
|
req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
|
||||||
@ -57,6 +58,35 @@ class HandleTests(test.TestCase):
|
|||||||
# message part of the first message. There should be only one message
|
# message part of the first message. There should be only one message
|
||||||
# in this test case.
|
# in this test case.
|
||||||
self.assertIn(message, req.horizon['async_messages'][0][1])
|
self.assertIn(message, req.horizon['async_messages'][0][1])
|
||||||
# verifies that the exec message which in this case is not trusted
|
# verifies that the exec message which in this case is in the
|
||||||
# is not in the message content.
|
# message content.
|
||||||
self.assertNotIn(exc_msg, req.horizon['async_messages'][0][1])
|
self.assertIn(exc_msg, req.horizon['async_messages'][0][1])
|
||||||
|
|
||||||
|
def test_handle_exception_with_empty_details(self):
|
||||||
|
message = u"Couldn't make the thing"
|
||||||
|
details = ""
|
||||||
|
expected = ['error', message, '']
|
||||||
|
req = self.request
|
||||||
|
req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
|
||||||
|
|
||||||
|
try:
|
||||||
|
raise Exception(message)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(req, message, details=details)
|
||||||
|
|
||||||
|
self.assertCountEqual(req.horizon['async_messages'], [expected])
|
||||||
|
|
||||||
|
def test_handle_exception_with_details(self):
|
||||||
|
message = u"Couldn't make the thing"
|
||||||
|
exc_msg = u"Exception string"
|
||||||
|
details = "custom detail message"
|
||||||
|
expected = ['error', message + '\u2026' + details, '']
|
||||||
|
req = self.request
|
||||||
|
req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
|
||||||
|
|
||||||
|
try:
|
||||||
|
raise Exception(exc_msg)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(req, message, details=details)
|
||||||
|
|
||||||
|
self.assertCountEqual(req.horizon['async_messages'], [expected])
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
[`blueprint refactor-error-messages <https://blueprints.launchpad.net/horizon/+spec/refactor-error-messages>`_]
|
||||||
|
User can see detailed error message on the horizon UI.
|
||||||
|
This blueprint adds a hyperlink "details" in the alert box.
|
||||||
|
So now when an exception occurs a user can click on this hyperlink
|
||||||
|
"details" which shows original error message included in a corresponding
|
||||||
|
error. This may help a user to understand what happens in detail.
|
Loading…
Reference in New Issue
Block a user