diff --git a/crowdin_api/api_resources/teams/resource.py b/crowdin_api/api_resources/teams/resource.py index e08d172..acd0fd8 100644 --- a/crowdin_api/api_resources/teams/resource.py +++ b/crowdin_api/api_resources/teams/resource.py @@ -3,7 +3,9 @@ from crowdin_api.api_resources.abstract.resources import BaseResource from crowdin_api.api_resources.teams.types \ import Permissions, TeamPatchRequest, TeamByProjectRole, GroupTeamPatchRequest +from crowdin_api.api_resources.users.enums import ProjectRole from crowdin_api.sorting import Sorting +from crowdin_api.utils import convert_to_query_string, convert_enum_to_string_if_exists class TeamsResource(BaseResource): @@ -125,9 +127,14 @@ def add_team_to_project( def list_teams( self, - orderBy: Optional[Sorting] = None, + order_by: Optional[Sorting] = None, offset: Optional[int] = None, - limit: Optional[int] = None + limit: Optional[int] = None, + search: Optional[str] = None, + project_ids: Optional[Iterable[int]] = None, + project_roles: Optional[Iterable[ProjectRole]] = None, + language_ids: Optional[Iterable[str]] = None, + group_ids: Optional[Iterable[int]] = None, ): """ List Teams. @@ -136,7 +143,14 @@ def list_teams( https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.getMany """ - params = {"orderBy": orderBy} + params = { + "orderBy": order_by, + "search": search, + "projectIds": convert_to_query_string(project_ids, lambda project_id: str(project_id)), + "projectRoles": convert_to_query_string(project_roles, lambda role: convert_enum_to_string_if_exists(role)), + "languageIds": convert_to_query_string(language_ids, lambda language_id: str(language_id)), + "groupIds": convert_to_query_string(group_ids, lambda group_id: str(group_id)) + } params.update(self.get_page_params(offset=offset, limit=limit)) return self._get_entire_data( diff --git a/crowdin_api/api_resources/teams/tests/test_teams_resources.py b/crowdin_api/api_resources/teams/tests/test_teams_resources.py index 3e3f988..eed4236 100644 --- a/crowdin_api/api_resources/teams/tests/test_teams_resources.py +++ b/crowdin_api/api_resources/teams/tests/test_teams_resources.py @@ -5,7 +5,7 @@ from crowdin_api.api_resources import TeamsResource from crowdin_api.api_resources.enums import PatchOperation from crowdin_api.api_resources.teams.enums import ListTeamsOrderBy, TeamPatchPath -from crowdin_api.api_resources.users.enums import ListGroupTeamsOrderBy +from crowdin_api.api_resources.users.enums import ListGroupTeamsOrderBy, ProjectRole from crowdin_api.requester import APIRequester from crowdin_api.sorting import Sorting, SortingOrder, SortingRule @@ -302,15 +302,29 @@ def test_add_team_to_project(self, m_request, incoming_data, request_data, base_ {}, { "orderBy": None, + "search": None, + "projectIds": None, + "projectRoles": None, + "languageIds": None, + "groupIds": None, "limit": 25, "offset": 0, }, ), ( { - "orderBy": Sorting( + "order_by": Sorting( [SortingRule(ListTeamsOrderBy.ID, SortingOrder.DESC)] ), + "search": "Alex", + "project_ids": [1, 2, 3], + "project_roles": [ + ProjectRole.MEMBER, + ProjectRole.TRANSLATOR, + ProjectRole.PROOFREADER + ], + "language_ids": ["uk", "es", "it"], + "group_ids": [10, 11, 12], "limit": 10, "offset": 2, }, @@ -318,6 +332,11 @@ def test_add_team_to_project(self, m_request, incoming_data, request_data, base_ "orderBy": Sorting( [SortingRule(ListTeamsOrderBy.ID, SortingOrder.DESC)] ), + "search": "Alex", + "projectIds": "1,2,3", + "projectRoles": "member,translator,proofreader", + "languageIds": "uk,es,it", + "groupIds": "10,11,12", "limit": 10, "offset": 2, }, diff --git a/crowdin_api/api_resources/users/enums.py b/crowdin_api/api_resources/users/enums.py index 7c5b689..559873e 100644 --- a/crowdin_api/api_resources/users/enums.py +++ b/crowdin_api/api_resources/users/enums.py @@ -17,8 +17,12 @@ class UserPatchPath(Enum): class ProjectRole(Enum): + MANAGER = "manager" + DEVELOPER = "developer" TRANSLATOR = "translator" PROOFREADER = "proofreader" + LANGUAGE_COORDINATOR = "language_coordinator" + MEMBER = "member" class ListProjectMembersCrowdinOrderBy(Enum): @@ -27,6 +31,17 @@ class ListProjectMembersCrowdinOrderBy(Enum): FULL_NAME = "fullName" +class ListUsersOrderBy(Enum): + ID = "id" + USERNAME = "username" + FIRST_NAME = "firstName" + LAST_NAME = "lastName" + EMAIL = "email" + STATUS = "status" + CREATED_AT = "createdAt" + LAST_SEEN = "lastSeen" + + class ListProjectMembersEnterpriseOrderBy(Enum): ID = "id" USERNAME = "username" @@ -48,3 +63,21 @@ class ListGroupTeamsOrderBy(Enum): NAME = "name" CREATED_AT = "createdAt" UPDATED_AT = "updatedAt" + + +class OrganizationRole(Enum): + ADMIN = "admin" + MANAGER = "manager" + VENDOR = "vendor" + CLIENT = "client" + + +class UserStatus(Enum): + ACTIVE = "active" + PENDING = "pending" + BLOCKED = "blocked" + + +class UserTwoFactorAuthStatus(Enum): + ENABLED = "enabled" + DISABLED = "disabled" diff --git a/crowdin_api/api_resources/users/resource.py b/crowdin_api/api_resources/users/resource.py index af8f41d..8cd2128 100644 --- a/crowdin_api/api_resources/users/resource.py +++ b/crowdin_api/api_resources/users/resource.py @@ -1,9 +1,11 @@ +from datetime import datetime from typing import Dict, Iterable, Optional from crowdin_api.api_resources.abstract.resources import BaseResource -from crowdin_api.api_resources.users.enums import UserRole +from crowdin_api.api_resources.users.enums import UserRole, OrganizationRole, UserStatus from crowdin_api.api_resources.users.types import UserPatchRequest, ProjectMemberRole, GroupManagerPatchRequest from crowdin_api.sorting import Sorting +from crowdin_api.utils import convert_to_query_string, convert_enum_to_string_if_exists class BaseUsersResource(BaseResource): @@ -341,3 +343,57 @@ def delete_user(self, userId: int): method="delete", path=self.get_users_path(userId=userId) ) + + def list_users( + self, + limit: Optional[int] = None, + offset: Optional[int] = None, + order_by: Optional[Sorting] = None, + status: Optional[UserStatus] = None, + search: Optional[str] = None, + two_factor: Optional[str] = None, + organization_roles: Optional[Iterable[OrganizationRole]] = None, + team_id: Optional[int] = None, + project_ids: Optional[Iterable[int]] = None, + project_roles: Optional[Iterable[str]] = None, + language_ids: Optional[Iterable[str]] = None, + group_ids: Optional[Iterable[int]] = None, + last_seen_from: Optional[datetime] = None, + last_seen_to: Optional[datetime] = None + ): + """ + List Users + + Link to documentation: + https://support.crowdin.com/developer/enterprise/api/v2/#tag/Users/operation/api.users.getMany + """ + + params = { + "limit": limit, + "offset": offset, + "orderBy": order_by, + "status": convert_enum_to_string_if_exists(status), + "search": search, + "twoFactor": convert_enum_to_string_if_exists(two_factor), + "organizationRoles": convert_to_query_string( + organization_roles, + lambda role: convert_enum_to_string_if_exists(role) + ), + "teamId": team_id, + "projectIds": convert_to_query_string(project_ids, lambda project_id: str(project_id)), + "projectRoles": convert_to_query_string( + project_roles, + lambda role: convert_enum_to_string_if_exists(role) + ), + "languageIds": convert_to_query_string(language_ids), + "groupIds": convert_to_query_string(group_ids), + "lastSeenFrom": last_seen_from.isoformat() if last_seen_from is not None else None, + "lastSeenTo": last_seen_to.isoformat() if last_seen_to is not None else None + } + params.update(self.get_page_params(offset=offset, limit=limit)) + + return self.requester.request( + method="get", + path=self.get_users_path(), + params=params + ) diff --git a/crowdin_api/api_resources/users/tests/test_users_resources.py b/crowdin_api/api_resources/users/tests/test_users_resources.py index 4523c8f..ce0587d 100644 --- a/crowdin_api/api_resources/users/tests/test_users_resources.py +++ b/crowdin_api/api_resources/users/tests/test_users_resources.py @@ -1,3 +1,4 @@ +from datetime import timezone, datetime from unittest import mock import pytest @@ -8,7 +9,12 @@ ListProjectMembersEnterpriseOrderBy, UserRole, UserPatchPath, - ListGroupManagersOrderBy + ListGroupManagersOrderBy, + ListUsersOrderBy, + UserStatus, + UserTwoFactorAuthStatus, + OrganizationRole, + ProjectRole ) from crowdin_api.api_resources.users.resource import ( UsersResource, @@ -612,3 +618,72 @@ def test_delete_user(self, m_request, base_absolut_url): resource = self.get_resource(base_absolut_url) assert resource.delete_user(userId=1) == "response" m_request.assert_called_once_with(method="delete", path="users/1") + + @pytest.mark.parametrize( + "in_params, request_params", + ( + ( + { + "limit": 10, + "offset": 2, + "order_by": Sorting( + [ + SortingRule(ListUsersOrderBy.CREATED_AT, SortingOrder.DESC), + SortingRule(ListUsersOrderBy.USERNAME) + ] + ), + "status": UserStatus.ACTIVE, + "search": "Alex", + "two_factor": UserTwoFactorAuthStatus.ENABLED, + "organization_roles": [ + OrganizationRole.MANAGER, + OrganizationRole.VENDOR, + OrganizationRole.CLIENT + ], + "team_id": 123, + "project_ids": [11, 22, 33], + "project_roles": [ + ProjectRole.MANAGER, + ProjectRole.DEVELOPER, + ProjectRole.LANGUAGE_COORDINATOR + ], + "language_ids": ["uk", "es", "it"], + "group_ids": [4, 5, 6], + "last_seen_from": datetime(2024, 1, 10, 10, 41, 33, tzinfo=timezone.utc), + "last_seen_to": datetime(2024, 1, 11, 10, 41, 33, tzinfo=timezone.utc) + }, + { + "limit": 10, + "offset": 2, + "orderBy": Sorting( + [ + SortingRule(ListUsersOrderBy.CREATED_AT, SortingOrder.DESC), + SortingRule(ListUsersOrderBy.USERNAME) + ] + ), + "status": "active", + "search": "Alex", + "twoFactor": "enabled", + "organizationRoles": "manager,vendor,client", + "teamId": 123, + "projectIds": "11,22,33", + "projectRoles": "manager,developer,language_coordinator", + "languageIds": "uk,es,it", + "groupIds": "4,5,6", + "lastSeenFrom": "2024-01-10T10:41:33+00:00", + "lastSeenTo": "2024-01-11T10:41:33+00:00" + } + ), + ) + ) + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_list_users(self, m_request, in_params, request_params, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert resource.list_users(**in_params) + m_request.assert_called_once_with( + method="get", + path="users", + params=request_params + ) diff --git a/crowdin_api/utils.py b/crowdin_api/utils.py new file mode 100644 index 0000000..cedc9c5 --- /dev/null +++ b/crowdin_api/utils.py @@ -0,0 +1,19 @@ +from enum import Enum +from typing import Optional, Iterable, Callable + + +def convert_to_query_string( + collection: Optional[Iterable], + converter: Optional[Callable[[object], str]] = None +) -> Optional[str]: + if not collection: + return None + + if converter is not None: + return ','.join(converter(item) for item in collection) + else: + return ','.join(str(item) for item in collection) + + +def convert_enum_to_string_if_exists(value: Optional[Enum]) -> Optional[str]: + return value.value if value is not None else None