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 6, 2023
1 parent 98ac25e commit f1f36cc
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 41 deletions.
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 @@ -87,6 +87,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 @@ -36,15 +36,14 @@
"workflows": [
{
"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}
]
"name": "Main"
}
],
"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
9 changes: 4 additions & 5 deletions python/apps/taiga/src/taiga/projects/projects/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def _apply_filters_to_project_queryset(
return qs.filter(**filter_data)


ProjectSelectRelated = list[Literal["workspace",]]
ProjectSelectRelated = list[Literal["workspace"]]


def _apply_select_related_to_project_queryset(
Expand All @@ -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 @@ -275,10 +275,9 @@ def apply_template_to_project_sync(template: ProjectTemplate, project: Project)
wf = workflows_repositories.create_workflow_sync(
name=workflow["name"],
slug=workflow["slug"],
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 @@ -134,8 +134,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
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- 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-05 13:25

import taiga.base.utils.datetime
from django.db import migrations, models


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

operations = [
migrations.AlterModelOptions(
name="workflow",
options={
"ordering": ["project", "created_at"],
"verbose_name": "workflow",
"verbose_name_plural": "workflows",
},
),
migrations.RemoveConstraint(
model_name="workflow",
name="workflows_workflow_unique_project_name",
),
migrations.RemoveField(
model_name="workflow",
name="order",
),
migrations.AddField(
model_name="workflow",
name="created_at",
field=models.DateTimeField(default=taiga.base.utils.datetime.aware_utcnow, verbose_name="created at"),
),
]
22 changes: 17 additions & 5 deletions python/apps/taiga/src/taiga/workflows/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
#
# Copyright (c) 2023-present Kaleidos INC

from typing import Any

from taiga.base.db import models
from taiga.base.utils.datetime import timestamp_mics
from taiga.base.db.mixins import CreatedAtMetaInfoMixin
from taiga.base.utils.slug import generate_incremental_int_suffix, slugify_uniquely_for_queryset
from taiga.projects.projects.models import Project


class Workflow(models.BaseModel):
class Workflow(models.BaseModel, CreatedAtMetaInfoMixin):
name = models.CharField(max_length=250, null=False, blank=False, verbose_name="name")
slug = models.LowerSlugField(max_length=250, null=False, blank=False, verbose_name="slug")
order = models.BigIntegerField(default=timestamp_mics, null=False, blank=False, verbose_name="order")
project = models.ForeignKey(
"projects.Project",
null=False,
Expand All @@ -28,19 +30,29 @@ class Meta:
verbose_name_plural = "workflows"
constraints = [
models.UniqueConstraint(fields=["project", "slug"], name="%(app_label)s_%(class)s_unique_project_slug"),
models.UniqueConstraint(fields=["project", "name"], name="%(app_label)s_%(class)s_unique_project_name"),
]
indexes = [
models.Index(fields=["project", "slug"]),
]
ordering = ["project", "order", "name"]
ordering = ["project", "created_at"]

def __str__(self) -> str:
return self.name

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,
)

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


class WorkflowStatus(models.BaseModel):
name = models.CharField(max_length=30, null=False, blank=False, verbose_name="name")
Expand Down
Loading

0 comments on commit f1f36cc

Please sign in to comment.