# -*- coding: utf-8 -*- import datetime as dt import six try: import dateutil except ImportError: dateutil = None from mongoengine import * from mongoengine import connection from tests.utils import MongoDBTestCase class TestDateTimeField(MongoDBTestCase): def test_datetime_from_empty_string(self): """ Ensure an exception is raised when trying to cast an empty string to datetime. """ class MyDoc(Document): dt = DateTimeField() md = MyDoc(dt='') self.assertRaises(ValidationError, md.save) def test_datetime_from_whitespace_string(self): """ Ensure an exception is raised when trying to cast a whitespace-only string to datetime. """ class MyDoc(Document): dt = DateTimeField() md = MyDoc(dt=' ') self.assertRaises(ValidationError, md.save) def test_default_value_utcnow(self): """Ensure that default field values are used when creating a document. """ class Person(Document): created = DateTimeField(default=dt.datetime.utcnow) utcnow = dt.datetime.utcnow() person = Person() person.validate() person_created_t0 = person.created self.assertLess(person.created - utcnow, dt.timedelta(seconds=1)) self.assertEqual(person_created_t0, person.created) # make sure it does not change self.assertEqual(person._data['created'], person.created) def test_handling_microseconds(self): """Tests showing pymongo datetime fields handling of microseconds. Microseconds are rounded to the nearest millisecond and pre UTC handling is wonky. See: http://api.mongodb.org/python/current/api/bson/son.html#dt """ class LogEntry(Document): date = DateTimeField() LogEntry.drop_collection() # Test can save dates log = LogEntry() log.date = dt.date.today() log.save() log.reload() self.assertEqual(log.date.date(), dt.date.today()) # Post UTC - microseconds are rounded (down) nearest millisecond and # dropped d1 = dt.datetime(1970, 1, 1, 0, 0, 1, 999) d2 = dt.datetime(1970, 1, 1, 0, 0, 1) log = LogEntry() log.date = d1 log.save() log.reload() self.assertNotEqual(log.date, d1) self.assertEqual(log.date, d2) # Post UTC - microseconds are rounded (down) nearest millisecond d1 = dt.datetime(1970, 1, 1, 0, 0, 1, 9999) d2 = dt.datetime(1970, 1, 1, 0, 0, 1, 9000) log.date = d1 log.save() log.reload() self.assertNotEqual(log.date, d1) self.assertEqual(log.date, d2) if not six.PY3: # Pre UTC dates microseconds below 1000 are dropped # This does not seem to be true in PY3 d1 = dt.datetime(1969, 12, 31, 23, 59, 59, 999) d2 = dt.datetime(1969, 12, 31, 23, 59, 59) log.date = d1 log.save() log.reload() self.assertNotEqual(log.date, d1) self.assertEqual(log.date, d2) def test_regular_usage(self): """Tests for regular datetime fields""" class LogEntry(Document): date = DateTimeField() LogEntry.drop_collection() d1 = dt.datetime(1970, 1, 1, 0, 0, 1) log = LogEntry() log.date = d1 log.validate() log.save() for query in (d1, d1.isoformat(' ')): log1 = LogEntry.objects.get(date=query) self.assertEqual(log, log1) if dateutil: log1 = LogEntry.objects.get(date=d1.isoformat('T')) self.assertEqual(log, log1) # create additional 19 log entries for a total of 20 for i in range(1971, 1990): d = dt.datetime(i, 1, 1, 0, 0, 1) LogEntry(date=d).save() self.assertEqual(LogEntry.objects.count(), 20) # Test ordering logs = LogEntry.objects.order_by("date") i = 0 while i < 19: self.assertTrue(logs[i].date <= logs[i + 1].date) i += 1 logs = LogEntry.objects.order_by("-date") i = 0 while i < 19: self.assertTrue(logs[i].date >= logs[i + 1].date) i += 1 # Test searching logs = LogEntry.objects.filter(date__gte=dt.datetime(1980, 1, 1)) self.assertEqual(logs.count(), 10) logs = LogEntry.objects.filter(date__lte=dt.datetime(1980, 1, 1)) self.assertEqual(logs.count(), 10) logs = LogEntry.objects.filter( date__lte=dt.datetime(1980, 1, 1), date__gte=dt.datetime(1975, 1, 1), ) self.assertEqual(logs.count(), 5) def test_datetime_validation(self): """Ensure that invalid values cannot be assigned to datetime fields. """ class LogEntry(Document): time = DateTimeField() log = LogEntry() log.time = dt.datetime.now() log.validate() log.time = dt.date.today() log.validate() log.time = dt.datetime.now().isoformat(' ') log.validate() log.time = '2019-05-16 21:42:57.897847' log.validate() if dateutil: log.time = dt.datetime.now().isoformat('T') log.validate() log.time = -1 self.assertRaises(ValidationError, log.validate) log.time = 'ABC' self.assertRaises(ValidationError, log.validate) log.time = '2019-05-16 21:GARBAGE:12' self.assertRaises(ValidationError, log.validate) log.time = '2019-05-16 21:42:57.GARBAGE' self.assertRaises(ValidationError, log.validate) log.time = '2019-05-16 21:42:57.123.456' self.assertRaises(ValidationError, log.validate) def test_parse_datetime_as_str(self): class DTDoc(Document): date = DateTimeField() date_str = '2019-03-02 22:26:01' # make sure that passing a parsable datetime works dtd = DTDoc() dtd.date = date_str self.assertIsInstance(dtd.date, six.string_types) dtd.save() dtd.reload() self.assertIsInstance(dtd.date, dt.datetime) self.assertEqual(str(dtd.date), date_str) dtd.date = 'January 1st, 9999999999' self.assertRaises(ValidationError, dtd.validate) class TestDateTimeTzAware(MongoDBTestCase): def test_datetime_tz_aware_mark_as_changed(self): # Reset the connections connection._connection_settings = {} connection._connections = {} connection._dbs = {} connect(db='mongoenginetest', tz_aware=True) class LogEntry(Document): time = DateTimeField() LogEntry.drop_collection() LogEntry(time=dt.datetime(2013, 1, 1, 0, 0, 0)).save() log = LogEntry.objects.first() log.time = dt.datetime(2013, 1, 1, 0, 0, 0) self.assertEqual(['time'], log._changed_fields)