Skip to content

Commit

Permalink
[skip ci] feat(workflow): u#1540 t#4000 create workflow endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
tdelatorre committed Sep 12, 2023
1 parent 81c2c81 commit 0c262f2
Show file tree
Hide file tree
Showing 23 changed files with 449 additions and 44 deletions.
Binary file modified .github/sql-fixtures/fixtures.sql
Binary file not shown.
3 changes: 3 additions & 0 deletions python/apps/taiga/src/taiga/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class Settings(BaseSettings):
INVITATION_RESEND_LIMIT: int = 10
INVITATION_RESEND_TIME: int = 10 # 10 minutes

# Workflows
MAX_NUM_WORKFLOWS: int = 8

# Tasks (linux crontab style)
CLEAN_EXPIRED_TOKENS_CRON: str = "0 0 * * *" # default: once a day
CLEAN_EXPIRED_USERS_CRON: str = "0 0 * * *" # default: once a day
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@
{
"slug": "main",
"name": "Main",
"order": 1,
"statuses": [
{"name": "New", "order": 1, "color": 1},
{"name": "Ready", "order": 2, "color": 2},
{"name": "In progress", "order": 3, "color": 3},
{"name": "Done", "order": 4, "color": 4}
]
"order": 1
}
],
"workflow_statuses": [
{"name": "New", "order": 1, "color": 1},
{"name": "Ready", "order": 2, "color": 2},
{"name": "In progress", "order": 3, "color": 3},
{"name": "Done", "order": 4, "color": 4}
]
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (c) 2023-present Kaleidos INC

# Generated by Django 4.2.3 on 2023-09-06 13:34

import django.contrib.postgres.fields.jsonb
from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("projects", "0006_alter_project_created_by_alter_project_workspace"),
]

operations = [
migrations.AddField(
model_name="projecttemplate",
name="workflow_statuses",
field=django.contrib.postgres.fields.jsonb.JSONField(
blank=True, null=True, verbose_name="workflow_statuses"
),
),
]
5 changes: 5 additions & 0 deletions python/apps/taiga/src/taiga/projects/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ def __repr__(self) -> str:
def slug(self) -> str:
return slugify(self.name)

@property
def num_workflows(self) -> int:
return len(self.workflows.all())

@property
def public_user_can_view(self) -> bool:
"""
Expand Down Expand Up @@ -99,6 +103,7 @@ class ProjectTemplate(models.BaseModel):
slug = models.LowerSlugField(max_length=250, null=False, blank=True, unique=True, verbose_name="slug")
roles = models.JSONField(null=True, blank=True, verbose_name="roles")
workflows = models.JSONField(null=True, blank=True, verbose_name="workflows")
workflow_statuses = models.JSONField(null=True, blank=True, verbose_name="workflow_statuses")

class Meta:
verbose_name = "project template"
Expand Down
6 changes: 3 additions & 3 deletions python/apps/taiga/src/taiga/projects/projects/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def _apply_select_related_to_project_queryset(
return qs.select_related(*select_related)


ProjectPrefetchRelated = list[Literal["workspace",]]
ProjectPrefetchRelated = list[Literal["workspace", "workflows"]]


def _apply_prefetch_related_to_project_queryset(
Expand Down Expand Up @@ -166,7 +166,7 @@ def list_projects(
def get_project(
filters: ProjectFilters = {},
select_related: ProjectSelectRelated = ["workspace"],
prefetch_related: ProjectPrefetchRelated = ["workspace"],
prefetch_related: ProjectPrefetchRelated = ["workspace", "workflows"],
) -> Project | None:
qs = _apply_filters_to_project_queryset(qs=DEFAULT_QUERYSET, filters=filters)
qs = _apply_select_related_to_project_queryset(qs=qs, select_related=select_related)
Expand Down Expand Up @@ -278,7 +278,7 @@ def apply_template_to_project_sync(template: ProjectTemplate, project: Project)
order=workflow["order"],
project=project,
)
for status in workflow["statuses"]:
for status in template.workflow_statuses:
workflows_repositories.create_workflow_status_sync(
name=status["name"],
color=status["color"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from taiga.base.serializers import UUIDB64, BaseModel
from taiga.projects.projects.serializers.mixins import ProjectLogoMixin
from taiga.workflows.serializers.nested import WorkflowNestedSerializer
from taiga.workspaces.workspaces.serializers.nested import WorkspaceNestedSerializer


Expand All @@ -28,6 +29,7 @@ class ProjectDetailSerializer(BaseModel, ProjectLogoMixin):
description: str
color: int
workspace: WorkspaceNestedSerializer
workflows: list[WorkflowNestedSerializer]

# User related fields
user_is_admin: bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

from taiga.projects.projects.models import Project
from taiga.projects.projects.serializers import ProjectDetailSerializer
from taiga.workflows.serializers.nested import WorkflowNestedSerializer
from taiga.workspaces.workspaces.serializers.nested import WorkspaceNestedSerializer


def serialize_project_detail(
project: Project,
workspace: WorkspaceNestedSerializer,
workflows: list[WorkflowNestedSerializer],
user_is_admin: bool,
user_is_member: bool,
user_has_pending_invitation: bool,
Expand All @@ -26,6 +28,7 @@ def serialize_project_detail(
color=project.color,
logo=project.logo,
workspace=workspace,
workflows=workflows,
user_is_admin=user_is_admin,
user_is_member=user_is_member,
user_has_pending_invitation=user_has_pending_invitation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from taiga.projects.roles import repositories as pj_roles_repositories
from taiga.users import services as users_services
from taiga.users.models import AnyUser, User
from taiga.workflows import services as workflows_services
from taiga.workspaces.memberships import repositories as workspace_memberships_repositories
from taiga.workspaces.workspaces import services as workspaces_services
from taiga.workspaces.workspaces.models import Workspace
Expand Down Expand Up @@ -134,8 +135,7 @@ async def list_workspace_invited_projects_for_user(workspace: Workspace, user: U

async def get_project(id: UUID) -> Project | None:
return await projects_repositories.get_project(
filters={"id": id},
select_related=["workspace"],
filters={"id": id}, select_related=["workspace"], prefetch_related=["workflows"]
)


Expand Down Expand Up @@ -166,9 +166,12 @@ async def get_project_detail(project: Project, user: AnyUser) -> ProjectDetailSe
else await pj_invitations_services.has_pending_project_invitation(user=user, project=project)
)

workflows = await workflows_services.list_workflows_nested(project_id=project.id)

return serializers_services.serialize_project_detail(
project=project,
workspace=workspace,
workflows=workflows,
user_is_admin=is_project_admin,
user_is_member=is_project_member,
user_permissions=user_permissions,
Expand Down
47 changes: 39 additions & 8 deletions python/apps/taiga/src/taiga/workflows/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
ReorderWorkflowStatusesValidator,
UpdateWorkflowStatusValidator,
WorkflowStatusValidator,
WorkflowValidator,
)
from taiga.workflows.models import Workflow, WorkflowStatus
from taiga.workflows.serializers import ReorderWorkflowStatusesSerializer, WorkflowSerializer, WorkflowStatusSerializer

# PERMISSIONS
CREATE_WORKFLOW = IsProjectAdmin()
LIST_WORKFLOWS = HasPerm("view_story")
GET_WORKFLOW = HasPerm("view_story")
CREATE_WORKFLOW_STATUS = IsProjectAdmin()
Expand All @@ -40,25 +42,54 @@
REORDER_WORKFLOW_STATUSES_200 = responses.http_status_200(model=ReorderWorkflowStatusesSerializer)


################################################
# create workflow
################################################


@routes.workflows.post(
"/projects/{project_id}/workflows",
name="project.workflow.create",
summary="Create workflows",
responses=GET_WORKFLOW_200 | ERROR_403 | ERROR_404 | ERROR_422,
response_model=None,
)
async def create_workflow(
project_id: B64UUID,
request: AuthRequest,
form: WorkflowValidator,
) -> WorkflowSerializer:
"""
Creates a workflow for a project
"""
project = await get_project_or_404(project_id)
await check_permissions(permissions=CREATE_WORKFLOW, user=request.user, obj=project)

return await workflows_services.create_workflow(
name=form.name,
project=project,
)


################################################
# list workflows
################################################


@routes.workflows.get(
"/projects/{id}/workflows",
"/projects/{project_id}/workflows",
name="project.workflow.list",
summary="List workflows",
responses=LIST_WORKFLOW_200 | ERROR_403 | ERROR_404 | ERROR_422,
response_model=None,
)
async def list_workflows(id: B64UUID, request: Request) -> list[WorkflowSerializer]:
async def list_workflows(project_id: B64UUID, request: Request) -> list[WorkflowSerializer]:
"""
List the workflows of a project
"""
project = await get_project_or_404(id)
project = await get_project_or_404(project_id)
await check_permissions(permissions=LIST_WORKFLOWS, user=request.user, obj=project)
return await workflows_services.list_workflows(project_id=id)
return await workflows_services.list_workflows(project_id=project_id)


################################################
Expand All @@ -67,23 +98,23 @@ async def list_workflows(id: B64UUID, request: Request) -> list[WorkflowSerializ


@routes.workflows.get(
"/projects/{id}/workflows/{workflow_slug}",
"/projects/{project_id}/workflows/{workflow_slug}",
name="project.workflow.get",
summary="Get project workflow",
responses=GET_WORKFLOW_200 | ERROR_403 | ERROR_404 | ERROR_422,
response_model=None,
)
async def get_workflow(
id: B64UUID,
project_id: B64UUID,
workflow_slug: str,
request: Request,
) -> WorkflowSerializer:
"""
Get the details of a workflow
"""
workflow = await get_workflow_or_404(project_id=id, workflow_slug=workflow_slug)
workflow = await get_workflow_or_404(project_id=project_id, workflow_slug=workflow_slug)
await check_permissions(permissions=GET_WORKFLOW, user=request.user, obj=workflow)
return await workflows_services.get_workflow_detail(project_id=id, workflow_slug=workflow_slug)
return await workflows_services.get_workflow_detail(project_id=project_id, workflow_slug=workflow_slug)


################################################
Expand Down
16 changes: 13 additions & 3 deletions python/apps/taiga/src/taiga/workflows/api/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,29 @@
from taiga.exceptions import api as ex


class Name(ConstrainedStr):
class WorkflowStatusName(ConstrainedStr):
strip_whitespace = True
min_length = 1
max_length = 30


class WorkflowName(ConstrainedStr):
strip_whitespace = True
min_length = 1
max_length = 40


class WorkflowValidator(BaseModel):
name: WorkflowName


class WorkflowStatusValidator(BaseModel):
name: Name
name: WorkflowStatusName
color: conint(gt=0, lt=9) # type: ignore


class UpdateWorkflowStatusValidator(BaseModel):
name: Name | None
name: WorkflowStatusName | None


class ReorderValidator(BaseModel):
Expand Down
12 changes: 11 additions & 1 deletion python/apps/taiga/src/taiga/workflows/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,30 @@
from taiga.events import events_manager
from taiga.projects.projects.models import Project
from taiga.workflows.events.content import (
CreateWorkflowContent,
CreateWorkflowStatusContent,
DeleteWorkflowStatusContent,
ReorderWorkflowStatusesContent,
UpdateWorkflowStatusContent,
)
from taiga.workflows.models import WorkflowStatus
from taiga.workflows.serializers import ReorderWorkflowStatusesSerializer
from taiga.workflows.serializers import ReorderWorkflowStatusesSerializer, WorkflowSerializer

CREATE_WORKFLOW = "workflows.create"
CREATE_WORKFLOW_STATUS = "workflowstatuses.create"
UPDATE_WORKFLOW_STATUS = "workflowstatuses.update"
REORDER_WORKFLOW_STATUS = "workflowstatuses.reorder"
DELETE_WORKFLOW_STATUS = "workflowstatuses.delete"


async def emit_event_when_workflow_is_created(project: Project, workflow: WorkflowSerializer) -> None:
await events_manager.publish_on_project_channel(
project=project,
type=CREATE_WORKFLOW,
content=CreateWorkflowContent(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
6 changes: 5 additions & 1 deletion python/apps/taiga/src/taiga/workflows/events/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
# Copyright (c) 2023-present Kaleidos INC

from taiga.base.serializers import BaseModel
from taiga.workflows.serializers import ReorderWorkflowStatusesSerializer, WorkflowStatusSerializer
from taiga.workflows.serializers import ReorderWorkflowStatusesSerializer, WorkflowSerializer, WorkflowStatusSerializer


class CreateWorkflowContent(BaseModel):
workflow: WorkflowSerializer


class CreateWorkflowStatusContent(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (c) 2023-present Kaleidos INC

# Generated by Django 4.2.3 on 2023-09-07 10:54

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("workflows", "0003_remove_workflowstatus_workflows_workflowstatus_unique_workflow_slug_and_more"),
]

operations = [
migrations.RemoveConstraint(
model_name="workflow",
name="workflows_workflow_unique_project_name",
),
migrations.AlterField(
model_name="workflow",
name="order",
field=models.DecimalField(decimal_places=10, default=100, max_digits=16, verbose_name="order"),
),
]
Loading

0 comments on commit 0c262f2

Please sign in to comment.