Initial commit: OCA Technical packages (595 packages)

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

View file

@ -0,0 +1,51 @@
# Base Rest Demo
Odoo addon: base_rest_demo
## Installation
```bash
pip install odoo-bringout-oca-rest-framework-base_rest_demo
```
## Dependencies
This addon depends on:
- base_rest
- base_rest_datamodel
- base_rest_pydantic
- component
- extendable
- pydantic
## Manifest Information
- **Name**: Base Rest Demo
- **Version**: 16.0.2.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_demo`.
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,113 @@
==============
Base Rest Demo
==============
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:18e5691a569699452f29ef673266f29b874bf5c931221af24817846eb59f85a1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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_demo
: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_demo
: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|
Demo addon to illustrate how to develop self documented REST services thanks
to the **base_rest** addon.
**Table of contents**
.. contents::
:local:
Usage
=====
This module provides sample REST services based on base_rest. The documentation
of provided services is available at the ``/api-docs`` URL. This frontend based
on `Swagger <https://swagger.io/>`_ is generated from the implemented
webservices.
Known issues / Roadmap
======================
The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement>`_
and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug>`_ can
be found on GitHub.
Changelog
=========
12.0.2.0.0
~~~~~~~~~~
* Licence changed from AGPL-3 to LGPL-3
12.0.1.0.0
~~~~~~~~~~
First official version.
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_demo%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>
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.
.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px
:target: https://github.com/lmignon
:alt: lmignon
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-lmignon|
This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/16.0/base_rest_demo>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,4 @@
from . import controllers
from . import datamodels
from . import pydantic_models
from . import services

View file

@ -0,0 +1,26 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
{
"name": "Base Rest Demo",
"summary": """
Demo addon for Base REST""",
"version": "16.0.2.0.4",
"development_status": "Beta",
"license": "LGPL-3",
"author": "ACSONE SA/NV, " "Odoo Community Association (OCA)",
"maintainers": ["lmignon"],
"website": "https://github.com/OCA/rest-framework",
"depends": [
"base_rest",
"base_rest_datamodel",
"base_rest_pydantic",
"component",
"extendable",
"pydantic",
],
"external_dependencies": {
"python": ["jsondiff", "extendable-pydantic", "marshmallow", "pydantic>=2.0.0"]
},
"installable": True,
}

View file

@ -0,0 +1 @@
from . import main

View file

@ -0,0 +1,32 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.base_rest.controllers import main
class BaseRestDemoPublicApiController(main.RestController):
_root_path = "/base_rest_demo_api/public/"
_collection_name = "base.rest.demo.public.services"
_default_auth = "public"
class BaseRestDemoPrivateApiController(main.RestController):
_root_path = "/base_rest_demo_api/private/"
_collection_name = "base.rest.demo.private.services"
_default_auth = "user"
class BaseRestDemoNewApiController(main.RestController):
_root_path = "/base_rest_demo_api/new_api/"
_collection_name = "base.rest.demo.new_api.services"
_default_auth = "public"
class BaseRestDemoJwtApiController(main.RestController):
# JWT Demo Controller, to be used with auth_jwt_demo
# https://github.com/OCA/server-auth/tree/15.0/auth_jwt_demo
_root_path = "/base_rest_demo_api/jwt/"
_collection_name = "base.rest.demo.jwt.services"
_default_auth = "jwt_demo_keycloak"
_component_context_provider = "auth_jwt_component_context_provider"
_default_cors = "*"

View file

@ -0,0 +1,5 @@
from . import country_info
from . import state_info
from . import partner_short_info
from . import partner_info
from . import partner_search_param

View file

@ -0,0 +1,13 @@
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from marshmallow import fields
from odoo.addons.datamodel.core import Datamodel
class CountryInfo(Datamodel):
_name = "country.info"
id = fields.Integer(required=True, allow_none=False)
name = fields.String(required=True, allow_none=False)

View file

@ -0,0 +1,21 @@
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from marshmallow import fields
from odoo.addons.datamodel.core import Datamodel
from odoo.addons.datamodel.fields import NestedModel
class PartnerInfo(Datamodel):
_name = "partner.info"
_inherit = "partner.short.info"
street = fields.String(required=True, allow_none=False)
street2 = fields.String(required=False, allow_none=True)
zip_code = fields.String(required=True, allow_none=False)
city = fields.String(required=True, allow_none=False)
phone = fields.String(required=False, allow_none=True)
state = NestedModel("state.info")
country = NestedModel("country.info")
is_company = fields.Boolean(required=False, allow_none=False)

View file

@ -0,0 +1,13 @@
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from marshmallow import fields
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)

View file

@ -0,0 +1,13 @@
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from marshmallow import fields
from odoo.addons.datamodel.core import Datamodel
class PartnerShortInfo(Datamodel):
_name = "partner.short.info"
id = fields.Integer(required=True, allow_none=False)
name = fields.String(required=True, allow_none=False)

View file

@ -0,0 +1,13 @@
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from marshmallow import fields
from odoo.addons.datamodel.core import Datamodel
class StateInfo(Datamodel):
_name = "state.info"
id = fields.Integer(required=True, allow_none=False)
name = fields.String(required=True, allow_none=False)

View file

@ -0,0 +1,49 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_rest_demo
#
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_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "Access error message"
msgstr ""
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "Missing message"
msgstr ""
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/partner_image_services.py:0
#, python-format
msgid "No image found for partner %s"
msgstr ""
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "UserError message"
msgstr ""
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "ValidationError message"
msgstr ""

View file

@ -0,0 +1,49 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_rest_demo
#
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_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "Access error message"
msgstr "Poruka greške pristupa"
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "Missing message"
msgstr "Nedostajuća poruka"
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/partner_image_services.py:0
#, python-format
msgid "No image found for partner %s"
msgstr "Nema pronađene slike za partnera %s"
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "UserError message"
msgstr "Poruka greške korisnika"
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "ValidationError message"
msgstr "Poruka greške validacije"

View file

@ -0,0 +1,52 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_rest_demo
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-01-15 11:35+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_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "Access error message"
msgstr "Messaggio errore accesso"
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "Missing message"
msgstr "Messaggio perdita"
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/partner_image_services.py:0
#, python-format
msgid "No image found for partner %s"
msgstr "Nessuna immagine per il partner %s"
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "UserError message"
msgstr "Messaggio errore utente"
#. module: base_rest_demo
#. odoo-python
#: code:addons/base_rest_demo/services/exception_services.py:0
#, python-format
msgid "ValidationError message"
msgstr "Messaggio errore validazione"

View file

@ -0,0 +1,6 @@
from . import naive_orm_model
from . import country_info
from . import state_info
from . import partner_short_info
from . import partner_info
from . import partner_search_param

View file

@ -0,0 +1,10 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from .naive_orm_model import NaiveOrmModel
class CountryInfo(NaiveOrmModel):
id: int
name: str

View file

@ -0,0 +1,10 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from extendable_pydantic import ExtendableModelMeta
from pydantic import BaseModel, ConfigDict
class NaiveOrmModel(BaseModel, metaclass=ExtendableModelMeta):
model_config: ConfigDict = ConfigDict(from_attributes=True)

View file

@ -0,0 +1,20 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import pydantic
from .country_info import CountryInfo
from .partner_short_info import PartnerShortInfo
from .state_info import StateInfo
class PartnerInfo(PartnerShortInfo):
street: str
street2: str = None
zip_code: str = pydantic.Field(..., alias="zip")
city: str
phone: str = None
state: StateInfo = pydantic.Field(..., alias="state_id")
country: CountryInfo = pydantic.Field(..., alias="country_id")
is_company: bool = None

View file

@ -0,0 +1,12 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from extendable_pydantic import ExtendableModelMeta
from pydantic import BaseModel
class PartnerSearchParam(BaseModel, metaclass=ExtendableModelMeta):
id: int = None
name: str = None

View file

@ -0,0 +1,10 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from .naive_orm_model import NaiveOrmModel
class PartnerShortInfo(NaiveOrmModel):
id: int
name: str

View file

@ -0,0 +1,10 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from .naive_orm_model import NaiveOrmModel
class StateInfo(NaiveOrmModel):
id: int
name: str

View file

@ -0,0 +1 @@
* Laurent Mignon <laurent.mignon@acsone.eu>

View file

@ -0,0 +1,2 @@
Demo addon to illustrate how to develop self documented REST services thanks
to the **base_rest** addon.

View file

@ -0,0 +1,9 @@
12.0.2.0.0
~~~~~~~~~~
* Licence changed from AGPL-3 to LGPL-3
12.0.1.0.0
~~~~~~~~~~
First official version.

View file

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

View file

@ -0,0 +1,4 @@
This module provides sample REST services based on base_rest. The documentation
of provided services is available at the ``/api-docs`` URL. This frontend based
on `Swagger <https://swagger.io/>`_ is generated from the implemented
webservices.

View file

@ -0,0 +1,7 @@
from . import ping_services
from . import partner_services
from . import partner_image_services
from . import partner_jwt_services
from . import exception_services
from . import partner_new_api_services
from . import partner_pydantic_services

View file

@ -0,0 +1,182 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from psycopg2 import errorcodes
from psycopg2.errors import OperationalError
from werkzeug.exceptions import MethodNotAllowed
from odoo import _
from odoo.exceptions import (
AccessDenied,
AccessError,
MissingError,
UserError,
ValidationError,
)
from odoo.http import SessionExpiredException
from odoo.service.model import MAX_TRIES_ON_CONCURRENCY_FAILURE
from odoo.addons.base_rest.components.service import to_int
from odoo.addons.component.core import Component
_CPT_RETRY = 0
class ExceptionService(Component):
_inherit = "base.rest.service"
_name = "exception.service"
_usage = "exception"
_collection = "base.rest.demo.public.services"
_description = """
Exception Services
Services to test hiw exception are handled by base_erst
"""
def user_error(self):
"""
Simulate an odoo.exceptions.UserError
Should be translated into BadRequest with a description into the json
body
"""
raise UserError(_("UserError message"))
def validation_error(self):
"""
Simulate an odoo.exceptions.ValidationError
Should be translated into BadRequest with a description into the json
body
"""
raise ValidationError(_("ValidationError message"))
def session_expired(self):
"""
Simulate an odoo.http.SessionExpiredException
Should be translated into Unauthorized without description into the
json body
"""
raise SessionExpiredException("Expired message")
def missing_error(self):
"""
Simulate an odoo.exceptions.MissingError
Should be translated into NotFound without description into the json
body
"""
raise MissingError(_("Missing message"))
def access_error(self):
"""
Simulate an odoo.exceptions.AccessError
Should be translated into Forbidden without description into the json
body
"""
raise AccessError(_("Access error message"))
def access_denied(self):
"""
Simulate an odoo.exceptions.AccessDenied
Should be translated into Forbidden without description into the json
body
"""
raise AccessDenied()
def http_exception(self):
"""
Simulate an werkzeug.exceptions.MethodNotAllowed
This exception is not by the framework
"""
raise MethodNotAllowed(description="Method not allowed message")
def bare_exception(self):
"""
Simulate a python exception.
Should be translated into InternalServerError without description into
the json body
"""
raise IOError("My IO error")
def retryable_error(self, nbr_retries):
"""This method is used in the test suite to check that the retrying
functionality in case of concurrency error on the database is working
correctly for retryable exceptions.
The output will be the number of retries that have been done.
This method is mainly used to test the retrying functionality
"""
global _CPT_RETRY
if _CPT_RETRY < nbr_retries:
_CPT_RETRY += 1
raise FakeConcurrentUpdateError("fake error")
tryno = _CPT_RETRY
_CPT_RETRY = 0
return {"retries": tryno}
# Validator
def _validator_user_error(self):
return {}
def _validator_return_user_error(self):
return {}
def _validator_validation_error(self):
return {}
def _validator_return_validation_error(self):
return {}
def _validator_session_expired(self):
return {}
def _validator_return_session_expired(self):
return {}
def _validator_missing_error(self):
return {}
def _validator_return_missing_error(self):
return {}
def _validator_access_error(self):
return {}
def _validator_return_access_error(self):
return {}
def _validator_access_denied(self):
return {}
def _validator_return_access_denied(self):
return {}
def _validator_http_exception(self):
return {}
def _validator_return_http_exception(self):
return {}
def _validator_bare_exception(self):
return {}
def _validator_return_bare_exception(self):
return {}
def _validator_retryable_error(self):
return {
"nbr_retries": {
"type": "integer",
"required": True,
"default": MAX_TRIES_ON_CONCURRENCY_FAILURE,
"coerce": to_int,
}
}
def _validator_return_retryable_error(self):
return {"retries": {"type": "integer"}}
class FakeConcurrentUpdateError(OperationalError):
@property
def pgcode(self):
return errorcodes.SERIALIZATION_FAILURE

View file

@ -0,0 +1,57 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import base64
from odoo import _
from odoo.exceptions import MissingError
from odoo.http import request
from odoo.addons.base_rest.components.service import skip_secure_response
from odoo.addons.component.core import Component
class PartnerImageService(Component):
_inherit = "base.rest.service"
_name = "partner_image.service"
_usage = "partner_image"
_collection = "base.rest.demo.private.services"
_description = """
Partner Image Services
Service used to retrieve the partner's image
Access to the partner image service is only allowed to authenticated
users.
If you are not authenticated go to <a href='/web/login'>Login</a>
"""
@skip_secure_response
def get(self, _id, size):
"""
Get partner's image
"""
field = "image"
if size == "small":
field = "image_small"
elif size == "medium":
field = "image_medium"
status, headers, content = self.env["ir.http"].binary_content(
model="res.partner", id=_id, field=field
)
if not content:
raise MissingError(_("No image found for partner %s") % _id)
image_base64 = base64.b64decode(content)
headers.append(("Content-Length", len(image_base64)))
response = request.make_response(image_base64, headers)
response.status_code = status
return response
# Validator
def _validator_get(self):
return {
"size": {
"type": "string",
"required": False,
"default": "small",
"allowed": ["small", "medium", "large"],
}
}

View file

@ -0,0 +1,10 @@
# 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 PingJwtService(Component):
_inherit = "ping.service"
_name = "ping.jwt.service"
_collection = "base.rest.demo.jwt.services"

View file

@ -0,0 +1,103 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.base_rest import restapi
from odoo.addons.base_rest_datamodel.restapi import Datamodel
from odoo.addons.component.core import Component
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=Datamodel("partner.info"),
auth="public",
)
def get(self, _id):
"""
Get partner's information
"""
partner = self._get(_id)
PartnerInfo = self.env.datamodels["partner.info"]
partner_info = PartnerInfo(partial=True)
partner_info.id = partner.id
partner_info.name = partner.name
partner_info.street = partner.street
partner_info.street2 = partner.street2
partner_info.zip_code = partner.zip
partner_info.city = partner.city
partner_info.phone = partner.phone
partner_info.country = self.env.datamodels["country.info"](
id=partner.country_id.id, name=partner.country_id.name
)
partner_info.state = self.env.datamodels["state.info"](
id=partner.state_id.id, name=partner.state_id.name
)
partner_info.is_company = partner.is_company
return partner_info
@restapi.method(
[(["/<string:name>/get", "/<string:name>"], "GET")],
output_param=Datamodel("partner.info"),
auth="public",
)
def get_by_name(self, name):
"""
Get partner's information by name.
"""
partner = self.env["res.partner"].search([("name", "=", name)], limit=1)
if not partner:
raise FileNotFoundError
PartnerInfo = self.env.datamodels["partner.info"]
partner_info = PartnerInfo(partial=True)
partner_info.id = partner.id
partner_info.name = partner.name
partner_info.street = partner.street
partner_info.street2 = partner.street2
partner_info.zip_code = partner.zip
partner_info.city = partner.city
partner_info.phone = partner.phone
partner_info.country = self.env.datamodels["country.info"](
id=partner.country_id.id, name=partner.country_id.name
)
partner_info.state = self.env.datamodels["state.info"](
id=partner.state_id.id, name=partner.state_id.name
)
partner_info.is_company = partner.is_company
return partner_info
@restapi.method(
[(["/", "/search"], "GET")],
input_param=Datamodel("partner.search.param"),
output_param=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 following method are 'private' and should be never never NEVER call
# from the controller.
def _get(self, _id):
return self.env["res.partner"].browse(_id)

View file

@ -0,0 +1,60 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.base_rest import restapi
from odoo.addons.base_rest_pydantic.restapi import PydanticModel, PydanticModelList
from odoo.addons.component.core import Component
from ..pydantic_models.partner_info import PartnerInfo
from ..pydantic_models.partner_search_param import PartnerSearchParam
from ..pydantic_models.partner_short_info import PartnerShortInfo
class PartnerNewApiService(Component):
_inherit = "base.rest.service"
_name = "partner.pydantic.service"
_usage = "partner_pydantic"
_collection = "base.rest.demo.new_api.services"
_description = """
Partner New API Services
Services developed with the new api provided by base_rest and pydantic
"""
@restapi.method(
[(["/<int:id>/get", "/<int:id>"], "GET")],
output_param=PydanticModel(PartnerInfo),
auth="public",
)
def get(self, _id):
"""
Get partner's information
"""
partner = self._get(_id)
return PartnerInfo.from_orm(partner)
@restapi.method(
[(["/", "/search"], "GET")],
input_param=PydanticModel(PartnerSearchParam),
output_param=PydanticModelList(PartnerShortInfo),
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 = []
for p in self.env["res.partner"].sudo().search(domain):
res.append(PartnerShortInfo.from_orm(p))
return res
# The following method are 'private' and should be never never NEVER call
# from the controller.
def _get(self, _id):
return self.env["res.partner"].sudo().browse(_id)

View file

@ -0,0 +1,163 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.base_rest.components.service import to_bool, to_int
from odoo.addons.component.core import Component
class PartnerService(Component):
_inherit = "base.rest.service"
_name = "partner.service"
_usage = "partner"
_collection = "base.rest.demo.private.services"
_description = """
Partner Services
Access to the partner services is only allowed to authenticated users.
If you are not authenticated go to <a href='/web/login'>Login</a>
"""
def get(self, _id):
"""
Get partner's informations
"""
return self._to_json(self._get(_id))
def search(self, name):
"""
Searh partner by name
"""
partners = self.env["res.partner"].name_search(name)
partners = self.env["res.partner"].browse([i[0] for i in partners])
rows = []
res = {"count": len(partners), "rows": rows}
for partner in partners:
rows.append(self._to_json(partner))
return res
# pylint:disable=method-required-super
def create(self, **params):
"""
Create a new partner
"""
partner = self.env["res.partner"].create(self._prepare_params(params))
return self._to_json(partner)
def update(self, _id, **params):
"""
Update partner informations
"""
partner = self._get(_id)
partner.write(self._prepare_params(params))
return self._to_json(partner)
def archive(self, _id, **params):
"""
Archive the given partner. This method is an empty method, IOW it
don't update the partner. This method is part of the demo data to
illustrate that historically it's not mandatory to defined a schema
describing the content of the response returned by a method.
This kind of definition is DEPRECATED and will no more supported in
the future.
:param _id:
:param params:
:return:
"""
return {"response": "Method archive called with id %s" % _id}
# The following method are 'private' and should be never never NEVER call
# from the controller.
def _get(self, _id):
return self.env["res.partner"].browse(_id)
def _prepare_params(self, params):
for key in ["country", "state"]:
if key in params:
val = params.pop(key)
if val.get("id"):
params["%s_id" % key] = val["id"]
return params
# Validator
def _validator_return_get(self):
res = self._validator_create()
res.update({"id": {"type": "integer", "required": True, "empty": False}})
return res
def _validator_search(self):
return {"name": {"type": "string", "nullable": False, "required": True}}
def _validator_return_search(self):
return {
"count": {"type": "integer", "required": True},
"rows": {
"type": "list",
"required": True,
"schema": {"type": "dict", "schema": self._validator_return_get()},
},
}
def _validator_create(self):
res = {
"name": {"type": "string", "required": True, "empty": False},
"street": {"type": "string", "required": True, "empty": False},
"street2": {"type": "string", "nullable": True},
"zip": {"type": "string", "required": True, "empty": False},
"city": {"type": "string", "required": True, "empty": False},
"phone": {"type": "string", "nullable": True, "empty": False},
"state": {
"type": "dict",
"schema": {
"id": {"type": "integer", "coerce": to_int, "nullable": True},
"name": {"type": "string"},
},
},
"country": {
"type": "dict",
"schema": {
"id": {
"type": "integer",
"coerce": to_int,
"required": True,
"nullable": False,
},
"name": {"type": "string"},
},
},
"is_company": {"coerce": to_bool, "type": "boolean"},
}
return res
def _validator_return_create(self):
return self._validator_return_get()
def _validator_update(self):
res = self._validator_create()
for key in res:
if "required" in res[key]:
del res[key]["required"]
return res
def _validator_return_update(self):
return self._validator_return_get()
def _validator_archive(self):
return {}
def _to_json(self, partner):
res = {
"id": partner.id,
"name": partner.name,
"street": partner.street,
"street2": partner.street2 or "",
"zip": partner.zip,
"city": partner.city,
"phone": partner.city,
}
if partner.country_id:
res["country"] = {
"id": partner.country_id.id,
"name": partner.country_id.name,
}
if partner.state_id:
res["state"] = {"id": partner.state_id.id, "name": partner.state_id.name}
return res

View file

@ -0,0 +1,85 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo.addons.base_rest.components.service import to_int
from odoo.addons.component.core import Component
class PingService(Component):
_inherit = "base.rest.service"
_name = "ping.service"
_usage = "ping"
_collection = "base.rest.demo.public.services"
_description = """
Ping Services
Access to the ping services is allowed to everyone
"""
# The following method are 'public' and can be called from the controller.
def get(self, _id, message):
"""
This method is used to get the information of the object specified
by Id.
"""
return {"message": message, "id": _id}
def search(self, **params):
"""
A search method to illustrate how you can define a complex request.
In the case of the methods 'get' and 'search' the parameters are
passed to the server as the query part of the service URL.
"""
return {"response": "Search called search with params %s" % params}
def update(self, _id, message):
"""
Update method description ...
"""
return {"response": "PUT called with message " + message}
# pylint:disable=method-required-super
def create(self, **params):
"""
Create method description ...
"""
return {"response": "POST called with message " + params["message"]}
def delete(self, _id):
"""
Delete method description ...
"""
return {"response": "DELETE called with id %s " % _id}
# Validator
def _validator_search(self):
return {
"param_string": {"type": "string"},
"param_required": {"type": "string", "required": True},
"limit": {"type": "integer", "default": 50, "coerce": to_int},
"offset": {"type": "integer", "default": 0, "coerce": to_int},
"params": {"type": "list", "schema": {"type": "string"}},
}
def _validator_return_search(self):
return {"response": {"type": "string"}}
# Validator
def _validator_get(self):
return {"message": {"type": "string"}}
def _validator_return_get(self):
return {"message": {"type": "string"}, "id": {"type": "integer"}}
def _validator_update(self):
return {"message": {"type": "string"}}
def _validator_return_update(self):
return {"response": {"type": "string"}}
def _validator_create(self):
return {"message": {"type": "string"}}
def _validator_return_create(self):
return {"response": {"type": "string"}}
def _validator_return_delete(self):
return {"response": {"type": "string"}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,459 @@
<!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 Demo</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-demo">
<h1 class="title">Base Rest Demo</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:18e5691a569699452f29ef673266f29b874bf5c931221af24817846eb59f85a1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<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_demo"><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_demo"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>Demo addon to illustrate how to develop self documented REST services thanks
to the <strong>base_rest</strong> addon.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#changelog" id="toc-entry-3">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-4">12.0.2.0.0</a></li>
<li><a class="reference internal" href="#section-2" id="toc-entry-5">12.0.1.0.0</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-6">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-7">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-8">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-9">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-10">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>This module provides sample REST services based on base_rest. The documentation
of provided services is available at the <tt class="docutils literal"><span class="pre">/api-docs</span></tt> URL. This frontend based
on <a class="reference external" href="https://swagger.io/">Swagger</a> is generated from the implemented
webservices.</p>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-2">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">roadmap</a>
and <a class="reference external" href="https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug">known issues</a> can
be found on GitHub.</p>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-3">Changelog</a></h1>
<div class="section" id="section-1">
<h2><a class="toc-backref" href="#toc-entry-4">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-2">
<h2><a class="toc-backref" href="#toc-entry-5">12.0.1.0.0</a></h2>
<p>First official version.</p>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-6">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_demo%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-7">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-8">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-9">Contributors</a></h2>
<ul class="simple">
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-10">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>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/lmignon"><img alt="lmignon" src="https://github.com/lmignon.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/rest-framework/tree/16.0/base_rest_demo">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>

View file

@ -0,0 +1,4 @@
from . import test_controller
from . import test_openapi
from . import test_exception
from . import test_service

View file

@ -0,0 +1,45 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import json
import os
from odoo.addons.base_rest.controllers.main import _PseudoCollection
from odoo.addons.base_rest.tests.common import BaseRestCase
from odoo.addons.component.core import WorkContext
from odoo.addons.extendable.tests.common import ExtendableMixin
DATA_DIR = os.path.join(os.path.realpath(os.path.dirname(__file__)), "data")
class CommonCase(BaseRestCase, ExtendableMixin):
@classmethod
def setUpClass(cls):
super().setUpClass()
collection = _PseudoCollection("base.rest.demo.private.services", cls.env)
cls.private_services_env = WorkContext(
model_name="rest.service.registration", collection=collection
)
collection = _PseudoCollection("base.rest.demo.public.services", cls.env)
cls.public_services_env = WorkContext(
model_name="rest.service.registration", collection=collection
)
collection = _PseudoCollection("base.rest.demo.new_api.services", cls.env)
cls.new_api_services_env = WorkContext(
model_name="rest.service.registration", collection=collection
)
cls.init_extendable_registry()
cls.addClassCleanup(cls.reset_extendable_registry)
# pylint: disable=W8106
def setUp(self):
# resolve an inheritance issue (common.TransactionCase does not call
# super)
BaseRestCase.setUp(self)
def get_canonical_json(file_name):
path = os.path.join(DATA_DIR, file_name)
with open(path, "r") as f:
return json.load(f)

View file

@ -0,0 +1,120 @@
{
"info": {
"description": "\nPartner Image Services\n\nService used to retrieve the partner's image\nAccess to the partner image service is only allowed to authenticated\nusers.\nIf you are not authenticated go to <a href='/web/login'>Login</a>\n",
"title": "partner_image REST services",
"version": ""
},
"servers": [
{
"url": "http://localhost:8069/base_rest_demo_api/private/partner_image"
}
],
"paths": {
"/{id}/get": {
"get": {
"summary": "\nGet partner's image\n",
"parameters": [
{
"name": "size",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": "small",
"schema": {
"type": "string",
"enum": ["small", "medium", "large"]
}
}
],
"responses": {
"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."
}
},
"security": [
{
"user": []
}
]
},
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
]
},
"/{id}": {
"get": {
"summary": "\nGet partner's image\n",
"parameters": [
{
"name": "size",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": "small",
"schema": {
"type": "string",
"enum": ["small", "medium", "large"]
}
}
],
"responses": {
"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."
}
},
"security": [
{
"user": []
}
]
},
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
]
}
},
"openapi": "3.0.0",
"components": {
"securitySchemes": {
"user": {
"type": "apiKey",
"in": "cookie",
"name": "session_id"
}
}
}
}

View file

@ -0,0 +1,303 @@
{
"info": {
"description": "\nPartner New API Services\nServices developed with the new api provided by base_rest and pydantic\n",
"title": "partner_pydantic REST services",
"version": ""
},
"servers": [
{
"url": "http://localhost:8069/base_rest_demo_api/new_api/partner_pydantic"
}
],
"paths": {
"/{id}/get": {
"get": {
"summary": "\nGet partner's information\n",
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PartnerInfo"
}
}
}
}
}
},
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
]
},
"/{id}": {
"get": {
"summary": "\nGet partner's information\n",
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PartnerInfo"
}
}
}
}
}
},
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
]
},
"/": {
"get": {
"summary": "\nSearch for partners\n:param partner_search_param: An instance of partner.search.param\n:return: List of partner.short.info\n",
"parameters": [
{
"name": "id",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "integer"
}
},
{
"name": "name",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "string"
}
}
],
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PartnerShortInfo"
}
}
}
}
}
}
}
},
"/search": {
"get": {
"summary": "\nSearch for partners\n:param partner_search_param: An instance of partner.search.param\n:return: List of partner.short.info\n",
"parameters": [
{
"name": "id",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "integer"
}
},
{
"name": "name",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "string"
}
}
],
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PartnerShortInfo"
}
}
}
}
}
}
}
}
},
"openapi": "3.0.0",
"components": {
"schemas": {
"CountryInfo": {
"properties": {
"id": {
"title": "Id",
"type": "integer"
},
"name": {
"title": "Name",
"type": "string"
}
},
"required": ["id", "name"],
"title": "CountryInfo",
"type": "object"
},
"StateInfo": {
"properties": {
"id": {
"title": "Id",
"type": "integer"
},
"name": {
"title": "Name",
"type": "string"
}
},
"required": ["id", "name"],
"title": "StateInfo",
"type": "object"
},
"PartnerInfo": {
"properties": {
"id": {
"title": "Id",
"type": "integer"
},
"name": {
"title": "Name",
"type": "string"
},
"street": {
"title": "Street",
"type": "string"
},
"street2": {
"default": null,
"title": "Street2",
"type": "string"
},
"zip_code": {
"title": "Zip Code",
"type": "string"
},
"city": {
"title": "City",
"type": "string"
},
"phone": {
"default": null,
"title": "Phone",
"type": "string"
},
"state": {
"$ref": "#/components/schemas/StateInfo"
},
"country": {
"$ref": "#/components/schemas/CountryInfo"
},
"is_company": {
"default": null,
"title": "Is Company",
"type": "boolean"
}
},
"required": ["id", "name", "street", "zip_code", "city", "state", "country"],
"title": "PartnerInfo",
"type": "object"
},
"PartnerShortInfo": {
"properties": {
"id": {
"title": "Id",
"type": "integer"
},
"name": {
"title": "Name",
"type": "string"
}
},
"required": ["id", "name"],
"title": "PartnerShortInfo",
"type": "object"
}
},
"securitySchemes": {
"user": {
"type": "apiKey",
"in": "cookie",
"name": "session_id"
}
}
}
}

View file

@ -0,0 +1,671 @@
{
"info": {
"description": "\nPing Services\nAccess to the ping services is allowed to everyone\n",
"title": "ping REST services",
"version": ""
},
"servers": [
{
"url": "http://localhost:8069/base_rest_demo_api/public/ping"
}
],
"paths": {
"/create": {
"post": {
"summary": "\nCreate method description ...\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
},
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"response": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/": {
"post": {
"summary": "\nCreate method description ...\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
},
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"response": {
"type": "string"
}
}
}
}
}
}
}
},
"get": {
"summary": "\nA search method to illustrate how you can define a complex request.\nIn the case of the methods 'get' and 'search' the parameters are\npassed to the server as the query part of the service URL.\n",
"parameters": [
{
"name": "limit",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": 50,
"schema": {
"type": "integer"
}
},
{
"name": "offset",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": 0,
"schema": {
"type": "integer"
}
},
{
"name": "param_required",
"in": "query",
"required": true,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "string"
}
},
{
"name": "param_string",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "string"
}
},
{
"name": "params[]",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
],
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"response": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/{id}/delete": {
"post": {
"summary": "\nDelete method description ...\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {}
}
}
}
},
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"response": {
"type": "string"
}
}
}
}
}
}
}
},
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
]
},
"/{id}": {
"delete": {
"summary": "\nDelete method description ...\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {}
}
}
}
},
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"response": {
"type": "string"
}
}
}
}
}
}
}
},
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"get": {
"summary": "\nThis method is used to get the information of the object specified\nby Id.\n",
"parameters": [
{
"name": "message",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "string"
}
}
],
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"message": {
"type": "string"
},
"id": {
"type": "integer"
}
}
}
}
}
}
}
},
"post": {
"summary": "\nUpdate method description ...\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
},
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"response": {
"type": "string"
}
}
}
}
}
}
}
},
"put": {
"summary": "\nUpdate method description ...\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
},
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"response": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/{id}/get": {
"get": {
"summary": "\nThis method is used to get the information of the object specified\nby Id.\n",
"parameters": [
{
"name": "message",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "string"
}
}
],
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"message": {
"type": "string"
},
"id": {
"type": "integer"
}
}
}
}
}
}
}
},
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
]
},
"/search": {
"get": {
"summary": "\nA search method to illustrate how you can define a complex request.\nIn the case of the methods 'get' and 'search' the parameters are\npassed to the server as the query part of the service URL.\n",
"parameters": [
{
"name": "limit",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": 50,
"schema": {
"type": "integer"
}
},
{
"name": "offset",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": 0,
"schema": {
"type": "integer"
}
},
{
"name": "param_required",
"in": "query",
"required": true,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "string"
}
},
{
"name": "param_string",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "string"
}
},
{
"name": "params[]",
"in": "query",
"required": false,
"allowEmptyValue": false,
"default": null,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
],
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"response": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/{id}/update": {
"post": {
"summary": "\nUpdate method description ...\n",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
},
"responses": {
"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."
},
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [],
"properties": {
"response": {
"type": "string"
}
}
}
}
}
}
}
},
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
]
}
},
"openapi": "3.0.0",
"components": {
"securitySchemes": {
"user": {
"type": "apiKey",
"in": "cookie",
"name": "session_id"
}
}
}
}

View file

@ -0,0 +1,32 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
# from odoo.http import controllers_per_module
from odoo.http import Controller
from ..controllers.main import (
BaseRestDemoJwtApiController,
BaseRestDemoNewApiController,
BaseRestDemoPrivateApiController,
BaseRestDemoPublicApiController,
)
from .common import CommonCase
class TestController(CommonCase):
def test_controller_registry(self):
# at the end of the start process, our tow controllers must into the
# controller registered
controllers = Controller.children_classes.get("base_rest_demo", [])
self.assertTrue(
any([issubclass(x, BaseRestDemoPrivateApiController) for x in controllers])
)
self.assertTrue(
any([issubclass(x, BaseRestDemoPublicApiController) for x in controllers])
)
self.assertTrue(
any([issubclass(x, BaseRestDemoNewApiController) for x in controllers])
)
self.assertTrue(
any([issubclass(x, BaseRestDemoJwtApiController) for x in controllers])
)

View file

@ -0,0 +1,120 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import json
import odoo.tools
from odoo.tests import HttpCase
from odoo.tests.common import tagged
from odoo.addons.base_rest.tests.common import RegistryMixin
@tagged("-at_install", "post_install")
class TestException(HttpCase, RegistryMixin):
@classmethod
def setUpClass(cls):
super(TestException, cls).setUpClass()
cls.setUpRegistry()
host = "127.0.0.1"
port = odoo.tools.config["http_port"]
cls.url = "http://%s:%d/base_rest_demo_api/public/exception" % (host, port)
def setUp(self):
super(TestException, self).setUp()
self.opener.headers["Content-Type"] = "application/json"
@odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http")
def test_user_error(self):
response = self.url_open(
"%s/user_error" % self.url,
"{}",
headers={"Accept-language": "en-US,en;q=0.5"},
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.headers["content-type"], "application/json")
body = json.loads(response.content.decode("utf-8"))
self.assertDictEqual(
body,
{
"code": 400,
"name": "Bad Request",
"description": "<p>UserError message</p>",
},
)
@odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http")
def test_validation_error(self):
response = self.url_open("%s/validation_error" % self.url, "{}")
self.assertEqual(response.status_code, 400)
self.assertEqual(response.headers["content-type"], "application/json")
body = json.loads(response.content.decode("utf-8"))
self.assertDictEqual(
body,
{
"code": 400,
"name": "Bad Request",
"description": "<p>ValidationError message</p>",
},
)
@odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http")
def test_session_expired(self):
response = self.url_open("%s/session_expired" % self.url, "{}")
self.assertEqual(response.status_code, 401)
self.assertEqual(response.headers["content-type"], "application/json")
body = json.loads(response.content.decode("utf-8"))
self.assertDictEqual(body, {"code": 401, "name": "Unauthorized"})
@odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http")
def test_missing_error(self):
response = self.url_open("%s/missing_error" % self.url, "{}")
self.assertEqual(response.status_code, 404)
self.assertEqual(response.headers["content-type"], "application/json")
body = json.loads(response.content.decode("utf-8"))
self.assertDictEqual(body, {"code": 404, "name": "Not Found"})
@odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http")
def test_access_error(self):
response = self.url_open("%s/access_error" % self.url, "{}")
self.assertEqual(response.status_code, 403)
self.assertEqual(response.headers["content-type"], "application/json")
body = json.loads(response.content.decode("utf-8"))
self.assertDictEqual(body, {"code": 403, "name": "Forbidden"})
@odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http")
def test_access_denied(self):
response = self.url_open("%s/access_denied" % self.url, "{}")
self.assertEqual(response.status_code, 403)
self.assertEqual(response.headers["content-type"], "application/json")
body = json.loads(response.content.decode("utf-8"))
self.assertDictEqual(body, {"code": 403, "name": "Forbidden"})
@odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http")
def test_http_exception(self):
response = self.url_open("%s/http_exception" % self.url, "{}")
self.assertEqual(response.status_code, 405)
self.assertEqual(response.headers["content-type"], "text/html; charset=utf-8")
body = response.content
self.assertIn(b"Method Not Allowed", body)
@odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http")
def test_bare_exception(self):
response = self.url_open("%s/bare_exception" % self.url, "{}")
self.assertEqual(response.status_code, 500)
self.assertEqual(response.headers["content-type"], "application/json")
body = json.loads(response.content.decode("utf-8"))
self.assertDictEqual(body, {"code": 500, "name": "Internal Server Error"})
@odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http")
def test_retrying(self):
"""Test that the retrying mechanism is working as expected with the
FastAPI endpoints in case of POST request with a file.
"""
nbr_retries = 3
response = self.url_open(
"%s/retryable_error" % self.url,
'{"nbr_retries": %d}' % nbr_retries,
timeout=20000,
)
self.assertEqual(response.status_code, 200, response.content)
self.assertDictEqual(response.json(), {"retries": nbr_retries})

View file

@ -0,0 +1,50 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import jsondiff
from .common import CommonCase, get_canonical_json
class TestOpenAPI(CommonCase):
def _fix_server_url(self, openapi_def):
# The server url depends of base_url. base_url depends of the odoo
# config
url = openapi_def["servers"][0]["url"]
url = url.replace("http://localhost:8069", self.base_url)
openapi_def["servers"][0]["url"] = url
def _fix_openapi_components(self, openapi_def):
"""
Remove additional components that could be added by others addons than
base_rest
"""
security_components = openapi_def.get("components", {}).get(
"securitySchemes", {}
)
unknow_keys = set(security_components.keys()) - {"user"}
for key in unknow_keys:
del security_components[key]
def assertOpenApiDef(self, service, canonical_json_file, default_auth):
openapi_def = service.to_openapi(default_auth=default_auth)
self._fix_openapi_components(openapi_def)
canonical_def = get_canonical_json(canonical_json_file)
self._fix_server_url(canonical_def)
self.assertFalse(jsondiff.diff(openapi_def, canonical_def))
def test_partner_api(self):
service = self.private_services_env.component(usage="partner")
self.assertOpenApiDef(service, "partner_api.json", "user")
def test_ping_api(self):
service = self.public_services_env.component(usage="ping")
self.assertOpenApiDef(service, "ping_api.json", "public")
def test_partner_image_api(self):
service = self.private_services_env.component(usage="partner_image")
self.assertOpenApiDef(service, "partner_image_api.json", "user")
def test_partner_pydantic_api(self):
service = self.new_api_services_env.component(usage="partner_pydantic")
self.assertOpenApiDef(service, "partner_pydantic_api.json", "public")

View file

@ -0,0 +1,54 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import json
from urllib.parse import quote
import odoo.tools
from odoo.tests import HttpCase
from odoo.tests.common import tagged
from odoo.addons.base_rest.tests.common import RegistryMixin
@tagged("-at_install", "post_install")
class TestService(HttpCase, RegistryMixin):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.setUpRegistry()
host = "127.0.0.1"
port = odoo.tools.config["http_port"]
cls.url = "http://%s:%d/base_rest_demo_api/new_api/partner" % (host, port)
cls.partner = cls.env.ref("base.partner_demo")
# Define a subset of the partner values to check against returned payload
cls.expected_partner_values = {
"country": {
"id": cls.partner.country_id.id,
"name": cls.partner.country_id.name,
},
"id": cls.partner.id,
"name": cls.partner.name,
"state": {"id": cls.partner.state_id.id, "name": cls.partner.state_id.name},
}
def test_get(self):
"""Test a new api GET method"""
self.authenticate("admin", "admin")
self.opener.headers["Content-Type"] = "application/json"
response = self.url_open(
"%s/%s" % (self.url, self.partner.id),
headers={"Accept-language": "en-US,en;q=0.5"},
)
body = json.loads(response.content.decode("utf-8"))
self.assertEqual(body, body | self.expected_partner_values)
def test_get_by_name(self):
"""Test a new api GET method with string argument"""
self.authenticate("admin", "admin")
self.opener.headers["Content-Type"] = "application/json"
response = self.url_open(
"%s/%s" % (self.url, quote(self.partner.name)),
headers={"Accept-language": "en-US,en;q=0.5"},
)
body = json.loads(response.content.decode("utf-8"))
self.assertEqual(body, body | self.expected_partner_values)

View file

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

View file

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

View file

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

View file

@ -0,0 +1,10 @@
# Dependencies
This addon depends on:
- [base_rest](../../odoo-bringout-oca-rest-framework-base_rest)
- [base_rest_datamodel](../../odoo-bringout-oca-rest-framework-base_rest_datamodel)
- [base_rest_pydantic](../../odoo-bringout-oca-rest-framework-base_rest_pydantic)
- [component](../../odoo-bringout-oca-connector-component)
- [extendable](../../odoo-bringout-oca-rest-framework-extendable)
- [pydantic](../../odoo-bringout-oca-rest-framework-pydantic)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,48 @@
[project]
name = "odoo-bringout-oca-rest-framework-base_rest_demo"
version = "16.0.0"
description = "Base Rest Demo -
Demo addon for Base REST"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-rest-framework-base_rest>=16.0.0",
"odoo-bringout-oca-rest-framework-base_rest_datamodel>=16.0.0",
"odoo-bringout-oca-rest-framework-base_rest_pydantic>=16.0.0",
"odoo-bringout-oca-rest-framework-component>=16.0.0",
"odoo-bringout-oca-rest-framework-extendable>=16.0.0",
"odoo-bringout-oca-rest-framework-pydantic>=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_demo"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]