Commit 8ee92f8a by Tom Christie

Refuse to downcast from datetime to date or time

parent b47ca158
...@@ -608,120 +608,126 @@ class DecimalField(Field): ...@@ -608,120 +608,126 @@ class DecimalField(Field):
# Date & time fields... # Date & time fields...
class DateField(Field): class DateTimeField(Field):
default_error_messages = { default_error_messages = {
'invalid': _('Date has wrong format. Use one of these formats instead: {format}'), 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'),
'datetime': _('Expected a date but got a datetime.'), 'date': _('Expected a datetime but got a date.'),
} }
format = api_settings.DATE_FORMAT format = api_settings.DATETIME_FORMAT
input_formats = api_settings.DATE_INPUT_FORMATS input_formats = api_settings.DATETIME_INPUT_FORMATS
default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
def __init__(self, format=empty, input_formats=None, *args, **kwargs): def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs):
self.format = format if format is not empty else self.format self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats self.input_formats = input_formats if input_formats is not None else self.input_formats
super(DateField, self).__init__(*args, **kwargs) self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone
super(DateTimeField, self).__init__(*args, **kwargs)
def enforce_timezone(self, value):
"""
When `self.default_timezone` is `None`, always return naive datetimes.
When `self.default_timezone` is not `None`, always return aware datetimes.
"""
if (self.default_timezone is not None) and not timezone.is_aware(value):
return timezone.make_aware(value, self.default_timezone)
elif (self.default_timezone is None) and timezone.is_aware(value):
return timezone.make_naive(value, timezone.UTC())
return value
def to_internal_value(self, value): def to_internal_value(self, value):
if isinstance(value, datetime.datetime): if (isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
self.fail('datetime') self.fail('date')
if isinstance(value, datetime.date): if isinstance(value, datetime.datetime):
return value return self.enforce_timezone(value)
for format in self.input_formats: for format in self.input_formats:
if format.lower() == ISO_8601: if format.lower() == ISO_8601:
try: try:
parsed = parse_date(value) parsed = parse_datetime(value)
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
else: else:
if parsed is not None: if parsed is not None:
return parsed return self.enforce_timezone(parsed)
else: else:
try: try:
parsed = datetime.datetime.strptime(value, format) parsed = datetime.datetime.strptime(value, format)
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
else: else:
return parsed.date() return self.enforce_timezone(parsed)
humanized_format = humanize_datetime.date_formats(self.input_formats) humanized_format = humanize_datetime.datetime_formats(self.input_formats)
self.fail('invalid', format=humanized_format) self.fail('invalid', format=humanized_format)
def to_representation(self, value): def to_representation(self, value):
if value is None or self.format is None: if value is None or self.format is None:
return value return value
if isinstance(value, datetime.datetime):
value = value.date()
if self.format.lower() == ISO_8601: if self.format.lower() == ISO_8601:
return value.isoformat() ret = value.isoformat()
if ret.endswith('+00:00'):
ret = ret[:-6] + 'Z'
return ret
return value.strftime(self.format) return value.strftime(self.format)
class DateTimeField(Field): class DateField(Field):
default_error_messages = { default_error_messages = {
'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'), 'invalid': _('Date has wrong format. Use one of these formats instead: {format}'),
'date': _('Expected a datetime but got a date.'), 'datetime': _('Expected a date but got a datetime.'),
} }
format = api_settings.DATETIME_FORMAT format = api_settings.DATE_FORMAT
input_formats = api_settings.DATETIME_INPUT_FORMATS input_formats = api_settings.DATE_INPUT_FORMATS
default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs): def __init__(self, format=empty, input_formats=None, *args, **kwargs):
self.format = format if format is not empty else self.format self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats self.input_formats = input_formats if input_formats is not None else self.input_formats
self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone super(DateField, self).__init__(*args, **kwargs)
super(DateTimeField, self).__init__(*args, **kwargs)
def enforce_timezone(self, value):
"""
When `self.default_timezone` is `None`, always return naive datetimes.
When `self.default_timezone` is not `None`, always return aware datetimes.
"""
if (self.default_timezone is not None) and not timezone.is_aware(value):
return timezone.make_aware(value, self.default_timezone)
elif (self.default_timezone is None) and timezone.is_aware(value):
return timezone.make_naive(value, timezone.UTC())
return value
def to_internal_value(self, value): def to_internal_value(self, value):
if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
self.fail('date')
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
return self.enforce_timezone(value) self.fail('datetime')
if isinstance(value, datetime.date):
return value
for format in self.input_formats: for format in self.input_formats:
if format.lower() == ISO_8601: if format.lower() == ISO_8601:
try: try:
parsed = parse_datetime(value) parsed = parse_date(value)
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
else: else:
if parsed is not None: if parsed is not None:
return self.enforce_timezone(parsed) return parsed
else: else:
try: try:
parsed = datetime.datetime.strptime(value, format) parsed = datetime.datetime.strptime(value, format)
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
else: else:
return self.enforce_timezone(parsed) return parsed.date()
humanized_format = humanize_datetime.datetime_formats(self.input_formats) humanized_format = humanize_datetime.date_formats(self.input_formats)
self.fail('invalid', format=humanized_format) self.fail('invalid', format=humanized_format)
def to_representation(self, value): def to_representation(self, value):
if value is None or self.format is None: if value is None or self.format is None:
return value return value
# Applying a `DateField` to a datetime value is almost always
# not a sensible thing to do, as it means naively dropping
# any explicit or implicit timezone info.
assert not isinstance(value, datetime.datetime), (
'Expected a `date`, but got a `datetime`. Refusing to coerce, '
'as this may mean losing timezone information. Use a custom '
'read-only field and deal with timezone issues explicitly.'
)
if self.format.lower() == ISO_8601: if self.format.lower() == ISO_8601:
ret = value.isoformat() return value.isoformat()
if ret.endswith('+00:00'):
ret = ret[:-6] + 'Z'
return ret
return value.strftime(self.format) return value.strftime(self.format)
...@@ -765,8 +771,14 @@ class TimeField(Field): ...@@ -765,8 +771,14 @@ class TimeField(Field):
if value is None or self.format is None: if value is None or self.format is None:
return value return value
if isinstance(value, datetime.datetime): # Applying a `TimeField` to a datetime value is almost always
value = value.time() # not a sensible thing to do, as it means naively dropping
# any explicit or implicit timezone info.
assert not isinstance(value, datetime.datetime), (
'Expected a `time`, but got a `datetime`. Refusing to coerce, '
'as this may mean losing timezone information. Use a custom '
'read-only field and deal with timezone issues explicitly.'
)
if self.format.lower() == ISO_8601: if self.format.lower() == ISO_8601:
return value.isoformat() return value.isoformat()
......
...@@ -173,7 +173,8 @@ class TestNullBooleanField(FieldValues): ...@@ -173,7 +173,8 @@ class TestNullBooleanField(FieldValues):
'null': None, 'null': None,
True: True, True: True,
False: False, False: False,
None: None None: None,
'other': True
} }
field = fields.NullBooleanField() field = fields.NullBooleanField()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment