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

@ -16,7 +16,7 @@ class SessionForm(forms.ModelForm):
# queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name")
# )
purchase = forms.ModelChoiceField(
queryset=Purchase.objects.order_by("edition__sort_name"),
queryset=Purchase.objects.all(),
widget=forms.Select(attrs={"autofocus": "autofocus"}),
)
@ -38,12 +38,12 @@ class SessionForm(forms.ModelForm):
]
class EditionChoiceField(forms.ModelChoiceField):
class EditionChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj) -> str:
return f"{obj.sort_name} ({obj.platform}, {obj.year_released})"
class IncludePlatformSelect(forms.Select):
class IncludePlatformSelect(forms.SelectMultiple):
def create_option(self, name, value, *args, **kwargs):
option = super().create_option(name, value, *args, **kwargs)
if platform_id := safe_getattr(value, "instance.platform.id"):
@ -58,7 +58,7 @@ class PurchaseForm(forms.ModelForm):
# Automatically update related_purchase <select/>
# to only include purchases of the selected edition.
related_purchase_by_edition_url = reverse("related_purchase_by_edition")
self.fields["edition"].widget.attrs.update(
self.fields["editions"].widget.attrs.update(
{
"hx-trigger": "load, click",
"hx-get": related_purchase_by_edition_url,
@ -67,15 +67,13 @@ class PurchaseForm(forms.ModelForm):
}
)
edition = EditionChoiceField(
editions = EditionChoiceField(
queryset=Edition.objects.order_by("sort_name"),
widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}),
)
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
related_purchase = forms.ModelChoiceField(
queryset=Purchase.objects.filter(type=Purchase.GAME).order_by(
"edition__sort_name"
),
queryset=Purchase.objects.filter(type=Purchase.GAME),
required=False,
)
@ -88,7 +86,7 @@ class PurchaseForm(forms.ModelForm):
}
model = Purchase
fields = [
"edition",
"editions",
"platform",
"date_purchased",
"date_refunded",

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.3 on 2025-01-07 20:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0041_purchase_converted_currency_purchase_converted_price_and_more'),
]
operations = [
migrations.AddField(
model_name='purchase',
name='editions_temp',
field=models.ManyToManyField(blank=True, related_name='temp_purchases', to='games.edition'),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 5.1.3 on 2025-01-07 20:17
from django.db import migrations
def migrate_edition_to_editions_temp(apps, schema_editor):
Purchase = apps.get_model("games", "Purchase")
for purchase in Purchase.objects.all():
if purchase.edition:
print(
f"Migrating Purchase {purchase.id} with Edition {purchase.edition.id}"
)
purchase.editions_temp.add(purchase.edition)
print(purchase.editions_temp.all())
purchase.save()
else:
print(f"No edition found for Purchase {purchase.id}")
class Migration(migrations.Migration):
dependencies = [
("games", "0042_purchase_editions_temp"),
]
operations = [
migrations.RunPython(migrate_edition_to_editions_temp),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.3 on 2025-01-07 20:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("games", "0043_auto_20250107_2117"),
]
operations = [
migrations.RemoveField(model_name="purchase", name="edition"),
migrations.RenameField(
model_name="purchase",
old_name="editions_temp",
new_name="editions",
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.3 on 2025-01-07 20:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0044_auto_20250107_2132'),
]
operations = [
migrations.AlterField(
model_name='purchase',
name='editions',
field=models.ManyToManyField(blank=True, related_name='purchases', to='games.edition'),
),
]

View File

@ -113,7 +113,7 @@ class Purchase(models.Model):
objects = PurchaseQueryset().as_manager()
edition = models.ForeignKey(Edition, on_delete=models.CASCADE)
editions = models.ManyToManyField(Edition, related_name="purchases", blank=True)
platform = models.ForeignKey(
Platform, on_delete=models.CASCADE, default=None, null=True, blank=True
)
@ -141,26 +141,28 @@ class Purchase(models.Model):
)
created_at = models.DateTimeField(auto_now_add=True)
@property
def first_edition(self):
return self.editions.first()
def __str__(self):
additional_info = [
self.get_type_display() if self.type != Purchase.GAME else "",
(
f"{self.edition.platform} version on {self.platform}"
if self.platform != self.edition.platform
f"{self.first_edition.platform} version on {self.platform}"
if self.platform != self.first_edition.platform
else self.platform
),
self.edition.year_released,
self.first_edition.year_released,
self.get_ownership_type_display(),
]
return f"{self.edition} ({', '.join(filter(None, map(str, additional_info)))})"
return f"{self.first_edition} ({', '.join(filter(None, map(str, additional_info)))})"
def is_game(self):
return self.type == self.GAME
def save(self, *args, **kwargs):
if self.type == Purchase.GAME:
self.name = ""
elif self.type != Purchase.GAME and not self.related_purchase:
if self.type != Purchase.GAME and not self.related_purchase:
raise ValidationError(
f"{self.get_type_display()} must have a related purchase."
)

View File

@ -1443,6 +1443,10 @@ input:checked + .toggle-bg {
margin-top: 1rem;
}
.ml-4 {
margin-left: 1rem;
}
.block {
display: block;
}
@ -1471,6 +1475,10 @@ input:checked + .toggle-bg {
display: grid;
}
.list-item {
display: list-item;
}
.hidden {
display: none;
}
@ -1652,6 +1660,14 @@ input:checked + .toggle-bg {
resize: both;
}
.list-inside {
list-style-position: inside;
}
.list-disc {
list-style-type: disc;
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}

View File

@ -2,11 +2,11 @@
{% load static %}
{% partialdef purchase-name %}
{% if purchase.type != 'game' %}
<c-gamelink :game_id=purchase.edition.game.id>
{{ purchase.name }} ({{ purchase.edition.name }} {{ purchase.get_type_display }})
<c-gamelink :game_id=purchase.first_edition.game.id>
{{ purchase.name }} ({{ purchase.first_edition.edition.name }} {{ purchase.get_type_display }})
</c-gamelink>
{% else %}
<c-gamelink :game_id=purchase.edition.game.id :name=purchase.edition.name />
<c-gamelink :game_id=purchase.first_edition.game.id :name=purchase.first_edition.name />
{% endif %}
{% endpartialdef %}
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">

View File

@ -0,0 +1,34 @@
<c-layouts.base>
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
<div class="flex flex-col gap-5 mb-3">
<span class="text-balance max-w-[30rem] text-4xl">
<span class="font-bold font-serif">{% if purchase.name %}{{ purchase.name }}{% else %}Unnamed purchase{% endif %}</span> <span class="text-slate-500 text-2xl">({{ purchase.editions.count }} games)</span>
</span>
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
<a href="{% url 'edit_purchase' purchase.id %}">
<button type="button"
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white">
Edit
</button>
</a>
<a href="{% url 'delete_purchase' purchase.id %}">
<button type="button"
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-red-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white">
Delete
</button>
</a>
</div>
<div>Price: {{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }} ({{ purchase.price | floatformat }} {{ purchase.price_currency }})</div>
<div>
<h2 class="text-base">Items:</h2>
<ul class="list-disc list-inside">
{% for edition in purchase.editions.all %}
<li><c-gamelink :game_id=edition.game.id :name=edition.name /></li>
{% endfor %}
</ul>
</div>
</div>
</div>
</c-layouts.base>

View File

@ -54,6 +54,11 @@ urlpatterns = [
purchase.delete_purchase,
name="delete_purchase",
),
path(
"purchase/<int:purchase_id>/view",
purchase.view_purchase,
name="view_purchase",
),
path(
"purchase/<int:purchase_id>/finish",
purchase.finish_purchase,

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 ""}",