Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(workflow): u#1540 t#4000 create workflow endpoint #483

Merged
merged 1 commit into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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-14 12:48

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"
),
),
]
1 change: 1 addition & 0 deletions python/apps/taiga/src/taiga/projects/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,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["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 = [],
) -> 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.models import Workflow
from taiga.workspaces.workspaces.serializers.nested import WorkspaceNestedSerializer


def serialize_project_detail(
project: Project,
workspace: WorkspaceNestedSerializer,
workflows: list[Workflow],
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
15 changes: 11 additions & 4 deletions python/apps/taiga/src/taiga/projects/projects/services/__init__.py
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 repositories as workflows_repositories
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 @@ -99,7 +100,7 @@ async def _create_project(
async def list_projects(workspace_id: UUID) -> list[Project]:
return await projects_repositories.list_projects(
filters={"workspace_id": workspace_id},
prefetch_related=["workspace"],
select_related=["workspace"],
)


Expand All @@ -113,7 +114,7 @@ async def list_workspace_projects_for_user(workspace: Workspace, user: User) ->

return await projects_repositories.list_projects(
filters={"workspace_id": workspace.id, "project_member_id": user.id},
prefetch_related=["workspace"],
select_related=["workspace"],
)


Expand All @@ -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,16 @@ 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_repositories.list_workflows(
filters={
"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
51 changes: 41 additions & 10 deletions python/apps/taiga/src/taiga/workflows/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@
from taiga.routers import routes
from taiga.workflows import services as workflows_services
from taiga.workflows.api.validators import (
CreateWorkflowStatusValidator,
CreateWorkflowValidator,
DeleteWorkflowStatusQuery,
ReorderWorkflowStatusesValidator,
UpdateWorkflowStatusValidator,
WorkflowStatusValidator,
)
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: CreateWorkflowValidator,
) -> 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 Expand Up @@ -115,7 +146,7 @@ async def create_workflow_status(
project_id: B64UUID,
workflow_slug: str,
request: AuthRequest,
form: WorkflowStatusValidator,
form: CreateWorkflowStatusValidator,
) -> WorkflowStatus:
"""
Creates a workflow status in the given project workflow
Expand Down
18 changes: 14 additions & 4 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 WorkflowStatusValidator(BaseModel):
name: Name
class WorkflowName(ConstrainedStr):
strip_whitespace = True
min_length = 1
max_length = 40


class CreateWorkflowValidator(BaseModel):
name: WorkflowName


class CreateWorkflowStatusValidator(BaseModel):
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
Loading
Loading