From e58a39186ba96d588bdb3344e0ee8aced12c2805 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Wed, 7 Dec 2022 17:05:27 +0100 Subject: [PATCH 1/6] [MIG] base_rest, base_rest_auth_api_key, base_rest_datamodel, base_rest_demo, base_rest_pydantic, datamodel, extendable: pre-commit stuff --- .pre-commit-config.yaml | 8 -------- base_rest/__manifest__.py | 2 +- base_rest_auth_api_key/__manifest__.py | 4 ++-- base_rest_datamodel/__manifest__.py | 2 +- base_rest_demo/__manifest__.py | 2 +- base_rest_pydantic/__manifest__.py | 2 +- datamodel/__manifest__.py | 2 +- extendable/__manifest__.py | 2 +- pydantic/__manifest__.py | 2 +- requirements.txt | 13 +++++++++++++ setup/base_rest/odoo/addons/base_rest | 1 + setup/base_rest/setup.py | 6 ++++++ .../odoo/addons/base_rest_auth_api_key | 1 + setup/base_rest_auth_api_key/setup.py | 6 ++++++ .../odoo/addons/base_rest_datamodel | 1 + setup/base_rest_datamodel/setup.py | 6 ++++++ setup/base_rest_demo/odoo/addons/base_rest_demo | 1 + setup/base_rest_demo/setup.py | 6 ++++++ .../odoo/addons/base_rest_pydantic | 1 + setup/base_rest_pydantic/setup.py | 6 ++++++ setup/datamodel/odoo/addons/datamodel | 1 + setup/datamodel/setup.py | 6 ++++++ setup/extendable/odoo/addons/extendable | 1 + setup/extendable/setup.py | 6 ++++++ setup/pydantic/odoo/addons/pydantic | 1 + setup/pydantic/setup.py | 6 ++++++ 26 files changed, 78 insertions(+), 17 deletions(-) create mode 120000 setup/base_rest/odoo/addons/base_rest create mode 100644 setup/base_rest/setup.py create mode 120000 setup/base_rest_auth_api_key/odoo/addons/base_rest_auth_api_key create mode 100644 setup/base_rest_auth_api_key/setup.py create mode 120000 setup/base_rest_datamodel/odoo/addons/base_rest_datamodel create mode 100644 setup/base_rest_datamodel/setup.py create mode 120000 setup/base_rest_demo/odoo/addons/base_rest_demo create mode 100644 setup/base_rest_demo/setup.py create mode 120000 setup/base_rest_pydantic/odoo/addons/base_rest_pydantic create mode 100644 setup/base_rest_pydantic/setup.py create mode 120000 setup/datamodel/odoo/addons/datamodel create mode 100644 setup/datamodel/setup.py create mode 120000 setup/extendable/odoo/addons/extendable create mode 100644 setup/extendable/setup.py create mode 120000 setup/pydantic/odoo/addons/pydantic create mode 100644 setup/pydantic/setup.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01bd8831..3505fac2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,9 @@ exclude: | (?x) # NOT INSTALLABLE ADDONS - ^base_rest/| - ^base_rest_auth_api_key/| ^base_rest_auth_jwt/| ^base_rest_auth_user_service/| - ^base_rest_datamodel/| - ^base_rest_demo/| - ^base_rest_pydantic/| - ^datamodel/| - ^extendable/| ^model_serializer/| - ^pydantic/| ^rest_log/| # END NOT INSTALLABLE ADDONS # Files and folders generated by bots, to avoid loops diff --git a/base_rest/__manifest__.py b/base_rest/__manifest__.py index fb9e6a1a..5e57d669 100644 --- a/base_rest/__manifest__.py +++ b/base_rest/__manifest__.py @@ -32,5 +32,5 @@ "apispec>=4.0.0", ] }, - "installable": False, + "installable": True, } diff --git a/base_rest_auth_api_key/__manifest__.py b/base_rest_auth_api_key/__manifest__.py index b0f56cfc..20c0c2d5 100644 --- a/base_rest_auth_api_key/__manifest__.py +++ b/base_rest_auth_api_key/__manifest__.py @@ -12,11 +12,11 @@ "website": "https://github.com/OCA/rest-framework", "depends": ["base_rest", "auth_api_key"], "maintainers": ["lmignon"], - "installable": False, + "installable": True, "auto_install": True, "external_dependencies": { "python": [ - "apispec>=4.0.0", + "apispec", ] }, } diff --git a/base_rest_datamodel/__manifest__.py b/base_rest_datamodel/__manifest__.py index 9fcebb68..9ffe5c54 100644 --- a/base_rest_datamodel/__manifest__.py +++ b/base_rest_datamodel/__manifest__.py @@ -13,5 +13,5 @@ "data": [], "demo": [], "external_dependencies": {"python": ["apispec>=4.0.0", "marshmallow"]}, - "installable": False, + "installable": True, } diff --git a/base_rest_demo/__manifest__.py b/base_rest_demo/__manifest__.py index 748da544..17ece5a0 100644 --- a/base_rest_demo/__manifest__.py +++ b/base_rest_demo/__manifest__.py @@ -23,5 +23,5 @@ "external_dependencies": { "python": ["jsondiff", "extendable-pydantic", "pydantic"] }, - "installable": False, + "installable": True, } diff --git a/base_rest_pydantic/__manifest__.py b/base_rest_pydantic/__manifest__.py index fad88ac6..16f414ac 100644 --- a/base_rest_pydantic/__manifest__.py +++ b/base_rest_pydantic/__manifest__.py @@ -12,7 +12,7 @@ "depends": ["base_rest"], "data": [], "demo": [], - "installable": False, + "installable": True, "external_dependencies": { "python": [ "pydantic", diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 45a117b4..902c7a3e 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -16,5 +16,5 @@ "data": [], "demo": [], "external_dependencies": {"python": ["marshmallow", "marshmallow-objects>=2.0.0"]}, - "installable": False, + "installable": True, } diff --git a/extendable/__manifest__.py b/extendable/__manifest__.py index abd9c3e9..f236466b 100644 --- a/extendable/__manifest__.py +++ b/extendable/__manifest__.py @@ -13,5 +13,5 @@ "website": "https://github.com/OCA/rest-framework", "depends": [], "external_dependencies": {"python": ["extendable"]}, - "installable": False, + "installable": True, } diff --git a/pydantic/__manifest__.py b/pydantic/__manifest__.py index 4af58b5f..9a1296bc 100644 --- a/pydantic/__manifest__.py +++ b/pydantic/__manifest__.py @@ -17,5 +17,5 @@ "external_dependencies": { "python": ["pydantic", "contextvars", "typing-extensions"] }, - "installable": False, + "installable": True, } diff --git a/requirements.txt b/requirements.txt index b64f49c0..33cb03c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,16 @@ # generated from manifests external_dependencies +apispec +apispec>=4.0.0 +cerberus +contextvars +extendable +extendable-pydantic graphene graphql_server +jsondiff +marshmallow +marshmallow-objects>=2.0.0 +parse-accept-language +pydantic +pyquerystring +typing-extensions diff --git a/setup/base_rest/odoo/addons/base_rest b/setup/base_rest/odoo/addons/base_rest new file mode 120000 index 00000000..ead43a36 --- /dev/null +++ b/setup/base_rest/odoo/addons/base_rest @@ -0,0 +1 @@ +../../../../base_rest \ No newline at end of file diff --git a/setup/base_rest/setup.py b/setup/base_rest/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/base_rest/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/base_rest_auth_api_key/odoo/addons/base_rest_auth_api_key b/setup/base_rest_auth_api_key/odoo/addons/base_rest_auth_api_key new file mode 120000 index 00000000..58566db9 --- /dev/null +++ b/setup/base_rest_auth_api_key/odoo/addons/base_rest_auth_api_key @@ -0,0 +1 @@ +../../../../base_rest_auth_api_key \ No newline at end of file diff --git a/setup/base_rest_auth_api_key/setup.py b/setup/base_rest_auth_api_key/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/base_rest_auth_api_key/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/base_rest_datamodel/odoo/addons/base_rest_datamodel b/setup/base_rest_datamodel/odoo/addons/base_rest_datamodel new file mode 120000 index 00000000..a809cfdf --- /dev/null +++ b/setup/base_rest_datamodel/odoo/addons/base_rest_datamodel @@ -0,0 +1 @@ +../../../../base_rest_datamodel \ No newline at end of file diff --git a/setup/base_rest_datamodel/setup.py b/setup/base_rest_datamodel/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/base_rest_datamodel/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/base_rest_demo/odoo/addons/base_rest_demo b/setup/base_rest_demo/odoo/addons/base_rest_demo new file mode 120000 index 00000000..cad58c8b --- /dev/null +++ b/setup/base_rest_demo/odoo/addons/base_rest_demo @@ -0,0 +1 @@ +../../../../base_rest_demo \ No newline at end of file diff --git a/setup/base_rest_demo/setup.py b/setup/base_rest_demo/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/base_rest_demo/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/base_rest_pydantic/odoo/addons/base_rest_pydantic b/setup/base_rest_pydantic/odoo/addons/base_rest_pydantic new file mode 120000 index 00000000..07264e9f --- /dev/null +++ b/setup/base_rest_pydantic/odoo/addons/base_rest_pydantic @@ -0,0 +1 @@ +../../../../base_rest_pydantic \ No newline at end of file diff --git a/setup/base_rest_pydantic/setup.py b/setup/base_rest_pydantic/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/base_rest_pydantic/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/datamodel/odoo/addons/datamodel b/setup/datamodel/odoo/addons/datamodel new file mode 120000 index 00000000..790184d6 --- /dev/null +++ b/setup/datamodel/odoo/addons/datamodel @@ -0,0 +1 @@ +../../../../datamodel \ No newline at end of file diff --git a/setup/datamodel/setup.py b/setup/datamodel/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/datamodel/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/extendable/odoo/addons/extendable b/setup/extendable/odoo/addons/extendable new file mode 120000 index 00000000..967b5984 --- /dev/null +++ b/setup/extendable/odoo/addons/extendable @@ -0,0 +1 @@ +../../../../extendable \ No newline at end of file diff --git a/setup/extendable/setup.py b/setup/extendable/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/extendable/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/pydantic/odoo/addons/pydantic b/setup/pydantic/odoo/addons/pydantic new file mode 120000 index 00000000..775eac29 --- /dev/null +++ b/setup/pydantic/odoo/addons/pydantic @@ -0,0 +1 @@ +../../../../pydantic \ No newline at end of file diff --git a/setup/pydantic/setup.py b/setup/pydantic/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/pydantic/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 640e90e034c97460ad203cb98f0512478fbb2226 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Mon, 28 Nov 2022 18:50:51 +0530 Subject: [PATCH 2/6] [MIG] base_rest, base_rest_auth_api_key, base_rest_datamodel, base_rest_demo, base_rest_pydantic, datamodel, extendable: Migration to 16.0 Co-authored-by: Nikul-OSI --- base_rest/__init__.py | 1 + base_rest/__manifest__.py | 9 +- .../apispec/base_rest_service_apispec.py | 6 +- base_rest/apispec/rest_method_param_plugin.py | 6 +- .../apispec/rest_method_security_plugin.py | 2 +- base_rest/components/service.py | 4 +- base_rest/controllers/main.py | 72 ++++----- base_rest/http.py | 149 ++++++++++-------- base_rest/models/rest_service_registration.py | 29 +++- base_rest/restapi.py | 2 +- base_rest/static/src/js/swagger.js | 16 ++ base_rest/static/src/js/swagger_ui.js | 2 +- base_rest/static/src/scss/base_rest.scss | 4 + base_rest/tests/common.py | 23 +-- base_rest/tests/test_controller_builder.py | 71 ++++++--- base_rest/views/openapi_template.xml | 11 -- base_rest_auth_api_key/__manifest__.py | 2 +- base_rest_datamodel/__manifest__.py | 4 +- base_rest_datamodel/tests/test_from_params.py | 2 +- base_rest_datamodel/tests/test_response.py | 3 +- base_rest_demo/__manifest__.py | 6 +- .../services/partner_new_api_services.py | 30 ++++ base_rest_demo/tests/__init__.py | 1 + base_rest_demo/tests/test_controller.py | 29 +--- base_rest_demo/tests/test_exception.py | 24 +-- base_rest_demo/tests/test_openapi.py | 6 +- base_rest_demo/tests/test_service.py | 54 +++++++ base_rest_pydantic/__manifest__.py | 4 +- base_rest_pydantic/tests/test_from_params.py | 3 +- base_rest_pydantic/tests/test_response.py | 3 +- datamodel/__manifest__.py | 5 +- datamodel/tests/test_build_datamodel.py | 3 +- extendable/__manifest__.py | 3 +- extendable/models/ir_http.py | 4 +- pydantic/__manifest__.py | 2 +- 35 files changed, 357 insertions(+), 238 deletions(-) create mode 100644 base_rest/static/src/js/swagger.js create mode 100644 base_rest_demo/tests/test_service.py diff --git a/base_rest/__init__.py b/base_rest/__init__.py index 431f67ab..d82e618a 100644 --- a/base_rest/__init__.py +++ b/base_rest/__init__.py @@ -1,3 +1,4 @@ +import logging from . import models from . import components from . import http diff --git a/base_rest/__manifest__.py b/base_rest/__manifest__.py index 5e57d669..e612d796 100644 --- a/base_rest/__manifest__.py +++ b/base_rest/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ Develop your own high level REST APIs for Odoo thanks to this addon. """, - "version": "15.0.1.2.0", + "version": "16.0.1.0.0", "development_status": "Beta", "license": "LGPL-3", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", @@ -18,18 +18,19 @@ "views/base_rest_view.xml", ], "assets": { - "web.assets_common": [ + "web.assets_frontend": [ "base_rest/static/src/scss/base_rest.scss", "base_rest/static/src/js/swagger_ui.js", + "base_rest/static/src/js/swagger.js", ], }, - "demo": [], "external_dependencies": { "python": [ "cerberus", "pyquerystring", "parse-accept-language", - "apispec>=4.0.0", + # adding version causes missing-manifest-dependency false positives + "apispec", ] }, "installable": True, diff --git a/base_rest/apispec/base_rest_service_apispec.py b/base_rest/apispec/base_rest_service_apispec.py index 94ebb2eb..02f30ffe 100644 --- a/base_rest/apispec/base_rest_service_apispec.py +++ b/base_rest/apispec/base_rest_service_apispec.py @@ -62,18 +62,18 @@ def _get_plugins(self): def _add_method_path(self, method): description = textwrap.dedent(method.__doc__ or "") - routing = method.routing + routing = method.original_routing for paths, method in routing["routes"]: for path in paths: self.path( path, operations={method.lower(): {"summary": description}}, - routing=routing, + original_routing=routing, ) def generate_paths(self): for _name, method in inspect.getmembers(self._service, inspect.ismethod): - routing = getattr(method, "routing", None) + routing = getattr(method, "original_routing", None) if not routing: continue self._add_method_path(method) diff --git a/base_rest/apispec/rest_method_param_plugin.py b/base_rest/apispec/rest_method_param_plugin.py index 64ad6503..68746ef1 100644 --- a/base_rest/apispec/rest_method_param_plugin.py +++ b/base_rest/apispec/rest_method_param_plugin.py @@ -25,7 +25,7 @@ def init_spec(self, spec): self.openapi_version = spec.openapi_version def operation_helper(self, path=None, operations=None, **kwargs): - routing = kwargs.get("routing") + routing = kwargs.get("original_routing") if not routing: super(RestMethodParamPlugin, self).operation_helper( path, operations, **kwargs @@ -33,14 +33,14 @@ def operation_helper(self, path=None, operations=None, **kwargs): if not operations: return for method, params in operations.items(): - parameters = self._generate_pamareters(routing, method, params) + parameters = self._generate_parameters(routing, method, params) if parameters: params["parameters"] = parameters responses = self._generate_responses(routing, method, params) if responses: params["responses"] = responses - def _generate_pamareters(self, routing, method, params): + def _generate_parameters(self, routing, method, params): parameters = params.get("parameters", []) # add default paramters provided by the sevice parameters.extend(self._default_parameters) diff --git a/base_rest/apispec/rest_method_security_plugin.py b/base_rest/apispec/rest_method_security_plugin.py index 0c2568fa..632f7cc7 100644 --- a/base_rest/apispec/rest_method_security_plugin.py +++ b/base_rest/apispec/rest_method_security_plugin.py @@ -23,7 +23,7 @@ def init_spec(self, spec): spec.components.security_scheme("user", user_scheme) def operation_helper(self, path=None, operations=None, **kwargs): - routing = kwargs.get("routing") + routing = kwargs.get("original_routing") if not routing: super(RestMethodSecurityPlugin, self).operation_helper( path, operations, **kwargs diff --git a/base_rest/components/service.py b/base_rest/components/service.py index 3793b3c8..e04a39da 100644 --- a/base_rest/components/service.py +++ b/base_rest/components/service.py @@ -93,7 +93,7 @@ def _prepare_input_params(self, method, params): method_name = method.__name__ if hasattr(method, "skip_secure_params"): return params - routing = getattr(method, "routing", None) + routing = getattr(method, "original_routing", None) if not routing: _logger.warning( "Method %s is not a public method of service %s", @@ -122,7 +122,7 @@ def _prepare_response(self, method, result): method_name = method.__name__ if hasattr(method, "skip_secure_response"): return result - routing = getattr(method, "routing", None) + routing = getattr(method, "original_routing", None) output_param = routing["output_param"] if not output_param: _logger.warning( diff --git a/base_rest/controllers/main.py b/base_rest/controllers/main.py index 3bfa0699..b4166d74 100644 --- a/base_rest/controllers/main.py +++ b/base_rest/controllers/main.py @@ -7,7 +7,7 @@ from werkzeug.exceptions import BadRequest from odoo import models -from odoo.http import Controller, ControllerType, Response, request +from odoo.http import Controller, Response, request from odoo.addons.component.core import WorkContext, _get_addon_name @@ -25,43 +25,7 @@ def __init__(self, name, env): self.id = None -class RestControllerType(ControllerType): - - # pylint: disable=E0213 - def __init__(cls, name, bases, attrs): # noqa: B902 - if ( - "RestController" in globals() - and RestController in bases - and Controller not in bases - ): - # to be registered as a controller into the ControllerType, - # our RestConrtroller must be a direct child of Controller - bases += (Controller,) - super(RestControllerType, cls).__init__(name, bases, attrs) - 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) - collection_name = getattr(cls, "_collection_name", None) - if root_path and collection_name: - cls._module = _get_addon_name(cls.__module__) - _rest_controllers_per_module[cls._module].append( - { - "root_path": root_path, - "collection_name": collection_name, - "controller_class": cls, - } - ) - _logger.debug( - "Added rest controller %s for module %s", - _rest_controllers_per_module[cls._module][-1], - cls._module, - ) - - -class RestController(Controller, metaclass=RestControllerType): +class RestController(Controller): """Generic REST Controller This controller is the base controller used by as base controller for all the REST @@ -130,6 +94,38 @@ class ControllerB(ControllerB): _component_context_provider = "component_context_provider" + @classmethod + def __init_subclass__(cls): + if ( + "RestController" in globals() + and RestController in cls.__bases__ + and Controller not in cls.__bases__ + ): + # Ensure that Controller's __init_subclass__ kicks in. + cls.__bases__ += (Controller,) + super().__init_subclass__() + if "RestController" not in globals() or not any( + issubclass(b, RestController) for b in cls.__bases__ + ): + return + # register the rest controller into the rest controllers registry + root_path = getattr(cls, "_root_path", None) + collection_name = getattr(cls, "_collection_name", None) + if root_path and collection_name: + cls._module = _get_addon_name(cls.__module__) + _rest_controllers_per_module[cls._module].append( + { + "root_path": root_path, + "collection_name": collection_name, + "controller_class": cls, + } + ) + _logger.debug( + "Added rest controller %s for module %s", + _rest_controllers_per_module[cls._module][-1], + cls._module, + ) + def _get_component_context(self, collection=None): """ This method can be inherited to add parameter into the component diff --git a/base_rest/http.py b/base_rest/http.py index 4a48aa8b..5dfef3d0 100644 --- a/base_rest/http.py +++ b/base_rest/http.py @@ -11,6 +11,7 @@ import traceback from collections import defaultdict +from markupsafe import escape from werkzeug.exceptions import ( BadRequest, Forbidden, @@ -19,9 +20,7 @@ NotFound, Unauthorized, ) -from werkzeug.utils import escape -import odoo from odoo.exceptions import ( AccessDenied, AccessError, @@ -29,12 +28,16 @@ UserError, ValidationError, ) -from odoo.http import HttpRequest, Root, SessionExpiredException, request +from odoo.http import ( + CSRF_FREE_METHODS, + MISSING_CSRF_WARNING, + Dispatcher, + SessionExpiredException, + request, +) from odoo.tools import ustr from odoo.tools.config import config -from .core import _rest_services_routes - _logger = logging.getLogger(__name__) try: @@ -69,7 +72,7 @@ def wrapJsonException(exception, include_description=False, extra_info=None): get_original_headers = exception.get_headers exception.traceback = "".join(traceback.format_exception(*sys.exc_info())) - def get_body(environ=None): + def get_body(environ=None, scope=None): res = {"code": exception.code, "name": escape(exception.name)} description = exception.get_description(environ) if config.get_misc("base_rest", "dev_mode"): @@ -80,7 +83,7 @@ def get_body(environ=None): res.update(extra_info or {}) return JSONEncoder().encode(res) - def get_headers(environ=None): + def get_headers(environ=None, scope=None): """Get a list of headers.""" _headers = [("Content-Type", "application/json")] for key, value in get_original_headers(environ=environ): @@ -116,29 +119,60 @@ def get_headers(environ=None): return exception -class HttpRestRequest(HttpRequest): - """Http request that always return json, usefull for rest api""" - - def __init__(self, httprequest): - super(HttpRestRequest, self).__init__(httprequest) - if self.httprequest.mimetype == "application/json": - data = self.httprequest.get_data().decode(self.httprequest.charset) - try: - self.params = json.loads(data) - except ValueError as e: - msg = "Invalid JSON data: %s" % str(e) - _logger.info("%s: %s", self.httprequest.path, msg) - raise BadRequest(msg) from e - elif self.httprequest.mimetype == "multipart/form-data": +class RestApiDispatcher(Dispatcher): + """Dispatcher for requests at routes for restapi types""" + + routing_type = "restapi" + + def pre_dispatch(self, rule, args): + res = super().pre_dispatch(rule, args) + httprequest = self.request.httprequest + self.request.params = args + if httprequest.mimetype == "application/json": + data = httprequest.get_data().decode(httprequest.charset) + if data: + try: + self.request.params.update(json.loads(data)) + except (ValueError, json.decoder.JSONDecodeError) as e: + msg = "Invalid JSON data: %s" % str(e) + _logger.info("%s: %s", self.request.httprequest.path, msg) + raise BadRequest(msg) from e + elif httprequest.mimetype == "multipart/form-data": # Do not reassign self.params pass else: # We reparse the query_string in order to handle data structure # more information on https://github.com/aventurella/pyquerystring - self.params = pyquerystring.parse( - self.httprequest.query_string.decode("utf-8") + self.request.params.update( + pyquerystring.parse(httprequest.query_string.decode("utf-8")) ) self._determine_context_lang() + return res + + def dispatch(self, endpoint, args): + """Same as odoo.http.HttpDispatcher, except for the early db check""" + params = dict(self.request.get_http_params(), **args) + + # Check for CSRF token for relevant requests + if ( + self.request.httprequest.method not in CSRF_FREE_METHODS + and endpoint.routing.get("csrf", True) + ): + token = params.pop("csrf_token", None) + if not self.request.validate_csrf(token): + if token is not None: + _logger.warning( + "CSRF validation failed on path '%s'", + self.request.httprequest.path, + ) + else: + _logger.warning(MISSING_CSRF_WARNING, request.httprequest.path) + raise BadRequest("Session expired (invalid CSRF token)") + + if self.request.db: + return self.request.registry["ir.http"]._dispatch(endpoint) + else: + return endpoint(**self.request.params) def _determine_context_lang(self): """ @@ -147,13 +181,13 @@ def _determine_context_lang(self): according to the priority of languages into the headers and those available into Odoo. """ - accepted_langs = self.httprequest.headers.get("Accept-language") + accepted_langs = self.request.httprequest.headers.get("Accept-language") if not accepted_langs: return parsed_accepted_langs = parse_accept_language(accepted_langs) installed_locale_langs = set() installed_locale_by_lang = defaultdict(list) - for lang_code, _name in self.env["res.lang"].get_installed(): + for lang_code, _name in self.request.env["res.lang"].get_installed(): installed_locale_langs.add(lang_code) installed_locale_by_lang[lang_code.split("_")[0]].append(lang_code) @@ -173,14 +207,14 @@ def _determine_context_lang(self): locale = locales[0] if locale: # reset the context to put our new lang. - context = dict(self._context) - context["lang"] = locale - # the setter defiend in odoo.http.WebRequest reset the env - # when setting a new context - self.context = context + self.request.update_context(lang=locale) break - def _handle_exception(self, exception): + @classmethod + def is_compatible_with(cls, request): + return True + + def handle_error(self, exception): """Called within an except block to allow converting exceptions to abitrary responses. Anything returned (except None) will be used as response.""" @@ -188,25 +222,23 @@ def _handle_exception(self, exception): # we don't want to return the login form as plain html page # we want to raise a proper exception return wrapJsonException(Unauthorized(ustr(exception))) - try: - return super(HttpRestRequest, self)._handle_exception(exception) - except MissingError as e: - extra_info = getattr(e, "rest_json_info", None) - return wrapJsonException(NotFound(ustr(e)), extra_info=extra_info) - except (AccessError, AccessDenied) as e: - extra_info = getattr(e, "rest_json_info", None) - return wrapJsonException(Forbidden(ustr(e)), extra_info=extra_info) - except (UserError, ValidationError) as e: - extra_info = getattr(e, "rest_json_info", None) + if isinstance(exception, MissingError): + extra_info = getattr(exception, "rest_json_info", None) + return wrapJsonException(NotFound(ustr(exception)), extra_info=extra_info) + if isinstance(exception, (AccessError, AccessDenied)): + extra_info = getattr(exception, "rest_json_info", None) + return wrapJsonException(Forbidden(ustr(exception)), extra_info=extra_info) + if isinstance(exception, (UserError, ValidationError)): + extra_info = getattr(exception, "rest_json_info", None) return wrapJsonException( - BadRequest(e.args[0]), include_description=True, extra_info=extra_info + BadRequest(exception.args[0]), + include_description=True, + extra_info=extra_info, ) - except HTTPException as e: - extra_info = getattr(e, "rest_json_info", None) - return wrapJsonException(e, extra_info=extra_info) - except Exception as e: # flake8: noqa: E722 - extra_info = getattr(e, "rest_json_info", None) - return wrapJsonException(InternalServerError(e), extra_info=extra_info) + if isinstance(exception, HTTPException): + return exception + extra_info = getattr(exception, "rest_json_info", None) + return wrapJsonException(InternalServerError(exception), extra_info=extra_info) def make_json_response(self, data, headers=None, cookies=None): data = JSONEncoder().encode(data) @@ -214,24 +246,3 @@ def make_json_response(self, data, headers=None, cookies=None): headers = {} headers["Content-Type"] = "application/json" return self.make_response(data, headers=headers, cookies=cookies) - - -ori_get_request = Root.get_request - - -def get_request(self, httprequest): - db = httprequest.session.db - if db and odoo.service.db.exp_db_exist(db): - # on the very first request processed by a worker, - # registry is not loaded yet - # so we enforce its loading here to make sure that - # _rest_services_databases is not empty - odoo.registry(db) - rest_routes = _rest_services_routes.get(db, []) - for root_path in rest_routes: - if httprequest.path.startswith(root_path): - return HttpRestRequest(httprequest) - return ori_get_request(self, httprequest) - - -Root.get_request = get_request diff --git a/base_rest/models/rest_service_registration.py b/base_rest/models/rest_service_registration.py index d5d6b975..6ccc829e 100644 --- a/base_rest/models/rest_service_registration.py +++ b/base_rest/models/rest_service_registration.py @@ -32,7 +32,7 @@ from ..tools import _inspect_methods # Decorator attribute added on a route function (cfr Odoo's route) -ROUTING_DECORATOR_ATTR = "routing" +ROUTING_DECORATOR_ATTR = "original_routing" _logger = logging.getLogger(__name__) @@ -61,11 +61,11 @@ def _register_hook(self): self.build_registry(services_registry) # we also have to remove the RestController from the # controller_per_module registry since it's an abstract controller - controllers = http.controllers_per_module["base_rest"] + controllers = http.Controller.children_classes["base_rest"] controllers = [ - (name, cls) for name, cls in controllers if "RestController" not in name + cls for cls in controllers if "RestController" not in cls.__name__ ] - http.controllers_per_module["base_rest"] = controllers + http.Controller.children_classes["base_rest"] = controllers # create the final controller providing the http routes for # the services available into the current database self._build_controllers_routes(services_registry) @@ -100,11 +100,23 @@ def _build_controller(self, service, controller_def): # instruct the registry that our fake addon is part of the loaded # modules + # TODO: causes + # Traceback (most recent call last): + # File "/home/odoo/odoo/service/server.py", line 1310, in preload_registries + # post_install_suite = loader.make_suite(module_names, 'post_install') + # File "/home/odoo/odoo/tests/loader.py", line 58, in make_suite + # return OdooSuite(sorted(tests, key=lambda t: t.test_sequence)) + # File "/home/odoo/odoo/tests/loader.py", line 54, in + # for m in get_test_modules(module_name) + # File "/home/odoo/odoo/tests/loader.py", line 22, in get_test_modules + # results = _get_tests_modules(importlib.util.find_spec(f'odoo.addons.{module}')) + # File "/home/odoo/odoo/tests/loader.py", line 31, in _get_tests_modules + # spec = importlib.util.find_spec('.tests', mod.name) + # AttributeError: 'NoneType' object has no attribute 'name' self.env.registry._init_modules.add(addon_name) # 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) + http.Controller.children_classes[addon_name].append(ctrl_cls) self._apply_defaults_to_controller_routes(controller_class=ctrl_cls) def _apply_defaults_to_controller_routes(self, controller_class): @@ -398,9 +410,9 @@ def _generate_methods(self): path_sep = "/" root_path = "{}{}{}".format(root_path, path_sep, self._service._usage) for name, method in _inspect_methods(self._service.__class__): - if not hasattr(method, "routing"): + if not hasattr(method, "original_routing"): continue - routing = method.routing + routing = method.original_routing for routes, http_method in routing["routes"]: method_name = "{}_{}".format(http_method.lower(), name) default_route = routes[0] @@ -424,6 +436,7 @@ def _generate_methods(self): route_params = dict( route=["{}{}".format(root_path, r) for r in routes], methods=[http_method], + type="restapi", ) for attr in {"auth", "cors", "csrf", "save_session"}: if attr in routing: diff --git a/base_rest/restapi.py b/base_rest/restapi.py index 799d4418..e7dfbe7d 100644 --- a/base_rest/restapi.py +++ b/base_rest/restapi.py @@ -104,7 +104,7 @@ def response_wrap(*args, **kw): response = f(*args, **kw) return response - response_wrap.routing = routing + response_wrap.original_routing = routing response_wrap.original_func = f return response_wrap diff --git a/base_rest/static/src/js/swagger.js b/base_rest/static/src/js/swagger.js new file mode 100644 index 00000000..f2845fb8 --- /dev/null +++ b/base_rest/static/src/js/swagger.js @@ -0,0 +1,16 @@ +odoo.define("base_rest.swagger", function (require) { + "use strict"; + + var publicWidget = require("web.public.widget"); + var SwaggerUi = require("base_rest.swagger_ui"); + + publicWidget.registry.Swagger = publicWidget.Widget.extend({ + selector: "#swagger-ui", + start: function () { + var def = this._super.apply(this, arguments); + var swagger_ui = new SwaggerUi("#swagger-ui"); + swagger_ui.start(); + return def; + }, + }); +}); diff --git a/base_rest/static/src/js/swagger_ui.js b/base_rest/static/src/js/swagger_ui.js index 933c101e..c3ca1ae6 100644 --- a/base_rest/static/src/js/swagger_ui.js +++ b/base_rest/static/src/js/swagger_ui.js @@ -38,7 +38,7 @@ odoo.define("base_rest.swagger_ui", function (require) { onComplete: function () { if (this.web_btn === undefined) { this.web_btn = $( - "" + "" ); $(".topbar").prepend(this.web_btn); } diff --git a/base_rest/static/src/scss/base_rest.scss b/base_rest/static/src/scss/base_rest.scss index 47b92d17..e74f2c31 100644 --- a/base_rest/static/src/scss/base_rest.scss +++ b/base_rest/static/src/scss/base_rest.scss @@ -1,3 +1,7 @@ +#swagger-ui { + overflow: scroll; +} + .swg-odoo-web-btn { font-family: fontAwesome !important; display: block; diff --git a/base_rest/tests/common.py b/base_rest/tests/common.py index 71ac4d43..751d755c 100644 --- a/base_rest/tests/common.py +++ b/base_rest/tests/common.py @@ -57,15 +57,15 @@ class RestServiceRegistryCase(ComponentRegistryCase): # pylint: disable=W8106 @staticmethod def _setup_registry(class_or_instance): + class_or_instance._registry_init_modules = set( + class_or_instance.env.registry._init_modules + ) ComponentRegistryCase._setup_registry(class_or_instance) class_or_instance._service_registry = RestServicesRegistry() # take a copy of registered controllers - controllers = http.controllers_per_module - http.controllers_per_module = controllers - - class_or_instance._controllers_per_module = copy.deepcopy( - http.controllers_per_module + class_or_instance._controller_children_classes = copy.deepcopy( + http.Controller.children_classes ) class_or_instance._original_addon_rest_controllers_per_module = copy.deepcopy( _rest_controllers_per_module[_get_addon_name(class_or_instance.__module__)] @@ -143,8 +143,13 @@ def my_controller_route_without_auth_2(self): @staticmethod def _teardown_registry(class_or_instance): + class_or_instance.env.registry._init_modules = ( + class_or_instance._registry_init_modules + ) ComponentRegistryCase._teardown_registry(class_or_instance) - http.controllers_per_module = class_or_instance._controllers_per_module + http.Controller.children_classes = ( + class_or_instance._controller_children_classes + ) db_name = get_db_name() _component_databases[db_name] = class_or_instance._original_components _rest_services_databases[ @@ -175,16 +180,16 @@ def _get_controller_for(service): service._collection.replace(".", "_"), service._usage.replace(".", "_"), ) - controllers = http.controllers_per_module.get(addon_name, []) + controllers = http.Controller.children_classes.get(addon_name, []) if not controllers: return - return controllers[0][1] + return controllers[0] @staticmethod def _get_controller_route_methods(controller): methods = {} for name, method in _inspect_methods(controller): - if hasattr(method, "routing"): + if hasattr(method, "original_routing"): methods[name] = method return methods diff --git a/base_rest/tests/test_controller_builder.py b/base_rest/tests/test_controller_builder.py index babac520..ac6c37d1 100644 --- a/base_rest/tests/test_controller_builder.py +++ b/base_rest/tests/test_controller_builder.py @@ -107,7 +107,7 @@ def _validator_my_instance_method(self): # the generated method_name is always the {http_method}_{method_name} method = routes["get_get"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["GET"], "auth": "public", @@ -118,12 +118,13 @@ def _validator_my_instance_method(self): "/test_controller/ping/", ], "save_session": True, + "type": "restapi", }, ) method = routes["get_search"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["GET"], "auth": "public", @@ -131,12 +132,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping/search", "/test_controller/ping/"], "save_session": True, + "type": "restapi", }, ) method = routes["post_update"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["POST"], "auth": "public", @@ -147,12 +149,13 @@ def _validator_my_instance_method(self): "/test_controller/ping/", ], "save_session": True, + "type": "restapi", }, ) method = routes["put_update"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["PUT"], "auth": "public", @@ -160,12 +163,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping/"], "save_session": True, + "type": "restapi", }, ) method = routes["post_create"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["POST"], "auth": "public", @@ -173,12 +177,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping/create", "/test_controller/ping/"], "save_session": True, + "type": "restapi", }, ) method = routes["post_delete"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["POST"], "auth": "public", @@ -186,12 +191,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping//delete"], "save_session": True, + "type": "restapi", }, ) method = routes["delete_delete"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["DELETE"], "auth": "public", @@ -199,12 +205,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping/"], "save_session": True, + "type": "restapi", }, ) method = routes["post_my_method"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["POST"], "auth": "public", @@ -212,12 +219,13 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping/my_method"], "save_session": True, + "type": "restapi", }, ) method = routes["post_my_instance_method"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["POST"], "auth": "public", @@ -225,6 +233,7 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping//my_instance_method"], "save_session": True, + "type": "restapi", }, ) @@ -284,7 +293,7 @@ def _get_partner_schema(self): method = routes["get_get"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["GET"], "auth": "public", @@ -295,12 +304,13 @@ def _get_partner_schema(self): "/test_controller/partner/", ], "save_session": True, + "type": "restapi", }, ) method = routes["get_get_name"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["GET"], "auth": "public", @@ -308,12 +318,13 @@ def _get_partner_schema(self): "csrf": False, "routes": ["/test_controller/partner//get_name"], "save_session": True, + "type": "restapi", }, ) method = routes["post_update_name"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["POST"], "auth": "user", @@ -321,6 +332,7 @@ def _get_partner_schema(self): "csrf": False, "routes": ["/test_controller/partner//change_name"], "save_session": True, + "type": "restapi", }, ) @@ -378,7 +390,7 @@ def update_name(self, _id, **params): method = routes["get_get"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["GET"], "auth": "public", @@ -389,12 +401,13 @@ def update_name(self, _id, **params): "/test_controller/partner/", ], "save_session": True, + "type": "restapi", }, ) method = routes["get_get_name"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["GET"], "auth": "public", @@ -402,12 +415,13 @@ def update_name(self, _id, **params): "csrf": False, "routes": ["/test_controller/partner//get_name"], "save_session": True, + "type": "restapi", }, ) method = routes["post_update_name"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["POST"], "auth": "user", @@ -415,6 +429,7 @@ def update_name(self, _id, **params): "csrf": False, "routes": ["/test_controller/partner//change_name"], "save_session": True, + "type": "restapi", }, ) @@ -493,24 +508,28 @@ def _validator_get(self): ("save_session", default_save_session), ]: self.assertEqual( - routes["get_new_api_method_without"].routing[attr], + routes["get_new_api_method_without"].original_routing[attr], default, "wrong %s" % attr, ) - self.assertEqual(routes["get_new_api_method_with"].routing["auth"], "public") self.assertEqual( - routes["get_new_api_method_with"].routing["cors"], "http://my_site" + routes["get_new_api_method_with"].original_routing["auth"], "public" ) self.assertEqual( - routes["get_new_api_method_with"].routing["csrf"], not default_csrf + routes["get_new_api_method_with"].original_routing["cors"], "http://my_site" ) self.assertEqual( - routes["get_new_api_method_with"].routing["save_session"], + routes["get_new_api_method_with"].original_routing["csrf"], not default_csrf + ) + self.assertEqual( + routes["get_new_api_method_with"].original_routing["save_session"], not default_save_session, ) self.assertEqual( - routes["get_get"].routing["auth"], default_auth, "wrong auth for get_get" + routes["get_get"].original_routing["auth"], + default_auth, + "wrong auth for get_get", ) for attr, default in [ @@ -520,12 +539,12 @@ def _validator_get(self): ("save_session", default_save_session), ]: self.assertEqual( - routes["my_controller_route_without"].routing[attr], + routes["my_controller_route_without"].original_routing[attr], default, "wrong %s" % attr, ) - routing = routes["my_controller_route_with"].routing + routing = routes["my_controller_route_with"].original_routing for attr, value in [ ("auth", "public"), ("cors", "http://with_cors"), @@ -539,7 +558,7 @@ def _validator_get(self): "wrong %s" % attr, ) self.assertEqual( - routes["my_controller_route_without_auth_2"].routing["auth"], + routes["my_controller_route_without_auth_2"].original_routing["auth"], None, "wrong auth for my_controller_route_without_auth_2", ) @@ -584,7 +603,7 @@ def _validator_get(self): routes = self._get_controller_route_methods(controller) self.assertEqual( - routes["get_new_api_method_with_public_or"].routing["auth"], + routes["get_new_api_method_with_public_or"].original_routing["auth"], "public_or_my_default_auth", ) @@ -622,7 +641,7 @@ def _validator_get(self): routes = self._get_controller_route_methods(controller) self.assertEqual( - routes["get_new_api_method_with_public_or"].routing["auth"], + routes["get_new_api_method_with_public_or"].original_routing["auth"], "my_default_auth", ) diff --git a/base_rest/views/openapi_template.xml b/base_rest/views/openapi_template.xml index 0bcf2d90..64e53e94 100644 --- a/base_rest/views/openapi_template.xml +++ b/base_rest/views/openapi_template.xml @@ -56,17 +56,6 @@
- - diff --git a/base_rest_auth_api_key/__manifest__.py b/base_rest_auth_api_key/__manifest__.py index 20c0c2d5..6f5d4e30 100644 --- a/base_rest_auth_api_key/__manifest__.py +++ b/base_rest_auth_api_key/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ Base Rest: Add support for the auth_api_key security policy into the openapi documentation""", - "version": "15.0.1.0.0", + "version": "16.0.1.0.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/rest-framework", diff --git a/base_rest_datamodel/__manifest__.py b/base_rest_datamodel/__manifest__.py index 9ffe5c54..21b11528 100644 --- a/base_rest_datamodel/__manifest__.py +++ b/base_rest_datamodel/__manifest__.py @@ -5,13 +5,11 @@ "name": "Base Rest Datamodel", "summary": """ Datamodel binding for base_rest""", - "version": "15.0.1.1.0", + "version": "16.0.1.0.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/rest-framework", "depends": ["base_rest", "datamodel"], - "data": [], - "demo": [], "external_dependencies": {"python": ["apispec>=4.0.0", "marshmallow"]}, "installable": True, } diff --git a/base_rest_datamodel/tests/test_from_params.py b/base_rest_datamodel/tests/test_from_params.py index 348701c9..282ec210 100644 --- a/base_rest_datamodel/tests/test_from_params.py +++ b/base_rest_datamodel/tests/test_from_params.py @@ -1,6 +1,6 @@ # Copyright 2021 Wakari SRL # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -import mock +from unittest import mock from odoo.exceptions import UserError diff --git a/base_rest_datamodel/tests/test_response.py b/base_rest_datamodel/tests/test_response.py index 0424324f..731627b3 100644 --- a/base_rest_datamodel/tests/test_response.py +++ b/base_rest_datamodel/tests/test_response.py @@ -1,7 +1,8 @@ # Copyright 2021 Wakari SRL # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from unittest import mock + import marshmallow -import mock from odoo.addons.base_rest_datamodel import restapi from odoo.addons.datamodel import fields diff --git a/base_rest_demo/__manifest__.py b/base_rest_demo/__manifest__.py index 17ece5a0..a803368d 100644 --- a/base_rest_demo/__manifest__.py +++ b/base_rest_demo/__manifest__.py @@ -5,7 +5,7 @@ "name": "Base Rest Demo", "summary": """ Demo addon for Base REST""", - "version": "15.0.1.0.1", + "version": "16.0.1.0.0", "development_status": "Beta", "license": "LGPL-3", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", @@ -18,10 +18,8 @@ "component", "extendable", ], - "data": [], - "demo": [], "external_dependencies": { - "python": ["jsondiff", "extendable-pydantic", "pydantic"] + "python": ["jsondiff", "extendable-pydantic", "marshmallow", "pydantic"] }, "installable": True, } diff --git a/base_rest_demo/services/partner_new_api_services.py b/base_rest_demo/services/partner_new_api_services.py index c87e4f2f..75ec4a04 100644 --- a/base_rest_demo/services/partner_new_api_services.py +++ b/base_rest_demo/services/partner_new_api_services.py @@ -43,6 +43,36 @@ def get(self, _id): partner_info.is_company = partner.is_company return partner_info + @restapi.method( + [(["//get", "/"], "GET")], + output_param=Datamodel("partner.info"), + auth="public", + ) + def get_by_name(self, name): + """ + Get partner's information by name. + """ + partner = self.env["res.partner"].search([("name", "=", name)], limit=1) + if not partner: + raise FileNotFoundError + PartnerInfo = self.env.datamodels["partner.info"] + partner_info = PartnerInfo(partial=True) + partner_info.id = partner.id + partner_info.name = partner.name + partner_info.street = partner.street + partner_info.street2 = partner.street2 + partner_info.zip_code = partner.zip + partner_info.city = partner.city + partner_info.phone = partner.phone + partner_info.country = self.env.datamodels["country.info"]( + id=partner.country_id.id, name=partner.country_id.name + ) + partner_info.state = self.env.datamodels["state.info"]( + id=partner.state_id.id, name=partner.state_id.name + ) + partner_info.is_company = partner.is_company + return partner_info + @restapi.method( [(["/", "/search"], "GET")], input_param=Datamodel("partner.search.param"), diff --git a/base_rest_demo/tests/__init__.py b/base_rest_demo/tests/__init__.py index 12a4d728..93852ae8 100644 --- a/base_rest_demo/tests/__init__.py +++ b/base_rest_demo/tests/__init__.py @@ -1,3 +1,4 @@ from . import test_controller from . import test_openapi from . import test_exception +from . import test_service diff --git a/base_rest_demo/tests/test_controller.py b/base_rest_demo/tests/test_controller.py index f8676543..cde51d55 100644 --- a/base_rest_demo/tests/test_controller.py +++ b/base_rest_demo/tests/test_controller.py @@ -1,7 +1,8 @@ # Copyright 2018 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo.http import controllers_per_module +# from odoo.http import controllers_per_module +from odoo.http import Controller from ..controllers.main import ( BaseRestDemoJwtApiController, @@ -16,38 +17,22 @@ class TestController(CommonCase): def test_controller_registry(self): # at the end of the start process, our tow controllers must into the # controller registered - controllers = controllers_per_module["base_rest_demo"] + controllers = Controller.children_classes.get("base_rest_demo", []) self.assertEqual(len(controllers), 4) self.assertIn( - ( - "odoo.addons.base_rest_demo.controllers.main." - "BaseRestDemoPrivateApiController", - BaseRestDemoPrivateApiController, - ), + BaseRestDemoPrivateApiController, controllers, ) self.assertIn( - ( - "odoo.addons.base_rest_demo.controllers.main." - "BaseRestDemoPublicApiController", - BaseRestDemoPublicApiController, - ), + BaseRestDemoPublicApiController, controllers, ) self.assertIn( - ( - "odoo.addons.base_rest_demo.controllers.main." - "BaseRestDemoNewApiController", - BaseRestDemoNewApiController, - ), + BaseRestDemoNewApiController, controllers, ) self.assertIn( - ( - "odoo.addons.base_rest_demo.controllers.main." - "BaseRestDemoJwtApiController", - BaseRestDemoJwtApiController, - ), + BaseRestDemoJwtApiController, controllers, ) diff --git a/base_rest_demo/tests/test_exception.py b/base_rest_demo/tests/test_exception.py index d8e1aae7..3f5aa134 100644 --- a/base_rest_demo/tests/test_exception.py +++ b/base_rest_demo/tests/test_exception.py @@ -23,9 +23,13 @@ def setUp(self): super(TestException, self).setUp() self.opener.headers["Content-Type"] = "application/json" - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_user_error(self): - response = self.url_open("%s/user_error" % self.url, "{}") + response = self.url_open( + "%s/user_error" % self.url, + "{}", + headers={"Accept-language": "en-US,en;q=0.5"}, + ) self.assertEqual(response.status_code, 400) self.assertEqual(response.headers["content-type"], "application/json") body = json.loads(response.content.decode("utf-8")) @@ -38,7 +42,7 @@ def test_user_error(self): }, ) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_validation_error(self): response = self.url_open("%s/validation_error" % self.url, "{}") self.assertEqual(response.status_code, 400) @@ -53,7 +57,7 @@ def test_validation_error(self): }, ) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_session_expired(self): response = self.url_open("%s/session_expired" % self.url, "{}") self.assertEqual(response.status_code, 401) @@ -61,7 +65,7 @@ def test_session_expired(self): body = json.loads(response.content.decode("utf-8")) self.assertDictEqual(body, {"code": 401, "name": "Unauthorized"}) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_missing_error(self): response = self.url_open("%s/missing_error" % self.url, "{}") self.assertEqual(response.status_code, 404) @@ -69,7 +73,7 @@ def test_missing_error(self): body = json.loads(response.content.decode("utf-8")) self.assertDictEqual(body, {"code": 404, "name": "Not Found"}) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_access_error(self): response = self.url_open("%s/access_error" % self.url, "{}") self.assertEqual(response.status_code, 403) @@ -77,7 +81,7 @@ def test_access_error(self): body = json.loads(response.content.decode("utf-8")) self.assertDictEqual(body, {"code": 403, "name": "Forbidden"}) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_access_denied(self): response = self.url_open("%s/access_denied" % self.url, "{}") self.assertEqual(response.status_code, 403) @@ -85,15 +89,15 @@ def test_access_denied(self): body = json.loads(response.content.decode("utf-8")) self.assertDictEqual(body, {"code": 403, "name": "Forbidden"}) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_http_exception(self): response = self.url_open("%s/http_exception" % self.url, "{}") self.assertEqual(response.status_code, 405) - self.assertEqual(response.headers["content-type"], "text/html") + self.assertEqual(response.headers["content-type"], "text/html; charset=utf-8") body = response.content self.assertIn(b"Method Not Allowed", body) - @odoo.tools.mute_logger("odoo.addons.base_rest.http") + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") def test_bare_exception(self): response = self.url_open("%s/bare_exception" % self.url, "{}") self.assertEqual(response.status_code, 500) diff --git a/base_rest_demo/tests/test_openapi.py b/base_rest_demo/tests/test_openapi.py index 08ecd501..70c2d65a 100644 --- a/base_rest_demo/tests/test_openapi.py +++ b/base_rest_demo/tests/test_openapi.py @@ -11,7 +11,7 @@ def _fix_server_url(self, openapi_def): # The server url depends of base_url. base_url depends of the odoo # config url = openapi_def["servers"][0]["url"] - url.replace("http://localhost:8069", self.base_url) + url = url.replace("http://localhost:8069", self.base_url) openapi_def["servers"][0]["url"] = url def _fix_openapi_components(self, openapi_def): @@ -26,10 +26,10 @@ def _fix_openapi_components(self, openapi_def): for key in unknow_keys: del security_components[key] - def assertOpenApiDef(self, service, canocincal_json_file, default_auth): + def assertOpenApiDef(self, service, canonical_json_file, default_auth): openapi_def = service.to_openapi(default_auth=default_auth) self._fix_openapi_components(openapi_def) - canonical_def = get_canonical_json(canocincal_json_file) + canonical_def = get_canonical_json(canonical_json_file) self._fix_server_url(canonical_def) self.assertFalse(jsondiff.diff(openapi_def, canonical_def)) diff --git a/base_rest_demo/tests/test_service.py b/base_rest_demo/tests/test_service.py new file mode 100644 index 00000000..b1df78a3 --- /dev/null +++ b/base_rest_demo/tests/test_service.py @@ -0,0 +1,54 @@ +# Copyright 2023 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import json +from urllib.parse import quote + +import odoo.tools +from odoo.tests import HttpCase +from odoo.tests.common import tagged + +from odoo.addons.base_rest.tests.common import RegistryMixin + + +@tagged("-at_install", "post_install") +class TestService(HttpCase, RegistryMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.setUpRegistry() + host = "127.0.0.1" + port = odoo.tools.config["http_port"] + cls.url = "http://%s:%d/base_rest_demo_api/new_api/partner" % (host, port) + cls.partner = cls.env.ref("base.partner_demo") + # Define a subset of the partner values to check against returned payload + cls.expected_partner_values = { + "country": { + "id": cls.partner.country_id.id, + "name": cls.partner.country_id.name, + }, + "id": cls.partner.id, + "name": cls.partner.name, + "state": {"id": cls.partner.state_id.id, "name": cls.partner.state_id.name}, + } + + def test_get(self): + """Test a new api GET method""" + self.authenticate("admin", "admin") + self.opener.headers["Content-Type"] = "application/json" + response = self.url_open( + "%s/%s" % (self.url, self.partner.id), + headers={"Accept-language": "en-US,en;q=0.5"}, + ) + body = json.loads(response.content.decode("utf-8")) + self.assertEqual(body, body | self.expected_partner_values) + + def test_get_by_name(self): + """Test a new api GET method with string argument""" + self.authenticate("admin", "admin") + self.opener.headers["Content-Type"] = "application/json" + response = self.url_open( + "%s/%s" % (self.url, quote(self.partner.name)), + headers={"Accept-language": "en-US,en;q=0.5"}, + ) + body = json.loads(response.content.decode("utf-8")) + self.assertEqual(body, body | self.expected_partner_values) diff --git a/base_rest_pydantic/__manifest__.py b/base_rest_pydantic/__manifest__.py index 16f414ac..ea83d15c 100644 --- a/base_rest_pydantic/__manifest__.py +++ b/base_rest_pydantic/__manifest__.py @@ -5,13 +5,11 @@ "name": "Base Rest Datamodel", "summary": """ Pydantic binding for base_rest""", - "version": "15.0.4.3.0", + "version": "16.0.1.0.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/rest-framework", "depends": ["base_rest"], - "data": [], - "demo": [], "installable": True, "external_dependencies": { "python": [ diff --git a/base_rest_pydantic/tests/test_from_params.py b/base_rest_pydantic/tests/test_from_params.py index 2933d5f4..4316bdd8 100644 --- a/base_rest_pydantic/tests/test_from_params.py +++ b/base_rest_pydantic/tests/test_from_params.py @@ -1,8 +1,7 @@ # Copyright 2021 Wakari SRL # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). from typing import Type - -import mock +from unittest import mock from odoo.exceptions import UserError from odoo.tests.common import TransactionCase diff --git a/base_rest_pydantic/tests/test_response.py b/base_rest_pydantic/tests/test_response.py index 71a4f706..baf0ec52 100644 --- a/base_rest_pydantic/tests/test_response.py +++ b/base_rest_pydantic/tests/test_response.py @@ -1,8 +1,7 @@ # Copyright 2021 Wakari SRL # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). from typing import List - -import mock +from unittest import mock from odoo.tests.common import TransactionCase diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 902c7a3e..af37c113 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,15 +6,12 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "15.0.1.0.1", + "version": "16.0.1.0.0", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", "maintainers": ["lmignon"], "website": "https://github.com/OCA/rest-framework", - "depends": [], - "data": [], - "demo": [], "external_dependencies": {"python": ["marshmallow", "marshmallow-objects>=2.0.0"]}, "installable": True, } diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index fde04c0d..29a2002b 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -2,7 +2,8 @@ # Copyright 2019 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) -import mock +from unittest import mock + from marshmallow_objects.models import Model as MarshmallowModel from odoo import SUPERUSER_ID, api diff --git a/extendable/__manifest__.py b/extendable/__manifest__.py index f236466b..fa684dc0 100644 --- a/extendable/__manifest__.py +++ b/extendable/__manifest__.py @@ -5,13 +5,12 @@ "name": "Extendable", "summary": """ Extendable classes registry loader for Odoo""", - "version": "15.0.1.0.1", + "version": "16.0.1.0.0", "development_status": "Beta", "maintainers": ["lmignon"], "license": "LGPL-3", "author": "ACSONE SA/NV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/rest-framework", - "depends": [], "external_dependencies": {"python": ["extendable"]}, "installable": True, } diff --git a/extendable/models/ir_http.py b/extendable/models/ir_http.py index 577179c4..e050ef0e 100644 --- a/extendable/models/ir_http.py +++ b/extendable/models/ir_http.py @@ -15,9 +15,9 @@ class IrHttp(models.AbstractModel): _inherit = "ir.http" @classmethod - def _dispatch(cls): + def _dispatch(cls, endpoint): with cls._extendable_context_registry(): - return super()._dispatch() + return super()._dispatch(endpoint) @classmethod @contextmanager diff --git a/pydantic/__manifest__.py b/pydantic/__manifest__.py index 9a1296bc..811629d1 100644 --- a/pydantic/__manifest__.py +++ b/pydantic/__manifest__.py @@ -5,7 +5,7 @@ "name": "Pydantic", "summary": """ Utility addon to ease mapping between Pydantic and Odoo models""", - "version": "15.0.1.1.1", + "version": "16.0.1.0.0", "development_status": "Beta", "license": "LGPL-3", "maintainers": ["lmignon"], From 4c6417ab9e58da006ebea5950286a9fab10b1c33 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Tue, 6 Dec 2022 15:58:11 +0100 Subject: [PATCH 3/6] [RFR] base_rest: don't add fake modules to the registry --- base_rest/models/rest_service_registration.py | 19 +++---------------- base_rest/tests/common.py | 18 ++++++++---------- base_rest_demo/tests/test_controller.py | 1 - 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/base_rest/models/rest_service_registration.py b/base_rest/models/rest_service_registration.py index 6ccc829e..3f34971e 100644 --- a/base_rest/models/rest_service_registration.py +++ b/base_rest/models/rest_service_registration.py @@ -90,29 +90,16 @@ def _build_controller(self, service, controller_def): # generate an addon name used to register our new controller for # the current database - addon_name = "{}_{}_{}".format( + addon_name = base_controller_cls._module + identifier = "{}_{}_{}".format( self.env.cr.dbname, service._collection.replace(".", "_"), service._usage.replace(".", "_"), ) + base_controller_cls._identifier = identifier # put our new controller into the new addon module ctrl_cls.__module__ = "odoo.addons.{}".format(addon_name) - # instruct the registry that our fake addon is part of the loaded - # modules - # TODO: causes - # Traceback (most recent call last): - # File "/home/odoo/odoo/service/server.py", line 1310, in preload_registries - # post_install_suite = loader.make_suite(module_names, 'post_install') - # File "/home/odoo/odoo/tests/loader.py", line 58, in make_suite - # return OdooSuite(sorted(tests, key=lambda t: t.test_sequence)) - # File "/home/odoo/odoo/tests/loader.py", line 54, in - # for m in get_test_modules(module_name) - # File "/home/odoo/odoo/tests/loader.py", line 22, in get_test_modules - # results = _get_tests_modules(importlib.util.find_spec(f'odoo.addons.{module}')) - # File "/home/odoo/odoo/tests/loader.py", line 31, in _get_tests_modules - # spec = importlib.util.find_spec('.tests', mod.name) - # AttributeError: 'NoneType' object has no attribute 'name' self.env.registry._init_modules.add(addon_name) # register our conroller into the list of available controllers diff --git a/base_rest/tests/common.py b/base_rest/tests/common.py index 751d755c..4d7e9f5f 100644 --- a/base_rest/tests/common.py +++ b/base_rest/tests/common.py @@ -57,9 +57,6 @@ class RestServiceRegistryCase(ComponentRegistryCase): # pylint: disable=W8106 @staticmethod def _setup_registry(class_or_instance): - class_or_instance._registry_init_modules = set( - class_or_instance.env.registry._init_modules - ) ComponentRegistryCase._setup_registry(class_or_instance) class_or_instance._service_registry = RestServicesRegistry() @@ -143,9 +140,6 @@ def my_controller_route_without_auth_2(self): @staticmethod def _teardown_registry(class_or_instance): - class_or_instance.env.registry._init_modules = ( - class_or_instance._registry_init_modules - ) ComponentRegistryCase._teardown_registry(class_or_instance) http.Controller.children_classes = ( class_or_instance._controller_children_classes @@ -174,16 +168,20 @@ def _build_services(class_or_instance, *classes): ) @staticmethod - def _get_controller_for(service): - addon_name = "{}_{}_{}".format( + def _get_controller_for(service, addon="base_rest"): + identifier = "{}_{}_{}".format( get_db_name(), service._collection.replace(".", "_"), service._usage.replace(".", "_"), ) - controllers = http.Controller.children_classes.get(addon_name, []) + controllers = [ + controller + for controller in http.Controller.children_classes.get(addon, []) + if getattr(controller, "_identifier", None) == identifier + ] if not controllers: return - return controllers[0] + return controllers[-1] @staticmethod def _get_controller_route_methods(controller): diff --git a/base_rest_demo/tests/test_controller.py b/base_rest_demo/tests/test_controller.py index cde51d55..678e5261 100644 --- a/base_rest_demo/tests/test_controller.py +++ b/base_rest_demo/tests/test_controller.py @@ -18,7 +18,6 @@ def test_controller_registry(self): # at the end of the start process, our tow controllers must into the # controller registered controllers = Controller.children_classes.get("base_rest_demo", []) - self.assertEqual(len(controllers), 4) self.assertIn( BaseRestDemoPrivateApiController, From aae391667a055ab5a814dc7b85a585a2c3def54a Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Tue, 6 Dec 2022 20:03:40 +0100 Subject: [PATCH 4/6] [RFR] base_rest is deprecated --- base_rest/__init__.py | 6 ++++++ base_rest/__manifest__.py | 2 +- base_rest/readme/DESCRIPTION.rst | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/base_rest/__init__.py b/base_rest/__init__.py index d82e618a..5947223b 100644 --- a/base_rest/__init__.py +++ b/base_rest/__init__.py @@ -2,3 +2,9 @@ from . import models from . import components from . import http + +logging.getLogger(__file__).warning( + "base_rest is deprecated and not fully supported anymore on Odoo 16. " + "Please migrate to the FastAPI migration module. " + "See https://github.com/OCA/rest-framework/pull/291.", +) diff --git a/base_rest/__manifest__.py b/base_rest/__manifest__.py index e612d796..f29fe6ef 100644 --- a/base_rest/__manifest__.py +++ b/base_rest/__manifest__.py @@ -10,7 +10,7 @@ "development_status": "Beta", "license": "LGPL-3", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", - "maintainers": ["lmignon"], + "maintainers": [], "website": "https://github.com/OCA/rest-framework", "depends": ["component", "web"], "data": [ diff --git a/base_rest/readme/DESCRIPTION.rst b/base_rest/readme/DESCRIPTION.rst index 96f88c97..28c8759f 100644 --- a/base_rest/readme/DESCRIPTION.rst +++ b/base_rest/readme/DESCRIPTION.rst @@ -1,3 +1,7 @@ +This addon is deprecated and not fully supported anymore on Odoo 16. +Please migrate to the FastAPI migration module. +See https://github.com/OCA/rest-framework/pull/291. + This addon provides the basis to develop high level REST APIs for Odoo. As Odoo becomes one of the central pieces of enterprise IT systems, it often From f240419837cc9c5051b33c2cb7560acbf84f0dd9 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Wed, 7 Dec 2022 16:17:29 +0100 Subject: [PATCH 5/6] [RFR] base_rest: use routing attribute constant throughout --- .../apispec/base_rest_service_apispec.py | 7 +- base_rest/apispec/rest_method_param_plugin.py | 3 +- .../apispec/rest_method_security_plugin.py | 4 +- base_rest/components/service.py | 5 +- base_rest/models/rest_service_registration.py | 10 +-- base_rest/restapi.py | 4 +- base_rest/tests/common.py | 4 +- base_rest/tests/test_controller_builder.py | 68 ++++++++++++------- base_rest/tools.py | 2 + 9 files changed, 63 insertions(+), 44 deletions(-) diff --git a/base_rest/apispec/base_rest_service_apispec.py b/base_rest/apispec/base_rest_service_apispec.py index 02f30ffe..72276d40 100644 --- a/base_rest/apispec/base_rest_service_apispec.py +++ b/base_rest/apispec/base_rest_service_apispec.py @@ -7,6 +7,7 @@ from apispec import APISpec from ..core import _rest_services_databases +from ..tools import ROUTING_DECORATOR_ATTR from .rest_method_param_plugin import RestMethodParamPlugin from .rest_method_security_plugin import RestMethodSecurityPlugin from .restapi_method_route_plugin import RestApiMethodRoutePlugin @@ -62,18 +63,18 @@ def _get_plugins(self): def _add_method_path(self, method): description = textwrap.dedent(method.__doc__ or "") - routing = method.original_routing + routing = getattr(method, ROUTING_DECORATOR_ATTR) for paths, method in routing["routes"]: for path in paths: self.path( path, operations={method.lower(): {"summary": description}}, - original_routing=routing, + **{ROUTING_DECORATOR_ATTR: routing}, ) def generate_paths(self): for _name, method in inspect.getmembers(self._service, inspect.ismethod): - routing = getattr(method, "original_routing", None) + routing = getattr(method, ROUTING_DECORATOR_ATTR, None) if not routing: continue self._add_method_path(method) diff --git a/base_rest/apispec/rest_method_param_plugin.py b/base_rest/apispec/rest_method_param_plugin.py index 68746ef1..dde70782 100644 --- a/base_rest/apispec/rest_method_param_plugin.py +++ b/base_rest/apispec/rest_method_param_plugin.py @@ -4,6 +4,7 @@ from apispec import BasePlugin from ..restapi import RestMethodParam +from ..tools import ROUTING_DECORATOR_ATTR class RestMethodParamPlugin(BasePlugin): @@ -25,7 +26,7 @@ def init_spec(self, spec): self.openapi_version = spec.openapi_version def operation_helper(self, path=None, operations=None, **kwargs): - routing = kwargs.get("original_routing") + routing = kwargs.get(ROUTING_DECORATOR_ATTR) if not routing: super(RestMethodParamPlugin, self).operation_helper( path, operations, **kwargs diff --git a/base_rest/apispec/rest_method_security_plugin.py b/base_rest/apispec/rest_method_security_plugin.py index 632f7cc7..bdc3d0e5 100644 --- a/base_rest/apispec/rest_method_security_plugin.py +++ b/base_rest/apispec/rest_method_security_plugin.py @@ -3,6 +3,8 @@ from apispec import BasePlugin +from ..tools import ROUTING_DECORATOR_ATTR + class RestMethodSecurityPlugin(BasePlugin): """ @@ -23,7 +25,7 @@ def init_spec(self, spec): spec.components.security_scheme("user", user_scheme) def operation_helper(self, path=None, operations=None, **kwargs): - routing = kwargs.get("original_routing") + routing = kwargs.get(ROUTING_DECORATOR_ATTR) if not routing: super(RestMethodSecurityPlugin, self).operation_helper( path, operations, **kwargs diff --git a/base_rest/components/service.py b/base_rest/components/service.py index e04a39da..ec9d1ba9 100644 --- a/base_rest/components/service.py +++ b/base_rest/components/service.py @@ -11,6 +11,7 @@ from odoo.addons.component.core import AbstractComponent from ..apispec.base_rest_service_apispec import BaseRestServiceAPISpec +from ..tools import ROUTING_DECORATOR_ATTR _logger = logging.getLogger(__name__) @@ -93,7 +94,7 @@ def _prepare_input_params(self, method, params): method_name = method.__name__ if hasattr(method, "skip_secure_params"): return params - routing = getattr(method, "original_routing", None) + routing = getattr(method, ROUTING_DECORATOR_ATTR, None) if not routing: _logger.warning( "Method %s is not a public method of service %s", @@ -122,7 +123,7 @@ def _prepare_response(self, method, result): method_name = method.__name__ if hasattr(method, "skip_secure_response"): return result - routing = getattr(method, "original_routing", None) + routing = getattr(method, ROUTING_DECORATOR_ATTR, None) output_param = routing["output_param"] if not output_param: _logger.warning( diff --git a/base_rest/models/rest_service_registration.py b/base_rest/models/rest_service_registration.py index 3f34971e..ee44c3dd 100644 --- a/base_rest/models/rest_service_registration.py +++ b/base_rest/models/rest_service_registration.py @@ -29,11 +29,7 @@ _rest_services_databases, _rest_services_routes, ) -from ..tools import _inspect_methods - -# Decorator attribute added on a route function (cfr Odoo's route) -ROUTING_DECORATOR_ATTR = "original_routing" - +from ..tools import ROUTING_DECORATOR_ATTR, _inspect_methods _logger = logging.getLogger(__name__) @@ -397,9 +393,9 @@ def _generate_methods(self): path_sep = "/" root_path = "{}{}{}".format(root_path, path_sep, self._service._usage) for name, method in _inspect_methods(self._service.__class__): - if not hasattr(method, "original_routing"): + routing = getattr(method, ROUTING_DECORATOR_ATTR, None) + if routing is None: continue - routing = method.original_routing for routes, http_method in routing["routes"]: method_name = "{}_{}".format(http_method.lower(), name) default_route = routes[0] diff --git a/base_rest/restapi.py b/base_rest/restapi.py index e7dfbe7d..ce61e702 100644 --- a/base_rest/restapi.py +++ b/base_rest/restapi.py @@ -10,7 +10,7 @@ from odoo import _, http from odoo.exceptions import UserError, ValidationError -from .tools import cerberus_to_json +from .tools import ROUTING_DECORATOR_ATTR, cerberus_to_json def method(routes, input_param=None, output_param=None, **kw): @@ -104,7 +104,7 @@ def response_wrap(*args, **kw): response = f(*args, **kw) return response - response_wrap.original_routing = routing + setattr(response_wrap, ROUTING_DECORATOR_ATTR, routing) response_wrap.original_func = f return response_wrap diff --git a/base_rest/tests/common.py b/base_rest/tests/common.py index 4d7e9f5f..cd242678 100644 --- a/base_rest/tests/common.py +++ b/base_rest/tests/common.py @@ -26,7 +26,7 @@ _rest_controllers_per_module, _rest_services_databases, ) -from ..tools import _inspect_methods +from ..tools import ROUTING_DECORATOR_ATTR, _inspect_methods class RegistryMixin(object): @@ -187,7 +187,7 @@ def _get_controller_for(service, addon="base_rest"): def _get_controller_route_methods(controller): methods = {} for name, method in _inspect_methods(controller): - if hasattr(method, "original_routing"): + if hasattr(method, ROUTING_DECORATOR_ATTR): methods[name] = method return methods diff --git a/base_rest/tests/test_controller_builder.py b/base_rest/tests/test_controller_builder.py index ac6c37d1..4a0d0cb3 100644 --- a/base_rest/tests/test_controller_builder.py +++ b/base_rest/tests/test_controller_builder.py @@ -5,6 +5,7 @@ from odoo.addons.component.core import Component from .. import restapi +from ..tools import ROUTING_DECORATOR_ATTR from .common import TransactionRestServiceRegistryCase @@ -107,7 +108,7 @@ def _validator_my_instance_method(self): # the generated method_name is always the {http_method}_{method_name} method = routes["get_get"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -124,7 +125,7 @@ def _validator_my_instance_method(self): method = routes["get_search"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -138,7 +139,7 @@ def _validator_my_instance_method(self): method = routes["post_update"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "public", @@ -155,7 +156,7 @@ def _validator_my_instance_method(self): method = routes["put_update"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["PUT"], "auth": "public", @@ -169,7 +170,7 @@ def _validator_my_instance_method(self): method = routes["post_create"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "public", @@ -183,7 +184,7 @@ def _validator_my_instance_method(self): method = routes["post_delete"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "public", @@ -197,7 +198,7 @@ def _validator_my_instance_method(self): method = routes["delete_delete"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["DELETE"], "auth": "public", @@ -211,7 +212,7 @@ def _validator_my_instance_method(self): method = routes["post_my_method"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "public", @@ -225,7 +226,7 @@ def _validator_my_instance_method(self): method = routes["post_my_instance_method"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "public", @@ -293,7 +294,7 @@ def _get_partner_schema(self): method = routes["get_get"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -310,7 +311,7 @@ def _get_partner_schema(self): method = routes["get_get_name"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -324,7 +325,7 @@ def _get_partner_schema(self): method = routes["post_update_name"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "user", @@ -390,7 +391,7 @@ def update_name(self, _id, **params): method = routes["get_get"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -407,7 +408,7 @@ def update_name(self, _id, **params): method = routes["get_get_name"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["GET"], "auth": "public", @@ -421,7 +422,7 @@ def update_name(self, _id, **params): method = routes["post_update_name"] self.assertDictEqual( - method.original_routing, + getattr(method, ROUTING_DECORATOR_ATTR), { "methods": ["POST"], "auth": "user", @@ -508,26 +509,33 @@ def _validator_get(self): ("save_session", default_save_session), ]: self.assertEqual( - routes["get_new_api_method_without"].original_routing[attr], + getattr(routes["get_new_api_method_without"], ROUTING_DECORATOR_ATTR)[ + attr + ], default, "wrong %s" % attr, ) self.assertEqual( - routes["get_new_api_method_with"].original_routing["auth"], "public" + getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)["auth"], + "public", ) self.assertEqual( - routes["get_new_api_method_with"].original_routing["cors"], "http://my_site" + getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)["cors"], + "http://my_site", ) self.assertEqual( - routes["get_new_api_method_with"].original_routing["csrf"], not default_csrf + getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)["csrf"], + not default_csrf, ) self.assertEqual( - routes["get_new_api_method_with"].original_routing["save_session"], + getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)[ + "save_session" + ], not default_save_session, ) self.assertEqual( - routes["get_get"].original_routing["auth"], + getattr(routes["get_get"], ROUTING_DECORATOR_ATTR)["auth"], default_auth, "wrong auth for get_get", ) @@ -539,12 +547,14 @@ def _validator_get(self): ("save_session", default_save_session), ]: self.assertEqual( - routes["my_controller_route_without"].original_routing[attr], + getattr(routes["my_controller_route_without"], ROUTING_DECORATOR_ATTR)[ + attr + ], default, "wrong %s" % attr, ) - routing = routes["my_controller_route_with"].original_routing + routing = getattr(routes["my_controller_route_with"], ROUTING_DECORATOR_ATTR) for attr, value in [ ("auth", "public"), ("cors", "http://with_cors"), @@ -558,7 +568,9 @@ def _validator_get(self): "wrong %s" % attr, ) self.assertEqual( - routes["my_controller_route_without_auth_2"].original_routing["auth"], + getattr( + routes["my_controller_route_without_auth_2"], ROUTING_DECORATOR_ATTR + )["auth"], None, "wrong auth for my_controller_route_without_auth_2", ) @@ -603,7 +615,9 @@ def _validator_get(self): routes = self._get_controller_route_methods(controller) self.assertEqual( - routes["get_new_api_method_with_public_or"].original_routing["auth"], + getattr( + routes["get_new_api_method_with_public_or"], ROUTING_DECORATOR_ATTR + )["auth"], "public_or_my_default_auth", ) @@ -641,7 +655,9 @@ def _validator_get(self): routes = self._get_controller_route_methods(controller) self.assertEqual( - routes["get_new_api_method_with_public_or"].original_routing["auth"], + getattr( + routes["get_new_api_method_with_public_or"], ROUTING_DECORATOR_ATTR + )["auth"], "my_default_auth", ) diff --git a/base_rest/tools.py b/base_rest/tools.py index 6449fcf7..92e78e85 100644 --- a/base_rest/tools.py +++ b/base_rest/tools.py @@ -6,6 +6,8 @@ _logger = logging.getLogger(__name__) +# Decorator attribute added on a route function (cfr Odoo's route) +ROUTING_DECORATOR_ATTR = "original_routing" SUPPORTED_META = ["title", "description", "example", "examples"] From 64a2d8201fe95fe65de6b7d6f7ea041d0f13871c Mon Sep 17 00:00:00 2001 From: Maxime Franco Date: Wed, 25 Jan 2023 08:30:35 +0100 Subject: [PATCH 6/6] [FIX] base_rest_demo - odoo-addon-pydantic is used inside naive_orm_model class, but was missing in dependency --- base_rest_demo/__manifest__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/base_rest_demo/__manifest__.py b/base_rest_demo/__manifest__.py index a803368d..0baff6ef 100644 --- a/base_rest_demo/__manifest__.py +++ b/base_rest_demo/__manifest__.py @@ -17,6 +17,7 @@ "base_rest_pydantic", "component", "extendable", + "pydantic", ], "external_dependencies": { "python": ["jsondiff", "extendable-pydantic", "marshmallow", "pydantic"]