Search code examples
mongodbdjango-rest-frameworkmongoengine

how to use django rest filtering with mongoengine for ListField


I am trying to filter queryset for Many To Many Relationship. But its not working. I am trying to get objects of TestTag document with keyword name.

models :


class TestKeyword(Document):
    name = StringField(required=True)


class TestTag(Document):
    tag = StringField(max_length=100, null=True)  
    keywords = ListField(ReferenceField(TestKeyword), null=True)

Filter:

import django_mongoengine_filter as filters
from app.models import TestTag

class TestTagFilter(filters.FilterSet):
    class Meta:
        model = TestTag
        fields = ['tag', 'keywords__name']

class TestTag(ModelViewSet):
    queryset = TestTag.objects.all()
    serializer_class = TestTagSerializer
    # override filter_queryset function
    def filter_queryset(self, queryset):
        filter = TestTagFilter(self.request.query_params, queryset=queryset)
        return filter.qs

Solution

  • A general solution that worked for me is described bellow:

    First make sure you have the following packages in your environment:

    Django
    djangorestframework
    django-rest-framework-mongoengine
    mongoengine
    django-filter
    # patched version of django-mongoengine-filter to support Django 4.0
    # https://github.com/oussjarrousse/django-mongoengine-filter 
    # Pull request https://github.com/barseghyanartur/django-mongoengine-filter/pull/16 or download the original if you are using Django 3.x
    django-mongoengine-filter
    

    The idea in this answer is to add filtering support to django-rest-framework-mongoengine using django-mongoengine-filter that is an replacement or an extension to django-filter and should work the same way as django-filter.

    First let's edit the project/settings.py file. Find the INSTALLED_APPS variable and make sure the following "Django apps" are added:

    # in settings.py:
    
    INSTALLED_APPS = [
        # ...,
        "rest_framework",
        "rest_framework_mongoengine",
        "django_filters",
        # ...,
    ]
    

    the app django_filters is required to add classes related to filtering infrastructure, and other things including html templates for DRF.

    Then in the variable REST_FRAMEWORK we need to edit the values associated with the key: DEFAULT_FILTER_BACKENDS

    # in settings.py:
    
    REST_FRAMEWORK = {
        # ...
        "DEFAULT_FILTER_BACKENDS": [
            "filters.DjangoMongoEngineFilterBackend",
            # ...
        ],
        # ...
    }
    

    DjangoMongoEngineFilterBackend is a custom built filter backend that we need to add to the folder (depending on how you structure your project) in the file filters

    # in filters.py:
    from django_filters.rest_framework.backends import DjangoFilterBackend
    
    class DjangoMongoEngineFilterBackend(DjangoFilterBackend):
        # filterset_base = django_mongoengine_filter.FilterSet
        """
        Patching the DjangoFilterBackend to allow for MongoEngine support
        """
    
        def get_filterset_class(self, view, queryset=None):
            """
            Return the `FilterSet` class used to filter the queryset.
            """
            filterset_class = getattr(view, "filterset_class", None)
            filterset_fields = getattr(view, "filterset_fields", None)
    
            if filterset_class:
                filterset_model = filterset_class._meta.model
    
                # FilterSets do not need to specify a Meta class
                if filterset_model and queryset is not None:
                    element = queryset.first()
                    if element:
                        queryset_model = element.__class__
                        assert issubclass(
                            queryset_model, filterset_model
                        ), "FilterSet model %s does not match queryset model %s" % (
                            filterset_model,
                            str(queryset_model),
                        )
    
                return filterset_class
    
            if filterset_fields and queryset is not None:
                MetaBase = getattr(self.filterset_base, "Meta", object)
    
                element = queryset.first()
                if element:
                    queryset_model = element.__class__
                    class AutoFilterSet(self.filterset_base):
                        class Meta(MetaBase):
                            model = queryset_model
                            fields = filterset_fields
    
                return AutoFilterSet
    
            return None
    

    This custom filter backend will not raise the exceptions that the original django-filter filter backend would raise. The django-filter DjangoFilterBackend access the key model in QuerySet as in queryset.model, however that key does not exist in MongoEngine.

    Maybe making it available in MongoEngine should be considered: https://github.com/MongoEngine/mongoengine/issues/2707 https://github.com/umutbozkurt/django-rest-framework-mongoengine/issues/294

    Now we can add a custom filter to the ViewSet:

    # in views.py
    from rest_framework_mongoengine.viewsets import ModelViewSet
    
    class MyModelViewSet(ModelViewSet):
        serializer_class = MyModelSerializer
        filter_fields = ["a_string_field", "a_boolean_field"]
        filterset_class = MyModelFilter
    
        def get_queryset(self):
            queryset = MyModel.objects.all()
            return queryset
    

    Finally let's get back to filters.py and add the MyModelFilter

    # in filters.py
    from django_mongoengine_filter import FilterSet, StringField, BooleanField
    class MyModelFilter(FilterSet):
        """
        MyModelFilter is a FilterSet that is designed to work with the django-filter.
        However the original django-mongoengine-filter is outdated and is causing some troubles
        with Django>=4.0.
        """
    
        class Meta:
            model = MyModel
            fields = [
                "a_string_field",
                "a_boolean_field",
            ]
    
        a_string_field = StringFilter()
        a_boolean_field = BooleanFilter()
    

    That should do the trick.