Repository: MongoEngine/mongoengine

Issue:
Cannot query and filter fields of subclasses of embedded documents
Tested on 0.8.4.

``` python
from mongoengine import *

connect('test')

class BaseSettings(EmbeddedDocument):
    meta = {'allow_inheritance': True}

class AdminSettings(BaseSettings):
    foo = StringField()

class Person(Document):
    settings = EmbeddedDocumentField(BaseSettings)

Person.objects(settings__foo='bar') # raises 'Cannot resolve field "foo"'
Person.objects.only('settings.foo') # raises 'Cannot resolve field "foo"'
```



Discussion:
Will fix in 0.9

Any plan for 0.9 release?

`Person.objects(settings__foo='bar')` exception already solved with #744 

I can still reproduce it in 0.10.6.
#744 solves this problem for `Document` subclasses, not `EmbeddedDocument` subclasses.

Any news?
Any updates? I'm also running into this issue. 

Resolving patch:
diff --git a/mongoengine/fields.py b/mongoengine/fields.py
--- a/mongoengine/fields.py
+++ b/mongoengine/fields.py
@@ -700,7 +700,11 @@ def validate(self, value, clean=True):
         self.document_type.validate(value, clean)
 
     def lookup_member(self, member_name):
-        return self.document_type._fields.get(member_name)
+        doc_and_subclasses = [self.document_type] + self.document_type.__subclasses__()
+        for doc_type in doc_and_subclasses:
+            field = doc_type._fields.get(member_name)
+            if field:
+                return field
 
     def prepare_query_value(self, op, value):
         if value is not None and not isinstance(value, self.document_type):
@@ -747,12 +751,13 @@ def validate(self, value, clean=True):
         value.validate(clean=clean)
 
     def lookup_member(self, member_name):
-        if self.choices:
-            for choice in self.choices:
-                field = choice._fields.get(member_name)
+        document_choices = self.choices or []
+        for document_choice in document_choices:
+            doc_and_subclasses = [document_choice] + document_choice.__subclasses__()
+            for doc_type in doc_and_subclasses:
+                field = doc_type._fields.get(member_name)
                 if field:
                     return field
-        return None
 
     def to_mongo(self, document, use_db_field=True, fields=None):
         if document is None:


Test patch:
diff --git a/tests/fields/fields.py b/tests/fields/fields.py
--- a/tests/fields/fields.py
+++ b/tests/fields/fields.py
@@ -8,11 +8,10 @@
 
 from mongoengine import Document, StringField, IntField, DateTimeField, DateField, ValidationError, \
     ComplexDateTimeField, FloatField, ListField, ReferenceField, DictField, EmbeddedDocument, EmbeddedDocumentField, \
-    GenericReferenceField, DoesNotExist, NotRegistered, GenericEmbeddedDocumentField, OperationError, DynamicField, \
-    FieldDoesNotExist, EmbeddedDocumentListField, MultipleObjectsReturned, NotUniqueError, BooleanField, ObjectIdField, \
-    SortedListField, GenericLazyReferenceField, LazyReferenceField, DynamicDocument
-from mongoengine.base import (BaseField, EmbeddedDocumentList,
-                              _document_registry)
+    GenericReferenceField, DoesNotExist, NotRegistered, OperationError, DynamicField, \
+    FieldDoesNotExist, EmbeddedDocumentListField, MultipleObjectsReturned, NotUniqueError, BooleanField,\
+    ObjectIdField, SortedListField, GenericLazyReferenceField, LazyReferenceField, DynamicDocument
+from mongoengine.base import (BaseField, EmbeddedDocumentList, _document_registry)
 
 from tests.utils import MongoDBTestCase
 
@@ -1769,79 +1768,6 @@ class Shirt(Document):
         with self.assertRaises(ValidationError):
             shirt.validate()
 
-    def test_choices_validation_documents(self):
-        """
-        Ensure fields with document choices validate given a valid choice.
-        """
-        class UserComments(EmbeddedDocument):
-            author = StringField()
-            message = StringField()
-
-        class BlogPost(Document):
-            comments = ListField(
-                GenericEmbeddedDocumentField(choices=(UserComments,))
-            )
-
-        # Ensure Validation Passes
-        BlogPost(comments=[
-            UserComments(author='user2', message='message2'),
-        ]).save()
-
-    def test_choices_validation_documents_invalid(self):
-        """
-        Ensure fields with document choices validate given an invalid choice.
-        This should throw a ValidationError exception.
-        """
-        class UserComments(EmbeddedDocument):
-            author = StringField()
-            message = StringField()
-
-        class ModeratorComments(EmbeddedDocument):
-            author = StringField()
-            message = StringField()
-
-        class BlogPost(Document):
-            comments = ListField(
-                GenericEmbeddedDocumentField(choices=(UserComments,))
-            )
-
-        # Single Entry Failure
-        post = BlogPost(comments=[
-            ModeratorComments(author='mod1', message='message1'),
-        ])
-        self.assertRaises(ValidationError, post.save)
-
-        # Mixed Entry Failure
-        post = BlogPost(comments=[
-            ModeratorComments(author='mod1', message='message1'),
-            UserComments(author='user2', message='message2'),
-        ])
-        self.assertRaises(ValidationError, post.save)
-
-    def test_choices_validation_documents_inheritance(self):
-        """
-        Ensure fields with document choices validate given subclass of choice.
-        """
-        class Comments(EmbeddedDocument):
-            meta = {
-                'abstract': True
-            }
-            author = StringField()
-            message = StringField()
-
-        class UserComments(Comments):
-            pass
-
-        class BlogPost(Document):
-            comments = ListField(
-                GenericEmbeddedDocumentField(choices=(Comments,))
-            )
-
-        # Save Valid EmbeddedDocument Type
-        BlogPost(comments=[
-            UserComments(author='user2', message='message2'),
-        ]).save()
-
     def test_choices_get_field_display(self):
         """Test dynamic helper for returning the display value of a choices
         field.
@@ -1958,85 +1884,6 @@ class Shirt(Document):
             self.assertEqual(error_dict['size'], SIZE_MESSAGE)
             self.assertEqual(error_dict['color'], COLOR_MESSAGE)
 
-    def test_generic_embedded_document(self):
-        class Car(EmbeddedDocument):
-            name = StringField()
-
-        class Dish(EmbeddedDocument):
-            food = StringField(required=True)
-            number = IntField()
-
-        class Person(Document):
-            name = StringField()
-            like = GenericEmbeddedDocumentField()
-
-        Person.drop_collection()
-
-        person = Person(name='Test User')
-        person.like = Car(name='Fiat')
-        person.save()
-
-        person = Person.objects.first()
-        self.assertIsInstance(person.like, Car)
-
-        person.like = Dish(food="arroz", number=15)
-        person.save()
-
-        person = Person.objects.first()
-        self.assertIsInstance(person.like, Dish)
-
-    def test_generic_embedded_document_choices(self):
-        """Ensure you can limit GenericEmbeddedDocument choices."""
-        class Car(EmbeddedDocument):
-            name = StringField()
-
-        class Dish(EmbeddedDocument):
-            food = StringField(required=True)
-            number = IntField()
-
-        class Person(Document):
-            name = StringField()
-            like = GenericEmbeddedDocumentField(choices=(Dish,))
-
-        Person.drop_collection()
-
-        person = Person(name='Test User')
-        person.like = Car(name='Fiat')
-        self.assertRaises(ValidationError, person.validate)
-
-        person.like = Dish(food="arroz", number=15)
-        person.save()
-
-        person = Person.objects.first()
-        self.assertIsInstance(person.like, Dish)
-
-    def test_generic_list_embedded_document_choices(self):
-        """Ensure you can limit GenericEmbeddedDocument choices inside
-        a list field.
-        """
-        class Car(EmbeddedDocument):
-            name = StringField()
-
-        class Dish(EmbeddedDocument):
-            food = StringField(required=True)
-            number = IntField()
-
-        class Person(Document):
-            name = StringField()
-            likes = ListField(GenericEmbeddedDocumentField(choices=(Dish,)))
-
-        Person.drop_collection()
-
-        person = Person(name='Test User')
-        person.likes = [Car(name='Fiat')]
-        self.assertRaises(ValidationError, person.validate)
-
-        person.likes = [Dish(food="arroz", number=15)]
-        person.save()
-
-        person = Person.objects.first()
-        self.assertIsInstance(person.likes[0], Dish)
-
     def test_recursive_validation(self):
         """Ensure that a validation result to_dict is available."""
         class Author(EmbeddedDocument):
@@ -2702,44 +2549,5 @@ class CustomData(Document):
         self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a'])
 
 
-class TestEmbeddedDocumentField(MongoDBTestCase):
-    def test___init___(self):
-        class MyDoc(EmbeddedDocument):
-            name = StringField()
-
-        field = EmbeddedDocumentField(MyDoc)
-        self.assertEqual(field.document_type_obj, MyDoc)
-
-        field2 = EmbeddedDocumentField('MyDoc')
-        self.assertEqual(field2.document_type_obj, 'MyDoc')
-
-    def test___init___throw_error_if_document_type_is_not_EmbeddedDocument(self):
-        with self.assertRaises(ValidationError):
-            EmbeddedDocumentField(dict)
-
-    def test_document_type_throw_error_if_not_EmbeddedDocument_subclass(self):
-
-        class MyDoc(Document):
-            name = StringField()
-
-        emb = EmbeddedDocumentField('MyDoc')
-        with self.assertRaises(ValidationError) as ctx:
-            emb.document_type
-        self.assertIn('Invalid embedded document class provided to an EmbeddedDocumentField', str(ctx.exception))
-
-    def test_embedded_document_field_only_allow_subclasses_of_embedded_document(self):
-        # Relates to #1661
-        class MyDoc(Document):
-            name = StringField()
-
-        with self.assertRaises(ValidationError):
-            class MyFailingDoc(Document):
-                emb = EmbeddedDocumentField(MyDoc)
-
-        with self.assertRaises(ValidationError):
-            class MyFailingdoc2(Document):
-                emb = EmbeddedDocumentField('MyDoc')
-
-
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/fields/test_embedded_document_field.py b/tests/fields/test_embedded_document_field.py
new file mode 100644
--- /dev/null
+++ b/tests/fields/test_embedded_document_field.py
@@ -0,0 +1,344 @@
+# -*- coding: utf-8 -*-
+from mongoengine import Document, StringField, ValidationError, EmbeddedDocument, EmbeddedDocumentField, \
+    InvalidQueryError, LookUpError, IntField, GenericEmbeddedDocumentField, ListField, EmbeddedDocumentListField, \
+    ReferenceField
+
+from tests.utils import MongoDBTestCase
+
+
+class TestEmbeddedDocumentField(MongoDBTestCase):
+    def test___init___(self):
+        class MyDoc(EmbeddedDocument):
+            name = StringField()
+
+        field = EmbeddedDocumentField(MyDoc)
+        self.assertEqual(field.document_type_obj, MyDoc)
+
+        field2 = EmbeddedDocumentField('MyDoc')
+        self.assertEqual(field2.document_type_obj, 'MyDoc')
+
+    def test___init___throw_error_if_document_type_is_not_EmbeddedDocument(self):
+        with self.assertRaises(ValidationError):
+            EmbeddedDocumentField(dict)
+
+    def test_document_type_throw_error_if_not_EmbeddedDocument_subclass(self):
+
+        class MyDoc(Document):
+            name = StringField()
+
+        emb = EmbeddedDocumentField('MyDoc')
+        with self.assertRaises(ValidationError) as ctx:
+            emb.document_type
+        self.assertIn('Invalid embedded document class provided to an EmbeddedDocumentField', str(ctx.exception))
+
+    def test_embedded_document_field_only_allow_subclasses_of_embedded_document(self):
+        # Relates to #1661
+        class MyDoc(Document):
+            name = StringField()
+
+        with self.assertRaises(ValidationError):
+            class MyFailingDoc(Document):
+                emb = EmbeddedDocumentField(MyDoc)
+
+        with self.assertRaises(ValidationError):
+            class MyFailingdoc2(Document):
+                emb = EmbeddedDocumentField('MyDoc')
+
+    def test_query_embedded_document_attribute(self):
+        class AdminSettings(EmbeddedDocument):
+            foo1 = StringField()
+            foo2 = StringField()
+
+        class Person(Document):
+            settings = EmbeddedDocumentField(AdminSettings)
+            name = StringField()
+
+        Person.drop_collection()
+
+        p = Person(
+            settings=AdminSettings(foo1='bar1', foo2='bar2'),
+            name='John',
+        ).save()
+
+        # Test non exiting attribute
+        with self.assertRaises(InvalidQueryError) as ctx_err:
+            Person.objects(settings__notexist='bar').first()
+        self.assertEqual(unicode(ctx_err.exception), u'Cannot resolve field "notexist"')
+
+        with self.assertRaises(LookUpError):
+            Person.objects.only('settings.notexist')
+
+        # Test existing attribute
+        self.assertEqual(Person.objects(settings__foo1='bar1').first().id, p.id)
+        only_p = Person.objects.only('settings.foo1').first()
+        self.assertEqual(only_p.settings.foo1, p.settings.foo1)
+        self.assertIsNone(only_p.settings.foo2)
+        self.assertIsNone(only_p.name)
+
+        exclude_p = Person.objects.exclude('settings.foo1').first()
+        self.assertIsNone(exclude_p.settings.foo1)
+        self.assertEqual(exclude_p.settings.foo2, p.settings.foo2)
+        self.assertEqual(exclude_p.name, p.name)
+
+    def test_query_embedded_document_attribute_with_inheritance(self):
+        class BaseSettings(EmbeddedDocument):
+            meta = {'allow_inheritance': True}
+            base_foo = StringField()
+
+        class AdminSettings(BaseSettings):
+            sub_foo = StringField()
+
+        class Person(Document):
+            settings = EmbeddedDocumentField(BaseSettings)
+
+        Person.drop_collection()
+
+        p = Person(settings=AdminSettings(base_foo='basefoo', sub_foo='subfoo'))
+        p.save()
+
+        # Test non exiting attribute
+        with self.assertRaises(InvalidQueryError) as ctx_err:
+            self.assertEqual(Person.objects(settings__notexist='bar').first().id, p.id)
+        self.assertEqual(unicode(ctx_err.exception), u'Cannot resolve field "notexist"')
+
+        # Test existing attribute
+        self.assertEqual(Person.objects(settings__base_foo='basefoo').first().id, p.id)
+        self.assertEqual(Person.objects(settings__sub_foo='subfoo').first().id, p.id)
+
+        only_p = Person.objects.only('settings.base_foo', 'settings._cls').first()
+        self.assertEqual(only_p.settings.base_foo, 'basefoo')
+        self.assertIsNone(only_p.settings.sub_foo)
+
+    def test_query_list_embedded_document_with_inheritance(self):
+        class Post(EmbeddedDocument):
+            title = StringField(max_length=120, required=True)
+            meta = {'allow_inheritance': True}
+
+        class TextPost(Post):
+            content = StringField()
+
+        class MoviePost(Post):
+            author = StringField()
+
+        class Record(Document):
+            posts = ListField(EmbeddedDocumentField(Post))
+
+        record_movie = Record(posts=[MoviePost(author='John', title='foo')]).save()
+        record_text = Record(posts=[TextPost(content='a', title='foo')]).save()
+
+        records = list(Record.objects(posts__author=record_movie.posts[0].author))
+        self.assertEqual(len(records), 1)
+        self.assertEqual(records[0].id, record_movie.id)
+
+        records = list(Record.objects(posts__content=record_text.posts[0].content))
+        self.assertEqual(len(records), 1)
+        self.assertEqual(records[0].id, record_text.id)
+
+        self.assertEqual(Record.objects(posts__title='foo').count(), 2)
+
+
+class TestGenericEmbeddedDocumentField(MongoDBTestCase):
+
+    def test_generic_embedded_document(self):
+        class Car(EmbeddedDocument):
+            name = StringField()
+
+        class Dish(EmbeddedDocument):
+            food = StringField(required=True)
+            number = IntField()
+
+        class Person(Document):
+            name = StringField()
+            like = GenericEmbeddedDocumentField()
+
+        Person.drop_collection()
+
+        person = Person(name='Test User')
+        person.like = Car(name='Fiat')
+        person.save()
+
+        person = Person.objects.first()
+        self.assertIsInstance(person.like, Car)
+
+        person.like = Dish(food="arroz", number=15)
+        person.save()
+
+        person = Person.objects.first()
+        self.assertIsInstance(person.like, Dish)
+
+    def test_generic_embedded_document_choices(self):
+        """Ensure you can limit GenericEmbeddedDocument choices."""
+        class Car(EmbeddedDocument):
+            name = StringField()
+
+        class Dish(EmbeddedDocument):
+            food = StringField(required=True)
+            number = IntField()
+
+        class Person(Document):
+            name = StringField()
+            like = GenericEmbeddedDocumentField(choices=(Dish,))
+
+        Person.drop_collection()
+
+        person = Person(name='Test User')
+        person.like = Car(name='Fiat')
+        self.assertRaises(ValidationError, person.validate)
+
+        person.like = Dish(food="arroz", number=15)
+        person.save()
+
+        person = Person.objects.first()
+        self.assertIsInstance(person.like, Dish)
+
+    def test_generic_list_embedded_document_choices(self):
+        """Ensure you can limit GenericEmbeddedDocument choices inside
+        a list field.
+        """
+        class Car(EmbeddedDocument):
+            name = StringField()
+
+        class Dish(EmbeddedDocument):
+            food = StringField(required=True)
+            number = IntField()
+
+        class Person(Document):
+            name = StringField()
+            likes = ListField(GenericEmbeddedDocumentField(choices=(Dish,)))
+
+        Person.drop_collection()
+
+        person = Person(name='Test User')
+        person.likes = [Car(name='Fiat')]
+        self.assertRaises(ValidationError, person.validate)
+
+        person.likes = [Dish(food="arroz", number=15)]
+        person.save()
+
+        person = Person.objects.first()
+        self.assertIsInstance(person.likes[0], Dish)
+
+    def test_choices_validation_documents(self):
+        """
+        Ensure fields with document choices validate given a valid choice.
+        """
+        class UserComments(EmbeddedDocument):
+            author = StringField()
+            message = StringField()
+
+        class BlogPost(Document):
+            comments = ListField(
+                GenericEmbeddedDocumentField(choices=(UserComments,))
+            )
+
+        # Ensure Validation Passes
+        BlogPost(comments=[
+            UserComments(author='user2', message='message2'),
+        ]).save()
+
+    def test_choices_validation_documents_invalid(self):
+        """
+        Ensure fields with document choices validate given an invalid choice.
+        This should throw a ValidationError exception.
+        """
+        class UserComments(EmbeddedDocument):
+            author = StringField()
+            message = StringField()
+
+        class ModeratorComments(EmbeddedDocument):
+            author = StringField()
+            message = StringField()
+
+        class BlogPost(Document):
+            comments = ListField(
+                GenericEmbeddedDocumentField(choices=(UserComments,))
+            )
+
+        # Single Entry Failure
+        post = BlogPost(comments=[
+            ModeratorComments(author='mod1', message='message1'),
+        ])
+        self.assertRaises(ValidationError, post.save)
+
+        # Mixed Entry Failure
+        post = BlogPost(comments=[
+            ModeratorComments(author='mod1', message='message1'),
+            UserComments(author='user2', message='message2'),
+        ])
+        self.assertRaises(ValidationError, post.save)
+
+    def test_choices_validation_documents_inheritance(self):
+        """
+        Ensure fields with document choices validate given subclass of choice.
+        """
+        class Comments(EmbeddedDocument):
+            meta = {
+                'abstract': True
+            }
+            author = StringField()
+            message = StringField()
+
+        class UserComments(Comments):
+            pass
+
+        class BlogPost(Document):
+            comments = ListField(
+                GenericEmbeddedDocumentField(choices=(Comments,))
+            )
+
+        # Save Valid EmbeddedDocument Type
+        BlogPost(comments=[
+            UserComments(author='user2', message='message2'),
+        ]).save()
+
+    def test_query_generic_embedded_document_attribute(self):
+        class AdminSettings(EmbeddedDocument):
+            foo1 = StringField()
+
+        class NonAdminSettings(EmbeddedDocument):
+            foo2 = StringField()
+
+        class Person(Document):
+            settings = GenericEmbeddedDocumentField(choices=(AdminSettings, NonAdminSettings))
+
+        Person.drop_collection()
+
+        p1 = Person(settings=AdminSettings(foo1='bar1')).save()
+        p2 = Person(settings=NonAdminSettings(foo2='bar2')).save()
+
+        # Test non exiting attribute
+        with self.assertRaises(InvalidQueryError) as ctx_err:
+            Person.objects(settings__notexist='bar').first()
+        self.assertEqual(unicode(ctx_err.exception), u'Cannot resolve field "notexist"')
+
+        with self.assertRaises(LookUpError):
+            Person.objects.only('settings.notexist')
+
+        # Test existing attribute
+        self.assertEqual(Person.objects(settings__foo1='bar1').first().id, p1.id)
+        self.assertEqual(Person.objects(settings__foo2='bar2').first().id, p2.id)
+
+    def test_query_generic_embedded_document_attribute_with_inheritance(self):
+        class BaseSettings(EmbeddedDocument):
+            meta = {'allow_inheritance': True}
+            base_foo = StringField()
+
+        class AdminSettings(BaseSettings):
+            sub_foo = StringField()
+
+        class Person(Document):
+            settings = GenericEmbeddedDocumentField(choices=[BaseSettings])
+
+        Person.drop_collection()
+
+        p = Person(settings=AdminSettings(base_foo='basefoo', sub_foo='subfoo'))
+        p.save()
+
+        # Test non exiting attribute
+        with self.assertRaises(InvalidQueryError) as ctx_err:
+            self.assertEqual(Person.objects(settings__notexist='bar').first().id, p.id)
+        self.assertEqual(unicode(ctx_err.exception), u'Cannot resolve field "notexist"')
+
+        # Test existing attribute
+        self.assertEqual(Person.objects(settings__base_foo='basefoo').first().id, p.id)
+        self.assertEqual(Person.objects(settings__sub_foo='subfoo').first().id, p.id)