This commit is contained in:
Arthur Hanson 2024-04-23 18:00:16 +02:00 committed by GitHub
commit 746f40d5f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 228 additions and 2 deletions

View File

@ -1,6 +1,6 @@
import decimal
import re
from datetime import datetime, date
from datetime import datetime, date, timezone
import django_filters
from django import forms
@ -599,6 +599,232 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
return filter_instance
def _parse_hh_mm_ss_ff(self, tstr):
# Parses things of the form HH[:?MM[:?SS[{.,}fff[fff]]]]
# TODO: Remove when drop python 3.10
len_str = len(tstr)
time_comps = [0, 0, 0, 0]
pos = 0
for comp in range(0, 3):
if (len_str - pos) < 2:
raise ValueError(_("Incomplete time component"))
time_comps[comp] = int(tstr[pos:pos + 2])
pos += 2
next_char = tstr[pos:pos + 1]
if comp == 0:
has_sep = next_char == ':'
if not next_char or comp >= 2:
break
if has_sep and next_char != ':':
raise ValueError(_("Invalid time separator: %c") % next_char)
pos += has_sep
if pos < len_str:
if tstr[pos] not in '.,':
raise ValueError(_("Invalid microsecond component"))
else:
pos += 1
len_remainder = len_str - pos
if len_remainder >= 6:
to_parse = 6
else:
to_parse = len_remainder
time_comps[3] = int(tstr[pos:(pos + to_parse)])
if to_parse < 6:
_FRACTION_CORRECTION = [100000, 10000, 1000, 100, 10]
time_comps[3] *= _FRACTION_CORRECTION[to_parse - 1]
if (len_remainder > to_parse and not all(map(_is_ascii_digit, tstr[(pos + to_parse):]))):
raise ValueError(_("Non-digit values in unparsed fraction"))
return time_comps
def _parse_isoformat_date(self, dtstr):
# It is assumed that this is an ASCII-only string of lengths 7, 8 or 10,
# see the comment on Modules/_datetimemodule.c:_find_isoformat_datetime_separator
# TODO: Remove when drop python 3.10
assert len(dtstr) in (7, 8, 10)
year = int(dtstr[0:4])
has_sep = dtstr[4] == '-'
pos = 4 + has_sep
if dtstr[pos:pos + 1] == "W":
# YYYY-?Www-?D?
pos += 1
weekno = int(dtstr[pos:pos + 2])
pos += 2
dayno = 1
if len(dtstr) > pos:
if (dtstr[pos:pos + 1] == '-') != has_sep:
raise ValueError("Inconsistent use of dash separator")
pos += has_sep
dayno = int(dtstr[pos:pos + 1])
return list(datetime._isoweek_to_gregorian(year, weekno, dayno))
else:
month = int(dtstr[pos:pos + 2])
pos += 2
if (dtstr[pos:pos + 1] == "-") != has_sep:
raise ValueError("Inconsistent use of dash separator")
pos += has_sep
day = int(dtstr[pos:pos + 2])
return [year, month, day]
def _parse_isoformat_time(self, tstr):
# Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]
# TODO: Remove when drop python 3.10
len_str = len(tstr)
if len_str < 2:
raise ValueError(_("Isoformat time too short"))
# This is equivalent to re.search('[+-Z]', tstr), but faster
tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1 or tstr.find('Z') + 1)
timestr = tstr[:tz_pos - 1] if tz_pos > 0 else tstr
time_comps = self._parse_hh_mm_ss_ff(timestr)
tzi = None
if tz_pos == len_str and tstr[-1] == 'Z':
tzi = timezone.utc
elif tz_pos > 0:
tzstr = tstr[tz_pos:]
# Valid time zone strings are:
# HH len: 2
# HHMM len: 4
# HH:MM len: 5
# HHMMSS len: 6
# HHMMSS.f+ len: 7+
# HH:MM:SS len: 8
# HH:MM:SS.f+ len: 10+
if len(tzstr) in (0, 1, 3):
raise ValueError(_("Malformed time zone string"))
tz_comps = self._parse_hh_mm_ss_ff(tzstr)
if all(x == 0 for x in tz_comps):
tzi = timezone.utc
else:
tzsign = -1 if tstr[tz_pos - 1] == '-' else 1
td = datetime.timedelta(
hours=tz_comps[0], minutes=tz_comps[1],
seconds=tz_comps[2], microseconds=tz_comps[3])
tzi = timezone(tzsign * td)
time_comps.append(tzi)
return time_comps
# Helpers for parsing the result of isoformat()
# TODO: Remove when drop python 3.10
def _is_ascii_digit(self, c):
return c in "0123456789"
def _find_isoformat_datetime_separator(self, dtstr):
# See the comment in _datetimemodule.c:_find_isoformat_datetime_separator
# TODO: Remove when drop python 3.10
len_dtstr = len(dtstr)
if len_dtstr == 7:
return 7
assert len_dtstr > 7
date_separator = "-"
week_indicator = "W"
if dtstr[4] == date_separator:
if dtstr[5] == week_indicator:
if len_dtstr < 8:
raise ValueError("Invalid ISO string")
if len_dtstr > 8 and dtstr[8] == date_separator:
if len_dtstr == 9:
raise ValueError("Invalid ISO string")
if len_dtstr > 10 and self._is_ascii_digit(dtstr[10]):
# This is as far as we need to resolve the ambiguity for
# the moment - if we have YYYY-Www-##, the separator is
# either a hyphen at 8 or a number at 10.
#
# We'll assume it's a hyphen at 8 because it's way more
# likely that someone will use a hyphen as a separator than
# a number, but at this point it's really best effort
# because this is an extension of the spec anyway.
# TODO(pganssle): Document this
return 8
return 10
else:
# YYYY-Www (8)
return 8
else:
# YYYY-MM-DD (10)
return 10
else:
if dtstr[4] == week_indicator:
# YYYYWww (7) or YYYYWwwd (8)
idx = 7
while idx < len_dtstr:
if not self._is_ascii_digit(dtstr[idx]):
break
idx += 1
if idx < 9:
return idx
if idx % 2 == 0:
# If the index of the last number is even, it's YYYYWwwd
return 7
else:
return 8
else:
# YYYYMMDD (8)
return 8
def fromisoformat(self, date_string):
"""Construct a datetime from a string in one of the ISO 8601 formats."""
# TODO: Remove when drop python 3.10
if not isinstance(date_string, str):
raise TypeError('fromisoformat: argument must be str')
if len(date_string) < 7:
raise ValueError(f'Invalid isoformat string: {date_string!r}')
# Split this at the separator
try:
separator_location = self._find_isoformat_datetime_separator(date_string)
dstr = date_string[0:separator_location]
tstr = date_string[(separator_location + 1):]
date_components = self._parse_isoformat_date(dstr)
except ValueError:
raise ValueError(
f'Invalid isoformat string: {date_string!r}') from None
if tstr:
try:
time_components = self._parse_isoformat_time(tstr)
except ValueError:
raise ValueError(
f'Invalid isoformat string: {date_string!r}') from None
else:
time_components = [0, 0, 0, 0, None]
return (date_components + time_components)
def validate(self, value):
"""
Validate a value according to the field's type validation rules.
@ -656,7 +882,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
elif self.type == CustomFieldTypeChoices.TYPE_DATETIME:
if type(value) is not datetime:
try:
datetime.fromisoformat(value)
self.fromisoformat(value)
except ValueError:
raise ValidationError(
_("Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS).")