From b83407017a03b51e823003985a81fd39c33f4ee4 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 25 Oct 2021 17:22:23 +0200 Subject: [PATCH 01/91] Add endpoint_route_handler --- endpoint_route_handler/README.rst | 1 + endpoint_route_handler/__init__.py | 1 + endpoint_route_handler/__manifest__.py | 16 + endpoint_route_handler/models/__init__.py | 2 + .../models/endpoint_route_handler.py | 279 ++++++++++++ endpoint_route_handler/models/ir_http.py | 119 +++++ .../readme/CONTRIBUTORS.rst | 1 + endpoint_route_handler/readme/DESCRIPTION.rst | 4 + endpoint_route_handler/readme/ROADMAP.rst | 2 + endpoint_route_handler/readme/USAGE.rst | 38 ++ endpoint_route_handler/registry.py | 84 ++++ .../security/ir.model.access.csv | 3 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 425 ++++++++++++++++++ endpoint_route_handler/tests/__init__.py | 2 + endpoint_route_handler/tests/common.py | 52 +++ endpoint_route_handler/tests/test_endpoint.py | 87 ++++ .../tests/test_endpoint_controller.py | 72 +++ 18 files changed, 1188 insertions(+) create mode 100644 endpoint_route_handler/README.rst create mode 100644 endpoint_route_handler/__init__.py create mode 100644 endpoint_route_handler/__manifest__.py create mode 100644 endpoint_route_handler/models/__init__.py create mode 100644 endpoint_route_handler/models/endpoint_route_handler.py create mode 100644 endpoint_route_handler/models/ir_http.py create mode 100644 endpoint_route_handler/readme/CONTRIBUTORS.rst create mode 100644 endpoint_route_handler/readme/DESCRIPTION.rst create mode 100644 endpoint_route_handler/readme/ROADMAP.rst create mode 100644 endpoint_route_handler/readme/USAGE.rst create mode 100644 endpoint_route_handler/registry.py create mode 100644 endpoint_route_handler/security/ir.model.access.csv create mode 100644 endpoint_route_handler/static/description/icon.png create mode 100644 endpoint_route_handler/static/description/index.html create mode 100644 endpoint_route_handler/tests/__init__.py create mode 100644 endpoint_route_handler/tests/common.py create mode 100644 endpoint_route_handler/tests/test_endpoint.py create mode 100644 endpoint_route_handler/tests/test_endpoint_controller.py diff --git a/endpoint_route_handler/README.rst b/endpoint_route_handler/README.rst new file mode 100644 index 00000000..89bcd6c2 --- /dev/null +++ b/endpoint_route_handler/README.rst @@ -0,0 +1 @@ +wait for the bot ;) diff --git a/endpoint_route_handler/__init__.py b/endpoint_route_handler/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/endpoint_route_handler/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py new file mode 100644 index 00000000..bca59d25 --- /dev/null +++ b/endpoint_route_handler/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2021 Camptcamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": " Route route handler", + "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", + "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", + ], +} diff --git a/endpoint_route_handler/models/__init__.py b/endpoint_route_handler/models/__init__.py new file mode 100644 index 00000000..1755a4e5 --- /dev/null +++ b/endpoint_route_handler/models/__init__.py @@ -0,0 +1,2 @@ +from . import endpoint_route_handler +from . import ir_http diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py new file mode 100644 index 00000000..28fbf0cd --- /dev/null +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -0,0 +1,279 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +from werkzeug.routing import Rule + +from odoo import _, api, exceptions, fields, http, models + +# from odoo.addons.base_sparse_field.models.fields import Serialized +from ..registry import EndpointRegistry + +ENDPOINT_ROUTE_CONSUMER_MODELS = { + # by db +} + + +class EndpointRouteHandler(models.AbstractModel): + + _name = "endpoint.route.handler" + _description = "Endpoint Route handler" + + 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_group = fields.Char(help="Use this to classify routes together") + route_type = fields.Selection(selection="_selection_route_type", default="http") + auth_type = fields.Selection( + selection="_selection_auth_type", default="user_endpoint" + ) + request_content_type = fields.Selection(selection="_selection_request_content_type") + # TODO: this is limiting the possibility of supporting more than one method. + request_method = fields.Selection( + selection="_selection_request_method", required=True + ) + # # TODO: validate params? Just for doc? Maybe use Cerberus? + # # -> For now let the implementer validate the params in the snippet. + # request_params = Serialized(help="TODO") + + endpoint_hash = fields.Char( + compute="_compute_endpoint_hash", help="Identify the route with its main params" + ) + + csrf = fields.Boolean(default=False) + + _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. + # TODO: shall we check for route existance in the registry instead? + all_models = self._get_endpoint_route_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_route_consumer_models(self): + global ENDPOINT_ROUTE_CONSUMER_MODELS + if ENDPOINT_ROUTE_CONSUMER_MODELS.get(self.env.cr.dbname): + return ENDPOINT_ROUTE_CONSUMER_MODELS.get(self.env.cr.dbname) + models = [] + route_model = "endpoint.route.handler" + for model in self.env.values(): + if ( + model._name != route_model + and not model._abstract + and route_model in model._inherit + ): + models.append(model._name) + ENDPOINT_ROUTE_CONSUMER_MODELS[self.env.cr.dbname] = 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"), + ] + + @api.depends(lambda self: self._controller_fields()) + def _compute_endpoint_hash(self): + # Do not use read to be able to play this on NewId records too + # (NewId records are classified as missing in ACL check). + # values = self.read(self._controller_fields()) + values = [ + {fname: rec[fname] for fname in self._controller_fields()} for rec in self + ] + for rec, vals in zip(self, values): + vals.pop("id", None) + rec.endpoint_hash = hash(tuple(vals.values())) + + def _controller_fields(self): + return ["route", "auth_type", "request_method"] + + @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() + + # TODO: move to something better? Eg: computed field? + # Shall we use the route_group? TBD! + _endpoint_route_prefix = "" + + 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("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.") + ) + + # 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._unregister_controller() + return super().unlink() + + def _register_hook(self): + super()._register_hook() + if not self._abstract: + self.search([])._register_controllers() + + def _register_controllers(self): + if self._abstract: + self._refresh_endpoint_data() + for rec in self: + rec._register_controller() + + def _refresh_endpoint_data(self): + """Enforce refresh of route computed fields. + + Required for NewId records when using this model as a tool. + """ + self._compute_endpoint_hash() + self._compute_route() + + @property + def _endpoint_registry(self): + return EndpointRegistry.registry_for(self.env.cr.dbname) + + def _register_controller(self, endpoint_handler=None, key=None): + rule = self._make_controller_rule(endpoint_handler=endpoint_handler) + key = key or self._endpoint_registry_unique_key() + self._endpoint_registry.add_or_update_rule(key, rule) + self._logger.info( + "Registered controller %s (auth: %s)", self.route, self.auth_type + ) + + def _make_controller_rule(self, endpoint_handler=None): + route, routing, endpoint_hash = self._get_routing_info() + endpoint_handler = endpoint_handler or self._default_endpoint_handler() + assert callable(endpoint_handler) + endpoint = http.EndPoint(endpoint_handler, routing) + rule = Rule(route, endpoint=endpoint, methods=routing["methods"]) + rule.merge_slashes = False + rule._auto_endpoint = True + rule._endpoint_hash = endpoint_hash + rule._endpoint_group = self.route_group + return rule + + def _default_endpoint_handler(self): + """Provide default endpoint handler. + + :return: bound method of a controller (eg: MyController()._my_handler) + """ + raise NotImplementedError("No default endpoint handler defined.") + + def _get_routing_info(self): + route = self.route + routing = dict( + type=self.route_type, + auth=self.auth_type, + methods=[self.request_method], + routes=[route], + csrf=self.csrf, + ) + return route, routing, self.endpoint_hash + + def _endpoint_registry_unique_key(self): + return "{0._name}:{0.id}".format(self) + + def _unregister_controller(self, key=None): + key = key or self._endpoint_registry_unique_key() + self._endpoint_registry.drop_rule(key) diff --git a/endpoint_route_handler/models/ir_http.py b/endpoint_route_handler/models/ir_http.py new file mode 100644 index 00000000..63c940b6 --- /dev/null +++ b/endpoint_route_handler/models/ir_http.py @@ -0,0 +1,119 @@ +# 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 ..registry import EndpointRegistry + +_logger = logging.getLogger(__name__) + + +def safely_add_rule(rmap, rule): + """Add rule to given route map without breaking.""" + if rule.endpoint not in rmap._rules_by_endpoint: + # When the rmap gets re-generated, unbound the old one. + if rule.map: + rule.bind(rmap, rebind=True) + else: + rmap.add(rule) + _logger.info("LOADED %s", str(rule)) + + +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"): + cr = http.request.env.cr + endpoint_registry = EndpointRegistry.registry_for(cr.dbname) + if not hasattr(cls, "_endpoint_routing_map_loaded"): + # First load, register all endpoint routes + cls._load_endpoint_routing_map(rmap, endpoint_registry) + 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) + endpoint_registry.reset_update_required() + return rmap + + @classmethod + def _clear_routing_map(cls): + super()._clear_routing_map() + if hasattr(cls, "_endpoint_routing_map_loaded"): + delattr(cls, "_endpoint_routing_map_loaded") + + @classmethod + def _load_endpoint_routing_map(cls, rmap, endpoint_registry): + for rule in endpoint_registry.get_rules(): + safely_add_rule(rmap, 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, endpoint_registry): + """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: + safely_add_rule(rmap, 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_route_handler/readme/CONTRIBUTORS.rst b/endpoint_route_handler/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..f1c71bce --- /dev/null +++ b/endpoint_route_handler/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Simone Orsi diff --git a/endpoint_route_handler/readme/DESCRIPTION.rst b/endpoint_route_handler/readme/DESCRIPTION.rst new file mode 100644 index 00000000..66675612 --- /dev/null +++ b/endpoint_route_handler/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +Technical module that provides a base handler +for adding and removing controller routes on the fly. + +Can be used as a mixin or as a tool. diff --git a/endpoint_route_handler/readme/ROADMAP.rst b/endpoint_route_handler/readme/ROADMAP.rst new file mode 100644 index 00000000..18233e35 --- /dev/null +++ b/endpoint_route_handler/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* add api docs helpers +* allow multiple HTTP methods on the same endpoint diff --git a/endpoint_route_handler/readme/USAGE.rst b/endpoint_route_handler/readme/USAGE.rst new file mode 100644 index 00000000..75864398 --- /dev/null +++ b/endpoint_route_handler/readme/USAGE.rst @@ -0,0 +1,38 @@ +As a mixin +~~~~~~~~~~ + +Use standard Odoo inheritance:: + + class MyModel(models.Model): + _name = "my.model" + _inherit = "endpoint.route.handler" + +Once you have this, each `my.model` record will generate a route. +You can have a look at the `endpoint` module to see a real life example. + + +As a tool +~~~~~~~~~ + +Initialize non stored route handlers and generate routes from them. +For instance:: + + route_handler = self.env["endpoint.route.handler"] + endpoint_handler = MyController()._my_handler + vals = { + "name": "My custom route", + "route": "/my/custom/route", + "request_method": "GET", + "auth_type": "public", + } + new_route = route_handler.new(vals) + new_route._refresh_endpoint_data() # required only for NewId records + new_route._register_controller(endpoint_handler=endpoint_handler, key="my-custom-route") + +Of course, what happens when the endpoint gets called +depends on the logic defined on the controller method. + +In both cases (mixin and tool) when a new route is generated or an existing one is updated, +the `ir.http.routing_map` (which holds all Odoo controllers) will be updated. + +You can see a real life example on `shopfloor.app` model. diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py new file mode 100644 index 00000000..3b4adf2c --- /dev/null +++ b/endpoint_route_handler/registry.py @@ -0,0 +1,84 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +_REGISTRY_BY_DB = {} + + +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 = {} + self._routing_update_required = False + self._rules_to_load = [] + self._rules_to_drop = [] + + def get_rules(self): + return self._mapping.values() + + # TODO: add test + def get_rules_by_group(self, group): + for key, rule in self._mapping.items(): + if rule._endpoint_group == group: + yield (key, rule) + + def add_or_update_rule(self, key, rule, force=False): + existing = self._mapping.get(key) + if not existing: + self._mapping[key] = rule + self._rules_to_load.append(rule) + self._routing_update_required = True + 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 = [] + + @classmethod + def registry_for(cls, dbname): + if dbname not in _REGISTRY_BY_DB: + _REGISTRY_BY_DB[dbname] = cls() + return _REGISTRY_BY_DB[dbname] + + @classmethod + def wipe_registry_for(cls, dbname): + if dbname in _REGISTRY_BY_DB: + del _REGISTRY_BY_DB[dbname] diff --git a/endpoint_route_handler/security/ir.model.access.csv b/endpoint_route_handler/security/ir.model.access.csv new file mode 100644 index 00000000..ec4133f1 --- /dev/null +++ b/endpoint_route_handler/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_endpoint_route_handler_mngr_edit,endpoint_route_handler mngr edit,model_endpoint_route_handler,base.group_system,1,1,1,1 +access_endpoint_route_handler_edit,endpoint_route_handler edit,model_endpoint_route_handler,,1,0,0,0 diff --git a/endpoint_route_handler/static/description/icon.png b/endpoint_route_handler/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_route_handler/static/description/index.html b/endpoint_route_handler/static/description/index.html new file mode 100644 index 00000000..aa3410eb --- /dev/null +++ b/endpoint_route_handler/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_route_handler/tests/__init__.py b/endpoint_route_handler/tests/__init__.py new file mode 100644 index 00000000..6885a0f9 --- /dev/null +++ b/endpoint_route_handler/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_endpoint +from . import test_endpoint_controller diff --git a/endpoint_route_handler/tests/common.py b/endpoint_route_handler/tests/common.py new file mode 100644 index 00000000..e5427ce4 --- /dev/null +++ b/endpoint_route_handler/tests/common.py @@ -0,0 +1,52 @@ +# 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() + cls.route_handler = cls.env["endpoint.route.handler"] + + @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_route_handler/tests/test_endpoint.py b/endpoint_route_handler/tests/test_endpoint.py new file mode 100644 index 00000000..3a4d8479 --- /dev/null +++ b/endpoint_route_handler/tests/test_endpoint.py @@ -0,0 +1,87 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +from functools import partial + +from odoo.http import Controller + +from ..registry import EndpointRegistry +from .common import CommonEndpoint + + +class TestEndpoint(CommonEndpoint): + def tearDown(self): + self.env["ir.http"]._clear_routing_map() + EndpointRegistry.wipe_registry_for(self.env.cr.dbname) + super().tearDown() + + def _make_new_route(self, **kw): + vals = { + "name": "Test custom route", + "route": "/my/test/route", + "request_method": "GET", + } + vals.update(kw) + new_route = self.route_handler.new(vals) + new_route._refresh_endpoint_data() + return new_route + + def test_as_tool_base_data(self): + new_route = self._make_new_route() + self.assertEqual(new_route.route, "/my/test/route") + first_hash = new_route.endpoint_hash + self.assertTrue(first_hash) + new_route.route += "/new" + new_route._refresh_endpoint_data() + self.assertNotEqual(new_route.endpoint_hash, first_hash) + + def test_as_tool_register_controller_no_default(self): + new_route = self._make_new_route() + # No specific controller + with self.assertRaisesRegex( + NotImplementedError, "No default endpoint handler defined." + ): + new_route._register_controller() + + def test_as_tool_register_controller(self): + new_route = self._make_new_route() + + class TestController(Controller): + def _do_something(self, route): + return "ok" + + endpoint_handler = partial(TestController()._do_something, new_route.route) + with self._get_mocked_request() as req: + req.registry._init_modules = set() + new_route._register_controller(endpoint_handler=endpoint_handler) + # Ensure the routing rule is registered + rmap = self.env["ir.http"].routing_map() + self.assertIn("/my/test/route", [x.rule for x in rmap._rules]) + # Ensure is updated when needed + new_route.route += "/new" + new_route._refresh_endpoint_data() + with self._get_mocked_request() as req: + new_route._register_controller(endpoint_handler=endpoint_handler) + rmap = self.env["ir.http"].routing_map() + self.assertNotIn("/my/test/route", [x.rule for x in rmap._rules]) + self.assertIn("/my/test/route/new", [x.rule for x in rmap._rules]) + + def test_as_tool_register_controller_dynamic_route(self): + route = "/my/app/" + new_route = self._make_new_route(route=route) + + class TestController(Controller): + def _do_something(self, foo=None): + return "ok" + + endpoint_handler = TestController()._do_something + with self._get_mocked_request() as req: + req.registry._init_modules = set() + new_route._register_controller(endpoint_handler=endpoint_handler) + # Ensure the routing rule is registered + rmap = self.env["ir.http"].routing_map() + self.assertIn(route, [x.rule for x in rmap._rules]) + + # TODO: test unregister diff --git a/endpoint_route_handler/tests/test_endpoint_controller.py b/endpoint_route_handler/tests/test_endpoint_controller.py new file mode 100644 index 00000000..717166b7 --- /dev/null +++ b/endpoint_route_handler/tests/test_endpoint_controller.py @@ -0,0 +1,72 @@ +# Copyright 2021 Camptcamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import os +import unittest +from functools import partial + +from odoo.http import Controller +from odoo.tests.common import HttpCase + +from ..registry import EndpointRegistry + + +class TestController(Controller): + def _do_something1(self, foo=None): + return f"Got: {foo}" + + def _do_something2(self, default_arg, foo=None): + return f"{default_arg} -> got: {foo}" + + +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "EndpointHttpCase skipped") +class EndpointHttpCase(HttpCase): + def setUp(self): + super().setUp() + self.route_handler = self.env["endpoint.route.handler"] + + def tearDown(self): + EndpointRegistry.wipe_registry_for(self.env.cr.dbname) + super().tearDown() + + def _make_new_route(self, register=True, **kw): + vals = { + "name": "Test custom route", + "request_method": "GET", + } + vals.update(kw) + new_route = self.route_handler.new(vals) + new_route._refresh_endpoint_data() + return new_route + + def _register_controller(self, route_obj, endpoint_handler=None): + endpoint_handler = endpoint_handler or TestController()._do_something1 + route_obj._register_controller(endpoint_handler=endpoint_handler) + + def test_call(self): + new_route = self._make_new_route(route="/my/test/") + self._register_controller(new_route) + + route = "/my/test/working" + response = self.url_open(route) + self.assertEqual(response.status_code, 401) + # Let's login now + self.authenticate("admin", "admin") + response = self.url_open(route) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"Got: working") + + def test_call_advanced_endpoint_handler(self): + new_route = self._make_new_route(route="/my/advanced/test/") + endpoint_handler = partial(TestController()._do_something2, "DEFAULT") + self._register_controller(new_route, endpoint_handler=endpoint_handler) + + route = "/my/advanced/test/working" + response = self.url_open(route) + self.assertEqual(response.status_code, 401) + # Let's login now + self.authenticate("admin", "admin") + response = self.url_open(route) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"DEFAULT -> got: working") From 73ab61cc133bfb25ef42996bb80100d3088240eb Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 18 Nov 2021 12:56:01 +0100 Subject: [PATCH 02/91] endpoint_route_handler: reduce log noise --- endpoint_route_handler/models/endpoint_route_handler.py | 2 +- endpoint_route_handler/models/ir_http.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index 28fbf0cd..2ca80bec 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -237,7 +237,7 @@ def _register_controller(self, endpoint_handler=None, key=None): rule = self._make_controller_rule(endpoint_handler=endpoint_handler) key = key or self._endpoint_registry_unique_key() self._endpoint_registry.add_or_update_rule(key, rule) - self._logger.info( + self._logger.debug( "Registered controller %s (auth: %s)", self.route, self.auth_type ) diff --git a/endpoint_route_handler/models/ir_http.py b/endpoint_route_handler/models/ir_http.py index 63c940b6..742b7140 100644 --- a/endpoint_route_handler/models/ir_http.py +++ b/endpoint_route_handler/models/ir_http.py @@ -21,7 +21,7 @@ def safely_add_rule(rmap, rule): rule.bind(rmap, rebind=True) else: rmap.add(rule) - _logger.info("LOADED %s", str(rule)) + _logger.debug("LOADED %s", str(rule)) class IrHttp(models.AbstractModel): From e63272afc0c2ae800d9001e113fad28b8b383749 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 18 Nov 2021 14:04:05 +0000 Subject: [PATCH 03/91] endpoint_route_handler 14.0.1.0.1 --- endpoint_route_handler/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index bca59d25..d192fd0a 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -4,7 +4,7 @@ { "name": " Route route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "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 b91e1231d5a11f1096e4ebab3598f8f9d8e98d81 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 28 Dec 2021 14:11:40 +0100 Subject: [PATCH 04/91] endpoint_route_handler: fix archive/unarchive When an endpoint is archived it must be dropped. When it's unarchive it must be restored. --- .../models/endpoint_route_handler.py | 36 ++++++++++++++----- endpoint_route_handler/registry.py | 4 +-- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index 2ca80bec..3de5d5b4 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -193,27 +193,39 @@ def _check_request_method(self): @api.model_create_multi def create(self, vals_list): - res = super().create(vals_list) - if not self._abstract: - res._register_controllers() - return res + rec = super().create(vals_list) + if not self._abstract and rec.active: + rec._register_controllers() + return rec 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() + if not self._abstract: + self._handle_route_updates(vals) return res + def _handle_route_updates(self, vals): + if "active" in vals: + if vals["active"]: + self._register_controllers() + else: + self._unregister_controllers() + return True + if any([x in vals for x in self._controller_fields()]): + self._register_controllers() + return True + return False + def unlink(self): if not self._abstract: - for rec in self: - rec._unregister_controller() + self._unregister_controllers() return super().unlink() def _register_hook(self): super()._register_hook() if not self._abstract: - self.search([])._register_controllers() + # Look explicitly for active records + self.search([("active", "=", True)])._register_controllers() def _register_controllers(self): if self._abstract: @@ -221,6 +233,12 @@ def _register_controllers(self): for rec in self: rec._register_controller() + def _unregister_controllers(self): + if self._abstract: + self._refresh_endpoint_data() + for rec in self: + rec._unregister_controller() + def _refresh_endpoint_data(self): """Enforce refresh of route computed fields. diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 3b4adf2c..0f7290ea 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -36,7 +36,7 @@ def get_rules_by_group(self, group): def add_or_update_rule(self, key, rule, force=False): existing = self._mapping.get(key) - if not existing: + if not existing or force: self._mapping[key] = rule self._rules_to_load.append(rule) self._routing_update_required = True @@ -50,7 +50,7 @@ def add_or_update_rule(self, key, rule, force=False): return True def drop_rule(self, key): - existing = self._mapping.get(key) + existing = self._mapping.pop(key, None) if not existing: return False # Override and set as to be updated From 54abfc9a2d5eb91021a2760e3d0393f8ac017522 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 29 Dec 2021 15:01:01 +0100 Subject: [PATCH 05/91] endpoint_route_handler: fix multi env handling Routing maps are generated **per env** which means that every new env will have its own routing map attached to `ir.http` registry class. This is not desired (as per core Odoo comment) but it's like this today :/ Hence, before this change, the routing map could be mis-aligned across different envs leading to random responses for custom endpoints. This refactoring simplifies a lot the handling of the rules leaving to std `_generate_routing_rules` the duty to yield rules and to `routing_map` to generate them for the new route map. EndpointRegistry memory consumption is improved too thanks to smaller data to store and to the usage of __slots__. --- .../models/endpoint_route_handler.py | 40 +++--- endpoint_route_handler/models/ir_http.py | 127 +++++++----------- endpoint_route_handler/registry.py | 93 ++++++++----- endpoint_route_handler/tests/common.py | 1 + endpoint_route_handler/tests/test_endpoint.py | 8 +- .../tests/test_endpoint_controller.py | 1 + 6 files changed, 139 insertions(+), 131 deletions(-) diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index 3de5d5b4..b38cfa0f 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -4,8 +4,6 @@ import logging -from werkzeug.routing import Rule - from odoo import _, api, exceptions, fields, http, models # from odoo.addons.base_sparse_field.models.fields import Serialized @@ -200,8 +198,7 @@ def create(self, vals_list): def write(self, vals): res = super().write(vals) - if not self._abstract: - self._handle_route_updates(vals) + self._handle_route_updates(vals) return res def _handle_route_updates(self, vals): @@ -224,14 +221,16 @@ def unlink(self): def _register_hook(self): super()._register_hook() if not self._abstract: - # Look explicitly for active records - self.search([("active", "=", True)])._register_controllers() + # Look explicitly for active records. + # Pass `init` to not set the registry as updated + # since this piece of code runs only when the model is loaded. + self.search([("active", "=", True)])._register_controllers(init=True) - def _register_controllers(self): + def _register_controllers(self, init=False): if self._abstract: self._refresh_endpoint_data() for rec in self: - rec._register_controller() + rec._register_controller(init=init) def _unregister_controllers(self): if self._abstract: @@ -251,24 +250,29 @@ def _refresh_endpoint_data(self): def _endpoint_registry(self): return EndpointRegistry.registry_for(self.env.cr.dbname) - def _register_controller(self, endpoint_handler=None, key=None): - rule = self._make_controller_rule(endpoint_handler=endpoint_handler) - key = key or self._endpoint_registry_unique_key() - self._endpoint_registry.add_or_update_rule(key, rule) + def _register_controller(self, endpoint_handler=None, key=None, init=False): + rule = self._make_controller_rule(endpoint_handler=endpoint_handler, key=key) + self._endpoint_registry.add_or_update_rule(rule, init=init) self._logger.debug( "Registered controller %s (auth: %s)", self.route, self.auth_type ) - def _make_controller_rule(self, endpoint_handler=None): + def _make_controller_rule(self, endpoint_handler=None, key=None): + key = key or self._endpoint_registry_unique_key() route, routing, endpoint_hash = self._get_routing_info() endpoint_handler = endpoint_handler or self._default_endpoint_handler() assert callable(endpoint_handler) endpoint = http.EndPoint(endpoint_handler, routing) - rule = Rule(route, endpoint=endpoint, methods=routing["methods"]) - rule.merge_slashes = False - rule._auto_endpoint = True - rule._endpoint_hash = endpoint_hash - rule._endpoint_group = self.route_group + rule = self._endpoint_registry.make_rule( + # fmt: off + key, + route, + endpoint, + routing, + endpoint_hash, + route_group=self.route_group + # fmt: on + ) return rule def _default_endpoint_handler(self): diff --git a/endpoint_route_handler/models/ir_http.py b/endpoint_route_handler/models/ir_http.py index 742b7140..ee3c41bf 100644 --- a/endpoint_route_handler/models/ir_http.py +++ b/endpoint_route_handler/models/ir_http.py @@ -3,6 +3,7 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import logging +from itertools import chain import werkzeug @@ -13,92 +14,66 @@ _logger = logging.getLogger(__name__) -def safely_add_rule(rmap, rule): - """Add rule to given route map without breaking.""" - if rule.endpoint not in rmap._rules_by_endpoint: - # When the rmap gets re-generated, unbound the old one. - if rule.map: - rule.bind(rmap, rebind=True) - else: - rmap.add(rule) - _logger.debug("LOADED %s", str(rule)) - - 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"): - cr = http.request.env.cr - endpoint_registry = EndpointRegistry.registry_for(cr.dbname) - if not hasattr(cls, "_endpoint_routing_map_loaded"): - # First load, register all endpoint routes - cls._load_endpoint_routing_map(rmap, endpoint_registry) - 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) - endpoint_registry.reset_update_required() - return rmap + def _generate_routing_rules(cls, modules, converters): + # Override to inject custom endpoint rules. + return chain( + super()._generate_routing_rules(modules, converters), + cls._endpoint_routing_rules(), + ) @classmethod - def _clear_routing_map(cls): - super()._clear_routing_map() - if hasattr(cls, "_endpoint_routing_map_loaded"): - delattr(cls, "_endpoint_routing_map_loaded") + def _endpoint_routing_rules(cls): + """Yield custom endpoint rules""" + cr = http.request.env.cr + e_registry = EndpointRegistry.registry_for(cr.dbname) + for endpoint_rule in e_registry.get_rules(): + _logger.debug("LOADING %s", endpoint_rule) + endpoint = endpoint_rule.endpoint + for url in endpoint_rule.routing["routes"]: + yield (url, endpoint, endpoint_rule.routing) @classmethod - def _load_endpoint_routing_map(cls, rmap, endpoint_registry): - for rule in endpoint_registry.get_rules(): - safely_add_rule(rmap, 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])) + def routing_map(cls, key=None): + cr = http.request.env.cr + e_registry = EndpointRegistry.registry_for(cr.dbname) + + # Each `env` will have its own `ir.http` "class instance" + # thus, each instance will have its own routing map. + # Hence, we must keep track of which instances have been updated + # to make sure routing rules are always up to date across envs. + # + # In the original `routing_map` method it's reported in a comment + # that the routing map should be unique instead of being duplicated + # across envs... well, this is how it works today so we have to deal w/ it. + http_id = cls._endpoint_make_http_id() + + is_routing_map_new = not hasattr(cls, "_routing_map") + if is_routing_map_new or not e_registry.ir_http_seen(http_id): + # When the routing map is not ready yet, simply track current instance + e_registry.ir_http_track(http_id) + _logger.debug("ir_http instance `%s` tracked", http_id) + elif e_registry.ir_http_seen(http_id) and e_registry.routing_update_required( + http_id + ): + # This instance was already tracked + # and meanwhile the registry got updated: + # ensure all routes are re-loaded. + _logger.info( + "Endpoint registry updated, reset routing ma for `%s`", http_id + ) + cls._routing_map = {} + cls._rewrite_len = {} + e_registry.reset_update_required(http_id) + return super().routing_map(key=key) @classmethod - def _reload_endpoint_routing_map(cls, rmap, endpoint_registry): - """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: - safely_add_rule(rmap, rule) - _logger.info("Endpoint routing map re-loaded") + def _endpoint_make_http_id(cls): + """Generate current ir.http class ID.""" + return id(cls) @classmethod def _auth_method_user_endpoint(cls): diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 0f7290ea..2c76599c 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -10,20 +10,20 @@ class EndpointRegistry: 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. - + * track registered endpoints + * track routes to be updated for specific ir.http instances + * retrieve routing rules to load in ir.http routing map """ + __slots__ = ("_mapping", "_http_ids", "_http_ids_to_update") + def __init__(self): + # collect EndpointRule objects self._mapping = {} - self._routing_update_required = False - self._rules_to_load = [] - self._rules_to_drop = [] + # collect ids of ir.http instances + self._http_ids = set() + # collect ids of ir.http instances that need update + self._http_ids_to_update = set() def get_rules(self): return self._mapping.values() @@ -31,46 +31,46 @@ def get_rules(self): # TODO: add test def get_rules_by_group(self, group): for key, rule in self._mapping.items(): - if rule._endpoint_group == group: + if rule.endpoint_group == group: yield (key, rule) - def add_or_update_rule(self, key, rule, force=False): + def add_or_update_rule(self, rule, force=False, init=False): + """Add or update an existing rule. + + :param rule: instance of EndpointRule + :param force: replace a rule forcedly + :param init: given when adding rules for the first time + """ + key = rule.key existing = self._mapping.get(key) if not existing or force: self._mapping[key] = rule - self._rules_to_load.append(rule) - self._routing_update_required = True + if not init: + self._refresh_update_required() return True - if existing._endpoint_hash != rule._endpoint_hash: + 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 + if not init: + self._refresh_update_required() return True def drop_rule(self, key): existing = self._mapping.pop(key, None) if not existing: return False - # Override and set as to be updated - self._rules_to_drop.append(existing) - self._routing_update_required = True + self._refresh_update_required() 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, http_id): + return http_id in self._http_ids_to_update - def routing_update_required(self): - return self._routing_update_required + def _refresh_update_required(self): + for http_id in self._http_ids: + self._http_ids_to_update.add(http_id) - def reset_update_required(self): - self._routing_update_required = False - self._rules_to_drop = [] - self._rules_to_load = [] + def reset_update_required(self, http_id): + self._http_ids_to_update.discard(http_id) @classmethod def registry_for(cls, dbname): @@ -82,3 +82,32 @@ def registry_for(cls, dbname): def wipe_registry_for(cls, dbname): if dbname in _REGISTRY_BY_DB: del _REGISTRY_BY_DB[dbname] + + def ir_http_track(self, _id): + self._http_ids.add(_id) + + def ir_http_seen(self, _id): + return _id in self._http_ids + + @staticmethod + def make_rule(*a, **kw): + return EndpointRule(*a, **kw) + + +class EndpointRule: + """Hold information for a custom endpoint rule.""" + + __slots__ = ("key", "route", "endpoint", "routing", "endpoint_hash", "route_group") + + def __init__(self, key, route, endpoint, routing, endpoint_hash, route_group=None): + self.key = key + self.route = route + self.endpoint = endpoint + self.routing = routing + self.endpoint_hash = endpoint_hash + self.route_group = route_group + + def __repr__(self): + return f"{self.key}: {self.route}" + ( + f"[{self.route_group}]" if self.route_group else "" + ) diff --git a/endpoint_route_handler/tests/common.py b/endpoint_route_handler/tests/common.py index e5427ce4..dfad6614 100644 --- a/endpoint_route_handler/tests/common.py +++ b/endpoint_route_handler/tests/common.py @@ -49,4 +49,5 @@ def _get_mocked_request( for k, v in request_attrs.items(): setattr(mocked_request, k, v) mocked_request.make_response = lambda data, **kw: data + mocked_request.registry._init_modules = set() yield mocked_request diff --git a/endpoint_route_handler/tests/test_endpoint.py b/endpoint_route_handler/tests/test_endpoint.py index 3a4d8479..4be6cb02 100644 --- a/endpoint_route_handler/tests/test_endpoint.py +++ b/endpoint_route_handler/tests/test_endpoint.py @@ -53,8 +53,7 @@ def _do_something(self, route): return "ok" endpoint_handler = partial(TestController()._do_something, new_route.route) - with self._get_mocked_request() as req: - req.registry._init_modules = set() + with self._get_mocked_request(): new_route._register_controller(endpoint_handler=endpoint_handler) # Ensure the routing rule is registered rmap = self.env["ir.http"].routing_map() @@ -62,7 +61,7 @@ def _do_something(self, route): # Ensure is updated when needed new_route.route += "/new" new_route._refresh_endpoint_data() - with self._get_mocked_request() as req: + with self._get_mocked_request(): new_route._register_controller(endpoint_handler=endpoint_handler) rmap = self.env["ir.http"].routing_map() self.assertNotIn("/my/test/route", [x.rule for x in rmap._rules]) @@ -77,8 +76,7 @@ def _do_something(self, foo=None): return "ok" endpoint_handler = TestController()._do_something - with self._get_mocked_request() as req: - req.registry._init_modules = set() + with self._get_mocked_request(): new_route._register_controller(endpoint_handler=endpoint_handler) # Ensure the routing rule is registered rmap = self.env["ir.http"].routing_map() diff --git a/endpoint_route_handler/tests/test_endpoint_controller.py b/endpoint_route_handler/tests/test_endpoint_controller.py index 717166b7..e4e6e5e8 100644 --- a/endpoint_route_handler/tests/test_endpoint_controller.py +++ b/endpoint_route_handler/tests/test_endpoint_controller.py @@ -28,6 +28,7 @@ def setUp(self): def tearDown(self): EndpointRegistry.wipe_registry_for(self.env.cr.dbname) + self.env["ir.http"]._clear_routing_map() super().tearDown() def _make_new_route(self, register=True, **kw): From 2cb48f88d5c3e3f745b3da9b5867f02f1134d331 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 12 Jan 2022 07:24:27 +0000 Subject: [PATCH 06/91] endpoint_route_handler 14.0.1.0.2 --- endpoint_route_handler/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index d192fd0a..a3546df4 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -4,7 +4,7 @@ { "name": " Route route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "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 072b7e6c69cb41ee731da0b408f7e339ff0875dc Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 14 Jan 2022 07:36:41 +0100 Subject: [PATCH 07/91] Misc fix of authorship name --- endpoint_route_handler/__manifest__.py | 2 +- endpoint_route_handler/models/endpoint_route_handler.py | 2 +- endpoint_route_handler/models/ir_http.py | 2 +- endpoint_route_handler/registry.py | 2 +- endpoint_route_handler/tests/common.py | 2 +- endpoint_route_handler/tests/test_endpoint.py | 2 +- endpoint_route_handler/tests/test_endpoint_controller.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index a3546df4..bd435c9b 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__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_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index b38cfa0f..f195e7cd 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.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_route_handler/models/ir_http.py b/endpoint_route_handler/models/ir_http.py index ee3c41bf..ecd6a062 100644 --- a/endpoint_route_handler/models/ir_http.py +++ b/endpoint_route_handler/models/ir_http.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_route_handler/registry.py b/endpoint_route_handler/registry.py index 2c76599c..0c821243 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.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_route_handler/tests/common.py b/endpoint_route_handler/tests/common.py index dfad6614..5654acce 100644 --- a/endpoint_route_handler/tests/common.py +++ b/endpoint_route_handler/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_route_handler/tests/test_endpoint.py b/endpoint_route_handler/tests/test_endpoint.py index 4be6cb02..bad34676 100644 --- a/endpoint_route_handler/tests/test_endpoint.py +++ b/endpoint_route_handler/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_route_handler/tests/test_endpoint_controller.py b/endpoint_route_handler/tests/test_endpoint_controller.py index e4e6e5e8..f29f2245 100644 --- a/endpoint_route_handler/tests/test_endpoint_controller.py +++ b/endpoint_route_handler/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). From 0679960e8f6f506709b79d7d8d1730a6f09f8d05 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 14 Jan 2022 08:52:16 +0000 Subject: [PATCH 08/91] endpoint_route_handler 14.0.1.0.3 --- endpoint_route_handler/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index bd435c9b..4c083309 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -4,7 +4,7 @@ { "name": " Route route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "version": "14.0.1.0.2", + "version": "14.0.1.0.3", "license": "LGPL-3", "development_status": "Alpha", "author": "Camptocamp,Odoo Community Association (OCA)", From 257c98a7e8104d9c8215ed597ebf8120c910c572 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 19 Jan 2022 10:23:03 +0100 Subject: [PATCH 09/91] endpoint_route_handler: fix rules by group --- endpoint_route_handler/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 0c821243..486927e0 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -31,7 +31,7 @@ def get_rules(self): # TODO: add test def get_rules_by_group(self, group): for key, rule in self._mapping.items(): - if rule.endpoint_group == group: + if rule.route_group == group: yield (key, rule) def add_or_update_rule(self, rule, force=False, init=False): From 583310e9466bd08c6df3336ad5a6195f13c12193 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 19 Jan 2022 10:03:05 +0000 Subject: [PATCH 10/91] endpoint_route_handler 14.0.1.0.4 --- endpoint_route_handler/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index 4c083309..420dd2ae 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -4,7 +4,7 @@ { "name": " Route route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "version": "14.0.1.0.3", + "version": "14.0.1.0.4", "license": "LGPL-3", "development_status": "Alpha", "author": "Camptocamp,Odoo Community Association (OCA)", From 97792d65919282130673632c34befdc7afcf11cc Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 4 Apr 2022 11:13:02 +0200 Subject: [PATCH 11/91] endpoint_route_handler: dev status = Beta --- endpoint_route_handler/__manifest__.py | 2 +- endpoint_route_handler/readme/ROADMAP.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index 420dd2ae..623fd4e0 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -6,7 +6,7 @@ "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", "version": "14.0.1.0.4", "license": "LGPL-3", - "development_status": "Alpha", + "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", "maintainers": ["simahawk"], "website": "https://github.com/OCA/edi", diff --git a/endpoint_route_handler/readme/ROADMAP.rst b/endpoint_route_handler/readme/ROADMAP.rst index 18233e35..ff6c5a3d 100644 --- a/endpoint_route_handler/readme/ROADMAP.rst +++ b/endpoint_route_handler/readme/ROADMAP.rst @@ -1,2 +1,8 @@ +* /!\ IMPORTANT /!\ when working w/ multiple workers + you MUST restart the instance every time you add or modify a route from the UI + (eg: w/ the endpoint module) otherwise is not granted that the routing map + is going to be up to date on all workers. + @simahawk as already a POC to fix this. + * add api docs helpers * allow multiple HTTP methods on the same endpoint From a2e222813cb72019003f02baeb859921257f7ac6 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 4 Apr 2022 09:27:20 +0000 Subject: [PATCH 12/91] endpoint_route_handler 14.0.1.1.0 --- endpoint_route_handler/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index 623fd4e0..28ff6c7f 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -4,7 +4,7 @@ { "name": " Route route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "version": "14.0.1.0.4", + "version": "14.0.1.1.0", "license": "LGPL-3", "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", From 287f6b234217c5b1f8bc6de3ed90aa94c5fba484 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 15 Jun 2022 21:02:55 +0200 Subject: [PATCH 13/91] endpoint_route_handler: move to OCA/web-api --- endpoint_route_handler/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index 28ff6c7f..a2def7d7 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -9,7 +9,7 @@ "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", "maintainers": ["simahawk"], - "website": "https://github.com/OCA/edi", + "website": "https://github.com/OCA/web-api", "data": [ "security/ir.model.access.csv", ], From 3f9b060a70873734910e47ae8cbae8d62e62c87b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 15 Jul 2022 12:32:12 +0000 Subject: [PATCH 14/91] [UPD] README.rst --- endpoint_route_handler/README.rst | 136 +++++++++++++++++- .../static/description/index.html | 103 +++++++++---- 2 files changed, 213 insertions(+), 26 deletions(-) diff --git a/endpoint_route_handler/README.rst b/endpoint_route_handler/README.rst index 89bcd6c2..65fdc237 100644 --- a/endpoint_route_handler/README.rst +++ b/endpoint_route_handler/README.rst @@ -1 +1,135 @@ -wait for the bot ;) +==================== + Route route handler +==================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb--api-lightgray.png?logo=github + :target: https://github.com/OCA/web-api/tree/14.0/endpoint_route_handler + :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_route_handler + :alt: Translate me on Weblate + +|badge1| |badge2| |badge3| |badge4| + +Technical module that provides a base handler +for adding and removing controller routes on the fly. + +Can be used as a mixin or as a tool. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +As a mixin +~~~~~~~~~~ + +Use standard Odoo inheritance:: + + class MyModel(models.Model): + _name = "my.model" + _inherit = "endpoint.route.handler" + +Once you have this, each `my.model` record will generate a route. +You can have a look at the `endpoint` module to see a real life example. + + +As a tool +~~~~~~~~~ + +Initialize non stored route handlers and generate routes from them. +For instance:: + + route_handler = self.env["endpoint.route.handler"] + endpoint_handler = MyController()._my_handler + vals = { + "name": "My custom route", + "route": "/my/custom/route", + "request_method": "GET", + "auth_type": "public", + } + new_route = route_handler.new(vals) + new_route._refresh_endpoint_data() # required only for NewId records + new_route._register_controller(endpoint_handler=endpoint_handler, key="my-custom-route") + +Of course, what happens when the endpoint gets called +depends on the logic defined on the controller method. + +In both cases (mixin and tool) when a new route is generated or an existing one is updated, +the `ir.http.routing_map` (which holds all Odoo controllers) will be updated. + +You can see a real life example on `shopfloor.app` model. + +Known issues / Roadmap +====================== + +* /!\ IMPORTANT /!\ when working w/ multiple workers + you MUST restart the instance every time you add or modify a route from the UI + (eg: w/ the endpoint module) otherwise is not granted that the routing map + is going to be up to date on all workers. + @simahawk as already a POC to fix this. + +* add api docs helpers +* allow multiple HTTP methods on the same endpoint + +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_route_handler/static/description/index.html b/endpoint_route_handler/static/description/index.html index aa3410eb..cade7b27 100644 --- a/endpoint_route_handler/static/description/index.html +++ b/endpoint_route_handler/static/description/index.html @@ -4,7 +4,7 @@ -Endpoint +Route route handler -
-

Endpoint

+
+

Route route handler

-

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

-
+

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

+

Technical module that provides a base handler +for adding and removing controller routes on the fly.

+

Can be used as a mixin or as a tool.

Table of contents

+
+

Usage

+
+

As a mixin

+

Use standard Odoo inheritance:

+
+class MyModel(models.Model):
+    _name = "my.model"
+    _inherit = "endpoint.route.handler"
+
+

Once you have this, each my.model record will generate a route. +You can have a look at the endpoint module to see a real life example.

+
+
+

As a tool

+

Initialize non stored route handlers and generate routes from them. +For instance:

+
+route_handler = self.env["endpoint.route.handler"]
+endpoint_handler = MyController()._my_handler
+vals = {
+    "name": "My custom route",
+    "route": "/my/custom/route",
+    "request_method": "GET",
+    "auth_type": "public",
+}
+new_route = route_handler.new(vals)
+new_route._refresh_endpoint_data()  # required only for NewId records
+new_route._register_controller(endpoint_handler=endpoint_handler, key="my-custom-route")
+
+

Of course, what happens when the endpoint gets called +depends on the logic defined on the controller method.

+

In both cases (mixin and tool) when a new route is generated or an existing one is updated, +the ir.http.routing_map (which holds all Odoo controllers) will be updated.

+

You can see a real life example on shopfloor.app model.

+
+
+
+

Known issues / Roadmap

+
    +
  • /!IMPORTANT /!when working w/ multiple workers +you MUST restart the instance every time you add or modify a route from the UI +(eg: w/ the endpoint module) otherwise is not granted that the routing map +is going to be up to date on all workers. +@simahawk as already a POC to fix this.
  • +
  • add api docs helpers
  • +
  • allow multiple HTTP methods on the same endpoint
  • +
+
-

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 b3db4bf36acc50dd12c746a8bebb68ba0a112330 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 15 Jul 2022 12:33:48 +0000 Subject: [PATCH 15/91] [UPD] Update endpoint_route_handler.pot --- .../i18n/endpoint_route_handler.pot | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 endpoint_route_handler/i18n/endpoint_route_handler.pot diff --git a/endpoint_route_handler/i18n/endpoint_route_handler.pot b/endpoint_route_handler/i18n/endpoint_route_handler.pot new file mode 100644 index 00000000..e9583b15 --- /dev/null +++ b/endpoint_route_handler/i18n/endpoint_route_handler.pot @@ -0,0 +1,127 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * endpoint_route_handler +# +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_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__active +msgid "Active" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__auth_type +msgid "Auth Type" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__csrf +msgid "Csrf" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__display_name +#: model:ir.model.fields,field_description:endpoint_route_handler.field_ir_http__display_name +msgid "Display Name" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__endpoint_hash +msgid "Endpoint Hash" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model,name:endpoint_route_handler.model_endpoint_route_handler +msgid "Endpoint Route handler" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model,name:endpoint_route_handler.model_ir_http +msgid "HTTP Routing" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__id +#: model:ir.model.fields,field_description:endpoint_route_handler.field_ir_http__id +msgid "ID" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler__endpoint_hash +msgid "Identify the route with its main params" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler____last_update +#: model:ir.model.fields,field_description:endpoint_route_handler.field_ir_http____last_update +msgid "Last Modified on" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__name +msgid "Name" +msgstr "" + +#. module: endpoint_route_handler +#: code:addons/endpoint_route_handler/models/endpoint_route_handler.py:0 +#, python-format +msgid "" +"Non unique route(s): %(routes)s.\n" +"Found in model(s): %(models)s.\n" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__request_content_type +msgid "Request Content Type" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__request_method +msgid "Request Method" +msgstr "" + +#. module: endpoint_route_handler +#: code:addons/endpoint_route_handler/models/endpoint_route_handler.py:0 +#, python-format +msgid "Request method is required for POST and PUT." +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__route +msgid "Route" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__route_group +msgid "Route Group" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__route_type +msgid "Route Type" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler__route_group +msgid "Use this to classify routes together" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_route_handler_endpoint_route_unique +msgid "You can register an endpoint route only once." +msgstr "" + +#. module: endpoint_route_handler +#: code:addons/endpoint_route_handler/models/endpoint_route_handler.py:0 +#, python-format +msgid "`%s` uses a blacklisted routed = `%s`" +msgstr "" From b0d59228478985d2594a0d856dbedb1e7f11e7cd Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 15 Jul 2022 12:38:24 +0000 Subject: [PATCH 16/91] [UPD] Update endpoint_route_handler.pot --- endpoint_route_handler/i18n/endpoint_route_handler.pot | 2 ++ 1 file changed, 2 insertions(+) diff --git a/endpoint_route_handler/i18n/endpoint_route_handler.pot b/endpoint_route_handler/i18n/endpoint_route_handler.pot index e9583b15..ef162fd0 100644 --- a/endpoint_route_handler/i18n/endpoint_route_handler.pot +++ b/endpoint_route_handler/i18n/endpoint_route_handler.pot @@ -116,6 +116,8 @@ msgid "Use this to classify routes together" msgstr "" #. module: endpoint_route_handler +#: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_endpoint_endpoint_route_unique +#: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_mixin_endpoint_route_unique #: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_route_handler_endpoint_route_unique msgid "You can register an endpoint route only once." msgstr "" From 73c516a251d5de739432fb65f9274a1e909dc30f Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 15 Jun 2022 21:39:58 +0200 Subject: [PATCH 17/91] endpoint_route_handler: fix cross worker lookup --- endpoint_route_handler/__init__.py | 1 + endpoint_route_handler/__manifest__.py | 1 + .../controllers/__init__.py | 1 + endpoint_route_handler/controllers/main.py | 18 + endpoint_route_handler/exceptions.py | 7 + .../models/endpoint_route_handler.py | 104 +++--- endpoint_route_handler/models/ir_http.py | 41 +-- endpoint_route_handler/post_init_hook.py | 14 + endpoint_route_handler/registry.py | 318 ++++++++++++++---- endpoint_route_handler/tests/__init__.py | 1 + endpoint_route_handler/tests/common.py | 4 +- .../tests/fake_controllers.py | 29 ++ endpoint_route_handler/tests/test_endpoint.py | 124 +++++-- .../tests/test_endpoint_controller.py | 42 ++- endpoint_route_handler/tests/test_registry.py | 169 ++++++++++ 15 files changed, 676 insertions(+), 198 deletions(-) create mode 100644 endpoint_route_handler/controllers/__init__.py create mode 100644 endpoint_route_handler/controllers/main.py create mode 100644 endpoint_route_handler/exceptions.py create mode 100644 endpoint_route_handler/post_init_hook.py create mode 100644 endpoint_route_handler/tests/fake_controllers.py create mode 100644 endpoint_route_handler/tests/test_registry.py diff --git a/endpoint_route_handler/__init__.py b/endpoint_route_handler/__init__.py index 0650744f..a0cb2972 100644 --- a/endpoint_route_handler/__init__.py +++ b/endpoint_route_handler/__init__.py @@ -1 +1,2 @@ from . import models +from .post_init_hook import post_init_hook diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index a2def7d7..cb7ad67d 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -13,4 +13,5 @@ "data": [ "security/ir.model.access.csv", ], + "post_init_hook": "post_init_hook", } diff --git a/endpoint_route_handler/controllers/__init__.py b/endpoint_route_handler/controllers/__init__.py new file mode 100644 index 00000000..12a7e529 --- /dev/null +++ b/endpoint_route_handler/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/endpoint_route_handler/controllers/main.py b/endpoint_route_handler/controllers/main.py new file mode 100644 index 00000000..0bdd9f21 --- /dev/null +++ b/endpoint_route_handler/controllers/main.py @@ -0,0 +1,18 @@ +# Copyright 2021 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +import logging + +from werkzeug.exceptions import NotFound + +from odoo import http + +_logger = logging.getLogger(__file__) + + +class EndpointNotFoundController(http.Controller): + def auto_not_found(self, endpoint_route, **params): + _logger.error("Non registered endpoint for %s", endpoint_route) + raise NotFound() diff --git a/endpoint_route_handler/exceptions.py b/endpoint_route_handler/exceptions.py new file mode 100644 index 00000000..4420a7b0 --- /dev/null +++ b/endpoint_route_handler/exceptions.py @@ -0,0 +1,7 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +class EndpointHandlerNotFound(Exception): + """Raise when an endpoint handler is not found.""" diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index f195e7cd..16294f3b 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -4,9 +4,8 @@ import logging -from odoo import _, api, exceptions, fields, http, models +from odoo import _, api, exceptions, fields, models -# from odoo.addons.base_sparse_field.models.fields import Serialized from ..registry import EndpointRegistry ENDPOINT_ROUTE_CONSUMER_MODELS = { @@ -47,8 +46,9 @@ class EndpointRouteHandler(models.AbstractModel): endpoint_hash = fields.Char( compute="_compute_endpoint_hash", help="Identify the route with its main params" ) - csrf = fields.Boolean(default=False) + # TODO: add flag to prevent route updates on save -> + # should be handled by specific actions + filter in a tree view + btn on form _sql_constraints = [ ( @@ -209,6 +209,7 @@ def _handle_route_updates(self, vals): self._unregister_controllers() return True if any([x in vals for x in self._controller_fields()]): + self._logger.info("Route modified for %s", self.ids) self._register_controllers() return True return False @@ -218,69 +219,99 @@ def unlink(self): self._unregister_controllers() return super().unlink() + def _refresh_endpoint_data(self): + """Enforce refresh of route computed fields. + + Required for NewId records when using this model as a tool. + """ + self._compute_endpoint_hash() + self._compute_route() + + @property + def _endpoint_registry(self): + return EndpointRegistry.registry_for(self.env.cr) + def _register_hook(self): super()._register_hook() if not self._abstract: + self._logger.info("Register controllers") # Look explicitly for active records. # Pass `init` to not set the registry as updated # since this piece of code runs only when the model is loaded. self.search([("active", "=", True)])._register_controllers(init=True) - def _register_controllers(self, init=False): + def _register_controllers(self, init=False, options=None): if self._abstract: self._refresh_endpoint_data() - for rec in self: - rec._register_controller(init=init) + + rules = [rec._make_controller_rule(options=options) for rec in self] + self._endpoint_registry.update_rules(rules, init=init) + if not init: + # When envs are already loaded we must signal changes + self._force_routing_map_refresh() + self._logger.debug( + "Registered controllers: %s", ", ".join(self.mapped("route")) + ) + + def _force_routing_map_refresh(self): + """Signal changes to make all routing maps refresh.""" + self.env["ir.http"]._clear_routing_map() + self.env.registry.registry_invalidated = True + self.env.registry.signal_changes() def _unregister_controllers(self): if self._abstract: self._refresh_endpoint_data() - for rec in self: - rec._unregister_controller() + keys = tuple([rec._endpoint_registry_unique_key() for rec in self]) + self._endpoint_registry.drop_rules(keys) - def _refresh_endpoint_data(self): - """Enforce refresh of route computed fields. - - Required for NewId records when using this model as a tool. - """ - self._compute_endpoint_hash() - self._compute_route() + def _endpoint_registry_unique_key(self): + return "{0._name}:{0.id}".format(self) - @property - def _endpoint_registry(self): - return EndpointRegistry.registry_for(self.env.cr.dbname) + # TODO: consider if useful or not for single records + def _register_single_controller(self, options=None, key=None, init=False): + """Shortcut to register one single controller. - def _register_controller(self, endpoint_handler=None, key=None, init=False): - rule = self._make_controller_rule(endpoint_handler=endpoint_handler, key=key) - self._endpoint_registry.add_or_update_rule(rule, init=init) + WARNING: as this triggers envs invalidation via `_force_routing_map_refresh` + do not abuse of this method to register more than one route. + """ + rule = self._make_controller_rule(options=options, key=key) + self._endpoint_registry.update_rules([rule], init=init) + if not init: + self._force_routing_map_refresh() self._logger.debug( "Registered controller %s (auth: %s)", self.route, self.auth_type ) - def _make_controller_rule(self, endpoint_handler=None, key=None): + def _make_controller_rule(self, options=None, key=None): key = key or self._endpoint_registry_unique_key() route, routing, endpoint_hash = self._get_routing_info() - endpoint_handler = endpoint_handler or self._default_endpoint_handler() - assert callable(endpoint_handler) - endpoint = http.EndPoint(endpoint_handler, routing) - rule = self._endpoint_registry.make_rule( + options = options or self._default_endpoint_options() + return self._endpoint_registry.make_rule( # fmt: off key, route, - endpoint, + options, routing, endpoint_hash, route_group=self.route_group # fmt: on ) - return rule - def _default_endpoint_handler(self): - """Provide default endpoint handler. + def _default_endpoint_options(self): + options = {"handler": self._default_endpoint_options_handler()} + return options - :return: bound method of a controller (eg: MyController()._my_handler) - """ - raise NotImplementedError("No default endpoint handler defined.") + def _default_endpoint_options_handler(self): + self._logger.warning( + "No specific endpoint handler options defined for: %s, falling back to default", + self._name, + ) + base_path = "odoo.addons.endpoint_route_handler.controllers.main" + return { + "klass_dotted_path": f"{base_path}.EndpointNotFoundController", + "method_name": "auto_not_found", + } def _get_routing_info(self): route = self.route @@ -292,10 +323,3 @@ def _get_routing_info(self): csrf=self.csrf, ) return route, routing, self.endpoint_hash - - def _endpoint_registry_unique_key(self): - return "{0._name}:{0.id}".format(self) - - def _unregister_controller(self, key=None): - key = key or self._endpoint_registry_unique_key() - self._endpoint_registry.drop_rule(key) diff --git a/endpoint_route_handler/models/ir_http.py b/endpoint_route_handler/models/ir_http.py index ecd6a062..52d719ea 100644 --- a/endpoint_route_handler/models/ir_http.py +++ b/endpoint_route_handler/models/ir_http.py @@ -29,52 +29,13 @@ def _generate_routing_rules(cls, modules, converters): def _endpoint_routing_rules(cls): """Yield custom endpoint rules""" cr = http.request.env.cr - e_registry = EndpointRegistry.registry_for(cr.dbname) + e_registry = EndpointRegistry.registry_for(cr) for endpoint_rule in e_registry.get_rules(): _logger.debug("LOADING %s", endpoint_rule) endpoint = endpoint_rule.endpoint for url in endpoint_rule.routing["routes"]: yield (url, endpoint, endpoint_rule.routing) - @classmethod - def routing_map(cls, key=None): - cr = http.request.env.cr - e_registry = EndpointRegistry.registry_for(cr.dbname) - - # Each `env` will have its own `ir.http` "class instance" - # thus, each instance will have its own routing map. - # Hence, we must keep track of which instances have been updated - # to make sure routing rules are always up to date across envs. - # - # In the original `routing_map` method it's reported in a comment - # that the routing map should be unique instead of being duplicated - # across envs... well, this is how it works today so we have to deal w/ it. - http_id = cls._endpoint_make_http_id() - - is_routing_map_new = not hasattr(cls, "_routing_map") - if is_routing_map_new or not e_registry.ir_http_seen(http_id): - # When the routing map is not ready yet, simply track current instance - e_registry.ir_http_track(http_id) - _logger.debug("ir_http instance `%s` tracked", http_id) - elif e_registry.ir_http_seen(http_id) and e_registry.routing_update_required( - http_id - ): - # This instance was already tracked - # and meanwhile the registry got updated: - # ensure all routes are re-loaded. - _logger.info( - "Endpoint registry updated, reset routing ma for `%s`", http_id - ) - cls._routing_map = {} - cls._rewrite_len = {} - e_registry.reset_update_required(http_id) - return super().routing_map(key=key) - - @classmethod - def _endpoint_make_http_id(cls): - """Generate current ir.http class ID.""" - return id(cls) - @classmethod def _auth_method_user_endpoint(cls): """Special method for user auth which raises Unauthorized when needed. diff --git a/endpoint_route_handler/post_init_hook.py b/endpoint_route_handler/post_init_hook.py new file mode 100644 index 00000000..2f64fffd --- /dev/null +++ b/endpoint_route_handler/post_init_hook.py @@ -0,0 +1,14 @@ +# Copyright 2022 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +from .registry import EndpointRegistry + +_logger = logging.getLogger(__name__) + + +def post_init_hook(cr, registry): + # this is the trigger that sends notifications when jobs change + _logger.info("Create table") + EndpointRegistry._setup_table(cr) diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 486927e0..14f508bd 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -2,7 +2,58 @@ # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -_REGISTRY_BY_DB = {} +import importlib +import json +from functools import partial + +from psycopg2 import sql +from psycopg2.extras import execute_values + +from odoo import http, tools +from odoo.tools import DotDict + +from odoo.addons.base.models.ir_model import query_insert + +from .exceptions import EndpointHandlerNotFound + + +def query_multi_update(cr, table_name, rows, cols): + """Update multiple rows at once. + + :param `cr`: active db cursor + :param `table_name`: sql table to update + :param `rows`: list of dictionaries with write-ready values + :param `cols`: list of keys representing columns' names + """ + # eg: key=c.key, route=c.route + keys = sql.SQL(",").join([sql.SQL("{0}=c.{0}".format(col)) for col in cols]) + col_names = sql.SQL(",").join([sql.Identifier(col) for col in cols]) + template = ( + sql.SQL("(") + + sql.SQL(",").join([sql.SQL("%({})s".format(col)) for col in cols]) + + sql.SQL(")") + ) + query = sql.SQL( + """ + UPDATE {table} AS t SET + {keys} + FROM (VALUES {values}) + AS c({col_names}) + WHERE c.key = t.key + RETURNING t.key + """ + ).format( + table=sql.Identifier(table_name), + keys=keys, + col_names=col_names, + values=sql.Placeholder(), + ) + execute_values( + cr, + query.as_string(cr._cnx), + rows, + template=template.as_string(cr._cnx), + ) class EndpointRegistry: @@ -11,103 +62,232 @@ class EndpointRegistry: Used to: * track registered endpoints - * track routes to be updated for specific ir.http instances * retrieve routing rules to load in ir.http routing map """ - __slots__ = ("_mapping", "_http_ids", "_http_ids_to_update") + __slots__ = "cr" + _table = "endpoint_route" + _columns = ( + # name, type, comment + ("key", "VARCHAR", ""), # TODO: create index + ("route", "VARCHAR", ""), + ("opts", "text", ""), + ("routing", "text", ""), + ("endpoint_hash", "VARCHAR(32)", ""), # TODO: add uniq constraint + ("route_group", "VARCHAR(32)", ""), + ) + + def __init__(self, cr): + self.cr = cr - def __init__(self): - # collect EndpointRule objects - self._mapping = {} - # collect ids of ir.http instances - self._http_ids = set() - # collect ids of ir.http instances that need update - self._http_ids_to_update = set() + def get_rules(self, keys=None, where=None): + for row in self._get_rules(keys=keys, where=where): + yield EndpointRule.from_row(self.cr.dbname, row) - def get_rules(self): - return self._mapping.values() + def _get_rules(self, keys=None, where=None): + query = "SELECT * FROM endpoint_route" + if keys and not where: + where = "key in (%s)" + self.cr.execute(query, keys) + return self.cr.fetchall() + elif where: + query += " " + where + self.cr.execute(query) + return self.cr.fetchall() + + def _get_rule(self, key): + query = "SELECT * FROM endpoint_route WHERE key = %s" + self.cr.execute(query, (key,)) + row = self.cr.fetchone() + if row: + return EndpointRule.from_row(self.cr.dbname, row) + + def _lock_rows(self, keys): + sql = "SELECT id FROM endpoint_route WHERE key IN %s FOR UPDATE" + self.cr.execute(sql, (tuple(keys),), log_exceptions=False) + + def _update(self, rows_mapping): + self._lock_rows(tuple(rows_mapping.keys())) + return query_multi_update( + self.cr, + self._table, + tuple(rows_mapping.values()), + EndpointRule._ordered_columns(), + ) + + def _create(self, rows_mapping): + return query_insert(self.cr, self._table, list(rows_mapping.values())) - # TODO: add test def get_rules_by_group(self, group): - for key, rule in self._mapping.items(): - if rule.route_group == group: - yield (key, rule) + rules = self.get_rules(where=f"WHERE route_group='{group}'") + return rules - def add_or_update_rule(self, rule, force=False, init=False): - """Add or update an existing rule. + def update_rules(self, rules, init=False): + """Add or update rules. - :param rule: instance of EndpointRule - :param force: replace a rule forcedly + :param rule: list of instances of EndpointRule + :param force: replace rules forcedly :param init: given when adding rules for the first time """ - key = rule.key - existing = self._mapping.get(key) - if not existing or force: - self._mapping[key] = rule - if not init: - self._refresh_update_required() - return True - if existing.endpoint_hash != rule.endpoint_hash: - # Override and set as to be updated - self._mapping[key] = rule - if not init: - self._refresh_update_required() - return True - - def drop_rule(self, key): - existing = self._mapping.pop(key, None) - if not existing: - return False - self._refresh_update_required() - return True - - def routing_update_required(self, http_id): - return http_id in self._http_ids_to_update - - def _refresh_update_required(self): - for http_id in self._http_ids: - self._http_ids_to_update.add(http_id) + keys = [x.key for x in rules] + existing = {x.key: x for x in self.get_rules(keys=keys)} + to_create = {} + to_update = {} + for rule in rules: + if rule.key in existing: + to_update[rule.key] = rule.to_row() + else: + to_create[rule.key] = rule.to_row() + res = False + if to_create: + self._create(to_create) + res = True + if to_update: + self._update(to_update) + res = True + return res - def reset_update_required(self, http_id): - self._http_ids_to_update.discard(http_id) + def drop_rules(self, keys): + self.cr.execute("DELETE FROM endpoint_route WHERE key IN %s", (tuple(keys),)) + return True @classmethod - def registry_for(cls, dbname): - if dbname not in _REGISTRY_BY_DB: - _REGISTRY_BY_DB[dbname] = cls() - return _REGISTRY_BY_DB[dbname] + def registry_for(cls, cr): + return cls(cr) @classmethod - def wipe_registry_for(cls, dbname): - if dbname in _REGISTRY_BY_DB: - del _REGISTRY_BY_DB[dbname] - - def ir_http_track(self, _id): - self._http_ids.add(_id) + def wipe_registry_for(cls, cr): + cr.execute("TRUNCATE endpoint_route") - def ir_http_seen(self, _id): - return _id in self._http_ids + def make_rule(self, *a, **kw): + return EndpointRule(self.cr.dbname, *a, **kw) - @staticmethod - def make_rule(*a, **kw): - return EndpointRule(*a, **kw) + @classmethod + def _setup_table(cls, cr): + if not tools.sql.table_exists(cr, cls._table): + tools.sql.create_model_table(cr, cls._table, columns=cls._columns) class EndpointRule: """Hold information for a custom endpoint rule.""" - __slots__ = ("key", "route", "endpoint", "routing", "endpoint_hash", "route_group") + __slots__ = ( + "_dbname", + "key", + "route", + "opts", + "endpoint_hash", + "routing", + "route_group", + ) - def __init__(self, key, route, endpoint, routing, endpoint_hash, route_group=None): + def __init__( + self, dbname, key, route, options, routing, endpoint_hash, route_group=None + ): + self._dbname = dbname self.key = key self.route = route - self.endpoint = endpoint + self.options = options self.routing = routing self.endpoint_hash = endpoint_hash self.route_group = route_group def __repr__(self): - return f"{self.key}: {self.route}" + ( - f"[{self.route_group}]" if self.route_group else "" + # FIXME: use class name, remove key + return ( + f"<{self.__class__.__name__}: {self.key}" + + (f" #{self.route_group}" if self.route_group else "nogroup") + + ">" ) + + @classmethod + def _ordered_columns(cls): + return [k for k in cls.__slots__ if not k.startswith("_")] + + @property + def options(self): + return DotDict(self.opts) + + @options.setter + def options(self, value): + """Validate options. + + See `_get_handler` for more info. + """ + assert "klass_dotted_path" in value["handler"] + assert "method_name" in value["handler"] + self.opts = value + + @classmethod + def from_row(cls, dbname, row): + key, route, options, routing, endpoint_hash, route_group = row[1:] + # TODO: #jsonb-ref + options = json.loads(options) + routing = json.loads(routing) + init_args = ( + dbname, + key, + route, + options, + routing, + endpoint_hash, + route_group, + ) + return cls(*init_args) + + def to_dict(self): + return {k: getattr(self, k) for k in self._ordered_columns()} + + def to_row(self): + row = self.to_dict() + for k, v in row.items(): + if isinstance(v, (dict, list)): + row[k] = json.dumps(v) + return row + + @property + def endpoint(self): + """Lookup http.Endpoint to be used for the routing map.""" + handler = self._get_handler() + pargs = self.handler_options.get("default_pargs", ()) + kwargs = self.handler_options.get("default_kwargs", {}) + method = partial(handler, *pargs, **kwargs) + return http.EndPoint(method, self.routing) + + @property + def handler_options(self): + return self.options.handler + + def _get_handler(self): + """Resolve endpoint handler lookup. + + `options` must contain `handler` key to provide: + + * the controller's klass via `klass_dotted_path` + * the controller's method to use via `method_name` + + Lookup happens by: + + 1. importing the controller klass module + 2. loading the klass + 3. accessing the method via its name + + If any of them is not found, a specific exception is raised. + """ + mod_path, klass_name = self.handler_options.klass_dotted_path.rsplit(".", 1) + try: + mod = importlib.import_module(mod_path) + except ImportError as exc: + raise EndpointHandlerNotFound(f"Module `{mod_path}` not found") from exc + try: + klass = getattr(mod, klass_name) + except AttributeError as exc: + raise EndpointHandlerNotFound(f"Class `{klass_name}` not found") from exc + method_name = self.handler_options.method_name + try: + method = getattr(klass(), method_name) + except AttributeError as exc: + raise EndpointHandlerNotFound( + f"Method name `{method_name}` not found" + ) from exc + return method diff --git a/endpoint_route_handler/tests/__init__.py b/endpoint_route_handler/tests/__init__.py index 6885a0f9..e1ad88a9 100644 --- a/endpoint_route_handler/tests/__init__.py +++ b/endpoint_route_handler/tests/__init__.py @@ -1,2 +1,3 @@ +from . import test_registry from . import test_endpoint from . import test_endpoint_controller diff --git a/endpoint_route_handler/tests/common.py b/endpoint_route_handler/tests/common.py index 5654acce..b7f3d4f1 100644 --- a/endpoint_route_handler/tests/common.py +++ b/endpoint_route_handler/tests/common.py @@ -36,9 +36,9 @@ def _setup_records(cls): @contextlib.contextmanager def _get_mocked_request( - self, httprequest=None, extra_headers=None, request_attrs=None + self, env=None, httprequest=None, extra_headers=None, request_attrs=None ): - with MockRequest(self.env) as mocked_request: + with MockRequest(env or self.env) as mocked_request: mocked_request.httprequest = ( DotDict(httprequest) if httprequest else mocked_request.httprequest ) diff --git a/endpoint_route_handler/tests/fake_controllers.py b/endpoint_route_handler/tests/fake_controllers.py new file mode 100644 index 00000000..bd493973 --- /dev/null +++ b/endpoint_route_handler/tests/fake_controllers.py @@ -0,0 +1,29 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import http + + +class CTRLFake(http.Controller): + # Shortcut for dotted path + _path = "odoo.addons.endpoint_route_handler.tests.fake_controllers.CTRLFake" + + def handler1(self, arg1, arg2=2): + return arg1, arg2 + + def handler2(self, arg1, arg2=2): + return arg1, arg2 + + def custom_handler(self, custom=None): + return f"Got: {custom}" + + +class TestController(http.Controller): + _path = "odoo.addons.endpoint_route_handler.tests.fake_controllers.TestController" + + def _do_something1(self, foo=None): + return f"Got: {foo}" + + def _do_something2(self, default_arg, foo=None): + return f"{default_arg} -> got: {foo}" diff --git a/endpoint_route_handler/tests/test_endpoint.py b/endpoint_route_handler/tests/test_endpoint.py index bad34676..a8d06f80 100644 --- a/endpoint_route_handler/tests/test_endpoint.py +++ b/endpoint_route_handler/tests/test_endpoint.py @@ -2,19 +2,33 @@ # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from contextlib import contextmanager -from functools import partial - -from odoo.http import Controller +import odoo +from odoo.tools import mute_logger from ..registry import EndpointRegistry from .common import CommonEndpoint +from .fake_controllers import CTRLFake + + +@contextmanager +def new_rollbacked_env(): + # Borrowed from `component` + registry = odoo.registry(odoo.tests.common.get_db_name()) + uid = odoo.SUPERUSER_ID + cr = registry.cursor() + try: + yield odoo.api.Environment(cr, uid, {}) + finally: + cr.rollback() # we shouldn't have to commit anything + cr.close() class TestEndpoint(CommonEndpoint): def tearDown(self): self.env["ir.http"]._clear_routing_map() - EndpointRegistry.wipe_registry_for(self.env.cr.dbname) + EndpointRegistry.wipe_registry_for(self.env.cr) super().tearDown() def _make_new_route(self, **kw): @@ -37,49 +51,111 @@ def test_as_tool_base_data(self): new_route._refresh_endpoint_data() self.assertNotEqual(new_route.endpoint_hash, first_hash) - def test_as_tool_register_controller_no_default(self): + @mute_logger("odoo.addons.base.models.ir_http") + def test_as_tool_register_single_controller(self): new_route = self._make_new_route() - # No specific controller - with self.assertRaisesRegex( - NotImplementedError, "No default endpoint handler defined." - ): - new_route._register_controller() + options = { + "handler": { + "klass_dotted_path": CTRLFake._path, + "method_name": "custom_handler", + } + } - def test_as_tool_register_controller(self): - new_route = self._make_new_route() + with self._get_mocked_request(): + new_route._register_single_controller(options=options, init=True) + # Ensure the routing rule is registered + rmap = self.env["ir.http"].routing_map() + self.assertIn("/my/test/route", [x.rule for x in rmap._rules]) + + # Ensure is updated when needed + new_route.route += "/new" + new_route._refresh_endpoint_data() + with self._get_mocked_request(): + new_route._register_single_controller(options=options, init=True) + rmap = self.env["ir.http"]._clear_routing_map() + rmap = self.env["ir.http"].routing_map() + self.assertNotIn("/my/test/route", [x.rule for x in rmap._rules]) + self.assertIn("/my/test/route/new", [x.rule for x in rmap._rules]) - class TestController(Controller): - def _do_something(self, route): - return "ok" + @mute_logger("odoo.addons.base.models.ir_http") + def test_as_tool_register_controllers(self): + new_route = self._make_new_route() + options = { + "handler": { + "klass_dotted_path": CTRLFake._path, + "method_name": "custom_handler", + } + } - endpoint_handler = partial(TestController()._do_something, new_route.route) with self._get_mocked_request(): - new_route._register_controller(endpoint_handler=endpoint_handler) + new_route._register_controllers(options=options, init=True) # Ensure the routing rule is registered rmap = self.env["ir.http"].routing_map() self.assertIn("/my/test/route", [x.rule for x in rmap._rules]) + # Ensure is updated when needed new_route.route += "/new" new_route._refresh_endpoint_data() with self._get_mocked_request(): - new_route._register_controller(endpoint_handler=endpoint_handler) + new_route._register_controllers(options=options, init=True) + rmap = self.env["ir.http"]._clear_routing_map() rmap = self.env["ir.http"].routing_map() self.assertNotIn("/my/test/route", [x.rule for x in rmap._rules]) self.assertIn("/my/test/route/new", [x.rule for x in rmap._rules]) - def test_as_tool_register_controller_dynamic_route(self): + @mute_logger("odoo.addons.base.models.ir_http") + def test_as_tool_register_controllers_dynamic_route(self): route = "/my/app/" new_route = self._make_new_route(route=route) - class TestController(Controller): - def _do_something(self, foo=None): - return "ok" + options = { + "handler": { + "klass_dotted_path": CTRLFake._path, + "method_name": "custom_handler", + } + } - endpoint_handler = TestController()._do_something with self._get_mocked_request(): - new_route._register_controller(endpoint_handler=endpoint_handler) + new_route._register_controllers(options=options, init=True) # Ensure the routing rule is registered rmap = self.env["ir.http"].routing_map() self.assertIn(route, [x.rule for x in rmap._rules]) + @mute_logger("odoo.addons.base.models.ir_http", "odoo.modules.registry") + def test_cross_env_consistency(self): + """Ensure route updates are propagated to all envs.""" + route = "/my/app/" + new_route = self._make_new_route(route=route) + + options = { + "handler": { + "klass_dotted_path": CTRLFake._path, + "method_name": "custom_handler", + } + } + + env1 = self.env + with self._get_mocked_request(): + with new_rollbacked_env() as env2: + # Load maps + env1["ir.http"].routing_map() + env2["ir.http"].routing_map() + # Register route in current env. + # By using `init=True` we don't trigger env signals + # (simulating when the registry is loaded for the 1st time + # by `_register_hook`). + # In this case we expect the test to fail + # as there's no propagation to the other env. + new_route._register_controllers(options=options, init=True) + rmap = self.env["ir.http"].routing_map() + self.assertNotIn(route, [x.rule for x in rmap._rules]) + rmap = env2["ir.http"].routing_map() + self.assertNotIn(route, [x.rule for x in rmap._rules]) + # Now w/out init -> works + new_route._register_controllers(options=options) + rmap = self.env["ir.http"].routing_map() + self.assertIn(route, [x.rule for x in rmap._rules]) + rmap = env2["ir.http"].routing_map() + self.assertIn(route, [x.rule for x in rmap._rules]) + # TODO: test unregister diff --git a/endpoint_route_handler/tests/test_endpoint_controller.py b/endpoint_route_handler/tests/test_endpoint_controller.py index f29f2245..4b98feea 100644 --- a/endpoint_route_handler/tests/test_endpoint_controller.py +++ b/endpoint_route_handler/tests/test_endpoint_controller.py @@ -4,20 +4,11 @@ import os import unittest -from functools import partial -from odoo.http import Controller from odoo.tests.common import HttpCase from ..registry import EndpointRegistry - - -class TestController(Controller): - def _do_something1(self, foo=None): - return f"Got: {foo}" - - def _do_something2(self, default_arg, foo=None): - return f"{default_arg} -> got: {foo}" +from .fake_controllers import TestController @unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "EndpointHttpCase skipped") @@ -27,11 +18,11 @@ def setUp(self): self.route_handler = self.env["endpoint.route.handler"] def tearDown(self): - EndpointRegistry.wipe_registry_for(self.env.cr.dbname) + EndpointRegistry.wipe_registry_for(self.env.cr) self.env["ir.http"]._clear_routing_map() super().tearDown() - def _make_new_route(self, register=True, **kw): + def _make_new_route(self, options=None, **kw): vals = { "name": "Test custom route", "request_method": "GET", @@ -39,16 +30,17 @@ def _make_new_route(self, register=True, **kw): vals.update(kw) new_route = self.route_handler.new(vals) new_route._refresh_endpoint_data() + new_route._register_controllers(options=options) return new_route - def _register_controller(self, route_obj, endpoint_handler=None): - endpoint_handler = endpoint_handler or TestController()._do_something1 - route_obj._register_controller(endpoint_handler=endpoint_handler) - def test_call(self): - new_route = self._make_new_route(route="/my/test/") - self._register_controller(new_route) - + options = { + "handler": { + "klass_dotted_path": TestController._path, + "method_name": "_do_something1", + } + } + self._make_new_route(route="/my/test/", options=options) route = "/my/test/working" response = self.url_open(route) self.assertEqual(response.status_code, 401) @@ -59,10 +51,14 @@ def test_call(self): self.assertEqual(response.content, b"Got: working") def test_call_advanced_endpoint_handler(self): - new_route = self._make_new_route(route="/my/advanced/test/") - endpoint_handler = partial(TestController()._do_something2, "DEFAULT") - self._register_controller(new_route, endpoint_handler=endpoint_handler) - + options = { + "handler": { + "klass_dotted_path": TestController._path, + "method_name": "_do_something2", + "default_pargs": ("DEFAULT",), + } + } + self._make_new_route(route="/my/advanced/test/", options=options) route = "/my/advanced/test/working" response = self.url_open(route) self.assertEqual(response.status_code, 401) diff --git a/endpoint_route_handler/tests/test_registry.py b/endpoint_route_handler/tests/test_registry.py new file mode 100644 index 00000000..a1b54ed1 --- /dev/null +++ b/endpoint_route_handler/tests/test_registry.py @@ -0,0 +1,169 @@ +# Copyright 2021 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import http +from odoo.tests.common import SavepointCase, tagged + +from odoo.addons.endpoint_route_handler.exceptions import EndpointHandlerNotFound +from odoo.addons.endpoint_route_handler.registry import EndpointRegistry + +from .fake_controllers import CTRLFake + + +@tagged("-at_install", "post_install") +class TestRegistry(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + EndpointRegistry.wipe_registry_for(cls.env.cr) + cls.reg = EndpointRegistry.registry_for(cls.env.cr) + + def tearDown(self): + EndpointRegistry.wipe_registry_for(self.env.cr) + super().tearDown() + + def _count_rules(self, groups=("test_route_handler",)): + # NOTE: use alwways groups to filter in your tests + # because some other module might add rules for testing. + self.env.cr.execute( + "SELECT COUNT(id) FROM endpoint_route WHERE route_group IN %s", (groups,) + ) + return self.env.cr.fetchone()[0] + + def test_registry_empty(self): + self.assertEqual(list(self.reg.get_rules()), []) + self.assertEqual(self._count_rules(), 0) + + def _make_rules(self, stop=5, start=1, **kw): + res = [] + for i in range(start, stop): + key = f"route{i}" + route = f"/test/{i}" + options = { + "handler": { + "klass_dotted_path": CTRLFake._path, + "method_name": "handler1", + } + } + routing = {"routes": []} + endpoint_hash = i + route_group = "test_route_handler" + rule = self.reg.make_rule( + key, + route, + options, + routing, + endpoint_hash, + route_group=route_group, + ) + for k, v in kw.items(): + setattr(rule, k, v) + res.append(rule) + self.reg.update_rules(res) + return res + + def test_add_rule(self): + self._make_rules(stop=5) + self.assertEqual(self._count_rules(), 4) + self.assertEqual(self.reg._get_rule("route1").endpoint_hash, "1") + self.assertEqual(self.reg._get_rule("route2").endpoint_hash, "2") + self.assertEqual(self.reg._get_rule("route3").endpoint_hash, "3") + self.assertEqual(self.reg._get_rule("route4").endpoint_hash, "4") + + def test_get_rules(self): + self._make_rules(stop=4) + self.assertEqual(self._count_rules(), 3) + self.reg.get_rules() + self.assertEqual( + [x.key for x in self.reg.get_rules()], ["route1", "route2", "route3"] + ) + self._make_rules(start=10, stop=14) + self.assertEqual(self._count_rules(), 7) + self.reg.get_rules() + self.assertEqual( + sorted([x.key for x in self.reg.get_rules()]), + sorted( + [ + "route1", + "route2", + "route3", + "route10", + "route11", + "route12", + "route13", + ] + ), + ) + + def test_update_rule(self): + rule1, rule2 = self._make_rules(stop=3) + self.assertEqual( + self.reg._get_rule("route1").handler_options.method_name, "handler1" + ) + self.assertEqual( + self.reg._get_rule("route2").handler_options.method_name, "handler1" + ) + rule1.options = { + "handler": { + "klass_dotted_path": CTRLFake._path, + "method_name": "handler2", + } + } + rule2.options = { + "handler": { + "klass_dotted_path": CTRLFake._path, + "method_name": "handler3", + } + } + self.reg.update_rules([rule1, rule2]) + self.assertEqual( + self.reg._get_rule("route1").handler_options.method_name, "handler2" + ) + self.assertEqual( + self.reg._get_rule("route2").handler_options.method_name, "handler3" + ) + + def test_drop_rule(self): + rules = self._make_rules(stop=3) + self.assertEqual(self._count_rules(), 2) + self.reg.drop_rules([x.key for x in rules]) + self.assertEqual(self._count_rules(), 0) + + def test_endpoint_lookup_ko(self): + options = { + "handler": { + "klass_dotted_path": "no.where.to.be.SeenKlass", + "method_name": "foo", + } + } + rule = self._make_rules(stop=2, options=options)[0] + with self.assertRaises(EndpointHandlerNotFound): + rule.endpoint # pylint: disable=pointless-statement + + def test_endpoint_lookup_ok(self): + rule = self._make_rules(stop=2)[0] + self.assertTrue(isinstance(rule.endpoint, http.EndPoint)) + self.assertEqual(rule.endpoint("one"), ("one", 2)) + + def test_endpoint_lookup_ok_args(self): + options = { + "handler": { + "klass_dotted_path": CTRLFake._path, + "method_name": "handler1", + "default_pargs": ("one",), + } + } + rule = self._make_rules(stop=2, options=options)[0] + self.assertTrue(isinstance(rule.endpoint, http.EndPoint)) + self.assertEqual(rule.endpoint(), ("one", 2)) + + def test_get_rule_by_group(self): + self.assertEqual(self._count_rules(), 0) + self._make_rules(stop=4, route_group="one") + self._make_rules(start=5, stop=7, route_group="two") + self.assertEqual(self._count_rules(groups=("one", "two")), 5) + rules = self.reg.get_rules_by_group("one") + self.assertEqual([rule.key for rule in rules], ["route1", "route2", "route3"]) + rules = self.reg.get_rules_by_group("two") + self.assertEqual([rule.key for rule in rules], ["route5", "route6"]) From 7fada27090ac3d2d1dbe6b6b7f4833b5550f5ced Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 25 Jul 2022 17:23:20 +0200 Subject: [PATCH 18/91] endpoint_route_handler: add flag to control sync To avoid multiple invalidation of all envs on each edit or create of persistent records, a new flag is introduced: 'registry_sync'. This flag delays the sync of the rule registry till manual action occurs. Records in the UI are decorated accordingly to notify users of the need to reflect changes on ther registry to make them effective. The sync happens in a post commit hook to ensure all values are in place for the affected records. --- endpoint_route_handler/__manifest__.py | 4 +- .../migrations/14.0.1.2.0/pre-migrate.py | 20 +++ endpoint_route_handler/models/__init__.py | 1 + .../models/endpoint_route_handler.py | 90 +++--------- .../models/endpoint_route_sync_mixin.py | 131 ++++++++++++++++++ 5 files changed, 172 insertions(+), 74 deletions(-) create mode 100644 endpoint_route_handler/migrations/14.0.1.2.0/pre-migrate.py create mode 100644 endpoint_route_handler/models/endpoint_route_sync_mixin.py diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index cb7ad67d..4e7be3c5 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -2,9 +2,9 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). { - "name": " Route route handler", + "name": "Endpoint route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "version": "14.0.1.1.0", + "version": "14.0.1.2.0", "license": "LGPL-3", "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", diff --git a/endpoint_route_handler/migrations/14.0.1.2.0/pre-migrate.py b/endpoint_route_handler/migrations/14.0.1.2.0/pre-migrate.py new file mode 100644 index 00000000..b448ff23 --- /dev/null +++ b/endpoint_route_handler/migrations/14.0.1.2.0/pre-migrate.py @@ -0,0 +1,20 @@ +# Copyright 2022 Camptocamp SA (http://www.camptocamp.com) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +# fmt: off +from odoo.addons.endpoint_route_handler.registry import ( + EndpointRegistry, # pylint: disable=odoo-addons-relative-import +) + +# fmt: on + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + + EndpointRegistry._setup_table(cr) diff --git a/endpoint_route_handler/models/__init__.py b/endpoint_route_handler/models/__init__.py index 1755a4e5..562e67ae 100644 --- a/endpoint_route_handler/models/__init__.py +++ b/endpoint_route_handler/models/__init__.py @@ -1,2 +1,3 @@ +from . import endpoint_route_sync_mixin from . import endpoint_route_handler from . import ir_http diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index 16294f3b..81e22a9f 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -6,8 +6,6 @@ from odoo import _, api, exceptions, fields, models -from ..registry import EndpointRegistry - ENDPOINT_ROUTE_CONSUMER_MODELS = { # by db } @@ -16,9 +14,9 @@ class EndpointRouteHandler(models.AbstractModel): _name = "endpoint.route.handler" + _inherit = "endpoint.route.sync.mixin" _description = "Endpoint Route handler" - active = fields.Boolean(default=True) name = fields.Char(required=True) route = fields.Char( required=True, @@ -47,6 +45,7 @@ class EndpointRouteHandler(models.AbstractModel): compute="_compute_endpoint_hash", help="Identify the route with its main params" ) csrf = fields.Boolean(default=False) + # TODO: add flag to prevent route updates on save -> # should be handled by specific actions + filter in a tree view + btn on form @@ -132,20 +131,21 @@ def _selection_request_content_type(self): ("application/x-www-form-urlencoded", "Form"), ] - @api.depends(lambda self: self._controller_fields()) + @api.depends(lambda self: self._routing_impacting_fields()) def _compute_endpoint_hash(self): # Do not use read to be able to play this on NewId records too # (NewId records are classified as missing in ACL check). - # values = self.read(self._controller_fields()) + # values = self.read(self._routing_impacting_fields()) values = [ - {fname: rec[fname] for fname in self._controller_fields()} for rec in self + {fname: rec[fname] for fname in self._routing_impacting_fields()} + for rec in self ] for rec, vals in zip(self, values): vals.pop("id", None) rec.endpoint_hash = hash(tuple(vals.values())) - def _controller_fields(self): - return ["route", "auth_type", "request_method"] + def _routing_impacting_fields(self): + return ("route", "auth_type", "request_method") @api.depends("route") def _compute_route(self): @@ -187,38 +187,6 @@ def _check_request_method(self): _("Request method is required for POST and PUT.") ) - # Handle automatic route registration - - @api.model_create_multi - def create(self, vals_list): - rec = super().create(vals_list) - if not self._abstract and rec.active: - rec._register_controllers() - return rec - - def write(self, vals): - res = super().write(vals) - self._handle_route_updates(vals) - return res - - def _handle_route_updates(self, vals): - if "active" in vals: - if vals["active"]: - self._register_controllers() - else: - self._unregister_controllers() - return True - if any([x in vals for x in self._controller_fields()]): - self._logger.info("Route modified for %s", self.ids) - self._register_controllers() - return True - return False - - def unlink(self): - if not self._abstract: - self._unregister_controllers() - return super().unlink() - def _refresh_endpoint_data(self): """Enforce refresh of route computed fields. @@ -227,43 +195,21 @@ def _refresh_endpoint_data(self): self._compute_endpoint_hash() self._compute_route() - @property - def _endpoint_registry(self): - return EndpointRegistry.registry_for(self.env.cr) - - def _register_hook(self): - super()._register_hook() - if not self._abstract: - self._logger.info("Register controllers") - # Look explicitly for active records. - # Pass `init` to not set the registry as updated - # since this piece of code runs only when the model is loaded. - self.search([("active", "=", True)])._register_controllers(init=True) - def _register_controllers(self, init=False, options=None): - if self._abstract: + if self and self._abstract: self._refresh_endpoint_data() - - rules = [rec._make_controller_rule(options=options) for rec in self] - self._endpoint_registry.update_rules(rules, init=init) - if not init: - # When envs are already loaded we must signal changes - self._force_routing_map_refresh() - self._logger.debug( - "Registered controllers: %s", ", ".join(self.mapped("route")) - ) - - def _force_routing_map_refresh(self): - """Signal changes to make all routing maps refresh.""" - self.env["ir.http"]._clear_routing_map() - self.env.registry.registry_invalidated = True - self.env.registry.signal_changes() + super()._register_controllers(init=init, options=options) def _unregister_controllers(self): - if self._abstract: + if self and self._abstract: self._refresh_endpoint_data() - keys = tuple([rec._endpoint_registry_unique_key() for rec in self]) - self._endpoint_registry.drop_rules(keys) + super()._unregister_controllers() + + def _prepare_endpoint_rules(self, options=None): + return [rec._make_controller_rule(options=options) for rec in self] + + def _registered_endpoint_rule_keys(self): + return tuple([rec._endpoint_registry_unique_key() for rec in self]) def _endpoint_registry_unique_key(self): return "{0._name}:{0.id}".format(self) diff --git a/endpoint_route_handler/models/endpoint_route_sync_mixin.py b/endpoint_route_handler/models/endpoint_route_sync_mixin.py new file mode 100644 index 00000000..328a1b8a --- /dev/null +++ b/endpoint_route_handler/models/endpoint_route_sync_mixin.py @@ -0,0 +1,131 @@ +# Copyright 2021 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging +from functools import partial + +from odoo import api, fields, models + +from ..registry import EndpointRegistry + +_logger = logging.getLogger(__file__) + + +class EndpointRouteSyncMixin(models.AbstractModel): + """Mixin to handle synchronization of custom routes to the registry. + + Consumers of this mixin gain: + + * handling of sync state + * sync helpers + * automatic registration of routes on boot + + Consumers of this mixin must implement: + + * `_prepare_endpoint_rules` to retrieve all the `EndpointRule` to register + * `_registered_endpoint_rule_keys` to retrieve all keys of registered rules + """ + + _name = "endpoint.route.sync.mixin" + _description = "Endpoint Route sync mixin" + + active = fields.Boolean(default=True) + registry_sync = fields.Boolean( + help="ON: the record has been modified and registry was not notified." + "\nNo change will be active until this flag is set to false via proper action." + "\n\nOFF: record in line with the registry, nothing to do.", + default=False, + copy=False, + ) + + def write(self, vals): + if any([x in vals for x in self._routing_impacting_fields() + ("active",)]): + # Mark as out of sync + vals["registry_sync"] = False + res = super().write(vals) + if vals.get("registry_sync"): + # NOTE: this is not done on create to allow bulk reload of the envs + # and avoid multiple env restarts in case of multiple edits + # on one or more records in a row. + self._add_after_commit_hook(self.ids) + return res + + @api.model + def _add_after_commit_hook(self, record_ids): + self.env.cr.postcommit.add( + partial(self._handle_registry_sync_post_commit, record_ids), + ) + + def _handle_registry_sync(self, record_ids=None): + """Register and un-register controllers for given records.""" + record_ids = record_ids or self.ids + _logger.info("%s sync registry for %s", self._name, str(record_ids)) + records = self.browse(record_ids).exists() + records.filtered(lambda x: x.active)._register_controllers() + records.filtered(lambda x: not x.active)._unregister_controllers() + + def _handle_registry_sync_post_commit(self, record_ids=None): + """Handle registry sync after commit. + + When the sync is triggered as a post-commit hook + the env has been flushed already and the cursor committed, of course. + Hence, we must commit explicitly. + """ + self._handle_registry_sync(record_ids=record_ids) + self.env.cr.commit() # pylint: disable=invalid-commit + + @property + def _endpoint_registry(self): + return EndpointRegistry.registry_for(self.env.cr) + + def _register_hook(self): + super()._register_hook() + if not self._abstract: + # Ensure existing active records are loaded at startup. + # Pass `init` to bypass routing map refresh + # since this piece of code runs only when the model is loaded. + domain = [("active", "=", True), ("registry_sync", "=", True)] + self.search(domain)._register_controllers(init=True) + + def unlink(self): + if not self._abstract: + self._unregister_controllers() + return super().unlink() + + def _register_controllers(self, init=False, options=None): + if not self: + return + rules = self._prepare_endpoint_rules(options=options) + self._endpoint_registry.update_rules(rules, init=init) + if not init: + # When envs are already loaded we must signal changes + self._force_routing_map_refresh() + _logger.debug( + "%s registered controllers: %s", + self._name, + ", ".join([r.route for r in rules]), + ) + + def _force_routing_map_refresh(self): + """Signal changes to make all routing maps refresh.""" + self.env["ir.http"]._clear_routing_map() # TODO: redundant? + self.env.registry.registry_invalidated = True + self.env.registry.signal_changes() + + def _unregister_controllers(self): + if not self: + return + self._endpoint_registry.drop_rules(self._registered_endpoint_rule_keys()) + + def _routing_impacting_fields(self, options=None): + """Return list of fields that have impact on routing for current record.""" + raise NotImplementedError() + + def _prepare_endpoint_rules(self, options=None): + """Return list of `EndpointRule` instances for current record.""" + raise NotImplementedError() + + def _registered_endpoint_rule_keys(self): + """Return list of registered `EndpointRule` unique keys for current record.""" + raise NotImplementedError() From eb7482b504d095c56c3df55df0e17029a664b13f Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 27 Jul 2022 12:24:28 +0200 Subject: [PATCH 19/91] endpoint_route_handler: add constraints --- endpoint_route_handler/registry.py | 60 +++++++++++-------- endpoint_route_handler/tests/test_registry.py | 19 +++++- 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 14f508bd..3719c69a 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -69,14 +69,41 @@ class EndpointRegistry: _table = "endpoint_route" _columns = ( # name, type, comment - ("key", "VARCHAR", ""), # TODO: create index + ("key", "VARCHAR", ""), ("route", "VARCHAR", ""), ("opts", "text", ""), ("routing", "text", ""), - ("endpoint_hash", "VARCHAR(32)", ""), # TODO: add uniq constraint + ("endpoint_hash", "VARCHAR(32)", ""), ("route_group", "VARCHAR(32)", ""), ) + @classmethod + def registry_for(cls, cr): + return cls(cr) + + @classmethod + def wipe_registry_for(cls, cr): + cr.execute("TRUNCATE endpoint_route") + + @classmethod + def _setup_table(cls, cr): + if not tools.sql.table_exists(cr, cls._table): + tools.sql.create_model_table(cr, cls._table, columns=cls._columns) + tools.sql.create_unique_index( + cr, + "endpoint_route__key_uniq", + cls._table, + [ + "key", + ], + ) + tools.sql.add_constraint( + cr, + cls._table, + "endpoint_route__endpoint_hash_uniq", + "unique(endpoint_hash)", + ) + def __init__(self, cr): self.cr = cr @@ -84,21 +111,19 @@ def get_rules(self, keys=None, where=None): for row in self._get_rules(keys=keys, where=where): yield EndpointRule.from_row(self.cr.dbname, row) - def _get_rules(self, keys=None, where=None): + def _get_rules(self, keys=None, where=None, one=False): query = "SELECT * FROM endpoint_route" + pargs = () if keys and not where: - where = "key in (%s)" - self.cr.execute(query, keys) - return self.cr.fetchall() + query += " WHERE key IN %s" + pargs = (tuple(keys),) elif where: query += " " + where - self.cr.execute(query) - return self.cr.fetchall() + self.cr.execute(query, pargs) + return self.cr.fetchone() if one else self.cr.fetchall() def _get_rule(self, key): - query = "SELECT * FROM endpoint_route WHERE key = %s" - self.cr.execute(query, (key,)) - row = self.cr.fetchone() + row = self._get_rules(keys=(key,), one=True) if row: return EndpointRule.from_row(self.cr.dbname, row) @@ -151,22 +176,9 @@ def drop_rules(self, keys): self.cr.execute("DELETE FROM endpoint_route WHERE key IN %s", (tuple(keys),)) return True - @classmethod - def registry_for(cls, cr): - return cls(cr) - - @classmethod - def wipe_registry_for(cls, cr): - cr.execute("TRUNCATE endpoint_route") - def make_rule(self, *a, **kw): return EndpointRule(self.cr.dbname, *a, **kw) - @classmethod - def _setup_table(cls, cr): - if not tools.sql.table_exists(cr, cls._table): - tools.sql.create_model_table(cr, cls._table, columns=cls._columns) - class EndpointRule: """Hold information for a custom endpoint rule.""" diff --git a/endpoint_route_handler/tests/test_registry.py b/endpoint_route_handler/tests/test_registry.py index a1b54ed1..8942f969 100644 --- a/endpoint_route_handler/tests/test_registry.py +++ b/endpoint_route_handler/tests/test_registry.py @@ -2,6 +2,8 @@ # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from psycopg2.errors import UniqueViolation + from odoo import http from odoo.tests.common import SavepointCase, tagged @@ -74,7 +76,6 @@ def test_add_rule(self): def test_get_rules(self): self._make_rules(stop=4) self.assertEqual(self._count_rules(), 3) - self.reg.get_rules() self.assertEqual( [x.key for x in self.reg.get_rules()], ["route1", "route2", "route3"] ) @@ -124,6 +125,22 @@ def test_update_rule(self): self.reg._get_rule("route2").handler_options.method_name, "handler3" ) + def test_rule_constraints(self): + rule1, rule2 = self._make_rules(stop=3) + msg = ( + 'duplicate key value violates unique constraint "endpoint_route__key_uniq"' + ) + with self.assertRaisesRegex(UniqueViolation, msg), self.env.cr.savepoint(): + self.reg._create({rule1.key: rule1.to_row()}) + msg = ( + "duplicate key value violates unique constraint " + '"endpoint_route__endpoint_hash_uniq"' + ) + with self.assertRaisesRegex(UniqueViolation, msg), self.env.cr.savepoint(): + rule2.endpoint_hash = rule1.endpoint_hash + rule2.key = "key3" + self.reg._create({rule2.key: rule2.to_row()}) + def test_drop_rule(self): rules = self._make_rules(stop=3) self.assertEqual(self._count_rules(), 2) From 8e64d15be98cc4524f2d5a4efb557cae1967fb1f Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 1 Nov 2022 18:31:37 +0100 Subject: [PATCH 20/91] endpoint_route_handler: add auto timestamp to routes --- .../models/endpoint_route_handler.py | 8 +- .../models/endpoint_route_sync_mixin.py | 9 -- endpoint_route_handler/models/ir_http.py | 30 +++++- endpoint_route_handler/registry.py | 37 ++++++- endpoint_route_handler/tests/test_endpoint.py | 97 ++++++++++++------- endpoint_route_handler/tests/test_registry.py | 38 +++++++- 6 files changed, 162 insertions(+), 57 deletions(-) diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index 81e22a9f..c9e766b0 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -216,15 +216,9 @@ def _endpoint_registry_unique_key(self): # TODO: consider if useful or not for single records def _register_single_controller(self, options=None, key=None, init=False): - """Shortcut to register one single controller. - - WARNING: as this triggers envs invalidation via `_force_routing_map_refresh` - do not abuse of this method to register more than one route. - """ + """Shortcut to register one single controller.""" rule = self._make_controller_rule(options=options, key=key) self._endpoint_registry.update_rules([rule], init=init) - if not init: - self._force_routing_map_refresh() self._logger.debug( "Registered controller %s (auth: %s)", self.route, self.auth_type ) diff --git a/endpoint_route_handler/models/endpoint_route_sync_mixin.py b/endpoint_route_handler/models/endpoint_route_sync_mixin.py index 328a1b8a..e0368e15 100644 --- a/endpoint_route_handler/models/endpoint_route_sync_mixin.py +++ b/endpoint_route_handler/models/endpoint_route_sync_mixin.py @@ -98,21 +98,12 @@ def _register_controllers(self, init=False, options=None): return rules = self._prepare_endpoint_rules(options=options) self._endpoint_registry.update_rules(rules, init=init) - if not init: - # When envs are already loaded we must signal changes - self._force_routing_map_refresh() _logger.debug( "%s registered controllers: %s", self._name, ", ".join([r.route for r in rules]), ) - def _force_routing_map_refresh(self): - """Signal changes to make all routing maps refresh.""" - self.env["ir.http"]._clear_routing_map() # TODO: redundant? - self.env.registry.registry_invalidated = True - self.env.registry.signal_changes() - def _unregister_controllers(self): if not self: return diff --git a/endpoint_route_handler/models/ir_http.py b/endpoint_route_handler/models/ir_http.py index 52d719ea..b21e8e46 100644 --- a/endpoint_route_handler/models/ir_http.py +++ b/endpoint_route_handler/models/ir_http.py @@ -17,6 +17,10 @@ class IrHttp(models.AbstractModel): _inherit = "ir.http" + @classmethod + def _endpoint_route_registry(cls, env): + return EndpointRegistry.registry_for(env.cr) + @classmethod def _generate_routing_rules(cls, modules, converters): # Override to inject custom endpoint rules. @@ -28,14 +32,36 @@ def _generate_routing_rules(cls, modules, converters): @classmethod def _endpoint_routing_rules(cls): """Yield custom endpoint rules""" - cr = http.request.env.cr - e_registry = EndpointRegistry.registry_for(cr) + e_registry = cls._endpoint_route_registry(http.request.env) for endpoint_rule in e_registry.get_rules(): _logger.debug("LOADING %s", endpoint_rule) endpoint = endpoint_rule.endpoint for url in endpoint_rule.routing["routes"]: yield (url, endpoint, endpoint_rule.routing) + @classmethod + def routing_map(cls, key=None): + last_update = cls._get_routing_map_last_update(http.request.env) + if not hasattr(cls, "_routing_map"): + # routing map just initialized, store last update for this env + cls._endpoint_route_last_update = last_update + elif cls._endpoint_route_last_update < last_update: + _logger.info("Endpoint registry updated, reset routing map") + cls._routing_map = {} + cls._rewrite_len = {} + cls._endpoint_route_last_update = last_update + return super().routing_map(key=key) + + @classmethod + def _get_routing_map_last_update(cls, env): + return cls._endpoint_route_registry(env).last_update() + + @classmethod + def _clear_routing_map(cls): + super()._clear_routing_map() + if hasattr(cls, "_endpoint_route_last_update"): + cls._endpoint_route_last_update = 0 + @classmethod def _auth_method_user_endpoint(cls): """Special method for user auth which raises Unauthorized when needed. diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 3719c69a..4589d563 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -75,6 +75,7 @@ class EndpointRegistry: ("routing", "text", ""), ("endpoint_hash", "VARCHAR(32)", ""), ("route_group", "VARCHAR(32)", ""), + ("updated_at", "TIMESTAMP NOT NULL DEFAULT NOW()", ""), ) @classmethod @@ -104,6 +105,26 @@ def _setup_table(cls, cr): "unique(endpoint_hash)", ) + cr.execute( + """ + CREATE OR REPLACE FUNCTION endpoint_route_set_timestamp() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = NOW(); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + """ + ) + cr.execute( + """ + CREATE TRIGGER trigger_endpoint_route_set_timestamp + BEFORE UPDATE ON endpoint_route + FOR EACH ROW + EXECUTE PROCEDURE endpoint_route_set_timestamp(); + """ + ) + def __init__(self, cr): self.cr = cr @@ -179,6 +200,20 @@ def drop_rules(self, keys): def make_rule(self, *a, **kw): return EndpointRule(self.cr.dbname, *a, **kw) + def last_update(self): + self.cr.execute( + """ + SELECT updated_at + FROM endpoint_route + ORDER BY updated_at DESC + LIMIT 1 + """ + ) + res = self.cr.fetchone() + if res: + return res[0].timestamp() + return 0.0 + class EndpointRule: """Hold information for a custom endpoint rule.""" @@ -232,7 +267,7 @@ def options(self, value): @classmethod def from_row(cls, dbname, row): - key, route, options, routing, endpoint_hash, route_group = row[1:] + key, route, options, routing, endpoint_hash, route_group = row[1:-1] # TODO: #jsonb-ref options = json.loads(options) routing = json.loads(routing) diff --git a/endpoint_route_handler/tests/test_endpoint.py b/endpoint_route_handler/tests/test_endpoint.py index a8d06f80..ca2add6c 100644 --- a/endpoint_route_handler/tests/test_endpoint.py +++ b/endpoint_route_handler/tests/test_endpoint.py @@ -1,9 +1,11 @@ # Copyright 2021 Camptocamp SA # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - +import time from contextlib import contextmanager +import mock + import odoo from odoo.tools import mute_logger @@ -25,25 +27,27 @@ def new_rollbacked_env(): cr.close() +def make_new_route(env, **kw): + model = env["endpoint.route.handler"] + vals = { + "name": "Test custom route", + "route": "/my/test/route", + "request_method": "GET", + } + vals.update(kw) + new_route = model.new(vals) + new_route._refresh_endpoint_data() + return new_route + + class TestEndpoint(CommonEndpoint): def tearDown(self): self.env["ir.http"]._clear_routing_map() EndpointRegistry.wipe_registry_for(self.env.cr) super().tearDown() - def _make_new_route(self, **kw): - vals = { - "name": "Test custom route", - "route": "/my/test/route", - "request_method": "GET", - } - vals.update(kw) - new_route = self.route_handler.new(vals) - new_route._refresh_endpoint_data() - return new_route - def test_as_tool_base_data(self): - new_route = self._make_new_route() + new_route = make_new_route(self.env) self.assertEqual(new_route.route, "/my/test/route") first_hash = new_route.endpoint_hash self.assertTrue(first_hash) @@ -53,7 +57,7 @@ def test_as_tool_base_data(self): @mute_logger("odoo.addons.base.models.ir_http") def test_as_tool_register_single_controller(self): - new_route = self._make_new_route() + new_route = make_new_route(self.env) options = { "handler": { "klass_dotted_path": CTRLFake._path, @@ -79,7 +83,7 @@ def test_as_tool_register_single_controller(self): @mute_logger("odoo.addons.base.models.ir_http") def test_as_tool_register_controllers(self): - new_route = self._make_new_route() + new_route = make_new_route(self.env) options = { "handler": { "klass_dotted_path": CTRLFake._path, @@ -106,8 +110,7 @@ def test_as_tool_register_controllers(self): @mute_logger("odoo.addons.base.models.ir_http") def test_as_tool_register_controllers_dynamic_route(self): route = "/my/app/" - new_route = self._make_new_route(route=route) - + new_route = make_new_route(self.env, route=route) options = { "handler": { "klass_dotted_path": CTRLFake._path, @@ -121,12 +124,18 @@ def test_as_tool_register_controllers_dynamic_route(self): rmap = self.env["ir.http"].routing_map() self.assertIn(route, [x.rule for x in rmap._rules]) + +class TestEndpointCrossEnv(CommonEndpoint): + def setUp(self): + super().setUp() + self.env["ir.http"]._clear_routing_map() + EndpointRegistry.wipe_registry_for(self.env.cr) + @mute_logger("odoo.addons.base.models.ir_http", "odoo.modules.registry") def test_cross_env_consistency(self): """Ensure route updates are propagated to all envs.""" route = "/my/app/" - new_route = self._make_new_route(route=route) - + new_route = make_new_route(self.env, route=route) options = { "handler": { "klass_dotted_path": CTRLFake._path, @@ -135,27 +144,47 @@ def test_cross_env_consistency(self): } env1 = self.env + EndpointRegistry.registry_for(self.env.cr) + new_route._register_controllers(options=options) + + # Simulate 1st route created in the past + last_update0 = time.time() - 10000 + path = "odoo.addons.endpoint_route_handler.registry.EndpointRegistry" with self._get_mocked_request(): with new_rollbacked_env() as env2: - # Load maps - env1["ir.http"].routing_map() - env2["ir.http"].routing_map() - # Register route in current env. - # By using `init=True` we don't trigger env signals - # (simulating when the registry is loaded for the 1st time - # by `_register_hook`). - # In this case we expect the test to fail - # as there's no propagation to the other env. - new_route._register_controllers(options=options, init=True) - rmap = self.env["ir.http"].routing_map() - self.assertNotIn(route, [x.rule for x in rmap._rules]) - rmap = env2["ir.http"].routing_map() - self.assertNotIn(route, [x.rule for x in rmap._rules]) - # Now w/out init -> works + with mock.patch(path + ".last_update") as mocked: + mocked.return_value = last_update0 + # Load maps + env1["ir.http"].routing_map() + env2["ir.http"].routing_map() + self.assertEqual( + env1["ir.http"]._endpoint_route_last_update, last_update0 + ) + self.assertEqual( + env2["ir.http"]._endpoint_route_last_update, last_update0 + ) + rmap = self.env["ir.http"].routing_map() + self.assertIn(route, [x.rule for x in rmap._rules]) + rmap = env2["ir.http"].routing_map() + self.assertIn(route, [x.rule for x in rmap._rules]) + + # add new route + route = "/my/new/" + new_route = make_new_route(self.env, route=route) new_route._register_controllers(options=options) + + # with mock.patch(path + ".last_update") as mocked: + # mocked.return_value = last_update0 + 1000 + rmap = self.env["ir.http"].routing_map() self.assertIn(route, [x.rule for x in rmap._rules]) rmap = env2["ir.http"].routing_map() self.assertIn(route, [x.rule for x in rmap._rules]) + self.assertTrue( + env1["ir.http"]._endpoint_route_last_update > last_update0 + ) + self.assertTrue( + env2["ir.http"]._endpoint_route_last_update > last_update0 + ) # TODO: test unregister diff --git a/endpoint_route_handler/tests/test_registry.py b/endpoint_route_handler/tests/test_registry.py index 8942f969..3b28edc2 100644 --- a/endpoint_route_handler/tests/test_registry.py +++ b/endpoint_route_handler/tests/test_registry.py @@ -2,10 +2,11 @@ # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from psycopg2.errors import UniqueViolation +from psycopg2 import DatabaseError from odoo import http from odoo.tests.common import SavepointCase, tagged +from odoo.tools import mute_logger from odoo.addons.endpoint_route_handler.exceptions import EndpointHandlerNotFound from odoo.addons.endpoint_route_handler.registry import EndpointRegistry @@ -18,9 +19,12 @@ class TestRegistry(SavepointCase): @classmethod def setUpClass(cls): super().setUpClass() - EndpointRegistry.wipe_registry_for(cls.env.cr) cls.reg = EndpointRegistry.registry_for(cls.env.cr) + def setUp(self): + super().setUp() + EndpointRegistry.wipe_registry_for(self.env.cr) + def tearDown(self): EndpointRegistry.wipe_registry_for(self.env.cr) super().tearDown() @@ -37,6 +41,31 @@ def test_registry_empty(self): self.assertEqual(list(self.reg.get_rules()), []) self.assertEqual(self._count_rules(), 0) + def test_last_update(self): + self.assertEqual(self.reg.last_update(), 0.0) + rule1, rule2 = self._make_rules(stop=3) + last_update0 = self.reg.last_update() + self.assertTrue(last_update0 > 0) + rule1.options = { + "handler": { + "klass_dotted_path": CTRLFake._path, + "method_name": "handler2", + } + } + # FIXME: to test timestamp we have to mock psql datetime. + # self.reg.update_rules([rule1]) + # last_update1 = self.reg.last_update() + # self.assertTrue(last_update1 > last_update0) + # rule2.options = { + # "handler": { + # "klass_dotted_path": CTRLFake._path, + # "method_name": "handler2", + # } + # } + # self.reg.update_rules([rule2]) + # last_update2 = self.reg.last_update() + # self.assertTrue(last_update2 > last_update1) + def _make_rules(self, stop=5, start=1, **kw): res = [] for i in range(start, stop): @@ -125,18 +154,19 @@ def test_update_rule(self): self.reg._get_rule("route2").handler_options.method_name, "handler3" ) + @mute_logger("odoo.sql_db") def test_rule_constraints(self): rule1, rule2 = self._make_rules(stop=3) msg = ( 'duplicate key value violates unique constraint "endpoint_route__key_uniq"' ) - with self.assertRaisesRegex(UniqueViolation, msg), self.env.cr.savepoint(): + with self.assertRaisesRegex(DatabaseError, msg), self.env.cr.savepoint(): self.reg._create({rule1.key: rule1.to_row()}) msg = ( "duplicate key value violates unique constraint " '"endpoint_route__endpoint_hash_uniq"' ) - with self.assertRaisesRegex(UniqueViolation, msg), self.env.cr.savepoint(): + with self.assertRaisesRegex(DatabaseError, msg), self.env.cr.savepoint(): rule2.endpoint_hash = rule1.endpoint_hash rule2.key = "key3" self.reg._create({rule2.key: rule2.to_row()}) From d7800295b01d5115b83a4f17f2173b0ffc739774 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 25 Jul 2022 17:27:11 +0200 Subject: [PATCH 21/91] endpoint_route_handler: fix typo in validator --- endpoint_route_handler/models/endpoint_route_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index c9e766b0..4e2bfbf7 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -184,7 +184,7 @@ 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.") + _("Request content type is required for POST and PUT.") ) def _refresh_endpoint_data(self): From 86c1b6a004ed2b49ee4d8ef954e30e3e654bc866 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 16 Feb 2023 17:42:15 +0100 Subject: [PATCH 22/91] endpoint_route_handler: add new tool model --- endpoint_route_handler/models/__init__.py | 1 + .../models/endpoint_route_handler.py | 18 -------- .../models/endpoint_route_handler_tool.py | 46 +++++++++++++++++++ .../models/endpoint_route_sync_mixin.py | 2 +- endpoint_route_handler/readme/USAGE.rst | 29 ++++++++++-- .../security/ir.model.access.csv | 6 ++- endpoint_route_handler/tests/test_endpoint.py | 6 +-- .../tests/test_endpoint_controller.py | 3 +- 8 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 endpoint_route_handler/models/endpoint_route_handler_tool.py diff --git a/endpoint_route_handler/models/__init__.py b/endpoint_route_handler/models/__init__.py index 562e67ae..2115e503 100644 --- a/endpoint_route_handler/models/__init__.py +++ b/endpoint_route_handler/models/__init__.py @@ -1,3 +1,4 @@ from . import endpoint_route_sync_mixin from . import endpoint_route_handler +from . import endpoint_route_handler_tool from . import ir_http diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index 4e2bfbf7..78061d6c 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -187,24 +187,6 @@ def _check_request_method(self): _("Request content type is required for POST and PUT.") ) - def _refresh_endpoint_data(self): - """Enforce refresh of route computed fields. - - Required for NewId records when using this model as a tool. - """ - self._compute_endpoint_hash() - self._compute_route() - - def _register_controllers(self, init=False, options=None): - if self and self._abstract: - self._refresh_endpoint_data() - super()._register_controllers(init=init, options=options) - - def _unregister_controllers(self): - if self and self._abstract: - self._refresh_endpoint_data() - super()._unregister_controllers() - def _prepare_endpoint_rules(self, options=None): return [rec._make_controller_rule(options=options) for rec in self] diff --git a/endpoint_route_handler/models/endpoint_route_handler_tool.py b/endpoint_route_handler/models/endpoint_route_handler_tool.py new file mode 100644 index 00000000..f699457f --- /dev/null +++ b/endpoint_route_handler/models/endpoint_route_handler_tool.py @@ -0,0 +1,46 @@ +# Copyright 2023 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +from odoo import api, models + + +class EndpointRouteHandlerTool(models.TransientModel): + """Model meant to be used as a tool. + + From v15 on we cannot initialize AbstractModel using `new()` anymore. + Here we proxy the abstract model with a transient model so that we can initialize it + but we don't care at all about storing it in the DB. + """ + + # TODO: try using `_auto = False` + + _name = "endpoint.route.handler.tool" + _inherit = "endpoint.route.handler" + _description = "Endpoint Route handler tool" + + def _refresh_endpoint_data(self): + """Enforce refresh of route computed fields. + + Required for NewId records when using this model as a tool. + """ + self._compute_endpoint_hash() + self._compute_route() + + def _register_controllers(self, init=False, options=None): + if self: + self._refresh_endpoint_data() + return super()._register_controllers(init=init, options=options) + + def _unregister_controllers(self): + if self: + self._refresh_endpoint_data() + return super()._unregister_controllers() + + @api.model + def new(self, values=None, origin=None, ref=None): + values = values or {} # note: in core odoo they use `{}` as defaul arg :/ + res = super().new(values=values, origin=origin, ref=ref) + res._refresh_endpoint_data() + return res diff --git a/endpoint_route_handler/models/endpoint_route_sync_mixin.py b/endpoint_route_handler/models/endpoint_route_sync_mixin.py index e0368e15..e91dd511 100644 --- a/endpoint_route_handler/models/endpoint_route_sync_mixin.py +++ b/endpoint_route_handler/models/endpoint_route_sync_mixin.py @@ -81,7 +81,7 @@ def _endpoint_registry(self): def _register_hook(self): super()._register_hook() - if not self._abstract: + if not self._abstract and not self._transient: # Ensure existing active records are loaded at startup. # Pass `init` to bypass routing map refresh # since this piece of code runs only when the model is loaded. diff --git a/endpoint_route_handler/readme/USAGE.rst b/endpoint_route_handler/readme/USAGE.rst index 75864398..b0c1581b 100644 --- a/endpoint_route_handler/readme/USAGE.rst +++ b/endpoint_route_handler/readme/USAGE.rst @@ -10,6 +10,20 @@ Use standard Odoo inheritance:: Once you have this, each `my.model` record will generate a route. You can have a look at the `endpoint` module to see a real life example. +The options of the routing rules are defined by the method `_default_endpoint_options`. +Here's an example from the `endpoint` module:: + + def _default_endpoint_options_handler(self): + return { + "klass_dotted_path": "odoo.addons.endpoint.controllers.main.EndpointController", + "method_name": "auto_endpoint", + "default_pargs": (self.route,), + } + +As you can see, you have to pass the references to the controller class and the method to use +when the endpoint is called. And you can prepare some default arguments to pass. +In this case, the route of the current record. + As a tool ~~~~~~~~~ @@ -17,7 +31,7 @@ As a tool Initialize non stored route handlers and generate routes from them. For instance:: - route_handler = self.env["endpoint.route.handler"] + route_handler = self.env["endpoint.route.handler.tool"] endpoint_handler = MyController()._my_handler vals = { "name": "My custom route", @@ -26,8 +40,17 @@ For instance:: "auth_type": "public", } new_route = route_handler.new(vals) - new_route._refresh_endpoint_data() # required only for NewId records - new_route._register_controller(endpoint_handler=endpoint_handler, key="my-custom-route") + new_route._register_controller() + +You can override options and define - for instance - a different controller method:: + + options = { + "handler": { + "klass_dotted_path": "odoo.addons.my_module.controllers.SpecialController", + "method_name": "my_special_handler", + } + } + new_route._register_controller(options=options) Of course, what happens when the endpoint gets called depends on the logic defined on the controller method. diff --git a/endpoint_route_handler/security/ir.model.access.csv b/endpoint_route_handler/security/ir.model.access.csv index ec4133f1..c070dc56 100644 --- a/endpoint_route_handler/security/ir.model.access.csv +++ b/endpoint_route_handler/security/ir.model.access.csv @@ -1,3 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_endpoint_route_handler_mngr_edit,endpoint_route_handler mngr edit,model_endpoint_route_handler,base.group_system,1,1,1,1 -access_endpoint_route_handler_edit,endpoint_route_handler edit,model_endpoint_route_handler,,1,0,0,0 +access_endpoint_route_handler_tool_mngr_edit,endpoint_route_handler mngr edit,model_endpoint_route_handler,base.group_system,1,1,1,1 +access_endpoint_route_handler_tool_edit,endpoint_route_handler edit,model_endpoint_route_handler,,1,0,0,0 +access_endpoint_route_handler_mngr_edit,endpoint_route_handler_tool mngr edit,model_endpoint_route_handler_tool,base.group_system,1,1,1,1 +access_endpoint_route_handler_edit,endpoint_route_handler_tool edit,model_endpoint_route_handler_tool,,1,0,0,0 diff --git a/endpoint_route_handler/tests/test_endpoint.py b/endpoint_route_handler/tests/test_endpoint.py index ca2add6c..4f7e0eed 100644 --- a/endpoint_route_handler/tests/test_endpoint.py +++ b/endpoint_route_handler/tests/test_endpoint.py @@ -28,7 +28,7 @@ def new_rollbacked_env(): def make_new_route(env, **kw): - model = env["endpoint.route.handler"] + model = env["endpoint.route.handler.tool"] vals = { "name": "Test custom route", "route": "/my/test/route", @@ -36,7 +36,6 @@ def make_new_route(env, **kw): } vals.update(kw) new_route = model.new(vals) - new_route._refresh_endpoint_data() return new_route @@ -52,7 +51,6 @@ def test_as_tool_base_data(self): first_hash = new_route.endpoint_hash self.assertTrue(first_hash) new_route.route += "/new" - new_route._refresh_endpoint_data() self.assertNotEqual(new_route.endpoint_hash, first_hash) @mute_logger("odoo.addons.base.models.ir_http") @@ -73,7 +71,6 @@ def test_as_tool_register_single_controller(self): # Ensure is updated when needed new_route.route += "/new" - new_route._refresh_endpoint_data() with self._get_mocked_request(): new_route._register_single_controller(options=options, init=True) rmap = self.env["ir.http"]._clear_routing_map() @@ -99,7 +96,6 @@ def test_as_tool_register_controllers(self): # Ensure is updated when needed new_route.route += "/new" - new_route._refresh_endpoint_data() with self._get_mocked_request(): new_route._register_controllers(options=options, init=True) rmap = self.env["ir.http"]._clear_routing_map() diff --git a/endpoint_route_handler/tests/test_endpoint_controller.py b/endpoint_route_handler/tests/test_endpoint_controller.py index 4b98feea..919e893f 100644 --- a/endpoint_route_handler/tests/test_endpoint_controller.py +++ b/endpoint_route_handler/tests/test_endpoint_controller.py @@ -15,7 +15,7 @@ class EndpointHttpCase(HttpCase): def setUp(self): super().setUp() - self.route_handler = self.env["endpoint.route.handler"] + self.route_handler = self.env["endpoint.route.handler.tool"] def tearDown(self): EndpointRegistry.wipe_registry_for(self.env.cr) @@ -29,7 +29,6 @@ def _make_new_route(self, options=None, **kw): } vals.update(kw) new_route = self.route_handler.new(vals) - new_route._refresh_endpoint_data() new_route._register_controllers(options=options) return new_route From 782776956c097af55836795073665851a6f67ff3 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 1 Mar 2023 09:36:56 +0100 Subject: [PATCH 23/91] endpoint_route_handler: use sequence as version --- .../migrations/14.0.1.2.0/pre-migrate.py | 2 +- endpoint_route_handler/models/ir_http.py | 16 +-- endpoint_route_handler/post_init_hook.py | 2 +- endpoint_route_handler/registry.py | 114 +++++++++++++----- endpoint_route_handler/tests/test_endpoint.py | 44 +++---- endpoint_route_handler/tests/test_registry.py | 6 + 6 files changed, 115 insertions(+), 69 deletions(-) diff --git a/endpoint_route_handler/migrations/14.0.1.2.0/pre-migrate.py b/endpoint_route_handler/migrations/14.0.1.2.0/pre-migrate.py index b448ff23..4e5e3b0e 100644 --- a/endpoint_route_handler/migrations/14.0.1.2.0/pre-migrate.py +++ b/endpoint_route_handler/migrations/14.0.1.2.0/pre-migrate.py @@ -17,4 +17,4 @@ def migrate(cr, version): if not version: return - EndpointRegistry._setup_table(cr) + EndpointRegistry._setup_db(cr) diff --git a/endpoint_route_handler/models/ir_http.py b/endpoint_route_handler/models/ir_http.py index b21e8e46..e684f0d2 100644 --- a/endpoint_route_handler/models/ir_http.py +++ b/endpoint_route_handler/models/ir_http.py @@ -41,26 +41,26 @@ def _endpoint_routing_rules(cls): @classmethod def routing_map(cls, key=None): - last_update = cls._get_routing_map_last_update(http.request.env) + last_version = cls._get_routing_map_last_version(http.request.env) if not hasattr(cls, "_routing_map"): # routing map just initialized, store last update for this env - cls._endpoint_route_last_update = last_update - elif cls._endpoint_route_last_update < last_update: + cls._endpoint_route_last_version = last_version + elif cls._endpoint_route_last_version < last_version: _logger.info("Endpoint registry updated, reset routing map") cls._routing_map = {} cls._rewrite_len = {} - cls._endpoint_route_last_update = last_update + cls._endpoint_route_last_version = last_version return super().routing_map(key=key) @classmethod - def _get_routing_map_last_update(cls, env): - return cls._endpoint_route_registry(env).last_update() + def _get_routing_map_last_version(cls, env): + return cls._endpoint_route_registry(env).last_version() @classmethod def _clear_routing_map(cls): super()._clear_routing_map() - if hasattr(cls, "_endpoint_route_last_update"): - cls._endpoint_route_last_update = 0 + if hasattr(cls, "_endpoint_route_last_version"): + cls._endpoint_route_last_version = 0 @classmethod def _auth_method_user_endpoint(cls): diff --git a/endpoint_route_handler/post_init_hook.py b/endpoint_route_handler/post_init_hook.py index 2f64fffd..d706f0e9 100644 --- a/endpoint_route_handler/post_init_hook.py +++ b/endpoint_route_handler/post_init_hook.py @@ -11,4 +11,4 @@ def post_init_hook(cr, registry): # this is the trigger that sends notifications when jobs change _logger.info("Create table") - EndpointRegistry._setup_table(cr) + EndpointRegistry._setup_db(cr) diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 4589d563..c47265ec 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -7,6 +7,7 @@ from functools import partial from psycopg2 import sql +from psycopg2.extensions import AsIs from psycopg2.extras import execute_values from odoo import http, tools @@ -87,43 +88,81 @@ def wipe_registry_for(cls, cr): cr.execute("TRUNCATE endpoint_route") @classmethod - def _setup_table(cls, cr): + def _setup_db(cls, cr): if not tools.sql.table_exists(cr, cls._table): - tools.sql.create_model_table(cr, cls._table, columns=cls._columns) - tools.sql.create_unique_index( - cr, - "endpoint_route__key_uniq", - cls._table, - [ - "key", - ], - ) - tools.sql.add_constraint( - cr, - cls._table, - "endpoint_route__endpoint_hash_uniq", - "unique(endpoint_hash)", - ) - - cr.execute( - """ - CREATE OR REPLACE FUNCTION endpoint_route_set_timestamp() + cls._setup_db_table(cr) + cls._setup_db_timestamp(cr) + cls._setup_db_version(cr) + + @classmethod + def _setup_db_table(cls, cr): + """Create routing table and indexes""" + tools.sql.create_model_table(cr, cls._table, columns=cls._columns) + tools.sql.create_unique_index( + cr, + "endpoint_route__key_uniq", + cls._table, + [ + "key", + ], + ) + tools.sql.add_constraint( + cr, + cls._table, + "endpoint_route__endpoint_hash_uniq", + "unique(endpoint_hash)", + ) + + @classmethod + def _setup_db_timestamp(cls, cr): + """Create trigger to update rows timestamp on updates""" + cr.execute( + """ + CREATE OR REPLACE FUNCTION endpoint_route_set_timestamp() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = NOW(); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + """ + ) + cr.execute( + """ + CREATE TRIGGER trigger_endpoint_route_set_timestamp + BEFORE UPDATE ON %(table)s + FOR EACH ROW + EXECUTE PROCEDURE endpoint_route_set_timestamp(); + """, + {"table": AsIs(cls._table)}, + ) + + @classmethod + def _setup_db_version(cls, cr): + """Create sequence and triggers to keep track of routes' version""" + cr.execute( + """ + SELECT 1 FROM pg_class WHERE RELNAME = 'endpoint_route_version' + """ + ) + if not cr.fetchone(): + sql = """ + CREATE SEQUENCE endpoint_route_version INCREMENT BY 1 START WITH 1; + CREATE OR REPLACE FUNCTION increment_endpoint_route_version() RETURNS TRIGGER AS $$ BEGIN - NEW.updated_at = NOW(); - RETURN NEW; + PERFORM nextval('endpoint_route_version'); + RETURN NEW; END; - $$ LANGUAGE plpgsql; - """ - ) - cr.execute( - """ - CREATE TRIGGER trigger_endpoint_route_set_timestamp - BEFORE UPDATE ON endpoint_route - FOR EACH ROW - EXECUTE PROCEDURE endpoint_route_set_timestamp(); + $$ language plpgsql; + CREATE TRIGGER update_endpoint_route_version_trigger + BEFORE INSERT ON %(table)s + for each row execute procedure increment_endpoint_route_version(); + CREATE TRIGGER insert_endpoint_route_version_trigger + BEFORE UPDATE ON %(table)s + for each row execute procedure increment_endpoint_route_version(); """ - ) + cr.execute(sql, {"table": AsIs(cls._table)}) def __init__(self, cr): self.cr = cr @@ -214,6 +253,17 @@ def last_update(self): return res[0].timestamp() return 0.0 + def last_version(self): + self.cr.execute( + """ + SELECT last_value FROM endpoint_route_version + """ + ) + res = self.cr.fetchone() + if res: + return res[0] + return -1 + class EndpointRule: """Hold information for a custom endpoint rule.""" diff --git a/endpoint_route_handler/tests/test_endpoint.py b/endpoint_route_handler/tests/test_endpoint.py index 4f7e0eed..a62663dd 100644 --- a/endpoint_route_handler/tests/test_endpoint.py +++ b/endpoint_route_handler/tests/test_endpoint.py @@ -1,11 +1,8 @@ # Copyright 2021 Camptocamp SA # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -import time from contextlib import contextmanager -import mock - import odoo from odoo.tools import mute_logger @@ -140,47 +137,40 @@ def test_cross_env_consistency(self): } env1 = self.env - EndpointRegistry.registry_for(self.env.cr) + reg = EndpointRegistry.registry_for(self.env.cr) new_route._register_controllers(options=options) - # Simulate 1st route created in the past - last_update0 = time.time() - 10000 - path = "odoo.addons.endpoint_route_handler.registry.EndpointRegistry" + last_version0 = reg.last_version() with self._get_mocked_request(): with new_rollbacked_env() as env2: - with mock.patch(path + ".last_update") as mocked: - mocked.return_value = last_update0 - # Load maps - env1["ir.http"].routing_map() - env2["ir.http"].routing_map() - self.assertEqual( - env1["ir.http"]._endpoint_route_last_update, last_update0 - ) - self.assertEqual( - env2["ir.http"]._endpoint_route_last_update, last_update0 - ) - rmap = self.env["ir.http"].routing_map() - self.assertIn(route, [x.rule for x in rmap._rules]) - rmap = env2["ir.http"].routing_map() - self.assertIn(route, [x.rule for x in rmap._rules]) + # Load maps + env1["ir.http"].routing_map() + env2["ir.http"].routing_map() + self.assertEqual( + env1["ir.http"]._endpoint_route_last_version, last_version0 + ) + self.assertEqual( + env2["ir.http"]._endpoint_route_last_version, last_version0 + ) + rmap = self.env["ir.http"].routing_map() + self.assertIn(route, [x.rule for x in rmap._rules]) + rmap = env2["ir.http"].routing_map() + self.assertIn(route, [x.rule for x in rmap._rules]) # add new route route = "/my/new/" new_route = make_new_route(self.env, route=route) new_route._register_controllers(options=options) - # with mock.patch(path + ".last_update") as mocked: - # mocked.return_value = last_update0 + 1000 - rmap = self.env["ir.http"].routing_map() self.assertIn(route, [x.rule for x in rmap._rules]) rmap = env2["ir.http"].routing_map() self.assertIn(route, [x.rule for x in rmap._rules]) self.assertTrue( - env1["ir.http"]._endpoint_route_last_update > last_update0 + env1["ir.http"]._endpoint_route_last_version > last_version0 ) self.assertTrue( - env2["ir.http"]._endpoint_route_last_update > last_update0 + env2["ir.http"]._endpoint_route_last_version > last_version0 ) # TODO: test unregister diff --git a/endpoint_route_handler/tests/test_registry.py b/endpoint_route_handler/tests/test_registry.py index 3b28edc2..04015211 100644 --- a/endpoint_route_handler/tests/test_registry.py +++ b/endpoint_route_handler/tests/test_registry.py @@ -66,6 +66,12 @@ def test_last_update(self): # last_update2 = self.reg.last_update() # self.assertTrue(last_update2 > last_update1) + def test_last_version(self): + last_version0 = self.reg.last_version() + self._make_rules(stop=3) + last_version1 = self.reg.last_version() + self.assertTrue(last_version1 > last_version0) + def _make_rules(self, stop=5, start=1, **kw): res = [] for i in range(start, stop): From f9c71debd2e345f6265d49080f8acc7c16f7d52e Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 8 Mar 2023 12:27:54 +0100 Subject: [PATCH 24/91] endpoint_route_handler: log table setup and wipe --- endpoint_route_handler/registry.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index c47265ec..45b405e0 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -4,6 +4,7 @@ import importlib import json +import logging from functools import partial from psycopg2 import sql @@ -17,6 +18,8 @@ from .exceptions import EndpointHandlerNotFound +_logger = logging.getLogger(__name__) + def query_multi_update(cr, table_name, rows, cols): """Update multiple rows at once. @@ -86,6 +89,7 @@ def registry_for(cls, cr): @classmethod def wipe_registry_for(cls, cr): cr.execute("TRUNCATE endpoint_route") + _logger.info("endpoint_route wiped") @classmethod def _setup_db(cls, cr): @@ -93,6 +97,7 @@ def _setup_db(cls, cr): cls._setup_db_table(cr) cls._setup_db_timestamp(cr) cls._setup_db_version(cr) + _logger.info("endpoint_route table set up") @classmethod def _setup_db_table(cls, cr): From a2d28ecbeee4660a3d54c7fa1013b6159f7a96fc Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 8 Mar 2023 12:29:01 +0100 Subject: [PATCH 25/91] endpoint_route_handler: fail gracefully when sync field not ready Depending on your modules inheritance and upgrade order when you introduce this mixin on an existing model it might happen that gets called before the model's table is ready (eg: another odoo service loading the env before the upgrade happens). Let if fail gracefully since the hook will be called again later. --- .../models/endpoint_route_sync_mixin.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/endpoint_route_handler/models/endpoint_route_sync_mixin.py b/endpoint_route_handler/models/endpoint_route_sync_mixin.py index e91dd511..328e4471 100644 --- a/endpoint_route_handler/models/endpoint_route_sync_mixin.py +++ b/endpoint_route_handler/models/endpoint_route_sync_mixin.py @@ -5,7 +5,7 @@ import logging from functools import partial -from odoo import api, fields, models +from odoo import api, fields, models, tools from ..registry import EndpointRegistry @@ -82,6 +82,8 @@ def _endpoint_registry(self): def _register_hook(self): super()._register_hook() if not self._abstract and not self._transient: + if not is_registry_sync_column_ready(self.env.cr, self._table): + return # Ensure existing active records are loaded at startup. # Pass `init` to bypass routing map refresh # since this piece of code runs only when the model is loaded. @@ -120,3 +122,20 @@ def _prepare_endpoint_rules(self, options=None): def _registered_endpoint_rule_keys(self): """Return list of registered `EndpointRule` unique keys for current record.""" raise NotImplementedError() + + +def is_registry_sync_column_ready(cr, table): + if tools.sql.column_exists(cr, table, "registry_sync"): + return True + # Depending on your modules inheritance and upgrade order + # when you introduce this mixin on an existing model + # it might happen that `_register_hook` + # gets called before the model's table is ready + # (eg: another odoo service loading the env before the upgrade happens). + # Let if fail gracefully since the hook will be called again later. + _logger.warning( + "Column %s.registry_sync is not ready yet. " + "Controllers registration skipped.", + table, + ) + return False From f8e1b8c9463af361021d77170a05676100a11f90 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 27 Mar 2023 15:10:45 +0200 Subject: [PATCH 26/91] endpoint_route_handler: get rid of register_hook As routes are registered automatically in the db after sync there's no reason to look for non registered routes at boot. Furthermore, this is causing access conflicts on the table when multiple instances w/ multiple workers are spawned. --- .../models/endpoint_route_sync_mixin.py | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/endpoint_route_handler/models/endpoint_route_sync_mixin.py b/endpoint_route_handler/models/endpoint_route_sync_mixin.py index 328e4471..9e03c48b 100644 --- a/endpoint_route_handler/models/endpoint_route_sync_mixin.py +++ b/endpoint_route_handler/models/endpoint_route_sync_mixin.py @@ -5,7 +5,7 @@ import logging from functools import partial -from odoo import api, fields, models, tools +from odoo import api, fields, models from ..registry import EndpointRegistry @@ -79,17 +79,6 @@ def _handle_registry_sync_post_commit(self, record_ids=None): def _endpoint_registry(self): return EndpointRegistry.registry_for(self.env.cr) - def _register_hook(self): - super()._register_hook() - if not self._abstract and not self._transient: - if not is_registry_sync_column_ready(self.env.cr, self._table): - return - # Ensure existing active records are loaded at startup. - # Pass `init` to bypass routing map refresh - # since this piece of code runs only when the model is loaded. - domain = [("active", "=", True), ("registry_sync", "=", True)] - self.search(domain)._register_controllers(init=True) - def unlink(self): if not self._abstract: self._unregister_controllers() @@ -122,20 +111,3 @@ def _prepare_endpoint_rules(self, options=None): def _registered_endpoint_rule_keys(self): """Return list of registered `EndpointRule` unique keys for current record.""" raise NotImplementedError() - - -def is_registry_sync_column_ready(cr, table): - if tools.sql.column_exists(cr, table, "registry_sync"): - return True - # Depending on your modules inheritance and upgrade order - # when you introduce this mixin on an existing model - # it might happen that `_register_hook` - # gets called before the model's table is ready - # (eg: another odoo service loading the env before the upgrade happens). - # Let if fail gracefully since the hook will be called again later. - _logger.warning( - "Column %s.registry_sync is not ready yet. " - "Controllers registration skipped.", - table, - ) - return False From 0ba76e1586c002e4ebc76fe5572902adfcbbafa3 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 21 Apr 2023 11:57:59 +0200 Subject: [PATCH 27/91] endpoint_route_handler: fix auto_not_found param --- endpoint_route_handler/models/endpoint_route_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index 78061d6c..38fd46d3 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -233,6 +233,7 @@ def _default_endpoint_options_handler(self): return { "klass_dotted_path": f"{base_path}.EndpointNotFoundController", "method_name": "auto_not_found", + "default_pargs": (self.route,), } def _get_routing_info(self): From 94014238dace8eb21587b37124d5819b1c7c6a62 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 11 May 2023 17:36:37 +0200 Subject: [PATCH 28/91] endpoint_route_handler: 14.0.2.0.0 --- endpoint_route_handler/__manifest__.py | 2 +- .../migrations/{14.0.1.2.0 => 14.0.2.0.0}/pre-migrate.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename endpoint_route_handler/migrations/{14.0.1.2.0 => 14.0.2.0.0}/pre-migrate.py (100%) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index 4e7be3c5..c962de00 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Endpoint route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "version": "14.0.1.2.0", + "version": "14.0.2.0.0", "license": "LGPL-3", "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", diff --git a/endpoint_route_handler/migrations/14.0.1.2.0/pre-migrate.py b/endpoint_route_handler/migrations/14.0.2.0.0/pre-migrate.py similarity index 100% rename from endpoint_route_handler/migrations/14.0.1.2.0/pre-migrate.py rename to endpoint_route_handler/migrations/14.0.2.0.0/pre-migrate.py From 7cdb0562a7c648aa3e96f6659ac74a74274ed7dc Mon Sep 17 00:00:00 2001 From: oca-ci Date: Thu, 11 May 2023 16:27:59 +0000 Subject: [PATCH 29/91] [UPD] Update endpoint_route_handler.pot --- .../i18n/endpoint_route_handler.pot | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/endpoint_route_handler/i18n/endpoint_route_handler.pot b/endpoint_route_handler/i18n/endpoint_route_handler.pot index ef162fd0..de1733d7 100644 --- a/endpoint_route_handler/i18n/endpoint_route_handler.pot +++ b/endpoint_route_handler/i18n/endpoint_route_handler.pot @@ -15,27 +15,44 @@ msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__active +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__active +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_sync_mixin__active msgid "Active" msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__auth_type +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__auth_type msgid "Auth Type" msgstr "" +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__create_uid +msgid "Created by" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__create_date +msgid "Created on" +msgstr "" + #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__csrf +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__csrf msgid "Csrf" msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__display_name +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__display_name +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_sync_mixin__display_name #: model:ir.model.fields,field_description:endpoint_route_handler.field_ir_http__display_name msgid "Display Name" msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__endpoint_hash +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__endpoint_hash msgid "Endpoint Hash" msgstr "" @@ -44,6 +61,16 @@ msgstr "" msgid "Endpoint Route handler" msgstr "" +#. module: endpoint_route_handler +#: model:ir.model,name:endpoint_route_handler.model_endpoint_route_handler_tool +msgid "Endpoint Route handler tool" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model,name:endpoint_route_handler.model_endpoint_route_sync_mixin +msgid "Endpoint Route sync mixin" +msgstr "" + #. module: endpoint_route_handler #: model:ir.model,name:endpoint_route_handler.model_ir_http msgid "HTTP Routing" @@ -51,23 +78,39 @@ msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__id +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__id +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_sync_mixin__id #: model:ir.model.fields,field_description:endpoint_route_handler.field_ir_http__id msgid "ID" msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler__endpoint_hash +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler_tool__endpoint_hash msgid "Identify the route with its main params" msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler____last_update +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool____last_update +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_sync_mixin____last_update #: model:ir.model.fields,field_description:endpoint_route_handler.field_ir_http____last_update msgid "Last Modified on" msgstr "" +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__write_date +msgid "Last Updated on" +msgstr "" + #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__name +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__name msgid "Name" msgstr "" @@ -79,39 +122,63 @@ msgid "" "Found in model(s): %(models)s.\n" msgstr "" +#. module: endpoint_route_handler +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler__registry_sync +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler_tool__registry_sync +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_sync_mixin__registry_sync +msgid "" +"ON: the record has been modified and registry was not notified.\n" +"No change will be active until this flag is set to false via proper action.\n" +"\n" +"OFF: record in line with the registry, nothing to do." +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__registry_sync +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__registry_sync +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_sync_mixin__registry_sync +msgid "Registry Sync" +msgstr "" + #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__request_content_type +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__request_content_type msgid "Request Content Type" msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__request_method +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__request_method msgid "Request Method" msgstr "" #. module: endpoint_route_handler #: code:addons/endpoint_route_handler/models/endpoint_route_handler.py:0 #, python-format -msgid "Request method is required for POST and PUT." +msgid "Request content type is required for POST and PUT." msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__route +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__route msgid "Route" msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__route_group +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__route_group msgid "Route Group" msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__route_type +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__route_type msgid "Route Type" msgstr "" #. module: endpoint_route_handler #: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler__route_group +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler_tool__route_group msgid "Use this to classify routes together" msgstr "" @@ -119,6 +186,7 @@ msgstr "" #: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_endpoint_endpoint_route_unique #: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_mixin_endpoint_route_unique #: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_route_handler_endpoint_route_unique +#: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_route_handler_tool_endpoint_route_unique msgid "You can register an endpoint route only once." msgstr "" From 2b2ca16dc259dc77cbd529345dcf5c103f3779d1 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 11 May 2023 16:30:09 +0000 Subject: [PATCH 30/91] [UPD] README.rst --- endpoint_route_handler/README.rst | 35 +++++++++++++++---- .../static/description/index.html | 34 ++++++++++++++---- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/endpoint_route_handler/README.rst b/endpoint_route_handler/README.rst index 65fdc237..fae93fa2 100644 --- a/endpoint_route_handler/README.rst +++ b/endpoint_route_handler/README.rst @@ -1,6 +1,6 @@ -==================== - Route route handler -==================== +====================== +Endpoint route handler +====================== .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! @@ -47,6 +47,20 @@ Use standard Odoo inheritance:: Once you have this, each `my.model` record will generate a route. You can have a look at the `endpoint` module to see a real life example. +The options of the routing rules are defined by the method `_default_endpoint_options`. +Here's an example from the `endpoint` module:: + + def _default_endpoint_options_handler(self): + return { + "klass_dotted_path": "odoo.addons.endpoint.controllers.main.EndpointController", + "method_name": "auto_endpoint", + "default_pargs": (self.route,), + } + +As you can see, you have to pass the references to the controller class and the method to use +when the endpoint is called. And you can prepare some default arguments to pass. +In this case, the route of the current record. + As a tool ~~~~~~~~~ @@ -54,7 +68,7 @@ As a tool Initialize non stored route handlers and generate routes from them. For instance:: - route_handler = self.env["endpoint.route.handler"] + route_handler = self.env["endpoint.route.handler.tool"] endpoint_handler = MyController()._my_handler vals = { "name": "My custom route", @@ -63,8 +77,17 @@ For instance:: "auth_type": "public", } new_route = route_handler.new(vals) - new_route._refresh_endpoint_data() # required only for NewId records - new_route._register_controller(endpoint_handler=endpoint_handler, key="my-custom-route") + new_route._register_controller() + +You can override options and define - for instance - a different controller method:: + + options = { + "handler": { + "klass_dotted_path": "odoo.addons.my_module.controllers.SpecialController", + "method_name": "my_special_handler", + } + } + new_route._register_controller(options=options) Of course, what happens when the endpoint gets called depends on the logic defined on the controller method. diff --git a/endpoint_route_handler/static/description/index.html b/endpoint_route_handler/static/description/index.html index cade7b27..c2a2b5d9 100644 --- a/endpoint_route_handler/static/description/index.html +++ b/endpoint_route_handler/static/description/index.html @@ -4,7 +4,7 @@ -Route route handler +Endpoint route handler -
-

Route route handler

+
+

Endpoint route handler

-

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

+

Beta License: LGPL-3 OCA/web-api Translate me on Weblate Try me on Runboat

Technical module that provides a base handler for adding and removing controller routes on the fly.

Can be used as a mixin or as a tool.

Table of contents

-

Usage

+

Usage

-

As a mixin

+

As a mixin

Use standard Odoo inheritance:

 class MyModel(models.Model):
@@ -416,7 +418,7 @@ 

As a mixin

In this case, the route of the current record.

-

As a tool

+

As a tool

Initialize non stored route handlers and generate routes from them. For instance:

@@ -449,7 +451,7 @@ 

As a tool

-

Known issues / Roadmap

+

Known issues / Roadmap

  • add api docs helpers

  • @@ -475,36 +477,36 @@

    Known issues / Roadmap

    so the lookup of the controller can be simplified to a basic py obj that holds the routing info.

-

Bug Tracker

+

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 +If you spotted it first, help us to smash it by providing a detailed and welcomed 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.

Current maintainer:

-

simahawk

+

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 f40da31da954a65faccc88bb1f8918552a8f9d8b Mon Sep 17 00:00:00 2001 From: mymage Date: Thu, 29 Feb 2024 12:18:30 +0000 Subject: [PATCH 35/91] Added translation using Weblate (Italian) --- endpoint_route_handler/i18n/it.po | 198 ++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 endpoint_route_handler/i18n/it.po diff --git a/endpoint_route_handler/i18n/it.po b/endpoint_route_handler/i18n/it.po new file mode 100644 index 00000000..c54aa102 --- /dev/null +++ b/endpoint_route_handler/i18n/it.po @@ -0,0 +1,198 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * endpoint_route_handler +# +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: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__active +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__active +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_sync_mixin__active +msgid "Active" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__auth_type +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__auth_type +msgid "Auth Type" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__create_uid +msgid "Created by" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__create_date +msgid "Created on" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__csrf +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__csrf +msgid "Csrf" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__display_name +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__display_name +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_sync_mixin__display_name +#: model:ir.model.fields,field_description:endpoint_route_handler.field_ir_http__display_name +msgid "Display Name" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__endpoint_hash +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__endpoint_hash +msgid "Endpoint Hash" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model,name:endpoint_route_handler.model_endpoint_route_handler +msgid "Endpoint Route handler" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model,name:endpoint_route_handler.model_endpoint_route_handler_tool +msgid "Endpoint Route handler tool" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model,name:endpoint_route_handler.model_endpoint_route_sync_mixin +msgid "Endpoint Route sync mixin" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model,name:endpoint_route_handler.model_ir_http +msgid "HTTP Routing" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__id +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__id +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_sync_mixin__id +#: model:ir.model.fields,field_description:endpoint_route_handler.field_ir_http__id +msgid "ID" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler__endpoint_hash +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler_tool__endpoint_hash +msgid "Identify the route with its main params" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler____last_update +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool____last_update +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_sync_mixin____last_update +#: model:ir.model.fields,field_description:endpoint_route_handler.field_ir_http____last_update +msgid "Last Modified on" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__write_date +msgid "Last Updated on" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__name +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__name +msgid "Name" +msgstr "" + +#. module: endpoint_route_handler +#: code:addons/endpoint_route_handler/models/endpoint_route_handler.py:0 +#, python-format +msgid "" +"Non unique route(s): %(routes)s.\n" +"Found in model(s): %(models)s.\n" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler__registry_sync +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler_tool__registry_sync +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_sync_mixin__registry_sync +msgid "" +"ON: the record has been modified and registry was not notified.\n" +"No change will be active until this flag is set to false via proper action.\n" +"\n" +"OFF: record in line with the registry, nothing to do." +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__registry_sync +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__registry_sync +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_sync_mixin__registry_sync +msgid "Registry Sync" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__request_content_type +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__request_content_type +msgid "Request Content Type" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__request_method +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__request_method +msgid "Request Method" +msgstr "" + +#. module: endpoint_route_handler +#: code:addons/endpoint_route_handler/models/endpoint_route_handler.py:0 +#, python-format +msgid "Request content type is required for POST and PUT." +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__route +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__route +msgid "Route" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__route_group +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__route_group +msgid "Route Group" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler__route_type +#: model:ir.model.fields,field_description:endpoint_route_handler.field_endpoint_route_handler_tool__route_type +msgid "Route Type" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler__route_group +#: model:ir.model.fields,help:endpoint_route_handler.field_endpoint_route_handler_tool__route_group +msgid "Use this to classify routes together" +msgstr "" + +#. module: endpoint_route_handler +#: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_endpoint_endpoint_route_unique +#: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_mixin_endpoint_route_unique +#: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_route_handler_endpoint_route_unique +#: model:ir.model.constraint,message:endpoint_route_handler.constraint_endpoint_route_handler_tool_endpoint_route_unique +msgid "You can register an endpoint route only once." +msgstr "" + +#. module: endpoint_route_handler +#: code:addons/endpoint_route_handler/models/endpoint_route_handler.py:0 +#, python-format +msgid "`%s` uses a blacklisted routed = `%s`" +msgstr "" From b55c972c8a937d716c93ea65b962491005aace1e Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Mon, 4 Mar 2024 18:20:43 +0100 Subject: [PATCH 36/91] [FIX] endpoint_route_handler: Use dedicated cursor for registry When the request cursor is used to instantiate the EndpointRegistry in the call to routing_map, the READ REPEATABLE isolation level will ensure that any value read from the DB afterwards, will be the same than when the first SELECT is executed. This is breaking the oauth flow as the oauth token that is written at the beggining of the oauth process cannot be read by the cursor computing the session token, which will read an old value. Therefore when the session security check is performed, the session token is outdated as the new session token is computed using an up to date cursor. By using a dedicated cursor to instantiate the EndpointRegistry, we ensure no read is performed on the database using the request cursor which will in turn use the updated value of the oauth token to compute the session token, and the security check will not fail. --- endpoint_route_handler/models/ir_http.py | 51 +++++++++++++++++------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/endpoint_route_handler/models/ir_http.py b/endpoint_route_handler/models/ir_http.py index e684f0d2..0dee55cc 100644 --- a/endpoint_route_handler/models/ir_http.py +++ b/endpoint_route_handler/models/ir_http.py @@ -7,7 +7,7 @@ import werkzeug -from odoo import http, models +from odoo import http, models, registry as registry_get from ..registry import EndpointRegistry @@ -18,8 +18,8 @@ class IrHttp(models.AbstractModel): _inherit = "ir.http" @classmethod - def _endpoint_route_registry(cls, env): - return EndpointRegistry.registry_for(env.cr) + def _endpoint_route_registry(cls, cr): + return EndpointRegistry.registry_for(cr) @classmethod def _generate_routing_rules(cls, modules, converters): @@ -32,7 +32,7 @@ def _generate_routing_rules(cls, modules, converters): @classmethod def _endpoint_routing_rules(cls): """Yield custom endpoint rules""" - e_registry = cls._endpoint_route_registry(http.request.env) + e_registry = cls._endpoint_route_registry(http.request.env.cr) for endpoint_rule in e_registry.get_rules(): _logger.debug("LOADING %s", endpoint_rule) endpoint = endpoint_rule.endpoint @@ -41,20 +41,41 @@ def _endpoint_routing_rules(cls): @classmethod def routing_map(cls, key=None): - last_version = cls._get_routing_map_last_version(http.request.env) - if not hasattr(cls, "_routing_map"): - # routing map just initialized, store last update for this env - cls._endpoint_route_last_version = last_version - elif cls._endpoint_route_last_version < last_version: - _logger.info("Endpoint registry updated, reset routing map") - cls._routing_map = {} - cls._rewrite_len = {} - cls._endpoint_route_last_version = last_version + # When the request cursor is used to instantiate the EndpointRegistry + # in the call to routing_map, the READ REPEATABLE isolation level + # will ensure that any value read from the DB afterwards, will be the + # same than when the first SELECT is executed. + # + # This is breaking the oauth flow as the oauth token that is written + # at the beggining of the oauth process cannot be read by the cursor + # computing the session token, which will read an old value. Therefore + # when the session security check is performed, the session token + # is outdated as the new session token is computed using an up to date + # cursor. + # + # By using a dedicated cursor to instantiate the EndpointRegistry, we + # ensure no read is performed on the database using the request cursor + # which will in turn use the updated value of the oauth token to compute + # the session token, and the security check will not fail. + registry = registry_get(http.request.env.cr.dbname) + with registry.cursor() as cr: + last_version = cls._get_routing_map_last_version(cr) + if not hasattr(cls, "_routing_map"): + _logger.debug( + "routing map just initialized, store last update for this env" + ) + # routing map just initialized, store last update for this env + cls._endpoint_route_last_version = last_version + elif cls._endpoint_route_last_version < last_version: + _logger.info("Endpoint registry updated, reset routing map") + cls._routing_map = {} + cls._rewrite_len = {} + cls._endpoint_route_last_version = last_version return super().routing_map(key=key) @classmethod - def _get_routing_map_last_version(cls, env): - return cls._endpoint_route_registry(env).last_version() + def _get_routing_map_last_version(cls, cr): + return cls._endpoint_route_registry(cr).last_version() @classmethod def _clear_routing_map(cls): From ca32a7817263042f01707f7ba5bfda2d1c164cfa Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 4 Jul 2024 07:13:41 +0000 Subject: [PATCH 37/91] [BOT] post-merge updates --- endpoint_route_handler/README.rst | 2 +- endpoint_route_handler/__manifest__.py | 2 +- endpoint_route_handler/static/description/index.html | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/endpoint_route_handler/README.rst b/endpoint_route_handler/README.rst index c8a7962a..d819641d 100644 --- a/endpoint_route_handler/README.rst +++ b/endpoint_route_handler/README.rst @@ -7,7 +7,7 @@ Endpoint route handler !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:b67dfaf93ba5b7b697405ae1415b6b559fc25eb02541ca224cf5bf57e635f725 + !! source digest: sha256:28f206335b37233ef5bc9d2991f373c1e8575aaad1ab576f13f78693fbb868b9 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index 3a0b14f8..0ab42b01 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Endpoint route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "version": "14.0.2.0.1", + "version": "14.0.2.0.2", "license": "LGPL-3", "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", diff --git a/endpoint_route_handler/static/description/index.html b/endpoint_route_handler/static/description/index.html index f5b4c2d1..cac166c5 100644 --- a/endpoint_route_handler/static/description/index.html +++ b/endpoint_route_handler/static/description/index.html @@ -1,4 +1,3 @@ - @@ -367,7 +366,7 @@

Endpoint route handler

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:b67dfaf93ba5b7b697405ae1415b6b559fc25eb02541ca224cf5bf57e635f725 +!! source digest: sha256:28f206335b37233ef5bc9d2991f373c1e8575aaad1ab576f13f78693fbb868b9 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: LGPL-3 OCA/web-api Translate me on Weblate Try me on Runboat

Technical module that provides a base handler From 7eeaa87c0a51de4fc8dd2cf02fa78cf4177476fc Mon Sep 17 00:00:00 2001 From: OriolMForgeFlow Date: Mon, 17 Jul 2023 13:02:38 +0200 Subject: [PATCH 38/91] [IMP] endpoint_route_handler: add request_content_type application_json_utf8 --- endpoint_route_handler/models/endpoint_route_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index 38fd46d3..20a149b7 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -129,6 +129,7 @@ def _selection_request_content_type(self): ("application/json", "JSON"), ("application/xml", "XML"), ("application/x-www-form-urlencoded", "Form"), + ("application/json; charset=utf-8", "JSON_UTF8 (Deprecated)"), ] @api.depends(lambda self: self._routing_impacting_fields()) From 3d625641574b5f8e6c3d46ac04c0eabbb4e65b24 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 8 Jul 2024 08:58:38 +0000 Subject: [PATCH 39/91] [BOT] post-merge updates --- endpoint_route_handler/README.rst | 2 +- endpoint_route_handler/__manifest__.py | 2 +- .../static/description/index.html | 13 ++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/endpoint_route_handler/README.rst b/endpoint_route_handler/README.rst index d819641d..abc7ddab 100644 --- a/endpoint_route_handler/README.rst +++ b/endpoint_route_handler/README.rst @@ -7,7 +7,7 @@ Endpoint route handler !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:28f206335b37233ef5bc9d2991f373c1e8575aaad1ab576f13f78693fbb868b9 + !! source digest: sha256:441f31f154913353d8a88d37067308bd8f9ec293c07b4ced67c52fd3cf82b5e6 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index 0ab42b01..c50b186f 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Endpoint route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "version": "14.0.2.0.2", + "version": "14.0.2.1.0", "license": "LGPL-3", "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", diff --git a/endpoint_route_handler/static/description/index.html b/endpoint_route_handler/static/description/index.html index cac166c5..c9bd50a4 100644 --- a/endpoint_route_handler/static/description/index.html +++ b/endpoint_route_handler/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -366,7 +367,7 @@

Endpoint route handler

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:28f206335b37233ef5bc9d2991f373c1e8575aaad1ab576f13f78693fbb868b9 +!! source digest: sha256:441f31f154913353d8a88d37067308bd8f9ec293c07b4ced67c52fd3cf82b5e6 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: LGPL-3 OCA/web-api Translate me on Weblate Try me on Runboat

Technical module that provides a base handler @@ -500,7 +501,9 @@

Contributors

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +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.

From d4949db583383626fd55826a7fa2eb20ef7f28f8 Mon Sep 17 00:00:00 2001 From: GuillemCForgeFlow Date: Thu, 7 Nov 2024 15:36:57 +0100 Subject: [PATCH 40/91] [IMP]endpoint_route_handler: black, isort, prettier --- endpoint_route_handler/__manifest__.py | 4 +--- .../migrations/14.0.2.0.0/pre-migrate.py | 2 +- endpoint_route_handler/registry.py | 12 ++-------- endpoint_route_handler/tests/common.py | 5 +---- endpoint_route_handler/tests/test_registry.py | 22 ++++--------------- 5 files changed, 9 insertions(+), 36 deletions(-) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index c50b186f..91758828 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -10,8 +10,6 @@ "author": "Camptocamp,Odoo Community Association (OCA)", "maintainers": ["simahawk"], "website": "https://github.com/OCA/web-api", - "data": [ - "security/ir.model.access.csv", - ], + "data": ["security/ir.model.access.csv",], "post_init_hook": "post_init_hook", } diff --git a/endpoint_route_handler/migrations/14.0.2.0.0/pre-migrate.py b/endpoint_route_handler/migrations/14.0.2.0.0/pre-migrate.py index 4e5e3b0e..40f781da 100644 --- a/endpoint_route_handler/migrations/14.0.2.0.0/pre-migrate.py +++ b/endpoint_route_handler/migrations/14.0.2.0.0/pre-migrate.py @@ -5,7 +5,7 @@ # fmt: off from odoo.addons.endpoint_route_handler.registry import ( - EndpointRegistry, # pylint: disable=odoo-addons-relative-import + EndpointRegistry # pylint: disable=odoo-addons-relative-import, ) # fmt: on diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 45b405e0..57b4d006 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -53,10 +53,7 @@ def query_multi_update(cr, table_name, rows, cols): values=sql.Placeholder(), ) execute_values( - cr, - query.as_string(cr._cnx), - rows, - template=template.as_string(cr._cnx), + cr, query.as_string(cr._cnx), rows, template=template.as_string(cr._cnx), ) @@ -104,12 +101,7 @@ def _setup_db_table(cls, cr): """Create routing table and indexes""" tools.sql.create_model_table(cr, cls._table, columns=cls._columns) tools.sql.create_unique_index( - cr, - "endpoint_route__key_uniq", - cls._table, - [ - "key", - ], + cr, "endpoint_route__key_uniq", cls._table, ["key",], ) tools.sql.add_constraint( cr, diff --git a/endpoint_route_handler/tests/common.py b/endpoint_route_handler/tests/common.py index b7f3d4f1..9e2681ec 100644 --- a/endpoint_route_handler/tests/common.py +++ b/endpoint_route_handler/tests/common.py @@ -25,10 +25,7 @@ def _setup_env(cls): @classmethod def _setup_context(cls): - return dict( - cls.env.context, - tracking_disable=True, - ) + return dict(cls.env.context, tracking_disable=True,) @classmethod def _setup_records(cls): diff --git a/endpoint_route_handler/tests/test_registry.py b/endpoint_route_handler/tests/test_registry.py index 04015211..4664c428 100644 --- a/endpoint_route_handler/tests/test_registry.py +++ b/endpoint_route_handler/tests/test_registry.py @@ -47,10 +47,7 @@ def test_last_update(self): last_update0 = self.reg.last_update() self.assertTrue(last_update0 > 0) rule1.options = { - "handler": { - "klass_dotted_path": CTRLFake._path, - "method_name": "handler2", - } + "handler": {"klass_dotted_path": CTRLFake._path, "method_name": "handler2",} } # FIXME: to test timestamp we have to mock psql datetime. # self.reg.update_rules([rule1]) @@ -87,12 +84,7 @@ def _make_rules(self, stop=5, start=1, **kw): endpoint_hash = i route_group = "test_route_handler" rule = self.reg.make_rule( - key, - route, - options, - routing, - endpoint_hash, - route_group=route_group, + key, route, options, routing, endpoint_hash, route_group=route_group, ) for k, v in kw.items(): setattr(rule, k, v) @@ -141,16 +133,10 @@ def test_update_rule(self): self.reg._get_rule("route2").handler_options.method_name, "handler1" ) rule1.options = { - "handler": { - "klass_dotted_path": CTRLFake._path, - "method_name": "handler2", - } + "handler": {"klass_dotted_path": CTRLFake._path, "method_name": "handler2",} } rule2.options = { - "handler": { - "klass_dotted_path": CTRLFake._path, - "method_name": "handler3", - } + "handler": {"klass_dotted_path": CTRLFake._path, "method_name": "handler3",} } self.reg.update_rules([rule1, rule2]) self.assertEqual( From 604236de7972c1a3e6fbee4aaabcde0d7f31e27c Mon Sep 17 00:00:00 2001 From: GuillemCForgeFlow Date: Tue, 29 Oct 2024 09:22:13 +0100 Subject: [PATCH 41/91] [BKP][ADD]endpoint_route_handler Backport from `web-api` repo v14: https://github.com/OCA/web-api/tree/14.0/endpoint_route_handler --- endpoint_route_handler/__manifest__.py | 4 ++-- .../migrations/14.0.2.0.0/pre-migrate.py | 20 ------------------- .../models/endpoint_route_sync_mixin.py | 4 ++-- .../readme/CONTRIBUTORS.rst | 1 + endpoint_route_handler/registry.py | 7 +++++-- endpoint_route_handler/tests/test_registry.py | 6 +++--- 6 files changed, 13 insertions(+), 29 deletions(-) delete mode 100644 endpoint_route_handler/migrations/14.0.2.0.0/pre-migrate.py diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index 91758828..f9b75e83 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -4,12 +4,12 @@ { "name": "Endpoint route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "version": "14.0.2.1.0", + "version": "13.0.1.0.0", "license": "LGPL-3", "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", "maintainers": ["simahawk"], "website": "https://github.com/OCA/web-api", - "data": ["security/ir.model.access.csv",], + "data": ["security/ir.model.access.csv"], "post_init_hook": "post_init_hook", } diff --git a/endpoint_route_handler/migrations/14.0.2.0.0/pre-migrate.py b/endpoint_route_handler/migrations/14.0.2.0.0/pre-migrate.py deleted file mode 100644 index 40f781da..00000000 --- a/endpoint_route_handler/migrations/14.0.2.0.0/pre-migrate.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2022 Camptocamp SA (http://www.camptocamp.com) -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - -import logging - -# fmt: off -from odoo.addons.endpoint_route_handler.registry import ( - EndpointRegistry # pylint: disable=odoo-addons-relative-import, -) - -# fmt: on - -_logger = logging.getLogger(__name__) - - -def migrate(cr, version): - if not version: - return - - EndpointRegistry._setup_db(cr) diff --git a/endpoint_route_handler/models/endpoint_route_sync_mixin.py b/endpoint_route_handler/models/endpoint_route_sync_mixin.py index 9e03c48b..01a997c6 100644 --- a/endpoint_route_handler/models/endpoint_route_sync_mixin.py +++ b/endpoint_route_handler/models/endpoint_route_sync_mixin.py @@ -53,8 +53,8 @@ def write(self, vals): @api.model def _add_after_commit_hook(self, record_ids): - self.env.cr.postcommit.add( - partial(self._handle_registry_sync_post_commit, record_ids), + self.env.cr.after( + "commit", partial(self._handle_registry_sync_post_commit, record_ids), ) def _handle_registry_sync(self, record_ids=None): diff --git a/endpoint_route_handler/readme/CONTRIBUTORS.rst b/endpoint_route_handler/readme/CONTRIBUTORS.rst index f1c71bce..37423d21 100644 --- a/endpoint_route_handler/readme/CONTRIBUTORS.rst +++ b/endpoint_route_handler/readme/CONTRIBUTORS.rst @@ -1 +1,2 @@ * Simone Orsi +* Guillem Casassas diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 57b4d006..5096f2bf 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -68,6 +68,7 @@ class EndpointRegistry: __slots__ = "cr" _table = "endpoint_route" + # pylint: disable=W8105 _columns = ( # name, type, comment ("key", "VARCHAR", ""), @@ -99,9 +100,11 @@ def _setup_db(cls, cr): @classmethod def _setup_db_table(cls, cr): """Create routing table and indexes""" - tools.sql.create_model_table(cr, cls._table, columns=cls._columns) + tools.sql.create_model_table(cr, cls._table) + for name, sql_type, _comment in cls._columns: + tools.sql.create_column(cr, cls._table, name, sql_type, _comment) tools.sql.create_unique_index( - cr, "endpoint_route__key_uniq", cls._table, ["key",], + cr, "endpoint_route__key_uniq", cls._table, ["key"], ) tools.sql.add_constraint( cr, diff --git a/endpoint_route_handler/tests/test_registry.py b/endpoint_route_handler/tests/test_registry.py index 4664c428..36cf5c20 100644 --- a/endpoint_route_handler/tests/test_registry.py +++ b/endpoint_route_handler/tests/test_registry.py @@ -47,7 +47,7 @@ def test_last_update(self): last_update0 = self.reg.last_update() self.assertTrue(last_update0 > 0) rule1.options = { - "handler": {"klass_dotted_path": CTRLFake._path, "method_name": "handler2",} + "handler": {"klass_dotted_path": CTRLFake._path, "method_name": "handler2"} } # FIXME: to test timestamp we have to mock psql datetime. # self.reg.update_rules([rule1]) @@ -133,10 +133,10 @@ def test_update_rule(self): self.reg._get_rule("route2").handler_options.method_name, "handler1" ) rule1.options = { - "handler": {"klass_dotted_path": CTRLFake._path, "method_name": "handler2",} + "handler": {"klass_dotted_path": CTRLFake._path, "method_name": "handler2"} } rule2.options = { - "handler": {"klass_dotted_path": CTRLFake._path, "method_name": "handler3",} + "handler": {"klass_dotted_path": CTRLFake._path, "method_name": "handler3"} } self.reg.update_rules([rule1, rule2]) self.assertEqual( From bb4b98bdc7aaf3fb50b21f586c3cf300f866455e Mon Sep 17 00:00:00 2001 From: JordiMForgeFlow Date: Fri, 22 Nov 2024 11:35:31 +0100 Subject: [PATCH 42/91] [IMP] endpoint_route_handler: pre-commit stuff --- .../odoo/addons/endpoint_route_handler | 1 + setup/endpoint_route_handler/setup.py | 6 ++++++ 2 files changed, 7 insertions(+) create mode 120000 setup/endpoint_route_handler/odoo/addons/endpoint_route_handler create mode 100644 setup/endpoint_route_handler/setup.py diff --git a/setup/endpoint_route_handler/odoo/addons/endpoint_route_handler b/setup/endpoint_route_handler/odoo/addons/endpoint_route_handler new file mode 120000 index 00000000..d67d70dd --- /dev/null +++ b/setup/endpoint_route_handler/odoo/addons/endpoint_route_handler @@ -0,0 +1 @@ +../../../../endpoint_route_handler \ No newline at end of file diff --git a/setup/endpoint_route_handler/setup.py b/setup/endpoint_route_handler/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/endpoint_route_handler/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 9f8fd39f98e4f5eecfd931e93ee15ff1142ab304 Mon Sep 17 00:00:00 2001 From: JordiMForgeFlow Date: Fri, 22 Nov 2024 11:36:21 +0100 Subject: [PATCH 43/91] [MIG] endpoint_route_handler: Migration to V12 --- endpoint_route_handler/__manifest__.py | 2 +- .../models/endpoint_route_handler.py | 22 +++--- .../models/endpoint_route_handler_tool.py | 4 +- endpoint_route_handler/models/ir_http.py | 72 ++++++++----------- endpoint_route_handler/registry.py | 22 ++++-- .../tests/fake_controllers.py | 9 ++- endpoint_route_handler/tests/test_registry.py | 4 +- 7 files changed, 70 insertions(+), 65 deletions(-) diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index f9b75e83..7da848aa 100644 --- a/endpoint_route_handler/__manifest__.py +++ b/endpoint_route_handler/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Endpoint route handler", "summary": """Provide mixin and tool to generate custom endpoints on the fly.""", - "version": "13.0.1.0.0", + "version": "12.0.1.0.0", "license": "LGPL-3", "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index 20a149b7..84b2b12f 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -22,7 +22,6 @@ class EndpointRouteHandler(models.AbstractModel): required=True, index=True, compute="_compute_route", - inverse="_inverse_route", readonly=False, store=True, copy=False, @@ -148,21 +147,27 @@ def _compute_endpoint_hash(self): def _routing_impacting_fields(self): return ("route", "auth_type", "request_method") - @api.depends("route") def _compute_route(self): for rec in self: rec.route = rec._clean_route() - def _inverse_route(self): + def write(self, vals): + if "route" not in vals: + return super().write(vals) + res = True for rec in self: - rec.route = rec._clean_route() + new_vals = vals.copy() + cleaned_route = rec._clean_route(vals["route"]) + new_vals["route"] = cleaned_route + res = res and super(EndpointRouteHandler, rec).write(new_vals) + return res # TODO: move to something better? Eg: computed field? # Shall we use the route_group? TBD! _endpoint_route_prefix = "" - def _clean_route(self): - route = (self.route or "").strip() + def _clean_route(self, route_to_write=False): + route = (route_to_write or self.route or "").strip() if not route.startswith("/"): route = "/" + route prefix = self._endpoint_route_prefix @@ -227,12 +232,13 @@ def _default_endpoint_options(self): def _default_endpoint_options_handler(self): self._logger.warning( - "No specific endpoint handler options defined for: %s, falling back to default", + "No specific endpoint handler options defined for: %s, " + "falling back to default", self._name, ) base_path = "odoo.addons.endpoint_route_handler.controllers.main" return { - "klass_dotted_path": f"{base_path}.EndpointNotFoundController", + "klass_dotted_path": "{}.EndpointNotFoundController".format(base_path), "method_name": "auto_not_found", "default_pargs": (self.route,), } diff --git a/endpoint_route_handler/models/endpoint_route_handler_tool.py b/endpoint_route_handler/models/endpoint_route_handler_tool.py index f699457f..a640a9cc 100644 --- a/endpoint_route_handler/models/endpoint_route_handler_tool.py +++ b/endpoint_route_handler/models/endpoint_route_handler_tool.py @@ -39,8 +39,8 @@ def _unregister_controllers(self): return super()._unregister_controllers() @api.model - def new(self, values=None, origin=None, ref=None): + def new(self, values=None, ref=None): values = values or {} # note: in core odoo they use `{}` as defaul arg :/ - res = super().new(values=values, origin=origin, ref=ref) + res = super().new(values=values, ref=ref) res._refresh_endpoint_data() return res diff --git a/endpoint_route_handler/models/ir_http.py b/endpoint_route_handler/models/ir_http.py index 0dee55cc..4a066242 100644 --- a/endpoint_route_handler/models/ir_http.py +++ b/endpoint_route_handler/models/ir_http.py @@ -3,11 +3,10 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import logging -from itertools import chain import werkzeug -from odoo import http, models, registry as registry_get +from odoo import http, models from ..registry import EndpointRegistry @@ -21,14 +20,6 @@ class IrHttp(models.AbstractModel): def _endpoint_route_registry(cls, cr): return EndpointRegistry.registry_for(cr) - @classmethod - def _generate_routing_rules(cls, modules, converters): - # Override to inject custom endpoint rules. - return chain( - super()._generate_routing_rules(modules, converters), - cls._endpoint_routing_rules(), - ) - @classmethod def _endpoint_routing_rules(cls): """Yield custom endpoint rules""" @@ -37,41 +28,36 @@ def _endpoint_routing_rules(cls): _logger.debug("LOADING %s", endpoint_rule) endpoint = endpoint_rule.endpoint for url in endpoint_rule.routing["routes"]: - yield (url, endpoint, endpoint_rule.routing) + yield url, endpoint, endpoint_rule.routing @classmethod - def routing_map(cls, key=None): - # When the request cursor is used to instantiate the EndpointRegistry - # in the call to routing_map, the READ REPEATABLE isolation level - # will ensure that any value read from the DB afterwards, will be the - # same than when the first SELECT is executed. - # - # This is breaking the oauth flow as the oauth token that is written - # at the beggining of the oauth process cannot be read by the cursor - # computing the session token, which will read an old value. Therefore - # when the session security check is performed, the session token - # is outdated as the new session token is computed using an up to date - # cursor. - # - # By using a dedicated cursor to instantiate the EndpointRegistry, we - # ensure no read is performed on the database using the request cursor - # which will in turn use the updated value of the oauth token to compute - # the session token, and the security check will not fail. - registry = registry_get(http.request.env.cr.dbname) - with registry.cursor() as cr: - last_version = cls._get_routing_map_last_version(cr) - if not hasattr(cls, "_routing_map"): - _logger.debug( - "routing map just initialized, store last update for this env" - ) - # routing map just initialized, store last update for this env - cls._endpoint_route_last_version = last_version - elif cls._endpoint_route_last_version < last_version: - _logger.info("Endpoint registry updated, reset routing map") - cls._routing_map = {} - cls._rewrite_len = {} - cls._endpoint_route_last_version = last_version - return super().routing_map(key=key) + def routing_map(cls): + last_version = cls._get_routing_map_last_version(http.request.env.cr) + if not hasattr(cls, "_routing_map"): + _logger.debug( + "routing map just initialized, store last update for this env" + ) + # routing map just initialized, store last update for this env + cls._endpoint_route_last_version = last_version + elif cls._endpoint_route_last_version < last_version: + _logger.info("Endpoint registry updated, reset routing map") + cls._endpoint_route_last_version = last_version + res = super().routing_map() + # Inject custom endpoint rules + for url, endpoint, routing in cls._endpoint_routing_rules(): + xtra_keys = ( + 'defaults subdomain build_only strict_slashes redirect_to alias host' + ).split() + kw = {k: routing[k] for k in xtra_keys if k in routing} + rule = werkzeug.routing.Rule( + url, + endpoint=endpoint, + methods=routing['methods'], + **kw + ) + rule.merge_slashes = False + res.add(rule) + return res @classmethod def _get_routing_map_last_version(cls, cr): diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 5096f2bf..cb534884 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -204,7 +204,8 @@ def _create(self, rows_mapping): return query_insert(self.cr, self._table, list(rows_mapping.values())) def get_rules_by_group(self, group): - rules = self.get_rules(where=f"WHERE route_group='{group}'") + where = "WHERE route_group='{group}'".format(group=group) + rules = self.get_rules(where=where) return rules def update_rules(self, rules, init=False): @@ -291,10 +292,15 @@ def __init__( def __repr__(self): # FIXME: use class name, remove key + route_group = self.route_group if self.route_group else "nogroup" return ( - f"<{self.__class__.__name__}: {self.key}" - + (f" #{self.route_group}" if self.route_group else "nogroup") + "<{class_name}: {key}" + + " #{route_group}" + ">" + ).format( + class_name=self.__class__.__name__, + key=self.key, + route_group=route_group ) @classmethod @@ -375,16 +381,20 @@ def _get_handler(self): try: mod = importlib.import_module(mod_path) except ImportError as exc: - raise EndpointHandlerNotFound(f"Module `{mod_path}` not found") from exc + raise EndpointHandlerNotFound( + "Module `{}` not found".format(mod_path) + ) from exc try: klass = getattr(mod, klass_name) except AttributeError as exc: - raise EndpointHandlerNotFound(f"Class `{klass_name}` not found") from exc + raise EndpointHandlerNotFound( + "Class `{}` not found".format(klass_name) + ) from exc method_name = self.handler_options.method_name try: method = getattr(klass(), method_name) except AttributeError as exc: raise EndpointHandlerNotFound( - f"Method name `{method_name}` not found" + "Method name `{}` not found".format(method_name) ) from exc return method diff --git a/endpoint_route_handler/tests/fake_controllers.py b/endpoint_route_handler/tests/fake_controllers.py index bd493973..4ecc51e5 100644 --- a/endpoint_route_handler/tests/fake_controllers.py +++ b/endpoint_route_handler/tests/fake_controllers.py @@ -16,14 +16,17 @@ def handler2(self, arg1, arg2=2): return arg1, arg2 def custom_handler(self, custom=None): - return f"Got: {custom}" + return "Got: {}".format(custom) class TestController(http.Controller): _path = "odoo.addons.endpoint_route_handler.tests.fake_controllers.TestController" def _do_something1(self, foo=None): - return f"Got: {foo}" + return "Got: {}".format(foo) def _do_something2(self, default_arg, foo=None): - return f"{default_arg} -> got: {foo}" + return "{default_arg} -> got: {foo}".format( + default_arg=default_arg, + foo=foo + ) diff --git a/endpoint_route_handler/tests/test_registry.py b/endpoint_route_handler/tests/test_registry.py index 36cf5c20..2524b37b 100644 --- a/endpoint_route_handler/tests/test_registry.py +++ b/endpoint_route_handler/tests/test_registry.py @@ -72,8 +72,8 @@ def test_last_version(self): def _make_rules(self, stop=5, start=1, **kw): res = [] for i in range(start, stop): - key = f"route{i}" - route = f"/test/{i}" + key = "route{}".format(i) + route = "/test/{}".format(i) options = { "handler": { "klass_dotted_path": CTRLFake._path, From 5fd4287334e33e0bca4d23707f593b1455cde5a1 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 13 Sep 2021 16:08:22 +0200 Subject: [PATCH 44/91] 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 8fbaee243a2daa4ad6d25a15f6f3cbb863556486 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 Oct 2021 13:52:00 +0200 Subject: [PATCH 45/91] 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 735995dac2495a49bba66140d5d30e54cd109804 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 25 Oct 2021 17:45:26 +0200 Subject: [PATCH 46/91] 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 47/91] [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 0aa3bb929fae56633fe366e9c8d0448b3a06625f Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 9 Nov 2021 18:47:33 +0000 Subject: [PATCH 48/91] 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 8477dcffe8315b2a36aa22c024541bc421f3389b Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 28 Dec 2021 14:12:26 +0100 Subject: [PATCH 49/91] 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 156cc6452b5f44cd755319297eb96040eafc182c Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 29 Dec 2021 15:06:57 +0100 Subject: [PATCH 50/91] 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 0a28f12610b38ffccb1fcb6bf18532aadac47f3f Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 12 Nov 2021 13:41:59 +0100 Subject: [PATCH 51/91] 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 4f8f1a5c973f3c62ae606541b824aced83662ad9 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 12 Jan 2022 07:24:29 +0000 Subject: [PATCH 52/91] 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 b4dd5689bdc6f915dd350794e5e66394c2954f74 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 12 Jan 2022 07:55:40 +0000 Subject: [PATCH 53/91] 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 bec80ece5e5144eb3f2fdf2456e07cb88a4ad74e Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 14 Jan 2022 07:36:41 +0100 Subject: [PATCH 54/91] 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 df2da5bcc30b40ba9ea6f7583fdbc99e47afcdde Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 14 Jan 2022 08:52:17 +0000 Subject: [PATCH 55/91] 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 0d4f2be7a0f3b764dc10866c4e0705ee387ef2de Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 18 Feb 2022 17:16:10 +0100 Subject: [PATCH 56/91] 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 619882230f9552fab6b3ec827655290e2794b27a Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 11 Mar 2022 18:08:34 +0000 Subject: [PATCH 57/91] 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 e114e17150a688c5cc12a389163ffee46a0ee5c7 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 15 Jun 2022 21:13:56 +0200 Subject: [PATCH 58/91] 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 9b704e44f515fb47113273ad5b279f579e297834 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 15 Jul 2022 12:38:23 +0000 Subject: [PATCH 59/91] [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 1b3fbf08697221cc2089750bf5db7a2f50cb2dd3 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 15 Jul 2022 12:40:04 +0000 Subject: [PATCH 60/91] [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 aa886683fd9ec8fb6c36a40bae361f3cdbb3780f Mon Sep 17 00:00:00 2001 From: Claude R Perrin Date: Thu, 15 Sep 2022 18:32:51 +0000 Subject: [PATCH 61/91] 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 7877a66303ef0f4dba758b53cf4f6b050df77700 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 15 Jun 2022 21:40:20 +0200 Subject: [PATCH 62/91] 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 010de38d8bdaa2ed0fb830183c95b247604e4319 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 25 Jul 2022 17:26:30 +0200 Subject: [PATCH 63/91] 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 @@
+ +