Streamline evaluating game status

This commit is contained in:
2026-05-12 12:44:06 +02:00
parent 913c7d3a98
commit bf2b86ba1f
3 changed files with 77 additions and 31 deletions
+16 -2
View File
@@ -4,7 +4,7 @@ from datetime import timedelta
import requests import requests
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import F, Sum from django.db.models import F, Q, Sum
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
from django.db.models.fields.generated import GeneratedField from django.db.models.fields.generated import GeneratedField
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
@@ -66,7 +66,8 @@ class Game(models.Model):
return self.name return self.name
def finished(self): def finished(self):
return self.status == self.Status.FINISHED return (self.status == self.Status.FINISHED or
self.playevents.filter(ended__isnull=False).exists())
def abandoned(self): def abandoned(self):
return self.status == self.Status.ABANDONED return self.status == self.Status.ABANDONED
@@ -120,6 +121,19 @@ class PurchaseQueryset(models.QuerySet):
def games_only(self): def games_only(self):
return self.filter(type=Purchase.GAME) return self.filter(type=Purchase.GAME)
def finished(self):
return self.filter(
Q(games__status="f") | Q(games__playevents__ended__isnull=False)
).distinct()
def abandoned(self):
return self.filter(games__status="a").distinct()
def dropped(self):
return self.filter(
Q(games__status="a") | Q(date_refunded__isnull=False)
).distinct()
class Purchase(models.Model): class Purchase(models.Model):
PHYSICAL = "ph" PHYSICAL = "ph"
+55 -25
View File
@@ -2,9 +2,9 @@ from datetime import datetime, timedelta
from typing import Any, Callable from typing import Any, Callable
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Avg, Count, ExpressionWrapper, F, Prefetch, Q, Sum, fields from django.db.models import Avg, Count, ExpressionWrapper, F, Max, OuterRef, Prefetch, Q, Subquery, Sum, fields
from django.db.models.functions import TruncDate, TruncMonth from django.db.models.functions import TruncDate, TruncMonth
from django.db.models.manager import BaseManager
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
@@ -90,26 +90,34 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
this_year_purchases = Purchase.objects.all() this_year_purchases = Purchase.objects.all()
this_year_purchases_with_currency = this_year_purchases.select_related("games") this_year_purchases_with_currency = this_year_purchases.select_related("games")
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter( this_year_purchases_without_refunded = Purchase.objects.filter(
date_refunded=None date_refunded=None
) )
this_year_purchases_refunded = this_year_purchases_with_currency.refunded() this_year_purchases_refunded = Purchase.objects.refunded()
this_year_purchases_unfinished_dropped_nondropped = ( this_year_purchases_unfinished_dropped_nondropped = (
this_year_purchases_without_refunded.filter(date_finished__isnull=True) this_year_purchases_without_refunded.filter(
~Q(games__status="f")
& ~Q(games__playevents__ended__isnull=False)
)
.filter(infinite=False) .filter(infinite=False)
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC)) .filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
) # do not count battle passes etc. ) # do not count battle passes etc.
this_year_purchases_unfinished = ( this_year_purchases_unfinished = (
this_year_purchases_unfinished_dropped_nondropped.filter( this_year_purchases_unfinished_dropped_nondropped.filter(
date_dropped__isnull=True ~Q(games__status="r")
& ~Q(games__status="a")
) )
) )
this_year_purchases_dropped = ( this_year_purchases_dropped = (
this_year_purchases_unfinished_dropped_nondropped.filter( this_year_purchases.filter(
date_dropped__isnull=False ~Q(games__status="f")
& ~Q(games__playevents__ended__isnull=False)
) )
.filter(Q(games__status="a") | Q(date_refunded__isnull=False))
.filter(infinite=False)
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
) )
this_year_purchases_without_refunded_count = ( this_year_purchases_without_refunded_count = (
@@ -124,13 +132,28 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
* 100 * 100
) )
purchases_finished_this_year: BaseManager[Purchase] = Purchase.objects.finished() _finished_purchases_qs = Purchase.objects.finished()
purchases_finished_this_year_released_this_year = ( _finished_with_date = _finished_purchases_qs.annotate(
purchases_finished_this_year.all().order_by("date_finished") date_finished=Subquery(
Purchase.objects.filter(pk=OuterRef("pk"))
.annotate(max_ended=Max("games__playevents__ended"))
.values("max_ended")[:1]
)
)
purchases_finished_this_year = _finished_with_date
purchases_finished_this_year_released_this_year = _finished_with_date.order_by(
"-date_finished"
) )
purchased_this_year_finished_this_year = ( purchased_this_year_finished_this_year = (
this_year_purchases_without_refunded.all() this_year_purchases_without_refunded.filter(pk__in=_finished_purchases_qs.values("pk"))
).order_by("date_finished") .annotate(
date_finished=Subquery(
Purchase.objects.filter(pk=OuterRef("pk"))
.annotate(max_ended=Max("games__playevents__ended"))
.values("max_ended")[:1]
)
)
).order_by("-date_finished")
this_year_spendings = this_year_purchases_without_refunded.aggregate( this_year_spendings = this_year_purchases_without_refunded.aggregate(
total_spent=Sum(F("converted_price")) total_spent=Sum(F("converted_price"))
@@ -139,7 +162,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
games_with_playtime = Game.objects.filter( games_with_playtime = Game.objects.filter(
sessions__in=this_year_sessions sessions__in=this_year_sessions
).distinct() ).distinct().annotate(
total_playtime=Sum(F("sessions__duration_total"))
).filter(total_playtime__gt=timedelta(0))
month_playtimes = ( month_playtimes = (
this_year_sessions.annotate(month=TruncMonth("timestamp_start")) this_year_sessions.annotate(month=TruncMonth("timestamp_start"))
.values("month") .values("month")
@@ -166,7 +191,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
) )
backlog_decrease_count = ( backlog_decrease_count = (
Purchase.objects.all().intersection(purchases_finished_this_year).count() purchases_finished_this_year.count()
) )
first_play_date = "N/A" first_play_date = "N/A"
@@ -310,25 +335,30 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
# not infinite # not infinite
# only Game and DLC # only Game and DLC
this_year_purchases_unfinished_dropped_nondropped = ( this_year_purchases_unfinished_dropped_nondropped = (
this_year_purchases_without_refunded.exclude( this_year_purchases_without_refunded.filter(
games__in=Game.objects.filter(status="f") ~Q(games__status="f")
& ~Q(games__playevents__ended__year=year)
) )
.filter(infinite=False) .filter(infinite=False)
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC)) .filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
) )
# not finished # unfinished = not finished AND not dropped
this_year_purchases_unfinished = ( this_year_purchases_unfinished = (
this_year_purchases_unfinished_dropped_nondropped.exclude( this_year_purchases_unfinished_dropped_nondropped.filter(
games__status__in="ura" ~Q(games__status="r")
& ~Q(games__status="a")
) )
) )
# abandoned # dropped = abandoned OR retired OR refunded (OR logic for transition)
# retired
this_year_purchases_dropped = ( this_year_purchases_dropped = (
this_year_purchases_unfinished_dropped_nondropped.exclude( this_year_purchases.filter(
games__in=Game.objects.filter(status="ar") ~Q(games__status="f")
& ~Q(games__playevents__ended__year=year)
) )
.filter(Q(games__status="a") | Q(date_refunded__isnull=False))
.filter(infinite=False)
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
) )
this_year_purchases_without_refunded_count = ( this_year_purchases_without_refunded_count = (
@@ -343,7 +373,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
* 100 * 100
) )
purchases_finished_this_year = Purchase.objects.filter( purchases_finished_this_year = Purchase.objects.finished().filter(
games__playevents__ended__year=year games__playevents__ended__year=year
).annotate(game_name=F("games__name"), date_finished=F("games__playevents__ended")) ).annotate(game_name=F("games__name"), date_finished=F("games__playevents__ended"))
purchases_finished_this_year_released_this_year = ( purchases_finished_this_year_released_this_year = (
+6 -4
View File
@@ -190,8 +190,9 @@ def view_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
@login_required @login_required
def drop_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse: def drop_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
purchase = get_object_or_404(Purchase, id=purchase_id) purchase = get_object_or_404(Purchase, id=purchase_id)
purchase.date_dropped = timezone.now() for game in purchase.games.all():
purchase.save() game.status = Game.Status.ABANDONED
game.save()
return redirect("games:list_purchases") return redirect("games:list_purchases")
@@ -233,8 +234,9 @@ def refund_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
@login_required @login_required
def finish_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse: def finish_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
purchase = get_object_or_404(Purchase, id=purchase_id) purchase = get_object_or_404(Purchase, id=purchase_id)
purchase.date_finished = timezone.now() for game in purchase.games.all():
purchase.save() game.status = Game.Status.FINISHED
game.save()
return redirect("games:list_purchases") return redirect("games:list_purchases")