From 53780fe5f029219497866dd9b5079a3fabd41e57 Mon Sep 17 00:00:00 2001 From: David Beal Date: Thu, 9 Oct 2025 12:03:03 +0200 Subject: [PATCH 1/5] IMP fastapi_user_manager: add abstract method to create user --- fastapi_user_manager/routers/user.py | 31 +++++++++++++++------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/fastapi_user_manager/routers/user.py b/fastapi_user_manager/routers/user.py index f4951ac10..35ebd3cd1 100644 --- a/fastapi_user_manager/routers/user.py +++ b/fastapi_user_manager/routers/user.py @@ -16,6 +16,16 @@ user_router = APIRouter(tags=["user"]) +def create_user(env, data): + vals = { + "name": data.name, + "login": data.email, + "phone": data.phone, + "mobile": data.mobile, + } + return env["res.users"].create(vals) + + @user_router.post("/user") def update_user_data( data: UserSc, @@ -26,19 +36,18 @@ def update_user_data( """ update user personal data of authenticated user """ + ##### /!\ Doit on chercher l'utilisateur via le nom et l'email + ##### ou via le mail seulement ? + ##### il me semble que cela est spécifique au projet + ##### je pense qu'il vaudrait mieux faire une methode spécifique + ##### de recherche à surchager dans le projet user = env["res.users"].search( [("name", "=", data.name), ("email", "=", data.email)] ) if user: return UserSc.from_res_user(user) else: - vals = { - "name": data.name, - "login": data.email, - "phone": data.phone, - "mobile": data.mobile, - } - user = env["res.users"].create(vals) + user = create_user(env, data) return UserSc.from_res_user(user) # helper = env["api.user.router"].new() # user = helper.create(data) @@ -82,13 +91,7 @@ def _create_user(self, data: UserSc) -> ResUsers: if user: return user else: - vals = { - "name": data.name, - "email": data.email, - "phone": data.phone, - "mobile": data.mobile, - } - user = self.env["res.users"].create(vals) + user = create_user(self.env, data) return user # def _get_user_values(self, data: CustomerUpdate) -> dict: From 3742d19d6356b4b097e32d5a2ff702a979a563e9 Mon Sep 17 00:00:00 2001 From: David Beal Date: Thu, 9 Oct 2025 12:26:11 +0200 Subject: [PATCH 2/5] add _post_process_user_creation method --- fastapi_user_manager/data/endpoint.xml | 2 +- fastapi_user_manager/routers/user.py | 8 +++++++- fastapi_user_manager/schemas/schemas.py | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/fastapi_user_manager/data/endpoint.xml b/fastapi_user_manager/data/endpoint.xml index 7d903e900..5d20d0387 100644 --- a/fastapi_user_manager/data/endpoint.xml +++ b/fastapi_user_manager/data/endpoint.xml @@ -1,4 +1,4 @@ - + User Manager user_manager diff --git a/fastapi_user_manager/routers/user.py b/fastapi_user_manager/routers/user.py index 35ebd3cd1..61e48a79d 100644 --- a/fastapi_user_manager/routers/user.py +++ b/fastapi_user_manager/routers/user.py @@ -23,7 +23,9 @@ def create_user(env, data): "phone": data.phone, "mobile": data.mobile, } - return env["res.users"].create(vals) + user = env["res.users"].create(vals) + env["api.user.router"]._post_process_user_creation(user, data.misc) + return user @user_router.post("/user") @@ -94,6 +96,10 @@ def _create_user(self, data: UserSc) -> ResUsers: user = create_user(self.env, data) return user + def _post_process_user_creation(self, user, misc): + """inherit it to adapt to your needs""" + pass + # def _get_user_values(self, data: CustomerUpdate) -> dict: # values = data.to_user_vals() # lang_id = data.lang_id diff --git a/fastapi_user_manager/schemas/schemas.py b/fastapi_user_manager/schemas/schemas.py index 6676a6d0d..c4ccb542a 100644 --- a/fastapi_user_manager/schemas/schemas.py +++ b/fastapi_user_manager/schemas/schemas.py @@ -10,6 +10,7 @@ class UserScUpdate(StrictExtendableBaseModel, extra="ignore"): mobile: str | None = None opt_in: bool | None = None lang_id: int | None = None + misc: str | None = None def to_user_vals(self) -> dict: fields = self._get_user_update_fields() @@ -22,6 +23,7 @@ def _get_user_update_fields(self): "name", "phone", "mobile", + "misc", ] @@ -34,6 +36,7 @@ class UserSc(StrictExtendableBaseModel): name: str | None = None phone: str | None = None mobile: str | None = None + misc: str | None = None @classmethod def from_res_user(cls, odoo_rec): @@ -42,4 +45,5 @@ def from_res_user(cls, odoo_rec): name=odoo_rec.name or None, phone=odoo_rec.phone or None, mobile=odoo_rec.mobile or None, + misc=odoo_rec.misc or None, ) From d6ae616dc5578017feb6b4d2014cbcd9a322fc3b Mon Sep 17 00:00:00 2001 From: David Beal Date: Thu, 9 Oct 2025 14:58:47 +0200 Subject: [PATCH 3/5] fixup --- fastapi_user_manager/schemas/schemas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fastapi_user_manager/schemas/schemas.py b/fastapi_user_manager/schemas/schemas.py index c4ccb542a..95db99f82 100644 --- a/fastapi_user_manager/schemas/schemas.py +++ b/fastapi_user_manager/schemas/schemas.py @@ -45,5 +45,4 @@ def from_res_user(cls, odoo_rec): name=odoo_rec.name or None, phone=odoo_rec.phone or None, mobile=odoo_rec.mobile or None, - misc=odoo_rec.misc or None, ) From 3d109fd20785f11de10e5186dfe5302114b2d1ad Mon Sep 17 00:00:00 2001 From: Bonnerue Date: Thu, 9 Oct 2025 23:25:28 +0200 Subject: [PATCH 4/5] =?UTF-8?q?Add=20update/delete/list=5Fcr=C3=A9ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/fastapi_endpoint.py | 2 +- fastapi_user_manager/routers/user.py | 168 +++++++----------- fastapi_user_manager/schemas/schemas.py | 16 +- 3 files changed, 80 insertions(+), 106 deletions(-) diff --git a/fastapi_user_manager/models/fastapi_endpoint.py b/fastapi_user_manager/models/fastapi_endpoint.py index a91aaa1bf..ed74a0d86 100644 --- a/fastapi_user_manager/models/fastapi_endpoint.py +++ b/fastapi_user_manager/models/fastapi_endpoint.py @@ -86,7 +86,7 @@ def api_key_based_authenticated_partner_impl( env["res.users"] .sudo() .search([("api_key_user", "=", api_key)], limit=1) - .parner_id + .partner_id ) if not partner: raise HTTPException( diff --git a/fastapi_user_manager/routers/user.py b/fastapi_user_manager/routers/user.py index 61e48a79d..0d89d908b 100644 --- a/fastapi_user_manager/routers/user.py +++ b/fastapi_user_manager/routers/user.py @@ -10,65 +10,72 @@ from fastapi import APIRouter, Depends -from ..schemas.schemas import UserSc, UserScUpdate +from ..schemas.schemas import UserSc, UserScDel, UserScUpdate # create a router user_router = APIRouter(tags=["user"]) -def create_user(env, data): - vals = { - "name": data.name, - "login": data.email, - "phone": data.phone, - "mobile": data.mobile, - } - user = env["res.users"].create(vals) - env["api.user.router"]._post_process_user_creation(user, data.misc) - return user +@user_router.post("/user/create") +def create_user_data( + data: list[UserSc], + env: Annotated[api.Environment, Depends(odoo_env)], + partner: Annotated[api.Environment, Depends(authenticated_partner_env)], +) -> dict: + """ + create user personal data of authenticated user + """ + + result = {} + for d in data: + user = env["res.users"].search( + ["|", ("login", "=", d.login), ("login", "=", d.email)] + ) + if user: + result[f"{user.name}"] = "No_modif" + else: + user = env["api.user.router"].create_user(d) + result[f"{user.name}"] = "New ok" + + return result -@user_router.post("/user") +@user_router.post("/user/update") def update_user_data( - data: UserSc, + data: UserScUpdate, env: Annotated[api.Environment, Depends(odoo_env)], partner: Annotated[api.Environment, Depends(authenticated_partner_env)], - # user: Annotated[ResUsers, Depends(odoo_env)], -) -> UserSc: +): """ update user personal data of authenticated user """ - ##### /!\ Doit on chercher l'utilisateur via le nom et l'email - ##### ou via le mail seulement ? - ##### il me semble que cela est spécifique au projet - ##### je pense qu'il vaudrait mieux faire une methode spécifique - ##### de recherche à surchager dans le projet - user = env["res.users"].search( - [("name", "=", data.name), ("email", "=", data.email)] + # UserScUpdate.to_user_vals(data) + # helper = env["api.user.router"].new({"user": user}) + updated_user = env["api.user.router"]._update_user(data) + if updated_user: + return "Update OK" + else: + return "No update, no user found" + # + + +@user_router.post("/user/archive") +def archive_user_data( + data: UserScDel, + env: Annotated[api.Environment, Depends(odoo_env)], + partner: Annotated[api.Environment, Depends(authenticated_partner_env)], +): + """ """ + user_to_del = env["res.users"].search( + [ + ("login", "=", data.login), + ] ) - if user: - return UserSc.from_res_user(user) + if user_to_del: + user_to_del.active = False + return "Archived user ok" else: - user = create_user(env, data) - return UserSc.from_res_user(user) - # helper = env["api.user.router"].new() - # user = helper.create(data) - # return UserSc.from_res_user(user) - - -# def update_user_data( -# data: UserScUpdate, -# env: Annotated[api.Environment, Depends(odoo_env)], -# # env: Annotated[api.Environment, Depends(authenticated_partner_env)], -# # user: Annotated[ResUsers, Depends(odoo_env)], -# ) -> ResUsers: -# """ -# update user personal data of authenticated user -# """ -# UserScUpdate.to_user_vals(data) -# helper = env["api.user.router"].new({"user": user}) -# updated_user = helper._update_user(data) -# return UserSc.from_res_user(updated_user) + return "Error" class ApiUserRouter(models.AbstractModel): @@ -78,66 +85,29 @@ class ApiUserRouter(models.AbstractModel): # user = fields.Many2one(comodel_name="res.users") def _update_user(self, data: UserScUpdate) -> ResUsers: - self.ensure_one() + user = self.env["res.users"].search([("login", "=", data.login)]) values = self._get_user_values(data) - user = self.user user.write(values) # self._handle_shopinvader_customer_opt_in(data) return user - def _create_user(self, data: UserSc) -> ResUsers: - self.ensure_one() - user = self.env["res.users"].search( - [("name", "=", data.name), ("email", "=", data.email)] - ) - if user: - return user - else: - user = create_user(self.env, data) - return user + def create_user(self, data: UserSc): + vals = { + "name": data.name, + "login": data.email, + "phone": data.phone, + "mobile": data.mobile, + } + user = self.env["res.users"].create(vals) + user = self._post_process_user_creation(user, data.misc) + return user def _post_process_user_creation(self, user, misc): """inherit it to adapt to your needs""" - pass - - # def _get_user_values(self, data: CustomerUpdate) -> dict: - # values = data.to_user_vals() - # lang_id = data.lang_id - # if bool(lang_id): - # values["lang"] = self.env["res.lang"].browse(lang_id).code - # return values - - -# @user_router.get("/users", response_model=list[UserInfo]) -# def get_users(env: Annotated[Environment, Depends(odoo_env)]) -> list[UserInfo]: -# return [ -# UserInfo(name=user.name, email=user.email or "") -# for user in env["res.users"].search([]) -# ] - - -# @customer_router.get("/customer") -# def get_customer_data( -# partner: Annotated[ResPartner, Depends(authenticated_partner)], -# ) -> Customer: -# """ -# Get customer personal data of authenticated user -# """ -# return Customer.from_res_partner(partner) - - -# @customer_router.post( -# "/customer", -# ) -# def update_customer_data( -# data: CustomerUpdate, -# env: Annotated[api.Environment, Depends(authenticated_partner_env)], -# partner: Annotated[ResPartner, Depends(authenticated_partner)], -# ) -> Customer: -# """ -# update customer personal data of authenticated user -# """ -# CustomerUpdate.to_res_partner_vals(data) -# helper = env["shopinvader_api_customer.router.helper"].new({"partner": partner}) -# updated_partner = helper._update_shopinvader_customer(data) -# return Customer.from_res_partner(updated_partner) + user = user.write(misc) + return user + + def _get_user_values(self, data: UserScUpdate): + """inherit it to adapt to your needs""" + values = data.misc + return values diff --git a/fastapi_user_manager/schemas/schemas.py b/fastapi_user_manager/schemas/schemas.py index 95db99f82..9bec51fab 100644 --- a/fastapi_user_manager/schemas/schemas.py +++ b/fastapi_user_manager/schemas/schemas.py @@ -6,11 +6,8 @@ class UserScUpdate(StrictExtendableBaseModel, extra="ignore"): used to update user details """ - name: str | None = None - mobile: str | None = None - opt_in: bool | None = None - lang_id: int | None = None - misc: str | None = None + login: str | None = None + misc: dict | None = None def to_user_vals(self) -> dict: fields = self._get_user_update_fields() @@ -32,11 +29,12 @@ class UserSc(StrictExtendableBaseModel): used to get user details """ + login: str | None = None email: str name: str | None = None phone: str | None = None mobile: str | None = None - misc: str | None = None + misc: dict | None = None @classmethod def from_res_user(cls, odoo_rec): @@ -46,3 +44,9 @@ def from_res_user(cls, odoo_rec): phone=odoo_rec.phone or None, mobile=odoo_rec.mobile or None, ) + + +class UserScDel(StrictExtendableBaseModel): + email: str + name: str + login: str From 377771fe78d444c870ccc5a52d9d8b2d66ae0f2c Mon Sep 17 00:00:00 2001 From: Bonnerue Date: Wed, 15 Oct 2025 09:03:16 +0200 Subject: [PATCH 5/5] add role/company et test --- fastapi_user_manager/__manifest__.py | 1 + .../demo/fastapi_endpoint_demo.xml | 58 +++++++ fastapi_user_manager/routers/user.py | 14 +- fastapi_user_manager/schemas/schemas.py | 8 +- fastapi_user_manager/tests/__init__.py | 1 + .../tests/test_fastapi_user_manager.py | 148 ++++++++++++++++++ 6 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 fastapi_user_manager/demo/fastapi_endpoint_demo.xml create mode 100644 fastapi_user_manager/tests/__init__.py create mode 100644 fastapi_user_manager/tests/test_fastapi_user_manager.py diff --git a/fastapi_user_manager/__manifest__.py b/fastapi_user_manager/__manifest__.py index 765e08712..708324e52 100644 --- a/fastapi_user_manager/__manifest__.py +++ b/fastapi_user_manager/__manifest__.py @@ -14,6 +14,7 @@ "fastapi", "pydantic", ], + "demo": ["demo/fastapi_endpoint_demo.xml"], "data": [ "views/res_users_view.xml", "views/fastapi_endpoint_view.xml", diff --git a/fastapi_user_manager/demo/fastapi_endpoint_demo.xml b/fastapi_user_manager/demo/fastapi_endpoint_demo.xml new file mode 100644 index 000000000..a90297aa6 --- /dev/null +++ b/fastapi_user_manager/demo/fastapi_endpoint_demo.xml @@ -0,0 +1,58 @@ + + + + + + My Demo Endpoint User User + my_demo_app_user_user + + + + + + My Demo Endpoint Group User + + + + + + + + Fastapi Demo Endpoint user + + user_manager + /fastapi_user + http_basic + + + + + + + + + + + + + + diff --git a/fastapi_user_manager/routers/user.py b/fastapi_user_manager/routers/user.py index 0d89d908b..6dce2afcf 100644 --- a/fastapi_user_manager/routers/user.py +++ b/fastapi_user_manager/routers/user.py @@ -1,6 +1,7 @@ from typing import Annotated from odoo import api, models +from odoo.fields import Command from odoo.addons.base.models.res_users import Users as ResUsers from odoo.addons.fastapi.dependencies import ( @@ -94,17 +95,26 @@ def _update_user(self, data: UserScUpdate) -> ResUsers: def create_user(self, data: UserSc): vals = { "name": data.name, - "login": data.email, + "login": data.login, + "email": data.email, "phone": data.phone, "mobile": data.mobile, } + company_id = self.env["res.company"].search([("name", "=", data.company)]) + vals["company_id"] = company_id.id + if hasattr(self.env["res.users"], "role_ids"): + roles = self.env["res.users.role"].search([("name", "in", data.role)]) + vals["role_line_ids"] = [ + Command.create({"role_id": rol_id}) for rol_id in roles.ids + ] user = self.env["res.users"].create(vals) user = self._post_process_user_creation(user, data.misc) return user def _post_process_user_creation(self, user, misc): """inherit it to adapt to your needs""" - user = user.write(misc) + if misc: + user = user.write(misc) return user def _get_user_values(self, data: UserScUpdate): diff --git a/fastapi_user_manager/schemas/schemas.py b/fastapi_user_manager/schemas/schemas.py index 9bec51fab..4c0563316 100644 --- a/fastapi_user_manager/schemas/schemas.py +++ b/fastapi_user_manager/schemas/schemas.py @@ -29,11 +29,13 @@ class UserSc(StrictExtendableBaseModel): used to get user details """ - login: str | None = None - email: str + login: str + email: str | None = None name: str | None = None phone: str | None = None mobile: str | None = None + company: str | None = None + role: list | None = None misc: dict | None = None @classmethod @@ -43,6 +45,8 @@ def from_res_user(cls, odoo_rec): name=odoo_rec.name or None, phone=odoo_rec.phone or None, mobile=odoo_rec.mobile or None, + compny=odoo_rec.company_id.name or None, + role=odoo_rec.role_id.name or None, ) diff --git a/fastapi_user_manager/tests/__init__.py b/fastapi_user_manager/tests/__init__.py new file mode 100644 index 000000000..2cca9274d --- /dev/null +++ b/fastapi_user_manager/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fastapi_user_manager diff --git a/fastapi_user_manager/tests/test_fastapi_user_manager.py b/fastapi_user_manager/tests/test_fastapi_user_manager.py new file mode 100644 index 000000000..b46ea1bfc --- /dev/null +++ b/fastapi_user_manager/tests/test_fastapi_user_manager.py @@ -0,0 +1,148 @@ +# Copyright 2025 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/LGPL). + + +from requests import Response + +from odoo.addons.fastapi.tests.common import FastAPITransactionCase + +from fastapi import status + +from ..routers.user import user_router + + +class FastAPIDemoCase(FastAPITransactionCase): + """The fastapi lib comes with a useful testclient that let's you + easily test your endpoints. Moreover, the dependency overrides functionality + allows you to provide specific implementation for part of the code to avoid + to rely on some tricky http stuff for example: authentication + + This test class is an example on how you can test your own code + """ + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.default_fastapi_router = user_router + cls.default_fastapi_running_user = cls.env["res.users"].create( + { + "name": "My demo user endpoint user", + "login": "My_user_user_app", + } + ) + cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create( + {"name": "FastAPI Demo"} + ) + + def test_create_user(self) -> None: + with self._create_test_client() as test_client: + response: Response = test_client.get( + "/user/create", + params=[ + { + "name": "Test_one", + "login": "Test_one", + "company": "YourCompany", + }, + # { + # "name": "Test_two", + # "login": "Test_two", + # "company": "YourCompany", + # }, + ], + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual( + response.json(), + """Test_one = New ok + Test_two = New ok + """, + ) + + +# def test_who_ami(self) -> None: +# with self._create_test_client() as test_client: +# response: Response = test_client.get("/demo/who_ami") +# self.assertEqual(response.status_code, status.HTTP_200_OK) +# partner = self.default_fastapi_authenticated_partner +# self.assertDictEqual( +# response.json(), +# { +# "name": partner.name, +# "display_name": partner.display_name, +# }, +# ) +# +# def test_endpoint_info(self) -> None: +# demo_app = self.env.ref("fastapi.fastapi_endpoint_demo") +# with self._create_test_client( +# dependency_overrides={fastapi_endpoint: partial(lambda a: a, demo_app)} +# ) as test_client: +# response: Response = test_client.get("/demo/endpoint_app_info") +# self.assertEqual(response.status_code, status.HTTP_200_OK) +# self.assertDictEqual( +# response.json(), +# DemoEndpointAppInfo.model_validate(demo_app).model_dump(by_alias=True), +# ) +# +# def test_exception_raised(self) -> None: +# with self.assertRaisesRegex(UserError, "User Error"): +# with self._create_test_client() as test_client: +# test_client.get( +# "/demo/exception", +# params={ +# "exception_type": DemoExceptionType.user_error.value, +# "error_message": "User Error", +# }, +# ) +# +# with self.assertRaisesRegex(NotImplementedError, "Bare Exception"): +# with self._create_test_client() as test_client: +# test_client.get( +# "/demo/exception", +# params={ +# "exception_type": DemoExceptionType.bare_exception.value, +# "error_message": "Bare Exception", +# }, +# ) +# +# @mute_logger("odoo.addons.fastapi.tests.common") +# def test_exception_not_raised(self) -> None: +# with self._create_test_client(raise_server_exceptions=False) as test_client: +# response: Response = test_client.get( +# "/demo/exception", +# params={ +# "exception_type": DemoExceptionType.user_error.value, +# "error_message": "User Error", +# }, +# ) +# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) +# self.assertDictEqual(response.json(), {"detail": "User Error"}) +# +# with self._create_test_client(raise_server_exceptions=False) as test_client: +# response: Response = test_client.get( +# "/demo/exception", +# params={ +# "exception_type": DemoExceptionType.bare_exception.value, +# "error_message": "Bare Exception", +# }, +# ) +# self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) +# self.assertDictEqual(response.json(), {"detail": "Internal Server Error"}) +# +# class TestPortalDoc(Commoncase): +# @classmethod +# def setUpClass(cls): +# super().setUpClass() +# cls.default_fastapi_router = user_error +# +# cls.user_one = cls.env["res.users"].create( +# { +# "name": "Test_one", +# "login": "Test_one", +# } +# ) +# +# def test_create_user(self): +# route = "/" +#