Fix filter stuff
This commit is contained in:
@@ -128,9 +128,12 @@ def _split_modifier(
|
||||
first offered mode otherwise.
|
||||
"""
|
||||
default_match = match_modes[0][0] if match_modes else ""
|
||||
if modifier in _PRESENCE_MODIFIERS:
|
||||
if modifier in _PRESENCE_MODIFIERS or not match_modes:
|
||||
# When there's no match-mode select, the modifier stays whole — it IS
|
||||
# the full criterion modifier (enum/choice fields). Only split when a
|
||||
# match-mode axis exists to receive the non-presence part.
|
||||
return modifier, default_match
|
||||
if modifier and match_modes:
|
||||
if modifier:
|
||||
return "", modifier
|
||||
return "", default_match
|
||||
|
||||
|
||||
+11
-11
@@ -318,17 +318,17 @@ class _SetCriterion(_Criterion):
|
||||
if modifier in (Modifier.EXCLUDES, Modifier.NOT_EQUALS):
|
||||
return ~Q(**{f"{field_name}__in": self.value}) if self.value else Q()
|
||||
if modifier == Modifier.INCLUDES_ALL:
|
||||
# Logical AND of equalities ("related to every value"). NOTE: for a
|
||||
# *multi-valued* relation this only behaves as "has all" when each
|
||||
# equality lands on its own join — i.e. applied via chained
|
||||
# ``.filter()`` calls or a ``pk__in`` subquery, not a single
|
||||
# ``.filter(Q(rel=a) & Q(rel=b))`` (which would require one related
|
||||
# row to equal both). M2M callers (e.g. PurchaseFilter.games) build
|
||||
# that subquery; see PurchaseFilter._games_to_q.
|
||||
q = Q()
|
||||
for value in self.value:
|
||||
q &= Q(**{field_name: value})
|
||||
return q
|
||||
# INCLUDES_ALL ("related to all of these") is only meaningful for
|
||||
# many-to-many fields. A naive Q(field=a) & Q(field=b) collapses
|
||||
# to a single join requiring one through-row to equal both values
|
||||
# (impossible), so the generic criterion layer cannot build a
|
||||
# correct Q. M2M callers must supply their own Q builder at the
|
||||
# filter level — see PurchaseFilter._games_to_q for the subquery
|
||||
# pattern (chained .filter() calls + pk__in).
|
||||
assert False, (
|
||||
"INCLUDES_ALL requires a filter-level Q builder for M2M fields. "
|
||||
"See PurchaseFilter._games_to_q for the chained-subquery pattern."
|
||||
)
|
||||
raise ValueError(f"Unsupported modifier {modifier} for {type(self).__name__}")
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -293,27 +293,85 @@
|
||||
--leading-5: 20px;
|
||||
--radius-base: 12px;
|
||||
--color-body: var(--color-gray-600);
|
||||
--color-body-subtle: var(--color-gray-500);
|
||||
--color-heading: var(--color-gray-900);
|
||||
--color-fg-brand-subtle: var(--color-blue-200);
|
||||
--color-fg-brand: var(--color-blue-700);
|
||||
--color-fg-brand-strong: var(--color-blue-900);
|
||||
--color-fg-success: var(--color-emerald-700);
|
||||
--color-fg-success-strong: var(--color-emerald-900);
|
||||
--color-fg-danger: var(--color-rose-700);
|
||||
--color-fg-danger-strong: var(--color-rose-900);
|
||||
--color-fg-warning-subtle: var(--color-orange-600);
|
||||
--color-fg-warning: var(--color-orange-900);
|
||||
--color-fg-yellow: var(--color-yellow-400);
|
||||
--color-fg-disabled: var(--color-gray-400);
|
||||
--color-fg-purple: var(--color-purple-600);
|
||||
--color-fg-cyan: var(--color-cyan-600);
|
||||
--color-fg-indigo: var(--color-indigo-600);
|
||||
--color-fg-pink: var(--color-pink-600);
|
||||
--color-fg-lime: var(--color-lime-600);
|
||||
--color-neutral-primary-soft: var(--color-white);
|
||||
--color-neutral-primary: var(--color-white);
|
||||
--color-neutral-primary-medium: var(--color-white);
|
||||
--color-neutral-primary-strong: var(--color-white);
|
||||
--color-neutral-secondary-soft: var(--color-gray-50);
|
||||
--color-neutral-secondary: var(--color-gray-50);
|
||||
--color-neutral-secondary-medium: var(--color-gray-50);
|
||||
--color-neutral-secondary-strong: var(--color-gray-50);
|
||||
--color-neutral-secondary-strongest: var(--color-gray-50);
|
||||
--color-neutral-tertiary-soft: var(--color-gray-100);
|
||||
--color-neutral-tertiary: var(--color-gray-100);
|
||||
--color-neutral-tertiary-medium: var(--color-gray-100);
|
||||
--color-neutral-quaternary: var(--color-gray-200);
|
||||
--color-neutral-quaternary-medium: var(--color-gray-200);
|
||||
--color-gray: var(--color-gray-300);
|
||||
--color-brand-softer: var(--color-blue-50);
|
||||
--color-brand-soft: var(--color-blue-100);
|
||||
--color-brand: var(--color-blue-700);
|
||||
--color-brand-medium: var(--color-blue-200);
|
||||
--color-brand-strong: var(--color-blue-800);
|
||||
--color-success-soft: var(--color-emerald-50);
|
||||
--color-success: var(--color-emerald-700);
|
||||
--color-success-medium: var(--color-emerald-100);
|
||||
--color-success-strong: var(--color-emerald-800);
|
||||
--color-danger-soft: var(--color-rose-50);
|
||||
--color-danger: var(--color-rose-700);
|
||||
--color-danger-medium: var(--color-rose-100);
|
||||
--color-danger-strong: var(--color-rose-800);
|
||||
--color-warning-soft: var(--color-orange-50);
|
||||
--color-warning: var(--color-orange-500);
|
||||
--color-warning-medium: var(--color-orange-100);
|
||||
--color-warning-strong: var(--color-orange-700);
|
||||
--color-dark-soft: var(--color-gray-800);
|
||||
--color-dark: var(--color-gray-800);
|
||||
--color-dark-strong: var(--color-gray-900);
|
||||
--color-disabled: var(--color-gray-100);
|
||||
--color-purple: var(--color-purple-500);
|
||||
--color-sky: var(--color-sky-500);
|
||||
--color-teal: var(--color-teal-600);
|
||||
--color-pink: var(--color-pink-600);
|
||||
--color-cyan: var(--color-cyan-500);
|
||||
--color-fuchsia: var(--color-fuchsia-600);
|
||||
--color-indigo: var(--color-indigo-600);
|
||||
--color-orange: var(--color-orange-400);
|
||||
--color-buffer: var(--color-white);
|
||||
--color-buffer-medium: var(--color-white);
|
||||
--color-buffer-strong: var(--color-white);
|
||||
--color-muted: var(--color-gray-50);
|
||||
--color-light-subtle: var(--color-gray-100);
|
||||
--color-light: var(--color-gray-100);
|
||||
--color-light-medium: var(--color-gray-100);
|
||||
--color-default-subtle: var(--color-gray-200);
|
||||
--color-default: var(--color-gray-200);
|
||||
--color-default-medium: var(--color-gray-200);
|
||||
--color-default-strong: var(--color-gray-200);
|
||||
--color-success-subtle: var(--color-emerald-200);
|
||||
--color-danger-subtle: var(--color-rose-200);
|
||||
--color-warning-subtle: var(--color-orange-200);
|
||||
--color-brand-subtle: var(--color-blue-200);
|
||||
--color-brand-light: var(--color-blue-600);
|
||||
--color-dark-subtle: var(--color-gray-800);
|
||||
--color-dark-backdrop: var(--color-gray-950);
|
||||
--color-accent: #7c3aed;
|
||||
}
|
||||
@@ -823,12 +881,18 @@
|
||||
.start-0 {
|
||||
inset-inline-start: calc(var(--spacing) * 0);
|
||||
}
|
||||
.end-1 {
|
||||
inset-inline-end: calc(var(--spacing) * 1);
|
||||
}
|
||||
.end-1\.5 {
|
||||
inset-inline-end: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.top-0 {
|
||||
top: calc(var(--spacing) * 0);
|
||||
}
|
||||
.top-1 {
|
||||
top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.top-1\/2 {
|
||||
top: calc(1 / 2 * 100%);
|
||||
}
|
||||
@@ -850,6 +914,9 @@
|
||||
.bottom-0 {
|
||||
bottom: calc(var(--spacing) * 0);
|
||||
}
|
||||
.bottom-1 {
|
||||
bottom: calc(var(--spacing) * 1);
|
||||
}
|
||||
.bottom-1\.5 {
|
||||
bottom: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
@@ -1559,9 +1626,15 @@
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.w-1 {
|
||||
width: calc(var(--spacing) * 1);
|
||||
}
|
||||
.w-1\/2 {
|
||||
width: calc(1 / 2 * 100%);
|
||||
}
|
||||
.w-2 {
|
||||
width: calc(var(--spacing) * 2);
|
||||
}
|
||||
.w-2\.5 {
|
||||
width: calc(var(--spacing) * 2.5);
|
||||
}
|
||||
@@ -1679,6 +1752,9 @@
|
||||
.shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.-translate-x-full {
|
||||
--tw-translate-x: -100%;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@@ -1695,6 +1771,10 @@
|
||||
--tw-translate-x: 100%;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1 {
|
||||
--tw-translate-y: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1\/2 {
|
||||
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@@ -2080,12 +2160,18 @@
|
||||
.bg-amber-50 {
|
||||
background-color: var(--color-amber-50);
|
||||
}
|
||||
.bg-amber-500 {
|
||||
background-color: var(--color-amber-500);
|
||||
}
|
||||
.bg-amber-500\/15 {
|
||||
background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 15%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-amber-500) 15%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-black {
|
||||
background-color: var(--color-black);
|
||||
}
|
||||
.bg-black\/70 {
|
||||
background-color: color-mix(in srgb, #000 70%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -2110,6 +2196,9 @@
|
||||
background-color: color-mix(in oklab, var(--color-brand) 15%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-dark-backdrop {
|
||||
background-color: var(--color-dark-backdrop);
|
||||
}
|
||||
.bg-dark-backdrop\/70 {
|
||||
background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 70%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -2128,12 +2217,18 @@
|
||||
.bg-gray-500 {
|
||||
background-color: var(--color-gray-500);
|
||||
}
|
||||
.bg-gray-800 {
|
||||
background-color: var(--color-gray-800);
|
||||
}
|
||||
.bg-gray-800\/20 {
|
||||
background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 20%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-gray-800) 20%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-gray-900 {
|
||||
background-color: var(--color-gray-900);
|
||||
}
|
||||
.bg-gray-900\/50 {
|
||||
background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 50%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -2263,6 +2358,18 @@
|
||||
fill: white !important;
|
||||
}
|
||||
}
|
||||
.apexcharts-gridline {
|
||||
stroke: var(--color-default) !important;
|
||||
.dark & {
|
||||
stroke: var(--color-default) !important;
|
||||
}
|
||||
}
|
||||
.apexcharts-xcrosshairs {
|
||||
stroke: var(--color-default) !important;
|
||||
.dark & {
|
||||
stroke: var(--color-default) !important;
|
||||
}
|
||||
}
|
||||
.apexcharts-ycrosshairs {
|
||||
stroke: var(--color-default) !important;
|
||||
.dark & {
|
||||
@@ -2321,6 +2428,9 @@
|
||||
.px-6 {
|
||||
padding-inline: calc(var(--spacing) * 6);
|
||||
}
|
||||
.py-0 {
|
||||
padding-block: calc(var(--spacing) * 0);
|
||||
}
|
||||
.py-0\.5 {
|
||||
padding-block: calc(var(--spacing) * 0.5);
|
||||
}
|
||||
@@ -2547,6 +2657,9 @@
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
.text-wrap {
|
||||
text-wrap: wrap;
|
||||
}
|
||||
.whitespace-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -2682,6 +2795,9 @@
|
||||
.line-through {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
.no-underline {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
.no-underline\! {
|
||||
text-decoration-line: none !important;
|
||||
}
|
||||
@@ -2748,6 +2864,10 @@
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-filter {
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
|
||||
@@ -70,9 +70,11 @@
|
||||
// Two orthogonal axes: a presence modifier (NOT_NULL/IS_NULL) from the
|
||||
// pinned (Any)/(None) pseudo-options clears the value set, while the
|
||||
// match mode (INCLUDES/INCLUDES_ALL/EXCLUDES) governs how the include set
|
||||
// matches. Fields without a match-mode select default to INCLUDES.
|
||||
// matches. Fields without a data-match attribute have no match-mode select
|
||||
// — the full modifier lives in data-modifier (e.g. enum/choice fields).
|
||||
var presence = widget.getAttribute("data-modifier");
|
||||
var match = widget.getAttribute("data-match") || "INCLUDES";
|
||||
var matchVal = widget.getAttribute("data-match");
|
||||
var match = matchVal || presence || "INCLUDES";
|
||||
if (presence === "NOT_NULL" || presence === "IS_NULL") {
|
||||
filter[field] = { modifier: presence };
|
||||
} else if (included.length > 0 || excluded.length > 0) {
|
||||
|
||||
@@ -158,3 +158,27 @@ class FilterBarRenderingTest(TestCase):
|
||||
# for the double-escape bug the dedup fixed.
|
||||
self.assertIn(""status"", html)
|
||||
self.assertNotIn(""", html)
|
||||
|
||||
def test_game_filter_bar_preserves_excludes_modifier(self):
|
||||
"""An enum field with an EXCLUDES modifier renders data-modifier correctly
|
||||
so the JS roundtrip preserves the modifier (regression: _split_modifier
|
||||
silently dropped non-presence modifiers when match_modes was None)."""
|
||||
filter_json = json.dumps(
|
||||
{
|
||||
"status": {
|
||||
"value": [{"id": "f", "label": "Finished"}],
|
||||
"modifier": "EXCLUDES",
|
||||
}
|
||||
}
|
||||
)
|
||||
html = str(
|
||||
FilterBar(
|
||||
filter_json=filter_json, preset_list_url="/l", preset_save_url="/s"
|
||||
)
|
||||
)
|
||||
# The full modifier is stored on data-modifier when there's no match-mode
|
||||
# select (enum/choice fields). No data-match attribute is present.
|
||||
self.assertIn('data-modifier="EXCLUDES"', html)
|
||||
self.assertNotIn("data-match=", html)
|
||||
self.assertIn("Finished", html)
|
||||
self.assertNoEscapedTags(html)
|
||||
|
||||
+10
-6
@@ -101,10 +101,12 @@ class TestChoiceCriterion:
|
||||
c = ChoiceCriterion(value=["f"], excludes=["a"], modifier=Modifier.EXCLUDES)
|
||||
assert c.to_q("status") == ~Q(status__in=["f"]) & ~Q(status__in=["a"])
|
||||
|
||||
def test_includes_all(self):
|
||||
"""INCLUDES_ALL ANDs an equality per value (shared with MultiCriterion)."""
|
||||
def test_includes_all_requires_filter_builder(self):
|
||||
"""INCLUDES_ALL cannot be built by the generic criterion layer — it
|
||||
requires a filter-level Q builder (see PurchaseFilter._games_to_q)."""
|
||||
c = ChoiceCriterion(value=["f", "p"], modifier=Modifier.INCLUDES_ALL)
|
||||
assert c.to_q("status") == Q(status="f") & Q(status="p")
|
||||
with pytest.raises(AssertionError, match="INCLUDES_ALL requires"):
|
||||
c.to_q("status")
|
||||
|
||||
def test_not_equals(self):
|
||||
c = ChoiceCriterion(value=["f"], modifier=Modifier.NOT_EQUALS)
|
||||
@@ -136,10 +138,12 @@ class TestMultiCriterion:
|
||||
c = MultiCriterion(value=[1], excludes=[2], modifier=Modifier.EXCLUDES)
|
||||
assert c.to_q("game_id") == ~Q(game_id__in=[1]) & ~Q(game_id__in=[2])
|
||||
|
||||
def test_includes_all(self):
|
||||
"""INCLUDES_ALL requires the row to relate to every value (M2M)."""
|
||||
def test_includes_all_requires_filter_builder(self):
|
||||
"""INCLUDES_ALL cannot be built by the generic criterion layer — it
|
||||
requires a filter-level Q builder (see PurchaseFilter._games_to_q)."""
|
||||
c = MultiCriterion(value=[1, 2], modifier=Modifier.INCLUDES_ALL)
|
||||
assert c.to_q("games") == Q(games=1) & Q(games=2)
|
||||
with pytest.raises(AssertionError, match="INCLUDES_ALL requires"):
|
||||
c.to_q("games")
|
||||
|
||||
def test_is_null(self):
|
||||
c = MultiCriterion(value=[], modifier=Modifier.IS_NULL)
|
||||
|
||||
Reference in New Issue
Block a user