Сделайте "django-filter" динамическим

У меня есть django-фильтр, который работает для одной категории, и я пытаюсь сделать его динамическим, чтобы он работал для всех категорий сайта электронной коммерции.

Вот модель:

    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

Вот версия, которая работает:

    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)

Вот что я попытался сделать динамическим:

    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))

Проблема в том, что я не уверен, к какому методу django-filter подключиться, как вы можете видеть cls.get_declared_filters['min_price'], я перепробовал много подобных методов.

Итак, я пытаюсь добавить дополнительные поля в класс ListingFilter.

Для тех, кто столкнулся с этой проблемой, вот мое решение:

    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'))

Вот форма кверисета sub_fields:

    {
        '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']
        ]
    }

Я буду писать о решении на Medium, ловите его там.

Вернуться на верх