diff --git a/endpoint/README.rst b/endpoint/README.rst index 844a1f99..eb4b64b5 100644 --- a/endpoint/README.rst +++ b/endpoint/README.rst @@ -82,6 +82,9 @@ Contributors ------------ - Simone Orsi +- `Quartile `__: + + - Yoshi Tashiro Maintainers ----------- diff --git a/endpoint/controllers/main.py b/endpoint/controllers/main.py index afae8a10..269094c5 100644 --- a/endpoint/controllers/main.py +++ b/endpoint/controllers/main.py @@ -9,6 +9,7 @@ from odoo import http from odoo.http import Response, request +from odoo.tools.json import json_default class EndpointControllerMixin: @@ -28,12 +29,21 @@ def _handle_result(self, result): payload = result.get("payload", "") status = result.get("status_code", 200) headers = result.get("headers", {}) - return self._make_json_response(payload, headers=headers, status=status) + return self._make_json_response( + payload, + headers=headers, + status=status, + json_default=result.get("json_default"), + ) # TODO: probably not needed anymore as controllers are automatically registered def _make_json_response(self, payload, headers=None, status=200, **kw): # TODO: guess out type? - data = json.dumps(payload) + # An endpoint can pass its own encoder hook, which then replaces Odoo's + # for the whole payload: it is expected to delegate to json_default for + # the types it does not render itself. + default = kw.get("json_default") or json_default + data = json.dumps(payload, default=default) if headers is None: headers = {} headers["Content-Type"] = "application/json" diff --git a/endpoint/demo/endpoint_demo.xml b/endpoint/demo/endpoint_demo.xml index 290cfe30..aa8a57a2 100644 --- a/endpoint/demo/endpoint_demo.xml +++ b/endpoint/demo/endpoint_demo.xml @@ -81,4 +81,19 @@ result = {"response": Response(request.params.get("your_name", ""))} result = {"payload": "Method used:" + request.httprequest.method} + + + Demo Endpoint 8 + /demo/native_types + GET + public + + code + +result = {"payload": { + "a_date": datetime.date(2026, 1, 15), + "a_datetime": datetime.datetime(2026, 1, 15, 10, 30, 0), +}} + + diff --git a/endpoint/readme/CONTRIBUTORS.md b/endpoint/readme/CONTRIBUTORS.md index 2b66303a..669019e0 100644 --- a/endpoint/readme/CONTRIBUTORS.md +++ b/endpoint/readme/CONTRIBUTORS.md @@ -1 +1,3 @@ - Simone Orsi \<\> +- [Quartile](https://www.quartile.co): + - Yoshi Tashiro diff --git a/endpoint/static/description/index.html b/endpoint/static/description/index.html index 9dc3dc30..f7af53ba 100644 --- a/endpoint/static/description/index.html +++ b/endpoint/static/description/index.html @@ -428,6 +428,10 @@

Authors

Contributors

diff --git a/endpoint/tests/test_endpoint.py b/endpoint/tests/test_endpoint.py index 4a1b3e82..9827b31a 100644 --- a/endpoint/tests/test_endpoint.py +++ b/endpoint/tests/test_endpoint.py @@ -4,14 +4,18 @@ import json import textwrap +from datetime import date from unittest import mock import psycopg2 import werkzeug from odoo import exceptions +from odoo.http import Response from odoo.tools.misc import mute_logger +from odoo.addons.endpoint.controllers.main import EndpointController + from .common import CommonEndpoint @@ -247,3 +251,27 @@ def test_registry_sync(self): def test_duplicate(self): endpoint = self.endpoint.copy() self.assertTrue(endpoint.route.endswith("/COPY_FIXME")) + + def _json_response(self, result): + """Render a result through the controller, as a request would.""" + with self._get_mocked_request() as req: + req.make_response = lambda data, **kw: Response(data, **kw) + return json.loads(EndpointController()._handle_result(result).data) + + def test_handle_result_json_default(self): + """A result can carry its own encoder for values json cannot render. + + The result dict is the only channel available: the controller sees + what the endpoint returned, not the endpoint itself. + """ + payload = {"val": date(2026, 1, 15)} + self.assertEqual( + self._json_response( + {"payload": payload, "json_default": lambda val: "hooked"} + ), + {"val": "hooked"}, + ) + # Without a hook, values fall back to Odoo's own encoder. + self.assertEqual( + self._json_response({"payload": payload}), {"val": "2026-01-15"} + ) diff --git a/endpoint/tests/test_endpoint_controller.py b/endpoint/tests/test_endpoint_controller.py index fb5e84fd..7ac38aa1 100644 --- a/endpoint/tests/test_endpoint_controller.py +++ b/endpoint/tests/test_endpoint_controller.py @@ -77,3 +77,15 @@ def test_call6(self): def test_call7(self): response = self.url_open("/demo/bad_method", data="ok") self.assertEqual(response.status_code, 405) + + def test_call_payload_native_types(self): + """Values the plain json encoder cannot handle must not break a payload. + + Dates come from any record field, and reach a payload whenever a + snippet passes a field value through. + """ + response = self.url_open("/demo/native_types") + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode()) + self.assertEqual(data["a_date"], "2026-01-15") + self.assertEqual(data["a_datetime"], "2026-01-15 10:30:00")