Skip to content

Commit

Permalink
Allow null and strings as date values
Browse files Browse the repository at this point in the history
When creating or modifying Django instances, it is possible to use a
string for DateField, DateTimeField, and DurationField values.  Before
writing to the database, these are converted to Date, DateTime, etc.,
but when reading the value from the new instance, it will still be a
string. This code uses the Django utilities to parse strings into
datetime objects before serializing them to JSON for the cache.

For nullable fields, None is a possible value, and this is serialized as
null for the cache.
  • Loading branch information
jwhitlock committed Nov 4, 2015
1 parent a9f0719 commit 8373074
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 0 deletions.
16 changes: 16 additions & 0 deletions drf_cached_instances/cache.py
Expand Up @@ -12,6 +12,8 @@
get_model = apps.get_model
except ImportError: # pragma: nocover
from django.db.models.loading import get_model
from django.utils.dateparse import parse_date, parse_datetime, parse_duration
from django.utils.six import string_types

from .models import PkOnlyModel, PkOnlyQueryset

Expand Down Expand Up @@ -241,13 +243,17 @@ def field_date_from_json(self, date_triple):

def field_date_to_json(self, day):
"""Convert a date to a date triple."""
if isinstance(day, string_types):
day = parse_date(day)
return [day.year, day.month, day.day] if day else None

def field_datetime_from_json(self, json_val):
"""Convert a UTC timestamp to a UTC datetime."""
if type(json_val) == int:
seconds = int(json_val)
dt = datetime.fromtimestamp(seconds, utc)
elif json_val is None:
dt = None
else:
seconds, microseconds = [int(x) for x in json_val.split('.')]
dt = datetime.fromtimestamp(seconds, utc)
Expand All @@ -259,6 +265,10 @@ def field_datetime_to_json(self, dt):
datetimes w/o timezone will be assumed to be in UTC
"""
if isinstance(dt, string_types):
dt = parse_datetime(dt)
if not dt:
return None
ts = timegm(dt.utctimetuple())
if dt.microsecond:
return "{0}.{1:0>6d}".format(ts, dt.microsecond)
Expand All @@ -273,6 +283,8 @@ def field_timedelta_from_json(self, json_val):
"""
if isinstance(json_val, str):
return timedelta(seconds=float(json_val))
elif json_val is None:
return None
else:
return timedelta(seconds=json_val)

Expand All @@ -282,6 +294,10 @@ def field_timedelta_to_json(self, td):
If there are fractions of a second the return value will be a
string, otherwise it will be an int.
"""
if isinstance(td, string_types):
td = parse_duration(td)
if not td:
return None
try:
if td.microseconds > 0:
return str(td.total_seconds())
Expand Down
39 changes: 39 additions & 0 deletions tests/test_cache.py
Expand Up @@ -306,6 +306,19 @@ def test_date(self):
out = self.cache.field_date_from_json(converted)
self.assertEqual(out, the_date)

def test_date_string(self):
"""A date string can be stored and retrieved as a datetime.date."""
the_date = '2015-11-04'
converted = self.cache.field_date_to_json(the_date)
self.assertEqual(converted, [2015, 11, 4])
out = self.cache.field_date_from_json(converted)
self.assertEqual(out, date(2015, 11, 4))

def test_date_other_string(self):
"""A non-date string is stored as None."""
self.assertIsNone(self.cache.field_date_to_json('today'))
self.assertIsNone(self.cache.field_date_from_json(None))

def test_datetime_with_ms(self):
"""A datetime with milliseconds can be stored and retrieved."""
dt = datetime(2014, 9, 22, 8, 52, 0, 123456, UTC)
Expand All @@ -330,6 +343,19 @@ def test_datetime_without_timezone(self):
out = self.cache.field_datetime_from_json(converted)
self.assertEqual(out, datetime(2014, 9, 22, 8, 52, 0, 123456, UTC))

def test_datetime_string(self):
"""A datetime string can be stored and retrieved as a datetime."""
dt = "2015-11-04 16:40:04.892566"
converted = self.cache.field_datetime_to_json(dt)
self.assertEqual(converted, '1446655204.892566')
out = self.cache.field_datetime_from_json(converted)
self.assertEqual(out, datetime(2015, 11, 4, 16, 40, 4, 892566, UTC))

def test_datetime_other_string(self):
"""A non-datetime string is stored as null."""
self.assertIsNone(self.cache.field_datetime_to_json("now"))
self.assertIsNone(self.cache.field_datetime_from_json(None))

def test_timedelta_without_fractional_seconds(self):
"""Timedelta without fractional seconds can be stored and retrieved."""
td = timedelta(days=10, hours=1, minutes=9, seconds=5)
Expand All @@ -347,6 +373,19 @@ def test_timedelta_with_fractional_seconds(self):
out = self.cache.field_timedelta_from_json(converted)
self.assertEqual(out, td)

def test_timedelta_duration_string(self):
"""A duration string can be stored and retrieved as a timedelta."""
td = "5 3:30:15.99"
converted = self.cache.field_timedelta_to_json(td)
self.assertEqual(converted, '444615.99')
out = self.cache.field_timedelta_from_json(converted)
self.assertEqual(out, timedelta(5, 12615, 990000))

def test_timedelta_other_string(self):
"""A non-duration string is stored as null."""
self.assertIsNone(self.cache.field_timedelta_to_json("a long time"))
self.assertIsNone(self.cache.field_timedelta_from_json(None))

def test_pklist(self):
"""A list of primary keys is retrieved as a PkOnlyQueryset."""
converted = self.cache.field_pklist_to_json(User, (1, 2, 3))
Expand Down

0 comments on commit 8373074

Please sign in to comment.