Numeric range filters could only express BETWEEN/GREATER_THAN/LESS_THAN via the RangeSlider widget — no way to match NULL/missing values (the original ask in #32) or exact/not-between. The criteria backend already supported all 8 numeric modifiers + value2, so this is a UI swap. - Add NumberFilter component, modeled 1:1 on StringFilter: an 8-modifier radio grid plus two number inputs, with the second input revealed only for BETWEEN/NOT_BETWEEN and both disabled for IS_NULL/NOT_NULL. Initial state is server-rendered so the widget never flashes. - Migrate all 17 numeric range fields (game/session/purchase/playevent) to NumberFilter; drop the now-dead min/max aggregate queries. - filter-bar.ts: serialize numberFields by modifier (mirroring textFields) and wire the modifier radios via event delegation on the persistent custom element so they survive htmx swaps of the inner bar body. Apply the same delegation fix to the existing string filters. - Remove RangeSlider entirely: component, range-slider.ts, its custom element registration/props, and the range-slider e2e tests. Backward compatible: old slider filters stored {value, value2, modifier}, the same JSON shape NumberFilter reads, so saved presets keep working. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,45 @@ class TestIntCriterion:
|
||||
year_released__gte=2020, year_released__lte=2024
|
||||
)
|
||||
|
||||
def test_not_between(self):
|
||||
c = IntCriterion(value=2020, value2=2024, modifier=Modifier.NOT_BETWEEN)
|
||||
assert c.to_q("year_released") == Q(year_released__lt=2020) | Q(
|
||||
year_released__gt=2024
|
||||
)
|
||||
|
||||
def test_greater_than(self):
|
||||
c = IntCriterion(value=10, modifier=Modifier.GREATER_THAN)
|
||||
assert c.to_q("session_count") == Q(session_count__gt=10)
|
||||
|
||||
def test_less_than(self):
|
||||
c = IntCriterion(value=10, modifier=Modifier.LESS_THAN)
|
||||
assert c.to_q("session_count") == Q(session_count__lt=10)
|
||||
|
||||
def test_is_null(self):
|
||||
c = IntCriterion(modifier=Modifier.IS_NULL)
|
||||
assert c.to_q("year_released") == Q(year_released__isnull=True)
|
||||
|
||||
def test_not_null(self):
|
||||
c = IntCriterion(modifier=Modifier.NOT_NULL)
|
||||
assert c.to_q("year_released") == Q(year_released__isnull=False)
|
||||
|
||||
def test_round_trip_json_between(self):
|
||||
"""value/value2/modifier survive dict → dataclass → dict unchanged."""
|
||||
original = IntCriterion(value=2020, value2=2024, modifier=Modifier.BETWEEN)
|
||||
as_dict = original.to_json()
|
||||
assert as_dict == {
|
||||
"value": 2020,
|
||||
"value2": 2024,
|
||||
"modifier": Modifier.BETWEEN,
|
||||
}
|
||||
assert IntCriterion.from_json(as_dict) == original
|
||||
|
||||
def test_round_trip_json_is_null(self):
|
||||
original = IntCriterion(modifier=Modifier.IS_NULL)
|
||||
restored = IntCriterion.from_json(original.to_json())
|
||||
assert restored == original
|
||||
assert restored.to_q("year_released") == Q(year_released__isnull=True)
|
||||
|
||||
|
||||
class TestBoolCriterion:
|
||||
def test_equals_true(self):
|
||||
|
||||
Reference in New Issue
Block a user