How to Order Queryset by EAV Attribute in Django Using django-filters?

I'm working on a Django project where I'm using an EAV (Entity-Attribute-Value) model to store dynamic attributes for my Product model. I need to allow users to filter and order products by price, which is stored as an EAV attribute (slug='price').

My Setup: I’m using django-filters for filtering. Price is stored in the Value model (part of Django EAV). I'm using a Subquery annotation to retrieve the price from Value. Ordering by price is not working correctly.

    class BaseInFilterWidget(django_filters.widgets.SuffixedMultiWidget):
    suffixes = ['choice']
    
    def __init__(self, *args, **kwargs):
        widgets = [forms.SelectMultiple,]
        super().__init__(widgets, *args, **kwargs)

    def value_from_datadict(self, data, files, name):
        """ Override this method to properly extract the value from the request. """
        # Since the key ends with '_choices', we need to handle it here.
        name_choices = f"{name}_choice"
        return data.getlist(name_choices)

class ProductFilter(django_filters.FilterSet):

    class Meta:
        model=product.Product
        fields=[]

    def __init__(self, *args, **kwargs):
        super(ProductFilter, self).__init__(*args, **kwargs)

        multiple_choice = False

        for slug in self.data:
            if slug in ['category', 'location', 'order_by']:
                if slug == 'category':
                    self.filters[slug] = django_filters.ModelChoiceFilter(
                        field_name='category',
                        queryset=Category.objects.all(),
                        label='Category',
                        to_field_name='slug'
                    )
                elif slug == 'location':
                    self.filters[slug] = django_filters.ModelChoiceFilter(
                        field_name='location',
                        queryset=Location.objects.all(),
                        label='Location',
                        to_field_name='slug'
                    )
                elif slug == 'order_by':
                        self.filters[slug] = django_filters.OrderingFilter(
                            fields=(
                                ("created", "created"),
                                ("price", "price")
                            ),
                            field_labels={
                                'price': 'price Added',
                            },
                            label='Sort by'
                    )
                continue
                

            if slug.endswith('_choice'):
                multiple_choice = True
                slug = slug[:-7]
            elif slug.endswith('_max') or slug.endswith('_min'):
                slug = slug[:-4]
            field_name = f"eav__{slug}"
            if hasattr(product.Product().eav, slug):
                if multiple_choice:
                    self.filters[slug] = django_filters.BaseInFilter(
                                field_name=field_name,
                                lookup_expr='in',
                                widget=BaseInFilterWidget
                            )
                else:
                    self.filters[slug] = django_filters.RangeFilter(field_name=field_name)

    def filter_queryset(self, queryset):
        # If ordering by price is requested, annotate the queryset with a 'price' field.
        order_by_value = self.data.get('order_by', '')
        order_by_fields = order_by_value.split(',') if order_by_value else []
        if 'price' in order_by_fields or '-price' in order_by_fields:
            price_subquery = Value.objects.filter(
                product=OuterRef('pk'),
                attribute__slug='price'
            ).values('value_float')[:1]
            queryset = queryset.annotate(
                price=Subquery(price_subquery, output_field=FloatField())
            )
        return super(ProductFilter, self).filter_queryset(queryset)
    
    @property
    def qs(self):
        parent = super(ProductFilter, self).qs

        try:
            return parent
        except TypeError as e:
            print("hello")

I need to do order_by price

There are few fixes are required in your code bcz

  1. the Value model stores attributes in value_float,value_int,value_text, etc so please ensure that price is store under value_float, otherwise annotation might not work as expected.

  2. in the Subquery annotation make sure price is correctly annotated before ordering

  3. in OrderingFilter ensure the price is applied before queryset is ordered.


class BaseInFilterWidget(django_filters.widgets.SuffixedMultiWidget):
    suffixes = ['choice']

    def __init__(self, *args, **kwargs):
        widgets = [forms.SelectMultiple]
        super().__init__(widgets, *args, **kwargs)

    def value_from_datadict(self, data, files, name):
        """ Extract the correct value from the request data. """
        name_choices = f"{name}_choice"
        return data.getlist(name_choices)


class ProductFilter(django_filters.FilterSet):

    class Meta:
        model = Product  # change from product.Product
        fields = []

    def __init__(self, *args, **kwargs):
        super(ProductFilter, self).__init__(*args, **kwargs)

        multiple_choice = False

        for slug in self.data:
            if slug in ['category', 'location', 'order_by']:
                if slug == 'category':
                    self.filters[slug] = django_filters.ModelChoiceFilter(
                        field_name='category',
                        queryset=Category.objects.all(),
                        label='Category',
                        to_field_name='slug'
                    )
                elif slug == 'location':
                    self.filters[slug] = django_filters.ModelChoiceFilter(
                        field_name='location',
                        queryset=Location.objects.all(),
                        label='Location',
                        to_field_name='slug'
                    )
                elif slug == 'order_by':
                    self.filters[slug] = django_filters.OrderingFilter(
                        fields=(
                            ("created", "created"),
                            ("price", "price"),
                        ),
                        field_labels={
                            'price': 'Price',   # changed from Price Added
                        },
                        label='Sort by'
                    )
                continue

            if slug.endswith('_choice'):
                multiple_choice = True
                slug = slug[:-7]
            elif slug.endswith('_max') or slug.endswith('_min'):
                slug = slug[:-4]

            field_name = f"eav__{slug}"
            if hasattr(Product().eav, slug):  # now here again direct Product is used
                if multiple_choice:
                    self.filters[slug] = django_filters.BaseInFilter(
                        field_name=field_name,
                        lookup_expr='in',
                        widget=BaseInFilterWidget
                    )
                else:
                    self.filters[slug] = django_filters.RangeFilter(field_name=field_name)

    def filter_queryset(self, queryset):
              # anotate the queryset with the price value before the  ordering
        price_subquery = Value.objects.filter(
            entity_id=OuterRef('pk'),      # ensure this references the correct product field
            attribute__slug='price'
        ).values('value_float')[:1]

        queryset = queryset.annotate(
            price=Subquery(price_subquery, output_field=FloatField())
        )

        order_by_value = self.data.get('order_by', '')
        if order_by_value:
            order_by_fields = order_by_value.split(',')
            queryset = queryset.order_by(*order_by_fields)

        return super(ProductFilter, self).filter_queryset(queryset)

   @property
    def qs(self):
        parent = super(ProductFilter, self).qs

        try:
            return parent
        except TypeError as e:
            print("hello")
Back to Top