Merge "Enable localizable REST API responses via the Accept-Language header"
This commit is contained in:
commit
5cb5d7b097
@ -23,6 +23,7 @@ import webob.exc
|
|||||||
|
|
||||||
from neutron.api.v2 import attributes
|
from neutron.api.v2 import attributes
|
||||||
from neutron.common import exceptions
|
from neutron.common import exceptions
|
||||||
|
from neutron.openstack.common import gettextutils
|
||||||
from neutron.openstack.common import log as logging
|
from neutron.openstack.common import log as logging
|
||||||
from neutron import wsgi
|
from neutron import wsgi
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ def Resource(controller, faults=None, deserializers=None, serializers=None):
|
|||||||
action = args.pop('action', None)
|
action = args.pop('action', None)
|
||||||
content_type = format_types.get(fmt,
|
content_type = format_types.get(fmt,
|
||||||
request.best_match_content_type())
|
request.best_match_content_type())
|
||||||
|
language = request.best_match_language()
|
||||||
deserializer = deserializers.get(content_type)
|
deserializer = deserializers.get(content_type)
|
||||||
serializer = serializers.get(content_type)
|
serializer = serializers.get(content_type)
|
||||||
|
|
||||||
@ -83,6 +85,7 @@ def Resource(controller, faults=None, deserializers=None, serializers=None):
|
|||||||
except (exceptions.NeutronException,
|
except (exceptions.NeutronException,
|
||||||
netaddr.AddrFormatError) as e:
|
netaddr.AddrFormatError) as e:
|
||||||
LOG.exception(_('%s failed'), action)
|
LOG.exception(_('%s failed'), action)
|
||||||
|
e = translate(e, language)
|
||||||
body = serializer.serialize({'NeutronError': e})
|
body = serializer.serialize({'NeutronError': e})
|
||||||
kwargs = {'body': body, 'content_type': content_type}
|
kwargs = {'body': body, 'content_type': content_type}
|
||||||
for fault in faults:
|
for fault in faults:
|
||||||
@ -91,10 +94,12 @@ def Resource(controller, faults=None, deserializers=None, serializers=None):
|
|||||||
raise webob.exc.HTTPInternalServerError(**kwargs)
|
raise webob.exc.HTTPInternalServerError(**kwargs)
|
||||||
except webob.exc.HTTPException as e:
|
except webob.exc.HTTPException as e:
|
||||||
LOG.exception(_('%s failed'), action)
|
LOG.exception(_('%s failed'), action)
|
||||||
|
translate(e, language)
|
||||||
e.body = serializer.serialize({'NeutronError': e})
|
e.body = serializer.serialize({'NeutronError': e})
|
||||||
e.content_type = content_type
|
e.content_type = content_type
|
||||||
raise
|
raise
|
||||||
except NotImplementedError as e:
|
except NotImplementedError as e:
|
||||||
|
e = translate(e, language)
|
||||||
# NOTE(armando-migliaccio): from a client standpoint
|
# NOTE(armando-migliaccio): from a client standpoint
|
||||||
# it makes sense to receive these errors, because
|
# it makes sense to receive these errors, because
|
||||||
# extensions may or may not be implemented by
|
# extensions may or may not be implemented by
|
||||||
@ -111,6 +116,7 @@ def Resource(controller, faults=None, deserializers=None, serializers=None):
|
|||||||
# Do not expose details of 500 error to clients.
|
# Do not expose details of 500 error to clients.
|
||||||
msg = _('Request Failed: internal server error while '
|
msg = _('Request Failed: internal server error while '
|
||||||
'processing your request.')
|
'processing your request.')
|
||||||
|
msg = translate(msg, language)
|
||||||
body = serializer.serialize({'NeutronError': msg})
|
body = serializer.serialize({'NeutronError': msg})
|
||||||
kwargs = {'body': body, 'content_type': content_type}
|
kwargs = {'body': body, 'content_type': content_type}
|
||||||
raise webob.exc.HTTPInternalServerError(**kwargs)
|
raise webob.exc.HTTPInternalServerError(**kwargs)
|
||||||
@ -126,3 +132,24 @@ def Resource(controller, faults=None, deserializers=None, serializers=None):
|
|||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
body=body)
|
body=body)
|
||||||
return resource
|
return resource
|
||||||
|
|
||||||
|
|
||||||
|
def translate(translatable, locale):
|
||||||
|
"""Translates the object to the given locale.
|
||||||
|
|
||||||
|
If the object is an exception its translatable elements are translated
|
||||||
|
in place, if the object is a translatable string it is translated and
|
||||||
|
returned. Otherwise, the object is returned as-is.
|
||||||
|
|
||||||
|
:param translatable: the object to be translated
|
||||||
|
:param locale: the locale to translate to
|
||||||
|
:returns: the translated object, or the object as-is if it
|
||||||
|
was not translated
|
||||||
|
"""
|
||||||
|
localize = gettextutils.get_localized_message
|
||||||
|
if isinstance(translatable, Exception):
|
||||||
|
translatable.message = localize(translatable.message, locale)
|
||||||
|
if isinstance(translatable, webob.exc.HTTPError):
|
||||||
|
translatable.detail = localize(translatable.detail, locale)
|
||||||
|
return translatable
|
||||||
|
return localize(translatable, locale)
|
||||||
|
@ -27,6 +27,9 @@ from oslo.config import cfg
|
|||||||
from neutron.common import config
|
from neutron.common import config
|
||||||
from neutron import service
|
from neutron import service
|
||||||
|
|
||||||
|
from neutron.openstack.common import gettextutils
|
||||||
|
gettextutils.install('neutron', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
eventlet.monkey_patch()
|
eventlet.monkey_patch()
|
||||||
|
@ -25,6 +25,7 @@ import webtest
|
|||||||
from neutron.api.v2 import resource as wsgi_resource
|
from neutron.api.v2 import resource as wsgi_resource
|
||||||
from neutron.common import exceptions as q_exc
|
from neutron.common import exceptions as q_exc
|
||||||
from neutron import context
|
from neutron import context
|
||||||
|
from neutron.openstack.common import gettextutils
|
||||||
from neutron.tests import base
|
from neutron.tests import base
|
||||||
from neutron import wsgi
|
from neutron import wsgi
|
||||||
|
|
||||||
@ -98,8 +99,23 @@ class RequestTestCase(base.BaseTestCase):
|
|||||||
def test_context_without_neutron_context(self):
|
def test_context_without_neutron_context(self):
|
||||||
self.assertTrue(self.req.context.is_admin)
|
self.assertTrue(self.req.context.is_admin)
|
||||||
|
|
||||||
|
def test_best_match_language(self):
|
||||||
|
# Here we test that we are actually invoking language negotiation
|
||||||
|
# by webop and also that the default locale always available is en-US
|
||||||
|
request = wsgi.Request.blank('/')
|
||||||
|
gettextutils.get_available_languages = mock.MagicMock()
|
||||||
|
gettextutils.get_available_languages.return_value = ['known-language',
|
||||||
|
'es', 'zh']
|
||||||
|
request.headers['Accept-Language'] = 'known-language'
|
||||||
|
language = request.best_match_language()
|
||||||
|
self.assertEqual(language, 'known-language')
|
||||||
|
request.headers['Accept-Language'] = 'unknown-language'
|
||||||
|
language = request.best_match_language()
|
||||||
|
self.assertEqual(language, 'en_US')
|
||||||
|
|
||||||
|
|
||||||
class ResourceTestCase(base.BaseTestCase):
|
class ResourceTestCase(base.BaseTestCase):
|
||||||
|
|
||||||
def test_unmapped_neutron_error_with_json(self):
|
def test_unmapped_neutron_error_with_json(self):
|
||||||
msg = u'\u7f51\u7edc'
|
msg = u'\u7f51\u7edc'
|
||||||
|
|
||||||
@ -136,6 +152,29 @@ class ResourceTestCase(base.BaseTestCase):
|
|||||||
self.assertEqual(wsgi.XMLDeserializer().deserialize(res.body),
|
self.assertEqual(wsgi.XMLDeserializer().deserialize(res.body),
|
||||||
expected_res)
|
expected_res)
|
||||||
|
|
||||||
|
@mock.patch('neutron.openstack.common.gettextutils.Message.data',
|
||||||
|
new_callable=mock.PropertyMock)
|
||||||
|
def test_unmapped_neutron_error_localized(self, mock_translation):
|
||||||
|
gettextutils.install('blaa', lazy=True)
|
||||||
|
msg_translation = 'Translated error'
|
||||||
|
mock_translation.return_value = msg_translation
|
||||||
|
msg = _('Unmapped error')
|
||||||
|
|
||||||
|
class TestException(q_exc.NeutronException):
|
||||||
|
message = msg
|
||||||
|
|
||||||
|
controller = mock.MagicMock()
|
||||||
|
controller.test.side_effect = TestException()
|
||||||
|
resource = webtest.TestApp(wsgi_resource.Resource(controller))
|
||||||
|
|
||||||
|
environ = {'wsgiorg.routing_args': (None, {'action': 'test',
|
||||||
|
'format': 'json'})}
|
||||||
|
|
||||||
|
res = resource.get('', extra_environ=environ, expect_errors=True)
|
||||||
|
self.assertEqual(res.status_int, exc.HTTPInternalServerError.code)
|
||||||
|
self.assertIn(msg_translation,
|
||||||
|
str(wsgi.JSONDeserializer().deserialize(res.body)))
|
||||||
|
|
||||||
def test_mapped_neutron_error_with_json(self):
|
def test_mapped_neutron_error_with_json(self):
|
||||||
msg = u'\u7f51\u7edc'
|
msg = u'\u7f51\u7edc'
|
||||||
|
|
||||||
@ -176,6 +215,31 @@ class ResourceTestCase(base.BaseTestCase):
|
|||||||
self.assertEqual(wsgi.XMLDeserializer().deserialize(res.body),
|
self.assertEqual(wsgi.XMLDeserializer().deserialize(res.body),
|
||||||
expected_res)
|
expected_res)
|
||||||
|
|
||||||
|
@mock.patch('neutron.openstack.common.gettextutils.Message.data',
|
||||||
|
new_callable=mock.PropertyMock)
|
||||||
|
def test_mapped_neutron_error_localized(self, mock_translation):
|
||||||
|
gettextutils.install('blaa', lazy=True)
|
||||||
|
msg_translation = 'Translated error'
|
||||||
|
mock_translation.return_value = msg_translation
|
||||||
|
msg = _('Unmapped error')
|
||||||
|
|
||||||
|
class TestException(q_exc.NeutronException):
|
||||||
|
message = msg
|
||||||
|
|
||||||
|
controller = mock.MagicMock()
|
||||||
|
controller.test.side_effect = TestException()
|
||||||
|
faults = {TestException: exc.HTTPGatewayTimeout}
|
||||||
|
resource = webtest.TestApp(wsgi_resource.Resource(controller,
|
||||||
|
faults=faults))
|
||||||
|
|
||||||
|
environ = {'wsgiorg.routing_args': (None, {'action': 'test',
|
||||||
|
'format': 'json'})}
|
||||||
|
|
||||||
|
res = resource.get('', extra_environ=environ, expect_errors=True)
|
||||||
|
self.assertEqual(res.status_int, exc.HTTPGatewayTimeout.code)
|
||||||
|
self.assertIn(msg_translation,
|
||||||
|
str(wsgi.JSONDeserializer().deserialize(res.body)))
|
||||||
|
|
||||||
def test_http_error(self):
|
def test_http_error(self):
|
||||||
controller = mock.MagicMock()
|
controller = mock.MagicMock()
|
||||||
controller.test.side_effect = exc.HTTPGatewayTimeout()
|
controller.test.side_effect = exc.HTTPGatewayTimeout()
|
||||||
|
@ -37,6 +37,7 @@ import webob.exc
|
|||||||
from neutron.common import constants
|
from neutron.common import constants
|
||||||
from neutron.common import exceptions as exception
|
from neutron.common import exceptions as exception
|
||||||
from neutron import context
|
from neutron import context
|
||||||
|
from neutron.openstack.common import gettextutils
|
||||||
from neutron.openstack.common import jsonutils
|
from neutron.openstack.common import jsonutils
|
||||||
from neutron.openstack.common import log as logging
|
from neutron.openstack.common import log as logging
|
||||||
|
|
||||||
@ -299,6 +300,12 @@ class Request(webob.Request):
|
|||||||
return _type
|
return _type
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def best_match_language(self):
|
||||||
|
"""Determine language for returned response."""
|
||||||
|
all_languages = gettextutils.get_available_languages('neutron')
|
||||||
|
return self.accept_language.best_match(all_languages,
|
||||||
|
default_match='en_US')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def context(self):
|
def context(self):
|
||||||
if 'neutron.context' not in self.environ:
|
if 'neutron.context' not in self.environ:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user