Django-filter with DRF - How to do 'and' when applying multiple values with the same lookup?

后端 未结 2 1901
春和景丽
春和景丽 2021-02-04 18:16

This is a slightly simplified example of the filterset I\'m using, which I\'m using with the DjangoFilterBackend for Django Rest Framework. I\'d like to be able to send a reques

相关标签:
2条回答
  • 2021-02-04 18:51

    You can create custom list field something like this:

    from django.forms.widgets import SelectMultiple
    from django import forms
    
    class ListField(forms.Field):
        widget = SelectMultiple
    
        def __init__(self, field, *args, **kwargs):
            super(ListField, self).__init__( *args, **kwargs)
            self.field = field
    
        def validate(self, value):
            super(ListField, self).validate(value)
            for val in value:
                self.field.validate(val)
    
        def run_validators(self, value):
            for val in value:
                self.field.run_validators(val)
    
        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 [self.field.to_python(val) for val in value]
    

    and create custom filter using MultipleChoiceFilter:

    class ContainsListFilter(django_filters.MultipleChoiceFilter):
        field_class = ListField
    
        def get_filter_predicate(self, v):
            name = '%s__contains' % self.name
            try:
                return {name: getattr(v, self.field.to_field_name)}
            except (AttributeError, TypeError):
                return {name: v}
    

    After that you can create FilterSet with your custom filter:

    from django.forms import CharField
    
    class StorageLocationFilter(django_filters.FilterSet):
        title_contains = ContainsListFilter(field=CharField())
    

    Working for me. Hope it will be useful for you.

    0 讨论(0)
  • 2021-02-04 18:58

    The main problem is that you need a filter that understands how to operate on multiple values. There are basically two options:

    • Use MultipleChoiceFilter (not recommended for this instance)
    • Write a custom filter class

    Using MultipleChoiceFilter

    class BookmarkFilter(django_filters.FilterSet):
        title__contains = django_filters.MultipleChoiceFilter(
            name='title',
            lookup_expr='contains',
            conjoined=True,  # uses AND instead of OR
            choices=[???],
        )
    
        class Meta:
            ...
    

    While this retains your desired syntax, the problem is that you have to construct a list of choices. I'm not sure if you can simplify/reduce the possible choices, but off the cuff it seems like you would need to fetch all titles from the database, split the titles into distinct words, then create a set to remove duplicates. This seems like it would be expensive/slow depending on how many records you have.

    Custom Filter

    Alternatively, you can create a custom filter class - something like the following:

    class MultiValueCharFilter(filters.BaseCSVFilter, filters.CharFilter):
        def filter(self, qs, value):
            # value is either a list or an 'empty' value
            values = value or []
    
            for value in values:
                qs = super(MultiValueCharFilter, self).filter(qs, value)
    
            return qs
    
    
    class BookmarkFilter(django_filters.FilterSet):
        title__contains = MultiValueCharFilter(name='title', lookup_expr='contains')
    
        class Meta:
            ...
    

    Usage (notice that the values are comma-separated):

    GET /api/bookmarks/?title__contains=word1,word2
    

    Result:

    qs.filter(title__contains='word1').filter(title__contains='word2')
    

    The syntax is changed a bit, but the CSV-based filter doesn't need to construct an unnecessary set of choices.

    Note that it isn't really possible to support the ?title__contains=word1&title__contains=word2 syntax as the widget can't render a suitable html input. You would either need to use SelectMultiple (which again, requires choices), or use javascript on the client to add/remove additional text inputs with the same name attribute.


    Without going into too much detail, filters and filtersets are just an extension of Django's forms.

    • A Filter has a form Field, which in turn has a Widget.
    • A FilterSet is composed of Filters.
    • A FilterSet generates an inner form based on its filters' fields.

    Responsibilities of each filter component:

    • The widget retrieves the raw value from the data QueryDict.
    • The field validates the raw value.
    • The filter constructs the filter() call to the queryset, using the validated value.

    In order to apply multiple values for the same filter, you would need a filter, field, and widget that understand how to operate on multiple values.


    The custom filter achieves this by mixing in BaseCSVFilter, which in turn mixes in a "comma-separation => list" functionality into the composed field and widget classes.

    I'd recommend looking at the source code for the CSV mixins, but in short:

    • The widget splits the incoming value into a list of values.
    • The field validates the entire list of values by validating individual values on the 'main' field class (such as CharField or IntegerField). The field also derives the mixed in widget.
    • The filter simply derives the mixed in field class.

    The CSV filter was intended to be used with in and range lookups, which accept a list of values. In this case, contains expects a single value. The filter() method fixes this by iterating over the values and chaining together individual filter calls.

    0 讨论(0)
提交回复
热议问题