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
21 changes: 21 additions & 0 deletions endpoint/models/endpoint_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ class EndpointMixin(models.AbstractModel):
)
exec_as_user_id = fields.Many2one(comodel_name="res.users")
company_id = fields.Many2one("res.company", string="Company")
full_url_display = fields.Char(
compute="_compute_full_url_display",
help="Absolute URL of this endpoint, ready to copy/paste. Resolved "
"from the current request when available (matching what you're "
"actually browsing), otherwise falls back to the 'web.base.url' "
"system parameter.",
)

def _selection_exec_mode(self):
return [("code", "Execute code")]
Expand All @@ -63,6 +70,20 @@ def _compute_code_snippet_docs(self):
for rec in self:
rec.code_snippet_docs = textwrap.dedent(rec._default_code_snippet_docs())

@api.depends("route")
def _compute_full_url_display(self):
base_url = self._get_current_base_url()
for rec in self:
rec.full_url_display = f"{base_url}{rec.route}" if rec.route else False

def _get_current_base_url(self):
# Prefer the host the caller is actually browsing on: `web.base.url`
# is a manually maintained system parameter and easily out of sync
# (wrong scheme, stale domain after a move, ...).
if http.request:
return http.request.httprequest.url_root.rstrip("/")
return self.env["ir.config_parameter"].sudo().get_param("web.base.url")

@api.constrains("exec_mode")
def _check_exec_mode(self):
for rec in self:
Expand Down
23 changes: 23 additions & 0 deletions endpoint/tests/test_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,29 @@ def test_duplicate(self):
endpoint = self.endpoint.copy()
self.assertTrue(endpoint.route.endswith("/COPY_FIXME"))

def test_full_url_display_from_request(self):
# Prefer the current request's own host over `web.base.url`.
self.endpoint.invalidate_recordset(["full_url_display"])
with self._get_mocked_request(
httprequest={"url_root": "https://mocked.example.com/"}
):
self.assertEqual(
self.endpoint.full_url_display,
f"https://mocked.example.com{self.endpoint.route}",
)

def test_full_url_display_fallback_to_web_base_url(self):
# No request in context (e.g. a cron or a shell session): fall back
# to the `web.base.url` system parameter.
self.env["ir.config_parameter"].sudo().set_param(
"web.base.url", "https://fallback.example.com"
)
self.endpoint.invalidate_recordset(["full_url_display"])
self.assertEqual(
self.endpoint.full_url_display,
f"https://fallback.example.com{self.endpoint.route}",
)

def _json_response(self, result):
"""Render a result through the controller, as a request would."""
with self._get_mocked_request() as req:
Expand Down
8 changes: 8 additions & 0 deletions endpoint/views/endpoint_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
invisible="active"
/>
<sheet>
<div class="alert alert-info py-1" role="alert" invisible="not id">
<field
name="full_url_display"
nolabel="1"
readonly="1"
widget="CopyClipboardURL"
/>
</div>
<div
class="alert alert-warning"
role="alert"
Expand Down
6 changes: 5 additions & 1 deletion webservice_core/models/webservice_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# @author Simone Orsi <simahawk@gmail.com>
# @author Alexandre Fayolle <alexandre.fayolle@camptocamp.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo import api, fields, models


class WebserviceBackend(models.Model):
Expand All @@ -29,6 +29,10 @@ class WebserviceBackend(models.Model):
def _get_base_url(self):
return self.url

@api.depends("url")
def _compute_full_url_display(self):
return super()._compute_full_url_display()

def _compute_endpoint_count(self):
data = self.env["webservice.endpoint"]._read_group(
[("backend_id", "in", self.ids)], ["backend_id"], ["__count"]
Expand Down
7 changes: 7 additions & 0 deletions webservice_core/models/webservice_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ def _compute_display_name(self):
def _get_base_url(self):
return self.backend_id.url

def _get_display_url(self):
return self._compose_url(self.path)

@api.depends("path", "backend_id.url")
def _compute_full_url_display(self):
return super()._compute_full_url_display()

def _call_prepare(self, **kwargs):
# Precedence: call-time kwarg > endpoint config > backend config.
# Each `_call_prepare` lets existing `kwargs` win over its own
Expand Down
41 changes: 32 additions & 9 deletions webservice_core/models/webservice_request_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ class WebserviceRequestMixin(models.AbstractModel):
"An explicit `timeout` kwarg passed to `call()` wins over this. "
"Left empty, the call waits indefinitely.",
)
full_url_display = fields.Char(
compute="_compute_full_url_display",
help="Best-effort absolute URL, ready to copy/paste. "
"Any '{placeholder}' token is left unresolved: fill it in before use.",
)
header_ids = fields.One2many(
"webservice.header",
"res_id",
Expand Down Expand Up @@ -276,19 +281,37 @@ def _get_headers_for_api_key(self, **kw):
return {self.api_key_header: self.api_key}

def _get_url(self, url=None, url_params=None, **kwargs):
url = self._compose_url(url)
url_params = url_params or kwargs
return url.format(**url_params)

def _compose_url(self, url=None):
base = self._get_base_url()
if not url:
url = base
elif not url.startswith(base):
return base
if not url.startswith(base):
if not url.startswith("http"):
url = f"{base.rstrip('/')}/{url.lstrip('/')}"
else:
# TODO: if url is given, we should validate the domain
# to avoid abusing a webservice backend for different calls.
pass
return f"{base.rstrip('/')}/{url.lstrip('/')}"
# TODO: if url is given, we should validate the domain
# to avoid abusing a webservice backend for different calls.
return url

url_params = url_params or kwargs
return url.format(**url_params)
@api.depends()
def _compute_full_url_display(self):
for rec in self:
rec.full_url_display = rec._get_display_url()

def _get_display_url(self):
"""Best-effort absolute URL for display/copy in the UI.

Unlike ``_get_url()``, ``{placeholder}`` tokens are never resolved
here: there is no call-time value to fill them with, so they are
left as-is for the user to replace.

Models with a relative part of their own (e.g. an endpoint's
``path``) should override this to compose it with ``_compose_url()``.
"""
return self._get_base_url()

def _resolve_query_params(self, params, url_params):
url_params = url_params or {}
Expand Down
3 changes: 3 additions & 0 deletions webservice_core/tests/test_webservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def _setup_records(cls):
)
return res

def test_full_url_display(self):
self.assertEqual(self.webservice.full_url_display, self.url)

def test_web_service_not_found(self):
with self.assertRaises(http_exceptions.ConnectionError):
self.webservice.call("get")
Expand Down
17 changes: 17 additions & 0 deletions webservice_core/tests/test_webservice_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,20 @@ def test_timeout_call_kwarg_overrides_endpoint(self):
with mock.patch("requests.request") as mocked_request:
self.endpoint.call(url_params={"order_id": 42}, timeout=1.0)
self.assertEqual(self._get_requests_timeout(mocked_request), 1.0)

def test_full_url_display(self):
# `path` has an unresolved `{order_id}` placeholder: left as-is.
self.assertEqual(
self.endpoint.full_url_display, f"{self.url}orders/{{order_id}}/status"
)

def test_full_url_display_no_path(self):
endpoint = self.endpoint.copy({"tech_name": "no_path", "path": False})
self.assertEqual(endpoint.full_url_display, self.url)

def test_full_url_display_updates_with_backend_url(self):
self.backend.url = "https://other.demo.odoo/"
self.assertEqual(
self.endpoint.full_url_display,
"https://other.demo.odoo/orders/{order_id}/status",
)
12 changes: 12 additions & 0 deletions webservice_core/views/webservice_backend.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
<form>
<header />
<sheet>
<div
class="alert alert-info py-1"
role="alert"
invisible="not full_url_display"
>
<field
name="full_url_display"
nolabel="1"
readonly="1"
widget="CopyClipboardURL"
/>
</div>
<div class="oe_button_box" name="button_box">
<button
type="object"
Expand Down
12 changes: 12 additions & 0 deletions webservice_core/views/webservice_endpoint.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
<field name="arch" type="xml">
<form>
<sheet>
<div
class="alert alert-info py-1"
role="alert"
invisible="not full_url_display"
>
<field
name="full_url_display"
nolabel="1"
readonly="1"
widget="CopyClipboardURL"
/>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only" />
<h1>
Expand Down
Loading