purchases can now refer to multiple editions
All checks were successful
Django CI/CD / test (push) Successful in 1m3s
Django CI/CD / build-and-push (push) Successful in 2m36s

allows purchases to be for more than one game
This commit is contained in:
2025-01-08 21:00:19 +01:00
parent cd90d60475
commit c2853a3ecc
16 changed files with 308 additions and 84 deletions

View File

@ -13,6 +13,7 @@ from common.components import (
Button,
Div,
Icon,
LinkedPurchase,
NameWithPlatformIcon,
Popover,
PopoverTruncated,
@ -162,7 +163,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
to_attr="nongame_related_purchases",
)
game_purchases_prefetch: Prefetch[Purchase] = Prefetch(
"purchase_set",
"purchases",
queryset=Purchase.objects.filter(type=Purchase.GAME).prefetch_related(
nongame_related_purchases_prefetch
),
@ -174,14 +175,14 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
.order_by("year_released")
)
purchases = Purchase.objects.filter(edition__game=game).order_by("date_purchased")
purchases = Purchase.objects.filter(editions__game=game).order_by("date_purchased")
sessions = Session.objects.prefetch_related("device").filter(
purchase__edition__game=game
purchase__editions__game=game
)
session_count = sessions.count()
session_count_without_manual = (
Session.objects.without_manual().filter(purchase__edition__game=game).count()
Session.objects.without_manual().filter(purchase__editions__game=game).count()
)
if sessions:
@ -242,10 +243,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
"columns": ["Name", "Type", "Date", "Price", "Actions"],
"rows": [
[
NameWithPlatformIcon(
name=purchase.name if purchase.name else purchase.edition.name,
platform=purchase.platform,
),
LinkedPurchase(purchase),
purchase.get_type_display(),
purchase.date_purchased.strftime(dateformat),
PurchasePrice(purchase),
@ -271,7 +269,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
],
}
sessions_all = Session.objects.filter(purchase__edition__game=game).order_by(
sessions_all = Session.objects.filter(purchase__editions__game=game).order_by(
"-timestamp_start"
)
last_session = None
@ -300,7 +298,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
args=[last_session.pk],
),
children=Popover(
popover_content=last_session.purchase.edition.name,
popover_content=last_session.purchase.first_edition.name,
children=[
Button(
icon=True,
@ -308,7 +306,9 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
size="xs",
children=[
Icon("play"),
truncate(f"{last_session.purchase.edition.name}"),
truncate(
f"{last_session.purchase.first_edition.name}"
),
],
)
],
@ -324,7 +324,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
NameWithPlatformIcon(
name=session.purchase.name
if session.purchase.name
else session.purchase.edition.name,
else session.purchase.first_edition.name,
platform=session.purchase.platform,
),
f"{local_strftime(session.timestamp_start)}{f"{local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}",
@ -375,7 +375,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
"editions": editions,
"game": game,
"playrange": playrange,
"purchase_count": Purchase.objects.filter(edition__game=game).count(),
"purchase_count": Purchase.objects.filter(editions__game=game).count(),
"session_average_without_manual": round(
safe_division(
total_hours_without_manual, int(session_count_without_manual)

View File

@ -2,7 +2,7 @@ from datetime import datetime
from typing import Any, Callable
from django.contrib.auth.decorators import login_required
from django.db.models import Avg, Count, ExpressionWrapper, F, Q, Sum, fields
from django.db.models import Avg, Count, ExpressionWrapper, F, Prefetch, Q, Sum, fields
from django.db.models.functions import TruncDate, TruncMonth
from django.db.models.manager import BaseManager
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
@ -49,7 +49,9 @@ def use_custom_redirect(
@login_required
def stats_alltime(request: HttpRequest) -> HttpResponse:
year = "Alltime"
this_year_sessions = Session.objects.all().select_related("purchase__edition")
this_year_sessions = Session.objects.all().prefetch_related(
Prefetch("purchase__editions")
)
this_year_sessions_with_durations = this_year_sessions.annotate(
duration=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"),
@ -58,10 +60,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
)
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
this_year_games = Game.objects.filter(
edition__purchase__session__in=this_year_sessions
editions__purchase__session__in=this_year_sessions
).distinct()
this_year_games_with_session_counts = this_year_games.annotate(
session_count=Count("edition__purchase__session"),
session_count=Count("editions__purchase__session"),
)
game_highest_session_count = this_year_games_with_session_counts.order_by(
"-session_count"
@ -78,7 +80,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
).distinct()
this_year_purchases = Purchase.objects.all()
this_year_purchases_with_currency = this_year_purchases.select_related("edition")
this_year_purchases_with_currency = this_year_purchases.select_related("editions")
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
date_refunded=None
)
@ -127,11 +129,11 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
total_spent = this_year_spendings["total_spent"] or 0
games_with_playtime = (
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
Game.objects.filter(editions__purchase__session__in=this_year_sessions)
.annotate(
total_playtime=Sum(
F("edition__purchase__session__duration_calculated")
+ F("edition__purchase__session__duration_manual")
F("editions__purchase__session__duration_calculated")
+ F("editions__purchase__session__duration_manual")
)
)
.values("id", "name", "total_playtime")
@ -146,9 +148,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
month["playtime"] = format_duration(month["playtime"], "%2.0H")
highest_session_average_game = (
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
Game.objects.filter(editions__purchase__session__in=this_year_sessions)
.annotate(
session_average=Avg("edition__purchase__session__duration_calculated")
session_average=Avg("editions__purchase__session__duration_calculated")
)
.order_by("-session_average")
.first()
@ -175,10 +177,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
last_play_date = "N/A"
if this_year_sessions:
first_session = this_year_sessions.earliest()
first_play_game = first_session.purchase.edition.game
first_play_game = first_session.purchase.first_edition.game
first_play_date = first_session.timestamp_start.strftime(dateformat)
last_session = this_year_sessions.latest()
last_play_game = last_session.purchase.edition.game
last_play_game = last_session.purchase.first_edition.game
last_play_date = last_session.timestamp_start.strftime(dateformat)
all_purchased_this_year_count = this_year_purchases_with_currency.count()
@ -227,7 +229,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
else 0
),
"longest_session_game": (
longest_session.purchase.edition.game if longest_session else None
longest_session.purchase.first_edition.game if longest_session else None
),
"highest_session_count": (
game_highest_session_count.session_count
@ -266,7 +268,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
return HttpResponseRedirect(reverse("stats_alltime"))
this_year_sessions = Session.objects.filter(
timestamp_start__year=year
).select_related("purchase__edition")
).prefetch_related("purchase__editions")
this_year_sessions_with_durations = this_year_sessions.annotate(
duration=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"),
@ -275,12 +277,12 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
)
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
this_year_games = Game.objects.filter(
edition__purchase__session__in=this_year_sessions
edition__purchases__session__in=this_year_sessions
).distinct()
this_year_games_with_session_counts = this_year_games.annotate(
session_count=Count(
"edition__purchase__session",
filter=Q(edition__purchase__session__timestamp_start__year=year),
"edition__purchases__session",
filter=Q(edition__purchases__session__timestamp_start__year=year),
)
)
game_highest_session_count = this_year_games_with_session_counts.order_by(
@ -298,7 +300,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
).distinct()
this_year_purchases = Purchase.objects.filter(date_purchased__year=year)
this_year_purchases_with_currency = this_year_purchases.select_related("edition")
this_year_purchases_with_currency = this_year_purchases.prefetch_related("editions")
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
date_refunded=None
).exclude(ownership_type=Purchase.DEMO)
@ -335,7 +337,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year)
purchases_finished_this_year_released_this_year = (
purchases_finished_this_year.filter(edition__year_released=year).order_by(
purchases_finished_this_year.filter(editions__year_released=year).order_by(
"date_finished"
)
)
@ -349,11 +351,11 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
total_spent = this_year_spendings["total_spent"] or 0
games_with_playtime = (
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
Game.objects.filter(edition__purchases__session__in=this_year_sessions)
.annotate(
total_playtime=Sum(
F("edition__purchase__session__duration_calculated")
+ F("edition__purchase__session__duration_manual")
F("edition__purchases__session__duration_calculated")
+ F("edition__purchases__session__duration_manual")
)
)
.values("id", "name", "total_playtime")
@ -368,9 +370,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
month["playtime"] = format_duration(month["playtime"], "%2.0H")
highest_session_average_game = (
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
Game.objects.filter(edition__purchases__session__in=this_year_sessions)
.annotate(
session_average=Avg("edition__purchase__session__duration_calculated")
session_average=Avg("edition__purchases__session__duration_calculated")
)
.order_by("-session_average")
.first()
@ -401,10 +403,10 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
last_play_game = None
if this_year_sessions:
first_session = this_year_sessions.earliest()
first_play_game = first_session.purchase.edition.game
first_play_game = first_session.purchase.first_edition.game
first_play_date = first_session.timestamp_start.strftime(dateformat)
last_session = this_year_sessions.latest()
last_play_game = last_session.purchase.edition.game
last_play_game = last_session.purchase.first_edition.game
last_play_date = last_session.timestamp_start.strftime(dateformat)
all_purchased_this_year_count = this_year_purchases_with_currency.count()
@ -421,7 +423,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
),
"total_games": this_year_played_purchases.count(),
"total_2023_games": this_year_played_purchases.filter(
edition__year_released=year
editions__year_released=year
).count(),
"top_10_games_by_playtime": top_10_games_by_playtime,
"year": year,
@ -432,16 +434,16 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
"spent_per_game": int(
safe_division(total_spent, this_year_purchases_without_refunded_count)
),
"all_finished_this_year": purchases_finished_this_year.select_related(
"edition"
"all_finished_this_year": purchases_finished_this_year.prefetch_related(
"editions"
).order_by("date_finished"),
"all_finished_this_year_count": purchases_finished_this_year.count(),
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.select_related(
"edition"
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.prefetch_related(
"editions"
).order_by("date_finished"),
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(),
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.select_related(
"edition"
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.prefetch_related(
"editions"
).order_by("date_finished"),
"total_sessions": this_year_sessions.count(),
"unique_days": unique_days["dates"],
@ -471,7 +473,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
else 0
),
"longest_session_game": (
longest_session.purchase.edition.game if longest_session else None
longest_session.purchase.first_edition.game if longest_session else None
),
"highest_session_count": (
game_highest_session_count.session_count

View File

@ -13,7 +13,7 @@ from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from common.components import A, Button, Icon, LinkedNameWithPlatformIcon, PurchasePrice
from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice
from common.time import dateformat
from games.forms import PurchaseForm
from games.models import Edition, Purchase
@ -58,11 +58,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
],
"rows": [
[
LinkedNameWithPlatformIcon(
name=purchase.edition.name,
game_id=purchase.edition.game.pk,
platform=purchase.platform,
),
LinkedPurchase(purchase),
purchase.get_type_display(),
PurchasePrice(purchase),
purchase.infinite,
@ -173,7 +169,7 @@ def add_purchase(request: HttpRequest, edition_id: int = 0) -> HttpResponse:
context["form"] = form
context["title"] = "Add New Purchase"
context["script_name"] = "add_purchase.js"
# context["script_name"] = "add_purchase.js"
return render(request, "add_purchase.html", context)
@ -189,7 +185,7 @@ def edit_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
context["title"] = "Edit Purchase"
context["form"] = form
context["purchase_id"] = str(purchase_id)
context["script_name"] = "add_purchase.js"
# context["script_name"] = "add_purchase.js"
return render(request, "add_purchase.html", context)
@ -200,6 +196,12 @@ def delete_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
return redirect("list_purchases")
@login_required
def view_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
purchase = get_object_or_404(Purchase, id=purchase_id)
return render(request, "view_purchase.html", {"purchase": purchase})
@login_required
def drop_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
purchase = get_object_or_404(Purchase, id=purchase_id)

View File

@ -97,7 +97,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
args=[last_session.pk],
),
children=Popover(
popover_content=last_session.purchase.edition.name,
popover_content=last_session.purchase.first_edition.name,
children=[
Button(
icon=True,
@ -106,7 +106,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
children=[
Icon("play"),
truncate(
f"{last_session.purchase.edition.name}"
f"{last_session.purchase.first_edition.name}"
),
],
)
@ -131,8 +131,8 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
"rows": [
[
LinkedNameWithPlatformIcon(
name=session.purchase.edition.name,
game_id=session.purchase.edition.game.pk,
name=session.purchase.first_edition.name,
game_id=session.purchase.first_edition.game.pk,
platform=session.purchase.platform,
),
f"{local_strftime(session.timestamp_start)}{f"{local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}",