diff --git a/docs/apireference.rst b/docs/apireference.rst index 03e44e63..1a243d2e 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -46,6 +46,8 @@ Fields .. autoclass:: mongoengine.EmbeddedDocumentField +.. autoclass:: mongoengine.DictField + .. autoclass:: mongoengine.ListField .. autoclass:: mongoengine.ObjectIdField diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 0a12cb6d..0862ffd0 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -39,6 +39,7 @@ are as follows: * :class:`~mongoengine.FloatField` * :class:`~mongoengine.DateTimeField` * :class:`~mongoengine.ListField` +* :class:`~mongoengine.DictField` * :class:`~mongoengine.ObjectIdField` * :class:`~mongoengine.EmbeddedDocumentField` * :class:`~mongoengine.ReferenceField` @@ -75,6 +76,23 @@ document class as the first argument:: comment2 = Comment('Nice article!') page = Page(comments=[comment1, comment2]) +Dictionary Fields +----------------- +Often, an embedded document may be used instead of a dictionary -- generally +this is recommended as dictionaries don't support validation or custom field +types. However, sometimes you will not know the structure of what you want to +store; in this situation a :class:`~mongoengine.DictField` is appropriate:: + + class SurveyResponse(Document): + date = DateTimeField() + user = ReferenceField(User) + answers = DictField() + + survey_response = SurveyResponse(date=datetime.now(), user=request.user) + response_form = ResponseForm(request.POST) + survey_response.answers = response_form.cleaned_data() + survey_response.save() + Reference fields ---------------- References may be stored to other documents in the database using the diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 6a7e95a4..67ac69c3 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -8,7 +8,7 @@ import datetime __all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField', - 'DateTimeField', 'EmbeddedDocumentField', 'ListField', + 'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField', 'ObjectIdField', 'ReferenceField', 'ValidationError'] @@ -179,6 +179,28 @@ class ListField(BaseField): return self.field.lookup_member(member_name) +class DictField(BaseField): + """A dictionary field that wraps a standard Python dictionary. This is + similar to an embedded document, but the structure is not defined. + + .. versionadded:: 0.2.3 + """ + + def validate(self, value): + """Make sure that a list of valid fields is being used. + """ + if not isinstance(value, dict): + raise ValidationError('Only dictionaries may be used in a ' + 'DictField') + + if any(('.' in k or '$' in k) for k in value): + raise ValidationError('Invalid dictionary key name - keys may not ' + 'contain "." or "$" characters') + + def lookup_member(self, member_name): + return BaseField(name=member_name) + + class ReferenceField(BaseField): """A reference to a document that will be automatically dereferenced on access (lazily). diff --git a/tests/fields.py b/tests/fields.py index 599e3d75..7949cc36 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -176,6 +176,28 @@ class FieldTest(unittest.TestCase): post.comments = 'yay' self.assertRaises(ValidationError, post.validate) + def test_dict_validation(self): + """Ensure that dict types work as expected. + """ + class BlogPost(Document): + info = DictField() + + post = BlogPost() + post.info = 'my post' + self.assertRaises(ValidationError, post.validate) + + post.info = ['test', 'test'] + self.assertRaises(ValidationError, post.validate) + + post.info = {'$title': 'test'} + self.assertRaises(ValidationError, post.validate) + + post.info = {'the.title': 'test'} + self.assertRaises(ValidationError, post.validate) + + post.info = {'title': 'test'} + post.validate() + def test_embedded_document_validation(self): """Ensure that invalid embedded documents cannot be assigned to embedded document fields. diff --git a/tests/queryset.py b/tests/queryset.py index 00f3e461..6d45bdae 100644 --- a/tests/queryset.py +++ b/tests/queryset.py @@ -277,6 +277,22 @@ class QuerySetTest(unittest.TestCase): BlogPost.drop_collection() + def test_find_dict_item(self): + """Ensure that DictField items may be found. + """ + class BlogPost(Document): + info = DictField() + + BlogPost.drop_collection() + + post = BlogPost(info={'title': 'test'}) + post.save() + + post_obj = BlogPost.objects(info__title='test').first() + self.assertEqual(post_obj.id, post.id) + + BlogPost.drop_collection() + def test_q(self): class BlogPost(Document): publish_date = DateTimeField()