diff --git a/apps/api/plane/tests/unit/utils/test_legacy_filter_converter.py b/apps/api/plane/tests/unit/utils/test_legacy_filter_converter.py new file mode 100644 index 00000000000..45b452f3059 --- /dev/null +++ b/apps/api/plane/tests/unit/utils/test_legacy_filter_converter.py @@ -0,0 +1,35 @@ +# Copyright (c) 2023-present Plane Software, Inc. and contributors +# SPDX-License-Identifier: AGPL-3.0-only +# See the LICENSE file for details. + +import pytest + +from plane.utils.filters.converters import LegacyToRichFiltersConverter + + +@pytest.mark.unit +class TestLegacyToRichFiltersConverterDateOrdering: + """ + _convert_date_value must order date range bounds chronologically, + not lexicographically. See: https://github.com/makeplane/plane/issues/9567 + """ + + def setup_method(self): + self.converter = LegacyToRichFiltersConverter() + + def test_non_iso_date_range_is_ordered_chronologically(self): + # '9/1/2023' (Sep 1) < '10/1/2023' (Oct 1) chronologically, but + # '10/1/2023' < '9/1/2023' lexicographically ('1' < '9'). + # The old code used string min/max and produced a reversed range. + result = self.converter._convert_date_value( + "target_date", + ["9/1/2023;after", "10/1/2023;before"], + ) + assert result["target_date__range"] == "2023-09-01,2023-10-01" + + def test_iso_date_range_still_works(self): + result = self.converter._convert_date_value( + "target_date", + ["2023-09-01;after", "2023-12-31;before"], + ) + assert result["target_date__range"] == "2023-09-01,2023-12-31" \ No newline at end of file diff --git a/apps/api/plane/utils/filters/converters.py b/apps/api/plane/utils/filters/converters.py index 4d37c2b0b17..567acb0b628 100644 --- a/apps/api/plane/utils/filters/converters.py +++ b/apps/api/plane/utils/filters/converters.py @@ -306,8 +306,13 @@ def _convert_date_value(self, field_name: str, values: List[str], strict: bool = result = {} if len(after_dates) == 1 and len(before_dates) == 1 and len(exact_dates) == 0: # Simple range: one after and one before - start_date = min(after_dates[0], before_dates[0]) - end_date = max(after_dates[0], before_dates[0]) + # Parse to datetime objects so comparison is chronological, not lexicographic. + # String min/max only works for zero-padded ISO YYYY-MM-DD; other formats + # accepted by _validate_date (e.g. M/D/YYYY) would produce a reversed range. + after_date = dateutil_parse(after_dates[0]).date() + before_date = dateutil_parse(before_dates[0]).date() + start_date = min(after_date, before_date).isoformat() + end_date = max(after_date, before_date).isoformat() self._add_rich_filter(result, field_name, "range", [start_date, end_date]) elif len(exact_dates) == 1 and len(after_dates) == 0 and len(before_dates) == 0: # Single exact date