From 2ffdbc7fc0e01905af8edc284039ab713b43e9ed Mon Sep 17 00:00:00 2001 From: Jimmy Shen Date: Sun, 19 Apr 2015 03:26:14 -0400 Subject: [PATCH] fixed microsecond-level ordering/filtering bug with ComplexDateTimeField as well as unused separator option --- docs/changelog.rst | 1 + mongoengine/fields.py | 41 +++++++++++------------------------------ tests/fields/fields.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 188247e6..3dbcc2f8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ Changes in 0.9.X - DEV - Support += and *= for ListField #595 - Use sets for populating dbrefs to dereference - Fixed unpickled documents replacing the global field's list. #888 +- Fixed storage of microseconds in ComplexDateTimeField and unused separator option. #910 Changes in 0.9.0 ================ diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 3c1db4ac..b5ed8bb2 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -458,37 +458,22 @@ class ComplexDateTimeField(StringField): """ def __init__(self, separator=',', **kwargs): - self.names = ['year', 'month', 'day', 'hour', 'minute', 'second', - 'microsecond'] - self.separtor = separator + self.names = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'] + self.separator = separator + self.format = separator.join(['%Y', '%m', '%d', '%H', '%M', '%S', '%f']) super(ComplexDateTimeField, self).__init__(**kwargs) - def _leading_zero(self, number): - """ - Converts the given number to a string. - - If it has only one digit, a leading zero so as it has always at least - two digits. - """ - if int(number) < 10: - return "0%s" % number - else: - return str(number) - def _convert_from_datetime(self, val): """ Convert a `datetime` object to a string representation (which will be stored in MongoDB). This is the reverse function of `_convert_from_string`. - >>> a = datetime(2011, 6, 8, 20, 26, 24, 192284) - >>> RealDateTimeField()._convert_from_datetime(a) - '2011,06,08,20,26,24,192284' + >>> a = datetime(2011, 6, 8, 20, 26, 24, 92284) + >>> ComplexDateTimeField()._convert_from_datetime(a) + '2011,06,08,20,26,24,092284' """ - data = [] - for name in self.names: - data.append(self._leading_zero(getattr(val, name))) - return ','.join(data) + return val.strftime(self.format) def _convert_from_string(self, data): """ @@ -496,16 +481,12 @@ class ComplexDateTimeField(StringField): will manipulate). This is the reverse function of `_convert_from_datetime`. - >>> a = '2011,06,08,20,26,24,192284' + >>> a = '2011,06,08,20,26,24,092284' >>> ComplexDateTimeField()._convert_from_string(a) - datetime.datetime(2011, 6, 8, 20, 26, 24, 192284) + datetime.datetime(2011, 6, 8, 20, 26, 24, 92284) """ - data = data.split(',') - data = map(int, data) - values = {} - for i in range(7): - values[self.names[i]] = data[i] - return datetime.datetime(**values) + values = map(int, data.split(self.separator)) + return datetime.datetime(*values) def __get__(self, instance, owner): data = super(ComplexDateTimeField, self).__get__(instance, owner) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 5e7f34b5..96686a8b 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -5,6 +5,9 @@ sys.path[0:0] = [""] import datetime import unittest import uuid +import math +import itertools +import re try: import dateutil @@ -689,6 +692,7 @@ class FieldTest(unittest.TestCase): """ class LogEntry(Document): date = ComplexDateTimeField() + date_with_dots = ComplexDateTimeField(separator='.') LogEntry.drop_collection() @@ -729,6 +733,18 @@ class FieldTest(unittest.TestCase): log1 = LogEntry.objects.get(date=d1) self.assertEqual(log, log1) + # Test string padding + microsecond = map(int, [math.pow(10, x) for x in xrange(6)]) + mm = dd = hh = ii = ss = [1, 10] + + for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond): + stored = LogEntry(date=datetime.datetime(*values)).to_mongo()['date'] + self.assertTrue(re.match('^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$', stored) is not None) + + # Test separator + stored = LogEntry(date_with_dots=datetime.datetime(2014, 1, 1)).to_mongo()['date_with_dots'] + self.assertTrue(re.match('^\d{4}.\d{2}.\d{2}.\d{2}.\d{2}.\d{2}.\d{6}$', stored) is not None) + LogEntry.drop_collection() def test_complexdatetime_usage(self): @@ -787,6 +803,25 @@ class FieldTest(unittest.TestCase): LogEntry.drop_collection() + # Test microsecond-level ordering/filtering + for microsecond in (99, 999, 9999, 10000): + LogEntry(date=datetime.datetime(2015, 1, 1, 0, 0, 0, microsecond)).save() + + logs = list(LogEntry.objects.order_by('date')) + for next_idx, log in enumerate(logs[:-1], start=1): + next_log = logs[next_idx] + self.assertTrue(log.date < next_log.date) + + logs = list(LogEntry.objects.order_by('-date')) + for next_idx, log in enumerate(logs[:-1], start=1): + next_log = logs[next_idx] + self.assertTrue(log.date > next_log.date) + + logs = LogEntry.objects.filter(date__lte=datetime.datetime(2015, 1, 1, 0, 0, 0, 10000)) + self.assertEqual(logs.count(), 4) + + LogEntry.drop_collection() + def test_list_validation(self): """Ensure that a list field only accepts lists with valid elements. """