From e57b5ed67fc8581d1de24840636b6227ceb44695 Mon Sep 17 00:00:00 2001 From: Vaghinak Basentsyan Date: Wed, 11 Feb 2026 12:57:35 +0400 Subject: [PATCH 1/2] Add filter by project user role --- .../lib/app/interface/sdk_interface.py | 5 ++ .../lib/core/entities/filters.py | 10 ++- .../lib/infrastructure/controller.py | 64 +++++++++++++------ .../lib/infrastructure/query_builder.py | 42 ++++++++++-- .../work_management/test_list_users.py | 10 +++ 5 files changed, 106 insertions(+), 25 deletions(-) diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index cb2190794..4c3dcf05e 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -562,6 +562,11 @@ def list_users( - email__starts: str - email__ends: str + Following params if project is selected:: + + - role: Literal[“Annotator”, “QA”] + - role__in: List[Literal[“Annotator”, “QA”]] + Following params if project is not selected:: - state: Literal[“Confirmed”, “Pending”] diff --git a/src/superannotate/lib/core/entities/filters.py b/src/superannotate/lib/core/entities/filters.py index 0fe71a1d3..ac72d2f86 100644 --- a/src/superannotate/lib/core/entities/filters.py +++ b/src/superannotate/lib/core/entities/filters.py @@ -40,7 +40,7 @@ class ProjectFilters(BaseFilters): status__notin: List[Literal["NotStarted", "InProgress", "Completed", "OnHold"]] -class UserFilters(TypedDict, total=False): +class BaseUserFilters(TypedDict, total=False): id: Optional[int] id__in: Optional[List[int]] email: Optional[str] @@ -48,6 +48,14 @@ class UserFilters(TypedDict, total=False): email__contains: Optional[str] email__starts: Optional[str] email__ends: Optional[str] + + +class ProjectUserFilters(BaseUserFilters, total=False): + role: Optional[str] + role__in: Optional[List[str]] + + +class TeamUserFilters(BaseUserFilters, total=False): state: Optional[str] state__in: Optional[List[str]] role: Optional[str] diff --git a/src/superannotate/lib/infrastructure/controller.py b/src/superannotate/lib/infrastructure/controller.py index b61717397..8a64b16e1 100644 --- a/src/superannotate/lib/infrastructure/controller.py +++ b/src/superannotate/lib/infrastructure/controller.py @@ -33,7 +33,8 @@ from lib.core.entities.classes import AnnotationClassEntity from lib.core.entities.filters import ItemFilters from lib.core.entities.filters import ProjectFilters -from lib.core.entities.filters import UserFilters +from lib.core.entities.filters import ProjectUserFilters +from lib.core.entities.filters import TeamUserFilters from lib.core.entities.integrations import IntegrationEntity from lib.core.entities.items import ProjectCategoryEntity from lib.core.entities.work_managament import ScoreEntity @@ -56,8 +57,10 @@ from lib.infrastructure.query_builder import IncludeHandler from lib.infrastructure.query_builder import ItemFilterHandler from lib.infrastructure.query_builder import ProjectFilterHandler +from lib.infrastructure.query_builder import ProjectUserRoleFilterHandler from lib.infrastructure.query_builder import QueryBuilderChain -from lib.infrastructure.query_builder import UserFilterHandler +from lib.infrastructure.query_builder import TeamUserRoleFilterHandler +from lib.infrastructure.query_builder import TeamUserStateFilterHandler from lib.infrastructure.repositories import S3Repository from lib.infrastructure.serviceprovider import ServiceProvider from lib.infrastructure.services.http_client import HttpClient @@ -205,27 +208,50 @@ def list_users( if project: parent_entity = CustomFieldEntityEnum.PROJECT project_id = context["project_id"] = project.id + valid_fields = generate_schema( + ProjectUserFilters.__annotations__, + self.service_provider.get_custom_fields_templates( + context, CustomFieldEntityEnum.CONTRIBUTOR, parent=parent_entity + ), + ) + chain = QueryBuilderChain( + [ + FieldValidationHandler(valid_fields.keys()), + ProjectUserRoleFilterHandler( + team_id=self.service_provider.client.team_id, + project=project, + service_provider=self.service_provider, + entity=CustomFieldEntityEnum.CONTRIBUTOR, + parent=parent_entity, + ), + ] + ) else: parent_entity = CustomFieldEntityEnum.TEAM project_id = None - valid_fields = generate_schema( - UserFilters.__annotations__, - self.service_provider.get_custom_fields_templates( - context, CustomFieldEntityEnum.CONTRIBUTOR, parent=parent_entity - ), - ) - chain = QueryBuilderChain( - [ - FieldValidationHandler(valid_fields.keys()), - UserFilterHandler( - team_id=self.service_provider.client.team_id, - project_id=project_id, - service_provider=self.service_provider, - entity=CustomFieldEntityEnum.CONTRIBUTOR, - parent=parent_entity, + valid_fields = generate_schema( + TeamUserFilters.__annotations__, + self.service_provider.get_custom_fields_templates( + context, CustomFieldEntityEnum.CONTRIBUTOR, parent=parent_entity ), - ] - ) + ) + chain = QueryBuilderChain( + [ + FieldValidationHandler(valid_fields.keys()), + TeamUserRoleFilterHandler( + team_id=self.service_provider.client.team_id, + service_provider=self.service_provider, + entity=CustomFieldEntityEnum.CONTRIBUTOR, + parent=parent_entity, + ), + TeamUserStateFilterHandler( + team_id=self.service_provider.client.team_id, + service_provider=self.service_provider, + entity=CustomFieldEntityEnum.CONTRIBUTOR, + parent=parent_entity, + ), + ] + ) query = chain.handle(filters, EmptyQuery()) if project and include and "categories" in include: diff --git a/src/superannotate/lib/infrastructure/query_builder.py b/src/superannotate/lib/infrastructure/query_builder.py index c12aaea63..5d05dab1e 100644 --- a/src/superannotate/lib/infrastructure/query_builder.py +++ b/src/superannotate/lib/infrastructure/query_builder.py @@ -164,17 +164,20 @@ class BaseCustomFieldHandler(AbstractQueryHandler): def __init__( self, team_id: int, - project_id: Optional[int], service_provider: BaseServiceProvider, entity: CustomFieldEntityEnum, parent: CustomFieldEntityEnum, + project: Optional[ProjectEntity] = None, ): self._service_provider = service_provider self._entity = entity self._parent = parent self._team_id = team_id - self._project_id = project_id - self._context = {"team_id": self._team_id, "project_id": self._project_id} + self._project = project + self._context = { + "team_id": self._team_id, + "project_id": project.id if project else None, + } def _handle_custom_field_key(self, key) -> Tuple[str, str, Optional[str]]: for custom_field in sorted( @@ -261,7 +264,7 @@ def handle(self, filters: Dict[str, Any], query: Query = None) -> Query: return super().handle(filters, query) -class UserFilterHandler(BaseCustomFieldHandler): +class TeamUserRoleFilterHandler(BaseCustomFieldHandler): def _handle_special_fields(self, keys: List[str], val): """ Handle special fields like 'custom_fields__'. @@ -276,7 +279,36 @@ def _handle_special_fields(self, keys: List[str], val): raise AppException("Invalid user role provided.") except (KeyError, AttributeError): raise AppException("Invalid user role provided.") - elif keys[0] == "state": + return super()._handle_special_fields(keys, val) + + +class ProjectUserRoleFilterHandler(BaseCustomFieldHandler): + def _handle_special_fields(self, keys: List[str], val): + """ + Handle special fields like 'custom_fields__'. + """ + if keys[0] == "role": + try: + if isinstance(val, list): + val = [ + self._service_provider.get_role_id(self._project, i) + for i in val + ] + elif isinstance(val, str): + val = self._service_provider.get_role_id(self._project, val) + else: + raise AppException("Invalid user role provided.") + except (KeyError, AttributeError): + raise AppException("Invalid user role provided.") + return super()._handle_special_fields(keys, val) + + +class TeamUserStateFilterHandler(BaseCustomFieldHandler): + def _handle_special_fields(self, keys: List[str], val): + """ + Handle special fields like 'custom_fields__'. + """ + if keys[0] == "state": try: if isinstance(val, list): val = [WMUserStateEnum[i].value for i in val] diff --git a/tests/integration/work_management/test_list_users.py b/tests/integration/work_management/test_list_users.py index 8265ade03..ff013d485 100644 --- a/tests/integration/work_management/test_list_users.py +++ b/tests/integration/work_management/test_list_users.py @@ -33,6 +33,16 @@ def test_pending_users(self): assert project["contributors"][1]["state"] == "PENDING" + @pytest.mark.skip(reason="For not send real email") + def test_project_role_filter_users(self): + test_email = "test1@superannotate.com" + sa.invite_contributors_to_team(emails=[test_email]) + sa.add_contributors_to_project(self.PROJECT_NAME, [test_email], "Annotator") + users = sa.list_users(project=self.PROJECT_NAME, role="QA") + assert len(users) == 0 + users = sa.list_users(project=self.PROJECT_NAME, role="Annotator") + assert len(users) == 2 + def test_list_users_by_project_name(self): project_users = sa.list_users(project=self.PROJECT_NAME) assert len(project_users) == 1 From aa3dc4ef8b062ce2665ba492c670e60365cfd0ab Mon Sep 17 00:00:00 2001 From: Vaghinak Basentsyan Date: Thu, 12 Feb 2026 09:30:56 +0400 Subject: [PATCH 2/2] Docstring update --- src/superannotate/lib/app/interface/sdk_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index 4c3dcf05e..5a62d0f3e 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -564,8 +564,8 @@ def list_users( Following params if project is selected:: - - role: Literal[“Annotator”, “QA”] - - role__in: List[Literal[“Annotator”, “QA”]] + - role: str + - role__in: List[str] Following params if project is not selected::