Consolidate Multi/Choice criteria into a shared _SetCriterion base
MultiCriterion and ChoiceCriterion were near-duplicate copies whose INCLUDES branches had drifted — the exclude-only bug existed in one but not the other. Extract the shared include/exclude/null set-membership logic into a _SetCriterion base implemented once (INCLUDES with empty-list guards, EQUALS as an alias, IS_NULL/NOT_NULL); subclasses contribute only their value type and their own modifiers via _extra_q (INCLUDES_ALL for Multi; EXCLUDES/NOT_EQUALS for Choice). Behaviour preserved (full modifier vocabulary kept); the duplication that caused the drift is gone. Surfacing the modifier axis and harmonizing EXCLUDES is tracked in #10. https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
This commit is contained in:
+57
-46
@@ -267,78 +267,89 @@ class BoolCriterion(_Criterion):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MultiCriterion(_Criterion):
|
class _SetCriterion(_Criterion):
|
||||||
|
"""Shared base for set-membership criteria (``MultiCriterion`` /
|
||||||
|
``ChoiceCriterion``).
|
||||||
|
|
||||||
|
``value`` is the include set and ``excludes`` the exclude set. The common
|
||||||
|
modifiers are implemented once here so the two subclasses cannot drift:
|
||||||
|
|
||||||
|
- ``INCLUDES`` — in ``value`` (when non-empty) AND not in ``excludes`` (when
|
||||||
|
non-empty). Empty lists contribute no constraint, so an exclude-only
|
||||||
|
criterion means "everything except ``excludes``".
|
||||||
|
- ``EQUALS`` — alias of ``INCLUDES``.
|
||||||
|
- ``IS_NULL`` / ``NOT_NULL`` — presence; the lists are ignored.
|
||||||
|
|
||||||
|
Subclasses contribute their own modifiers (e.g. ``INCLUDES_ALL``) by
|
||||||
|
overriding ``_extra_q``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
value: list = field(default_factory=list)
|
||||||
|
excludes: list = field(default_factory=list)
|
||||||
|
modifier: Modifier = Modifier.INCLUDES
|
||||||
|
|
||||||
|
def to_q(self, field_name: str) -> Q:
|
||||||
|
modifier = self.modifier
|
||||||
|
if modifier in (Modifier.INCLUDES, Modifier.EQUALS):
|
||||||
|
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
|
||||||
|
if modifier == Modifier.IS_NULL:
|
||||||
|
return Q(**{f"{field_name}__isnull": True})
|
||||||
|
if modifier == Modifier.NOT_NULL:
|
||||||
|
return Q(**{f"{field_name}__isnull": False})
|
||||||
|
extra = self._extra_q(field_name)
|
||||||
|
if extra is not None:
|
||||||
|
return extra
|
||||||
|
raise ValueError(f"Unsupported modifier {modifier} for {type(self).__name__}")
|
||||||
|
|
||||||
|
def _extra_q(self, field_name: str) -> Q | None:
|
||||||
|
"""Hook for subclass-specific modifiers; ``None`` means unsupported."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MultiCriterion(_SetCriterion):
|
||||||
"""Filter on a many-to-many or ForeignKey relationship by ID list."""
|
"""Filter on a many-to-many or ForeignKey relationship by ID list."""
|
||||||
|
|
||||||
value: list[int] = field(default_factory=list)
|
value: list[int] = field(default_factory=list)
|
||||||
excludes: list[int] = field(default_factory=list)
|
excludes: list[int] = field(default_factory=list)
|
||||||
modifier: Modifier = Modifier.INCLUDES
|
|
||||||
|
|
||||||
def to_q(self, field_name: str) -> Q:
|
def _extra_q(self, field_name: str) -> Q | None:
|
||||||
m = self.modifier
|
if self.modifier == Modifier.EXCLUDES:
|
||||||
if m == Modifier.INCLUDES:
|
|
||||||
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
|
|
||||||
if m == Modifier.EXCLUDES:
|
|
||||||
return ~Q(**{f"{field_name}__in": self.value})
|
return ~Q(**{f"{field_name}__in": self.value})
|
||||||
if m == Modifier.INCLUDES_ALL:
|
if self.modifier == Modifier.INCLUDES_ALL:
|
||||||
q = Q()
|
q = Q()
|
||||||
for v in self.value:
|
for value in self.value:
|
||||||
q &= Q(**{field_name: v})
|
q &= Q(**{field_name: value})
|
||||||
return q
|
return q
|
||||||
if m == Modifier.IS_NULL:
|
return None
|
||||||
return Q(**{f"{field_name}__isnull": True})
|
|
||||||
if m == Modifier.NOT_NULL:
|
|
||||||
return Q(**{f"{field_name}__isnull": False})
|
|
||||||
raise ValueError(f"Unsupported modifier {m} for multi field")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ChoiceCriterion(_Criterion):
|
class ChoiceCriterion(_SetCriterion):
|
||||||
"""Filter on a choice/enum field with multi-select include/exclude.
|
"""Filter on a choice/enum field with multi-select include/exclude.
|
||||||
|
|
||||||
Used by FilterSelect widgets for status, ownership_type, etc.
|
Used by FilterSelect widgets for status, ownership_type, etc.
|
||||||
Supports INCLUDES, EXCLUDES, EQUALS, IS_NULL, NOT_NULL modifiers.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
value: list[str] = field(default_factory=list)
|
value: list[str] = field(default_factory=list)
|
||||||
excludes: list[str] = field(default_factory=list)
|
excludes: list[str] = field(default_factory=list)
|
||||||
modifier: Modifier = Modifier.INCLUDES
|
|
||||||
|
|
||||||
def to_q(self, field_name: str) -> Q:
|
def _extra_q(self, field_name: str) -> Q | None:
|
||||||
m = self.modifier
|
if self.modifier == Modifier.EXCLUDES:
|
||||||
if m == Modifier.INCLUDES:
|
|
||||||
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
|
|
||||||
if m == Modifier.EXCLUDES:
|
|
||||||
q = Q()
|
q = Q()
|
||||||
if self.value:
|
if self.value:
|
||||||
q &= ~Q(**{f"{field_name}__in": 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
|
||||||
if m == Modifier.EQUALS:
|
if self.modifier == Modifier.NOT_EQUALS:
|
||||||
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
|
|
||||||
if m == Modifier.NOT_EQUALS:
|
|
||||||
return ~Q(**{f"{field_name}__in": self.value})
|
return ~Q(**{f"{field_name}__in": self.value})
|
||||||
if m == Modifier.IS_NULL:
|
return None
|
||||||
return Q(**{f"{field_name}__isnull": True})
|
|
||||||
if m == Modifier.NOT_NULL:
|
|
||||||
return Q(**{f"{field_name}__isnull": False})
|
|
||||||
raise ValueError(f"Unsupported modifier {m} for choice field")
|
|
||||||
|
|
||||||
|
|
||||||
# ── OperatorFilter base ────────────────────────────────────────────────────
|
# ── OperatorFilter base ────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user