Add emulated property to sessions
This commit is contained in:
parent
c2853a3ecc
commit
ba6028e43d
|
@ -8,6 +8,7 @@
|
||||||
* Add all-time stats
|
* Add all-time stats
|
||||||
* Manage purchases
|
* Manage purchases
|
||||||
* Automatically convert purchase prices
|
* Automatically convert purchase prices
|
||||||
|
* Add emulated property to sessions
|
||||||
|
|
||||||
## Improved
|
## Improved
|
||||||
* mark refunded purchases red on game overview
|
* mark refunded purchases red on game overview
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils.safestring import SafeText, mark_safe
|
from django.utils.safestring import SafeText, mark_safe
|
||||||
|
|
||||||
from common.utils import truncate
|
from common.utils import truncate
|
||||||
from games.models import Purchase
|
from games.models import Edition, Game, Purchase, Session
|
||||||
|
|
||||||
HTMLAttribute = tuple[str, str | int | bool]
|
HTMLAttribute = tuple[str, str | int | bool]
|
||||||
HTMLTag = str
|
HTMLTag = str
|
||||||
|
@ -32,7 +32,7 @@ def Component(
|
||||||
attributesList = [f'{name}="{value}"' for name, value in attributes]
|
attributesList = [f'{name}="{value}"' for name, value in attributes]
|
||||||
# make attribute list into a string
|
# make attribute list into a string
|
||||||
# and insert space between tag and attribute list
|
# and insert space between tag and attribute list
|
||||||
attributesBlob = f" {" ".join(attributesList)}"
|
attributesBlob = f" {' '.join(attributesList)}"
|
||||||
tag: str = ""
|
tag: str = ""
|
||||||
if tag_name != "":
|
if tag_name != "":
|
||||||
tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>"
|
tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>"
|
||||||
|
@ -188,27 +188,6 @@ def Icon(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText:
|
|
||||||
link = reverse("view_game", args=[int(game_id)])
|
|
||||||
a_content = Div(
|
|
||||||
[("class", "inline-flex gap-2 items-center")],
|
|
||||||
[
|
|
||||||
Icon(
|
|
||||||
platform.icon,
|
|
||||||
[("title", platform.name)],
|
|
||||||
),
|
|
||||||
PopoverTruncated(name),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
return mark_safe(
|
|
||||||
A(
|
|
||||||
url=link,
|
|
||||||
children=[a_content],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def LinkedPurchase(purchase: Purchase) -> SafeText:
|
def LinkedPurchase(purchase: Purchase) -> SafeText:
|
||||||
link = reverse("view_purchase", args=[int(purchase.id)])
|
link = reverse("view_purchase", args=[int(purchase.id)])
|
||||||
link_content = ""
|
link_content = ""
|
||||||
|
@ -250,19 +229,63 @@ def LinkedPurchase(purchase: Purchase) -> SafeText:
|
||||||
return mark_safe(A(url=link, children=[a_content]))
|
return mark_safe(A(url=link, children=[a_content]))
|
||||||
|
|
||||||
|
|
||||||
def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
|
def NameWithIcon(
|
||||||
|
name: str = "",
|
||||||
|
platform: str = "",
|
||||||
|
game_id: int = 0,
|
||||||
|
session_id: int = 0,
|
||||||
|
purchase_id: int = 0,
|
||||||
|
edition_id: int = 0,
|
||||||
|
linkify: bool = True,
|
||||||
|
emulated: bool = False,
|
||||||
|
) -> SafeText:
|
||||||
|
create_link = False
|
||||||
|
link = ""
|
||||||
|
edition = None
|
||||||
|
platform = None
|
||||||
|
if (
|
||||||
|
game_id != 0 or session_id != 0 or purchase_id != 0 or edition_id != 0
|
||||||
|
) and linkify:
|
||||||
|
create_link = True
|
||||||
|
if session_id:
|
||||||
|
session = Session.objects.get(pk=session_id)
|
||||||
|
emulated = session.emulated
|
||||||
|
edition = session.purchase.first_edition
|
||||||
|
game_id = edition.game.pk
|
||||||
|
if purchase_id:
|
||||||
|
purchase = Purchase.objects.get(pk=purchase_id)
|
||||||
|
edition = purchase.first_edition
|
||||||
|
game_id = purchase.edition.game.pk
|
||||||
|
if edition_id:
|
||||||
|
edition = Edition.objects.get(pk=edition_id)
|
||||||
|
game_id = edition.game.pk
|
||||||
|
if game_id:
|
||||||
|
game = Game.objects.get(pk=game_id)
|
||||||
|
name = edition.name if edition else game.name
|
||||||
|
platform = edition.platform if edition else None
|
||||||
|
link = reverse("view_game", args=[int(game_id)])
|
||||||
content = Div(
|
content = Div(
|
||||||
[("class", "inline-flex gap-2 items-center")],
|
[("class", "inline-flex gap-2 items-center")],
|
||||||
[
|
[
|
||||||
Icon(
|
Icon(
|
||||||
platform.icon,
|
platform.icon,
|
||||||
[("title", platform.name)],
|
[("title", platform.name)],
|
||||||
),
|
)
|
||||||
|
if platform
|
||||||
|
else "",
|
||||||
|
Icon("emulated", [("title", "Emulated")]) if emulated else "",
|
||||||
PopoverTruncated(name),
|
PopoverTruncated(name),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return mark_safe(content)
|
return mark_safe(
|
||||||
|
A(
|
||||||
|
url=link,
|
||||||
|
children=[content],
|
||||||
|
)
|
||||||
|
if create_link
|
||||||
|
else content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def PurchasePrice(purchase) -> str:
|
def PurchasePrice(purchase) -> str:
|
||||||
|
|
|
@ -33,6 +33,7 @@ class SessionForm(forms.ModelForm):
|
||||||
"timestamp_start",
|
"timestamp_start",
|
||||||
"timestamp_end",
|
"timestamp_end",
|
||||||
"duration_manual",
|
"duration_manual",
|
||||||
|
"emulated",
|
||||||
"device",
|
"device",
|
||||||
"note",
|
"note",
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.1.3 on 2025-01-29 11:49
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('games', '0045_alter_purchase_editions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='session',
|
||||||
|
name='emulated',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -141,6 +141,10 @@ class Purchase(models.Model):
|
||||||
)
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def standardized_name(self):
|
||||||
|
return self.name if self.name else self.first_edition.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def first_edition(self):
|
def first_edition(self):
|
||||||
return self.editions.first()
|
return self.editions.first()
|
||||||
|
@ -220,6 +224,8 @@ class Session(models.Model):
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
note = models.TextField(blank=True, null=True)
|
note = models.TextField(blank=True, null=True)
|
||||||
|
emulated = models.BooleanField(default=False)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
modified_at = models.DateTimeField(auto_now=True)
|
modified_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
|
@ -1443,10 +1443,6 @@ input:checked + .toggle-bg {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-4 {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -1475,10 +1471,6 @@ input:checked + .toggle-bg {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
|
||||||
display: list-item;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<c-vars title="Emulated" />
|
||||||
|
<c-svg :title=title viewbox="0 0 48 48">
|
||||||
|
<c-slot name="path">
|
||||||
|
M 8.5 5 C 6.0324991 5 4 7.0324991 4 9.5 L 4 30.5 C 4 32.967501 6.0324991 35 8.5 35 L 17 35 L 17 40 L 13.5 40 A 1.50015 1.50015 0 1 0 13.5 43 L 18.253906 43 A 1.50015 1.50015 0 0 0 18.740234 43 L 29.253906 43 A 1.50015 1.50015 0 0 0 29.740234 43 L 34.5 43 A 1.50015 1.50015 0 1 0 34.5 40 L 31 40 L 31 35 L 39.5 35 C 41.967501 35 44 32.967501 44 30.5 L 44 9.5 C 44 7.0324991 41.967501 5 39.5 5 L 8.5 5 z M 8.5 8 L 39.5 8 C 40.346499 8 41 8.6535009 41 9.5 L 41 30.5 C 41 31.346499 40.346499 32 39.5 32 L 29.746094 32 A 1.50015 1.50015 0 0 0 29.259766 32 L 18.746094 32 A 1.50015 1.50015 0 0 0 18.259766 32 L 8.5 32 C 7.6535009 32 7 31.346499 7 30.5 L 7 9.5 C 7 8.6535009 7.6535009 8 8.5 8 z M 17.5 12 C 16.136406 12 15 13.136406 15 14.5 L 15 25.5 C 15 26.863594 16.136406 28 17.5 28 L 30.5 28 C 31.863594 28 33 26.863594 33 25.5 L 33 14.5 C 33 13.136406 31.863594 12 30.5 12 L 17.5 12 z M 18 18 L 30 18 L 30 25 L 18 25 L 18 18 z M 20 35 L 28 35 L 28 40 L 20 40 L 20 35 z
|
||||||
|
</c-slot>
|
||||||
|
</c-svg>
|
|
@ -11,7 +11,7 @@ from common.components import (
|
||||||
A,
|
A,
|
||||||
Button,
|
Button,
|
||||||
Icon,
|
Icon,
|
||||||
LinkedNameWithPlatformIcon,
|
NameWithIcon,
|
||||||
PopoverTruncated,
|
PopoverTruncated,
|
||||||
)
|
)
|
||||||
from common.time import dateformat, local_strftime
|
from common.time import dateformat, local_strftime
|
||||||
|
@ -54,11 +54,7 @@ def list_editions(request: HttpRequest) -> HttpResponse:
|
||||||
],
|
],
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
LinkedNameWithPlatformIcon(
|
NameWithIcon(edition_id=edition.pk),
|
||||||
name=edition.name,
|
|
||||||
game_id=edition.game.id,
|
|
||||||
platform=edition.platform,
|
|
||||||
),
|
|
||||||
PopoverTruncated(
|
PopoverTruncated(
|
||||||
edition.name
|
edition.name
|
||||||
if edition.game.name != edition.name
|
if edition.game.name != edition.name
|
||||||
|
|
|
@ -14,7 +14,7 @@ from common.components import (
|
||||||
Div,
|
Div,
|
||||||
Icon,
|
Icon,
|
||||||
LinkedPurchase,
|
LinkedPurchase,
|
||||||
NameWithPlatformIcon,
|
NameWithIcon,
|
||||||
Popover,
|
Popover,
|
||||||
PopoverTruncated,
|
PopoverTruncated,
|
||||||
PurchasePrice,
|
PurchasePrice,
|
||||||
|
@ -67,18 +67,7 @@ def list_games(request: HttpRequest) -> HttpResponse:
|
||||||
],
|
],
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
A(
|
NameWithIcon(game_id=game.pk),
|
||||||
[
|
|
||||||
(
|
|
||||||
"href",
|
|
||||||
reverse(
|
|
||||||
"view_game",
|
|
||||||
args=[game.pk],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
PopoverTruncated(game.name),
|
|
||||||
),
|
|
||||||
PopoverTruncated(
|
PopoverTruncated(
|
||||||
game.sort_name
|
game.sort_name
|
||||||
if game.sort_name is not None and game.name != game.sort_name
|
if game.sort_name is not None and game.name != game.sort_name
|
||||||
|
@ -212,10 +201,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
||||||
],
|
],
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
NameWithPlatformIcon(
|
NameWithIcon(edition_id=edition.pk),
|
||||||
name=edition.name,
|
|
||||||
platform=edition.platform,
|
|
||||||
),
|
|
||||||
edition.year_released,
|
edition.year_released,
|
||||||
render_to_string(
|
render_to_string(
|
||||||
"cotton/button_group.html",
|
"cotton/button_group.html",
|
||||||
|
@ -321,13 +307,10 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
||||||
"columns": ["Edition", "Date", "Duration", "Actions"],
|
"columns": ["Edition", "Date", "Duration", "Actions"],
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
NameWithPlatformIcon(
|
NameWithIcon(
|
||||||
name=session.purchase.name
|
session_id=session.pk,
|
||||||
if session.purchase.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 ""}",
|
f"{local_strftime(session.timestamp_start)}{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}",
|
||||||
(
|
(
|
||||||
format_duration(session.duration_calculated, durationformat)
|
format_duration(session.duration_calculated, durationformat)
|
||||||
if session.duration_calculated
|
if session.duration_calculated
|
||||||
|
|
|
@ -15,7 +15,7 @@ from common.components import (
|
||||||
Div,
|
Div,
|
||||||
Form,
|
Form,
|
||||||
Icon,
|
Icon,
|
||||||
LinkedNameWithPlatformIcon,
|
NameWithIcon,
|
||||||
Popover,
|
Popover,
|
||||||
)
|
)
|
||||||
from common.time import (
|
from common.time import (
|
||||||
|
@ -130,12 +130,8 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
||||||
],
|
],
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
LinkedNameWithPlatformIcon(
|
NameWithIcon(session_id=session.pk),
|
||||||
name=session.purchase.first_edition.name,
|
f"{local_strftime(session.timestamp_start)}{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}",
|
||||||
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 ""}",
|
|
||||||
(
|
(
|
||||||
format_duration(session.duration_calculated, durationformat)
|
format_duration(session.duration_calculated, durationformat)
|
||||||
if session.duration_calculated
|
if session.duration_calculated
|
||||||
|
|
Loading…
Reference in New Issue