diff --git a/games/templates/view_purchase.html b/games/templates/view_purchase.html
new file mode 100644
index 0000000..aa3290f
--- /dev/null
+++ b/games/templates/view_purchase.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+ {% if purchase.name %}{{ purchase.name }}{% else %}Unnamed purchase{% endif %} ({{ purchase.editions.count }} games)
+
+
+
Price: {{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }} ({{ purchase.price | floatformat }} {{ purchase.price_currency }})
+
+
Items:
+
+ {% for edition in purchase.editions.all %}
+
+ {% endfor %}
+
+
+
+
+
+
diff --git a/games/urls.py b/games/urls.py
index 946b64d..ccb398f 100644
--- a/games/urls.py
+++ b/games/urls.py
@@ -54,6 +54,11 @@ urlpatterns = [
purchase.delete_purchase,
name="delete_purchase",
),
+ path(
+ "purchase/
/view",
+ purchase.view_purchase,
+ name="view_purchase",
+ ),
path(
"purchase//finish",
purchase.finish_purchase,
diff --git a/games/views/game.py b/games/views/game.py
index d55fb14..0aac65c 100644
--- a/games/views/game.py
+++ b/games/views/game.py
@@ -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)
diff --git a/games/views/general.py b/games/views/general.py
index 3ca554e..4834046 100644
--- a/games/views/general.py
+++ b/games/views/general.py
@@ -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
diff --git a/games/views/purchase.py b/games/views/purchase.py
index 9191925..daa6424 100644
--- a/games/views/purchase.py
+++ b/games/views/purchase.py
@@ -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)
diff --git a/games/views/session.py b/games/views/session.py
index 0c38eb3..f886a9b 100644
--- a/games/views/session.py
+++ b/games/views/session.py
@@ -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 ""}",