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

View file

@ -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

View file

@ -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",

View file

@ -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
]
)

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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
]
)

View file

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

View file

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

View file

@ -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

View file

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

View file

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

View file

@ -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",