[GIT-175] fix: completed_at updation logic for work items (#9044)

* chore: update completed_at logic updation in Issue save method

* fix: update error handling

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: use StateGroup

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Sangeetha
2026-05-12 13:39:54 +05:30
committed by GitHub
parent 4c1bdd1d62
commit 4225bc59de
+35 -27
View File
@@ -18,12 +18,11 @@ from django import apps
# Module imports # Module imports
from plane.utils.html_processor import strip_tags from plane.utils.html_processor import strip_tags
from plane.utils.path_validator import sanitize_filename from plane.utils.path_validator import sanitize_filename
from plane.db.mixins import SoftDeletionManager from plane.db.mixins import SoftDeletionManager, ChangeTrackerMixin
from plane.utils.exception_logger import log_exception from plane.utils.exception_logger import log_exception
from .project import ProjectBaseModel 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 .state import StateGroup from .state import StateGroup
@@ -102,7 +101,9 @@ class IssueManager(SoftDeletionManager):
) )
class Issue(ProjectBaseModel): class Issue(ChangeTrackerMixin, ProjectBaseModel):
TRACKED_FIELDS = ["state_id"]
PRIORITY_CHOICES = ( PRIORITY_CHOICES = (
("urgent", "Urgent"), ("urgent", "Urgent"),
("high", "High"), ("high", "High"),
@@ -177,30 +178,8 @@ class Issue(ProjectBaseModel):
ordering = ("-created_at",) ordering = ("-created_at",)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.state is None: self._ensure_default_state()
try: kwargs = self._sync_completed_at(kwargs)
from plane.db.models import State
default_state = State.objects.filter(
~models.Q(is_triage=True), project=self.project, default=True
).first()
if default_state is None:
random_state = State.objects.filter(~models.Q(is_triage=True), project=self.project).first()
self.state = random_state
else:
self.state = default_state
except ImportError:
pass
else:
try:
from plane.db.models import State
if self.state.group == "completed":
self.completed_at = timezone.now()
else:
self.completed_at = None
except ImportError:
pass
if self._state.adding: if self._state.adding:
with transaction.atomic(): with transaction.atomic():
@@ -246,6 +225,35 @@ class Issue(ProjectBaseModel):
"""Return name of the issue""" """Return name of the issue"""
return f"{self.name} <{self.project.name}>" return f"{self.name} <{self.project.name}>"
def _ensure_default_state(self):
"""Assign a default state when none is set."""
if self.state is not None:
return
try:
from plane.db.models import State
default_state = State.objects.filter(~models.Q(is_triage=True), project=self.project, default=True).first()
self.state = default_state or State.objects.filter(~models.Q(is_triage=True), project=self.project).first()
except ImportError as e:
log_exception(e)
def _sync_completed_at(self, kwargs):
"""Update completed_at when state changes. Returns kwargs."""
if not self.state:
return kwargs
if not self._state.adding and not self.has_changed("state_id"):
return kwargs
if self.state.group == StateGroup.COMPLETED.value:
self.completed_at = timezone.now()
else:
self.completed_at = None
update_fields = kwargs.get("update_fields")
if update_fields is not None:
kwargs["update_fields"] = list(set(update_fields) | {"completed_at"})
return kwargs
class IssueBlocker(ProjectBaseModel): class IssueBlocker(ProjectBaseModel):
block = models.ForeignKey(Issue, related_name="blocker_issues", on_delete=models.CASCADE) block = models.ForeignKey(Issue, related_name="blocker_issues", on_delete=models.CASCADE)