From 22d7834ae96a8d7973d0c406025e82e8c2d8c65a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 15:27:10 +0000 Subject: [PATCH] Fix exclude-only multi filters matching nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- common/criteria.py | 4 +++- tests/test_filters.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/common/criteria.py b/common/criteria.py index ba4406b..6ae3493 100644 --- a/common/criteria.py +++ b/common/criteria.py @@ -277,7 +277,9 @@ class MultiCriterion(_Criterion): def to_q(self, field_name: str) -> Q: m = self.modifier 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: q &= ~Q(**{f"{field_name}__in": self.excludes}) return q diff --git a/tests/test_filters.py b/tests/test_filters.py index f25b617..84363f3 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -10,6 +10,7 @@ from common.criteria import ( ChoiceCriterion, IntCriterion, Modifier, + MultiCriterion, StringCriterion, ) from common.components import FilterBar @@ -98,6 +99,29 @@ class TestChoiceCriterion: 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: """Verify ChoiceCriterion produces correct DB results."""