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/__init__.py b/base_rest/__init__.py
index 431f67ab..5947223b 100644
--- a/base_rest/__init__.py
+++ b/base_rest/__init__.py
@@ -1,3 +1,10 @@
+import logging
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 fb9e6a1a..f29fe6ef 100644
--- a/base_rest/__manifest__.py
+++ b/base_rest/__manifest__.py
@@ -6,11 +6,11 @@
"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)",
- "maintainers": ["lmignon"],
+ "maintainers": [],
"website": "https://github.com/OCA/rest-framework",
"depends": ["component", "web"],
"data": [
@@ -18,19 +18,20 @@
"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": False,
+ "installable": True,
}
diff --git a/base_rest/apispec/base_rest_service_apispec.py b/base_rest/apispec/base_rest_service_apispec.py
index 94ebb2eb..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.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}},
- routing=routing,
+ **{ROUTING_DECORATOR_ATTR: routing},
)
def generate_paths(self):
for _name, method in inspect.getmembers(self._service, inspect.ismethod):
- routing = getattr(method, "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 64ad6503..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("routing")
+ routing = kwargs.get(ROUTING_DECORATOR_ATTR)
if not routing:
super(RestMethodParamPlugin, self).operation_helper(
path, operations, **kwargs
@@ -33,14 +34,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..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("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 3793b3c8..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, "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, "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/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..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 = "routing"
-
+from ..tools import ROUTING_DECORATOR_ATTR, _inspect_methods
_logger = logging.getLogger(__name__)
@@ -61,11 +57,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)
@@ -90,21 +86,20 @@ 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
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 +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, "routing"):
+ routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
+ if routing is None:
continue
- routing = method.routing
for routes, http_method in routing["routes"]:
method_name = "{}_{}".format(http_method.lower(), name)
default_route = routes[0]
@@ -424,6 +419,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/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
diff --git a/base_rest/restapi.py b/base_rest/restapi.py
index 799d4418..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.routing = routing
+ setattr(response_wrap, ROUTING_DECORATOR_ATTR, 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..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):
@@ -61,11 +61,8 @@ def _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__)]
@@ -144,7 +141,9 @@ def my_controller_route_without_auth_2(self):
@staticmethod
def _teardown_registry(class_or_instance):
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[
@@ -169,22 +168,26 @@ 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.controllers_per_module.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][1]
+ return controllers[-1]
@staticmethod
def _get_controller_route_methods(controller):
methods = {}
for name, method in _inspect_methods(controller):
- if hasattr(method, "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 babac520..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.routing,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
@@ -118,12 +119,13 @@ def _validator_my_instance_method(self):
"/test_controller/ping/",
],
"save_session": True,
+ "type": "restapi",
},
)
method = routes["get_search"]
self.assertDictEqual(
- method.routing,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
@@ -131,12 +133,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,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "public",
@@ -147,12 +150,13 @@ def _validator_my_instance_method(self):
"/test_controller/ping/",
],
"save_session": True,
+ "type": "restapi",
},
)
method = routes["put_update"]
self.assertDictEqual(
- method.routing,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["PUT"],
"auth": "public",
@@ -160,12 +164,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,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "public",
@@ -173,12 +178,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,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "public",
@@ -186,12 +192,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,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["DELETE"],
"auth": "public",
@@ -199,12 +206,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,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "public",
@@ -212,12 +220,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,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "public",
@@ -225,6 +234,7 @@ def _validator_my_instance_method(self):
"csrf": False,
"routes": ["/test_controller/ping//my_instance_method"],
"save_session": True,
+ "type": "restapi",
},
)
@@ -284,7 +294,7 @@ def _get_partner_schema(self):
method = routes["get_get"]
self.assertDictEqual(
- method.routing,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
@@ -295,12 +305,13 @@ def _get_partner_schema(self):
"/test_controller/partner/",
],
"save_session": True,
+ "type": "restapi",
},
)
method = routes["get_get_name"]
self.assertDictEqual(
- method.routing,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
@@ -308,12 +319,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,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "user",
@@ -321,6 +333,7 @@ def _get_partner_schema(self):
"csrf": False,
"routes": ["/test_controller/partner//change_name"],
"save_session": True,
+ "type": "restapi",
},
)
@@ -378,7 +391,7 @@ def update_name(self, _id, **params):
method = routes["get_get"]
self.assertDictEqual(
- method.routing,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
@@ -389,12 +402,13 @@ def update_name(self, _id, **params):
"/test_controller/partner/",
],
"save_session": True,
+ "type": "restapi",
},
)
method = routes["get_get_name"]
self.assertDictEqual(
- method.routing,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
@@ -402,12 +416,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,
+ getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "user",
@@ -415,6 +430,7 @@ def update_name(self, _id, **params):
"csrf": False,
"routes": ["/test_controller/partner//change_name"],
"save_session": True,
+ "type": "restapi",
},
)
@@ -493,24 +509,35 @@ def _validator_get(self):
("save_session", default_save_session),
]:
self.assertEqual(
- routes["get_new_api_method_without"].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"].routing["auth"], "public")
self.assertEqual(
- routes["get_new_api_method_with"].routing["cors"], "http://my_site"
+ getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)["auth"],
+ "public",
+ )
+ self.assertEqual(
+ getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)["cors"],
+ "http://my_site",
)
self.assertEqual(
- routes["get_new_api_method_with"].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"].routing["save_session"],
+ getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)[
+ "save_session"
+ ],
not default_save_session,
)
self.assertEqual(
- routes["get_get"].routing["auth"], default_auth, "wrong auth for get_get"
+ getattr(routes["get_get"], ROUTING_DECORATOR_ATTR)["auth"],
+ default_auth,
+ "wrong auth for get_get",
)
for attr, default in [
@@ -520,12 +547,14 @@ def _validator_get(self):
("save_session", default_save_session),
]:
self.assertEqual(
- routes["my_controller_route_without"].routing[attr],
+ getattr(routes["my_controller_route_without"], ROUTING_DECORATOR_ATTR)[
+ attr
+ ],
default,
"wrong %s" % attr,
)
- routing = routes["my_controller_route_with"].routing
+ routing = getattr(routes["my_controller_route_with"], ROUTING_DECORATOR_ATTR)
for attr, value in [
("auth", "public"),
("cors", "http://with_cors"),
@@ -539,7 +568,9 @@ def _validator_get(self):
"wrong %s" % attr,
)
self.assertEqual(
- routes["my_controller_route_without_auth_2"].routing["auth"],
+ getattr(
+ routes["my_controller_route_without_auth_2"], ROUTING_DECORATOR_ATTR
+ )["auth"],
None,
"wrong auth for my_controller_route_without_auth_2",
)
@@ -584,7 +615,9 @@ def _validator_get(self):
routes = self._get_controller_route_methods(controller)
self.assertEqual(
- routes["get_new_api_method_with_public_or"].routing["auth"],
+ getattr(
+ routes["get_new_api_method_with_public_or"], ROUTING_DECORATOR_ATTR
+ )["auth"],
"public_or_my_default_auth",
)
@@ -622,7 +655,9 @@ def _validator_get(self):
routes = self._get_controller_route_methods(controller)
self.assertEqual(
- routes["get_new_api_method_with_public_or"].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"]
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 b0f56cfc..6f5d4e30 100644
--- a/base_rest_auth_api_key/__manifest__.py
+++ b/base_rest_auth_api_key/__manifest__.py
@@ -6,17 +6,17 @@
"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",
"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..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": False,
+ "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 748da544..0baff6ef 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)",
@@ -17,11 +17,10 @@
"base_rest_pydantic",
"component",
"extendable",
+ "pydantic",
],
- "data": [],
- "demo": [],
"external_dependencies": {
- "python": ["jsondiff", "extendable-pydantic", "pydantic"]
+ "python": ["jsondiff", "extendable-pydantic", "marshmallow", "pydantic"]
},
- "installable": False,
+ "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..678e5261 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,21 @@ 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"]
- self.assertEqual(len(controllers), 4)
+ controllers = Controller.children_classes.get("base_rest_demo", [])
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 fad88ac6..ea83d15c 100644
--- a/base_rest_pydantic/__manifest__.py
+++ b/base_rest_pydantic/__manifest__.py
@@ -5,14 +5,12 @@
"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": False,
+ "installable": True,
"external_dependencies": {
"python": [
"pydantic",
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 45a117b4..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": False,
+ "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 abd9c3e9..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": False,
+ "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 4af58b5f..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"],
@@ -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,
+)