Support action API requests
In some cases, the application needs to expose APIs that do not map to the standard HTTP methods. We support these requests using POST with a mandatory 'action' query parameter that maps to the associated call in the associated resource model. Change-Id: I43bfaa954a85fa434beb1c5cf7b1ae18036f9ede
This commit is contained in:
parent
8a4cc97862
commit
35b93f512f
|
@ -138,8 +138,8 @@ class ElementHandler(AbstractApiHandler):
|
|||
"""
|
||||
|
||||
def __init__(self, path_regex, model,
|
||||
collection_handler=None, allow_read=True, allow_replace=True,
|
||||
allow_update=True, allow_delete=True):
|
||||
collection_handler=None, allow_read=True, allow_actions=True,
|
||||
allow_replace=True, allow_update=True, allow_delete=True):
|
||||
"""Initialize an element handler.
|
||||
|
||||
Args:
|
||||
|
@ -160,6 +160,7 @@ class ElementHandler(AbstractApiHandler):
|
|||
self.model = model
|
||||
self.collection_handler = collection_handler
|
||||
self.allow_read = allow_read
|
||||
self.allow_actions = allow_actions
|
||||
self.allow_replace = allow_replace
|
||||
self.allow_update = allow_update
|
||||
self.allow_delete = allow_delete
|
||||
|
@ -181,7 +182,8 @@ class ElementHandler(AbstractApiHandler):
|
|||
"""
|
||||
if request.method == 'GET' and self.allow_read:
|
||||
return self.read(request)
|
||||
#TODO(pballand): POST for controller semantics
|
||||
elif request.method == 'POST' and self.allow_actions:
|
||||
return self.action(request)
|
||||
elif request.method == 'PUT' and self.allow_replace:
|
||||
return self.replace(request)
|
||||
elif request.method == 'PATCH' and self.allow_update:
|
||||
|
@ -203,6 +205,30 @@ class ElementHandler(AbstractApiHandler):
|
|||
status=httplib.OK,
|
||||
content_type='application/json')
|
||||
|
||||
def action(self, request):
|
||||
# Non-CRUD operations must specify an 'action' parameter
|
||||
action = request.params.getall('action')
|
||||
if len(action) != 1:
|
||||
if len(action) > 1:
|
||||
errstr = "Action parameter may not be provided multiple times."
|
||||
else:
|
||||
errstr = "Missing required action parameter."
|
||||
return error_response(httplib.BAD_REQUEST, 400, errstr)
|
||||
model_method = "%s_action" % action[0]
|
||||
f = getattr(self.model, model_method, None)
|
||||
if f is None:
|
||||
return NOT_SUPPORTED_RESPONSE
|
||||
try:
|
||||
response = f(request.params, context=self._get_context(request),
|
||||
request=request)
|
||||
if isinstance(response, webob.Response):
|
||||
return response
|
||||
return webob.Response(body="%s\n" % json.dumps(response),
|
||||
status=httplib.OK,
|
||||
content_type='application/json')
|
||||
except TypeError:
|
||||
return NOT_SUPPORTED_RESPONSE
|
||||
|
||||
def replace(self, request):
|
||||
if not hasattr(self.model, 'update_item'):
|
||||
return NOT_SUPPORTED_RESPONSE
|
||||
|
|
|
@ -16,6 +16,7 @@ import httplib
|
|||
import json
|
||||
import unittest
|
||||
import uuid
|
||||
import webob
|
||||
|
||||
from congress.api import webservice
|
||||
from congress.tests import base
|
||||
|
@ -145,6 +146,64 @@ class TestSimpleDataModel(unittest.TestCase):
|
|||
model.delete_item(self.UNADDED_ID, {}, context=context),
|
||||
|
||||
|
||||
class TestElementHandler(base.TestCase):
|
||||
|
||||
def test_read(self):
|
||||
# TODO(pballand): write tests
|
||||
pass
|
||||
|
||||
def test_action(self):
|
||||
element_handler = webservice.ElementHandler(r'/', '')
|
||||
element_handler.model = webservice.SimpleDataModel("test")
|
||||
request = MagicMock()
|
||||
request.path = "/"
|
||||
|
||||
response = element_handler.action(request)
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(json.loads(response.body)['description'],
|
||||
"Missing required action parameter.")
|
||||
|
||||
request.params = MagicMock()
|
||||
request.params.getall.return_value = ['do_test']
|
||||
request.params["action"] = "do_test"
|
||||
request.path = "/"
|
||||
response = element_handler.action(request)
|
||||
self.assertEqual(501, response.status_code)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(json.loads(response.body)['description'],
|
||||
"Method not supported")
|
||||
|
||||
# test action impl returning python primitives
|
||||
simple_data = [1, 2]
|
||||
element_handler.model.do_test_action = lambda *a, **kwa: simple_data
|
||||
response = element_handler.action(request)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(json.loads(response.body), simple_data)
|
||||
|
||||
# test action impl returning custom webob response
|
||||
custom_data = webob.Response(body="test", status=599,
|
||||
content_type="custom/test")
|
||||
element_handler.model.do_test_action = lambda *a, **kwa: custom_data
|
||||
response = element_handler.action(request)
|
||||
self.assertEqual(599, response.status_code)
|
||||
self.assertEqual('custom/test', response.content_type)
|
||||
self.assertEqual(response.body, "test")
|
||||
|
||||
def test_replace(self):
|
||||
# TODO(pballand): write tests
|
||||
pass
|
||||
|
||||
def test_update(self):
|
||||
# TODO(pballand): write tests
|
||||
pass
|
||||
|
||||
def test_delete(self):
|
||||
# TODO(pballand): write tests
|
||||
pass
|
||||
|
||||
|
||||
class TestCollectionHandler(base.TestCase):
|
||||
|
||||
def test_get_action_type(self):
|
||||
|
|
Loading…
Reference in New Issue