Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,47 @@
# Base Rest
Odoo addon: base_rest
## Installation
```bash
pip install odoo-bringout-oca-rest-framework-base_rest
```
## Dependencies
This addon depends on:
- component
- web
## Manifest Information
- **Name**: Base Rest
- **Version**: 16.0.1.0.4
- **Category**: N/A
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/rest-framework](https://github.com/OCA/rest-framework) branch 16.0, addon `base_rest`.
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,422 @@
=========
Base Rest
=========
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:517d5b1d74542047b404d2130e5d9239fe591f43b1a89ca02339766c8c8a6584
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github
:target: https://github.com/OCA/rest-framework/tree/16.0/base_rest
:alt: OCA/rest-framework
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-base_rest
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
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
becomes necessary to set up specialized service interfaces, so existing
systems can interact with Odoo.
While the XML-RPC interface of Odoo comes handy in such situations, it
requires a deep understanding of Odoos internal data model. When used
extensively, it creates a strong coupling between Odoo internals and client
systems, therefore increasing maintenance costs.
Once developed, an `OpenApi <https://spec.openapis.org/oas/v3.0.3>`_ documentation
is generated from the source code and available via a
`Swagger UI <https://swagger.io/tools/swagger-ui/>`_ served by your odoo server
at `https://my_odoo_server/api-docs`.
**Table of contents**
.. contents::
:local:
Configuration
=============
If an error occurs when calling a method of a service (ie missing parameter,
..) the system returns only a general description of the problem without
details. This is done on purpose to ensure maximum opacity on implementation
details and therefore lower security issue.
This restriction can be problematic when the services are accessed by an
external system in development. To know the details of an error it is indeed
necessary to have access to the log of the server. It is not always possible
to provide this kind of access. That's why you can configure the server to run
these services in development mode.
To run the REST API in development mode you must add a new section
'**[base_rest]**' with the option '**dev_mode=True**' in the server config
file.
.. code-block:: cfg
[base_rest]
dev_mode=True
When the REST API runs in development mode, the original description and a
stack trace is returned in case of error. **Be careful to not use this mode
in production**.
Usage
=====
To add your own REST service you must provides at least 2 classes.
* A Component providing the business logic of your service,
* A Controller to register your service.
The business logic of your service must be implemented into a component
(``odoo.addons.component.core.Component``) that inherit from
'base.rest.service'
Initially, base_rest expose by default all public methods defined in a service.
The conventions for accessing methods via HTTP were as follows:
* The method ``def get(self, _id)`` if defined, is accessible via HTTP GET routes ``<string:_service_name>/<int:_id>`` and ``<string:_service_name>/<int:_id>/get``.
* The method ``def search(self, **params)`` if defined, is accessible via the HTTP GET routes ``<string:_service_name>/`` and ``<string:_service_name>/search``.
* The method ``def delete(self, _id)`` if defined, is accessible via the HTTP DELETE route ``<string:_service_name>/<int:_id>``.
* The ``def update(self, _id, **params)`` method, if defined, is accessible via the HTTP PUT route ``<string:_service_name>/<int:_id>``.
* Other methods are only accessible via HTTP POST routes ``<string:_service_name>`` or ``<string:_service_name>/<string:method_name>`` or ``<string:_service_name>/<int:_id>`` or ``<string:_service_name>/<int:_id>/<string:method_name>``
.. code-block:: python
from odoo.addons.component.core import Component
class PingService(Component):
_inherit = 'base.rest.service'
_name = 'ping.service'
_usage = 'ping'
_collection = 'my_module.services'
# The following method are 'public' and can be called from the controller.
def get(self, _id, message):
return {
'response': 'Get called with message ' + message}
def search(self, message):
return {
'response': 'Search called search with message ' + message}
def update(self, _id, message):
return {'response': 'PUT called with message ' + message}
# pylint:disable=method-required-super
def create(self, **params):
return {'response': 'POST called with message ' + params['message']}
def delete(self, _id):
return {'response': 'DELETE called with id %s ' % _id}
# Validator
def _validator_search(self):
return {'message': {'type': 'string'}}
# Validator
def _validator_get(self):
# no parameters by default
return {}
def _validator_update(self):
return {'message': {'type': 'string'}}
def _validator_create(self):
return {'message': {'type': 'string'}}
Once you have implemented your services (ping, ...), you must tell to Odoo
how to access to these services. This process is done by implementing a
controller that inherits from ``odoo.addons.base_rest.controllers.main.RestController``
.. code-block:: python
from odoo.addons.base_rest.controllers import main
class MyRestController(main.RestController):
_root_path = '/my_services_api/'
_collection_name = my_module.services
In your controller, _'root_path' is used to specify the root of the path to
access to your services and '_collection_name' is the name of the collection
providing the business logic for the requested service/
By inheriting from ``RestController`` the following routes will be registered
to access to your services
.. code-block:: python
@route([
ROOT_PATH + '<string:_service_name>',
ROOT_PATH + '<string:_service_name>/search',
ROOT_PATH + '<string:_service_name>/<int:_id>',
ROOT_PATH + '<string:_service_name>/<int:_id>/get'
], methods=['GET'], auth="user", csrf=False)
def get(self, _service_name, _id=None, **params):
method_name = 'get' if _id else 'search'
return self._process_method(_service_name, method_name, _id, params)
@route([
ROOT_PATH + '<string:_service_name>',
ROOT_PATH + '<string:_service_name>/<string:method_name>',
ROOT_PATH + '<string:_service_name>/<int:_id>',
ROOT_PATH + '<string:_service_name>/<int:_id>/<string:method_name>'
], methods=['POST'], auth="user", csrf=False)
def modify(self, _service_name, _id=None, method_name=None, **params):
if not method_name:
method_name = 'update' if _id else 'create'
if method_name == 'get':
_logger.error("HTTP POST with method name 'get' is not allowed. "
"(service name: %s)", _service_name)
raise BadRequest()
return self._process_method(_service_name, method_name, _id, params)
@route([
ROOT_PATH + '<string:_service_name>/<int:_id>',
], methods=['PUT'], auth="user", csrf=False)
def update(self, _service_name, _id, **params):
return self._process_method(_service_name, 'update', _id, params)
@route([
ROOT_PATH + '<string:_service_name>/<int:_id>',
], methods=['DELETE'], auth="user", csrf=False)
def delete(self, _service_name, _id):
return self._process_method(_service_name, 'delete', _id)
As result an HTTP GET call to 'http://my_odoo/my_services_api/ping' will be
dispatched to the method ``PingService.search``
In addition to easily exposing your methods, the module allows you to define
data schemas to which the exchanged data must conform. These schemas are defined
on the basis of `Cerberus schemas <https://docs.python-cerberus.org/en/stable/>`_
and associated to the methods using the
following naming convention. For a method `my_method`:
* ``def _validator_my_method(self):`` will be called to get the schema required to
validate the input parameters.
* ``def _validator_return_my_method(self):`` if defined, will be called to get
the schema used to validate the response.
In order to offer even more flexibility, a new API has been developed.
This new API replaces the implicit approach used to expose a service by the use
of a python decorator to explicitly mark a method as being available via the
REST API: ``odoo.addons.base_rest.restapi.method``.
.. code-block:: python
class PartnerNewApiService(Component):
_inherit = "base.rest.service"
_name = "partner.new_api.service"
_usage = "partner"
_collection = "base.rest.demo.new_api.services"
_description = """
Partner New API Services
Services developed with the new api provided by base_rest
"""
@restapi.method(
[(["/<int:id>/get", "/<int:id>"], "GET")],
output_param=restapi.CerberusValidator("_get_partner_schema"),
auth="public",
)
def get(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
def _get_partner_schema(self):
return {
"name": {"type": "string", "required": True}
}
@restapi.method(
[(["/list", "/"], "GET")],
output_param=restapi.CerberusListValidator("_get_partner_schema"),
auth="public",
)
def list(self):
partners = self.env["res.partner"].search([])
return [{"name": p.name} for p in partners]
Thanks to this new api, you are now free to specify your own routes but also
to use other object types as parameter or response to your methods.
For example, `base_rest_datamodel` allows you to use Datamodel object instance
into your services.
.. code-block:: python
from marshmallow import fields
from odoo.addons.base_rest import restapi
from odoo.addons.component.core import Component
from odoo.addons.datamodel.core import Datamodel
class PartnerSearchParam(Datamodel):
_name = "partner.search.param"
id = fields.Integer(required=False, allow_none=False)
name = fields.String(required=False, allow_none=False)
class PartnerShortInfo(Datamodel):
_name = "partner.short.info"
id = fields.Integer(required=True, allow_none=False)
name = fields.String(required=True, allow_none=False)
class PartnerNewApiService(Component):
_inherit = "base.rest.service"
_name = "partner.new_api.service"
_usage = "partner"
_collection = "base.rest.demo.new_api.services"
_description = """
Partner New API Services
Services developed with the new api provided by base_rest
"""
@restapi.method(
[(["/", "/search"], "GET")],
input_param=restapi.Datamodel("partner.search.param"),
output_param=restapi.Datamodel("partner.short.info", is_list=True),
auth="public",
)
def search(self, partner_search_param):
"""
Search for partners
:param partner_search_param: An instance of partner.search.param
:return: List of partner.short.info
"""
domain = []
if partner_search_param.name:
domain.append(("name", "like", partner_search_param.name))
if partner_search_param.id:
domain.append(("id", "=", partner_search_param.id))
res = []
PartnerShortInfo = self.env.datamodels["partner.short.info"]
for p in self.env["res.partner"].search(domain):
res.append(PartnerShortInfo(id=p.id, name=p.name))
return res
The BaseRestServiceContextProvider provides context for your services,
including authenticated_partner_id.
You are free to redefine the method _get_authenticated_partner_id() to pass the
authenticated_partner_id based on the authentication mechanism of your choice.
See base_rest_auth_jwt for an example.
In addition, authenticated_partner_id is available in record rule evaluation context.
Known issues / Roadmap
======================
The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Abase_rest>`_
and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Abase_rest>`_ can
be found on GitHub.
Changelog
=========
16.0.1.0.2 (2023-10-07)
~~~~~~~~~~~~~~~~~~~~~~~
**Features**
- Add support for oauth2 security scheme in the Swagger UI. If your openapi
specification contains a security scheme of type oauth2, the Swagger UI will
display a login button in the top right corner. In order to finalize the
login process, a redirect URL must be provided when initializing the Swagger
UI. The Swagger UI is now initialized with a `oauth2RedirectUrl` option that
references a oauth2-redirect.html file provided by the swagger-ui lib and served
by the current addon. (`#379 <https://github.com/OCA/rest-framework/issues/379>`_)
12.0.2.0.1
~~~~~~~~~~
* _validator_...() methods can now return a cerberus ``Validator`` object
instead of a schema dictionnary, for additional flexibility (e.g. allowing
validator options such as ``allow_unknown``).
12.0.2.0.0
~~~~~~~~~~
* Licence changed from AGPL-3 to LGPL-3
12.0.1.0.1
~~~~~~~~~~
* Fix issue when rendering the jsonapi documentation if no documentation is
provided on a method part of the REST api.
12.0.1.0.0
~~~~~~~~~~
First official version. The addon has been incubated into the
`Shopinvader repository <https://github.com/akretion/odoo-shopinvader>`_ from
Akretion. For more information you need to look at the git log.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/rest-framework/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/rest-framework/issues/new?body=module:%20base_rest%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* ACSONE SA/NV
Contributors
~~~~~~~~~~~~
* Laurent Mignon <laurent.mignon@acsone.eu>
* Sébastien Beau <sebastien.beau@akretion.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/16.0/base_rest>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +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.",
)

View file

@ -0,0 +1,37 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
{
"name": "Base Rest",
"summary": """
Develop your own high level REST APIs for Odoo thanks to this addon.
""",
"version": "16.0.1.0.4",
"development_status": "Beta",
"license": "LGPL-3",
"author": "ACSONE SA/NV, " "Odoo Community Association (OCA)",
"maintainers": [],
"website": "https://github.com/OCA/rest-framework",
"depends": ["component", "web"],
"data": [
"views/openapi_template.xml",
"views/base_rest_view.xml",
],
"assets": {
"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",
],
},
"external_dependencies": {
"python": [
"cerberus",
"pyquerystring",
"parse-accept-language",
# adding version causes missing-manifest-dependency false positives
"apispec",
]
},
"installable": True,
}

View file

@ -0,0 +1,80 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import inspect
import textwrap
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
class BaseRestServiceAPISpec(APISpec):
"""
APISpec object from base.rest.service component
"""
def __init__(self, service_component, **params):
self._service = service_component
super(BaseRestServiceAPISpec, self).__init__(
title="%s REST services" % self._service._usage,
version="",
openapi_version="3.0.0",
info={
"description": textwrap.dedent(
getattr(self._service, "_description", "") or ""
)
},
servers=self._get_servers(),
plugins=self._get_plugins(),
)
self._params = params
def _get_servers(self):
env = self._service.env
services_registry = _rest_services_databases.get(env.cr.dbname, {})
collection_path = ""
for path, spec in list(services_registry.items()):
if spec["collection_name"] == self._service._collection:
collection_path = path
break
base_url = env["ir.config_parameter"].sudo().get_param("web.base.url")
return [
{
"url": "%s/%s/%s"
% (
base_url.strip("/"),
collection_path.strip("/"),
self._service._usage,
)
}
]
def _get_plugins(self):
return [
RestApiMethodRoutePlugin(self._service),
RestMethodParamPlugin(self._service),
RestMethodSecurityPlugin(self._service),
]
def _add_method_path(self, method):
description = textwrap.dedent(method.__doc__ or "")
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_DECORATOR_ATTR: routing},
)
def generate_paths(self):
for _name, method in inspect.getmembers(self._service, inspect.ismethod):
routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
if not routing:
continue
self._add_method_path(method)

View file

@ -0,0 +1,78 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from apispec import BasePlugin
from ..restapi import RestMethodParam
from ..tools import ROUTING_DECORATOR_ATTR
class RestMethodParamPlugin(BasePlugin):
"""
APISpec plugin to generate path parameters and responses from a services
method
"""
def __init__(self, service):
super(RestMethodParamPlugin, self).__init__()
self._service = service
self._default_parameters = service._get_openapi_default_parameters()
self._default_responses = service._get_openapi_default_responses()
# pylint: disable=W8110
def init_spec(self, spec):
super(RestMethodParamPlugin, self).init_spec(spec)
self.spec = spec
self.openapi_version = spec.openapi_version
def operation_helper(self, path=None, operations=None, **kwargs):
routing = kwargs.get(ROUTING_DECORATOR_ATTR)
if not routing:
super(RestMethodParamPlugin, self).operation_helper(
path, operations, **kwargs
)
if not operations:
return
for method, params in operations.items():
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_parameters(self, routing, method, params):
parameters = params.get("parameters", [])
# add default paramters provided by the sevice
parameters.extend(self._default_parameters)
input_param = routing.get("input_param")
if input_param and isinstance(input_param, RestMethodParam):
if method == "get":
# get quey params from RequestMethodParam object
parameters.extend(
input_param.to_openapi_query_parameters(self._service, self.spec)
)
else:
# get requestBody from RequestMethodParam object
request_body = params.get("requestBody", {})
request_body.update(
input_param.to_openapi_requestbody(self._service, self.spec)
)
params["requestBody"] = request_body
# sort paramters to ease comparison into unittests
parameters.sort(key=lambda a: a["name"])
return parameters
def _generate_responses(self, routing, method, params):
responses = params.get("responses", {})
# add default responses provided by the service
responses.update(self._default_responses.copy())
output_param = routing.get("output_param")
if output_param and isinstance(output_param, RestMethodParam):
responses = params.get("responses", {})
# get response from RequestMethodParam object
responses.update(self._default_responses.copy())
responses.update(
output_param.to_openapi_responses(self._service, self.spec)
)
return responses

View file

@ -0,0 +1,39 @@
# Copyright 2021 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from apispec import BasePlugin
from ..tools import ROUTING_DECORATOR_ATTR
class RestMethodSecurityPlugin(BasePlugin):
"""
APISpec plugin to generate path security from a services method
"""
def __init__(self, service, user_auths=("user",)):
super(RestMethodSecurityPlugin, self).__init__()
self._service = service
self._supported_user_auths = user_auths
# pylint: disable=W8110
def init_spec(self, spec):
super(RestMethodSecurityPlugin, self).init_spec(spec)
self.spec = spec
self.openapi_version = spec.openapi_version
user_scheme = {"type": "apiKey", "in": "cookie", "name": "session_id"}
spec.components.security_scheme("user", user_scheme)
def operation_helper(self, path=None, operations=None, **kwargs):
routing = kwargs.get(ROUTING_DECORATOR_ATTR)
if not routing:
super(RestMethodSecurityPlugin, self).operation_helper(
path, operations, **kwargs
)
if not operations:
return
auth = routing.get("auth", self.spec._params.get("default_auth"))
if auth in self._supported_user_auths:
for _method, params in operations.items():
security = params.setdefault("security", [])
security.append({"user": []})

View file

@ -0,0 +1,62 @@
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import re
import werkzeug.routing
from apispec import BasePlugin
from werkzeug.routing import Map, Rule
# from flask-restplus
RE_URL = re.compile(r"<(?:[^:<>]+:)?([^<>]+)>")
DEFAULT_CONVERTER_MAPPING = {
werkzeug.routing.UnicodeConverter: ("string", None),
werkzeug.routing.IntegerConverter: ("integer", "int32"),
werkzeug.routing.FloatConverter: ("number", "float"),
werkzeug.routing.UUIDConverter: ("string", "uuid"),
}
DEFAULT_TYPE = ("string", None)
class RestApiMethodRoutePlugin(BasePlugin):
"""
APISpec plugin to generate path from a restapi.method route
"""
def __init__(self, service):
super(RestApiMethodRoutePlugin, self).__init__()
self.converter_mapping = dict(DEFAULT_CONVERTER_MAPPING)
self._service = service
@staticmethod
def route2openapi(path):
"""Convert an odoo route to an OpenAPI-compliant path.
:param str path: Odoo route path template.
"""
return RE_URL.sub(r"{\1}", path)
# Greatly inspired by flask-apispec
def route_to_params(self, route):
"""Get parameters from Odoo route"""
# odoo route are Werkzeug Rule
rule = Rule(route)
Map(rules=[rule])
params = []
for argument in rule.arguments:
param = {"in": "path", "name": argument, "required": True}
type_, format_ = self.converter_mapping.get(
type(rule._converters[argument]), DEFAULT_TYPE
)
schema = {"type": type_}
if format_ is not None:
schema["format"] = format_
param["schema"] = schema
params.append(param)
return params
def path_helper(self, path, operations, parameters, **kwargs):
for param in self.route_to_params(path):
parameters.append(param)
return self.route2openapi(path)

View file

@ -0,0 +1,4 @@
from . import service
from . import service_context_provider
from . import cerberus_validator
from . import user_component_context_provider

View file

@ -0,0 +1,53 @@
# Copyright 2021 Camptocamp
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.component.core import Component
class BaseRestCerberusValidator(Component):
"""Component used to lookup the input/output validator methods
To modify in your services collection::
class MyCollectionRestCerberusValidator(Component):
_name = "mycollection.rest.cerberus.validator"
_inherit = "base.rest.cerberus.validator"
_usage = "cerberus.validator"
_collection = "mycollection"
def get_validator_handler(self, service, method_name, direction):
# customize
def has_validator_handler(self, service, method_name, direction):
# customize
"""
_name = "base.rest.cerberus.validator"
_usage = "cerberus.validator"
_is_rest_service_component = False # marker to retrieve REST components
def get_validator_handler(self, service, method_name, direction):
"""Get the validator handler for a method
By default, it returns the method on the current service instance. It
can be customized to delegate the validators to another component.
The returned method will be called without arguments, and is expected
to return the schema.
direction is either "input" for request schema or "output" for responses.
"""
return getattr(service, method_name)
def has_validator_handler(self, service, method_name, direction):
"""Return if the service has a validator handler for a method
By default, it returns True if the the method exists on the service. It
can be customized to delegate the validators to another component.
The returned method will be called without arguments, and is expected
to return the schema.
direction is either "input" for request schema or "output" for responses.
"""
return hasattr(service, method_name)

View file

@ -0,0 +1,217 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import logging
from werkzeug import Response
from werkzeug.exceptions import NotFound
from odoo.http import request
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__)
def to_int(val):
# The javascript VM ducktape only use float and so pass float
# to the api, the werkzeug request interpret params as unicode
# so we have to convert params from string to float to int
if isinstance(val, int):
return val
if val:
return int(float(val))
return None
def to_bool(val):
return val in ("true", "True", "1", True)
def skip_secure_params(func):
"""
Used to decorate methods
:param func:
:return:
"""
func.skip_secure_params = True
return func
def skip_secure_response(func):
"""
Used to decorate methods
:param func:
:return:
"""
func.skip_secure_response = True
return func
class BaseRestService(AbstractComponent):
_name = "base.rest.service"
_description = None # description included into the openapi doc
_is_rest_service_component = True # marker to retrieve REST components
def _prepare_extra_log(self, func, params, secure_params, res):
httprequest = request.httprequest
headers = dict(httprequest.headers)
return {
"application": "Rest Service",
"request_url": httprequest.url,
"request_method": httprequest.method,
"params": params,
"headers": headers,
"secure_params": secure_params,
"res": res,
"status": 200,
}
def _log_call(self, func, params, secure_params, res):
"""If you want to enjoy the advanced log install the module
logging_json"""
if request:
httprequest = request.httprequest
extra = self._prepare_extra_log(func, params, secure_params, res)
args = [httprequest.url, httprequest.method]
message = "REST call url %s method %s"
_logger.debug(message, *args, extra=extra)
def _prepare_input_params(self, method, params):
"""
Internal method used to process the input_param parameter. The
result will be used to call the final method. The processing is
delegated to the `resapi.RestMethodParam` instance specified by the
restapi.method` decorator on the method.
:param method:
:param params:
:return:
"""
method_name = method.__name__
if hasattr(method, "skip_secure_params"):
return params
routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
if not routing:
_logger.warning(
"Method %s is not a public method of service %s",
method_name,
self._name,
)
raise NotFound()
input_param = routing["input_param"]
if input_param:
return input_param.from_params(self, params)
return {}
def _prepare_response(self, method, result):
"""
Internal method used to process the result of the method called by the
controller. The result of this process is returned to the controller
The processing is delegated to the `resapi.RestMethodParam` instance
specified by the `restapi.method` decorator on the method.
:param method: method
:param response:
:return: dict/json or `http.Response`
"""
method_name = method
if callable(method):
method_name = method.__name__
if hasattr(method, "skip_secure_response"):
return result
routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
output_param = routing["output_param"]
if not output_param:
_logger.warning(
"DEPRECATED: You must define an output schema for method %s "
"in service %s",
method_name,
self._name,
)
return result
return output_param.to_response(self, result)
def dispatch(self, method_name, *args, params=None):
"""
This method dispatch the call to the final method.
Before the call parameters are processed by the
`restapi.RestMethodParam` object specified as input_param object.
The result of the method is therefore given to the
`restapi.RestMethodParam` object specified as output_param to build
the final response returned by the service
:param method_name:
:param *args: query path paramters args
:param params: A dictionary with the parameters of the method. Once
secured and sanitized, these parameters will be passed
to the method as keyword args.
:return:
"""
method = getattr(self, method_name, object())
params = params or {}
secure_params = self._prepare_input_params(method, params)
if isinstance(secure_params, dict):
# for backward compatibility methods expecting json params
# are declared as m(self, p1=None, p2=None) or m(self, **params)
res = method(*args, **secure_params)
else:
res = method(*args, secure_params)
self._log_call(method, params, secure_params, res)
if isinstance(res, Response):
return res
return self._prepare_response(method, res)
def _validator_delete(self):
"""
Default validator for delete method.
By default delete should never be called with parameters.
"""
return {}
def _validator_get(self):
"""
Default validator for get method.
By default get should not be called with parameters.
"""
return {}
def _get_api_spec(self, **params):
return BaseRestServiceAPISpec(self, **params)
def to_openapi(self, **params):
"""
Return the description of this REST service as an OpenAPI json document
:return: json document
"""
api_spec = self._get_api_spec(**params)
api_spec.generate_paths()
return api_spec.to_dict()
def _get_openapi_default_parameters(self):
return []
def _get_openapi_default_responses(self):
return {
"400": {"description": "One of the given parameter is not valid"},
"401": {
"description": "The user is not authorized. Authentication "
"is required"
},
"404": {"description": "Requested resource not found"},
"403": {
"description": "You don't have the permission to access the "
"requested resource."
},
}
@property
def request(self):
return self.work.request
@property
def controller(self):
return self.work.controller

View file

@ -0,0 +1,26 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.component.core import Component
class BaseRestServiceContextProvider(Component):
_name = "base.rest.service.context.provider"
_usage = "component_context_provider"
def __init__(self, work_context):
super().__init__(work_context)
self.request = work_context.request
# pylint: disable=assignment-from-none
self.authenticated_partner_id = self._get_authenticated_partner_id()
def _get_authenticated_partner_id(self):
return None
def _get_component_context(self):
return {
"request": self.request,
"authenticated_partner_id": self.authenticated_partner_id,
"collection": self.collection,
}

View file

@ -0,0 +1,12 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.component.core import AbstractComponent
class AbstractUserAuthenticatedPartnerProvider(AbstractComponent):
_name = "abstract.user.authenticated.partner.provider"
def _get_authenticated_partner_id(self):
return self.env.user.partner_id.id

View file

@ -0,0 +1,6 @@
# Copyright 2016 Akretion (http://www.akretion.com)
# Sébastien BEAU <sebastien.beau@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import main
from . import api_docs

View file

@ -0,0 +1,113 @@
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import json
from contextlib import contextmanager
from odoo.http import Controller, request, route
from odoo.addons.component.core import WorkContext
from ..core import _rest_services_databases
from .main import _PseudoCollection
class ApiDocsController(Controller):
def make_json_response(self, data, headers=None, cookies=None):
data = json.dumps(data)
if headers is None:
headers = {}
headers["Content-Type"] = "application/json"
return request.make_response(data, headers=headers, cookies=cookies)
@route(
["/api-docs", "/api-docs/index.html"],
methods=["GET"],
type="http",
auth="public",
)
def index(self, **params):
self._get_api_urls()
primary_name = params.get("urls.primaryName")
swagger_settings = {
"urls": self._get_api_urls(),
"urls.primaryName": primary_name,
}
values = {"swagger_settings": swagger_settings}
return request.render("base_rest.openapi", values)
@route("/api-docs/<path:collection>/<string:service_name>.json", auth="public")
def api(self, collection, service_name):
with self.service_and_controller_class(collection, service_name) as (
service,
controller_class,
):
openapi_doc = service.to_openapi(
default_auth=controller_class._default_auth
)
return self.make_json_response(openapi_doc)
def _get_api_urls(self):
"""
This method lookup into the dictionary of registered REST service
for the current database to built the list of available REST API
:return:
"""
services_registry = _rest_services_databases.get(request.env.cr.dbname, {})
api_urls = []
for rest_root_path, spec in list(services_registry.items()):
collection_path = rest_root_path[1:-1] # remove '/'
collection_name = spec["collection_name"]
for service in self._get_service_in_collection(collection_name):
api_urls.append(
{
"name": "{}: {}".format(collection_path, service._usage),
"url": "/api-docs/%s/%s.json"
% (collection_path, service._usage),
}
)
api_urls = sorted(api_urls, key=lambda k: k["name"])
return api_urls
def _filter_service_components(self, components):
reg_model = request.env["rest.service.registration"]
return [c for c in components if reg_model._filter_service_component(c)]
def _get_service_in_collection(self, collection_name):
with self.work_on_component(collection_name) as work:
components = work.components_registry.lookup(collection_name)
services = self._filter_service_components(components)
services = [work.component(usage=s._usage) for s in services]
return services
@contextmanager
def service_and_controller_class(self, collection_path, service_name):
"""
Return the component that implements the methods of the requested
service.
:param collection_path:
:param service_name:
:return: an instance of invader.service component,
the base controller class serving the service
"""
services_spec = self._get_services_specs(collection_path)
collection_name = services_spec["collection_name"]
controller_class = services_spec["controller_class"]
with self.work_on_component(collection_name) as work:
service = work.component(usage=service_name)
yield service, controller_class
@contextmanager
def work_on_component(self, collection_name):
"""
Return the all the components implementing REST services
:param collection_name:
:return: a WorkContext instance
"""
collection = _PseudoCollection(collection_name, request.env)
yield WorkContext(model_name="rest.service.registration", collection=collection)
def _get_services_specs(self, path):
services_registry = _rest_services_databases.get(request.env.cr.dbname, {})
return services_registry["/" + path + "/"]

View file

@ -0,0 +1,201 @@
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
from contextlib import contextmanager
from werkzeug.exceptions import BadRequest
from odoo import models
from odoo.http import Controller, Response, request
from odoo.addons.component.core import WorkContext, _get_addon_name
from ..core import _rest_controllers_per_module
_logger = logging.getLogger(__name__)
class _PseudoCollection(object):
__slots__ = "_name", "env", "id"
def __init__(self, name, env):
self._name = name
self.env = env
self.id = None
class RestController(Controller):
"""Generic REST Controller
This controller is the base controller used by as base controller for all the REST
controller generated from the service components.
You must inherit of this controller into your code to register the root path
used to serve all the services defined for the given collection name.
This registration requires 2 parameters:
_root_path:
_collection_name:
Only one controller by _collection_name, _root_path should exists into an
odoo database. If more than one controller exists, a warning is issued into
the log at startup and the concrete controller used as base class
for the services registered into the collection name and served at the
root path is not predictable.
Module A:
class ControllerA(RestController):
_root_path='/my_path/'
_collection_name='my_services_collection'
Module B depends A: A
class ControllerB(ControllerA): / \
pass B C
/
Module C depends A: D
class ControllerC(ControllerA):
pass
Module D depends B:
class ControllerB(ControllerB):
pass
In the preceding illustration, services in module C will never be served
by controller D or B. Therefore if the generic dispatch method is overridden
in B or D, this override wil never apply to services in C since in Odoo
controllers are not designed to be inherited. That's why it's an error
to have more than one controller registered for the same root path and
collection name.
The following properties can be specified to define common properties to
apply to generated REST routes.
_default_auth: The default authentication to apply to all pre defined routes.
default: 'user'
_default_cors: The default Access-Control-Allow-Origin cors directive value.
default: None
_default_csrf: Whether CSRF protection should be enabled for the route.
default: False
_default_save_session: Whether session should be saved into the session store
default: True
"""
_root_path = None
_collection_name = None
# The default authentication to apply to all pre defined routes.
_default_auth = "user"
# The default Access-Control-Allow-Origin cors directive value.
_default_cors = None
# Whether CSRF protection should be enabled for the route.
_default_csrf = False
# Whether session should be saved into the session store
_default_save_session = True
_component_context_provider = "component_context_provider"
@classmethod
def __init_subclass__(cls):
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
context
:return: dict of key value.
"""
work = WorkContext(
model_name="rest.service.registration",
collection=collection or self.default_collection,
request=request,
controller=self,
)
provider = work.component(usage=self._component_context_provider)
return provider._get_component_context()
def make_response(self, data):
if isinstance(data, Response):
# The response has been build by the called method...
return data
# By default return result as json
return request.make_json_response(data)
@property
def collection_name(self):
return self._collection_name
@property
def default_collection(self):
return _PseudoCollection(self.collection_name, request.env)
@contextmanager
def work_on_component(self, collection=None):
"""
Return the component that implements the methods of the requested
service.
:param service_name:
:return: an instance of base.rest.service component
"""
collection = collection or self.default_collection
component_ctx = self._get_component_context(collection=collection)
env = collection.env
collection.env = env(
context=dict(
env.context,
authenticated_partner_id=component_ctx.get("authenticated_partner_id"),
)
)
yield WorkContext(model_name="rest.service.registration", **component_ctx)
@contextmanager
def service_component(self, service_name, collection=None):
"""
Return the component that implements the methods of the requested
service.
:param service_name:
:return: an instance of base.rest.service component
"""
with self.work_on_component(collection=collection) as work:
service = work.component(usage=service_name)
yield service
def _validate_method_name(self, method_name):
if method_name.startswith("_"):
_logger.error(
"REST API called with an unallowed method "
"name: %s.\n Method can't start with '_'",
method_name,
)
raise BadRequest()
return True
def _process_method(
self, service_name, method_name, *args, collection=None, params=None
):
self._validate_method_name(method_name)
if isinstance(collection, models.Model) and not collection:
raise request.not_found()
with self.service_component(service_name, collection=collection) as service:
result = service.dispatch(method_name, *args, params=params)
return self.make_response(result)

View file

@ -0,0 +1,22 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import collections
class RestServicesDatabases(dict):
"""Holds a registry of REST services for each database"""
_rest_services_databases = RestServicesDatabases()
_rest_services_routes = collections.defaultdict(set)
_rest_controllers_per_module = collections.defaultdict(list)
class RestServicesRegistry(dict):
"""Holds a registry of REST services where key is the root of the path on
which the methods of your ` RestController`` are registred and value is the
name of the collection on which your ``RestServiceComponent`` implementing
the business logic of your service is registered."""

View file

@ -0,0 +1,252 @@
# Copyright 2018 ACSONE SA/NV
# Copyright 2017 Akretion (http://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import datetime
import decimal
import json
import logging
import sys
import traceback
from collections import defaultdict
from markupsafe import escape
from werkzeug.exceptions import (
BadRequest,
Forbidden,
HTTPException,
InternalServerError,
NotFound,
Unauthorized,
)
from odoo.exceptions import (
AccessDenied,
AccessError,
MissingError,
UserError,
ValidationError,
)
from odoo.http import (
CSRF_FREE_METHODS,
MISSING_CSRF_WARNING,
Dispatcher,
SessionExpiredException,
request,
)
from odoo.tools import ustr
from odoo.tools.config import config
_logger = logging.getLogger(__name__)
try:
import pyquerystring
from accept_language import parse_accept_language
except (ImportError, IOError) as err:
_logger.debug(err)
class JSONEncoder(json.JSONEncoder):
def default(self, obj): # pylint: disable=E0202,arguments-differ
if isinstance(obj, datetime.datetime):
return obj.isoformat()
elif isinstance(obj, datetime.date):
return obj.isoformat()
elif isinstance(obj, decimal.Decimal):
return float(obj)
return super(JSONEncoder, self).default(obj)
BLACKLISTED_LOG_PARAMS = ("password",)
def wrapJsonException(exception, include_description=False, extra_info=None):
"""Wrap exceptions to be rendered as JSON.
:param exception: an instance of an exception
:param include_description: include full description in payload
:param extra_info: dict to provide extra keys to include in payload
"""
get_original_headers = exception.get_headers
exception.traceback = "".join(traceback.format_exception(*sys.exc_info()))
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"):
# return exception info only if base_rest is in dev_mode
res.update({"traceback": exception.traceback, "description": description})
elif include_description:
res["description"] = description
res.update(extra_info or {})
return JSONEncoder().encode(res)
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):
if key != "Content-Type":
_headers.append(key, value)
return _headers
exception.get_body = get_body
exception.get_headers = get_headers
if request:
httprequest = request.httprequest
headers = dict(httprequest.headers)
headers.pop("Api-Key", None)
message = (
"RESTFULL call to url %s with method %s and params %s "
"raise the following error %s"
)
params = (
request.params.copy()
if hasattr(request, "params")
else request.get_http_params().copy()
)
for k in params.keys():
if k in BLACKLISTED_LOG_PARAMS:
params[k] = "<redacted>"
args = (httprequest.url, httprequest.method, params, exception)
extra = {
"application": "REST Services",
"url": httprequest.url,
"method": httprequest.method,
"params": params,
"headers": headers,
"status": exception.code,
"exception_body": exception.get_body(),
}
_logger.exception(message, *args, extra=extra)
return exception
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.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):
"""
In this function, we parse the preferred languages specified into the
'Accept-language' http header. The lang into the context is initialized
according to the priority of languages into the headers and those
available into Odoo.
"""
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.request.env["res.lang"].get_installed():
installed_locale_langs.add(lang_code)
installed_locale_by_lang[lang_code.split("_")[0]].append(lang_code)
# parsed_acccepted_langs is sorted by priority (higher first)
for lang in parsed_accepted_langs:
# we first check if a locale (en_GB) is available into the list of
# available locales into Odoo
locale = None
if lang.locale in installed_locale_langs:
locale = lang.locale
# if no locale language is installed, we look for an available
# locale for the given language (en). We return the first one
# found for this language.
else:
locales = installed_locale_by_lang.get(lang.language)
if locales:
locale = locales[0]
if locale:
# reset the context to put our new lang.
self.request.update_context(lang=locale)
break
@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."""
if isinstance(exception, SessionExpiredException):
# 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)))
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(exception.args[0]),
include_description=True,
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)
if headers is None:
headers = {}
headers["Content-Type"] = "application/json"
return self.make_response(data, headers=headers, cookies=cookies)

View file

@ -0,0 +1,91 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_rest
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "%(key)'s JSON content is malformed: %(error)s"
msgstr ""
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest %s"
msgstr ""
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest item %(idx)s :%(errors)s"
msgstr ""
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest: Not enough items in the list (%(current)s < %(expected)s)"
msgstr ""
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest: Too many items in the list (%(current)s > %(expected)s)"
msgstr ""
#. module: base_rest
#: model:ir.ui.menu,name:base_rest.menu_rest_api_docs
msgid "Docs"
msgstr ""
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "Invalid Response %s"
msgstr ""
#. module: base_rest
#: model:ir.actions.act_url,name:base_rest.action_rest_api_docs
#: model:ir.ui.menu,name:base_rest.menu_rest_api_root
msgid "REST API"
msgstr ""
#. module: base_rest
#: model:ir.model,name:base_rest.model_rest_service_registration
msgid "REST Services Registration Model"
msgstr ""
#. module: base_rest
#: model:ir.model,name:base_rest.model_ir_rule
msgid "Record Rule"
msgstr ""
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "Unable to get cerberus schema from %s"
msgstr ""
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "You must provide a dict of RestMethodParam"
msgstr ""

View file

@ -0,0 +1,91 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_rest
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "%(key)'s JSON content is malformed: %(error)s"
msgstr "%(key)s JSON sadržaj je neispravno formatiran: %(error)s"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest %s"
msgstr "Neispravni zahtjev %s"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest item %(idx)s :%(errors)s"
msgstr "Neispravni zahtjev stavka %(idx)s :%(errors)s"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest: Not enough items in the list (%(current)s < %(expected)s)"
msgstr "Neispravni zahtjev: Nedovoljno stavki u listi (%(current)s < %(expected)s)"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest: Too many items in the list (%(current)s > %(expected)s)"
msgstr "Neispravni zahtjev: Previše stavki u listi (%(current)s > %(expected)s)"
#. module: base_rest
#: model:ir.ui.menu,name:base_rest.menu_rest_api_docs
msgid "Docs"
msgstr "Dokumentacija"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "Invalid Response %s"
msgstr "Neispravni odgovor %s"
#. module: base_rest
#: model:ir.actions.act_url,name:base_rest.action_rest_api_docs
#: model:ir.ui.menu,name:base_rest.menu_rest_api_root
msgid "REST API"
msgstr "REST API"
#. module: base_rest
#: model:ir.model,name:base_rest.model_rest_service_registration
msgid "REST Services Registration Model"
msgstr "Model registracije REST usluga"
#. module: base_rest
#: model:ir.model,name:base_rest.model_ir_rule
msgid "Record Rule"
msgstr "Pravilo zapisa"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "Unable to get cerberus schema from %s"
msgstr "Ne mogu dobiti cerberus šemu iz %s"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "You must provide a dict of RestMethodParam"
msgstr "Morate pružiti dict od RestMethodParam"

View file

@ -0,0 +1,98 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_rest
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-01-15 17:34+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "%(key)'s JSON content is malformed: %(error)s"
msgstr "Il contenuto JSON di %(key) è formattato male: %(error)s"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest %s"
msgstr "Richiesta errata %s"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest item %(idx)s :%(errors)s"
msgstr "Elemento richiesta errata %(idx)s :%(errors)s"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest: Not enough items in the list (%(current)s < %(expected)s)"
msgstr ""
"Richiesta errata: non ci sono sufficienti elementi nella lista (%(current)s "
"< %(expected)s)"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "BadRequest: Too many items in the list (%(current)s > %(expected)s)"
msgstr ""
"Richiesta errata: ci sono troppi elementi nella lista (%(current)s > "
"%(expected)s)"
#. module: base_rest
#: model:ir.ui.menu,name:base_rest.menu_rest_api_docs
msgid "Docs"
msgstr "Documenti"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "Invalid Response %s"
msgstr "Risposta non valida %s"
#. module: base_rest
#: model:ir.actions.act_url,name:base_rest.action_rest_api_docs
#: model:ir.ui.menu,name:base_rest.menu_rest_api_root
msgid "REST API"
msgstr "API REST"
#. module: base_rest
#: model:ir.model,name:base_rest.model_rest_service_registration
msgid "REST Services Registration Model"
msgstr "Modello registrazione servizio REST"
#. module: base_rest
#: model:ir.model,name:base_rest.model_ir_rule
msgid "Record Rule"
msgstr "Regola su record"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "Unable to get cerberus schema from %s"
msgstr "Impossibile ottenere lo schema Cerberus da %s"
#. module: base_rest
#. odoo-python
#: code:addons/base_rest/restapi.py:0
#, python-format
msgid "You must provide a dict of RestMethodParam"
msgstr "Bisogna fornire un dizionario del parametro metodo REST"

View file

@ -0,0 +1,2 @@
from . import ir_rule
from . import rest_service_registration

View file

@ -0,0 +1,27 @@
# Copyright 2021 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
class IrRule(models.Model):
"""Add authenticated_partner_id in record rule evaluation context.
This come from the env context, which is populated by the base_rest service layer
context provider.
"""
_inherit = "ir.rule"
@api.model
def _eval_context(self):
ctx = super()._eval_context()
if "authenticated_partner_id" in self.env.context:
ctx["authenticated_partner_id"] = self.env.context[
"authenticated_partner_id"
]
return ctx
def _compute_domain_keys(self):
"""Return the list of context keys to use for caching ``_compute_domain``."""
return super()._compute_domain_keys() + ["authenticated_partner_id"]

View file

@ -0,0 +1,452 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
"""
REST Service Registy Builder
============================
Register available REST services at the build of a registry.
This code is inspired by ``odoo.addons.component.builder.ComponentBuilder``
"""
import inspect
import logging
from werkzeug.routing import Map, Rule
import odoo
from odoo import http, models
from odoo.addons.component.core import WorkContext
from .. import restapi
from ..components.service import BaseRestService
from ..controllers.main import _PseudoCollection
from ..core import (
RestServicesRegistry,
_rest_controllers_per_module,
_rest_services_databases,
_rest_services_routes,
)
from ..tools import ROUTING_DECORATOR_ATTR, _inspect_methods
_logger = logging.getLogger(__name__)
class RestServiceRegistration(models.AbstractModel):
"""Register REST services into the REST services registry
This class allows us to hook the registration of the root urls of all
the REST controllers installed into the current database at the end of the
Odoo's registry loading, using ``_register_hook``. This method is called
after all modules are loaded, so we are sure that we only register REST
services installed into the current database.
"""
_name = "rest.service.registration"
_description = "REST Services Registration Model"
def _register_hook(self):
# This method is called by Odoo when the registry is built,
# so in case the registry is rebuilt (cache invalidation, ...),
# we have to to rebuild the registry. We use a new
# registry so we have an empty cache and we'll add services in it.
services_registry = self._init_global_registry()
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.Controller.children_classes["base_rest"]
controllers = [
cls for cls in controllers if "RestController" not in cls.__name__
]
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)
def _build_controllers_routes(self, services_registry):
for controller_def in services_registry.values():
for service in self._get_services(controller_def["collection_name"]):
self._prepare_non_decorated_endpoints(service)
self._build_controller(service, controller_def)
def _prepare_non_decorated_endpoints(self, service):
# Autogenerate routing info where missing
RestApiMethodTransformer(service).fix()
def _build_controller(self, service, controller_def):
_logger.debug("Build service %s for controller_def %s", service, controller_def)
base_controller_cls = controller_def["controller_class"]
# build our new controller class
ctrl_cls = RestApiServiceControllerGenerator(
service, base_controller_cls
).generate()
# generate an addon name used to register our new controller for
# the current database
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)
self.env.registry._init_modules.add(addon_name)
# register our conroller into the list of available controllers
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):
"""
Apply default routes properties defined on the controller_class to
routes where properties are missing
Set the automatic auth on controller's routes.
During definition of new controller, the _default_auth should be
applied on every routes (cfr @route odoo's decorator).
This auth attribute should be applied only if the route doesn't already
define it.
:return:
"""
for _name, method in _inspect_methods(controller_class):
routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
if not routing:
continue
self._apply_default_auth_if_not_set(controller_class, routing)
self._apply_default_if_not_set(controller_class, routing, "csrf")
self._apply_default_if_not_set(controller_class, routing, "save_session")
self._apply_default_cors_if_not_set(controller_class, routing)
def _apply_default_if_not_set(self, controller_class, routing, attr_name):
default_attr_name = "_default_" + attr_name
if hasattr(controller_class, default_attr_name) and attr_name not in routing:
routing[attr_name] = getattr(controller_class, default_attr_name)
def _apply_default_auth_if_not_set(self, controller_class, routing):
default_attr_name = "_default_auth"
default_auth = getattr(controller_class, default_attr_name, None)
if default_auth:
if "auth" in routing:
auth = routing["auth"]
if auth == "public_or_default":
alternative_auth = "public_or_" + default_auth
if getattr(
self.env["ir.http"], "_auth_method_%s" % alternative_auth, None
):
routing["auth"] = alternative_auth
else:
_logger.debug(
"No %s auth method available: Fallback on %s",
alternative_auth,
default_auth,
)
routing["auth"] = default_auth
else:
routing["auth"] = default_auth
def _apply_default_cors_if_not_set(self, controller_class, routing):
default_attr_name = "_default_cors"
if hasattr(controller_class, default_attr_name) and "cors" not in routing:
cors = getattr(controller_class, default_attr_name)
routing["cors"] = cors
if cors and "OPTIONS" not in routing.get("methods", ["OPTIONS"]):
# add http method 'OPTIONS' required by cors if the route is
# restricted to specific method
routing["methods"].append("OPTIONS")
def _get_services(self, collection_name):
collection = _PseudoCollection(collection_name, self.env)
work = WorkContext(
model_name="rest.service.registration", collection=collection
)
component_classes = work._lookup_components(usage=None, model_name=None)
# removes component without collection that are not a rest service
component_classes = [
c for c in component_classes if self._filter_service_component(c)
]
return [comp(work) for comp in component_classes]
@staticmethod
def _filter_service_component(comp):
return (
issubclass(comp, BaseRestService)
and comp._collection
and comp._usage
and getattr(comp, "_is_rest_service_component", True)
)
def build_registry(self, services_registry, states=None, exclude_addons=None):
if not states:
states = ("installed", "to upgrade")
# we load REST, controllers following the order of the 'addons'
# dependencies to ensure that controllers defined in a more
# specialized addon and overriding more generic one takes precedences
# on the generic one into the registry
graph = odoo.modules.graph.Graph()
graph.add_module(self.env.cr, "base")
query = "SELECT name " "FROM ir_module_module " "WHERE state IN %s "
params = [tuple(states)]
if exclude_addons:
query += " AND name NOT IN %s "
params.append(tuple(exclude_addons))
self.env.cr.execute(query, params)
module_list = [name for (name,) in self.env.cr.fetchall() if name not in graph]
graph.add_modules(self.env.cr, module_list)
for module in graph:
self.load_services(module.name, services_registry)
def load_services(self, module, services_registry):
controller_defs = _rest_controllers_per_module.get(module, [])
for controller_def in controller_defs:
root_path = controller_def["root_path"]
is_base_contoller = not getattr(
controller_def["controller_class"], "_generated", False
)
if is_base_contoller:
current_controller = (
services_registry[root_path]["controller_class"]
if root_path in services_registry
else None
)
services_registry[controller_def["root_path"]] = controller_def
self._register_rest_route(controller_def["root_path"])
if (
current_controller
and current_controller != controller_def["controller_class"]
):
_logger.error(
"Only one REST controller can be safely declared for root path %s\n "
"Registering controller %s\n "
"Registered controller%s\n",
root_path,
controller_def,
services_registry[controller_def["root_path"]],
)
def _init_global_registry(self):
services_registry = RestServicesRegistry()
_rest_services_databases[self.env.cr.dbname] = services_registry
return services_registry
def _register_rest_route(self, route_path):
"""Register given route path to be handles as RestRequest.
See base_rest.http.get_request.
"""
_rest_services_routes[self.env.cr.dbname].add(route_path)
class RestApiMethodTransformer(object):
"""Helper class to generate and apply the missing restapi.method decorator
to service's methods defined without decorator.
Before 10/12.0.3.0.0 methods exposed by a service was based on implicit
conventions. This transformer is used to keep this functionality by
generating and applying the missing decorators. As result all the methods
exposed are decorated and the processing can be based on these decorators.
"""
def __init__(self, service):
self._service = service
def fix(self):
methods_to_fix = []
for name, method in _inspect_methods(self._service.__class__):
if not self._is_public_api_method(name):
continue
if not hasattr(method, ROUTING_DECORATOR_ATTR):
methods_to_fix.append(method)
for method in methods_to_fix:
self._fix_method_decorator(method)
def _is_public_api_method(self, method_name):
if method_name.startswith("_"):
return False
if not hasattr(self._service, method_name):
return False
if hasattr(BaseRestService, method_name):
# exclude methods from base class
return False
return True
def _fix_method_decorator(self, method):
method_name = method.__name__
routes = self._method_to_routes(method)
input_param = self._method_to_input_param(method)
output_param = self._method_to_output_param(method)
decorated_method = restapi.method(
routes=routes, input_param=input_param, output_param=output_param
)(getattr(self._service.__class__, method_name))
setattr(self._service.__class__, method_name, decorated_method)
def _method_to_routes(self, method):
"""
Generate the restapi.method's routes
:param method:
:return: A list of routes used to get access to the method
"""
method_name = method.__name__
signature = inspect.signature(method)
id_in_path_required = "_id" in signature.parameters
path = "/{}".format(method_name)
if id_in_path_required:
path = "/<int:id>" + path
if method_name in ("get", "search"):
paths = [path]
path = "/"
if id_in_path_required:
path = "/<int:id>"
paths.append(path)
return [(paths, "GET")]
elif method_name == "delete":
routes = [(path, "POST")]
path = "/"
if id_in_path_required:
path = "/<int:id>"
routes.append((path, "DELETE"))
elif method_name == "update":
paths = [path]
path = "/"
if id_in_path_required:
path = "/<int:id>"
paths.append(path)
routes = [(paths, "POST"), (path, "PUT")]
elif method_name == "create":
paths = [path]
path = "/"
if id_in_path_required:
path = "/<int:id>"
paths.append(path)
routes = [(paths, "POST")]
else:
routes = [(path, "POST")]
return routes
def _method_to_param(self, validator_method_name, direction):
validator_component = self._service.component(usage="cerberus.validator")
if validator_component.has_validator_handler(
self._service, validator_method_name, direction
):
return restapi.CerberusValidator(schema=validator_method_name)
return None
def _method_to_input_param(self, method):
validator_method_name = "_validator_{}".format(method.__name__)
return self._method_to_param(validator_method_name, "input")
def _method_to_output_param(self, method):
validator_method_name = "_validator_return_{}".format(method.__name__)
return self._method_to_param(validator_method_name, "output")
class RestApiServiceControllerGenerator(object):
"""
An object helper used to generate the http.Controller required to serve
the method decorated with the `@restappi.method` decorator
"""
def __init__(self, service, base_controller):
self._service = service
self._service_name = service._usage
self._base_controller = base_controller
@property
def _new_cls_name(self):
controller_name = self._base_controller.__name__
return "{}{}".format(
controller_name, self._service._usage.title().replace(".", "_")
)
def generate(self):
"""
:return: A new controller child of base_controller defining the routes
required to serve the method of the services.
"""
controller = type(
self._new_cls_name, (self._base_controller,), self._generate_methods()
)
controller._generated = True
return controller
def _generate_methods(self):
"""Generate controller's methods and associated routes
This method inspect the service definition and generate the appropriate
methods and routing rules for all the methods decorated with @restappi.method
:return: A dictionary of method name : method
"""
methods = {}
_globals = {}
root_path = self._base_controller._root_path
path_sep = ""
if root_path[-1] != "/":
path_sep = "/"
root_path = "{}{}{}".format(root_path, path_sep, self._service._usage)
for name, method in _inspect_methods(self._service.__class__):
routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
if routing is None:
continue
for routes, http_method in routing["routes"]:
method_name = "{}_{}".format(http_method.lower(), name)
default_route = routes[0]
rule = Rule(default_route)
Map(rules=[rule])
if rule.arguments:
method = METHOD_TMPL_WITH_ARGS.format(
method_name=method_name,
service_name=self._service_name,
service_method_name=name,
args=", ".join([c[1] for c in rule._trace if c[0]]),
)
else:
method = METHOD_TMPL.format(
method_name=method_name,
service_name=self._service_name,
service_method_name=name,
)
exec(method, _globals)
method_exec = _globals[method_name]
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:
route_params[attr] = routing[attr]
method_exec = http.route(**route_params)(method_exec)
methods[method_name] = method_exec
return methods
METHOD_TMPL = """
def {method_name}(self, collection=None, **kwargs):
return self._process_method(
"{service_name}",
"{service_method_name}",
collection=collection,
params=kwargs
)
"""
METHOD_TMPL_WITH_ARGS = """
def {method_name}(self, {args}, collection=None, **kwargs):
return self._process_method(
"{service_name}",
"{service_method_name}",
*[{args}],
collection=collection,
params=kwargs
)
"""

View file

@ -0,0 +1,23 @@
If an error occurs when calling a method of a service (ie missing parameter,
..) the system returns only a general description of the problem without
details. This is done on purpose to ensure maximum opacity on implementation
details and therefore lower security issue.
This restriction can be problematic when the services are accessed by an
external system in development. To know the details of an error it is indeed
necessary to have access to the log of the server. It is not always possible
to provide this kind of access. That's why you can configure the server to run
these services in development mode.
To run the REST API in development mode you must add a new section
'**[base_rest]**' with the option '**dev_mode=True**' in the server config
file.
.. code-block:: cfg
[base_rest]
dev_mode=True
When the REST API runs in development mode, the original description and a
stack trace is returned in case of error. **Be careful to not use this mode
in production**.

View file

@ -0,0 +1,2 @@
* Laurent Mignon <laurent.mignon@acsone.eu>
* Sébastien Beau <sebastien.beau@akretion.com>

View file

@ -0,0 +1,19 @@
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
becomes necessary to set up specialized service interfaces, so existing
systems can interact with Odoo.
While the XML-RPC interface of Odoo comes handy in such situations, it
requires a deep understanding of Odoos internal data model. When used
extensively, it creates a strong coupling between Odoo internals and client
systems, therefore increasing maintenance costs.
Once developed, an `OpenApi <https://spec.openapis.org/oas/v3.0.3>`_ documentation
is generated from the source code and available via a
`Swagger UI <https://swagger.io/tools/swagger-ui/>`_ served by your odoo server
at `https://my_odoo_server/api-docs`.

View file

@ -0,0 +1,38 @@
16.0.1.0.2 (2023-10-07)
~~~~~~~~~~~~~~~~~~~~~~~
**Features**
- Add support for oauth2 security scheme in the Swagger UI. If your openapi
specification contains a security scheme of type oauth2, the Swagger UI will
display a login button in the top right corner. In order to finalize the
login process, a redirect URL must be provided when initializing the Swagger
UI. The Swagger UI is now initialized with a `oauth2RedirectUrl` option that
references a oauth2-redirect.html file provided by the swagger-ui lib and served
by the current addon. (`#379 <https://github.com/OCA/rest-framework/issues/379>`_)
12.0.2.0.1
~~~~~~~~~~
* _validator_...() methods can now return a cerberus ``Validator`` object
instead of a schema dictionnary, for additional flexibility (e.g. allowing
validator options such as ``allow_unknown``).
12.0.2.0.0
~~~~~~~~~~
* Licence changed from AGPL-3 to LGPL-3
12.0.1.0.1
~~~~~~~~~~
* Fix issue when rendering the jsonapi documentation if no documentation is
provided on a method part of the REST api.
12.0.1.0.0
~~~~~~~~~~
First official version. The addon has been incubated into the
`Shopinvader repository <https://github.com/akretion/odoo-shopinvader>`_ from
Akretion. For more information you need to look at the git log.

View file

@ -0,0 +1,3 @@
The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Abase_rest>`_
and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Abase_rest>`_ can
be found on GitHub.

View file

@ -0,0 +1,247 @@
To add your own REST service you must provides at least 2 classes.
* A Component providing the business logic of your service,
* A Controller to register your service.
The business logic of your service must be implemented into a component
(``odoo.addons.component.core.Component``) that inherit from
'base.rest.service'
Initially, base_rest expose by default all public methods defined in a service.
The conventions for accessing methods via HTTP were as follows:
* The method ``def get(self, _id)`` if defined, is accessible via HTTP GET routes ``<string:_service_name>/<int:_id>`` and ``<string:_service_name>/<int:_id>/get``.
* The method ``def search(self, **params)`` if defined, is accessible via the HTTP GET routes ``<string:_service_name>/`` and ``<string:_service_name>/search``.
* The method ``def delete(self, _id)`` if defined, is accessible via the HTTP DELETE route ``<string:_service_name>/<int:_id>``.
* The ``def update(self, _id, **params)`` method, if defined, is accessible via the HTTP PUT route ``<string:_service_name>/<int:_id>``.
* Other methods are only accessible via HTTP POST routes ``<string:_service_name>`` or ``<string:_service_name>/<string:method_name>`` or ``<string:_service_name>/<int:_id>`` or ``<string:_service_name>/<int:_id>/<string:method_name>``
.. code-block:: python
from odoo.addons.component.core import Component
class PingService(Component):
_inherit = 'base.rest.service'
_name = 'ping.service'
_usage = 'ping'
_collection = 'my_module.services'
# The following method are 'public' and can be called from the controller.
def get(self, _id, message):
return {
'response': 'Get called with message ' + message}
def search(self, message):
return {
'response': 'Search called search with message ' + message}
def update(self, _id, message):
return {'response': 'PUT called with message ' + message}
# pylint:disable=method-required-super
def create(self, **params):
return {'response': 'POST called with message ' + params['message']}
def delete(self, _id):
return {'response': 'DELETE called with id %s ' % _id}
# Validator
def _validator_search(self):
return {'message': {'type': 'string'}}
# Validator
def _validator_get(self):
# no parameters by default
return {}
def _validator_update(self):
return {'message': {'type': 'string'}}
def _validator_create(self):
return {'message': {'type': 'string'}}
Once you have implemented your services (ping, ...), you must tell to Odoo
how to access to these services. This process is done by implementing a
controller that inherits from ``odoo.addons.base_rest.controllers.main.RestController``
.. code-block:: python
from odoo.addons.base_rest.controllers import main
class MyRestController(main.RestController):
_root_path = '/my_services_api/'
_collection_name = my_module.services
In your controller, _'root_path' is used to specify the root of the path to
access to your services and '_collection_name' is the name of the collection
providing the business logic for the requested service/
By inheriting from ``RestController`` the following routes will be registered
to access to your services
.. code-block:: python
@route([
ROOT_PATH + '<string:_service_name>',
ROOT_PATH + '<string:_service_name>/search',
ROOT_PATH + '<string:_service_name>/<int:_id>',
ROOT_PATH + '<string:_service_name>/<int:_id>/get'
], methods=['GET'], auth="user", csrf=False)
def get(self, _service_name, _id=None, **params):
method_name = 'get' if _id else 'search'
return self._process_method(_service_name, method_name, _id, params)
@route([
ROOT_PATH + '<string:_service_name>',
ROOT_PATH + '<string:_service_name>/<string:method_name>',
ROOT_PATH + '<string:_service_name>/<int:_id>',
ROOT_PATH + '<string:_service_name>/<int:_id>/<string:method_name>'
], methods=['POST'], auth="user", csrf=False)
def modify(self, _service_name, _id=None, method_name=None, **params):
if not method_name:
method_name = 'update' if _id else 'create'
if method_name == 'get':
_logger.error("HTTP POST with method name 'get' is not allowed. "
"(service name: %s)", _service_name)
raise BadRequest()
return self._process_method(_service_name, method_name, _id, params)
@route([
ROOT_PATH + '<string:_service_name>/<int:_id>',
], methods=['PUT'], auth="user", csrf=False)
def update(self, _service_name, _id, **params):
return self._process_method(_service_name, 'update', _id, params)
@route([
ROOT_PATH + '<string:_service_name>/<int:_id>',
], methods=['DELETE'], auth="user", csrf=False)
def delete(self, _service_name, _id):
return self._process_method(_service_name, 'delete', _id)
As result an HTTP GET call to 'http://my_odoo/my_services_api/ping' will be
dispatched to the method ``PingService.search``
In addition to easily exposing your methods, the module allows you to define
data schemas to which the exchanged data must conform. These schemas are defined
on the basis of `Cerberus schemas <https://docs.python-cerberus.org/en/stable/>`_
and associated to the methods using the
following naming convention. For a method `my_method`:
* ``def _validator_my_method(self):`` will be called to get the schema required to
validate the input parameters.
* ``def _validator_return_my_method(self):`` if defined, will be called to get
the schema used to validate the response.
In order to offer even more flexibility, a new API has been developed.
This new API replaces the implicit approach used to expose a service by the use
of a python decorator to explicitly mark a method as being available via the
REST API: ``odoo.addons.base_rest.restapi.method``.
.. code-block:: python
class PartnerNewApiService(Component):
_inherit = "base.rest.service"
_name = "partner.new_api.service"
_usage = "partner"
_collection = "base.rest.demo.new_api.services"
_description = """
Partner New API Services
Services developed with the new api provided by base_rest
"""
@restapi.method(
[(["/<int:id>/get", "/<int:id>"], "GET")],
output_param=restapi.CerberusValidator("_get_partner_schema"),
auth="public",
)
def get(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
def _get_partner_schema(self):
return {
"name": {"type": "string", "required": True}
}
@restapi.method(
[(["/list", "/"], "GET")],
output_param=restapi.CerberusListValidator("_get_partner_schema"),
auth="public",
)
def list(self):
partners = self.env["res.partner"].search([])
return [{"name": p.name} for p in partners]
Thanks to this new api, you are now free to specify your own routes but also
to use other object types as parameter or response to your methods.
For example, `base_rest_datamodel` allows you to use Datamodel object instance
into your services.
.. code-block:: python
from marshmallow import fields
from odoo.addons.base_rest import restapi
from odoo.addons.component.core import Component
from odoo.addons.datamodel.core import Datamodel
class PartnerSearchParam(Datamodel):
_name = "partner.search.param"
id = fields.Integer(required=False, allow_none=False)
name = fields.String(required=False, allow_none=False)
class PartnerShortInfo(Datamodel):
_name = "partner.short.info"
id = fields.Integer(required=True, allow_none=False)
name = fields.String(required=True, allow_none=False)
class PartnerNewApiService(Component):
_inherit = "base.rest.service"
_name = "partner.new_api.service"
_usage = "partner"
_collection = "base.rest.demo.new_api.services"
_description = """
Partner New API Services
Services developed with the new api provided by base_rest
"""
@restapi.method(
[(["/", "/search"], "GET")],
input_param=restapi.Datamodel("partner.search.param"),
output_param=restapi.Datamodel("partner.short.info", is_list=True),
auth="public",
)
def search(self, partner_search_param):
"""
Search for partners
:param partner_search_param: An instance of partner.search.param
:return: List of partner.short.info
"""
domain = []
if partner_search_param.name:
domain.append(("name", "like", partner_search_param.name))
if partner_search_param.id:
domain.append(("id", "=", partner_search_param.id))
res = []
PartnerShortInfo = self.env.datamodels["partner.short.info"]
for p in self.env["res.partner"].search(domain):
res.append(PartnerShortInfo(id=p.id, name=p.name))
return res
The BaseRestServiceContextProvider provides context for your services,
including authenticated_partner_id.
You are free to redefine the method _get_authenticated_partner_id() to pass the
authenticated_partner_id based on the authentication mechanism of your choice.
See base_rest_auth_jwt for an example.
In addition, authenticated_partner_id is available in record rule evaluation context.

View file

@ -0,0 +1,433 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import abc
import functools
import json
from cerberus import Validator
from odoo import _, http
from odoo.exceptions import UserError, ValidationError
from .tools import ROUTING_DECORATOR_ATTR, cerberus_to_json
def method(routes, input_param=None, output_param=None, **kw):
"""Decorator marking the decorated method as being a handler for
REST requests. The method must be part of a component inheriting from
``base.rest.service``.
:param routes: list of tuple (path, http method). path is a string or
array.
Each tuple determines which http requests and http method
will match the decorated method. The path part can be a
single string or an array of strings. See werkzeug's routing
documentation for the format of path expression (
http://werkzeug.pocoo.org/docs/routing/ ).
:param: input_param: An instance of an object that implemented
``RestMethodParam``. When processing a request, the http
handler first call the from_request method and then call the
decorated method with the result of this call.
:param: output_param: An instance of an object that implemented
``RestMethodParam``. When processing the result of the
call to the decorated method, the http handler first call
the `to_response` method with this result and then return
the result of this call.
:param auth: The type of authentication method. A special auth method
named 'public_or_default' can be used. In such a case
when the HTTP route will be generated, the auth method
will be computed from the '_default_auth' property defined
on the controller with 'public_or_' as prefix.
The purpose of 'public_or_default' auth method is to provide
a way to specify that a method should work for anonymous users
but can be enhanced when an authenticated user is know.
It implies that when the 'default' auth part of 'public_or_default'
will be replaced by the default_auth specified on the controller
in charge of registering the web services, an auth method with
the same name is defined into odoo to provide such a behavior.
In the following example, the auth method on my ping service
will be `public_or_jwt` since this authentication method is
provided by the auth_jwt addon.
.. code-block:: python
class PingService(Component):
_inherit = "base.rest.service"
_name = "ping_service"
_usage = "ping"
_collection = "test.api.services"
@restapi.method(
[(["/<string:message>""], "GET")],
auth="public_or_auth",
)
def _ping(self, message):
return {"message": message}
class MyRestController(main.RestController):
_root_path = '/test/'
_collection_name = "test.api.services"
_default_auth = "jwt'
:param cors: The Access-Control-Allow-Origin cors directive value. When
set, this automatically adds OPTIONS to allowed http methods
so the Odoo request handler will accept it.
:param bool csrf: Whether CSRF protection should be enabled for the route.
Defaults to ``False``
:param bool save_session: Whether HTTP session should be saved into the
session store: Default to ``True``
"""
def decorator(f):
_routes = []
for paths, http_methods in routes:
if not isinstance(paths, list):
paths = [paths]
if not isinstance(http_methods, list):
http_methods = [http_methods]
if kw.get("cors") and "OPTIONS" not in http_methods:
http_methods.append("OPTIONS")
for m in http_methods:
_routes.append(([p for p in paths], m))
routing = {
"routes": _routes,
"input_param": input_param,
"output_param": output_param,
}
routing.update(kw)
@functools.wraps(f)
def response_wrap(*args, **kw):
response = f(*args, **kw)
return response
setattr(response_wrap, ROUTING_DECORATOR_ATTR, routing)
response_wrap.original_func = f
return response_wrap
return decorator
class RestMethodParam(abc.ABC):
@abc.abstractmethod
def from_params(self, service, params):
"""
This method is called to process the parameters received at the
controller. This method should validate and sanitize these paramaters.
It could also be used to transform these parameters into the format
expected by the called method
:param service:
:param request: `HttpRequest.params`
:return: Value into the format expected by the method
"""
@abc.abstractmethod
def to_response(self, service, result) -> http.Response:
"""
This method is called to prepare the result of the call to the method
in a format suitable by the controller (http.Response or JSON dict).
It's responsible for validating and sanitizing the result.
:param service:
:param obj:
:return: http.Response or JSON dict
"""
@abc.abstractmethod
def to_openapi_query_parameters(self, service, spec) -> dict:
return {}
@abc.abstractmethod
def to_openapi_requestbody(self, service, spec) -> dict:
return {}
@abc.abstractmethod
def to_openapi_responses(self, service, spec) -> dict:
return {}
@abc.abstractmethod
def to_json_schema(self, service, spec, direction) -> dict:
return {}
class BinaryData(RestMethodParam):
def __init__(self, mediatypes="*/*", required=False):
if not isinstance(mediatypes, list):
mediatypes = [mediatypes]
self._mediatypes = mediatypes
self._required = required
def to_json_schema(self, service, spec, direction):
return {
"type": "string",
"format": "binary",
"required": self._required,
}
@property
def _binary_content_schema(self):
return {
mediatype: {"schema": self.to_json_schema(None, None, None)}
for mediatype in self._mediatypes
}
def to_openapi_requestbody(self, service, spec):
return {"content": self._binary_content_schema}
def to_openapi_query_parameters(self, service, spec):
raise NotImplementedError(
"BinaryData are not (?yet?) supported as query paramters"
)
def to_openapi_responses(self, service, spec):
return {"200": {"content": self._binary_content_schema}}
def to_response(self, service, result):
if not isinstance(result, http.Response):
# The response has not been build by the called method...
result = self._to_http_response(result)
return result
def from_params(self, service, params):
return params
def _to_http_response(self, result):
mediatype = self._mediatypes[0] if len(self._mediatypes) == 1 else "*/*"
headers = [
("Content-Type", mediatype),
("X-Content-Type-Options", "nosniff"),
("Content-Disposition", http.content_disposition("file")),
("Content-Length", len(result)),
]
return http.request.make_response(result, headers)
class CerberusValidator(RestMethodParam):
def __init__(self, schema):
"""
:param schema: can be dict as cerberus schema, an instance of
cerberus.Validator or a sting with the method name to
call on the service to get the schema or the validator
"""
self._schema = schema
def from_params(self, service, params):
validator = self.get_cerberus_validator(service, "input")
if validator.validate(params):
return validator.document
raise UserError(_("BadRequest %s") % validator.errors)
def to_response(self, service, result):
validator = self.get_cerberus_validator(service, "output")
if validator.validate(result):
return validator.document
raise SystemError(_("Invalid Response %s") % validator.errors)
def to_openapi_query_parameters(self, service, spec):
json_schema = self.to_json_schema(service, spec, "input")
parameters = []
for prop, spec in list(json_schema["properties"].items()):
params = {
"name": prop,
"in": "query",
"required": prop in json_schema["required"],
"allowEmptyValue": spec.get("nullable", False),
"default": spec.get("default"),
}
if spec.get("schema"):
params["schema"] = spec.get("schema")
else:
params["schema"] = {"type": spec["type"]}
if spec.get("items"):
params["schema"]["items"] = spec.get("items")
if "enum" in spec:
params["schema"]["enum"] = spec["enum"]
parameters.append(params)
if spec["type"] == "array":
# To correctly handle array into the url query string,
# the name must ends with []
params["name"] = params["name"] + "[]"
return parameters
def to_openapi_requestbody(self, service, spec):
json_schema = self.to_json_schema(service, spec, "input")
return {"content": {"application/json": {"schema": json_schema}}}
def to_openapi_responses(self, service, spec):
json_schema = self.to_json_schema(service, spec, "output")
return {"200": {"content": {"application/json": {"schema": json_schema}}}}
def get_cerberus_validator(self, service, direction):
assert direction in ("input", "output")
schema = self._schema
if isinstance(self._schema, str):
validator_component = service.component(usage="cerberus.validator")
schema = validator_component.get_validator_handler(
service, self._schema, direction
)()
if isinstance(schema, Validator):
return schema
if isinstance(schema, dict):
return Validator(schema, purge_unknown=True)
raise Exception(_("Unable to get cerberus schema from %s") % self._schema)
def to_json_schema(self, service, spec, direction):
schema = self.get_cerberus_validator(service, direction).schema
return cerberus_to_json(schema)
class CerberusListValidator(CerberusValidator):
def __init__(self, schema, min_items=None, max_items=None, unique_items=None):
"""
:param schema: Cerberus list item schema
can be dict as cerberus schema, an instance of
cerberus.Validator or a sting with the method name to
call on the service to get the schema or the validator
:param min_items: A list instance is valid against "min_items" if its
size is greater than, or equal to, min_items.
The value MUST be a non-negative integer.
:param max_items: A list instance is valid against "max_items" if its
size is less than, or equal to, max_items.
The value MUST be a non-negative integer.
:param unique_items: Used to document that the list should only
contain unique items.
(Not enforced at validation time)
"""
super(CerberusListValidator, self).__init__(schema=schema)
self._min_items = min_items
self._max_items = max_items
self._unique_items = unique_items
def from_params(self, service, params):
return self._do_validate(service, data=params, direction="input")
def to_response(self, service, result):
return self._do_validate(service, data=result, direction="output")
def to_openapi_query_parameters(self, service, spec):
raise NotImplementedError("List are not (?yet?) supported as query paramters")
# pylint: disable=W8120,W8115
def _do_validate(self, service, data, direction):
validator = self.get_cerberus_validator(service, direction)
values = []
ExceptionClass = UserError if direction == "input" else SystemError
for idx, p in enumerate(data):
if not validator.validate(p):
raise ExceptionClass(
_(
"BadRequest item %(idx)s :%(errors)s",
idx=idx,
errors=validator.errors,
)
)
values.append(validator.document)
if self._min_items is not None and len(values) < self._min_items:
raise ExceptionClass(
_(
"BadRequest: Not enough items in the list (%(current)s < %(expected)s)",
current=len(values),
expected=self._min_items,
)
)
if self._max_items is not None and len(values) > self._max_items:
raise ExceptionClass(
_(
"BadRequest: Too many items in the list (%(current)s > %(expected)s)",
current=len(values),
expected=self._max_items,
)
)
return values
def to_json_schema(self, service, spec, direction):
cerberus_schema = self.get_cerberus_validator(service, direction).schema
json_schema = cerberus_to_json(cerberus_schema)
json_schema = {"type": "array", "items": json_schema}
if self._min_items is not None:
json_schema["minItems"] = self._min_items
if self._max_items is not None:
json_schema["maxItems"] = self._max_items
if self._unique_items is not None:
json_schema["uniqueItems"] = self._unique_items
return json_schema
class MultipartFormData(RestMethodParam):
def __init__(self, parts):
"""This allows to create multipart/form-data endpoints.
:param parts: list of RestMethodParam
"""
if not isinstance(parts, dict):
raise ValidationError(_("You must provide a dict of RestMethodParam"))
self._parts = parts
def to_openapi_properties(self, service, spec, direction):
properties = {}
for key, part in self._parts.items():
properties[key] = part.to_json_schema(service, spec, direction)
return properties
def to_openapi_encoding(self):
encodings = {}
for key, part in self._parts.items():
if isinstance(part, BinaryData):
encodings[key] = {"contentType": ", ".join(part._mediatypes)}
return encodings
def to_json_schema(self, service, spec, direction):
res = {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": self.to_openapi_properties(service, spec, direction),
}
}
}
encoding = self.to_openapi_encoding()
if len(encoding) > 0:
res["multipart/form-data"]["schema"]["encoding"] = encoding
return res
def from_params(self, service, params):
for key, part in self._parts.items():
param = None
if isinstance(part, BinaryData):
param = part.from_params(service, params[key])
else:
# If the part is not Binary, it should be JSON
try:
json_param = json.loads(
params[key]
) # multipart ony sends its parts as string
except json.JSONDecodeError as error:
raise ValidationError(
_(
"%(key)'s JSON content is malformed: %(error)s",
key=key,
error=error,
)
) from error
param = part.from_params(service, json_param)
params[key] = param
return params
def to_openapi_query_parameters(self, service, spec):
raise NotImplementedError(
"MultipartFormData are not (?yet?) supported as query paramters"
)
def to_openapi_requestbody(self, service, spec):
return {"content": self.to_json_schema(service, spec, "input")}
def to_openapi_responses(self, service, spec):
return {"200": {"content": self.to_json_schema(service, spec, "output")}}
def to_response(self, service, result):
raise NotImplementedError()

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -0,0 +1,753 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Base Rest</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="base-rest">
<h1 class="title">Base Rest</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:517d5b1d74542047b404d2130e5d9239fe591f43b1a89ca02339766c8c8a6584
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/rest-framework/tree/16.0/base_rest"><img alt="OCA/rest-framework" src="https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-base_rest"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This addon is deprecated and not fully supported anymore on Odoo 16.
Please migrate to the FastAPI migration module.
See <a class="reference external" href="https://github.com/OCA/rest-framework/pull/291">https://github.com/OCA/rest-framework/pull/291</a>.</p>
<p>This addon provides the basis to develop high level REST APIs for Odoo.</p>
<p>As Odoo becomes one of the central pieces of enterprise IT systems, it often
becomes necessary to set up specialized service interfaces, so existing
systems can interact with Odoo.</p>
<p>While the XML-RPC interface of Odoo comes handy in such situations, it
requires a deep understanding of Odoos internal data model. When used
extensively, it creates a strong coupling between Odoo internals and client
systems, therefore increasing maintenance costs.</p>
<p>Once developed, an <a class="reference external" href="https://spec.openapis.org/oas/v3.0.3">OpenApi</a> documentation
is generated from the source code and available via a
<a class="reference external" href="https://swagger.io/tools/swagger-ui/">Swagger UI</a> served by your odoo server
at <cite>https://my_odoo_server/api-docs</cite>.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-3">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#changelog" id="toc-entry-4">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-5">16.0.1.0.2 (2023-10-07)</a></li>
<li><a class="reference internal" href="#section-2" id="toc-entry-6">12.0.2.0.1</a></li>
<li><a class="reference internal" href="#section-3" id="toc-entry-7">12.0.2.0.0</a></li>
<li><a class="reference internal" href="#section-4" id="toc-entry-8">12.0.1.0.1</a></li>
<li><a class="reference internal" href="#section-5" id="toc-entry-9">12.0.1.0.0</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-10">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-11">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-12">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-13">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-14">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>If an error occurs when calling a method of a service (ie missing parameter,
..) the system returns only a general description of the problem without
details. This is done on purpose to ensure maximum opacity on implementation
details and therefore lower security issue.</p>
<p>This restriction can be problematic when the services are accessed by an
external system in development. To know the details of an error it is indeed
necessary to have access to the log of the server. It is not always possible
to provide this kind of access. Thats why you can configure the server to run
these services in development mode.</p>
<p>To run the REST API in development mode you must add a new section
<strong>[base_rest]</strong> with the option <strong>dev_mode=True</strong> in the server config
file.</p>
<pre class="code cfg literal-block">
<span class="k">[base_rest]</span><span class="w">
</span><span class="na">dev_mode</span><span class="o">=</span><span class="s">True</span>
</pre>
<p>When the REST API runs in development mode, the original description and a
stack trace is returned in case of error. <strong>Be careful to not use this mode
in production</strong>.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<p>To add your own REST service you must provides at least 2 classes.</p>
<ul class="simple">
<li>A Component providing the business logic of your service,</li>
<li>A Controller to register your service.</li>
</ul>
<p>The business logic of your service must be implemented into a component
(<tt class="docutils literal">odoo.addons.component.core.Component</tt>) that inherit from
base.rest.service</p>
<p>Initially, base_rest expose by default all public methods defined in a service.
The conventions for accessing methods via HTTP were as follows:</p>
<ul class="simple">
<li>The method <tt class="docutils literal">def get(self, _id)</tt> if defined, is accessible via HTTP GET routes <tt class="docutils literal"><span class="pre">&lt;string:_service_name&gt;/&lt;int:_id&gt;</span></tt> and <tt class="docutils literal"><span class="pre">&lt;string:_service_name&gt;/&lt;int:_id&gt;/get</span></tt>.</li>
<li>The method <tt class="docutils literal">def search(self, **params)</tt> if defined, is accessible via the HTTP GET routes <tt class="docutils literal">&lt;string:_service_name&gt;/</tt> and <tt class="docutils literal"><span class="pre">&lt;string:_service_name&gt;/search</span></tt>.</li>
<li>The method <tt class="docutils literal">def delete(self, _id)</tt> if defined, is accessible via the HTTP DELETE route <tt class="docutils literal"><span class="pre">&lt;string:_service_name&gt;/&lt;int:_id&gt;</span></tt>.</li>
<li>The <tt class="docutils literal">def update(self, _id, **params)</tt> method, if defined, is accessible via the HTTP PUT route <tt class="docutils literal"><span class="pre">&lt;string:_service_name&gt;/&lt;int:_id&gt;</span></tt>.</li>
<li>Other methods are only accessible via HTTP POST routes <tt class="docutils literal">&lt;string:_service_name&gt;</tt> or <tt class="docutils literal"><span class="pre">&lt;string:_service_name&gt;/&lt;string:method_name&gt;</span></tt> or <tt class="docutils literal"><span class="pre">&lt;string:_service_name&gt;/&lt;int:_id&gt;</span></tt> or <tt class="docutils literal"><span class="pre">&lt;string:_service_name&gt;/&lt;int:_id&gt;/&lt;string:method_name&gt;</span></tt></li>
</ul>
<pre class="code python literal-block">
<span class="kn">from</span><span class="w"> </span><span class="nn">odoo.addons.component.core</span><span class="w"> </span><span class="kn">import</span> <span class="n">Component</span><span class="w">
</span><span class="k">class</span><span class="w"> </span><span class="nc">PingService</span><span class="p">(</span><span class="n">Component</span><span class="p">):</span><span class="w">
</span> <span class="n">_inherit</span> <span class="o">=</span> <span class="s1">'base.rest.service'</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s1">'ping.service'</span><span class="w">
</span> <span class="n">_usage</span> <span class="o">=</span> <span class="s1">'ping'</span><span class="w">
</span> <span class="n">_collection</span> <span class="o">=</span> <span class="s1">'my_module.services'</span><span class="w">
</span> <span class="c1"># The following method are 'public' and can be called from the controller.</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">_id</span><span class="p">,</span> <span class="n">message</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="p">{</span><span class="w">
</span> <span class="s1">'response'</span><span class="p">:</span> <span class="s1">'Get called with message '</span> <span class="o">+</span> <span class="n">message</span><span class="p">}</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">search</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="p">{</span><span class="w">
</span> <span class="s1">'response'</span><span class="p">:</span> <span class="s1">'Search called search with message '</span> <span class="o">+</span> <span class="n">message</span><span class="p">}</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">update</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">_id</span><span class="p">,</span> <span class="n">message</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="p">{</span><span class="s1">'response'</span><span class="p">:</span> <span class="s1">'PUT called with message '</span> <span class="o">+</span> <span class="n">message</span><span class="p">}</span><span class="w">
</span> <span class="c1"># pylint:disable=method-required-super</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">create</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">params</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="p">{</span><span class="s1">'response'</span><span class="p">:</span> <span class="s1">'POST called with message '</span> <span class="o">+</span> <span class="n">params</span><span class="p">[</span><span class="s1">'message'</span><span class="p">]}</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">delete</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">_id</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="p">{</span><span class="s1">'response'</span><span class="p">:</span> <span class="s1">'DELETE called with id </span><span class="si">%s</span><span class="s1"> '</span> <span class="o">%</span> <span class="n">_id</span><span class="p">}</span><span class="w">
</span> <span class="c1"># Validator</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">_validator_search</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="p">{</span><span class="s1">'message'</span><span class="p">:</span> <span class="p">{</span><span class="s1">'type'</span><span class="p">:</span> <span class="s1">'string'</span><span class="p">}}</span><span class="w">
</span> <span class="c1"># Validator</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">_validator_get</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span> <span class="c1"># no parameters by default</span><span class="w">
</span> <span class="k">return</span> <span class="p">{}</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">_validator_update</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="p">{</span><span class="s1">'message'</span><span class="p">:</span> <span class="p">{</span><span class="s1">'type'</span><span class="p">:</span> <span class="s1">'string'</span><span class="p">}}</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">_validator_create</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="p">{</span><span class="s1">'message'</span><span class="p">:</span> <span class="p">{</span><span class="s1">'type'</span><span class="p">:</span> <span class="s1">'string'</span><span class="p">}}</span>
</pre>
<p>Once you have implemented your services (ping, …), you must tell to Odoo
how to access to these services. This process is done by implementing a
controller that inherits from <tt class="docutils literal">odoo.addons.base_rest.controllers.main.RestController</tt></p>
<pre class="code python literal-block">
<span class="kn">from</span><span class="w"> </span><span class="nn">odoo.addons.base_rest.controllers</span><span class="w"> </span><span class="kn">import</span> <span class="n">main</span><span class="w">
</span><span class="k">class</span><span class="w"> </span><span class="nc">MyRestController</span><span class="p">(</span><span class="n">main</span><span class="o">.</span><span class="n">RestController</span><span class="p">):</span><span class="w">
</span> <span class="n">_root_path</span> <span class="o">=</span> <span class="s1">'/my_services_api/'</span><span class="w">
</span> <span class="n">_collection_name</span> <span class="o">=</span> <span class="n">my_module</span><span class="o">.</span><span class="n">services</span>
</pre>
<p>In your controller, _root_path is used to specify the root of the path to
access to your services and _collection_name is the name of the collection
providing the business logic for the requested service/</p>
<p>By inheriting from <tt class="docutils literal">RestController</tt> the following routes will be registered
to access to your services</p>
<pre class="code python literal-block">
<span class="nd">&#64;route</span><span class="p">([</span><span class="w">
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'&lt;string:_service_name&gt;'</span><span class="p">,</span><span class="w">
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'&lt;string:_service_name&gt;/search'</span><span class="p">,</span><span class="w">
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'&lt;string:_service_name&gt;/&lt;int:_id&gt;'</span><span class="p">,</span><span class="w">
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'&lt;string:_service_name&gt;/&lt;int:_id&gt;/get'</span><span class="w">
</span><span class="p">],</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s1">'GET'</span><span class="p">],</span> <span class="n">auth</span><span class="o">=</span><span class="s2">&quot;user&quot;</span><span class="p">,</span> <span class="n">csrf</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span><span class="k">def</span><span class="w"> </span><span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">_service_name</span><span class="p">,</span> <span class="n">_id</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="o">**</span><span class="n">params</span><span class="p">):</span><span class="w">
</span> <span class="n">method_name</span> <span class="o">=</span> <span class="s1">'get'</span> <span class="k">if</span> <span class="n">_id</span> <span class="k">else</span> <span class="s1">'search'</span><span class="w">
</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_method</span><span class="p">(</span><span class="n">_service_name</span><span class="p">,</span> <span class="n">method_name</span><span class="p">,</span> <span class="n">_id</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span><span class="w">
</span><span class="nd">&#64;route</span><span class="p">([</span><span class="w">
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'&lt;string:_service_name&gt;'</span><span class="p">,</span><span class="w">
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'&lt;string:_service_name&gt;/&lt;string:method_name&gt;'</span><span class="p">,</span><span class="w">
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'&lt;string:_service_name&gt;/&lt;int:_id&gt;'</span><span class="p">,</span><span class="w">
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'&lt;string:_service_name&gt;/&lt;int:_id&gt;/&lt;string:method_name&gt;'</span><span class="w">
</span><span class="p">],</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s1">'POST'</span><span class="p">],</span> <span class="n">auth</span><span class="o">=</span><span class="s2">&quot;user&quot;</span><span class="p">,</span> <span class="n">csrf</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span><span class="k">def</span><span class="w"> </span><span class="nf">modify</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">_service_name</span><span class="p">,</span> <span class="n">_id</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">method_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="o">**</span><span class="n">params</span><span class="p">):</span><span class="w">
</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">method_name</span><span class="p">:</span><span class="w">
</span> <span class="n">method_name</span> <span class="o">=</span> <span class="s1">'update'</span> <span class="k">if</span> <span class="n">_id</span> <span class="k">else</span> <span class="s1">'create'</span><span class="w">
</span> <span class="k">if</span> <span class="n">method_name</span> <span class="o">==</span> <span class="s1">'get'</span><span class="p">:</span><span class="w">
</span> <span class="n">_logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">&quot;HTTP POST with method name 'get' is not allowed. &quot;</span><span class="w">
</span> <span class="s2">&quot;(service name: </span><span class="si">%s</span><span class="s2">)&quot;</span><span class="p">,</span> <span class="n">_service_name</span><span class="p">)</span><span class="w">
</span> <span class="k">raise</span> <span class="n">BadRequest</span><span class="p">()</span><span class="w">
</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_method</span><span class="p">(</span><span class="n">_service_name</span><span class="p">,</span> <span class="n">method_name</span><span class="p">,</span> <span class="n">_id</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span><span class="w">
</span><span class="nd">&#64;route</span><span class="p">([</span><span class="w">
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'&lt;string:_service_name&gt;/&lt;int:_id&gt;'</span><span class="p">,</span><span class="w">
</span><span class="p">],</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s1">'PUT'</span><span class="p">],</span> <span class="n">auth</span><span class="o">=</span><span class="s2">&quot;user&quot;</span><span class="p">,</span> <span class="n">csrf</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span><span class="k">def</span><span class="w"> </span><span class="nf">update</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">_service_name</span><span class="p">,</span> <span class="n">_id</span><span class="p">,</span> <span class="o">**</span><span class="n">params</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_method</span><span class="p">(</span><span class="n">_service_name</span><span class="p">,</span> <span class="s1">'update'</span><span class="p">,</span> <span class="n">_id</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span><span class="w">
</span><span class="nd">&#64;route</span><span class="p">([</span><span class="w">
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'&lt;string:_service_name&gt;/&lt;int:_id&gt;'</span><span class="p">,</span><span class="w">
</span><span class="p">],</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s1">'DELETE'</span><span class="p">],</span> <span class="n">auth</span><span class="o">=</span><span class="s2">&quot;user&quot;</span><span class="p">,</span> <span class="n">csrf</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span><span class="k">def</span><span class="w"> </span><span class="nf">delete</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">_service_name</span><span class="p">,</span> <span class="n">_id</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_method</span><span class="p">(</span><span class="n">_service_name</span><span class="p">,</span> <span class="s1">'delete'</span><span class="p">,</span> <span class="n">_id</span><span class="p">)</span>
</pre>
<p>As result an HTTP GET call to <a class="reference external" href="http://my_odoo/my_services_api/ping">http://my_odoo/my_services_api/ping</a> will be
dispatched to the method <tt class="docutils literal">PingService.search</tt></p>
<p>In addition to easily exposing your methods, the module allows you to define
data schemas to which the exchanged data must conform. These schemas are defined
on the basis of <a class="reference external" href="https://docs.python-cerberus.org/en/stable/">Cerberus schemas</a>
and associated to the methods using the
following naming convention. For a method <cite>my_method</cite>:</p>
<ul class="simple">
<li><tt class="docutils literal">def _validator_my_method(self):</tt> will be called to get the schema required to
validate the input parameters.</li>
<li><tt class="docutils literal">def _validator_return_my_method(self):</tt> if defined, will be called to get
the schema used to validate the response.</li>
</ul>
<p>In order to offer even more flexibility, a new API has been developed.</p>
<p>This new API replaces the implicit approach used to expose a service by the use
of a python decorator to explicitly mark a method as being available via the
REST API: <tt class="docutils literal">odoo.addons.base_rest.restapi.method</tt>.</p>
<pre class="code python literal-block">
<span class="k">class</span><span class="w"> </span><span class="nc">PartnerNewApiService</span><span class="p">(</span><span class="n">Component</span><span class="p">):</span><span class="w">
</span> <span class="n">_inherit</span> <span class="o">=</span> <span class="s2">&quot;base.rest.service&quot;</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s2">&quot;partner.new_api.service&quot;</span><span class="w">
</span> <span class="n">_usage</span> <span class="o">=</span> <span class="s2">&quot;partner&quot;</span><span class="w">
</span> <span class="n">_collection</span> <span class="o">=</span> <span class="s2">&quot;base.rest.demo.new_api.services&quot;</span><span class="w">
</span> <span class="n">_description</span> <span class="o">=</span> <span class="s2">&quot;&quot;&quot;
Partner New API Services
Services developed with the new api provided by base_rest
&quot;&quot;&quot;</span><span class="w">
</span> <span class="nd">&#64;restapi</span><span class="o">.</span><span class="n">method</span><span class="p">(</span><span class="w">
</span> <span class="p">[([</span><span class="s2">&quot;/&lt;int:id&gt;/get&quot;</span><span class="p">,</span> <span class="s2">&quot;/&lt;int:id&gt;&quot;</span><span class="p">],</span> <span class="s2">&quot;GET&quot;</span><span class="p">)],</span><span class="w">
</span> <span class="n">output_param</span><span class="o">=</span><span class="n">restapi</span><span class="o">.</span><span class="n">CerberusValidator</span><span class="p">(</span><span class="s2">&quot;_get_partner_schema&quot;</span><span class="p">),</span><span class="w">
</span> <span class="n">auth</span><span class="o">=</span><span class="s2">&quot;public&quot;</span><span class="p">,</span><span class="w">
</span> <span class="p">)</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">_id</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="p">{</span><span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">&quot;res.partner&quot;</span><span class="p">]</span><span class="o">.</span><span class="n">browse</span><span class="p">(</span><span class="n">_id</span><span class="p">)</span><span class="o">.</span><span class="n">name</span><span class="p">}</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">_get_partner_schema</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span> <span class="k">return</span> <span class="p">{</span><span class="w">
</span> <span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&quot;type&quot;</span><span class="p">:</span> <span class="s2">&quot;string&quot;</span><span class="p">,</span> <span class="s2">&quot;required&quot;</span><span class="p">:</span> <span class="kc">True</span><span class="p">}</span><span class="w">
</span> <span class="p">}</span><span class="w">
</span> <span class="nd">&#64;restapi</span><span class="o">.</span><span class="n">method</span><span class="p">(</span><span class="w">
</span> <span class="p">[([</span><span class="s2">&quot;/list&quot;</span><span class="p">,</span> <span class="s2">&quot;/&quot;</span><span class="p">],</span> <span class="s2">&quot;GET&quot;</span><span class="p">)],</span><span class="w">
</span> <span class="n">output_param</span><span class="o">=</span><span class="n">restapi</span><span class="o">.</span><span class="n">CerberusListValidator</span><span class="p">(</span><span class="s2">&quot;_get_partner_schema&quot;</span><span class="p">),</span><span class="w">
</span> <span class="n">auth</span><span class="o">=</span><span class="s2">&quot;public&quot;</span><span class="p">,</span><span class="w">
</span> <span class="p">)</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">list</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span> <span class="n">partners</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">&quot;res.partner&quot;</span><span class="p">]</span><span class="o">.</span><span class="n">search</span><span class="p">([])</span><span class="w">
</span> <span class="k">return</span> <span class="p">[{</span><span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="n">p</span><span class="o">.</span><span class="n">name</span><span class="p">}</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">partners</span><span class="p">]</span>
</pre>
<p>Thanks to this new api, you are now free to specify your own routes but also
to use other object types as parameter or response to your methods.
For example, <cite>base_rest_datamodel</cite> allows you to use Datamodel object instance
into your services.</p>
<pre class="code python literal-block">
<span class="kn">from</span><span class="w"> </span><span class="nn">marshmallow</span><span class="w"> </span><span class="kn">import</span> <span class="n">fields</span><span class="w">
</span><span class="kn">from</span><span class="w"> </span><span class="nn">odoo.addons.base_rest</span><span class="w"> </span><span class="kn">import</span> <span class="n">restapi</span><span class="w">
</span><span class="kn">from</span><span class="w"> </span><span class="nn">odoo.addons.component.core</span><span class="w"> </span><span class="kn">import</span> <span class="n">Component</span><span class="w">
</span><span class="kn">from</span><span class="w"> </span><span class="nn">odoo.addons.datamodel.core</span><span class="w"> </span><span class="kn">import</span> <span class="n">Datamodel</span><span class="w">
</span><span class="k">class</span><span class="w"> </span><span class="nc">PartnerSearchParam</span><span class="p">(</span><span class="n">Datamodel</span><span class="p">):</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s2">&quot;partner.search.param&quot;</span><span class="w">
</span> <span class="nb">id</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">Integer</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span> <span class="n">name</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span><span class="k">class</span><span class="w"> </span><span class="nc">PartnerShortInfo</span><span class="p">(</span><span class="n">Datamodel</span><span class="p">):</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s2">&quot;partner.short.info&quot;</span><span class="w">
</span> <span class="nb">id</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">Integer</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span> <span class="n">name</span> <span class="o">=</span> <span class="n">fields</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">allow_none</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span><span class="k">class</span><span class="w"> </span><span class="nc">PartnerNewApiService</span><span class="p">(</span><span class="n">Component</span><span class="p">):</span><span class="w">
</span> <span class="n">_inherit</span> <span class="o">=</span> <span class="s2">&quot;base.rest.service&quot;</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s2">&quot;partner.new_api.service&quot;</span><span class="w">
</span> <span class="n">_usage</span> <span class="o">=</span> <span class="s2">&quot;partner&quot;</span><span class="w">
</span> <span class="n">_collection</span> <span class="o">=</span> <span class="s2">&quot;base.rest.demo.new_api.services&quot;</span><span class="w">
</span> <span class="n">_description</span> <span class="o">=</span> <span class="s2">&quot;&quot;&quot;
Partner New API Services
Services developed with the new api provided by base_rest
&quot;&quot;&quot;</span><span class="w">
</span> <span class="nd">&#64;restapi</span><span class="o">.</span><span class="n">method</span><span class="p">(</span><span class="w">
</span> <span class="p">[([</span><span class="s2">&quot;/&quot;</span><span class="p">,</span> <span class="s2">&quot;/search&quot;</span><span class="p">],</span> <span class="s2">&quot;GET&quot;</span><span class="p">)],</span><span class="w">
</span> <span class="n">input_param</span><span class="o">=</span><span class="n">restapi</span><span class="o">.</span><span class="n">Datamodel</span><span class="p">(</span><span class="s2">&quot;partner.search.param&quot;</span><span class="p">),</span><span class="w">
</span> <span class="n">output_param</span><span class="o">=</span><span class="n">restapi</span><span class="o">.</span><span class="n">Datamodel</span><span class="p">(</span><span class="s2">&quot;partner.short.info&quot;</span><span class="p">,</span> <span class="n">is_list</span><span class="o">=</span><span class="kc">True</span><span class="p">),</span><span class="w">
</span> <span class="n">auth</span><span class="o">=</span><span class="s2">&quot;public&quot;</span><span class="p">,</span><span class="w">
</span> <span class="p">)</span><span class="w">
</span> <span class="k">def</span><span class="w"> </span><span class="nf">search</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">partner_search_param</span><span class="p">):</span><span class="w">
</span><span class="sd">&quot;&quot;&quot;
Search for partners
:param partner_search_param: An instance of partner.search.param
:return: List of partner.short.info
&quot;&quot;&quot;</span><span class="w">
</span> <span class="n">domain</span> <span class="o">=</span> <span class="p">[]</span><span class="w">
</span> <span class="k">if</span> <span class="n">partner_search_param</span><span class="o">.</span><span class="n">name</span><span class="p">:</span><span class="w">
</span> <span class="n">domain</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="s2">&quot;name&quot;</span><span class="p">,</span> <span class="s2">&quot;like&quot;</span><span class="p">,</span> <span class="n">partner_search_param</span><span class="o">.</span><span class="n">name</span><span class="p">))</span><span class="w">
</span> <span class="k">if</span> <span class="n">partner_search_param</span><span class="o">.</span><span class="n">id</span><span class="p">:</span><span class="w">
</span> <span class="n">domain</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="s2">&quot;id&quot;</span><span class="p">,</span> <span class="s2">&quot;=&quot;</span><span class="p">,</span> <span class="n">partner_search_param</span><span class="o">.</span><span class="n">id</span><span class="p">))</span><span class="w">
</span> <span class="n">res</span> <span class="o">=</span> <span class="p">[]</span><span class="w">
</span> <span class="n">PartnerShortInfo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">datamodels</span><span class="p">[</span><span class="s2">&quot;partner.short.info&quot;</span><span class="p">]</span><span class="w">
</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">&quot;res.partner&quot;</span><span class="p">]</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">domain</span><span class="p">):</span><span class="w">
</span> <span class="n">res</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">PartnerShortInfo</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="n">p</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="n">p</span><span class="o">.</span><span class="n">name</span><span class="p">))</span><span class="w">
</span> <span class="k">return</span> <span class="n">res</span>
</pre>
<p>The BaseRestServiceContextProvider provides context for your services,
including authenticated_partner_id.
You are free to redefine the method _get_authenticated_partner_id() to pass the
authenticated_partner_id based on the authentication mechanism of your choice.
See base_rest_auth_jwt for an example.</p>
<p>In addition, authenticated_partner_id is available in record rule evaluation context.</p>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-3">Known issues / Roadmap</a></h1>
<p>The <a class="reference external" href="https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Abase_rest">roadmap</a>
and <a class="reference external" href="https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Abase_rest">known issues</a> can
be found on GitHub.</p>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-4">Changelog</a></h1>
<div class="section" id="section-1">
<h2><a class="toc-backref" href="#toc-entry-5">16.0.1.0.2 (2023-10-07)</a></h2>
<p><strong>Features</strong></p>
<ul class="simple">
<li>Add support for oauth2 security scheme in the Swagger UI. If your openapi
specification contains a security scheme of type oauth2, the Swagger UI will
display a login button in the top right corner. In order to finalize the
login process, a redirect URL must be provided when initializing the Swagger
UI. The Swagger UI is now initialized with a <cite>oauth2RedirectUrl</cite> option that
references a oauth2-redirect.html file provided by the swagger-ui lib and served
by the current addon. (<a class="reference external" href="https://github.com/OCA/rest-framework/issues/379">#379</a>)</li>
</ul>
</div>
<div class="section" id="section-2">
<h2><a class="toc-backref" href="#toc-entry-6">12.0.2.0.1</a></h2>
<ul class="simple">
<li>_validator_…() methods can now return a cerberus <tt class="docutils literal">Validator</tt> object
instead of a schema dictionnary, for additional flexibility (e.g. allowing
validator options such as <tt class="docutils literal">allow_unknown</tt>).</li>
</ul>
</div>
<div class="section" id="section-3">
<h2><a class="toc-backref" href="#toc-entry-7">12.0.2.0.0</a></h2>
<ul class="simple">
<li>Licence changed from AGPL-3 to LGPL-3</li>
</ul>
</div>
<div class="section" id="section-4">
<h2><a class="toc-backref" href="#toc-entry-8">12.0.1.0.1</a></h2>
<ul class="simple">
<li>Fix issue when rendering the jsonapi documentation if no documentation is
provided on a method part of the REST api.</li>
</ul>
</div>
<div class="section" id="section-5">
<h2><a class="toc-backref" href="#toc-entry-9">12.0.1.0.0</a></h2>
<p>First official version. The addon has been incubated into the
<a class="reference external" href="https://github.com/akretion/odoo-shopinvader">Shopinvader repository</a> from
Akretion. For more information you need to look at the git log.</p>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-10">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/rest-framework/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/rest-framework/issues/new?body=module:%20base_rest%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-11">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-12">Authors</a></h2>
<ul class="simple">
<li>ACSONE SA/NV</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-13">Contributors</a></h2>
<ul class="simple">
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
<li>Sébastien Beau &lt;<a class="reference external" href="mailto:sebastien.beau&#64;akretion.com">sebastien.beau&#64;akretion.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-14">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/rest-framework/tree/16.0/base_rest">OCA/rest-framework</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

View file

@ -0,0 +1,60 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "https://petstore.swagger.io/v2/swagger.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
</html>

View file

@ -0,0 +1,75 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
window.addEventListener('DOMContentLoaded', function () {
run();
});
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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;
},
});
});

View file

@ -0,0 +1,59 @@
/* global SwaggerUIBundle, SwaggerUIStandalonePreset*/
odoo.define("base_rest.swagger_ui", function (require) {
"use strict";
var core = require("web.core");
var SwaggerUI = core.Class.extend({
init: function (selector) {
this.selector = selector;
this.$el = $(this.selector);
},
_swagger_bundle_settings: function () {
const defaults = {
dom_id: this.selector,
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: "StandaloneLayout",
operationsSorter: function (a, b) {
var methodsOrder = [
"get",
"post",
"put",
"delete",
"patch",
"options",
"trace",
];
var result =
methodsOrder.indexOf(a.get("method")) -
methodsOrder.indexOf(b.get("method"));
if (result === 0) {
result = a.get("path").localeCompare(b.get("path"));
}
return result;
},
tagsSorter: "alpha",
onComplete: function () {
if (this.web_btn === undefined) {
this.web_btn = $(
"<a class='fa fa-th-large swg-odoo-web-btn' href='/web' accesskey='h'></a>"
);
$(".topbar").prepend(this.web_btn);
}
},
oauth2RedirectUrl:
window.location.origin +
"/base_rest/static/lib/swagger-ui-3.51.1/oauth2-redirect.html",
};
const config = this.$el.data("settings");
return Object.assign({}, defaults, config);
},
start: function () {
this.ui = SwaggerUIBundle(this._swagger_bundle_settings());
},
});
return SwaggerUI;
});

View file

@ -0,0 +1,17 @@
#swagger-ui {
overflow: scroll;
}
.swg-odoo-web-btn {
font-family: fontAwesome !important;
display: block;
float: left;
padding-top: 8px;
padding-right: 0px;
padding-bottom: 8px;
padding-left: 8px;
}
.col {
width: unset;
}

View file

@ -0,0 +1,6 @@
from . import common
from . import test_cerberus_list_validator
from . import test_cerberus_validator
from . import test_controller_builder
from . import test_openapi_generator
from . import test_service_context_provider

View file

@ -0,0 +1,263 @@
# Copyright 2017 Akretion (http://www.akretion.com).
# Copyright 2020 ACSONE SA/NV
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# @author Laurent Mignon <laurent.mignon@acsone.eu>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import copy
from odoo import http
from odoo.tests.common import TransactionCase, get_db_name
from odoo.addons.component.core import (
WorkContext,
_component_databases,
_get_addon_name,
)
from odoo.addons.component.tests.common import (
ComponentRegistryCase,
TransactionComponentCase,
new_rollbacked_env,
)
from ..controllers.main import RestController, _PseudoCollection
from ..core import (
RestServicesRegistry,
_rest_controllers_per_module,
_rest_services_databases,
)
from ..tools import ROUTING_DECORATOR_ATTR, _inspect_methods
class RegistryMixin(object):
@classmethod
def setUpRegistry(cls):
with new_rollbacked_env() as env:
service_registration = env["rest.service.registration"]
# build the registry of every installed addons
services_registry = service_registration._init_global_registry()
cls._services_registry = services_registry
# ensure that we load only the services of the 'installed'
# modules, not 'to install', which means we load only the
# dependencies of the tested addons, not the siblings or
# children addons
service_registration.build_registry(
services_registry, states=("installed",)
)
# build the services of the current tested addon
current_addon = _get_addon_name(cls.__module__)
service_registration.load_services(current_addon, services_registry)
env["rest.service.registration"]._build_controllers_routes(
services_registry
)
class RestServiceRegistryCase(ComponentRegistryCase):
# pylint: disable=W8106
@staticmethod
def _setup_registry(class_or_instance):
ComponentRegistryCase._setup_registry(class_or_instance)
class_or_instance._service_registry = RestServicesRegistry()
# take a copy of registered controllers
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__)]
)
db_name = get_db_name()
# makes the test component registry available for the db name
class_or_instance._original_component_databases = _component_databases.get(
db_name
)
_component_databases[db_name] = class_or_instance.comp_registry
# makes the test service registry available for the db name
class_or_instance._original_services_registry = _rest_services_databases.get(
db_name, {}
)
_rest_services_databases[db_name] = class_or_instance._service_registry
# build the services and controller of every installed addons
# but the current addon (when running with pytest/nosetest, we
# simulate the --test-enable behavior by excluding the current addon
# which is in 'to install' / 'to upgrade' with --test-enable).
current_addon = _get_addon_name(class_or_instance.__module__)
with new_rollbacked_env() as env:
RestServiceRegistration = env["rest.service.registration"]
RestServiceRegistration.build_registry(
class_or_instance._service_registry,
states=("installed",),
exclude_addons=[current_addon],
)
RestServiceRegistration._build_controllers_routes(
class_or_instance._service_registry
)
# register our components
class_or_instance.comp_registry.load_components("base_rest")
# Define a base test controller here to avoid to have this controller
# registered outside tests
class_or_instance._collection_name = "base.rest.test"
BaseTestController = class_or_instance._get_test_controller(class_or_instance)
class_or_instance._BaseTestController = BaseTestController
class_or_instance._controller_route_method_names = {
"my_controller_route_without",
"my_controller_route_with",
"my_controller_route_without_auth_2",
}
@staticmethod
def _get_test_controller(class_or_instance, root_path="/test_controller/"):
class BaseTestController(RestController):
_root_path = root_path
_collection_name = class_or_instance._collection_name
_default_auth = "public"
@http.route("/my_controller_route_without")
def my_controller_route_without(self):
return {}
@http.route(
"/my_controller_route_with",
auth="public",
cors="http://with_cors",
csrf="False",
save_session="False",
)
def my_controller_route_with(self):
return {}
@http.route("/my_controller_route_without_auth_2", auth=None)
def my_controller_route_without_auth_2(self):
return {}
return BaseTestController
@staticmethod
def _teardown_registry(class_or_instance):
ComponentRegistryCase._teardown_registry(class_or_instance)
http.Controller.children_classes = (
class_or_instance._controller_children_classes
)
db_name = get_db_name()
_component_databases[db_name] = class_or_instance._original_component_databases
_rest_services_databases[
db_name
] = class_or_instance._original_services_registry
class_or_instance._service_registry = {}
_rest_controllers_per_module[
_get_addon_name(class_or_instance.__module__)
] = class_or_instance._original_addon_rest_controllers_per_module
@staticmethod
def _build_services(class_or_instance, *classes):
class_or_instance._build_components(*classes)
with new_rollbacked_env() as env:
RestServiceRegistration = env["rest.service.registration"]
current_addon = _get_addon_name(class_or_instance.__module__)
RestServiceRegistration.load_services(
current_addon, class_or_instance._service_registry
)
RestServiceRegistration._build_controllers_routes(
class_or_instance._service_registry
)
@staticmethod
def _get_controller_for(service, addon="base_rest"):
identifier = "{}_{}_{}".format(
get_db_name(),
service._collection.replace(".", "_"),
service._usage.replace(".", "_"),
)
controllers = [
controller
for controller in http.Controller.children_classes.get(addon, [])
if getattr(controller, "_identifier", None) == identifier
]
if not controllers:
return
return controllers[-1]
@staticmethod
def _get_controller_route_methods(controller):
methods = {}
for name, method in _inspect_methods(controller):
if hasattr(method, ROUTING_DECORATOR_ATTR):
methods[name] = method
return methods
@staticmethod
def _get_service_component(class_or_instance, usage, collection=None):
collection = collection or _PseudoCollection(
class_or_instance._collection_name, class_or_instance.env
)
work = WorkContext(
model_name="rest.service.registration",
collection=collection,
components_registry=class_or_instance.comp_registry,
)
return work.component(usage=usage)
class TransactionRestServiceRegistryCase(TransactionCase, RestServiceRegistryCase):
"""Adds Odoo Transaction to inherited from ComponentRegistryCase.
This class doesn't set up the registry for you.
You're supposed to explicitly call `_setup_registry` and `_teardown_registry`
when you need it, either on setUpClass and tearDownClass or setUp and tearDown.
class MyTestCase(TransactionRestServiceRegistryCase):
def setUp(self):
super().setUp()
self._setup_registry(self)
def tearDown(self):
self._teardown_registry(self)
super().tearDown()
class MyTestCase(TransactionRestServiceRegistryCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._setup_registry(cls)
@classmethod
def tearDownClass(cls):
cls._teardown_registry(cls)
super().tearDownClass()
"""
# pylint: disable=W8106
@classmethod
def setUpClass(cls):
# resolve an inheritance issue (common.TransactionCase does not use
# super)
TransactionCase.setUpClass()
cls.base_url = cls.env["ir.config_parameter"].get_param("web.base.url")
@classmethod
def tearDownClass(cls):
TransactionCase.tearDownClass()
class BaseRestCase(TransactionComponentCase, RegistryMixin):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.setUpRegistry()
cls.base_url = cls.env["ir.config_parameter"].get_param("web.base.url")
cls.registry.enter_test_mode(cls.env.cr)
# pylint: disable=W8110
@classmethod
def tearDownClass(cls):
cls.registry.leave_test_mode()
super().tearDownClass()

View file

@ -0,0 +1,247 @@
# Copyright 2020 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import unittest
from cerberus import Validator
from odoo.exceptions import UserError
from odoo.tests.common import BaseCase, MetaCase
from ..components.cerberus_validator import BaseRestCerberusValidator
from ..restapi import CerberusListValidator
class TestCerberusListValidator(BaseCase, MetaCase("DummyCase", (object,), {})):
"""Test all the methods that must be implemented by CerberusListValidator to
be a valid RestMethodParam"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.simple_schema = {
"name": {"type": "string", "required": True, "nullable": True},
"title": {
"type": "string",
"nullable": False,
"required": False,
"allowed": ["mr", "mm"],
},
}
cls.nested_schema = {
"name": {"type": "string", "required": True, "empty": False},
"country": {
"type": "dict",
"schema": {
"id": {"type": "integer", "required": True, "nullable": False},
"name": {"type": "string"},
},
},
}
cls.simple_schema_list_validator = CerberusListValidator(
schema=cls.simple_schema, min_items=1, max_items=2, unique_items=True
)
cls.nested_schema_list_validator = CerberusListValidator(
schema=cls.nested_schema
)
cls.maxDiff = None
def test_to_openapi_responses(self):
res = self.simple_schema_list_validator.to_openapi_responses(None, None)
self.assertDictEqual(
res,
{
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"nullable": True, "type": "string"},
"title": {
"enum": ["mr", "mm"],
"nullable": False,
"type": "string",
},
},
},
"maxItems": 2,
"minItems": 1,
"type": "array",
"uniqueItems": True,
}
}
}
}
},
)
res = self.nested_schema_list_validator.to_openapi_responses(None, None)
self.assertDictEqual(
res,
{
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"country": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"nullable": False,
"type": "integer",
},
"name": {"type": "string"},
},
},
},
},
"type": "array",
}
}
}
}
},
)
def test_to_openapi_requestbody(self):
res = self.simple_schema_list_validator.to_openapi_requestbody(None, None)
self.assertEqual(
res,
{
"content": {
"application/json": {
"schema": {
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"nullable": True, "type": "string"},
"title": {
"enum": ["mr", "mm"],
"nullable": False,
"type": "string",
},
},
},
"maxItems": 2,
"minItems": 1,
"type": "array",
"uniqueItems": True,
}
}
}
},
)
res = self.nested_schema_list_validator.to_openapi_requestbody(None, None)
self.assertDictEqual(
res,
{
"content": {
"application/json": {
"schema": {
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"country": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"nullable": False,
"type": "integer",
},
"name": {"type": "string"},
},
},
},
},
"type": "array",
}
}
}
},
)
def test_to_openapi_query_parameters(self):
with self.assertRaises(NotImplementedError):
self.simple_schema_list_validator.to_openapi_query_parameters(None, None)
def test_from_params_ignore_unknown(self):
params = [{"name": "test", "unknown": True}]
res = self.simple_schema_list_validator.from_params(None, params=params)
self.assertListEqual(res, [{"name": "test"}])
def test_from_params_validation(self):
# minItems / maxItems
with self.assertRaises(UserError):
# minItems = 1
self.simple_schema_list_validator.from_params(None, params=[])
with self.assertRaises(UserError):
# maxItems = 2
self.simple_schema_list_validator.from_params(
None, params=[{"name": "test"}, {"name": "test"}, {"name": "test"}]
)
with self.assertRaises(UserError):
# name required
self.simple_schema_list_validator.from_params(None, params=[{}])
def test_to_response_ignore_unknown(self):
result = [{"name": "test", "unknown": True}]
res = self.simple_schema_list_validator.to_response(None, result=result)
self.assertListEqual(res, [{"name": "test"}])
def test_to_response_validation(self):
# If a response is not conform to the expected schema it's considered
# as a programmatic error not a user error
with self.assertRaises(SystemError):
# minItems = 1
self.simple_schema_list_validator.to_response(None, result=[])
with self.assertRaises(SystemError):
# maxItems = 2
self.simple_schema_list_validator.to_response(
None, result=[{"name": "test"}, {"name": "test"}, {"name": "test"}]
)
with self.assertRaises(SystemError):
# name required
self.simple_schema_list_validator.to_response(None, result=[{}])
def test_schema_lookup_from_string(self):
class MyService(object):
def _get_simple_schema(self):
return {"name": {"type": "string", "required": True, "nullable": True}}
def component(self, *args, **kwargs):
return BaseRestCerberusValidator(unittest.mock.Mock())
v = CerberusListValidator(schema="_get_simple_schema")
validator = v.get_cerberus_validator(MyService(), "output")
self.assertTrue(validator)
self.assertDictEqual(
validator.root_schema.schema,
{"name": {"type": "string", "required": True, "nullable": True}},
)
def test_schema_lookup_from_string_custom_validator(self):
class MyService(object):
def _get_simple_schema(self):
return Validator(
{"name": {"type": "string", "required": False}}, require_all=True
)
def component(self, *args, **kwargs):
return BaseRestCerberusValidator(unittest.mock.Mock())
v = CerberusListValidator(schema="_get_simple_schema")
validator = v.get_cerberus_validator(MyService(), "input")
self.assertTrue(validator.require_all)

View file

@ -0,0 +1,449 @@
# Copyright 2020 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import unittest
from cerberus import Validator
from odoo.exceptions import UserError
from odoo.tests.common import BaseCase, MetaCase
from ..components.cerberus_validator import BaseRestCerberusValidator
from ..restapi import CerberusValidator
from ..tools import cerberus_to_json
class TestCerberusValidator(BaseCase, MetaCase("DummyCase", (object,), {})):
"""Test all the methods that must be implemented by CerberusValidator to
be a valid RestMethodParam"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.simple_schema = {
"name": {"type": "string", "required": True, "nullable": True},
"title": {
"type": "string",
"nullable": False,
"required": False,
"allowed": ["mr", "mm"],
},
"age": {"type": "integer", "default": 18},
"interests": {"type": "list", "schema": {"type": "string"}},
}
cls.nested_schema = {
"name": {"type": "string", "required": True, "empty": False},
"country": {
"type": "dict",
"schema": {
"id": {"type": "integer", "required": True, "nullable": False},
"name": {"type": "string"},
},
},
"is_company": {"type": "boolean"},
}
cls.simple_schema_cerberus_validator = CerberusValidator(
schema=cls.simple_schema
)
cls.nested_schema_cerberus_validator = CerberusValidator(
schema=cls.nested_schema
)
def test_to_openapi_responses(self):
res = self.simple_schema_cerberus_validator.to_openapi_responses(None, None)
self.assertDictEqual(
res,
{
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"nullable": True, "type": "string"},
"title": {
"enum": ["mr", "mm"],
"nullable": False,
"type": "string",
},
"age": {"default": 18, "type": "integer"},
"interests": {
"type": "array",
"items": {"type": "string"},
},
},
}
}
}
}
},
)
res = self.nested_schema_cerberus_validator.to_openapi_responses(None, None)
self.assertDictEqual(
res,
{
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"country": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"nullable": False,
"type": "integer",
},
"name": {"type": "string"},
},
},
"is_company": {"type": "boolean"},
},
}
}
}
}
},
)
def test_to_openapi_requestbody(self):
res = self.simple_schema_cerberus_validator.to_openapi_requestbody(None, None)
self.assertEqual(
res,
{
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"nullable": True, "type": "string"},
"title": {
"enum": ["mr", "mm"],
"nullable": False,
"type": "string",
},
"age": {"default": 18, "type": "integer"},
"interests": {
"type": "array",
"items": {"type": "string"},
},
},
}
}
}
},
)
res = self.nested_schema_cerberus_validator.to_openapi_requestbody(None, None)
self.assertDictEqual(
res,
{
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"country": {
"type": "object",
"required": ["id"],
"properties": {
"id": {"nullable": False, "type": "integer"},
"name": {"type": "string"},
},
},
"is_company": {"type": "boolean"},
},
}
}
}
},
)
def test_to_openapi_query_parameters(self):
res = self.simple_schema_cerberus_validator.to_openapi_query_parameters(
None, None
)
self.assertListEqual(
res,
[
{
"name": "name",
"in": "query",
"required": True,
"allowEmptyValue": True,
"default": None,
"schema": {"type": "string"},
},
{
"name": "title",
"in": "query",
"required": False,
"allowEmptyValue": False,
"default": None,
"schema": {"type": "string", "enum": ["mr", "mm"]},
},
{
"name": "age",
"in": "query",
"required": False,
"allowEmptyValue": False,
"default": 18,
"schema": {"type": "integer"},
},
{
"name": "interests[]",
"in": "query",
"required": False,
"allowEmptyValue": False,
"default": None,
"schema": {"type": "array", "items": {"type": "string"}},
},
],
)
res = self.nested_schema_cerberus_validator.to_openapi_query_parameters(
None, None
)
self.assertListEqual(
res,
[
{
"name": "name",
"in": "query",
"required": True,
"allowEmptyValue": False,
"default": None,
"schema": {"type": "string"},
},
{
"name": "country",
"in": "query",
"required": False,
"allowEmptyValue": False,
"default": None,
"schema": {"type": "object"},
},
{
"name": "is_company",
"in": "query",
"required": False,
"allowEmptyValue": False,
"default": None,
"schema": {"type": "boolean"},
},
],
)
def test_from_params_add_default(self):
params = {"name": "test"}
res = self.simple_schema_cerberus_validator.from_params(None, params=params)
self.assertDictEqual(res, {"name": "test", "age": 18})
def test_from_params_ignore_unknown(self):
params = {"name": "test", "unknown": True}
res = self.simple_schema_cerberus_validator.from_params(None, params=params)
self.assertDictEqual(res, {"name": "test", "age": 18})
def test_from_params_validation(self):
# name is required
with self.assertRaises(UserError):
self.simple_schema_cerberus_validator.from_params(None, params={})
def test_to_response_add_default(self):
result = {"name": "test"}
res = self.simple_schema_cerberus_validator.to_response(None, result=result)
self.assertDictEqual(res, {"name": "test", "age": 18})
def test_to_response_ignore_unknown(self):
result = {"name": "test", "unknown": True}
res = self.simple_schema_cerberus_validator.to_response(None, result=result)
self.assertDictEqual(res, {"name": "test", "age": 18})
def test_to_response_validation(self):
# name is required
# If a response is not conform to the expected schema it's considered
# as a programmatic error not a user error
with self.assertRaises(SystemError):
self.simple_schema_cerberus_validator.to_response(None, result={})
def test_schema_lookup_from_string(self):
class MyService(object):
def _get_simple_schema(self):
return {"name": {"type": "string", "required": True, "nullable": True}}
def component(self, *args, **kwargs):
return BaseRestCerberusValidator(unittest.mock.Mock())
v = CerberusValidator(schema="_get_simple_schema")
validator = v.get_cerberus_validator(MyService(), "output")
self.assertTrue(validator)
self.assertDictEqual(
validator.root_schema.schema,
{"name": {"type": "string", "required": True, "nullable": True}},
)
def test_schema_lookup_from_string_custom_validator(self):
class MyService(object):
def _get_simple_schema(self):
return Validator(
{"name": {"type": "string", "required": False}}, require_all=True
)
def component(self, *args, **kwargs):
return BaseRestCerberusValidator(unittest.mock.Mock())
v = CerberusValidator(schema="_get_simple_schema")
validator = v.get_cerberus_validator(MyService(), "input")
self.assertTrue(validator.require_all)
def test_custom_validator_handler(self):
assertEq = self.assertEqual
class CustomBaseRestCerberusValidator(BaseRestCerberusValidator):
def get_validator_handler(self, service, method_name, direction):
# In your implementation, this is where you can handle how the
# validator is retrieved / computed (dispatch to dedicated
# components...).
assertEq(service, my_service)
assertEq(method_name, "my_endpoint")
assertEq(direction, "input")
# A callable with no parameter is expected.
return lambda: Validator(
{"name": {"type": "string", "required": False}}, require_all=True
)
def has_validator_handler(self, service, method_name, direction):
return True
class MyService(object):
def component(self, *args, **kwargs):
return CustomBaseRestCerberusValidator(unittest.mock.Mock())
my_service = MyService()
v = CerberusValidator(schema="my_endpoint")
validator = v.get_cerberus_validator(my_service, "input")
self.assertTrue(validator.require_all)
def test_cerberus_key_value_mapping_to_openapi(self):
schema = {
"indexes": {
"type": "dict",
"required": True,
"nullable": True,
"keysrules": {"type": "string"},
"valuesrules": {"type": "string"},
}
}
openapi = cerberus_to_json(schema)
self.assertDictEqual(
openapi,
{
"type": "object",
"required": ["indexes"],
"properties": {
"indexes": {
"nullable": True,
"type": "object",
"additionalProperties": {"type": "string"},
}
},
},
)
schema = {
"indexes": {
"type": "dict",
"required": True,
"nullable": True,
"keysrules": {"type": "string"},
"valuesrules": {
"type": "dict",
"schema": {
"id": {
"type": "integer",
"required": True,
"nullable": False,
},
"name": {"type": "string"},
},
},
}
}
openapi = cerberus_to_json(schema)
self.assertDictEqual(
openapi,
{
"type": "object",
"required": ["indexes"],
"properties": {
"indexes": {
"nullable": True,
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"nullable": False,
"type": "integer",
},
"name": {"type": "string"},
},
},
}
},
},
)
def test_cerberus_meta_to_openapi(self):
schema = {
"indexes": {
"type": "dict",
"meta": {
"description": "A key/value mapping where Key is the model "
"name used to fill the index and value is the "
"index name",
"example": {
"shopinvader.category": "demo_elasticsearch_backend_"
"shopinvader_category_en_US",
"shopinvader.variant": "demo_elasticsearch_backend_"
"shopinvader_variant_en_US",
},
},
"required": True,
"nullable": True,
"keysrules": {"type": "string"},
"valuesrules": {"type": "string"},
}
}
openapi = cerberus_to_json(schema)
self.assertDictEqual(
openapi,
{
"type": "object",
"required": ["indexes"],
"properties": {
"indexes": {
"description": "A key/value mapping where Key is the model "
"name used to fill the index and value is "
"the index name",
"example": {
"shopinvader.category": "demo_elasticsearch_backend_"
"shopinvader_category_en_US",
"shopinvader.variant": "demo_elasticsearch_backend_"
"shopinvader_variant_en_US",
},
"nullable": True,
"type": "object",
"additionalProperties": {"type": "string"},
}
},
},
)

View file

@ -0,0 +1,671 @@
# Copyright 2020 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from contextlib import contextmanager
from odoo.addons.component.core import Component
from .. import restapi
from ..tools import ROUTING_DECORATOR_ATTR
from .common import TransactionRestServiceRegistryCase
class TestControllerBuilder(TransactionRestServiceRegistryCase):
"""Test Odoo controller builder
In this class we test the generation of odoo controllers from the services
component
"""
def setUp(self):
super().setUp()
self._setup_registry(self)
def tearDown(self):
self._teardown_registry(self)
super().tearDown()
def test_01(self):
"""Test controller generated for old API services
In this test we check that the controller generated for services with
methods not decorated with the restapi.method decorator contains
the required method to route requests to services. In the original
implementation, these routes where hardcoded into the base controller.
"""
# pylint: disable=R7980
class TestServiceOldApi(Component):
_inherit = "base.rest.service"
_name = "test.ping.service"
_usage = "ping"
_collection = self._collection_name
_description = "test"
def get(self, _id, message):
pass
def search(self, message):
return {"response": "Search called search with message " + message}
def update(self, _id, message):
return {"response": "PUT called with message " + message}
# pylint:disable=method-required-super
def create(self, **params):
return {"response": "POST called with message " + params["message"]}
def delete(self, _id):
return {"response": "DELETE called with id %s " % _id}
def my_method(self, **params):
pass
def my_instance_method(self, _id, **params):
pass
# Validator
def _validator_search(self):
return {"message": {"type": "string"}}
# Validator
def _validator_get(self):
# no parameters by default
return {}
def _validator_update(self):
return {"message": {"type": "string"}}
def _validator_create(self):
return {"message": {"type": "string"}}
def _validator_my_method(self):
return {"message": {"type": "string"}}
def _validator_my_instance_method(self):
return {"message": {"type": "string"}}
self.assertFalse(self._get_controller_for(TestServiceOldApi))
self._build_services(self, TestServiceOldApi)
controller = self._get_controller_for(TestServiceOldApi)
routes = self._get_controller_route_methods(controller)
self.assertSetEqual(
set(routes.keys()),
{
"get_get",
"get_search",
"post_update",
"put_update",
"post_create",
"post_delete",
"delete_delete",
"post_my_method",
"post_my_instance_method",
}
| self._controller_route_method_names,
)
self.assertTrue(controller)
# the generated method_name is always the {http_method}_{method_name}
method = routes["get_get"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": [
"/test_controller/ping/<int:id>/get",
"/test_controller/ping/<int:id>",
],
"save_session": True,
"type": "restapi",
},
)
method = routes["get_search"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": ["/test_controller/ping/search", "/test_controller/ping/"],
"save_session": True,
"type": "restapi",
},
)
method = routes["post_update"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": [
"/test_controller/ping/<int:id>/update",
"/test_controller/ping/<int:id>",
],
"save_session": True,
"type": "restapi",
},
)
method = routes["put_update"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["PUT"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": ["/test_controller/ping/<int:id>"],
"save_session": True,
"type": "restapi",
},
)
method = routes["post_create"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": ["/test_controller/ping/create", "/test_controller/ping/"],
"save_session": True,
"type": "restapi",
},
)
method = routes["post_delete"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": ["/test_controller/ping/<int:id>/delete"],
"save_session": True,
"type": "restapi",
},
)
method = routes["delete_delete"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["DELETE"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": ["/test_controller/ping/<int:id>"],
"save_session": True,
"type": "restapi",
},
)
method = routes["post_my_method"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": ["/test_controller/ping/my_method"],
"save_session": True,
"type": "restapi",
},
)
method = routes["post_my_instance_method"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": ["/test_controller/ping/<int:id>/my_instance_method"],
"save_session": True,
"type": "restapi",
},
)
def test_02(self):
"""Test controller generated from services with new API methods
In this case we check that the generated controller for a service
where the methods are decorated with restapi.method contains the
required method to route the requests to the methods
"""
# pylint: disable=R7980
class TestServiceNewApi(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "test"
@restapi.method(
[(["/<int:id>/get", "/<int:id>"], "GET")],
output_param=restapi.CerberusValidator("_get_partner_schema"),
auth="public",
)
def get(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
@restapi.method(
[(["/<int:id>/get_name"], "GET")],
output_param=restapi.CerberusValidator("_get_partner_schema"),
auth="public",
)
def get_name(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
@restapi.method(
[(["/<int:id>/change_name"], "POST")],
input_param=restapi.CerberusValidator("_get_partner_schema"),
auth="user",
)
def update_name(self, _id, **params):
pass
def _get_partner_schema(self):
return {"name": {"type": "string", "required": True}}
self.assertFalse(self._get_controller_for(TestServiceNewApi))
self._build_services(self, TestServiceNewApi)
controller = self._get_controller_for(TestServiceNewApi)
routes = self._get_controller_route_methods(controller)
self.assertSetEqual(
set(routes.keys()),
{"get_get", "get_get_name", "post_update_name"}
| self._controller_route_method_names,
)
method = routes["get_get"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": [
"/test_controller/partner/<int:id>/get",
"/test_controller/partner/<int:id>",
],
"save_session": True,
"type": "restapi",
},
)
method = routes["get_get_name"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": ["/test_controller/partner/<int:id>/get_name"],
"save_session": True,
"type": "restapi",
},
)
method = routes["post_update_name"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "user",
"cors": None,
"csrf": False,
"routes": ["/test_controller/partner/<int:id>/change_name"],
"save_session": True,
"type": "restapi",
},
)
def test_03(self):
"""Check that the controller builder takes care of services inheritance"""
# pylint: disable=R7980
class TestPartnerService(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "test"
@restapi.method(
[(["/<int:id>/get", "/<int:id>"], "GET")],
output_param=restapi.CerberusValidator("_get_partner_schema"),
auth="public",
)
def get(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
def _get_partner_schema(self):
return {"name": {"type": "string", "required": True}}
class TestInheritPartnerService(Component):
_inherit = "test.partner.service"
@restapi.method(
[(["/<int:id>/get_name"], "GET")],
output_param=restapi.CerberusValidator("_get_partner_schema"),
auth="public",
)
def get_name(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
@restapi.method(
[(["/<int:id>/change_name"], "POST")],
input_param=restapi.CerberusValidator("_get_partner_schema"),
auth="user",
)
def update_name(self, _id, **params):
pass
self.assertFalse(self._get_controller_for(TestPartnerService))
self._build_services(self, TestPartnerService, TestInheritPartnerService)
controller = self._get_controller_for(TestPartnerService)
routes = self._get_controller_route_methods(controller)
self.assertSetEqual(
set(routes.keys()),
{"get_get", "get_get_name", "post_update_name"}
| self._controller_route_method_names,
)
method = routes["get_get"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": [
"/test_controller/partner/<int:id>/get",
"/test_controller/partner/<int:id>",
],
"save_session": True,
"type": "restapi",
},
)
method = routes["get_get_name"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["GET"],
"auth": "public",
"cors": None,
"csrf": False,
"routes": ["/test_controller/partner/<int:id>/get_name"],
"save_session": True,
"type": "restapi",
},
)
method = routes["post_update_name"]
self.assertDictEqual(
getattr(method, ROUTING_DECORATOR_ATTR),
{
"methods": ["POST"],
"auth": "user",
"cors": None,
"csrf": False,
"routes": ["/test_controller/partner/<int:id>/change_name"],
"save_session": True,
"type": "restapi",
},
)
class TestControllerBuilder2(TransactionRestServiceRegistryCase):
"""Test Odoo controller builder
In this class we test the generation of odoo controllers from the services
component
The test requires a fresh base controller
"""
def setUp(self):
super().setUp()
self._setup_registry(self)
def tearDown(self):
self._teardown_registry(self)
super().tearDown()
def test_04(self):
"""Test controller generated from services with new API methods and
old api takes into account the _default_auth
Routes directly defined on the RestConroller without auth should also
use the _default_auth
"""
default_auth = "my_default_auth"
default_cors = "*"
default_csrf = True
default_save_session = True
self._BaseTestController._default_auth = default_auth
self._BaseTestController._default_cors = default_cors
self._BaseTestController._default_csrf = default_csrf
self._BaseTestController._default_save_session = default_save_session
# pylint: disable=R7980
class TestService(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "test"
@restapi.method(
[(["/new_api_method_with"], "GET")],
auth="public",
cors="http://my_site",
csrf=not default_csrf,
save_session=not default_save_session,
)
def new_api_method_with(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
@restapi.method([(["/new_api_method_without"], "GET")])
def new_api_method_without(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
# OLD API method
def get(self, _id, message):
pass
# Validator
def _validator_get(self):
# no parameters by default
return {}
self._build_services(self, TestService)
controller = self._get_controller_for(TestService)
routes = self._get_controller_route_methods(controller)
for attr, default in [
("auth", default_auth),
("cors", default_cors),
("csrf", default_csrf),
("save_session", default_save_session),
]:
self.assertEqual(
getattr(routes["get_new_api_method_without"], ROUTING_DECORATOR_ATTR)[
attr
],
default,
"wrong %s" % attr,
)
self.assertEqual(
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(
getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)["csrf"],
not default_csrf,
)
self.assertEqual(
getattr(routes["get_new_api_method_with"], ROUTING_DECORATOR_ATTR)[
"save_session"
],
not default_save_session,
)
self.assertEqual(
getattr(routes["get_get"], ROUTING_DECORATOR_ATTR)["auth"],
default_auth,
"wrong auth for get_get",
)
for attr, default in [
("auth", default_auth),
("cors", default_cors),
("csrf", default_csrf),
("save_session", default_save_session),
]:
self.assertEqual(
getattr(routes["my_controller_route_without"], ROUTING_DECORATOR_ATTR)[
attr
],
default,
"wrong %s" % attr,
)
routing = getattr(routes["my_controller_route_with"], ROUTING_DECORATOR_ATTR)
for attr, value in [
("auth", "public"),
("cors", "http://with_cors"),
("csrf", "False"),
("save_session", "False"),
]:
self.assertEqual(
routing[attr],
value,
"wrong %s" % attr,
)
self.assertEqual(
getattr(
routes["my_controller_route_without_auth_2"], ROUTING_DECORATOR_ATTR
)["auth"],
None,
"wrong auth for my_controller_route_without_auth_2",
)
def test_05(self):
"""Test auth="public_or_default" on restapi.method
The auth method on the route should be public_or_my_default_auth
since the ir.http model provides the _auth_method_public_or_my_default_auth methods
"""
default_auth = "my_default_auth"
self._BaseTestController._default_auth = default_auth
# pylint: disable=R7980
class TestService(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "test"
@restapi.method(
[(["/new_api_method_with_public_or"], "GET")], auth="public_or_default"
)
def new_api_method_with_public_or(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
# Validator
def _validator_get(self):
# no parameters by default
return {}
# delare the auth méthod on ir.http
with _add_method(
self.env["ir.http"],
"_auth_method_public_or_my_default_auth",
lambda a: True,
):
self._build_services(self, TestService)
controller = self._get_controller_for(TestService)
routes = self._get_controller_route_methods(controller)
self.assertEqual(
getattr(
routes["get_new_api_method_with_public_or"], ROUTING_DECORATOR_ATTR
)["auth"],
"public_or_my_default_auth",
)
def test_06(self):
"""Test auth="public_or_default" on restapi.method
The auth method on the route should be the default_auth configurerd on the controller
since the ir.http model doesn't provides the _auth_method_public_or_my_default_auth
methods
"""
default_auth = "my_default_auth"
self._BaseTestController._default_auth = default_auth
# pylint: disable=R7980
class TestService(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "test"
@restapi.method(
[(["/new_api_method_with_public_or"], "GET")], auth="public_or_default"
)
def new_api_method_with_public_or(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
# Validator
def _validator_get(self):
# no parameters by default
return {}
self._build_services(self, TestService)
controller = self._get_controller_for(TestService)
routes = self._get_controller_route_methods(controller)
self.assertEqual(
getattr(
routes["get_new_api_method_with_public_or"], ROUTING_DECORATOR_ATTR
)["auth"],
"my_default_auth",
)
@contextmanager
def _add_method(obj, name, method):
try:
setattr(obj.__class__, name, method)
yield
finally:
delattr(obj.__class__, name)

View file

@ -0,0 +1,338 @@
# Copyright 2020 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.component.core import Component
from .. import restapi
from .common import TransactionRestServiceRegistryCase
class TestOpenAPIGenerator(TransactionRestServiceRegistryCase):
"""Test openapi document generation from REST services"""
def setUp(self):
super().setUp()
self._setup_registry(self)
def tearDown(self):
self._teardown_registry(self)
super().tearDown()
def test_01(self):
"""Simple test case"""
# pylint: disable=R7980
class PartnerService(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "Sercice description"
@restapi.method(
[(["/<int:id>/get", "/<int:id>"], "GET")],
output_param=restapi.CerberusValidator("_get_partner_schema"),
auth="public",
)
def get(self, _id):
"""Get the partner information"""
def _get_partner_schema(self):
return {"name": {"type": "string", "required": True}}
self._build_services(self, PartnerService)
service = self._get_service_component(self, "partner")
openapi = service.to_openapi()
self.assertTrue(openapi)
# The service url is available at base_url/controller._root_path/_usage
url = openapi["servers"][0]["url"]
self.assertEqual(self.base_url + "/test_controller/partner", url)
# The title is generated from the service usage
# The service info must contains a title and a description
info = openapi["info"]
self.assertEqual(info["title"], "%s REST services" % PartnerService._usage)
self.assertEqual(info["description"], PartnerService._description)
paths = openapi["paths"]
# The paths must contains 2 entries (1 by routes)
self.assertSetEqual({"/{id}/get", "/{id}"}, set(openapi["paths"]))
for p in ["/{id}/get", "/{id}"]:
path = paths[p]
# The method for the paths is get
self.assertIn("get", path)
# The summary is the method docstring
get = path["get"]
self.assertEqual(get["summary"], "Get the partner information")
# The reponse for status 200 is the openapi schema generated from
# the cerberus schema returned by the _get_partner_schema method
resp = None
for item in get["responses"].items():
if item[0] == "200":
resp = item[1]
self.assertTrue(resp)
self.assertDictEqual(
resp,
{
"content": {
"application/json": {
"schema": {
"properties": {"name": {"type": "string"}},
"required": ["name"],
"type": "object",
}
}
}
},
)
# The path contains parameters
self.assertDictEqual(
path.get("parameters", [{}])[0],
{
"in": "path",
"name": "id",
"required": True,
"schema": {"type": "integer", "format": "int32"},
},
)
def test_02(self):
"""Test path parameters
The new api allows you to define paths parameters. In this test
we check that these parameters are into the openapi specification
"""
# pylint: disable=R7980
class PartnerService(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "Sercice description"
@restapi.method(
[(["/<int:id>/update_name/<string:name>"], "POST")], auth="public"
)
def update_name(self, _id, _name):
"""update_name"""
self._build_services(self, PartnerService)
service = self._get_service_component(self, "partner")
openapi = service.to_openapi()
self.assertTrue(openapi)
paths = openapi["paths"]
self.assertIn("/{id}/update_name/{name}", paths)
path = paths["/{id}/update_name/{name}"]
self.assertIn("post", path)
parameters = path["parameters"]
self.assertEqual(2, len(parameters))
name_param = {}
id_param = {}
for p in parameters:
if p["name"] == "id":
id_param = p
else:
name_param = p
self.assertDictEqual(
name_param,
{
"in": "path",
"name": "name",
"required": True,
"schema": {"type": "string"},
},
)
self.assertDictEqual(
id_param,
{
"in": "path",
"name": "id",
"required": True,
"schema": {"type": "integer", "format": "int32"},
},
)
# pylint: disable=W8110
def test_03(self):
"""Test default parameters and default responses
You can define default parameters and responses at service level.
In this test we check that these parameters and responses are into the
openapi specification
"""
default_params = [
{
"name": "API-KEY",
"in": "header",
"description": "API key for Authorization",
"required": True,
"schema": {"type": "string"},
"style": "simple",
}
]
# pylint: disable=R7980
class PartnerService(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "Sercice description"
@restapi.method(
[(["/<int:id>/update_name/<string:name>"], "POST")], auth="public"
)
def update_name(self, _id, _name):
"""update_name"""
def _get_openapi_default_parameters(self):
defaults = super()._get_openapi_default_parameters()
defaults.extend(default_params)
return defaults
def _get_openapi_default_responses(self):
responses = super()._get_openapi_default_responses().copy()
responses["999"] = "TEST"
return responses
self._build_services(self, PartnerService)
service = self._get_service_component(self, "partner")
openapi = service.to_openapi()
paths = openapi["paths"]
self.assertIn("/{id}/update_name/{name}", paths)
path = paths["/{id}/update_name/{name}"]
self.assertIn("post", path)
parameters = path["post"].get("parameters", [])
self.assertListEqual(parameters, default_params)
responses = path["post"].get("responses", [])
self.assertIn("999", responses)
def test_04(self):
"""Binary and Multipart form-data test case"""
# pylint: disable=R7980
class AttachmentService(Component):
_inherit = "base.rest.service"
_name = "test.attachment.service"
_usage = "attachment"
_collection = self._collection_name
_description = "Sercice description"
@restapi.method(
routes=[(["/<int:id>/download"], "GET")],
output_param=restapi.BinaryData(required=True),
)
def download(self, _id):
"""download the attachment"""
@restapi.method(
routes=[(["/create"], "POST")],
input_param=restapi.MultipartFormData(
{
"file": restapi.BinaryData(
mediatypes=["image/png", "image/jpeg"]
),
"params": restapi.CerberusValidator("_get_attachment_schema"),
}
),
output_param=restapi.CerberusValidator("_get_attachment_schema"),
)
# pylint: disable=W8106
def create(self, file, params):
"""create the attachment"""
def _get_attachment_schema(self):
return {"name": {"type": "string", "required": True}}
self._build_services(self, AttachmentService)
service = self._get_service_component(self, "attachment")
openapi = service.to_openapi()
paths = openapi["paths"]
# The paths must contains 2 entries (1 by routes)
self.assertSetEqual({"/{id}/download", "/create"}, set(openapi["paths"]))
path_download = paths["/{id}/download"]
resp_download = None
for item in path_download["get"]["responses"].items():
if item[0] == "200":
resp_download = item[1]
self.assertTrue(resp_download)
self.assertDictEqual(
resp_download,
{
"content": {
"*/*": {
"schema": {
"type": "string",
"format": "binary",
"required": True,
}
}
}
},
)
# The path contains parameters
self.assertDictEqual(
path_download.get("parameters", [{}])[0],
{
"in": "path",
"name": "id",
"required": True,
"schema": {"type": "integer", "format": "int32"},
},
)
path_create = paths["/create"]
resp_create = None
for item in path_create["post"]["responses"].items():
if item[0] == "200":
resp_create = item[1]
self.assertTrue(resp_create)
self.assertDictEqual(
resp_create,
{
"content": {
"application/json": {
"schema": {
"properties": {"name": {"type": "string"}},
"required": ["name"],
"type": "object",
}
}
}
},
)
request_create = path_create["post"]["requestBody"]
self.assertDictEqual(
request_create,
{
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"file": {
"type": "string",
"format": "binary",
"required": False,
},
"params": {
"type": "object",
"properties": {
"name": {
"type": "string",
},
},
"required": ["name"],
},
},
"encoding": {
"file": {"contentType": "image/png, image/jpeg"}
},
}
}
}
},
)

View file

@ -0,0 +1,150 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.component.core import Component
from odoo.addons.website.tools import MockRequest
from .. import restapi
from .common import BaseRestCase, TransactionRestServiceRegistryCase
class TestServiceContextProvider(TransactionRestServiceRegistryCase):
"""Test Odoo service context provider
In this class we test the context provided by the service context provider
"""
def setUp(self):
super().setUp()
self._setup_registry(self)
def tearDown(self):
self._teardown_registry(self)
super().tearDown()
def test_01(self):
"""Test authenticated_partner_id
In this case we check that the default service context provider provides
no authenticated_partner_id
"""
# pylint: disable=R7980
class TestServiceNewApi(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "test"
@restapi.method(
[(["/<int:id>/get", "/<int:id>"], "GET")],
output_param=restapi.CerberusValidator("_get_partner_schema"),
auth="public",
)
def get(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
self._build_services(self, TestServiceNewApi)
controller = self._get_controller_for(TestServiceNewApi)
with MockRequest(self.env), controller().service_component(
"partner"
) as service:
self.assertFalse(service.work.authenticated_partner_id)
def test_02(self):
"""Test authenticated_partner_id
In this case we check that the 'abstract.user.authenticated.partner.provider'
service context provider provides the current user's partner as
authenticated_partner_id
"""
# pylint: disable=R7880
class TestComponentContextprovider(Component):
_name = "test.component.context.provider"
_inherit = [
"abstract.user.authenticated.partner.provider",
"base.rest.service.context.provider",
]
_usage = "test_component_context_provider"
self._BaseTestController._component_context_provider = (
"test_component_context_provider"
)
# pylint: disable=R7980
class TestServiceNewApi(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "test"
@restapi.method(
[(["/<int:id>/get", "/<int:id>"], "GET")],
output_param=restapi.CerberusValidator("_get_partner_schema"),
auth="public",
)
def get(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
self._build_components(TestComponentContextprovider)
self._build_services(self, TestServiceNewApi)
controller = self._get_controller_for(TestServiceNewApi)
with MockRequest(self.env), controller().service_component(
"partner"
) as service:
self.assertEqual(
service.work.authenticated_partner_id, self.env.user.partner_id.id
)
def test_03(self):
"""Test authenticated_partner_id
In this case we check that redefining the method _get_authenticated_partner_id
changes the authenticated_partner_id provided by the service context provider
"""
# pylint: disable=R7880
class TestComponentContextprovider(Component):
_name = "test.component.context.provider"
_inherit = "base.rest.service.context.provider"
_usage = "test_component_context_provider"
def _get_authenticated_partner_id(self):
return 9999
self._BaseTestController._component_context_provider = (
"test_component_context_provider"
)
# pylint: disable=R7980
class TestServiceNewApi(Component):
_inherit = "base.rest.service"
_name = "test.partner.service"
_usage = "partner"
_collection = self._collection_name
_description = "test"
@restapi.method(
[(["/<int:id>/get", "/<int:id>"], "GET")],
output_param=restapi.CerberusValidator("_get_partner_schema"),
auth="public",
)
def get(self, _id):
return {"name": self.env["res.partner"].browse(_id).name}
self._build_components(TestComponentContextprovider)
self._build_services(self, TestServiceNewApi)
controller = self._get_controller_for(TestServiceNewApi)
with MockRequest(self.env), controller().service_component(
"partner"
) as service:
self.assertEqual(service.work.authenticated_partner_id, 9999)
class CommonCase(BaseRestCase):
# dummy test method to pass codecov
def test_04(self):
self.assertEqual(self.registry.test_cr, self.cr)

View file

@ -0,0 +1,147 @@
# Copyright 2018 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import inspect
import logging
from collections import OrderedDict
_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"]
def cerberus_to_json(schema):
"""Convert a Cerberus schema to a JSON schema"""
result = OrderedDict()
required = []
properties = OrderedDict()
result["type"] = "object"
result["required"] = required
result["properties"] = properties
for field, spec in list(schema.items()):
props = _get_field_props(spec)
properties[field] = props
if spec.get("required"):
required.append(field)
# sort required to get the same order on each run and easy comparison into
# the tests
required.sort()
return result
def _get_field_props(spec): # noqa: C901
resp = OrderedDict()
# dictionary of tuple (json type, json fromat) by cerberus type for
# cerberus types requiring a specific mapping to a json type/format
type_map = {
"dict": ("object",),
"list": ("array",),
"objectid": ("string", "objectid"),
"datetime": ("string", "date-time"),
"float": ("number", "float"),
}
_type = spec.get("type")
if _type is None:
return resp
if "description" in spec:
resp["description"] = spec["description"]
if "meta" in spec:
for key in SUPPORTED_META:
value = spec["meta"].get(key)
if value:
resp[key] = value
if "allowed" in spec:
resp["enum"] = spec["allowed"]
if "default" in spec:
resp["default"] = spec["default"]
if "minlength" in spec:
if _type == "string":
resp["minLength"] = spec["minlength"]
elif _type == "list":
resp["minItems"] = spec["minlength"]
if "maxlength" in spec:
if _type == "string":
resp["maxLength"] = spec["maxlength"]
elif _type == "list":
resp["maxItems"] = spec["maxlength"]
if "min" in spec:
if _type in ["number", "integer", "float"]:
resp["minimum"] = spec["min"]
if "max" in spec:
if _type in ["number", "integer", "float"]:
resp["maximum"] = spec["max"]
if "readonly" in spec:
resp["readOnly"] = spec["readonly"]
if "regex" in spec:
resp["pattern"] = spec["regex"]
if "nullable" in spec:
resp["nullable"] = spec["nullable"]
if "allowed" in spec:
resp["enum"] = spec["allowed"]
json_type = type_map.get(_type, (_type,))
resp["type"] = json_type[0]
if json_type[0] == "object":
if "schema" in spec:
resp.update(cerberus_to_json(spec["schema"]))
additional_properties = {}
if "keysrules" in spec:
rule_value_type = spec["keysrules"].get("type", "string")
if rule_value_type != "string":
_logger.debug(
"Openapi only support key/value mapping definition where"
" the keys are strings. Received %s",
rule_value_type,
)
if "valuesrules" in spec:
values_rules = spec["valuesrules"]
rule_value_type = values_rules.get("type", "string")
additional_properties["type"] = rule_value_type
if "schema" in values_rules:
additional_properties.update(cerberus_to_json(values_rules["schema"]))
if additional_properties:
resp["additionalProperties"] = additional_properties
elif json_type[0] == "array":
if "schema" in spec:
resp["items"] = _get_field_props(spec["schema"])
else:
resp["items"] = {"type": "string"}
else:
try:
resp["format"] = json_type[1]
# pylint:disable=except-pass
except IndexError:
pass
return resp
def _inspect_methods(cls):
"""Return all methods of a given class as (name, value) pairs sorted by
name.
inspect.getmembers was initially used. Unfortunately, instance's properties
was accessed into the loop and could raise some exception since we are
into the startup process and all the resources are not yet initialized.
"""
results = []
for attribute in inspect.classify_class_attrs(cls):
if attribute.kind != "method":
continue
name = attribute.name
method = getattr(cls, name)
results.append((name, method))
results.sort(key=lambda pair: pair[0])
return results

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="menu_rest_api_root" model="ir.ui.menu">
<field name="name">REST API</field>
<field name="sequence" eval="400" />
<field name="web_icon">base_rest,static/description/icon.png</field>
</record>
<record id="action_rest_api_docs" model="ir.actions.act_url">
<field name="name">REST API</field>
<field name="url">/api-docs</field>
<field name="target">self</field>
</record>
<record id="menu_rest_api_docs" model="ir.ui.menu">
<field name="parent_id" ref="menu_rest_api_root" />
<field name="name">Docs</field>
<field name="sequence" eval="100" />
<field name="action" ref="action_rest_api_docs" />
</record>
</odoo>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2017 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<template id="openapi" name="OpenAPI">
<t t-call="web.layout">
<t t-set="head">
<meta name="generator" content="Odoo OpenAPI" />
<meta name="description" content="Odoo OpenAPI UI" />
<link
rel="stylesheet"
href="/base_rest/static/lib/swagger-ui-3.51.1/swagger-ui.css"
/>
<script
type="text/javascript"
src="/base_rest/static/lib/swagger-ui-3.51.1/swagger-ui-bundle.js"
/>
<script
type="text/javascript"
src="/base_rest/static/lib/swagger-ui-3.51.1/swagger-ui-standalone-preset.js"
/>
<script type="text/javascript">
odoo.session_info = {
is_superuser:<t
t-esc="json.dumps(request.env.user._is_superuser())"
/>,
is_frontend: true,
};
</script>
<t t-call-assets="web.assets_common" t-js="false" />
<t t-call-assets="base_rest.assets_swagger" t-js="false" />
<link
href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700"
rel="stylesheet"
/>
<link
rel="icon"
type="image/png"
href="/base_rest/static/lib/swagger-ui-3.51.1/favicon-32x32.png"
sizes="32x32"
/>
<link
rel="icon"
type="image/png"
href="/base_rest/static/lib/swagger-ui-3.51.1/favicon-16x16.png"
sizes="16x16"
/>
<t t-call-assets="web.assets_common" t-css="false" />
<t t-call-assets="web.assets_frontend" t-css="false" />
<t t-call-assets="base_rest.assets_swagger" t-css="false" />
</t>
<t t-set="head" t-value="head" />
</t>
<body>
<div id="swagger-ui" t-att-data-settings='json.dumps(swagger_settings)' />
</body>
</template>
</odoo>

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Base_rest Module - base_rest
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for base_rest. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,17 @@
# Controllers
HTTP routes provided by this module.
```mermaid
sequenceDiagram
participant U as User/Client
participant C as Module Controllers
participant O as ORM/Views
U->>C: HTTP GET/POST (routes)
C->>O: ORM operations, render templates
O-->>U: HTML/JSON/PDF
```
Notes
- See files in controllers/ for route definitions.

View file

@ -0,0 +1,6 @@
# Dependencies
This addon depends on:
- [component](../../odoo-bringout-oca-connector-component)
- [web](../../odoo-bringout-oca-ocb-web)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon base_rest or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-rest-framework-base_rest"
# or
uv pip install odoo-bringout-oca-rest-framework-base_rest"
```

View file

@ -0,0 +1,13 @@
# Models
Detected core models and extensions in base_rest.
```mermaid
classDiagram
class rest_service_registration
class ir_rule
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: base_rest. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon base_rest
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,8 @@
# Security
This module does not define custom security rules or access controls beyond Odoo defaults.
Default Odoo security applies:
- Base user access through standard groups
- Model access inherited from dependencies
- No custom row-level security rules

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon base_rest
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1,45 @@
[project]
name = "odoo-bringout-oca-rest-framework-base_rest"
version = "16.0.0"
description = "Base Rest -
Develop your own high level REST APIs for Odoo thanks to this addon.
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-rest-framework-component>=16.0.0",
"odoo-bringout-oca-ocb-web>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["base_rest"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]