From 696a7c0adbaf56973b76c55f4081370fcaceed7f Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Wed, 8 Jan 2025 11:58:22 +0100 Subject: [PATCH 01/58] [FIX] fastapi: Avoid zombie threads Each time a fastapi app is created, a new event loop thread is created by the ASGIMiddleware. Unfortunately, every time the cache is cleared, a new app is created with a new even loop thread. This leads to an increase in the number of threads created to manage the asyncio event loop, even though many of them are no longer in use. To avoid this problem, the thread in charge of the event loop is now created only once per thread / process and the result is stored in the thread's local storage. If a new instance of an app needs to be created following a cache reset, this ensures that the same event loop is reused. refs #484 --- fastapi/models/fastapi_endpoint.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 1312f07d1..9f110cfdd 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -1,7 +1,9 @@ # Copyright 2022 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). +import asyncio import logging +import threading from functools import partial from itertools import chain from typing import Any, Callable, Dict, List, Tuple @@ -19,6 +21,26 @@ _logger = logging.getLogger(__name__) +# Thread-local storage for event loops +# Using a thread-local storage allows to have a dedicated event loop per thread +# and avoid the need to create a new event loop for each request. It's also +# compatible with the multi-worker mode of Odoo. +_event_loop_storage = threading.local() + + +def get_or_create_event_loop() -> asyncio.AbstractEventLoop: + """ + Get or create a reusable event loop for the current thread. + """ + if not hasattr(_event_loop_storage, "loop"): + loop = asyncio.new_event_loop() + loop_thread = threading.Thread(target=loop.run_forever, daemon=True) + loop_thread.start() + _event_loop_storage.loop = loop + _event_loop_storage.thread = loop_thread + return _event_loop_storage.loop + + class FastapiEndpoint(models.Model): _name = "fastapi.endpoint" @@ -213,7 +235,8 @@ def get_app(self, root_path): app = FastAPI() app.mount(record.root_path, record._get_app()) self._clear_fastapi_exception_handlers(app) - return ASGIMiddleware(app) + event_loop = get_or_create_event_loop() + return ASGIMiddleware(app, loop=event_loop) def _clear_fastapi_exception_handlers(self, app: FastAPI) -> None: """ From 8c090cd6b2af9082c0fc5235a25c93e8d314aac6 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 10 Jan 2025 14:44:18 +0100 Subject: [PATCH 02/58] [IMP] fastapi: add event loop lifecycle management This commit adds event loop lifecycle management to the FastAPI dispatcher. Before this commit, an event loop and the thread to run it were created each time a FastAPI app was created. The drawback of this approach is that when the app was destroyed (for example, when the cache of app was cleared), the event loop and the thread were not properly stopped, which could lead to memory leaks and zombie threads. This commit fixes this issue by creating a pool of event loops and threads that are shared among all FastAPI apps. On each call to a FastAPI app, a event loop is requested from the pool and is returned to the pool when the app is destroyed. At request time of an event loop, the pool try to reuse an existing event loop and if no event loop is available, a new event loop is created. The cache of the FastAPI app is also refactored to use it's own mechanism. It's now based on a dictionary of queues by root path by database, where each queue is a pool of FastAPI app. This allows a better management of the invalidation of the cache. It's now possible to invalidate the cache of FastAPI app by root path without affecting the cache of others root paths. --- fastapi/fastapi_dispatcher.py | 24 ++++++------- fastapi/middleware.py | 26 ++++++++++++++ fastapi/models/fastapi_endpoint.py | 35 +++--------------- fastapi/pools/__init__.py | 7 ++++ fastapi/pools/event_loop.py | 58 ++++++++++++++++++++++++++++++ fastapi/pools/fastapi_app.py | 52 +++++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 43 deletions(-) create mode 100644 fastapi/middleware.py create mode 100644 fastapi/pools/__init__.py create mode 100644 fastapi/pools/event_loop.py create mode 100644 fastapi/pools/fastapi_app.py diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index 1a8eb3532..3edf29f14 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -8,6 +8,7 @@ from .context import odoo_env_ctx from .error_handlers import convert_exception_to_status_body +from .pools import fastapi_app_pool class FastApiDispatcher(Dispatcher): @@ -24,18 +25,17 @@ def dispatch(self, endpoint, args): root_path = "/" + environ["PATH_INFO"].split("/")[1] # TODO store the env into contextvar to be used by the odoo_env # depends method - fastapi_endpoint = self.request.env["fastapi.endpoint"].sudo() - app = fastapi_endpoint.get_app(root_path) - uid = fastapi_endpoint.get_uid(root_path) - data = BytesIO() - with self._manage_odoo_env(uid): - for r in app(environ, self._make_response): - data.write(r) - if self.inner_exception: - raise self.inner_exception - return self.request.make_response( - data.getvalue(), headers=self.headers, status=self.status - ) + with fastapi_app_pool.get_app(root_path, request.env) as app: + uid = request.env["fastapi.endpoint"].sudo().get_uid(root_path) + data = BytesIO() + with self._manage_odoo_env(uid): + for r in app(environ, self._make_response): + data.write(r) + if self.inner_exception: + raise self.inner_exception + return self.request.make_response( + data.getvalue(), headers=self.headers, status=self.status + ) def handle_error(self, exc): headers = getattr(exc, "headers", None) diff --git a/fastapi/middleware.py b/fastapi/middleware.py new file mode 100644 index 000000000..8f63c2339 --- /dev/null +++ b/fastapi/middleware.py @@ -0,0 +1,26 @@ +# Copyright 2025 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). +""" +ASGI middleware for FastAPI. + +This module provides an ASGI middleware for FastAPI applications. The middleware +is designed to ensure managed the lifecycle of the threads used to as event loop +for the ASGI application. + +""" + +from typing import Iterable + +import a2wsgi +from a2wsgi.asgi import ASGIResponder +from a2wsgi.wsgi_typing import Environ, StartResponse + +from .pools import event_loop_pool + + +class ASGIMiddleware(a2wsgi.ASGIMiddleware): + def __call__( + self, environ: Environ, start_response: StartResponse + ) -> Iterable[bytes]: + with event_loop_pool.get_event_loop() as loop: + return ASGIResponder(self.app, loop)(environ, start_response) diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 9f110cfdd..9db835e0f 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -1,14 +1,11 @@ # Copyright 2022 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). -import asyncio import logging -import threading from functools import partial from itertools import chain from typing import Any, Callable, Dict, List, Tuple -from a2wsgi import ASGIMiddleware from starlette.middleware import Middleware from starlette.routing import Mount @@ -17,30 +14,12 @@ from fastapi import APIRouter, Depends, FastAPI from .. import dependencies +from ..middleware import ASGIMiddleware +from ..pools import fastapi_app_pool _logger = logging.getLogger(__name__) -# Thread-local storage for event loops -# Using a thread-local storage allows to have a dedicated event loop per thread -# and avoid the need to create a new event loop for each request. It's also -# compatible with the multi-worker mode of Odoo. -_event_loop_storage = threading.local() - - -def get_or_create_event_loop() -> asyncio.AbstractEventLoop: - """ - Get or create a reusable event loop for the current thread. - """ - if not hasattr(_event_loop_storage, "loop"): - loop = asyncio.new_event_loop() - loop_thread = threading.Thread(target=loop.run_forever, daemon=True) - loop_thread.start() - _event_loop_storage.loop = loop - _event_loop_storage.thread = loop_thread - return _event_loop_storage.loop - - class FastapiEndpoint(models.Model): _name = "fastapi.endpoint" @@ -220,14 +199,9 @@ def _endpoint_registry_route_unique_key(self, routing: Dict[str, Any]): return f"{self._name}:{self.id}:{path}" def _reset_app(self): - self.get_app.clear_cache(self) + fastapi_app_pool.invalidate(self.root_path, self.env) @api.model - @tools.ormcache("root_path") - # TODO cache on thread local by db to enable to get 1 middelware by - # thread when odoo runs in multi threads mode and to allows invalidate - # specific entries in place og the overall cache as we have to do into - # the _rest_app method def get_app(self, root_path): record = self.search([("root_path", "=", root_path)]) if not record: @@ -235,8 +209,7 @@ def get_app(self, root_path): app = FastAPI() app.mount(record.root_path, record._get_app()) self._clear_fastapi_exception_handlers(app) - event_loop = get_or_create_event_loop() - return ASGIMiddleware(app, loop=event_loop) + return ASGIMiddleware(app) def _clear_fastapi_exception_handlers(self, app: FastAPI) -> None: """ diff --git a/fastapi/pools/__init__.py b/fastapi/pools/__init__.py new file mode 100644 index 000000000..08ab00781 --- /dev/null +++ b/fastapi/pools/__init__.py @@ -0,0 +1,7 @@ +from .event_loop import EventLoopPool +from .fastapi_app import FastApiAppPool + +event_loop_pool = EventLoopPool() +fastapi_app_pool = FastApiAppPool() + +__all__ = ["event_loop_pool", "fastapi_app_pool"] diff --git a/fastapi/pools/event_loop.py b/fastapi/pools/event_loop.py new file mode 100644 index 000000000..a0a02a8f3 --- /dev/null +++ b/fastapi/pools/event_loop.py @@ -0,0 +1,58 @@ +# Copyright 2025 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). + +import asyncio +import queue +import threading +from contextlib import contextmanager +from typing import Generator + + +class EventLoopPool: + def __init__(self): + self.pool = queue.Queue[tuple[asyncio.AbstractEventLoop, threading.Thread]]() + + def __get_event_loop_and_thread( + self, + ) -> tuple[asyncio.AbstractEventLoop, threading.Thread]: + """ + Get an event loop from the pool. If no event loop is available, create a new one. + """ + try: + return self.pool.get_nowait() + except queue.Empty: + loop = asyncio.new_event_loop() + thread = threading.Thread(target=loop.run_forever, daemon=True) + thread.start() + return loop, thread + + def __return_event_loop( + self, loop: asyncio.AbstractEventLoop, thread: threading.Thread + ) -> None: + """ + Return an event loop to the pool for reuse. + """ + self.pool.put((loop, thread)) + + def shutdown(self): + """ + Shutdown all event loop threads in the pool. + """ + while not self.pool.empty(): + loop, thread = self.pool.get_nowait() + loop.call_soon_threadsafe(loop.stop) + thread.join() + loop.close() + + @contextmanager + def get_event_loop(self) -> Generator[asyncio.AbstractEventLoop, None, None]: + """ + Get an event loop from the pool. If no event loop is available, create a new one. + + After the context manager exits, the event loop is returned to the pool for reuse. + """ + loop, thread = self.__get_event_loop_and_thread() + try: + yield loop + finally: + self.__return_event_loop(loop, thread) diff --git a/fastapi/pools/fastapi_app.py b/fastapi/pools/fastapi_app.py new file mode 100644 index 000000000..3508ea373 --- /dev/null +++ b/fastapi/pools/fastapi_app.py @@ -0,0 +1,52 @@ +# Copyright 2025 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). + +import queue +from collections import defaultdict +from contextlib import contextmanager +from typing import Generator + +from odoo.api import Environment + +from fastapi import FastAPI + + +class FastApiAppPool: + def __init__(self): + self._queue_by_db_by_root_path: dict[ + str, dict[str, queue.Queue[FastAPI]] + ] = defaultdict(lambda: defaultdict(queue.Queue)) + + def __get_app(self, env: Environment, root_path: str) -> FastAPI: + db_name = env.cr.dbname + try: + return self._queue_by_db_by_root_path[db_name][root_path].get_nowait() + except queue.Empty: + env["fastapi.endpoint"].sudo() + return env["fastapi.endpoint"].sudo().get_app(root_path) + + def __return_app(self, env: Environment, app: FastAPI, root_path: str) -> None: + db_name = env.cr.dbname + self._queue_by_db_by_root_path[db_name][root_path].put(app) + + @contextmanager + def get_app( + self, root_path: str, env: Environment + ) -> Generator[FastAPI, None, None]: + """Return a FastAPI app to be used in a context manager. + + The app is retrieved from the pool if available, otherwise a new one is created. + The app is returned to the pool after the context manager exits. + + When used into the FastApiDispatcher class this ensures that the app is reused + across multiple requests but only one request at a time uses an app. + """ + app = self.__get_app(env, root_path) + try: + yield app + finally: + self.__return_app(env, app, root_path) + + def invalidate(self, root_path: str, env: Environment) -> None: + db_name = env.cr.dbname + self._queue_by_db_by_root_path[db_name][root_path] = queue.Queue() From 2db95d5a4a02ec72814310cebd0703c3ea3b4bc5 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 10 Jan 2025 16:10:02 +0100 Subject: [PATCH 03/58] [FIX] fastapi: Graceful shutdown of event loop On server shutdown, ensure that created the event loops are closed properly. --- fastapi/pools/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fastapi/pools/__init__.py b/fastapi/pools/__init__.py index 08ab00781..31f1fb388 100644 --- a/fastapi/pools/__init__.py +++ b/fastapi/pools/__init__.py @@ -1,7 +1,11 @@ from .event_loop import EventLoopPool from .fastapi_app import FastApiAppPool +from odoo.service.server import CommonServer event_loop_pool = EventLoopPool() fastapi_app_pool = FastApiAppPool() + +CommonServer.on_stop(event_loop_pool.shutdown) + __all__ = ["event_loop_pool", "fastapi_app_pool"] From 1bf9751e64a8148fcc2b66b654b55b13b8ba4e98 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 10 Jan 2025 16:36:35 +0100 Subject: [PATCH 04/58] [FIX] fastapi: Ensure thread safety of the FastAPI app cache defaultdict in python is not thread safe. Since this data structure is used to store the cache of FastAPI apps, we must ensure that the access to this cache is thread safe. This is done by using a lock to protect the access to the cache. --- fastapi/pools/fastapi_app.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/fastapi/pools/fastapi_app.py b/fastapi/pools/fastapi_app.py index 3508ea373..ffdd4d5e8 100644 --- a/fastapi/pools/fastapi_app.py +++ b/fastapi/pools/fastapi_app.py @@ -2,6 +2,7 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). import queue +import threading from collections import defaultdict from contextlib import contextmanager from typing import Generator @@ -16,18 +17,25 @@ def __init__(self): self._queue_by_db_by_root_path: dict[ str, dict[str, queue.Queue[FastAPI]] ] = defaultdict(lambda: defaultdict(queue.Queue)) + self._lock = threading.Lock() - def __get_app(self, env: Environment, root_path: str) -> FastAPI: + def __get_pool(self, env: Environment, root_path: str) -> queue.Queue[FastAPI]: db_name = env.cr.dbname + with self._lock: + # default dict is not thread safe but the use + return self._queue_by_db_by_root_path[db_name][root_path] + + def __get_app(self, env: Environment, root_path: str) -> FastAPI: + pool = self.__get_pool(env, root_path) try: - return self._queue_by_db_by_root_path[db_name][root_path].get_nowait() + return pool.get_nowait() except queue.Empty: env["fastapi.endpoint"].sudo() return env["fastapi.endpoint"].sudo().get_app(root_path) def __return_app(self, env: Environment, app: FastAPI, root_path: str) -> None: - db_name = env.cr.dbname - self._queue_by_db_by_root_path[db_name][root_path].put(app) + pool = self.__get_pool(env, root_path) + pool.put(app) @contextmanager def get_app( @@ -48,5 +56,6 @@ def get_app( self.__return_app(env, app, root_path) def invalidate(self, root_path: str, env: Environment) -> None: - db_name = env.cr.dbname - self._queue_by_db_by_root_path[db_name][root_path] = queue.Queue() + with self._lock: + db_name = env.cr.dbname + self._queue_by_db_by_root_path[db_name][root_path] = queue.Queue() From e657680226992ee28df1ffa4d4c306f201838c06 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Mon, 13 Jan 2025 17:27:08 +0100 Subject: [PATCH 05/58] [IMP] fastapi: Improves app cache lifecycle This commit improves the lifecycle of the fastapi app cache. It first ensures that the cache is effectively invalidated when changes are made to the app configuration even if theses changes occur into an other server instance. It also remove the use of a locking mechanism put in place to ensure a thread safe access to a value into the cache to avoid potential concurrency issue when a default value is set to the cache at access time. This lock could lead to unnecessary contention and reduce the performance benefits of queue.Queue's fine-grained internal synchronization for a questionable gain. The only expected gain was to avoid the useless creation of a queue.Queue instance that would never be used since at the time of puting the value into the cache we are sure that a value is already present into the dictionary. --- fastapi/fastapi_dispatcher.py | 2 +- fastapi/models/fastapi_endpoint.py | 17 ++++-- fastapi/pools/fastapi_app.py | 85 +++++++++++++++++++++++++++--- 3 files changed, 91 insertions(+), 13 deletions(-) diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index 3edf29f14..3f2390d4d 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -25,7 +25,7 @@ def dispatch(self, endpoint, args): root_path = "/" + environ["PATH_INFO"].split("/")[1] # TODO store the env into contextvar to be used by the odoo_env # depends method - with fastapi_app_pool.get_app(root_path, request.env) as app: + with fastapi_app_pool.get_app(env=request.env, root_path=root_path) as app: uid = request.env["fastapi.endpoint"].sudo().get_uid(root_path) data = BytesIO() with self._manage_odoo_env(uid): diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 9db835e0f..25e3dc468 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -15,7 +15,6 @@ from .. import dependencies from ..middleware import ASGIMiddleware -from ..pools import fastapi_app_pool _logger = logging.getLogger(__name__) @@ -122,10 +121,10 @@ def _registered_endpoint_rule_keys(self): return tuple(res) @api.model - def _routing_impacting_fields(self) -> Tuple[str]: + def _routing_impacting_fields(self) -> Tuple[str, ...]: """The list of fields requiring to refresh the mount point of the pp into odoo if modified""" - return ("root_path",) + return ("root_path", "save_http_session") # # end of endpoint.route.sync.mixin methods implementation @@ -199,7 +198,17 @@ def _endpoint_registry_route_unique_key(self, routing: Dict[str, Any]): return f"{self._name}:{self.id}:{path}" def _reset_app(self): - fastapi_app_pool.invalidate(self.root_path, self.env) + self._reset_app_cache_marker.clear_cache(self) + + @tools.ormcache() + def _reset_app_cache_marker(self): + """This methos is used to get a way to mark the orm cache as dirty + when the app is reset. By marking the cache as dirty, the system + will signal to others instances that the cache is not up to date + and that they should invalidate their cache as well. This is required + to ensure that any change requiring a reset of the app is propagated + to all the running instances. + """ @api.model def get_app(self, root_path): diff --git a/fastapi/pools/fastapi_app.py b/fastapi/pools/fastapi_app.py index ffdd4d5e8..991374179 100644 --- a/fastapi/pools/fastapi_app.py +++ b/fastapi/pools/fastapi_app.py @@ -1,6 +1,6 @@ # Copyright 2025 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). - +import logging import queue import threading from collections import defaultdict @@ -11,19 +11,65 @@ from fastapi import FastAPI +_logger = logging.getLogger(__name__) + class FastApiAppPool: + """Pool of FastAPI apps. + + This class manages a pool of FastAPI apps. The pool is organized by database name + and root path. Each pool is a queue of FastAPI apps. + + The pool is used to reuse FastAPI apps across multiple requests. This is useful + to avoid the overhead of creating a new FastAPI app for each request. The pool + ensures that only one request at a time uses an app. + + The proper way to use the pool is to use the get_app method as a context manager. + This ensures that the app is returned to the pool after the context manager exits. + The get_app method is designed to ensure that the app made available to the + caller is unique and not used by another caller at the same time. + + .. code-block:: python + + with fastapi_app_pool.get_app(env=request.env, root_path=root_path) as app: + # use the app + + The pool is invalidated when the cache registry is updated. This ensures that + the pool is always up-to-date with the latest app configuration. It also + ensures that the invalidation is done even in the case of a modification occurring + in a different worker process or thread or server instance. This mechanism + works because every time an attribute of the fastapi.endpoint model is modified + and this attribute is part of the list returned by the `_fastapi_app_fields`, + or `_routing_impacting_fields` methods, we reset the cache of a marker method + `_reset_app_cache_marker`. As side effect, the cache registry is marked to be + updated by the increment of the `cache_sequence` SQL sequence. This cache sequence + on the registry is reloaded from the DB on each request made to a specific database. + When an app is retrieved from the pool, we always compare the cache sequence of + the pool with the cache sequence of the registry. If the two sequences are different, + we invalidate the pool and save the new cache sequence on the pool. + + The cache is based on a defaultdict of defaultdict of queue.Queue. We are cautious + that the use of defaultdict is not thread-safe for operations that modify the + dictionary. However the only operation that modifies the dictionary is the + first access to a new key. If two threads access the same key at the same time, + the two threads will create two different queues. This is not a problem since + at the time of returning an app to the pool, we are sure that a queue exists + for the key into the cache and all the created apps are returned to the same + valid queue. And the end, the lack of thread-safety for the defaultdict could + only lead to a negligible overhead of creating a new queue that will never be + used. This is why we consider that the use of defaultdict is safe in this context. + """ + def __init__(self): self._queue_by_db_by_root_path: dict[ str, dict[str, queue.Queue[FastAPI]] ] = defaultdict(lambda: defaultdict(queue.Queue)) + self.__cache_sequence = 0 self._lock = threading.Lock() def __get_pool(self, env: Environment, root_path: str) -> queue.Queue[FastAPI]: db_name = env.cr.dbname - with self._lock: - # default dict is not thread safe but the use - return self._queue_by_db_by_root_path[db_name][root_path] + return self._queue_by_db_by_root_path[db_name][root_path] def __get_app(self, env: Environment, root_path: str) -> FastAPI: pool = self.__get_pool(env, root_path) @@ -39,7 +85,7 @@ def __return_app(self, env: Environment, app: FastAPI, root_path: str) -> None: @contextmanager def get_app( - self, root_path: str, env: Environment + self, env: Environment, root_path: str ) -> Generator[FastAPI, None, None]: """Return a FastAPI app to be used in a context manager. @@ -49,13 +95,36 @@ def get_app( When used into the FastApiDispatcher class this ensures that the app is reused across multiple requests but only one request at a time uses an app. """ + self._check_cache(env) app = self.__get_app(env, root_path) try: yield app finally: self.__return_app(env, app, root_path) - def invalidate(self, root_path: str, env: Environment) -> None: - with self._lock: - db_name = env.cr.dbname + @property + def cache_sequence(self) -> int: + return self.__cache_sequence + + @cache_sequence.setter + def cache_sequence(self, value: int) -> None: + if value != self.__cache_sequence: + with self._lock: + self.__cache_sequence = value + + def _check_cache(self, env: Environment) -> None: + cache_sequence = env.registry.cache_sequence + if cache_sequence != self.cache_sequence and self.cache_sequence != 0: + _logger.info( + "Cache registry updated, reset fastapi_app pool for the current " + "database" + ) + self.invalidate(env) + self.cache_sequence = cache_sequence + + def invalidate(self, env: Environment, root_path: str | None = None) -> None: + db_name = env.cr.dbname + if root_path: self._queue_by_db_by_root_path[db_name][root_path] = queue.Queue() + elif db_name in self._queue_by_db_by_root_path: + del self._queue_by_db_by_root_path[db_name] From 488e90be9959fbff256606d5ad3c7466ca1eba41 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 25 Feb 2025 12:42:50 +0100 Subject: [PATCH 06/58] [FIX] extendable_fastapi: Inherit from last declared fastapi dispatcher --- extendable_fastapi/fastapi_dispatcher.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extendable_fastapi/fastapi_dispatcher.py b/extendable_fastapi/fastapi_dispatcher.py index b940df9ce..10fcfaf22 100644 --- a/extendable_fastapi/fastapi_dispatcher.py +++ b/extendable_fastapi/fastapi_dispatcher.py @@ -3,6 +3,8 @@ from contextlib import contextmanager +from odoo.http import _dispatchers + from odoo.addons.extendable.registry import _extendable_registries_database from odoo.addons.fastapi.fastapi_dispatcher import ( FastApiDispatcher as BaseFastApiDispatcher, @@ -11,7 +13,9 @@ from extendable import context -class FastApiDispatcher(BaseFastApiDispatcher): +# Inherit from last registered fastapi dispatcher +# This handles multiple overload of dispatchers +class FastApiDispatcher(_dispatchers.get("fastapi", BaseFastApiDispatcher)): routing_type = "fastapi" def dispatch(self, endpoint, args): From f175fa644396c871d66d6c32ed63b0d27f6deed9 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 25 Feb 2025 12:57:09 +0100 Subject: [PATCH 07/58] [ADD] fastapi_encrypted_errors --- fastapi_encrypted_errors/README.rst | 99 ++++ fastapi_encrypted_errors/__init__.py | 3 + fastapi_encrypted_errors/__manifest__.py | 25 + .../fastapi_dispatcher.py | 35 ++ fastapi_encrypted_errors/models/__init__.py | 1 + .../models/fastapi_endpoint.py | 47 ++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 3 + fastapi_encrypted_errors/readme/USAGE.rst | 7 + .../security/ir.model.access.csv | 2 + .../static/description/index.html | 439 ++++++++++++++++++ fastapi_encrypted_errors/tests/__init__.py | 1 + .../tests/test_fastapi_encrypted_errors.py | 74 +++ .../views/fastapi_endpoint_views.xml | 31 ++ fastapi_encrypted_errors/wizards/__init__.py | 1 + .../wizards/wizard_fastapi_decrypt_errors.py | 40 ++ .../wizard_fastapi_decrypt_errors_views.xml | 47 ++ requirements.txt | 1 + .../odoo/addons/fastapi_encrypted_errors | 1 + setup/fastapi_encrypted_errors/setup.py | 6 + 20 files changed, 866 insertions(+) create mode 100644 fastapi_encrypted_errors/README.rst create mode 100644 fastapi_encrypted_errors/__init__.py create mode 100644 fastapi_encrypted_errors/__manifest__.py create mode 100644 fastapi_encrypted_errors/fastapi_dispatcher.py create mode 100644 fastapi_encrypted_errors/models/__init__.py create mode 100644 fastapi_encrypted_errors/models/fastapi_endpoint.py create mode 100644 fastapi_encrypted_errors/readme/CONTRIBUTORS.rst create mode 100644 fastapi_encrypted_errors/readme/DESCRIPTION.rst create mode 100644 fastapi_encrypted_errors/readme/USAGE.rst create mode 100644 fastapi_encrypted_errors/security/ir.model.access.csv create mode 100644 fastapi_encrypted_errors/static/description/index.html create mode 100644 fastapi_encrypted_errors/tests/__init__.py create mode 100644 fastapi_encrypted_errors/tests/test_fastapi_encrypted_errors.py create mode 100644 fastapi_encrypted_errors/views/fastapi_endpoint_views.xml create mode 100644 fastapi_encrypted_errors/wizards/__init__.py create mode 100644 fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py create mode 100644 fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml create mode 120000 setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors create mode 100644 setup/fastapi_encrypted_errors/setup.py diff --git a/fastapi_encrypted_errors/README.rst b/fastapi_encrypted_errors/README.rst new file mode 100644 index 000000000..7027f164e --- /dev/null +++ b/fastapi_encrypted_errors/README.rst @@ -0,0 +1,99 @@ +======================== +FastAPI Encrypted Errors +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:296fbef824a5eb64e9bbaedc382ef15e41d146f0cc59d458c0d76de46a54358e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_encrypted_errors + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi_encrypted_errors + :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/rest-framework&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a "ref" field in the error response of FastAPI. +This field is an AES encrypted string that contains the error message / traceback. +This encrypted string can be decrypted using the endpoint decrypt error wizard. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +First you have to enable the encryption for an endpoint by checking the `Encrypt Errors` checkbox +in the endpoint configuration. + +To decrypt an error message, you can use the "Decrypt Error" wizard in the +FastAPI menu. + +You can regenerate a new key by clicking on the "Regenerate Key" button next to the `Errors Secret Key` field. + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_: + + * Florian Mounier + +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-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainer `__: + +|maintainer-paradoxxxzero| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi_encrypted_errors/__init__.py b/fastapi_encrypted_errors/__init__.py new file mode 100644 index 000000000..d7f65dfa1 --- /dev/null +++ b/fastapi_encrypted_errors/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import wizards +from . import fastapi_dispatcher diff --git a/fastapi_encrypted_errors/__manifest__.py b/fastapi_encrypted_errors/__manifest__.py new file mode 100644 index 000000000..254901404 --- /dev/null +++ b/fastapi_encrypted_errors/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "FastAPI Encrypted Errors", + "summary": "Adds encrypted error messages to FastAPI error responses.", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Akretion,Odoo Community Association (OCA)", + "maintainers": ["paradoxxxzero"], + "website": "https://github.com/OCA/rest-framework", + "depends": [ + "fastapi", + ], + "data": [ + "security/ir.model.access.csv", + "views/fastapi_endpoint_views.xml", + "wizards/wizard_fastapi_decrypt_errors_views.xml", + ], + "demo": [], + "external_dependencies": { + "python": ["cryptography"], + }, +} diff --git a/fastapi_encrypted_errors/fastapi_dispatcher.py b/fastapi_encrypted_errors/fastapi_dispatcher.py new file mode 100644 index 000000000..a3f0524a2 --- /dev/null +++ b/fastapi_encrypted_errors/fastapi_dispatcher.py @@ -0,0 +1,35 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.http import _dispatchers + +from odoo.addons.fastapi.error_handlers import convert_exception_to_status_body +from odoo.addons.fastapi.fastapi_dispatcher import ( + FastApiDispatcher as BaseFastApiDispatcher, +) + + +# Inherit from last registered fastapi dispatcher +# This handles multiple overload of dispatchers +class FastApiDispatcher(_dispatchers.get("fastapi", BaseFastApiDispatcher)): + routing_type = "fastapi" + + def handle_error(self, exc): + environ = self._get_environ() + root_path = "/" + environ["PATH_INFO"].split("/")[1] + fastapi_endpoint = ( + self.request.env["fastapi.endpoint"] + .sudo() + .search([("root_path", "=", root_path)]) + ) + if fastapi_endpoint.encrypt_errors: + headers = getattr(exc, "headers", None) + status_code, body = convert_exception_to_status_body(exc) + if body: + body["ref"] = fastapi_endpoint._encrypt_error(exc) + return self.request.make_json_response( + body, status=status_code, headers=headers + ) + + return super().handle_error(exc) diff --git a/fastapi_encrypted_errors/models/__init__.py b/fastapi_encrypted_errors/models/__init__.py new file mode 100644 index 000000000..b825fab92 --- /dev/null +++ b/fastapi_encrypted_errors/models/__init__.py @@ -0,0 +1 @@ +from . import fastapi_endpoint diff --git a/fastapi_encrypted_errors/models/fastapi_endpoint.py b/fastapi_encrypted_errors/models/fastapi_endpoint.py new file mode 100644 index 000000000..add3ca2ec --- /dev/null +++ b/fastapi_encrypted_errors/models/fastapi_endpoint.py @@ -0,0 +1,47 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import traceback +import zlib + +from cryptography.fernet import Fernet + +from odoo import fields, models + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + encrypt_errors = fields.Boolean( + help="Encrypt errors before sending them to the client.", + ) + encrypted_errors_secret_key = fields.Char( + help="The secret key used to encrypt errors before sending them to the client.", + default=lambda _: Fernet.generate_key(), + readonly=True, + ) + + def action_generate_encrypted_errors_secret_key(self): + for record in self: + record.encrypted_errors_secret_key = Fernet.generate_key() + + def _encrypt_error(self, exc): + self.ensure_one() + if not self.encrypt_errors or not self.encrypted_errors_secret_key: + return + + # Get full traceback + error = "".join(traceback.format_exception(exc)) + # zlib compression works quite well on tracebacks + error = zlib.compress(error.encode("utf-8")) + f = Fernet(self.encrypted_errors_secret_key) + return f.encrypt(error) + + def _decrypt_error(self, error): + self.ensure_one() + if not self.encrypt_errors or not self.encrypted_errors_secret_key: + return + + f = Fernet(self.encrypted_errors_secret_key) + error = f.decrypt(error) + return zlib.decompress(error).decode("utf-8") diff --git a/fastapi_encrypted_errors/readme/CONTRIBUTORS.rst b/fastapi_encrypted_errors/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..a4d0ad922 --- /dev/null +++ b/fastapi_encrypted_errors/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Akretion `_: + + * Florian Mounier diff --git a/fastapi_encrypted_errors/readme/DESCRIPTION.rst b/fastapi_encrypted_errors/readme/DESCRIPTION.rst new file mode 100644 index 000000000..3ea245c61 --- /dev/null +++ b/fastapi_encrypted_errors/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module adds a "ref" field in the error response of FastAPI. +This field is an AES encrypted string that contains the error message / traceback. +This encrypted string can be decrypted using the endpoint decrypt error wizard. diff --git a/fastapi_encrypted_errors/readme/USAGE.rst b/fastapi_encrypted_errors/readme/USAGE.rst new file mode 100644 index 000000000..041077d9b --- /dev/null +++ b/fastapi_encrypted_errors/readme/USAGE.rst @@ -0,0 +1,7 @@ +First you have to enable the encryption for an endpoint by checking the `Encrypt Errors` checkbox +in the endpoint configuration. + +To decrypt an error message, you can use the "Decrypt Error" wizard in the +FastAPI menu. + +You can regenerate a new key by clicking on the "Regenerate Key" button next to the `Errors Secret Key` field. diff --git a/fastapi_encrypted_errors/security/ir.model.access.csv b/fastapi_encrypted_errors/security/ir.model.access.csv new file mode 100644 index 000000000..d102f0c82 --- /dev/null +++ b/fastapi_encrypted_errors/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fastapi_wizard_auth_partner_impersonate,wizard_fastapi_decrypt_errors,model_wizard_fastapi_decrypt_errors,fastapi.group_fastapi_manager,1,1,1,1 diff --git a/fastapi_encrypted_errors/static/description/index.html b/fastapi_encrypted_errors/static/description/index.html new file mode 100644 index 000000000..0ffce082e --- /dev/null +++ b/fastapi_encrypted_errors/static/description/index.html @@ -0,0 +1,439 @@ + + + + + +FastAPI Encrypted Errors + + + +
+

FastAPI Encrypted Errors

+ + +

Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

This module adds a “ref” field in the error response of FastAPI. +This field is an AES encrypted string that contains the error message / traceback. +This encrypted string can be decrypted using the endpoint decrypt error wizard.

+

Table of contents

+ +
+

Usage

+

First you have to enable the encryption for an endpoint by checking the Encrypt Errors checkbox +in the endpoint configuration.

+

To decrypt an error message, you can use the “Decrypt Error” wizard in the +FastAPI menu.

+

You can regenerate a new key by clicking on the “Regenerate Key” button next to the Errors Secret Key field.

+
+
+

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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

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.

+

Current maintainer:

+

paradoxxxzero

+

This module is part of the OCA/rest-framework project on GitHub.

+

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

+
+
+
+ + diff --git a/fastapi_encrypted_errors/tests/__init__.py b/fastapi_encrypted_errors/tests/__init__.py new file mode 100644 index 000000000..8fe3d60a6 --- /dev/null +++ b/fastapi_encrypted_errors/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fastapi_encrypted_errors diff --git a/fastapi_encrypted_errors/tests/test_fastapi_encrypted_errors.py b/fastapi_encrypted_errors/tests/test_fastapi_encrypted_errors.py new file mode 100644 index 000000000..8aa1174f7 --- /dev/null +++ b/fastapi_encrypted_errors/tests/test_fastapi_encrypted_errors.py @@ -0,0 +1,74 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import os +import unittest + +from odoo.tests.common import HttpCase + +from odoo.addons.fastapi.schemas import DemoExceptionType + +from fastapi import status + + +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") +class FastAPIEncryptedErrorsCase(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") + cls.fastapi_demo_app._handle_registry_sync() + cls.fastapi_demo_app.write({"encrypt_errors": True}) + lang = ( + cls.env["res.lang"] + .with_context(active_test=False) + .search([("code", "=", "fr_BE")]) + ) + lang.active = True + + def test_encrypted_errors_in_response(self): + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.user_error.value}" + "&error_message=User Error" + ) + response = self.url_open(route, timeout=200) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + res = response.json() + self.assertEqual(res["detail"], "User Error") + self.assertIn("ref", res) + + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.bare_exception.value}" + "&error_message=Internal Server Error" + ) + response = self.url_open(route, timeout=200) + self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) + res = response.json() + self.assertEqual(res["detail"], "Internal Server Error") + self.assertIn("ref", res) + + def test_encrypted_errors_decrypt(self): + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.bare_exception.value}" + "&error_message=Internal Server Error" + ) + response = self.url_open(route, timeout=200) + self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) + res = response.json() + self.assertEqual(res["detail"], "Internal Server Error") + self.assertIn("ref", res) + ref = res["ref"] + self.assertNotIn("Traceback (most recent call last)", ref) + self.assertNotIn("NotImplementedError: Internal Server Error", ref) + + wizard = self.env["wizard.fastapi.decrypt.errors"].create({"error": ref}) + wizard.action_decrypt_error() + self.assertIn("Traceback (most recent call last)", wizard.decrypted_error) + self.assertIn( + "NotImplementedError: Internal Server Error", wizard.decrypted_error + ) diff --git a/fastapi_encrypted_errors/views/fastapi_endpoint_views.xml b/fastapi_encrypted_errors/views/fastapi_endpoint_views.xml new file mode 100644 index 000000000..fce00f36f --- /dev/null +++ b/fastapi_encrypted_errors/views/fastapi_endpoint_views.xml @@ -0,0 +1,31 @@ + + + + + + fastapi.endpoint + + + + +
+
+
+
+
+
diff --git a/fastapi_encrypted_errors/wizards/__init__.py b/fastapi_encrypted_errors/wizards/__init__.py new file mode 100644 index 000000000..a160dabbb --- /dev/null +++ b/fastapi_encrypted_errors/wizards/__init__.py @@ -0,0 +1 @@ +from . import wizard_fastapi_decrypt_errors diff --git a/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py new file mode 100644 index 000000000..d19bd6d3a --- /dev/null +++ b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors.py @@ -0,0 +1,40 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import traceback + +from odoo import fields, models + + +class WizardFastapiDecryptErrors(models.TransientModel): + _name = "wizard.fastapi.decrypt.errors" + _description = "Wizard to decrypt FastAPI errors" + + error = fields.Text(required=True) + fastapi_endpoint_id = fields.Many2one( + "fastapi.endpoint", + string="FastAPI Endpoint", + required=True, + default=lambda self: self.env["fastapi.endpoint"].search([], limit=1), + ) + decrypted_error = fields.Text(readonly=True) + + def action_decrypt_error(self): + self.ensure_one() + try: + error = self.fastapi_endpoint_id._decrypt_error(self.error.encode("utf-8")) + except Exception: + self.decrypted_error = ( + "Error while decrypting error: \n\n" + traceback.format_exc() + ) + else: + self.decrypted_error = error + + return { + "type": "ir.actions.act_window", + "res_model": "wizard.fastapi.decrypt.errors", + "view_mode": "form", + "view_type": "form", + "res_id": self.id, + "target": "new", + } diff --git a/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml new file mode 100644 index 000000000..62b8a803d --- /dev/null +++ b/fastapi_encrypted_errors/wizards/wizard_fastapi_decrypt_errors_views.xml @@ -0,0 +1,47 @@ + + + + + wizard.fastapi.decrypt.errors + +
+ + + + + +
+
+ +
+
+
+ + + Decrypt Error + wizard.fastapi.decrypt.errors + ir.actions.act_window + form + new + + + + + +
diff --git a/requirements.txt b/requirements.txt index 7e0b84839..fe14d9447 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ apispec apispec>=4.0.0 cerberus contextvars +cryptography extendable-pydantic extendable-pydantic>=1.2.0 extendable>=0.0.4 diff --git a/setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors b/setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors new file mode 120000 index 000000000..101a9234a --- /dev/null +++ b/setup/fastapi_encrypted_errors/odoo/addons/fastapi_encrypted_errors @@ -0,0 +1 @@ +../../../../fastapi_encrypted_errors \ No newline at end of file diff --git a/setup/fastapi_encrypted_errors/setup.py b/setup/fastapi_encrypted_errors/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/fastapi_encrypted_errors/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 983b2ebbee9dfa6d8a8d34bfdda45e8f597f734b Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 26 Feb 2025 19:18:35 +0100 Subject: [PATCH 08/58] [FIX] fastapi: headers is not a dict, it can have multiple values for a key --- fastapi/fastapi_dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index bfb56825c..096293158 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -51,7 +51,7 @@ def handle_error(self, exc): def _make_response(self, status_mapping, headers_tuple, content): self.status = status_mapping[:3] - self.headers = dict(headers_tuple) + self.headers = headers_tuple # in case of exception, the method asgi_done_callback of the # ASGIResponder will trigger an "a2wsgi.error" event with the exception # instance stored in a tuple with the type of the exception and the traceback. From 5026f7b12d6d738fcb9302229828db7e7e7f7b6c Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 14 Mar 2025 13:37:41 +0100 Subject: [PATCH 09/58] [FIX] fastapi: remove useless code --- fastapi/pools/fastapi_app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fastapi/pools/fastapi_app.py b/fastapi/pools/fastapi_app.py index 991374179..29725c49c 100644 --- a/fastapi/pools/fastapi_app.py +++ b/fastapi/pools/fastapi_app.py @@ -76,7 +76,6 @@ def __get_app(self, env: Environment, root_path: str) -> FastAPI: try: return pool.get_nowait() except queue.Empty: - env["fastapi.endpoint"].sudo() return env["fastapi.endpoint"].sudo().get_app(root_path) def __return_app(self, env: Environment, app: FastAPI, root_path: str) -> None: From 1e73351275c1f54f04020300362a93f810446dd5 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 14 Mar 2025 12:47:19 +0000 Subject: [PATCH 10/58] [BOT] post-merge updates --- README.md | 2 +- fastapi/README.rst | 2 +- fastapi/__manifest__.py | 2 +- fastapi/static/description/index.html | 186 +++++++++++++------------- 4 files changed, 96 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index e941e7b5f..f4b0e7f4c 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ addon | version | maintainers | summary [datamodel](datamodel/) | 16.0.1.0.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Extendable classes registry loader for Odoo [extendable_fastapi](extendable_fastapi/) | 16.0.2.1.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Allows the use of extendable into fastapi apps -[fastapi](fastapi/) | 16.0.1.4.5 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Odoo FastAPI endpoint +[fastapi](fastapi/) | 16.0.1.5.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Odoo FastAPI endpoint [fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | JWT bearer token authentication for FastAPI. [fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Test/demo module for fastapi_auth_jwt. [graphql_base](graphql_base/) | 16.0.1.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Base GraphQL/GraphiQL controller diff --git a/fastapi/README.rst b/fastapi/README.rst index 574ecc086..be5dbc0f1 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:ccbcb06116d31f370fa16dda9fb82b273ff770e72e77a346c20ef68a4150500f + !! source digest: sha256:f2c0f5d73971853a3588843d02c9deffd2bce00bfbdcaac73ac9c11851e7a9cc !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index fa0e6a0d4..db63f08d5 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.4.5", + "version": "16.0.1.5.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index 3e40636da..562ec9d8f 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -367,7 +367,7 @@

Odoo FastAPI

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

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

This addon provides the basis to smoothly integrate the FastAPI @@ -505,9 +505,9 @@

What’s building an API with fas

Then, you need to declare your app by defining a model that inherits from ‘fastapi.endpoint’ and add your app name into the app field. For example:

-from odoo import fields, models
+from odoo import fields, models
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -530,10 +530,10 @@ 

What’s building an API with fas

Now, you can create your first router. For that, you need to define a global variable into your fastapi_endpoint module called for example ‘demo_api_router’

-from fastapi import APIRouter
-from odoo import fields, models
+from fastapi import APIRouter
+from odoo import fields, models
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -547,10 +547,10 @@ 

What’s building an API with fas

To make your router available to your app, you need to add it to the list of routers returned by the _get_fastapi_routers method of your fastapi_endpoint model.

-from fastapi import APIRouter
-from odoo import api, fields, models
+from fastapi import APIRouter
+from odoo import api, fields, models
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -558,7 +558,7 @@ 

What’s building an API with fas selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} ) - def _get_fastapi_routers(self): + def _get_fastapi_routers(self): if self.app == "demo": return [demo_api_router] return super()._get_fastapi_routers() @@ -569,17 +569,17 @@

What’s building an API with fas

Now, you can start adding routes to your router. For example, let’s add a route that returns a list of partners.

-from typing import Annotated
+from typing import Annotated
 
-from fastapi import APIRouter
-from pydantic import BaseModel
+from fastapi import APIRouter
+from pydantic import BaseModel
 
-from odoo import api, fields, models
-from odoo.api import Environment
+from odoo import api, fields, models
+from odoo.api import Environment
 
-from odoo.addons.fastapi.dependencies import odoo_env
+from odoo.addons.fastapi.dependencies import odoo_env
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -587,7 +587,7 @@ 

What’s building an API with fas selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} ) - def _get_fastapi_routers(self): + def _get_fastapi_routers(self): if self.app == "demo": return [demo_api_router] return super()._get_fastapi_routers() @@ -595,12 +595,12 @@

What’s building an API with fas # create a router demo_api_router = APIRouter() -class PartnerInfo(BaseModel): +class PartnerInfo(BaseModel): name: str email: str @demo_api_router.get("/partners", response_model=list[PartnerInfo]) -def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]: +def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]: return [ PartnerInfo(name=partner.name, email=partner.email) for partner in env["res.partner"].search([]) @@ -665,13 +665,13 @@

Dealing with the odoo environment function returns the current odoo environment. You can use it to access the odoo models and the database from your route handlers.

-from typing import Annotated
+from typing import Annotated
 
-from odoo.api import Environment
-from odoo.addons.fastapi.dependencies import odoo_env
+from odoo.api import Environment
+from odoo.addons.fastapi.dependencies import odoo_env
 
 @demo_api_router.get("/partners", response_model=list[PartnerInfo])
-def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]:
+def get_partners(env: Annotated[Environment, Depends(odoo_env)]) -> list[PartnerInfo]:
     return [
         PartnerInfo(name=partner.name, email=partner.email)
         for partner in env["res.partner"].search([])
@@ -719,7 +719,7 @@ 

The dependency injection mechanis that the method depends of two parameters: ‘endpoint_id’ and ‘env’. Each of these parameters are dependencies themselves.

-def fastapi_endpoint_id() -> int:
+def fastapi_endpoint_id() -> int:
     """This method is overriden by default to make the fastapi.endpoint record
     available for your endpoint method. To get the fastapi.endpoint record
     in your method, you just need to add a dependency on the fastapi_endpoint method
@@ -727,7 +727,7 @@ 

The dependency injection mechanis """ -def fastapi_endpoint( +def fastapi_endpoint( _id: Annotated[int, Depends(fastapi_endpoint_id)], env: Annotated[Environment, Depends(odoo_env)], ) -> "FastapiEndpoint": @@ -743,7 +743,7 @@

The dependency injection mechanis registering a specific method that returns the id of the current fastapi endpoint model instance for the original method.

-def _get_app(self) -> FastAPI:
+def _get_app(self) -> FastAPI:
     app = FastAPI(**self._prepare_fastapi_endpoint_params())
     for router in self._get_fastapi_routers():
         app.include_router(prefix=self.root_path, router=router)
@@ -764,11 +764,11 @@ 

The authentication mechanism<

When you define a route handler, you can inject the ‘authenticated_partner’ dependency as a parameter of your route handler.

-from odoo.addons.base.models.res_partner import Partner
+from odoo.addons.base.models.res_partner import Partner
 
 
 @demo_api_router.get("/partners", response_model=list[PartnerInfo])
-def get_partners(
+def get_partners(
     env: Annotated[Environment, Depends(odoo_env)], partner: Annotated[Partner, Depends(authenticated_partner)]
 ) -> list[PartnerInfo]:
     return [
@@ -784,7 +784,7 @@ 

The authentication mechanism< ‘odoo.addons.fastapi.dependencies’ module and relies on functionalities provided by the ‘fastapi.security’ module.

-def authenticated_partner(
+def authenticated_partner(
     env: Annotated[Environment, Depends(odoo_env)],
     security: Annotated[HTTPBasicCredentials, Depends(HTTPBasic())],
 ) -> "res.partner":
@@ -822,9 +822,9 @@ 

The authentication mechanism< authentication by using an api key or via basic auth. Since basic auth is already implemented, we will only implement the api key authentication mechanism.

-from fastapi.security import APIKeyHeader
+from fastapi.security import APIKeyHeader
 
-def api_key_based_authenticated_partner_impl(
+def api_key_based_authenticated_partner_impl(
     api_key: Annotated[str, Depends(
         APIKeyHeader(
             name="api-key",
@@ -849,9 +849,9 @@ 

The authentication mechanism< can allows the user to select one of these authentication mechanisms by adding a selection field on the fastapi endpoint model.

-from odoo import fields, models
+from odoo import fields, models
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -874,8 +874,8 @@ 

The authentication mechanism< provide the right implementation of the ‘authenticated_partner’ dependency when the app is instantiated.

-from odoo.addons.fastapi.dependencies import authenticated_partner
-class FastapiEndpoint(models.Model):
+from odoo.addons.fastapi.dependencies import authenticated_partner
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -887,7 +887,7 @@ 

The authentication mechanism< string="Authenciation method", ) - def _get_app(self) -> FastAPI: + def _get_app(self) -> FastAPI: app = super()._get_app() if self.app == "demo": # Here we add the overrides to the authenticated_partner_impl method @@ -926,10 +926,10 @@

Managing configuration parameters dependency method to retrieve the ‘fastapi.endpoint’ record associated to the current request.

-from pydantic import BaseModel, Field
-from odoo.addons.fastapi.dependencies import fastapi_endpoint
+from pydantic import BaseModel, Field
+from odoo.addons.fastapi.dependencies import fastapi_endpoint
 
-class EndpointAppInfo(BaseModel):
+class EndpointAppInfo(BaseModel):
   id: str
   name: str
   app: str
@@ -943,7 +943,7 @@ 

Managing configuration parameters response_model=EndpointAppInfo, dependencies=[Depends(authenticated_partner)], ) - async def endpoint_app_info( + async def endpoint_app_info( endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], ) -> EndpointAppInfo: """Returns the current endpoint configuration""" @@ -962,7 +962,7 @@

Managing configuration parameters name of the fields that impact the instantiation of the app into the returned list.

-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
@@ -975,7 +975,7 @@ 

Managing configuration parameters ) @api.model - def _fastapi_app_fields(self) -> List[str]: + def _fastapi_app_fields(self) -> List[str]: fields = super()._fastapi_app_fields() fields.append("demo_auth_method") return fields @@ -1035,16 +1035,16 @@

Changing the implementation of th inherit from the model where the implementation is defined and override the method ‘echo’.

-from pydantic import BaseModel
-from fastapi import Depends, APIRouter
-from odoo import models
-from odoo.addons.fastapi.dependencies import odoo_env
+from pydantic import BaseModel
+from fastapi import Depends, APIRouter
+from odoo import models
+from odoo.addons.fastapi.dependencies import odoo_env
 
-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
-    def _get_fastapi_routers(self) -> List[APIRouter]:
+    def _get_fastapi_routers(self) -> List[APIRouter]:
         routers = super()._get_fastapi_routers()
         routers.append(demo_api_router)
         return routers
@@ -1056,29 +1056,29 @@ 

Changing the implementation of th response_model=EchoResponse, dependencies=[Depends(odoo_env)], ) -async def echo( +async def echo( message: str, odoo_env: Annotated[Environment, Depends(odoo_env)], ) -> EchoResponse: """Echo the message""" return EchoResponse(message=odoo_env["demo.fastapi.endpoint"].echo(message)) -class EchoResponse(BaseModel): +class EchoResponse(BaseModel): message: str -class DemoEndpoint(models.AbstractModel): +class DemoEndpoint(models.AbstractModel): _name = "demo.fastapi.endpoint" _description = "Demo Endpoint" - def echo(self, message: str) -> str: + def echo(self, message: str) -> str: return message -class DemoEndpointInherit(models.AbstractModel): +class DemoEndpointInherit(models.AbstractModel): _inherit = "demo.fastapi.endpoint" - def echo(self, message: str) -> str: + def echo(self, message: str) -> str: return f"Hello {message}"

@@ -1109,14 +1109,14 @@

Adding a new route handler
-from odoo.addons.fastapi.models.fastapi_endpoint_demo import demo_api_router
+from odoo.addons.fastapi.models.fastapi_endpoint_demo import demo_api_router
 
 @demo_api_router.get(
     "/echo2",
     response_model=EchoResponse,
     dependencies=[Depends(odoo_env)],
 )
-async def echo2(
+async def echo2(
     message: str,
     odoo_env: Annotated[Environment, Depends(odoo_env)],
 ) -> EchoResponse:
@@ -1131,11 +1131,11 @@ 

Adding a new route handler‘_get_fastapi_routers’ of the model ‘fastapi.endpoint’ you are inheriting from into your new addon.

-class FastapiEndpoint(models.Model):
+class FastapiEndpoint(models.Model):
 
     _inherit = "fastapi.endpoint"
 
-    def _get_fastapi_routers(self) -> List[APIRouter]:
+    def _get_fastapi_routers(self) -> List[APIRouter]:
         routers = super()._get_fastapi_routers()
         if self.app == "demo":
             routers.append(additional_demo_api_router)
@@ -1148,7 +1148,7 @@ 

Adding a new route handler response_model=EchoResponse, dependencies=[Depends(odoo_env)], ) -async def echo2( +async def echo2( message: str, odoo_env: Annotated[Environment, Depends(odoo_env)], ) -> EchoResponse: @@ -1174,10 +1174,10 @@

Extending the model used as para

When you want to allow other addons to extend a pydantic model, you must first define the model as an extendable model by using a dedicated metaclass

-from pydantic import BaseModel
-from extendable_pydantic import ExtendableModelMeta
+from pydantic import BaseModel
+from extendable_pydantic import ExtendableModelMeta
 
-class Partner(BaseModel, metaclass=ExtendableModelMeta):
+class Partner(BaseModel, metaclass=ExtendableModelMeta):
   name = 0.1
   model_config = ConfigDict(from_attributes=True)
 
@@ -1190,7 +1190,7 @@

Extending the model used as para response_model=Location, dependencies=[Depends(authenticated_partner)], ) -async def partner( +async def partner( partner: Annotated[ResPartner, Depends(authenticated_partner)], ) -> Partner: """Return the location""" @@ -1199,10 +1199,10 @@

Extending the model used as para

If you need to add a new field into the model ‘Partner’, you can extend it in your new addon by defining a new model that inherits from the model ‘Partner’.

-from typing import Optional
-from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner
+from typing import Optional
+from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner
 
-class PartnerExtended(Partner, extends=Partner):
+class PartnerExtended(Partner, extends=Partner):
     email: Optional[str]
 

If your new addon is installed in a database, a call to the route handler @@ -1320,20 +1320,20 @@

How to test your fastapi app tests for routers in an addon that doesn’t provide a fastapi endpoint).

With this base class, writing a test for a route handler is as simple as:

-from odoo.fastapi.tests.common import FastAPITransactionCase
+from odoo.fastapi.tests.common import FastAPITransactionCase
 
-from odoo.addons.fastapi import dependencies
-from odoo.addons.fastapi.routers import demo_router
+from odoo.addons.fastapi import dependencies
+from odoo.addons.fastapi.routers import demo_router
 
-class FastAPIDemoCase(FastAPITransactionCase):
+class FastAPIDemoCase(FastAPITransactionCase):
 
     @classmethod
-    def setUpClass(cls) -> None:
+    def setUpClass(cls) -> None:
         super().setUpClass()
         cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user")
         cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"})
 
-    def test_hello_world(self) -> None:
+    def test_hello_world(self) -> None:
         with self._create_test_client(router=demo_router) as test_client:
             response: Response = test_client.get("/demo/")
         self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -1343,20 +1343,20 @@ 

How to test your fastapi app have created a test client for the whole app by not specifying the router but the app instead.

-from odoo.fastapi.tests.common import FastAPITransactionCase
+from odoo.fastapi.tests.common import FastAPITransactionCase
 
-from odoo.addons.fastapi import dependencies
-from odoo.addons.fastapi.routers import demo_router
+from odoo.addons.fastapi import dependencies
+from odoo.addons.fastapi.routers import demo_router
 
-class FastAPIDemoCase(FastAPITransactionCase):
+class FastAPIDemoCase(FastAPITransactionCase):
 
     @classmethod
-    def setUpClass(cls) -> None:
+    def setUpClass(cls) -> None:
         super().setUpClass()
         cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user")
         cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create({"name": "FastAPI Demo"})
 
-    def test_hello_world(self) -> None:
+    def test_hello_world(self) -> None:
         demo_endpoint = self.env.ref("fastapi.fastapi_endpoint_demo")
         with self._create_test_client(app=demo_endpoint._get_app()) as test_client:
             response: Response = test_client.get(f"{demo_endpoint.root_path}/demo/")
@@ -1491,14 +1491,14 @@ 

Development of a search route ha handler enclosed in a json document that contains the count of records.
-from typing import Annotated
-from pydantic import BaseModel
+from typing import Annotated
+from pydantic import BaseModel
 
-from odoo.api import Environment
-from odoo.addons.fastapi.dependencies import paging, authenticated_partner_env
-from odoo.addons.fastapi.schemas import PagedCollection, Paging
+from odoo.api import Environment
+from odoo.addons.fastapi.dependencies import paging, authenticated_partner_env
+from odoo.addons.fastapi.schemas import PagedCollection, Paging
 
-class SaleOrder(BaseModel):
+class SaleOrder(BaseModel):
     id: int
     name: str
     model_config = ConfigDict(from_attributes=True)
@@ -1509,7 +1509,7 @@ 

Development of a search route ha response_model=PagedCollection[SaleOrder], response_model_exclude_unset=True, ) -def get_sale_orders( +def get_sale_orders( paging: Annotated[Paging, Depends(paging)], env: Annotated[Environment, Depends(authenticated_partner_env)], ) -> PagedCollection[SaleOrder]: @@ -1600,15 +1600,15 @@

FastAPI addons directory structu router = APIRouter(tags=["items"]) router.get("/items", response_model=List[Item]) -def list_items(): +def list_items(): pass

In the ‘__init__.py’ file, you will import the router and add it to the global router or your addon.

-from fastapi import APIRouter
+from fastapi import APIRouter
 
-from .items import router as items_router
+from .items import router as items_router
 
 router = APIRouter()
 router.include_router(items_router)
@@ -1621,22 +1621,22 @@ 

FastAPI addons directory structu For example, in your ‘my_model.py’ file, you will define a model like this:

-from pydantic import BaseModel
+from pydantic import BaseModel
 
-class MyModel(BaseModel):
+class MyModel(BaseModel):
     name: str
     description: str = None
 

In the ‘__init__.py’ file, you will import the model’s classes from the files in the directory.

-from .my_model import MyModel
+from .my_model import MyModel
 

This will allow to always import the models from the schemas module whatever the models are spread across different files or defined in the ‘schemas.py’ file.

-from x_api_addon.schemas import MyModel
+from x_api_addon.schemas import MyModel
 
  • The ‘dependencies.py’ file contains the custom dependencies that you From 5505a6ae4e1fd189c90c0ca404cf80a519c2bd4f Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Mon, 17 Mar 2025 14:11:27 +0100 Subject: [PATCH 11/58] [FIX] fastapi: Ensure no default eventloop thread is created Before this change, an eventloop thread was created on each instanciation of the ASGIMiddleware class. We want to avoid this behavior to avoid the creation of zombie threads --- fastapi/middleware.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fastapi/middleware.py b/fastapi/middleware.py index 8f63c2339..b88c40652 100644 --- a/fastapi/middleware.py +++ b/fastapi/middleware.py @@ -13,12 +13,26 @@ import a2wsgi from a2wsgi.asgi import ASGIResponder +from a2wsgi.asgi_typing import ASGIApp from a2wsgi.wsgi_typing import Environ, StartResponse from .pools import event_loop_pool class ASGIMiddleware(a2wsgi.ASGIMiddleware): + def __init__( + self, + app: ASGIApp, + wait_time: float | None = None, + ) -> None: + # We don't want to use the default event loop policy + # because we want to manage the event loop ourselves + # using the event loop pool. + # Since the the base class check if the given loop is + # None, we can pass False to avoid the initialization + # of the default event loop + super().__init__(app, wait_time, False) + def __call__( self, environ: Environ, start_response: StartResponse ) -> Iterable[bytes]: From cac7b3c9128ada28ea9c0bc3de08809eab477be5 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 2 Apr 2025 20:31:23 +0000 Subject: [PATCH 12/58] [BOT] post-merge updates --- README.md | 2 +- fastapi/README.rst | 2 +- fastapi/__manifest__.py | 2 +- fastapi/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f4b0e7f4c..668d37a41 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ addon | version | maintainers | summary [datamodel](datamodel/) | 16.0.1.0.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Extendable classes registry loader for Odoo [extendable_fastapi](extendable_fastapi/) | 16.0.2.1.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Allows the use of extendable into fastapi apps -[fastapi](fastapi/) | 16.0.1.5.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Odoo FastAPI endpoint +[fastapi](fastapi/) | 16.0.1.5.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Odoo FastAPI endpoint [fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | JWT bearer token authentication for FastAPI. [fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Test/demo module for fastapi_auth_jwt. [graphql_base](graphql_base/) | 16.0.1.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Base GraphQL/GraphiQL controller diff --git a/fastapi/README.rst b/fastapi/README.rst index be5dbc0f1..272e4806a 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:f2c0f5d73971853a3588843d02c9deffd2bce00bfbdcaac73ac9c11851e7a9cc + !! source digest: sha256:9007cc333e60840b5aef872ea7ee577f48b513c8b56fedd8ab7ae0e0623c677e !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index db63f08d5..3ecda5d9f 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.5.0", + "version": "16.0.1.5.1", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index 562ec9d8f..e4bda532f 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -367,7 +367,7 @@

    Odoo FastAPI

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:f2c0f5d73971853a3588843d02c9deffd2bce00bfbdcaac73ac9c11851e7a9cc +!! source digest: sha256:9007cc333e60840b5aef872ea7ee577f48b513c8b56fedd8ab7ae0e0623c677e !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI From b0f142a9357664e634a47e8c0a71bc2c9fa3a45c Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 10 Apr 2025 10:59:52 +0200 Subject: [PATCH 13/58] [IMP] base_rest_demo: Test to validate the retrying mechanism Add a new test to ensure that if a retryable error occurs when processing a call to a base_rest endpoint, the retrying mechanism of odoo works as expected --- base_rest_demo/services/exception_services.py | 42 +++++++++++++++++++ base_rest_demo/tests/test_exception.py | 14 +++++++ 2 files changed, 56 insertions(+) diff --git a/base_rest_demo/services/exception_services.py b/base_rest_demo/services/exception_services.py index afc39988b..d07d02774 100644 --- a/base_rest_demo/services/exception_services.py +++ b/base_rest_demo/services/exception_services.py @@ -1,6 +1,8 @@ # Copyright 2018 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from psycopg2 import errorcodes +from psycopg2.errors import OperationalError from werkzeug.exceptions import MethodNotAllowed from odoo import _ @@ -12,9 +14,13 @@ ValidationError, ) from odoo.http import SessionExpiredException +from odoo.service.model import MAX_TRIES_ON_CONCURRENCY_FAILURE +from odoo.addons.base_rest.components.service import to_int from odoo.addons.component.core import Component +_CPT_RETRY = 0 + class ExceptionService(Component): _inherit = "base.rest.service" @@ -90,6 +96,23 @@ def bare_exception(self): """ raise IOError("My IO error") + def retryable_error(self, nbr_retries): + """This method is used in the test suite to check that the retrying + functionality in case of concurrency error on the database is working + correctly for retryable exceptions. + + The output will be the number of retries that have been done. + + This method is mainly used to test the retrying functionality + """ + global _CPT_RETRY + if _CPT_RETRY < nbr_retries: + _CPT_RETRY += 1 + raise FakeConcurrentUpdateError("fake error") + tryno = _CPT_RETRY + _CPT_RETRY = 0 + return {"retries": tryno} + # Validator def _validator_user_error(self): return {} @@ -138,3 +161,22 @@ def _validator_bare_exception(self): def _validator_return_bare_exception(self): return {} + + def _validator_retryable_error(self): + return { + "nbr_retries": { + "type": "integer", + "required": True, + "default": MAX_TRIES_ON_CONCURRENCY_FAILURE, + "coerce": to_int, + } + } + + def _validator_return_retryable_error(self): + return {"retries": {"type": "integer"}} + + +class FakeConcurrentUpdateError(OperationalError): + @property + def pgcode(self): + return errorcodes.SERIALIZATION_FAILURE diff --git a/base_rest_demo/tests/test_exception.py b/base_rest_demo/tests/test_exception.py index 3f5aa134d..9d955e914 100644 --- a/base_rest_demo/tests/test_exception.py +++ b/base_rest_demo/tests/test_exception.py @@ -104,3 +104,17 @@ def test_bare_exception(self): self.assertEqual(response.headers["content-type"], "application/json") body = json.loads(response.content.decode("utf-8")) self.assertDictEqual(body, {"code": 500, "name": "Internal Server Error"}) + + @odoo.tools.mute_logger("odoo.addons.base_rest.http", "odoo.http") + def test_retrying(self): + """Test that the retrying mechanism is working as expected with the + FastAPI endpoints in case of POST request with a file. + """ + nbr_retries = 3 + response = self.url_open( + "%s/retryable_error" % self.url, + '{"nbr_retries": %d}' % nbr_retries, + timeout=20000, + ) + self.assertEqual(response.status_code, 200, response.content) + self.assertDictEqual(response.json(), {"retries": nbr_retries}) From a3bae8c57c493d35843779b0475cc06ba9a0206f Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 10 Apr 2025 11:04:50 +0200 Subject: [PATCH 14/58] [FIX] rest_log: Don't prevent the retrying mechanism In case of a retryable error, the initial error must bubble up to the retrying mechanism. If this kind of error is wrapped into another one, the retrying mechanism no more works --- rest_log/components/service.py | 11 ++++++ rest_log/tests/common.py | 10 +++++ rest_log/tests/test_db_logging.py | 65 ++++++++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/rest_log/components/service.py b/rest_log/components/service.py index bbb45503f..9aedbf11b 100644 --- a/rest_log/components/service.py +++ b/rest_log/components/service.py @@ -7,10 +7,12 @@ import logging import traceback +from psycopg2.errors import OperationalError from werkzeug.urls import url_encode, url_join from odoo import exceptions, registry from odoo.http import Response, request +from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY from odoo.addons.base_rest.http import JSONEncoder from odoo.addons.component.core import AbstractComponent @@ -111,6 +113,15 @@ def _dispatch_exception( log_entry_url = self._get_log_entry_url(log_entry) except Exception as e: _logger.exception("Rest Log Error Creation: %s", e) + # let the OperationalError bubble up to the retrying mechanism + # We can't wrap the OperationalError because we want to let it + # bubble up to the retrying mechanism, it will be handled by + # the default handler at the end of the chain. + if ( + isinstance(orig_exception, OperationalError) + and orig_exception.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY + ): + raise orig_exception raise exception_klass(exc_msg, log_entry_url) from orig_exception def _get_exception_message(self, exception): diff --git a/rest_log/tests/common.py b/rest_log/tests/common.py index f43a4db47..63e16bd01 100644 --- a/rest_log/tests/common.py +++ b/rest_log/tests/common.py @@ -4,6 +4,9 @@ import contextlib +from psycopg2 import errorcodes +from psycopg2.errors import OperationalError + from odoo import exceptions from odoo.addons.base_rest import restapi @@ -42,6 +45,7 @@ def fail(self, how): "value": ValueError, "validation": exceptions.ValidationError, "user": exceptions.UserError, + "retryable": FakeConcurrentUpdateError, } raise exc[how]("Failed as you wanted!") @@ -61,3 +65,9 @@ def _get_mocked_request(self, env=None, httprequest=None, extra_headers=None): headers.update(extra_headers or {}) mocked_request.httprequest.headers = headers yield mocked_request + + +class FakeConcurrentUpdateError(OperationalError): + @property + def pgcode(self): + return errorcodes.SERIALIZATION_FAILURE diff --git a/rest_log/tests/test_db_logging.py b/rest_log/tests/test_db_logging.py index 01c1991ae..998f0c8e0 100644 --- a/rest_log/tests/test_db_logging.py +++ b/rest_log/tests/test_db_logging.py @@ -13,7 +13,7 @@ from odoo.addons.component.tests.common import new_rollbacked_env from odoo.addons.rest_log import exceptions as log_exceptions # pylint: disable=W7950 -from .common import TestDBLoggingMixin +from .common import FakeConcurrentUpdateError, TestDBLoggingMixin class TestDBLogging(TransactionRestServiceRegistryCase, TestDBLoggingMixin): @@ -374,3 +374,66 @@ def test_log_exception_value(self): self._test_exception( "value", log_exceptions.RESTServiceDispatchException, "ValueError", "severe" ) + + +class TestDBLoggingRetryableError( + TransactionRestServiceRegistryCase, TestDBLoggingMixin +): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_registry(cls) + + @classmethod + def tearDownClass(cls): + # pylint: disable=W8110 + cls._teardown_registry(cls) + super().tearDownClass() + + def _test_exception(self, test_type, wrapping_exc, exc_name, severity): + log_model = self.env["rest.log"].sudo() + initial_entries = log_model.search([]) + # Context: we are running in a transaction case which uses savepoints. + # The log machinery is going to rollback the transation when catching errors. + # Hence we need a completely separated env for the service. + with new_rollbacked_env() as new_env: + # Init fake collection w/ new env + collection = _PseudoCollection(self._collection_name, new_env) + service = self._get_service(self, collection=collection) + with self._get_mocked_request(env=new_env): + try: + service.dispatch("fail", test_type) + except Exception as err: + # Not using `assertRaises` to inspect the exception directly + self.assertTrue(isinstance(err, wrapping_exc)) + self.assertEqual( + service._get_exception_message(err), "Failed as you wanted!" + ) + + with new_rollbacked_env() as new_env: + log_model = new_env["rest.log"].sudo() + entry = log_model.search([]) - initial_entries + expected = { + "collection": service._collection, + "state": "failed", + "result": "null", + "exception_name": exc_name, + "exception_message": "Failed as you wanted!", + "severity": severity, + } + self.assertRecordValues(entry, [expected]) + + @staticmethod + def _get_test_controller(class_or_instance, root_path=None): + return super()._get_test_controller( + class_or_instance, root_path="/test_log_exception_retryable/" + ) + + def test_log_exception_retryable(self): + # retryable error must bubble up to the retrying mechanism + self._test_exception( + "retryable", + FakeConcurrentUpdateError, + "odoo.addons.rest_log.tests.common.FakeConcurrentUpdateError", + "warning", + ) From 90ae5a4dabe491bd294a767ec5aa9d5e959249dd Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 10 Apr 2025 09:50:09 +0000 Subject: [PATCH 15/58] [BOT] post-merge updates --- README.md | 2 +- fastapi/README.rst | 2 +- fastapi/__manifest__.py | 2 +- fastapi/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 668d37a41..83eb9f635 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ addon | version | maintainers | summary [datamodel](datamodel/) | 16.0.1.0.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Extendable classes registry loader for Odoo [extendable_fastapi](extendable_fastapi/) | 16.0.2.1.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Allows the use of extendable into fastapi apps -[fastapi](fastapi/) | 16.0.1.5.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Odoo FastAPI endpoint +[fastapi](fastapi/) | 16.0.1.6.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Odoo FastAPI endpoint [fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | JWT bearer token authentication for FastAPI. [fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Test/demo module for fastapi_auth_jwt. [graphql_base](graphql_base/) | 16.0.1.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Base GraphQL/GraphiQL controller diff --git a/fastapi/README.rst b/fastapi/README.rst index 272e4806a..d12578ae5 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -7,7 +7,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:9007cc333e60840b5aef872ea7ee577f48b513c8b56fedd8ab7ae0e0623c677e + !! source digest: sha256:1a9e09ed26e6e827518e1712413b5b62003b71f34e738c845ad3a0712505104f !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index 3ecda5d9f..2a62f9ca2 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.5.1", + "version": "16.0.1.6.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index e4bda532f..f7468b32d 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -367,7 +367,7 @@

    Odoo FastAPI

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:9007cc333e60840b5aef872ea7ee577f48b513c8b56fedd8ab7ae0e0623c677e +!! source digest: sha256:1a9e09ed26e6e827518e1712413b5b62003b71f34e738c845ad3a0712505104f !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI From e0e48e3c555ccf4ce24b95655e1b1e1a6b59e247 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 10 Apr 2025 11:21:03 +0000 Subject: [PATCH 16/58] [BOT] post-merge updates --- README.md | 2 +- extendable_fastapi/README.rst | 2 +- extendable_fastapi/__manifest__.py | 2 +- extendable_fastapi/static/description/index.html | 14 ++++++++------ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 83eb9f635..36f9724b1 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ addon | version | maintainers | summary [base_rest_pydantic](base_rest_pydantic/) | 16.0.2.0.1 | | Pydantic binding for base_rest [datamodel](datamodel/) | 16.0.1.0.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Extendable classes registry loader for Odoo -[extendable_fastapi](extendable_fastapi/) | 16.0.2.1.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Allows the use of extendable into fastapi apps +[extendable_fastapi](extendable_fastapi/) | 16.0.2.1.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Allows the use of extendable into fastapi apps [fastapi](fastapi/) | 16.0.1.6.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Odoo FastAPI endpoint [fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | JWT bearer token authentication for FastAPI. [fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Test/demo module for fastapi_auth_jwt. diff --git a/extendable_fastapi/README.rst b/extendable_fastapi/README.rst index 4213d94dc..2a93a308f 100644 --- a/extendable_fastapi/README.rst +++ b/extendable_fastapi/README.rst @@ -7,7 +7,7 @@ Extendable Fastapi !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:4e4f5d96294f860ce7f0c4e023431f8ed9ca011c318b5ba4a3cfcd15c31eac1a + !! source digest: sha256:1c3abf7259ae8390271785fb3b8f7754dadf175dc1530a27386ae9a77635e4b2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/extendable_fastapi/__manifest__.py b/extendable_fastapi/__manifest__.py index 56bb68bc1..44bc64aa1 100644 --- a/extendable_fastapi/__manifest__.py +++ b/extendable_fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Extendable Fastapi", "summary": """ Allows the use of extendable into fastapi apps""", - "version": "16.0.2.1.1", + "version": "16.0.2.1.2", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/extendable_fastapi/static/description/index.html b/extendable_fastapi/static/description/index.html index 88bfd690b..fdf779b2d 100644 --- a/extendable_fastapi/static/description/index.html +++ b/extendable_fastapi/static/description/index.html @@ -1,4 +1,3 @@ - @@ -9,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -367,7 +367,7 @@

    Extendable Fastapi

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:4e4f5d96294f860ce7f0c4e023431f8ed9ca011c318b5ba4a3cfcd15c31eac1a +!! source digest: sha256:1c3abf7259ae8390271785fb3b8f7754dadf175dc1530a27386ae9a77635e4b2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon is a technical addon used to allows the use of @@ -443,7 +443,9 @@

    Contributors

    Maintainers

    This module is maintained by the OCA.

    -Odoo Community Association + +Odoo Community Association +

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

    From a2d1a400e905941f5d506e866d25a4e0308039db Mon Sep 17 00:00:00 2001 From: Sebastiano Picchi Date: Thu, 24 Apr 2025 09:42:57 +0000 Subject: [PATCH 17/58] Translated using Weblate (Italian) Currently translated at 100.0% (3 of 3 strings) Translation: rest-framework-16.0/rest-framework-16.0-fastapi_auth_jwt_demo Translate-URL: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi_auth_jwt_demo/it/ --- fastapi_auth_jwt_demo/i18n/it.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fastapi_auth_jwt_demo/i18n/it.po b/fastapi_auth_jwt_demo/i18n/it.po index 8fb0db61e..5bb4f8b21 100644 --- a/fastapi_auth_jwt_demo/i18n/it.po +++ b/fastapi_auth_jwt_demo/i18n/it.po @@ -6,15 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-01-15 11:35+0000\n" -"Last-Translator: mymage \n" +"PO-Revision-Date: 2025-04-24 12:23+0000\n" +"Last-Translator: Sebastiano Picchi \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.17\n" +"X-Generator: Weblate 5.10.4\n" #. module: fastapi_auth_jwt_demo #: model:ir.model.fields,field_description:fastapi_auth_jwt_demo.field_fastapi_endpoint__app @@ -29,4 +29,4 @@ msgstr "Autorizzazione endpoint demo JWT" #. module: fastapi_auth_jwt_demo #: model:ir.model,name:fastapi_auth_jwt_demo.model_fastapi_endpoint msgid "FastAPI Endpoint" -msgstr "Endopoint FastAPI" +msgstr "Endpoint FastAPI" From e24654c6f44d4ab7b508ad7959656d151b6807fa Mon Sep 17 00:00:00 2001 From: Sebastiano Picchi Date: Thu, 24 Apr 2025 09:42:23 +0000 Subject: [PATCH 18/58] Translated using Weblate (Italian) Currently translated at 100.0% (43 of 43 strings) Translation: rest-framework-16.0/rest-framework-16.0-fastapi Translate-URL: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi/it/ --- fastapi/i18n/it.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fastapi/i18n/it.po b/fastapi/i18n/it.po index a1ab5e904..a55194a6a 100644 --- a/fastapi/i18n/it.po +++ b/fastapi/i18n/it.po @@ -6,15 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-10-03 10:06+0000\n" -"Last-Translator: mymage \n" +"PO-Revision-Date: 2025-04-24 12:23+0000\n" +"Last-Translator: Sebastiano Picchi \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.6.2\n" +"X-Generator: Weblate 5.10.4\n" #. module: fastapi #: model:ir.model.fields,help:fastapi.field_fastapi_endpoint__description @@ -104,7 +104,7 @@ msgstr "FastAPI" #: model:ir.model,name:fastapi.model_fastapi_endpoint #: model:ir.ui.menu,name:fastapi.fastapi_endpoint_menu msgid "FastAPI Endpoint" -msgstr "Endopoint FastAPI" +msgstr "Endpoint FastAPI" #. module: fastapi #: model:res.groups,name:fastapi.group_fastapi_endpoint_runner From 91e529cf0dc9d7216eb862465079bfd4dd526468 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Wed, 14 May 2025 11:39:18 +0200 Subject: [PATCH 19/58] [FIX] datamodel: Not compatible with marshmallow >= 4.0.0 --- datamodel/__manifest__.py | 4 ++- datamodel/static/description/index.html | 36 +++++++++++++------------ requirements.txt | 1 + 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index fbef3d494..c8fd79a1f 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -12,6 +12,8 @@ "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", "maintainers": ["lmignon"], "website": "https://github.com/OCA/rest-framework", - "external_dependencies": {"python": ["marshmallow", "marshmallow-objects>=2.0.0"]}, + "external_dependencies": { + "python": ["marshmallow<4.0.0", "marshmallow-objects>=2.0.0"] + }, "installable": True, } diff --git a/datamodel/static/description/index.html b/datamodel/static/description/index.html index 7b11ce833..fca018e56 100644 --- a/datamodel/static/description/index.html +++ b/datamodel/static/description/index.html @@ -1,4 +1,3 @@ - @@ -9,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -393,20 +393,20 @@

    Usage

    To define your own datamodel you just need to create a class that inherits from odoo.addons.datamodel.core.Datamodel

    -from marshmallow import fields
    +from marshmallow import fields
     
    -from odoo.addons.base_rest import restapi
    -from odoo.addons.component.core import Component
    -from odoo.addons.datamodel.core import Datamodel
    +from odoo.addons.base_rest import restapi
    +from odoo.addons.component.core import Component
    +from odoo.addons.datamodel.core import Datamodel
     
     
    -class PartnerShortInfo(Datamodel):
    +class PartnerShortInfo(Datamodel):
         _name = "partner.short.info"
     
         id = fields.Integer(required=True, allow_none=False)
         name = fields.String(required=True, allow_none=False)
     
    -class PartnerInfo(Datamodel):
    +class PartnerInfo(Datamodel):
         _name = "partner.info"
         _inherit = "partner.short.info"
     
    @@ -419,18 +419,18 @@ 

    Usage

    As for odoo models, you can extend the base datamodel by inheriting of base.

    -class Base(Datamodel):
    +class Base(Datamodel):
         _inherit = "base"
     
    -    def _my_method(self):
    +    def _my_method(self):
             pass
     

    Datamodels are available through the datamodels registry provided by the Odoo’s environment.

    -class ResPartner(Model):
    +class ResPartner(Model):
         _inherit = "res.partner"
     
    -    def _to_partner_info(self):
    +    def _to_partner_info(self):
             PartnerInfo = self.env.datamodels["partner.info"]
             partner_info = PartnerInfo(partial=True)
             partner_info.id = partner.id
    @@ -445,10 +445,10 @@ 

    Usage

    The Odoo’s environment is also available into the datamodel instance.

    -class MyDataModel(Datamodel):
    +class MyDataModel(Datamodel):
         _name = "my.data.model"
     
    -    def _my_method(self):
    +    def _my_method(self):
             partners = self.env["res.partner"].search([])
     
    @@ -498,7 +498,9 @@

    Contributors

    Maintainers

    This module is maintained by the OCA.

    -Odoo Community Association + +Odoo Community Association +

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

    diff --git a/requirements.txt b/requirements.txt index 7e0b84839..bf2e66750 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ graphql_server jsondiff marshmallow marshmallow-objects>=2.0.0 +marshmallow<4.0.0 parse-accept-language pydantic pydantic>=2.0.0 From 3285f490f525be57530e89be2b55b7410ad4c724 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 14 May 2025 13:34:04 +0000 Subject: [PATCH 20/58] [BOT] post-merge updates --- README.md | 2 +- datamodel/README.rst | 2 +- datamodel/__manifest__.py | 2 +- datamodel/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 36f9724b1..6cba02a1a 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ addon | version | maintainers | summary [base_rest_datamodel](base_rest_datamodel/) | 16.0.1.0.0 | | Datamodel binding for base_rest [base_rest_demo](base_rest_demo/) | 16.0.2.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Demo addon for Base REST [base_rest_pydantic](base_rest_pydantic/) | 16.0.2.0.1 | | Pydantic binding for base_rest -[datamodel](datamodel/) | 16.0.1.0.1 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization +[datamodel](datamodel/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Extendable classes registry loader for Odoo [extendable_fastapi](extendable_fastapi/) | 16.0.2.1.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Allows the use of extendable into fastapi apps [fastapi](fastapi/) | 16.0.1.6.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Odoo FastAPI endpoint diff --git a/datamodel/README.rst b/datamodel/README.rst index 1209730ea..bad3f8c95 100644 --- a/datamodel/README.rst +++ b/datamodel/README.rst @@ -7,7 +7,7 @@ Datamodel !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:5411d4f742eb933a4d05f5f6e1784a7ddc042e7f22b1c08d535e225c306a6955 + !! source digest: sha256:220702c6d930c27e2dbb4016e1f6bef6e1b9c7b96cff4b3c5ad27fdc5733ad26 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index c8fd79a1f..97b1a6310 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "16.0.1.0.1", + "version": "16.0.1.0.2", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", diff --git a/datamodel/static/description/index.html b/datamodel/static/description/index.html index fca018e56..b290f07ce 100644 --- a/datamodel/static/description/index.html +++ b/datamodel/static/description/index.html @@ -367,7 +367,7 @@

    Datamodel

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:5411d4f742eb933a4d05f5f6e1784a7ddc042e7f22b1c08d535e225c306a6955 +!! source digest: sha256:220702c6d930c27e2dbb4016e1f6bef6e1b9c7b96cff4b3c5ad27fdc5733ad26 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon allows you to define simple data models supporting serialization/deserialization From 01013f6f5ce27f3ec1ec9a1e019b623272679d79 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 14 May 2025 13:37:43 +0000 Subject: [PATCH 21/58] [BOT] post-merge updates --- README.md | 4 ++-- base_rest_demo/README.rst | 2 +- base_rest_demo/__manifest__.py | 2 +- base_rest_demo/static/description/index.html | 14 ++++++++------ rest_log/README.rst | 2 +- rest_log/__manifest__.py | 2 +- rest_log/static/description/index.html | 2 +- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6cba02a1a..646fcee39 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ addon | version | maintainers | summary [base_rest](base_rest/) | 16.0.1.0.3 | | Develop your own high level REST APIs for Odoo thanks to this addon. [base_rest_auth_api_key](base_rest_auth_api_key/) | 16.0.1.0.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Base Rest: Add support for the auth_api_key security policy into the openapi documentation [base_rest_datamodel](base_rest_datamodel/) | 16.0.1.0.0 | | Datamodel binding for base_rest -[base_rest_demo](base_rest_demo/) | 16.0.2.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Demo addon for Base REST +[base_rest_demo](base_rest_demo/) | 16.0.2.0.3 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Demo addon for Base REST [base_rest_pydantic](base_rest_pydantic/) | 16.0.2.0.1 | | Pydantic binding for base_rest [datamodel](datamodel/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Extendable classes registry loader for Odoo @@ -35,7 +35,7 @@ addon | version | maintainers | summary [graphql_base](graphql_base/) | 16.0.1.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Base GraphQL/GraphiQL controller [graphql_demo](graphql_demo/) | 16.0.1.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | GraphQL Demo [pydantic](pydantic/) | 16.0.1.0.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Utility addon to ease mapping between Pydantic and Odoo models -[rest_log](rest_log/) | 16.0.1.0.2 | [![simahawk](https://github.com/simahawk.png?size=30px)](https://github.com/simahawk) | Track REST API calls into DB +[rest_log](rest_log/) | 16.0.1.0.3 | [![simahawk](https://github.com/simahawk.png?size=30px)](https://github.com/simahawk) | Track REST API calls into DB Unported addons diff --git a/base_rest_demo/README.rst b/base_rest_demo/README.rst index fe4048f21..ad4e0eae6 100644 --- a/base_rest_demo/README.rst +++ b/base_rest_demo/README.rst @@ -7,7 +7,7 @@ Base Rest Demo !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:50ac989c1e6343bcb3f97a2f8df7392224cb7d06989f88fba9a14a17ae40d3dd + !! source digest: sha256:d55a089f3426610ea4b4762a7e51b8572ae0852a03bf126d2eb6ff4a5c1451f9 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/base_rest_demo/__manifest__.py b/base_rest_demo/__manifest__.py index 3a161363d..a9efda8c8 100644 --- a/base_rest_demo/__manifest__.py +++ b/base_rest_demo/__manifest__.py @@ -5,7 +5,7 @@ "name": "Base Rest Demo", "summary": """ Demo addon for Base REST""", - "version": "16.0.2.0.2", + "version": "16.0.2.0.3", "development_status": "Beta", "license": "LGPL-3", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", diff --git a/base_rest_demo/static/description/index.html b/base_rest_demo/static/description/index.html index 58bb0ebf0..3c5ed59ae 100644 --- a/base_rest_demo/static/description/index.html +++ b/base_rest_demo/static/description/index.html @@ -1,4 +1,3 @@ - @@ -9,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -367,7 +367,7 @@

    Base Rest Demo

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:50ac989c1e6343bcb3f97a2f8df7392224cb7d06989f88fba9a14a17ae40d3dd +!! source digest: sha256:d55a089f3426610ea4b4762a7e51b8572ae0852a03bf126d2eb6ff4a5c1451f9 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    Demo addon to illustrate how to develop self documented REST services thanks @@ -442,7 +442,9 @@

    Contributors

    Maintainers

    This module is maintained by the OCA.

    -Odoo Community Association + +Odoo Community Association +

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

    diff --git a/rest_log/README.rst b/rest_log/README.rst index d7dcae803..dfed37602 100644 --- a/rest_log/README.rst +++ b/rest_log/README.rst @@ -7,7 +7,7 @@ REST Log !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:f93e58dbd77af1254adb0cd7ace54af0b0609569883ed4a22028f11f0b663139 + !! source digest: sha256:ed7eb7cec756c78c39eb3dc04ca382ea64926defada6d97aa72e859db389d8b2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/rest_log/__manifest__.py b/rest_log/__manifest__.py index 60efce45b..478bee1cd 100644 --- a/rest_log/__manifest__.py +++ b/rest_log/__manifest__.py @@ -5,7 +5,7 @@ { "name": "REST Log", "summary": "Track REST API calls into DB", - "version": "16.0.1.0.2", + "version": "16.0.1.0.3", "development_status": "Beta", "website": "https://github.com/OCA/rest-framework", "author": "Camptocamp, ACSONE, Odoo Community Association (OCA)", diff --git a/rest_log/static/description/index.html b/rest_log/static/description/index.html index 3c9f139e5..56f26b2bf 100644 --- a/rest_log/static/description/index.html +++ b/rest_log/static/description/index.html @@ -367,7 +367,7 @@

    REST Log

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

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    When exposing REST services is often useful to see what’s happening From ed9d8bc23a81cda171010e77f759353a613fb123 Mon Sep 17 00:00:00 2001 From: PicchiSeba Date: Thu, 27 Feb 2025 12:42:09 +0100 Subject: [PATCH 22/58] [IMP]fastapi: obtain endpoint via common method --- fastapi/fastapi_dispatcher.py | 6 +++--- fastapi/models/fastapi_endpoint.py | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index c41beb2d2..63aedd4f4 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -27,11 +27,11 @@ def dispatch(self, endpoint, args): # don't parse the httprequest let starlette parse the stream self.request.params = {} # dict(self.request.get_http_params(), **args) environ = self._get_environ() - root_path = "/" + environ["PATH_INFO"].split("/")[1] + path = environ["PATH_INFO"] # TODO store the env into contextvar to be used by the odoo_env # depends method - with fastapi_app_pool.get_app(env=request.env, root_path=root_path) as app: - uid = request.env["fastapi.endpoint"].sudo().get_uid(root_path) + with fastapi_app_pool.get_app(env=request.env, root_path=path) as app: + uid = request.env["fastapi.endpoint"].sudo().get_uid(path) data = BytesIO() with self._manage_odoo_env(uid): for r in app(environ, self._make_response): diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 25e3dc468..45dd13319 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -211,8 +211,15 @@ def _reset_app_cache_marker(self): """ @api.model - def get_app(self, root_path): - record = self.search([("root_path", "=", root_path)]) + @tools.ormcache("path") + def get_endpoint(self, path): + root_path = "/" + path.split("/")[1] + endpoint = self.search([("root_path", "=", root_path)])[:1] or False + return endpoint + + @api.model + def get_app(self, path): + record = self.get_endpoint(path) if not record: return None app = FastAPI() @@ -236,9 +243,9 @@ def _clear_fastapi_exception_handlers(self, app: FastAPI) -> None: self._clear_fastapi_exception_handlers(route.app) @api.model - @tools.ormcache("root_path") - def get_uid(self, root_path): - record = self.search([("root_path", "=", root_path)]) + @tools.ormcache("path") + def get_uid(self, path): + record = self.get_endpoint(path) if not record: return None return record.user_id.id From ebd6afe74e7b5029c3a594ba14dabb20dbca6c61 Mon Sep 17 00:00:00 2001 From: jesusvmayor Date: Mon, 26 May 2025 20:00:43 +0200 Subject: [PATCH 23/58] [FIX]base_rest: Fix rest controllers inheritance. --- base_rest/controllers/main.py | 7 ------- base_rest_demo/tests/test_controller.py | 21 ++++++++------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/base_rest/controllers/main.py b/base_rest/controllers/main.py index b4166d741..a768fb70f 100644 --- a/base_rest/controllers/main.py +++ b/base_rest/controllers/main.py @@ -96,13 +96,6 @@ class ControllerB(ControllerB): @classmethod def __init_subclass__(cls): - if ( - "RestController" in globals() - and RestController in cls.__bases__ - and Controller not in cls.__bases__ - ): - # Ensure that Controller's __init_subclass__ kicks in. - cls.__bases__ += (Controller,) super().__init_subclass__() if "RestController" not in globals() or not any( issubclass(b, RestController) for b in cls.__bases__ diff --git a/base_rest_demo/tests/test_controller.py b/base_rest_demo/tests/test_controller.py index 678e52618..43a84f489 100644 --- a/base_rest_demo/tests/test_controller.py +++ b/base_rest_demo/tests/test_controller.py @@ -18,20 +18,15 @@ def test_controller_registry(self): # at the end of the start process, our tow controllers must into the # controller registered controllers = Controller.children_classes.get("base_rest_demo", []) - - self.assertIn( - BaseRestDemoPrivateApiController, - controllers, + self.assertTrue( + any([issubclass(x, BaseRestDemoPrivateApiController) for x in controllers]) ) - self.assertIn( - BaseRestDemoPublicApiController, - controllers, + self.assertTrue( + any([issubclass(x, BaseRestDemoPublicApiController) for x in controllers]) ) - self.assertIn( - BaseRestDemoNewApiController, - controllers, + self.assertTrue( + any([issubclass(x, BaseRestDemoNewApiController) for x in controllers]) ) - self.assertIn( - BaseRestDemoJwtApiController, - controllers, + self.assertTrue( + any([issubclass(x, BaseRestDemoJwtApiController) for x in controllers]) ) From c1855a0c01595b0c21127293ff75a48c0541837b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 28 May 2025 09:00:14 +0000 Subject: [PATCH 24/58] [BOT] post-merge updates --- README.md | 4 +- base_rest/README.rst | 2 +- base_rest/__manifest__.py | 2 +- base_rest/static/description/index.html | 60 ++++++++++---------- base_rest_demo/README.rst | 2 +- base_rest_demo/__manifest__.py | 2 +- base_rest_demo/static/description/index.html | 2 +- 7 files changed, 37 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 646fcee39..8a995725f 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ Available addons ---------------- addon | version | maintainers | summary --- | --- | --- | --- -[base_rest](base_rest/) | 16.0.1.0.3 | | Develop your own high level REST APIs for Odoo thanks to this addon. +[base_rest](base_rest/) | 16.0.1.0.4 | | Develop your own high level REST APIs for Odoo thanks to this addon. [base_rest_auth_api_key](base_rest_auth_api_key/) | 16.0.1.0.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Base Rest: Add support for the auth_api_key security policy into the openapi documentation [base_rest_datamodel](base_rest_datamodel/) | 16.0.1.0.0 | | Datamodel binding for base_rest -[base_rest_demo](base_rest_demo/) | 16.0.2.0.3 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Demo addon for Base REST +[base_rest_demo](base_rest_demo/) | 16.0.2.0.4 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Demo addon for Base REST [base_rest_pydantic](base_rest_pydantic/) | 16.0.2.0.1 | | Pydantic binding for base_rest [datamodel](datamodel/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Extendable classes registry loader for Odoo diff --git a/base_rest/README.rst b/base_rest/README.rst index 0bac51c24..3556e64b2 100644 --- a/base_rest/README.rst +++ b/base_rest/README.rst @@ -7,7 +7,7 @@ Base Rest !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:35fb4211e3cdea0ba35463860d4d0cc7bc2909ab39e5b5af9a6733ced5b96232 + !! source digest: sha256:517d5b1d74542047b404d2130e5d9239fe591f43b1a89ca02339766c8c8a6584 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/base_rest/__manifest__.py b/base_rest/__manifest__.py index 27f38e2c5..1b6d113ad 100644 --- a/base_rest/__manifest__.py +++ b/base_rest/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ Develop your own high level REST APIs for Odoo thanks to this addon. """, - "version": "16.0.1.0.3", + "version": "16.0.1.0.4", "development_status": "Beta", "license": "LGPL-3", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", diff --git a/base_rest/static/description/index.html b/base_rest/static/description/index.html index 459de1d5e..b26ae50ef 100644 --- a/base_rest/static/description/index.html +++ b/base_rest/static/description/index.html @@ -367,7 +367,7 @@

    Base Rest

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:35fb4211e3cdea0ba35463860d4d0cc7bc2909ab39e5b5af9a6733ced5b96232 +!! source digest: sha256:517d5b1d74542047b404d2130e5d9239fe591f43b1a89ca02339766c8c8a6584 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon is deprecated and not fully supported anymore on Odoo 16. @@ -450,10 +450,10 @@

    Usage

  • Other methods are only accessible via HTTP POST routes <string:_service_name> or <string:_service_name>/<string:method_name> or <string:_service_name>/<int:_id> or <string:_service_name>/<int:_id>/<string:method_name>
  • -from odoo.addons.component.core import Component
    +from odoo.addons.component.core import Component
     
     
    -class PingService(Component):
    +class PingService(Component):
         _inherit = 'base.rest.service'
         _name = 'ping.service'
         _usage = 'ping'
    @@ -461,46 +461,46 @@ 

    Usage

    # The following method are 'public' and can be called from the controller. - def get(self, _id, message): + def get(self, _id, message): return { 'response': 'Get called with message ' + message} - def search(self, message): + def search(self, message): return { 'response': 'Search called search with message ' + message} - def update(self, _id, message): + def update(self, _id, message): return {'response': 'PUT called with message ' + message} # pylint:disable=method-required-super - def create(self, **params): + def create(self, **params): return {'response': 'POST called with message ' + params['message']} - def delete(self, _id): + def delete(self, _id): return {'response': 'DELETE called with id %s ' % _id} # Validator - def _validator_search(self): + def _validator_search(self): return {'message': {'type': 'string'}} # Validator - def _validator_get(self): + def _validator_get(self): # no parameters by default return {} - def _validator_update(self): + def _validator_update(self): return {'message': {'type': 'string'}} - def _validator_create(self): + def _validator_create(self): return {'message': {'type': 'string'}}

    Once you have implemented your services (ping, …), you must tell to Odoo how to access to these services. This process is done by implementing a controller that inherits from odoo.addons.base_rest.controllers.main.RestController

    -from odoo.addons.base_rest.controllers import main
    +from odoo.addons.base_rest.controllers import main
     
    -class MyRestController(main.RestController):
    +class MyRestController(main.RestController):
         _root_path = '/my_services_api/'
         _collection_name = my_module.services
     
    @@ -516,7 +516,7 @@

    Usage

    ROOT_PATH + '<string:_service_name>/<int:_id>', ROOT_PATH + '<string:_service_name>/<int:_id>/get' ], methods=['GET'], auth="user", csrf=False) -def get(self, _service_name, _id=None, **params): +def get(self, _service_name, _id=None, **params): method_name = 'get' if _id else 'search' return self._process_method(_service_name, method_name, _id, params) @@ -526,7 +526,7 @@

    Usage

    ROOT_PATH + '<string:_service_name>/<int:_id>', ROOT_PATH + '<string:_service_name>/<int:_id>/<string:method_name>' ], methods=['POST'], auth="user", csrf=False) -def modify(self, _service_name, _id=None, method_name=None, **params): +def modify(self, _service_name, _id=None, method_name=None, **params): if not method_name: method_name = 'update' if _id else 'create' if method_name == 'get': @@ -538,13 +538,13 @@

    Usage

    @route([ ROOT_PATH + '<string:_service_name>/<int:_id>', ], methods=['PUT'], auth="user", csrf=False) -def update(self, _service_name, _id, **params): +def update(self, _service_name, _id, **params): return self._process_method(_service_name, 'update', _id, params) @route([ ROOT_PATH + '<string:_service_name>/<int:_id>', ], methods=['DELETE'], auth="user", csrf=False) -def delete(self, _service_name, _id): +def delete(self, _service_name, _id): return self._process_method(_service_name, 'delete', _id)

    As result an HTTP GET call to ‘http://my_odoo/my_services_api/ping’ will be @@ -565,7 +565,7 @@

    Usage

    of a python decorator to explicitly mark a method as being available via the REST API: odoo.addons.base_rest.restapi.method.

    -class PartnerNewApiService(Component):
    +class PartnerNewApiService(Component):
         _inherit = "base.rest.service"
         _name = "partner.new_api.service"
         _usage = "partner"
    @@ -580,10 +580,10 @@ 

    Usage

    output_param=restapi.CerberusValidator("_get_partner_schema"), auth="public", ) - def get(self, _id): + def get(self, _id): return {"name": self.env["res.partner"].browse(_id).name} - def _get_partner_schema(self): + def _get_partner_schema(self): return { "name": {"type": "string", "required": True} } @@ -593,7 +593,7 @@

    Usage

    output_param=restapi.CerberusListValidator("_get_partner_schema"), auth="public", ) - def list(self): + def list(self): partners = self.env["res.partner"].search([]) return [{"name": p.name} for p in partners]
    @@ -602,28 +602,28 @@

    Usage

    For example, base_rest_datamodel allows you to use Datamodel object instance into your services.

    -from marshmallow import fields
    +from marshmallow import fields
     
    -from odoo.addons.base_rest import restapi
    -from odoo.addons.component.core import Component
    -from odoo.addons.datamodel.core import Datamodel
    +from odoo.addons.base_rest import restapi
    +from odoo.addons.component.core import Component
    +from odoo.addons.datamodel.core import Datamodel
     
     
    -class PartnerSearchParam(Datamodel):
    +class PartnerSearchParam(Datamodel):
         _name = "partner.search.param"
     
         id = fields.Integer(required=False, allow_none=False)
         name = fields.String(required=False, allow_none=False)
     
     
    -class PartnerShortInfo(Datamodel):
    +class PartnerShortInfo(Datamodel):
         _name = "partner.short.info"
     
         id = fields.Integer(required=True, allow_none=False)
         name = fields.String(required=True, allow_none=False)
     
     
    -class PartnerNewApiService(Component):
    +class PartnerNewApiService(Component):
         _inherit = "base.rest.service"
         _name = "partner.new_api.service"
         _usage = "partner"
    @@ -639,7 +639,7 @@ 

    Usage

    output_param=restapi.Datamodel("partner.short.info", is_list=True), auth="public", ) - def search(self, partner_search_param): + def search(self, partner_search_param): """ Search for partners :param partner_search_param: An instance of partner.search.param diff --git a/base_rest_demo/README.rst b/base_rest_demo/README.rst index ad4e0eae6..b07df2cc4 100644 --- a/base_rest_demo/README.rst +++ b/base_rest_demo/README.rst @@ -7,7 +7,7 @@ Base Rest Demo !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:d55a089f3426610ea4b4762a7e51b8572ae0852a03bf126d2eb6ff4a5c1451f9 + !! source digest: sha256:18e5691a569699452f29ef673266f29b874bf5c931221af24817846eb59f85a1 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/base_rest_demo/__manifest__.py b/base_rest_demo/__manifest__.py index a9efda8c8..564a8536c 100644 --- a/base_rest_demo/__manifest__.py +++ b/base_rest_demo/__manifest__.py @@ -5,7 +5,7 @@ "name": "Base Rest Demo", "summary": """ Demo addon for Base REST""", - "version": "16.0.2.0.3", + "version": "16.0.2.0.4", "development_status": "Beta", "license": "LGPL-3", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", diff --git a/base_rest_demo/static/description/index.html b/base_rest_demo/static/description/index.html index 3c5ed59ae..62b5d66ed 100644 --- a/base_rest_demo/static/description/index.html +++ b/base_rest_demo/static/description/index.html @@ -367,7 +367,7 @@

    Base Rest Demo

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:d55a089f3426610ea4b4762a7e51b8572ae0852a03bf126d2eb6ff4a5c1451f9 +!! source digest: sha256:18e5691a569699452f29ef673266f29b874bf5c931221af24817846eb59f85a1 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    Demo addon to illustrate how to develop self documented REST services thanks From bf65b2a3405f27248730d58d266a8452d567c96f Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 2 Jun 2025 12:56:59 +0200 Subject: [PATCH 25/58] [ADD] auth_partner, fastapi_auth_partner --- auth_partner/README.rst | 102 ++++ auth_partner/__init__.py | 2 + auth_partner/__manifest__.py | 38 ++ auth_partner/data/email_data.xml | 67 +++ auth_partner/demo/auth_directory_demo.xml | 9 + auth_partner/demo/auth_partner_demo.xml | 8 + auth_partner/demo/res_partner_demo.xml | 7 + auth_partner/models/__init__.py | 3 + auth_partner/models/auth_directory.py | 209 ++++++++ auth_partner/models/auth_partner.py | 310 ++++++++++++ auth_partner/models/res_partner.py | 34 ++ auth_partner/readme/CONTRIBUTORS.rst | 4 + auth_partner/readme/DESCRIPTION.rst | 12 + auth_partner/readme/USAGE.rst | 8 + auth_partner/security/ir.model.access.csv | 8 + auth_partner/security/ir_rule.xml | 26 + auth_partner/security/res_group.xml | 16 + auth_partner/static/description/index.html | 447 ++++++++++++++++ auth_partner/tests/__init__.py | 1 + auth_partner/tests/common.py | 60 +++ auth_partner/tests/test_auth_partner.py | 357 +++++++++++++ auth_partner/views/auth_directory_view.xml | 92 ++++ auth_partner/views/auth_partner_view.xml | 98 ++++ auth_partner/views/res_partner_view.xml | 22 + auth_partner/wizards/__init__.py | 2 + .../wizard_auth_partner_force_set_password.py | 37 ++ ...d_auth_partner_force_set_password_view.xml | 38 ++ .../wizard_auth_partner_reset_password.py | 59 +++ ...izard_auth_partner_reset_password_view.xml | 42 ++ fastapi_auth_partner/README.rst | 136 +++++ fastapi_auth_partner/__init__.py | 4 + fastapi_auth_partner/__manifest__.py | 32 ++ .../demo/fastapi_endpoint_demo.xml | 26 + fastapi_auth_partner/dependencies.py | 74 +++ fastapi_auth_partner/models/__init__.py | 3 + fastapi_auth_partner/models/auth_directory.py | 51 ++ fastapi_auth_partner/models/auth_partner.py | 82 +++ .../models/fastapi_endpoint.py | 55 ++ fastapi_auth_partner/readme/CONTRIBUTORS.rst | 4 + fastapi_auth_partner/readme/DESCRIPTION.rst | 2 + fastapi_auth_partner/readme/USAGE.rst | 52 ++ fastapi_auth_partner/routers/__init__.py | 1 + fastapi_auth_partner/routers/auth.py | 252 +++++++++ fastapi_auth_partner/schemas.py | 40 ++ .../security/ir.model.access.csv | 2 + fastapi_auth_partner/security/res_group.xml | 13 + .../static/description/index.html | 477 ++++++++++++++++++ fastapi_auth_partner/tests/__init__.py | 2 + fastapi_auth_partner/tests/test_auth.py | 243 +++++++++ .../tests/test_fastapi_auth_partner_demo.py | 93 ++++ .../views/auth_directory_view.xml | 29 ++ .../views/auth_partner_view.xml | 31 ++ .../views/fastapi_endpoint_view.xml | 25 + fastapi_auth_partner/wizards/__init__.py | 2 + .../wizard_auth_partner_impersonate.py | 29 ++ .../wizard_auth_partner_impersonate_view.xml | 43 ++ .../wizard_auth_partner_reset_password.py | 18 + ...izard_auth_partner_reset_password_view.xml | 17 + requirements.txt | 2 + setup/auth_partner/odoo/addons/auth_partner | 1 + setup/auth_partner/setup.py | 6 + .../odoo/addons/fastapi_auth_partner | 1 + setup/fastapi_auth_partner/setup.py | 6 + 63 files changed, 3972 insertions(+) create mode 100644 auth_partner/README.rst create mode 100644 auth_partner/__init__.py create mode 100644 auth_partner/__manifest__.py create mode 100644 auth_partner/data/email_data.xml create mode 100644 auth_partner/demo/auth_directory_demo.xml create mode 100644 auth_partner/demo/auth_partner_demo.xml create mode 100644 auth_partner/demo/res_partner_demo.xml create mode 100644 auth_partner/models/__init__.py create mode 100644 auth_partner/models/auth_directory.py create mode 100644 auth_partner/models/auth_partner.py create mode 100644 auth_partner/models/res_partner.py create mode 100644 auth_partner/readme/CONTRIBUTORS.rst create mode 100644 auth_partner/readme/DESCRIPTION.rst create mode 100644 auth_partner/readme/USAGE.rst create mode 100644 auth_partner/security/ir.model.access.csv create mode 100644 auth_partner/security/ir_rule.xml create mode 100644 auth_partner/security/res_group.xml create mode 100644 auth_partner/static/description/index.html create mode 100644 auth_partner/tests/__init__.py create mode 100644 auth_partner/tests/common.py create mode 100644 auth_partner/tests/test_auth_partner.py create mode 100644 auth_partner/views/auth_directory_view.xml create mode 100644 auth_partner/views/auth_partner_view.xml create mode 100644 auth_partner/views/res_partner_view.xml create mode 100644 auth_partner/wizards/__init__.py create mode 100644 auth_partner/wizards/wizard_auth_partner_force_set_password.py create mode 100644 auth_partner/wizards/wizard_auth_partner_force_set_password_view.xml create mode 100644 auth_partner/wizards/wizard_auth_partner_reset_password.py create mode 100644 auth_partner/wizards/wizard_auth_partner_reset_password_view.xml create mode 100644 fastapi_auth_partner/README.rst create mode 100644 fastapi_auth_partner/__init__.py create mode 100644 fastapi_auth_partner/__manifest__.py create mode 100644 fastapi_auth_partner/demo/fastapi_endpoint_demo.xml create mode 100644 fastapi_auth_partner/dependencies.py create mode 100644 fastapi_auth_partner/models/__init__.py create mode 100644 fastapi_auth_partner/models/auth_directory.py create mode 100644 fastapi_auth_partner/models/auth_partner.py create mode 100644 fastapi_auth_partner/models/fastapi_endpoint.py create mode 100644 fastapi_auth_partner/readme/CONTRIBUTORS.rst create mode 100644 fastapi_auth_partner/readme/DESCRIPTION.rst create mode 100644 fastapi_auth_partner/readme/USAGE.rst create mode 100644 fastapi_auth_partner/routers/__init__.py create mode 100644 fastapi_auth_partner/routers/auth.py create mode 100644 fastapi_auth_partner/schemas.py create mode 100644 fastapi_auth_partner/security/ir.model.access.csv create mode 100644 fastapi_auth_partner/security/res_group.xml create mode 100644 fastapi_auth_partner/static/description/index.html create mode 100644 fastapi_auth_partner/tests/__init__.py create mode 100644 fastapi_auth_partner/tests/test_auth.py create mode 100644 fastapi_auth_partner/tests/test_fastapi_auth_partner_demo.py create mode 100644 fastapi_auth_partner/views/auth_directory_view.xml create mode 100644 fastapi_auth_partner/views/auth_partner_view.xml create mode 100644 fastapi_auth_partner/views/fastapi_endpoint_view.xml create mode 100644 fastapi_auth_partner/wizards/__init__.py create mode 100644 fastapi_auth_partner/wizards/wizard_auth_partner_impersonate.py create mode 100644 fastapi_auth_partner/wizards/wizard_auth_partner_impersonate_view.xml create mode 100644 fastapi_auth_partner/wizards/wizard_auth_partner_reset_password.py create mode 100644 fastapi_auth_partner/wizards/wizard_auth_partner_reset_password_view.xml create mode 120000 setup/auth_partner/odoo/addons/auth_partner create mode 100644 setup/auth_partner/setup.py create mode 120000 setup/fastapi_auth_partner/odoo/addons/fastapi_auth_partner create mode 100644 setup/fastapi_auth_partner/setup.py diff --git a/auth_partner/README.rst b/auth_partner/README.rst new file mode 100644 index 000000000..ca979ef04 --- /dev/null +++ b/auth_partner/README.rst @@ -0,0 +1,102 @@ +============ +Partner Auth +============ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c9e735f01c49bc7974e3b9354b6157e19c7486a71626ad8eef81104b628d476b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/auth_partner + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-auth_partner + :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/rest-framework&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds to the partners the ability to authenticate through directories. + +This module does not implement any routing, it only provides the basic mechanisms in a directory for: + + - Registering a partner and sending an welcome email (to validate email address): `_signup` + - Authenticating a partner: `_login` + - Validating a partner email using a token: `_validate_email` + - Impersonating: `_impersonate`, `_impersonating` + - Resetting the password with a unique token sent by mail: `_request_reset_password`, `_set_password` + - Sending an invite mail when registering a partner from odoo interface for the partner to enter a password: `_send_invite`, `_set_password` + +For a routing implementation, see the `fastapi_auth_partner <../fastapi_auth_partner>`_ module. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +This module isn't meant to be used standalone but you can still see the directories and authenticable partners in: + +Settings > Technical > Partner Authentication > Partner + +and + +Settings > Technical > Partner Authentication > Directory + + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_: + + * Sébastien Beau + * Florian Mounier + +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. + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/auth_partner/__init__.py b/auth_partner/__init__.py new file mode 100644 index 000000000..aee8895e7 --- /dev/null +++ b/auth_partner/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/auth_partner/__manifest__.py b/auth_partner/__manifest__.py new file mode 100644 index 000000000..99e8a94fa --- /dev/null +++ b/auth_partner/__manifest__.py @@ -0,0 +1,38 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Partner Auth", + "summary": "Implements the base features for a authenticable partner", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/rest-framework", + "depends": [ + "auth_signup", + "mail", + "queue_job", + "server_environment", + ], + "data": [ + "security/res_group.xml", + "security/ir.model.access.csv", + "security/ir_rule.xml", + "data/email_data.xml", + "wizards/wizard_auth_partner_force_set_password_view.xml", + "wizards/wizard_auth_partner_reset_password_view.xml", + "views/auth_partner_view.xml", + "views/auth_directory_view.xml", + "views/res_partner_view.xml", + ], + "demo": [ + "demo/res_partner_demo.xml", + "demo/auth_directory_demo.xml", + "demo/auth_partner_demo.xml", + ], + "external_dependencies": { + "python": ["itsdangerous", "pyjwt"], + }, +} diff --git a/auth_partner/data/email_data.xml b/auth_partner/data/email_data.xml new file mode 100644 index 000000000..92193d06d --- /dev/null +++ b/auth_partner/data/email_data.xml @@ -0,0 +1,67 @@ + + + + Auth Directory: Reset Password + noreply@example.org + Reset Password + {{object.partner_id.id}} + + + ${object.partner_id.lang} + +

    + Hi + Click on the following link to reset your password + Reset Password +
    + + + + + Auth Directory: Set Password + noreply@example.org + Welcome + {{object.partner_id.id}} + + + {{object.partner_id.lang}} + +
    + Hi + Welcome, your account have been created + Click on the following link to set your password + Set Password +
    +
    +
    + + + Auth Directory: Validate Email + noreply@example.org + Welcome + {{object.partner_id.id}} + + + {{object.partner_id.lang}} + +
    + Hi + Welcome to the site, please click on the following link to verify your email + Validate Email +
    +
    +
    + + diff --git a/auth_partner/demo/auth_directory_demo.xml b/auth_partner/demo/auth_directory_demo.xml new file mode 100644 index 000000000..81708b69a --- /dev/null +++ b/auth_partner/demo/auth_directory_demo.xml @@ -0,0 +1,9 @@ + + + + Demo Auth Directory + + + + + diff --git a/auth_partner/demo/auth_partner_demo.xml b/auth_partner/demo/auth_partner_demo.xml new file mode 100644 index 000000000..93dda262c --- /dev/null +++ b/auth_partner/demo/auth_partner_demo.xml @@ -0,0 +1,8 @@ + + + + + + Super-secret$1 + + diff --git a/auth_partner/demo/res_partner_demo.xml b/auth_partner/demo/res_partner_demo.xml new file mode 100644 index 000000000..43a063524 --- /dev/null +++ b/auth_partner/demo/res_partner_demo.xml @@ -0,0 +1,7 @@ + + + + Demo auth partner + partner-auth@example.org + + diff --git a/auth_partner/models/__init__.py b/auth_partner/models/__init__.py new file mode 100644 index 000000000..6259e6d10 --- /dev/null +++ b/auth_partner/models/__init__.py @@ -0,0 +1,3 @@ +from . import auth_directory +from . import auth_partner +from . import res_partner diff --git a/auth_partner/models/auth_directory.py b/auth_partner/models/auth_directory.py new file mode 100644 index 000000000..fe2663640 --- /dev/null +++ b/auth_partner/models/auth_directory.py @@ -0,0 +1,209 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, timezone +from secrets import token_urlsafe + +import jwt + +from odoo import _, fields, models +from odoo.exceptions import UserError + +from odoo.addons.queue_job.delay import chain + + +class AuthDirectory(models.Model): + _name = "auth.directory" + _description = "Auth Directory" + _inherit = "server.env.mixin" + + name = fields.Char(required=True) + auth_partner_ids = fields.One2many("auth.partner", "directory_id", "Auth Partners") + set_password_token_duration = fields.Integer( + default=1440, help="In minute, default 1440 minutes => 24h", required=True + ) + impersonating_token_duration = fields.Integer( + default=60, help="In seconds, default 60 seconds", required=True + ) + reset_password_template_id = fields.Many2one( + "mail.template", + "Mail Template Forget Password", + required=True, + default=lambda self: self.env.ref( + "auth_partner.email_reset_password", + raise_if_not_found=False, + ), + ) + set_password_template_id = fields.Many2one( + "mail.template", + "Mail Template New Password", + required=True, + default=lambda self: self.env.ref( + "auth_partner.email_set_password", + raise_if_not_found=False, + ), + ) + validate_email_template_id = fields.Many2one( + "mail.template", + "Mail Template Validate Email", + required=True, + default=lambda self: self.env.ref( + "auth_partner.email_validate_email", + raise_if_not_found=False, + ), + ) + secret_key = fields.Char( + groups="base.group_system", + required=True, + default=lambda self: self._generate_default_secret_key(), + ) + count_partner = fields.Integer(compute="_compute_count_partner") + + impersonating_user_ids = fields.Many2many( + "res.users", + "auth_directory_impersonating_user_rel", + "directory_id", + "user_id", + string="Impersonating Users", + help="These odoo users can impersonate any partner of this directory", + default=lambda self: ( + self.env.ref("base.user_root") | self.env.ref("base.user_admin") + ).ids, + groups="auth_partner.group_auth_partner_manager", + ) + force_verified_email = fields.Boolean( + help="If checked, email must be verified to be able to log in" + ) + + def _generate_default_secret_key(self): + # generate random ~64 chars secret key + return token_urlsafe(64) + + def action_regenerate_secret_key(self): + self.ensure_one() + self.secret_key = self._generate_default_secret_key() + + def _compute_count_partner(self): + data = self.env["auth.partner"].read_group( + [ + ("directory_id", "in", self.ids), + ], + ["directory_id"], + groupby=["directory_id"], + lazy=False, + ) + res = {item["directory_id"][0]: item["__count"] for item in data} + + for record in self: + record.count_partner = res.get(record.id, 0) + + def _get_template(self, type_or_template): + if isinstance(type_or_template, str): + return getattr(self, type_or_template + "_template_id", None) + return type_or_template + + def _prepare_mail_context(self, context): + return context or {} + + def _send_mail_background( + self, type_or_template, auth_partner, callback_job=None, **context + ): + """ + Send an email asynchronously to the auth_partner + using the template defined in the directory + """ + self.ensure_one() + auth_partner.ensure_one() + # Load context synchronously + context = self._prepare_mail_context(context) + + job = self.delayable()._send_mail_impl( + type_or_template, auth_partner, **context + ) + if callback_job: + job = chain(job, callback_job) + return job.delay() + + def _send_mail(self, type_or_template, auth_partner, **context): + """Send an email to the auth_partner using the template defined in the directory""" + self.ensure_one() + auth_partner.ensure_one() + context = self._prepare_mail_context(context) + + self._send_mail_impl(type_or_template, auth_partner, **context) + + def _send_mail_impl(self, type_or_template, auth_partner, **context): + template = self.sudo()._get_template(type_or_template) + if not template: + raise UserError( + _("No email template defined for %(template)s in %(directory)s") + % {"template": type_or_template, "directory": self.name} + ) + template.sudo().with_context(**context).send_mail( + auth_partner.id, force_send=True, raise_exception=True + ) + + return f"Mail {template.name} sent to {auth_partner.login}" + + def _generate_token(self, action, auth_partner, expiration_delta, key_salt=""): + # We need to sudo here as secret_key is a protected field + self = self.sudo() + return jwt.encode( + { + "exp": datetime.now(tz=timezone.utc) + expiration_delta, + "aud": str(self.id), + "action": action, + "ap": auth_partner.id, + }, + self.secret_key + key_salt, + algorithm="HS256", + ) + + def _decode_token( + self, + token, + action, + key_salt=None, + ): + # We need to sudo here as secret_key is a protected field + self = self.sudo() + key = self.secret_key + if key_salt: + try: + obj = jwt.decode( + token, algorithms=["HS256"], options={"verify_signature": False} + ) + except jwt.PyJWTError as e: + raise UserError(_("Invalid Token")) from e + probable_auth_partner = self.env["auth.partner"].browse(obj["ap"]) + if not probable_auth_partner: + raise UserError(_("Invalid Token")) + key += key_salt(probable_auth_partner) + + try: + obj = jwt.decode( + token, + key, + audience=str(self.id), + options={"require": ["exp", "aud", "ap", "action"]}, + algorithms=["HS256"], + ) + except jwt.PyJWTError as e: + raise UserError(_("Invalid Token")) from e + + auth_partner = self.env["auth.partner"].browse(obj["ap"]) + + if ( + obj["action"] != action + or not auth_partner + or auth_partner.directory_id != self + ): + raise UserError(_("Invalid token")) + + return auth_partner + + @property + def _server_env_fields(self): + return {"secret_key": {}} diff --git a/auth_partner/models/auth_partner.py b/auth_partner/models/auth_partner.py new file mode 100644 index 000000000..737e14529 --- /dev/null +++ b/auth_partner/models/auth_partner.py @@ -0,0 +1,310 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import timedelta + +import passlib + +from odoo import _, api, fields, models +from odoo.exceptions import AccessDenied + +# please read passlib great documentation +# https://passlib.readthedocs.io +# https://passlib.readthedocs.io/en/stable/narr/quickstart.html#choosing-a-hash +# be carefull odoo requirements use an old version of passlib +DEFAULT_CRYPT_CONTEXT = passlib.context.CryptContext(["pbkdf2_sha512"]) + +_logger = logging.getLogger(__name__) + + +class AuthPartner(models.Model): + _name = "auth.partner" + _description = "Auth Partner" + _rec_name = "login" + + partner_id = fields.Many2one( + "res.partner", "Partner", required=True, ondelete="cascade", index=True + ) + directory_id = fields.Many2one( + "auth.directory", "Directory", required=True, index=True + ) + user_can_impersonate = fields.Boolean( + compute="_compute_user_can_impersonate", + help="Technical field to check if the user can impersonate", + ) + impersonating_user_ids = fields.Many2many( + related="directory_id.impersonating_user_ids", + ) + login = fields.Char( + compute="_compute_login", + store=True, + required=True, + index=True, + precompute=True, + ) + password = fields.Char(compute="_compute_password", inverse="_inverse_password") + encrypted_password = fields.Char(index=True) + nbr_pending_reset_sent = fields.Integer( + index=True, + help=( + "Number of pending reset sent from your customer." + "This field is usefull when after a migration from an other system " + "you ask all you customer to reset their password and you send" + "different mail depending on the number of reminder" + ), + ) + date_last_request_reset_pwd = fields.Datetime( + help="Date of the last password reset request" + ) + date_last_sucessfull_reset_pwd = fields.Datetime( + help="Date of the last sucessfull password reset" + ) + date_last_impersonation = fields.Datetime( + help="Date of the last sucessfull impersonation" + ) + + mail_verified = fields.Boolean( + help="This field is set to True when the user has clicked on the link sent by email" + ) + + _sql_constraints = [ + ( + "directory_login_uniq", + "unique (directory_id, login)", + "Login must be uniq per directory !", + ), + ] + + @api.depends("partner_id.email") + def _compute_login(self): + for record in self: + record.login = record.partner_id.email + + def _crypt_context(self): + return DEFAULT_CRYPT_CONTEXT + + def _check_no_empty(self, login, password): + # double check by security but calling this through a service should + # already have check this + if not ( + isinstance(password, str) and password and isinstance(login, str) and login + ): + _logger.warning("Invalid login/password for sign in") + raise AccessDenied() + + def _get_hashed_password(self, directory, login): + self.flush() + self.env.cr.execute( + """ + SELECT id, COALESCE(encrypted_password, '') + FROM auth_partner + WHERE login=%s AND directory_id=%s""", + (login, directory.id), + ) + hashed = self.env.cr.fetchone() + if hashed and hashed[1]: + # ensure that we have a auth.partner and this partner have a password set + return hashed + else: + raise AccessDenied() + + def _compute_password(self): + for record in self: + record.password = "" + + def _inverse_password(self): + for record in self: + ctx = record._crypt_context() + hash_ = getattr(ctx, "hash", ctx.encrypt) + record.encrypted_password = hash_(record.password) + record.password = "" + + def _prepare_partner_auth_signup(self, directory, vals): + return { + "login": vals["login"].lower(), + "password": vals["password"], + "directory_id": directory.id, + } + + def _prepare_partner_signup(self, directory, vals): + return { + "name": vals["name"], + "email": vals["login"].lower(), + "auth_partner_ids": [ + (0, 0, self._prepare_partner_auth_signup(directory, vals)) + ], + } + + @api.model + def _signup(self, directory, **kwargs): + partner = self.env["res.partner"].create( + [ + self._prepare_partner_signup(directory, kwargs), + ] + ) + auth_partner = partner.auth_partner_ids + directory._send_mail_background( + "validate_email", + auth_partner, + token=auth_partner._generate_validate_email_token(), + ) + return auth_partner + + @api.model + def _login(self, directory, login, password, **kwargs): + self._check_no_empty(login, password) + login = login.lower() + try: + _id, hashed = self._get_hashed_password(directory, login) + valid, replacement = self._crypt_context().verify_and_update( + password, hashed + ) + + auth_partner = valid and self.browse(_id) + except AccessDenied: + # We do not want to leak information about the login, + # always raise the same exception + auth_partner = None + + if not auth_partner or not auth_partner.partner_id.active: + raise AccessDenied(_("Invalid Login or Password")) + + if directory.sudo().force_verified_email and not auth_partner.mail_verified: + raise AccessDenied( + _( + "Email address not validated. Validate your email address by " + "clicking on the link in the email sent to you or request a new " + "password. " + ) + ) + + if replacement is not None: + auth_partner.encrypted_password = replacement + + return auth_partner + + @api.model + def _validate_email(self, directory, token): + auth_partner = directory._decode_token(token, "validate_email") + auth_partner.write({"mail_verified": True}) + return auth_partner + + def _get_impersonate_url(self, token, **kwargs): + # You should override this method according to the impersonation url + # your framework is using + + base = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + url = f"{base}/auth/impersonate/{token}" + return url + + def _get_impersonate_action(self, token, **kwargs): + return { + "type": "ir.actions.act_url", + "url": self._get_impersonate_url(token, **kwargs), + "target": "new", + } + + def impersonate(self): + self.ensure_one() + if self.env.user not in self.impersonating_user_ids: + raise AccessDenied(_("You are not allowed to impersonate this user")) + + token = self._generate_impersonating_token() + return self._get_impersonate_action(token) + + @api.depends_context("uid") + def _compute_user_can_impersonate(self): + for record in self: + record.user_can_impersonate = self.env.user in record.impersonating_user_ids + + @api.model + def _impersonating(self, directory, token): + partner_auth = directory._decode_token( + token, + "impersonating", + key_salt=lambda auth_partner: ( + auth_partner.date_last_impersonation.isoformat() + if auth_partner.date_last_impersonation + else "never" + ), + ) + partner_auth.date_last_impersonation = fields.Datetime.now() + return partner_auth + + def _on_reset_password_sent(self): + self.ensure_one() + self.date_last_request_reset_pwd = fields.Datetime.now() + self.date_last_sucessfull_reset_pwd = None + self.nbr_pending_reset_sent += 1 + + def _send_invite(self): + self.ensure_one() + self.directory_id._send_mail_background( + "set_password", + self, + callback_job=self.delayable()._on_reset_password_sent(), + token=self._generate_set_password_token(), + ) + + def send_invite(self): + for rec in self: + rec._send_invite() + + def _request_reset_password(self): + return self.directory_id._send_mail_background( + "reset_password", + self, + callback_job=self.delayable()._on_reset_password_sent(), + token=self._generate_set_password_token(), + ) + + def _set_password(self, directory, token, password): + auth_partner = directory._decode_token( + token, + "set_password", + # See `_generate_set_password_token` for the key_salt + key_salt=lambda auth_partner: auth_partner.encrypted_password or "empty", + ) + auth_partner.write( + { + "password": password, + "mail_verified": True, + } + ) + auth_partner.date_last_sucessfull_reset_pwd = fields.Datetime.now() + auth_partner.nbr_pending_reset_sent = 0 + return auth_partner + + def _generate_set_password_token(self, expiration_delta=None): + # Here we use the current encrypted_password as key_salt to ensure that + # the token will be used to reset the password only once. + return self.directory_id._generate_token( + "set_password", + self, + expiration_delta + or timedelta(minutes=self.directory_id.set_password_token_duration), + key_salt=self.encrypted_password or "empty", + ) + + def _generate_validate_email_token(self): + return self.directory_id._generate_token( + # 30 days seem to be a good value, no need for configuration + "validate_email", + self, + timedelta(days=30), + ) + + def _generate_impersonating_token(self): + return self.directory_id._generate_token( + "impersonating", + self, + timedelta(minutes=self.directory_id.impersonating_token_duration), + key_salt=( + self.date_last_impersonation.isoformat() + if self.date_last_impersonation + else "never" + ), + ) diff --git a/auth_partner/models/res_partner.py b/auth_partner/models/res_partner.py new file mode 100644 index 000000000..0a398533d --- /dev/null +++ b/auth_partner/models/res_partner.py @@ -0,0 +1,34 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + auth_partner_ids = fields.One2many("auth.partner", "partner_id", "Partner Auth") + auth_partner_count = fields.Integer( + compute="_compute_auth_partner_count", compute_sudo=True + ) + + def _compute_auth_partner_count(self): + data = self.env["auth.partner"].read_group( + [ + ("partner_id", "in", self.ids), + ], + ["partner_id"], + groupby=["partner_id"], + lazy=False, + ) + res = {item["partner_id"][0]: item["__count"] for item in data} + + for record in self: + record.auth_partner_count = res.get(record.id, 0) + + def _get_auth_partner_for_directory(self, directory): + return self.sudo().auth_partner_ids.filtered( + lambda r: r.directory_id == directory + ) diff --git a/auth_partner/readme/CONTRIBUTORS.rst b/auth_partner/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..bae3cc9a1 --- /dev/null +++ b/auth_partner/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Akretion `_: + + * Sébastien Beau + * Florian Mounier diff --git a/auth_partner/readme/DESCRIPTION.rst b/auth_partner/readme/DESCRIPTION.rst new file mode 100644 index 000000000..2a63b69ea --- /dev/null +++ b/auth_partner/readme/DESCRIPTION.rst @@ -0,0 +1,12 @@ +This module adds to the partners the ability to authenticate through directories. + +This module does not implement any routing, it only provides the basic mechanisms in a directory for: + + - Registering a partner and sending an welcome email (to validate email address): `_signup` + - Authenticating a partner: `_login` + - Validating a partner email using a token: `_validate_email` + - Impersonating: `_impersonate`, `_impersonating` + - Resetting the password with a unique token sent by mail: `_request_reset_password`, `_set_password` + - Sending an invite mail when registering a partner from odoo interface for the partner to enter a password: `_send_invite`, `_set_password` + +For a routing implementation, see the `fastapi_auth_partner <../fastapi_auth_partner>`_ module. diff --git a/auth_partner/readme/USAGE.rst b/auth_partner/readme/USAGE.rst new file mode 100644 index 000000000..39cb46f62 --- /dev/null +++ b/auth_partner/readme/USAGE.rst @@ -0,0 +1,8 @@ +This module isn't meant to be used standalone but you can still see the directories and authenticable partners in: + +Settings > Technical > Partner Authentication > Partner + +and + +Settings > Technical > Partner Authentication > Directory + diff --git a/auth_partner/security/ir.model.access.csv b/auth_partner/security/ir.model.access.csv new file mode 100644 index 000000000..cdcead759 --- /dev/null +++ b/auth_partner/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_auth_directory,auth_directory_system,model_auth_directory,base.group_system,1,1,1,1 +access_auth_directory_read,auth_directory_manager,model_auth_directory,group_auth_partner_manager,1,0,0,0 +access_auth_partner,auth_partner_manager,model_auth_partner,group_auth_partner_manager,1,1,1,1 +api_access_auth_partner,auth_partner_api,model_auth_partner,group_auth_partner_api,1,1,0,0 +api_access_res_partner,res_partner_api,base.model_res_partner,group_auth_partner_api,1,0,0,0 +api_access_wizard_auth_partner_reset_password,wizard_auth_partner_reset_password,model_wizard_auth_partner_reset_password,group_auth_partner_manager,1,1,1,1 +api_access_wizard_auth_partner_force_set_password,wizard_auth_partner_force_set_password,model_wizard_auth_partner_force_set_password,group_auth_partner_manager,1,1,1,1 diff --git a/auth_partner/security/ir_rule.xml b/auth_partner/security/ir_rule.xml new file mode 100644 index 000000000..01a9e6020 --- /dev/null +++ b/auth_partner/security/ir_rule.xml @@ -0,0 +1,26 @@ + + + + Auth API (res_partner) + + + [('id','=', authenticated_partner_id)] + + + + + + + + Auth API (auth_partner) + + + [('partner_id','=', authenticated_partner_id)] + + + + + + diff --git a/auth_partner/security/res_group.xml b/auth_partner/security/res_group.xml new file mode 100644 index 000000000..a912c7d2f --- /dev/null +++ b/auth_partner/security/res_group.xml @@ -0,0 +1,16 @@ + + + + API Partner Auth Manager + + + + + + API Partner Auth Access + + + diff --git a/auth_partner/static/description/index.html b/auth_partner/static/description/index.html new file mode 100644 index 000000000..15a31ebef --- /dev/null +++ b/auth_partner/static/description/index.html @@ -0,0 +1,447 @@ + + + + + +Partner Auth + + + +
    +

    Partner Auth

    + + +

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    This module adds to the partners the ability to authenticate through directories.

    +

    This module does not implement any routing, it only provides the basic mechanisms in a directory for:

    +
    +
      +
    • Registering a partner and sending an welcome email (to validate email address): _signup
    • +
    • Authenticating a partner: _login
    • +
    • Validating a partner email using a token: _validate_email
    • +
    • Impersonating: _impersonate, _impersonating
    • +
    • Resetting the password with a unique token sent by mail: _request_reset_password, _set_password
    • +
    • Sending an invite mail when registering a partner from odoo interface for the partner to enter a password: _send_invite, _set_password
    • +
    +
    +

    For a routing implementation, see the fastapi_auth_partner module.

    +

    Table of contents

    + +
    +

    Usage

    +

    This module isn’t meant to be used standalone but you can still see the directories and authenticable partners in:

    +

    Settings > Technical > Partner Authentication > Partner

    +

    and

    +

    Settings > Technical > Partner Authentication > Directory

    +
    +
    +

    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.

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • Akretion
    • +
    +
    +
    +

    Contributors

    +
      +
    • Akretion:
        +
      • Sébastien Beau
      • +
      • Florian Mounier
      • +
      +
    • +
    +
    +
    +

    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/rest-framework project on GitHub.

    +

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

    +
    +
    +
    + + diff --git a/auth_partner/tests/__init__.py b/auth_partner/tests/__init__.py new file mode 100644 index 000000000..ee9a639f6 --- /dev/null +++ b/auth_partner/tests/__init__.py @@ -0,0 +1 @@ +from . import test_auth_partner diff --git a/auth_partner/tests/common.py b/auth_partner/tests/common.py new file mode 100644 index 000000000..6c6f777f6 --- /dev/null +++ b/auth_partner/tests/common.py @@ -0,0 +1,60 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager +from typing import Any + +from odoo.tests.common import TransactionCase + +from odoo.addons.mail.tests.common import MockEmail + + +class CommonTestAuthPartner(TransactionCase, MockEmail): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, queue_job__no_delay=True)) + + cls.partner = cls.env.ref("auth_partner.res_partner_auth_demo") + cls.other_partner = cls.partner.copy( + {"name": "Other Partner", "email": "other-partner-auth@example.org"} + ) + cls.auth_partner = cls.partner.auth_partner_ids + + cls.directory = cls.env.ref("auth_partner.demo_directory") + cls.directory.impersonating_user_ids = cls.env.ref("base.user_admin") + + cls.other_auth_partner = cls.env["auth.partner"].create( + { + "login": cls.other_partner.email, + "password": "Super-secret3", + "directory_id": cls.directory.id, + "partner_id": cls.other_partner.id, + } + ) + cls.other_directory = cls.directory.copy({"name": "Other Directory"}) + + @contextmanager + def new_mails(self): + mailmail = self.env["mail.mail"] + + class MailsProxy(mailmail.__class__): + __slots__ = ["_prev", "__weakref__"] + + def __init__(self): + object.__setattr__(self, "_prev", mailmail.search([])) + + def __getattribute__(self, name: str) -> Any: + mails = mailmail.search([]) - object.__getattribute__(self, "_prev") + return object.__getattribute__(mails, name) + + new_mails = MailsProxy() + with self.mock_mail_gateway(): + yield new_mails + + @contextmanager + def assert_no_new_mail(self): + with self.new_mails() as new_mails: + yield + self.assertFalse(new_mails) diff --git a/auth_partner/tests/test_auth_partner.py b/auth_partner/tests/test_auth_partner.py new file mode 100644 index 000000000..40d9e8478 --- /dev/null +++ b/auth_partner/tests/test_auth_partner.py @@ -0,0 +1,357 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager +from datetime import datetime, timedelta + +from freezegun import freeze_time + +from odoo.exceptions import AccessDenied, UserError + +from .common import CommonTestAuthPartner + + +class TestAuthPartner(CommonTestAuthPartner): + @contextmanager + def assert_no_new_mail(self): + with self.new_mails() as new_mails: + yield + self.assertFalse(new_mails) + + def test_default_secret_key(self): + self.assertGreaterEqual(len(self.directory.secret_key), 64) + + def test_login_ok(self): + with self.assert_no_new_mail(): + auth_partner = self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.org", + password="Super-secret$1", + ) + self.assertTrue(auth_partner) + + def test_login_inactive_partner(self): + self.partner.active = False + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.org", + password="Super-secret$1", + ) + + def test_login_no_auth(self): + self.auth_partner.unlink() + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.org", + password="Super-secret$1", + ) + + def test_login_wrong_password(self): + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, login="partner-auth@example.org", password="wrong" + ) + + def test_login_mail_not_verified(self): + self.directory.force_verified_email = True + with self.assertRaisesRegex(AccessDenied, "Email address not validated"): + self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.org", + password="Super-secret$1", + ) + + def test_login_wrong_login(self): + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.com", + password="Super-secret$1", + ) + + def test_login_wrong_directory(self): + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.other_directory, + login="partner-auth@example.com", + password="Super-secret$1", + ) + + def test_signup(self): + with self.new_mails() as new_mails: + new_auth_partner = self.env["auth.partner"]._signup( + self.directory, + name="New Partner", + login="new-partner-auth@example.org", + password="NewSecret", + ) + self.assertTrue(new_auth_partner) + # Ensure we can't read the password + self.assertNotEqual(new_auth_partner.password, "NewSecret") + + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Welcome") + self.assertIn("Welcome to the site, please", new_mails.body) + + auth_partner = self.env["auth.partner"]._login( + self.directory, login="new-partner-auth@example.org", password="NewSecret" + ) + self.assertTrue(auth_partner) + self.assertEqual(auth_partner, new_auth_partner) + + def test_signup_wrong_directory(self): + new_auth_partner = self.env["auth.partner"]._signup( + self.other_directory, + name="New Partner", + login="new-partner-auth@example.org", + password="NewSecret", + ) + self.assertTrue(new_auth_partner) + + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="new-partner-auth@example.org", + password="NewSecret", + ) + + def test_signup_same_login_other_directory(self): + new_auth_partner = self.env["auth.partner"]._signup( + self.directory, + name="New Partner", + login="new-partner-auth@example.org", + password="NewSecret", + ) + self.assertTrue(new_auth_partner) + new_auth_partner_2 = self.env["auth.partner"]._signup( + self.other_directory, + name="New Partner", + login="new-partner-auth@example.org", + password="NewSecret2", + ) + self.assertTrue(new_auth_partner_2) + self.assertNotEqual(new_auth_partner, new_auth_partner_2) + + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="new-partner-auth@example.org", + password="NewSecret2", + ) + + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.other_directory, + login="new-partner-auth@example.org", + password="NewSecret", + ) + + def test_validate_email_ok(self): + self.assertFalse(self.auth_partner.mail_verified) + token = self.auth_partner._generate_validate_email_token() + self.auth_partner._validate_email(self.directory, token) + self.assertTrue(self.auth_partner.mail_verified) + + def test_validate_email_required_login(self): + self.directory.force_verified_email = True + token = self.auth_partner._generate_validate_email_token() + self.auth_partner._validate_email(self.directory, token) + with self.assert_no_new_mail(): + auth_partner = self.env["auth.partner"]._login( + self.directory, + login="partner-auth@example.org", + password="Super-secret$1", + ) + self.assertTrue(auth_partner) + + def test_validate_email_wrong_token(self): + self.assertFalse(self.auth_partner.mail_verified) + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.auth_partner._validate_email(self.directory, "wrong") + self.assertFalse(self.auth_partner.mail_verified) + + def test_validate_email_token(self): + with self.new_mails() as new_mails: + new_auth_partner = self.env["auth.partner"]._signup( + self.directory, + name="New Partner", + login="new-partner-auth@example.org", + password="NewSecret", + ) + self.assertFalse(new_auth_partner.mail_verified) + token = new_mails.body.split("token=")[1].split('">')[0] + new_auth_partner._validate_email(self.directory, token) + self.assertTrue(new_auth_partner.mail_verified) + + def test_impersonate_ok(self): + action = self.auth_partner.with_user( + self.env.ref("base.user_admin") + ).impersonate() + token = action["url"].split("/")[-1] + + auth_partner = self.env["auth.partner"]._impersonating(self.directory, token) + self.assertEqual(auth_partner, self.auth_partner) + + def test_impersonate_once(self): + action = self.auth_partner.with_user( + self.env.ref("base.user_admin") + ).impersonate() + token = action["url"].split("/")[-1] + + self.env["auth.partner"]._impersonating(self.directory, token) + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.env["auth.partner"]._impersonating(self.directory, token) + + def test_impersonate_wrong_directory(self): + action = self.auth_partner.with_user( + self.env.ref("base.user_admin") + ).impersonate() + token = action["url"].split("/")[-1] + + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.env["auth.partner"]._impersonating(self.other_directory, token) + + def test_impersonate_wrong_user(self): + with self.assertRaisesRegex(AccessDenied, "not allowed to impersonate"): + self.auth_partner.with_user(self.env.ref("base.default_user")).impersonate() + + def test_impersonate_not_expired_token(self): + self.directory.impersonating_token_duration = 100 + action = self.auth_partner.with_user( + self.env.ref("base.user_admin") + ).impersonate() + token = action["url"].split("/")[-1] + + with freeze_time(datetime.now() + timedelta(hours=1)): + self.env["auth.partner"]._impersonating(self.directory, token) + + def test_impersonate_expired_token(self): + self.directory.impersonating_token_duration = 100 + action = self.auth_partner.with_user( + self.env.ref("base.user_admin") + ).impersonate() + token = action["url"].split("/")[-1] + + with ( + freeze_time(datetime.now() + timedelta(hours=2)), + self.assertRaisesRegex(UserError, "Invalid Token"), + ): + self.env["auth.partner"]._impersonating(self.directory, token) + + def test_set_password_ok(self): + self.auth_partner._set_password( + self.directory, + self.auth_partner._generate_set_password_token(), + "ResetSecret", + ) + auth_partner = self.env["auth.partner"]._login( + self.directory, login="partner-auth@example.org", password="ResetSecret" + ) + self.assertTrue(auth_partner) + + def test_set_password_wrong_token(self): + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.auth_partner._set_password(self.directory, "wrong", "ResetSecret") + + def test_set_password_once(self): + token = self.auth_partner._generate_set_password_token() + self.auth_partner._set_password(self.directory, token, "ResetSecret") + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.auth_partner._set_password(self.directory, token, "ResetSecret") + + def test_set_password_not_expired_token(self): + self.directory.set_password_token_duration = 100 + token = self.auth_partner._generate_set_password_token() + + with freeze_time(datetime.now() + timedelta(hours=1)): + self.auth_partner._set_password(self.directory, token, "ResetSecret") + + auth_partner = self.env["auth.partner"]._login( + self.directory, login="partner-auth@example.org", password="ResetSecret" + ) + self.assertTrue(auth_partner) + + def test_set_password_expired_token(self): + self.directory.set_password_token_duration = 100 + token = self.auth_partner._generate_set_password_token() + + with ( + freeze_time(datetime.now() + timedelta(hours=2)), + self.assertRaisesRegex(UserError, "Invalid Token"), + ): + self.auth_partner._set_password(self.directory, token, "ResetSecret") + + def test_reset_password_ok(self): + with self.new_mails() as new_mails: + self.auth_partner._request_reset_password() + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Reset Password") + self.assertIn( + "Click on the following link to reset your password", new_mails.body + ) + + token = new_mails.body.split("token=")[1].split('">')[0] + self.auth_partner._set_password(self.directory, token, "ResetSecret") + auth_partner = self.env["auth.partner"]._login( + self.directory, login="partner-auth@example.org", password="ResetSecret" + ) + self.assertTrue(auth_partner) + + def test_reset_password_wrong_partner(self): + with self.new_mails() as new_mails: + self.auth_partner._request_reset_password() + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Reset Password") + self.assertIn( + "Click on the following link to reset your password", new_mails.body + ) + + token = new_mails.body.split("token=")[1].split('">')[0] + # This should probably raise instead of reseting the auth_partner password + self.other_auth_partner._set_password(self.directory, token, "ResetSecret") + with self.assertRaisesRegex(AccessDenied, "Invalid Login or Password"): + self.env["auth.partner"]._login( + self.directory, + login="other-partner-auth@example.org", + password="ResetSecret", + ) + + def test_reset_password_once(self): + with self.new_mails() as new_mails: + self.auth_partner._request_reset_password() + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Reset Password") + token = new_mails.body.split("token=")[1].split('">')[0] + self.auth_partner._set_password(self.directory, token, "ResetSecret") + + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.auth_partner._set_password(self.directory, token, "ResetSecret2") + + def test_send_invite_set_password_ok(self): + with self.new_mails() as new_mails: + self.auth_partner._send_invite() + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Welcome") + self.assertIn("your account have been created", new_mails.body) + token = new_mails.body.split("token=")[1].split('">')[0] + + self.auth_partner._set_password(self.directory, token, "ResetSecret") + auth_partner = self.env["auth.partner"]._login( + self.directory, login="partner-auth@example.org", password="ResetSecret" + ) + self.assertTrue(auth_partner) + + def test_send_invite_set_password_once(self): + with self.new_mails() as new_mails: + self.auth_partner._send_invite() + self.assertEqual(len(new_mails), 1) + self.assertEqual(new_mails.subject, "Welcome") + + token = new_mails.body.split("token=")[1].split('">')[0] + self.auth_partner._set_password(self.directory, token, "ResetSecret") + + with self.assertRaisesRegex(UserError, "Invalid Token"): + self.auth_partner._set_password(self.directory, token, "ResetSecret2") diff --git a/auth_partner/views/auth_directory_view.xml b/auth_partner/views/auth_directory_view.xml new file mode 100644 index 000000000..1d8c58cfd --- /dev/null +++ b/auth_partner/views/auth_directory_view.xml @@ -0,0 +1,92 @@ + + + + auth.directory + + + + + + + + + auth.directory + +
    +
    +
    + +
    + +
    +
    +

    + +

    +
    + + + + + + + + + + + + + + +
    +
    +
    +
    + + + auth.directory + + + + + + + + + Directory + ir.actions.act_window + auth.directory + tree,form + + [] + {} + + + +
    diff --git a/auth_partner/views/auth_partner_view.xml b/auth_partner/views/auth_partner_view.xml new file mode 100644 index 000000000..bc9459442 --- /dev/null +++ b/auth_partner/views/auth_partner_view.xml @@ -0,0 +1,98 @@ + + + + auth.partner + + + + + + + + + + + + + + + auth.partner + +
    +
    +
    + +
    +

    + + +

    +
    + + + + + + + +
    +
    +
    +
    + + + auth.partner + + + + + + + + + + + + + Partner + ir.actions.act_window + auth.partner + tree,form + + [] + {} + + + + +
    diff --git a/auth_partner/views/res_partner_view.xml b/auth_partner/views/res_partner_view.xml new file mode 100644 index 000000000..9978e92b2 --- /dev/null +++ b/auth_partner/views/res_partner_view.xml @@ -0,0 +1,22 @@ + + + + res.partner + + +
    + +
    +
    +
    +
    diff --git a/auth_partner/wizards/__init__.py b/auth_partner/wizards/__init__.py new file mode 100644 index 000000000..2f8025a36 --- /dev/null +++ b/auth_partner/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import wizard_auth_partner_reset_password +from . import wizard_auth_partner_force_set_password diff --git a/auth_partner/wizards/wizard_auth_partner_force_set_password.py b/auth_partner/wizards/wizard_auth_partner_force_set_password.py new file mode 100644 index 000000000..66952d3bb --- /dev/null +++ b/auth_partner/wizards/wizard_auth_partner_force_set_password.py @@ -0,0 +1,37 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class WizardAuthPartnerForceSetPassword(models.TransientModel): + _name = "wizard.auth.partner.force.set.password" + _description = "Wizard Partner Auth Reset Password" + + password = fields.Char(required=True) + password_confirm = fields.Char(string="Confirm Password", required=True) + + @api.constrains("password", "password_confirm") + def _check_password(self): + for wizard in self: + if wizard.password != wizard.password_confirm: + raise ValidationError( + _("Password and Confirm Password must be the same") + ) + + def action_force_set_password(self): + self.ensure_one() + if self.env.context.get("active_model") != "auth.partner": + raise UserError(_("This wizard can only be used on auth.partner")) + auth_partner_id = self.env.context.get("active_id") + if not auth_partner_id: + raise UserError(_("No active_id in context")) + + auth_partner = self.env["auth.partner"].browse(auth_partner_id) + + auth_partner.write({"password": self.password}) + + return {"type": "ir.actions.act_window_close"} diff --git a/auth_partner/wizards/wizard_auth_partner_force_set_password_view.xml b/auth_partner/wizards/wizard_auth_partner_force_set_password_view.xml new file mode 100644 index 000000000..8742520c5 --- /dev/null +++ b/auth_partner/wizards/wizard_auth_partner_force_set_password_view.xml @@ -0,0 +1,38 @@ + + + + + + wizard.auth.partner.force.set.password + +
    + + + + +
    +
    + +
    +
    +
    + + + Set Password + wizard.auth.partner.force.set.password + form + new + + +
    diff --git a/auth_partner/wizards/wizard_auth_partner_reset_password.py b/auth_partner/wizards/wizard_auth_partner_reset_password.py new file mode 100644 index 000000000..e69610877 --- /dev/null +++ b/auth_partner/wizards/wizard_auth_partner_reset_password.py @@ -0,0 +1,59 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo import api, fields, models + + +class WizardAuthPartnerResetPassword(models.TransientModel): + _name = "wizard.auth.partner.reset.password" + _description = "Wizard Partner Auth Reset Password" + + delay = fields.Selection( + [ + ("manually", "Manually"), + ("6-hours", "6 Hours"), + ("2-days", "2-days"), + ("7-days", "7 Days"), + ("14-days", "14 Days"), + ], + default="6-hours", + required=True, + ) + template_id = fields.Many2one( + "mail.template", + "Mail Template", + required=True, + domain=[("model_id", "=", "auth.partner")], + ) + date_validity = fields.Datetime( + compute="_compute_date_validity", store=True, readonly=False + ) + + @api.depends("delay") + def _compute_date_validity(self): + for record in self: + if record.delay != "manually": + duration, key = record.delay.split("-") + record.date_validity = datetime.now() + timedelta( + **{key: float(duration)} + ) + + def action_reset_password(self): + expiration_delta = None + if self.delay != "manually": + duration, key = self.delay.split("-") + expiration_delta = timedelta(**{key: float(duration)}) + + for auth_partner in self.env["auth.partner"].browse( + self._context["active_ids"] + ): + auth_partner.directory_id._send_mail_background( + self.template_id, + auth_partner, + callback_job=auth_partner.delayable()._on_reset_password_sent(), + token=auth_partner._generate_set_password_token(expiration_delta), + ) diff --git a/auth_partner/wizards/wizard_auth_partner_reset_password_view.xml b/auth_partner/wizards/wizard_auth_partner_reset_password_view.xml new file mode 100644 index 000000000..f35f4ec29 --- /dev/null +++ b/auth_partner/wizards/wizard_auth_partner_reset_password_view.xml @@ -0,0 +1,42 @@ + + + + + wizard.auth.partner.reset.password + +
    + An email will be send with a token to each customer, you can specify the date until the link is valid + + + + + +
    +
    + +
    +
    +
    + + + Send Reset Password Instruction + wizard.auth.partner.reset.password + ir.actions.act_window + form + new + + + + +
    diff --git a/fastapi_auth_partner/README.rst b/fastapi_auth_partner/README.rst new file mode 100644 index 000000000..69e036674 --- /dev/null +++ b/fastapi_auth_partner/README.rst @@ -0,0 +1,136 @@ +==================== +Fastapi Auth Partner +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:57e60d4a203cd5e613e07fe0c1f6a207bd6b77f537ed5cbbbe72a1cc2184f0de + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_auth_partner + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi_auth_partner + :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/rest-framework&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module is the FastAPI implementation of `auth_partner <../auth_partner>`_ +it provides all the routes to manage the authentication of partners. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +First you have to add the auth router to your FastAPI endpoint and the authentication dependency to your app dependencies: + +.. code-block:: python + + from odoo.addons.fastapi import dependencies + from odoo.addons.fastapi_auth_partner.dependencies import ( + auth_partner_authenticated_partner, + ) + from odoo.addons.fastapi_auth_partner.routers.auth import auth_router + + class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + def _get_fastapi_routers(self): + if self.app == "myapp": + return [ + auth_router, + ] + return super()._get_fastapi_routers() + + def _get_app_dependencies_overrides(self): + res = super()._get_app_dependencies_overrides() + if self.app == "myapp": + res.update( + { + dependencies.authenticated_partner_impl: auth_partner_authenticated_partner, + } + ) + return res + +Next you can manage your authenticable partners and directories in the Odoo interface: + +FastAPI > Authentication > Partner + +and + +FastAPI > Authentication > Directory + +Next you must set the directory used for the authentication in the FastAPI endpoint: + +FastAPI > FastAPI Endpoint > myapp > Directory + +Then you can use the auth router to authenticate your requests: + +- POST /auth/register to register a partner +- POST /auth/login to authenticate a partner +- POST /auth/logout to unauthenticate a partner +- POST /auth/validate_email to validate a partner email +- POST /auth/request_reset_password to request a password reset +- POST /auth/set_password to set a new password +- GET /auth/profile to get the partner profile +- GET /auth/impersonate to impersonate a partner + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_: + + * Sébastien Beau + * Florian Mounier + +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. + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi_auth_partner/__init__.py b/fastapi_auth_partner/__init__.py new file mode 100644 index 000000000..3f274f8d1 --- /dev/null +++ b/fastapi_auth_partner/__init__.py @@ -0,0 +1,4 @@ +from . import models +from . import routers +from . import schemas +from . import wizards diff --git a/fastapi_auth_partner/__manifest__.py b/fastapi_auth_partner/__manifest__.py new file mode 100644 index 000000000..e5b486e4c --- /dev/null +++ b/fastapi_auth_partner/__manifest__.py @@ -0,0 +1,32 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Fastapi Auth Partner", + "summary": """This provides an implementation of auth_partner for FastAPI""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/rest-framework", + "depends": [ + "extendable_fastapi", + "auth_partner", + ], + "data": [ + "security/res_group.xml", + "security/ir.model.access.csv", + "views/auth_partner_view.xml", + "views/auth_directory_view.xml", + "views/fastapi_endpoint_view.xml", + "wizards/wizard_auth_partner_impersonate_view.xml", + "wizards/wizard_auth_partner_reset_password_view.xml", + ], + "demo": [ + "demo/fastapi_endpoint_demo.xml", + ], + "external_dependencies": { + "python": ["itsdangerous"], + }, +} diff --git a/fastapi_auth_partner/demo/fastapi_endpoint_demo.xml b/fastapi_auth_partner/demo/fastapi_endpoint_demo.xml new file mode 100644 index 000000000..bf017151c --- /dev/null +++ b/fastapi_auth_partner/demo/fastapi_endpoint_demo.xml @@ -0,0 +1,26 @@ + + + + Fastapi Auth Partner Demo Endpoint + + demo + /fastapi_auth_partner_demo + auth_partner + + + https://api.example.com/ + https://www.example.com/ + + + + + + diff --git a/fastapi_auth_partner/dependencies.py b/fastapi_auth_partner/dependencies.py new file mode 100644 index 000000000..5d336e59c --- /dev/null +++ b/fastapi_auth_partner/dependencies.py @@ -0,0 +1,74 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +import sys +from typing import Any, Dict, Union + +from itsdangerous import URLSafeTimedSerializer +from starlette.status import HTTP_401_UNAUTHORIZED + +from odoo.api import Environment + +from odoo.addons.base.models.res_partner import Partner +from odoo.addons.fastapi.dependencies import fastapi_endpoint, odoo_env +from odoo.addons.fastapi.models import FastapiEndpoint + +from fastapi import Cookie, Depends, HTTPException, Request, Response + +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + +_logger = logging.getLogger(__name__) + + +Payload = Dict[str, Any] + + +class AuthPartner: + def __init__(self, allow_unauthenticated: bool = False): + self.allow_unauthenticated = allow_unauthenticated + + def __call__( + self, + request: Request, + response: Response, + env: Annotated[ + Environment, + Depends(odoo_env), + ], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + fastapi_auth_partner: Annotated[Union[str, None], Cookie()] = None, + ) -> Partner: + if not fastapi_auth_partner and self.allow_unauthenticated: + return env["res.partner"].with_user(env.ref("base.public_user")).browse() + + elif fastapi_auth_partner: + directory = endpoint.sudo().directory_id + try: + vals = URLSafeTimedSerializer( + directory.cookie_secret_key or directory.secret_key + ).loads(fastapi_auth_partner, max_age=directory.cookie_duration * 60) + except Exception as e: + _logger.error("Invalid cookies error %s", e) + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED) from e + if vals["did"] == directory.id and vals["pid"]: + partner = env["res.partner"].browse(vals["pid"]).exists() + if partner: + auth_partner = partner._get_auth_partner_for_directory(directory) + if auth_partner: + if directory.sliding_session: + helper = env["fastapi.auth.service"].new( + {"endpoint_id": endpoint} + ) + helper._set_auth_cookie(auth_partner, request, response) + return partner + _logger.info("Could not determine partner from 'fastapi_auth_partner' cookie.") + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED) + + +auth_partner_authenticated_partner = AuthPartner() +auth_partner_optionally_authenticated_partner = AuthPartner(allow_unauthenticated=True) diff --git a/fastapi_auth_partner/models/__init__.py b/fastapi_auth_partner/models/__init__.py new file mode 100644 index 000000000..526f7a263 --- /dev/null +++ b/fastapi_auth_partner/models/__init__.py @@ -0,0 +1,3 @@ +from . import auth_directory +from . import auth_partner +from . import fastapi_endpoint diff --git a/fastapi_auth_partner/models/auth_directory.py b/fastapi_auth_partner/models/auth_directory.py new file mode 100644 index 000000000..b671bf96d --- /dev/null +++ b/fastapi_auth_partner/models/auth_directory.py @@ -0,0 +1,51 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class AuthDirectory(models.Model): + _inherit = "auth.directory" + + fastapi_endpoint_ids = fields.One2many( + "fastapi.endpoint", + "directory_id", + string="FastAPI Endpoints", + ) + + cookie_secret_key = fields.Char( + groups="base.group_system", + help="The secret key used to sign the cookie", + required=True, + default=lambda self: self._generate_default_secret_key(), + ) + cookie_duration = fields.Integer( + default=525600, + help="In minute, default 525600 minutes => 1 year", + required=True, + ) + sliding_session = fields.Boolean() + + def action_regenerate_cookie_secret_key(self): + self.ensure_one() + self.cookie_secret_key = self._generate_default_secret_key() + + def _prepare_mail_context(self, context): + rv = super()._prepare_mail_context(context) + endpoint_id = self.env.context.get("_fastapi_endpoint_id") + + if endpoint_id: + endpoint = self.env["fastapi.endpoint"].browse(endpoint_id) + rv["public_url"] = endpoint.public_url or endpoint.public_api_url + + return rv + + @property + def _server_env_fields(self): + return { + **super()._server_env_fields, + "cookie_secret_key": {}, + } diff --git a/fastapi_auth_partner/models/auth_partner.py b/fastapi_auth_partner/models/auth_partner.py new file mode 100644 index 000000000..5cba6581b --- /dev/null +++ b/fastapi_auth_partner/models/auth_partner.py @@ -0,0 +1,82 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, models +from odoo.exceptions import AccessDenied, UserError +from odoo.http import request + + +class AuthPartner(models.Model): + _inherit = "auth.partner" + + def local_impersonate(self): + """Local impersonate for dev mode""" + self.ensure_one() + + if not self.env.user._is_admin(): + raise AccessDenied(_("Only admin can impersonate locally")) + + if not hasattr(request, "future_response"): + raise UserError( + _("Please install base_future_response for local impersonate to work") + ) + + for endpoint in self.directory_id.fastapi_endpoint_ids: + helper = self.env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + helper._set_auth_cookie(self, request.httprequest, request.future_response) + + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Impersonation successful"), + "message": _("You are now impersonating %s\n%%s") % self.login, + "links": [ + { + "label": f"{endpoint.app.title()} api docs", + "url": endpoint.docs_url, + } + for endpoint in self.directory_id.fastapi_endpoint_ids + ], + "type": "success", + "sticky": False, + }, + } + + def _get_impersonate_url(self, token, **kwargs): + endpoint = kwargs.get("endpoint") + if not endpoint: + return super()._get_impersonate_url(token, **kwargs) + + base = ( + endpoint.public_api_url + or endpoint.public_url + or ( + self.env["ir.config_parameter"].sudo().get_param("web.base.url") + + endpoint.root_path + ) + ) + return f"{base.rstrip('/')}/auth/impersonate/{token}" + + def _get_impersonate_action(self, token, **kwargs): + # Get the endpoint from a wizard + endpoint_id = self.env.context.get("fastapi_endpoint_id") + endpoint = None + + if endpoint_id: + endpoint = self.env["fastapi.endpoint"].browse(endpoint_id) + + if not endpoint: + endpoints = self.directory_id.fastapi_endpoint_ids + if len(endpoints) == 1: + endpoint = endpoints + else: + wizard = self.env["ir.actions.act_window"]._for_xml_id( + "fastapi_auth_partner.auth_partner_action_impersonate" + ) + wizard["context"] = {"default_auth_partner_id": self.id} + return wizard + + return super()._get_impersonate_action(token, endpoint=endpoint, **kwargs) diff --git a/fastapi_auth_partner/models/fastapi_endpoint.py b/fastapi_auth_partner/models/fastapi_endpoint.py new file mode 100644 index 000000000..cab4f80c5 --- /dev/null +++ b/fastapi_auth_partner/models/fastapi_endpoint.py @@ -0,0 +1,55 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from typing import List + +from odoo import fields, models + +from fastapi import APIRouter + +from ..routers.auth import auth_router + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + app = fields.Selection( + selection_add=[("demo", "Demo Endpoint")], ondelete={"demo": "cascade"} + ) + demo_auth_method = fields.Selection( + selection_add=[ + ("auth_partner", "Partner Auth"), + ], + string="Authentication method", + ) + directory_id = fields.Many2one("auth.directory") + + is_auth_partner = fields.Boolean( + compute="_compute_is_auth_partner", + help="Technical field to know if the auth method is partner", + ) + public_api_url: str = fields.Char( + help="The public URL of the API.\n" + "This URL is used in impersonation to set the cookie on the right API " + "domain if you use a reverse proxy to serve the API.\n" + "Defaults to the public_url if not set or the odoo url if not set either." + ) + # More info in https://github.com/OCA/rest-framework/pull/438/files + public_url: str = fields.Char( + help="The public URL of the site.\n" + "This URL is used for the impersonation final redirect. " + "And can also be used in the mail template to construct links.\n" + "Default to the public_api_url if not set or the odoo url if not set either." + ) + + def _get_fastapi_routers(self) -> List[APIRouter]: + routers = super()._get_fastapi_routers() + if self.app == "demo" and self.demo_auth_method == "auth_partner": + routers.append(auth_router) + return routers + + def _compute_is_auth_partner(self): + for rec in self: + rec.is_auth_partner = auth_router in rec._get_fastapi_routers() diff --git a/fastapi_auth_partner/readme/CONTRIBUTORS.rst b/fastapi_auth_partner/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..bae3cc9a1 --- /dev/null +++ b/fastapi_auth_partner/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Akretion `_: + + * Sébastien Beau + * Florian Mounier diff --git a/fastapi_auth_partner/readme/DESCRIPTION.rst b/fastapi_auth_partner/readme/DESCRIPTION.rst new file mode 100644 index 000000000..e2fa8ca8d --- /dev/null +++ b/fastapi_auth_partner/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module is the FastAPI implementation of `auth_partner <../auth_partner>`_ +it provides all the routes to manage the authentication of partners. diff --git a/fastapi_auth_partner/readme/USAGE.rst b/fastapi_auth_partner/readme/USAGE.rst new file mode 100644 index 000000000..316655690 --- /dev/null +++ b/fastapi_auth_partner/readme/USAGE.rst @@ -0,0 +1,52 @@ +First you have to add the auth router to your FastAPI endpoint and the authentication dependency to your app dependencies: + +.. code-block:: python + + from odoo.addons.fastapi import dependencies + from odoo.addons.fastapi_auth_partner.dependencies import ( + auth_partner_authenticated_partner, + ) + from odoo.addons.fastapi_auth_partner.routers.auth import auth_router + + class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + def _get_fastapi_routers(self): + if self.app == "myapp": + return [ + auth_router, + ] + return super()._get_fastapi_routers() + + def _get_app_dependencies_overrides(self): + res = super()._get_app_dependencies_overrides() + if self.app == "myapp": + res.update( + { + dependencies.authenticated_partner_impl: auth_partner_authenticated_partner, + } + ) + return res + +Next you can manage your authenticable partners and directories in the Odoo interface: + +FastAPI > Authentication > Partner + +and + +FastAPI > Authentication > Directory + +Next you must set the directory used for the authentication in the FastAPI endpoint: + +FastAPI > FastAPI Endpoint > myapp > Directory + +Then you can use the auth router to authenticate your requests: + +- POST /auth/register to register a partner +- POST /auth/login to authenticate a partner +- POST /auth/logout to unauthenticate a partner +- POST /auth/validate_email to validate a partner email +- POST /auth/request_reset_password to request a password reset +- POST /auth/set_password to set a new password +- GET /auth/profile to get the partner profile +- GET /auth/impersonate to impersonate a partner diff --git a/fastapi_auth_partner/routers/__init__.py b/fastapi_auth_partner/routers/__init__.py new file mode 100644 index 000000000..582cb2cd7 --- /dev/null +++ b/fastapi_auth_partner/routers/__init__.py @@ -0,0 +1 @@ +from .auth import auth_router diff --git a/fastapi_auth_partner/routers/auth.py b/fastapi_auth_partner/routers/auth.py new file mode 100644 index 000000000..b06f4fcff --- /dev/null +++ b/fastapi_auth_partner/routers/auth.py @@ -0,0 +1,252 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import sys + +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + +from datetime import datetime, timedelta, timezone + +from itsdangerous import URLSafeTimedSerializer + +from odoo import _, fields, models, tools +from odoo.api import Environment +from odoo.exceptions import ValidationError + +from odoo.addons.base.models.res_partner import Partner +from odoo.addons.fastapi.dependencies import fastapi_endpoint, odoo_env +from odoo.addons.fastapi.models import FastapiEndpoint + +from fastapi import APIRouter, Depends, Request, Response +from fastapi.responses import RedirectResponse + +from ..dependencies import auth_partner_authenticated_partner +from ..schemas import ( + AuthForgetPasswordInput, + AuthLoginInput, + AuthPartnerResponse, + AuthRegisterInput, + AuthSetPasswordInput, + AuthValidateEmailInput, +) + +COOKIE_AUTH_NAME = "fastapi_auth_partner" + +auth_router = APIRouter(tags=["auth"]) + + +@auth_router.post("/auth/register", status_code=201) +def register( + data: AuthRegisterInput, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + request: Request, + response: Response, +) -> AuthPartnerResponse: + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + auth_partner = helper._signup(data) + helper._set_auth_cookie(auth_partner, request, response) + return AuthPartnerResponse.from_auth_partner(auth_partner) + + +@auth_router.post("/auth/login") +def login( + data: AuthLoginInput, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + request: Request, + response: Response, +) -> AuthPartnerResponse: + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + auth_partner = helper._login(data) + helper._set_auth_cookie(auth_partner, request, response) + return AuthPartnerResponse.from_auth_partner(auth_partner) + + +@auth_router.post("/auth/logout", status_code=205) +def logout( + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + response: Response, +): + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + helper._logout() + helper._clear_auth_cookie(response) + return {} + + +@auth_router.post("/auth/validate_email") +def validate_email( + data: AuthValidateEmailInput, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], +): + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + helper._validate_email(data) + return {} + + +@auth_router.post("/auth/request_reset_password") +def request_reset_password( + data: AuthForgetPasswordInput, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], +): + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + helper._request_reset_password(data) + return {} + + +@auth_router.post("/auth/set_password") +def set_password( + data: AuthSetPasswordInput, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + request: Request, + response: Response, +) -> AuthPartnerResponse: + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + auth_partner = helper._set_password(data) + helper._set_auth_cookie(auth_partner, request, response) + return AuthPartnerResponse.from_auth_partner(auth_partner) + + +@auth_router.get("/auth/profile") +def profile( + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + partner: Annotated[Partner, Depends(auth_partner_authenticated_partner)], +) -> AuthPartnerResponse: + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + auth_partner = helper._get_auth_from_partner(partner) + return AuthPartnerResponse.from_auth_partner(auth_partner) + + +@auth_router.get("/auth/impersonate/{token}") +def impersonate( + token: str, + env: Annotated[Environment, Depends(odoo_env)], + endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + request: Request, +) -> RedirectResponse: + helper = env["fastapi.auth.service"].new({"endpoint_id": endpoint}) + auth_partner = helper._impersonate(token) + base = ( + endpoint.public_url + or endpoint.public_api_url + or ( + env["ir.config_parameter"].sudo().get_param("web.base.url") + + endpoint.root_path + ) + ) + response = RedirectResponse(url=base) + helper._set_auth_cookie(auth_partner, request, response) + return response + + +class AuthService(models.AbstractModel): + _name = "fastapi.auth.service" + _description = "Fastapi Auth Service" + + endpoint_id = fields.Many2one("fastapi.endpoint", required=True) + directory_id = fields.Many2one("auth.directory") + + def new(self, vals, **kwargs): + rec = super().new(vals, **kwargs) + # Can't have computed / related field in AbstractModel + rec.directory_id = rec.endpoint_id.directory_id + # Auto add endpoint context for mail context + return rec.with_context(_fastapi_endpoint_id=vals["endpoint_id"].id) + + def _get_auth_from_partner(self, partner): + return partner._get_auth_partner_for_directory(self.directory_id) + + def _signup(self, data): + auth_partner = ( + self.env["auth.partner"].sudo()._signup(self.directory_id, **data.dict()) + ) + return auth_partner + + def _login(self, data): + return self.env["auth.partner"].sudo()._login(self.directory_id, **data.dict()) + + def _impersonate(self, token): + return self.env["auth.partner"].sudo()._impersonating(self.directory_id, token) + + def _logout(self): + pass + + def _set_password(self, data): + return ( + self.env["auth.partner"] + .sudo() + ._set_password(self.directory_id, data.token, data.password) + ) + + def _request_reset_password(self, data): + # There can be only one auth_partner per login per directory + auth_partner = ( + self.env["auth.partner"] + .sudo() + .search( + [ + ("directory_id", "=", self.directory_id.id), + ("login", "=", data.login.lower()), + ] + ) + ) + + if not auth_partner: + # do not leak information, no partner no mail sent + return + + return auth_partner.sudo()._request_reset_password() + + def _validate_email(self, data): + return ( + self.env["auth.partner"] + .sudo() + ._validate_email(self.directory_id, data.token) + ) + + def _prepare_cookie_payload(self, partner): + # use short key to reduce cookie size + return { + "did": self.directory_id.id, + "pid": partner.id, + } + + def _prepare_cookie(self, partner): + secret = self.directory_id.cookie_secret_key or self.directory_id.secret_key + if not secret: + raise ValidationError(_("No cookie secret key defined")) + payload = self._prepare_cookie_payload(partner) + value = URLSafeTimedSerializer(secret).dumps(payload) + exp = ( + datetime.now(timezone.utc) + + timedelta(minutes=self.directory_id.cookie_duration) + ).timestamp() + vals = { + "value": value, + "expires": exp, + "httponly": True, + "secure": True, + "samesite": "strict", + } + if tools.config.get("test_enable"): + # do not force https for test + vals["secure"] = False + return vals + + def _set_auth_cookie(self, auth_partner, request, response): + response.set_cookie( + COOKIE_AUTH_NAME, **self.sudo()._prepare_cookie(auth_partner.partner_id) + ) + + def _clear_auth_cookie(self, response): + response.set_cookie(COOKIE_AUTH_NAME, max_age=0) diff --git a/fastapi_auth_partner/schemas.py b/fastapi_auth_partner/schemas.py new file mode 100644 index 000000000..27bec5f05 --- /dev/null +++ b/fastapi_auth_partner/schemas.py @@ -0,0 +1,40 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from extendable_pydantic import StrictExtendableBaseModel + + +class AuthLoginInput(StrictExtendableBaseModel): + login: str + password: str + + +class AuthRegisterInput(StrictExtendableBaseModel): + name: str + login: str + password: str + + +class AuthForgetPasswordInput(StrictExtendableBaseModel): + login: str + + +class AuthSetPasswordInput(StrictExtendableBaseModel): + token: str + password: str + + +class AuthValidateEmailInput(StrictExtendableBaseModel): + token: str + + +class AuthPartnerResponse(StrictExtendableBaseModel): + login: str + mail_verified: bool + + @classmethod + def from_auth_partner(cls, odoo_rec): + return cls.model_construct( + login=odoo_rec.login, mail_verified=odoo_rec.mail_verified + ) diff --git a/fastapi_auth_partner/security/ir.model.access.csv b/fastapi_auth_partner/security/ir.model.access.csv new file mode 100644 index 000000000..b52ae0751 --- /dev/null +++ b/fastapi_auth_partner/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +api_access_fastapi_wizard_auth_partner_impersonate,fastapi_wizard_auth_partner_impersonate,model_wizard_auth_partner_impersonate,auth_partner.group_auth_partner_manager,1,1,1,1 diff --git a/fastapi_auth_partner/security/res_group.xml b/fastapi_auth_partner/security/res_group.xml new file mode 100644 index 000000000..c7f87fb9e --- /dev/null +++ b/fastapi_auth_partner/security/res_group.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/fastapi_auth_partner/static/description/index.html b/fastapi_auth_partner/static/description/index.html new file mode 100644 index 000000000..7eb80ab0a --- /dev/null +++ b/fastapi_auth_partner/static/description/index.html @@ -0,0 +1,477 @@ + + + + + +Fastapi Auth Partner + + + +
    +

    Fastapi Auth Partner

    + + +

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    This module is the FastAPI implementation of auth_partner +it provides all the routes to manage the authentication of partners.

    +

    Table of contents

    + +
    +

    Usage

    +

    First you have to add the auth router to your FastAPI endpoint and the authentication dependency to your app dependencies:

    +
    +from odoo.addons.fastapi import dependencies
    +from odoo.addons.fastapi_auth_partner.dependencies import (
    +  auth_partner_authenticated_partner,
    +)
    +from odoo.addons.fastapi_auth_partner.routers.auth import auth_router
    +
    +class FastapiEndpoint(models.Model):
    +    _inherit = "fastapi.endpoint"
    +
    +    def _get_fastapi_routers(self):
    +      if self.app == "myapp":
    +          return [
    +              auth_router,
    +          ]
    +      return super()._get_fastapi_routers()
    +
    +    def _get_app_dependencies_overrides(self):
    +        res = super()._get_app_dependencies_overrides()
    +        if self.app == "myapp":
    +            res.update(
    +                {
    +                    dependencies.authenticated_partner_impl: auth_partner_authenticated_partner,
    +                }
    +            )
    +        return res
    +
    +

    Next you can manage your authenticable partners and directories in the Odoo interface:

    +

    FastAPI > Authentication > Partner

    +

    and

    +

    FastAPI > Authentication > Directory

    +

    Next you must set the directory used for the authentication in the FastAPI endpoint:

    +

    FastAPI > FastAPI Endpoint > myapp > Directory

    +

    Then you can use the auth router to authenticate your requests:

    +
      +
    • POST /auth/register to register a partner
    • +
    • POST /auth/login to authenticate a partner
    • +
    • POST /auth/logout to unauthenticate a partner
    • +
    • POST /auth/validate_email to validate a partner email
    • +
    • POST /auth/request_reset_password to request a password reset
    • +
    • POST /auth/set_password to set a new password
    • +
    • GET /auth/profile to get the partner profile
    • +
    • GET /auth/impersonate to impersonate a partner
    • +
    +
    +
    +

    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.

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • Akretion
    • +
    +
    +
    +

    Contributors

    +
      +
    • Akretion:
        +
      • Sébastien Beau
      • +
      • Florian Mounier
      • +
      +
    • +
    +
    +
    +

    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/rest-framework project on GitHub.

    +

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

    +
    +
    +
    + + diff --git a/fastapi_auth_partner/tests/__init__.py b/fastapi_auth_partner/tests/__init__.py new file mode 100644 index 000000000..021c23763 --- /dev/null +++ b/fastapi_auth_partner/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_auth +from . import test_fastapi_auth_partner_demo diff --git a/fastapi_auth_partner/tests/test_auth.py b/fastapi_auth_partner/tests/test_auth.py new file mode 100644 index 000000000..04f24ccf5 --- /dev/null +++ b/fastapi_auth_partner/tests/test_auth.py @@ -0,0 +1,243 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import json +from contextlib import contextmanager +from functools import partial + +from requests import Response + +from odoo.tests.common import tagged +from odoo.tools import mute_logger + +from odoo.addons.auth_partner.tests.common import CommonTestAuthPartner +from odoo.addons.extendable_fastapi.tests.common import FastAPITransactionCase +from odoo.addons.fastapi.dependencies import fastapi_endpoint + +from fastapi import status + +from ..routers.auth import auth_router + + +class CommonTestAuth(FastAPITransactionCase): + @contextmanager + def _create_test_client(self, **kwargs): + self.env.invalidate_all() + with mute_logger("httpx"): + with super()._create_test_client(**kwargs) as test_client: + yield test_client + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.demo_app = cls.env.ref("fastapi_auth_partner.fastapi_endpoint_demo") + cls.env = cls.env(context=dict(cls.env.context, queue_job__no_delay=True)) + cls.default_fastapi_router = auth_router + cls.default_fastapi_app = cls.demo_app._get_app() + cls.default_fastapi_dependency_overrides = { + fastapi_endpoint: partial(lambda a: a, cls.demo_app) + } + cls.default_fastapi_odoo_env = cls.env + cls.default_fastapi_running_user = cls.demo_app.user_id + + def _register_partner(self): + with self._create_test_client() as test_client, self.new_mails() as new_mails: + response: Response = test_client.post( + "/auth/register", + content=json.dumps( + { + "name": "Loriot", + "login": "loriot@example.org", + "password": "supersecret", + } + ), + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) + return response, new_mails + + def _login(self, test_client, password="supersecret"): + response: Response = test_client.post( + "/auth/login", + content=json.dumps( + { + "login": "loriot@example.org", + "password": password, + } + ), + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + return response + + +@tagged("post_install", "-at_install") +class TestFastapiAuthPartner(CommonTestAuth, CommonTestAuthPartner): + def test_register(self): + response, new_mails = self._register_partner() + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.text) + self.assertEqual( + response.json(), {"login": "loriot@example.org", "mail_verified": False} + ) + self.assertEqual(len(new_mails), 1) + self.assertIn( + "please click on the following link to verify your email", + str(new_mails.body), + ) + + def test_login(self): + self._register_partner() + with self._create_test_client() as test_client: + response = self._login(test_client) + self.assertEqual( + response.json(), {"login": "loriot@example.org", "mail_verified": False} + ) + + def test_logout(self): + self._register_partner() + with self._create_test_client() as test_client: + response: Response = test_client.post("/auth/logout") + self.assertEqual( + response.status_code, status.HTTP_205_RESET_CONTENT, response.text + ) + + def test_request_reset_password(self): + self._register_partner() + with self._create_test_client() as test_client, self.new_mails() as new_mails: + response: Response = test_client.post( + "/auth/request_reset_password", + content=json.dumps({"login": "loriot@example.org"}), + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + self.assertFalse( + self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .mail_verified, + ) + self.assertEqual(len(new_mails), 1) + self.assertIn( + "Click on the following link to reset your password", + str(new_mails.body), + ) + token = str(new_mails.body).split("token=")[1].split('">')[0] + response: Response = test_client.post( + "/auth/set_password", + content=json.dumps( + { + "password": "megasecret", + "token": token, + } + ), + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + + self.assertTrue( + self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .mail_verified, + ) + response = self._login(test_client, password="megasecret") + self.assertEqual( + response.json(), {"login": "loriot@example.org", "mail_verified": True} + ) + + def test_validate_email(self): + self._register_partner() + mail = self.env["mail.mail"].search([], limit=1, order="id desc") + self.assertIn( + "please click on the following link to verify your email", str(mail.body) + ) + self.assertFalse( + self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .mail_verified, + ) + token = str(mail.body).split("token=")[1].split('">')[0] + with self._create_test_client() as test_client: + response: Response = test_client.post( + "/auth/validate_email", + content=json.dumps({"token": token}), + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.text) + + self.assertTrue( + self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .mail_verified, + ) + + def test_impersonate(self): + self.demo_app.public_url = self.demo_app.public_api_url = False + self._register_partner() + auth_partner = self.env["auth.partner"].search( + [("login", "=", "loriot@example.org")] + ) + self.assertEqual(len(auth_partner), 1) + action = auth_partner.with_user(self.env.ref("base.user_admin")).impersonate() + url = action["url"].split("fastapi_auth_partner_demo", 1)[1] + + with self._create_test_client() as test_client: + response: Response = test_client.get(url, follow_redirects=False) + self.assertEqual(response.status_code, status.HTTP_307_TEMPORARY_REDIRECT) + self.assertTrue( + response.headers["location"].endswith("/fastapi_auth_partner_demo") + ) + self.assertIn("fastapi_auth_partner", response.cookies) + + def test_impersonate_api_url(self): + self._register_partner() + auth_partner = self.env["auth.partner"].search( + [("login", "=", "loriot@example.org")] + ) + self.assertEqual(len(auth_partner), 1) + action = auth_partner.with_user(self.env.ref("base.user_admin")).impersonate() + self.assertTrue( + action["url"].startswith("https://api.example.com/auth/impersonate/") + ) + action["url"].split("auth/impersonate/", 1)[1] + + def test_wizard_auth_partner_impersonate(self): + self._register_partner() + action = ( + self.env["wizard.auth.partner.impersonate"] + .create( + { + "auth_partner_id": self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .id, + "fastapi_endpoint_id": self.demo_app.id, + } + ) + .with_user(self.env.ref("base.user_admin")) + .action_impersonate() + ) + self.assertTrue( + action["url"].startswith("https://api.example.com/auth/impersonate/") + ) + + def test_wizard_auth_partner_reset_password(self): + self._register_partner() + + template = self.env.ref("auth_partner.email_reset_password") + template.body_html = template.body_html.replace( + "https://example.org/", "{{ object.env.context['public_url'] }}" + ) + with self.new_mails() as new_mails: + self.env["wizard.auth.partner.reset.password"].create( + { + "delay": "2-days", + "template_id": template.id, + "fastapi_endpoint_id": self.demo_app.id, + } + ).with_context( + active_ids=self.env["auth.partner"] + .search([("login", "=", "loriot@example.org")]) + .ids + ).action_reset_password() + + self.assertEqual(len(new_mails), 1) + self.assertIn( + "Click on the following link to reset your password", str(new_mails.body) + ) + self.assertIn( + "https://www.example.com/password/reset?token=", str(new_mails.body) + ) diff --git a/fastapi_auth_partner/tests/test_fastapi_auth_partner_demo.py b/fastapi_auth_partner/tests/test_fastapi_auth_partner_demo.py new file mode 100644 index 000000000..0539cef17 --- /dev/null +++ b/fastapi_auth_partner/tests/test_fastapi_auth_partner_demo.py @@ -0,0 +1,93 @@ +# Copyright 2023 ACSONE SA/NV +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +import sys + +from odoo import tests + +from odoo.addons.base.models.res_partner import Partner +from odoo.addons.fastapi_auth_partner.dependencies import AuthPartner + +from fastapi import Depends, status + +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + +from odoo.addons.fastapi_auth_partner.routers.auth import auth_router +from odoo.addons.fastapi_auth_partner.schemas import AuthPartnerResponse + + +@auth_router.get("/auth/whoami-public-or-partner") +def whoami_public_or_partner( + partner: Annotated[ + Partner, + Depends(AuthPartner(allow_unauthenticated=True)), + ], +) -> AuthPartnerResponse: + if partner: + return AuthPartnerResponse.from_auth_partner(partner.auth_partner_ids) + return AuthPartnerResponse(login="no-one", mail_verified=False) + + +@tests.tagged("post_install", "-at_install") +class TestEndToEnd(tests.HttpCase): + def setUp(self): + super().setUp() + endpoint = self.env.ref("fastapi_auth_partner.fastapi_endpoint_demo") + endpoint._handle_registry_sync() + + self.fastapi_demo_app = self.env.ref("fastapi.fastapi_endpoint_demo") + self.fastapi_demo_app._handle_registry_sync() + + def _register_partner(self): + return self.url_open( + "/fastapi_auth_partner_demo/auth/register", + timeout=1000, + data=json.dumps( + { + "name": "Loriot", + "login": "loriot@example.org", + "password": "supersecret", + } + ), + ) + + def test_register(self): + response = self._register_partner() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual( + response.json(), {"login": "loriot@example.org", "mail_verified": False} + ) + self.assertIn("fastapi_auth_partner", response.cookies) + + def test_profile(self): + self._register_partner() + resp = self.url_open("/fastapi_auth_partner_demo/auth/profile") + resp.raise_for_status() + data = resp.json() + self.assertEqual( + data, + {"login": "loriot@example.org", "mail_verified": False}, + ) + + def test_profile_forbidden(self): + """A end-to-end test with negative authentication.""" + resp = self.url_open("/fastapi_auth_partner_demo/auth/profile") + self.assertEqual(resp.status_code, 401) + + def test_public(self): + """A end-to-end test for anonymous/public access.""" + resp = self.url_open("/fastapi_auth_partner_demo/auth/whoami-public-or-partner") + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json(), {"login": "no-one", "mail_verified": False}) + + self._register_partner() + resp = self.url_open("/fastapi_auth_partner_demo/auth/whoami-public-or-partner") + self.assertEqual( + resp.json(), {"login": "loriot@example.org", "mail_verified": False} + ) diff --git a/fastapi_auth_partner/views/auth_directory_view.xml b/fastapi_auth_partner/views/auth_directory_view.xml new file mode 100644 index 000000000..fd115531c --- /dev/null +++ b/fastapi_auth_partner/views/auth_directory_view.xml @@ -0,0 +1,29 @@ + + + + auth.directory + + +
    +
    + + + + + + +
    + + +
    diff --git a/fastapi_auth_partner/views/auth_partner_view.xml b/fastapi_auth_partner/views/auth_partner_view.xml new file mode 100644 index 000000000..0417e1f32 --- /dev/null +++ b/fastapi_auth_partner/views/auth_partner_view.xml @@ -0,0 +1,31 @@ + + + + auth.partner + + + + + + + + + diff --git a/fastapi_auth_partner/views/fastapi_endpoint_view.xml b/fastapi_auth_partner/views/fastapi_endpoint_view.xml new file mode 100644 index 000000000..10c72e35d --- /dev/null +++ b/fastapi_auth_partner/views/fastapi_endpoint_view.xml @@ -0,0 +1,25 @@ + + + + fastapi.endpoint + + + + + + + + + + + + + + + diff --git a/fastapi_auth_partner/wizards/__init__.py b/fastapi_auth_partner/wizards/__init__.py new file mode 100644 index 000000000..adc3f5233 --- /dev/null +++ b/fastapi_auth_partner/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import wizard_auth_partner_impersonate +from . import wizard_auth_partner_reset_password diff --git a/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate.py b/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate.py new file mode 100644 index 000000000..8d04cef3c --- /dev/null +++ b/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate.py @@ -0,0 +1,29 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class WizardAuthPartnerImpersonate(models.TransientModel): + _name = "wizard.auth.partner.impersonate" + _description = "Wizard Partner Auth Impersonate" + + auth_partner_id = fields.Many2one( + "auth.partner", + required=True, + ) + auth_directory_id = fields.Many2one( + "auth.directory", + related="auth_partner_id.directory_id", + ) + fastapi_endpoint_id = fields.Many2one( + "fastapi.endpoint", + required=True, + ) + + def action_impersonate(self): + return self.auth_partner_id.with_context( + fastapi_endpoint_id=self.fastapi_endpoint_id.id + ).impersonate() diff --git a/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate_view.xml b/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate_view.xml new file mode 100644 index 000000000..e9640c5df --- /dev/null +++ b/fastapi_auth_partner/wizards/wizard_auth_partner_impersonate_view.xml @@ -0,0 +1,43 @@ + + + + + wizard.auth.partner.impersonate + +
    + Please choose an endpoint: + + + + + +
    +
    + +
    +
    +
    + + + Impersonate + wizard.auth.partner.impersonate + ir.actions.act_window + form + new + + + +
    diff --git a/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password.py b/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password.py new file mode 100644 index 000000000..eed208113 --- /dev/null +++ b/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password.py @@ -0,0 +1,18 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class WizardAuthPartnerResetPassword(models.TransientModel): + _inherit = "wizard.auth.partner.reset.password" + + fastapi_endpoint_id = fields.Many2one( + "fastapi.endpoint", + ) + + def action_reset_password(self): + if self.fastapi_endpoint_id: + self = self.with_context(_fastapi_endpoint_id=self.fastapi_endpoint_id.id) + return super(WizardAuthPartnerResetPassword, self).action_reset_password() diff --git a/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password_view.xml b/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password_view.xml new file mode 100644 index 000000000..1495e72d0 --- /dev/null +++ b/fastapi_auth_partner/wizards/wizard_auth_partner_reset_password_view.xml @@ -0,0 +1,17 @@ + + + + + wizard.auth.partner.reset.password + + + + + + + + + diff --git a/requirements.txt b/requirements.txt index bf2e66750..1b44acbbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ extendable>=0.0.4 fastapi>=0.110.0 graphene graphql_server +itsdangerous jsondiff marshmallow marshmallow-objects>=2.0.0 @@ -17,6 +18,7 @@ marshmallow<4.0.0 parse-accept-language pydantic pydantic>=2.0.0 +pyjwt pyquerystring python-multipart typing-extensions diff --git a/setup/auth_partner/odoo/addons/auth_partner b/setup/auth_partner/odoo/addons/auth_partner new file mode 120000 index 000000000..736694d4a --- /dev/null +++ b/setup/auth_partner/odoo/addons/auth_partner @@ -0,0 +1 @@ +../../../../auth_partner \ No newline at end of file diff --git a/setup/auth_partner/setup.py b/setup/auth_partner/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/auth_partner/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/fastapi_auth_partner/odoo/addons/fastapi_auth_partner b/setup/fastapi_auth_partner/odoo/addons/fastapi_auth_partner new file mode 120000 index 000000000..481ffc2a2 --- /dev/null +++ b/setup/fastapi_auth_partner/odoo/addons/fastapi_auth_partner @@ -0,0 +1 @@ +../../../../fastapi_auth_partner \ No newline at end of file diff --git a/setup/fastapi_auth_partner/setup.py b/setup/fastapi_auth_partner/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/fastapi_auth_partner/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 3b35ee0d66ccb8133bf82ddd78cf8952182a5142 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 3 Jun 2025 13:40:57 +0000 Subject: [PATCH 26/58] [UPD] Update fastapi_encrypted_errors.pot --- .../i18n/fastapi_encrypted_errors.pot | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 fastapi_encrypted_errors/i18n/fastapi_encrypted_errors.pot diff --git a/fastapi_encrypted_errors/i18n/fastapi_encrypted_errors.pot b/fastapi_encrypted_errors/i18n/fastapi_encrypted_errors.pot new file mode 100644 index 000000000..c15568724 --- /dev/null +++ b/fastapi_encrypted_errors/i18n/fastapi_encrypted_errors.pot @@ -0,0 +1,113 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fastapi_encrypted_errors +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.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: fastapi_encrypted_errors +#: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_view_form +msgid "Close" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__create_uid +msgid "Created by" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__create_date +msgid "Created on" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.actions.act_window,name:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_action_decrypt_error +#: model:ir.ui.menu,name:fastapi_encrypted_errors.menu_fastapi_decrypt_errors +#: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_view_form +msgid "Decrypt Error" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__decrypted_error +msgid "Decrypted Error" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__display_name +msgid "Display Name" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_fastapi_endpoint__encrypt_errors +msgid "Encrypt Errors" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,help:fastapi_encrypted_errors.field_fastapi_endpoint__encrypt_errors +msgid "Encrypt errors before sending them to the client." +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_fastapi_endpoint__encrypted_errors_secret_key +msgid "Encrypted Errors Secret Key" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__error +msgid "Error" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model,name:fastapi_encrypted_errors.model_fastapi_endpoint +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__fastapi_endpoint_id +msgid "FastAPI Endpoint" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.fastapi_endpoint_form_view +msgid "Generate Secret Key" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__id +msgid "ID" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_view_form +msgid "Label" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,help:fastapi_encrypted_errors.field_fastapi_endpoint__encrypted_errors_secret_key +msgid "" +"The secret key used to encrypt errors before sending them to the client." +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model,name:fastapi_encrypted_errors.model_wizard_fastapi_decrypt_errors +msgid "Wizard to decrypt FastAPI errors" +msgstr "" From 68c0eb6215f748a8cd7087cf40d72a7c7acfb632 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 3 Jun 2025 13:43:46 +0000 Subject: [PATCH 27/58] [BOT] post-merge updates --- README.md | 29 +++++++++--------- fastapi_encrypted_errors/README.rst | 8 +++-- fastapi_encrypted_errors/__manifest__.py | 2 +- .../static/description/icon.png | Bin 0 -> 10254 bytes .../static/description/index.html | 28 ++++++++++------- setup/_metapackage/VERSION.txt | 2 +- setup/_metapackage/setup.py | 1 + 7 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 fastapi_encrypted_errors/static/description/icon.png diff --git a/README.md b/README.md index 8a995725f..b808c2b7c 100644 --- a/README.md +++ b/README.md @@ -22,29 +22,30 @@ Available addons addon | version | maintainers | summary --- | --- | --- | --- [base_rest](base_rest/) | 16.0.1.0.4 | | Develop your own high level REST APIs for Odoo thanks to this addon. -[base_rest_auth_api_key](base_rest_auth_api_key/) | 16.0.1.0.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Base Rest: Add support for the auth_api_key security policy into the openapi documentation +[base_rest_auth_api_key](base_rest_auth_api_key/) | 16.0.1.0.0 | lmignon | Base Rest: Add support for the auth_api_key security policy into the openapi documentation [base_rest_datamodel](base_rest_datamodel/) | 16.0.1.0.0 | | Datamodel binding for base_rest -[base_rest_demo](base_rest_demo/) | 16.0.2.0.4 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Demo addon for Base REST +[base_rest_demo](base_rest_demo/) | 16.0.2.0.4 | lmignon | Demo addon for Base REST [base_rest_pydantic](base_rest_pydantic/) | 16.0.2.0.1 | | Pydantic binding for base_rest -[datamodel](datamodel/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | This addon allows you to define simple data models supporting serialization/deserialization -[extendable](extendable/) | 16.0.1.0.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Extendable classes registry loader for Odoo -[extendable_fastapi](extendable_fastapi/) | 16.0.2.1.2 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Allows the use of extendable into fastapi apps -[fastapi](fastapi/) | 16.0.1.6.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Odoo FastAPI endpoint -[fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | JWT bearer token authentication for FastAPI. -[fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Test/demo module for fastapi_auth_jwt. -[graphql_base](graphql_base/) | 16.0.1.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Base GraphQL/GraphiQL controller -[graphql_demo](graphql_demo/) | 16.0.1.0.1 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | GraphQL Demo -[pydantic](pydantic/) | 16.0.1.0.0 | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Utility addon to ease mapping between Pydantic and Odoo models -[rest_log](rest_log/) | 16.0.1.0.3 | [![simahawk](https://github.com/simahawk.png?size=30px)](https://github.com/simahawk) | Track REST API calls into DB +[datamodel](datamodel/) | 16.0.1.0.2 | lmignon | This addon allows you to define simple data models supporting serialization/deserialization +[extendable](extendable/) | 16.0.1.0.2 | lmignon | Extendable classes registry loader for Odoo +[extendable_fastapi](extendable_fastapi/) | 16.0.2.1.2 | lmignon | Allows the use of extendable into fastapi apps +[fastapi](fastapi/) | 16.0.1.6.0 | lmignon | Odoo FastAPI endpoint +[fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | sbidoul | JWT bearer token authentication for FastAPI. +[fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | sbidoul | Test/demo module for fastapi_auth_jwt. +[fastapi_encrypted_errors](fastapi_encrypted_errors/) | 16.0.1.0.1 | paradoxxxzero | Adds encrypted error messages to FastAPI error responses. +[graphql_base](graphql_base/) | 16.0.1.0.1 | sbidoul | Base GraphQL/GraphiQL controller +[graphql_demo](graphql_demo/) | 16.0.1.0.1 | sbidoul | GraphQL Demo +[pydantic](pydantic/) | 16.0.1.0.0 | lmignon | Utility addon to ease mapping between Pydantic and Odoo models +[rest_log](rest_log/) | 16.0.1.0.3 | simahawk | Track REST API calls into DB Unported addons --------------- addon | version | maintainers | summary --- | --- | --- | --- -[base_rest_auth_jwt](base_rest_auth_jwt/) | 15.0.1.1.0 (unported) | [![lmignon](https://github.com/lmignon.png?size=30px)](https://github.com/lmignon) | Base Rest: Add support for the auth_jwt security policy into the openapi documentation +[base_rest_auth_jwt](base_rest_auth_jwt/) | 15.0.1.1.0 (unported) | lmignon | Base Rest: Add support for the auth_jwt security policy into the openapi documentation [base_rest_auth_user_service](base_rest_auth_user_service/) | 15.0.1.0.1 (unported) | | Login/logout from session using a REST call -[model_serializer](model_serializer/) | 15.0.1.2.0 (unported) | [![fdegrave](https://github.com/fdegrave.png?size=30px)](https://github.com/fdegrave) | Automatically translate Odoo models into Datamodels for (de)serialization +[model_serializer](model_serializer/) | 15.0.1.2.0 (unported) | fdegrave | Automatically translate Odoo models into Datamodels for (de)serialization [//]: # (end addons) diff --git a/fastapi_encrypted_errors/README.rst b/fastapi_encrypted_errors/README.rst index 7027f164e..2688aba39 100644 --- a/fastapi_encrypted_errors/README.rst +++ b/fastapi_encrypted_errors/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ======================== FastAPI Encrypted Errors ======================== @@ -7,13 +11,13 @@ FastAPI Encrypted Errors !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:296fbef824a5eb64e9bbaedc382ef15e41d146f0cc59d458c0d76de46a54358e + !! source digest: sha256:15dc8276c61dfb1e08999782de2a451f1d9531ada4979f1d4e02ae39e873406c !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github diff --git a/fastapi_encrypted_errors/__manifest__.py b/fastapi_encrypted_errors/__manifest__.py index 254901404..c0a5ca515 100644 --- a/fastapi_encrypted_errors/__manifest__.py +++ b/fastapi_encrypted_errors/__manifest__.py @@ -5,7 +5,7 @@ { "name": "FastAPI Encrypted Errors", "summary": "Adds encrypted error messages to FastAPI error responses.", - "version": "16.0.1.0.0", + "version": "16.0.1.0.1", "license": "AGPL-3", "author": "Akretion,Odoo Community Association (OCA)", "maintainers": ["paradoxxxzero"], diff --git a/fastapi_encrypted_errors/static/description/icon.png b/fastapi_encrypted_errors/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 zcmbt)WmufcvhH9Zc!C8B?l8#UE&&o;gF7=g3=D(IAOS+K1lK^25Zv7%L4sRw_uvvF z*qyAk?>c**=lnR&y+1yw{;I3Hy6Ua2{<d0kcR+VvBo; zA_X`>;1;xAPL9rQqFxd#f5{a^zW*uaW+r3+U{|fRunu`GZhy$X z8_|Zi{zd#vIokczl8Xh*4Wi@i0+C?Rg1AB5VOEg8B>buLFCi~r5DPd2ED7QP2>^LO zKpr7+?*I1bPaFSLLEa0l2$tj*;u8Qtc=&(RUc*VK@ zjIN{I--GfO@vl+&r^eqy_BZ3dndN_PDzMc*W^!?dIsWAWU@LBjBg6^f4F6*!-hUYh zY$Xb}gF8b0%S1Ac@c%Rs()UCiEu3v6SiFE>h_!{gBb-H2{e=wB5o!YkT0>#LKZFw$ z?CuD0Gvfsb(|XbVxx0AL0%`gG2X+6|f;jiTHU9shtjoW-{2!| zMN*WuOj6elhD4zqgjNpX>F#JP{)hAbenX<+FPr>7jXM&q{|x+pbj8cU<=>Ej zWE1_%qoFVzDAZB%g@v<+1ud%<#2E~ML11jOV5pUZoXktGmzB38%te^i-3o9i$lge>z>tBcK|P2K0H9w{l#|i%$~egM)Ys{q>p<9yaE*%v2cy1wXE{AXqG1_b znfyg@Fq*e@yC)^(@$R*j^E;skyEM6pmL$1ctg*mWiWM&q1{nj>E^)Odw$RPr zhjesSk}k}@-e_%uZTy0t_*TJD&6%*HV0KH>xE@oBex6CL@`Ty3nH_2OF#M?6j(j|9 znRKGSfp3Q2i+|>}w?>8g$>r`|OcvG5r;p)z8DO8+O>EvYQ=_~`p}9!ReUEjUnNL@6 z+C*aoo67(sd|7QgW54@V9Y8PnBW$Q+7ZsRFA}Vj*viA!yWUfb!s*yJi6JKsXZCH4j z*B%nJpad-DDvJ8d>xrxkkh6A}i7V3nULqHCiG~|)YY6{NE3M}c^s#PQhzhsJUf^QW zR+F;up-dN*!)M1ZYl@d0HoqfVD2PNiQcPdzq4NDKO!8mUl{!t*ntBg_+-+lRlI0~Lr>5v!PiQj|hD7B-YFIs~6hIY*R6USZA zlb}=UxqxpSzIsL3pPmiuixCN|3LFBd?0Ih8Y6GWQ;U>dkdXtQaQ&8H|TGAQbuHY=F z_R83&B{1_hP7L#$^eAe?GPB_83y#HZKTwD>e-@E2P>Gk$BBb9|Ivfmdp za~s>3=aj(;xmz8n)sI}uFO$|C>0CZbcTY$Bq6~L-Bc9=vl@X#0S~Q@j8iKzuPeQE_ zQSI)wNz~CvJ>!%QszoCfUm9}h^DL!WYAN|FtMO#kpDXq74sYC87(uvv*jiCjV?Ta& zgO1D0OP3TEN3YnBpD6GnmsEolzEbGM{&VlTz_)J(o{nl0+TmNt{xL%L6G&UR$^aYC zQOA#W7R%9JsC5oTZJE>_?!Ci}mNH{0ObyUd%Q!k%5J8Z`8sR!m`~|Taje`(bLD7=a z-{-=d7w;k@DIrgU{I@K}eN`>S**Lg<@ChAf$M(&kV9TLUixqFQ>YoYHrI!K#R6`S> z%?d5hQ@&;Gje<|uRQZb%Hhibocl9(buI?=0aZW{JYXx?ZS@Lr%G8L<d+riEi2~+{HfHK{K^VrGYNi{2-WJOiC>Pz?f*)cxKCl>1H1=$jb!^ zpmYw>eoiM0Hy7$xbbX_e5o*+{7T2&-t%-h4i7MMo;k|tSqQAeNkwHS9hWY#EV7r3| zTmOmN{;b9OUZpp`LP(I9Wo%R#$b6YdH7GD4*p6>a2N2A04pQ*n;INQMh%+mj;x7>S z_(H?uJ^n!r1)kJH1*s+%$al#?C^Cw{H@RA^QGB=Dubyc)XUaY>f`(VKTlIO-YNCp{1n zOl*>jT?Dtf5fD$DY-j&B*Xmn|2-u2OB zBL@-lFs5lhcQKXBR*cIXmi%~EJcc^5#Xpg!E^A6sXf1#$qJGRpmU~A zcdj-cvBfx(fIRAMU(1obztJR%I7v3R-%$#~r!0sS^I(iC*5i6296*88A7I=_JhU3p zya!aCti0R5*RFT%LW0R|;u&oJ6=P-c$le4J0bi}u!!@;xzao|l6fJ{;Mld9hGhrJg zr_B)=4yktp)yPB@tCC_L9h1>GzXD6DA!W7xt{1)8!07~gONkEWC8@y%lciB{9ojy) zWm$drJ_9uVJ>Q$-`@q%OM7_S>(K=__CGYB~@@mE^Z=eT|x0Rv?Z-N)LLWR zod*Zy3v)iMX@usPX-OKBDgC8yq?fMhqf8H)A&C)Hi29YFn!NVf5!J0-F{wC&L5-3`#id=4?=2>Zp6Pdu4N6#bG&atu7 z8IET&ciXy_Tp4YjMx3yIAbw#_e2#jgGJ~ogkv-|M7|%Gio%2@mnS89NKUOM#Bzg4_ z9e9oN;^m>G*#?)AawODi6YckRPmkSKD_4b4WFpj|@|eS!B0WN@?QscYzTH`~6e%iz z!z1>ps)CG37%(E=kZ_>re)@ODv^0^=rWU^*m;6M&gD10EYImO98JVabRe5{#wrogYUKPB@_(#e7Ej9_x;n1oHDj5GawU)A&1hWj|HzJB(q{vMTX>jOW;Jz zBsW&SqTaR7!NXXg_A}$XnFpg_n)Zi;{e9eb*k|b(y$a}12boJ7rqQXQpVhU8HxHTl zt8Ln!KLFyfq!%}hdMXle^qajw2g6S{z&7tQ6J(w9 z3+!HTO{_TqM{9o$RR~lKFf4b4(xLUP?QG;McNFQc_Yd_mig9Ejy9%q~Ye>rIn3};U z)w&1@QCK;cC(;x0G&YuSad+>{c@ZsFJcUdcs@PP-x{mrO)|6_#CjMlXsMJx;Cr?FF zVFrlt@$Z-Ll^*7d0#`5Uez@bb{Xn(BQLhScBhF!6+aIso0=l{PP7P(6-ru>nVy%AP z+|eZpY(ooMU7rtG$l#14v=Z?@ebOjm(A2)5k_${|wAA$oq+;42wiS78ezjgWWnTrF z`1!i2h{fM91aD8uxz?tZpE(PsL37e3$*I6%un5Bzzpn10p`j72R;3=Oaug_|Z(y)@ z9$SJN@-5d1tNIy0=7|d&_HAnDx!yDd-u#qmfuDh)0a_CVje{hvQz9rDFHJTpQ0Dg@ zGQ3t*gZlcFSXfx%OG@Cds&NDROxd^osY_)abmo^dKMUY!R~kGH%*;rutPF@Mx$zrv z6Q1soKnYYRW#;Bi-!H)>Br0<`y+Wy~p7_<>{ljuG`Dpje=v1x}-ND<)bWBr|<}v6B zkDTUZ^@VsH>CyR}ml4j2rB{}0q8eGwX>ExkI9yZN0)(P}$N(yi$AxmBY#Xj`(7zs{ zJbn2&jE`-*0lww_r;|fNaWm_xp;c9JHIv|RExZGKP%18qjgYa);`N-^VqXNVz{~)~ z?^&D;ouy!pKPy?%@xH`A zSR z7x%N3@o&{YEjfa|1;*eW_4TU{ zt;qCcY3Hj(<0DJuny*QL!y!StcG{>bhpUP%eVMq=1xcR>yZT8X9)1;rXOmQjPcANs zr>&Qb{rr66;s|4v3iGmQlMjr9j;G6pqNs%;TsyVNd3{i~hpDX8ugdcnd&UQJzj)rH zh>S6#n`cCJ9CwHv<2Ht$o`R5(h#r||VB?%J?s5W48;^o)b`Pi1^~}5{Y19lg{&W@LfHt*gc1`w$RfLrK{~H?A1$5 z;5v?AIhpN%gQsR6+Act9-3y z8>jCTMnWQq-^s3#Lb|WalgB$k3F>}lyCxs<2&A;LS0}s#<|hPx9kM#B+Lu2DiD_3P zelg;N!80(j@HNc2pXs}re%sHi+{aqBt~qUOy86?zN>7)yiCEJqy@2Gh#gzJE6j6Rx zBQK{77zW?gLWtQ20Dzntu16k9^N>DQ@Nmbx*mOg=F=k)8VJfM%y(Xu41;8YCz+@K| z9u7vhlT`BOnk_oMTeC;u@OhhoTeA`^34^iMihCLM_uVD>rI-9@4l7ocZl@DJ8FWZU zB0lRBIqkHj4#pE&mD(X!e!~;G$`7f47k* zOznM2@`&KM(|f5}sz)z%2}yJ5YmMj5Zwzr-W?v3R&@KuJ+l0zo==N@)nsbMHqHV}w z7#_ntMGCNM21RuH^SYG+RH0sHUsF2z7ams57@2xbPj0y5)8h+caqv@P^q!do+}>+X zzUBx|mikTawzXWYzJ4(AqAJpBF4ObmD_@gyg->oFGB6`k(8+?rFRV5P1yDkFM=8(c z%RI)iG(rKtq-^V%B_(R9;tk6WIzA?x@cESTXg zWYDBxkoNB5v6J8BP&n@HVtBNb@r+XYpjgub zR4oE*$ffXJuh2g8TCaLnpNoSxJ~Jx@ayx9z5Osa)=AI#bg^5eQb<6gpR%c+Qs#N*e z@XE4pAmjdI#0%pV7sIN>mNa^jTkd=<==2_#t-}9Ju&Z^|Lp$%B92@eN%=MRc)LK$% z@!XAg;dQ8bt=@ZNey7+a(dy^o;QKGP@Rb5NJYQRrGEC{J=FB(Irw-MAfoP(9RK;)&jlxSCT=W;ODCf($WqRFhqN#LR^qVhK zWhEp4`{Nnk;n0FHj}eNCZpRM`Y-@MIM&pvr7zQOZ3Ik5;CmZbR99b&22(!-07YNF) z$o0MKej-jnvQV39{TH4r2R5univa1{ASc|VOTi4c@`t2FId|xkh5typ-rdU;1j){adk@*+( zkHj{5B~eSy&HrPOOvl_FJ98)0V;^d`0-u0FTslgiLBQVGSTiSyu zgMGAu&R}SbNa-DgKJb?;fe3Qys$?=;5?V`eRiq*Kj$I`}Z*x4rC~eNM=DsOq(=nUW>(+7o@O8K-_U(X? zTyg032nXKax5W~SF5|eBj%r8Fa>i!ejC72*sd}zJ)t7Xy!gFvM`c4@*Iw>z$u)j_l zR-Uqxymg}>Ti>i%9j*4kwfC33i~kyIQ``n)r(L z!|H2*)Mwj4dk%e*L0tgFdW185>j4<7YwLXwcOsed`%6mS{+=&d@d!B}GkbDV*0 zNIWzW^|trz!&;qeI&mPiVDOUL70xpqVv0fpN9tjpu)@1LD9D<9}9{57j9!W$`zC6&i zl9lKkmPh`x)5+h>>JtiRNNBW5$_)%-)#+SVSGsjX2T=+SRX05>yJZd`1hyk<@{%1+ zDu^k>J$d*Qz6BZMwHx!@O**^Tx&fsHDw%$@J0nfj^je^Ihy*aIx{B(hkBvSvh46Z9 zRO)BjjXL_IHXKo~$4es=8Wxk;Y+&nVBCXA;=MVuLgVn8Mk(*y^+kP3f?Pr~4^A}hXj9UHS}qeI%XKD3KhHnkrNH0(Y20BWl&!Kfm`EVh2;i5C zpirU^K0nc2-I{cqvjZKVx z=&hH#-d=gDWjVE}cMNAPJf;#NYdQ=h`twjX6yquXuCNgGx1~uk{YHAmFpQF`ZLGC=~ukEyj?cFDI zH=@XvV#AY1EY4qb`y*;Ki>KuFB|2|toL7__Cr0S1Dl{s#y0=~7HSq~&7lpBc*VLua zvv3r&-LM*{hq%IYP7<@)dG-G$kMrZaqs(MYoZ zugEeJ@u(ip9rMoVtoFe;dF`^Br5x7v!rr5`hb5mJ#ocGqXHnm9m`yILjd0>UQSMv) z^v}l5^bM6RZ6M%{mkI) zHOoSp&dX)*xUt+kXscna#a`XxI;Ul2Sxa^i5sZc=(Q)oA^2-_;!pfYHAul+oA@Ilelm;rw@FYR+SIaWS?;_ zUdw<|qqaYq(nqu>rG48E9dYAoT6GH;QRuBYK1}W#C_Z_?7~k*pJ3?MzVt&rhZTsBy zw?nN$_Z>kimtwWcy`0?G#!)&7GjOcxCQps@p&ml8>~z(t=sjhR$6aFh!Vw5GA(lTh z5GM)jCwloa6a}7mdfqNYE7oi`Jv$m5>5qR%9eZ=)=a z+K4j5NpcDHHdepCS+P*{@o=yNp&TE(Sd4b0Notqso-Kt_mhDk1<-fa>T4KdY2N`U) zxu41vD%T&k$Gl?CW81%7r#-o1TZ0&PCcy}L4TPiV;sz`|S!&w8-s$rLdM zF&)>@`7=)65PWn#oi|8tXNb|((2ojf9d0fNZ^l7xY~dX~%*Xf-v2W-2n$i~s!4?H; z2qbQscFN21tqB{|x1+(^G~xQSrvX&Y;V-%?b1}zjBQX{GOFcVYTcwm>>}>6^HA=$x zn+z^Biv_5}0!#@7z1~YXJFCT2?D^jm+kH7jAqBo?M@ZdMl|2|66oLnSJXUOJtVLxe z0vH)N^t*qrjq=eFRMV>BFEfS)-2RzKlt973;d3D}4edwIE>kGc5-o=JV56ird)RlS z{Jg@0t-b#Ife80%!E~(7`qkZ8O~Q-8_{j7G&tqwX&&>^tm-#*{v7j-f1n0}mCR#7P z-4FkajD2$9?4Fc7-C_|0Z_G^bxIs%tWk|aFgSQ(qkM+5PRh=g&ZeAZg35$-kn~}_;~&fP-dCNCzg>{gyW!~LZpn?aZ~Va3~H0Ta)z z<4XPVk@;#%1S@fq<(2#8T04#8$mz>vM;(jek0>Qh!K%t5*4tU(fVYwD3Ri~=D!AmI zV$Dt#TEDX7{lpW%tF&DOlTO)vZodn_%wYu~)ZQ}Qo^cBbDHd{YajkzNxttQW>ST<^ z2~^xhB_y1sjIF5;xchvCn{QVugIE2eYZDZ!-Y-4lJdb34*k({@M zJ5!9Di^||~(IZ4iOoAbtggao+CaYvJynmB^;4r-tY2gS_*P!?U?hlEX;l+^*{%B2n z)|1j9wOHQQ^5Xha>{Cu8_w^8=#6;Dz7kU~RgTqn;ynDm6{xdlkf2vk0UK^oS3yVy4 zE+v&qnlYtPHBk#X&2}r7`@K`J@^e~Qm?iRJ*tbAaZDZTmB&mWMkZp7Kj7^kth#_uX z5z>gC(8Xz|Ie(+#&wiF3;Aey|Db(R*-U)!6;l_5@u?-$>j0SgEl5+c}Lfe-$p-dFH zB_$bC<)x6#A_2Uuo8=^l1@}vK!gvbF#b&MoH8ac3xMxUz$LFb8KU(x$YhtHanM_sw zYOFMBX2iNNSe&a}!;G9nv(tsW4@%3iQcqczOCF*JOBQ@4Orw=o?_vc(9$hfO`>U6& zyY_CUa9pASiJpmv`@oR!k;&$`h8!)$uS=}d-fPddfIdMDUW@%3y1LI(1Q=e$)sz(QC*E;Nfl99YTgk+|@jl`+iF?<_D?4YqV0Zl)lO8YWC@1ZWW^mi{5ePQN<~FQ2NMG$|K{py5akJa zkezmqhN)>MGMp$7=sOo2(7ppv``dCIwf&MaQQis7S596kkiw8Do(jO?EY4iJ4Hec6 z4Hymzu`w)cI9Pbq6GPtTP)x&Lmk;FT=ZCB4>(5}c0?;2l`p&?>&<;2(P8a3lOTNP# zdEzF5qDpkRR&PZC&cS{7xD@qV;(g5X%xI?m$9Q -FastAPI Encrypted Errors +README.rst -
    -

    FastAPI Encrypted Errors

    +
    + + +Odoo Community Association + +
    +

    FastAPI Encrypted Errors

    -

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This module adds a “ref” field in the error response of FastAPI. This field is an AES encrypted string that contains the error message / traceback. This encrypted string can be decrypted using the endpoint decrypt error wizard.

    @@ -387,7 +392,7 @@

    FastAPI Encrypted Errors

    -

    Usage

    +

    Usage

    First you have to enable the encryption for an endpoint by checking the Encrypt Errors checkbox in the endpoint configuration.

    To decrypt an error message, you can use the “Decrypt Error” wizard in the @@ -395,7 +400,7 @@

    Usage

    You can regenerate a new key by clicking on the “Regenerate Key” button next to the Errors Secret Key field.

    -

    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 @@ -403,15 +408,15 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Akretion
    -

    Contributors

    +

    Contributors

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -435,5 +440,6 @@

    Maintainers

    +
    diff --git a/setup/_metapackage/VERSION.txt b/setup/_metapackage/VERSION.txt index c641082ff..086eb2e47 100644 --- a/setup/_metapackage/VERSION.txt +++ b/setup/_metapackage/VERSION.txt @@ -1 +1 @@ -16.0.20231212.0 \ No newline at end of file +16.0.20250603.0 \ No newline at end of file diff --git a/setup/_metapackage/setup.py b/setup/_metapackage/setup.py index e1afa5ea8..415e9c7d8 100644 --- a/setup/_metapackage/setup.py +++ b/setup/_metapackage/setup.py @@ -19,6 +19,7 @@ 'odoo-addon-fastapi>=16.0dev,<16.1dev', 'odoo-addon-fastapi_auth_jwt>=16.0dev,<16.1dev', 'odoo-addon-fastapi_auth_jwt_demo>=16.0dev,<16.1dev', + 'odoo-addon-fastapi_encrypted_errors>=16.0dev,<16.1dev', 'odoo-addon-graphql_base>=16.0dev,<16.1dev', 'odoo-addon-graphql_demo>=16.0dev,<16.1dev', 'odoo-addon-pydantic>=16.0dev,<16.1dev', From ef98bb27a1e202ce97ccf9a33a6e045241702abe Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 3 Jun 2025 14:39:31 +0000 Subject: [PATCH 28/58] [UPD] Update auth_partner.pot --- auth_partner/i18n/auth_partner.pot | 554 +++++++++++++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 auth_partner/i18n/auth_partner.pot diff --git a/auth_partner/i18n/auth_partner.pot b/auth_partner/i18n/auth_partner.pot new file mode 100644 index 000000000..b9e8a3e77 --- /dev/null +++ b/auth_partner/i18n/auth_partner.pot @@ -0,0 +1,554 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_partner +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.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: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__14-days +msgid "14 Days" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__2-days +msgid "2-days" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__6-hours +msgid "6 Hours" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__7-days +msgid "7 Days" +msgstr "" + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_reset_password +msgid "" +"
    \n" +" Hi \n" +" Click on the following link to reset your password\n" +" Reset Password\n" +"
    \n" +" " +msgstr "" + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_validate_email +msgid "" +"
    \n" +" Hi \n" +" Welcome to the site, please click on the following link to verify your email\n" +" Validate Email\n" +"
    \n" +" " +msgstr "" + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_set_password +msgid "" +"
    \n" +" Hi \n" +" Welcome, your account have been created\n" +" Click on the following link to set your password\n" +" Set Password\n" +"
    \n" +" " +msgstr "" + +#. module: auth_partner +#: model:res.groups,name:auth_partner.group_auth_partner_api +msgid "API Partner Auth Access" +msgstr "" + +#. module: auth_partner +#: model:res.groups,name:auth_partner.group_auth_partner_manager +msgid "API Partner Auth Manager" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.view_partner_form +msgid "Account" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "" +"An email will be send with a token to each customer, you can specify the " +"date until the link is valid" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_auth_directory +msgid "Auth Directory" +msgstr "" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_reset_password +msgid "Auth Directory: Reset Password" +msgstr "" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_set_password +msgid "Auth Directory: Set Password" +msgstr "" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_validate_email +msgid "Auth Directory: Validate Email" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Auth Partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_count +#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_count +msgid "Auth Partner Count" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__auth_partner_ids +msgid "Auth Partners" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Cancel" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password_confirm +msgid "Confirm Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_res_partner +msgid "Contact" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__count_partner +msgid "Count Partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_uid +msgid "Created by" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_date +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_date +msgid "Created on" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_impersonation +msgid "Date Last Impersonation" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_request_reset_pwd +msgid "Date Last Request Reset Pwd" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd +msgid "Date Last Sucessfull Reset Pwd" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__date_validity +msgid "Date Validity" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_request_reset_pwd +msgid "Date of the last password reset request" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_impersonation +msgid "Date of the last sucessfull impersonation" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd +msgid "Date of the last sucessfull password reset" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__delay +msgid "Delay" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_directory_action +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__directory_id +#: model:ir.ui.menu,name:auth_partner.auth_directory_menu +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Directory" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_search +msgid "Directory Auth" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__display_name +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__display_name +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__display_name +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__display_name +msgid "Display Name" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "" +"Email address not validated. Validate your email address by clicking on the " +"link in the email sent to you or request a new password. " +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__encrypted_password +msgid "Encrypted Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__force_verified_email +msgid "Force Verified Email" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Group By" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__id +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__id +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__id +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__id +msgid "ID" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__force_verified_email +msgid "If checked, email must be verified to be able to log in" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +msgid "Impersonate" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_token_duration +msgid "Impersonating Token Duration" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_user_ids +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__impersonating_user_ids +msgid "Impersonating Users" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__set_password_token_duration +msgid "In minute, default 1440 minutes => 24h" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_token_duration +msgid "In seconds, default 60 seconds" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Invalid Login or Password" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#: code:addons/auth_partner/models/auth_directory.py:0 +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "Invalid Token" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "Invalid token" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Label" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory____last_update +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner____last_update +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password____last_update +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password____last_update +msgid "Last Modified on" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_date +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_date +msgid "Last Updated on" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__login +msgid "Login" +msgstr "" + +#. module: auth_partner +#: model:ir.model.constraint,message:auth_partner.constraint_auth_partner_directory_login_uniq +msgid "Login must be uniq per directory !" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__template_id +msgid "Mail Template" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__reset_password_template_id +msgid "Mail Template Forget Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_template_id +msgid "Mail Template New Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__validate_email_template_id +msgid "Mail Template Validate Email" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__mail_verified +msgid "Mail Verified" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__manually +msgid "Manually" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__name +msgid "Name" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__nbr_pending_reset_sent +msgid "Nbr Pending Reset Sent" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "No active_id in context" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "No email template defined for %(template)s in %(directory)s" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__nbr_pending_reset_sent +msgid "" +"Number of pending reset sent from your customer.This field is usefull when " +"after a migration from an other system you ask all you customer to reset " +"their password and you senddifferent mail depending on the number of " +"reminder" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_action +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__partner_id +#: model:ir.ui.menu,name:auth_partner.auth_partner_menu +msgid "Partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_ids +#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_ids +msgid "Partner Auth" +msgstr "" + +#. module: auth_partner +#: model:ir.ui.menu,name:auth_partner.auth +msgid "Partner Authentication" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__password +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password +msgid "Password" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "Password and Confirm Password must be the same" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +msgid "Regenerate secret key" +msgstr "" + +#. module: auth_partner +#: model:mail.template,subject:auth_partner.email_reset_password +msgid "Reset Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key +msgid "Secret Key" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_default +msgid "Secret Key Env Default" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_is_editable +msgid "Secret Key Env Is Editable" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +msgid "Send Invite" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Send Reset Password" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_action_reset_password +msgid "Send Reset Password Instruction" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_force_set_password_action +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +msgid "Set Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_token_duration +msgid "Set Password Token Duration" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__user_can_impersonate +msgid "Technical field to check if the user can impersonate" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_user_ids +#: model:ir.model.fields,help:auth_partner.field_auth_partner__impersonating_user_ids +msgid "These odoo users can impersonate any partner of this directory" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__mail_verified +msgid "" +"This field is set to True when the user has clicked on the link sent by " +"email" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "This wizard can only be used on auth.partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__user_can_impersonate +msgid "User Can Impersonate" +msgstr "" + +#. module: auth_partner +#: model:mail.template,subject:auth_partner.email_set_password +#: model:mail.template,subject:auth_partner.email_validate_email +msgid "Welcome" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_wizard_auth_partner_force_set_password +#: model:ir.model,name:auth_partner.model_wizard_auth_partner_reset_password +msgid "Wizard Partner Auth Reset Password" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "You are not allowed to impersonate this user" +msgstr "" From c049ffaaadcbdd1dae95ac21b5d280659756bfb5 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 3 Jun 2025 14:39:34 +0000 Subject: [PATCH 29/58] [UPD] Update fastapi.pot --- fastapi/i18n/fastapi.pot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/i18n/fastapi.pot b/fastapi/i18n/fastapi.pot index 834c779cf..ca38189a1 100644 --- a/fastapi/i18n/fastapi.pot +++ b/fastapi/i18n/fastapi.pot @@ -47,7 +47,7 @@ msgstr "" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__demo_auth_method -msgid "Authenciation method" +msgid "Authentication method" msgstr "" #. module: fastapi From 3338168a047b1adb6321ff14a36edf688c49f38c Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 3 Jun 2025 14:39:35 +0000 Subject: [PATCH 30/58] [UPD] Update fastapi_auth_partner.pot --- .../i18n/fastapi_auth_partner.pot | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 fastapi_auth_partner/i18n/fastapi_auth_partner.pot diff --git a/fastapi_auth_partner/i18n/fastapi_auth_partner.pot b/fastapi_auth_partner/i18n/fastapi_auth_partner.pot new file mode 100644 index 000000000..fd3569e94 --- /dev/null +++ b/fastapi_auth_partner/i18n/fastapi_auth_partner.pot @@ -0,0 +1,263 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fastapi_auth_partner +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.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: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__app +msgid "App" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_auth_directory +msgid "Auth Directory" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__auth_partner_id +msgid "Auth Partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.ui.menu,name:fastapi_auth_partner.fastapi_auth +msgid "Authentication" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__demo_auth_method +msgid "Authentication method" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Cancel" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__cookie_duration +msgid "Cookie Duration" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__cookie_secret_key +msgid "Cookie Secret Key" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__create_uid +msgid "Created by" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__create_date +msgid "Created on" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields.selection,name:fastapi_auth_partner.selection__fastapi_endpoint__app__demo +msgid "Demo Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_auth_service__directory_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__directory_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__auth_directory_id +#: model:ir.ui.menu,name:fastapi_auth_partner.auth_directory_menu +msgid "Directory" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__display_name +msgid "Display Name" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_auth_service__endpoint_id +msgid "Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_fastapi_endpoint +msgid "FastAPI Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__fastapi_endpoint_ids +msgid "FastAPI Endpoints" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_fastapi_auth_service +msgid "Fastapi Auth Service" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__fastapi_endpoint_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_reset_password__fastapi_endpoint_id +msgid "Fastapi Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__id +msgid "ID" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.actions.act_window,name:fastapi_auth_partner.auth_partner_action_impersonate +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Impersonate" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Impersonation successful" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_auth_directory__cookie_duration +msgid "In minute, default 525600 minutes => 1 year" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__is_auth_partner +msgid "Is Auth Partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Label" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.auth_partner_view_form +msgid "Local Impersonate" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/routers/auth.py:0 +#, python-format +msgid "No cookie secret key defined" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Only admin can impersonate locally" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.ui.menu,name:fastapi_auth_partner.auth_partner_menu +msgid "Partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields.selection,name:fastapi_auth_partner.selection__fastapi_endpoint__demo_auth_method__auth_partner +msgid "Partner Auth" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Please choose an endpoint:" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Please install base_future_response for local impersonate to work" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__public_api_url +msgid "Public Api Url" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__public_url +msgid "Public Url" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.auth_directory_view_form +msgid "Regenerate cookie secret key" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__sliding_session +msgid "Sliding Session" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__is_auth_partner +msgid "Technical field to know if the auth method is partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__public_api_url +msgid "" +"The public URL of the API.\n" +"This URL is used in impersonation to set the cookie on the right API domain if you use a reverse proxy to serve the API.\n" +"Defaults to the public_url if not set or the odoo url if not set either." +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__public_url +msgid "" +"The public URL of the site.\n" +"This URL is used for the impersonation final redirect. And can also be used in the mail template to construct links.\n" +"Default to the public_api_url if not set or the odoo url if not set either." +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_auth_directory__cookie_secret_key +msgid "The secret key used to sign the cookie" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_wizard_auth_partner_impersonate +msgid "Wizard Partner Auth Impersonate" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_wizard_auth_partner_reset_password +msgid "Wizard Partner Auth Reset Password" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "" +"You are now impersonating %s\n" +"%%s" +msgstr "" From 9db366183502f31990cafba05c767559dc590682 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 3 Jun 2025 14:42:54 +0000 Subject: [PATCH 31/58] [BOT] post-merge updates --- README.md | 2 ++ auth_partner/README.rst | 8 +++-- auth_partner/static/description/icon.png | Bin 0 -> 10254 bytes auth_partner/static/description/index.html | 28 +++++++++++------- fastapi_auth_partner/README.rst | 8 +++-- .../static/description/icon.png | Bin 0 -> 10254 bytes .../static/description/index.html | 28 +++++++++++------- setup/_metapackage/VERSION.txt | 2 +- setup/_metapackage/setup.py | 2 ++ 9 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 auth_partner/static/description/icon.png create mode 100644 fastapi_auth_partner/static/description/icon.png diff --git a/README.md b/README.md index b808c2b7c..0c6dca0ce 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Available addons ---------------- addon | version | maintainers | summary --- | --- | --- | --- +[auth_partner](auth_partner/) | 16.0.1.0.0 | | Implements the base features for a authenticable partner [base_rest](base_rest/) | 16.0.1.0.4 | | Develop your own high level REST APIs for Odoo thanks to this addon. [base_rest_auth_api_key](base_rest_auth_api_key/) | 16.0.1.0.0 | lmignon | Base Rest: Add support for the auth_api_key security policy into the openapi documentation [base_rest_datamodel](base_rest_datamodel/) | 16.0.1.0.0 | | Datamodel binding for base_rest @@ -32,6 +33,7 @@ addon | version | maintainers | summary [fastapi](fastapi/) | 16.0.1.6.0 | lmignon | Odoo FastAPI endpoint [fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | sbidoul | JWT bearer token authentication for FastAPI. [fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | sbidoul | Test/demo module for fastapi_auth_jwt. +[fastapi_auth_partner](fastapi_auth_partner/) | 16.0.1.0.0 | | This provides an implementation of auth_partner for FastAPI [fastapi_encrypted_errors](fastapi_encrypted_errors/) | 16.0.1.0.1 | paradoxxxzero | Adds encrypted error messages to FastAPI error responses. [graphql_base](graphql_base/) | 16.0.1.0.1 | sbidoul | Base GraphQL/GraphiQL controller [graphql_demo](graphql_demo/) | 16.0.1.0.1 | sbidoul | GraphQL Demo diff --git a/auth_partner/README.rst b/auth_partner/README.rst index ca979ef04..6f646f298 100644 --- a/auth_partner/README.rst +++ b/auth_partner/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ============ Partner Auth ============ @@ -7,13 +11,13 @@ Partner Auth !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:c9e735f01c49bc7974e3b9354b6157e19c7486a71626ad8eef81104b628d476b + !! source digest: sha256:33a8bc75dc8127331753aa9a54fe3a5b56f7d51a23cc7e9eb0000cc55f78c689 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github diff --git a/auth_partner/static/description/icon.png b/auth_partner/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 zcmbt)WmufcvhH9Zc!C8B?l8#UE&&o;gF7=g3=D(IAOS+K1lK^25Zv7%L4sRw_uvvF z*qyAk?>c**=lnR&y+1yw{;I3Hy6Ua2{<d0kcR+VvBo; zA_X`>;1;xAPL9rQqFxd#f5{a^zW*uaW+r3+U{|fRunu`GZhy$X z8_|Zi{zd#vIokczl8Xh*4Wi@i0+C?Rg1AB5VOEg8B>buLFCi~r5DPd2ED7QP2>^LO zKpr7+?*I1bPaFSLLEa0l2$tj*;u8Qtc=&(RUc*VK@ zjIN{I--GfO@vl+&r^eqy_BZ3dndN_PDzMc*W^!?dIsWAWU@LBjBg6^f4F6*!-hUYh zY$Xb}gF8b0%S1Ac@c%Rs()UCiEu3v6SiFE>h_!{gBb-H2{e=wB5o!YkT0>#LKZFw$ z?CuD0Gvfsb(|XbVxx0AL0%`gG2X+6|f;jiTHU9shtjoW-{2!| zMN*WuOj6elhD4zqgjNpX>F#JP{)hAbenX<+FPr>7jXM&q{|x+pbj8cU<=>Ej zWE1_%qoFVzDAZB%g@v<+1ud%<#2E~ML11jOV5pUZoXktGmzB38%te^i-3o9i$lge>z>tBcK|P2K0H9w{l#|i%$~egM)Ys{q>p<9yaE*%v2cy1wXE{AXqG1_b znfyg@Fq*e@yC)^(@$R*j^E;skyEM6pmL$1ctg*mWiWM&q1{nj>E^)Odw$RPr zhjesSk}k}@-e_%uZTy0t_*TJD&6%*HV0KH>xE@oBex6CL@`Ty3nH_2OF#M?6j(j|9 znRKGSfp3Q2i+|>}w?>8g$>r`|OcvG5r;p)z8DO8+O>EvYQ=_~`p}9!ReUEjUnNL@6 z+C*aoo67(sd|7QgW54@V9Y8PnBW$Q+7ZsRFA}Vj*viA!yWUfb!s*yJi6JKsXZCH4j z*B%nJpad-DDvJ8d>xrxkkh6A}i7V3nULqHCiG~|)YY6{NE3M}c^s#PQhzhsJUf^QW zR+F;up-dN*!)M1ZYl@d0HoqfVD2PNiQcPdzq4NDKO!8mUl{!t*ntBg_+-+lRlI0~Lr>5v!PiQj|hD7B-YFIs~6hIY*R6USZA zlb}=UxqxpSzIsL3pPmiuixCN|3LFBd?0Ih8Y6GWQ;U>dkdXtQaQ&8H|TGAQbuHY=F z_R83&B{1_hP7L#$^eAe?GPB_83y#HZKTwD>e-@E2P>Gk$BBb9|Ivfmdp za~s>3=aj(;xmz8n)sI}uFO$|C>0CZbcTY$Bq6~L-Bc9=vl@X#0S~Q@j8iKzuPeQE_ zQSI)wNz~CvJ>!%QszoCfUm9}h^DL!WYAN|FtMO#kpDXq74sYC87(uvv*jiCjV?Ta& zgO1D0OP3TEN3YnBpD6GnmsEolzEbGM{&VlTz_)J(o{nl0+TmNt{xL%L6G&UR$^aYC zQOA#W7R%9JsC5oTZJE>_?!Ci}mNH{0ObyUd%Q!k%5J8Z`8sR!m`~|Taje`(bLD7=a z-{-=d7w;k@DIrgU{I@K}eN`>S**Lg<@ChAf$M(&kV9TLUixqFQ>YoYHrI!K#R6`S> z%?d5hQ@&;Gje<|uRQZb%Hhibocl9(buI?=0aZW{JYXx?ZS@Lr%G8L<d+riEi2~+{HfHK{K^VrGYNi{2-WJOiC>Pz?f*)cxKCl>1H1=$jb!^ zpmYw>eoiM0Hy7$xbbX_e5o*+{7T2&-t%-h4i7MMo;k|tSqQAeNkwHS9hWY#EV7r3| zTmOmN{;b9OUZpp`LP(I9Wo%R#$b6YdH7GD4*p6>a2N2A04pQ*n;INQMh%+mj;x7>S z_(H?uJ^n!r1)kJH1*s+%$al#?C^Cw{H@RA^QGB=Dubyc)XUaY>f`(VKTlIO-YNCp{1n zOl*>jT?Dtf5fD$DY-j&B*Xmn|2-u2OB zBL@-lFs5lhcQKXBR*cIXmi%~EJcc^5#Xpg!E^A6sXf1#$qJGRpmU~A zcdj-cvBfx(fIRAMU(1obztJR%I7v3R-%$#~r!0sS^I(iC*5i6296*88A7I=_JhU3p zya!aCti0R5*RFT%LW0R|;u&oJ6=P-c$le4J0bi}u!!@;xzao|l6fJ{;Mld9hGhrJg zr_B)=4yktp)yPB@tCC_L9h1>GzXD6DA!W7xt{1)8!07~gONkEWC8@y%lciB{9ojy) zWm$drJ_9uVJ>Q$-`@q%OM7_S>(K=__CGYB~@@mE^Z=eT|x0Rv?Z-N)LLWR zod*Zy3v)iMX@usPX-OKBDgC8yq?fMhqf8H)A&C)Hi29YFn!NVf5!J0-F{wC&L5-3`#id=4?=2>Zp6Pdu4N6#bG&atu7 z8IET&ciXy_Tp4YjMx3yIAbw#_e2#jgGJ~ogkv-|M7|%Gio%2@mnS89NKUOM#Bzg4_ z9e9oN;^m>G*#?)AawODi6YckRPmkSKD_4b4WFpj|@|eS!B0WN@?QscYzTH`~6e%iz z!z1>ps)CG37%(E=kZ_>re)@ODv^0^=rWU^*m;6M&gD10EYImO98JVabRe5{#wrogYUKPB@_(#e7Ej9_x;n1oHDj5GawU)A&1hWj|HzJB(q{vMTX>jOW;Jz zBsW&SqTaR7!NXXg_A}$XnFpg_n)Zi;{e9eb*k|b(y$a}12boJ7rqQXQpVhU8HxHTl zt8Ln!KLFyfq!%}hdMXle^qajw2g6S{z&7tQ6J(w9 z3+!HTO{_TqM{9o$RR~lKFf4b4(xLUP?QG;McNFQc_Yd_mig9Ejy9%q~Ye>rIn3};U z)w&1@QCK;cC(;x0G&YuSad+>{c@ZsFJcUdcs@PP-x{mrO)|6_#CjMlXsMJx;Cr?FF zVFrlt@$Z-Ll^*7d0#`5Uez@bb{Xn(BQLhScBhF!6+aIso0=l{PP7P(6-ru>nVy%AP z+|eZpY(ooMU7rtG$l#14v=Z?@ebOjm(A2)5k_${|wAA$oq+;42wiS78ezjgWWnTrF z`1!i2h{fM91aD8uxz?tZpE(PsL37e3$*I6%un5Bzzpn10p`j72R;3=Oaug_|Z(y)@ z9$SJN@-5d1tNIy0=7|d&_HAnDx!yDd-u#qmfuDh)0a_CVje{hvQz9rDFHJTpQ0Dg@ zGQ3t*gZlcFSXfx%OG@Cds&NDROxd^osY_)abmo^dKMUY!R~kGH%*;rutPF@Mx$zrv z6Q1soKnYYRW#;Bi-!H)>Br0<`y+Wy~p7_<>{ljuG`Dpje=v1x}-ND<)bWBr|<}v6B zkDTUZ^@VsH>CyR}ml4j2rB{}0q8eGwX>ExkI9yZN0)(P}$N(yi$AxmBY#Xj`(7zs{ zJbn2&jE`-*0lww_r;|fNaWm_xp;c9JHIv|RExZGKP%18qjgYa);`N-^VqXNVz{~)~ z?^&D;ouy!pKPy?%@xH`A zSR z7x%N3@o&{YEjfa|1;*eW_4TU{ zt;qCcY3Hj(<0DJuny*QL!y!StcG{>bhpUP%eVMq=1xcR>yZT8X9)1;rXOmQjPcANs zr>&Qb{rr66;s|4v3iGmQlMjr9j;G6pqNs%;TsyVNd3{i~hpDX8ugdcnd&UQJzj)rH zh>S6#n`cCJ9CwHv<2Ht$o`R5(h#r||VB?%J?s5W48;^o)b`Pi1^~}5{Y19lg{&W@LfHt*gc1`w$RfLrK{~H?A1$5 z;5v?AIhpN%gQsR6+Act9-3y z8>jCTMnWQq-^s3#Lb|WalgB$k3F>}lyCxs<2&A;LS0}s#<|hPx9kM#B+Lu2DiD_3P zelg;N!80(j@HNc2pXs}re%sHi+{aqBt~qUOy86?zN>7)yiCEJqy@2Gh#gzJE6j6Rx zBQK{77zW?gLWtQ20Dzntu16k9^N>DQ@Nmbx*mOg=F=k)8VJfM%y(Xu41;8YCz+@K| z9u7vhlT`BOnk_oMTeC;u@OhhoTeA`^34^iMihCLM_uVD>rI-9@4l7ocZl@DJ8FWZU zB0lRBIqkHj4#pE&mD(X!e!~;G$`7f47k* zOznM2@`&KM(|f5}sz)z%2}yJ5YmMj5Zwzr-W?v3R&@KuJ+l0zo==N@)nsbMHqHV}w z7#_ntMGCNM21RuH^SYG+RH0sHUsF2z7ams57@2xbPj0y5)8h+caqv@P^q!do+}>+X zzUBx|mikTawzXWYzJ4(AqAJpBF4ObmD_@gyg->oFGB6`k(8+?rFRV5P1yDkFM=8(c z%RI)iG(rKtq-^V%B_(R9;tk6WIzA?x@cESTXg zWYDBxkoNB5v6J8BP&n@HVtBNb@r+XYpjgub zR4oE*$ffXJuh2g8TCaLnpNoSxJ~Jx@ayx9z5Osa)=AI#bg^5eQb<6gpR%c+Qs#N*e z@XE4pAmjdI#0%pV7sIN>mNa^jTkd=<==2_#t-}9Ju&Z^|Lp$%B92@eN%=MRc)LK$% z@!XAg;dQ8bt=@ZNey7+a(dy^o;QKGP@Rb5NJYQRrGEC{J=FB(Irw-MAfoP(9RK;)&jlxSCT=W;ODCf($WqRFhqN#LR^qVhK zWhEp4`{Nnk;n0FHj}eNCZpRM`Y-@MIM&pvr7zQOZ3Ik5;CmZbR99b&22(!-07YNF) z$o0MKej-jnvQV39{TH4r2R5univa1{ASc|VOTi4c@`t2FId|xkh5typ-rdU;1j){adk@*+( zkHj{5B~eSy&HrPOOvl_FJ98)0V;^d`0-u0FTslgiLBQVGSTiSyu zgMGAu&R}SbNa-DgKJb?;fe3Qys$?=;5?V`eRiq*Kj$I`}Z*x4rC~eNM=DsOq(=nUW>(+7o@O8K-_U(X? zTyg032nXKax5W~SF5|eBj%r8Fa>i!ejC72*sd}zJ)t7Xy!gFvM`c4@*Iw>z$u)j_l zR-Uqxymg}>Ti>i%9j*4kwfC33i~kyIQ``n)r(L z!|H2*)Mwj4dk%e*L0tgFdW185>j4<7YwLXwcOsed`%6mS{+=&d@d!B}GkbDV*0 zNIWzW^|trz!&;qeI&mPiVDOUL70xpqVv0fpN9tjpu)@1LD9D<9}9{57j9!W$`zC6&i zl9lKkmPh`x)5+h>>JtiRNNBW5$_)%-)#+SVSGsjX2T=+SRX05>yJZd`1hyk<@{%1+ zDu^k>J$d*Qz6BZMwHx!@O**^Tx&fsHDw%$@J0nfj^je^Ihy*aIx{B(hkBvSvh46Z9 zRO)BjjXL_IHXKo~$4es=8Wxk;Y+&nVBCXA;=MVuLgVn8Mk(*y^+kP3f?Pr~4^A}hXj9UHS}qeI%XKD3KhHnkrNH0(Y20BWl&!Kfm`EVh2;i5C zpirU^K0nc2-I{cqvjZKVx z=&hH#-d=gDWjVE}cMNAPJf;#NYdQ=h`twjX6yquXuCNgGx1~uk{YHAmFpQF`ZLGC=~ukEyj?cFDI zH=@XvV#AY1EY4qb`y*;Ki>KuFB|2|toL7__Cr0S1Dl{s#y0=~7HSq~&7lpBc*VLua zvv3r&-LM*{hq%IYP7<@)dG-G$kMrZaqs(MYoZ zugEeJ@u(ip9rMoVtoFe;dF`^Br5x7v!rr5`hb5mJ#ocGqXHnm9m`yILjd0>UQSMv) z^v}l5^bM6RZ6M%{mkI) zHOoSp&dX)*xUt+kXscna#a`XxI;Ul2Sxa^i5sZc=(Q)oA^2-_;!pfYHAul+oA@Ilelm;rw@FYR+SIaWS?;_ zUdw<|qqaYq(nqu>rG48E9dYAoT6GH;QRuBYK1}W#C_Z_?7~k*pJ3?MzVt&rhZTsBy zw?nN$_Z>kimtwWcy`0?G#!)&7GjOcxCQps@p&ml8>~z(t=sjhR$6aFh!Vw5GA(lTh z5GM)jCwloa6a}7mdfqNYE7oi`Jv$m5>5qR%9eZ=)=a z+K4j5NpcDHHdepCS+P*{@o=yNp&TE(Sd4b0Notqso-Kt_mhDk1<-fa>T4KdY2N`U) zxu41vD%T&k$Gl?CW81%7r#-o1TZ0&PCcy}L4TPiV;sz`|S!&w8-s$rLdM zF&)>@`7=)65PWn#oi|8tXNb|((2ojf9d0fNZ^l7xY~dX~%*Xf-v2W-2n$i~s!4?H; z2qbQscFN21tqB{|x1+(^G~xQSrvX&Y;V-%?b1}zjBQX{GOFcVYTcwm>>}>6^HA=$x zn+z^Biv_5}0!#@7z1~YXJFCT2?D^jm+kH7jAqBo?M@ZdMl|2|66oLnSJXUOJtVLxe z0vH)N^t*qrjq=eFRMV>BFEfS)-2RzKlt973;d3D}4edwIE>kGc5-o=JV56ird)RlS z{Jg@0t-b#Ife80%!E~(7`qkZ8O~Q-8_{j7G&tqwX&&>^tm-#*{v7j-f1n0}mCR#7P z-4FkajD2$9?4Fc7-C_|0Z_G^bxIs%tWk|aFgSQ(qkM+5PRh=g&ZeAZg35$-kn~}_;~&fP-dCNCzg>{gyW!~LZpn?aZ~Va3~H0Ta)z z<4XPVk@;#%1S@fq<(2#8T04#8$mz>vM;(jek0>Qh!K%t5*4tU(fVYwD3Ri~=D!AmI zV$Dt#TEDX7{lpW%tF&DOlTO)vZodn_%wYu~)ZQ}Qo^cBbDHd{YajkzNxttQW>ST<^ z2~^xhB_y1sjIF5;xchvCn{QVugIE2eYZDZ!-Y-4lJdb34*k({@M zJ5!9Di^||~(IZ4iOoAbtggao+CaYvJynmB^;4r-tY2gS_*P!?U?hlEX;l+^*{%B2n z)|1j9wOHQQ^5Xha>{Cu8_w^8=#6;Dz7kU~RgTqn;ynDm6{xdlkf2vk0UK^oS3yVy4 zE+v&qnlYtPHBk#X&2}r7`@K`J@^e~Qm?iRJ*tbAaZDZTmB&mWMkZp7Kj7^kth#_uX z5z>gC(8Xz|Ie(+#&wiF3;Aey|Db(R*-U)!6;l_5@u?-$>j0SgEl5+c}Lfe-$p-dFH zB_$bC<)x6#A_2Uuo8=^l1@}vK!gvbF#b&MoH8ac3xMxUz$LFb8KU(x$YhtHanM_sw zYOFMBX2iNNSe&a}!;G9nv(tsW4@%3iQcqczOCF*JOBQ@4Orw=o?_vc(9$hfO`>U6& zyY_CUa9pASiJpmv`@oR!k;&$`h8!)$uS=}d-fPddfIdMDUW@%3y1LI(1Q=e$)sz(QC*E;Nfl99YTgk+|@jl`+iF?<_D?4YqV0Zl)lO8YWC@1ZWW^mi{5ePQN<~FQ2NMG$|K{py5akJa zkezmqhN)>MGMp$7=sOo2(7ppv``dCIwf&MaQQis7S596kkiw8Do(jO?EY4iJ4Hec6 z4Hymzu`w)cI9Pbq6GPtTP)x&Lmk;FT=ZCB4>(5}c0?;2l`p&?>&<;2(P8a3lOTNP# zdEzF5qDpkRR&PZC&cS{7xD@qV;(g5X%xI?m$9Q -Partner Auth +README.rst -
    -

    Partner Auth

    +
    + + +Odoo Community Association + +
    +

    Partner Auth

    -

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This module adds to the partners the ability to authenticate through directories.

    This module does not implement any routing, it only provides the basic mechanisms in a directory for:

    @@ -397,14 +402,14 @@

    Partner Auth

    -

    Usage

    +

    Usage

    This module isn’t meant to be used standalone but you can still see the directories and authenticable partners in:

    Settings > Technical > Partner Authentication > Partner

    and

    Settings > Technical > Partner Authentication > Directory

    -

    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 @@ -412,15 +417,15 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Akretion
    -

    Contributors

    +

    Contributors

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -443,5 +448,6 @@

    Maintainers

    +
    diff --git a/fastapi_auth_partner/README.rst b/fastapi_auth_partner/README.rst index 69e036674..fc8471c13 100644 --- a/fastapi_auth_partner/README.rst +++ b/fastapi_auth_partner/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ==================== Fastapi Auth Partner ==================== @@ -7,13 +11,13 @@ Fastapi Auth Partner !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:57e60d4a203cd5e613e07fe0c1f6a207bd6b77f537ed5cbbbe72a1cc2184f0de + !! source digest: sha256:2ebd9377ca7b035ab9fb0383513aacb5ca8645f69d5d85c171883b40b439017e !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github diff --git a/fastapi_auth_partner/static/description/icon.png b/fastapi_auth_partner/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 zcmbt)WmufcvhH9Zc!C8B?l8#UE&&o;gF7=g3=D(IAOS+K1lK^25Zv7%L4sRw_uvvF z*qyAk?>c**=lnR&y+1yw{;I3Hy6Ua2{<d0kcR+VvBo; zA_X`>;1;xAPL9rQqFxd#f5{a^zW*uaW+r3+U{|fRunu`GZhy$X z8_|Zi{zd#vIokczl8Xh*4Wi@i0+C?Rg1AB5VOEg8B>buLFCi~r5DPd2ED7QP2>^LO zKpr7+?*I1bPaFSLLEa0l2$tj*;u8Qtc=&(RUc*VK@ zjIN{I--GfO@vl+&r^eqy_BZ3dndN_PDzMc*W^!?dIsWAWU@LBjBg6^f4F6*!-hUYh zY$Xb}gF8b0%S1Ac@c%Rs()UCiEu3v6SiFE>h_!{gBb-H2{e=wB5o!YkT0>#LKZFw$ z?CuD0Gvfsb(|XbVxx0AL0%`gG2X+6|f;jiTHU9shtjoW-{2!| zMN*WuOj6elhD4zqgjNpX>F#JP{)hAbenX<+FPr>7jXM&q{|x+pbj8cU<=>Ej zWE1_%qoFVzDAZB%g@v<+1ud%<#2E~ML11jOV5pUZoXktGmzB38%te^i-3o9i$lge>z>tBcK|P2K0H9w{l#|i%$~egM)Ys{q>p<9yaE*%v2cy1wXE{AXqG1_b znfyg@Fq*e@yC)^(@$R*j^E;skyEM6pmL$1ctg*mWiWM&q1{nj>E^)Odw$RPr zhjesSk}k}@-e_%uZTy0t_*TJD&6%*HV0KH>xE@oBex6CL@`Ty3nH_2OF#M?6j(j|9 znRKGSfp3Q2i+|>}w?>8g$>r`|OcvG5r;p)z8DO8+O>EvYQ=_~`p}9!ReUEjUnNL@6 z+C*aoo67(sd|7QgW54@V9Y8PnBW$Q+7ZsRFA}Vj*viA!yWUfb!s*yJi6JKsXZCH4j z*B%nJpad-DDvJ8d>xrxkkh6A}i7V3nULqHCiG~|)YY6{NE3M}c^s#PQhzhsJUf^QW zR+F;up-dN*!)M1ZYl@d0HoqfVD2PNiQcPdzq4NDKO!8mUl{!t*ntBg_+-+lRlI0~Lr>5v!PiQj|hD7B-YFIs~6hIY*R6USZA zlb}=UxqxpSzIsL3pPmiuixCN|3LFBd?0Ih8Y6GWQ;U>dkdXtQaQ&8H|TGAQbuHY=F z_R83&B{1_hP7L#$^eAe?GPB_83y#HZKTwD>e-@E2P>Gk$BBb9|Ivfmdp za~s>3=aj(;xmz8n)sI}uFO$|C>0CZbcTY$Bq6~L-Bc9=vl@X#0S~Q@j8iKzuPeQE_ zQSI)wNz~CvJ>!%QszoCfUm9}h^DL!WYAN|FtMO#kpDXq74sYC87(uvv*jiCjV?Ta& zgO1D0OP3TEN3YnBpD6GnmsEolzEbGM{&VlTz_)J(o{nl0+TmNt{xL%L6G&UR$^aYC zQOA#W7R%9JsC5oTZJE>_?!Ci}mNH{0ObyUd%Q!k%5J8Z`8sR!m`~|Taje`(bLD7=a z-{-=d7w;k@DIrgU{I@K}eN`>S**Lg<@ChAf$M(&kV9TLUixqFQ>YoYHrI!K#R6`S> z%?d5hQ@&;Gje<|uRQZb%Hhibocl9(buI?=0aZW{JYXx?ZS@Lr%G8L<d+riEi2~+{HfHK{K^VrGYNi{2-WJOiC>Pz?f*)cxKCl>1H1=$jb!^ zpmYw>eoiM0Hy7$xbbX_e5o*+{7T2&-t%-h4i7MMo;k|tSqQAeNkwHS9hWY#EV7r3| zTmOmN{;b9OUZpp`LP(I9Wo%R#$b6YdH7GD4*p6>a2N2A04pQ*n;INQMh%+mj;x7>S z_(H?uJ^n!r1)kJH1*s+%$al#?C^Cw{H@RA^QGB=Dubyc)XUaY>f`(VKTlIO-YNCp{1n zOl*>jT?Dtf5fD$DY-j&B*Xmn|2-u2OB zBL@-lFs5lhcQKXBR*cIXmi%~EJcc^5#Xpg!E^A6sXf1#$qJGRpmU~A zcdj-cvBfx(fIRAMU(1obztJR%I7v3R-%$#~r!0sS^I(iC*5i6296*88A7I=_JhU3p zya!aCti0R5*RFT%LW0R|;u&oJ6=P-c$le4J0bi}u!!@;xzao|l6fJ{;Mld9hGhrJg zr_B)=4yktp)yPB@tCC_L9h1>GzXD6DA!W7xt{1)8!07~gONkEWC8@y%lciB{9ojy) zWm$drJ_9uVJ>Q$-`@q%OM7_S>(K=__CGYB~@@mE^Z=eT|x0Rv?Z-N)LLWR zod*Zy3v)iMX@usPX-OKBDgC8yq?fMhqf8H)A&C)Hi29YFn!NVf5!J0-F{wC&L5-3`#id=4?=2>Zp6Pdu4N6#bG&atu7 z8IET&ciXy_Tp4YjMx3yIAbw#_e2#jgGJ~ogkv-|M7|%Gio%2@mnS89NKUOM#Bzg4_ z9e9oN;^m>G*#?)AawODi6YckRPmkSKD_4b4WFpj|@|eS!B0WN@?QscYzTH`~6e%iz z!z1>ps)CG37%(E=kZ_>re)@ODv^0^=rWU^*m;6M&gD10EYImO98JVabRe5{#wrogYUKPB@_(#e7Ej9_x;n1oHDj5GawU)A&1hWj|HzJB(q{vMTX>jOW;Jz zBsW&SqTaR7!NXXg_A}$XnFpg_n)Zi;{e9eb*k|b(y$a}12boJ7rqQXQpVhU8HxHTl zt8Ln!KLFyfq!%}hdMXle^qajw2g6S{z&7tQ6J(w9 z3+!HTO{_TqM{9o$RR~lKFf4b4(xLUP?QG;McNFQc_Yd_mig9Ejy9%q~Ye>rIn3};U z)w&1@QCK;cC(;x0G&YuSad+>{c@ZsFJcUdcs@PP-x{mrO)|6_#CjMlXsMJx;Cr?FF zVFrlt@$Z-Ll^*7d0#`5Uez@bb{Xn(BQLhScBhF!6+aIso0=l{PP7P(6-ru>nVy%AP z+|eZpY(ooMU7rtG$l#14v=Z?@ebOjm(A2)5k_${|wAA$oq+;42wiS78ezjgWWnTrF z`1!i2h{fM91aD8uxz?tZpE(PsL37e3$*I6%un5Bzzpn10p`j72R;3=Oaug_|Z(y)@ z9$SJN@-5d1tNIy0=7|d&_HAnDx!yDd-u#qmfuDh)0a_CVje{hvQz9rDFHJTpQ0Dg@ zGQ3t*gZlcFSXfx%OG@Cds&NDROxd^osY_)abmo^dKMUY!R~kGH%*;rutPF@Mx$zrv z6Q1soKnYYRW#;Bi-!H)>Br0<`y+Wy~p7_<>{ljuG`Dpje=v1x}-ND<)bWBr|<}v6B zkDTUZ^@VsH>CyR}ml4j2rB{}0q8eGwX>ExkI9yZN0)(P}$N(yi$AxmBY#Xj`(7zs{ zJbn2&jE`-*0lww_r;|fNaWm_xp;c9JHIv|RExZGKP%18qjgYa);`N-^VqXNVz{~)~ z?^&D;ouy!pKPy?%@xH`A zSR z7x%N3@o&{YEjfa|1;*eW_4TU{ zt;qCcY3Hj(<0DJuny*QL!y!StcG{>bhpUP%eVMq=1xcR>yZT8X9)1;rXOmQjPcANs zr>&Qb{rr66;s|4v3iGmQlMjr9j;G6pqNs%;TsyVNd3{i~hpDX8ugdcnd&UQJzj)rH zh>S6#n`cCJ9CwHv<2Ht$o`R5(h#r||VB?%J?s5W48;^o)b`Pi1^~}5{Y19lg{&W@LfHt*gc1`w$RfLrK{~H?A1$5 z;5v?AIhpN%gQsR6+Act9-3y z8>jCTMnWQq-^s3#Lb|WalgB$k3F>}lyCxs<2&A;LS0}s#<|hPx9kM#B+Lu2DiD_3P zelg;N!80(j@HNc2pXs}re%sHi+{aqBt~qUOy86?zN>7)yiCEJqy@2Gh#gzJE6j6Rx zBQK{77zW?gLWtQ20Dzntu16k9^N>DQ@Nmbx*mOg=F=k)8VJfM%y(Xu41;8YCz+@K| z9u7vhlT`BOnk_oMTeC;u@OhhoTeA`^34^iMihCLM_uVD>rI-9@4l7ocZl@DJ8FWZU zB0lRBIqkHj4#pE&mD(X!e!~;G$`7f47k* zOznM2@`&KM(|f5}sz)z%2}yJ5YmMj5Zwzr-W?v3R&@KuJ+l0zo==N@)nsbMHqHV}w z7#_ntMGCNM21RuH^SYG+RH0sHUsF2z7ams57@2xbPj0y5)8h+caqv@P^q!do+}>+X zzUBx|mikTawzXWYzJ4(AqAJpBF4ObmD_@gyg->oFGB6`k(8+?rFRV5P1yDkFM=8(c z%RI)iG(rKtq-^V%B_(R9;tk6WIzA?x@cESTXg zWYDBxkoNB5v6J8BP&n@HVtBNb@r+XYpjgub zR4oE*$ffXJuh2g8TCaLnpNoSxJ~Jx@ayx9z5Osa)=AI#bg^5eQb<6gpR%c+Qs#N*e z@XE4pAmjdI#0%pV7sIN>mNa^jTkd=<==2_#t-}9Ju&Z^|Lp$%B92@eN%=MRc)LK$% z@!XAg;dQ8bt=@ZNey7+a(dy^o;QKGP@Rb5NJYQRrGEC{J=FB(Irw-MAfoP(9RK;)&jlxSCT=W;ODCf($WqRFhqN#LR^qVhK zWhEp4`{Nnk;n0FHj}eNCZpRM`Y-@MIM&pvr7zQOZ3Ik5;CmZbR99b&22(!-07YNF) z$o0MKej-jnvQV39{TH4r2R5univa1{ASc|VOTi4c@`t2FId|xkh5typ-rdU;1j){adk@*+( zkHj{5B~eSy&HrPOOvl_FJ98)0V;^d`0-u0FTslgiLBQVGSTiSyu zgMGAu&R}SbNa-DgKJb?;fe3Qys$?=;5?V`eRiq*Kj$I`}Z*x4rC~eNM=DsOq(=nUW>(+7o@O8K-_U(X? zTyg032nXKax5W~SF5|eBj%r8Fa>i!ejC72*sd}zJ)t7Xy!gFvM`c4@*Iw>z$u)j_l zR-Uqxymg}>Ti>i%9j*4kwfC33i~kyIQ``n)r(L z!|H2*)Mwj4dk%e*L0tgFdW185>j4<7YwLXwcOsed`%6mS{+=&d@d!B}GkbDV*0 zNIWzW^|trz!&;qeI&mPiVDOUL70xpqVv0fpN9tjpu)@1LD9D<9}9{57j9!W$`zC6&i zl9lKkmPh`x)5+h>>JtiRNNBW5$_)%-)#+SVSGsjX2T=+SRX05>yJZd`1hyk<@{%1+ zDu^k>J$d*Qz6BZMwHx!@O**^Tx&fsHDw%$@J0nfj^je^Ihy*aIx{B(hkBvSvh46Z9 zRO)BjjXL_IHXKo~$4es=8Wxk;Y+&nVBCXA;=MVuLgVn8Mk(*y^+kP3f?Pr~4^A}hXj9UHS}qeI%XKD3KhHnkrNH0(Y20BWl&!Kfm`EVh2;i5C zpirU^K0nc2-I{cqvjZKVx z=&hH#-d=gDWjVE}cMNAPJf;#NYdQ=h`twjX6yquXuCNgGx1~uk{YHAmFpQF`ZLGC=~ukEyj?cFDI zH=@XvV#AY1EY4qb`y*;Ki>KuFB|2|toL7__Cr0S1Dl{s#y0=~7HSq~&7lpBc*VLua zvv3r&-LM*{hq%IYP7<@)dG-G$kMrZaqs(MYoZ zugEeJ@u(ip9rMoVtoFe;dF`^Br5x7v!rr5`hb5mJ#ocGqXHnm9m`yILjd0>UQSMv) z^v}l5^bM6RZ6M%{mkI) zHOoSp&dX)*xUt+kXscna#a`XxI;Ul2Sxa^i5sZc=(Q)oA^2-_;!pfYHAul+oA@Ilelm;rw@FYR+SIaWS?;_ zUdw<|qqaYq(nqu>rG48E9dYAoT6GH;QRuBYK1}W#C_Z_?7~k*pJ3?MzVt&rhZTsBy zw?nN$_Z>kimtwWcy`0?G#!)&7GjOcxCQps@p&ml8>~z(t=sjhR$6aFh!Vw5GA(lTh z5GM)jCwloa6a}7mdfqNYE7oi`Jv$m5>5qR%9eZ=)=a z+K4j5NpcDHHdepCS+P*{@o=yNp&TE(Sd4b0Notqso-Kt_mhDk1<-fa>T4KdY2N`U) zxu41vD%T&k$Gl?CW81%7r#-o1TZ0&PCcy}L4TPiV;sz`|S!&w8-s$rLdM zF&)>@`7=)65PWn#oi|8tXNb|((2ojf9d0fNZ^l7xY~dX~%*Xf-v2W-2n$i~s!4?H; z2qbQscFN21tqB{|x1+(^G~xQSrvX&Y;V-%?b1}zjBQX{GOFcVYTcwm>>}>6^HA=$x zn+z^Biv_5}0!#@7z1~YXJFCT2?D^jm+kH7jAqBo?M@ZdMl|2|66oLnSJXUOJtVLxe z0vH)N^t*qrjq=eFRMV>BFEfS)-2RzKlt973;d3D}4edwIE>kGc5-o=JV56ird)RlS z{Jg@0t-b#Ife80%!E~(7`qkZ8O~Q-8_{j7G&tqwX&&>^tm-#*{v7j-f1n0}mCR#7P z-4FkajD2$9?4Fc7-C_|0Z_G^bxIs%tWk|aFgSQ(qkM+5PRh=g&ZeAZg35$-kn~}_;~&fP-dCNCzg>{gyW!~LZpn?aZ~Va3~H0Ta)z z<4XPVk@;#%1S@fq<(2#8T04#8$mz>vM;(jek0>Qh!K%t5*4tU(fVYwD3Ri~=D!AmI zV$Dt#TEDX7{lpW%tF&DOlTO)vZodn_%wYu~)ZQ}Qo^cBbDHd{YajkzNxttQW>ST<^ z2~^xhB_y1sjIF5;xchvCn{QVugIE2eYZDZ!-Y-4lJdb34*k({@M zJ5!9Di^||~(IZ4iOoAbtggao+CaYvJynmB^;4r-tY2gS_*P!?U?hlEX;l+^*{%B2n z)|1j9wOHQQ^5Xha>{Cu8_w^8=#6;Dz7kU~RgTqn;ynDm6{xdlkf2vk0UK^oS3yVy4 zE+v&qnlYtPHBk#X&2}r7`@K`J@^e~Qm?iRJ*tbAaZDZTmB&mWMkZp7Kj7^kth#_uX z5z>gC(8Xz|Ie(+#&wiF3;Aey|Db(R*-U)!6;l_5@u?-$>j0SgEl5+c}Lfe-$p-dFH zB_$bC<)x6#A_2Uuo8=^l1@}vK!gvbF#b&MoH8ac3xMxUz$LFb8KU(x$YhtHanM_sw zYOFMBX2iNNSe&a}!;G9nv(tsW4@%3iQcqczOCF*JOBQ@4Orw=o?_vc(9$hfO`>U6& zyY_CUa9pASiJpmv`@oR!k;&$`h8!)$uS=}d-fPddfIdMDUW@%3y1LI(1Q=e$)sz(QC*E;Nfl99YTgk+|@jl`+iF?<_D?4YqV0Zl)lO8YWC@1ZWW^mi{5ePQN<~FQ2NMG$|K{py5akJa zkezmqhN)>MGMp$7=sOo2(7ppv``dCIwf&MaQQis7S596kkiw8Do(jO?EY4iJ4Hec6 z4Hymzu`w)cI9Pbq6GPtTP)x&Lmk;FT=ZCB4>(5}c0?;2l`p&?>&<;2(P8a3lOTNP# zdEzF5qDpkRR&PZC&cS{7xD@qV;(g5X%xI?m$9Q -Fastapi Auth Partner +README.rst -
    -

    Fastapi Auth Partner

    +
    + + +Odoo Community Association + +
    +

    Fastapi Auth Partner

    -

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This module is the FastAPI implementation of auth_partner it provides all the routes to manage the authentication of partners.

    Table of contents

    @@ -386,7 +391,7 @@

    Fastapi Auth Partner

    -

    Usage

    +

    Usage

    First you have to add the auth router to your FastAPI endpoint and the authentication dependency to your app dependencies:

     from odoo.addons.fastapi import dependencies
    @@ -434,7 +439,7 @@ 

    Usage

    -

    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 @@ -442,15 +447,15 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Akretion
    -

    Contributors

    +

    Contributors

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -473,5 +478,6 @@

    Maintainers

    +
    diff --git a/setup/_metapackage/VERSION.txt b/setup/_metapackage/VERSION.txt index 086eb2e47..13f42e886 100644 --- a/setup/_metapackage/VERSION.txt +++ b/setup/_metapackage/VERSION.txt @@ -1 +1 @@ -16.0.20250603.0 \ No newline at end of file +16.0.20250603.1 \ No newline at end of file diff --git a/setup/_metapackage/setup.py b/setup/_metapackage/setup.py index 415e9c7d8..f06d16f9e 100644 --- a/setup/_metapackage/setup.py +++ b/setup/_metapackage/setup.py @@ -8,6 +8,7 @@ description="Meta package for oca-rest-framework Odoo addons", version=version, install_requires=[ + 'odoo-addon-auth_partner>=16.0dev,<16.1dev', 'odoo-addon-base_rest>=16.0dev,<16.1dev', 'odoo-addon-base_rest_auth_api_key>=16.0dev,<16.1dev', 'odoo-addon-base_rest_datamodel>=16.0dev,<16.1dev', @@ -19,6 +20,7 @@ 'odoo-addon-fastapi>=16.0dev,<16.1dev', 'odoo-addon-fastapi_auth_jwt>=16.0dev,<16.1dev', 'odoo-addon-fastapi_auth_jwt_demo>=16.0dev,<16.1dev', + 'odoo-addon-fastapi_auth_partner>=16.0dev,<16.1dev', 'odoo-addon-fastapi_encrypted_errors>=16.0dev,<16.1dev', 'odoo-addon-graphql_base>=16.0dev,<16.1dev', 'odoo-addon-graphql_demo>=16.0dev,<16.1dev', From b94bdf8e9622a3db975f7cf78cf0ec86c267c8bb Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 3 Jun 2025 14:43:06 +0000 Subject: [PATCH 32/58] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: rest-framework-16.0/rest-framework-16.0-fastapi Translate-URL: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi/ --- fastapi/i18n/it.po | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fastapi/i18n/it.po b/fastapi/i18n/it.po index a55194a6a..5680bfb47 100644 --- a/fastapi/i18n/it.po +++ b/fastapi/i18n/it.po @@ -50,8 +50,8 @@ msgstr "In archivio" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__demo_auth_method -msgid "Authenciation method" -msgstr "Metodo autenticazione" +msgid "Authentication method" +msgstr "" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__company_id @@ -264,3 +264,6 @@ msgstr "" #, python-format msgid "`%(name)s` uses a blacklisted root_path = `%(root_path)s`" msgstr "`%(name)s` utilizza un root_path bloccato = `%(root_path)s`" + +#~ msgid "Authenciation method" +#~ msgstr "Metodo autenticazione" From e7f989eead8b39b602322d7a333e9bde39622fc9 Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 4 Jun 2025 08:30:09 +0000 Subject: [PATCH 33/58] Translated using Weblate (Italian) Currently translated at 100.0% (43 of 43 strings) Translation: rest-framework-16.0/rest-framework-16.0-fastapi Translate-URL: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi/it/ --- fastapi/i18n/it.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastapi/i18n/it.po b/fastapi/i18n/it.po index 5680bfb47..818698a62 100644 --- a/fastapi/i18n/it.po +++ b/fastapi/i18n/it.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2025-04-24 12:23+0000\n" -"Last-Translator: Sebastiano Picchi \n" +"PO-Revision-Date: 2025-06-04 09:40+0000\n" +"Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" @@ -51,7 +51,7 @@ msgstr "In archivio" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__demo_auth_method msgid "Authentication method" -msgstr "" +msgstr "Metodo autenticazione" #. module: fastapi #: model:ir.model.fields,field_description:fastapi.field_fastapi_endpoint__company_id From ef640cc5700b0fe48a596973e41cc3a0c33665ba Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 4 Jun 2025 09:40:03 +0000 Subject: [PATCH 34/58] Added translation using Weblate (Italian) --- fastapi_auth_partner/i18n/it.po | 264 ++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 fastapi_auth_partner/i18n/it.po diff --git a/fastapi_auth_partner/i18n/it.po b/fastapi_auth_partner/i18n/it.po new file mode 100644 index 000000000..2e8ddc374 --- /dev/null +++ b/fastapi_auth_partner/i18n/it.po @@ -0,0 +1,264 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fastapi_auth_partner +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__app +msgid "App" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_auth_directory +msgid "Auth Directory" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__auth_partner_id +msgid "Auth Partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.ui.menu,name:fastapi_auth_partner.fastapi_auth +msgid "Authentication" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__demo_auth_method +msgid "Authentication method" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Cancel" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__cookie_duration +msgid "Cookie Duration" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__cookie_secret_key +msgid "Cookie Secret Key" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__create_uid +msgid "Created by" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__create_date +msgid "Created on" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields.selection,name:fastapi_auth_partner.selection__fastapi_endpoint__app__demo +msgid "Demo Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_auth_service__directory_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__directory_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__auth_directory_id +#: model:ir.ui.menu,name:fastapi_auth_partner.auth_directory_menu +msgid "Directory" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__display_name +msgid "Display Name" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_auth_service__endpoint_id +msgid "Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_fastapi_endpoint +msgid "FastAPI Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__fastapi_endpoint_ids +msgid "FastAPI Endpoints" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_fastapi_auth_service +msgid "Fastapi Auth Service" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__fastapi_endpoint_id +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_reset_password__fastapi_endpoint_id +msgid "Fastapi Endpoint" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__id +msgid "ID" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.actions.act_window,name:fastapi_auth_partner.auth_partner_action_impersonate +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Impersonate" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Impersonation successful" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_auth_directory__cookie_duration +msgid "In minute, default 525600 minutes => 1 year" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__is_auth_partner +msgid "Is Auth Partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Label" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.auth_partner_view_form +msgid "Local Impersonate" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/routers/auth.py:0 +#, python-format +msgid "No cookie secret key defined" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Only admin can impersonate locally" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.ui.menu,name:fastapi_auth_partner.auth_partner_menu +msgid "Partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields.selection,name:fastapi_auth_partner.selection__fastapi_endpoint__demo_auth_method__auth_partner +msgid "Partner Auth" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form +msgid "Please choose an endpoint:" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Please install base_future_response for local impersonate to work" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__public_api_url +msgid "Public Api Url" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__public_url +msgid "Public Url" +msgstr "" + +#. module: fastapi_auth_partner +#: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.auth_directory_view_form +msgid "Regenerate cookie secret key" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__sliding_session +msgid "Sliding Session" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__is_auth_partner +msgid "Technical field to know if the auth method is partner" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__public_api_url +msgid "" +"The public URL of the API.\n" +"This URL is used in impersonation to set the cookie on the right API domain if you use a reverse proxy to serve the API.\n" +"Defaults to the public_url if not set or the odoo url if not set either." +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__public_url +msgid "" +"The public URL of the site.\n" +"This URL is used for the impersonation final redirect. And can also be used in the mail template to construct links.\n" +"Default to the public_api_url if not set or the odoo url if not set either." +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model.fields,help:fastapi_auth_partner.field_auth_directory__cookie_secret_key +msgid "The secret key used to sign the cookie" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_wizard_auth_partner_impersonate +msgid "Wizard Partner Auth Impersonate" +msgstr "" + +#. module: fastapi_auth_partner +#: model:ir.model,name:fastapi_auth_partner.model_wizard_auth_partner_reset_password +msgid "Wizard Partner Auth Reset Password" +msgstr "" + +#. module: fastapi_auth_partner +#. odoo-python +#: code:addons/fastapi_auth_partner/models/auth_partner.py:0 +#, python-format +msgid "" +"You are now impersonating %s\n" +"%%s" +msgstr "" From 659d54be726ffeb6adaabf8a65b60c8db9c016ea Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 4 Jun 2025 09:40:11 +0000 Subject: [PATCH 35/58] Added translation using Weblate (Italian) --- fastapi_encrypted_errors/i18n/it.po | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 fastapi_encrypted_errors/i18n/it.po diff --git a/fastapi_encrypted_errors/i18n/it.po b/fastapi_encrypted_errors/i18n/it.po new file mode 100644 index 000000000..a1fd0db53 --- /dev/null +++ b/fastapi_encrypted_errors/i18n/it.po @@ -0,0 +1,114 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fastapi_encrypted_errors +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: fastapi_encrypted_errors +#: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_view_form +msgid "Close" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__create_uid +msgid "Created by" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__create_date +msgid "Created on" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.actions.act_window,name:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_action_decrypt_error +#: model:ir.ui.menu,name:fastapi_encrypted_errors.menu_fastapi_decrypt_errors +#: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_view_form +msgid "Decrypt Error" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__decrypted_error +msgid "Decrypted Error" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__display_name +msgid "Display Name" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_fastapi_endpoint__encrypt_errors +msgid "Encrypt Errors" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,help:fastapi_encrypted_errors.field_fastapi_endpoint__encrypt_errors +msgid "Encrypt errors before sending them to the client." +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_fastapi_endpoint__encrypted_errors_secret_key +msgid "Encrypted Errors Secret Key" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__error +msgid "Error" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model,name:fastapi_encrypted_errors.model_fastapi_endpoint +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__fastapi_endpoint_id +msgid "FastAPI Endpoint" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.fastapi_endpoint_form_view +msgid "Generate Secret Key" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__id +msgid "ID" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_view_form +msgid "Label" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model.fields,help:fastapi_encrypted_errors.field_fastapi_endpoint__encrypted_errors_secret_key +msgid "" +"The secret key used to encrypt errors before sending them to the client." +msgstr "" + +#. module: fastapi_encrypted_errors +#: model:ir.model,name:fastapi_encrypted_errors.model_wizard_fastapi_decrypt_errors +msgid "Wizard to decrypt FastAPI errors" +msgstr "" From c235c3bb3bdf1f1c24570a972443dd05809dea96 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Wed, 4 Jun 2025 12:12:18 +0200 Subject: [PATCH 36/58] [FIX] fastapi: Ensures inner exception is reset --- fastapi/fastapi_dispatcher.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index c41beb2d2..0ded74c3d 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -52,6 +52,7 @@ def handle_error(self, exc): def _make_response(self, status_mapping, headers_tuple, content): self.status = status_mapping[:3] self.headers = headers_tuple + self.inner_exception = None # in case of exception, the method asgi_done_callback of the # ASGIResponder will trigger an "a2wsgi.error" event with the exception # instance stored in a tuple with the type of the exception and the traceback. From 04c3f46219d9efd259434a74f7f0e5ee2c6a8104 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 4 Jun 2025 10:26:45 +0000 Subject: [PATCH 37/58] [BOT] post-merge updates --- README.md | 2 +- fastapi/README.rst | 8 ++- fastapi/__manifest__.py | 2 +- fastapi/static/description/index.html | 88 ++++++++++++++------------- 4 files changed, 55 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 0c6dca0ce..1ef824e95 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ addon | version | maintainers | summary [datamodel](datamodel/) | 16.0.1.0.2 | lmignon | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | lmignon | Extendable classes registry loader for Odoo [extendable_fastapi](extendable_fastapi/) | 16.0.2.1.2 | lmignon | Allows the use of extendable into fastapi apps -[fastapi](fastapi/) | 16.0.1.6.0 | lmignon | Odoo FastAPI endpoint +[fastapi](fastapi/) | 16.0.1.6.1 | lmignon | Odoo FastAPI endpoint [fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | sbidoul | JWT bearer token authentication for FastAPI. [fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | sbidoul | Test/demo module for fastapi_auth_jwt. [fastapi_auth_partner](fastapi_auth_partner/) | 16.0.1.0.0 | | This provides an implementation of auth_partner for FastAPI diff --git a/fastapi/README.rst b/fastapi/README.rst index d12578ae5..417ab2d2f 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ============ Odoo FastAPI ============ @@ -7,13 +11,13 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:1a9e09ed26e6e827518e1712413b5b62003b71f34e738c845ad3a0712505104f + !! source digest: sha256:3407249de169bb59ec2b991f1a1eff6e935d6a372e06d1ad27588cb7d9fad1f0 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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 +.. |badge2| image:: https://img.shields.io/badge/license-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%2Frest--framework-lightgray.png?logo=github diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index 2a62f9ca2..9339cac8d 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.6.0", + "version": "16.0.1.6.1", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index f7468b32d..1acafecb0 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -3,7 +3,7 @@ -Odoo FastAPI +README.rst -
    -

    Odoo FastAPI

    +
    + + +Odoo Community Association + +
    +

    Odoo FastAPI

    -

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI framework into Odoo.

    This integration allows you to use all the goodies from FastAPI to build custom @@ -453,9 +458,9 @@

    Odoo FastAPI

    -

    Usage

    +

    Usage

    -

    What’s building an API with fastapi?

    +

    What’s building an API with fastapi?

    FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. This addons let’s you keep advantage of the fastapi framework and use it with Odoo.

    @@ -659,7 +664,7 @@

    What’s building an API with fas

    -

    Dealing with the odoo environment

    +

    Dealing with the odoo environment

    The ‘odoo.addons.fastapi.dependencies’ module provides a set of functions that you can use to inject reusable dependencies into your routes. For example, the ‘odoo_env’ function returns the current odoo environment. You can use it to access the @@ -707,7 +712,7 @@

    Dealing with the odoo environment

    -

    The dependency injection mechanism

    +

    The dependency injection mechanism

    The ‘odoo_env’ dependency relies on a simple implementation that retrieves the current odoo environment from ContextVar variable initialized at the start of the request processing by the specific request dispatcher processing the @@ -757,7 +762,7 @@

    The dependency injection mechanis endpoint configuration.

    -

    The authentication mechanism

    +

    The authentication mechanism

    To make our app not tightly coupled with a specific authentication mechanism, we will use the ‘authenticated_partner’ dependency. As for the ‘fastapi_endpoint’ this dependency depends on an abstract dependency.

    @@ -918,7 +923,7 @@

    The authentication mechanism<

    -

    Managing configuration parameters for your app

    +

    Managing configuration parameters for your app

    As we have seen in the previous section, you can add configuration fields on the fastapi endpoint model to allow the user to configure your app (as for any odoo model you extend). When you need to access these configuration fields @@ -982,7 +987,7 @@

    Managing configuration parameters

    -

    Dealing with languages

    +

    Dealing with languages

    The fastapi addon parses the Accept-Language header of the request to determine the language to use. This parsing is done by respecting the RFC 7231 specification. That means that the language is determined by the first language found in the header that is @@ -994,7 +999,7 @@

    Dealing with languages

    of your app to instruct the api consumers how to request a specific language.

    -

    How to extend an existing app

    +

    How to extend an existing app

    When you develop a fastapi app, in a native python app it’s not possible to extend an existing one. This limitation doesn’t apply to the fastapi addon because the fastapi endpoint model is designed to be extended. However, the @@ -1022,7 +1027,7 @@

    How to extend an existing app

    -

    Changing the implementation of the route handler

    +

    Changing the implementation of the route handler

    Let’s say that you want to change the implementation of the route handler ‘/demo/echo’. Since a route handler is just a python method, it could seems a tedious task since we are not into a model method and therefore we can’t @@ -1091,7 +1096,7 @@

    Changing the implementation of th

    -

    Overriding the dependencies of the route handler

    +

    Overriding the dependencies of the route handler

    As you’ve previously seen, the dependency injection mechanism of fastapi is very powerful. By designing your route handler to rely on dependencies with a specific functional scope, you can easily change the implementation of the @@ -1103,7 +1108,7 @@

    Overriding the dependencies of t ‘odoo/addons/fastapi/models/fastapi_endpoint_demo.py’)

    -

    Adding a new route handler

    +

    Adding a new route handler

    Let’s say that you want to add a new route handler ‘/demo/echo2’. You could be tempted to add this new route handler in your new addons by importing the router of the existing app and adding the new route handler to @@ -1160,7 +1165,7 @@

    Adding a new route handler

    -

    Extending the model used as parameter or as response of the route handler

    +

    Extending the model used as parameter or as response of the route handler

    The fastapi python library uses the pydantic library to define the models. By default, once a model is defined, it’s not possible to extend it. However, a companion python library called @@ -1228,7 +1233,7 @@

    Extending the model used as para default values for the new optional fields.

    -

    Managing security into the route handlers

    +

    Managing security into the route handlers

    By default the route handlers are processed using the user configured on the ‘fastapi.endpoint’ model instance. (default is the Public user). You have seen previously how to define a dependency that will be used to enforce @@ -1298,7 +1303,7 @@

    Managing security into the route

    -

    How to test your fastapi app

    +

    How to test your fastapi app

    Thanks to the starlette test client, it’s possible to test your fastapi app in a very simple way. With the test client, you can call your route handlers as if they were real http endpoints. The test client is available in the @@ -1365,7 +1370,7 @@

    How to test your fastapi app

    -

    Overall considerations when you develop an fastapi app

    +

    Overall considerations when you develop an fastapi app

    Developing a fastapi app requires to follow some good practices to ensure that the app is robust and easy to maintain. Here are some of them:

    -

    Development of a search route handler

    +

    Development of a search route handler

    The ‘odoo-addon-fastapi’ module provides 2 useful piece of code to help you be consistent when writing a route handler for a search route.

      @@ -1529,7 +1534,7 @@

      Development of a search route ha

    -

    Error handling

    +

    Error handling

    The error handling is a very important topic in the design of the fastapi integration with odoo. By default, when instantiating the fastapi app, the fastapi library declare a default exception handler that will catch any exception raised by the @@ -1547,7 +1552,7 @@

    Error handling

    add a custom exception handler in your app, it will be ignored.

    -

    FastAPI addons directory structure

    +

    FastAPI addons directory structure

    When you develop a new addon to expose an api with fastapi, it’s a good practice to follow the same directory structure and naming convention for the files related to the api. It will help you to easily find the files related to the api @@ -1651,14 +1656,14 @@

    FastAPI addons directory structu
    -

    What’s next?

    +

    What’s next?

    The ‘odoo-addon-fastapi’ module is still in its early stage of development. It will evolve over time to integrate your feedback and to provide the missing features. It’s now up to you to try it and to provide your feedback.

    -

    Known issues / Roadmap

    +

    Known issues / Roadmap

    The roadmap and known issues can be found on GitHub.

    @@ -1670,9 +1675,9 @@

    Known issues / Roadmap

    WebSockets and to stream large responses.

    -

    Changelog

    +

    Changelog

    -

    16.0.1.4.1 (2024-07-08)

    +

    16.0.1.4.1 (2024-07-08)

    Bugfixes

    -

    16.0.1.4.0 (2024-06-06)

    +

    16.0.1.4.0 (2024-06-06)

    Bugfixes

    • This change is a complete rewrite of the way the transactions are managed when @@ -1724,7 +1729,7 @@

      16.0.1.4.0 (2024-06-06)

    -

    16.0.1.2.6 (2024-02-20)

    +

    16.0.1.2.6 (2024-02-20)

    Bugfixes

    -

    16.0.1.2.5 (2024-01-17)

    +

    16.0.1.2.5 (2024-01-17)

    Bugfixes

    • Odoo has done an update and now, it checks domains of ir.rule on creation and modification.

      @@ -1747,7 +1752,7 @@

      16.0.1.2.5 (2024-01-17)

    -

    16.0.1.2.3 (2023-12-21)

    +

    16.0.1.2.3 (2023-12-21)

    Bugfixes

    • In case of exception in endpoint execution, close the database cursor after rollback.

      @@ -1757,7 +1762,7 @@

      16.0.1.2.3 (2023-12-21)

    -

    16.0.1.2.2 (2023-12-12)

    +

    16.0.1.2.2 (2023-12-12)

    Bugfixes

    • When using the ‘FastAPITransactionCase’ class, allows to specify a specific @@ -1772,7 +1777,7 @@

      16.0.1.2.2 (2023-12-12)

    -

    16.0.1.2.1 (2023-11-03)

    +

    16.0.1.2.1 (2023-11-03)

    Bugfixes

    • Fix a typo in the Field declaration of the ‘count’ attribute of the ‘PagedCollection’ schema.

      @@ -1781,7 +1786,7 @@

      16.0.1.2.1 (2023-11-03)

    -

    16.0.1.2.0 (2023-10-13)

    +

    16.0.1.2.0 (2023-10-13)

    Features

    • The field total in the PagedCollection schema is replaced by the field count. @@ -1795,7 +1800,7 @@

      16.0.1.2.0 (2023-10-13)

    -

    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 @@ -1803,21 +1808,21 @@

    Bug Tracker

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

    + From b8e155b96879388889337267b8fcd42be5376aed Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 4 Jun 2025 13:27:34 +0000 Subject: [PATCH 38/58] Added translation using Weblate (Italian) --- auth_partner/i18n/it.po | 555 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 auth_partner/i18n/it.po diff --git a/auth_partner/i18n/it.po b/auth_partner/i18n/it.po new file mode 100644 index 000000000..1800041ef --- /dev/null +++ b/auth_partner/i18n/it.po @@ -0,0 +1,555 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_partner +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__14-days +msgid "14 Days" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__2-days +msgid "2-days" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__6-hours +msgid "6 Hours" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__7-days +msgid "7 Days" +msgstr "" + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_reset_password +msgid "" +"
    \n" +" Hi \n" +" Click on the following link to reset your password\n" +" Reset Password\n" +"
    \n" +" " +msgstr "" + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_validate_email +msgid "" +"
    \n" +" Hi \n" +" Welcome to the site, please click on the following link to verify your email\n" +" Validate Email\n" +"
    \n" +" " +msgstr "" + +#. module: auth_partner +#: model:mail.template,body_html:auth_partner.email_set_password +msgid "" +"
    \n" +" Hi \n" +" Welcome, your account have been created\n" +" Click on the following link to set your password\n" +" Set Password\n" +"
    \n" +" " +msgstr "" + +#. module: auth_partner +#: model:res.groups,name:auth_partner.group_auth_partner_api +msgid "API Partner Auth Access" +msgstr "" + +#. module: auth_partner +#: model:res.groups,name:auth_partner.group_auth_partner_manager +msgid "API Partner Auth Manager" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.view_partner_form +msgid "Account" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "" +"An email will be send with a token to each customer, you can specify the " +"date until the link is valid" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_auth_directory +msgid "Auth Directory" +msgstr "" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_reset_password +msgid "Auth Directory: Reset Password" +msgstr "" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_set_password +msgid "Auth Directory: Set Password" +msgstr "" + +#. module: auth_partner +#: model:mail.template,name:auth_partner.email_validate_email +msgid "Auth Directory: Validate Email" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Auth Partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_count +#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_count +msgid "Auth Partner Count" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__auth_partner_ids +msgid "Auth Partners" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Cancel" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password_confirm +msgid "Confirm Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_res_partner +msgid "Contact" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__count_partner +msgid "Count Partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_uid +msgid "Created by" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_date +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__create_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_date +msgid "Created on" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_impersonation +msgid "Date Last Impersonation" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_request_reset_pwd +msgid "Date Last Request Reset Pwd" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd +msgid "Date Last Sucessfull Reset Pwd" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__date_validity +msgid "Date Validity" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_request_reset_pwd +msgid "Date of the last password reset request" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_impersonation +msgid "Date of the last sucessfull impersonation" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd +msgid "Date of the last sucessfull password reset" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__delay +msgid "Delay" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_directory_action +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__directory_id +#: model:ir.ui.menu,name:auth_partner.auth_directory_menu +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Directory" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_search +msgid "Directory Auth" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__display_name +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__display_name +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__display_name +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__display_name +msgid "Display Name" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "" +"Email address not validated. Validate your email address by clicking on the " +"link in the email sent to you or request a new password. " +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__encrypted_password +msgid "Encrypted Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__force_verified_email +msgid "Force Verified Email" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search +msgid "Group By" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__id +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__id +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__id +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__id +msgid "ID" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__force_verified_email +msgid "If checked, email must be verified to be able to log in" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +msgid "Impersonate" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_token_duration +msgid "Impersonating Token Duration" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_user_ids +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__impersonating_user_ids +msgid "Impersonating Users" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__set_password_token_duration +msgid "In minute, default 1440 minutes => 24h" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_token_duration +msgid "In seconds, default 60 seconds" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "Invalid Login or Password" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#: code:addons/auth_partner/models/auth_directory.py:0 +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "Invalid Token" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "Invalid token" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Label" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory____last_update +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner____last_update +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password____last_update +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password____last_update +msgid "Last Modified on" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_uid +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_date +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__write_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_date +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_date +msgid "Last Updated on" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__login +msgid "Login" +msgstr "" + +#. module: auth_partner +#: model:ir.model.constraint,message:auth_partner.constraint_auth_partner_directory_login_uniq +msgid "Login must be uniq per directory !" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__template_id +msgid "Mail Template" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__reset_password_template_id +msgid "Mail Template Forget Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_template_id +msgid "Mail Template New Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__validate_email_template_id +msgid "Mail Template Validate Email" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__mail_verified +msgid "Mail Verified" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__manually +msgid "Manually" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__name +msgid "Name" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__nbr_pending_reset_sent +msgid "Nbr Pending Reset Sent" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "No active_id in context" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_directory.py:0 +#, python-format +msgid "No email template defined for %(template)s in %(directory)s" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__nbr_pending_reset_sent +msgid "" +"Number of pending reset sent from your customer.This field is usefull when " +"after a migration from an other system you ask all you customer to reset " +"their password and you senddifferent mail depending on the number of " +"reminder" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_action +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__partner_id +#: model:ir.ui.menu,name:auth_partner.auth_partner_menu +msgid "Partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_ids +#: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_ids +msgid "Partner Auth" +msgstr "" + +#. module: auth_partner +#: model:ir.ui.menu,name:auth_partner.auth +msgid "Partner Authentication" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__password +#: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password +msgid "Password" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "Password and Confirm Password must be the same" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form +msgid "Regenerate secret key" +msgstr "" + +#. module: auth_partner +#: model:mail.template,subject:auth_partner.email_reset_password +msgid "Reset Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key +msgid "Secret Key" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_default +msgid "Secret Key Env Default" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_is_editable +msgid "Secret Key Env Is Editable" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +msgid "Send Invite" +msgstr "" + +#. module: auth_partner +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form +msgid "Send Reset Password" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_action_reset_password +msgid "Send Reset Password Instruction" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: auth_partner +#: model:ir.actions.act_window,name:auth_partner.auth_partner_force_set_password_action +#: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form +#: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form +msgid "Set Password" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_token_duration +msgid "Set Password Token Duration" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__user_can_impersonate +msgid "Technical field to check if the user can impersonate" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_user_ids +#: model:ir.model.fields,help:auth_partner.field_auth_partner__impersonating_user_ids +msgid "These odoo users can impersonate any partner of this directory" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,help:auth_partner.field_auth_partner__mail_verified +msgid "" +"This field is set to True when the user has clicked on the link sent by " +"email" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 +#, python-format +msgid "This wizard can only be used on auth.partner" +msgstr "" + +#. module: auth_partner +#: model:ir.model.fields,field_description:auth_partner.field_auth_partner__user_can_impersonate +msgid "User Can Impersonate" +msgstr "" + +#. module: auth_partner +#: model:mail.template,subject:auth_partner.email_set_password +#: model:mail.template,subject:auth_partner.email_validate_email +msgid "Welcome" +msgstr "" + +#. module: auth_partner +#: model:ir.model,name:auth_partner.model_wizard_auth_partner_force_set_password +#: model:ir.model,name:auth_partner.model_wizard_auth_partner_reset_password +msgid "Wizard Partner Auth Reset Password" +msgstr "" + +#. module: auth_partner +#. odoo-python +#: code:addons/auth_partner/models/auth_partner.py:0 +#, python-format +msgid "You are not allowed to impersonate this user" +msgstr "" From c838b2f5a7ef9889a02f5315f043a15a3423e6d2 Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 4 Jun 2025 14:12:24 +0000 Subject: [PATCH 39/58] Translated using Weblate (Italian) Currently translated at 100.0% (45 of 45 strings) Translation: rest-framework-16.0/rest-framework-16.0-fastapi_auth_partner Translate-URL: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi_auth_partner/it/ --- fastapi_auth_partner/i18n/it.po | 100 ++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/fastapi_auth_partner/i18n/it.po b/fastapi_auth_partner/i18n/it.po index 2e8ddc374..6add7ce35 100644 --- a/fastapi_auth_partner/i18n/it.po +++ b/fastapi_auth_partner/i18n/it.po @@ -6,69 +6,71 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2025-06-04 16:26+0000\n" +"Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__app msgid "App" -msgstr "" +msgstr "Applicazione" #. module: fastapi_auth_partner #: model:ir.model,name:fastapi_auth_partner.model_auth_directory msgid "Auth Directory" -msgstr "" +msgstr "Cartella autorizzazione" #. module: fastapi_auth_partner #: model:ir.model,name:fastapi_auth_partner.model_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__auth_partner_id msgid "Auth Partner" -msgstr "" +msgstr "Partner autorizzazione" #. module: fastapi_auth_partner #: model:ir.ui.menu,name:fastapi_auth_partner.fastapi_auth msgid "Authentication" -msgstr "" +msgstr "Autenticazione" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__demo_auth_method msgid "Authentication method" -msgstr "" +msgstr "Metodo autenticazione" #. module: fastapi_auth_partner #: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form msgid "Cancel" -msgstr "" +msgstr "Annulla" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__cookie_duration msgid "Cookie Duration" -msgstr "" +msgstr "Durata cookie" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__cookie_secret_key msgid "Cookie Secret Key" -msgstr "" +msgstr "Chiave segreta cookie" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__create_uid msgid "Created by" -msgstr "" +msgstr "Creato da" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__create_date msgid "Created on" -msgstr "" +msgstr "Creato il" #. module: fastapi_auth_partner #: model:ir.model.fields.selection,name:fastapi_auth_partner.selection__fastapi_endpoint__app__demo msgid "Demo Endpoint" -msgstr "" +msgstr "Endpoint esempio" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_auth_service__directory_id @@ -76,152 +78,152 @@ msgstr "" #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__auth_directory_id #: model:ir.ui.menu,name:fastapi_auth_partner.auth_directory_menu msgid "Directory" -msgstr "" +msgstr "Cartella" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__display_name msgid "Display Name" -msgstr "" +msgstr "Nome visualizzato" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_auth_service__endpoint_id msgid "Endpoint" -msgstr "" +msgstr "Endpoint" #. module: fastapi_auth_partner #: model:ir.model,name:fastapi_auth_partner.model_fastapi_endpoint msgid "FastAPI Endpoint" -msgstr "" +msgstr "Endopoint FastAPI" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__fastapi_endpoint_ids msgid "FastAPI Endpoints" -msgstr "" +msgstr "Endpoint FastAPI" #. module: fastapi_auth_partner #: model:ir.model,name:fastapi_auth_partner.model_fastapi_auth_service msgid "Fastapi Auth Service" -msgstr "" +msgstr "Servizio autenticazione FastAPI" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__fastapi_endpoint_id #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_reset_password__fastapi_endpoint_id msgid "Fastapi Endpoint" -msgstr "" +msgstr "Endopoint FastAPI" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__id msgid "ID" -msgstr "" +msgstr "ID" #. module: fastapi_auth_partner #: model:ir.actions.act_window,name:fastapi_auth_partner.auth_partner_action_impersonate #: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form msgid "Impersonate" -msgstr "" +msgstr "Imita" #. module: fastapi_auth_partner #. odoo-python #: code:addons/fastapi_auth_partner/models/auth_partner.py:0 #, python-format msgid "Impersonation successful" -msgstr "" +msgstr "Imitazione riuscita" #. module: fastapi_auth_partner #: model:ir.model.fields,help:fastapi_auth_partner.field_auth_directory__cookie_duration msgid "In minute, default 525600 minutes => 1 year" -msgstr "" +msgstr "In minuti, predefinito minuti => 1 anno" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__is_auth_partner msgid "Is Auth Partner" -msgstr "" +msgstr "È partner autorizzazione" #. module: fastapi_auth_partner #: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form msgid "Label" -msgstr "" +msgstr "Etichetta" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate____last_update msgid "Last Modified on" -msgstr "" +msgstr "Ultima modifica il" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Ultimo aggiornamento di" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_wizard_auth_partner_impersonate__write_date msgid "Last Updated on" -msgstr "" +msgstr "Ultimo aggiornamento il" #. module: fastapi_auth_partner #: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.auth_partner_view_form msgid "Local Impersonate" -msgstr "" +msgstr "Imitazione locale" #. module: fastapi_auth_partner #. odoo-python #: code:addons/fastapi_auth_partner/routers/auth.py:0 #, python-format msgid "No cookie secret key defined" -msgstr "" +msgstr "Nessuna chiave segreta cookie definita" #. module: fastapi_auth_partner #. odoo-python #: code:addons/fastapi_auth_partner/models/auth_partner.py:0 #, python-format msgid "Only admin can impersonate locally" -msgstr "" +msgstr "Solo l'amministratore può imitare localmente" #. module: fastapi_auth_partner #: model:ir.ui.menu,name:fastapi_auth_partner.auth_partner_menu msgid "Partner" -msgstr "" +msgstr "Partner" #. module: fastapi_auth_partner #: model:ir.model.fields.selection,name:fastapi_auth_partner.selection__fastapi_endpoint__demo_auth_method__auth_partner msgid "Partner Auth" -msgstr "" +msgstr "Autorizzazione partner" #. module: fastapi_auth_partner #: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.wizard_auth_partner_impersonate_view_form msgid "Please choose an endpoint:" -msgstr "" +msgstr "Scegliere un endpoint:" #. module: fastapi_auth_partner #. odoo-python #: code:addons/fastapi_auth_partner/models/auth_partner.py:0 #, python-format msgid "Please install base_future_response for local impersonate to work" -msgstr "" +msgstr "Installare base_future_response per far funzionare l'imitazione locale" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__public_api_url msgid "Public Api Url" -msgstr "" +msgstr "URL API pubblico" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_fastapi_endpoint__public_url msgid "Public Url" -msgstr "" +msgstr "URL pubblico" #. module: fastapi_auth_partner #: model_terms:ir.ui.view,arch_db:fastapi_auth_partner.auth_directory_view_form msgid "Regenerate cookie secret key" -msgstr "" +msgstr "Rigenera chiave segreta cookie" #. module: fastapi_auth_partner #: model:ir.model.fields,field_description:fastapi_auth_partner.field_auth_directory__sliding_session msgid "Sliding Session" -msgstr "" +msgstr "Sessione scorrevole" #. module: fastapi_auth_partner #: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__is_auth_partner msgid "Technical field to know if the auth method is partner" -msgstr "" +msgstr "Campo tecnico per sapere se il metodo di autorizzazione è partner" #. module: fastapi_auth_partner #: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__public_api_url @@ -230,6 +232,11 @@ msgid "" "This URL is used in impersonation to set the cookie on the right API domain if you use a reverse proxy to serve the API.\n" "Defaults to the public_url if not set or the odoo url if not set either." msgstr "" +"URL pubblico dell'API.\n" +"Questo URL viene utilizzato nell'imitazione per impostare il cookie sul " +"dominio API corretto se si utilizza un reverse proxy per servire l'API.\n" +"Il valore predefinito è public_url se non impostato, oppure l'URL di Odoo se " +"non impostato." #. module: fastapi_auth_partner #: model:ir.model.fields,help:fastapi_auth_partner.field_fastapi_endpoint__public_url @@ -238,21 +245,26 @@ msgid "" "This URL is used for the impersonation final redirect. And can also be used in the mail template to construct links.\n" "Default to the public_api_url if not set or the odoo url if not set either." msgstr "" +"URL pubblico del sito.\n" +"Questo URL viene utilizzato per il reindirizzamento finale dell'imitazione. " +"Può anche essere utilizzato nel modello di posta per creare link.\n" +"Impostato di default su public_api_url se non impostato, oppure su Odoo URL " +"se non impostato." #. module: fastapi_auth_partner #: model:ir.model.fields,help:fastapi_auth_partner.field_auth_directory__cookie_secret_key msgid "The secret key used to sign the cookie" -msgstr "" +msgstr "La chiave segreta usata per firmare il cookie" #. module: fastapi_auth_partner #: model:ir.model,name:fastapi_auth_partner.model_wizard_auth_partner_impersonate msgid "Wizard Partner Auth Impersonate" -msgstr "" +msgstr "Procedura guidata imitazione autorizzazione partner" #. module: fastapi_auth_partner #: model:ir.model,name:fastapi_auth_partner.model_wizard_auth_partner_reset_password msgid "Wizard Partner Auth Reset Password" -msgstr "" +msgstr "Procedura guidata reset password autorizzazione partner" #. module: fastapi_auth_partner #. odoo-python @@ -262,3 +274,5 @@ msgid "" "You are now impersonating %s\n" "%%s" msgstr "" +"Osa si sta imitando %s\n" +"%%s" From a0da317bf09ac7efad0c7e9055d6dbcbb6703012 Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 4 Jun 2025 14:07:22 +0000 Subject: [PATCH 40/58] Translated using Weblate (Italian) Currently translated at 100.0% (19 of 19 strings) Translation: rest-framework-16.0/rest-framework-16.0-fastapi_encrypted_errors Translate-URL: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi_encrypted_errors/it/ --- fastapi_encrypted_errors/i18n/it.po | 42 ++++++++++++++++------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/fastapi_encrypted_errors/i18n/it.po b/fastapi_encrypted_errors/i18n/it.po index a1fd0db53..1dea61661 100644 --- a/fastapi_encrypted_errors/i18n/it.po +++ b/fastapi_encrypted_errors/i18n/it.po @@ -6,109 +6,113 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2025-06-04 16:26+0000\n" +"Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" #. module: fastapi_encrypted_errors #: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_view_form msgid "Close" -msgstr "" +msgstr "Chiudi" #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__create_uid msgid "Created by" -msgstr "" +msgstr "Creato da" #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__create_date msgid "Created on" -msgstr "" +msgstr "Creato il" #. module: fastapi_encrypted_errors #: model:ir.actions.act_window,name:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_action_decrypt_error #: model:ir.ui.menu,name:fastapi_encrypted_errors.menu_fastapi_decrypt_errors #: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_view_form msgid "Decrypt Error" -msgstr "" +msgstr "Decifra errore" #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__decrypted_error msgid "Decrypted Error" -msgstr "" +msgstr "Errore decifrato" #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__display_name msgid "Display Name" -msgstr "" +msgstr "Nome visualizzato" #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_fastapi_endpoint__encrypt_errors msgid "Encrypt Errors" -msgstr "" +msgstr "Errori cifrati" #. module: fastapi_encrypted_errors #: model:ir.model.fields,help:fastapi_encrypted_errors.field_fastapi_endpoint__encrypt_errors msgid "Encrypt errors before sending them to the client." -msgstr "" +msgstr "Cifra gli errori prima di inviarli al client." #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_fastapi_endpoint__encrypted_errors_secret_key msgid "Encrypted Errors Secret Key" -msgstr "" +msgstr "Chiave segreta errori cifrati" #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__error msgid "Error" -msgstr "" +msgstr "Errore" #. module: fastapi_encrypted_errors #: model:ir.model,name:fastapi_encrypted_errors.model_fastapi_endpoint #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__fastapi_endpoint_id msgid "FastAPI Endpoint" -msgstr "" +msgstr "Endpoint FastAPI" #. module: fastapi_encrypted_errors #: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.fastapi_endpoint_form_view msgid "Generate Secret Key" -msgstr "" +msgstr "Genera chiave segreta" #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__id msgid "ID" -msgstr "" +msgstr "ID" #. module: fastapi_encrypted_errors #: model_terms:ir.ui.view,arch_db:fastapi_encrypted_errors.wizard_fastapi_decrypt_errors_view_form msgid "Label" -msgstr "" +msgstr "Etichetta" #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors____last_update msgid "Last Modified on" -msgstr "" +msgstr "Ultima modifica il" #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Ultimo aggiornamento di" #. module: fastapi_encrypted_errors #: model:ir.model.fields,field_description:fastapi_encrypted_errors.field_wizard_fastapi_decrypt_errors__write_date msgid "Last Updated on" -msgstr "" +msgstr "Ultimo aggiornamento il" #. module: fastapi_encrypted_errors #: model:ir.model.fields,help:fastapi_encrypted_errors.field_fastapi_endpoint__encrypted_errors_secret_key msgid "" "The secret key used to encrypt errors before sending them to the client." msgstr "" +"La chiave segreta utilizzata per cifrare gli errori prima di inviarli al " +"client." #. module: fastapi_encrypted_errors #: model:ir.model,name:fastapi_encrypted_errors.model_wizard_fastapi_decrypt_errors msgid "Wizard to decrypt FastAPI errors" -msgstr "" +msgstr "Procedura guidata per decifrare gli errori FastAPI" From 4926f48470e833f31cc242a9fa66b764e6b72099 Mon Sep 17 00:00:00 2001 From: PicchiSeba Date: Mon, 5 May 2025 14:51:21 +0200 Subject: [PATCH 41/58] [IMP]fastapi: enable multi-slash routes --- fastapi/demo/fastapi_endpoint_demo.xml | 11 +++++++++++ fastapi/models/fastapi_endpoint.py | 13 +++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/fastapi/demo/fastapi_endpoint_demo.xml b/fastapi/demo/fastapi_endpoint_demo.xml index a1c34e34b..7016b807c 100644 --- a/fastapi/demo/fastapi_endpoint_demo.xml +++ b/fastapi/demo/fastapi_endpoint_demo.xml @@ -44,4 +44,15 @@ methods. See documentation to learn more about how to create a new app. http_basic + + + Fastapi Multi-Slash Demo Endpoint + + Like the other demo endpoint but with multi-slash + + demo + /fastapi/demo + http_basic + + diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 45dd13319..9f3b256c4 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -213,8 +213,17 @@ def _reset_app_cache_marker(self): @api.model @tools.ormcache("path") def get_endpoint(self, path): - root_path = "/" + path.split("/")[1] - endpoint = self.search([("root_path", "=", root_path)])[:1] or False + # try to match the request url with the most similar endpoint + endpoints_by_length = self.search([]).sorted( + lambda fe: len(fe.root_path), reverse=True + ) + endpoint = False + while endpoints_by_length: + candidate_endpoint = endpoints_by_length[0] + if path.startswith(candidate_endpoint.root_path): + endpoint = candidate_endpoint + break + endpoints_by_length -= candidate_endpoint return endpoint @api.model From 55adbd2ad17db342efee51b2b8b119bddd27c1f3 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Wed, 14 May 2025 15:23:59 +0200 Subject: [PATCH 42/58] [IMP] fastapi: Ensures endpoint matching is based on path parts When we lookup the fastapi.endpoint to use to process a request at a given path, we must ensure that the matching mechanism on the path and the endpoint root_path is bases on the path parts. e.g. /a/b is not prefix of /a/bc. In the same time improves robustness of the matching mechanism and avoid useless SQL queries --- fastapi/models/fastapi_endpoint.py | 79 ++++++++++++++++++++++++------ fastapi/tests/test_fastapi.py | 20 ++++++++ 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/fastapi/models/fastapi_endpoint.py b/fastapi/models/fastapi_endpoint.py index 9f3b256c4..93130c69d 100644 --- a/fastapi/models/fastapi_endpoint.py +++ b/fastapi/models/fastapi_endpoint.py @@ -198,6 +198,8 @@ def _endpoint_registry_route_unique_key(self, routing: Dict[str, Any]): return f"{self._name}:{self.id}:{path}" def _reset_app(self): + self._get_id_by_root_path_map.clear_cache(self) + self._get_id_for_path.clear_cache(self) self._reset_app_cache_marker.clear_cache(self) @tools.ormcache() @@ -211,24 +213,71 @@ def _reset_app_cache_marker(self): """ @api.model - @tools.ormcache("path") - def get_endpoint(self, path): - # try to match the request url with the most similar endpoint - endpoints_by_length = self.search([]).sorted( - lambda fe: len(fe.root_path), reverse=True + def _normalize_url_path(self, path) -> str: + """ + Normalize a URL path: + * Remove redundant slashes, + * Remove trailing slash (unless it's the root), + * Lowercase for case-insensitive matching + """ + parts = [part.lower() for part in path.strip().split("/") if part] + return "/" + "/".join(parts) + + @api.model + def _is_suburl(self, path, prefix) -> bool: + """ + Check if 'path' is a subpath of 'prefix' in URL logic: + * Must start with the prefix followed by a slash + This will ensure that the matching is done one the path + parts and ensures that e.g. /a/b is not prefix of /a/bc. + """ + path = self._normalize_url_path(path) + prefix = self._normalize_url_path(prefix) + + if path == prefix: + return True + if path.startswith(prefix + "/"): + return True + return False + + @api.model + def _find_first_matching_url_path(self, paths, prefix) -> str | None: + """ + Return the first path that is a subpath of 'prefix', + ordered by longest URL path first (most number of segments). + """ + # Sort by number of segments (shallowest first) + sorted_paths = sorted( + paths, + key=lambda p: len(self._normalize_url_path(p).split("/")), + reverse=True, ) - endpoint = False - while endpoints_by_length: - candidate_endpoint = endpoints_by_length[0] - if path.startswith(candidate_endpoint.root_path): - endpoint = candidate_endpoint - break - endpoints_by_length -= candidate_endpoint - return endpoint + + for path in sorted_paths: + if self._is_suburl(prefix, path): + return path + return None + + @api.model + @tools.ormcache() + def _get_id_by_root_path_map(self): + return {r.root_path: r.id for r in self.search([])} + + @api.model + @tools.ormcache("path") + def _get_id_for_path(self, path): + id_by_path = self._get_id_by_root_path_map() + root_path = self._find_first_matching_url_path(id_by_path.keys(), path) + return id_by_path.get(root_path) + + @api.model + def _get_endpoint(self, path): + id_ = self._get_id_for_path(path) + return self.browse(id_) if id_ else None @api.model def get_app(self, path): - record = self.get_endpoint(path) + record = self._get_endpoint(path) if not record: return None app = FastAPI() @@ -254,7 +303,7 @@ def _clear_fastapi_exception_handlers(self, app: FastAPI) -> None: @api.model @tools.ormcache("path") def get_uid(self, path): - record = self.get_endpoint(path) + record = self._get_endpoint(path) if not record: return None return record.user_id.id diff --git a/fastapi/tests/test_fastapi.py b/fastapi/tests/test_fastapi.py index 37b11a961..91d4c9063 100644 --- a/fastapi/tests/test_fastapi.py +++ b/fastapi/tests/test_fastapi.py @@ -169,3 +169,23 @@ def test_no_commit_on_exception(self) -> None: expected_message="test", expected_status_code=status.HTTP_409_CONFLICT, ) + + def test_url_matching(self): + # Test the URL mathing method on the endpoint + paths = ["/fastapi", "/fastapi_demo", "/fastapi/v1"] + EndPoint = self.env["fastapi.endpoint"] + self.assertEqual( + EndPoint._find_first_matching_url_path(paths, "/fastapi_demo/test"), + "/fastapi_demo", + ) + self.assertEqual( + EndPoint._find_first_matching_url_path(paths, "/fastapi/test"), "/fastapi" + ) + self.assertEqual( + EndPoint._find_first_matching_url_path(paths, "/fastapi/v2/test"), + "/fastapi", + ) + self.assertEqual( + EndPoint._find_first_matching_url_path(paths, "/fastapi/v1/test"), + "/fastapi/v1", + ) From 666a6b1d9b3d9a89f905c38890cacdb16956d0ef Mon Sep 17 00:00:00 2001 From: PicchiSeba Date: Wed, 11 Jun 2025 09:13:59 +0200 Subject: [PATCH 43/58] [IMP]fastapi: add test for multi-slash scenario --- fastapi/demo/fastapi_endpoint_demo.xml | 2 +- fastapi/tests/test_fastapi.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/fastapi/demo/fastapi_endpoint_demo.xml b/fastapi/demo/fastapi_endpoint_demo.xml index 7016b807c..ad3fd9da0 100644 --- a/fastapi/demo/fastapi_endpoint_demo.xml +++ b/fastapi/demo/fastapi_endpoint_demo.xml @@ -51,7 +51,7 @@ methods. See documentation to learn more about how to create a new app. Like the other demo endpoint but with multi-slash demo - /fastapi/demo + /fastapi/demo-multi http_basic diff --git a/fastapi/tests/test_fastapi.py b/fastapi/tests/test_fastapi.py index 91d4c9063..fbda932fb 100644 --- a/fastapi/tests/test_fastapi.py +++ b/fastapi/tests/test_fastapi.py @@ -20,7 +20,11 @@ class FastAPIHttpCase(HttpCase): def setUpClass(cls): super().setUpClass() cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") - cls.fastapi_demo_app._handle_registry_sync() + cls.fastapi_multi_demo_app = cls.env.ref( + "fastapi.fastapi_endpoint_multislash_demo" + ) + cls.fastapi_apps = cls.fastapi_demo_app + cls.fastapi_multi_demo_app + cls.fastapi_apps._handle_registry_sync() lang = ( cls.env["res.lang"] .with_context(active_test=False) @@ -189,3 +193,9 @@ def test_url_matching(self): EndPoint._find_first_matching_url_path(paths, "/fastapi/v1/test"), "/fastapi/v1", ) + + def test_multi_slash(self): + route = "/fastapi/demo-multi/demo/" + response = self.url_open(route, timeout=20) + self.assertEqual(response.status_code, 200) + self.assertIn(self.fastapi_multi_demo_app.root_path, str(response.url)) From bd4b5de10ff5b93a65a1c293adff79756283cc79 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Wed, 11 Jun 2025 17:31:42 +0200 Subject: [PATCH 44/58] [FIX] fastapi: Flush before exiting Fastapi environment As the fastapi dispatcher is altering the public request's environment before calling the endpoint, it will reuse the same DB cursor from this environment. Therefore, if fields are being marked to be recomputed during the execution of the fastapi endpoint, these will not be recomputed until the cursor is being commited, what happens when we are not anymore authenticated through as fastapi user. This means the recomputation of computed fields will be executed with the public user, what could potentially lead to inconsistencies or result in an AccessError. By calling flush before exiting the contextmanager yielding the altered Odoo environment, we ensure all the pending computations are executed while being authenticated with the fastapi user which must have more access rights than the public user. --- fastapi/fastapi_dispatcher.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fastapi/fastapi_dispatcher.py b/fastapi/fastapi_dispatcher.py index 0ded74c3d..bc64898d7 100644 --- a/fastapi/fastapi_dispatcher.py +++ b/fastapi/fastapi_dispatcher.py @@ -115,5 +115,10 @@ def _manage_odoo_env(self, uid=None): token = odoo_env_ctx.set(env) try: yield + # Flush here to ensure all pending computations are being executed with + # authenticated fastapi user before exiting this context manager, as it + # would otherwise be done using the public user on the commit of the DB + # cursor, what could potentially lead to inconsistencies or AccessError. + env.flush_all() finally: odoo_env_ctx.reset(token) From 4a640271d36de0272ccc2a13fd4acb51c9b3364c Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 16 Jun 2025 12:28:10 +0000 Subject: [PATCH 45/58] [BOT] post-merge updates --- README.md | 2 +- fastapi/README.rst | 2 +- fastapi/__manifest__.py | 2 +- fastapi/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1ef824e95..df50277fa 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ addon | version | maintainers | summary [datamodel](datamodel/) | 16.0.1.0.2 | lmignon | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | lmignon | Extendable classes registry loader for Odoo [extendable_fastapi](extendable_fastapi/) | 16.0.2.1.2 | lmignon | Allows the use of extendable into fastapi apps -[fastapi](fastapi/) | 16.0.1.6.1 | lmignon | Odoo FastAPI endpoint +[fastapi](fastapi/) | 16.0.1.6.2 | lmignon | Odoo FastAPI endpoint [fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | sbidoul | JWT bearer token authentication for FastAPI. [fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | sbidoul | Test/demo module for fastapi_auth_jwt. [fastapi_auth_partner](fastapi_auth_partner/) | 16.0.1.0.0 | | This provides an implementation of auth_partner for FastAPI diff --git a/fastapi/README.rst b/fastapi/README.rst index 417ab2d2f..04b741b38 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -11,7 +11,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:3407249de169bb59ec2b991f1a1eff6e935d6a372e06d1ad27588cb7d9fad1f0 + !! source digest: sha256:f7d5e583b51b78474647a09501cec5d4efa9cf7c1a746837bb2e4e99a4601566 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index 9339cac8d..99d428d6c 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.6.1", + "version": "16.0.1.6.2", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index 1acafecb0..47e60fc8d 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -372,7 +372,7 @@

    Odoo FastAPI

    !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:3407249de169bb59ec2b991f1a1eff6e935d6a372e06d1ad27588cb7d9fad1f0 +!! source digest: sha256:f7d5e583b51b78474647a09501cec5d4efa9cf7c1a746837bb2e4e99a4601566 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI From 6ab603d441d8368d7385ebc8d5f22b010a39a347 Mon Sep 17 00:00:00 2001 From: sygel Date: Mon, 16 Jun 2025 13:24:17 +0200 Subject: [PATCH 46/58] [FIX] fastapi: Running User Rule Domain --- fastapi/security/ir_rule+acl.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/security/ir_rule+acl.xml b/fastapi/security/ir_rule+acl.xml index 592a4646f..76640c423 100644 --- a/fastapi/security/ir_rule+acl.xml +++ b/fastapi/security/ir_rule+acl.xml @@ -39,7 +39,7 @@ ['|', ('user_id', '=', user.id), ('id', '=', authenticated_partner_id)] + > ['|', ('user_ids', '=', user.id), ('id', '=', authenticated_partner_id)] From c91304c4f06eac89e71140352a449626796eb25b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 19 Jun 2025 12:19:12 +0000 Subject: [PATCH 47/58] [BOT] post-merge updates --- README.md | 2 +- fastapi/README.rst | 2 +- fastapi/__manifest__.py | 2 +- fastapi/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index df50277fa..8995005b9 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ addon | version | maintainers | summary [datamodel](datamodel/) | 16.0.1.0.2 | lmignon | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | lmignon | Extendable classes registry loader for Odoo [extendable_fastapi](extendable_fastapi/) | 16.0.2.1.2 | lmignon | Allows the use of extendable into fastapi apps -[fastapi](fastapi/) | 16.0.1.6.2 | lmignon | Odoo FastAPI endpoint +[fastapi](fastapi/) | 16.0.1.6.3 | lmignon | Odoo FastAPI endpoint [fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | sbidoul | JWT bearer token authentication for FastAPI. [fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | sbidoul | Test/demo module for fastapi_auth_jwt. [fastapi_auth_partner](fastapi_auth_partner/) | 16.0.1.0.0 | | This provides an implementation of auth_partner for FastAPI diff --git a/fastapi/README.rst b/fastapi/README.rst index 04b741b38..1f7d2a90b 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -11,7 +11,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:f7d5e583b51b78474647a09501cec5d4efa9cf7c1a746837bb2e4e99a4601566 + !! source digest: sha256:f832fd7de3263bedeb0b99bfd1dbf1a59f50b2865fe87a9b66a98dc3d6500aeb !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index 99d428d6c..3b75a08d5 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.6.2", + "version": "16.0.1.6.3", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index 47e60fc8d..67343e88f 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -372,7 +372,7 @@

    Odoo FastAPI

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

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI From 6f480bf1de5160e4d5881388d1672f9d664a7fa1 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 25 Jun 2025 08:59:09 +0000 Subject: [PATCH 48/58] [BOT] post-merge updates --- README.md | 2 +- fastapi/README.rst | 2 +- fastapi/__manifest__.py | 2 +- fastapi/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8995005b9..abe55b5a9 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ addon | version | maintainers | summary [datamodel](datamodel/) | 16.0.1.0.2 | lmignon | This addon allows you to define simple data models supporting serialization/deserialization [extendable](extendable/) | 16.0.1.0.2 | lmignon | Extendable classes registry loader for Odoo [extendable_fastapi](extendable_fastapi/) | 16.0.2.1.2 | lmignon | Allows the use of extendable into fastapi apps -[fastapi](fastapi/) | 16.0.1.6.3 | lmignon | Odoo FastAPI endpoint +[fastapi](fastapi/) | 16.0.1.7.0 | lmignon | Odoo FastAPI endpoint [fastapi_auth_jwt](fastapi_auth_jwt/) | 16.0.1.0.4 | sbidoul | JWT bearer token authentication for FastAPI. [fastapi_auth_jwt_demo](fastapi_auth_jwt_demo/) | 16.0.2.0.1 | sbidoul | Test/demo module for fastapi_auth_jwt. [fastapi_auth_partner](fastapi_auth_partner/) | 16.0.1.0.0 | | This provides an implementation of auth_partner for FastAPI diff --git a/fastapi/README.rst b/fastapi/README.rst index 1f7d2a90b..b30166c11 100644 --- a/fastapi/README.rst +++ b/fastapi/README.rst @@ -11,7 +11,7 @@ Odoo FastAPI !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:f832fd7de3263bedeb0b99bfd1dbf1a59f50b2865fe87a9b66a98dc3d6500aeb + !! source digest: sha256:d7b9919d3058c69a37cd990e0d0a3e4b0fa55d146ab2713f8834e4833313ddd7 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/fastapi/__manifest__.py b/fastapi/__manifest__.py index 3b75a08d5..3f0c5d810 100644 --- a/fastapi/__manifest__.py +++ b/fastapi/__manifest__.py @@ -5,7 +5,7 @@ "name": "Odoo FastAPI", "summary": """ Odoo FastAPI endpoint""", - "version": "16.0.1.6.3", + "version": "16.0.1.7.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/fastapi/static/description/index.html b/fastapi/static/description/index.html index 67343e88f..704315102 100644 --- a/fastapi/static/description/index.html +++ b/fastapi/static/description/index.html @@ -372,7 +372,7 @@

    Odoo FastAPI

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

    Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    This addon provides the basis to smoothly integrate the FastAPI From 7afbd96f2d8a3ce60a26b108818be37feecd86ef Mon Sep 17 00:00:00 2001 From: mymage Date: Wed, 25 Jun 2025 07:16:15 +0000 Subject: [PATCH 49/58] Translated using Weblate (Italian) Currently translated at 100.0% (90 of 90 strings) Translation: rest-framework-16.0/rest-framework-16.0-auth_partner Translate-URL: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-auth_partner/it/ --- auth_partner/i18n/it.po | 209 ++++++++++++++++++++++++---------------- 1 file changed, 126 insertions(+), 83 deletions(-) diff --git a/auth_partner/i18n/it.po b/auth_partner/i18n/it.po index 1800041ef..a67f0f833 100644 --- a/auth_partner/i18n/it.po +++ b/auth_partner/i18n/it.po @@ -6,33 +6,35 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2025-06-25 09:25+0000\n" +"Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" #. module: auth_partner #: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__14-days msgid "14 Days" -msgstr "" +msgstr "14 Giorni" #. module: auth_partner #: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__2-days msgid "2-days" -msgstr "" +msgstr "2 Giorni" #. module: auth_partner #: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__6-hours msgid "6 Hours" -msgstr "" +msgstr "6 Ore" #. module: auth_partner #: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__7-days msgid "7 Days" -msgstr "" +msgstr "7 Giorni" #. module: auth_partner #: model:mail.template,body_html:auth_partner.email_reset_password @@ -44,6 +46,15 @@ msgid "" " \n" " " msgstr "" +"

    \n" +" Salve \n" +" fare clic sul seguente collegamento per resettare la passwrod\n" +" Resetta " +"password\n" +"
    \n" +" " #. module: auth_partner #: model:mail.template,body_html:auth_partner.email_validate_email @@ -55,6 +66,16 @@ msgid "" " \n" " " msgstr "" +"
    \n" +" Salve \n" +" Benvenuto sul sito, clicca sul seguente collegamento per " +"verificare la tua e-mail\n" +" Valida " +"e-mail\n" +"
    \n" +" " #. module: auth_partner #: model:mail.template,body_html:auth_partner.email_set_password @@ -67,22 +88,32 @@ msgid "" " \n" " " msgstr "" +"
    \n" +" Salve \n" +" Benvenuto, il tuo account è stato creato\n" +" Clicca sul collegamento seguente per impostare la tua password\n" +" Imposta " +"password\n" +"
    \n" +" " #. module: auth_partner #: model:res.groups,name:auth_partner.group_auth_partner_api msgid "API Partner Auth Access" -msgstr "" +msgstr "Autorizzazione accesso API del partner" #. module: auth_partner #: model:res.groups,name:auth_partner.group_auth_partner_manager msgid "API Partner Auth Manager" -msgstr "" +msgstr "Responsabile autorizzazione API del partner" #. module: auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form #: model_terms:ir.ui.view,arch_db:auth_partner.view_partner_form msgid "Account" -msgstr "" +msgstr "Conto" #. module: auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form @@ -90,65 +121,67 @@ msgid "" "An email will be send with a token to each customer, you can specify the " "date until the link is valid" msgstr "" +"Verrà inviata una e-mail con un token ad ogni cliente, si può indicare la " +"data entro cui il collegamento è valido" #. module: auth_partner #: model:ir.model,name:auth_partner.model_auth_directory msgid "Auth Directory" -msgstr "" +msgstr "Cartella autorizzazione" #. module: auth_partner #: model:mail.template,name:auth_partner.email_reset_password msgid "Auth Directory: Reset Password" -msgstr "" +msgstr "Cartella autorizzazione: reimposta password" #. module: auth_partner #: model:mail.template,name:auth_partner.email_set_password msgid "Auth Directory: Set Password" -msgstr "" +msgstr "Cartella autorizzazione: imposta password" #. module: auth_partner #: model:mail.template,name:auth_partner.email_validate_email msgid "Auth Directory: Validate Email" -msgstr "" +msgstr "Cartella autorizzazione: valida e-mail" #. module: auth_partner #: model:ir.model,name:auth_partner.model_auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form #: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search msgid "Auth Partner" -msgstr "" +msgstr "Partner autorizzazione" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_count #: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_count msgid "Auth Partner Count" -msgstr "" +msgstr "Conteggio partner autorizzazione" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__auth_partner_ids msgid "Auth Partners" -msgstr "" +msgstr "Partner autorizzazione" #. module: auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form #: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form msgid "Cancel" -msgstr "" +msgstr "Annulla" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password_confirm msgid "Confirm Password" -msgstr "" +msgstr "Conferma password" #. module: auth_partner #: model:ir.model,name:auth_partner.model_res_partner msgid "Contact" -msgstr "" +msgstr "Contatto" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__count_partner msgid "Count Partner" -msgstr "" +msgstr "Conteggio partner" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_uid @@ -156,7 +189,7 @@ msgstr "" #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_uid #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_uid msgid "Created by" -msgstr "" +msgstr "Creato da" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__create_date @@ -164,47 +197,47 @@ msgstr "" #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__create_date #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__create_date msgid "Created on" -msgstr "" +msgstr "Creato il" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_impersonation msgid "Date Last Impersonation" -msgstr "" +msgstr "Data ultima imitazione" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_request_reset_pwd msgid "Date Last Request Reset Pwd" -msgstr "" +msgstr "Data ultima richiesta reimpostazione password" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd msgid "Date Last Sucessfull Reset Pwd" -msgstr "" +msgstr "Data ultima reimpostazione password riuscita" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__date_validity msgid "Date Validity" -msgstr "" +msgstr "Validità data" #. module: auth_partner #: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_request_reset_pwd msgid "Date of the last password reset request" -msgstr "" +msgstr "Data ultima richiesta reimpostazione password" #. module: auth_partner #: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_impersonation msgid "Date of the last sucessfull impersonation" -msgstr "" +msgstr "Data ultima imitazione riuscita" #. module: auth_partner #: model:ir.model.fields,help:auth_partner.field_auth_partner__date_last_sucessfull_reset_pwd msgid "Date of the last sucessfull password reset" -msgstr "" +msgstr "Data ultima reimpostazione password riuscita" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__delay msgid "Delay" -msgstr "" +msgstr "Ritardo" #. module: auth_partner #: model:ir.actions.act_window,name:auth_partner.auth_directory_action @@ -212,13 +245,13 @@ msgstr "" #: model:ir.ui.menu,name:auth_partner.auth_directory_menu #: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search msgid "Directory" -msgstr "" +msgstr "Cartella" #. module: auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form #: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_search msgid "Directory Auth" -msgstr "" +msgstr "Autorizzazione cartella" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__display_name @@ -226,7 +259,7 @@ msgstr "" #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__display_name #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__display_name msgid "Display Name" -msgstr "" +msgstr "Nome visualizzato" #. module: auth_partner #. odoo-python @@ -236,21 +269,24 @@ msgid "" "Email address not validated. Validate your email address by clicking on the " "link in the email sent to you or request a new password. " msgstr "" +"Indirizzo e-mail non validato. Validare il proprio indirizzo e-mail facendo " +"click sul collegamento nella e-mail inviata per la richiesta di una nuova " +"password. " #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__encrypted_password msgid "Encrypted Password" -msgstr "" +msgstr "Password criptata" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__force_verified_email msgid "Force Verified Email" -msgstr "" +msgstr "Forza e-mail verificata" #. module: auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_search msgid "Group By" -msgstr "" +msgstr "Raggruppa per" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__id @@ -258,45 +294,46 @@ msgstr "" #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__id #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__id msgid "ID" -msgstr "" +msgstr "ID" #. module: auth_partner #: model:ir.model.fields,help:auth_partner.field_auth_directory__force_verified_email msgid "If checked, email must be verified to be able to log in" msgstr "" +"Se selezionata, l'e-mail deve essere verificata per consentire l'accesso" #. module: auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form msgid "Impersonate" -msgstr "" +msgstr "Imita" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_token_duration msgid "Impersonating Token Duration" -msgstr "" +msgstr "Durata token imitazione" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__impersonating_user_ids #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__impersonating_user_ids msgid "Impersonating Users" -msgstr "" +msgstr "Utenti imitazione" #. module: auth_partner #: model:ir.model.fields,help:auth_partner.field_auth_directory__set_password_token_duration msgid "In minute, default 1440 minutes => 24h" -msgstr "" +msgstr "In minuti,predefinito 1440 minuti => 24 ore" #. module: auth_partner #: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_token_duration msgid "In seconds, default 60 seconds" -msgstr "" +msgstr "In secondi, predefinito 60 secondi" #. module: auth_partner #. odoo-python #: code:addons/auth_partner/models/auth_partner.py:0 #, python-format msgid "Invalid Login or Password" -msgstr "" +msgstr "Nome o password errati" #. module: auth_partner #. odoo-python @@ -305,20 +342,20 @@ msgstr "" #: code:addons/auth_partner/models/auth_directory.py:0 #, python-format msgid "Invalid Token" -msgstr "" +msgstr "Token non valido" #. module: auth_partner #. odoo-python #: code:addons/auth_partner/models/auth_directory.py:0 #, python-format msgid "Invalid token" -msgstr "" +msgstr "Token non valido" #. module: auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form #: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form msgid "Label" -msgstr "" +msgstr "Etichetta" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory____last_update @@ -326,7 +363,7 @@ msgstr "" #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password____last_update #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password____last_update msgid "Last Modified on" -msgstr "" +msgstr "Ultima modifica il" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_uid @@ -334,7 +371,7 @@ msgstr "" #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_uid #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Ultimo aggiornamento di" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__write_date @@ -342,71 +379,71 @@ msgstr "" #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__write_date #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__write_date msgid "Last Updated on" -msgstr "" +msgstr "Ultimo aggiornamento il" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__login msgid "Login" -msgstr "" +msgstr "Login" #. module: auth_partner #: model:ir.model.constraint,message:auth_partner.constraint_auth_partner_directory_login_uniq msgid "Login must be uniq per directory !" -msgstr "" +msgstr "La login deve essere univoca per cartella!" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_reset_password__template_id msgid "Mail Template" -msgstr "" +msgstr "Modello e-mail" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__reset_password_template_id msgid "Mail Template Forget Password" -msgstr "" +msgstr "Modello e-mail password dimenticata" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_template_id msgid "Mail Template New Password" -msgstr "" +msgstr "Modello e-mail nuova password" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__validate_email_template_id msgid "Mail Template Validate Email" -msgstr "" +msgstr "Modello e-mail validazione e-mail" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__mail_verified msgid "Mail Verified" -msgstr "" +msgstr "E-mail verificata" #. module: auth_partner #: model:ir.model.fields.selection,name:auth_partner.selection__wizard_auth_partner_reset_password__delay__manually msgid "Manually" -msgstr "" +msgstr "Manualmente" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__name msgid "Name" -msgstr "" +msgstr "Nome" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__nbr_pending_reset_sent msgid "Nbr Pending Reset Sent" -msgstr "" +msgstr "N° reset inviati in attesa" #. module: auth_partner #. odoo-python #: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 #, python-format msgid "No active_id in context" -msgstr "" +msgstr "Manca active_id nel context" #. module: auth_partner #. odoo-python #: code:addons/auth_partner/models/auth_directory.py:0 #, python-format msgid "No email template defined for %(template)s in %(directory)s" -msgstr "" +msgstr "Modello e-mail non definito per %(template)s in %(directory)s" #. module: auth_partner #: model:ir.model.fields,help:auth_partner.field_auth_partner__nbr_pending_reset_sent @@ -416,105 +453,109 @@ msgid "" "their password and you senddifferent mail depending on the number of " "reminder" msgstr "" +"Numero di reimpostazioni in sospeso inviate dal cliente. Questo campo è " +"utile quando, dopo una migrazione da un altro sistema, si chiede a tutti i " +"tuoi clienti di reimpostare la propria password e si inviano e-mail diverse " +"a seconda del numero di promemoria" #. module: auth_partner #: model:ir.actions.act_window,name:auth_partner.auth_partner_action #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__partner_id #: model:ir.ui.menu,name:auth_partner.auth_partner_menu msgid "Partner" -msgstr "" +msgstr "Partner" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_res_partner__auth_partner_ids #: model:ir.model.fields,field_description:auth_partner.field_res_users__auth_partner_ids msgid "Partner Auth" -msgstr "" +msgstr "Autorizzazione partner" #. module: auth_partner #: model:ir.ui.menu,name:auth_partner.auth msgid "Partner Authentication" -msgstr "" +msgstr "Autenticazione partner" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__password #: model:ir.model.fields,field_description:auth_partner.field_wizard_auth_partner_force_set_password__password msgid "Password" -msgstr "" +msgstr "Password" #. module: auth_partner #. odoo-python #: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 #, python-format msgid "Password and Confirm Password must be the same" -msgstr "" +msgstr "La password e la conferma devono essere uguali" #. module: auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.auth_directory_view_form msgid "Regenerate secret key" -msgstr "" +msgstr "Rigenera chiave segreta" #. module: auth_partner #: model:mail.template,subject:auth_partner.email_reset_password msgid "Reset Password" -msgstr "" +msgstr "Resetta password" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key msgid "Secret Key" -msgstr "" +msgstr "Chiave segreta" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_default msgid "Secret Key Env Default" -msgstr "" +msgstr "Chiave segreta ambiente predefinita" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__secret_key_env_is_editable msgid "Secret Key Env Is Editable" -msgstr "" +msgstr "La chiave segreta è modificabile" #. module: auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form msgid "Send Invite" -msgstr "" +msgstr "Invia invito" #. module: auth_partner #: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_reset_password_view_form msgid "Send Reset Password" -msgstr "" +msgstr "Invia reset password" #. module: auth_partner #: model:ir.actions.act_window,name:auth_partner.auth_partner_action_reset_password msgid "Send Reset Password Instruction" -msgstr "" +msgstr "Invia istruzioni reset password" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__server_env_defaults msgid "Server Env Defaults" -msgstr "" +msgstr "Predefiniti ambiente server" #. module: auth_partner #: model:ir.actions.act_window,name:auth_partner.auth_partner_force_set_password_action #: model_terms:ir.ui.view,arch_db:auth_partner.auth_partner_view_form #: model_terms:ir.ui.view,arch_db:auth_partner.wizard_auth_partner_force_set_password_view_form msgid "Set Password" -msgstr "" +msgstr "Imposta password" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_directory__set_password_token_duration msgid "Set Password Token Duration" -msgstr "" +msgstr "Durata token impostazione password" #. module: auth_partner #: model:ir.model.fields,help:auth_partner.field_auth_partner__user_can_impersonate msgid "Technical field to check if the user can impersonate" -msgstr "" +msgstr "Campo tecnico per controllare se l'utente può imitare" #. module: auth_partner #: model:ir.model.fields,help:auth_partner.field_auth_directory__impersonating_user_ids #: model:ir.model.fields,help:auth_partner.field_auth_partner__impersonating_user_ids msgid "These odoo users can impersonate any partner of this directory" -msgstr "" +msgstr "Questi utenti Odoo possono imitare qualsiasi partner in questa cartella" #. module: auth_partner #: model:ir.model.fields,help:auth_partner.field_auth_partner__mail_verified @@ -522,34 +563,36 @@ msgid "" "This field is set to True when the user has clicked on the link sent by " "email" msgstr "" +"Questo campo è impostato a true quando l'utente ha cliccato nel collegamento " +"inviato per e-mail" #. module: auth_partner #. odoo-python #: code:addons/auth_partner/wizards/wizard_auth_partner_force_set_password.py:0 #, python-format msgid "This wizard can only be used on auth.partner" -msgstr "" +msgstr "Questa procedura guidata può essere usata solo su auth.partner" #. module: auth_partner #: model:ir.model.fields,field_description:auth_partner.field_auth_partner__user_can_impersonate msgid "User Can Impersonate" -msgstr "" +msgstr "L'utente può imitare" #. module: auth_partner #: model:mail.template,subject:auth_partner.email_set_password #: model:mail.template,subject:auth_partner.email_validate_email msgid "Welcome" -msgstr "" +msgstr "Benvenuto" #. module: auth_partner #: model:ir.model,name:auth_partner.model_wizard_auth_partner_force_set_password #: model:ir.model,name:auth_partner.model_wizard_auth_partner_reset_password msgid "Wizard Partner Auth Reset Password" -msgstr "" +msgstr "Procedura guidata reset password autorizzazione partner" #. module: auth_partner #. odoo-python #: code:addons/auth_partner/models/auth_partner.py:0 #, python-format msgid "You are not allowed to impersonate this user" -msgstr "" +msgstr "Non si è autorizzati a imitare questo utente" From 1d4d74e6cc3c215afdc5154592f34904cd093e83 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 15 Jul 2025 10:33:50 +0200 Subject: [PATCH 50/58] [ADD] fastapi_log --- fastapi_log/README.rst | 99 +++++ fastapi_log/__init__.py | 2 + fastapi_log/__manifest__.py | 23 + fastapi_log/fastapi_dispatcher.py | 76 ++++ fastapi_log/models/__init__.py | 2 + fastapi_log/models/fastapi_endpoint.py | 35 ++ fastapi_log/models/fastapi_log.py | 227 ++++++++++ fastapi_log/readme/CONTRIBUTORS.md | 1 + fastapi_log/readme/DESCRIPTION.md | 3 + fastapi_log/readme/USAGE.md | 6 + fastapi_log/security/ir_model_access.xml | 17 + fastapi_log/security/res_groups.xml | 17 + fastapi_log/static/description/index.html | 438 +++++++++++++++++++ fastapi_log/tests/__init__.py | 1 + fastapi_log/tests/test_fastapi_log.py | 161 +++++++ fastapi_log/views/fastapi_endpoint_views.xml | 42 ++ fastapi_log/views/fastapi_log_views.xml | 124 ++++++ setup/fastapi_log/odoo/addons/fastapi_log | 1 + setup/fastapi_log/setup.py | 6 + 19 files changed, 1281 insertions(+) create mode 100644 fastapi_log/README.rst create mode 100644 fastapi_log/__init__.py create mode 100644 fastapi_log/__manifest__.py create mode 100644 fastapi_log/fastapi_dispatcher.py create mode 100644 fastapi_log/models/__init__.py create mode 100644 fastapi_log/models/fastapi_endpoint.py create mode 100644 fastapi_log/models/fastapi_log.py create mode 100644 fastapi_log/readme/CONTRIBUTORS.md create mode 100644 fastapi_log/readme/DESCRIPTION.md create mode 100644 fastapi_log/readme/USAGE.md create mode 100644 fastapi_log/security/ir_model_access.xml create mode 100644 fastapi_log/security/res_groups.xml create mode 100644 fastapi_log/static/description/index.html create mode 100644 fastapi_log/tests/__init__.py create mode 100644 fastapi_log/tests/test_fastapi_log.py create mode 100644 fastapi_log/views/fastapi_endpoint_views.xml create mode 100644 fastapi_log/views/fastapi_log_views.xml create mode 120000 setup/fastapi_log/odoo/addons/fastapi_log create mode 100644 setup/fastapi_log/setup.py diff --git a/fastapi_log/README.rst b/fastapi_log/README.rst new file mode 100644 index 000000000..6464683d2 --- /dev/null +++ b/fastapi_log/README.rst @@ -0,0 +1,99 @@ +=========== +Fastapi Log +=========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ef0c0bceb8ae27bcfebaebc22e2fb4747475f2a2c60dd2d410bc40b6efee9b6a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_log + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi_log + :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/rest-framework&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows an endpoint to activate full request logging in a +database model. + +It is useful to debug production issues or to monitor the usage of a +specific endpoint. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To activate logging for an endpoint, you have to check the +``Log Requests`` checkbox in the endpoint's configuration. This will log +all requests and responses for that endpoint. + +A smart button will be displayed in the endpoint's form view to access +the endpoint logs. A global log view is also available in the +``FastAPI Logs`` menu. + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion + +Contributors +------------ + +- Florian Mounier florian.mounier@akretion.com + +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-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainer `__: + +|maintainer-paradoxxxzero| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi_log/__init__.py b/fastapi_log/__init__.py new file mode 100644 index 000000000..d54296502 --- /dev/null +++ b/fastapi_log/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import fastapi_dispatcher diff --git a/fastapi_log/__manifest__.py b/fastapi_log/__manifest__.py new file mode 100644 index 000000000..c10eaf057 --- /dev/null +++ b/fastapi_log/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Fastapi Log", + "version": "16.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "summary": "Log Fastapi requests in database", + "category": "Tools", + "depends": ["fastapi"], + "website": "https://github.com/OCA/rest-framework", + "data": [ + "security/res_groups.xml", + "security/ir_model_access.xml", + "views/fastapi_endpoint_views.xml", + "views/fastapi_log_views.xml", + ], + "maintainers": ["paradoxxxzero"], + "demo": [], + "installable": True, + "license": "AGPL-3", +} diff --git a/fastapi_log/fastapi_dispatcher.py b/fastapi_log/fastapi_dispatcher.py new file mode 100644 index 000000000..3e0a68f26 --- /dev/null +++ b/fastapi_log/fastapi_dispatcher.py @@ -0,0 +1,76 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import registry, tools +from odoo.http import _dispatchers + +from odoo.addons.fastapi.fastapi_dispatcher import ( + FastApiDispatcher as BaseFastApiDispatcher, +) + +_logger = logging.getLogger(__name__) + + +# Inherit from last registered fastapi dispatcher +# This handles multiple overload of dispatchers +class FastApiDispatcher(_dispatchers.get("fastapi", BaseFastApiDispatcher)): + routing_type = "fastapi" + + def dispatch(self, endpoint, args): + self.request.params = {} + environ = self._get_environ() + root_path = "/" + environ["PATH_INFO"].split("/")[1] + fastapi_endpoint = ( + self.request.env["fastapi.endpoint"] + .sudo() + .search([("root_path", "=", root_path)]) + ) + if fastapi_endpoint.log_requests: + log = None + try: + if tools.config["test_enable"]: + cr = getattr( + self.request.env.registry, "test_log_cr", self.request.env.cr + ) + else: + # Create an independent cursor + cr = registry(self.request.env.cr.dbname).cursor() + + env = self.request.env(cr=cr, su=True) + try: + # cf fastapi _get_environ + request = self.request.httprequest._HTTPRequest__wrapped + except AttributeError: + request = self.request.httprequest + + log = env["fastapi.log"].log_request( + request, environ, fastapi_endpoint.id + ) + except Exception as e: + _logger.warning("Failed to log request", exc_info=e) + + try: + response = super().dispatch(endpoint, args) + except Exception as e: + try: + log and log.log_exception(e) + except Exception as e: + _logger.warning("Failed to log exception", exc_info=e) + raise e + else: + try: + log and log.log_response(response) + except Exception as e: + _logger.warning("Failed to log response", exc_info=e) + finally: + if not tools.config["test_enable"]: + try: + cr.commit() # pylint: disable=E8102 + finally: + cr.close() + return response + + else: + return super().dispatch(endpoint, args) diff --git a/fastapi_log/models/__init__.py b/fastapi_log/models/__init__.py new file mode 100644 index 000000000..cddd4099d --- /dev/null +++ b/fastapi_log/models/__init__.py @@ -0,0 +1,2 @@ +from . import fastapi_endpoint +from . import fastapi_log diff --git a/fastapi_log/models/fastapi_endpoint.py b/fastapi_log/models/fastapi_endpoint.py new file mode 100644 index 000000000..e2649f812 --- /dev/null +++ b/fastapi_log/models/fastapi_endpoint.py @@ -0,0 +1,35 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + log_requests = fields.Boolean( + help="Log requests in database.", + ) + + fastapi_log_ids = fields.One2many( + "fastapi.log", + "endpoint_id", + string="Logs", + ) + + fastapi_log_count = fields.Integer( + compute="_compute_fastapi_log_count", + string="Logs Count", + ) + + @api.depends("fastapi_log_ids") + def _compute_fastapi_log_count(self): + data = self.env["fastapi.log"].read_group( + [("endpoint_id", "in", self.ids)], + ["endpoint_id"], + ["endpoint_id"], + ) + mapped_data = {m["endpoint_id"][0]: m["endpoint_id_count"] for m in data} + for record in self: + record.fastapi_log_count = mapped_data.get(record.id, 0) diff --git a/fastapi_log/models/fastapi_log.py b/fastapi_log/models/fastapi_log.py new file mode 100644 index 000000000..526d38213 --- /dev/null +++ b/fastapi_log/models/fastapi_log.py @@ -0,0 +1,227 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import base64 +import json +import time +from traceback import format_exception + +from starlette.exceptions import HTTPException as StarletteHTTPException +from werkzeug.exceptions import HTTPException as WerkzeugHTTPException + +from odoo import api, fields, models + + +class FastapiLog(models.Model): + _name = "fastapi.log" + _description = "Fastapi Log" + _order = "id desc" + + endpoint_id = fields.Many2one( + "fastapi.endpoint", + string="Endpoint", + required=True, + ondelete="cascade", + index=True, + ) + + # Request + request_url = fields.Char() + request_method = fields.Char() + request_headers = fields.Json() + request_body = fields.Binary(attachment=False) + request_date = fields.Datetime() + request_time = fields.Float() + + # Response + response_status_code = fields.Integer() + response_headers = fields.Json() + response_body = fields.Binary(attachment=False) + response_date = fields.Datetime() + response_time = fields.Float() + + stack_trace = fields.Text() + + # Derived fields + name = fields.Char(compute="_compute_name", store=True) + time = fields.Float(compute="_compute_time", store=True) + request_preview = fields.Text(compute="_compute_request_preview") + response_preview = fields.Text(compute="_compute_response_preview") + request_b64 = fields.Binary( + string="Request Content", compute="_compute_request_b64" + ) + response_b64 = fields.Binary( + string="Response Content", compute="_compute_response_b64" + ) + request_headers_preview = fields.Text(compute="_compute_headers_preview") + response_headers_preview = fields.Text(compute="_compute_headers_preview") + request_content_type = fields.Char( + compute="_compute_request_headers_derived", store=True + ) + request_content_length = fields.Integer( + compute="_compute_request_headers_derived", store=True + ) + referrer = fields.Char(compute="_compute_request_headers_derived", store=True) + response_content_type = fields.Char( + compute="_compute_response_headers_derived", store=True + ) + response_content_length = fields.Integer( + compute="_compute_response_headers_derived", store=True + ) + + def _headers_to_dict(self, headers): + try: + return {key.lower(): value for key, value in headers.items()} + except AttributeError: + return {} + + def _current_time(self): + return time.time_ns() / 1e9 + + @api.model + def log_request(self, request, environ, endpoint_id): + body = None + # Be careful to not consume the request body if it hasn't been wrapped + stream = environ.get("wsgi.input") + if stream and stream.seekable(): + body = stream.read() + stream.seek(0) + + return self.create( + { + "endpoint_id": endpoint_id, + "request_url": request.url, + "request_method": request.method, + "request_headers": self._headers_to_dict(request.headers), + "request_body": body, + "request_date": fields.Datetime.now(), + "request_time": self._current_time(), + } + ) + + @api.model + def log_response(self, response): + return self.write( + { + "response_status_code": response.status_code, + "response_headers": self._headers_to_dict(response.headers), + "response_body": response.data, + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + ) + + @api.model + def log_exception(self, exception): + self.write( + { + "stack_trace": "".join(format_exception(exception)), + } + ) + if isinstance(exception, StarletteHTTPException): + return self.write( + { + "response_status_code": exception.status_code, + "response_headers": self._headers_to_dict(exception.headers), + "response_body": exception.detail, + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + ) + if isinstance(exception, WerkzeugHTTPException): + return self.write( + { + "response_status_code": exception.code, + "response_headers": self._headers_to_dict(exception.get_headers()), + "response_body": exception.get_body(), + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + ) + try: + return self.log_response( + self.env.registry["ir.http"]._handle_error(exception) + ) + except Exception: + return self.write( + { + "response_status_code": 599, + "response_body": str(exception), + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + ) + + @api.depends("request_url", "request_method", "request_date") + def _compute_name(self): + for record in self: + record.name = ( + f"{record.request_date.isoformat()} - " + f"[{record.request_method} {record.request_url}" + ) + + @api.depends("request_time", "response_time") + def _compute_time(self): + for record in self: + if record.request_time and record.response_time: + record.time = record.response_time - record.request_time + else: + record.time = 0 + + @api.depends("request_headers") + def _compute_request_headers_derived(self): + for record in self: + headers = record.request_headers or {} + record.request_content_type = headers.get("content-type", "") + record.request_content_length = headers.get("content-length", 0) + record.referrer = headers.get("referer", "") + + @api.depends("response_headers") + def _compute_response_headers_derived(self): + for record in self: + headers = record.response_headers or {} + record.response_content_type = headers.get("content-type", "") + record.response_content_length = headers.get("content-length", 0) + + @api.depends("request_body") + def _compute_request_preview(self): + for record in self.with_context(bin_size=False): + record.request_preview = record._body_preview(record.request_body) + + @api.depends("response_body") + def _compute_response_preview(self): + for record in self.with_context(bin_size=False): + record.response_preview = record._body_preview(record.response_body) + + def _body_preview(self, body): + # Display the first 1000 characters of the body if it's a text content + body_preview = False + if body: + try: + body_preview = body.decode("utf-8", errors="ignore") + if len(body_preview) > 1000: + body_preview = body_preview[:1000] + "...\n(...)" + except UnicodeDecodeError: + body_preview = False + return body_preview + + @api.depends("request_headers", "response_headers") + def _compute_headers_preview(self): + for record in self: + record.request_headers_preview = record._headers_preview( + record.request_headers + ) + record.response_headers_preview = record._headers_preview( + record.response_headers + ) + + def _headers_preview(self, headers): + return json.dumps(headers, sort_keys=True, indent=4) if headers else False + + @api.depends("request_body") + def _compute_request_b64(self): + self.request_b64 = base64.b64encode(self.request_body or b"") + + @api.depends("response_body") + def _compute_response_b64(self): + self.response_b64 = base64.b64encode(self.response_body or b"") diff --git a/fastapi_log/readme/CONTRIBUTORS.md b/fastapi_log/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..328a37da8 --- /dev/null +++ b/fastapi_log/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Florian Mounier diff --git a/fastapi_log/readme/DESCRIPTION.md b/fastapi_log/readme/DESCRIPTION.md new file mode 100644 index 000000000..60edac6e4 --- /dev/null +++ b/fastapi_log/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module allows an endpoint to activate full request logging in a database model. + +It is useful to debug production issues or to monitor the usage of a specific endpoint. diff --git a/fastapi_log/readme/USAGE.md b/fastapi_log/readme/USAGE.md new file mode 100644 index 000000000..420859a01 --- /dev/null +++ b/fastapi_log/readme/USAGE.md @@ -0,0 +1,6 @@ +To activate logging for an endpoint, you have to check the `Log Requests` checkbox in +the endpoint's configuration. This will log all requests and responses for that +endpoint. + +A smart button will be displayed in the endpoint's form view to access the endpoint +logs. A global log view is also available in the `FastAPI Logs` menu. diff --git a/fastapi_log/security/ir_model_access.xml b/fastapi_log/security/ir_model_access.xml new file mode 100644 index 000000000..ea4cd5edf --- /dev/null +++ b/fastapi_log/security/ir_model_access.xml @@ -0,0 +1,17 @@ + + + + + Fastapi Log: Read access + + + + + + + + diff --git a/fastapi_log/security/res_groups.xml b/fastapi_log/security/res_groups.xml new file mode 100644 index 000000000..3eec366d1 --- /dev/null +++ b/fastapi_log/security/res_groups.xml @@ -0,0 +1,17 @@ + + + + + + Fastapi Log Access + + + + diff --git a/fastapi_log/static/description/index.html b/fastapi_log/static/description/index.html new file mode 100644 index 000000000..0a76e9f5d --- /dev/null +++ b/fastapi_log/static/description/index.html @@ -0,0 +1,438 @@ + + + + + +Fastapi Log + + + +
    +

    Fastapi Log

    + + +

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    This module allows an endpoint to activate full request logging in a +database model.

    +

    It is useful to debug production issues or to monitor the usage of a +specific endpoint.

    +

    Table of contents

    + +
    +

    Usage

    +

    To activate logging for an endpoint, you have to check the +Log Requests checkbox in the endpoint’s configuration. This will log +all requests and responses for that endpoint.

    +

    A smart button will be displayed in the endpoint’s form view to access +the endpoint logs. A global log view is also available in the +FastAPI Logs menu.

    +
    +
    +

    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.

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • Akretion
    • +
    +
    + +
    +

    Maintainers

    +

    This module is maintained by the OCA.

    + +Odoo Community Association + +

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

    +

    Current maintainer:

    +

    paradoxxxzero

    +

    This module is part of the OCA/rest-framework project on GitHub.

    +

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

    +
    +
    +
    + + diff --git a/fastapi_log/tests/__init__.py b/fastapi_log/tests/__init__.py new file mode 100644 index 000000000..41a525a04 --- /dev/null +++ b/fastapi_log/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fastapi_log diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py new file mode 100644 index 000000000..77df06ee9 --- /dev/null +++ b/fastapi_log/tests/test_fastapi_log.py @@ -0,0 +1,161 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import os +import threading +import unittest +from contextlib import contextmanager + +from odoo.sql_db import TestCursor +from odoo.tests.common import HttpCase, RecordCapturer + +from odoo.addons.fastapi.schemas import DemoExceptionType + +from fastapi import status + + +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") +class FastAPIEncryptedErrorsCase(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") + cls.fastapi_demo_app._handle_registry_sync() + cls.fastapi_demo_app.write({"log_requests": True}) + lang = ( + cls.env["res.lang"] + .with_context(active_test=False) + .search([("code", "=", "fr_BE")]) + ) + lang.active = True + + def setUp(self): + super().setUp() + # Use a side test cursor to be able to get exception logs + reg = self.env.registry + reg.test_log_lock = threading.RLock() + reg.test_log_cr = TestCursor(reg._db.cursor(), reg.test_log_lock) + + def tearDown(self): + reg = self.env.registry + reg.test_log_cr.rollback() + reg.test_log_cr.close() + reg.test_log_cr = None + reg.test_log_lock = None + super().tearDown() + + @contextmanager + def log_capturer(self): + with RecordCapturer( + self.env(cr=self.env.registry.test_log_cr)["fastapi.log"], + [("endpoint_id", "=", self.fastapi_demo_app.id)], + ) as capturer: + yield capturer + + def test_no_log_if_disabled(self): + self.fastapi_demo_app.write({"log_requests": False}) + + with self.log_capturer() as capturer: + response = self.url_open("/fastapi_demo/demo", timeout=200) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertFalse(capturer.records) + + def test_log_simple(self): + with self.log_capturer() as capturer: + response = self.url_open("/fastapi_demo/demo", timeout=200) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertEqual(len(capturer.records), 1) + log = capturer.records[0] + self.assertTrue(log.request_url.endswith("/fastapi_demo/demo")) + self.assertEqual(log.request_method, "GET") + self.assertEqual(log.response_status_code, 200) + self.assertTrue(log.time > 0) + + def test_log_exception(self): + with self.log_capturer() as capturer: + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.user_error.value}" + "&error_message=User Error" + ) + response = self.url_open(route, timeout=200) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + self.assertEqual(len(capturer.records), 1) + log = capturer.records[0] + self.assertIn("/fastapi_demo/demo/exception", log.request_url) + self.assertEqual(log.request_method, "GET") + self.assertEqual(log.response_status_code, 400) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b"User Error", log.response_body) + self.assertIn("odoo.exceptions.UserError: User Error\n", log.stack_trace) + + def test_log_bare_exception(self): + with self.log_capturer() as capturer: + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.bare_exception.value}" + "&error_message=Internal Server Error" + ) + response = self.url_open(route, timeout=200) + self.assertEqual( + response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + self.assertEqual(len(capturer.records), 1) + log = capturer.records[0] + self.assertIn("/fastapi_demo/demo/exception", log.request_url) + self.assertEqual(log.request_method, "GET") + self.assertEqual(log.response_status_code, 500) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b"Internal Server Error", log.response_body) + self.assertIn("NotImplementedError: Internal Server Error\n", log.stack_trace) + + def test_log_retrying_post(self): + with self.log_capturer() as capturer: + nbr_retries = 2 + route = f"/fastapi_demo/demo/retrying?nbr_retries={nbr_retries}" + response = self.url_open( + route, timeout=20, files={"file": ("test.txt", b"test")} + ) + self.assertEqual(response.status_code, 200) + self.assertDictEqual( + response.json(), {"retries": nbr_retries, "file": "test"} + ) + + self.assertEqual(len(capturer.records), 3) + log = capturer.records[0] + self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertEqual(log.request_method, "POST") + self.assertEqual(log.response_status_code, 200) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b'"retries":2', log.response_body) + self.assertIn(b'"file":"test"', log.response_body) + self.assertFalse(log.stack_trace) + log = capturer.records[1] + self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertEqual(log.request_method, "POST") + self.assertEqual(log.response_status_code, 500) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b'{"detail": "Internal Server Error"}', log.response_body) + self.assertIn( + "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", + log.stack_trace, + ) + log = capturer.records[2] + self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertEqual(log.request_method, "POST") + self.assertEqual(log.response_status_code, 500) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b'{"detail": "Internal Server Error"}', log.response_body) + self.assertIn( + "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", + log.stack_trace, + ) diff --git a/fastapi_log/views/fastapi_endpoint_views.xml b/fastapi_log/views/fastapi_endpoint_views.xml new file mode 100644 index 000000000..7997cd9c0 --- /dev/null +++ b/fastapi_log/views/fastapi_endpoint_views.xml @@ -0,0 +1,42 @@ + + + + + + Fastapi Log + fastapi.log + tree,form + [('endpoint_id', '=', active_id)] + {'default_endpoint_id': active_id} + + + + + fastapi.endpoint + + + +
    + +
    + + + + + + +
    + +
    diff --git a/fastapi_log/views/fastapi_log_views.xml b/fastapi_log/views/fastapi_log_views.xml new file mode 100644 index 000000000..021442ffc --- /dev/null +++ b/fastapi_log/views/fastapi_log_views.xml @@ -0,0 +1,124 @@ + + + + + Fastapi Log + fastapi.log + tree,form + + + + fastapi.log.form + fastapi.log + +
    + +
    +

    + +

    +
    + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + fastapi.log.tree + fastapi.log + + + + + + + + + + + + + + + + fastapi.log.search + fastapi.log + + + + + + + + + + + + + + + + + + +
    diff --git a/setup/fastapi_log/odoo/addons/fastapi_log b/setup/fastapi_log/odoo/addons/fastapi_log new file mode 120000 index 000000000..4996c1e31 --- /dev/null +++ b/setup/fastapi_log/odoo/addons/fastapi_log @@ -0,0 +1 @@ +../../../../fastapi_log \ No newline at end of file diff --git a/setup/fastapi_log/setup.py b/setup/fastapi_log/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/fastapi_log/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 7febae8577bfff50bb4b0342973d85e0bb15478c Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Tue, 15 Apr 2025 11:50:18 +0200 Subject: [PATCH 51/58] [REF] fastapi_log: Extract common features to `api_log` This way other APIs might use the new module `api_log` to store logs. --- api_log/README.rst | 84 ++++ api_log/__init__.py | 1 + api_log/__manifest__.py | 20 + api_log/models/__init__.py | 1 + api_log/models/api_log.py | 213 +++++++++ api_log/readme/CONTRIBUTORS.md | 5 + api_log/readme/DESCRIPTION.md | 1 + .../security/ir_model_access.xml | 8 +- .../security/res_groups.xml | 4 +- api_log/static/description/index.html | 425 ++++++++++++++++++ api_log/tests/__init__.py | 1 + api_log/tests/common.py | 8 + api_log/tests/test_api_log.py | 30 ++ api_log/views/api_log_views.xml | 110 +++++ fastapi_log/__manifest__.py | 7 +- fastapi_log/fastapi_dispatcher.py | 38 +- fastapi_log/models/__init__.py | 2 +- fastapi_log/models/api_log.py | 58 +++ fastapi_log/models/fastapi_endpoint.py | 20 +- fastapi_log/models/fastapi_log.py | 227 ---------- fastapi_log/tests/test_fastapi_log.py | 45 +- fastapi_log/views/fastapi_endpoint_views.xml | 10 +- fastapi_log/views/fastapi_log_views.xml | 128 ++---- setup/api_log/odoo/addons/api_log | 1 + setup/api_log/setup.py | 6 + 25 files changed, 1058 insertions(+), 395 deletions(-) create mode 100644 api_log/README.rst create mode 100644 api_log/__init__.py create mode 100644 api_log/__manifest__.py create mode 100644 api_log/models/__init__.py create mode 100644 api_log/models/api_log.py create mode 100644 api_log/readme/CONTRIBUTORS.md create mode 100644 api_log/readme/DESCRIPTION.md rename {fastapi_log => api_log}/security/ir_model_access.xml (65%) rename {fastapi_log => api_log}/security/res_groups.xml (78%) create mode 100644 api_log/static/description/index.html create mode 100644 api_log/tests/__init__.py create mode 100644 api_log/tests/common.py create mode 100644 api_log/tests/test_api_log.py create mode 100644 api_log/views/api_log_views.xml create mode 100644 fastapi_log/models/api_log.py delete mode 100644 fastapi_log/models/fastapi_log.py create mode 120000 setup/api_log/odoo/addons/api_log create mode 100644 setup/api_log/setup.py diff --git a/api_log/README.rst b/api_log/README.rst new file mode 100644 index 000000000..842595a30 --- /dev/null +++ b/api_log/README.rst @@ -0,0 +1,84 @@ +======= +API Log +======= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ef0c0bceb8ae27bcfebaebc22e2fb4747475f2a2c60dd2d410bc40b6efee9b6a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/api_log + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-api_log + :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/rest-framework&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to store request and response logs for any API. + +**Table of contents** + +.. contents:: + :local: + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion + +Contributors +------------ + +- Florian Mounier florian.mounier@akretion.com + +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-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainer `__: + +|maintainer-paradoxxxzero| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/api_log/__init__.py b/api_log/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/api_log/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/api_log/__manifest__.py b/api_log/__manifest__.py new file mode 100644 index 000000000..84193a908 --- /dev/null +++ b/api_log/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "API Log", + "version": "16.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "license": "AGPL-3", + "summary": "Log API requests in database", + "category": "Tools", + "depends": ["web"], + "website": "https://github.com/OCA/rest-framework", + "data": [ + "security/res_groups.xml", + "security/ir_model_access.xml", + "views/api_log_views.xml", + ], + "maintainers": ["paradoxxxzero"], +} diff --git a/api_log/models/__init__.py b/api_log/models/__init__.py new file mode 100644 index 000000000..0f340289c --- /dev/null +++ b/api_log/models/__init__.py @@ -0,0 +1 @@ +from . import api_log diff --git a/api_log/models/api_log.py b/api_log/models/api_log.py new file mode 100644 index 000000000..16bc7b973 --- /dev/null +++ b/api_log/models/api_log.py @@ -0,0 +1,213 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import base64 +import json +import time +from traceback import format_exception + +from werkzeug.exceptions import HTTPException as WerkzeugHTTPException + +from odoo import api, fields, models + + +class APILog(models.Model): + _name = "api.log" + _description = "Log for API" + + # Request + request_url = fields.Char() + request_method = fields.Char() + request_headers = fields.Json() + request_body = fields.Binary(attachment=False) + request_date = fields.Datetime() + request_time = fields.Float() + + # Response + response_status_code = fields.Integer() + response_headers = fields.Json() + response_body = fields.Binary(attachment=False) + response_date = fields.Datetime() + response_time = fields.Float() + + stack_trace = fields.Text() + + # Derived fields + name = fields.Char(compute="_compute_name", store=True) + time = fields.Float(compute="_compute_time", store=True) + request_preview = fields.Text(compute="_compute_request_preview") + response_preview = fields.Text(compute="_compute_response_preview") + request_b64 = fields.Binary( + string="Request Content", compute="_compute_request_b64" + ) + response_b64 = fields.Binary( + string="Response Content", compute="_compute_response_b64" + ) + request_headers_preview = fields.Text(compute="_compute_headers_preview") + response_headers_preview = fields.Text(compute="_compute_headers_preview") + request_content_type = fields.Char( + compute="_compute_request_headers_derived", store=True + ) + request_content_length = fields.Integer( + compute="_compute_request_headers_derived", store=True + ) + referrer = fields.Char(compute="_compute_request_headers_derived", store=True) + response_content_type = fields.Char( + compute="_compute_response_headers_derived", store=True + ) + response_content_length = fields.Integer( + compute="_compute_response_headers_derived", store=True + ) + + def _headers_to_dict(self, headers): + try: + return {key.lower(): value for key, value in headers.items()} + except AttributeError: + return {} + + def _current_time(self): + return time.time_ns() / 1e9 + + @api.model + def _get_http_request(self, request): + return request.httprequest + + @api.model + def _get_request_body(self, request): + """Take extra care with the request's body because it might get consumed.""" + httprequest = self._get_http_request(request) + return httprequest.data + + @api.model + def _prepare_log_request(self, request): + httprequest = self._get_http_request(request) + log_request_values = { + "request_url": httprequest.url, + "request_method": httprequest.method, + "request_headers": self._headers_to_dict(httprequest.headers), + "request_body": self._get_request_body(request), + "request_date": fields.Datetime.now(), + "request_time": self._current_time(), + } + return log_request_values + + @api.model + def log_request(self, request, override_log_values=None): + log_request_values = self._prepare_log_request(request) + if override_log_values: + log_request_values.update(override_log_values) + return self.sudo().create(log_request_values) + + def _prepare_log_response(self, response): + return { + "response_status_code": response.status_code, + "response_headers": self._headers_to_dict(response.headers), + "response_body": response.data, + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + + def log_response(self, response): + log_response_values = self._prepare_log_response(response) + return self.sudo().write(log_response_values) + + def _prepare_log_exception(self, exception): + values = { + "stack_trace": "".join(format_exception(exception)), + "response_body": str(exception), + "response_date": fields.Datetime.now(), + "response_time": self._current_time(), + } + + if isinstance(exception, WerkzeugHTTPException): + values.update( + { + "response_status_code": exception.code, + "response_headers": self._headers_to_dict(exception.get_headers()), + "response_body": exception.get_body(), + } + ) + return values + + def log_exception(self, exception): + try: + exc_handling_response = self.env.registry["ir.http"]._handle_error( + exception + ) + self.log_response(exc_handling_response) + except Exception as handling_exception: + exception = handling_exception + log_exception_values = self._prepare_log_exception(exception) + return self.sudo().write(log_exception_values) + + @api.depends("request_url", "request_method", "request_date") + def _compute_name(self): + for log in self: + log.name = ( + f"{log.request_date.isoformat()} - " + f"[{log.request_method}] {log.request_url}" + ) + + @api.depends("request_time", "response_time") + def _compute_time(self): + for log in self: + if log.request_time and log.response_time: + log.time = log.response_time - log.request_time + else: + log.time = 0 + + @api.depends("request_headers") + def _compute_request_headers_derived(self): + for log in self: + headers = log.request_headers or {} + log.request_content_type = headers.get("content-type", "") + log.request_content_length = headers.get("content-length", 0) + log.referrer = headers.get("referer", "") + + @api.depends("response_headers") + def _compute_response_headers_derived(self): + for log in self: + headers = log.response_headers or {} + log.response_content_type = headers.get("content-type", "") + log.response_content_length = headers.get("content-length", 0) + + @api.depends("request_body") + def _compute_request_preview(self): + for log in self.with_context(bin_size=False): + log.request_preview = log._body_preview(log.request_body) + + @api.depends("response_body") + def _compute_response_preview(self): + for log in self.with_context(bin_size=False): + log.response_preview = log._body_preview(log.response_body) + + def _body_preview(self, body): + # Display the first 1000 characters of the body if it's a text content + body_preview = False + if body: + try: + body_preview = body.decode("utf-8", errors="ignore") + if len(body_preview) > 1000: + body_preview = body_preview[:1000] + "...\n(...)" + except UnicodeDecodeError: + body_preview = False + return body_preview + + @api.depends("request_headers", "response_headers") + def _compute_headers_preview(self): + for log in self: + log.request_headers_preview = log._headers_preview(log.request_headers) + log.response_headers_preview = log._headers_preview(log.response_headers) + + def _headers_preview(self, headers): + return json.dumps(headers, sort_keys=True, indent=4) if headers else False + + @api.depends("request_body") + def _compute_request_b64(self): + for log in self: + log.request_b64 = base64.b64encode(log.request_body or b"") + + @api.depends("response_body") + def _compute_response_b64(self): + for log in self: + log.response_b64 = base64.b64encode(log.response_body or b"") diff --git a/api_log/readme/CONTRIBUTORS.md b/api_log/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..599c28bb2 --- /dev/null +++ b/api_log/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- Florian Mounier +- Guewen Baconnier +- Simone Orsi +- [PyTech](https://www.pytech.it): + - Simone Rubino \<\> diff --git a/api_log/readme/DESCRIPTION.md b/api_log/readme/DESCRIPTION.md new file mode 100644 index 000000000..6018fc343 --- /dev/null +++ b/api_log/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows to store request and response logs for any API. diff --git a/fastapi_log/security/ir_model_access.xml b/api_log/security/ir_model_access.xml similarity index 65% rename from fastapi_log/security/ir_model_access.xml rename to api_log/security/ir_model_access.xml index ea4cd5edf..a092c0d3a 100644 --- a/fastapi_log/security/ir_model_access.xml +++ b/api_log/security/ir_model_access.xml @@ -5,10 +5,10 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> - - Fastapi Log: Read access - - + + API Log: Read access + + diff --git a/fastapi_log/security/res_groups.xml b/api_log/security/res_groups.xml similarity index 78% rename from fastapi_log/security/res_groups.xml rename to api_log/security/res_groups.xml index 3eec366d1..8b9ddf38b 100644 --- a/fastapi_log/security/res_groups.xml +++ b/api_log/security/res_groups.xml @@ -6,8 +6,8 @@ --> - - Fastapi Log Access + + API Log Access + + + + +API Log + + + +
    +

    API Log

    + + +

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    This module allows to store request and response logs for any API.

    +

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

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • Akretion
    • +
    +
    + +
    +

    Maintainers

    +

    This module is maintained by the OCA.

    + +Odoo Community Association + +

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

    +

    Current maintainer:

    +

    paradoxxxzero

    +

    This module is part of the OCA/rest-framework project on GitHub.

    +

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

    +
    +
    +
    + + diff --git a/api_log/tests/__init__.py b/api_log/tests/__init__.py new file mode 100644 index 000000000..7f84a8e4f --- /dev/null +++ b/api_log/tests/__init__.py @@ -0,0 +1 @@ +from . import test_api_log diff --git a/api_log/tests/common.py b/api_log/tests/common.py new file mode 100644 index 000000000..e54316db6 --- /dev/null +++ b/api_log/tests/common.py @@ -0,0 +1,8 @@ +from odoo.tests.common import HttpCase + + +class CommonAPILog(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.log_model = cls.env["api.log"] diff --git a/api_log/tests/test_api_log.py b/api_log/tests/test_api_log.py new file mode 100644 index 000000000..abed32678 --- /dev/null +++ b/api_log/tests/test_api_log.py @@ -0,0 +1,30 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import requests + +from odoo.http import Request, Response + +from odoo.addons.api_log.tests.common import CommonAPILog + + +class TestAPILog(CommonAPILog): + def test_log_request(self): + base_url = self.base_url() + httprequest = requests.Request( + url=base_url, + method="GET", + ) + request = Request(httprequest) + log = self.log_model.log_request(request) + + self.assertEqual(log.request_url, base_url) + self.assertEqual(log.request_method, "GET") + + def test_log_response(self): + response = Response() + log = self.log_model.create({}) + log.log_response(response) + + self.assertEqual(log.response_status_code, 200) diff --git a/api_log/views/api_log_views.xml b/api_log/views/api_log_views.xml new file mode 100644 index 000000000..cc09bbdaf --- /dev/null +++ b/api_log/views/api_log_views.xml @@ -0,0 +1,110 @@ + + + + + API Log + api.log + tree,form + + + + api.log.form + api.log + +
    + +
    +

    + +

    +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + api.log.tree + api.log + + + + + + + + + + + + + + + api.log.search + api.log + + + + + + + + + + + + + + + + +
    diff --git a/fastapi_log/__manifest__.py b/fastapi_log/__manifest__.py index c10eaf057..8334dc5c2 100644 --- a/fastapi_log/__manifest__.py +++ b/fastapi_log/__manifest__.py @@ -8,11 +8,12 @@ "author": "Akretion, Odoo Community Association (OCA)", "summary": "Log Fastapi requests in database", "category": "Tools", - "depends": ["fastapi"], + "depends": [ + "api_log", + "fastapi", + ], "website": "https://github.com/OCA/rest-framework", "data": [ - "security/res_groups.xml", - "security/ir_model_access.xml", "views/fastapi_endpoint_views.xml", "views/fastapi_log_views.xml", ], diff --git a/fastapi_log/fastapi_dispatcher.py b/fastapi_log/fastapi_dispatcher.py index 3e0a68f26..6a762cd51 100644 --- a/fastapi_log/fastapi_dispatcher.py +++ b/fastapi_log/fastapi_dispatcher.py @@ -28,37 +28,30 @@ def dispatch(self, endpoint, args): .search([("root_path", "=", root_path)]) ) if fastapi_endpoint.log_requests: - log = None - try: - if tools.config["test_enable"]: - cr = getattr( - self.request.env.registry, "test_log_cr", self.request.env.cr - ) - else: - # Create an independent cursor - cr = registry(self.request.env.cr.dbname).cursor() - - env = self.request.env(cr=cr, su=True) - try: - # cf fastapi _get_environ - request = self.request.httprequest._HTTPRequest__wrapped - except AttributeError: - request = self.request.httprequest - - log = env["fastapi.log"].log_request( - request, environ, fastapi_endpoint.id + if tools.config["test_enable"]: + cr = getattr( + self.request.env.registry, "test_log_cr", self.request.env.cr ) + else: + # Create an independent cursor + cr = registry(self.request.env.cr.dbname).cursor() + + env = self.request.env(cr=cr, su=True) + request = self.request + try: + log = env["api.log"].log_request(request) except Exception as e: _logger.warning("Failed to log request", exc_info=e) + log = None try: response = super().dispatch(endpoint, args) - except Exception as e: + except Exception as response_exc: try: - log and log.log_exception(e) + log and log.log_exception(response_exc) except Exception as e: _logger.warning("Failed to log exception", exc_info=e) - raise e + raise response_exc else: try: log and log.log_response(response) @@ -71,6 +64,5 @@ def dispatch(self, endpoint, args): finally: cr.close() return response - else: return super().dispatch(endpoint, args) diff --git a/fastapi_log/models/__init__.py b/fastapi_log/models/__init__.py index cddd4099d..23ac9cf0b 100644 --- a/fastapi_log/models/__init__.py +++ b/fastapi_log/models/__init__.py @@ -1,2 +1,2 @@ +from . import api_log from . import fastapi_endpoint -from . import fastapi_log diff --git a/fastapi_log/models/api_log.py b/fastapi_log/models/api_log.py new file mode 100644 index 000000000..ee28db599 --- /dev/null +++ b/fastapi_log/models/api_log.py @@ -0,0 +1,58 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from starlette.exceptions import HTTPException as StarletteHTTPException + +from odoo import api, fields, models + + +class FastapiLog(models.Model): + _inherit = "api.log" + + fastapi_endpoint_id = fields.Many2one( + comodel_name="fastapi.endpoint", + string="Endpoint", + ondelete="cascade", + index=True, + ) + + @api.model + def _get_request_body(self, request): + # Be careful to not consume the request body if it hasn't been wrapped + dispatcher = request.dispatcher + if dispatcher.routing_type == "fastapi": + environ = dispatcher._get_environ() + stream = environ.get("wsgi.input") + if stream and stream.seekable(): + request_body = stream.read() + stream.seek(0) + else: + request_body = super()._get_request_body(request) + return request_body + + @api.model + def _prepare_log_request(self, request): + log_request_values = super()._prepare_log_request(request) + dispatcher = request.dispatcher + if dispatcher.routing_type == "fastapi": + environ = dispatcher._get_environ() + endpoint = ( + request.env["fastapi.endpoint"] + .sudo() + ._get_endpoint(environ["PATH_INFO"]) + ) + log_request_values["fastapi_endpoint_id"] = endpoint.id + return log_request_values + + def _prepare_log_exception(self, exception): + values = super()._prepare_log_exception(exception) + if isinstance(exception, StarletteHTTPException): + values.update( + { + "response_status_code": exception.status_code, + "response_headers": self._headers_to_dict(exception.headers), + "response_body": exception.detail, + } + ) + return values diff --git a/fastapi_log/models/fastapi_endpoint.py b/fastapi_log/models/fastapi_endpoint.py index e2649f812..62789fc4c 100644 --- a/fastapi_log/models/fastapi_endpoint.py +++ b/fastapi_log/models/fastapi_endpoint.py @@ -13,8 +13,8 @@ class FastapiEndpoint(models.Model): ) fastapi_log_ids = fields.One2many( - "fastapi.log", - "endpoint_id", + comodel_name="api.log", + inverse_name="fastapi_endpoint_id", string="Logs", ) @@ -25,11 +25,13 @@ class FastapiEndpoint(models.Model): @api.depends("fastapi_log_ids") def _compute_fastapi_log_count(self): - data = self.env["fastapi.log"].read_group( - [("endpoint_id", "in", self.ids)], - ["endpoint_id"], - ["endpoint_id"], + groups = self.env["api.log"].read_group( + [("fastapi_endpoint_id", "in", self.ids)], + ["fastapi_endpoint_id"], + ["fastapi_endpoint_id"], ) - mapped_data = {m["endpoint_id"][0]: m["endpoint_id_count"] for m in data} - for record in self: - record.fastapi_log_count = mapped_data.get(record.id, 0) + mapped_data = { + g["fastapi_endpoint_id"][0]: g["fastapi_endpoint_id_count"] for g in groups + } + for endpoint in self: + endpoint.fastapi_log_count = mapped_data.get(endpoint.id, 0) diff --git a/fastapi_log/models/fastapi_log.py b/fastapi_log/models/fastapi_log.py deleted file mode 100644 index 526d38213..000000000 --- a/fastapi_log/models/fastapi_log.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2025 Akretion (http://www.akretion.com). -# @author Florian Mounier -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import base64 -import json -import time -from traceback import format_exception - -from starlette.exceptions import HTTPException as StarletteHTTPException -from werkzeug.exceptions import HTTPException as WerkzeugHTTPException - -from odoo import api, fields, models - - -class FastapiLog(models.Model): - _name = "fastapi.log" - _description = "Fastapi Log" - _order = "id desc" - - endpoint_id = fields.Many2one( - "fastapi.endpoint", - string="Endpoint", - required=True, - ondelete="cascade", - index=True, - ) - - # Request - request_url = fields.Char() - request_method = fields.Char() - request_headers = fields.Json() - request_body = fields.Binary(attachment=False) - request_date = fields.Datetime() - request_time = fields.Float() - - # Response - response_status_code = fields.Integer() - response_headers = fields.Json() - response_body = fields.Binary(attachment=False) - response_date = fields.Datetime() - response_time = fields.Float() - - stack_trace = fields.Text() - - # Derived fields - name = fields.Char(compute="_compute_name", store=True) - time = fields.Float(compute="_compute_time", store=True) - request_preview = fields.Text(compute="_compute_request_preview") - response_preview = fields.Text(compute="_compute_response_preview") - request_b64 = fields.Binary( - string="Request Content", compute="_compute_request_b64" - ) - response_b64 = fields.Binary( - string="Response Content", compute="_compute_response_b64" - ) - request_headers_preview = fields.Text(compute="_compute_headers_preview") - response_headers_preview = fields.Text(compute="_compute_headers_preview") - request_content_type = fields.Char( - compute="_compute_request_headers_derived", store=True - ) - request_content_length = fields.Integer( - compute="_compute_request_headers_derived", store=True - ) - referrer = fields.Char(compute="_compute_request_headers_derived", store=True) - response_content_type = fields.Char( - compute="_compute_response_headers_derived", store=True - ) - response_content_length = fields.Integer( - compute="_compute_response_headers_derived", store=True - ) - - def _headers_to_dict(self, headers): - try: - return {key.lower(): value for key, value in headers.items()} - except AttributeError: - return {} - - def _current_time(self): - return time.time_ns() / 1e9 - - @api.model - def log_request(self, request, environ, endpoint_id): - body = None - # Be careful to not consume the request body if it hasn't been wrapped - stream = environ.get("wsgi.input") - if stream and stream.seekable(): - body = stream.read() - stream.seek(0) - - return self.create( - { - "endpoint_id": endpoint_id, - "request_url": request.url, - "request_method": request.method, - "request_headers": self._headers_to_dict(request.headers), - "request_body": body, - "request_date": fields.Datetime.now(), - "request_time": self._current_time(), - } - ) - - @api.model - def log_response(self, response): - return self.write( - { - "response_status_code": response.status_code, - "response_headers": self._headers_to_dict(response.headers), - "response_body": response.data, - "response_date": fields.Datetime.now(), - "response_time": self._current_time(), - } - ) - - @api.model - def log_exception(self, exception): - self.write( - { - "stack_trace": "".join(format_exception(exception)), - } - ) - if isinstance(exception, StarletteHTTPException): - return self.write( - { - "response_status_code": exception.status_code, - "response_headers": self._headers_to_dict(exception.headers), - "response_body": exception.detail, - "response_date": fields.Datetime.now(), - "response_time": self._current_time(), - } - ) - if isinstance(exception, WerkzeugHTTPException): - return self.write( - { - "response_status_code": exception.code, - "response_headers": self._headers_to_dict(exception.get_headers()), - "response_body": exception.get_body(), - "response_date": fields.Datetime.now(), - "response_time": self._current_time(), - } - ) - try: - return self.log_response( - self.env.registry["ir.http"]._handle_error(exception) - ) - except Exception: - return self.write( - { - "response_status_code": 599, - "response_body": str(exception), - "response_date": fields.Datetime.now(), - "response_time": self._current_time(), - } - ) - - @api.depends("request_url", "request_method", "request_date") - def _compute_name(self): - for record in self: - record.name = ( - f"{record.request_date.isoformat()} - " - f"[{record.request_method} {record.request_url}" - ) - - @api.depends("request_time", "response_time") - def _compute_time(self): - for record in self: - if record.request_time and record.response_time: - record.time = record.response_time - record.request_time - else: - record.time = 0 - - @api.depends("request_headers") - def _compute_request_headers_derived(self): - for record in self: - headers = record.request_headers or {} - record.request_content_type = headers.get("content-type", "") - record.request_content_length = headers.get("content-length", 0) - record.referrer = headers.get("referer", "") - - @api.depends("response_headers") - def _compute_response_headers_derived(self): - for record in self: - headers = record.response_headers or {} - record.response_content_type = headers.get("content-type", "") - record.response_content_length = headers.get("content-length", 0) - - @api.depends("request_body") - def _compute_request_preview(self): - for record in self.with_context(bin_size=False): - record.request_preview = record._body_preview(record.request_body) - - @api.depends("response_body") - def _compute_response_preview(self): - for record in self.with_context(bin_size=False): - record.response_preview = record._body_preview(record.response_body) - - def _body_preview(self, body): - # Display the first 1000 characters of the body if it's a text content - body_preview = False - if body: - try: - body_preview = body.decode("utf-8", errors="ignore") - if len(body_preview) > 1000: - body_preview = body_preview[:1000] + "...\n(...)" - except UnicodeDecodeError: - body_preview = False - return body_preview - - @api.depends("request_headers", "response_headers") - def _compute_headers_preview(self): - for record in self: - record.request_headers_preview = record._headers_preview( - record.request_headers - ) - record.response_headers_preview = record._headers_preview( - record.response_headers - ) - - def _headers_preview(self, headers): - return json.dumps(headers, sort_keys=True, indent=4) if headers else False - - @api.depends("request_body") - def _compute_request_b64(self): - self.request_b64 = base64.b64encode(self.request_body or b"") - - @api.depends("response_body") - def _compute_response_b64(self): - self.response_b64 = base64.b64encode(self.response_body or b"") diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py index 77df06ee9..085c9a410 100644 --- a/fastapi_log/tests/test_fastapi_log.py +++ b/fastapi_log/tests/test_fastapi_log.py @@ -7,15 +7,16 @@ from contextlib import contextmanager from odoo.sql_db import TestCursor -from odoo.tests.common import HttpCase, RecordCapturer +from odoo.tests.common import RecordCapturer +from odoo.addons.api_log.tests.common import CommonAPILog from odoo.addons.fastapi.schemas import DemoExceptionType from fastapi import status @unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") -class FastAPIEncryptedErrorsCase(HttpCase): +class FastAPIEncryptedErrorsCase(CommonAPILog): @classmethod def setUpClass(cls): super().setUpClass() @@ -47,8 +48,8 @@ def tearDown(self): @contextmanager def log_capturer(self): with RecordCapturer( - self.env(cr=self.env.registry.test_log_cr)["fastapi.log"], - [("endpoint_id", "=", self.fastapi_demo_app.id)], + self.env(cr=self.env.registry.test_log_cr)[self.log_model._name], + [("fastapi_endpoint_id", "=", self.fastapi_demo_app.id)], ) as capturer: yield capturer @@ -128,7 +129,19 @@ def test_log_retrying_post(self): ) self.assertEqual(len(capturer.records), 3) - log = capturer.records[0] + for log in capturer.records[0:-1]: + self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertEqual(log.request_method, "POST") + self.assertEqual(log.response_status_code, 500) + self.assertTrue(log.time > 0) + self.assertTrue(log.response_body) + self.assertIn(b"fake error", log.response_body) + self.assertIn( + "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", + log.stack_trace, + ) + + log = capturer.records[-1] self.assertIn("/fastapi_demo/demo/retrying", log.request_url) self.assertEqual(log.request_method, "POST") self.assertEqual(log.response_status_code, 200) @@ -137,25 +150,3 @@ def test_log_retrying_post(self): self.assertIn(b'"retries":2', log.response_body) self.assertIn(b'"file":"test"', log.response_body) self.assertFalse(log.stack_trace) - log = capturer.records[1] - self.assertIn("/fastapi_demo/demo/retrying", log.request_url) - self.assertEqual(log.request_method, "POST") - self.assertEqual(log.response_status_code, 500) - self.assertTrue(log.time > 0) - self.assertTrue(log.response_body) - self.assertIn(b'{"detail": "Internal Server Error"}', log.response_body) - self.assertIn( - "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", - log.stack_trace, - ) - log = capturer.records[2] - self.assertIn("/fastapi_demo/demo/retrying", log.request_url) - self.assertEqual(log.request_method, "POST") - self.assertEqual(log.response_status_code, 500) - self.assertTrue(log.time > 0) - self.assertTrue(log.response_body) - self.assertIn(b'{"detail": "Internal Server Error"}', log.response_body) - self.assertIn( - "odoo.addons.fastapi.routers.demo_router.FakeConcurrentUpdateError: fake error", - log.stack_trace, - ) diff --git a/fastapi_log/views/fastapi_endpoint_views.xml b/fastapi_log/views/fastapi_endpoint_views.xml index 7997cd9c0..bf90e7ea1 100644 --- a/fastapi_log/views/fastapi_endpoint_views.xml +++ b/fastapi_log/views/fastapi_endpoint_views.xml @@ -8,10 +8,10 @@ Fastapi Log - fastapi.log + api.log tree,form - [('endpoint_id', '=', active_id)] - {'default_endpoint_id': active_id} + [('fastapi_endpoint_id', '=', active_id)] + {'default_fastapi_endpoint_id': active_id} @@ -25,7 +25,7 @@ type="action" name="%(fastapi_log.fastapi_log_action_from_endpoint)s" icon="fa-book" - groups="fastapi_log.group_fastapi_log" + groups="api_log.group_api_log" attrs="{'invisible': [('fastapi_log_count', '=', 0)]}" > @@ -34,7 +34,7 @@ - +
    diff --git a/fastapi_log/views/fastapi_log_views.xml b/fastapi_log/views/fastapi_log_views.xml index 021442ffc..1f33e422f 100644 --- a/fastapi_log/views/fastapi_log_views.xml +++ b/fastapi_log/views/fastapi_log_views.xml @@ -5,115 +5,55 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> - - Fastapi Log - fastapi.log - tree,form - - - - fastapi.log.form - fastapi.log + + Add Fastapi fields to API log form view + api.log + -
    - -
    -

    - -

    -
    - - - - - - - - - - - - - - - - - - - - - -
    -
    + + +
    - - fastapi.log.tree - fastapi.log + + Add Fastapi fields to API log tree view + api.log + - - - - - - - - - + + - - fastapi.log.search - fastapi.log + + Add Fastapi fields to API log search view + api.log + - - - - - + + + + - - - - - - - + + + Fastapi Logs + api.log + [ + ("fastapi_endpoint_id", "!=", False) + ] + tree,form + + Date: Tue, 22 Apr 2025 13:16:56 +0200 Subject: [PATCH 52/58] [ADD] fastapi_log_mail --- fastapi_log_mail/README.rst | 93 ++++ fastapi_log_mail/__init__.py | 1 + fastapi_log_mail/__manifest__.py | 22 + fastapi_log_mail/models/__init__.py | 4 + fastapi_log_mail/models/api_log.py | 16 + fastapi_log_mail/models/fastapi_endpoint.py | 15 + fastapi_log_mail/readme/CONFIGURE.md | 1 + fastapi_log_mail/readme/CONTRIBUTORS.md | 2 + fastapi_log_mail/readme/DESCRIPTION.md | 1 + .../static/description/index.html | 435 ++++++++++++++++++ fastapi_log_mail/tests/__init__.py | 1 + .../tests/test_fastapi_log_mail.py | 50 ++ .../views/fastapi_endpoint_views.xml | 24 + .../odoo/addons/fastapi_log_mail | 1 + setup/fastapi_log_mail/setup.py | 6 + 15 files changed, 672 insertions(+) create mode 100644 fastapi_log_mail/README.rst create mode 100644 fastapi_log_mail/__init__.py create mode 100644 fastapi_log_mail/__manifest__.py create mode 100644 fastapi_log_mail/models/__init__.py create mode 100644 fastapi_log_mail/models/api_log.py create mode 100644 fastapi_log_mail/models/fastapi_endpoint.py create mode 100644 fastapi_log_mail/readme/CONFIGURE.md create mode 100644 fastapi_log_mail/readme/CONTRIBUTORS.md create mode 100644 fastapi_log_mail/readme/DESCRIPTION.md create mode 100644 fastapi_log_mail/static/description/index.html create mode 100644 fastapi_log_mail/tests/__init__.py create mode 100644 fastapi_log_mail/tests/test_fastapi_log_mail.py create mode 100644 fastapi_log_mail/views/fastapi_endpoint_views.xml create mode 120000 setup/fastapi_log_mail/odoo/addons/fastapi_log_mail create mode 100644 setup/fastapi_log_mail/setup.py diff --git a/fastapi_log_mail/README.rst b/fastapi_log_mail/README.rst new file mode 100644 index 000000000..2f3303c13 --- /dev/null +++ b/fastapi_log_mail/README.rst @@ -0,0 +1,93 @@ +=========== +Fastapi Log +=========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ef0c0bceb8ae27bcfebaebc22e2fb4747475f2a2c60dd2d410bc40b6efee9b6a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/fastapi_log_mail + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-fastapi_log_mail + :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/rest-framework&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to send an email when an exception occurs in an +endpoint. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +In any endpoint that has logging enabled, insert an email template in +"Error E-mail Template". + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* PyTech + +Contributors +------------ + +- `PyTech `__: + + - Simone Rubino + +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-SirPyTech| image:: https://github.com/SirPyTech.png?size=40px + :target: https://github.com/SirPyTech + :alt: SirPyTech + +Current `maintainer `__: + +|maintainer-SirPyTech| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fastapi_log_mail/__init__.py b/fastapi_log_mail/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/fastapi_log_mail/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/fastapi_log_mail/__manifest__.py b/fastapi_log_mail/__manifest__.py new file mode 100644 index 000000000..a402dce47 --- /dev/null +++ b/fastapi_log_mail/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fastapi Log", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "PyTech, Odoo Community Association (OCA)", + "maintainers": [ + "SirPyTech", + ], + "website": "https://github.com/OCA/rest-framework", + "summary": "Email exceptions of Endpoints.", + "category": "Tools", + "depends": [ + "fastapi_log", + "mail", + ], + "data": [ + "views/fastapi_endpoint_views.xml", + ], +} diff --git a/fastapi_log_mail/models/__init__.py b/fastapi_log_mail/models/__init__.py new file mode 100644 index 000000000..89f5ea517 --- /dev/null +++ b/fastapi_log_mail/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import api_log +from . import fastapi_endpoint diff --git a/fastapi_log_mail/models/api_log.py b/fastapi_log_mail/models/api_log.py new file mode 100644 index 000000000..257bd642f --- /dev/null +++ b/fastapi_log_mail/models/api_log.py @@ -0,0 +1,16 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import models + + +class FastapiLog(models.Model): + _inherit = "api.log" + + def log_exception(self, exception): + res = super().log_exception(exception) + mail_template = self.fastapi_endpoint_id.fastapi_log_mail_template_id + if mail_template: + mail_template.sudo().send_mail(self.id) + return res diff --git a/fastapi_log_mail/models/fastapi_endpoint.py b/fastapi_log_mail/models/fastapi_endpoint.py new file mode 100644 index 000000000..0aef2c454 --- /dev/null +++ b/fastapi_log_mail/models/fastapi_endpoint.py @@ -0,0 +1,15 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + fastapi_log_mail_template_id = fields.Many2one( + comodel_name="mail.template", + domain=[("model_id.model", "=", "api.log")], + string="Error E-mail Template", + help="Select the email template that will be sent when an error is logged.", + ) diff --git a/fastapi_log_mail/readme/CONFIGURE.md b/fastapi_log_mail/readme/CONFIGURE.md new file mode 100644 index 000000000..fd221d770 --- /dev/null +++ b/fastapi_log_mail/readme/CONFIGURE.md @@ -0,0 +1 @@ +In any endpoint that has logging enabled, insert an email template in "Error E-mail Template". \ No newline at end of file diff --git a/fastapi_log_mail/readme/CONTRIBUTORS.md b/fastapi_log_mail/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..6e720b67d --- /dev/null +++ b/fastapi_log_mail/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [PyTech](https://www.pytech.it): + - Simone Rubino \<\> diff --git a/fastapi_log_mail/readme/DESCRIPTION.md b/fastapi_log_mail/readme/DESCRIPTION.md new file mode 100644 index 000000000..8eccf80b8 --- /dev/null +++ b/fastapi_log_mail/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows to send an email when an exception occurs in an endpoint. diff --git a/fastapi_log_mail/static/description/index.html b/fastapi_log_mail/static/description/index.html new file mode 100644 index 000000000..1ec0ff159 --- /dev/null +++ b/fastapi_log_mail/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +Fastapi Log + + + +
    +

    Fastapi Log

    + + +

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    This module allows to send an email when an exception occurs in an +endpoint.

    +

    Table of contents

    + +
    +

    Configuration

    +

    In any endpoint that has logging enabled, insert an email template in +“Error E-mail Template”.

    +
    +
    +

    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.

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • PyTech
    • +
    +
    +
    +

    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.

    +

    Current maintainer:

    +

    SirPyTech

    +

    This module is part of the OCA/rest-framework project on GitHub.

    +

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

    +
    +
    +
    + + diff --git a/fastapi_log_mail/tests/__init__.py b/fastapi_log_mail/tests/__init__.py new file mode 100644 index 000000000..0d3e465bc --- /dev/null +++ b/fastapi_log_mail/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fastapi_log_mail diff --git a/fastapi_log_mail/tests/test_fastapi_log_mail.py b/fastapi_log_mail/tests/test_fastapi_log_mail.py new file mode 100644 index 000000000..74cad8ba8 --- /dev/null +++ b/fastapi_log_mail/tests/test_fastapi_log_mail.py @@ -0,0 +1,50 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import os +import unittest + +from odoo.addons.api_log.tests.common import CommonAPILog +from odoo.addons.fastapi.schemas import DemoExceptionType +from odoo.addons.mail.tests.common import MailCase + + +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") +class FastAPIEncryptedErrorsCase(CommonAPILog, MailCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") + + cls.fastapi_demo_app._handle_registry_sync() + cls.fastapi_demo_app.log_requests = True + cls.fastapi_demo_app.fastapi_log_mail_template_id = cls.env[ + "mail.template" + ].create( + { + "name": "Test exception email template", + "model_id": cls.env.ref("api_log.model_api_log").id, + } + ) + + def test_endpoint_exception_send_email(self): + """If an endpoint has an email template, + when an exception occurs an email is sent using the configured template. + """ + # Arrange + mail_template = self.fastapi_demo_app.fastapi_log_mail_template_id + route = ( + "/fastapi_demo/demo/exception?" + f"exception_type={DemoExceptionType.user_error.value}" + "&error_message=User Error" + ) + # pre-condition + self.assertTrue(mail_template) + + # Act + with self.mock_mail_gateway(): + self.url_open(route, timeout=200) + + # Assert + sent_email = self._filter_mail() + self.assertTrue(sent_email) diff --git a/fastapi_log_mail/views/fastapi_endpoint_views.xml b/fastapi_log_mail/views/fastapi_endpoint_views.xml new file mode 100644 index 000000000..fdd6deaaa --- /dev/null +++ b/fastapi_log_mail/views/fastapi_endpoint_views.xml @@ -0,0 +1,24 @@ + + + + + fastapi.endpoint + + + + + + + + diff --git a/setup/fastapi_log_mail/odoo/addons/fastapi_log_mail b/setup/fastapi_log_mail/odoo/addons/fastapi_log_mail new file mode 120000 index 000000000..0708fcac1 --- /dev/null +++ b/setup/fastapi_log_mail/odoo/addons/fastapi_log_mail @@ -0,0 +1 @@ +../../../../fastapi_log_mail \ No newline at end of file diff --git a/setup/fastapi_log_mail/setup.py b/setup/fastapi_log_mail/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/fastapi_log_mail/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 7df6af28d2427a2e60665ee817443c896f9de783 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Mon, 28 Apr 2025 12:00:02 +0200 Subject: [PATCH 53/58] [FIX] fastapi_log: Manage multi-slash endpoints --- fastapi_log/fastapi_dispatcher.py | 3 +-- fastapi_log/tests/test_fastapi_log.py | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/fastapi_log/fastapi_dispatcher.py b/fastapi_log/fastapi_dispatcher.py index 6a762cd51..a870ed407 100644 --- a/fastapi_log/fastapi_dispatcher.py +++ b/fastapi_log/fastapi_dispatcher.py @@ -21,11 +21,10 @@ class FastApiDispatcher(_dispatchers.get("fastapi", BaseFastApiDispatcher)): def dispatch(self, endpoint, args): self.request.params = {} environ = self._get_environ() - root_path = "/" + environ["PATH_INFO"].split("/")[1] fastapi_endpoint = ( self.request.env["fastapi.endpoint"] .sudo() - .search([("root_path", "=", root_path)]) + ._get_endpoint(environ["PATH_INFO"]) ) if fastapi_endpoint.log_requests: if tools.config["test_enable"]: diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py index 085c9a410..584c36078 100644 --- a/fastapi_log/tests/test_fastapi_log.py +++ b/fastapi_log/tests/test_fastapi_log.py @@ -21,6 +21,7 @@ class FastAPIEncryptedErrorsCase(CommonAPILog): def setUpClass(cls): super().setUpClass() cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") + cls.fastapi_demo_app.root_path += "/test" cls.fastapi_demo_app._handle_registry_sync() cls.fastapi_demo_app.write({"log_requests": True}) lang = ( @@ -57,19 +58,19 @@ def test_no_log_if_disabled(self): self.fastapi_demo_app.write({"log_requests": False}) with self.log_capturer() as capturer: - response = self.url_open("/fastapi_demo/demo", timeout=200) + response = self.url_open("/fastapi_demo/test/demo", timeout=200) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertFalse(capturer.records) def test_log_simple(self): with self.log_capturer() as capturer: - response = self.url_open("/fastapi_demo/demo", timeout=200) + response = self.url_open("/fastapi_demo/test/demo", timeout=200) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(capturer.records), 1) log = capturer.records[0] - self.assertTrue(log.request_url.endswith("/fastapi_demo/demo")) + self.assertTrue(log.request_url.endswith("/fastapi_demo/test/demo")) self.assertEqual(log.request_method, "GET") self.assertEqual(log.response_status_code, 200) self.assertTrue(log.time > 0) @@ -77,7 +78,7 @@ def test_log_simple(self): def test_log_exception(self): with self.log_capturer() as capturer: route = ( - "/fastapi_demo/demo/exception?" + "/fastapi_demo/test/demo/exception?" f"exception_type={DemoExceptionType.user_error.value}" "&error_message=User Error" ) @@ -86,7 +87,7 @@ def test_log_exception(self): self.assertEqual(len(capturer.records), 1) log = capturer.records[0] - self.assertIn("/fastapi_demo/demo/exception", log.request_url) + self.assertIn("/fastapi_demo/test/demo/exception", log.request_url) self.assertEqual(log.request_method, "GET") self.assertEqual(log.response_status_code, 400) self.assertTrue(log.time > 0) @@ -97,7 +98,7 @@ def test_log_exception(self): def test_log_bare_exception(self): with self.log_capturer() as capturer: route = ( - "/fastapi_demo/demo/exception?" + "/fastapi_demo/test/demo/exception?" f"exception_type={DemoExceptionType.bare_exception.value}" "&error_message=Internal Server Error" ) @@ -108,7 +109,7 @@ def test_log_bare_exception(self): self.assertEqual(len(capturer.records), 1) log = capturer.records[0] - self.assertIn("/fastapi_demo/demo/exception", log.request_url) + self.assertIn("/fastapi_demo/test/demo/exception", log.request_url) self.assertEqual(log.request_method, "GET") self.assertEqual(log.response_status_code, 500) self.assertTrue(log.time > 0) @@ -119,7 +120,7 @@ def test_log_bare_exception(self): def test_log_retrying_post(self): with self.log_capturer() as capturer: nbr_retries = 2 - route = f"/fastapi_demo/demo/retrying?nbr_retries={nbr_retries}" + route = f"/fastapi_demo/test/demo/retrying?nbr_retries={nbr_retries}" response = self.url_open( route, timeout=20, files={"file": ("test.txt", b"test")} ) @@ -130,7 +131,7 @@ def test_log_retrying_post(self): self.assertEqual(len(capturer.records), 3) for log in capturer.records[0:-1]: - self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertIn("/fastapi_demo/test/demo/retrying", log.request_url) self.assertEqual(log.request_method, "POST") self.assertEqual(log.response_status_code, 500) self.assertTrue(log.time > 0) @@ -142,7 +143,7 @@ def test_log_retrying_post(self): ) log = capturer.records[-1] - self.assertIn("/fastapi_demo/demo/retrying", log.request_url) + self.assertIn("/fastapi_demo/test/demo/retrying", log.request_url) self.assertEqual(log.request_method, "POST") self.assertEqual(log.response_status_code, 200) self.assertTrue(log.time > 0) From 2548ca950af041fa98428610b10f601308d56420 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Mon, 28 Apr 2025 12:42:53 +0200 Subject: [PATCH 54/58] [IMP] api_log: Hide sensitive headers --- api_log/models/api_log.py | 23 ++++++++++++++++++++++- api_log/tests/test_api_log.py | 12 ++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/api_log/models/api_log.py b/api_log/models/api_log.py index 16bc7b973..a74ea1621 100644 --- a/api_log/models/api_log.py +++ b/api_log/models/api_log.py @@ -59,9 +59,30 @@ class APILog(models.Model): compute="_compute_response_headers_derived", store=True ) + @api.model + def _headers_hidden_keys(self): + """Header keys that should not be logged. + + They might contains sensitive data. + """ + return ( + "Api-Key", + "Cookie", + ) + + @api.model + def _sanitize_headers_dict(self, headers_dict): + keys_to_hide = self._headers_hidden_keys() + for key in headers_dict: + if key in keys_to_hide: + headers_dict[key] = "" + return headers_dict + + @api.model def _headers_to_dict(self, headers): try: - return {key.lower(): value for key, value in headers.items()} + headers_dict = {key: value for key, value in headers.items()} + return self._sanitize_headers_dict(headers_dict) except AttributeError: return {} diff --git a/api_log/tests/test_api_log.py b/api_log/tests/test_api_log.py index abed32678..52035a4c9 100644 --- a/api_log/tests/test_api_log.py +++ b/api_log/tests/test_api_log.py @@ -12,7 +12,15 @@ class TestAPILog(CommonAPILog): def test_log_request(self): base_url = self.base_url() + secret_api_key = "my-secret-api-key" + secret_cookie = "my-secret-biscuit" + public_header_value = "public_header_value" httprequest = requests.Request( + headers={ + "Api-Key": secret_api_key, + "Cookie": secret_cookie, + "Public-Header": public_header_value, + }, url=base_url, method="GET", ) @@ -21,6 +29,10 @@ def test_log_request(self): self.assertEqual(log.request_url, base_url) self.assertEqual(log.request_method, "GET") + headers_values = log.request_headers.values() + self.assertNotIn(secret_api_key, headers_values) + self.assertNotIn(secret_cookie, headers_values) + self.assertIn(public_header_value, headers_values) def test_log_response(self): response = Response() From 03327899a3f5795e073e8cc8f23c030e773d5210 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 25 Jun 2025 11:42:48 +0200 Subject: [PATCH 55/58] [IMP] api_log: Add collection of logs --- api_log/README.rst | 3 ++ api_log/models/__init__.py | 1 + api_log/models/api_log.py | 36 +++++++++++++++++++++ api_log/models/api_log_collection.py | 46 +++++++++++++++++++++++++++ api_log/static/description/index.html | 4 +++ api_log/tests/common.py | 5 ++- api_log/tests/test_api_log.py | 5 +-- api_log/views/api_log_views.xml | 8 +++++ 8 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 api_log/models/api_log_collection.py diff --git a/api_log/README.rst b/api_log/README.rst index 842595a30..1de6387d3 100644 --- a/api_log/README.rst +++ b/api_log/README.rst @@ -57,6 +57,9 @@ Contributors ------------ - Florian Mounier florian.mounier@akretion.com +- `PyTech `__: + + - Simone Rubino Maintainers ----------- diff --git a/api_log/models/__init__.py b/api_log/models/__init__.py index 0f340289c..2f4388e55 100644 --- a/api_log/models/__init__.py +++ b/api_log/models/__init__.py @@ -1 +1,2 @@ +from . import api_log_collection from . import api_log diff --git a/api_log/models/api_log.py b/api_log/models/api_log.py index a74ea1621..362c49887 100644 --- a/api_log/models/api_log.py +++ b/api_log/models/api_log.py @@ -1,6 +1,8 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + import base64 import json import time @@ -15,6 +17,21 @@ class APILog(models.Model): _name = "api.log" _description = "Log for API" + collection_ref = fields.Reference( + selection="_selection_collection_ref", + index=True, + ) + collection_model = fields.Char( + compute="_compute_collection", + store=True, + index=True, + ) + collection_id = fields.Integer( + compute="_compute_collection", + store=True, + index=True, + ) + # Request request_url = fields.Char() request_method = fields.Char() @@ -59,6 +76,25 @@ class APILog(models.Model): compute="_compute_response_headers_derived", store=True ) + @api.model + def _selection_collection_ref(self): + return [] + + @api.depends( + "collection_ref", + ) + def _compute_collection(self): + for log in self: + collection = log.collection_ref + if collection: + collection_model = collection._name + collection_id = collection.id + else: + collection_model = False + collection_id = False + log.collection_model = collection_model + log.collection_id = collection_id + @api.model def _headers_hidden_keys(self): """Header keys that should not be logged. diff --git a/api_log/models/api_log_collection.py b/api_log/models/api_log_collection.py new file mode 100644 index 000000000..93b0f3ece --- /dev/null +++ b/api_log/models/api_log_collection.py @@ -0,0 +1,46 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class APILogCollection(models.AbstractModel): + _name = "api.log_collection.mixin" + _description = "Collection of API logs" + + log_requests = fields.Boolean( + help="Log requests in database.", + ) + + log_ids = fields.One2many( + comodel_name="api.log", + compute="_compute_log_ids", + string="Logs", + ) + + def _compute_log_ids(self): + for collection in self: + collection.log_ids = self.env["api.log"].search( + [("collection_ref", "=", "%s,%s" % (collection._name, collection.id))] + ) + + def action_logs(self): + collections_refs = [ + "%s,%s" % (collection._name, collection.id) for collection in self + ] + return { + "type": "ir.actions.act_window", + "res_model": "api.log", + "name": "Logs", + "view_type": "form", + "view_mode": "tree,form", + "target": "current", + "domain": [ + ( + "collection_ref", + "in", + collections_refs, + ), + ], + "context": dict(self.env.context), + } diff --git a/api_log/static/description/index.html b/api_log/static/description/index.html index b37001907..e00096eb9 100644 --- a/api_log/static/description/index.html +++ b/api_log/static/description/index.html @@ -403,6 +403,10 @@

    Authors

    Contributors

    diff --git a/api_log/tests/common.py b/api_log/tests/common.py index e54316db6..e02138286 100644 --- a/api_log/tests/common.py +++ b/api_log/tests/common.py @@ -1,7 +1,10 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + from odoo.tests.common import HttpCase -class CommonAPILog(HttpCase): +class Common(HttpCase): @classmethod def setUpClass(cls): super().setUpClass() diff --git a/api_log/tests/test_api_log.py b/api_log/tests/test_api_log.py index 52035a4c9..3a3868231 100644 --- a/api_log/tests/test_api_log.py +++ b/api_log/tests/test_api_log.py @@ -1,15 +1,16 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import requests from odoo.http import Request, Response -from odoo.addons.api_log.tests.common import CommonAPILog +from odoo.addons.api_log.tests.common import Common -class TestAPILog(CommonAPILog): +class TestAPILog(Common): def test_log_request(self): base_url = self.base_url() secret_api_key = "my-secret-api-key" diff --git a/api_log/views/api_log_views.xml b/api_log/views/api_log_views.xml index cc09bbdaf..4e8ba689f 100644 --- a/api_log/views/api_log_views.xml +++ b/api_log/views/api_log_views.xml @@ -45,6 +45,7 @@ + api.log + @@ -78,6 +80,7 @@ + + Date: Wed, 25 Jun 2025 11:44:12 +0200 Subject: [PATCH 56/58] [ADD] api_log_mail: Notify user about logged exceptions --- api_log_mail/README.rst | 93 +++++ api_log_mail/__init__.py | 1 + api_log_mail/__manifest__.py | 19 + api_log_mail/models/__init__.py | 4 + api_log_mail/models/api_log.py | 41 ++ api_log_mail/models/api_log_collection.py | 21 + api_log_mail/readme/CONFIGURE.md | 1 + api_log_mail/readme/CONTRIBUTORS.md | 2 + api_log_mail/readme/DESCRIPTION.md | 1 + api_log_mail/static/description/index.html | 435 ++++++++++++++++++++ setup/api_log_mail/odoo/addons/api_log_mail | 1 + setup/api_log_mail/setup.py | 6 + 12 files changed, 625 insertions(+) create mode 100644 api_log_mail/README.rst create mode 100644 api_log_mail/__init__.py create mode 100644 api_log_mail/__manifest__.py create mode 100644 api_log_mail/models/__init__.py create mode 100644 api_log_mail/models/api_log.py create mode 100644 api_log_mail/models/api_log_collection.py create mode 100644 api_log_mail/readme/CONFIGURE.md create mode 100644 api_log_mail/readme/CONTRIBUTORS.md create mode 100644 api_log_mail/readme/DESCRIPTION.md create mode 100644 api_log_mail/static/description/index.html create mode 120000 setup/api_log_mail/odoo/addons/api_log_mail create mode 100644 setup/api_log_mail/setup.py diff --git a/api_log_mail/README.rst b/api_log_mail/README.rst new file mode 100644 index 000000000..57e228500 --- /dev/null +++ b/api_log_mail/README.rst @@ -0,0 +1,93 @@ +==================== +API Log notification +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ef0c0bceb8ae27bcfebaebc22e2fb4747475f2a2c60dd2d410bc40b6efee9b6a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/16.0/api_log_mail + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-api_log_mail + :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/rest-framework&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to create an activity when an exception is logged in +an API logs collection. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +In any log collection that has logging enabled, insert an activity type +in "Error Activity type". + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* PyTech + +Contributors +------------ + +- `PyTech `__: + + - Simone Rubino + +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-SirPyTech| image:: https://github.com/SirPyTech.png?size=40px + :target: https://github.com/SirPyTech + :alt: SirPyTech + +Current `maintainer `__: + +|maintainer-SirPyTech| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/api_log_mail/__init__.py b/api_log_mail/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/api_log_mail/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/api_log_mail/__manifest__.py b/api_log_mail/__manifest__.py new file mode 100644 index 000000000..d334b7f9c --- /dev/null +++ b/api_log_mail/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "API Log notification", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "PyTech, Odoo Community Association (OCA)", + "maintainers": [ + "SirPyTech", + ], + "website": "https://github.com/OCA/rest-framework", + "summary": "Notify logged exceptions.", + "category": "Tools", + "depends": [ + "api_log", + "mail", + ], +} diff --git a/api_log_mail/models/__init__.py b/api_log_mail/models/__init__.py new file mode 100644 index 000000000..13ae7379a --- /dev/null +++ b/api_log_mail/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import api_log_collection +from . import api_log diff --git a/api_log_mail/models/api_log.py b/api_log_mail/models/api_log.py new file mode 100644 index 000000000..3973453c9 --- /dev/null +++ b/api_log_mail/models/api_log.py @@ -0,0 +1,41 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class FastapiLog(models.Model): + _name = "api.log" + _inherit = [ + "api.log", + "mail.activity.mixin", + # mail.thread is needed + # because message_subscribe is called + # during activity creation + "mail.thread", + ] + _mail_post_access = "read" # Access required to open an activity + + @api.model + def log_request(self, request, override_log_values=None): + return super( + FastapiLog, + self.with_context(tracking_disable=True), + ).log_request(request, override_log_values=override_log_values) + + def _notify_api_log_exception(self): + if collection := self.collection_ref: + activity_type = collection.api_log_mail_exception_activity_type_id + if activity_type: + self.sudo().activity_schedule( + activity_type_id=activity_type.id, + ) + + mail_template = collection.api_log_mail_exception_template_id + if mail_template: + mail_template.sudo().send_mail(self.id) + + def log_exception(self, exception): + res = super().log_exception(exception) + self._notify_api_log_exception() + return res diff --git a/api_log_mail/models/api_log_collection.py b/api_log_mail/models/api_log_collection.py new file mode 100644 index 000000000..cf7a87336 --- /dev/null +++ b/api_log_mail/models/api_log_collection.py @@ -0,0 +1,21 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class APILogCollection(models.AbstractModel): + _inherit = "api.log_collection.mixin" + + api_log_mail_exception_template_id = fields.Many2one( + comodel_name="mail.template", + domain=[("model_id.model", "=", "api.log")], + string="Error E-mail Template", + help="An email based on this template will be sent when an error is logged.", + ) + api_log_mail_exception_activity_type_id = fields.Many2one( + comodel_name="mail.activity.type", + domain=[("res_model", "=", "api.log")], + string="Error Activity type", + help="An activity of this type will be created when an error is logged.", + ) diff --git a/api_log_mail/readme/CONFIGURE.md b/api_log_mail/readme/CONFIGURE.md new file mode 100644 index 000000000..8c16db49d --- /dev/null +++ b/api_log_mail/readme/CONFIGURE.md @@ -0,0 +1 @@ +In any log collection that has logging enabled, insert an activity type in "Error Activity type". diff --git a/api_log_mail/readme/CONTRIBUTORS.md b/api_log_mail/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..6e720b67d --- /dev/null +++ b/api_log_mail/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [PyTech](https://www.pytech.it): + - Simone Rubino \<\> diff --git a/api_log_mail/readme/DESCRIPTION.md b/api_log_mail/readme/DESCRIPTION.md new file mode 100644 index 000000000..f1207db61 --- /dev/null +++ b/api_log_mail/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows to create an activity when an exception is logged in an API logs collection. diff --git a/api_log_mail/static/description/index.html b/api_log_mail/static/description/index.html new file mode 100644 index 000000000..2d5a03422 --- /dev/null +++ b/api_log_mail/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +API Log notification + + + +
    +

    API Log notification

    + + +

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    +

    This module allows to create an activity when an exception is logged in +an API logs collection.

    +

    Table of contents

    + +
    +

    Configuration

    +

    In any log collection that has logging enabled, insert an activity type +in “Error Activity type”.

    +
    +
    +

    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.

    +

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

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • PyTech
    • +
    +
    +
    +

    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.

    +

    Current maintainer:

    +

    SirPyTech

    +

    This module is part of the OCA/rest-framework project on GitHub.

    +

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

    +
    +
    +
    + + diff --git a/setup/api_log_mail/odoo/addons/api_log_mail b/setup/api_log_mail/odoo/addons/api_log_mail new file mode 120000 index 000000000..987cf27bc --- /dev/null +++ b/setup/api_log_mail/odoo/addons/api_log_mail @@ -0,0 +1 @@ +../../../../api_log_mail \ No newline at end of file diff --git a/setup/api_log_mail/setup.py b/setup/api_log_mail/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/api_log_mail/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 70b3eceacfd036f0aef00b485aac4bba5e0a4bae Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 25 Jun 2025 11:45:23 +0200 Subject: [PATCH 57/58] [IMP] fastapi_log: Adapt to log collection --- fastapi_log/README.rst | 5 +- fastapi_log/__manifest__.py | 2 +- fastapi_log/fastapi_dispatcher.py | 24 +++------ .../migrations/16.0.1.1.0/post-migration.py | 32 ++++++++++++ .../migrations/16.0.1.1.0/pre-migration.py | 20 ++++++++ fastapi_log/models/api_log.py | 23 ++++++--- fastapi_log/models/fastapi_endpoint.py | 37 +++----------- fastapi_log/readme/CONTRIBUTORS.md | 2 + fastapi_log/static/description/index.html | 4 ++ fastapi_log/tests/common.py | 35 +++++++++++++ fastapi_log/tests/test_fastapi_log.py | 50 ++----------------- fastapi_log/views/fastapi_endpoint_views.xml | 25 +++++----- fastapi_log/views/fastapi_log_views.xml | 43 +--------------- 13 files changed, 147 insertions(+), 155 deletions(-) create mode 100644 fastapi_log/migrations/16.0.1.1.0/post-migration.py create mode 100644 fastapi_log/migrations/16.0.1.1.0/pre-migration.py create mode 100644 fastapi_log/tests/common.py diff --git a/fastapi_log/README.rst b/fastapi_log/README.rst index 6464683d2..fdb54e937 100644 --- a/fastapi_log/README.rst +++ b/fastapi_log/README.rst @@ -71,7 +71,10 @@ Authors Contributors ------------ -- Florian Mounier florian.mounier@akretion.com +- Florian Mounier florian.mounier@akretion.com +- `PyTech `__: + + - Simone Rubino Maintainers ----------- diff --git a/fastapi_log/__manifest__.py b/fastapi_log/__manifest__.py index 8334dc5c2..27938d00e 100644 --- a/fastapi_log/__manifest__.py +++ b/fastapi_log/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Fastapi Log", - "version": "16.0.1.0.0", + "version": "16.0.1.1.0", "author": "Akretion, Odoo Community Association (OCA)", "summary": "Log Fastapi requests in database", "category": "Tools", diff --git a/fastapi_log/fastapi_dispatcher.py b/fastapi_log/fastapi_dispatcher.py index a870ed407..ba5071083 100644 --- a/fastapi_log/fastapi_dispatcher.py +++ b/fastapi_log/fastapi_dispatcher.py @@ -1,9 +1,10 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + import logging -from odoo import registry, tools from odoo.http import _dispatchers from odoo.addons.fastapi.fastapi_dispatcher import ( @@ -27,16 +28,8 @@ def dispatch(self, endpoint, args): ._get_endpoint(environ["PATH_INFO"]) ) if fastapi_endpoint.log_requests: - if tools.config["test_enable"]: - cr = getattr( - self.request.env.registry, "test_log_cr", self.request.env.cr - ) - else: - # Create an independent cursor - cr = registry(self.request.env.cr.dbname).cursor() - - env = self.request.env(cr=cr, su=True) request = self.request + env = request.env(su=True) try: log = env["api.log"].log_request(request) except Exception as e: @@ -50,18 +43,17 @@ def dispatch(self, endpoint, args): log and log.log_exception(response_exc) except Exception as e: _logger.warning("Failed to log exception", exc_info=e) + else: + # Be sure to commit/save the exception's log + env.cr.commit() + raise response_exc else: try: log and log.log_response(response) except Exception as e: _logger.warning("Failed to log response", exc_info=e) - finally: - if not tools.config["test_enable"]: - try: - cr.commit() # pylint: disable=E8102 - finally: - cr.close() + return response else: return super().dispatch(endpoint, args) diff --git a/fastapi_log/migrations/16.0.1.1.0/post-migration.py b/fastapi_log/migrations/16.0.1.1.0/post-migration.py new file mode 100644 index 000000000..b95831de3 --- /dev/null +++ b/fastapi_log/migrations/16.0.1.1.0/post-migration.py @@ -0,0 +1,32 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + endpoint_id_column = openupgrade.get_legacy_name("fastapi_endpoint_id") + openupgrade.logged_query( + env.cr, + """ + UPDATE api_log SET + collection_id=%(endpoint_id_column)s, + collection_model='fastapi.endpoint', + collection_ref='fastapi.endpoint,' || %(endpoint_id_column)s + WHERE %(endpoint_id_column)s IS NOT NULL + """ + % { + "endpoint_id_column": endpoint_id_column, + }, + ) + openupgrade.logged_query( + env.cr, + """ + ALTER TABLE api_log + DROP COLUMN %(endpoint_id_column)s + """ + % { + "endpoint_id_column": endpoint_id_column, + }, + ) diff --git a/fastapi_log/migrations/16.0.1.1.0/pre-migration.py b/fastapi_log/migrations/16.0.1.1.0/pre-migration.py new file mode 100644 index 000000000..9cdaa9143 --- /dev/null +++ b/fastapi_log/migrations/16.0.1.1.0/pre-migration.py @@ -0,0 +1,20 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + openupgrade.copy_columns( + env.cr, + { + "api_log": [ + ( + "fastapi_endpoint_id", + None, + None, + ), + ], + }, + ) diff --git a/fastapi_log/models/api_log.py b/fastapi_log/models/api_log.py index ee28db599..086e656b4 100644 --- a/fastapi_log/models/api_log.py +++ b/fastapi_log/models/api_log.py @@ -1,21 +1,24 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from starlette.exceptions import HTTPException as StarletteHTTPException -from odoo import api, fields, models +from odoo import api, models class FastapiLog(models.Model): _inherit = "api.log" - fastapi_endpoint_id = fields.Many2one( - comodel_name="fastapi.endpoint", - string="Endpoint", - ondelete="cascade", - index=True, - ) + @api.model + def _selection_collection_ref(self): + collections = super()._selection_collection_ref() + fastapi_endpoint_model = self.env["fastapi.endpoint"] + collections.append( + (fastapi_endpoint_model._name, fastapi_endpoint_model._description) + ) + return collections @api.model def _get_request_body(self, request): @@ -42,7 +45,11 @@ def _prepare_log_request(self, request): .sudo() ._get_endpoint(environ["PATH_INFO"]) ) - log_request_values["fastapi_endpoint_id"] = endpoint.id + log_request_values["collection_ref"] = "%s,%s" % ( + endpoint._name, + endpoint.id, + ) + return log_request_values def _prepare_log_exception(self, exception): diff --git a/fastapi_log/models/fastapi_endpoint.py b/fastapi_log/models/fastapi_endpoint.py index 62789fc4c..4770dcf24 100644 --- a/fastapi_log/models/fastapi_endpoint.py +++ b/fastapi_log/models/fastapi_endpoint.py @@ -1,37 +1,14 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import models class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" - - log_requests = fields.Boolean( - help="Log requests in database.", - ) - - fastapi_log_ids = fields.One2many( - comodel_name="api.log", - inverse_name="fastapi_endpoint_id", - string="Logs", - ) - - fastapi_log_count = fields.Integer( - compute="_compute_fastapi_log_count", - string="Logs Count", - ) - - @api.depends("fastapi_log_ids") - def _compute_fastapi_log_count(self): - groups = self.env["api.log"].read_group( - [("fastapi_endpoint_id", "in", self.ids)], - ["fastapi_endpoint_id"], - ["fastapi_endpoint_id"], - ) - mapped_data = { - g["fastapi_endpoint_id"][0]: g["fastapi_endpoint_id_count"] for g in groups - } - for endpoint in self: - endpoint.fastapi_log_count = mapped_data.get(endpoint.id, 0) + _name = "fastapi.endpoint" + _inherit = [ + "api.log_collection.mixin", + "fastapi.endpoint", + ] diff --git a/fastapi_log/readme/CONTRIBUTORS.md b/fastapi_log/readme/CONTRIBUTORS.md index 328a37da8..1e935bfb5 100644 --- a/fastapi_log/readme/CONTRIBUTORS.md +++ b/fastapi_log/readme/CONTRIBUTORS.md @@ -1 +1,3 @@ - Florian Mounier +- [PyTech](https://www.pytech.it): + - Simone Rubino \<\> diff --git a/fastapi_log/static/description/index.html b/fastapi_log/static/description/index.html index 0a76e9f5d..b0b206a30 100644 --- a/fastapi_log/static/description/index.html +++ b/fastapi_log/static/description/index.html @@ -416,6 +416,10 @@

    Authors

    Contributors

    diff --git a/fastapi_log/tests/common.py b/fastapi_log/tests/common.py new file mode 100644 index 000000000..186c8542e --- /dev/null +++ b/fastapi_log/tests/common.py @@ -0,0 +1,35 @@ +# Copyright 2025 Akretion (http://www.akretion.com). +# @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager + +from odoo.tests.common import RecordCapturer + +from odoo.addons.api_log.tests.common import Common as CommonAPILog + + +class Common(CommonAPILog): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") + cls.fastapi_demo_app.root_path += "/test" + cls.fastapi_demo_app._handle_registry_sync() + cls.fastapi_demo_app.write({"log_requests": True}) + lang = ( + cls.env["res.lang"] + .with_context(active_test=False) + .search([("code", "=", "fr_BE")]) + ) + lang.active = True + + @contextmanager + def log_capturer(self): + app = self.fastapi_demo_app + with RecordCapturer( + self.env[self.log_model._name], + [("collection_ref", "=", "%s,%s" % (app._name, app.id))], + ) as capturer: + yield capturer diff --git a/fastapi_log/tests/test_fastapi_log.py b/fastapi_log/tests/test_fastapi_log.py index 584c36078..dc8efff62 100644 --- a/fastapi_log/tests/test_fastapi_log.py +++ b/fastapi_log/tests/test_fastapi_log.py @@ -1,59 +1,19 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + import os -import threading import unittest -from contextlib import contextmanager - -from odoo.sql_db import TestCursor -from odoo.tests.common import RecordCapturer -from odoo.addons.api_log.tests.common import CommonAPILog from odoo.addons.fastapi.schemas import DemoExceptionType +from odoo.addons.fastapi_log.tests.common import Common from fastapi import status -@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") -class FastAPIEncryptedErrorsCase(CommonAPILog): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") - cls.fastapi_demo_app.root_path += "/test" - cls.fastapi_demo_app._handle_registry_sync() - cls.fastapi_demo_app.write({"log_requests": True}) - lang = ( - cls.env["res.lang"] - .with_context(active_test=False) - .search([("code", "=", "fr_BE")]) - ) - lang.active = True - - def setUp(self): - super().setUp() - # Use a side test cursor to be able to get exception logs - reg = self.env.registry - reg.test_log_lock = threading.RLock() - reg.test_log_cr = TestCursor(reg._db.cursor(), reg.test_log_lock) - - def tearDown(self): - reg = self.env.registry - reg.test_log_cr.rollback() - reg.test_log_cr.close() - reg.test_log_cr = None - reg.test_log_lock = None - super().tearDown() - - @contextmanager - def log_capturer(self): - with RecordCapturer( - self.env(cr=self.env.registry.test_log_cr)[self.log_model._name], - [("fastapi_endpoint_id", "=", self.fastapi_demo_app.id)], - ) as capturer: - yield capturer - +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "TestFastapiLog skipped") +class TestFastapiLog(Common): def test_no_log_if_disabled(self): self.fastapi_demo_app.write({"log_requests": False}) diff --git a/fastapi_log/views/fastapi_endpoint_views.xml b/fastapi_log/views/fastapi_endpoint_views.xml index bf90e7ea1..ac50560e8 100644 --- a/fastapi_log/views/fastapi_endpoint_views.xml +++ b/fastapi_log/views/fastapi_endpoint_views.xml @@ -2,33 +2,32 @@ - - Fastapi Log - api.log - tree,form - [('fastapi_endpoint_id', '=', active_id)] - {'default_fastapi_endpoint_id': active_id} - - - fastapi.endpoint
    +
    diff --git a/fastapi_log/views/fastapi_log_views.xml b/fastapi_log/views/fastapi_log_views.xml index 1f33e422f..3c19ba737 100644 --- a/fastapi_log/views/fastapi_log_views.xml +++ b/fastapi_log/views/fastapi_log_views.xml @@ -2,54 +2,15 @@ - - Add Fastapi fields to API log form view - api.log - - - - - - - - - - Add Fastapi fields to API log tree view - api.log - - - - - - - - - - Add Fastapi fields to API log search view - api.log - - - - - - - - - - - Fastapi Logs api.log [ - ("fastapi_endpoint_id", "!=", False) + ("collection_model", "=", "fastapi.endpoint"), ] tree,form From 7674bd7e31ede72057d51f32dc5a0108a164e22f Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 25 Jun 2025 11:46:38 +0200 Subject: [PATCH 58/58] [IMP] fastapi_log_mail: Adapt to log collection --- fastapi_log_mail/README.rst | 13 ++--- fastapi_log_mail/__init__.py | 1 - fastapi_log_mail/__manifest__.py | 8 +-- .../migrations/16.0.1.1.0/post-migration.py | 32 +++++++++++ .../migrations/16.0.1.1.0/pre-migration.py | 20 +++++++ fastapi_log_mail/models/__init__.py | 4 -- fastapi_log_mail/models/api_log.py | 16 ------ fastapi_log_mail/models/fastapi_endpoint.py | 15 ----- fastapi_log_mail/readme/CONFIGURE.md | 2 +- fastapi_log_mail/readme/DESCRIPTION.md | 2 +- .../static/description/index.html | 13 ++--- .../tests/test_fastapi_log_mail.py | 56 +++++++++++++++---- .../views/fastapi_endpoint_views.xml | 12 +++- 13 files changed, 126 insertions(+), 68 deletions(-) create mode 100644 fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py create mode 100644 fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py delete mode 100644 fastapi_log_mail/models/__init__.py delete mode 100644 fastapi_log_mail/models/api_log.py delete mode 100644 fastapi_log_mail/models/fastapi_endpoint.py diff --git a/fastapi_log_mail/README.rst b/fastapi_log_mail/README.rst index 2f3303c13..af99233ca 100644 --- a/fastapi_log_mail/README.rst +++ b/fastapi_log_mail/README.rst @@ -1,6 +1,6 @@ -=========== -Fastapi Log -=========== +======================== +FastAPI Log notification +======================== .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -28,8 +28,8 @@ Fastapi Log |badge1| |badge2| |badge3| |badge4| |badge5| -This module allows to send an email when an exception occurs in an -endpoint. +This module allows to create an activity when an exception is logged in +a fastapi endpoint. **Table of contents** @@ -39,8 +39,7 @@ endpoint. Configuration ============= -In any endpoint that has logging enabled, insert an email template in -"Error E-mail Template". +Configure a fastapi endpoint as explained in ``api_log_mail``. Bug Tracker =========== diff --git a/fastapi_log_mail/__init__.py b/fastapi_log_mail/__init__.py index 0650744f6..e69de29bb 100644 --- a/fastapi_log_mail/__init__.py +++ b/fastapi_log_mail/__init__.py @@ -1 +0,0 @@ -from . import models diff --git a/fastapi_log_mail/__manifest__.py b/fastapi_log_mail/__manifest__.py index a402dce47..8bcbc0621 100644 --- a/fastapi_log_mail/__manifest__.py +++ b/fastapi_log_mail/__manifest__.py @@ -2,19 +2,19 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { - "name": "Fastapi Log", - "version": "16.0.1.0.0", + "name": "FastAPI Log notification", + "version": "16.0.1.1.0", "license": "AGPL-3", "author": "PyTech, Odoo Community Association (OCA)", "maintainers": [ "SirPyTech", ], "website": "https://github.com/OCA/rest-framework", - "summary": "Email exceptions of Endpoints.", + "summary": "Notify logged exceptions.", "category": "Tools", "depends": [ "fastapi_log", - "mail", + "api_log_mail", ], "data": [ "views/fastapi_endpoint_views.xml", diff --git a/fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py b/fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py new file mode 100644 index 000000000..1903d6e19 --- /dev/null +++ b/fastapi_log_mail/migrations/16.0.1.1.0/post-migration.py @@ -0,0 +1,32 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + template_id_column = openupgrade.get_legacy_name( + "fastapi_log_mail_template_id", + ) + openupgrade.logged_query( + env.cr, + """ + UPDATE fastapi_endpoint SET + api_log_mail_exception_template_id=%(template_id_column)s + WHERE %(template_id_column)s IS NOT NULL + """ + % { + "template_id_column": template_id_column, + }, + ) + openupgrade.logged_query( + env.cr, + """ + ALTER TABLE fastapi_endpoint + DROP COLUMN %(template_id_column)s + """ + % { + "template_id_column": template_id_column, + }, + ) diff --git a/fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py b/fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py new file mode 100644 index 000000000..71f919a98 --- /dev/null +++ b/fastapi_log_mail/migrations/16.0.1.1.0/pre-migration.py @@ -0,0 +1,20 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + openupgrade.copy_columns( + env.cr, + { + "fastapi_endpoint": [ + ( + "fastapi_log_mail_template_id", + None, + None, + ), + ], + }, + ) diff --git a/fastapi_log_mail/models/__init__.py b/fastapi_log_mail/models/__init__.py deleted file mode 100644 index 89f5ea517..000000000 --- a/fastapi_log_mail/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from . import api_log -from . import fastapi_endpoint diff --git a/fastapi_log_mail/models/api_log.py b/fastapi_log_mail/models/api_log.py deleted file mode 100644 index 257bd642f..000000000 --- a/fastapi_log_mail/models/api_log.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2025 Simone Rubino - PyTech -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - - -from odoo import models - - -class FastapiLog(models.Model): - _inherit = "api.log" - - def log_exception(self, exception): - res = super().log_exception(exception) - mail_template = self.fastapi_endpoint_id.fastapi_log_mail_template_id - if mail_template: - mail_template.sudo().send_mail(self.id) - return res diff --git a/fastapi_log_mail/models/fastapi_endpoint.py b/fastapi_log_mail/models/fastapi_endpoint.py deleted file mode 100644 index 0aef2c454..000000000 --- a/fastapi_log_mail/models/fastapi_endpoint.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2025 Simone Rubino - PyTech -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import fields, models - - -class FastapiEndpoint(models.Model): - _inherit = "fastapi.endpoint" - - fastapi_log_mail_template_id = fields.Many2one( - comodel_name="mail.template", - domain=[("model_id.model", "=", "api.log")], - string="Error E-mail Template", - help="Select the email template that will be sent when an error is logged.", - ) diff --git a/fastapi_log_mail/readme/CONFIGURE.md b/fastapi_log_mail/readme/CONFIGURE.md index fd221d770..ca1622a8b 100644 --- a/fastapi_log_mail/readme/CONFIGURE.md +++ b/fastapi_log_mail/readme/CONFIGURE.md @@ -1 +1 @@ -In any endpoint that has logging enabled, insert an email template in "Error E-mail Template". \ No newline at end of file +Configure a fastapi endpoint as explained in `api_log_mail`. diff --git a/fastapi_log_mail/readme/DESCRIPTION.md b/fastapi_log_mail/readme/DESCRIPTION.md index 8eccf80b8..e92d7f261 100644 --- a/fastapi_log_mail/readme/DESCRIPTION.md +++ b/fastapi_log_mail/readme/DESCRIPTION.md @@ -1 +1 @@ -This module allows to send an email when an exception occurs in an endpoint. +This module allows to create an activity when an exception is logged in a fastapi endpoint. diff --git a/fastapi_log_mail/static/description/index.html b/fastapi_log_mail/static/description/index.html index 1ec0ff159..026bfe3b8 100644 --- a/fastapi_log_mail/static/description/index.html +++ b/fastapi_log_mail/static/description/index.html @@ -3,7 +3,7 @@ -Fastapi Log +FastAPI Log notification -
    -

    Fastapi Log

    +
    +

    FastAPI Log notification

    Beta License: AGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

    -

    This module allows to send an email when an exception occurs in an -endpoint.

    +

    This module allows to create an activity when an exception is logged in +a fastapi endpoint.

    Table of contents

      @@ -387,8 +387,7 @@

      Fastapi Log

    Configuration

    -

    In any endpoint that has logging enabled, insert an email template in -“Error E-mail Template”.

    +

    Configure a fastapi endpoint as explained in api_log_mail.

    Bug Tracker

    diff --git a/fastapi_log_mail/tests/test_fastapi_log_mail.py b/fastapi_log_mail/tests/test_fastapi_log_mail.py index 74cad8ba8..e4bde480a 100644 --- a/fastapi_log_mail/tests/test_fastapi_log_mail.py +++ b/fastapi_log_mail/tests/test_fastapi_log_mail.py @@ -1,24 +1,32 @@ # Copyright 2025 Akretion (http://www.akretion.com). # @author Florian Mounier +# Copyright 2025 Simone Rubino - PyTech # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + import os import unittest -from odoo.addons.api_log.tests.common import CommonAPILog from odoo.addons.fastapi.schemas import DemoExceptionType +from odoo.addons.fastapi_log.tests.common import Common from odoo.addons.mail.tests.common import MailCase +from fastapi import status + -@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "FastAPIEncryptedErrorsCase skipped") -class FastAPIEncryptedErrorsCase(CommonAPILog, MailCase): +@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "TestFastapiLogMail skipped") +class TestFastapiLogMail(Common, MailCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo") - - cls.fastapi_demo_app._handle_registry_sync() - cls.fastapi_demo_app.log_requests = True - cls.fastapi_demo_app.fastapi_log_mail_template_id = cls.env[ + cls.fastapi_demo_app.api_log_mail_exception_activity_type_id = cls.env[ + "mail.activity.type" + ].create( + { + "name": "Test exception activity type", + "res_model": "api.log", + } + ) + cls.fastapi_demo_app.api_log_mail_exception_template_id = cls.env[ "mail.template" ].create( { @@ -27,16 +35,42 @@ def setUpClass(cls): } ) + def test_endpoint_exception_create_activity(self): + """If an endpoint has an activity type, + when an exception occurs an activity of the configured type is created. + """ + # Arrange + app = self.fastapi_demo_app + activity_type = app.api_log_mail_exception_activity_type_id + route = ( + "/fastapi_demo/test/demo/exception?" + f"exception_type={DemoExceptionType.user_error.value}" + "&error_message=An error happened" + ) + # pre-condition + self.assertTrue(activity_type) + + # Act + with self.log_capturer() as capturer: + response = self.url_open(route, timeout=200) + + # Assert + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + log = capturer.records + self.assertEqual(len(log), 1) + self.assertTrue(log.activity_ids) + def test_endpoint_exception_send_email(self): """If an endpoint has an email template, when an exception occurs an email is sent using the configured template. """ # Arrange - mail_template = self.fastapi_demo_app.fastapi_log_mail_template_id + app = self.fastapi_demo_app + mail_template = app.api_log_mail_exception_template_id route = ( - "/fastapi_demo/demo/exception?" + "/fastapi_demo/test/demo/exception?" f"exception_type={DemoExceptionType.user_error.value}" - "&error_message=User Error" + "&error_message=An error happened" ) # pre-condition self.assertTrue(mail_template) diff --git a/fastapi_log_mail/views/fastapi_endpoint_views.xml b/fastapi_log_mail/views/fastapi_endpoint_views.xml index fdd6deaaa..6e1d27886 100644 --- a/fastapi_log_mail/views/fastapi_endpoint_views.xml +++ b/fastapi_log_mail/views/fastapi_endpoint_views.xml @@ -5,12 +5,22 @@ --> + Add log mail fields to endpoint form view fastapi.endpoint +