Repository: MongoEngine/mongoengine

Issue:
DateField
I know that there is no way to store dates in mongodb, but in code i have to. 
Is it some hook, where i can set DateTimeField in Document class, but declare some pre_save/post_save method, where i can convert from date to datetime with zeros and UTC tz and backward. Or shall I better write my own DateField inheriting from BaseField?



Discussion:
I think your own field would be best.
Perhaps you could also define as DateTimeField, but override the getters and setters to make it a Date?

Are such getters and setters hooks implemented already?

Why not store datetime and accept that it will also include seconds?

But i got dates in my code. It looks way more convenient put this date >  datetime and datetime > date transformation on ODM level rather than do it in app logic. 

+1 for DateField. 

When you make an auto filter generator **it bases on model class**. And the difference between `DateField` or `DateTimeField` is significant. 

For `DateTime` auto-generated queries will be like

``` python
{'created_on': {'$lt': datetime.datetime(2014, 6, 4, 0, 0, 1, tzinfo=...)
```

which obviously will "not work" with pure dates where we need 

``` python
{'$gte': day, '$lt': day + 1}
```

It has nothing to do with how mongoDB stores data. You need to distinguish it here.
Same thing as Decimal vs Float, btw. Mongo has no decimals but we want it in Python.

So everyone will come to declaring their own `DateField` sooner or later. 
So why just not add it to the core?

If MongoDB doesn't support date fields take it upstream (and report here when an issue was filed there) because that just plain wrong.
I'm +1 on emulating it for now since it's very useful.

I also came for the need of `DateField`. Any updates for this issue?

No date field? Seriously?

Resolving patch:
diff --git a/mongoengine/fields.py b/mongoengine/fields.py
--- a/mongoengine/fields.py
+++ b/mongoengine/fields.py
@@ -43,7 +43,7 @@
 
 __all__ = (
     'StringField', 'URLField', 'EmailField', 'IntField', 'LongField',
-    'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField',
+    'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField', 'DateField',
     'ComplexDateTimeField', 'EmbeddedDocumentField', 'ObjectIdField',
     'GenericEmbeddedDocumentField', 'DynamicField', 'ListField',
     'SortedListField', 'EmbeddedDocumentListField', 'DictField',
@@ -525,6 +525,22 @@ def prepare_query_value(self, op, value):
         return super(DateTimeField, self).prepare_query_value(op, self.to_mongo(value))
 
 
+class DateField(DateTimeField):
+    def to_mongo(self, value):
+        value = super(DateField, self).to_mongo(value)
+        # drop hours, minutes, seconds
+        if isinstance(value, datetime.datetime):
+            value = datetime.datetime(value.year, value.month, value.day)
+        return value
+
+    def to_python(self, value):
+        value = super(DateField, self).to_python(value)
+        # convert datetime to date
+        if isinstance(value, datetime.datetime):
+            value = datetime.date(value.year, value.month, value.day)
+        return value
+
+
 class ComplexDateTimeField(StringField):
     """
     ComplexDateTimeField handles microseconds exactly instead of rounding


Test patch:
diff --git a/tests/fields/fields.py b/tests/fields/fields.py
--- a/tests/fields/fields.py
+++ b/tests/fields/fields.py
@@ -46,6 +46,17 @@ class MyDoc(Document):
         md = MyDoc(dt='')
         self.assertRaises(ValidationError, md.save)
 
+    def test_date_from_empty_string(self):
+        """
+        Ensure an exception is raised when trying to
+        cast an empty string to datetime.
+        """
+        class MyDoc(Document):
+            dt = DateField()
+
+        md = MyDoc(dt='')
+        self.assertRaises(ValidationError, md.save)
+
     def test_datetime_from_whitespace_string(self):
         """
         Ensure an exception is raised when trying to
@@ -57,6 +68,17 @@ class MyDoc(Document):
         md = MyDoc(dt='   ')
         self.assertRaises(ValidationError, md.save)
 
+    def test_date_from_whitespace_string(self):
+        """
+        Ensure an exception is raised when trying to
+        cast a whitespace-only string to datetime.
+        """
+        class MyDoc(Document):
+            dt = DateField()
+
+        md = MyDoc(dt='   ')
+        self.assertRaises(ValidationError, md.save)
+
     def test_default_values_nothing_set(self):
         """Ensure that default field values are used when creating
         a document.
@@ -66,13 +88,14 @@ class Person(Document):
             age = IntField(default=30, required=False)
             userid = StringField(default=lambda: 'test', required=True)
             created = DateTimeField(default=datetime.datetime.utcnow)
+            day = DateField(default=datetime.date.today)
 
         person = Person(name="Ross")
 
         # Confirm saving now would store values
         data_to_be_saved = sorted(person.to_mongo().keys())
         self.assertEqual(data_to_be_saved,
-            ['age', 'created', 'name', 'userid']
+            ['age', 'created', 'day', 'name', 'userid']
         )
 
         self.assertTrue(person.validate() is None)
@@ -81,16 +104,18 @@ class Person(Document):
         self.assertEqual(person.age, person.age)
         self.assertEqual(person.userid, person.userid)
         self.assertEqual(person.created, person.created)
+        self.assertEqual(person.day, person.day)
 
         self.assertEqual(person._data['name'], person.name)
         self.assertEqual(person._data['age'], person.age)
         self.assertEqual(person._data['userid'], person.userid)
         self.assertEqual(person._data['created'], person.created)
+        self.assertEqual(person._data['day'], person.day)
 
         # Confirm introspection changes nothing
         data_to_be_saved = sorted(person.to_mongo().keys())
         self.assertEqual(
-            data_to_be_saved, ['age', 'created', 'name', 'userid'])
+            data_to_be_saved, ['age', 'created', 'day', 'name', 'userid'])
 
     def test_default_values_set_to_None(self):
         """Ensure that default field values are used even when
@@ -662,6 +687,32 @@ class LogEntry(Document):
         log.time = 'ABC'
         self.assertRaises(ValidationError, log.validate)
 
+    def test_date_validation(self):
+        """Ensure that invalid values cannot be assigned to datetime
+        fields.
+        """
+        class LogEntry(Document):
+            time = DateField()
+
+        log = LogEntry()
+        log.time = datetime.datetime.now()
+        log.validate()
+
+        log.time = datetime.date.today()
+        log.validate()
+
+        log.time = datetime.datetime.now().isoformat(' ')
+        log.validate()
+
+        if dateutil:
+            log.time = datetime.datetime.now().isoformat('T')
+            log.validate()
+
+        log.time = -1
+        self.assertRaises(ValidationError, log.validate)
+        log.time = 'ABC'
+        self.assertRaises(ValidationError, log.validate)
+
     def test_datetime_tz_aware_mark_as_changed(self):
         from mongoengine import connection
 
@@ -733,6 +784,51 @@ class LogEntry(Document):
             self.assertNotEqual(log.date, d1)
             self.assertEqual(log.date, d2)
 
+    def test_date(self):
+        """Tests showing pymongo date fields
+
+        See: http://api.mongodb.org/python/current/api/bson/son.html#dt
+        """
+        class LogEntry(Document):
+            date = DateField()
+
+        LogEntry.drop_collection()
+
+        # Test can save dates
+        log = LogEntry()
+        log.date = datetime.date.today()
+        log.save()
+        log.reload()
+        self.assertEqual(log.date, datetime.date.today())
+
+        d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999)
+        d2 = datetime.datetime(1970, 1, 1, 0, 0, 1)
+        log = LogEntry()
+        log.date = d1
+        log.save()
+        log.reload()
+        self.assertEqual(log.date, d1.date())
+        self.assertEqual(log.date, d2.date())
+
+        d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999)
+        d2 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9000)
+        log.date = d1
+        log.save()
+        log.reload()
+        self.assertEqual(log.date, d1.date())
+        self.assertEqual(log.date, d2.date())
+
+        if not six.PY3:
+            # Pre UTC dates microseconds below 1000 are dropped
+            # This does not seem to be true in PY3
+            d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, 999)
+            d2 = datetime.datetime(1969, 12, 31, 23, 59, 59)
+            log.date = d1
+            log.save()
+            log.reload()
+            self.assertEqual(log.date, d1.date())
+            self.assertEqual(log.date, d2.date())
+
     def test_datetime_usage(self):
         """Tests for regular datetime fields"""
         class LogEntry(Document):
@@ -787,6 +883,51 @@ class LogEntry(Document):
         )
         self.assertEqual(logs.count(), 5)
 
+    def test_date_usage(self):
+        """Tests for regular datetime fields"""
+        class LogEntry(Document):
+            date = DateField()
+
+        LogEntry.drop_collection()
+
+        d1 = datetime.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 = datetime.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=datetime.datetime(1980, 1, 1))
+        self.assertEqual(logs.count(), 10)
+
     def test_complexdatetime_storage(self):
         """Tests for complex datetime fields - which can handle
         microseconds without rounding.