From 905c8467a6f093ad10107addea7e35c5d173db1b Mon Sep 17 00:00:00 2001 From: Birger Schacht Date: Tue, 12 Nov 2024 13:03:50 +0100 Subject: [PATCH] feat(generic): introduce custom APIS DateIntervalFields 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. --- apis_core/generic/fields.py | 105 ++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 apis_core/generic/fields.py diff --git a/apis_core/generic/fields.py b/apis_core/generic/fields.py new file mode 100644 index 000000000..1e3e7d725 --- /dev/null +++ b/apis_core/generic/fields.py @@ -0,0 +1,105 @@ +from datetime import date +import re +from typing import Callable, List, Tuple +from django.db.models import DateField, CharField +from django.forms import ValidationError + +from apis_core.history.models import APISHistoryTableBase +from apis_core.utils import DateParser + + +class GenericDateIntervalField(CharField): + def contribute_to_class(self, cls, name): + super().contribute_to_class(cls, name) + if not issubclass(cls, APISHistoryTableBase): + DateField(editable=False, blank=True, null=True).contribute_to_class( + cls, f"{name}_date_sort" + ) + DateField(editable=False, blank=True, null=True).contribute_to_class( + cls, f"{name}_date_from" + ) + DateField(editable=False, blank=True, null=True).contribute_to_class( + cls, f"{name}_date_to" + ) + + +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 pre_save(self, model_instance, add): + name = self.attname + value = getattr(model_instance, name) + if not getattr(model_instance, "skip_date_parsing", False): + try: + date, date_from, date_to = self.parser(value) + print(date) + setattr(model_instance, f"{name}_date_sort", date) + 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}") + return super().pre_save(model_instance, add) + + +DEFAULT_DATE_REGEX = (r"(?P\d{1,2})\.(?P\d{1,2}).(?P\d{1,4})",) + + +class FuzzyDateRegexField(GenericDateIntervalField): + def __init__( + self, + regex_patterns: List[Tuple[str, str, str]] = [ + (DEFAULT_DATE_REGEX, DEFAULT_DATE_REGEX, DEFAULT_DATE_REGEX) + ], + *args, + **kwargs, + ): + self.regex_patterns = regex_patterns + super().__init__(*args, **kwargs) + + def _parse_date_using_regex_match(self, regex_match: re.Match) -> date: + match_dict = regex_match.groupdict() + if ( + "year" not in match_dict + or "month" not in match_dict + or "day" not in match_dict + ): + raise ValueError( + f"Regex pattern does not contain all needed named groups (year, month, day): {regex_match}" + ) + ret_date = f"{match_dict['year']}-{match_dict['month'] if match_dict['month'] is not None else '01'}-{match_dict['day'] if match_dict['day'] is not None else '01'}" + + return ret_date + + def _parse_date_using_list_of_regexes(self, value: str): + sort_pattern, from_pattern, to_pattern = self.regex_patterns + date_sort = re.search(sort_pattern, value) + date_from = re.search(from_pattern, value) + date_to = re.search(to_pattern, value) + if date_sort and date_from and date_to: + return ( + self._parse_date_using_regex_match(date_sort), + self._parse_date_using_regex_match(date_from), + self._parse_date_using_regex_match(date_to), + ) + return None, None, None + + def pre_save(self, model_instance, add): + name = self.attname + value = getattr(model_instance, name) + try: + date_sort, date_from, date_to = self._parse_date_using_list_of_regexes( + 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 with regex: {e}") + return super().pre_save(model_instance, add)