fix: state group choices (#8198)

This commit is contained in:
sriram veeraghanta 2025-11-28 18:06:00 +05:30 committed by GitHub
parent 2980836015
commit c7bf912cf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 87 additions and 142 deletions

View file

@ -1,7 +1,7 @@
# Module imports # Module imports
from .base import BaseSerializer from .base import BaseSerializer
from .issue import IssueExpandSerializer 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 from rest_framework import serializers
@ -108,7 +108,6 @@ class IntakeIssueUpdateSerializer(BaseSerializer):
Validate that if status is being changed to accepted (1), Validate that if status is being changed to accepted (1),
the project has a default state to transition to. the project has a default state to transition to.
""" """
from plane.db.models import State
# Check if status is being updated to accepted # Check if status is being updated to accepted
if attrs.get("status") == 1: if attrs.get("status") == 1:
@ -116,7 +115,7 @@ class IntakeIssueUpdateSerializer(BaseSerializer):
issue = intake_issue.issue issue = intake_issue.issue
# Check if issue is in TRIAGE state # 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 # Verify default state exists before allowing the update
default_state = State.objects.filter( default_state = State.objects.filter(
workspace=intake_issue.workspace, project=intake_issue.project, default=True 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. Update intake issue and transition associated issue state if accepted.
""" """
from plane.db.models import State
# Update the intake issue with validated data # Update the intake issue with validated data
instance = super().update(instance, 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 status is accepted (1), update the associated issue state from TRIAGE to default
if validated_data.get("status") == 1: if validated_data.get("status") == 1:
issue = instance.issue 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 # Get the default project state
default_state = State.objects.filter( default_state = State.objects.filter(
workspace=instance.workspace, project=instance.project, default=True workspace=instance.workspace, project=instance.project, default=True

View file

@ -1,6 +1,6 @@
# Module imports # Module imports
from .base import BaseSerializer from .base import BaseSerializer
from plane.db.models import State from plane.db.models import State, StateGroup
from rest_framework import serializers from rest_framework import serializers
@ -17,7 +17,7 @@ class StateSerializer(BaseSerializer):
if data.get("default", False): if data.get("default", False):
State.objects.filter(project_id=self.context.get("project_id")).update(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") raise serializers.ValidationError("Cannot create triage state")
return data return data

View file

@ -23,7 +23,7 @@ from plane.api.serializers import (
) )
from plane.app.permissions import ProjectLitePermission from plane.app.permissions import ProjectLitePermission
from plane.bgtasks.issue_activities_task import issue_activity 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 plane.utils.host import base_host
from .base import BaseAPIView from .base import BaseAPIView
from plane.db.models.intake import SourceType from plane.db.models.intake import SourceType
@ -170,8 +170,8 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
if not triage_state: if not triage_state:
triage_state = State.objects.create( triage_state = State.objects.create(
name="Intake Triage", name="Triage",
group=State.TRIAGE, group=StateGroup.TRIAGE.value,
project_id=project_id, project_id=project_id,
workspace_id=project.workspace_id, workspace_id=project.workspace_id,
color="#4E5355", color="#4E5355",

View file

@ -24,6 +24,7 @@ from plane.db.models import (
DeployBoard, DeployBoard,
ProjectMember, ProjectMember,
State, State,
DEFAULT_STATES,
Workspace, Workspace,
UserFavorite, UserFavorite,
) )
@ -232,47 +233,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
user_id=serializer.instance.project_lead, 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.objects.bulk_create(
[ [
State( State(
@ -285,7 +245,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
default=state.get("default", False), default=state.get("default", False),
created_by=request.user, created_by=request.user,
) )
for state in states for state in DEFAULT_STATES
] ]
) )

View file

@ -7,7 +7,7 @@ from .issue import IssueIntakeSerializer, LabelLiteSerializer, IssueDetailSerial
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from .state import StateLiteSerializer from .state import StateLiteSerializer
from .user import UserLiteSerializer 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): class IntakeSerializer(BaseSerializer):
@ -41,7 +41,6 @@ class IntakeIssueSerializer(BaseSerializer):
Validate that if status is being changed to accepted (1), Validate that if status is being changed to accepted (1),
the project has a default state to transition to. the project has a default state to transition to.
""" """
from plane.db.models import State
# Check if status is being updated to accepted # Check if status is being updated to accepted
if attrs.get("status") == 1: if attrs.get("status") == 1:
@ -49,7 +48,7 @@ class IntakeIssueSerializer(BaseSerializer):
issue = intake_issue.issue issue = intake_issue.issue
# Check if issue is in TRIAGE state # 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 # Verify default state exists before allowing the update
default_state = State.objects.filter( default_state = State.objects.filter(
workspace=intake_issue.workspace, project=intake_issue.project, default=True workspace=intake_issue.workspace, project=intake_issue.project, default=True
@ -63,20 +62,16 @@ class IntakeIssueSerializer(BaseSerializer):
return attrs return attrs
def update(self, instance, validated_data): def update(self, instance, validated_data):
from plane.db.models import State
# Update the intake issue # Update the intake issue
instance = super().update(instance, validated_data) instance = super().update(instance, validated_data)
# If status is accepted (1), transition the issue state from TRIAGE to default # If status is accepted (1), transition the issue state from TRIAGE to default
if validated_data.get("status") == 1: if validated_data.get("status") == 1:
issue = instance.issue 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 # Get the default project state
default_state = State.objects.filter( default_state = State.objects.filter(
workspace=instance.workspace, workspace=instance.workspace, project=instance.project, default=True
project=instance.project,
default=True
).first() ).first()
if default_state: if default_state:
issue.state = default_state issue.state = default_state

View file

@ -2,7 +2,7 @@
from .base import BaseSerializer from .base import BaseSerializer
from rest_framework import serializers from rest_framework import serializers
from plane.db.models import State from plane.db.models import State, StateGroup
class StateSerializer(BaseSerializer): class StateSerializer(BaseSerializer):
@ -25,8 +25,7 @@ class StateSerializer(BaseSerializer):
read_only_fields = ["workspace", "project"] read_only_fields = ["workspace", "project"]
def validate(self, attrs): def validate(self, attrs):
if attrs.get("group") == StateGroup.TRIAGE.value:
if attrs.get("group") == State.TRIAGE:
raise serializers.ValidationError("Cannot create triage state") raise serializers.ValidationError("Cannot create triage state")
return attrs return attrs

View file

@ -22,6 +22,7 @@ from plane.db.models import (
IntakeIssue, IntakeIssue,
Issue, Issue,
State, State,
StateGroup,
IssueLink, IssueLink,
FileAsset, FileAsset,
Project, Project,
@ -234,8 +235,8 @@ class IntakeIssueViewSet(BaseViewSet):
triage_state = State.triage_objects.filter(project_id=project_id, workspace__slug=slug).first() triage_state = State.triage_objects.filter(project_id=project_id, workspace__slug=slug).first()
if not triage_state: if not triage_state:
triage_state = State.objects.create( triage_state = State.objects.create(
name="Intake Triage", name="Triage",
group=State.TRIAGE, group=StateGroup.TRIAGE.value,
project_id=project_id, project_id=project_id,
workspace_id=project.workspace_id, workspace_id=project.workspace_id,
color="#4E5355", color="#4E5355",

View file

@ -31,6 +31,7 @@ from plane.db.models import (
ProjectIdentifier, ProjectIdentifier,
ProjectMember, ProjectMember,
State, State,
DEFAULT_STATES,
Workspace, Workspace,
WorkspaceMember, WorkspaceMember,
) )
@ -264,47 +265,6 @@ class ProjectViewSet(BaseViewSet):
user_id=serializer.data["project_lead"], 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.objects.bulk_create(
[ [
State( State(
@ -317,7 +277,7 @@ class ProjectViewSet(BaseViewSet):
default=state.get("default", False), default=state.get("default", False),
created_by=request.user, created_by=request.user,
) )
for state in states for state in DEFAULT_STATES
] ]
) )

View file

@ -135,7 +135,7 @@ class IntakeStateEndpoint(BaseAPIView):
state = State.triage_objects.filter(workspace__slug=slug, project_id=project_id).first() state = State.triage_objects.filter(workspace__slug=slug, project_id=project_id).first()
if not state: if not state:
return Response( return Response(
{"error": "Intake triage state not found"}, {"error": "Triage state not found"},
status=status.HTTP_404_NOT_FOUND, status=status.HTTP_404_NOT_FOUND,
) )

View file

@ -17,6 +17,7 @@ from plane.db.models import (
Project, Project,
ProjectMember, ProjectMember,
State, State,
StateGroup,
Label, Label,
Cycle, Cycle,
Module, Module,
@ -264,7 +265,9 @@ def create_issues(workspace, project, user_id, issue_count):
Faker.seed(0) Faker.seed(0)
states = ( 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) creators = ProjectMember.objects.filter(workspace=workspace, project=project).values_list("member_id", flat=True)

View file

@ -56,7 +56,7 @@ from .project import (
) )
from .session import Session from .session import Session
from .social_connection import SocialLoginConnection from .social_connection import SocialLoginConnection
from .state import State from .state import State, StateGroup, DEFAULT_STATES
from .user import Account, Profile, User from .user import Account, Profile, User
from .view import IssueView from .view import IssueView
from .webhook import Webhook, WebhookLog from .webhook import Webhook, WebhookLog

View file

@ -19,7 +19,7 @@ from .project import ProjectBaseModel
from plane.utils.uuid import convert_uuid_to_integer from plane.utils.uuid import convert_uuid_to_integer
from .description import Description from .description import Description
from plane.db.mixins import ChangeTrackerMixin from plane.db.mixins import ChangeTrackerMixin
from .state import State from .state import StateGroup
def get_default_properties(): def get_default_properties():
@ -98,7 +98,7 @@ class IssueManager(SoftDeletionManager):
) )
.filter(deleted_at__isnull=True) .filter(deleted_at__isnull=True)
.filter(state__is_triage=False) .filter(state__is_triage=False)
.exclude(state__group=State.TRIAGE) .exclude(state__group=StateGroup.TRIAGE.value)
.exclude(archived_at__isnull=False) .exclude(archived_at__isnull=False)
.exclude(project__archived_at__isnull=False) .exclude(project__archived_at__isnull=False)
.exclude(is_draft=True) .exclude(is_draft=True)

View file

@ -7,51 +7,80 @@ from django.db.models import Q
from .project import ProjectBaseModel 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): class StateManager(models.Manager):
"""Default manager - excludes triage states""" """Default manager - excludes triage states"""
def get_queryset(self): 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): class TriageStateManager(models.Manager):
"""Manager for triage states only""" """Manager for triage states only"""
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(group=State.TRIAGE) return super().get_queryset().filter(group=StateGroup.TRIAGE.value)
class State(ProjectBaseModel): 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") name = models.CharField(max_length=255, verbose_name="State Name")
description = models.TextField(verbose_name="State Description", blank=True) description = models.TextField(verbose_name="State Description", blank=True)
color = models.CharField(max_length=255, verbose_name="State Color") color = models.CharField(max_length=255, verbose_name="State Color")
slug = models.SlugField(max_length=100, blank=True) slug = models.SlugField(max_length=100, blank=True)
sequence = models.FloatField(default=65535) sequence = models.FloatField(default=65535)
group = models.CharField( group = models.CharField(
choices=( choices=StateGroup.choices,
("backlog", "Backlog"), default=StateGroup.BACKLOG,
("unstarted", "Unstarted"),
("started", "Started"),
("completed", "Completed"),
("cancelled", "Cancelled"),
("triage", "Triage"),
),
default="backlog",
max_length=20, max_length=20,
) )
is_triage = models.BooleanField(default=False) is_triage = models.BooleanField(default=False)

View file

@ -12,7 +12,7 @@ from rest_framework.response import Response
# Module imports # Module imports
from .base import BaseViewSet 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 ( from plane.app.serializers import (
IssueSerializer, IssueSerializer,
IntakeIssueSerializer, IntakeIssueSerializer,
@ -128,8 +128,8 @@ class IntakeIssuePublicViewSet(BaseViewSet):
if not triage_state: if not triage_state:
triage_state = State.objects.create( triage_state = State.objects.create(
name="Intake Triage", name="Triage",
group=State.TRIAGE, group=StateGroup.TRIAGE.value,
project_id=project_deploy_board.project_id, project_id=project_deploy_board.project_id,
workspace_id=project_deploy_board.workspace_id, workspace_id=project_deploy_board.workspace_id,
color="#4E5355", color="#4E5355",