Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions endpoint/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ Contributors
- Simone Orsi <simone.orsi@camptocamp.com>
- Iván Todorovich <ivan.todorovich@camptocamp.com>
- Alex Garcia <alex@studio73.es>
- `Quartile <https://www.quartile.co>`__:

- Yoshi Tashiro

Maintainers
-----------
Expand Down
14 changes: 12 additions & 2 deletions endpoint/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from odoo import http
from odoo.http import Response, request
from odoo.tools.json import json_default


class EndpointControllerMixin:
Expand All @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions endpoint/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
- Simone Orsi \<<simone.orsi@camptocamp.com>\>
- Iván Todorovich \<<ivan.todorovich@camptocamp.com>\>
- Alex Garcia \<<alex@studio73.es>\>
- [Quartile](https://www.quartile.co):
- Yoshi Tashiro
4 changes: 4 additions & 0 deletions endpoint/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,10 @@ <h3><a class="toc-backref" href="#toc-entry-12">Contributors</a></h3>
<li>Simone Orsi &lt;<a class="reference external" href="mailto:simone.orsi&#64;camptocamp.com">simone.orsi&#64;camptocamp.com</a>&gt;</li>
<li>Iván Todorovich &lt;<a class="reference external" href="mailto:ivan.todorovich&#64;camptocamp.com">ivan.todorovich&#64;camptocamp.com</a>&gt;</li>
<li>Alex Garcia &lt;<a class="reference external" href="mailto:alex&#64;studio73.es">alex&#64;studio73.es</a>&gt;</li>
<li><a class="reference external" href="https://www.quartile.co">Quartile</a>:<ul>
<li>Yoshi Tashiro</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
Expand Down
17 changes: 17 additions & 0 deletions endpoint/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,23 @@ def _setup_demo_records(env):
),
}
)
endpoints += env["endpoint.endpoint"].create(
{
"name": "Demo Endpoint 10",
"route": "/demo/native_types",
"request_method": "GET",
"auth_type": "public",
"exec_as_user_id": demo_user.id,
"exec_mode": "code",
"code_snippet": (
'result = {"payload": {'
'"rule_domain": env["ir.rule"]._compute_domain("res.partner"), '
'"a_date": datetime.date(2026, 1, 15), '
'"a_datetime": datetime.datetime(2026, 1, 15, 10, 30, 0)'
"}}"
),
}
)
return endpoints


Expand Down
28 changes: 28 additions & 0 deletions endpoint/tests/test_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -249,3 +253,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"}
)
13 changes: 13 additions & 0 deletions endpoint/tests/test_endpoint_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,16 @@ 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.

A Domain lands in a payload whenever a snippet passes an ORM helper's
return value through; dates come from any record field.
"""
response = self.url_open("/demo/native_types")
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode())
self.assertIsInstance(data["rule_domain"], list)
self.assertEqual(data["a_date"], "2026-01-15")
self.assertEqual(data["a_datetime"], "2026-01-15 10:30:00")
Loading