Skip to content

Commit

Permalink
Merge remote-tracking branch 'oca/13.0' into 14.0-forwardport-13.0
Browse files Browse the repository at this point in the history
  • Loading branch information
lmignon committed Mar 22, 2021
2 parents f132cb7 + 9beea35 commit f46d770
Show file tree
Hide file tree
Showing 16 changed files with 328 additions and 73 deletions.
61 changes: 36 additions & 25 deletions base_rest/apispec/rest_method_param_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,41 @@ def operation_helper(self, path=None, operations=None, **kwargs):
if not operations:
return
for method, params in operations.items():
input_param = routing.get("input_param")
output_param = routing.get("output_param")
if input_param and isinstance(input_param, RestMethodParam):
parameters = params.get("parameters", [])
# add default paramters provided by the sevice
parameters.extend(self._default_parameters)
if method == "get":
# get quey params from RequestMethodParam object
parameters.extend(
input_param.to_openapi_query_parameters(self._service)
)
# sort paramters to ease comparison into unittests
else:
# get requestBody from RequestMethodParam object
request_body = params.get("requestBody", {})
request_body.update(
input_param.to_openapi_requestbody(self._service)
)
params["requestBody"] = request_body
parameters.sort(key=lambda a: a["name"])
parameters = self._generate_pamareters(routing, method, params)
if parameters:
params["parameters"] = parameters
if output_param and isinstance(output_param, RestMethodParam):
responses = params.get("responses", {})
# get response from RequestMethodParam object
responses.update(self._default_responses.copy())
responses.update(output_param.to_openapi_responses(self._service))
responses = self._generate_responses(routing, method, params)
if responses:
params["responses"] = responses

def _generate_pamareters(self, routing, method, params):
parameters = params.get("parameters", [])
# add default paramters provided by the sevice
parameters.extend(self._default_parameters)
input_param = routing.get("input_param")
if input_param and isinstance(input_param, RestMethodParam):
if method == "get":
# get quey params from RequestMethodParam object
parameters.extend(
input_param.to_openapi_query_parameters(self._service)
)
else:
# get requestBody from RequestMethodParam object
request_body = params.get("requestBody", {})
request_body.update(input_param.to_openapi_requestbody(self._service))
params["requestBody"] = request_body
# sort paramters to ease comparison into unittests
parameters.sort(key=lambda a: a["name"])
return parameters

def _generate_responses(self, routing, method, params):
responses = params.get("responses", {})
# add default responses provided by the service
responses.update(self._default_responses.copy())
output_param = routing.get("output_param")
if output_param and isinstance(output_param, RestMethodParam):
responses = params.get("responses", {})
# get response from RequestMethodParam object
responses.update(self._default_responses.copy())
responses.update(output_param.to_openapi_responses(self._service))
return responses
4 changes: 3 additions & 1 deletion base_rest/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ def __init__(cls, name, bases, attrs): # noqa: B902
# our RestConrtroller must be a direct child of Controller
bases += (Controller,)
super(RestControllerType, cls).__init__(name, bases, attrs)
if "RestController" not in globals() or RestController not in bases:
if "RestController" not in globals() or not any(
issubclass(b, RestController) for b in bases
):
return
# register the rest controller into the rest controllers registry
root_path = getattr(cls, "_root_path", None)
Expand Down
26 changes: 25 additions & 1 deletion base_rest/models/rest_service_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
)
from ..tools import _inspect_methods

# Decorator attribute added on a route function (cfr Odoo's route)
ROUTING_DECORATOR_ATTR = "routing"


class RestServiceRegistation(models.AbstractModel):
"""Register REST services into the REST services registry
Expand Down Expand Up @@ -92,6 +95,27 @@ def _build_controller(self, service, controller_def):
# register our conroller into the list of available controllers
name_class = ("{}.{}".format(ctrl_cls.__module__, ctrl_cls.__name__), ctrl_cls)
http.controllers_per_module[addon_name].append(name_class)
self._update_auth_method_controller(controller_class=ctrl_cls)

def _update_auth_method_controller(self, controller_class):
"""
Set the automatic auth on controller's routes.
During definition of new controller, the _default_auth should be
applied on every routes (cfr @route odoo's decorator).
This auth attribute should be applied only if the route doesn't already
define it.
:return:
"""
# If the controller class doesn't have the _default_auth, we don't
# have to define it on every routes.
if not hasattr(controller_class, "_default_auth"):
return
controller_default_auth = {"auth": controller_class._default_auth}
for _name, method in _inspect_methods(controller_class):
routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
if routing is not None and not routing.get("auth"):
routing.update(controller_default_auth)

def _get_services(self, collection_name):
collection = _PseudoCollection(collection_name, self.env)
Expand Down Expand Up @@ -160,7 +184,7 @@ def fix(self):
for name, method in _inspect_methods(self._service.__class__):
if not self._is_public_api_method(name):
continue
if not hasattr(method, "routing"):
if not hasattr(method, ROUTING_DECORATOR_ATTR):
methods_to_fix.append(method)
for method in methods_to_fix:
self._fix_method_decorator(method)
Expand Down
17 changes: 17 additions & 0 deletions base_rest/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,24 @@ class BaseTestController(RestController):
_collection_name = class_or_instance._collection_name
_default_auth = "public"

@http.route("/my_controller_route_without_auth")
def my_controller_route_without_auth(self):
return {}

@http.route("/my_controller_route_with_auth_public", auth="public")
def my_controller_route_with_auth_public(self):
return {}

@http.route("/my_controller_route_without_auth_2", auth=None)
def my_controller_route_without_auth_2(self):
return {}

class_or_instance._BaseTestController = BaseTestController
class_or_instance._controller_route_method_names = {
"my_controller_route_without_auth",
"my_controller_route_with_auth_public",
"my_controller_route_without_auth_2",
}

@staticmethod
def _teardown_registry(class_or_instance):
Expand Down
88 changes: 85 additions & 3 deletions base_rest/tests/test_controller_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ def _validator_my_instance_method(self):
"delete_delete",
"post_my_method",
"post_my_instance_method",
},
}
| self._controller_route_method_names,
)
self.assertTrue(controller)
# the generated method_name is always the {http_method}_{method_name}
Expand Down Expand Up @@ -257,7 +258,9 @@ def _get_partner_schema(self):

routes = self._get_controller_route_methods(controller)
self.assertSetEqual(
set(routes.keys()), {"get_get", "get_get_name", "post_update_name"}
set(routes.keys()),
{"get_get", "get_get_name", "post_update_name"}
| self._controller_route_method_names,
)

method = routes["get_get"]
Expand Down Expand Up @@ -346,7 +349,9 @@ def update_name(self, _id, **params):

routes = self._get_controller_route_methods(controller)
self.assertSetEqual(
set(routes.keys()), {"get_get", "get_get_name", "post_update_name"}
set(routes.keys()),
{"get_get", "get_get_name", "post_update_name"}
| self._controller_route_method_names,
)

method = routes["get_get"]
Expand Down Expand Up @@ -387,3 +392,80 @@ def update_name(self, _id, **params):
"routes": ["/test_controller/partner/<int:id>/change_name"],
},
)


class TestControllerBuilder2(TransactionRestServiceRegistryCase):
"""Test Odoo controller builder
In this class we test the generation of odoo controllers from the services
component
The test requires a fresh base crontroller
"""

def test_04(self):
"""Test controller generated from services with new API methods and
old api takes into account the _default_auth
Routes directly defined on the RestConroller without auth should also
use the _default_auth
"""
default_auth = "my_default_auth"
self._BaseTestController._default_auth = default_auth

# pylint: disable=R7980
class TestService(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "test"

@restapi.method([(["/new_api_method_with_auth"], "GET")], auth="public")
def new_api_method_with_auth(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}

@restapi.method([(["/new_api_method_without_auth"], "GET")])
def new_api_method_without_auth(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}

# OLD API method withour auth
def get(self, _id, message):
pass

# Validator
def _validator_get(self):
# no parameters by default
return {}

self._build_services(self, TestService)
controller = self._get_controller_for(TestService)

routes = self._get_controller_route_methods(controller)
self.assertEqual(
routes["get_new_api_method_with_auth"].routing["auth"],
"public",
"wrong auth for get_new_api_method_with_auth",
)
self.assertEqual(
routes["get_new_api_method_without_auth"].routing["auth"],
default_auth,
"wrong auth for get_new_api_method_without_auth",
)
self.assertEqual(
routes["get_get"].routing["auth"], default_auth, "wrong auth for get_get"
)
self.assertEqual(
routes["my_controller_route_without_auth"].routing["auth"],
default_auth,
"wrong auth for my_controller_route_without_auth",
)
self.assertEqual(
routes["my_controller_route_with_auth_public"].routing["auth"],
"public",
"wrong auth for my_controller_route_with_auth_public",
)
self.assertEqual(
routes["my_controller_route_without_auth_2"].routing["auth"],
default_auth,
"wrong auth for my_controller_route_without_auth_2",
)
54 changes: 54 additions & 0 deletions base_rest/tests/test_openapi_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,57 @@ def update_name(self, _id, _name):
"schema": {"type": "integer", "format": "int32"},
},
)

def test_03(self):
"""Test default parameters and default responses
You can define default parameters and responses at service level.
In this test we check that these parameters and responses are into the
openapi specification
"""
default_params = [
{
"name": "API-KEY",
"in": "header",
"description": "API key for Authorization",
"required": True,
"schema": {"type": "string"},
"style": "simple",
}
]

# pylint: disable=R7980
class PartnerService(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "Sercice description"

@restapi.method(
[(["/<int:id>/update_name/<string:name>"], "POST")], auth="public"
)
def update_name(self, _id, _name):
"""update_name"""

def _get_openapi_default_parameters(self):
defaults = super()._get_openapi_default_parameters()
defaults.extend(default_params)
return defaults

def _get_openapi_default_responses(self):
responses = super()._get_openapi_default_responses().copy()
responses["999"] = "TEST"
return responses

self._build_services(self, PartnerService)
service = self._get_service_component(self, "partner")
openapi = service.to_openapi()
paths = openapi["paths"]
self.assertIn("/{id}/update_name/{name}", paths)
path = paths["/{id}/update_name/{name}"]
self.assertIn("post", path)
parameters = path["post"].get("parameters", [])
self.assertListEqual(parameters, default_params)
responses = path["post"].get("responses", [])
self.assertIn("999", responses)
22 changes: 14 additions & 8 deletions base_rest_demo/tests/data/partner_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,20 @@
}
}
},
"parameters": []
"responses": {
"400": {
"description": "One of the given parameter is not valid"
},
"401": {
"description": "The user is not authorized. Authentication is required"
},
"404": {
"description": "Requested resource not found"
},
"403": {
"description": "You don't have the permission to access the requested resource."
}
}
},
"parameters": [
{
Expand Down Expand Up @@ -102,7 +115,6 @@
}
}
},
"parameters": [],
"responses": {
"400": {
"description": "One of the given parameter is not valid"
Expand Down Expand Up @@ -247,7 +259,6 @@
}
}
},
"parameters": [],
"responses": {
"400": {
"description": "One of the given parameter is not valid"
Expand Down Expand Up @@ -436,7 +447,6 @@
"/{id}/get": {
"get": {
"summary": "\nGet partner's informations\n",
"parameters": [],
"responses": {
"400": {
"description": "One of the given parameter is not valid"
Expand Down Expand Up @@ -531,7 +541,6 @@
"/{id}": {
"get": {
"summary": "\nGet partner's informations\n",
"parameters": [],
"responses": {
"400": {
"description": "One of the given parameter is not valid"
Expand Down Expand Up @@ -685,7 +694,6 @@
}
}
},
"parameters": [],
"responses": {
"400": {
"description": "One of the given parameter is not valid"
Expand Down Expand Up @@ -828,7 +836,6 @@
}
}
},
"parameters": [],
"responses": {
"400": {
"description": "One of the given parameter is not valid"
Expand Down Expand Up @@ -1080,7 +1087,6 @@
}
}
},
"parameters": [],
"responses": {
"400": {
"description": "One of the given parameter is not valid"
Expand Down
Loading

0 comments on commit f46d770

Please sign in to comment.