Skip to content

Commit

Permalink
feat(workflows): u#2860 t#4028 update workflow endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
tdelatorre committed Sep 26, 2023
1 parent a6d8241 commit 55a86ff
Show file tree
Hide file tree
Showing 13 changed files with 336 additions and 11 deletions.
32 changes: 31 additions & 1 deletion python/apps/taiga/src/taiga/workflows/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from taiga.base.api.permissions import check_permissions
from taiga.base.validators import B64UUID
from taiga.exceptions import api as ex
from taiga.exceptions.api.errors import ERROR_403, ERROR_404, ERROR_422
from taiga.exceptions.api.errors import ERROR_400, ERROR_403, ERROR_404, ERROR_422
from taiga.permissions import HasPerm, IsProjectAdmin
from taiga.projects.projects.api import get_project_or_404
from taiga.routers import routes
Expand All @@ -23,6 +23,7 @@
DeleteWorkflowStatusQuery,
ReorderWorkflowStatusesValidator,
UpdateWorkflowStatusValidator,
UpdateWorkflowValidator,
)
from taiga.workflows.models import Workflow, WorkflowStatus
from taiga.workflows.serializers import ReorderWorkflowStatusesSerializer, WorkflowSerializer, WorkflowStatusSerializer
Expand All @@ -31,6 +32,7 @@
CREATE_WORKFLOW = IsProjectAdmin()
LIST_WORKFLOWS = HasPerm("view_story")
GET_WORKFLOW = HasPerm("view_story")
UPDATE_WORKFLOW = IsProjectAdmin()
CREATE_WORKFLOW_STATUS = IsProjectAdmin()
UPDATE_WORKFLOW_STATUS = IsProjectAdmin()
DELETE_WORKFLOW_STATUS = IsProjectAdmin()
Expand Down Expand Up @@ -117,6 +119,34 @@ async def get_workflow(
return await workflows_services.get_workflow_detail(project_id=project_id, workflow_slug=workflow_slug)


##########################################################
# update workflow
##########################################################


@routes.workflows.patch(
"/projects/{project_id}/workflows/{workflow_slug}",
name="project.workflow.update",
summary="Update workflow",
responses=GET_WORKFLOW_200 | ERROR_400 | ERROR_403 | ERROR_404 | ERROR_422,
response_model=None,
)
async def update_workflow(
project_id: B64UUID,
workflow_slug: str,
request: AuthRequest,
form: UpdateWorkflowValidator,
) -> WorkflowSerializer:
"""
Update workflow
"""
workflow = await get_workflow_or_404(project_id=project_id, workflow_slug=workflow_slug)
await check_permissions(permissions=UPDATE_WORKFLOW, user=request.user, obj=workflow)

values = form.dict(exclude_unset=True)
return await workflows_services.update_workflow(project_id=project_id, workflow=workflow, values=values)


################################################
# misc
################################################
Expand Down
4 changes: 4 additions & 0 deletions python/apps/taiga/src/taiga/workflows/api/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class CreateWorkflowStatusValidator(BaseModel):
color: conint(gt=0, lt=9) # type: ignore


class UpdateWorkflowValidator(BaseModel):
name: WorkflowName


class UpdateWorkflowStatusValidator(BaseModel):
name: WorkflowStatusName | None

Expand Down
12 changes: 12 additions & 0 deletions python/apps/taiga/src/taiga/workflows/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
CreateWorkflowStatusContent,
DeleteWorkflowStatusContent,
ReorderWorkflowStatusesContent,
UpdateWorkflowContent,
UpdateWorkflowStatusContent,
)
from taiga.workflows.models import WorkflowStatus
from taiga.workflows.serializers import ReorderWorkflowStatusesSerializer, WorkflowSerializer

CREATE_WORKFLOW = "workflows.create"
UPDATE_WORKFLOW = "workflows.update"
CREATE_WORKFLOW_STATUS = "workflowstatuses.create"
UPDATE_WORKFLOW_STATUS = "workflowstatuses.update"
REORDER_WORKFLOW_STATUS = "workflowstatuses.reorder"
Expand All @@ -32,6 +34,16 @@ async def emit_event_when_workflow_is_created(project: Project, workflow: Workfl
)


async def emit_event_when_workflow_is_updated(project: Project, workflow: WorkflowSerializer) -> None:
await events_manager.publish_on_project_channel(
project=project,
type=UPDATE_WORKFLOW,
content=UpdateWorkflowContent(
workflow=workflow,
),
)


async def emit_event_when_workflow_status_is_created(project: Project, workflow_status: WorkflowStatus) -> None:
await events_manager.publish_on_project_channel(
project=project,
Expand Down
4 changes: 4 additions & 0 deletions python/apps/taiga/src/taiga/workflows/events/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class CreateWorkflowContent(BaseModel):
workflow: WorkflowSerializer


class UpdateWorkflowContent(BaseModel):
workflow: WorkflowSerializer


class CreateWorkflowStatusContent(BaseModel):
workflow_status: WorkflowStatusSerializer

Expand Down
13 changes: 6 additions & 7 deletions python/apps/taiga/src/taiga/workflows/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,12 @@ def __repr__(self) -> str:
return f"<Workflow {self.name}>"

def save(self, *args: Any, **kwargs: Any) -> None:
if not self.slug:
self.slug = slugify_uniquely_for_queryset(
value=self.name,
queryset=self.project.workflows.all(),
generate_suffix=generate_incremental_int_suffix(),
use_always_suffix=False,
)
self.slug = slugify_uniquely_for_queryset(
value=self.name,
queryset=self.project.workflows.all(),
generate_suffix=generate_incremental_int_suffix(),
use_always_suffix=False,
)

super().save(*args, **kwargs)

Expand Down
14 changes: 14 additions & 0 deletions python/apps/taiga/src/taiga/workflows/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,20 @@ def get_workflow(
return None


##########################################################
# update workflow
##########################################################


@sync_to_async
def update_workflow(workflow: Workflow, values: dict[str, Any] = {}) -> Workflow:
for attr, value in values.items():
setattr(workflow, attr, value)

workflow.save()
return workflow


##########################################################
# WorkflowStatus - filters and querysets
##########################################################
Expand Down
18 changes: 18 additions & 0 deletions python/apps/taiga/src/taiga/workflows/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ async def get_workflow_detail(project_id: UUID, workflow_slug: str) -> WorkflowS
return serializers_services.serialize_workflow(workflow=workflow, workflow_statuses=workflow_statuses)


##########################################################
# update workflow
##########################################################


async def update_workflow(project_id: UUID, workflow: Workflow, values: dict[str, Any] = {}) -> WorkflowSerializer:
updated_workflow = await workflows_repositories.update_workflow(workflow=workflow, values=values)
updated_workflow_detail = await get_workflow_detail(project_id=project_id, workflow_slug=updated_workflow.slug)

# Emit event
await workflows_events.emit_event_when_workflow_is_updated(
project=workflow.project,
workflow=updated_workflow_detail,
)

return updated_workflow_detail


##########################################################
# create workflow status
##########################################################
Expand Down
55 changes: 55 additions & 0 deletions python/apps/taiga/tests/integration/taiga/workflows/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,61 @@ async def test_get_workflow_422_unprocessable_project_b64id(client):
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, response.text


#################################################################
# Workflow PATCH /projects/<pj_b64id>/workflows/{wf_slug}
#################################################################


async def test_update_workflow_200_ok(client):
project = await f.create_project()
workflow = await f.create_workflow(project=project)
data = {"name": "updated name"}

client.login(project.created_by)
response = client.patch(f"/projects/{project.b64id}/workflows/{workflow.slug}", json=data)
assert response.status_code == status.HTTP_200_OK, response.text


async def test_update_workflow_403_forbidden_permissions(client):
project = await f.create_project()
workflow = await f.create_workflow(project=project)
user = await f.create_user()
data = {"name": "updated name"}

client.login(user)
response = client.patch(f"/projects/{project.b64id}/workflows/{workflow.slug}", json=data)
assert response.status_code == status.HTTP_403_FORBIDDEN, response.text


async def test_update_workflow_404_not_found_project_b64id(client):
project = await f.create_project()
workflow = await f.create_workflow(project=project)
data = {"name": "updated name"}

client.login(project.created_by)
response = client.patch(f"/projects/{NOT_EXISTING_B64ID}/workflows/{workflow.slug}", json=data)
assert response.status_code == status.HTTP_404_NOT_FOUND, response.text


async def test_update_workflow_404_workflow_slug(client):
project = await f.create_project()
data = {"name": "updated name"}

client.login(project.created_by)
response = client.patch(f"/projects/{project.b64id}/workflows/{NOT_EXISTING_SLUG}", json=data)
assert response.status_code == status.HTTP_404_NOT_FOUND, response.text


async def test_update_workflow_422_unprocessable_project_b64id(client):
project = await f.create_project()
workflow = await f.create_workflow(project=project)
data = {"name": "updated name"}

client.login(project.created_by)
response = client.patch(f"/projects/{INVALID_B64ID}/workflows/{workflow.slug}", json=data)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, response.text


################################################################################
# WorkflowStatus POST /projects/<pj_b64id>/workflows/<wf_slug>/statuses
################################################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ async def test_get_project_without_workflow_ok() -> None:
assert workflow is None


##########################################################
# update_workflow
##########################################################


async def test_update_workflow():
workflow = await f.create_workflow()
updated_workflow = await repositories.update_workflow(
workflow=workflow,
values={"name": "Updated name"},
)
assert updated_workflow.name == "Updated name"


##########################################################
# create_workflow_status
##########################################################
Expand Down
23 changes: 23 additions & 0 deletions python/apps/taiga/tests/unit/taiga/workflows/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,29 @@ async def test_get_detailed_workflow_ok():
fake_workflows_repo.get_workflow.assert_awaited_once()


#######################################################
# update_workflow
#######################################################


async def test_update_workflow_ok():
project = f.build_project()
workflow = f.build_workflow(project=project)
values = {"name": "updated name"}

with (
patch("taiga.workflows.services.workflows_repositories", autospec=True) as fake_workflows_repo,
patch("taiga.workflows.services.get_workflow_detail", autospec=True),
patch("taiga.workflows.services.workflows_events", autospec=True) as fake_workflows_events,
):
updated_workflow = await services.update_workflow(project_id=project.id, workflow=workflow, values=values)
fake_workflows_repo.update_workflow.assert_awaited_once_with(workflow=workflow, values=values)
fake_workflows_events.emit_event_when_workflow_is_updated.assert_awaited_once_with(
project=project,
workflow=updated_workflow,
)


#######################################################
# create_workflow_status
#######################################################
Expand Down
13 changes: 13 additions & 0 deletions python/docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,19 @@ Content for:
```


#### `workflows.update`

It happens when a workflow has been updated.

Content for:
- project channel:
```
{
"workflow": {... "workflow object" ...}
}
```


#### `workflowstatuses.create`

It happens when a new workflow status has been created.
Expand Down
64 changes: 62 additions & 2 deletions python/docs/postman/taiga.postman_collection.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "302d4e8d-fe62-488f-a80e-cf3520c18277",
"_postman_id": "a9dc834f-1ba0-4cc9-907f-f5eac02b8f85",
"name": "taiga-next",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "9835734"
Expand Down Expand Up @@ -2356,7 +2356,9 @@
"script": {
"exec": [
"// Post-request execution tasks",
""
"pm.test(\"Environment variable settings\", function () {",
" pm.environment.set(\"wf-slug\", pm.response.json().slug);",
"});"
],
"type": "text/javascript"
}
Expand Down Expand Up @@ -2501,6 +2503,64 @@
},
"response": []
},
{
"name": "update workflow",
"event": [
{
"listen": "test",
"script": {
"exec": [
"// Post-request execution tasks",
"pm.test(\"Environment variable settings\", function () {",
" pm.environment.set(\"ref1\", pm.response.json().ref);",
" pm.environment.set(\"story_ref\", pm.response.json().ref);",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disabledSystemHeaders": {}
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{auth_token}}",
"type": "string"
}
]
},
"method": "PATCH",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"updated workflow\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{protocol}}://{{domain}}{{port}}{{api_url}}/projects/{{pj-id}}/workflows/{{wf-slug}}",
"protocol": "{{protocol}}",
"host": [
"{{domain}}{{port}}{{api_url}}"
],
"path": [
"projects",
"{{pj-id}}",
"workflows",
"{{wf-slug}}"
]
}
},
"response": []
},
{
"name": "create workflow status",
"event": [
Expand Down
Loading

1 comment on commit 55a86ff

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/taiga
   __main__.py71710%17–155
src/taiga/attachments
   admin.py27390%25, 28, 70
   events.py8275%15, 20
   models.py19289%61, 64
   repositories.py49197%62
src/taiga/auth/services
   __init__.py47098%90–>93
src/taiga/base
   mocks.py741280%93–94, 107–118, 143–>exit
src/taiga/base/api
   permissions.py49199%22
   requests.py11380%15, 21–23
src/taiga/base/db
   __init__.py4175%15
   commands.py54540%8–271
   sequences.py45297%91–92
src/taiga/base/db/admin
   forms.py14838%17–25
   utils.py11736%17–23
src/taiga/base/db/models
   fields.py15188%20
src/taiga/base/django
   __init__.py5260%17–19
   settings.py30293%16–17
   urls.py330%8–11
src/taiga/base/i18n
   __init__.py102988%171–>170, 173–>170, 192–193, 203–205, 215, 268, 279, 283
   choices.py34291%80, 87
   commands.py74740%8–108
src/taiga/base/logging
   formatters.py241050%22–>exit, 38, 44–45, 51–60
   middlewares.py23196%20
src/taiga/base/repositories
   neighbors.py39391%43–44, 65
src/taiga/base/sampledata
   commands.py11110%8–40
   constants.py15150%14–51
   demo_data.py2732730%8–529
   factories.py1221220%9–353
   test_data.py2902900%8–634
src/taiga/base/serializers
   fields.py20196%21
src/taiga/base/utils
   commands.py31310%8–60
   datetime.py441860%54, 67, 126–147
   enum.py14192%43
   images.py26387%21–22, 30
   json.py331545%30–35, 37, 39–44, 46, 50
   pprint.py660%8–17
   slug.py23485%22–25
src/taiga/base/validators/fields
   i18n.py16291%19–20
   uuid.py15290%19–20
src/taiga/comments
   admin.py29392%16, 20, 24
   events.py11373%15, 20, 25
   models.py20291%43, 46
src/taiga/commons/storage
   commands.py13130%8–40
   models.py16288%38, 41
   services.py9092%17–>16
   tasks.py990%8–19
src/taiga/conf
   __init__.py94395%112, 128–129
   tokens.py17190%34
src/taiga/emails
   commands.py35350%9–68
   sender.py18571%48–50, 54–73
   tasks.py39490%44–45, 53–54
src/taiga/events
   app.py18192%21
   events.py16288%31–32
   logging.py392331%18–>exit, 32–48, 51–60
   manager.py1171190%78–79, 223–233, 239–242
   responses.py33684%33, 37–41
   subscriber.py80495%51, 55, 88, 91, 108–>120
src/taiga/events/actions
   auth.py231144%27–32, 39–45
   event_handlers.py9456%18–20, 28–30
   projects.py553038%36–40, 48–64, 72–80, 88–96
   workspaces.py553038%36–40, 48–64, 72–80, 88–96
src/taiga/events/pubsub/backends
   base.py30198%53
   redis.py66594%41–43, 85–86
src/taiga/exceptions/api
   __init__.py27097%28–>30
   handlers.py27290%27–28
   middlewares.py26287%37, 41
src/taiga/integrations/auth
   services.py39782%50–56
src/taiga/integrations/github
   services.py40098%63–>68
src/taiga/mediafiles
   admin.py20291%24, 27
   models.py26292%84, 87
src/taiga/permissions
   services.py103198%28, 106–>109
src/taiga/permissions/validators
   fields.py24388%18–19, 45
src/taiga/projects/invitations
   models.py25292%89, 92
   repositories.py84198%135
src/taiga/projects/invitations/api
   validators.py65789%38–40, 65–66, 84–85
src/taiga/projects/invitations/events
   __init__.py33191%37–>exit, 49–>exit, 74–>exit, 102
src/taiga/projects/invitations/services
   __init__.py183199%58–>53, 286
src/taiga/projects/memberships
   models.py16288%51, 54
   repositories.py86297%91, 124
src/taiga/projects/memberships/events
   __init__.py16288%18–24
src/taiga/projects/memberships/services
   __init__.py45195%64–>69, 111
src/taiga/projects/projects
   admin.py531275%33–34, 39–42, 50–51, 56–59, 90, 94
   models.py61886%64, 67, 78, 113, 116, 119–121
   repositories.py115496%125, 126–>129, 155, 254–255
   tasks.py6188%17
src/taiga/projects/projects/services
   __init__.py107592%78, 87, 199, 205–>208, 213, 235
src/taiga/projects/references
   mixins.py13093%25–>28
src/taiga/projects/roles
   api.py35393%46–48
   models.py26386%50, 53, 57
   repositories.py44196%92
src/taiga/projects/roles/services
   __init__.py25194%24, 57–>62
src/taiga/stories/assignments
   models.py15287%42, 45
   repositories.py52197%49
src/taiga/stories/attachments
   api.py51197%159
   serializers.py11093%20–>28
src/taiga/stories/comments
   api.py58197%177
src/taiga/stories/stories
   admin.py25584%76–81, 85, 89
   models.py25292%73, 76
src/taiga/stories/stories/services
   __init__.py113297%233, 256
src/taiga/tasksqueue
   app.py20286%29–30
   commands.py65650%8–122
   logging.py331740%18–>exit, 29–39, 42–51
   manager.py561377%37, 41, 45, 49, 94, 163, 177, 186, 191, 198, 202, 209, 213
   task.py12192%23
src/taiga/tokens
   admin.py441081%67, 70, 73, 76, 104, 108, 112, 116, 120, 124
   models.py30487%59, 62, 78, 81
src/taiga/users
   admin.py51781%32, 41, 50, 87–95
   commands.py990%8–26
   models.py651086%71, 74, 90, 93, 97, 100, 103, 106, 127, 130
   repositories.py204398%209–>212, 335, 428–429
src/taiga/users/api
   __init__.py66296%216, 304
src/taiga/users/api/validators
   __init__.py42392%39–41
src/taiga/users/services
   __init__.py244795%75, 259, 262–>257, 297–>280, 306–>280, 322–>318, 345–>341, 366, 494–>499, 513–520, 521–>526, 524–525
src/taiga/workflows
   models.py40490%42, 45, 85, 88
src/taiga/workflows/services
   __init__.py113494%53, 232, 259, 330–>338, 345
src/taiga/workspaces/invitations
   models.py24292%84, 87
   permissions.py9182%19
   repositories.py77198%123
src/taiga/workspaces/invitations/api
   validators.py18288%24–25
src/taiga/workspaces/invitations/events
   __init__.py25191%36–>exit, 60–>exit, 71
src/taiga/workspaces/invitations/services
   __init__.py123199%222
src/taiga/workspaces/memberships
   api.py43196%126
   models.py15287%43, 46
   repositories.py63295%72, 105
src/taiga/workspaces/memberships/services
   __init__.py46197%117
src/taiga/workspaces/workspaces
   admin.py571962%25–26, 31–34, 49, 52–57, 61, 65, 73–74, 79–82
   models.py18290%32, 35
   repositories.py127299%232–233
src/taiga/workspaces/workspaces/services
   __init__.py54294%113, 116
TOTAL9887156683% 

Tests Skipped Failures Errors Time
1142 1 💤 0 ❌ 0 🔥 23m 33s ⏱️

Please sign in to comment.