diff --git a/crowdin_api/api_resources/tasks/resource.py b/crowdin_api/api_resources/tasks/resource.py index 589b3bb..6ebef30 100644 --- a/crowdin_api/api_resources/tasks/resource.py +++ b/crowdin_api/api_resources/tasks/resource.py @@ -27,6 +27,7 @@ TaskSettingsTemplateLanguages, ) from crowdin_api.sorting import Sorting +from crowdin_api.api_resources.tasks.types import TaskCommentPatchRequest class TasksResource(BaseResource): @@ -934,6 +935,133 @@ def list_user_tasks( return self._get_entire_data(method="get", path="user/tasks", params=params) + # Task Comments + def get_task_comments_path( + self, + projectId: int, + taskId: int, + taskCommentId: Optional[int] = None, + ): + if taskCommentId is not None: + return f"projects/{projectId}/tasks/{taskId}/comments/{taskCommentId}" + + return f"projects/{projectId}/tasks/{taskId}/comments" + + def list_task_comments( + self, + taskId: int, + projectId: Optional[int] = None, + orderBy: Optional[Sorting] = None, + page: Optional[int] = None, + offset: Optional[int] = None, + limit: Optional[int] = None, + ): + """ + List Task Comments. + + Link to documentation: + https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.comments.getMany + """ + + projectId = projectId or self.get_project_id() + params = {"orderBy": orderBy} + params.update(self.get_page_params(page=page, offset=offset, limit=limit)) + + return self._get_entire_data( + method="get", + path=self.get_task_comments_path(projectId=projectId, taskId=taskId), + params=params, + ) + + def add_task_comment( + self, + text: str, + taskId: int, + projectId: Optional[int] = None, + ): + """ + Add Task Comment. + + Link to documentation: + https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.comments.post + """ + + projectId = projectId or self.get_project_id() + + return self.requester.request( + method="post", + path=self.get_task_comments_path(projectId=projectId, taskId=taskId), + request_data={"text": text}, + ) + + def get_task_comment( + self, + taskCommentId: int, + taskId: int, + projectId: Optional[int] = None, + ): + """ + Get Task Comment. + + Link to documentation: + https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.comments.get + """ + + projectId = projectId or self.get_project_id() + + return self.requester.request( + method="get", + path=self.get_task_comments_path( + projectId=projectId, taskId=taskId, taskCommentId=taskCommentId + ), + ) + + def delete_task_comment( + self, + taskCommentId: int, + taskId: int, + projectId: Optional[int] = None, + ): + """ + Delete Task Comment. + + Link to documentation: + https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.comments.delete + """ + + projectId = projectId or self.get_project_id() + + return self.requester.request( + method="delete", + path=self.get_task_comments_path( + projectId=projectId, taskId=taskId, taskCommentId=taskCommentId + ), + ) + + def edit_task_comment( + self, + taskCommentId: int, + data: Iterable[TaskCommentPatchRequest], + taskId: int, + projectId: Optional[int] = None, + ): + """ + Edit Task Comment. + + Link to documentation: + https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.comments.patch + """ + + projectId = projectId or self.get_project_id() + + return self.requester.request( + method="patch", + path=self.get_task_comments_path( + projectId=projectId, taskId=taskId, taskCommentId=taskCommentId + ), + request_data=data, + ) + def edit_task_archived_status( self, taskId: int, isArchived: bool = True, projectId: Optional[int] = None ): diff --git a/crowdin_api/api_resources/tasks/tests/test_tasks_resources.py b/crowdin_api/api_resources/tasks/tests/test_tasks_resources.py index 68a181e..0a47b46 100644 --- a/crowdin_api/api_resources/tasks/tests/test_tasks_resources.py +++ b/crowdin_api/api_resources/tasks/tests/test_tasks_resources.py @@ -1327,6 +1327,97 @@ def test_edit_task_archived_status(self, m_request, base_absolut_url): request_data=[{"op": "replace", "path": "/isArchived", "value": False}], ) + # --- Task comments --- + @pytest.mark.parametrize( + "incoming_data, path", + ( + ({"projectId": 1, "taskId": 2}, "projects/1/tasks/2/comments"), + ( + {"projectId": 1, "taskId": 2, "taskCommentId": 3}, + "projects/1/tasks/2/comments/3", + ), + ), + ) + def test_get_task_comments_path(self, incoming_data, path, base_absolut_url): + resource = self.get_resource(base_absolut_url) + assert resource.get_task_comments_path(**incoming_data) == path + + @pytest.mark.parametrize( + "incoming_data, request_params", + ( + ({}, {"orderBy": None, "offset": 0, "limit": 25}), + ({"orderBy": Sorting([])}, {"orderBy": Sorting([]), "offset": 0, "limit": 25}), + ), + ) + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_list_task_comments(self, m_request, incoming_data, request_params, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert resource.list_task_comments(projectId=1, taskId=2, **incoming_data) == "response" + m_request.assert_called_once_with( + method="get", + params=request_params, + path=resource.get_task_comments_path(projectId=1, taskId=2), + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_add_task_comment(self, m_request, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert resource.add_task_comment(projectId=1, taskId=2, text="hello") == "response" + m_request.assert_called_once_with( + method="post", + path=resource.get_task_comments_path(projectId=1, taskId=2), + request_data={"text": "hello"}, + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_get_task_comment(self, m_request, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert resource.get_task_comment(projectId=1, taskId=2, taskCommentId=3) == "response" + m_request.assert_called_once_with( + method="get", + path=resource.get_task_comments_path(projectId=1, taskId=2, taskCommentId=3), + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_delete_task_comment(self, m_request, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert resource.delete_task_comment(projectId=1, taskId=2, taskCommentId=3) == "response" + m_request.assert_called_once_with( + method="delete", + path=resource.get_task_comments_path(projectId=1, taskId=2, taskCommentId=3), + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_edit_task_comment(self, m_request, base_absolut_url): + m_request.return_value = "response" + + data = [ + { + "value": "new text", + "op": PatchOperation.REPLACE, + "path": "/text", + } + ] + + resource = self.get_resource(base_absolut_url) + assert ( + resource.edit_task_comment(projectId=1, taskId=2, taskCommentId=3, data=data) + == "response" + ) + m_request.assert_called_once_with( + method="patch", + path=resource.get_task_comments_path(projectId=1, taskId=2, taskCommentId=3), + request_data=data, + ) + class TestEnterpriseTasksResource: resource_class = EnterpriseTasksResource diff --git a/crowdin_api/api_resources/tasks/types.py b/crowdin_api/api_resources/tasks/types.py index d32f6b8..c94e4cc 100644 --- a/crowdin_api/api_resources/tasks/types.py +++ b/crowdin_api/api_resources/tasks/types.py @@ -54,3 +54,11 @@ class EnterpriseTaskSettingsTemplateConfigLanguage(TypedDict): class EnterpriseTaskSettingsTemplateLanguages(TypedDict): languages: Iterable[EnterpriseTaskSettingsTemplateConfigLanguage] + + +# Task comments patch request (moved from removed task_comments package) +class TaskCommentPatchRequest(TypedDict): + value: Any + op: PatchOperation + # For now only /text is supported in edit operations + path: str