diff --git a/base_rest/__init__.py b/base_rest/__init__.py index 431f67abd..d82e618a8 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 5e57d669a..e612d7961 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 94ebb2ebc..02f30ffe9 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 64ad65039..68746ef1f 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 0c2568fa2..632f7cc76 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 3793b3c8a..e04a39da1 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 3bfa06992..b4166d741 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 4a48aa8bd..23ba02e87 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,56 @@ def get_headers(environ=None): return exception -class HttpRestRequest(HttpRequest): - """Http request that always return json, usefull for rest api""" +class RestApiDispatcher(Dispatcher): + """Dispatcher for requests at routes for restapi types""" + + routing_type = "restapi" - def __init__(self, httprequest): - super(HttpRestRequest, self).__init__(httprequest) - if self.httprequest.mimetype == "application/json": - data = self.httprequest.get_data().decode(self.httprequest.charset) + def pre_dispatch(self, rule, args): + res = super().pre_dispatch(rule, args) + httprequest = self.request.httprequest + if httprequest.mimetype == "application/json": + data = httprequest.get_data().decode(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) + _logger.info("%s: %s", self.request.path, msg) raise BadRequest(msg) from e - elif self.httprequest.mimetype == "multipart/form-data": + 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.params = 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""" + self.request.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 = self.request.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 +177,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 +203,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 +218,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 +242,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 d5d6b975b..6ccc829ea 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 799d44189..e7dfbe7d2 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 000000000..f2845fb86 --- /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 933c101ee..c3ca1ae6d 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 47b92d170..e74f2c31f 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 71ac4d43b..751d755c5 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 babac5202..05fc7eb1d 100644 --- a/base_rest/tests/test_controller_builder.py +++ b/base_rest/tests/test_controller_builder.py @@ -17,6 +17,8 @@ class TestControllerBuilder(TransactionRestServiceRegistryCase): def setUp(self): super().setUp() + # Store the original value for the registry's _init_modules + # because a fake addon is put in it for every registered service self._setup_registry(self) def tearDown(self): @@ -107,7 +109,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 +120,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 +134,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 +151,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 +165,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 +179,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 +193,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 +207,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 +221,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 +235,7 @@ def _validator_my_instance_method(self): "csrf": False, "routes": ["/test_controller/ping//my_instance_method"], "save_session": True, + "type": "restapi", }, ) @@ -284,7 +295,7 @@ def _get_partner_schema(self): method = routes["get_get"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["GET"], "auth": "public", @@ -295,12 +306,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 +320,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 +334,7 @@ def _get_partner_schema(self): "csrf": False, "routes": ["/test_controller/partner//change_name"], "save_session": True, + "type": "restapi", }, ) @@ -378,7 +392,7 @@ def update_name(self, _id, **params): method = routes["get_get"] self.assertDictEqual( - method.routing, + method.original_routing, { "methods": ["GET"], "auth": "public", @@ -389,12 +403,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 +417,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 +431,7 @@ def update_name(self, _id, **params): "csrf": False, "routes": ["/test_controller/partner//change_name"], "save_session": True, + "type": "restapi", }, ) @@ -493,24 +510,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 +541,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 +560,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 +605,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 +643,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 0bcf2d906..64e53e946 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 20c0c2d5b..6f5d4e301 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 9ffe5c544..21b11528c 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 348701c9b..282ec210b 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 0424324f5..731627b35 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 17ece5a02..a803368d5 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/tests/test_controller.py b/base_rest_demo/tests/test_controller.py index f86765430..cde51d550 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 d8e1aae73..3f5aa134d 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 08ecd5013..70c2d65a0 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_pydantic/__manifest__.py b/base_rest_pydantic/__manifest__.py index 16f414ac2..ea83d15c9 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 2933d5f4e..4316bdd8b 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 71a4f7063..baf0ec52c 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 902c7a3eb..af37c113d 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 fde04c0d2..29a2002b8 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 f236466b8..fa684dc0a 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 577179c46..e050ef0e2 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 9a1296bca..811629d14 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"],