diff --git a/apps/api/plane/api/serializers/intake.py b/apps/api/plane/api/serializers/intake.py index d2cdd5a31..40cbba38b 100644 --- a/apps/api/plane/api/serializers/intake.py +++ b/apps/api/plane/api/serializers/intake.py @@ -1,7 +1,7 @@ # Module imports from .base import BaseSerializer from .issue import IssueExpandSerializer -from plane.db.models import IntakeIssue, Issue +from plane.db.models import IntakeIssue, Issue, State, StateGroup from rest_framework import serializers @@ -108,7 +108,6 @@ class IntakeIssueUpdateSerializer(BaseSerializer): Validate that if status is being changed to accepted (1), the project has a default state to transition to. """ - from plane.db.models import State # Check if status is being updated to accepted if attrs.get("status") == 1: @@ -116,7 +115,7 @@ class IntakeIssueUpdateSerializer(BaseSerializer): issue = intake_issue.issue # Check if issue is in TRIAGE state - if issue.state and issue.state.group == State.TRIAGE: + if issue.state and issue.state.group == StateGroup.TRIAGE.value: # Verify default state exists before allowing the update default_state = State.objects.filter( workspace=intake_issue.workspace, project=intake_issue.project, default=True @@ -133,7 +132,6 @@ class IntakeIssueUpdateSerializer(BaseSerializer): """ Update intake issue and transition associated issue state if accepted. """ - from plane.db.models import State # Update the intake issue with validated data instance = super().update(instance, validated_data) @@ -141,7 +139,7 @@ class IntakeIssueUpdateSerializer(BaseSerializer): # If status is accepted (1), update the associated issue state from TRIAGE to default if validated_data.get("status") == 1: issue = instance.issue - if issue.state and issue.state.group == State.TRIAGE: + if issue.state and issue.state.group == StateGroup.TRIAGE.value: # Get the default project state default_state = State.objects.filter( workspace=instance.workspace, project=instance.project, default=True diff --git a/apps/api/plane/api/serializers/state.py b/apps/api/plane/api/serializers/state.py index 10b3e84f9..c279529b8 100644 --- a/apps/api/plane/api/serializers/state.py +++ b/apps/api/plane/api/serializers/state.py @@ -1,6 +1,6 @@ # Module imports from .base import BaseSerializer -from plane.db.models import State +from plane.db.models import State, StateGroup from rest_framework import serializers @@ -17,7 +17,7 @@ class StateSerializer(BaseSerializer): if data.get("default", False): State.objects.filter(project_id=self.context.get("project_id")).update(default=False) - if data.get("group", None) == State.TRIAGE: + if data.get("group", None) == StateGroup.TRIAGE.value: raise serializers.ValidationError("Cannot create triage state") return data diff --git a/apps/api/plane/api/views/intake.py b/apps/api/plane/api/views/intake.py index 081c2f9f2..216b27afc 100644 --- a/apps/api/plane/api/views/intake.py +++ b/apps/api/plane/api/views/intake.py @@ -23,7 +23,7 @@ from plane.api.serializers import ( ) from plane.app.permissions import ProjectLitePermission from plane.bgtasks.issue_activities_task import issue_activity -from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State +from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State, StateGroup from plane.utils.host import base_host from .base import BaseAPIView from plane.db.models.intake import SourceType @@ -170,8 +170,8 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView): if not triage_state: triage_state = State.objects.create( - name="Intake Triage", - group=State.TRIAGE, + name="Triage", + group=StateGroup.TRIAGE.value, project_id=project_id, workspace_id=project.workspace_id, color="#4E5355", diff --git a/apps/api/plane/api/views/project.py b/apps/api/plane/api/views/project.py index 2f3db4237..fa735f557 100644 --- a/apps/api/plane/api/views/project.py +++ b/apps/api/plane/api/views/project.py @@ -24,6 +24,7 @@ from plane.db.models import ( DeployBoard, ProjectMember, State, + DEFAULT_STATES, Workspace, UserFavorite, ) @@ -232,47 +233,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView): user_id=serializer.instance.project_lead, ) - # Default states - states = [ - { - "name": "Backlog", - "color": "#60646C", - "sequence": 15000, - "group": "backlog", - "default": True, - }, - { - "name": "Todo", - "color": "#60646C", - "sequence": 25000, - "group": "unstarted", - }, - { - "name": "In Progress", - "color": "#F59E0B", - "sequence": 35000, - "group": "started", - }, - { - "name": "Done", - "color": "#46A758", - "sequence": 45000, - "group": "completed", - }, - { - "name": "Cancelled", - "color": "#9AA4BC", - "sequence": 55000, - "group": "cancelled", - }, - { - "name": "Intake Triage", - "color": "#4E5355", - "sequence": 65000, - "group": State.TRIAGE, - }, - ] - State.objects.bulk_create( [ State( @@ -285,7 +245,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView): default=state.get("default", False), created_by=request.user, ) - for state in states + for state in DEFAULT_STATES ] ) diff --git a/apps/api/plane/app/serializers/intake.py b/apps/api/plane/app/serializers/intake.py index 9ed3494a7..bc75a0ce5 100644 --- a/apps/api/plane/app/serializers/intake.py +++ b/apps/api/plane/app/serializers/intake.py @@ -7,7 +7,7 @@ from .issue import IssueIntakeSerializer, LabelLiteSerializer, IssueDetailSerial from .project import ProjectLiteSerializer from .state import StateLiteSerializer from .user import UserLiteSerializer -from plane.db.models import Intake, IntakeIssue, Issue +from plane.db.models import Intake, IntakeIssue, Issue, StateGroup, State class IntakeSerializer(BaseSerializer): @@ -41,7 +41,6 @@ class IntakeIssueSerializer(BaseSerializer): Validate that if status is being changed to accepted (1), the project has a default state to transition to. """ - from plane.db.models import State # Check if status is being updated to accepted if attrs.get("status") == 1: @@ -49,7 +48,7 @@ class IntakeIssueSerializer(BaseSerializer): issue = intake_issue.issue # Check if issue is in TRIAGE state - if issue.state and issue.state.group == State.TRIAGE: + if issue.state and issue.state.group == StateGroup.TRIAGE.value: # Verify default state exists before allowing the update default_state = State.objects.filter( workspace=intake_issue.workspace, project=intake_issue.project, default=True @@ -63,20 +62,16 @@ class IntakeIssueSerializer(BaseSerializer): return attrs def update(self, instance, validated_data): - from plane.db.models import State - # Update the intake issue instance = super().update(instance, validated_data) # If status is accepted (1), transition the issue state from TRIAGE to default if validated_data.get("status") == 1: issue = instance.issue - if issue.state and issue.state.group == State.TRIAGE: + if issue.state and issue.state.group == StateGroup.TRIAGE.value: # Get the default project state default_state = State.objects.filter( - workspace=instance.workspace, - project=instance.project, - default=True + workspace=instance.workspace, project=instance.project, default=True ).first() if default_state: issue.state = default_state diff --git a/apps/api/plane/app/serializers/state.py b/apps/api/plane/app/serializers/state.py index f7ad053fd..cb56cfbe9 100644 --- a/apps/api/plane/app/serializers/state.py +++ b/apps/api/plane/app/serializers/state.py @@ -2,7 +2,7 @@ from .base import BaseSerializer from rest_framework import serializers -from plane.db.models import State +from plane.db.models import State, StateGroup class StateSerializer(BaseSerializer): @@ -25,8 +25,7 @@ class StateSerializer(BaseSerializer): read_only_fields = ["workspace", "project"] def validate(self, attrs): - - if attrs.get("group") == State.TRIAGE: + if attrs.get("group") == StateGroup.TRIAGE.value: raise serializers.ValidationError("Cannot create triage state") return attrs diff --git a/apps/api/plane/app/views/intake/base.py b/apps/api/plane/app/views/intake/base.py index d2dd920c8..ffd725037 100644 --- a/apps/api/plane/app/views/intake/base.py +++ b/apps/api/plane/app/views/intake/base.py @@ -22,6 +22,7 @@ from plane.db.models import ( IntakeIssue, Issue, State, + StateGroup, IssueLink, FileAsset, Project, @@ -234,8 +235,8 @@ class IntakeIssueViewSet(BaseViewSet): triage_state = State.triage_objects.filter(project_id=project_id, workspace__slug=slug).first() if not triage_state: triage_state = State.objects.create( - name="Intake Triage", - group=State.TRIAGE, + name="Triage", + group=StateGroup.TRIAGE.value, project_id=project_id, workspace_id=project.workspace_id, color="#4E5355", diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index 85207059a..3aa356491 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -31,6 +31,7 @@ from plane.db.models import ( ProjectIdentifier, ProjectMember, State, + DEFAULT_STATES, Workspace, WorkspaceMember, ) @@ -264,47 +265,6 @@ class ProjectViewSet(BaseViewSet): user_id=serializer.data["project_lead"], ) - # Default states - states = [ - { - "name": "Backlog", - "color": "#60646C", - "sequence": 15000, - "group": "backlog", - "default": True, - }, - { - "name": "Todo", - "color": "#60646C", - "sequence": 25000, - "group": "unstarted", - }, - { - "name": "In Progress", - "color": "#F59E0B", - "sequence": 35000, - "group": "started", - }, - { - "name": "Done", - "color": "#46A758", - "sequence": 45000, - "group": "completed", - }, - { - "name": "Cancelled", - "color": "#9AA4BC", - "sequence": 55000, - "group": "cancelled", - }, - { - "name": "Intake Triage", - "color": "#4E5355", - "sequence": 65000, - "group": State.TRIAGE, - }, - ] - State.objects.bulk_create( [ State( @@ -317,7 +277,7 @@ class ProjectViewSet(BaseViewSet): default=state.get("default", False), created_by=request.user, ) - for state in states + for state in DEFAULT_STATES ] ) diff --git a/apps/api/plane/app/views/state/base.py b/apps/api/plane/app/views/state/base.py index d5e876f94..ce1597a68 100644 --- a/apps/api/plane/app/views/state/base.py +++ b/apps/api/plane/app/views/state/base.py @@ -135,7 +135,7 @@ class IntakeStateEndpoint(BaseAPIView): state = State.triage_objects.filter(workspace__slug=slug, project_id=project_id).first() if not state: return Response( - {"error": "Intake triage state not found"}, + {"error": "Triage state not found"}, status=status.HTTP_404_NOT_FOUND, ) diff --git a/apps/api/plane/bgtasks/dummy_data_task.py b/apps/api/plane/bgtasks/dummy_data_task.py index dff8239c4..390bc160b 100644 --- a/apps/api/plane/bgtasks/dummy_data_task.py +++ b/apps/api/plane/bgtasks/dummy_data_task.py @@ -17,6 +17,7 @@ from plane.db.models import ( Project, ProjectMember, State, + StateGroup, Label, Cycle, Module, @@ -264,7 +265,9 @@ def create_issues(workspace, project, user_id, issue_count): Faker.seed(0) states = ( - State.objects.filter(workspace=workspace, project=project).exclude(group=State.TRIAGE).values_list("id", flat=True) + State.objects.filter(workspace=workspace, project=project) + .exclude(group=StateGroup.TRIAGE.value) + .values_list("id", flat=True) ) creators = ProjectMember.objects.filter(workspace=workspace, project=project).values_list("member_id", flat=True) diff --git a/apps/api/plane/db/models/__init__.py b/apps/api/plane/db/models/__init__.py index fcf77b936..d24a14564 100644 --- a/apps/api/plane/db/models/__init__.py +++ b/apps/api/plane/db/models/__init__.py @@ -56,7 +56,7 @@ from .project import ( ) from .session import Session from .social_connection import SocialLoginConnection -from .state import State +from .state import State, StateGroup, DEFAULT_STATES from .user import Account, Profile, User from .view import IssueView from .webhook import Webhook, WebhookLog diff --git a/apps/api/plane/db/models/issue.py b/apps/api/plane/db/models/issue.py index 9e040303c..d3377f0ad 100644 --- a/apps/api/plane/db/models/issue.py +++ b/apps/api/plane/db/models/issue.py @@ -19,7 +19,7 @@ from .project import ProjectBaseModel from plane.utils.uuid import convert_uuid_to_integer from .description import Description from plane.db.mixins import ChangeTrackerMixin -from .state import State +from .state import StateGroup def get_default_properties(): @@ -98,7 +98,7 @@ class IssueManager(SoftDeletionManager): ) .filter(deleted_at__isnull=True) .filter(state__is_triage=False) - .exclude(state__group=State.TRIAGE) + .exclude(state__group=StateGroup.TRIAGE.value) .exclude(archived_at__isnull=False) .exclude(project__archived_at__isnull=False) .exclude(is_draft=True) diff --git a/apps/api/plane/db/models/state.py b/apps/api/plane/db/models/state.py index 104954490..aeb08b8b2 100644 --- a/apps/api/plane/db/models/state.py +++ b/apps/api/plane/db/models/state.py @@ -7,51 +7,80 @@ from django.db.models import Q from .project import ProjectBaseModel +class StateGroup(models.TextChoices): + BACKLOG = "backlog", "Backlog" + UNSTARTED = "unstarted", "Unstarted" + STARTED = "started", "Started" + COMPLETED = "completed", "Completed" + CANCELLED = "cancelled", "Cancelled" + TRIAGE = "triage", "Triage" + + +# Default states +DEFAULT_STATES = [ + { + "name": "Backlog", + "color": "#60646C", + "sequence": 15000, + "group": StateGroup.BACKLOG.value, + "default": True, + }, + { + "name": "Todo", + "color": "#60646C", + "sequence": 25000, + "group": StateGroup.UNSTARTED.value, + }, + { + "name": "In Progress", + "color": "#F59E0B", + "sequence": 35000, + "group": StateGroup.STARTED.value, + }, + { + "name": "Done", + "color": "#46A758", + "sequence": 45000, + "group": StateGroup.COMPLETED.value, + }, + { + "name": "Cancelled", + "color": "#9AA4BC", + "sequence": 55000, + "group": StateGroup.CANCELLED.value, + }, + { + "name": "Triage", + "color": "#4E5355", + "sequence": 65000, + "group": StateGroup.TRIAGE.value, + }, +] + + class StateManager(models.Manager): """Default manager - excludes triage states""" def get_queryset(self): - return super().get_queryset().exclude(group=State.TRIAGE) + return super().get_queryset().exclude(group=StateGroup.TRIAGE.value) class TriageStateManager(models.Manager): """Manager for triage states only""" def get_queryset(self): - return super().get_queryset().filter(group=State.TRIAGE) + return super().get_queryset().filter(group=StateGroup.TRIAGE.value) class State(ProjectBaseModel): - BACKLOG = "backlog" - UNSTARTED = "unstarted" - STARTED = "started" - COMPLETED = "completed" - CANCELLED = "cancelled" - TRIAGE = "triage" - - GROUP_CHOICES = ( - (BACKLOG, "Backlog"), - (UNSTARTED, "Unstarted"), - (STARTED, "Started"), - (COMPLETED, "Completed"), - (CANCELLED, "Cancelled"), - (TRIAGE, "Triage"), - ) name = models.CharField(max_length=255, verbose_name="State Name") description = models.TextField(verbose_name="State Description", blank=True) color = models.CharField(max_length=255, verbose_name="State Color") slug = models.SlugField(max_length=100, blank=True) sequence = models.FloatField(default=65535) group = models.CharField( - choices=( - ("backlog", "Backlog"), - ("unstarted", "Unstarted"), - ("started", "Started"), - ("completed", "Completed"), - ("cancelled", "Cancelled"), - ("triage", "Triage"), - ), - default="backlog", + choices=StateGroup.choices, + default=StateGroup.BACKLOG, max_length=20, ) is_triage = models.BooleanField(default=False) diff --git a/apps/api/plane/space/views/intake.py b/apps/api/plane/space/views/intake.py index e82d0523e..7ea2dee91 100644 --- a/apps/api/plane/space/views/intake.py +++ b/apps/api/plane/space/views/intake.py @@ -12,7 +12,7 @@ from rest_framework.response import Response # Module imports from .base import BaseViewSet -from plane.db.models import IntakeIssue, Issue, IssueLink, FileAsset, DeployBoard, State +from plane.db.models import IntakeIssue, Issue, IssueLink, FileAsset, DeployBoard, State, StateGroup from plane.app.serializers import ( IssueSerializer, IntakeIssueSerializer, @@ -128,8 +128,8 @@ class IntakeIssuePublicViewSet(BaseViewSet): if not triage_state: triage_state = State.objects.create( - name="Intake Triage", - group=State.TRIAGE, + name="Triage", + group=StateGroup.TRIAGE.value, project_id=project_deploy_board.project_id, workspace_id=project_deploy_board.workspace_id, color="#4E5355",