-
Notifications
You must be signed in to change notification settings - Fork 770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add group validation & filtering #1167
base: main
Are you sure you want to change the base?
Conversation
This should improve inspectability, since these attributes are only set during FilterSet class creation.
bf3689f
to
fb54361
Compare
Hi @carltongibson. What do you want to do about the Django 1.11 failures? Q objects were made comparable in django/django#7517, however this change wasn't added until Django 2.0. I could either rework the failing tests, or we could drop support for Django 1.11 per the official recommendations. |
I think this is a much better approach. It looks like this would allow a single filter to be in multiple groups. Is that right? One concern I have is how you handle excludes in the
Excludes on related objects are weird. |
Yes, but it's complicated. Validation should work, but filtering depends on the setup. First, note the changes to MyModel.objects.filter(Q(field_a=1) | Q(field_b=2)) Without extraction though, you would end up with something like: MyModel.objects \
.filter(Q(field_a=1) | Q(field_b=2)) \
.filter(field_a=1) \
.filter(field_b=2) With a filter being shared across multiple groups, what would happen is that the filter would be extracted for the first group, but not the second. Some groups (like Long story short, filtering is going to be dependent on the implementations of the various groups. I don't think there is any rule that can be applied generally. Groups will just have to assert their expectations (e.g., as a mutually required group, I expect to filter on all my parameters) and provide as much helpful information as possible (maybe one of my params was preempted by another group).
I don't know if there's anything we can do here. That ORM call is not necessarily invalid, and exclusion across relationships generally requires consideration on the part of the developer. The
But maybe we could more specifically call out exclusion across relationships.
Oh yes. I also ran into this with DRF filters. See philipn/django-rest-framework-filters#217 |
Hmm. I don't like having the order of the group definitions matter for duplicate field use. Is there a downside to allowing a single field to be in multiple groups? The data extraction be rewritten as:
Also, do you know if there's an in depth explanation of expected exclude behaviour? Now I'm wondering if my second example would be considered a bug in django. |
I don't think the queries will generally make sense. For example, what would it mean for a filter to be a member of an class MyFilter(FilterSet):
class Meta:
model = MyModel
fields = ['field_a', 'field_b', 'field_c']
groups = [
ExclusiveGroup(filters=['field_a', 'field_b']),
CombinedGroup(filters=['field_b', 'field_c'], combine=operator.or_),
]
MyModel.objects.filter(field_b='b').filter(Q(field_b='b') | Q(field_c='c')) The above negates the combined query, and is somewhat pointless. Another example, take two exclusive groups (e.g., two mutually sets of arguments, of which some are shared). groups = [
ExclusiveGroup(filters=['field_a', 'field_b']),
ExclusiveGroup(filters=['field_b', 'field_c']),
] While the validation is sensible, filtering on MyModel.objects.filter(field_b='b').filter(field_b='b') The duplicate filter condition doesn't hurt the query, but it's not helping either. I'd like to see some concrete examples for overlapping groups. This can always be improved later, and in the meantime, maybe we could make a note in the docs?
It's described in a note about exclusion here. However, I'm not sure if your case is a bug or not. |
Wouldn't your first example currently result in: MyModel.objects.filter(field_b='b').filter(Q(field_c='c')) That would be significantly different from: MyModel.objects.filter(field_b='b').filter(Q(field_b='b') | Q(field_c='c'))
MyModel.objects.filter(field_b='b') # simplified |
Codecov Report
@@ Coverage Diff @@
## master #1167 +/- ##
==========================================
- Coverage 99.43% 98.35% -1.08%
==========================================
Files 15 16 +1
Lines 1235 1340 +105
Branches 0 226 +226
==========================================
+ Hits 1228 1318 +90
- Misses 7 8 +1
- Partials 0 14 +14
Continue to review full report at Codecov.
|
Sorry, I wasn't clear. I was assuming your proposed code changes, where a filter can be applied by overlapping groups. In this case, MyModel.objects.filter(field_b='b').filter(Q(field_b='b') | Q(field_c='c')) |
Yeah, either implementation could trip people up. Looking at your example again, my interpretation of the intent is that MyModel.objects.filter(Q(field_b='b') | Q(field_c='c')) But neither implementation does that. Mine results in: MyModel.objects.filter(field_b='b').filter(Q(field_b='b') | Q(field_c='c')) And I think yours results in: MyModel.objects.filter(field_b='b').filter(Q(field_c='c')) Maybe we could split validation groups and filtering groups into two different concepts. I don't really see why def get_data(self, cleaned_data):
return {
name: cleaned_data[name]
for name in self.filters
if name in cleaned_data
} Then only do group filtering for individual_data = cleaned_data.copy()
for group in self.groups:
if isinstance(group, CombinedGroup):
group_data = group.get_data(cleaned_data)
for name in group_data:
individual_data.pop(name, None)
queryset = group.filter(queryset, **group_data)
for name, value in individual_data.items(): (edited to fix a mistake in the filtering) |
The idea was ease of mental model. In general, django-filter does two things:
And I didn't want groups to deviate from this, where some groups do one thing, others something else. That said, this "ease of mental model" kind of falls apart when users need to actually know what's going on under the hood. What I'm leaning towards now is that a group may validate and it may filter, if it implements the respective methods. So, the |
Sounds good to me. Two important points to note in the docs:
More complex behaviour than that should use a custom FilterGroup. |
Hey @rpkilby.
Can we just ditch 1.11 I think. Let's go 2.2+ only. Simplifies everything. (You up to do that? 😀) |
Definitely. What is your timeline on the release? Aside from the Django failures, I'd still like to implement changes discussed above. |
No rush. Bang it into shape. Pull it together. Go. End of the month say? |
Oh yeah, I can manage that. If I'm being optimistic, end of week... but I've also got a lot of other stuff on my plate. |
No rush! 🙂 |
Sounds good. Did you have any thoughts on the documentation? If you could review the overall structure/idea, that would be useful.
|
I literally just ran into this use case. I am filtering through a many to many relationship and I do NOT want it to chain. I will give this a whirl. Sounds like it is basically ready. |
I am your first satisfied customer. THANK YOU! This is exactly what I needed. For now I added this branch as a requirement in my pipfile. |
Desperately seeking this functionality... Is there any update about whether / when this might be merged into the master? |
I added this branch to my requirements.txt for now.
…On Thu, Apr 9, 2020, 10:33 kense ***@***.***> wrote:
Desperately seeking this functionality... Is there any update about
whether / when this might be merged into the master?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#1167 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACZ5TIPNGM2SFJHJXWOWHZLRLXMB7ANCNFSM4KMK6LWQ>
.
|
any plan to merge this feature? |
Same demand here, hope to merge this feature into new master branch. |
+1, I am hoping this would solve multiple to-many filters. |
Same demand, any plan? |
Does anybody know why this PR was forgotten? :) |
Any updates on this? |
For anyone who still needs many-to-many filters to AND instead of OR, we were able to get similar functionality using this PR as a guide - I dropped a rough implementation here: https://gist.github.com/russmatney/7c757989ea3d6b1343df841ce5f33bc4 |
@carltongibson it's been 9 months since the docs were provided. Do you need help reviewing https://rpkilby.github.io/django-filter/ref/groups.html ? I need |
@ppolewicz The issue is that I'm (still) not sure whether this is the way I'd like to go here, not a lack of time to hit the merge button. |
To expand, it's a lot of extra API for something that can be done with a Filter and a multi-field already... |
@carltongibson What is I had this problem when I wanted to filter by different fields of related objects. I have class HouseFilterSet(FilterSet):
num_rooms = Filter(field_name='flats__num_rooms')
sq_ft = Filter(field_name='flats__sq_ft') results in House.objects.filter(flats__num_rooms=...).filter(flats__sq_ft=...) I believe that most users of House.objects.filter(flats__num_rooms=..., flats__sq_ft=...) Currently I solved it rather ugly by accumulating class HomeFilter(...):
...
def filter_queryset(self, request, queryset, view):
qs = []
for field in ('sq_ft', 'num_rooms'):
if (value := request.query_params.get(field)) is not None:
qs.append(Q(**{f'flats__{field}': value}))
if (value := request.query_params.get(f'{field}_min')) is not None:
qs.append(Q(**{f'flats__{field}__gte': value}))
if (value := request.query_params.get(f'{field}_max')) is not None:
qs.append(Q(**{f'flats__{field}__lte': value}))
if qs:
queryset = queryset.filter(reduce(and_, qs))
return queryset This would be solved by the groups PR, but let us know if there's any better solution, please. Honestly, |
@c0ntribut0r I'm referring to
No. There are two ways of cutting the cake here. Both are valid, depending on your context. See the Spanning multi-valued relationships docs for discussion. I appreciate folks want an out-of-box solution here, which is why I haven't closed this already, but every time I look at the proposed API here I think that it's too much. I would much rather stick with a narrower solution. |
I've been following this for awhile since I had an ugly version working that just pulled all the values I needed in the single filter method. After seeing the last comment about using Django's |
Imagine spending two weeks building a huge endpoint with
Already been discussed many times 4k stars on GitHub have never been so disappointing. |
Filtering is done as per the Spanning multi-valued relationships Django docs. If you need to filter in a single step see the snippet Kyle posted:
This PR is open as an aide memoir for implementing that solution in django-filter itself. |
This PR introduces a
Meta.groups
option to theFilterSet
, which allows users to define groups ofFilter
s that override the validation and filtering behavior for the filters in that group. Example:In the above, both
first_name
andlast_name
are mutually required, so if one of the filter params is provided, then both must be provided.This PR should resolve #1020 and is an alternative to #1164 (cc @jonathan-golorry, any thoughts on this PR as an alternative to yours?).
Context:
Users periodically request how to validate and filter against multiple parameters. The goto advice has been to implement something based on
MultiWidget
, however there are three main deficiencies in my mind:MultiWidget
,MultiValueField
, andFilter
.MultiWidget
/MultiValueField
API is somewhat confusing (maybe I just didn't grok it for some reason, but I remember struggling with it in the past)foo_min
andfoo_max
, but the errors are reported asfoo
.I think this ultimately stems from a mismatch in expectations. Filters are generally designed to operate on a single query parameter, while
MultiWidget
is not. Aside from the difficulties in implementing, there are other limitations elsewhere (e.g., lack of schema support).Proposal:
In contrast to a
MultiWidget
-based approach,Meta.groups
are specially handled by the filterset and provided its entire set of filter params. Groups must implement both validation and filtering, however they are slightly different.ExclusiveGroup
would validate that only one of its mutually exclusive parameters is provided. Validation of the individual filter parameters is still performed by the filter's underlying form field.Filter.filter
methods, however this is up to the group's implementation. For example,RequiredGroup
calls thefilter
methods of its individual filters, whileCombinedRequiredGroup
constructs Q objects that are combined via&
or|
(or some other function).In total, this PR implements four groups:
Notes:
RequiredGroup
effectively provides an alternative for the oldMeta.together
option.TODO: