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,