Make a "django-filter" dynamic

I have django-filter that works for one category and I am trying to make it dynamic for it to work for all categories of an eCommerce website.

Here is the model:

    class Listing(models.Model):
        sub_category = models.ForeignKey(SubCategory, on_delete=models.SET_NULL, related_name="sub_category", blank=False, null=True)
        is_active = models.BooleanField(default=True, null=True, blank=True)
        facilities = models.JSONField(default=dict, null=True, blank=True)
        nearby = models.JSONField(default=dict, null=True, blank=True)
        details = models.JSONField(default=dict, null=True, blank=True)

        # ... OTHER FIELDS

Here is the version that works:

    class ListingFilter(django_filters.FilterSet):

        class Meta:
            model = Listing 
            fields = {
                'sub_category__sub_category_name': ['contains'],
                'is_active': ['exact'],
            }

        country = django_filters.CharFilter(field_name="details__country", lookup_expr="icontains")
        min_price = django_filters.NumberFilter(
            method=lambda queryset, _, value: queryset.filter(details__price__gte=float(value))
        )
        max_price = django_filters.NumberFilter(
            method=lambda queryset, _, value: queryset.filter(details__price__lte=float(value))
        )
        kindergarten = django_filters.BooleanFilter(field_name="nearby__kindergarten", lookup_expr="exact")

           # ...OVER 40 OTHER FIELDS


    class ListingNode(DjangoObjectType):
        class Meta:
            model = Listing
            interfaces = (graphene.relay.Node, )


    class Query(graphene.ObjectType):
        one_listing = graphene.relay.Node.Field(ListingNode)
        all_listingss = DjangoFilterConnectionField(ListingNode, filterset_class=ListingFilter)

Here is what I have attempted to make it dynamic:

    class ListingFilter(django_filters.FilterSet):

        def __init_subclass__(cls, **kwargs):
            super().__init_subclass__(**kwargs)
            for field in Listing._meta.get_fields():
                field_name = (field.__str__().split('.'))[-1]
                if field_name == 'details':
                    cls.get_declared_filters['min_price'] = \
                        django_filters.NumberFilter(
                            field_name='details__price', 
                            lookup_expr='gte',
                            method='details_filter'
                    )

        class Meta:
            model = Listing
            fields = {
                'sub_category__sub_category_name': ['contains'],
                'is_active': ['exact'],
            }

        def details_filter(self, queryset, name, value):
            return queryset.filter(details__price__gte=float(value))

The problem is I'm not sure which django-filter method to hook into, as you can see cls.get_declared_filters['min_price'], I tried many methods like that.

So, what I'm trying to do is add additional fields to the ListingFilter class.

For anyone else who may run into this problem, here's my solution:

    def FilterFactory( model_class, class_name):
        sub_fields = SubCategory.objects.filter(category__category_name="real_estate").values('details', 'facilities', 'nearby')[0]
        class_vars = {
            'min_price': django_filters.NumberFilter(
                method=lambda queryset, _, value: queryset.filter(details__price__gte=float(value))
            )
        }
        
        class_vars['max_price'] = django_filters.NumberFilter(
            method=lambda queryset, _, value: queryset.filter(details__price__lte=float(value))
        )
        for key in list(sub_fields.keys()):
            for item in sub_fields[key]:
                if item[1] == 'string':
                    class_vars[item[0]] = django_filters.CharFilter(
                        field_name=f'{key}__{item[0]}', 
                        lookup_expr=item[2],
                    )
                    
                elif item[1] == 'number':
                    model_field = f'{key}__{item[0]}__{item[2]}'
                    class_vars[item[0]] = django_filters.NumberFilter(
                        field_name=f'{key}__{item[0]}', 
                        method=lambda queryset, _, value: queryset.filter(Q((model_field, float(value))))
                    )

                elif item[1] == 'bool':
                    model_field = f'{key}__{item[0]}__{item[2]}'
                    class_vars[item[0]] = django_filters.BooleanFilter(
                        field_name=f'{key}__{item[0]}', 
                        lookup_expr=item[2],
                    )
                    

        class_vars['Meta'] = type( 'Meta', (object, ), {
        "model": model_class,
        "fields": {
                    'sub_category__sub_category_name': ['contains'],
                    'is_active': ['exact'],
                }
            }
        )
        

        return type( class_name, (django_filters.FilterSet, ), class_vars )


    class ListingNode(DjangoObjectType):
        class Meta:
            model = Listing
            interfaces = (graphene.relay.Node, )


    class Query(graphene.ObjectType):
        one_listing = graphene.relay.Node.Field(ListingNode)
        all_listingss = DjangoFilterConnectionField(ListingNode, filterset_class=FilterFactory(  Listing, 'ListingFilter'))

Here's the shape of the sub_fields queryset:

    {
        'details': [
            ['bedrooms', 'number', 'contains'], ['building_size', 'number', 'gte'], 
            ['built_year', 'number', 'gte'], ['city', 'string', 'contains'], 
            ['country', 'string', 'contains'], ['floors', 'number', 'gte'], 
            ['max_price', 'number', 'lte'], ['plot_size', 'number', 'gte'], 
            ['min_price', 'number', 'gte'], ['renovated_year', 'number', 'gte'], 
            ['state', 'string', 'contains'], ['street_address', 'string', 'contains'], 
            ['title', 'string', 'contains'], ['zip', 'string', 'contains']
        ], 
        'facilities': [
            ['air_condition', 'bool', 'exact'], ['car_wash_area', 'bool', 'exact'], 
            ['cats_allowed', 'bool', 'exact'], ['child_friendly', 'bool', 'exact'], 
            ['dog_keepers', 'bool', 'exact'], ['dogs_allowed', 'bool', 'exact'], 
            ['electricity', 'bool', 'exact'], ['fireplace', 'bool', 'exact'], 
            ['garage', 'bool', 'exact'], ['gym_room', 'bool', 'exact'], 
            ['hot_tub', 'bool', 'exact'], ['internet', 'bool', 'exact'], 
            ['maintenance_24_hrs', 'bool', 'exact'], ['media_room', 'bool', 'exact'], 
            ['microwave', 'bool', 'exact'], ['oven', 'bool', 'exact'], ['pool', 'bool', 'exact'], 
            ['pool_table', 'bool', 'exact'], ['refrigerator', 'bool', 'exact'], ['water', 'bool', 'exact']
        ], 
        'nearby': [
            ['beach', 'bool', 'exact'], ['car_wash', 'bool', 'exact'], ['church', 'bool', 'exact'], 
            ['cinema', 'bool', 'exact'], ['dog_park', 'bool', 'exact'], ['grocery_store', 'bool', 'exact'], 
            ['gym', 'bool', 'exact'], ['high_school', 'bool', 'exact'], ['hiking', 'bool', 'exact'], 
            ['kindergarten', 'bool', 'exact'], ['library', 'bool', 'exact'], ['mosque', 'bool', 'exact'], 
            ['night_club', 'bool', 'exact'], ['primary_school', 'bool', 'exact'], 
            ['secondary_school', 'bool', 'exact'], ['shopping_center', 'bool', 'exact'], 
            ['synagogue', 'bool', 'exact'], ['theater', 'bool', 'exact'], ['university', 'bool', 'exact'], 
            ['user_fields', 'bool', 'exact']
        ]
    }

Will be writing about the solution on Medium, catch it there.

Back to Top