From 4225bc59de784f8991a8c30de77ca8b2b3fd8cc7 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Tue, 12 May 2026 13:39:54 +0530 Subject: [PATCH] [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> --- apps/api/plane/db/models/issue.py | 62 +++++++++++++++++-------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/apps/api/plane/db/models/issue.py b/apps/api/plane/db/models/issue.py index f4175c4785..fe23ee681d 100644 --- a/apps/api/plane/db/models/issue.py +++ b/apps/api/plane/db/models/issue.py @@ -18,12 +18,11 @@ from django import apps # Module imports from plane.utils.html_processor import strip_tags 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 .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 StateGroup @@ -102,7 +101,9 @@ class IssueManager(SoftDeletionManager): ) -class Issue(ProjectBaseModel): +class Issue(ChangeTrackerMixin, ProjectBaseModel): + TRACKED_FIELDS = ["state_id"] + PRIORITY_CHOICES = ( ("urgent", "Urgent"), ("high", "High"), @@ -177,30 +178,8 @@ class Issue(ProjectBaseModel): ordering = ("-created_at",) def save(self, *args, **kwargs): - if self.state is None: - try: - 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 + self._ensure_default_state() + kwargs = self._sync_completed_at(kwargs) if self._state.adding: with transaction.atomic(): @@ -246,6 +225,35 @@ class Issue(ProjectBaseModel): """Return name of the issue""" 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): block = models.ForeignKey(Issue, related_name="blocker_issues", on_delete=models.CASCADE)