Fix exclude-only multi filters matching nothing

MultiCriterion.to_q (used by SessionFilter for game/device) unconditionally added
field__in=value even when value was empty, and __in=[] matches no rows — so a
filter with only excludes (e.g. device excludes 11, no game/device includes)
returned zero results. Guard the empty value like ChoiceCriterion already does,
so an exclude-only criterion means 'all rows except the excluded ids'.

https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
This commit is contained in:
Claude
2026-06-08 15:27:10 +00:00
committed by Lukáš Kucharczyk
parent 60773e7755
commit 22d7834ae9
2 changed files with 27 additions and 1 deletions
+3 -1
View File
@@ -277,7 +277,9 @@ class MultiCriterion(_Criterion):
def to_q(self, field_name: str) -> Q: def to_q(self, field_name: str) -> Q:
m = self.modifier m = self.modifier
if m == Modifier.INCLUDES: if m == Modifier.INCLUDES:
q = Q(**{f"{field_name}__in": self.value}) q = Q()
if self.value:
q &= Q(**{f"{field_name}__in": self.value})
if self.excludes: if self.excludes:
q &= ~Q(**{f"{field_name}__in": self.excludes}) q &= ~Q(**{f"{field_name}__in": self.excludes})
return q return q
+24
View File
@@ -10,6 +10,7 @@ from common.criteria import (
ChoiceCriterion, ChoiceCriterion,
IntCriterion, IntCriterion,
Modifier, Modifier,
MultiCriterion,
StringCriterion, StringCriterion,
) )
from common.components import FilterBar from common.components import FilterBar
@@ -98,6 +99,29 @@ class TestChoiceCriterion:
assert c.to_q("status") == ~Q(status__in=["f"]) assert c.to_q("status") == ~Q(status__in=["f"])
class TestMultiCriterion:
def test_includes(self):
c = MultiCriterion(value=[797], modifier=Modifier.INCLUDES)
assert c.to_q("game_id") == Q(game_id__in=[797])
def test_excludes_only_empty_value(self):
"""Exclude one device with no includes — value=[], excludes=[11].
Regression: an empty ``value`` must not add ``__in=[]`` (which matches
nothing); the criterion should mean "all rows except device 11".
"""
c = MultiCriterion(value=[], excludes=[11], modifier=Modifier.INCLUDES)
assert c.to_q("device_id") == ~Q(device_id__in=[11])
def test_include_and_exclude(self):
c = MultiCriterion(value=[1], excludes=[2], modifier=Modifier.INCLUDES)
assert c.to_q("game_id") == Q(game_id__in=[1]) & ~Q(game_id__in=[2])
def test_is_null(self):
c = MultiCriterion(value=[], modifier=Modifier.IS_NULL)
assert c.to_q("device_id") == Q(device_id__isnull=True)
class TestChoiceCriterionAgainstDB: class TestChoiceCriterionAgainstDB:
"""Verify ChoiceCriterion produces correct DB results.""" """Verify ChoiceCriterion produces correct DB results."""