Skip to content

Commit

Permalink
feat(generic): introduce custom APIS DateIntervalFields
Browse files Browse the repository at this point in the history
This commit introduces a `FuzzyDateParserField` and a
`FuzzyDateRegexField`.
Both fields are based on the `GenericDateIntervalField`, which adds a
`_from`, a `_sort` and a `_to` field based on the field created. Those
three additional fields contain data that is calculated using either a
parser (in the case of the `FuzzyDateParserField`) or a regex (in the
case of the `FuzzyDateRegexField`).
The default parser for the `FuzzyDateParserField` is the one from the
`apis_core.utils.DateParser` module.
  • Loading branch information
b1rger committed Nov 20, 2024
1 parent e14c806 commit 96e83cf
Showing 1 changed file with 107 additions and 0 deletions.
107 changes: 107 additions & 0 deletions apis_core/generic/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import re
from datetime import date
from typing import Callable, Tuple

from django.db.models import CharField, DateField
from django.forms import ValidationError

from apis_core.history.models import APISHistoryTableBase
from apis_core.utils import DateParser


class GenericDateIntervalField(CharField):
""" Add additional fields to the model containing this field
The check using `hasattr` and `setattr` to check the existence
of the field is taken from https://github.com/django-money/django-money/blob/main/djmoney/models/fields.py#L255
Without that the migrations did fail again and again ...
"""
def add_generated_date_field(self, cls, name):
date_field = DateField(editable=False, blank=True, null=True)
cls.add_to_class(name, date_field)
setattr(self, f"_{name}", date_field)

def contribute_to_class(self, cls, name):
for field_name in [f"{name}_date_sort", f"{name}_date_from", f"{name}_date_to"]:
if not issubclass(cls, APISHistoryTableBase) and not hasattr(
self, f"_{field_name}"
):
self.add_generated_date_field(cls, field_name)
super().contribute_to_class(cls, name)
setattr(cls, name, self)


class FuzzyDateParserField(GenericDateIntervalField):
def __init__(
self,
parser: Callable[[str], Tuple[date, date, date]] = DateParser.parse_date,
*args,
**kwargs,
):
self.parser = parser
super().__init__(*args, **kwargs)

def _populate_fields(self, model_instance):
name = self.attname
value = getattr(model_instance, name)
if not getattr(model_instance, "skip_date_parsing", False):
try:
date_sort, date_from, date_to = self.parser(value)
setattr(model_instance, f"{name}_date_sort", date_sort)
setattr(model_instance, f"{name}_date_from", date_from)
setattr(model_instance, f"{name}_date_to", date_to)
except Exception as e:
raise ValidationError(f"Error parsing date string: {e}")

def pre_save(self, model_instance, add):
self._populate_fields(model_instance)
return super().pre_save(model_instance, add)


FROM_PATTERN = r"<from: (?P<day>\d{1,2})\.(?P<month>\d{1,2}).(?P<year>\d{1,4})>"
TO_PATTERN = r"<to: (?P<day>\d{1,2})\.(?P<month>\d{1,2}).(?P<year>\d{1,4})>"
SORT_PATTERN = r"<sort: (?P<day>\d{1,2})\.(?P<month>\d{1,2}).(?P<year>\d{1,4})>"


class FuzzyDateRegexField(FuzzyDateParserField):
"""This Field allows you to define different regexes for
extracting the `from`, the `to` and the `sort` values from
the string. So, the string "2024 <sort: 2024-06-31>" could
be parsed so that the `_date_sort` field is set to
datetime.date(2024, 6, 1).
The pattern hat to contain a <year> group, a <month> group and a <day> group.
"""

def __init__(
self,
from_pattern=FROM_PATTERN,
to_pattern=TO_PATTERN,
sort_pattern=SORT_PATTERN,
*args,
**kwargs,
):
self.from_pattern = from_pattern
self.to_pattern = to_pattern
self.sort_pattern = sort_pattern
super().__init__(*args, **kwargs)

def _match_to_date(self, regex_match: re.Match) -> date:
match_dict = regex_match.groupdict()
print(match_dict.keys())
if match_dict.keys() >= {"day", "month", "year"}:
month = match_dict.get("month", "01")
day = match_dict.get("day", "01")
return f"{match_dict['year']}-{month}-{day}"
raise ValueError(
f"Regex pattern does not contain all needed named groups (year, month, day): {match_dict}"
)

def _populate_fields(self, model_instance):
super()._populate_fields(model_instance)
name = self.attname
value = getattr(model_instance, name)
if match := re.search(self.sort_pattern, value):
setattr(model_instance, f"{name}_date_sort", self._match_to_date(match))
if match := re.search(self.from_pattern, value):
setattr(model_instance, f"{name}_date_from", self._match_to_date(match))
if match := re.search(self.to_pattern, value):
setattr(model_instance, f"{name}_date_to", self._match_to_date(match))

0 comments on commit 96e83cf

Please sign in to comment.