From ad15781d8f1b06562ab4fdcf255b60805952387e Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 6 Jun 2013 13:31:52 +0000 Subject: [PATCH] Fixed amibiguity and differing behaviour regarding field defaults (#349) Now field defaults are king, unsetting or setting to None on a field with a default means the default is reapplied. --- AUTHORS | 1 + docs/apireference.rst | 1 + docs/changelog.rst | 1 + mongoengine/base/fields.py | 38 ++++++-- mongoengine/document.py | 2 +- mongoengine/queryset/queryset.py | 6 +- tests/fields/fields.py | 144 ++++++++++++++++++++++++++----- 7 files changed, 160 insertions(+), 33 deletions(-) diff --git a/AUTHORS b/AUTHORS index 4977f735..4caed40c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -167,3 +167,4 @@ that much better: * zhy0216 (https://github.com/zhy0216) * istinspring (https://github.com/istinspring) * Massimo Santini (https://github.com/mapio) + * Nigel McNie (https://github.com/nigelmcnie) diff --git a/docs/apireference.rst b/docs/apireference.rst index 37370e20..0fa410e1 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -54,6 +54,7 @@ Querying Fields ====== +.. autoclass:: mongoengine.base.fields.BaseField .. autoclass:: mongoengine.fields.StringField .. autoclass:: mongoengine.fields.URLField .. autoclass:: mongoengine.fields.EmailField diff --git a/docs/changelog.rst b/docs/changelog.rst index 8ccd395e..bce26c5f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.8.2 ================ +- Fixed amibiguity and differing behaviour regarding field defaults (#349) - ImageFields now include PIL error messages if invalid error (#353) - Added lock when calling doc.Delete() for when signals have no sender (#350) - Reload forces read preference to be PRIMARY (#355) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 35075ec9..e4c88a79 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -36,6 +36,29 @@ class BaseField(object): unique=False, unique_with=None, primary_key=False, validation=None, choices=None, verbose_name=None, help_text=None): + """ + :param db_field: The database field to store this field in + (defaults to the name of the field) + :param name: Depreciated - use db_field + :param required: If the field is required. Whether it has to have a + value or not. Defaults to False. + :param default: (optional) The default value for this field if no value + has been set (or if the value has been unset). It Can be a + callable. + :param unique: Is the field value unique or not. Defaults to False. + :param unique_with: (optional) The other field this field should be + unique with. + :param primary_key: Mark this field as the primary key. Defaults to False. + :param validation: (optional) A callable to validate the value of the + field. Generally this is deprecated in favour of the + `FIELD.validate` method + :param choices: (optional) The valid choices + :param verbose_name: (optional) The verbose name for the field. + Designed to be human readable and is often used when generating + model forms from the document model. + :param help_text: (optional) The help text for this field and is often + used when generating model forms from the document model. + """ self.db_field = (db_field or name) if not primary_key else '_id' if name: msg = "Fields' 'name' attribute deprecated in favour of 'db_field'" @@ -65,14 +88,9 @@ class BaseField(object): if instance is None: # Document class being used rather than a document object return self - # Get value from document instance if available, if not use default - value = instance._data.get(self.name) - if value is None: - value = self.default - # Allow callable default values - if callable(value): - value = value() + # Get value from document instance if available + value = instance._data.get(self.name) EmbeddedDocument = _import_class('EmbeddedDocument') if isinstance(value, EmbeddedDocument) and value._instance is None: @@ -82,9 +100,11 @@ class BaseField(object): def __set__(self, instance, value): """Descriptor for assigning a value to a field in a document. """ - if value is None: + + # If setting to None and theres a default + # Then set the value to the default value + if value is None and self.default is not None: value = self.default - # Allow callable default values if callable(value): value = value() diff --git a/mongoengine/document.py b/mongoengine/document.py index 585fcf76..8e7ccc20 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -206,7 +206,7 @@ class Document(BaseDocument): if validate: self.validate(clean=clean) - if not write_concern: + if write_concern is None: write_concern = {"w": 1} doc = self.to_mongo() diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 7adfa659..d58a13b7 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -348,7 +348,7 @@ class QuerySet(object): """ Document = _import_class('Document') - if not write_concern: + if write_concern is None: write_concern = {} docs = doc_or_docs @@ -424,7 +424,7 @@ class QuerySet(object): queryset = self.clone() doc = queryset._document - if not write_concern: + if write_concern is None: write_concern = {} # Handle deletes where skips or limits have been applied or @@ -490,7 +490,7 @@ class QuerySet(object): if not update and not upsert: raise OperationError("No update parameters, would remove data") - if not write_concern: + if write_concern is None: write_concern = {} queryset = self.clone() diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 51184375..3e48a214 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -34,33 +34,137 @@ class FieldTest(unittest.TestCase): self.db.drop_collection('fs.files') self.db.drop_collection('fs.chunks') - def test_default_values(self): + def test_default_values_nothing_set(self): """Ensure that default field values are used when creating a document. """ class Person(Document): name = StringField() - age = IntField(default=30, help_text="Your real age") - userid = StringField(default=lambda: 'test', verbose_name="User Identity") - - person = Person(name='Test Person') - self.assertEqual(person._data['age'], 30) - self.assertEqual(person._data['userid'], 'test') - self.assertEqual(person._fields['name'].help_text, None) - self.assertEqual(person._fields['age'].help_text, "Your real age") - self.assertEqual(person._fields['userid'].verbose_name, "User Identity") - - class Person2(Document): + age = IntField(default=30, required=False) + userid = StringField(default=lambda: 'test', required=True) created = DateTimeField(default=datetime.datetime.utcnow) - person = Person2() - date1 = person.created - date2 = person.created - self.assertEqual(date1, date2) + person = Person(name="Ross") - person = Person2(created=None) - date1 = person.created - date2 = person.created - self.assertEqual(date1, date2) + # 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']) + + self.assertTrue(person.validate() is None) + + self.assertEqual(person.name, person.name) + self.assertEqual(person.age, person.age) + self.assertEqual(person.userid, person.userid) + self.assertEqual(person.created, person.created) + + 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) + + # Confirm introspection changes nothing + data_to_be_saved = sorted(person.to_mongo().keys()) + self.assertEqual(data_to_be_saved, ['age', 'created', 'name', 'userid']) + + def test_default_values_set_to_None(self): + """Ensure that default field values are used when creating a document. + """ + class Person(Document): + name = StringField() + age = IntField(default=30, required=False) + userid = StringField(default=lambda: 'test', required=True) + created = DateTimeField(default=datetime.datetime.utcnow) + + # Trying setting values to None + person = Person(name=None, age=None, userid=None, created=None) + + # Confirm saving now would store values + data_to_be_saved = sorted(person.to_mongo().keys()) + self.assertEqual(data_to_be_saved, ['age', 'created', 'userid']) + + self.assertTrue(person.validate() is None) + + self.assertEqual(person.name, person.name) + self.assertEqual(person.age, person.age) + self.assertEqual(person.userid, person.userid) + self.assertEqual(person.created, person.created) + + 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) + + # Confirm introspection changes nothing + data_to_be_saved = sorted(person.to_mongo().keys()) + self.assertEqual(data_to_be_saved, ['age', 'created', 'userid']) + + def test_default_values_when_setting_to_None(self): + """Ensure that default field values are used when creating a document. + """ + class Person(Document): + name = StringField() + age = IntField(default=30, required=False) + userid = StringField(default=lambda: 'test', required=True) + created = DateTimeField(default=datetime.datetime.utcnow) + + person = Person() + person.name = None + person.age = None + person.userid = None + person.created = None + + # Confirm saving now would store values + data_to_be_saved = sorted(person.to_mongo().keys()) + self.assertEqual(data_to_be_saved, ['age', 'created', 'userid']) + + self.assertTrue(person.validate() is None) + + self.assertEqual(person.name, person.name) + self.assertEqual(person.age, person.age) + self.assertEqual(person.userid, person.userid) + self.assertEqual(person.created, person.created) + + 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) + + # Confirm introspection changes nothing + data_to_be_saved = sorted(person.to_mongo().keys()) + self.assertEqual(data_to_be_saved, ['age', 'created', 'userid']) + + def test_default_values_when_deleting_value(self): + """Ensure that default field values are used when creating a document. + """ + class Person(Document): + name = StringField() + age = IntField(default=30, required=False) + userid = StringField(default=lambda: 'test', required=True) + created = DateTimeField(default=datetime.datetime.utcnow) + + person = Person(name="Ross") + del person.name + del person.age + del person.userid + del person.created + + data_to_be_saved = sorted(person.to_mongo().keys()) + self.assertEqual(data_to_be_saved, ['age', 'created', 'userid']) + + self.assertTrue(person.validate() is None) + + self.assertEqual(person.name, person.name) + self.assertEqual(person.age, person.age) + self.assertEqual(person.userid, person.userid) + self.assertEqual(person.created, person.created) + + 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) + + # Confirm introspection changes nothing + data_to_be_saved = sorted(person.to_mongo().keys()) + self.assertEqual(data_to_be_saved, ['age', 'created', 'userid']) def test_required_values(self): """Ensure that required field constraints are enforced.