From a7ff2962a6a22f8767ab0d16a8dee1402a576b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Mon, 8 Jun 2026 23:49:03 +0200 Subject: [PATCH] Add number of games filter to purchases --- common/components/filters.py | 21 +++++++++++ games/static/js/filter_bar.js | 11 ++++++ tests/test_filters.py | 67 +++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/common/components/filters.py b/common/components/filters.py index e32d3d8..af887dd 100644 --- a/common/components/filters.py +++ b/common/components/filters.py @@ -847,6 +847,16 @@ def PurchaseFilterBar( except Exception: price_range_min, price_range_max = 0, 100 + num_min, num_max = _parse_range(existing, "num_purchases") + try: + num_aggregate = Purchase.objects.aggregate( + num_min=models.Min("num_purchases"), num_max=models.Max("num_purchases") + ) + num_range_min = max(int(num_aggregate.get("num_min") or 0), 0) + num_range_max = max(int(num_aggregate.get("num_max") or 10), 1) + except Exception: + num_range_min, num_range_max = 0, 10 + fields = [ Component( tag_name="div", @@ -912,5 +922,16 @@ def PurchaseFilterBar( min_placeholder="0.00", max_placeholder="100.00", ), + RangeSlider( + label="Games in purchase", + input_name_prefix="filter-num-purchases", + min_value=num_min, + max_value=num_max, + range_min=num_range_min, + range_max=num_range_max, + step="1", + min_placeholder="e.g. 1", + max_placeholder="e.g. 5", + ), ] return _filter_bar(fields, filter_json, preset_list_url, preset_save_url) diff --git a/games/static/js/filter_bar.js b/games/static/js/filter_bar.js index 217e80b..b39e867 100644 --- a/games/static/js/filter_bar.js +++ b/games/static/js/filter_bar.js @@ -130,6 +130,17 @@ } } + // ── Purchase-specific: num_purchases ── + var numGamesMin = numberValue(form, "filter-num-purchases-min"); + var numGamesMax = numberValue(form, "filter-num-purchases-max"); + if (numGamesMin !== "" && numGamesMax !== "") { + filter.num_purchases = criterion(parseInt(numGamesMin, 10), parseInt(numGamesMax, 10), "BETWEEN"); + } else if (numGamesMin !== "") { + filter.num_purchases = criterion(parseInt(numGamesMin, 10), null, "GREATER_THAN"); + } else if (numGamesMax !== "") { + filter.num_purchases = criterion(parseInt(numGamesMax, 10), null, "LESS_THAN"); + } + if (mastered && mastered.checked) { filter.mastered = criterion(true, null, "EQUALS"); } diff --git a/tests/test_filters.py b/tests/test_filters.py index d229d34..f7f7f83 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -563,3 +563,70 @@ class TestFilterBarRendering: platform_section = html[platform_start:] # Should have at least one modifier option assert "(Any)" in platform_section or "(None)" in platform_section + + +class TestPurchaseNumPurchasesAgainstDB: + """num_purchases IntCriterion filters purchases by game count.""" + + def _seed(self): + import datetime + + from games.models import Game, Platform, Purchase + + platform, _ = Platform.objects.get_or_create(name="Test", icon="test") + a, _ = Game.objects.get_or_create(name="A", defaults={"platform": platform}) + b, _ = Game.objects.get_or_create(name="B", defaults={"platform": platform}) + c, _ = Game.objects.get_or_create(name="C", defaults={"platform": platform}) + + single = Purchase.objects.create( + platform=platform, date_purchased=datetime.date(2024, 1, 1) + ) + single.games.set([a]) + + double = Purchase.objects.create( + platform=platform, date_purchased=datetime.date(2024, 1, 1) + ) + double.games.set([a, b]) + + triple = Purchase.objects.create( + platform=platform, date_purchased=datetime.date(2024, 1, 1) + ) + triple.games.set([a, b, c]) + + return {"single": single, "double": double, "triple": triple} + + @pytest.mark.django_db + def test_between_two_and_three(self): + from games.filters import PurchaseFilter + from games.models import Purchase + + seeded = self._seed() + pf = PurchaseFilter.from_json( + {"num_purchases": {"value": 2, "value2": 3, "modifier": "BETWEEN"}} + ) + result = set(Purchase.objects.filter(pf.to_q())) + assert result == {seeded["double"], seeded["triple"]} + + @pytest.mark.django_db + def test_greater_than_one(self): + from games.filters import PurchaseFilter + from games.models import Purchase + + seeded = self._seed() + pf = PurchaseFilter.from_json( + {"num_purchases": {"value": 1, "modifier": "GREATER_THAN"}} + ) + result = set(Purchase.objects.filter(pf.to_q())) + assert result == {seeded["double"], seeded["triple"]} + + @pytest.mark.django_db + def test_equals_one(self): + from games.filters import PurchaseFilter + from games.models import Purchase + + seeded = self._seed() + pf = PurchaseFilter.from_json( + {"num_purchases": {"value": 1, "modifier": "EQUALS"}} + ) + result = set(Purchase.objects.filter(pf.to_q())) + assert result == {seeded["single"]}