Merge "Avoid duplicated message in Batch/DeleteAction"
This commit is contained in:
commit
f1eba8073d
|
@ -15,6 +15,7 @@
|
|||
from collections import defaultdict
|
||||
from collections import OrderedDict
|
||||
import copy
|
||||
import functools
|
||||
import logging
|
||||
import types
|
||||
|
||||
|
@ -28,6 +29,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.utils.translation import ungettext_lazy
|
||||
import six
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import messages
|
||||
from horizon.utils import functions
|
||||
from horizon.utils import html
|
||||
|
@ -775,10 +777,22 @@ class BatchAction(Action):
|
|||
{'action': self._get_action_name(past=True),
|
||||
'datum_display': datum_display})
|
||||
except Exception as ex:
|
||||
# Handle the exception but silence it since we'll display
|
||||
# an aggregate error message later. Otherwise we'd get
|
||||
# multiple error messages displayed to the user.
|
||||
action_failure.append(datum_display)
|
||||
handled_exc = isinstance(ex, exceptions.HandledException)
|
||||
if handled_exc:
|
||||
# In case of HandledException, an error message should be
|
||||
# handled in exceptions.handle() or other logic,
|
||||
# so we don't need to handle the error message here.
|
||||
# NOTE(amotoki): To raise HandledException from the logic,
|
||||
# pass escalate=True and do not pass redirect argument
|
||||
# to exceptions.handle().
|
||||
# If an exception is handled, the original exception object
|
||||
# is stored in ex.wrapped[1].
|
||||
ex = ex.wrapped[1]
|
||||
else:
|
||||
# Handle the exception but silence it since we'll display
|
||||
# an aggregate error message later. Otherwise we'd get
|
||||
# multiple error messages displayed to the user.
|
||||
action_failure.append(datum_display)
|
||||
action_description = (
|
||||
self._get_action_name(past=True).lower(), datum_display)
|
||||
LOG.warning(
|
||||
|
@ -879,6 +893,79 @@ class DeleteAction(BatchAction):
|
|||
"""
|
||||
|
||||
|
||||
class handle_exception_with_detail_message(object):
|
||||
"""Decorator to allow special exception handling in BatchAction.action().
|
||||
|
||||
An exception from BatchAction.action() or DeleteAction.delete() is
|
||||
normally caught by BatchAction.handle() and BatchAction.handle() displays
|
||||
an aggregated error message. However, there are cases where we would like
|
||||
to provide an error message which explains a failure reason if some
|
||||
exception occurs so that users can understand situation better.
|
||||
|
||||
This decorator allows us to do this kind of special handling easily.
|
||||
This can be applied to BatchAction.action() and DeleteAction.delete()
|
||||
methods.
|
||||
|
||||
:param normal_log_message: Log message template when an exception other
|
||||
than ``target_exception`` is detected. Keyword substituion "%(id)s"
|
||||
and "%(exc)s" can be used.
|
||||
|
||||
:param target_exception: Exception class should be handled specially.
|
||||
If this exception is caught, a log message will be logged using
|
||||
``target_log_message`` and a user visible will be shown using
|
||||
``target_user_message``. In this case, an aggregated error message
|
||||
generated by BatchAction.handle() does not include an object which
|
||||
causes this exception.
|
||||
|
||||
:param target_log_message: Log message template when an exception specified
|
||||
in ``target_exception`` is detected. Keyword substituion "%(id)s"
|
||||
and "%(exc)s" can be used.
|
||||
|
||||
:param target_user_message: User visible message template when an exception
|
||||
specified in ``target_exception`` is detected. It is recommended to
|
||||
use an internationalized string. Keyword substituion "%(name)s"
|
||||
and "%(exc)s" can be used.
|
||||
|
||||
:param logger_name: (optional) Logger name to be used.
|
||||
The usual pattern is to pass __name__ of a caller.
|
||||
This allows us to show a module name of a caller in a logged message.
|
||||
"""
|
||||
|
||||
def __init__(self, normal_log_message, target_exception,
|
||||
target_log_message, target_user_message, logger_name=None):
|
||||
self.logger = logging.getLogger(logger_name or __name__)
|
||||
self.normal_log_message = normal_log_message
|
||||
self.target_exception = target_exception
|
||||
self.target_log_message = target_log_message
|
||||
self.target_user_message = target_user_message
|
||||
|
||||
def __call__(self, fn):
|
||||
@functools.wraps(fn)
|
||||
def decorated(instance, request, obj_id):
|
||||
try:
|
||||
fn(instance, request, obj_id)
|
||||
except self.target_exception as e:
|
||||
self.logger.info(self.target_log_message,
|
||||
{'id': obj_id, 'exc': e})
|
||||
obj = instance.table.get_object_by_id(obj_id)
|
||||
name = instance.table.get_object_display(obj)
|
||||
msg = self.target_user_message % {'name': name, 'exc': e}
|
||||
# 'escalate=True' is required to notify the caller
|
||||
# (DeleteAction) of the failure. exceptions.handle() will
|
||||
# raise a wrapped exception of HandledException and BatchAction
|
||||
# will handle it. 'redirect' should not be passed here as
|
||||
# 'redirect' has a priority over 'escalate' argument.
|
||||
exceptions.handle(request, msg, escalate=True)
|
||||
except Exception as e:
|
||||
self.logger.info(self.normal_log_message,
|
||||
{'id': obj_id, 'exc': e})
|
||||
# NOTE: No exception handling is required here because
|
||||
# BatchAction.handle() does it. What we need to do is
|
||||
# just to re-raise the exception.
|
||||
raise
|
||||
return decorated
|
||||
|
||||
|
||||
class Deprecated(type):
|
||||
# TODO(lcastell) Replace class with similar functionality from
|
||||
# oslo_log.versionutils when it's finally added in 11.0
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import unittest
|
||||
import uuid
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django import forms
|
||||
from django import http
|
||||
|
@ -23,10 +26,13 @@ from django.template import defaultfilters
|
|||
from django.test.utils import override_settings
|
||||
from django.utils.translation import ungettext_lazy
|
||||
|
||||
import mock
|
||||
from mox3.mox import IsA
|
||||
import six
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import tables
|
||||
from horizon.tables import actions
|
||||
from horizon.tables import formset as table_formset
|
||||
from horizon.tables import views as table_views
|
||||
from horizon.test import helpers as test
|
||||
|
@ -1603,3 +1609,81 @@ class FormsetTableTests(test.TestCase):
|
|||
form_data = form.initial
|
||||
self.assertEqual('object_1', form_data['name'])
|
||||
self.assertEqual(2, form_data['value'])
|
||||
|
||||
|
||||
class MyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OtherException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BatchActionDecoratorTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
obj = uuid.uuid4()
|
||||
self.obj_id = 'id-%s' % str(obj)
|
||||
self.obj_name = 'name-%s' % str(obj)
|
||||
table = mock.Mock()
|
||||
table.get_object_by_id.return_value = obj
|
||||
table.get_object_display.return_value = self.obj_name
|
||||
|
||||
# getLogger is called insdie handle_exception_with_detail_message
|
||||
# decorator, so this needs to be mocked before using the decorator.
|
||||
self.logger = mock.Mock()
|
||||
with mock.patch('logging.getLogger',
|
||||
return_value=self.logger) as mock_getlogger:
|
||||
class MyAction(object):
|
||||
def __init__(self, table):
|
||||
self.table = table
|
||||
|
||||
@actions.handle_exception_with_detail_message(
|
||||
'normal log message %(id)s %(exc)s',
|
||||
MyException,
|
||||
'target log message %(id)s %(exc)s',
|
||||
'target user message %(name)s %(exc)s',
|
||||
'mylogger')
|
||||
def action(self, request, obj_id):
|
||||
self._to_be_mocked()
|
||||
|
||||
# This is required because if mock.patch replaces
|
||||
# a decorated method. We are testing a decorated method
|
||||
# so we need a separate method to mock,
|
||||
def _to_be_mocked(self):
|
||||
pass
|
||||
|
||||
self.action = MyAction(table)
|
||||
self.mock_getlogger = mock_getlogger
|
||||
|
||||
def test_normal_exception(self):
|
||||
myexc = OtherException()
|
||||
with mock.patch.object(self.action, '_to_be_mocked',
|
||||
side_effect=myexc):
|
||||
self.assertRaises(OtherException,
|
||||
self.action.action,
|
||||
mock.sentinel.request, self.obj_id)
|
||||
self.mock_getlogger.assert_called_once_with('mylogger')
|
||||
self.logger.info.assert_called_once_with(
|
||||
'normal log message %(id)s %(exc)s',
|
||||
{'id': self.obj_id, 'exc': myexc})
|
||||
|
||||
def test_target_exception(self):
|
||||
myexc = MyException()
|
||||
handled = exceptions.HandledException(myexc)
|
||||
with mock.patch.object(self.action, '_to_be_mocked',
|
||||
side_effect=myexc), \
|
||||
mock.patch.object(exceptions, 'handle',
|
||||
side_effect=handled) as mocked_handle:
|
||||
self.assertRaises(exceptions.HandledException,
|
||||
self.action.action,
|
||||
mock.sentinel.request, self.obj_id)
|
||||
self.mock_getlogger.assert_called_once_with('mylogger')
|
||||
self.logger.info.assert_called_once_with(
|
||||
'target log message %(id)s %(exc)s',
|
||||
{'id': self.obj_id, 'exc': myexc})
|
||||
mocked_handle.assert_called_once_with(
|
||||
mock.sentinel.request,
|
||||
'target user message %(name)s %(exc)s' %
|
||||
{'name': self.obj_name, 'exc': myexc},
|
||||
escalate=True)
|
||||
|
|
Loading…
Reference in New Issue