From 1484599d8750df9147e6a655b250fd58ff6570bc Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 25 Oct 2021 17:22:23 +0200 Subject: [PATCH 01/65] 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 zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z z?n?eq1XP>p-IM$Z^C;2L3itnbJZAip*Zo0aw2bs8@(s^~*8T9go!%dHcAz2lM;`yp zD=7&xjFV$S&5uDaiScyD?B-i1ze`+CoRtz`Wn+Zl&#s4&}MO{@N!ufrzjG$B79)Y2d3tBk&)TxUTw@QS0TEL_?njX|@vq?Uz(nBFK5Pq7*xj#u*R&i|?7+6# z+|r_n#SW&LXhtheZdah{ZVoqwyT{D>MC3nkFF#N)xLi{p7J1jXlmVeb;cP5?e(=f# zuT7fvjSbjS781v?7{)-X3*?>tq?)Yd)~|1{BDS(pqC zC}~H#WXlkUW*H5CDOo<)#x7%RY)A;ShGhI5s*#cRDA8YgqG(HeKDx+#(ZQ?386dv! zlXCO)w91~Vw4AmOcATuV653fa9R$fyK8ul%rG z-wfS zihugoZyr38Im?Zuh6@RcF~t1anQu7>#lPpb#}4cOA!EM11`%f*07RqOVkmX{p~KJ9 z^zP;K#|)$`^Rb{rnHGH{~>1(fawV0*Z#)}M`m8-?ZJV<+e}s9wE# z)l&az?w^5{)`S(%MRzxdNqrs1n*-=jS^_jqE*5XDrA0+VE`5^*p3CuM<&dZEeCjoz zR;uu_H9ZPZV|fQq`Cyw4nscrVwi!fE6ciMmX$!_hN7uF;jjKG)d2@aC4ropY)8etW=xJvni)8eHi`H$%#zn^WJ5NLc-rqk|u&&4Z6fD_m&JfSI1Bvb?b<*n&sfl0^t z=HnmRl`XrFvMKB%9}>PaA`m-fK6a0(8=qPkWS5bb4=v?XcWi&hRY?O5HdulRi4?fN zlsJ*N-0Qw+Yic@s0(2uy%F@ib;GjXt01Fmx5XbRo6+n|pP(&nodMoap^z{~q ziEeaUT@Mxe3vJSfI6?uLND(CNr=#^W<1b}jzW58bIfyWTDle$mmS(|x-0|2UlX+9k zQ^EX7Nw}?EzVoBfT(-LT|=9N@^hcn-_p&sqG z&*oVs2JSU+N4ZD`FhCAWaS;>|wH2G*Id|?pa#@>tyxX`+4HyIArWDvVrX)2WAOQff z0qyHu&-S@i^MS-+j--!pr4fPBj~_8({~e1bfcl0wI1kaoN>mJL6KUPQm5N7lB(ui1 zE-o%kq)&djzWJ}ob<-GfDlkB;F31j-VHKvQUGQ3sp`CwyGJk_i!y^sD0fqC@$9|jO zOqN!r!8-p==F@ZVP=U$qSpY(gQ0)59P1&t@y?5rvg<}E+GB}26NYPp4f2YFQrQtot5mn3wu_qprZ=>Ig-$ zbW26Ws~IgY>}^5w`vTB(G`PTZaDiGBo5o(tp)qli|NeV( z@H_=R8V39rt5J5YB2Ky?4eJJ#b`_iBe2ot~6%7mLt5t8Vwi^Jy7|jWXqa3amOIoRb zOr}WVFP--DsS`1WpN%~)t3R!arKF^Q$e12KEqU36AWwnCBICpH4XCsfnyrHr>$I$4 z!DpKX$OKLWarN7nv@!uIA+~RNO)l$$w}p(;b>mx8pwYvu;dD_unryX_NhT8*Tj>BTrTTL&!?O+%Rv;b?B??gSzdp?6Uug9{ zd@V08Z$BdI?fpoCS$)t4mg4rT8Q_I}h`0d-vYZ^|dOB*Q^S|xqTV*vIg?@fVFSmMpaw0qtTRbx} z({Pg?#{2`sc9)M5N$*N|4;^t$+QP?#mov zGVC@I*lBVrOU-%2y!7%)fAKjpEFsgQc4{amtiHb95KQEwvf<(3T<9-Zm$xIew#P22 zc2Ix|App^>v6(3L_MCU0d3W##AB0M~3D00EWoKZqsJYT(#@w$Y_H7G22M~ApVFTRHMI_3be)Lkn#0F*V8Pq zc}`Cjy$bE;FJ6H7p=0y#R>`}-m4(0F>%@P|?7fx{=R^uFdISRnZ2W_xQhD{YuR3t< z{6yxu=4~JkeA;|(J6_nv#>Nvs&FuLA&PW^he@t(UwFFE8)|a!R{`E`K`i^ZnyE4$k z;(749Ix|oi$c3QbEJ3b~D_kQsPz~fIUKym($a_7dJ?o+40*OLl^{=&oq$<#Q(yyrp z{J-FAniyAw9tPbe&IhQ|a`DqFTVQGQ&Gq3!C2==4x{6EJwiPZ8zub-iXoUtkJiG{} zPaR&}_fn8_z~(=;5lD-aPWD3z8PZS@AaUiomF!G8I}Mf>e~0g#BelA-5#`cj;O5>N 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 c949e3fbcbaab2284268921e04b30d787ece6e35 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 18 Nov 2021 12:56:01 +0100 Subject: [PATCH 02/65] 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 855269484e27d0cc3ccc5dd25bfe60c0aa28a614 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 18 Nov 2021 14:04:05 +0000 Subject: [PATCH 03/65] 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 b2f7d959eb29eb2d943897c8b7c651ed32f9c2d2 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 28 Dec 2021 14:11:40 +0100 Subject: [PATCH 04/65] 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 72e794958ede9d8970e19ff25ae91f7cf8adc1f2 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 29 Dec 2021 15:01:01 +0100 Subject: [PATCH 05/65] 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 78dbd6a5faf985d58814362cf2ece759a8a93e9f Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 12 Jan 2022 07:24:27 +0000 Subject: [PATCH 06/65] 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 2ed66f05c0c4f43e9ad3635bc4c358e68bb2c66d Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 14 Jan 2022 07:36:41 +0100 Subject: [PATCH 07/65] 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 5b9bd259fe1ec286bc26686924d4b48359bf8532 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 14 Jan 2022 08:52:16 +0000 Subject: [PATCH 08/65] 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 7f693c0dcfb64432dd56d1e8169f48a20a9d4c78 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 19 Jan 2022 10:23:03 +0100 Subject: [PATCH 09/65] 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 e6dec9afbf0c8acb08711788c32dcaaf901db4d6 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 19 Jan 2022 10:03:05 +0000 Subject: [PATCH 10/65] 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 658270c5990ee9ddad1b787317e03313a2d909a6 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 4 Apr 2022 11:13:02 +0200 Subject: [PATCH 11/65] 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 55f16a2101da2b43cb00cb9fe79ffdab4e16beaf Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 4 Apr 2022 09:27:20 +0000 Subject: [PATCH 12/65] 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 21941fe4860a3bf34f74b218a1e160e1387e2211 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 15 Jun 2022 21:02:55 +0200 Subject: [PATCH 13/65] 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 12ddc8055396d28bafc6099314b67643f471747e Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 15 Jul 2022 12:32:12 +0000 Subject: [PATCH 14/65] [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 99e0177a8532d43dabfa12d9e900b9a49aa24213 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 15 Jul 2022 12:33:48 +0000 Subject: [PATCH 15/65] [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 ba0986c8f9e676a0fe5898966269ef321b947950 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 15 Jul 2022 12:38:24 +0000 Subject: [PATCH 16/65] [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 4d1beaa923739b0d09c2b1a35ec905fb2ec2b91a Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 15 Jun 2022 21:39:58 +0200 Subject: [PATCH 17/65] 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 919cb88805d9294d714dacc6d9f2ead0bb149539 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 25 Jul 2022 17:23:20 +0200 Subject: [PATCH 18/65] 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 9e46c6e5a7f674a6230d4a1c79d5ca545e255137 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 27 Jul 2022 12:24:28 +0200 Subject: [PATCH 19/65] 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 41159c5f43d7383faec11c662e4436647f448df8 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 1 Nov 2022 18:31:37 +0100 Subject: [PATCH 20/65] 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 49e8353d6aa2a6962430b7f91e8355943c647496 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 25 Jul 2022 17:27:11 +0200 Subject: [PATCH 21/65] 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 2e37783cf487bad3c8d08a0b1e000fe873de1b54 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 16 Feb 2023 17:42:15 +0100 Subject: [PATCH 22/65] 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 99368fd29168a50d2434666c9b44d4c9869a35ee Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 1 Mar 2023 09:36:56 +0100 Subject: [PATCH 23/65] 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 eba5b3487060bd9d0064a491b4e26b5cd43cb7b9 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 8 Mar 2023 12:27:54 +0100 Subject: [PATCH 24/65] 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 b8653a81ad052bbfd42dcce7462d600aedb340c8 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 8 Mar 2023 12:29:01 +0100 Subject: [PATCH 25/65] 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 2bd76011ed8c8d6c395cb7aee3432b5a88c100d2 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 27 Mar 2023 15:10:45 +0200 Subject: [PATCH 26/65] 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 f915c73b68ebbfa346e68aced35c2fdef92a5b27 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 21 Apr 2023 11:57:59 +0200 Subject: [PATCH 27/65] 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 7cc701569abf6a4621cd10d230cfd418f734dfa5 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 11 May 2023 17:36:37 +0200 Subject: [PATCH 28/65] 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 76ad714126014e5a6890ea5b4bfffdf83893b229 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Thu, 11 May 2023 16:27:59 +0000 Subject: [PATCH 29/65] [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 e3e3ec0359b23c315340b232a43a014391bfd5df Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 11 May 2023 16:30:09 +0000 Subject: [PATCH 30/65] [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

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.

@@ -479,7 +479,7 @@

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.

@@ -505,7 +505,7 @@

Maintainers

promote its widespread use.

Current maintainer:

simahawk

-

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

+

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 9f8cbd8046e5c7aefc3dd5c8b18a9de326c45af2 Mon Sep 17 00:00:00 2001 From: OriolMForgeFlow Date: Mon, 17 Jul 2023 13:02:38 +0200 Subject: [PATCH 38/65] [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 5d63b224..09b97138 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 9b2e639b90a8b74c53c675e3056814bafbfb5be0 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 20 Jul 2023 11:26:17 +0000 Subject: [PATCH 39/65] endpoint_route_handler 16.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 f90270a8..a8e64308 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": "16.0.1.0.0", + "version": "16.0.1.1.0", "license": "LGPL-3", "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", From 395a263e505c64f92e7a690d22d2f9016b0fa393 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 25 Jul 2023 18:54:48 +0000 Subject: [PATCH 40/65] [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 a749d1f1..d919694d 100644 --- a/endpoint_route_handler/i18n/endpoint_route_handler.pot +++ b/endpoint_route_handler/i18n/endpoint_route_handler.pot @@ -176,6 +176,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 #: 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." From d5a668f95c537f6d88f147dcbdcaa966911606c1 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sun, 3 Sep 2023 17:59:48 +0000 Subject: [PATCH 41/65] [UPD] README.rst --- endpoint_route_handler/README.rst | 12 +++-- .../static/description/index.html | 52 ++++++++++--------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/endpoint_route_handler/README.rst b/endpoint_route_handler/README.rst index 0511f927..43ea8a18 100644 --- a/endpoint_route_handler/README.rst +++ b/endpoint_route_handler/README.rst @@ -2,10 +2,13 @@ Endpoint route handler ====================== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:297624cb3e41c272a265b40fe8f8a3b7fe0ddc991fd286937800f2ea5bd1728c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -19,8 +22,11 @@ Endpoint route handler .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png :target: https://translation.odoo-community.org/projects/web-api-16-0/web-api-16-0-endpoint_route_handler :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/web-api&target_branch=16.0 + :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| +|badge1| |badge2| |badge3| |badge4| |badge5| Technical module that provides a base handler for adding and removing controller routes on the fly. @@ -129,7 +135,7 @@ 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. diff --git a/endpoint_route_handler/static/description/index.html b/endpoint_route_handler/static/description/index.html index 89b6adaf..12228f1a 100644 --- a/endpoint_route_handler/static/description/index.html +++ b/endpoint_route_handler/static/description/index.html @@ -1,20 +1,20 @@ - + - + Endpoint route handler -
-

Endpoint route handler

+
+ + +Odoo Community Association + +
+

Endpoint route handler

-

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

+

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.

@@ -392,9 +397,9 @@

Endpoint route handler

-

Usage

+

Usage

-

As a mixin

+

As a mixin

Use standard Odoo inheritance:

 class MyModel(models.Model):
@@ -419,7 +424,7 @@ 

As a mixin

record.

-

As a tool

+

As a tool

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

@@ -454,7 +459,7 @@ 

As a tool

-

Known issues / Roadmap

+

Known issues / Roadmap

-

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 to smash it by providing a detailed and welcomed @@ -492,22 +497,22 @@

Bug Tracker

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 @@ -522,5 +527,6 @@

Maintainers

+
From 481eaf7c2cbac40804a5e53ec82306d12e6b6208 Mon Sep 17 00:00:00 2001 From: Raf Ven Date: Mon, 6 Oct 2025 14:36:17 +0200 Subject: [PATCH 64/65] [IMP] endpoint_route_handler: pre-commit stuff --- endpoint_route_handler/models/endpoint_route_handler.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/models/endpoint_route_handler.py index 19a8c766..8d6434bb 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/models/endpoint_route_handler.py @@ -80,10 +80,7 @@ def _check_route_unique_across_models(self): clashing_models.append(model) if clashing_models: raise exceptions.UserError( - _( - "Non unique route(s): %(routes)s.\n" - "Found in model(s): %(models)s.\n" - ) + _("Non unique route(s): %(routes)s.\nFound in model(s): %(models)s.\n") % {"routes": ", ".join(routes), "models": ", ".join(clashing_models)} ) From a1015b30e4c76677babd5aa2716141ccd1830dae Mon Sep 17 00:00:00 2001 From: Raf Ven Date: Tue, 7 Oct 2025 08:09:07 +0200 Subject: [PATCH 65/65] [MIG] endpoint_route_handler: Migration to 19.0 --- endpoint_route_handler/README.rst | 48 +++++++++---------- endpoint_route_handler/__init__.py | 1 + endpoint_route_handler/__manifest__.py | 2 +- endpoint_route_handler/models/__init__.py | 3 -- endpoint_route_handler/models/ir_http.py | 4 +- endpoint_route_handler/registry.py | 3 +- .../static/description/index.html | 10 ++-- endpoint_route_handler/tests/common.py | 2 +- endpoint_route_handler/wizard/__init__.py | 3 ++ .../endpoint_route_handler.py | 30 +++++++----- .../endpoint_route_handler_tool.py | 0 .../endpoint_route_sync_mixin.py | 0 12 files changed, 56 insertions(+), 50 deletions(-) create mode 100644 endpoint_route_handler/wizard/__init__.py rename endpoint_route_handler/{models => wizard}/endpoint_route_handler.py (91%) rename endpoint_route_handler/{models => wizard}/endpoint_route_handler_tool.py (100%) rename endpoint_route_handler/{models => wizard}/endpoint_route_sync_mixin.py (100%) diff --git a/endpoint_route_handler/README.rst b/endpoint_route_handler/README.rst index 5825aa49..45ad1b6b 100644 --- a/endpoint_route_handler/README.rst +++ b/endpoint_route_handler/README.rst @@ -21,13 +21,13 @@ Endpoint route handler :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/18.0/endpoint_route_handler + :target: https://github.com/OCA/web-api/tree/19.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-18-0/web-api-18-0-endpoint_route_handler + :target: https://translation.odoo-community.org/projects/web-api-19-0/web-api-19-0-endpoint_route_handler :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/web-api&target_branch=18.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/web-api&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -120,31 +120,31 @@ You can see a real life example on shopfloor.app model. Known issues / Roadmap ====================== -- add api docs helpers +- add api docs helpers -- allow multiple HTTP methods on the same endpoint +- allow multiple HTTP methods on the same endpoint -- multiple values for route and methods +- multiple values for route and methods - keep the same in the ui for now, later own we can imagine a - multi-value selection or just add text field w/ proper validation - and cleanup + keep the same in the ui for now, later own we can imagine a + multi-value selection or just add text field w/ proper validation + and cleanup - remove the route field in the table of endpoint_route + remove the route field in the table of endpoint_route - support a comma separated list of routes maybe support comma - separated list of methods use only routing.routes for generating - the rule sort and freeze its values to update the endpoint hash + support a comma separated list of routes maybe support comma + separated list of methods use only routing.routes for generating + the rule sort and freeze its values to update the endpoint hash - catch dup route exception on the sync to detect duplicated routes - and use the endpoint_hash to retrieve the real record (note: we - could store more info in the routing information which will stay in - the map) + catch dup route exception on the sync to detect duplicated routes + and use the endpoint_hash to retrieve the real record (note: we + could store more info in the routing information which will stay + in the map) - for customizing the rule behavior the endpoint the hook is to - override the registry lookup + for customizing the rule behavior the endpoint the hook is to + override the registry lookup - make EndpointRule class overridable on the registry + make EndpointRule class overridable on the registry NOTE in v16 we won't care anymore about odoo controller so the lookup of the controller can be simplified to a basic py obj that holds the @@ -156,7 +156,7 @@ 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 to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -171,8 +171,8 @@ Authors Contributors ------------ -- Simone Orsi -- Nguyen Minh Chien +- Simone Orsi +- Nguyen Minh Chien Maintainers ----------- @@ -195,6 +195,6 @@ Current `maintainer `__: |maintainer-simahawk| -This module is part of the `OCA/web-api `_ project on GitHub. +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/__init__.py b/endpoint_route_handler/__init__.py index a0cb2972..67914cd4 100644 --- a/endpoint_route_handler/__init__.py +++ b/endpoint_route_handler/__init__.py @@ -1,2 +1,3 @@ from . import models +from . import wizard from .post_init_hook import post_init_hook diff --git a/endpoint_route_handler/__manifest__.py b/endpoint_route_handler/__manifest__.py index 66cb988d..0003569b 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": "18.0.1.1.0", + "version": "19.0.1.0.0", "license": "LGPL-3", "development_status": "Beta", "author": "Camptocamp,Odoo Community Association (OCA)", diff --git a/endpoint_route_handler/models/__init__.py b/endpoint_route_handler/models/__init__.py index 2115e503..9a5eb718 100644 --- a/endpoint_route_handler/models/__init__.py +++ b/endpoint_route_handler/models/__init__.py @@ -1,4 +1 @@ -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/ir_http.py b/endpoint_route_handler/models/ir_http.py index 31d96f5a..e7b83c72 100644 --- a/endpoint_route_handler/models/ir_http.py +++ b/endpoint_route_handler/models/ir_http.py @@ -38,8 +38,8 @@ def _endpoint_routing_rules(cls): for url in endpoint_rule.routing["routes"]: yield (url, endpoint) - @tools.ormcache("key", "cls._endpoint_route_last_version()", cache="routing") - def routing_map(cls, key=None): + @tools.ormcache("key", "self._endpoint_route_last_version()", cache="routing") + def routing_map(self, key=None): res = super().routing_map(key=key) return res diff --git a/endpoint_route_handler/registry.py b/endpoint_route_handler/registry.py index 50c0c86c..34444a03 100644 --- a/endpoint_route_handler/registry.py +++ b/endpoint_route_handler/registry.py @@ -103,13 +103,14 @@ def _setup_db(cls, cr): 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( + tools.sql.create_index( cr, "endpoint_route__key_uniq", cls._table, [ "key", ], + unique=True, ) tools.sql.add_constraint( cr, diff --git a/endpoint_route_handler/static/description/index.html b/endpoint_route_handler/static/description/index.html index 1c8b19ec..b710e137 100644 --- a/endpoint_route_handler/static/description/index.html +++ b/endpoint_route_handler/static/description/index.html @@ -374,7 +374,7 @@

Endpoint route handler

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:b9dd15b039dbf78d7a01947b6f273b1487a97ce0911b41b0b5652b2dbbb38456 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

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

+

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.

@@ -476,8 +476,8 @@

Known issues / Roadmap

the rule sort and freeze its values to update the endpoint hash

catch dup route exception on the sync to detect duplicated routes and use the endpoint_hash to retrieve the real record (note: we -could store more info in the routing information which will stay in -the map)

+could store more info in the routing information which will stay +in the map)

for customizing the rule behavior the endpoint the hook is to override the registry lookup

make EndpointRule class overridable on the registry

@@ -493,7 +493,7 @@

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 to smash it by providing a detailed and welcomed -feedback.

+feedback.

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

@@ -522,7 +522,7 @@

Maintainers

promote its widespread use.

Current maintainer:

simahawk

-

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

+

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/tests/common.py b/endpoint_route_handler/tests/common.py index 2ed24331..a0caf9bd 100644 --- a/endpoint_route_handler/tests/common.py +++ b/endpoint_route_handler/tests/common.py @@ -7,7 +7,7 @@ from odoo.tests.common import TransactionCase, tagged from odoo.tools import DotDict -from odoo.addons.website.tools import MockRequest +from odoo.addons.http_routing.tests.common import MockRequest @tagged("-at_install", "post_install") diff --git a/endpoint_route_handler/wizard/__init__.py b/endpoint_route_handler/wizard/__init__.py new file mode 100644 index 00000000..2a17894a --- /dev/null +++ b/endpoint_route_handler/wizard/__init__.py @@ -0,0 +1,3 @@ +from . import endpoint_route_sync_mixin +from . import endpoint_route_handler +from . import endpoint_route_handler_tool diff --git a/endpoint_route_handler/models/endpoint_route_handler.py b/endpoint_route_handler/wizard/endpoint_route_handler.py similarity index 91% rename from endpoint_route_handler/models/endpoint_route_handler.py rename to endpoint_route_handler/wizard/endpoint_route_handler.py index 8d6434bb..bba74a54 100644 --- a/endpoint_route_handler/models/endpoint_route_handler.py +++ b/endpoint_route_handler/wizard/endpoint_route_handler.py @@ -4,7 +4,7 @@ import logging -from odoo import _, api, exceptions, fields, models +from odoo import api, exceptions, fields, models ENDPOINT_ROUTE_CONSUMER_MODELS = { # by db @@ -49,13 +49,10 @@ class EndpointRouteHandler(models.AbstractModel): # 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 = [ - ( - "endpoint_route_unique", - "unique(route)", - "You can register an endpoint route only once.", - ) - ] + _endpoint_route_unique = models.Constraint( + "unique(route)", + "You can register an endpoint route only once.", + ) @api.constrains("route") def _check_route_unique_across_models(self): @@ -80,8 +77,12 @@ def _check_route_unique_across_models(self): clashing_models.append(model) if clashing_models: raise exceptions.UserError( - _("Non unique route(s): %(routes)s.\nFound in model(s): %(models)s.\n") - % {"routes": ", ".join(routes), "models": ", ".join(clashing_models)} + self.env._( + "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): @@ -174,8 +175,11 @@ def _check_route(self): for rec in self: if rec.route in self._blacklist_routes: raise exceptions.UserError( - _("`%(name)s` uses a blacklisted routed = `%(route)s`") - % {"name": rec.name, "route": rec.route} + self.env._( + "`%(name)s` uses a blacklisted routed = `%(route)s`", + name=rec.name, + route=rec.route, + ) ) @api.constrains("request_method", "request_content_type") @@ -183,7 +187,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 content type is required for POST and PUT.") + self.env._("Request content type is required for POST and PUT.") ) def _prepare_endpoint_rules(self, options=None): diff --git a/endpoint_route_handler/models/endpoint_route_handler_tool.py b/endpoint_route_handler/wizard/endpoint_route_handler_tool.py similarity index 100% rename from endpoint_route_handler/models/endpoint_route_handler_tool.py rename to endpoint_route_handler/wizard/endpoint_route_handler_tool.py diff --git a/endpoint_route_handler/models/endpoint_route_sync_mixin.py b/endpoint_route_handler/wizard/endpoint_route_sync_mixin.py similarity index 100% rename from endpoint_route_handler/models/endpoint_route_sync_mixin.py rename to endpoint_route_handler/wizard/endpoint_route_sync_mixin.py