From 21a9d2b8092306452182736eebda237da0d8a56f Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 25 Apr 2026 10:34:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=A3=D0=9D=D0=9A=D0=A6=D0=98=D0=98=20-?= =?UTF-8?q?=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E=D0=95=D0=9A=D0=A2=D0=9D?= =?UTF-8?q?=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C=D0=A3=D0=9D=D0=98=D0=9A?= =?UTF-8?q?=D0=90=D0=A6=D0=98=D0=AF:=20=D0=B4=D0=B0=D1=82=D0=B0=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D1=82=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D1=87=20=D1=81=20=D0=B4=D0=B5=D0=B4=D0=BB?= =?UTF-8?q?=D0=B0=D0=B9=D0=BD=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/api/plane/api/serializers/issue.py | 4 ++ .../apps/api/plane/app/serializers/issue.py | 4 ++ .../apps/api/plane/space/serializer/issue.py | 4 ++ .../plane/tests/unit/utils/test_date_utils.py | 38 +++++++++++++++++++ plane-src/apps/api/plane/utils/date_utils.py | 10 +++++ 5 files changed, 60 insertions(+) create mode 100644 plane-src/apps/api/plane/tests/unit/utils/test_date_utils.py diff --git a/plane-src/apps/api/plane/api/serializers/issue.py b/plane-src/apps/api/plane/api/serializers/issue.py index 6468ddb..c0451bf 100644 --- a/plane-src/apps/api/plane/api/serializers/issue.py +++ b/plane-src/apps/api/plane/api/serializers/issue.py @@ -31,6 +31,7 @@ from plane.utils.content_validator import ( validate_html_content, validate_binary_data, ) +from plane.utils.date_utils import set_default_issue_start_date from .base import BaseSerializer from .cycle import CycleLiteSerializer, CycleSerializer @@ -73,6 +74,9 @@ class IssueSerializer(BaseSerializer): exclude = ["description_json", "description_stripped"] def validate(self, data): + if self.instance is None: + data = set_default_issue_start_date(data) + if ( data.get("start_date", None) is not None and data.get("target_date", None) is not None diff --git a/plane-src/apps/api/plane/app/serializers/issue.py b/plane-src/apps/api/plane/app/serializers/issue.py index ea187f1..261ef76 100644 --- a/plane-src/apps/api/plane/app/serializers/issue.py +++ b/plane-src/apps/api/plane/app/serializers/issue.py @@ -47,6 +47,7 @@ from plane.utils.content_validator import ( validate_html_content, validate_binary_data, ) +from plane.utils.date_utils import set_default_issue_start_date class IssueFlatSerializer(BaseSerializer): @@ -124,6 +125,9 @@ class IssueCreateSerializer(BaseSerializer): allow_triage = self.context.get("allow_triage_state", False) state_manager = State.triage_objects if allow_triage else State.objects + if self.instance is None: + attrs = set_default_issue_start_date(attrs) + if ( attrs.get("start_date", None) is not None and attrs.get("target_date", None) is not None diff --git a/plane-src/apps/api/plane/space/serializer/issue.py b/plane-src/apps/api/plane/space/serializer/issue.py index 51dd1f4..7b3f891 100644 --- a/plane-src/apps/api/plane/space/serializer/issue.py +++ b/plane-src/apps/api/plane/space/serializer/issue.py @@ -36,6 +36,7 @@ from plane.utils.content_validator import ( validate_html_content, validate_binary_data, ) +from plane.utils.date_utils import set_default_issue_start_date class IssueStateFlatSerializer(BaseSerializer): @@ -277,6 +278,9 @@ class IssueCreateSerializer(BaseSerializer): return data def validate(self, data): + if self.instance is None: + data = set_default_issue_start_date(data) + if ( data.get("start_date", None) is not None and data.get("target_date", None) is not None diff --git a/plane-src/apps/api/plane/tests/unit/utils/test_date_utils.py b/plane-src/apps/api/plane/tests/unit/utils/test_date_utils.py new file mode 100644 index 0000000..0e76e01 --- /dev/null +++ b/plane-src/apps/api/plane/tests/unit/utils/test_date_utils.py @@ -0,0 +1,38 @@ +# Copyright (c) 2023-present Plane Software, Inc. and contributors +# SPDX-License-Identifier: AGPL-3.0-only +# See the LICENSE file for details. + +from datetime import date + +import pytest + +from plane.utils.date_utils import set_default_issue_start_date + + +@pytest.mark.unit +class TestDefaultIssueStartDate: + def test_sets_today_for_future_target_date(self, monkeypatch): + monkeypatch.setattr("plane.utils.date_utils.timezone.localdate", lambda: date(2026, 4, 25)) + + attrs = set_default_issue_start_date({"target_date": date(2026, 5, 1)}) + + assert attrs["start_date"] == date(2026, 4, 25) + + def test_preserves_explicit_start_date(self, monkeypatch): + monkeypatch.setattr("plane.utils.date_utils.timezone.localdate", lambda: date(2026, 4, 25)) + + attrs = set_default_issue_start_date( + { + "start_date": date(2026, 4, 20), + "target_date": date(2026, 5, 1), + } + ) + + assert attrs["start_date"] == date(2026, 4, 20) + + def test_leaves_past_target_without_start_date(self, monkeypatch): + monkeypatch.setattr("plane.utils.date_utils.timezone.localdate", lambda: date(2026, 4, 25)) + + attrs = set_default_issue_start_date({"target_date": date(2026, 4, 20)}) + + assert "start_date" not in attrs diff --git a/plane-src/apps/api/plane/utils/date_utils.py b/plane-src/apps/api/plane/utils/date_utils.py index d25d5b1..1bd2c17 100644 --- a/plane-src/apps/api/plane/utils/date_utils.py +++ b/plane-src/apps/api/plane/utils/date_utils.py @@ -122,6 +122,16 @@ def get_chart_period_range( return period_ranges.get(date_filter, None) +def set_default_issue_start_date(attrs: Dict[str, Any]) -> Dict[str, Any]: + target_date = attrs.get("target_date") + if attrs.get("start_date") is None and target_date is not None: + today = timezone.localdate() + if target_date >= today: + attrs["start_date"] = today + + return attrs + + def get_analytics_filters( slug: str, user: User,