A sjmple DateQueryField that wraps dateparser

This commit is contained in:
shamoon committed 2026-10-22 00:04:03 -07:00
1 parent 232f9c8dcc
commit 23d00112d3
1 file changed
+49
+49
View File
@@ -11,6 +11,7 @@ from decimal import InvalidOperation
from typing import TYPE_CHECKING
from typing import Any
import dateparser
from django.contrib.contenttypes.models import ContentType
from django.db.models import Case
from django.db.models import CharField
@@ -25,6 +26,7 @@ from django.db.models import Value
from django.db.models import When
from django.db.models.functions import Cast
from django.db.models.functions import NullIf
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_filters import DateFilter
from django_filters.rest_framework import BooleanFilter
@@ -57,6 +59,7 @@ from documents.versioning import annotate_effective_content
if TYPE_CHECKING:
from collections.abc import Callable
from datetime import datetime
CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"]
ID_KWARGS = ["in", "exact"]
@@ -322,6 +325,48 @@ class MonetaryAmountField(serializers.Field):
return str(value)
class DateQueryField(serializers.DateField):
"""
Accepts either a fixed date or a relative date expression using `dateparser` and
resolves it against the provided reference datetime.
"""
default_error_messages = {
"expression": _(
'Expected a date or a relative date such as "today" or "in 30 days".',
),
}
def __init__(self, *, reference_datetime: datetime, **kwargs):
super().__init__(**kwargs)
self.reference_datetime = reference_datetime
def to_internal_value(self, value):
try:
# Let the parent class handle fixed-date formats and string validation first
return super().to_internal_value(value)
except serializers.ValidationError:
if not isinstance(value, str):
raise
try:
parsed = dateparser.parse(
value,
languages=["en"],
settings={
"PARSERS": ["relative-time"],
"RELATIVE_BASE": self.reference_datetime,
"TIMEZONE": str(self.reference_datetime.tzinfo),
"RETURN_AS_TIMEZONE_AWARE": True,
},
)
except (ValueError, OverflowError):
self.fail("expression")
if parsed is None:
self.fail("expression")
return parsed.astimezone(self.reference_datetime.tzinfo).date()
class SelectField(serializers.CharField):
def __init__(self, custom_field: CustomField) -> None:
self._options = custom_field.extra_data["select_options"]
@@ -463,6 +508,8 @@ class CustomFieldQueryParser:
raise serializers.ValidationError(
{self._validation_prefix: [_("Value must be valid JSON.")]},
)
# Save the current local time as the reference for the query
self._reference_datetime = timezone.localtime()
return (
self._parse_expr(expr, validation_prefix=self._validation_prefix),
self._annotations,
@@ -649,6 +696,8 @@ class CustomFieldQueryParser:
):
# DateField admits queries in the form of `year__exact`, etc. These take integers.
field = serializers.IntegerField()
elif custom_field.data_type == CustomField.FieldDataType.DATE:
field = DateQueryField(reference_datetime=self._reference_datetime)
elif custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
# We can be more specific here and make sure the value is a list.
field = serializers.ListField(child=serializers.IntegerField())