From fce3b5995344213a39ee2b6091e8118a707a4fca Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Wed, 21 Mar 2018 14:49:17 -0400 Subject: [PATCH] Support serializing date/datetime values before 1900 on python 2 Python 2 does not support strftime() for datetimes prior to 1900. This is fixed in Python 3.x but the python developers have signaled that they do not intend to backport the fix. The module django.utils.datetime_safe was written to workaround this issue. I've tried to use a light hand, only using the datetime_safe functions where serialization would otherwise throw a ValueError. --- rest_framework/fields.py | 38 +++++++++++++++++++++++++++----------- tests/test_fields.py | 27 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 58e28ed4c..abc2d478b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -18,7 +18,7 @@ from django.core.validators import ( ) from django.forms import FilePathField as DjangoFilePathField from django.forms import ImageField as DjangoImageField -from django.utils import six, timezone +from django.utils import datetime_safe, six, timezone from django.utils.dateparse import ( parse_date, parse_datetime, parse_duration, parse_time ) @@ -1152,6 +1152,12 @@ class DateTimeField(Field): self.timezone = default_timezone super(DateTimeField, self).__init__(*args, **kwargs) + def ensure_pre_1900_safe(self, value): + if isinstance(value, datetime.date): + if six.PY2 and value.year < 1900: + value = datetime_safe.new_datetime(value) + return value + def enforce_timezone(self, value): """ When `self.default_timezone` is `None`, always return naive datetimes. @@ -1162,16 +1168,18 @@ class DateTimeField(Field): if field_timezone is not None: if timezone.is_aware(value): try: - return value.astimezone(field_timezone) + value = value.astimezone(field_timezone) except OverflowError: self.fail('overflow') - try: - return timezone.make_aware(value, field_timezone) - except InvalidTimeError: - self.fail('make_aware', timezone=field_timezone) + else: + try: + value = timezone.make_aware(value, field_timezone) + except InvalidTimeError: + self.fail('make_aware', timezone=field_timezone) elif (field_timezone is None) and timezone.is_aware(value): - return timezone.make_naive(value, utc) - return value + value = timezone.make_naive(value, utc) + + return self.ensure_pre_1900_safe(value) def default_timezone(self): return timezone.get_current_timezone() if settings.USE_TZ else None @@ -1236,6 +1244,12 @@ class DateField(Field): self.input_formats = input_formats super(DateField, self).__init__(*args, **kwargs) + def ensure_pre_1900_safe(self, value): + if isinstance(value, datetime.date): + if six.PY2 and value.year < 1900: + value = datetime_safe.new_date(value) + return value + def to_internal_value(self, value): input_formats = getattr(self, 'input_formats', api_settings.DATE_INPUT_FORMATS) @@ -1243,7 +1257,7 @@ class DateField(Field): self.fail('datetime') if isinstance(value, datetime.date): - return value + return self.ensure_pre_1900_safe(value) for input_format in input_formats: if input_format.lower() == ISO_8601: @@ -1253,14 +1267,14 @@ class DateField(Field): pass else: if parsed is not None: - return parsed + return self.ensure_pre_1900_safe(parsed) else: try: parsed = self.datetime_parser(value, input_format) except (ValueError, TypeError): pass else: - return parsed.date() + return self.ensure_pre_1900_safe(parsed.date()) humanized_format = humanize_datetime.date_formats(input_formats) self.fail('invalid', format=humanized_format) @@ -1283,6 +1297,8 @@ class DateField(Field): 'read-only field and deal with timezone issues explicitly.' ) + value = self.ensure_pre_1900_safe(value) + if output_format.lower() == ISO_8601: return value.isoformat() diff --git a/tests/test_fields.py b/tests/test_fields.py index eee794eaa..cf9779ad9 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1120,6 +1120,7 @@ class TestDateField(FieldValues): valid_inputs = { '2001-01-01': datetime.date(2001, 1, 1), datetime.date(2001, 1, 1): datetime.date(2001, 1, 1), + '1800-01-01': datetime.date(1800, 1, 1), } invalid_inputs = { 'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].'], @@ -1129,6 +1130,8 @@ class TestDateField(FieldValues): outputs = { datetime.date(2001, 1, 1): '2001-01-01', '2001-01-01': '2001-01-01', + datetime.date(1800, 1, 1): '1800-01-01', + '1800-01-01': '1800-01-01', six.text_type('2016-01-10'): '2016-01-10', None: None, '': None, @@ -1174,6 +1177,18 @@ class TestNoOutputFormatDateField(FieldValues): field = serializers.DateField(format=None) +class TestPre1900DateField(FieldValues): + """ + Values for `DateField` prior to 1900 + """ + valid_inputs = {} + invalid_inputs = {} + outputs = { + datetime.date(1800, 1, 1): '01 Jan 1800', + } + field = serializers.DateField(format='%d %b %Y') + + class TestDateTimeField(FieldValues): """ Valid and invalid values for `DateTimeField`. @@ -1348,6 +1363,18 @@ class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): field = serializers.DateTimeField(default_timezone=MockTimezone()) +class TestPre1900DateTimeField(FieldValues): + """ + Values for `DateTimeField` prior to 1900. + """ + valid_inputs = {} + invalid_inputs = {} + outputs = { + datetime.datetime(1800, 1, 1, 13, 00): '01:00PM, 01 Jan 1800', + } + field = serializers.DateTimeField(format='%I:%M%p, %d %b %Y') + + class TestTimeField(FieldValues): """ Valid and invalid values for `TimeField`.