From 566cc64d987706c4db1b904fff463e4d49065776 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 13 Sep 2021 16:08:22 +0200 Subject: [PATCH 01/58] Add endpoint module --- endpoint/README.rst | 1 + endpoint/__init__.py | 2 + endpoint/__manifest__.py | 18 + endpoint/controllers/__init__.py | 1 + endpoint/controllers/main.py | 54 +++ endpoint/demo/endpoint_demo.xml | 86 +++++ endpoint/models/__init__.py | 3 + endpoint/models/endpoint_endpoint.py | 9 + endpoint/models/endpoint_mixin.py | 379 ++++++++++++++++++ endpoint/models/ir_http.py | 103 +++++ endpoint/readme/CONFIGURE.rst | 1 + endpoint/readme/CONTRIBUTORS.rst | 1 + endpoint/readme/DESCRIPTION.rst | 5 + endpoint/readme/ROADMAP.rst | 2 + endpoint/security/ir.model.access.csv | 2 + endpoint/static/description/icon.png | Bin 0 -> 9455 bytes endpoint/static/description/index.html | 425 +++++++++++++++++++++ endpoint/tests/__init__.py | 2 + endpoint/tests/common.py | 51 +++ endpoint/tests/test_endpoint.py | 163 ++++++++ endpoint/tests/test_endpoint_controller.py | 62 +++ endpoint/utils.py | 68 ++++ endpoint/views/endpoint_view.xml | 114 ++++++ 23 files changed, 1552 insertions(+) create mode 100644 endpoint/README.rst create mode 100644 endpoint/__init__.py create mode 100644 endpoint/__manifest__.py create mode 100644 endpoint/controllers/__init__.py create mode 100644 endpoint/controllers/main.py create mode 100644 endpoint/demo/endpoint_demo.xml create mode 100644 endpoint/models/__init__.py create mode 100644 endpoint/models/endpoint_endpoint.py create mode 100644 endpoint/models/endpoint_mixin.py create mode 100644 endpoint/models/ir_http.py create mode 100644 endpoint/readme/CONFIGURE.rst create mode 100644 endpoint/readme/CONTRIBUTORS.rst create mode 100644 endpoint/readme/DESCRIPTION.rst create mode 100644 endpoint/readme/ROADMAP.rst create mode 100644 endpoint/security/ir.model.access.csv create mode 100644 endpoint/static/description/icon.png create mode 100644 endpoint/static/description/index.html create mode 100644 endpoint/tests/__init__.py create mode 100644 endpoint/tests/common.py create mode 100644 endpoint/tests/test_endpoint.py create mode 100644 endpoint/tests/test_endpoint_controller.py create mode 100644 endpoint/utils.py create mode 100644 endpoint/views/endpoint_view.xml diff --git a/endpoint/README.rst b/endpoint/README.rst new file mode 100644 index 00000000..89bcd6c2 --- /dev/null +++ b/endpoint/README.rst @@ -0,0 +1 @@ +wait for the bot ;) diff --git a/endpoint/__init__.py b/endpoint/__init__.py new file mode 100644 index 00000000..91c5580f --- /dev/null +++ b/endpoint/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py new file mode 100644 index 00000000..5284bbfc --- /dev/null +++ b/endpoint/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2021 Camptcamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Endpoint", + "summary": """Provide custom endpoint machinery.""", + "version": "14.0.1.0.0", + "license": "LGPL-3", + "development_status": "Alpha", + "author": "Camptocamp,Odoo Community Association (OCA)", + "maintainers": ["simahawk"], + "website": "https://github.com/OCA/edi", + "data": [ + "security/ir.model.access.csv", + "demo/endpoint_demo.xml", + "views/endpoint_view.xml", + ], +} diff --git a/endpoint/controllers/__init__.py b/endpoint/controllers/__init__.py new file mode 100644 index 00000000..12a7e529 --- /dev/null +++ b/endpoint/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/endpoint/controllers/main.py b/endpoint/controllers/main.py new file mode 100644 index 00000000..615ca23e --- /dev/null +++ b/endpoint/controllers/main.py @@ -0,0 +1,54 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +import json + +from werkzeug.exceptions import NotFound + +from odoo import http +from odoo.http import Response, request + + +class EndpointControllerMixin: + def _handle_endpoint(self, env, endpoint_route, **params): + endpoint = self._find_endpoint(env, endpoint_route) + if not endpoint: + raise NotFound() + endpoint._validate_request(request) + result = endpoint._handle_request(request) + return self._handle_result(result) + + def _handle_result(self, result): + response = result.get("response") + if isinstance(response, Response): + # Full response already provided + return response + payload = result.get("payload", "") + status = result.get("status_code", 200) + headers = result.get("headers", {}) + return self._make_json_response(payload, headers=headers, status=status) + + # 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) + if headers is None: + headers = {} + headers["Content-Type"] = "application/json" + resp = request.make_response(data, headers=headers) + resp.status = str(status) + return resp + + def _find_endpoint(self, env, endpoint_route): + return env["endpoint.endpoint"]._find_endpoint(endpoint_route) + + def auto_endpoint(self, endpoint_route, **params): + """Default method to handle auto-generated endpoints""" + env = request.env + return self._handle_endpoint(env, endpoint_route, **params) + + +class EndpointController(http.Controller, EndpointControllerMixin): + pass diff --git a/endpoint/demo/endpoint_demo.xml b/endpoint/demo/endpoint_demo.xml new file mode 100644 index 00000000..0d6e6ffd --- /dev/null +++ b/endpoint/demo/endpoint_demo.xml @@ -0,0 +1,86 @@ + + + + + Demo Endpoint 1 + /demo/one + GET + code + +result = {"response": Response("ok")} + + + + + Demo Endpoint 2 + /demo/as_demo_user + GET + public + + code + +result = {"response": Response("My name is: " + user.name)} + + + + + Demo Endpoint 3 + /demo/json_data + GET + public + + code + +result = {"payload": {"a": 1, "b": 2}} + + + + + Demo Endpoint 4 + /demo/raise_not_found + GET + public + + code + +raise werkzeug.exceptions.NotFound() + + + + + Demo Endpoint 5 + /demo/raise_validation_error + GET + public + + code + +raise exceptions.ValidationError("Sorry, you cannot do this!") + + + + + Demo Endpoint 6 + /demo/value_from_request + GET + public + + code + +result = {"response": Response(request.params.get("your_name", ""))} + + + + + Demo Endpoint 7 + /demo/bad_method + GET + code + public + + +result = {"payload": "Method used:" + request.httprequest.method} + + + + diff --git a/endpoint/models/__init__.py b/endpoint/models/__init__.py new file mode 100644 index 00000000..edd1b58b --- /dev/null +++ b/endpoint/models/__init__.py @@ -0,0 +1,3 @@ +from . import endpoint_mixin +from . import endpoint_endpoint +from . import ir_http diff --git a/endpoint/models/endpoint_endpoint.py b/endpoint/models/endpoint_endpoint.py new file mode 100644 index 00000000..a7efb6ed --- /dev/null +++ b/endpoint/models/endpoint_endpoint.py @@ -0,0 +1,9 @@ +from odoo import models + + +class EndpointEndpoint(models.Model): + """Define a custom endpoint.""" + + _name = "endpoint.endpoint" + _inherit = "endpoint.mixin" + _description = "Endpoint" diff --git a/endpoint/models/endpoint_mixin.py b/endpoint/models/endpoint_mixin.py new file mode 100644 index 00000000..63b02ee9 --- /dev/null +++ b/endpoint/models/endpoint_mixin.py @@ -0,0 +1,379 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging +import textwrap +from functools import partial + +import werkzeug +from werkzeug.routing import Rule + +from odoo import _, api, exceptions, fields, http, models +from odoo.tools import safe_eval + +from odoo.addons.base_sparse_field.models.fields import Serialized + +from ..controllers.main import EndpointController +from ..utils import endpoint_registry + + +class EndpointMixin(models.AbstractModel): + + _name = "endpoint.mixin" + _description = "Endpoint mixin" + + active = fields.Boolean(default=True) + name = fields.Char(required=True) + route = fields.Char( + required=True, + index=True, + compute="_compute_route", + inverse="_inverse_route", + readonly=False, + store=True, + copy=False, + ) + route_type = fields.Selection(selection="_selection_route_type", default="http") + auth_type = fields.Selection( + selection="_selection_auth_type", default="user_endpoint" + ) + + options = Serialized() + request_content_type = fields.Selection( + selection="_selection_request_content_type", sparse="options" + ) + request_method = fields.Selection( + selection="_selection_request_method", sparse="options", required=True + ) + # # TODO: validate params? Just for doc? Maybe use Cerberus? + # # -> For now let the implementer validate the params in the snippet. + # request_params = fields.Char(help="TODO", sparse="options") + + exec_mode = fields.Selection( + selection="_selection_exec_mode", + required=True, + ) + code_snippet = fields.Text() + code_snippet_docs = fields.Text( + compute="_compute_code_snippet_docs", + default=lambda self: self._default_code_snippet_docs(), + ) + exec_as_user_id = fields.Many2one(comodel_name="res.users") + + endpoint_hash = fields.Char(compute="_compute_endpoint_hash") + + _sql_constraints = [ + ( + "endpoint_route_unique", + "unique(route)", + "You can register an endpoint route only once.", + ) + ] + + @property + def _logger(self): + return logging.getLogger(self._name) + + def _selection_route_type(self): + return [("http", "HTTP"), ("json", "JSON")] + + def _selection_auth_type(self): + return [("public", "Public"), ("user_endpoint", "User")] + + def _selection_request_method(self): + return [ + ("GET", "GET"), + ("POST", "POST"), + ("PUT", "PUT"), + ("DELETE", "DELETE"), + ] + + def _selection_request_content_type(self): + return [ + ("", "None"), + ("text/plain", "Text"), + ("text/csv", "CSV"), + ("application/json", "JSON"), + ("application/xml", "XML"), + ("application/x-www-form-urlencoded", "Form"), + ] + + # TODO: Is this needed at all since we can cook full responses? + def _selection_response_content_type(self): + return [ + # TODO: how to get a complete list? + # OR: shall we leave the text free? + ("text/plain", "Plain text"), + ("application/json", "JSON"), + ("application/xml", "XML"), + ] + + def _selection_exec_mode(self): + return [("code", "Execute code")] + + def _compute_code_snippet_docs(self): + for rec in self: + rec.code_snippet_docs = textwrap.dedent(rec._default_code_snippet_docs()) + + @api.depends(lambda self: self._controller_fields()) + def _compute_endpoint_hash(self): + values = self.read(self._controller_fields()) + for rec, vals in zip(self, values): + vals.pop("id", None) + rec.endpoint_hash = hash(tuple(vals.values())) + + @api.depends("route") + def _compute_route(self): + for rec in self: + rec.route = rec._clean_route() + + def _inverse_route(self): + for rec in self: + rec.route = rec._clean_route() + + _endpoint_route_prefix = "" + """Prefix for all routes, includes slashes. + """ + + def _clean_route(self): + route = (self.route or "").strip() + if not route.startswith("/"): + route = "/" + route + prefix = self._endpoint_route_prefix + if prefix and not route.startswith(prefix): + route = prefix + route + return route + + _blacklist_routes = ("/", "/web") # TODO: what else? + + @api.constrains("route") + def _check_route(self): + for rec in self: + if rec.route in self._blacklist_routes: + raise exceptions.UserError( + _("`%s` uses a blacklisted routed = `%s`") % (rec.name, rec.route) + ) + + @api.constrains("exec_mode") + def _check_exec_mode(self): + for rec in self: + rec._validate_exec_mode() + + def _validate_exec_mode(self): + validator = getattr(self, "_validate_exec__" + self.exec_mode, lambda x: True) + validator() + + def _validate_exec__code(self): + if self.exec_mode == "code" and not self._code_snippet_valued(): + raise exceptions.UserError( + _("Exec mode is set to `Code`: you must provide a piece of code") + ) + + @api.constrains("request_method", "request_content_type") + def _check_request_method(self): + for rec in self: + if rec.request_method in ("POST", "PUT") and not rec.request_content_type: + raise exceptions.UserError( + _("Request method is required for POST and PUT.") + ) + + @api.constrains("auth_type") + def _check_auth(self): + for rec in self: + if rec.auth_type == "public" and not rec.exec_as_user_id: + raise exceptions.UserError( + _("'Exec as user' is mandatory for public endpoints.") + ) + + def _default_code_snippet_docs(self): + return """ + Available vars: + + * env + * endpoint + * request + * datetime + * dateutil + * time + * user + * json + * Response + * werkzeug + * exceptions + + Must generate either an instance of ``Response`` into ``response`` var or: + + * payload + * headers + * status_code + + which are all optional. + """ + + def _get_code_snippet_eval_context(self, request): + """Prepare the context used when evaluating python code + + :returns: dict -- evaluation context given to safe_eval + """ + return { + "env": self.env, + "user": self.env.user, + "endpoint": self, + "request": request, + "datetime": safe_eval.datetime, + "dateutil": safe_eval.dateutil, + "time": safe_eval.time, + "json": safe_eval.json, + "Response": http.Response, + "werkzeug": safe_eval.wrap_module( + werkzeug, {"exceptions": ["NotFound", "BadRequest", "Unauthorized"]} + ), + "exceptions": safe_eval.wrap_module( + exceptions, ["UserError", "ValidationError"] + ), + } + + def _get_handler(self): + try: + return getattr(self, "_handle_exec__" + self.exec_mode) + except AttributeError: + raise exceptions.UserError( + _("Missing handler for exec mode %s") % self.exec_mode + ) + + def _handle_exec__code(self, request): + if not self._code_snippet_valued(): + return {} + eval_ctx = self._get_code_snippet_eval_context(request) + snippet = self.code_snippet + safe_eval.safe_eval(snippet, eval_ctx, mode="exec", nocopy=True) + result = eval_ctx.get("result") + if not isinstance(result, dict): + raise exceptions.UserError( + _("code_snippet should return a dict into `result` variable.") + ) + return result + + def _code_snippet_valued(self): + snippet = self.code_snippet or "" + return bool( + [ + not line.startswith("#") + for line in (snippet.splitlines()) + if line.strip("") + ] + ) + + def _validate_request(self, request): + http_req = request.httprequest + # TODO: likely not needed anymore + if self.auth_type != "public" and not request.env.user: + raise werkzeug.exceptions.Unauthorized() + if self.request_method and self.request_method != http_req.method: + self._logger.error("_validate_request: MethodNotAllowed") + raise werkzeug.exceptions.MethodNotAllowed() + if ( + self.request_content_type + and self.request_content_type != http_req.content_type + ): + self._logger.error("_validate_request: UnsupportedMediaType") + raise werkzeug.exceptions.UnsupportedMediaType() + + def _handle_request(self, request): + # Switch user for the whole process + self_with_user = self + if self.exec_as_user_id: + self_with_user = self.with_user(user=self.exec_as_user_id) + handler = self_with_user._get_handler() + try: + res = handler(request) + except self._bad_request_exceptions() as orig_exec: + self._logger.error("_validate_request: BadRequest") + raise werkzeug.exceptions.BadRequest() from orig_exec + return res + + def _bad_request_exceptions(self): + return (exceptions.UserError, exceptions.ValidationError) + + @api.model + def _find_endpoint(self, endpoint_route): + return self.sudo().search(self._find_endpoint_domain(endpoint_route), limit=1) + + def _find_endpoint_domain(self, endpoint_route): + return [("route", "=", endpoint_route)] + + # Handle automatic route registration + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + if not self._abstract: + res._register_controllers() + return res + + def write(self, vals): + res = super().write(vals) + if not self._abstract and any([x in vals for x in self._controller_fields()]): + self._register_controllers() + return res + + def unlink(self): + if not self._abstract: + for rec in self: + rec._drop_controller_rule() + return super().unlink() + + def _controller_fields(self): + return ["route", "auth_type", "request_method"] + + def _register_hook(self): + super()._register_hook() + if not self._abstract: + self.search([])._register_controllers() + + def _register_controllers(self): + for rec in self: + rec._register_controller() + + def _register_controller(self): + rule = self._make_controller_rule() + self._add_or_update_controller_rule(rule) + self._logger.info( + "Registered controller %s (auth: %s)", self.route, self.auth_type + ) + + _endpoint_base_controller_class = EndpointController + + def _make_controller_rule(self): + route, routing = self._get_routing_info() + base_controller = self._endpoint_base_controller_class() + endpoint = http.EndPoint( + partial(base_controller.auto_endpoint, self.route), routing + ) + rule = Rule(route, endpoint=endpoint, methods=routing["methods"]) + rule.merge_slashes = False + rule._auto_endpoint = True + rule._endpoint_hash = self.endpoint_hash + return rule + + def _get_routing_info(self): + route = self.route + routing = dict( + type=self.route_type, + auth=self.auth_type, + methods=[self.request_method], + routes=[route], + # TODO: make this configurable + # in case the endpoint is used for frontend stuff. + csrf=False, + ) + return route, routing + + def _add_or_update_controller_rule(self, rule): + key = "{0._name}:{0.id}".format(self) + endpoint_registry.add_or_update_rule(key, rule) + + def _drop_controller_rule(self): + key = "{0._name}:{0.id}".format(self) + endpoint_registry.drop_rule(key) diff --git a/endpoint/models/ir_http.py b/endpoint/models/ir_http.py new file mode 100644 index 00000000..d7992c9b --- /dev/null +++ b/endpoint/models/ir_http.py @@ -0,0 +1,103 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +import werkzeug + +from odoo import http, models + +from ..utils import endpoint_registry + +_logger = logging.getLogger(__name__) + + +class IrHttp(models.AbstractModel): + _inherit = "ir.http" + + @classmethod + def routing_map(cls, key=None): + # Override to inject custom endpoint routes + rmap = super().routing_map(key=key) + if hasattr(cls, "_routing_map"): + if not hasattr(cls, "_endpoint_routing_map_loaded"): + # First load, register all endpoint routes + cls._load_endpoint_routing_map(rmap) + cls._endpoint_routing_map_loaded = True + elif endpoint_registry.routing_update_required(): + # Some endpoint changed, we must reload + cls._reload_endpoint_routing_map(rmap) + endpoint_registry.reset_update_required() + return rmap + + @classmethod + def _load_endpoint_routing_map(cls, rmap): + for rule in endpoint_registry.get_rules(): + if rule.endpoint not in rmap._rules_by_endpoint: + rmap.add(rule) + _logger.info("Endpoint routing map loaded") + # If you have to debug, ncomment to print all routes + # print("\n".join([x.rule for x in rmap._rules])) + + @classmethod + def _reload_endpoint_routing_map(cls, rmap): + """Reload endpoints routing map. + + Take care of removing obsolete ones and add new ones. + The match is done using the `_endpoint_hash`. + + Typical log entries in case of route changes: + + [...] endpoint.endpoint: Registered controller /demo/one/new (auth: public) + [...] odoo.addons.endpoint.models.ir_http: DROPPED /demo/one + [...] odoo.addons.endpoint.models.ir_http: LOADED /demo/one/new + [...] odoo.addons.endpoint.models.ir_http: Endpoint routing map re-loaded + + and then on subsequent calls: + + [...] GET /demo/one HTTP/1.1" 404 - 3 0.001 0.006 + [...] GET /demo/one/new HTTP/1.1" 200 - 6 0.001 0.005 + + You can look for such entries in logs + to check visually that a route has been updated + """ + to_update = endpoint_registry.get_rules_to_update() + to_load = to_update["to_load"] + to_drop = to_update["to_drop"] + hashes_to_drop = [x._endpoint_hash for x in to_drop] + remove_count = 0 + for i, rule in enumerate(rmap._rules[:]): + if ( + hasattr(rule, "_endpoint_hash") + and rule._endpoint_hash in hashes_to_drop + ): + if rule.endpoint in rmap._rules_by_endpoint: + rmap._rules.pop(i - remove_count) + rmap._rules_by_endpoint.pop(rule.endpoint) + remove_count += 1 + _logger.info("DROPPED %s", str(rule)) + continue + for rule in to_load: + if rule.endpoint not in rmap._rules_by_endpoint: + rmap.add(rule) + _logger.info("LOADED %s", str(rule)) + _logger.info("Endpoint routing map re-loaded") + + @classmethod + def _auth_method_user_endpoint(cls): + """Special method for user auth which raises Unauthorized when needed. + + If you get an HTTP request (instead of a JSON one), + the standard `user` method raises `SessionExpiredException` + when there's no user session. + This leads to a redirect to `/web/login` + which is not desiderable for technical endpoints. + + This method makes sure that no matter the type of request we get, + a proper exception is raised. + """ + try: + cls._auth_method_user() + except http.SessionExpiredException: + raise werkzeug.exceptions.Unauthorized() diff --git a/endpoint/readme/CONFIGURE.rst b/endpoint/readme/CONFIGURE.rst new file mode 100644 index 00000000..0dc96770 --- /dev/null +++ b/endpoint/readme/CONFIGURE.rst @@ -0,0 +1 @@ +Go to "Technical -> Endpoints" and create a new endpoint. diff --git a/endpoint/readme/CONTRIBUTORS.rst b/endpoint/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..f1c71bce --- /dev/null +++ b/endpoint/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Simone Orsi diff --git a/endpoint/readme/DESCRIPTION.rst b/endpoint/readme/DESCRIPTION.rst new file mode 100644 index 00000000..4fcfc952 --- /dev/null +++ b/endpoint/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +Provide an endpoint framework allowing users to define their own custom endpoint. + +Thanks to endpoint mixin the endpoint records are automatically registered as real Odoo routes. + +You can easily code what you want in the code snippet. diff --git a/endpoint/readme/ROADMAP.rst b/endpoint/readme/ROADMAP.rst new file mode 100644 index 00000000..85756037 --- /dev/null +++ b/endpoint/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* add validation of request data +* add api docs diff --git a/endpoint/security/ir.model.access.csv b/endpoint/security/ir.model.access.csv new file mode 100644 index 00000000..2bfbff5d --- /dev/null +++ b/endpoint/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_endpoint_endpoint_edit,endpoint_endpoint edit,model_endpoint_endpoint,base.group_system,1,1,1,1 diff --git a/endpoint/static/description/icon.png b/endpoint/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/endpoint/static/description/index.html b/endpoint/static/description/index.html new file mode 100644 index 00000000..aa3410eb --- /dev/null +++ b/endpoint/static/description/index.html @@ -0,0 +1,425 @@ + + + + + + +Endpoint + + + +
+

Endpoint

+ + +

Alpha License: LGPL-3 OCA/edi Translate me on Weblate Try me on Runbot

+

This module creates Endpoint frameworks to be used globally

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/edi project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/endpoint/tests/__init__.py b/endpoint/tests/__init__.py new file mode 100644 index 00000000..6885a0f9 --- /dev/null +++ b/endpoint/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_endpoint +from . import test_endpoint_controller diff --git a/endpoint/tests/common.py b/endpoint/tests/common.py new file mode 100644 index 00000000..f3862607 --- /dev/null +++ b/endpoint/tests/common.py @@ -0,0 +1,51 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import contextlib + +from odoo.tests.common import SavepointCase, tagged +from odoo.tools import DotDict + +from odoo.addons.website.tools import MockRequest + + +@tagged("-at_install", "post_install") +class CommonEndpoint(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_env() + cls._setup_records() + + @classmethod + def _setup_env(cls): + cls.env = cls.env(context=cls._setup_context()) + + @classmethod + def _setup_context(cls): + return dict( + cls.env.context, + tracking_disable=True, + ) + + @classmethod + def _setup_records(cls): + pass + + @contextlib.contextmanager + def _get_mocked_request( + self, httprequest=None, extra_headers=None, request_attrs=None + ): + with MockRequest(self.env) as mocked_request: + mocked_request.httprequest = ( + DotDict(httprequest) if httprequest else mocked_request.httprequest + ) + headers = {} + headers.update(extra_headers or {}) + mocked_request.httprequest.headers = headers + request_attrs = request_attrs or {} + for k, v in request_attrs.items(): + setattr(mocked_request, k, v) + mocked_request.make_response = lambda data, **kw: data + yield mocked_request diff --git a/endpoint/tests/test_endpoint.py b/endpoint/tests/test_endpoint.py new file mode 100644 index 00000000..30f63633 --- /dev/null +++ b/endpoint/tests/test_endpoint.py @@ -0,0 +1,163 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import json +import textwrap + +import psycopg2 +import werkzeug + +from odoo import exceptions +from odoo.tools.misc import mute_logger + +from .common import CommonEndpoint + + +class TestEndpoint(CommonEndpoint): + @classmethod + def _setup_records(cls): + super()._setup_records() + cls.endpoint = cls.env.ref("endpoint.endpoint_demo_1") + + @mute_logger("odoo.sql_db") + def test_endpoint_unique(self): + with self.assertRaises(psycopg2.IntegrityError): + self.env["endpoint.endpoint"].create( + { + "name": "Endpoint", + "route": "/demo/one", + "exec_mode": "code", + } + ) + + def test_endpoint_validation(self): + with self.assertRaisesRegex( + exceptions.UserError, r"you must provide a piece of code" + ): + self.env["endpoint.endpoint"].create( + { + "name": "Endpoint 2", + "route": "/demo/2", + "exec_mode": "code", + "auth_type": "user_endpoint", + } + ) + with self.assertRaisesRegex( + exceptions.UserError, r"Request method is required for" + ): + self.env["endpoint.endpoint"].create( + { + "name": "Endpoint 3", + "route": "/demo/3", + "exec_mode": "code", + "code_snippet": "foo = 1", + "request_method": "POST", + "auth_type": "user_endpoint", + } + ) + with self.assertRaisesRegex( + exceptions.UserError, r"Request method is required for" + ): + self.endpoint.request_method = "POST" + + def test_endpoint_find(self): + self.assertEqual( + self.env["endpoint.endpoint"]._find_endpoint("/demo/one"), self.endpoint + ) + + def test_endpoint_code_eval_full_response(self): + with self._get_mocked_request() as req: + result = self.endpoint._handle_request(req) + resp = result["response"] + self.assertEqual(resp.status, "200 OK") + self.assertEqual(resp.data, b"ok") + + def test_endpoint_code_eval_free_vals(self): + self.endpoint.write( + { + "code_snippet": textwrap.dedent( + """ + result = { + "payload": json.dumps({"a": 1, "b": 2}), + "headers": [("content-type", "application/json")] + } + """ + ) + } + ) + with self._get_mocked_request() as req: + result = self.endpoint._handle_request(req) + payload = result["payload"] + self.assertEqual(json.loads(payload), {"a": 1, "b": 2}) + + @mute_logger("endpoint.endpoint") + def test_endpoint_validate_request(self): + endpoint = self.endpoint.copy( + { + "route": "/wrong", + "request_method": "POST", + "request_content_type": "text/plain", + } + ) + with self.assertRaises(werkzeug.exceptions.UnsupportedMediaType): + with self._get_mocked_request(httprequest={"method": "POST"}) as req: + endpoint._validate_request(req) + with self.assertRaises(werkzeug.exceptions.MethodNotAllowed): + with self._get_mocked_request( + httprequest={"method": "GET"}, + extra_headers=[("Content-type", "text/plain")], + ) as req: + endpoint._validate_request(req) + + def test_routing(self): + route, info = self.endpoint._get_routing_info() + self.assertEqual(route, "/demo/one") + self.assertEqual( + info, + { + "auth": "user_endpoint", + "methods": ["GET"], + "routes": ["/demo/one"], + "type": "http", + "csrf": False, + }, + ) + endpoint = self.endpoint.copy( + { + "route": "/new/one", + "request_method": "POST", + "request_content_type": "text/plain", + "auth_type": "public", + "exec_as_user_id": self.env.user.id, + } + ) + __, info = endpoint._get_routing_info() + self.assertEqual( + info, + { + "auth": "public", + "methods": ["POST"], + "routes": ["/new/one"], + "type": "http", + "csrf": False, + }, + ) + # check prefix + type(endpoint)._endpoint_route_prefix = "/foo" + endpoint._compute_route() + __, info = endpoint._get_routing_info() + self.assertEqual( + info, + { + "auth": "public", + "methods": ["POST"], + "routes": ["/foo/new/one"], + "type": "http", + "csrf": False, + }, + ) + type(endpoint)._endpoint_route_prefix = "" + + # TODO + # def test_unlink(self): diff --git a/endpoint/tests/test_endpoint_controller.py b/endpoint/tests/test_endpoint_controller.py new file mode 100644 index 00000000..8a7cbc68 --- /dev/null +++ b/endpoint/tests/test_endpoint_controller.py @@ -0,0 +1,62 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import json +import os +import unittest + +from odoo.tests.common import HttpCase +from odoo.tools.misc import mute_logger + + +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "EndpointHttpCase skipped") +class EndpointHttpCase(HttpCase): + def setUp(self): + super().setUp() + + def test_call1(self): + response = self.url_open("/demo/one") + self.assertEqual(response.status_code, 401) + # Let's login now + self.authenticate("admin", "admin") + response = self.url_open("/demo/one") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"ok") + + def test_call_route_update(self): + # Ensure that a route that gets updated is not available anymore + self.authenticate("admin", "admin") + endpoint = self.env.ref("endpoint.endpoint_demo_1") + endpoint.route += "/new" + response = self.url_open("/demo/one") + self.assertEqual(response.status_code, 404) + response = self.url_open("/demo/one/new") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"ok") + + def test_call2(self): + response = self.url_open("/demo/as_demo_user") + self.assertEqual(response.content, b"My name is: Marc Demo") + + def test_call3(self): + response = self.url_open("/demo/json_data") + data = json.loads(response.content.decode()) + self.assertEqual(data, {"a": 1, "b": 2}) + + @mute_logger("endpoint.endpoint") + def test_call4(self): + response = self.url_open("/demo/raise_validation_error") + self.assertEqual(response.status_code, 400) + + def test_call5(self): + response = self.url_open("/demo/none") + self.assertEqual(response.status_code, 404) + + def test_call6(self): + response = self.url_open("/demo/value_from_request?your_name=JonnyTest") + self.assertEqual(response.content, b"JonnyTest") + + def test_call7(self): + response = self.url_open("/demo/bad_method", data="ok") + self.assertEqual(response.status_code, 405) diff --git a/endpoint/utils.py b/endpoint/utils.py new file mode 100644 index 00000000..d8c4a729 --- /dev/null +++ b/endpoint/utils.py @@ -0,0 +1,68 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +_ENDPOINT_ROUTING_MAP = {} + + +class EndpointRegistry: + """Registry for endpoints. + + Used to: + + * track registered endpoints and their rules + * track routes to be updated or deleted + * retrieve routes to update for ir.http routing map + + When the flag ``_routing_update_required`` is ON + the routing map will be forcedly refreshed. + + """ + + def __init__(self): + self._mapping = _ENDPOINT_ROUTING_MAP + self._routing_update_required = False + self._rules_to_load = [] + self._rules_to_drop = [] + + def get_rules(self): + return self._mapping.values() + + def add_or_update_rule(self, key, rule): + existing = self._mapping.get(key) + if not existing: + self._mapping[key] = rule + return True + if existing._endpoint_hash != rule._endpoint_hash: + # Override and set as to be updated + self._rules_to_drop.append(existing) + self._rules_to_load.append(rule) + self._mapping[key] = rule + self._routing_update_required = True + return True + + def drop_rule(self, key): + existing = self._mapping.get(key) + if not existing: + return False + # Override and set as to be updated + self._rules_to_drop.append(existing) + self._routing_update_required = True + return True + + def get_rules_to_update(self): + return { + "to_drop": self._rules_to_drop, + "to_load": self._rules_to_load, + } + + def routing_update_required(self): + return self._routing_update_required + + def reset_update_required(self): + self._routing_update_required = False + self._rules_to_drop = [] + self._rules_to_load = [] + + +endpoint_registry = EndpointRegistry() diff --git a/endpoint/views/endpoint_view.xml b/endpoint/views/endpoint_view.xml new file mode 100644 index 00000000..40c57b3a --- /dev/null +++ b/endpoint/views/endpoint_view.xml @@ -0,0 +1,114 @@ + + + + + + endpoint.endpoint.form + endpoint.endpoint + +
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + endpoint.endpoint.search + endpoint.endpoint + + + + + + + + + + + endpoint.endpoint.tree + endpoint.endpoint + + + + + + + + + + + Endpoints + endpoint.endpoint + tree,form + [] + {} + + + + Endpoints + + + + + + From 97010951bc8ea0db5f592b2fa3053ffe37af875f Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 Oct 2021 13:52:00 +0200 Subject: [PATCH 02/58] endpoint: add cross model constraint --- endpoint/models/endpoint_mixin.py | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/endpoint/models/endpoint_mixin.py b/endpoint/models/endpoint_mixin.py index 63b02ee9..5d079976 100644 --- a/endpoint/models/endpoint_mixin.py +++ b/endpoint/models/endpoint_mixin.py @@ -17,6 +17,8 @@ from ..controllers.main import EndpointController from ..utils import endpoint_registry +ENDPOINT_MIXIN_CONSUMER_MODELS = [] + class EndpointMixin(models.AbstractModel): @@ -71,6 +73,47 @@ class EndpointMixin(models.AbstractModel): ) ] + @api.constrains("route") + def _check_route_unique_across_models(self): + """Make sure routes are unique across all models. + + The SQL constraint above, works only on one specific model/table. + Here we check that routes stay unique across all models. + This is mostly to make sure admins know that the route already exists + somewhere else, because route controllers are registered only once + for the same path. + """ + # TODO: add tests registering a fake model. + # However, @simahawk tested manually and it works. + all_models = self._get_endpoint_mixin_consumer_models() + routes = [x["route"] for x in self.read(["route"])] + clashing_models = [] + for model in all_models: + if model != self._name and self.env[model].sudo().search_count( + [("route", "in", routes)] + ): + clashing_models.append(model) + if clashing_models: + raise exceptions.UserError( + _( + "Non unique route(s): %(routes)s.\n" + "Found in model(s): %(models)s.\n" + ) + % {"routes": ", ".join(routes), "models": ", ".join(clashing_models)} + ) + + def _get_endpoint_mixin_consumer_models(self): + global ENDPOINT_MIXIN_CONSUMER_MODELS + if ENDPOINT_MIXIN_CONSUMER_MODELS: + return ENDPOINT_MIXIN_CONSUMER_MODELS + models = [] + mixin_name = "endpoint.mixin" + for model in self.env.values(): + if model._name != mixin_name and mixin_name in model._inherit: + models.append(model._name) + ENDPOINT_MIXIN_CONSUMER_MODELS = models + return models + @property def _logger(self): return logging.getLogger(self._name) From 0277dc144713992ab2f3e189724eeeb660fe8904 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 25 Oct 2021 17:45:26 +0200 Subject: [PATCH 03/58] endpoint: split out route handling --- endpoint/__manifest__.py | 1 + endpoint/models/__init__.py | 1 - endpoint/models/endpoint_mixin.py | 270 +-------------------- endpoint/models/ir_http.py | 103 -------- endpoint/readme/ROADMAP.rst | 2 +- endpoint/tests/test_endpoint.py | 24 +- endpoint/tests/test_endpoint_controller.py | 6 + endpoint/utils.py | 68 ------ endpoint/views/endpoint_view.xml | 1 + 9 files changed, 41 insertions(+), 435 deletions(-) delete mode 100644 endpoint/models/ir_http.py delete mode 100644 endpoint/utils.py diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index 5284bbfc..576a0556 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -10,6 +10,7 @@ "author": "Camptocamp,Odoo Community Association (OCA)", "maintainers": ["simahawk"], "website": "https://github.com/OCA/edi", + "depends": ["endpoint_route_handler"], "data": [ "security/ir.model.access.csv", "demo/endpoint_demo.xml", diff --git a/endpoint/models/__init__.py b/endpoint/models/__init__.py index edd1b58b..e5ecde3f 100644 --- a/endpoint/models/__init__.py +++ b/endpoint/models/__init__.py @@ -1,3 +1,2 @@ from . import endpoint_mixin from . import endpoint_endpoint -from . import ir_http diff --git a/endpoint/models/endpoint_mixin.py b/endpoint/models/endpoint_mixin.py index 5d079976..5496ccb9 100644 --- a/endpoint/models/endpoint_mixin.py +++ b/endpoint/models/endpoint_mixin.py @@ -2,56 +2,23 @@ # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -import logging import textwrap from functools import partial import werkzeug -from werkzeug.routing import Rule from odoo import _, api, exceptions, fields, http, models from odoo.tools import safe_eval -from odoo.addons.base_sparse_field.models.fields import Serialized - from ..controllers.main import EndpointController -from ..utils import endpoint_registry - -ENDPOINT_MIXIN_CONSUMER_MODELS = [] class EndpointMixin(models.AbstractModel): _name = "endpoint.mixin" + _inherit = "endpoint.route.handler" _description = "Endpoint mixin" - active = fields.Boolean(default=True) - name = fields.Char(required=True) - route = fields.Char( - required=True, - index=True, - compute="_compute_route", - inverse="_inverse_route", - readonly=False, - store=True, - copy=False, - ) - route_type = fields.Selection(selection="_selection_route_type", default="http") - auth_type = fields.Selection( - selection="_selection_auth_type", default="user_endpoint" - ) - - options = Serialized() - request_content_type = fields.Selection( - selection="_selection_request_content_type", sparse="options" - ) - request_method = fields.Selection( - selection="_selection_request_method", sparse="options", required=True - ) - # # TODO: validate params? Just for doc? Maybe use Cerberus? - # # -> For now let the implementer validate the params in the snippet. - # request_params = fields.Char(help="TODO", sparse="options") - exec_mode = fields.Selection( selection="_selection_exec_mode", required=True, @@ -63,95 +30,6 @@ class EndpointMixin(models.AbstractModel): ) exec_as_user_id = fields.Many2one(comodel_name="res.users") - endpoint_hash = fields.Char(compute="_compute_endpoint_hash") - - _sql_constraints = [ - ( - "endpoint_route_unique", - "unique(route)", - "You can register an endpoint route only once.", - ) - ] - - @api.constrains("route") - def _check_route_unique_across_models(self): - """Make sure routes are unique across all models. - - The SQL constraint above, works only on one specific model/table. - Here we check that routes stay unique across all models. - This is mostly to make sure admins know that the route already exists - somewhere else, because route controllers are registered only once - for the same path. - """ - # TODO: add tests registering a fake model. - # However, @simahawk tested manually and it works. - all_models = self._get_endpoint_mixin_consumer_models() - routes = [x["route"] for x in self.read(["route"])] - clashing_models = [] - for model in all_models: - if model != self._name and self.env[model].sudo().search_count( - [("route", "in", routes)] - ): - clashing_models.append(model) - if clashing_models: - raise exceptions.UserError( - _( - "Non unique route(s): %(routes)s.\n" - "Found in model(s): %(models)s.\n" - ) - % {"routes": ", ".join(routes), "models": ", ".join(clashing_models)} - ) - - def _get_endpoint_mixin_consumer_models(self): - global ENDPOINT_MIXIN_CONSUMER_MODELS - if ENDPOINT_MIXIN_CONSUMER_MODELS: - return ENDPOINT_MIXIN_CONSUMER_MODELS - models = [] - mixin_name = "endpoint.mixin" - for model in self.env.values(): - if model._name != mixin_name and mixin_name in model._inherit: - models.append(model._name) - ENDPOINT_MIXIN_CONSUMER_MODELS = models - return models - - @property - def _logger(self): - return logging.getLogger(self._name) - - def _selection_route_type(self): - return [("http", "HTTP"), ("json", "JSON")] - - def _selection_auth_type(self): - return [("public", "Public"), ("user_endpoint", "User")] - - def _selection_request_method(self): - return [ - ("GET", "GET"), - ("POST", "POST"), - ("PUT", "PUT"), - ("DELETE", "DELETE"), - ] - - def _selection_request_content_type(self): - return [ - ("", "None"), - ("text/plain", "Text"), - ("text/csv", "CSV"), - ("application/json", "JSON"), - ("application/xml", "XML"), - ("application/x-www-form-urlencoded", "Form"), - ] - - # TODO: Is this needed at all since we can cook full responses? - def _selection_response_content_type(self): - return [ - # TODO: how to get a complete list? - # OR: shall we leave the text free? - ("text/plain", "Plain text"), - ("application/json", "JSON"), - ("application/xml", "XML"), - ] - def _selection_exec_mode(self): return [("code", "Execute code")] @@ -159,45 +37,6 @@ def _compute_code_snippet_docs(self): for rec in self: rec.code_snippet_docs = textwrap.dedent(rec._default_code_snippet_docs()) - @api.depends(lambda self: self._controller_fields()) - def _compute_endpoint_hash(self): - values = self.read(self._controller_fields()) - for rec, vals in zip(self, values): - vals.pop("id", None) - rec.endpoint_hash = hash(tuple(vals.values())) - - @api.depends("route") - def _compute_route(self): - for rec in self: - rec.route = rec._clean_route() - - def _inverse_route(self): - for rec in self: - rec.route = rec._clean_route() - - _endpoint_route_prefix = "" - """Prefix for all routes, includes slashes. - """ - - def _clean_route(self): - route = (self.route or "").strip() - if not route.startswith("/"): - route = "/" + route - prefix = self._endpoint_route_prefix - if prefix and not route.startswith(prefix): - route = prefix + route - return route - - _blacklist_routes = ("/", "/web") # TODO: what else? - - @api.constrains("route") - def _check_route(self): - for rec in self: - if rec.route in self._blacklist_routes: - raise exceptions.UserError( - _("`%s` uses a blacklisted routed = `%s`") % (rec.name, rec.route) - ) - @api.constrains("exec_mode") def _check_exec_mode(self): for rec in self: @@ -208,19 +47,11 @@ def _validate_exec_mode(self): validator() def _validate_exec__code(self): - if self.exec_mode == "code" and not self._code_snippet_valued(): + if not self._code_snippet_valued(): raise exceptions.UserError( _("Exec mode is set to `Code`: you must provide a piece of code") ) - @api.constrains("request_method", "request_content_type") - def _check_request_method(self): - for rec in self: - if rec.request_method in ("POST", "PUT") and not rec.request_content_type: - raise exceptions.UserError( - _("Request method is required for POST and PUT.") - ) - @api.constrains("auth_type") def _check_auth(self): for rec in self: @@ -277,14 +108,6 @@ def _get_code_snippet_eval_context(self, request): ), } - def _get_handler(self): - try: - return getattr(self, "_handle_exec__" + self.exec_mode) - except AttributeError: - raise exceptions.UserError( - _("Missing handler for exec mode %s") % self.exec_mode - ) - def _handle_exec__code(self, request): if not self._code_snippet_valued(): return {} @@ -308,11 +131,11 @@ def _code_snippet_valued(self): ] ) + def _default_endpoint_handler(self): + return partial(EndpointController().auto_endpoint, self.route) + def _validate_request(self, request): http_req = request.httprequest - # TODO: likely not needed anymore - if self.auth_type != "public" and not request.env.user: - raise werkzeug.exceptions.Unauthorized() if self.request_method and self.request_method != http_req.method: self._logger.error("_validate_request: MethodNotAllowed") raise werkzeug.exceptions.MethodNotAllowed() @@ -323,6 +146,14 @@ def _validate_request(self, request): self._logger.error("_validate_request: UnsupportedMediaType") raise werkzeug.exceptions.UnsupportedMediaType() + def _get_handler(self): + try: + return getattr(self, "_handle_exec__" + self.exec_mode) + except AttributeError: + raise exceptions.UserError( + _("Missing handler for exec mode %s") % self.exec_mode + ) + def _handle_request(self, request): # Switch user for the whole process self_with_user = self @@ -345,78 +176,3 @@ def _find_endpoint(self, endpoint_route): def _find_endpoint_domain(self, endpoint_route): return [("route", "=", endpoint_route)] - - # Handle automatic route registration - - @api.model_create_multi - def create(self, vals_list): - res = super().create(vals_list) - if not self._abstract: - res._register_controllers() - return res - - def write(self, vals): - res = super().write(vals) - if not self._abstract and any([x in vals for x in self._controller_fields()]): - self._register_controllers() - return res - - def unlink(self): - if not self._abstract: - for rec in self: - rec._drop_controller_rule() - return super().unlink() - - def _controller_fields(self): - return ["route", "auth_type", "request_method"] - - def _register_hook(self): - super()._register_hook() - if not self._abstract: - self.search([])._register_controllers() - - def _register_controllers(self): - for rec in self: - rec._register_controller() - - def _register_controller(self): - rule = self._make_controller_rule() - self._add_or_update_controller_rule(rule) - self._logger.info( - "Registered controller %s (auth: %s)", self.route, self.auth_type - ) - - _endpoint_base_controller_class = EndpointController - - def _make_controller_rule(self): - route, routing = self._get_routing_info() - base_controller = self._endpoint_base_controller_class() - endpoint = http.EndPoint( - partial(base_controller.auto_endpoint, self.route), routing - ) - rule = Rule(route, endpoint=endpoint, methods=routing["methods"]) - rule.merge_slashes = False - rule._auto_endpoint = True - rule._endpoint_hash = self.endpoint_hash - return rule - - def _get_routing_info(self): - route = self.route - routing = dict( - type=self.route_type, - auth=self.auth_type, - methods=[self.request_method], - routes=[route], - # TODO: make this configurable - # in case the endpoint is used for frontend stuff. - csrf=False, - ) - return route, routing - - def _add_or_update_controller_rule(self, rule): - key = "{0._name}:{0.id}".format(self) - endpoint_registry.add_or_update_rule(key, rule) - - def _drop_controller_rule(self): - key = "{0._name}:{0.id}".format(self) - endpoint_registry.drop_rule(key) diff --git a/endpoint/models/ir_http.py b/endpoint/models/ir_http.py deleted file mode 100644 index d7992c9b..00000000 --- a/endpoint/models/ir_http.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright 2021 Camptcamp SA -# @author: Simone Orsi -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import logging - -import werkzeug - -from odoo import http, models - -from ..utils import endpoint_registry - -_logger = logging.getLogger(__name__) - - -class IrHttp(models.AbstractModel): - _inherit = "ir.http" - - @classmethod - def routing_map(cls, key=None): - # Override to inject custom endpoint routes - rmap = super().routing_map(key=key) - if hasattr(cls, "_routing_map"): - if not hasattr(cls, "_endpoint_routing_map_loaded"): - # First load, register all endpoint routes - cls._load_endpoint_routing_map(rmap) - cls._endpoint_routing_map_loaded = True - elif endpoint_registry.routing_update_required(): - # Some endpoint changed, we must reload - cls._reload_endpoint_routing_map(rmap) - endpoint_registry.reset_update_required() - return rmap - - @classmethod - def _load_endpoint_routing_map(cls, rmap): - for rule in endpoint_registry.get_rules(): - if rule.endpoint not in rmap._rules_by_endpoint: - rmap.add(rule) - _logger.info("Endpoint routing map loaded") - # If you have to debug, ncomment to print all routes - # print("\n".join([x.rule for x in rmap._rules])) - - @classmethod - def _reload_endpoint_routing_map(cls, rmap): - """Reload endpoints routing map. - - Take care of removing obsolete ones and add new ones. - The match is done using the `_endpoint_hash`. - - Typical log entries in case of route changes: - - [...] endpoint.endpoint: Registered controller /demo/one/new (auth: public) - [...] odoo.addons.endpoint.models.ir_http: DROPPED /demo/one - [...] odoo.addons.endpoint.models.ir_http: LOADED /demo/one/new - [...] odoo.addons.endpoint.models.ir_http: Endpoint routing map re-loaded - - and then on subsequent calls: - - [...] GET /demo/one HTTP/1.1" 404 - 3 0.001 0.006 - [...] GET /demo/one/new HTTP/1.1" 200 - 6 0.001 0.005 - - You can look for such entries in logs - to check visually that a route has been updated - """ - to_update = endpoint_registry.get_rules_to_update() - to_load = to_update["to_load"] - to_drop = to_update["to_drop"] - hashes_to_drop = [x._endpoint_hash for x in to_drop] - remove_count = 0 - for i, rule in enumerate(rmap._rules[:]): - if ( - hasattr(rule, "_endpoint_hash") - and rule._endpoint_hash in hashes_to_drop - ): - if rule.endpoint in rmap._rules_by_endpoint: - rmap._rules.pop(i - remove_count) - rmap._rules_by_endpoint.pop(rule.endpoint) - remove_count += 1 - _logger.info("DROPPED %s", str(rule)) - continue - for rule in to_load: - if rule.endpoint not in rmap._rules_by_endpoint: - rmap.add(rule) - _logger.info("LOADED %s", str(rule)) - _logger.info("Endpoint routing map re-loaded") - - @classmethod - def _auth_method_user_endpoint(cls): - """Special method for user auth which raises Unauthorized when needed. - - If you get an HTTP request (instead of a JSON one), - the standard `user` method raises `SessionExpiredException` - when there's no user session. - This leads to a redirect to `/web/login` - which is not desiderable for technical endpoints. - - This method makes sure that no matter the type of request we get, - a proper exception is raised. - """ - try: - cls._auth_method_user() - except http.SessionExpiredException: - raise werkzeug.exceptions.Unauthorized() diff --git a/endpoint/readme/ROADMAP.rst b/endpoint/readme/ROADMAP.rst index 85756037..48562e3a 100644 --- a/endpoint/readme/ROADMAP.rst +++ b/endpoint/readme/ROADMAP.rst @@ -1,2 +1,2 @@ * add validation of request data -* add api docs +* add api docs generation diff --git a/endpoint/tests/test_endpoint.py b/endpoint/tests/test_endpoint.py index 30f63633..b74a0a6c 100644 --- a/endpoint/tests/test_endpoint.py +++ b/endpoint/tests/test_endpoint.py @@ -40,6 +40,7 @@ def test_endpoint_validation(self): "name": "Endpoint 2", "route": "/demo/2", "exec_mode": "code", + "request_method": "GET", "auth_type": "user_endpoint", } ) @@ -111,7 +112,7 @@ def test_endpoint_validate_request(self): endpoint._validate_request(req) def test_routing(self): - route, info = self.endpoint._get_routing_info() + route, info, __ = self.endpoint._get_routing_info() self.assertEqual(route, "/demo/one") self.assertEqual( info, @@ -132,7 +133,7 @@ def test_routing(self): "exec_as_user_id": self.env.user.id, } ) - __, info = endpoint._get_routing_info() + __, info, __ = endpoint._get_routing_info() self.assertEqual( info, { @@ -146,7 +147,7 @@ def test_routing(self): # check prefix type(endpoint)._endpoint_route_prefix = "/foo" endpoint._compute_route() - __, info = endpoint._get_routing_info() + __, info, __ = endpoint._get_routing_info() self.assertEqual( info, { @@ -159,5 +160,18 @@ def test_routing(self): ) type(endpoint)._endpoint_route_prefix = "" - # TODO - # def test_unlink(self): + def test_unlink(self): + endpoint = self.endpoint.copy( + { + "route": "/delete/this", + "request_method": "POST", + "request_content_type": "text/plain", + "auth_type": "public", + "exec_as_user_id": self.env.user.id, + } + ) + registry = endpoint._endpoint_registry + route = endpoint.route + endpoint.unlink() + self.assertTrue(registry.routing_update_required()) + self.assertIn(route, [x.rule for x in registry._rules_to_drop]) diff --git a/endpoint/tests/test_endpoint_controller.py b/endpoint/tests/test_endpoint_controller.py index 8a7cbc68..ca2b4cad 100644 --- a/endpoint/tests/test_endpoint_controller.py +++ b/endpoint/tests/test_endpoint_controller.py @@ -9,6 +9,12 @@ from odoo.tests.common import HttpCase from odoo.tools.misc import mute_logger +# odoo.addons.base.models.res_users: Login successful for db:openerp_test login:admin from n/a +# endpoint.endpoint: Registered controller /demo/one/new (auth: user_endpoint) +# odoo.addons.endpoint.models.ir_http: DROPPED /demo/one +# odoo.addons.endpoint.models.ir_http: LOADED /demo/one/new +# odoo.addons.endpoint.models.ir_http: Endpoint routing map re-loaded + @unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "EndpointHttpCase skipped") class EndpointHttpCase(HttpCase): diff --git a/endpoint/utils.py b/endpoint/utils.py deleted file mode 100644 index d8c4a729..00000000 --- a/endpoint/utils.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2021 Camptcamp SA -# @author: Simone Orsi -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -_ENDPOINT_ROUTING_MAP = {} - - -class EndpointRegistry: - """Registry for endpoints. - - Used to: - - * track registered endpoints and their rules - * track routes to be updated or deleted - * retrieve routes to update for ir.http routing map - - When the flag ``_routing_update_required`` is ON - the routing map will be forcedly refreshed. - - """ - - def __init__(self): - self._mapping = _ENDPOINT_ROUTING_MAP - self._routing_update_required = False - self._rules_to_load = [] - self._rules_to_drop = [] - - def get_rules(self): - return self._mapping.values() - - def add_or_update_rule(self, key, rule): - existing = self._mapping.get(key) - if not existing: - self._mapping[key] = rule - return True - if existing._endpoint_hash != rule._endpoint_hash: - # Override and set as to be updated - self._rules_to_drop.append(existing) - self._rules_to_load.append(rule) - self._mapping[key] = rule - self._routing_update_required = True - return True - - def drop_rule(self, key): - existing = self._mapping.get(key) - if not existing: - return False - # Override and set as to be updated - self._rules_to_drop.append(existing) - self._routing_update_required = True - return True - - def get_rules_to_update(self): - return { - "to_drop": self._rules_to_drop, - "to_load": self._rules_to_load, - } - - def routing_update_required(self): - return self._routing_update_required - - def reset_update_required(self): - self._routing_update_required = False - self._rules_to_drop = [] - self._rules_to_load = [] - - -endpoint_registry = EndpointRegistry() diff --git a/endpoint/views/endpoint_view.xml b/endpoint/views/endpoint_view.xml index 40c57b3a..55c0e975 100644 --- a/endpoint/views/endpoint_view.xml +++ b/endpoint/views/endpoint_view.xml @@ -31,6 +31,7 @@ + Date: Tue, 9 Nov 2021 17:11:42 +0100 Subject: [PATCH 04/58] [FIX] endpoint: fix loading of demo data --- endpoint/__manifest__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index 576a0556..a006f379 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -13,7 +13,9 @@ "depends": ["endpoint_route_handler"], "data": [ "security/ir.model.access.csv", - "demo/endpoint_demo.xml", "views/endpoint_view.xml", ], + "demo": [ + "demo/endpoint_demo.xml", + ], } From aa6082d06a817327775630967f7401c11fb892f3 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 9 Nov 2021 18:47:33 +0000 Subject: [PATCH 05/58] endpoint 14.0.1.0.1 --- endpoint/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index a006f379..69873bfe 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Endpoint", "summary": """Provide custom endpoint machinery.""", - "version": "14.0.1.0.0", + "version": "14.0.1.0.1", "license": "LGPL-3", "development_status": "Alpha", "author": "Camptocamp,Odoo Community Association (OCA)", From 6a766623479356b8ded379d3d00f493bdeffe818 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 28 Dec 2021 14:12:26 +0100 Subject: [PATCH 06/58] endpoint: add tests for archive/unarchive --- endpoint/tests/test_endpoint.py | 19 +++++++++++++++++++ endpoint/tests/test_endpoint_controller.py | 7 +++++++ 2 files changed, 26 insertions(+) diff --git a/endpoint/tests/test_endpoint.py b/endpoint/tests/test_endpoint.py index b74a0a6c..561767a4 100644 --- a/endpoint/tests/test_endpoint.py +++ b/endpoint/tests/test_endpoint.py @@ -175,3 +175,22 @@ def test_unlink(self): endpoint.unlink() self.assertTrue(registry.routing_update_required()) self.assertIn(route, [x.rule for x in registry._rules_to_drop]) + + def test_archiving(self): + endpoint = self.endpoint.copy( + { + "route": "/enable-disable/this", + "request_method": "POST", + "request_content_type": "text/plain", + "auth_type": "public", + "exec_as_user_id": self.env.user.id, + } + ) + self.assertTrue(endpoint.active) + registry = endpoint._endpoint_registry + route = endpoint.route + self.assertTrue(registry.routing_update_required()) + self.assertIn(route, [x.rule for x in registry._rules_to_load]) + endpoint.active = False + self.assertIn(route, [x.rule for x in registry._rules_to_drop]) + self.assertTrue(registry.routing_update_required()) diff --git a/endpoint/tests/test_endpoint_controller.py b/endpoint/tests/test_endpoint_controller.py index ca2b4cad..85026cee 100644 --- a/endpoint/tests/test_endpoint_controller.py +++ b/endpoint/tests/test_endpoint_controller.py @@ -40,6 +40,13 @@ def test_call_route_update(self): response = self.url_open("/demo/one/new") self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"ok") + # Archive it + endpoint.active = False + response = self.url_open("/demo/one/new") + self.assertEqual(response.status_code, 404) + endpoint.active = True + response = self.url_open("/demo/one/new") + self.assertEqual(response.status_code, 200) def test_call2(self): response = self.url_open("/demo/as_demo_user") From 2e4c1abf18293a107667954a792b6d66e3668711 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 29 Dec 2021 15:06:57 +0100 Subject: [PATCH 07/58] endpoint: update tests --- endpoint/tests/test_endpoint.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/endpoint/tests/test_endpoint.py b/endpoint/tests/test_endpoint.py index 561767a4..a70c5351 100644 --- a/endpoint/tests/test_endpoint.py +++ b/endpoint/tests/test_endpoint.py @@ -171,10 +171,9 @@ def test_unlink(self): } ) registry = endpoint._endpoint_registry - route = endpoint.route endpoint.unlink() - self.assertTrue(registry.routing_update_required()) - self.assertIn(route, [x.rule for x in registry._rules_to_drop]) + http_id = self.env["ir.http"]._endpoint_make_http_id() + self.assertTrue(registry.routing_update_required(http_id)) def test_archiving(self): endpoint = self.endpoint.copy( @@ -188,9 +187,12 @@ def test_archiving(self): ) self.assertTrue(endpoint.active) registry = endpoint._endpoint_registry - route = endpoint.route - self.assertTrue(registry.routing_update_required()) - self.assertIn(route, [x.rule for x in registry._rules_to_load]) + http_id = self.env["ir.http"]._endpoint_make_http_id() + fake_2nd_http_id = id(2) + registry.ir_http_track(http_id) + self.assertFalse(registry.routing_update_required(http_id)) + self.assertFalse(registry.routing_update_required(fake_2nd_http_id)) + endpoint.active = False - self.assertIn(route, [x.rule for x in registry._rules_to_drop]) - self.assertTrue(registry.routing_update_required()) + self.assertTrue(registry.routing_update_required(http_id)) + self.assertFalse(registry.routing_update_required(fake_2nd_http_id)) From 6a15ca63fb79ad10d12531e64fa7468d0627a728 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 12 Nov 2021 13:41:59 +0100 Subject: [PATCH 08/58] endpoint: improve search/tree views --- endpoint/views/endpoint_view.xml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/endpoint/views/endpoint_view.xml b/endpoint/views/endpoint_view.xml index 55c0e975..27462baf 100644 --- a/endpoint/views/endpoint_view.xml +++ b/endpoint/views/endpoint_view.xml @@ -81,6 +81,18 @@ + + + + @@ -89,10 +101,11 @@ endpoint.endpoint.tree endpoint.endpoint - + + @@ -102,7 +115,7 @@ endpoint.endpoint tree,form [] - {} + {'search_default_all': 1} From 154ae297cd6b19427de4324eed2b6673437b1c72 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 12 Jan 2022 07:24:29 +0000 Subject: [PATCH 09/58] endpoint 14.0.1.0.2 --- endpoint/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index 69873bfe..c5ded900 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Endpoint", "summary": """Provide custom endpoint machinery.""", - "version": "14.0.1.0.1", + "version": "14.0.1.0.2", "license": "LGPL-3", "development_status": "Alpha", "author": "Camptocamp,Odoo Community Association (OCA)", From a5bc9c451f3f4d0abe1862ba99feb15360d6db37 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 12 Jan 2022 07:55:40 +0000 Subject: [PATCH 10/58] endpoint 14.0.1.1.0 --- endpoint/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index c5ded900..cde4d894 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Endpoint", "summary": """Provide custom endpoint machinery.""", - "version": "14.0.1.0.2", + "version": "14.0.1.1.0", "license": "LGPL-3", "development_status": "Alpha", "author": "Camptocamp,Odoo Community Association (OCA)", From d7f7d6aeb38677725498698c1c07db547f176fd3 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 14 Jan 2022 07:36:41 +0100 Subject: [PATCH 11/58] Misc fix of authorship name --- endpoint/__manifest__.py | 2 +- endpoint/controllers/main.py | 2 +- endpoint/models/endpoint_mixin.py | 2 +- endpoint/tests/common.py | 2 +- endpoint/tests/test_endpoint.py | 2 +- endpoint/tests/test_endpoint_controller.py | 2 +- endpoint/views/endpoint_view.xml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index cde4d894..8ea03269 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -1,4 +1,4 @@ -# Copyright 2021 Camptcamp SA +# Copyright 2021 Camptocamp SA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). { diff --git a/endpoint/controllers/main.py b/endpoint/controllers/main.py index 615ca23e..8c3a1480 100644 --- a/endpoint/controllers/main.py +++ b/endpoint/controllers/main.py @@ -1,4 +1,4 @@ -# Copyright 2021 Camptcamp SA +# Copyright 2021 Camptocamp SA # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). diff --git a/endpoint/models/endpoint_mixin.py b/endpoint/models/endpoint_mixin.py index 5496ccb9..706545d9 100644 --- a/endpoint/models/endpoint_mixin.py +++ b/endpoint/models/endpoint_mixin.py @@ -1,4 +1,4 @@ -# Copyright 2021 Camptcamp SA +# Copyright 2021 Camptocamp SA # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). diff --git a/endpoint/tests/common.py b/endpoint/tests/common.py index f3862607..48c944cd 100644 --- a/endpoint/tests/common.py +++ b/endpoint/tests/common.py @@ -1,4 +1,4 @@ -# Copyright 2021 Camptcamp SA +# Copyright 2021 Camptocamp SA # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). diff --git a/endpoint/tests/test_endpoint.py b/endpoint/tests/test_endpoint.py index a70c5351..2b4ea12c 100644 --- a/endpoint/tests/test_endpoint.py +++ b/endpoint/tests/test_endpoint.py @@ -1,4 +1,4 @@ -# Copyright 2021 Camptcamp SA +# Copyright 2021 Camptocamp SA # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). diff --git a/endpoint/tests/test_endpoint_controller.py b/endpoint/tests/test_endpoint_controller.py index 85026cee..03238133 100644 --- a/endpoint/tests/test_endpoint_controller.py +++ b/endpoint/tests/test_endpoint_controller.py @@ -1,4 +1,4 @@ -# Copyright 2021 Camptcamp SA +# Copyright 2021 Camptocamp SA # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). diff --git a/endpoint/views/endpoint_view.xml b/endpoint/views/endpoint_view.xml index 27462baf..3072ee33 100644 --- a/endpoint/views/endpoint_view.xml +++ b/endpoint/views/endpoint_view.xml @@ -1,5 +1,5 @@ - From 5ff04642d5adb5f03dfb729e799d665d28a3332f Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 14 Jan 2022 08:52:17 +0000 Subject: [PATCH 12/58] endpoint 14.0.1.1.1 --- endpoint/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index 8ea03269..cb3cc9e0 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Endpoint", "summary": """Provide custom endpoint machinery.""", - "version": "14.0.1.1.0", + "version": "14.0.1.1.1", "license": "LGPL-3", "development_status": "Alpha", "author": "Camptocamp,Odoo Community Association (OCA)", From fe1fdadcdbf6e93a29066016bc94a3f2185f38a6 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 18 Feb 2022 17:16:10 +0100 Subject: [PATCH 13/58] endpoint: block all RPC calls --- endpoint/__manifest__.py | 4 +-- endpoint/migrations/14.0.1.1.0/pre-migrate.py | 25 +++++++++++++++++++ endpoint/models/endpoint_mixin.py | 3 +++ endpoint/readme/DESCRIPTION.rst | 2 ++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 endpoint/migrations/14.0.1.1.0/pre-migrate.py diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index cb3cc9e0..02165c7d 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -4,13 +4,13 @@ { "name": "Endpoint", "summary": """Provide custom endpoint machinery.""", - "version": "14.0.1.1.1", + "version": "14.0.1.2.0", "license": "LGPL-3", "development_status": "Alpha", "author": "Camptocamp,Odoo Community Association (OCA)", "maintainers": ["simahawk"], "website": "https://github.com/OCA/edi", - "depends": ["endpoint_route_handler"], + "depends": ["endpoint_route_handler", "rpc_helper"], "data": [ "security/ir.model.access.csv", "views/endpoint_view.xml", diff --git a/endpoint/migrations/14.0.1.1.0/pre-migrate.py b/endpoint/migrations/14.0.1.1.0/pre-migrate.py new file mode 100644 index 00000000..7e8d6672 --- /dev/null +++ b/endpoint/migrations/14.0.1.1.0/pre-migrate.py @@ -0,0 +1,25 @@ +# Copyright 2022 Camptocamp SA (http://www.camptocamp.com) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + + env = api.Environment(cr, SUPERUSER_ID, {}) + module = env["ir.module.module"].search( + [ + ("name", "=", "rpc_helper"), + ("state", "=", "uninstalled"), + ] + ) + if module: + _logger.info("Install module rpc_helper") + module.write({"state": "to install"}) + return diff --git a/endpoint/models/endpoint_mixin.py b/endpoint/models/endpoint_mixin.py index 706545d9..a0c490b5 100644 --- a/endpoint/models/endpoint_mixin.py +++ b/endpoint/models/endpoint_mixin.py @@ -10,9 +10,12 @@ from odoo import _, api, exceptions, fields, http, models from odoo.tools import safe_eval +from odoo.addons.rpc_helper.decorator import disable_rpc + from ..controllers.main import EndpointController +@disable_rpc() # Block ALL RPC calls class EndpointMixin(models.AbstractModel): _name = "endpoint.mixin" diff --git a/endpoint/readme/DESCRIPTION.rst b/endpoint/readme/DESCRIPTION.rst index 4fcfc952..1be3e06c 100644 --- a/endpoint/readme/DESCRIPTION.rst +++ b/endpoint/readme/DESCRIPTION.rst @@ -3,3 +3,5 @@ Provide an endpoint framework allowing users to define their own custom endpoint Thanks to endpoint mixin the endpoint records are automatically registered as real Odoo routes. You can easily code what you want in the code snippet. + +NOTE: for security reasons any kind of RPC call is blocked on endpoint records. From e97106ce3c52f9c645acec5933b482bea90442f9 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 11 Mar 2022 18:08:34 +0000 Subject: [PATCH 14/58] endpoint 14.0.1.3.0 --- endpoint/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index 02165c7d..fd431a50 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Endpoint", "summary": """Provide custom endpoint machinery.""", - "version": "14.0.1.2.0", + "version": "14.0.1.3.0", "license": "LGPL-3", "development_status": "Alpha", "author": "Camptocamp,Odoo Community Association (OCA)", From 50ecc329796da6d3e7e132b24bf6eae2dfc513d5 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 15 Jun 2022 21:13:56 +0200 Subject: [PATCH 15/58] endpoint: move to web-api --- endpoint/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index fd431a50..048b5c1d 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -9,7 +9,7 @@ "development_status": "Alpha", "author": "Camptocamp,Odoo Community Association (OCA)", "maintainers": ["simahawk"], - "website": "https://github.com/OCA/edi", + "website": "https://github.com/OCA/web-api", "depends": ["endpoint_route_handler", "rpc_helper"], "data": [ "security/ir.model.access.csv", From 322f1364d216b6f6faab1acd6609e6538edc1501 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 15 Jul 2022 12:38:23 +0000 Subject: [PATCH 16/58] [UPD] Update endpoint.pot --- endpoint/i18n/endpoint.pot | 224 +++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 endpoint/i18n/endpoint.pot diff --git a/endpoint/i18n/endpoint.pot b/endpoint/i18n/endpoint.pot new file mode 100644 index 00000000..a4675ac2 --- /dev/null +++ b/endpoint/i18n/endpoint.pot @@ -0,0 +1,224 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * endpoint +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.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: endpoint +#: code:addons/endpoint/models/endpoint_mixin.py:0 +#, python-format +msgid "'Exec as user' is mandatory for public endpoints." +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__active +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__active +msgid "Active" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_search_view +msgid "All" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_search_view +msgid "Archived" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +msgid "Auth" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__auth_type +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__auth_type +msgid "Auth Type" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +msgid "Code" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +msgid "Code Help" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__code_snippet +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__code_snippet +msgid "Code Snippet" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__code_snippet_docs +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__code_snippet_docs +msgid "Code Snippet Docs" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__create_uid +msgid "Created by" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__create_date +msgid "Created on" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__csrf +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__csrf +msgid "Csrf" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__display_name +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__display_name +msgid "Display Name" +msgstr "" + +#. module: endpoint +#: model:ir.model,name:endpoint.model_endpoint_endpoint +msgid "Endpoint" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__endpoint_hash +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__endpoint_hash +msgid "Endpoint Hash" +msgstr "" + +#. module: endpoint +#: model:ir.model,name:endpoint.model_endpoint_mixin +msgid "Endpoint mixin" +msgstr "" + +#. module: endpoint +#: model:ir.actions.act_window,name:endpoint.endpoint_endpoint_act_window +#: model:ir.ui.menu,name:endpoint.endpoint_endpoint_menu +msgid "Endpoints" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__exec_as_user_id +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__exec_as_user_id +msgid "Exec As User" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__exec_mode +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__exec_mode +msgid "Exec Mode" +msgstr "" + +#. module: endpoint +#: code:addons/endpoint/models/endpoint_mixin.py:0 +#, python-format +msgid "Exec mode is set to `Code`: you must provide a piece of code" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__id +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__id +msgid "ID" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,help:endpoint.field_endpoint_endpoint__endpoint_hash +#: model:ir.model.fields,help:endpoint.field_endpoint_mixin__endpoint_hash +msgid "Identify the route with its main params" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint____last_update +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin____last_update +msgid "Last Modified on" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__write_date +msgid "Last Updated on" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +msgid "Main" +msgstr "" + +#. module: endpoint +#: code:addons/endpoint/models/endpoint_mixin.py:0 +#, python-format +msgid "Missing handler for exec mode %s" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__name +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__name +msgid "Name" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +msgid "Request" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__request_content_type +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__request_content_type +msgid "Request Content Type" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__request_method +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__request_method +msgid "Request Method" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__route +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__route +msgid "Route" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__route_group +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__route_group +msgid "Route Group" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__route_type +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__route_type +msgid "Route Type" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,help:endpoint.field_endpoint_endpoint__route_group +#: model:ir.model.fields,help:endpoint.field_endpoint_mixin__route_group +msgid "Use this to classify routes together" +msgstr "" + +#. module: endpoint +#: code:addons/endpoint/models/endpoint_mixin.py:0 +#, python-format +msgid "code_snippet should return a dict into `result` variable." +msgstr "" From f1b8edfebdc44a95dc501b97410eff5ee4629c56 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 15 Jul 2022 12:40:04 +0000 Subject: [PATCH 17/58] [UPD] README.rst --- endpoint/README.rst | 101 ++++++++++++++++++++++++- endpoint/static/description/index.html | 50 ++++++++---- 2 files changed, 134 insertions(+), 17 deletions(-) diff --git a/endpoint/README.rst b/endpoint/README.rst index 89bcd6c2..274ae9a6 100644 --- a/endpoint/README.rst +++ b/endpoint/README.rst @@ -1 +1,100 @@ -wait for the bot ;) +======== +Endpoint +======== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |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%2Fweb--api-lightgray.png?logo=github + :target: https://github.com/OCA/web-api/tree/14.0/endpoint + :alt: OCA/web-api +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-api-14-0/web-api-14-0-endpoint + :alt: Translate me on Weblate + +|badge1| |badge2| |badge3| |badge4| + +Provide an endpoint framework allowing users to define their own custom endpoint. + +Thanks to endpoint mixin the endpoint records are automatically registered as real Odoo routes. + +You can easily code what you want in the code snippet. + +NOTE: for security reasons any kind of RPC call is blocked on endpoint records. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Go to "Technical -> Endpoints" and create a new endpoint. + +Known issues / Roadmap +====================== + +* add validation of request data +* add api docs generation + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Simone Orsi + +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-simahawk| image:: https://github.com/simahawk.png?size=40px + :target: https://github.com/simahawk + :alt: simahawk + +Current `maintainer `__: + +|maintainer-simahawk| + +This module is part of the `OCA/web-api `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/endpoint/static/description/index.html b/endpoint/static/description/index.html index aa3410eb..0a1b8975 100644 --- a/endpoint/static/description/index.html +++ b/endpoint/static/description/index.html @@ -367,8 +367,11 @@

Endpoint

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Alpha License: LGPL-3 OCA/edi Translate me on Weblate Try me on Runbot

-

This module creates Endpoint frameworks to be used globally

+

Alpha License: LGPL-3 OCA/web-api Translate me on Weblate

+

Provide an endpoint framework allowing users to define their own custom endpoint.

+

Thanks to endpoint mixin the endpoint records are automatically registered as real Odoo routes.

+

You can easily code what you want in the code snippet.

+

NOTE: for security reasons any kind of RPC call is blocked on endpoint records.

Important

This is an alpha version, the data model and design can change at any time without warning. @@ -378,45 +381,60 @@

Endpoint

Table of contents

+
+

Configuration

+

Go to “Technical -> Endpoints” and create a new endpoint.

+
+
+

Known issues / Roadmap

+
    +
  • add validation of request data
  • +
  • add api docs generation
  • +
+
-

Bug Tracker

-

Bugs are tracked on GitHub Issues. +

Bug Tracker

+

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Camptocamp
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

-

This module is part of the OCA/edi project on GitHub.

+

Current maintainer:

+

simahawk

+

This module is part of the OCA/web-api project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

From 58a8569286c0c8c17c8eaefb036c5800328da5ba Mon Sep 17 00:00:00 2001 From: Claude R Perrin Date: Thu, 15 Sep 2022 18:32:51 +0000 Subject: [PATCH 18/58] Added translation using Weblate (French) --- endpoint/i18n/fr.po | 225 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 endpoint/i18n/fr.po diff --git a/endpoint/i18n/fr.po b/endpoint/i18n/fr.po new file mode 100644 index 00000000..d60c6b99 --- /dev/null +++ b/endpoint/i18n/fr.po @@ -0,0 +1,225 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * endpoint +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: fr\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" + +#. module: endpoint +#: code:addons/endpoint/models/endpoint_mixin.py:0 +#, python-format +msgid "'Exec as user' is mandatory for public endpoints." +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__active +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__active +msgid "Active" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_search_view +msgid "All" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_search_view +msgid "Archived" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +msgid "Auth" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__auth_type +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__auth_type +msgid "Auth Type" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +msgid "Code" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +msgid "Code Help" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__code_snippet +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__code_snippet +msgid "Code Snippet" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__code_snippet_docs +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__code_snippet_docs +msgid "Code Snippet Docs" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__create_uid +msgid "Created by" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__create_date +msgid "Created on" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__csrf +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__csrf +msgid "Csrf" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__display_name +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__display_name +msgid "Display Name" +msgstr "" + +#. module: endpoint +#: model:ir.model,name:endpoint.model_endpoint_endpoint +msgid "Endpoint" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__endpoint_hash +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__endpoint_hash +msgid "Endpoint Hash" +msgstr "" + +#. module: endpoint +#: model:ir.model,name:endpoint.model_endpoint_mixin +msgid "Endpoint mixin" +msgstr "" + +#. module: endpoint +#: model:ir.actions.act_window,name:endpoint.endpoint_endpoint_act_window +#: model:ir.ui.menu,name:endpoint.endpoint_endpoint_menu +msgid "Endpoints" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__exec_as_user_id +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__exec_as_user_id +msgid "Exec As User" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__exec_mode +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__exec_mode +msgid "Exec Mode" +msgstr "" + +#. module: endpoint +#: code:addons/endpoint/models/endpoint_mixin.py:0 +#, python-format +msgid "Exec mode is set to `Code`: you must provide a piece of code" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__id +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__id +msgid "ID" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,help:endpoint.field_endpoint_endpoint__endpoint_hash +#: model:ir.model.fields,help:endpoint.field_endpoint_mixin__endpoint_hash +msgid "Identify the route with its main params" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint____last_update +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin____last_update +msgid "Last Modified on" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__write_date +msgid "Last Updated on" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +msgid "Main" +msgstr "" + +#. module: endpoint +#: code:addons/endpoint/models/endpoint_mixin.py:0 +#, python-format +msgid "Missing handler for exec mode %s" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__name +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__name +msgid "Name" +msgstr "" + +#. module: endpoint +#: model_terms:ir.ui.view,arch_db:endpoint.endpoint_endpoint_form_view +msgid "Request" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__request_content_type +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__request_content_type +msgid "Request Content Type" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__request_method +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__request_method +msgid "Request Method" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__route +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__route +msgid "Route" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__route_group +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__route_group +msgid "Route Group" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,field_description:endpoint.field_endpoint_endpoint__route_type +#: model:ir.model.fields,field_description:endpoint.field_endpoint_mixin__route_type +msgid "Route Type" +msgstr "" + +#. module: endpoint +#: model:ir.model.fields,help:endpoint.field_endpoint_endpoint__route_group +#: model:ir.model.fields,help:endpoint.field_endpoint_mixin__route_group +msgid "Use this to classify routes together" +msgstr "" + +#. module: endpoint +#: code:addons/endpoint/models/endpoint_mixin.py:0 +#, python-format +msgid "code_snippet should return a dict into `result` variable." +msgstr "" From 921ab6588e60d122248f806e820c6cd7a94b4416 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 15 Jun 2022 21:40:20 +0200 Subject: [PATCH 19/58] endpoint: adapt to endpoint_route_handler --- endpoint/models/endpoint_mixin.py | 11 ++++++----- endpoint/tests/test_endpoint_controller.py | 9 --------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/endpoint/models/endpoint_mixin.py b/endpoint/models/endpoint_mixin.py index a0c490b5..98ff6724 100644 --- a/endpoint/models/endpoint_mixin.py +++ b/endpoint/models/endpoint_mixin.py @@ -3,7 +3,6 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import textwrap -from functools import partial import werkzeug @@ -12,8 +11,6 @@ from odoo.addons.rpc_helper.decorator import disable_rpc -from ..controllers.main import EndpointController - @disable_rpc() # Block ALL RPC calls class EndpointMixin(models.AbstractModel): @@ -134,8 +131,12 @@ def _code_snippet_valued(self): ] ) - def _default_endpoint_handler(self): - return partial(EndpointController().auto_endpoint, self.route) + def _default_endpoint_options_handler(self): + return { + "klass_dotted_path": "odoo.addons.endpoint.controllers.main.EndpointController", + "method_name": "auto_endpoint", + "default_pargs": (self.route,), + } def _validate_request(self, request): http_req = request.httprequest diff --git a/endpoint/tests/test_endpoint_controller.py b/endpoint/tests/test_endpoint_controller.py index 03238133..80c9a385 100644 --- a/endpoint/tests/test_endpoint_controller.py +++ b/endpoint/tests/test_endpoint_controller.py @@ -9,18 +9,9 @@ from odoo.tests.common import HttpCase from odoo.tools.misc import mute_logger -# odoo.addons.base.models.res_users: Login successful for db:openerp_test login:admin from n/a -# endpoint.endpoint: Registered controller /demo/one/new (auth: user_endpoint) -# odoo.addons.endpoint.models.ir_http: DROPPED /demo/one -# odoo.addons.endpoint.models.ir_http: LOADED /demo/one/new -# odoo.addons.endpoint.models.ir_http: Endpoint routing map re-loaded - @unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "EndpointHttpCase skipped") class EndpointHttpCase(HttpCase): - def setUp(self): - super().setUp() - def test_call1(self): response = self.url_open("/demo/one") self.assertEqual(response.status_code, 401) From f268fb2673ea042487c80a9ab06f919dacd98e2e Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 25 Jul 2022 17:26:30 +0200 Subject: [PATCH 20/58] endpoint: use registry_sync flag and improve tests --- endpoint/__manifest__.py | 1 + endpoint/data/server_action.xml | 13 ++++++++ endpoint/tests/test_endpoint.py | 53 +++++++++++++++++++++++--------- endpoint/views/endpoint_view.xml | 27 ++++++++++++++-- 4 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 endpoint/data/server_action.xml diff --git a/endpoint/__manifest__.py b/endpoint/__manifest__.py index 048b5c1d..a1dcb8f1 100644 --- a/endpoint/__manifest__.py +++ b/endpoint/__manifest__.py @@ -14,6 +14,7 @@ "data": [ "security/ir.model.access.csv", "views/endpoint_view.xml", + "data/server_action.xml", ], "demo": [ "demo/endpoint_demo.xml", diff --git a/endpoint/data/server_action.xml b/endpoint/data/server_action.xml new file mode 100644 index 00000000..2c0dc14b --- /dev/null +++ b/endpoint/data/server_action.xml @@ -0,0 +1,13 @@ + + + + Sync registry + ir.actions.server + + + code + +records.filtered(lambda x: not x.registry_sync).write({"registry_sync": True}) + + + diff --git a/endpoint/tests/test_endpoint.py b/endpoint/tests/test_endpoint.py index 2b4ea12c..8877bd84 100644 --- a/endpoint/tests/test_endpoint.py +++ b/endpoint/tests/test_endpoint.py @@ -5,6 +5,7 @@ import json import textwrap +import mock import psycopg2 import werkzeug @@ -92,7 +93,7 @@ def test_endpoint_code_eval_free_vals(self): payload = result["payload"] self.assertEqual(json.loads(payload), {"a": 1, "b": 2}) - @mute_logger("endpoint.endpoint") + @mute_logger("endpoint.endpoint", "odoo.modules.registry") def test_endpoint_validate_request(self): endpoint = self.endpoint.copy( { @@ -111,6 +112,7 @@ def test_endpoint_validate_request(self): ) as req: endpoint._validate_request(req) + @mute_logger("odoo.modules.registry") def test_routing(self): route, info, __ = self.endpoint._get_routing_info() self.assertEqual(route, "/demo/one") @@ -160,6 +162,7 @@ def test_routing(self): ) type(endpoint)._endpoint_route_prefix = "" + @mute_logger("odoo.modules.registry") def test_unlink(self): endpoint = self.endpoint.copy( { @@ -170,12 +173,15 @@ def test_unlink(self): "exec_as_user_id": self.env.user.id, } ) - registry = endpoint._endpoint_registry + endpoint._handle_registry_sync(endpoint.ids) + key = endpoint._endpoint_registry_unique_key() + reg = endpoint._endpoint_registry + self.assertEqual(reg._get_rule(key).route, "/delete/this") endpoint.unlink() - http_id = self.env["ir.http"]._endpoint_make_http_id() - self.assertTrue(registry.routing_update_required(http_id)) + self.assertEqual(reg._get_rule(key), None) - def test_archiving(self): + @mute_logger("odoo.modules.registry") + def test_archive(self): endpoint = self.endpoint.copy( { "route": "/enable-disable/this", @@ -185,14 +191,33 @@ def test_archiving(self): "exec_as_user_id": self.env.user.id, } ) + endpoint._handle_registry_sync(endpoint.ids) self.assertTrue(endpoint.active) - registry = endpoint._endpoint_registry - http_id = self.env["ir.http"]._endpoint_make_http_id() - fake_2nd_http_id = id(2) - registry.ir_http_track(http_id) - self.assertFalse(registry.routing_update_required(http_id)) - self.assertFalse(registry.routing_update_required(fake_2nd_http_id)) - + key = endpoint._endpoint_registry_unique_key() + reg = endpoint._endpoint_registry + self.assertEqual(reg._get_rule(key).route, "/enable-disable/this") endpoint.active = False - self.assertTrue(registry.routing_update_required(http_id)) - self.assertFalse(registry.routing_update_required(fake_2nd_http_id)) + endpoint._handle_registry_sync(endpoint.ids) + self.assertEqual(reg._get_rule(key), None) + + def test_registry_sync(self): + endpoint = self.env["endpoint.endpoint"].create( + { + "name": "New", + "route": "/not/active/yet", + "exec_mode": "code", + "code_snippet": "foo = 1", + "request_method": "GET", + "auth_type": "user_endpoint", + } + ) + self.assertFalse(endpoint.registry_sync) + key = endpoint._endpoint_registry_unique_key() + reg = endpoint._endpoint_registry + self.assertEqual(reg._get_rule(key), None) + with mock.patch.object(type(self.env.cr), "after") as mocked: + endpoint.registry_sync = True + self.assertEqual(mocked.call_args[0][0], "commit") + partial_func = mocked.call_args[0][1] + self.assertEqual(partial_func.args, ([endpoint.id],)) + self.assertEqual(partial_func.func.__name__, "_handle_registry_sync") diff --git a/endpoint/views/endpoint_view.xml b/endpoint/views/endpoint_view.xml index 3072ee33..196cde78 100644 --- a/endpoint/views/endpoint_view.xml +++ b/endpoint/views/endpoint_view.xml @@ -10,13 +10,26 @@
+ +