carltongibson / django-filter

A generic system for filtering Django QuerySets based on user selections
https://django-filter.readthedocs.io/en/main/
Other
4.39k stars 760 forks source link

Filter repeated key parameters as NumberField #1626

Closed Enorio closed 8 months ago

Enorio commented 8 months ago

Given the following Model and Filter:


class Bar(models.Model):
    foo = models.ForeignKey(...)

class BarFilter(FilterSet):
    class Meta:
        model = Bar
        fields = ['foo']
        filter_overrides = {
            models.ForeignKey: {
                'filter_class': filters.NumberFilter
            }
        }

I need to filter Bar given multiple Foo foreign key ids. For example GET .../bar/?foo=1&foo=2&foo=3

From what I tested, the endpoint will only filter foo=3, I'm guessing always the last value. I've done some debug in the code, and there is a step somewhere that converts the list received in only one value.

I've tried adding a 'lookup_expr': 'in' as an extra, but raises an exception saying 'decimal.Decimal' object is not iterable I also used the ModelMultipleChoiceFilter, but I don't want to return an error saying that some id doesn't exist.

Basically, what I want is given the endpoint, I can filter Foreign Keys from their id, and if some value doesn't exist, ignores it. For example:


foo_a = Foo(id=1)
foo_b = Foo(id=2)
foo_c = Foo(id=3)

bar_a = Bar(foo=foo_a)
bar_b = Bar(foo=foo_b)
bar_c = Bar(foo=foo_c)
GET .../bar/ = [bar_a, bar_b, bar_c]

GET .../bar/?foo=1&foo=2 = [bar_a, bar_b]

GET .../bar/?foo=4 = []

I'm using django-filter 21.1 and django-rest-framework, if it helps.

carltongibson commented 8 months ago

I also used the ModelMultipleChoiceFilter, but I don't want to return an error saying that some id doesn't exist.

OK, so then you'll want to define a widget that returns a list of items from the request.GET QueryDict:

class ManyWiget(Widget):
    def value_from_datadict(self, data, files, name):
        return data.getlist(name)

Then you'll want a Form field that uses that widget, again expecting a list.

class ManyIntField(Field):
    widget = ManyWidget 

    def to_python(self, value):
        if not value:
            return []
        elif not isinstance(value, (list, tuple)):
            raise ValidationError(
                self.error_messages["invalid_list"], code="invalid_list"
            )
        return [int(val) for val in value]

You'll need to think about validation there; you don't want to accept just any data.

Then you'll need a filter to use the field:

class ManyIntFilter(Filter):
    field = ManyIntField
    lookup_expr = "in"

But with that you'll be able to filter on many items, without erroring just because an ID doesn't exist.