rpkilby / django-filter

A generic system for filtering Django QuerySets based on user selections
https://django-filter.readthedocs.org
Other
0 stars 1 forks source link

Multi-param validation & filtering #12

Closed rpkilby closed 4 years ago

rpkilby commented 5 years ago

@carltongibson - just jotting down some preliminary thoughts. Do you have any feedback?


Context:

Users periodically request how to validate and filter against multiple params. The goto advice has been to implement something based on MultiWidget, however there are three main deficiencies in my mind:

Proposal:

Add new API surface around multi-param filtering. This consists of two things:

In practice this would look like:

class MyFilter(filters.FilterSet):
    field_a = filters.CharFilter(...)
    field_b = filters.CharFilter(...)

    class Meta:
        model = MyModel
        multi = {
            'group_a': ['field_a', 'field_b'],
        }

    def validate_multi_group_a(self, form, *, field_a, field_b):
        if not some_condition(field_a, field_b):
            form.add_error('field_a', '...')
            form.add_error('field_b', '...')

            # Note: not the following, since it halts validation
            raise ValidationError({'field_a': ['...'], 'field_b': ['...']})

    def filter_multi_group_a(self, qs, *, field_a, field_b):
        return qs.do_a_thing(field_a=field_a, field_b=field_b)

The above hooks are purely opt-in.

Notes:

carltongibson commented 5 years ago

OK. Nice.

Can I ask, How would it look if we had a GroupFilter class that took Filters and allowed implementing filter() plus validate()?

I.e. just pulling what you have here into a composed class, rather than having the API directly on FilterSet.

rpkilby commented 5 years ago

That was actually my first thought - leverage the declarative filters. And I've kind of gone back and forth on both implementations after writing several lengthy paragraphs on the matter. Changed my opinion mid-response.. 😅

At this point, I see two proposals:

  1. The one above.
  2. The below:

    # Note that FilterGroup is not a Filter subclass
    class MyFilterGroup(FilterGroup):
        def filter(self, qs, **params):
            ...
        def validate(self, form, **params):
            ...
    
    class MyFilter(FilterSet):
        field_a = CharFilter(...)
        field_b = CharFilter(...)
        group = MyFilterGroup(
            filters=['field_a', 'field_b'],
        )

At this point, I'm leaning towards option 2. Option 1 is still attractive in that the code is simple - it's not necessary to write custom classes. That said, there isn't any abstraction for reusability. We could write helper functions, but that's about it. Option 2 would allow us/users to write reusable FilterGroups. Right now, the only thing I don't like about the declarative approach is that they don't make sense as a declared attribute on the FilterSet. With Filters, the attribute name is the exposed query param. With FilterGroups, the attribute name is functionless. e.g., a user wouldn't ever include ?group=foo in their query string.

Maybe a third option is to mix the styles? Use FilterGroups in the meta options? e.g.,

# Note that FilterGroup is not a Filter subclass
class MyFilter(FilterSet):
    field_a = CharFilter(...)
    field_b = CharFilter(...)
    field_c = CharFilter(...)
    field_d = CharFilter(...)

    class Meta:
        model = MyModel
        fields = ['field_a', 'field_b', 'field_c', 'field_d']
        groups = [
            ExclusiveGroup(filters=['field_a', 'field_b']),
            RequiredGroup(filters=['field_c', 'field_d']),
        ]

How does that last option sound?

rpkilby commented 5 years ago

@carltongibson - I'm going to go ahead and WIP up that last example.

carltongibson commented 5 years ago

OK. I haven't come to a conclusion. (🎯: "Nice API") 👍

rpkilby commented 5 years ago

btw, I'm still working on this. I've just caught up on some unrelated Circle CI work, which is now finished. I hope to have this ready either this weekend or the next.

rpkilby commented 5 years ago

ach, I'm eventually going to finish this up. It's just taken a back seat to some work on DRF and djangorestframework-filters (still trying to get 1.0 out the door... it's only been 3+ years in the making!). Also, the PR should maybe address iss 1020.

rpkilby commented 4 years ago

Took me a year, but it's just about ready lol.