Implement date filters in purchase list

This commit is contained in:
2026-06-09 19:36:18 +02:00
parent 835caf6a71
commit dd2ebe5888
8 changed files with 939 additions and 135 deletions
+99 -36
View File
@@ -18,6 +18,7 @@ from django.db.models import Q
from common.criteria import (
BoolCriterion,
ChoiceCriterion,
DateCriterion,
FloatCriterion,
IntCriterion,
Modifier,
@@ -132,6 +133,7 @@ class GameFilter(OperatorFilter):
from django.db.models import Count
from games.models import Game
matching_ids = (
Game.objects.annotate(s_count=Count("sessions", distinct=True))
.filter(self.session_count.to_q("s_count"))
@@ -143,6 +145,7 @@ class GameFilter(OperatorFilter):
from django.db.models import Avg
from games.models import Game
matching_ids = (
Game.objects.annotate(s_avg=Avg("sessions__duration_total"))
.filter(self._playtime_to_q_for_field(self.session_average, "s_avg"))
@@ -154,6 +157,7 @@ class GameFilter(OperatorFilter):
from django.db.models import Count
from games.models import Game
matching_ids = (
Game.objects.annotate(p_count=Count("purchases", distinct=True))
.filter(self.purchase_count.to_q("p_count"))
@@ -165,6 +169,7 @@ class GameFilter(OperatorFilter):
from django.db.models import Count
from games.models import Game
matching_ids = (
Game.objects.annotate(pe_count=Count("playevents", distinct=True))
.filter(self.playevent_count.to_q("pe_count"))
@@ -176,9 +181,14 @@ class GameFilter(OperatorFilter):
from django.db.models import Sum
from games.models import Game
matching_ids = (
Game.objects.annotate(s_manual=Sum("sessions__duration_manual"))
.filter(self._playtime_to_q_for_field(self.manual_playtime_minutes, "s_manual"))
.filter(
self._playtime_to_q_for_field(
self.manual_playtime_minutes, "s_manual"
)
)
.values_list("id", flat=True)
)
q &= Q(id__in=matching_ids)
@@ -187,31 +197,47 @@ class GameFilter(OperatorFilter):
from django.db.models import Sum
from games.models import Game
matching_ids = (
Game.objects.annotate(s_calc=Sum("sessions__duration_calculated"))
.filter(self._playtime_to_q_for_field(self.calculated_playtime_minutes, "s_calc"))
.filter(
self._playtime_to_q_for_field(
self.calculated_playtime_minutes, "s_calc"
)
)
.values_list("id", flat=True)
)
q &= Q(id__in=matching_ids)
if self.device is not None:
from games.models import Session
session_q = self.device.to_q("device_id")
matching_ids = Session.objects.filter(session_q).values_list("game_id", flat=True)
matching_ids = Session.objects.filter(session_q).values_list(
"game_id", flat=True
)
q &= Q(id__in=matching_ids)
if self.session_emulated is not None:
from games.models import Session
emulated_ids = Session.objects.filter(emulated=self.session_emulated.value).values_list("game_id", flat=True)
emulated_ids = Session.objects.filter(
emulated=self.session_emulated.value
).values_list("game_id", flat=True)
if self.session_emulated.value:
q &= Q(id__in=emulated_ids)
else:
emulated_true_ids = Session.objects.filter(emulated=True).values_list("game_id", flat=True)
emulated_true_ids = Session.objects.filter(emulated=True).values_list(
"game_id", flat=True
)
q &= ~Q(id__in=emulated_true_ids)
if self.purchase_refunded is not None:
from games.models import Purchase
refunded_ids = Purchase.objects.filter(date_refunded__isnull=False).values_list("games__id", flat=True)
refunded_ids = Purchase.objects.filter(
date_refunded__isnull=False
).values_list("games__id", flat=True)
if self.purchase_refunded.value:
q &= Q(id__in=refunded_ids)
else:
@@ -219,7 +245,10 @@ class GameFilter(OperatorFilter):
if self.purchase_infinite is not None:
from games.models import Purchase
infinite_ids = Purchase.objects.filter(infinite=True).values_list("games__id", flat=True)
infinite_ids = Purchase.objects.filter(infinite=True).values_list(
"games__id", flat=True
)
if self.purchase_infinite.value:
q &= Q(id__in=infinite_ids)
else:
@@ -229,6 +258,7 @@ class GameFilter(OperatorFilter):
from django.db.models import Sum
from games.models import Game
matching_ids = (
Game.objects.annotate(p_total=Sum("purchases__converted_price"))
.filter(self.purchase_price_total.to_q("p_total"))
@@ -238,20 +268,29 @@ class GameFilter(OperatorFilter):
if self.purchase_price_any is not None:
from games.models import Purchase
price_q = self.purchase_price_any.to_q("converted_price")
matching_ids = Purchase.objects.filter(price_q).values_list("games__id", flat=True)
matching_ids = Purchase.objects.filter(price_q).values_list(
"games__id", flat=True
)
q &= Q(id__in=matching_ids)
if self.purchase_type is not None:
from games.models import Purchase
type_q = self.purchase_type.to_q("type")
matching_ids = Purchase.objects.filter(type_q).values_list("games__id", flat=True)
matching_ids = Purchase.objects.filter(type_q).values_list(
"games__id", flat=True
)
q &= Q(id__in=matching_ids)
if self.purchase_ownership_type is not None:
from games.models import Purchase
ownership_q = self.purchase_ownership_type.to_q("ownership_type")
matching_ids = Purchase.objects.filter(ownership_q).values_list("games__id", flat=True)
matching_ids = Purchase.objects.filter(ownership_q).values_list(
"games__id", flat=True
)
q &= Q(id__in=matching_ids)
if self.playevent_note is not None:
@@ -271,26 +310,38 @@ class GameFilter(OperatorFilter):
# Cross-entity filters
if self.session_filter is not None:
from games.models import Session
session_q = self.session_filter.to_q()
matching_ids = Session.objects.filter(session_q).values_list("game_id", flat=True)
matching_ids = Session.objects.filter(session_q).values_list(
"game_id", flat=True
)
q &= Q(id__in=matching_ids)
if self.purchase_filter is not None:
from games.models import Purchase
purchase_q = self.purchase_filter.to_q()
matching_ids = Purchase.objects.filter(purchase_q).values_list("games__id", flat=True)
matching_ids = Purchase.objects.filter(purchase_q).values_list(
"games__id", flat=True
)
q &= Q(id__in=matching_ids)
if self.playevent_filter is not None:
from games.models import PlayEvent
playevent_q = self.playevent_filter.to_q()
matching_ids = PlayEvent.objects.filter(playevent_q).values_list("game_id", flat=True)
matching_ids = PlayEvent.objects.filter(playevent_q).values_list(
"game_id", flat=True
)
q &= Q(id__in=matching_ids)
if self.platform_filter is not None:
from games.models import Platform
platform_q = self.platform_filter.to_q()
matching_ids = Platform.objects.filter(platform_q).values_list("id", flat=True)
matching_ids = Platform.objects.filter(platform_q).values_list(
"id", flat=True
)
q &= Q(platform_id__in=matching_ids)
# ── AND / OR / NOT sub-filters ──
@@ -375,9 +426,9 @@ class GameFilter(OperatorFilter):
include_q |= Q(id__in=matching_ids)
q &= ~include_q if negate_include else include_q
for term in criterion.excludes:
matching_ids = PlayEvent.objects.filter(
note__icontains=term
).values_list("game_id", flat=True)
matching_ids = PlayEvent.objects.filter(note__icontains=term).values_list(
"game_id", flat=True
)
q &= ~Q(id__in=matching_ids)
return q
@@ -418,6 +469,7 @@ class SessionFilter(OperatorFilter):
def _duration_to_q(self, c: IntCriterion, field: str) -> Q:
from datetime import timedelta
q = Q()
td_val = timedelta(minutes=c.value)
m = c.modifier
@@ -473,7 +525,9 @@ class SessionFilter(OperatorFilter):
if self.duration_manual_minutes is not None:
q &= self._duration_to_q(self.duration_manual_minutes, "duration_manual")
if self.duration_calculated_minutes is not None:
q &= self._duration_to_q(self.duration_calculated_minutes, "duration_calculated")
q &= self._duration_to_q(
self.duration_calculated_minutes, "duration_calculated"
)
if self.is_active is not None:
if self.is_active.value:
q &= Q(timestamp_end__isnull=True)
@@ -546,8 +600,8 @@ class PurchaseFilter(OperatorFilter):
name: StringCriterion | None = None
platform: ChoiceCriterion | None = None # platform_id
games: ChoiceCriterion | None = None # games (M2M IDs)
date_purchased: StringCriterion | None = None # date string
date_refunded: StringCriterion | None = None # date string
date_purchased: DateCriterion | None = None
date_refunded: DateCriterion | None = None
is_refunded: BoolCriterion | None = None # date_refunded IS NOT NULL
price: FloatCriterion | None = None # on price field
converted_price: FloatCriterion | None = None
@@ -633,7 +687,9 @@ class PurchaseFilter(OperatorFilter):
from games.models import Platform
platform_q = self.platform_filter.to_q()
matching_ids = Platform.objects.filter(platform_q).values_list("id", flat=True)
matching_ids = Platform.objects.filter(platform_q).values_list(
"id", flat=True
)
q &= Q(platform_id__in=matching_ids)
sub = self.sub_filter()
@@ -682,9 +738,9 @@ class PurchaseFilter(OperatorFilter):
subquery = subquery.filter(games=game_id)
if criterion.modifier == Modifier.INCLUDES_ONLY:
extra_ids = Game.objects.exclude(
id__in=criterion.value
).values_list("id", flat=True)
extra_ids = Game.objects.exclude(id__in=criterion.value).values_list(
"id", flat=True
)
if extra_ids:
subquery = subquery.exclude(games__in=extra_ids)
@@ -737,9 +793,8 @@ class DeviceFilter(OperatorFilter):
# Free-text search
if self.search is not None and self.search.value:
search_q = (
Q(name__icontains=self.search.value)
| Q(type__icontains=self.search.value)
search_q = Q(name__icontains=self.search.value) | Q(
type__icontains=self.search.value
)
if self.search.modifier == Modifier.EXCLUDES:
search_q = ~search_q
@@ -748,8 +803,11 @@ class DeviceFilter(OperatorFilter):
# Cross-entity filter: session_filter
if self.session_filter is not None:
from games.models import Session
session_q = self.session_filter.to_q()
matching_ids = Session.objects.filter(session_q).values_list("device_id", flat=True)
matching_ids = Session.objects.filter(session_q).values_list(
"device_id", flat=True
)
q &= Q(id__in=matching_ids)
sub = self.sub_filter()
@@ -801,9 +859,8 @@ class PlatformFilter(OperatorFilter):
# Free-text search
if self.search is not None and self.search.value:
search_q = (
Q(name__icontains=self.search.value)
| Q(group__icontains=self.search.value)
search_q = Q(name__icontains=self.search.value) | Q(
group__icontains=self.search.value
)
if self.search.modifier == Modifier.EXCLUDES:
search_q = ~search_q
@@ -812,15 +869,21 @@ class PlatformFilter(OperatorFilter):
# Cross-entity filter: game_filter
if self.game_filter is not None:
from games.models import Game
game_q = self.game_filter.to_q()
matching_ids = Game.objects.filter(game_q).values_list("platform_id", flat=True)
matching_ids = Game.objects.filter(game_q).values_list(
"platform_id", flat=True
)
q &= Q(id__in=matching_ids)
# Cross-entity filter: purchase_filter
if self.purchase_filter is not None:
from games.models import Purchase
purchase_q = self.purchase_filter.to_q()
matching_ids = Purchase.objects.filter(purchase_q).values_list("platform_id", flat=True)
matching_ids = Purchase.objects.filter(purchase_q).values_list(
"platform_id", flat=True
)
q &= Q(id__in=matching_ids)
sub = self.sub_filter()
@@ -877,9 +940,8 @@ class PlayEventFilter(OperatorFilter):
# Free-text search
if self.search is not None and self.search.value:
search_q = (
Q(game__name__icontains=self.search.value)
| Q(note__icontains=self.search.value)
search_q = Q(game__name__icontains=self.search.value) | Q(
note__icontains=self.search.value
)
if self.search.modifier == Modifier.EXCLUDES:
search_q = ~search_q
@@ -888,6 +950,7 @@ class PlayEventFilter(OperatorFilter):
# Cross-entity filter: game_filter
if self.game_filter is not None:
from games.models import Game
game_q = self.game_filter.to_q()
matching_ids = Game.objects.filter(game_q).values_list("id", flat=True)
q &= Q(game_id__in=matching_ids)