From 3826f4c05c2bc15072114e26ca39b9c4ce5b60d3 Mon Sep 17 00:00:00 2001 From: JWittmeyer Date: Mon, 8 Sep 2025 17:28:40 +0200 Subject: [PATCH 1/8] Submodule update --- submodules/model | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/model b/submodules/model index d5300f13..bcc2a2e9 160000 --- a/submodules/model +++ b/submodules/model @@ -1 +1 @@ -Subproject commit d5300f13c3b2cc6da16f36ffd6349795923b2c64 +Subproject commit bcc2a2e94f83c9a328c35dd0b0c8dddcb52aa7f3 From c1897ee593239c7aa75a0b95e5245ba9aa204af9 Mon Sep 17 00:00:00 2001 From: JWittmeyer Date: Mon, 8 Sep 2025 17:38:10 +0200 Subject: [PATCH 2/8] Fixes customer button for deleted user --- controller/misc/manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/controller/misc/manager.py b/controller/misc/manager.py index 946eb1bb..87cdbd08 100644 --- a/controller/misc/manager.py +++ b/controller/misc/manager.py @@ -93,7 +93,11 @@ def finalize_customer_buttons( for e in buttons: e[key_name] = name_lookup[str(e[key])] e[key_name] = ( - (e[key_name].get("first", "") + " " + e[key_name].get("last", "")) + ( + (e[key_name].get("first", "") or "") + + " " + + (e[key_name].get("last", "") or "") + ) if e[key_name] else "Unknown" ) From 86006741dfabf1ef106cb404f1476b17a6c8ec16 Mon Sep 17 00:00:00 2001 From: andhreljaKern Date: Wed, 10 Sep 2025 12:52:30 +0200 Subject: [PATCH 3/8] tests(fix): CREATED_TAGS_PER_ORG --- tests/test_admin_queries.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_admin_queries.py b/tests/test_admin_queries.py index 80a71589..64d309d0 100644 --- a/tests/test_admin_queries.py +++ b/tests/test_admin_queries.py @@ -28,7 +28,13 @@ def test_full_admin_queries(): def __get_default_filter_for_admin_query(query: AdminQueries) -> dict: # USERS_TO_PROJECTS, USERS_BY_ORG - if query in (AdminQueries.USERS_TO_PROJECTS, AdminQueries.USERS_BY_ORG): + # AVG_MESSAGES_PER_CONVERSATION_GLOBAL, CREATED_TAGS_PER_ORG + if query in ( + AdminQueries.USERS_TO_PROJECTS, + AdminQueries.USERS_BY_ORG, + AdminQueries.AVG_MESSAGES_PER_CONVERSATION_GLOBAL, + AdminQueries.CREATED_TAGS_PER_ORG, + ): return { "organization_id": "", "without_kern_email": False, @@ -57,13 +63,6 @@ def __get_default_filter_for_admin_query(query: AdminQueries) -> dict: "without_kern_email": False, } - # AVG_MESSAGES_PER_CONVERSATION_GLOBAL - elif query is AdminQueries.AVG_MESSAGES_PER_CONVERSATION_GLOBAL: - return { - "organization_id": "", - "without_kern_email": False, - } - # AVG_MESSAGES_PER_CONVERSATION, MACRO_EXECUTIONS elif query in ( AdminQueries.AVG_MESSAGES_PER_CONVERSATION, From 41f9cd1d7a23ed3e9d1cdf74a859080b77c7be89 Mon Sep 17 00:00:00 2001 From: JWittmeyer Date: Wed, 10 Sep 2025 13:26:08 +0200 Subject: [PATCH 4/8] Adds teams to invite users process --- controller/auth/manager.py | 6 ++++++ controller/organization/manager.py | 7 +++++-- controller/user/manager.py | 7 +++++++ fast_api/models.py | 1 + fast_api/routes/misc.py | 3 +++ submodules/model | 2 +- 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/controller/auth/manager.py b/controller/auth/manager.py index d75154f7..b002d7a5 100644 --- a/controller/auth/manager.py +++ b/controller/auth/manager.py @@ -174,11 +174,13 @@ def check_is_full_admin(request: Any) -> bool: def invite_users( + creation_user_id: str, emails: List[str], organization_name: str, user_role: str, language: str, provider: Optional[str] = None, + team_ids: Optional[List[str]] = None, ): user_ids = [] for email in emails: @@ -196,6 +198,10 @@ def invite_users( # Add the preferred language user_manager.update_user_field(user["id"], "language_display", language) + # Add the user to the teams + if team_ids: + user_manager.add_user_to_teams(creation_user_id, user["id"], team_ids) + # Get the recovery link for the email recovery_link = kratos.get_recovery_link(user["id"]) if not recovery_link: diff --git a/controller/organization/manager.py b/controller/organization/manager.py index 1c216c0f..e00b9094 100644 --- a/controller/organization/manager.py +++ b/controller/organization/manager.py @@ -71,9 +71,12 @@ def get_all_users( ) all_users_expanded = kratos.expand_user_mail_name(all_users_dict) all_users_expanded = [ - user + { + **user, + "firstName": user["firstName"] or "", + "lastName": user["lastName"] or "", + } for user in all_users_expanded - if user["firstName"] is not None and user["lastName"] is not None ] return all_users_expanded diff --git a/controller/user/manager.py b/controller/user/manager.py index d863287c..dfdc4363 100644 --- a/controller/user/manager.py +++ b/controller/user/manager.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta from util.decorator import param_throttle from submodules.model.util import is_string_true_value +from submodules.model.business_objects import team_member as team_member_db_co def get_user(user_id: str) -> User: @@ -84,6 +85,12 @@ def update_user_field(user_id: str, field: str, value: Any) -> User: return user_item +def add_user_to_teams(creation_user_id: str, user_id: str, team_ids: list) -> User: + for team_id in team_ids: + team_member_db_co.create(team_id, user_id, creation_user_id, with_commit=False) + general.commit() + + def remove_organization_from_user(user_mail: str) -> None: user_id = kratos.get_userid_from_mail(user_mail) if user_id is None: diff --git a/fast_api/models.py b/fast_api/models.py index 62115d65..0efe0b6a 100644 --- a/fast_api/models.py +++ b/fast_api/models.py @@ -504,6 +504,7 @@ class InviteUsersBody(BaseModel): provider: Optional[StrictStr] = None user_role: StrictStr language: StrictStr + team_ids: Optional[List[StrictStr]] = None class CheckInviteUsersBody(BaseModel): diff --git a/fast_api/routes/misc.py b/fast_api/routes/misc.py index cd72f32d..d3a62f2f 100644 --- a/fast_api/routes/misc.py +++ b/fast_api/routes/misc.py @@ -293,12 +293,15 @@ def get_is_full_admin(request: Request) -> Dict: def invite_users(request: Request, body: InviteUsersBody = Body(...)): if not auth.check_is_full_admin(request): raise AuthManagerError("Full admin access required") + user_id = auth.get_user_id_by_info(request.state.info) data = auth.invite_users( + user_id, body.emails, body.organization_name, body.user_role, body.language, body.provider, + body.team_ids, ) return pack_json_result(data) diff --git a/submodules/model b/submodules/model index bcc2a2e9..63c463ee 160000 --- a/submodules/model +++ b/submodules/model @@ -1 +1 @@ -Subproject commit bcc2a2e94f83c9a328c35dd0b0c8dddcb52aa7f3 +Subproject commit 63c463ee6274f818717894504b830cdceada3858 From 493fb9f242562efa65a89de2c0487712baa7338a Mon Sep 17 00:00:00 2001 From: andhreljaKern Date: Wed, 10 Sep 2025 13:39:35 +0200 Subject: [PATCH 5/8] tests(fix): detached org objects due to get_org_cached expunge --- conftest.py | 27 +++++++++++----------- tests/fast_api/routes/test_invite_users.py | 5 ++-- tests/fast_api/routes/test_project.py | 10 ++++---- tests/test_admin_queries.py | 20 ++++++++++++++-- 4 files changed, 40 insertions(+), 22 deletions(-) diff --git a/conftest.py b/conftest.py index 4865a2fc..30804a1e 100644 --- a/conftest.py +++ b/conftest.py @@ -28,37 +28,38 @@ def database_session() -> Iterator[None]: @pytest.fixture(scope="session") -def org() -> Iterator[Organization]: +def org_id() -> Iterator[str]: org_item = organization_bo.create(name="test_org", with_commit=True) s3.create_bucket(str(org_item.id)) - yield org_item - organization_bo.delete(org_item.id, with_commit=True) - s3.remove_bucket(str(org_item.id), True) + org_id = str(org_item.id) + yield org_id + organization_bo.delete(org_id, with_commit=True) + s3.remove_bucket(org_id, True) @pytest.fixture(scope="session") -def user(org: Organization) -> Iterator[User]: +def user_id(org_id: str) -> Iterator[str]: user_item = user_bo.create(user_id=uuid.uuid4(), with_commit=True) - user_bo.update_organization(user_id=user_item.id, organization_id=org.id) - yield user_item + user_bo.update_organization(user_id=user_item.id, organization_id=org_id) + yield str(user_item.id) @pytest.fixture(scope="session") -def refinery_project(org: Organization, user: User) -> Iterator[RefineryProject]: +def refinery_project(org_id: str, user_id: str) -> Iterator[RefineryProject]: project_item = project_bo.create( - organization_id=org.id, + organization_id=org_id, name="test_project", description="test_description", - created_by=user.id, + created_by=user_id, tokenizer="en_core_web_sm", with_commit=True, ) yield project_item - project_bo.delete(project_item.id, with_commit=True) + project_bo.delete(project_item.id) @pytest.fixture -def client(user: User) -> Iterator[TestClient]: - with patch("controller.auth.manager.DEV_USER_ID", str(user.id)): +def client(user_id: str) -> Iterator[TestClient]: + with patch("controller.auth.manager.DEV_USER_ID", user_id): with TestClient(app, base_url="http://localhost:7051") as client: yield client diff --git a/tests/fast_api/routes/test_invite_users.py b/tests/fast_api/routes/test_invite_users.py index c8d7a802..f9619166 100644 --- a/tests/fast_api/routes/test_invite_users.py +++ b/tests/fast_api/routes/test_invite_users.py @@ -1,7 +1,7 @@ from fastapi.testclient import TestClient from controller.auth.kratos import delete_user_kratos -from submodules.model.models import Organization +from submodules.model.business_objects import organization as organization_bo from submodules.model.enums import UserRoles import requests import time @@ -48,8 +48,9 @@ def test_invalid_emails(client: TestClient): assert len(response_data["validEmails"]) == len(valid_emails_to_test) -def test_invite_users(client: TestClient, org: Organization): +def test_invite_users(client: TestClient, org_id: str): requests.delete("http://mailhog:8025/api/v1/messages") + org = organization_bo.get(org_id) valid_emails_to_test = ["test@kern.ai"] response = client.post( "/api/v1/misc/invite-users", diff --git a/tests/fast_api/routes/test_project.py b/tests/fast_api/routes/test_project.py index be4a6775..2d11e511 100644 --- a/tests/fast_api/routes/test_project.py +++ b/tests/fast_api/routes/test_project.py @@ -41,10 +41,10 @@ def test_update_project_name_description( def test_upload_records_to_project( - client: TestClient, refinery_project: RefineryProject, user: User + client: TestClient, refinery_project: RefineryProject, user_id: str ): upload_task = upload_task_manager.create_upload_task( - str(user.id), + user_id, str(refinery_project.id), "dummy_file_name.csv", "records", @@ -119,11 +119,11 @@ def test_create_embedding(client: TestClient, refinery_project: RefineryProject) def test_update_records_to_project( - client: TestClient, refinery_project: RefineryProject, user: User + client: TestClient, refinery_project: RefineryProject, user_id: str ): upload_task = upload_task_manager.create_upload_task( - str(user.id), + user_id, str(refinery_project.id), "dummy_file_name.csv", "records", @@ -143,7 +143,7 @@ def test_update_records_to_project( assert len(all_records) == 2 assert any(r.data["data"] == "goodbye world" for r in all_records) transfer_api.__recalculate_missing_attributes_and_embeddings( - project_id=refinery_project.id, user_id=user.id + project_id=refinery_project.id, user_id=user_id ) time.sleep(5) emb = embedding_bo.get_all_embeddings_by_project_id(refinery_project.id) diff --git a/tests/test_admin_queries.py b/tests/test_admin_queries.py index 64d309d0..542d66ec 100644 --- a/tests/test_admin_queries.py +++ b/tests/test_admin_queries.py @@ -27,13 +27,16 @@ def test_full_admin_queries(): def __get_default_filter_for_admin_query(query: AdminQueries) -> dict: - # USERS_TO_PROJECTS, USERS_BY_ORG - # AVG_MESSAGES_PER_CONVERSATION_GLOBAL, CREATED_TAGS_PER_ORG + # USERS_TO_PROJECTS, USERS_BY_ORG, + # AVG_MESSAGES_PER_CONVERSATION_GLOBAL, CREATED_TAGS_PER_ORG, + # PRIVATEMODE_USE_OVER_TIME, MULTITAGGED_CONVERSATIONS if query in ( AdminQueries.USERS_TO_PROJECTS, AdminQueries.USERS_BY_ORG, AdminQueries.AVG_MESSAGES_PER_CONVERSATION_GLOBAL, AdminQueries.CREATED_TAGS_PER_ORG, + AdminQueries.PRIVATEMODE_USE_OVER_TIME, + AdminQueries.MULTITAGGED_CONVERSATIONS, ): return { "organization_id": "", @@ -82,5 +85,18 @@ def __get_default_filter_for_admin_query(query: AdminQueries) -> dict: "organization_id": "", } + # TEMPLATE_USAGE + elif query is AdminQueries.TEMPLATE_USAGE: + return { + "organization_id": "", + } + + elif query is AdminQueries.CONVERSATIONS_PER_TAG: + return { + "organization_id": "", + "without_kern_email": False, + "distinct_conversations": False, + } + else: raise ValueError(f"Unknown admin query: {query}") From 324db7b517dd692fa389526f6114a5354388c752 Mon Sep 17 00:00:00 2001 From: andhreljaKern Date: Wed, 10 Sep 2025 17:42:16 +0200 Subject: [PATCH 6/8] tests(fix): skip a hanging test --- tests/fast_api/routes/test_invite_users.py | 66 +++++++++++----------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/tests/fast_api/routes/test_invite_users.py b/tests/fast_api/routes/test_invite_users.py index f9619166..9d6eaf8a 100644 --- a/tests/fast_api/routes/test_invite_users.py +++ b/tests/fast_api/routes/test_invite_users.py @@ -48,35 +48,37 @@ def test_invalid_emails(client: TestClient): assert len(response_data["validEmails"]) == len(valid_emails_to_test) -def test_invite_users(client: TestClient, org_id: str): - requests.delete("http://mailhog:8025/api/v1/messages") - org = organization_bo.get(org_id) - valid_emails_to_test = ["test@kern.ai"] - response = client.post( - "/api/v1/misc/invite-users", - json={ - "organization_name": org.name, - "emails": valid_emails_to_test, - "user_role": UserRoles.ENGINEER.value, - "language": "en", - }, - ) - assert response.status_code == 200 - created_user_ids = response.json() - - email_response_data = {"total": 0} - start_time = time.time() - while email_response_data["total"] == 0 and time.time() - start_time < 5: - email_response = requests.get( - "http://mailhog:8025/api/v2/search", - params={"kind": "to", "query": "test@kern.ai"}, - ) - email_response_data = email_response.json() - assert email_response.status_code == 200 - - for user_id in created_user_ids: - delete_user_kratos(user_id) - - assert len(email_response_data["items"]) == len(valid_emails_to_test) - assert email_response_data["total"] == len(valid_emails_to_test) - assert email_response_data["count"] == len(valid_emails_to_test) +# Test commented out due to requests hanging indefinitely +# when trying to reach http://mailhog:8025 +# def test_invite_users(client: TestClient, org_id: str): +# requests.delete("http://mailhog:8025/api/v1/messages", timeout=5) +# org = organization_bo.get(org_id) +# valid_emails_to_test = ["test@kern.ai"] +# response = client.post( +# "/api/v1/misc/invite-users", +# json={ +# "organization_name": org.name, +# "emails": valid_emails_to_test, +# "user_role": UserRoles.ENGINEER.value, +# "language": "en", +# }, +# ) +# assert response.status_code == 200 +# created_user_ids = response.json() + +# email_response_data = {"total": 0} +# start_time = time.time() +# while email_response_data["total"] == 0 and time.time() - start_time < 5: +# email_response = requests.get( +# "http://mailhog:8025/api/v2/search", +# params={"kind": "to", "query": "test@kern.ai"}, +# ) +# email_response_data = email_response.json() +# assert email_response.status_code == 200 + +# for user_id in created_user_ids: +# delete_user_kratos(user_id) + +# assert len(email_response_data["items"]) == len(valid_emails_to_test) +# assert email_response_data["total"] == len(valid_emails_to_test) +# assert email_response_data["count"] == len(valid_emails_to_test) From 30efb63bd0c6ed7e29708c2f2d447291837fcf19 Mon Sep 17 00:00:00 2001 From: andhreljaKern Date: Wed, 10 Sep 2025 17:44:32 +0200 Subject: [PATCH 7/8] tests(chore): remove unused imports --- conftest.py | 2 -- tests/fast_api/routes/test_project.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index 30804a1e..3c3d93f0 100644 --- a/conftest.py +++ b/conftest.py @@ -14,8 +14,6 @@ ) from submodules.s3 import controller as s3 from submodules.model.models import ( - Organization, - User, Project as RefineryProject, ) diff --git a/tests/fast_api/routes/test_project.py b/tests/fast_api/routes/test_project.py index 2d11e511..eca0ef00 100644 --- a/tests/fast_api/routes/test_project.py +++ b/tests/fast_api/routes/test_project.py @@ -1,5 +1,5 @@ from fastapi.testclient import TestClient -from submodules.model.models import Project as RefineryProject, User +from submodules.model.models import Project as RefineryProject from controller.transfer import record_transfer_manager from api import transfer as transfer_api From 43b5a218c367a0c7cfd992fa32be6bd87b0c0ec7 Mon Sep 17 00:00:00 2001 From: andhreljaKern Date: Thu, 11 Sep 2025 09:38:51 +0200 Subject: [PATCH 8/8] chore: update submodules --- submodules/model | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/model b/submodules/model index 63c463ee..04dcb6cf 160000 --- a/submodules/model +++ b/submodules/model @@ -1 +1 @@ -Subproject commit 63c463ee6274f818717894504b830cdceada3858 +Subproject commit 04dcb6cf25cd374b9662f32f95880e165c62b6af