Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,2 @@
from . import ir_rule
from . import rest_service_registration

View file

@ -0,0 +1,27 @@
# Copyright 2021 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
class IrRule(models.Model):
"""Add authenticated_partner_id in record rule evaluation context.
This come from the env context, which is populated by the base_rest service layer
context provider.
"""
_inherit = "ir.rule"
@api.model
def _eval_context(self):
ctx = super()._eval_context()
if "authenticated_partner_id" in self.env.context:
ctx["authenticated_partner_id"] = self.env.context[
"authenticated_partner_id"
]
return ctx
def _compute_domain_keys(self):
"""Return the list of context keys to use for caching ``_compute_domain``."""
return super()._compute_domain_keys() + ["authenticated_partner_id"]

View file

@ -0,0 +1,452 @@
# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
"""
REST Service Registy Builder
============================
Register available REST services at the build of a registry.
This code is inspired by ``odoo.addons.component.builder.ComponentBuilder``
"""
import inspect
import logging
from werkzeug.routing import Map, Rule
import odoo
from odoo import http, models
from odoo.addons.component.core import WorkContext
from .. import restapi
from ..components.service import BaseRestService
from ..controllers.main import _PseudoCollection
from ..core import (
RestServicesRegistry,
_rest_controllers_per_module,
_rest_services_databases,
_rest_services_routes,
)
from ..tools import ROUTING_DECORATOR_ATTR, _inspect_methods
_logger = logging.getLogger(__name__)
class RestServiceRegistration(models.AbstractModel):
"""Register REST services into the REST services registry
This class allows us to hook the registration of the root urls of all
the REST controllers installed into the current database at the end of the
Odoo's registry loading, using ``_register_hook``. This method is called
after all modules are loaded, so we are sure that we only register REST
services installed into the current database.
"""
_name = "rest.service.registration"
_description = "REST Services Registration Model"
def _register_hook(self):
# This method is called by Odoo when the registry is built,
# so in case the registry is rebuilt (cache invalidation, ...),
# we have to to rebuild the registry. We use a new
# registry so we have an empty cache and we'll add services in it.
services_registry = self._init_global_registry()
self.build_registry(services_registry)
# we also have to remove the RestController from the
# controller_per_module registry since it's an abstract controller
controllers = http.Controller.children_classes["base_rest"]
controllers = [
cls for cls in controllers if "RestController" not in cls.__name__
]
http.Controller.children_classes["base_rest"] = controllers
# create the final controller providing the http routes for
# the services available into the current database
self._build_controllers_routes(services_registry)
def _build_controllers_routes(self, services_registry):
for controller_def in services_registry.values():
for service in self._get_services(controller_def["collection_name"]):
self._prepare_non_decorated_endpoints(service)
self._build_controller(service, controller_def)
def _prepare_non_decorated_endpoints(self, service):
# Autogenerate routing info where missing
RestApiMethodTransformer(service).fix()
def _build_controller(self, service, controller_def):
_logger.debug("Build service %s for controller_def %s", service, controller_def)
base_controller_cls = controller_def["controller_class"]
# build our new controller class
ctrl_cls = RestApiServiceControllerGenerator(
service, base_controller_cls
).generate()
# generate an addon name used to register our new controller for
# the current database
addon_name = base_controller_cls._module
identifier = "{}_{}_{}".format(
self.env.cr.dbname,
service._collection.replace(".", "_"),
service._usage.replace(".", "_"),
)
base_controller_cls._identifier = identifier
# put our new controller into the new addon module
ctrl_cls.__module__ = "odoo.addons.{}".format(addon_name)
self.env.registry._init_modules.add(addon_name)
# register our conroller into the list of available controllers
http.Controller.children_classes[addon_name].append(ctrl_cls)
self._apply_defaults_to_controller_routes(controller_class=ctrl_cls)
def _apply_defaults_to_controller_routes(self, controller_class):
"""
Apply default routes properties defined on the controller_class to
routes where properties are missing
Set the automatic auth on controller's routes.
During definition of new controller, the _default_auth should be
applied on every routes (cfr @route odoo's decorator).
This auth attribute should be applied only if the route doesn't already
define it.
:return:
"""
for _name, method in _inspect_methods(controller_class):
routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
if not routing:
continue
self._apply_default_auth_if_not_set(controller_class, routing)
self._apply_default_if_not_set(controller_class, routing, "csrf")
self._apply_default_if_not_set(controller_class, routing, "save_session")
self._apply_default_cors_if_not_set(controller_class, routing)
def _apply_default_if_not_set(self, controller_class, routing, attr_name):
default_attr_name = "_default_" + attr_name
if hasattr(controller_class, default_attr_name) and attr_name not in routing:
routing[attr_name] = getattr(controller_class, default_attr_name)
def _apply_default_auth_if_not_set(self, controller_class, routing):
default_attr_name = "_default_auth"
default_auth = getattr(controller_class, default_attr_name, None)
if default_auth:
if "auth" in routing:
auth = routing["auth"]
if auth == "public_or_default":
alternative_auth = "public_or_" + default_auth
if getattr(
self.env["ir.http"], "_auth_method_%s" % alternative_auth, None
):
routing["auth"] = alternative_auth
else:
_logger.debug(
"No %s auth method available: Fallback on %s",
alternative_auth,
default_auth,
)
routing["auth"] = default_auth
else:
routing["auth"] = default_auth
def _apply_default_cors_if_not_set(self, controller_class, routing):
default_attr_name = "_default_cors"
if hasattr(controller_class, default_attr_name) and "cors" not in routing:
cors = getattr(controller_class, default_attr_name)
routing["cors"] = cors
if cors and "OPTIONS" not in routing.get("methods", ["OPTIONS"]):
# add http method 'OPTIONS' required by cors if the route is
# restricted to specific method
routing["methods"].append("OPTIONS")
def _get_services(self, collection_name):
collection = _PseudoCollection(collection_name, self.env)
work = WorkContext(
model_name="rest.service.registration", collection=collection
)
component_classes = work._lookup_components(usage=None, model_name=None)
# removes component without collection that are not a rest service
component_classes = [
c for c in component_classes if self._filter_service_component(c)
]
return [comp(work) for comp in component_classes]
@staticmethod
def _filter_service_component(comp):
return (
issubclass(comp, BaseRestService)
and comp._collection
and comp._usage
and getattr(comp, "_is_rest_service_component", True)
)
def build_registry(self, services_registry, states=None, exclude_addons=None):
if not states:
states = ("installed", "to upgrade")
# we load REST, controllers following the order of the 'addons'
# dependencies to ensure that controllers defined in a more
# specialized addon and overriding more generic one takes precedences
# on the generic one into the registry
graph = odoo.modules.graph.Graph()
graph.add_module(self.env.cr, "base")
query = "SELECT name " "FROM ir_module_module " "WHERE state IN %s "
params = [tuple(states)]
if exclude_addons:
query += " AND name NOT IN %s "
params.append(tuple(exclude_addons))
self.env.cr.execute(query, params)
module_list = [name for (name,) in self.env.cr.fetchall() if name not in graph]
graph.add_modules(self.env.cr, module_list)
for module in graph:
self.load_services(module.name, services_registry)
def load_services(self, module, services_registry):
controller_defs = _rest_controllers_per_module.get(module, [])
for controller_def in controller_defs:
root_path = controller_def["root_path"]
is_base_contoller = not getattr(
controller_def["controller_class"], "_generated", False
)
if is_base_contoller:
current_controller = (
services_registry[root_path]["controller_class"]
if root_path in services_registry
else None
)
services_registry[controller_def["root_path"]] = controller_def
self._register_rest_route(controller_def["root_path"])
if (
current_controller
and current_controller != controller_def["controller_class"]
):
_logger.error(
"Only one REST controller can be safely declared for root path %s\n "
"Registering controller %s\n "
"Registered controller%s\n",
root_path,
controller_def,
services_registry[controller_def["root_path"]],
)
def _init_global_registry(self):
services_registry = RestServicesRegistry()
_rest_services_databases[self.env.cr.dbname] = services_registry
return services_registry
def _register_rest_route(self, route_path):
"""Register given route path to be handles as RestRequest.
See base_rest.http.get_request.
"""
_rest_services_routes[self.env.cr.dbname].add(route_path)
class RestApiMethodTransformer(object):
"""Helper class to generate and apply the missing restapi.method decorator
to service's methods defined without decorator.
Before 10/12.0.3.0.0 methods exposed by a service was based on implicit
conventions. This transformer is used to keep this functionality by
generating and applying the missing decorators. As result all the methods
exposed are decorated and the processing can be based on these decorators.
"""
def __init__(self, service):
self._service = service
def fix(self):
methods_to_fix = []
for name, method in _inspect_methods(self._service.__class__):
if not self._is_public_api_method(name):
continue
if not hasattr(method, ROUTING_DECORATOR_ATTR):
methods_to_fix.append(method)
for method in methods_to_fix:
self._fix_method_decorator(method)
def _is_public_api_method(self, method_name):
if method_name.startswith("_"):
return False
if not hasattr(self._service, method_name):
return False
if hasattr(BaseRestService, method_name):
# exclude methods from base class
return False
return True
def _fix_method_decorator(self, method):
method_name = method.__name__
routes = self._method_to_routes(method)
input_param = self._method_to_input_param(method)
output_param = self._method_to_output_param(method)
decorated_method = restapi.method(
routes=routes, input_param=input_param, output_param=output_param
)(getattr(self._service.__class__, method_name))
setattr(self._service.__class__, method_name, decorated_method)
def _method_to_routes(self, method):
"""
Generate the restapi.method's routes
:param method:
:return: A list of routes used to get access to the method
"""
method_name = method.__name__
signature = inspect.signature(method)
id_in_path_required = "_id" in signature.parameters
path = "/{}".format(method_name)
if id_in_path_required:
path = "/<int:id>" + path
if method_name in ("get", "search"):
paths = [path]
path = "/"
if id_in_path_required:
path = "/<int:id>"
paths.append(path)
return [(paths, "GET")]
elif method_name == "delete":
routes = [(path, "POST")]
path = "/"
if id_in_path_required:
path = "/<int:id>"
routes.append((path, "DELETE"))
elif method_name == "update":
paths = [path]
path = "/"
if id_in_path_required:
path = "/<int:id>"
paths.append(path)
routes = [(paths, "POST"), (path, "PUT")]
elif method_name == "create":
paths = [path]
path = "/"
if id_in_path_required:
path = "/<int:id>"
paths.append(path)
routes = [(paths, "POST")]
else:
routes = [(path, "POST")]
return routes
def _method_to_param(self, validator_method_name, direction):
validator_component = self._service.component(usage="cerberus.validator")
if validator_component.has_validator_handler(
self._service, validator_method_name, direction
):
return restapi.CerberusValidator(schema=validator_method_name)
return None
def _method_to_input_param(self, method):
validator_method_name = "_validator_{}".format(method.__name__)
return self._method_to_param(validator_method_name, "input")
def _method_to_output_param(self, method):
validator_method_name = "_validator_return_{}".format(method.__name__)
return self._method_to_param(validator_method_name, "output")
class RestApiServiceControllerGenerator(object):
"""
An object helper used to generate the http.Controller required to serve
the method decorated with the `@restappi.method` decorator
"""
def __init__(self, service, base_controller):
self._service = service
self._service_name = service._usage
self._base_controller = base_controller
@property
def _new_cls_name(self):
controller_name = self._base_controller.__name__
return "{}{}".format(
controller_name, self._service._usage.title().replace(".", "_")
)
def generate(self):
"""
:return: A new controller child of base_controller defining the routes
required to serve the method of the services.
"""
controller = type(
self._new_cls_name, (self._base_controller,), self._generate_methods()
)
controller._generated = True
return controller
def _generate_methods(self):
"""Generate controller's methods and associated routes
This method inspect the service definition and generate the appropriate
methods and routing rules for all the methods decorated with @restappi.method
:return: A dictionary of method name : method
"""
methods = {}
_globals = {}
root_path = self._base_controller._root_path
path_sep = ""
if root_path[-1] != "/":
path_sep = "/"
root_path = "{}{}{}".format(root_path, path_sep, self._service._usage)
for name, method in _inspect_methods(self._service.__class__):
routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
if routing is None:
continue
for routes, http_method in routing["routes"]:
method_name = "{}_{}".format(http_method.lower(), name)
default_route = routes[0]
rule = Rule(default_route)
Map(rules=[rule])
if rule.arguments:
method = METHOD_TMPL_WITH_ARGS.format(
method_name=method_name,
service_name=self._service_name,
service_method_name=name,
args=", ".join([c[1] for c in rule._trace if c[0]]),
)
else:
method = METHOD_TMPL.format(
method_name=method_name,
service_name=self._service_name,
service_method_name=name,
)
exec(method, _globals)
method_exec = _globals[method_name]
route_params = dict(
route=["{}{}".format(root_path, r) for r in routes],
methods=[http_method],
type="restapi",
)
for attr in {"auth", "cors", "csrf", "save_session"}:
if attr in routing:
route_params[attr] = routing[attr]
method_exec = http.route(**route_params)(method_exec)
methods[method_name] = method_exec
return methods
METHOD_TMPL = """
def {method_name}(self, collection=None, **kwargs):
return self._process_method(
"{service_name}",
"{service_method_name}",
collection=collection,
params=kwargs
)
"""
METHOD_TMPL_WITH_ARGS = """
def {method_name}(self, {args}, collection=None, **kwargs):
return self._process_method(
"{service_name}",
"{service_method_name}",
*[{args}],
collection=collection,
params=kwargs
)
"""