mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-18 08:52:02 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
47
odoo-bringout-oca-rest-framework-base_rest/README.md
Normal file
47
odoo-bringout-oca-rest-framework-base_rest/README.md
Normal 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
|
||||
422
odoo-bringout-oca-rest-framework-base_rest/base_rest/README.rst
Normal file
422
odoo-bringout-oca-rest-framework-base_rest/base_rest/README.rst
Normal 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 Odoo’s 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.
|
||||
|
|
@ -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.",
|
||||
)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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": []})
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from . import service
|
||||
from . import service_context_provider
|
||||
from . import cerberus_validator
|
||||
from . import user_component_context_provider
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 + "/"]
|
||||
|
|
@ -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)
|
||||
22
odoo-bringout-oca-rest-framework-base_rest/base_rest/core.py
Normal file
22
odoo-bringout-oca-rest-framework-base_rest/base_rest/core.py
Normal 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."""
|
||||
252
odoo-bringout-oca-rest-framework-base_rest/base_rest/http.py
Normal file
252
odoo-bringout-oca-rest-framework-base_rest/base_rest/http.py
Normal 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)
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import ir_rule
|
||||
from . import rest_service_registration
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
)
|
||||
"""
|
||||
|
|
@ -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**.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
* Laurent Mignon <laurent.mignon@acsone.eu>
|
||||
* Sébastien Beau <sebastien.beau@akretion.com>
|
||||
|
|
@ -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 Odoo’s 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`.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
0
odoo-bringout-oca-rest-framework-base_rest/base_rest/readme/newsfragments/.gitignore
vendored
Normal file
0
odoo-bringout-oca-rest-framework-base_rest/base_rest/readme/newsfragments/.gitignore
vendored
Normal file
433
odoo-bringout-oca-rest-framework-base_rest/base_rest/restapi.py
Normal file
433
odoo-bringout-oca-rest-framework-base_rest/base_rest/restapi.py
Normal 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 |
|
|
@ -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&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 Odoo’s 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. That’s 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"><string:_service_name>/<int:_id></span></tt> and <tt class="docutils literal"><span class="pre"><string:_service_name>/<int:_id>/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"><string:_service_name>/</tt> and <tt class="docutils literal"><span class="pre"><string:_service_name>/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"><string:_service_name>/<int:_id></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"><string:_service_name>/<int:_id></span></tt>.</li>
|
||||
<li>Other methods are only accessible via HTTP POST routes <tt class="docutils literal"><string:_service_name></tt> or <tt class="docutils literal"><span class="pre"><string:_service_name>/<string:method_name></span></tt> or <tt class="docutils literal"><span class="pre"><string:_service_name>/<int:_id></span></tt> or <tt class="docutils literal"><span class="pre"><string:_service_name>/<int:_id>/<string:method_name></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">@route</span><span class="p">([</span><span class="w">
|
||||
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'<string:_service_name>'</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'<string:_service_name>/search'</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'<string:_service_name>/<int:_id>'</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'<string:_service_name>/<int:_id>/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">"user"</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">@route</span><span class="p">([</span><span class="w">
|
||||
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'<string:_service_name>'</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'<string:_service_name>/<string:method_name>'</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'<string:_service_name>/<int:_id>'</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'<string:_service_name>/<int:_id>/<string:method_name>'</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">"user"</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">"HTTP POST with method name 'get' is not allowed. "</span><span class="w">
|
||||
</span> <span class="s2">"(service name: </span><span class="si">%s</span><span class="s2">)"</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">@route</span><span class="p">([</span><span class="w">
|
||||
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'<string:_service_name>/<int:_id>'</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">"user"</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">@route</span><span class="p">([</span><span class="w">
|
||||
</span> <span class="n">ROOT_PATH</span> <span class="o">+</span> <span class="s1">'<string:_service_name>/<int:_id>'</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">"user"</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">"base.rest.service"</span><span class="w">
|
||||
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s2">"partner.new_api.service"</span><span class="w">
|
||||
</span> <span class="n">_usage</span> <span class="o">=</span> <span class="s2">"partner"</span><span class="w">
|
||||
</span> <span class="n">_collection</span> <span class="o">=</span> <span class="s2">"base.rest.demo.new_api.services"</span><span class="w">
|
||||
</span> <span class="n">_description</span> <span class="o">=</span> <span class="s2">"""
|
||||
Partner New API Services
|
||||
Services developed with the new api provided by base_rest
|
||||
"""</span><span class="w">
|
||||
|
||||
</span> <span class="nd">@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">"/<int:id>/get"</span><span class="p">,</span> <span class="s2">"/<int:id>"</span><span class="p">],</span> <span class="s2">"GET"</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">"_get_partner_schema"</span><span class="p">),</span><span class="w">
|
||||
</span> <span class="n">auth</span><span class="o">=</span><span class="s2">"public"</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">"name"</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">"res.partner"</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">"name"</span><span class="p">:</span> <span class="p">{</span><span class="s2">"type"</span><span class="p">:</span> <span class="s2">"string"</span><span class="p">,</span> <span class="s2">"required"</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">@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">"/list"</span><span class="p">,</span> <span class="s2">"/"</span><span class="p">],</span> <span class="s2">"GET"</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">"_get_partner_schema"</span><span class="p">),</span><span class="w">
|
||||
</span> <span class="n">auth</span><span class="o">=</span><span class="s2">"public"</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">"res.partner"</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">"name"</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">"partner.search.param"</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">"partner.short.info"</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">"base.rest.service"</span><span class="w">
|
||||
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s2">"partner.new_api.service"</span><span class="w">
|
||||
</span> <span class="n">_usage</span> <span class="o">=</span> <span class="s2">"partner"</span><span class="w">
|
||||
</span> <span class="n">_collection</span> <span class="o">=</span> <span class="s2">"base.rest.demo.new_api.services"</span><span class="w">
|
||||
</span> <span class="n">_description</span> <span class="o">=</span> <span class="s2">"""
|
||||
Partner New API Services
|
||||
Services developed with the new api provided by base_rest
|
||||
"""</span><span class="w">
|
||||
|
||||
</span> <span class="nd">@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">"/"</span><span class="p">,</span> <span class="s2">"/search"</span><span class="p">],</span> <span class="s2">"GET"</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">"partner.search.param"</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">"partner.short.info"</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">"public"</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">"""
|
||||
Search for partners
|
||||
:param partner_search_param: An instance of partner.search.param
|
||||
:return: List of partner.short.info
|
||||
"""</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">"name"</span><span class="p">,</span> <span class="s2">"like"</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">"id"</span><span class="p">,</span> <span class="s2">"="</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">"partner.short.info"</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">"res.partner"</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 <<a class="reference external" href="mailto:laurent.mignon@acsone.eu">laurent.mignon@acsone.eu</a>></li>
|
||||
<li>Sébastien Beau <<a class="reference external" href="mailto:sebastien.beau@akretion.com">sebastien.beau@akretion.com</a>></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 |
|
|
@ -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>
|
||||
|
|
@ -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
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
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"},
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -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)
|
||||
147
odoo-bringout-oca-rest-framework-base_rest/base_rest/tools.py
Normal file
147
odoo-bringout-oca-rest-framework-base_rest/base_rest/tools.py
Normal 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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for base_rest. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [component](../../odoo-bringout-oca-connector-component)
|
||||
- [web](../../odoo-bringout-oca-ocb-web)
|
||||
4
odoo-bringout-oca-rest-framework-base_rest/doc/FAQ.md
Normal file
4
odoo-bringout-oca-rest-framework-base_rest/doc/FAQ.md
Normal 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.
|
||||
|
|
@ -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"
|
||||
```
|
||||
13
odoo-bringout-oca-rest-framework-base_rest/doc/MODELS.md
Normal file
13
odoo-bringout-oca-rest-framework-base_rest/doc/MODELS.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
7
odoo-bringout-oca-rest-framework-base_rest/doc/USAGE.md
Normal file
7
odoo-bringout-oca-rest-framework-base_rest/doc/USAGE.md
Normal 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
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
45
odoo-bringout-oca-rest-framework-base_rest/pyproject.toml
Normal file
45
odoo-bringout-oca-rest-framework-base_rest/pyproject.toml
Normal 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",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue