From 5d0e9aafc44e9571fba42377da76267e73e84efd Mon Sep 17 00:00:00 2001 From: "teresa.delatorre" Date: Wed, 27 Sep 2023 15:42:51 +0200 Subject: [PATCH] feat(story): u#3725 t#4044 update story workflow --- .../taiga/stories/stories/api/validators.py | 1 + .../stories/stories/services/__init__.py | 36 +++++++- .../stories/stories/services/exceptions.py | 8 ++ .../taiga/src/taiga/workflows/repositories.py | 2 +- .../taiga/stories/stories/test_api.py | 15 +++- .../taiga/stories/stories/test_services.py | 84 +++++++++++++++++++ .../postman/taiga.postman_collection.json | 4 +- 7 files changed, 142 insertions(+), 8 deletions(-) diff --git a/python/apps/taiga/src/taiga/stories/stories/api/validators.py b/python/apps/taiga/src/taiga/stories/stories/api/validators.py index 4bc41b216..fd09e8536 100644 --- a/python/apps/taiga/src/taiga/stories/stories/api/validators.py +++ b/python/apps/taiga/src/taiga/stories/stories/api/validators.py @@ -29,6 +29,7 @@ class UpdateStoryValidator(BaseModel): title: Title | None description: str | None status: B64UUID | None + workflow: str | None class ReorderValidator(BaseModel): diff --git a/python/apps/taiga/src/taiga/stories/stories/services/__init__.py b/python/apps/taiga/src/taiga/stories/stories/services/__init__.py index 4ce7c3160..4904caffb 100644 --- a/python/apps/taiga/src/taiga/stories/stories/services/__init__.py +++ b/python/apps/taiga/src/taiga/stories/stories/services/__init__.py @@ -108,7 +108,7 @@ async def list_paginated_stories( async def get_story(project_id: UUID, ref: int) -> Story | None: return await stories_repositories.get_story( filters={"ref": ref, "project_id": project_id}, - select_related=["project", "workspace"], + select_related=["project", "workspace", "workflow"], ) @@ -181,10 +181,30 @@ async def _validate_and_process_values_to_update(story: Story, values: dict[str, output["status"] = status # Calculate new order - latest_story = await stories_repositories.list_stories( - filters={"status_id": status.id}, order_by=["-order"], offset=0, limit=1 + output["order"] = await _calculate_next_order(status_id=status.id) + + elif workflow_slug := output.pop("workflow", None): + workflow = await workflows_repositories.get_workflow( + filters={"project_id": story.project_id, "slug": workflow_slug}, prefetch_related=["statuses"] + ) + + if not workflow: + raise ex.InvalidWorkflowError("The provided workflow is not valid.") + elif workflow.slug != story.workflow.slug: + output["workflow"] = workflow + + # Set first status + first_status = await workflows_repositories.list_workflow_statuses( + filters={"workflow_id": workflow.id}, order_by=["order"], offset=0, limit=1 ) - output["order"] = DEFAULT_ORDER_OFFSET + (latest_story[0].order if latest_story else 0) + + if not first_status: + raise ex.WorkflowHasNotStatusesError("The provided workflow hasn't any statuses.") + + output["status"] = first_status[0] + + # Calculate new order + output["order"] = await _calculate_next_order(status_id=first_status[0].id) return output @@ -304,6 +324,14 @@ async def reorder_stories( return reorder_story_serializer +async def _calculate_next_order(status_id: UUID) -> Decimal: + latest_story = await stories_repositories.list_stories( + filters={"status_id": status_id}, order_by=["-order"], offset=0, limit=1 + ) + + return DEFAULT_ORDER_OFFSET + (latest_story[0].order if latest_story else 0) + + ########################################################## # delete story ########################################################## diff --git a/python/apps/taiga/src/taiga/stories/stories/services/exceptions.py b/python/apps/taiga/src/taiga/stories/stories/services/exceptions.py index 16637b4f2..d5fa12582 100644 --- a/python/apps/taiga/src/taiga/stories/stories/services/exceptions.py +++ b/python/apps/taiga/src/taiga/stories/stories/services/exceptions.py @@ -13,6 +13,14 @@ class InvalidStatusError(TaigaServiceException): ... +class InvalidWorkflowError(TaigaServiceException): + ... + + +class WorkflowHasNotStatusesError(TaigaServiceException): + ... + + class InvalidStoryRefError(TaigaServiceException): ... diff --git a/python/apps/taiga/src/taiga/workflows/repositories.py b/python/apps/taiga/src/taiga/workflows/repositories.py index d15927c56..0176ce69c 100644 --- a/python/apps/taiga/src/taiga/workflows/repositories.py +++ b/python/apps/taiga/src/taiga/workflows/repositories.py @@ -149,7 +149,7 @@ def get_workflow( ########################################################## -# update workflow +# Workflow - update workflow ########################################################## diff --git a/python/apps/taiga/tests/integration/taiga/stories/stories/test_api.py b/python/apps/taiga/tests/integration/taiga/stories/stories/test_api.py index 4b05a9869..7d8a8accf 100644 --- a/python/apps/taiga/tests/integration/taiga/stories/stories/test_api.py +++ b/python/apps/taiga/tests/integration/taiga/stories/stories/test_api.py @@ -291,7 +291,7 @@ async def test_get_story_422_unprocessable_story_ref(client): ########################################################## -async def test_update_story_200_ok_unprotected_attribute_ok(client): +async def test_update_story_200_ok_unprotected_attribute_status_ok(client): project = await f.create_project() workflow = await project.workflows.afirst() status1 = await workflow.statuses.afirst() @@ -304,6 +304,19 @@ async def test_update_story_200_ok_unprotected_attribute_ok(client): assert response.status_code == status.HTTP_200_OK, response.text +async def test_update_story_200_ok_unprotected_attribute_workflow_ok(client): + project = await f.create_project() + workflow1 = await project.workflows.afirst() + status1 = await workflow1.statuses.afirst() + workflow2 = await f.create_workflow(project=project) + story = await f.create_story(project=project, workflow=workflow1, status=status1) + + data = {"version": story.version, "workflow": workflow2.slug} + client.login(project.created_by) + response = client.patch(f"/projects/{project.b64id}/stories/{story.ref}", json=data) + assert response.status_code == status.HTTP_200_OK, response.text + + async def test_update_story_200_ok_protected_attribute_ok(client): project = await f.create_project() workflow = await project.workflows.afirst() diff --git a/python/apps/taiga/tests/unit/taiga/stories/stories/test_services.py b/python/apps/taiga/tests/unit/taiga/stories/stories/test_services.py index 97d6d3ea1..254e797a7 100644 --- a/python/apps/taiga/tests/unit/taiga/stories/stories/test_services.py +++ b/python/apps/taiga/tests/unit/taiga/stories/stories/test_services.py @@ -399,6 +399,90 @@ async def test_validate_and_process_values_to_update_error_wrong_status(): fake_stories_repo.list_stories.assert_not_awaited() +async def test_validate_and_process_values_to_update_ok_with_workflow(): + project = f.build_project() + workflow1 = f.build_workflow(project=project) + status1 = f.build_workflow_status(workflow=workflow1) + story1 = f.build_story(project=project, workflow=workflow1, status=status1) + workflow2 = f.build_workflow(project=project) + status2 = f.build_workflow_status(workflow=workflow2) + status3 = f.build_workflow_status(workflow=workflow2) + story2 = f.build_story(project=project, workflow=workflow2, status=status2) + values = {"version": story1.version, "workflow": workflow2.slug} + + with ( + patch("taiga.stories.stories.services.stories_repositories", autospec=True) as fake_stories_repo, + patch("taiga.stories.stories.services.workflows_repositories", autospec=True) as fake_workflows_repo, + ): + fake_workflows_repo.get_workflow.return_value = workflow2 + fake_workflows_repo.list_workflow_statuses.return_value = [status2] + fake_stories_repo.list_stories.return_value = [story2, status3] + + valid_values = await services._validate_and_process_values_to_update(story=story1, values=values) + + fake_workflows_repo.get_workflow.assert_awaited_once_with( + filters={"project_id": story1.project_id, "slug": workflow2.slug}, prefetch_related=["statuses"] + ) + fake_workflows_repo.list_workflow_statuses.assert_awaited_once_with( + filters={"workflow_id": workflow2.id}, order_by=["order"], offset=0, limit=1 + ) + fake_stories_repo.list_stories.assert_awaited_once_with( + filters={"status_id": status2.id}, + order_by=["-order"], + offset=0, + limit=1, + ) + + assert valid_values["workflow"] == workflow2 + assert valid_values["order"] == services.DEFAULT_ORDER_OFFSET + story2.order + + +async def test_validate_and_process_values_to_update_error_wrong_workflow(): + story = f.build_story() + values = {"version": story.version, "workflow": "wrong_workflow"} + + with ( + patch("taiga.stories.stories.services.stories_repositories", autospec=True) as fake_stories_repo, + patch("taiga.stories.stories.services.workflows_repositories", autospec=True) as fake_workflows_repo, + ): + fake_workflows_repo.get_workflow.return_value = None + + with pytest.raises(ex.InvalidWorkflowError): + await services._validate_and_process_values_to_update(story=story, values=values) + + fake_workflows_repo.get_workflow.assert_awaited_once_with( + filters={"project_id": story.project_id, "slug": "wrong_workflow"}, prefetch_related=["statuses"] + ) + fake_stories_repo.list_stories.assert_not_awaited() + + +async def test_validate_and_process_values_to_update_error_workflow_without_statuses(): + project = f.build_project() + workflow1 = f.build_workflow(project=project) + status1 = f.build_workflow_status(workflow=workflow1) + story = f.build_story(project=project, workflow=workflow1, status=status1) + workflow2 = f.build_workflow(project=project, statuses=None) + values = {"version": story.version, "workflow": workflow2.slug} + + with ( + patch("taiga.stories.stories.services.stories_repositories", autospec=True) as fake_stories_repo, + patch("taiga.stories.stories.services.workflows_repositories", autospec=True) as fake_workflows_repo, + ): + fake_workflows_repo.get_workflow.return_value = workflow2 + fake_workflows_repo.list_workflow_statuses.return_value = [] + + with pytest.raises(ex.WorkflowHasNotStatusesError): + await services._validate_and_process_values_to_update(story=story, values=values) + + fake_workflows_repo.get_workflow.assert_awaited_once_with( + filters={"project_id": story.project_id, "slug": workflow2.slug}, prefetch_related=["statuses"] + ) + fake_workflows_repo.list_workflow_statuses.assert_awaited_once_with( + filters={"workflow_id": workflow2.id}, order_by=["order"], offset=0, limit=1 + ) + fake_stories_repo.list_stories.assert_not_awaited() + + ####################################################### # _calculate_offset ####################################################### diff --git a/python/docs/postman/taiga.postman_collection.json b/python/docs/postman/taiga.postman_collection.json index 7ff79ef28..97108daec 100644 --- a/python/docs/postman/taiga.postman_collection.json +++ b/python/docs/postman/taiga.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "a9dc834f-1ba0-4cc9-907f-f5eac02b8f85", + "_postman_id": "b95931e2-27bf-4229-bdcb-363116c53bea", "name": "taiga-next", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "9835734" @@ -3007,7 +3007,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"title\": \"New title\",\n \"description\": \"New description\",\n \"version\": {{story_version}}\n}", + "raw": "{\n \"title\": \"New title\",\n \"description\": \"New description\",\n \"version\": {{story_version}},\n \"workflow\": \"new-workflow\"\n}", "options": { "raw": { "language": "json"