Compare commits
21 Commits
remove_edi
...
fbd418882b
Author | SHA1 | Date | |
---|---|---|---|
fbd418882b
|
|||
6aef44d3dc
|
|||
63e307d251
|
|||
b0eb28618c
|
|||
b05a0bd502
|
|||
4fd5b8432b
|
|||
cbb1cbaf33
|
|||
29b71a57cc
|
|||
4db8d1a63b
|
|||
bf9f3d5f56
|
|||
9a7da8a9ec
|
|||
013375106b
|
|||
d1cb0edf87
|
|||
8b939e8e6f
|
|||
e5f9d90127
|
|||
3b297bfc1e
|
|||
4a0f5761e8
|
|||
34b2c8c751
|
|||
05b59e41b5
|
|||
a32996ccc3
|
|||
527cf4ba52 |
20
.pre-commit-config.yaml
Normal file
20
.pre-commit-config.yaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
repos:
|
||||||
|
# disable due to incomaptible formatting between
|
||||||
|
# black and ruff
|
||||||
|
# TODO: replace with ruff when it works on NixOS
|
||||||
|
# - repo: https://github.com/psf/black
|
||||||
|
# rev: 24.8.0
|
||||||
|
# hooks:
|
||||||
|
# - id: black
|
||||||
|
- repo: https://github.com/pycqa/isort
|
||||||
|
rev: 5.13.2
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
name: isort (python)
|
||||||
|
- repo: https://github.com/Riverside-Healthcare/djLint
|
||||||
|
rev: v1.34.0
|
||||||
|
hooks:
|
||||||
|
- id: djlint-reformat-django
|
||||||
|
args: ["--ignore", "H011"]
|
||||||
|
- id: djlint-django
|
||||||
|
args: ["--ignore", "H011"]
|
@ -7,7 +7,6 @@
|
|||||||
* Allow deleting purchases
|
* Allow deleting purchases
|
||||||
* Add all-time stats
|
* Add all-time stats
|
||||||
* Manage purchases
|
* Manage purchases
|
||||||
* Automatically convert purchase prices
|
|
||||||
|
|
||||||
## Improved
|
## Improved
|
||||||
* mark refunded purchases red on game overview
|
* mark refunded purchases red on game overview
|
||||||
|
@ -3,13 +3,11 @@ from string import ascii_lowercase
|
|||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from django.template import TemplateDoesNotExist
|
from django.template import TemplateDoesNotExist
|
||||||
from django.template.defaultfilters import floatformat
|
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import NoReverseMatch, reverse
|
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
|
|
||||||
|
|
||||||
HTMLAttribute = tuple[str, str | int | bool]
|
HTMLAttribute = tuple[str, str | int | bool]
|
||||||
HTMLTag = str
|
HTMLTag = str
|
||||||
@ -52,7 +50,6 @@ def randomid(seed: str = "", length: int = 10) -> str:
|
|||||||
def Popover(
|
def Popover(
|
||||||
popover_content: str,
|
popover_content: str,
|
||||||
wrapped_content: str = "",
|
wrapped_content: str = "",
|
||||||
wrapped_classes: str = "",
|
|
||||||
children: list[HTMLTag] = [],
|
children: list[HTMLTag] = [],
|
||||||
attributes: list[HTMLAttribute] = [],
|
attributes: list[HTMLAttribute] = [],
|
||||||
) -> str:
|
) -> str:
|
||||||
@ -65,43 +62,17 @@ def Popover(
|
|||||||
("id", id),
|
("id", id),
|
||||||
("wrapped_content", wrapped_content),
|
("wrapped_content", wrapped_content),
|
||||||
("popover_content", popover_content),
|
("popover_content", popover_content),
|
||||||
("wrapped_classes", wrapped_classes),
|
|
||||||
],
|
],
|
||||||
children=children,
|
children=children,
|
||||||
template="cotton/popover.html",
|
template="cotton/popover.html",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def PopoverTruncated(
|
def PopoverTruncated(input_string: str) -> str:
|
||||||
input_string: str,
|
if (truncated := truncate(input_string)) != input_string:
|
||||||
popover_content: str = "",
|
return Popover(wrapped_content=truncated, popover_content=input_string)
|
||||||
popover_if_not_truncated: bool = False,
|
|
||||||
length: int = 30,
|
|
||||||
ellipsis: str = "…",
|
|
||||||
endpart: str = "",
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Returns `input_string` truncated after `length` of characters
|
|
||||||
and displays the untruncated text in a popover HTML element.
|
|
||||||
The truncated text ends in `ellipsis`, and optionally
|
|
||||||
an always-visible `endpart` can be specified.
|
|
||||||
`popover_content` can be specified if:
|
|
||||||
1. It needs to be always displayed regardless if text is truncated.
|
|
||||||
2. It needs to differ from `input_string`.
|
|
||||||
"""
|
|
||||||
if (truncated := truncate(input_string, length, ellipsis, endpart)) != input_string:
|
|
||||||
return Popover(
|
|
||||||
wrapped_content=truncated,
|
|
||||||
popover_content=popover_content if popover_content else input_string,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if popover_content and popover_if_not_truncated:
|
return input_string
|
||||||
return Popover(
|
|
||||||
wrapped_content=input_string,
|
|
||||||
popover_content=popover_content if popover_content else "",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return input_string
|
|
||||||
|
|
||||||
|
|
||||||
def A(
|
def A(
|
||||||
@ -209,47 +180,6 @@ def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeTe
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def LinkedPurchase(purchase: Purchase) -> SafeText:
|
|
||||||
link = reverse("view_purchase", args=[int(purchase.id)])
|
|
||||||
link_content = ""
|
|
||||||
popover_content = ""
|
|
||||||
edition_count = purchase.editions.count()
|
|
||||||
popover_if_not_truncated = False
|
|
||||||
if edition_count == 1:
|
|
||||||
link_content += purchase.editions.first().name
|
|
||||||
popover_content = link_content
|
|
||||||
if edition_count > 1:
|
|
||||||
if purchase.name:
|
|
||||||
link_content += f"{purchase.name}"
|
|
||||||
popover_content += f"<h1>{purchase.name}</h1><br>"
|
|
||||||
else:
|
|
||||||
link_content += f"{edition_count} games"
|
|
||||||
popover_if_not_truncated = True
|
|
||||||
popover_content += f"""
|
|
||||||
<ul class="list-disc list-inside">
|
|
||||||
{"".join(f"<li>{edition.name}</li>" for edition in purchase.editions.all())}
|
|
||||||
</ul>
|
|
||||||
"""
|
|
||||||
icon = purchase.platform.icon if edition_count == 1 else "unspecified"
|
|
||||||
if link_content == "":
|
|
||||||
raise ValueError("link_content is empty!!")
|
|
||||||
a_content = Div(
|
|
||||||
[("class", "inline-flex gap-2 items-center")],
|
|
||||||
[
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
[("title", "Multiple")],
|
|
||||||
),
|
|
||||||
PopoverTruncated(
|
|
||||||
input_string=link_content,
|
|
||||||
popover_content=mark_safe(popover_content),
|
|
||||||
popover_if_not_truncated=popover_if_not_truncated,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return mark_safe(A(url=link, children=[a_content]))
|
|
||||||
|
|
||||||
|
|
||||||
def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
|
def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
|
||||||
content = Div(
|
content = Div(
|
||||||
[("class", "inline-flex gap-2 items-center")],
|
[("class", "inline-flex gap-2 items-center")],
|
||||||
@ -263,11 +193,3 @@ def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return mark_safe(content)
|
return mark_safe(content)
|
||||||
|
|
||||||
|
|
||||||
def PurchasePrice(purchase) -> str:
|
|
||||||
return Popover(
|
|
||||||
popover_content=f"{floatformat(purchase.price)} {purchase.price_currency}",
|
|
||||||
wrapped_content=f"{floatformat(purchase.converted_price)} {purchase.converted_currency}",
|
|
||||||
wrapped_classes="underline decoration-dotted",
|
|
||||||
)
|
|
||||||
|
@ -34,31 +34,14 @@ def safe_getattr(obj: object, attr_chain: str, default: Any | None = None) -> ob
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def truncate_(input_string: str, length: int = 30, ellipsis: str = "…") -> str:
|
def truncate(input_string: str, length: int = 30, ellipsis: str = "…") -> str:
|
||||||
return (
|
return (
|
||||||
(f"{input_string[:length-len(ellipsis)].rstrip()}{ellipsis}")
|
(f"{input_string[:length-len(ellipsis)]}{ellipsis}")
|
||||||
if len(input_string) > length
|
if len(input_string) > 30
|
||||||
else input_string
|
else input_string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def truncate(
|
|
||||||
input_string: str, length: int = 30, ellipsis: str = "…", endpart: str = ""
|
|
||||||
) -> str:
|
|
||||||
max_content_length = length - len(endpart)
|
|
||||||
if max_content_length < 0:
|
|
||||||
raise ValueError("Length cannot be shorter than the length of endpart.")
|
|
||||||
|
|
||||||
if len(input_string) > max_content_length:
|
|
||||||
return f"{input_string[:max_content_length - len(ellipsis)].rstrip()}{ellipsis}{endpart}"
|
|
||||||
|
|
||||||
return (
|
|
||||||
f"{input_string}{endpart}"
|
|
||||||
if len(input_string) + len(endpart) <= length
|
|
||||||
else f"{input_string[:length - len(ellipsis) - len(endpart)].rstrip()}{ellipsis}{endpart}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T", str, int, date)
|
T = TypeVar("T", str, int, date)
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ class SessionForm(forms.ModelForm):
|
|||||||
# queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name")
|
# queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name")
|
||||||
# )
|
# )
|
||||||
purchase = forms.ModelChoiceField(
|
purchase = forms.ModelChoiceField(
|
||||||
queryset=Purchase.objects.all(),
|
queryset=Purchase.objects.order_by("edition__sort_name"),
|
||||||
widget=forms.Select(attrs={"autofocus": "autofocus"}),
|
widget=forms.Select(attrs={"autofocus": "autofocus"}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,12 +38,12 @@ class SessionForm(forms.ModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class EditionChoiceField(forms.ModelMultipleChoiceField):
|
class EditionChoiceField(forms.ModelChoiceField):
|
||||||
def label_from_instance(self, obj) -> str:
|
def label_from_instance(self, obj) -> str:
|
||||||
return f"{obj.sort_name} ({obj.platform}, {obj.year_released})"
|
return f"{obj.sort_name} ({obj.platform}, {obj.year_released})"
|
||||||
|
|
||||||
|
|
||||||
class IncludePlatformSelect(forms.SelectMultiple):
|
class IncludePlatformSelect(forms.Select):
|
||||||
def create_option(self, name, value, *args, **kwargs):
|
def create_option(self, name, value, *args, **kwargs):
|
||||||
option = super().create_option(name, value, *args, **kwargs)
|
option = super().create_option(name, value, *args, **kwargs)
|
||||||
if platform_id := safe_getattr(value, "instance.platform.id"):
|
if platform_id := safe_getattr(value, "instance.platform.id"):
|
||||||
@ -58,7 +58,7 @@ class PurchaseForm(forms.ModelForm):
|
|||||||
# Automatically update related_purchase <select/>
|
# Automatically update related_purchase <select/>
|
||||||
# to only include purchases of the selected edition.
|
# to only include purchases of the selected edition.
|
||||||
related_purchase_by_edition_url = reverse("related_purchase_by_edition")
|
related_purchase_by_edition_url = reverse("related_purchase_by_edition")
|
||||||
self.fields["editions"].widget.attrs.update(
|
self.fields["edition"].widget.attrs.update(
|
||||||
{
|
{
|
||||||
"hx-trigger": "load, click",
|
"hx-trigger": "load, click",
|
||||||
"hx-get": related_purchase_by_edition_url,
|
"hx-get": related_purchase_by_edition_url,
|
||||||
@ -67,13 +67,15 @@ class PurchaseForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
editions = EditionChoiceField(
|
edition = EditionChoiceField(
|
||||||
queryset=Edition.objects.order_by("sort_name"),
|
queryset=Edition.objects.order_by("sort_name"),
|
||||||
widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}),
|
widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}),
|
||||||
)
|
)
|
||||||
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
||||||
related_purchase = forms.ModelChoiceField(
|
related_purchase = forms.ModelChoiceField(
|
||||||
queryset=Purchase.objects.filter(type=Purchase.GAME),
|
queryset=Purchase.objects.filter(type=Purchase.GAME).order_by(
|
||||||
|
"edition__sort_name"
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,7 +88,7 @@ class PurchaseForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
model = Purchase
|
model = Purchase
|
||||||
fields = [
|
fields = [
|
||||||
"editions",
|
"edition",
|
||||||
"platform",
|
"platform",
|
||||||
"date_purchased",
|
"date_purchased",
|
||||||
"date_refunded",
|
"date_refunded",
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,23 +0,0 @@
|
|||||||
# 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:
|
|
||||||
purchase.editions_temp.add(purchase.edition)
|
|
||||||
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),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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",
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-01-08 20:06
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("games", "0045_alter_purchase_editions"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="game",
|
|
||||||
name="platform",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
|
||||||
to="games.platform",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name="game",
|
|
||||||
unique_together={("name", "platform", "year_released")},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,61 +0,0 @@
|
|||||||
# Generated by Django 5.1.3 on 2025-01-19 20:29
|
|
||||||
|
|
||||||
from django.db import connection, migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def recreate_games(apps, schema_editor):
|
|
||||||
Edition = apps.get_model("games", "Edition")
|
|
||||||
Game = apps.get_model("games", "Game")
|
|
||||||
Purchase = apps.get_model("games", "Purchase")
|
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
print("Create table games_gametemp")
|
|
||||||
cursor.execute(
|
|
||||||
"CREATE TABLE games_gametemp AS SELECT * FROM games_game WHERE 1=0;"
|
|
||||||
)
|
|
||||||
|
|
||||||
for edition in Edition.objects.all():
|
|
||||||
print(f"Re-create edition with ID {edition.id}")
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO games_gametemp (
|
|
||||||
id, name, sort_name, year_released, platform_id, wikidata, created_at
|
|
||||||
)
|
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
||||||
""",
|
|
||||||
[
|
|
||||||
edition.id, # Reuse the Edition ID
|
|
||||||
edition.name,
|
|
||||||
edition.sort_name,
|
|
||||||
edition.year_released,
|
|
||||||
edition.platform_id,
|
|
||||||
Game.objects.get(id=edition.game.id).wikidata,
|
|
||||||
edition.created_at,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
print("Turn foreign keys off")
|
|
||||||
cursor.execute("PRAGMA foreign_keys = OFF;")
|
|
||||||
print("Drop table games_game")
|
|
||||||
cursor.execute("DROP TABLE games_game;")
|
|
||||||
print("Drop table games_edition")
|
|
||||||
cursor.execute("DROP TABLE games_edition;")
|
|
||||||
print("Rename table games_gametemp to games_game")
|
|
||||||
# cursor.execute("ALTER TABLE games_gametemp RENAME TO games_game;")
|
|
||||||
cursor.execute("CREATE TABLE games_game AS SELECT * FROM games_gametemp;")
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("games", "0046_game_platform_alter_game_unique_together"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(recreate_games),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="purchase",
|
|
||||||
name="editions",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True, related_name="purchases", to="games.game"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -9,6 +9,20 @@ from django.utils import timezone
|
|||||||
from common.time import format_duration
|
from common.time import format_duration
|
||||||
|
|
||||||
|
|
||||||
|
class Game(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
|
||||||
|
year_released = models.IntegerField(null=True, blank=True, default=None)
|
||||||
|
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
session_average: float | int | timedelta | None
|
||||||
|
session_count: int | None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Platform(models.Model):
|
class Platform(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
group = models.CharField(max_length=255, null=True, blank=True, default=None)
|
group = models.CharField(max_length=255, null=True, blank=True, default=None)
|
||||||
@ -30,26 +44,6 @@ def get_sentinel_platform():
|
|||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
|
|
||||||
class Game(models.Model):
|
|
||||||
class Meta:
|
|
||||||
unique_together = [["name", "platform", "year_released"]]
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
|
|
||||||
year_released = models.IntegerField(null=True, blank=True, default=None)
|
|
||||||
platform = models.ForeignKey(
|
|
||||||
Platform, on_delete=models.SET_DEFAULT, null=True, blank=True, default=None
|
|
||||||
)
|
|
||||||
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
session_average: float | int | timedelta | None
|
|
||||||
session_count: int | None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class Edition(models.Model):
|
class Edition(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [["name", "platform", "year_released"]]
|
unique_together = [["name", "platform", "year_released"]]
|
||||||
@ -119,7 +113,7 @@ class Purchase(models.Model):
|
|||||||
|
|
||||||
objects = PurchaseQueryset().as_manager()
|
objects = PurchaseQueryset().as_manager()
|
||||||
|
|
||||||
editions = models.ManyToManyField(Game, related_name="purchases", blank=True)
|
edition = models.ForeignKey(Edition, on_delete=models.CASCADE)
|
||||||
platform = models.ForeignKey(
|
platform = models.ForeignKey(
|
||||||
Platform, on_delete=models.CASCADE, default=None, null=True, blank=True
|
Platform, on_delete=models.CASCADE, default=None, null=True, blank=True
|
||||||
)
|
)
|
||||||
@ -147,28 +141,26 @@ class Purchase(models.Model):
|
|||||||
)
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
@property
|
|
||||||
def first_edition(self):
|
|
||||||
return self.editions.first()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
additional_info = [
|
additional_info = [
|
||||||
self.get_type_display() if self.type != Purchase.GAME else "",
|
self.get_type_display() if self.type != Purchase.GAME else "",
|
||||||
(
|
(
|
||||||
f"{self.first_edition.platform} version on {self.platform}"
|
f"{self.edition.platform} version on {self.platform}"
|
||||||
if self.platform != self.first_edition.platform
|
if self.platform != self.edition.platform
|
||||||
else self.platform
|
else self.platform
|
||||||
),
|
),
|
||||||
self.first_edition.year_released,
|
self.edition.year_released,
|
||||||
self.get_ownership_type_display(),
|
self.get_ownership_type_display(),
|
||||||
]
|
]
|
||||||
return f"{self.first_edition} ({', '.join(filter(None, map(str, additional_info)))})"
|
return f"{self.edition} ({', '.join(filter(None, map(str, additional_info)))})"
|
||||||
|
|
||||||
def is_game(self):
|
def is_game(self):
|
||||||
return self.type == self.GAME
|
return self.type == self.GAME
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.type != Purchase.GAME and not self.related_purchase:
|
if self.type == Purchase.GAME:
|
||||||
|
self.name = ""
|
||||||
|
elif self.type != Purchase.GAME and not self.related_purchase:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"{self.get_type_display()} must have a related purchase."
|
f"{self.get_type_display()} must have a related purchase."
|
||||||
)
|
)
|
||||||
|
@ -1652,14 +1652,6 @@ input:checked + .toggle-bg {
|
|||||||
resize: both;
|
resize: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-inside {
|
|
||||||
list-style-position: inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-disc {
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-4 {
|
.grid-cols-4 {
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@ -2200,10 +2192,6 @@ input:checked + .toggle-bg {
|
|||||||
text-decoration-color: #64748b;
|
text-decoration-color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.decoration-dotted {
|
|
||||||
text-decoration-style: dotted;
|
|
||||||
}
|
|
||||||
|
|
||||||
.opacity-0 {
|
.opacity-0 {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@ -3129,10 +3117,6 @@ textarea:disabled:is(.dark *) {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md\:justify-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
|
.md\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
|
||||||
--tw-space-x-reverse: 0;
|
--tw-space-x-reverse: 0;
|
||||||
margin-right: calc(2rem * var(--tw-space-x-reverse));
|
margin-right: calc(2rem * var(--tw-space-x-reverse));
|
||||||
@ -3276,13 +3260,3 @@ textarea:disabled:is(.dark *) {
|
|||||||
.\[\&_td\:last-child\]\:text-right td:last-child {
|
.\[\&_td\:last-child\]\:text-right td:last-child {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media not all and (min-width: 640px) {
|
|
||||||
.\[\&_td\:not\(\:first-child\)\:not\(\:last-child\)\]\:max-sm\:hidden td:not(:first-child):not(:last-child) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.\[\&_th\:not\(\:first-child\)\:not\(\:last-child\)\]\:max-sm\:hidden th:not(:first-child):not(:last-child) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
<span data-popover-target={{ id }} class="{{ wrapped_classes }}">{{ wrapped_content|default:slot }}</span>
|
<span data-popover-target={{ id }} class="{{ class }}">{{ wrapped_content|default:slot }}</span>
|
||||||
<div data-popover
|
<div data-popover
|
||||||
id="{{ id }}"
|
id="{{ id }}"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
class="absolute z-10 invisible inline-block text-sm text-white transition-opacity duration-300 bg-white border border-purple-200 rounded-lg shadow-sm opacity-0 dark:text-white dark:border-purple-600 dark:bg-purple-800">
|
class="absolute z-10 invisible inline-block text-sm text-white transition-opacity duration-300 bg-white border border-purple-200 rounded-lg shadow-sm opacity-0 dark:text-white dark:border-purple-600 dark:bg-purple-800">
|
||||||
<div class="px-3 py-2">{{ popover_content }}</div>
|
<div class="px-3 py-2">{{ popover_content }}</div>
|
||||||
<div data-popper-arrow></div>
|
<div data-popper-arrow></div>
|
||||||
<!-- for Tailwind CSS to generate decoration-dotted CSS from Python component -->
|
|
||||||
<span class="hidden decoration-dotted"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,20 +7,20 @@
|
|||||||
{{ header_action }}
|
{{ header_action }}
|
||||||
</c-table-header>
|
</c-table-header>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 [&_th:not(:first-child):not(:last-child)]:max-sm:hidden">
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="dark:divide-y [&_td:not(:first-child):not(:last-child)]:max-sm:hidden">
|
<tbody class="dark:divide-y">
|
||||||
{% for row in rows %}<c-table-row :data=row />{% endfor %}
|
{% for row in rows %}<c-table-row :data=row />{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if page_obj and elided_page_range %}
|
{% if page_obj and elided_page_range %}
|
||||||
<nav class="flex items-center flex-col md:flex-row md:justify-between px-6 py-4 dark:bg-gray-900 sm:rounded-b-lg"
|
<nav class="flex items-center flex-column md:flex-row justify-between px-6 py-4 dark:bg-gray-900 sm:rounded-b-lg"
|
||||||
aria-label="Table navigation">
|
aria-label="Table navigation">
|
||||||
<span class="text-sm text-center font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0 block w-full md:inline md:w-auto"><span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.start_index }}</span>—<span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.end_index }}</span> of <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.paginator.count }}</span></span>
|
<span class="text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0 block w-full md:inline md:w-auto">Showing <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.start_index }}</span>—<span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.end_index }}</span> of <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.paginator.count }}</span></span>
|
||||||
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
|
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
|
||||||
<li>
|
<li>
|
||||||
{% if page_obj.has_previous %}
|
{% if page_obj.has_previous %}
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% partialdef purchase-name %}
|
{% partialdef purchase-name %}
|
||||||
{% if purchase.type != 'game' %}
|
{% if purchase.type != 'game' %}
|
||||||
<c-gamelink :game_id=purchase.first_edition.game.id>
|
<c-gamelink :game_id=purchase.edition.game.id>
|
||||||
{{ purchase.name }} ({{ purchase.first_edition.edition.name }} {{ purchase.get_type_display }})
|
{{ purchase.name }} ({{ purchase.edition.name }} {{ purchase.get_type_display }})
|
||||||
</c-gamelink>
|
</c-gamelink>
|
||||||
{% else %}
|
{% else %}
|
||||||
<c-gamelink :game_id=purchase.first_edition.game.id :name=purchase.first_edition.name />
|
<c-gamelink :game_id=purchase.edition.game.id :name=purchase.edition.name />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endpartialdef %}
|
{% endpartialdef %}
|
||||||
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
|
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
|
||||||
@ -100,7 +100,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if month_playtimes %}
|
{% if month_playtime %}
|
||||||
<h1 class="text-5xl text-center my-6">Playtime per month</h1>
|
<h1 class="text-5xl text-center my-6">Playtime per month</h1>
|
||||||
<table class="responsive-table">
|
<table class="responsive-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -142,9 +142,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Spendings ({{ total_spent_currency }})</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2">Spendings ({{ total_spent_currency }})</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_spent }} ({{ spent_per_game }}/game)</td>
|
||||||
{{ total_spent | floatformat }} ({{ spent_per_game | floatformat }}/game)
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -255,7 +253,7 @@
|
|||||||
{% for purchase in purchased_unfinished %}
|
{% for purchase in purchased_unfinished %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.converted_price | floatformat }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.converted_price }}</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -276,7 +274,7 @@
|
|||||||
{% for purchase in all_purchased_this_year %}
|
{% for purchase in all_purchased_this_year %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.converted_price | floatformat }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.converted_price }}</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div id="game-info" class="mb-10">
|
<div id="game-info" class="mb-10">
|
||||||
<div class="flex gap-5 mb-3">
|
<div class="flex gap-5 mb-3">
|
||||||
<span class="text-balance max-w-[30rem] text-4xl">
|
<span class="text-balance max-w-[30rem] text-4xl">
|
||||||
<span class="font-bold font-serif">{{ game.name }}</span>{% if game.year_released %} <c-popover id="popover-year" popover_content="Original release year" class="text-slate-500 text-2xl">{{ game.year_released }}</c-popover>{% endif %}
|
<span class="font-bold font-serif">{{ game.name }}</span> <c-popover id="popover-year" popover_content="Original release year" class="text-slate-500 text-2xl">{{ game.year_released }}</c-popover>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4 dark:text-slate-400 mb-3">
|
<div class="flex gap-4 dark:text-slate-400 mb-3">
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
<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>
|
|
@ -54,11 +54,6 @@ urlpatterns = [
|
|||||||
purchase.delete_purchase,
|
purchase.delete_purchase,
|
||||||
name="delete_purchase",
|
name="delete_purchase",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"purchase/<int:purchase_id>/view",
|
|
||||||
purchase.view_purchase,
|
|
||||||
name="view_purchase",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"purchase/<int:purchase_id>/finish",
|
"purchase/<int:purchase_id>/finish",
|
||||||
purchase.finish_purchase,
|
purchase.finish_purchase,
|
||||||
|
@ -13,11 +13,9 @@ from common.components import (
|
|||||||
Button,
|
Button,
|
||||||
Div,
|
Div,
|
||||||
Icon,
|
Icon,
|
||||||
LinkedPurchase,
|
|
||||||
NameWithPlatformIcon,
|
NameWithPlatformIcon,
|
||||||
Popover,
|
Popover,
|
||||||
PopoverTruncated,
|
PopoverTruncated,
|
||||||
PurchasePrice,
|
|
||||||
)
|
)
|
||||||
from common.time import (
|
from common.time import (
|
||||||
dateformat,
|
dateformat,
|
||||||
@ -27,7 +25,7 @@ from common.time import (
|
|||||||
local_strftime,
|
local_strftime,
|
||||||
timeformat,
|
timeformat,
|
||||||
)
|
)
|
||||||
from common.utils import safe_division, truncate
|
from common.utils import format_float_or_int, safe_division, truncate
|
||||||
from games.forms import GameForm
|
from games.forms import GameForm
|
||||||
from games.models import Edition, Game, Purchase, Session
|
from games.models import Edition, Game, Purchase, Session
|
||||||
from games.views.general import use_custom_redirect
|
from games.views.general import use_custom_redirect
|
||||||
@ -163,7 +161,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
to_attr="nongame_related_purchases",
|
to_attr="nongame_related_purchases",
|
||||||
)
|
)
|
||||||
game_purchases_prefetch: Prefetch[Purchase] = Prefetch(
|
game_purchases_prefetch: Prefetch[Purchase] = Prefetch(
|
||||||
"purchases",
|
"purchase_set",
|
||||||
queryset=Purchase.objects.filter(type=Purchase.GAME).prefetch_related(
|
queryset=Purchase.objects.filter(type=Purchase.GAME).prefetch_related(
|
||||||
nongame_related_purchases_prefetch
|
nongame_related_purchases_prefetch
|
||||||
),
|
),
|
||||||
@ -175,14 +173,14 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
.order_by("year_released")
|
.order_by("year_released")
|
||||||
)
|
)
|
||||||
|
|
||||||
purchases = Purchase.objects.filter(editions__game=game).order_by("date_purchased")
|
purchases = Purchase.objects.filter(edition__game=game).order_by("date_purchased")
|
||||||
|
|
||||||
sessions = Session.objects.prefetch_related("device").filter(
|
sessions = Session.objects.prefetch_related("device").filter(
|
||||||
purchase__editions__game=game
|
purchase__edition__game=game
|
||||||
)
|
)
|
||||||
session_count = sessions.count()
|
session_count = sessions.count()
|
||||||
session_count_without_manual = (
|
session_count_without_manual = (
|
||||||
Session.objects.without_manual().filter(purchase__editions__game=game).count()
|
Session.objects.without_manual().filter(purchase__edition__game=game).count()
|
||||||
)
|
)
|
||||||
|
|
||||||
if sessions:
|
if sessions:
|
||||||
@ -243,10 +241,13 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
"columns": ["Name", "Type", "Date", "Price", "Actions"],
|
"columns": ["Name", "Type", "Date", "Price", "Actions"],
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
LinkedPurchase(purchase),
|
NameWithPlatformIcon(
|
||||||
|
name=purchase.name if purchase.name else purchase.edition.name,
|
||||||
|
platform=purchase.platform,
|
||||||
|
),
|
||||||
purchase.get_type_display(),
|
purchase.get_type_display(),
|
||||||
purchase.date_purchased.strftime(dateformat),
|
purchase.date_purchased.strftime(dateformat),
|
||||||
PurchasePrice(purchase),
|
f"{format_float_or_int(purchase.price)} {purchase.price_currency}",
|
||||||
render_to_string(
|
render_to_string(
|
||||||
"cotton/button_group.html",
|
"cotton/button_group.html",
|
||||||
{
|
{
|
||||||
@ -269,7 +270,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions_all = Session.objects.filter(purchase__editions__game=game).order_by(
|
sessions_all = Session.objects.filter(purchase__edition__game=game).order_by(
|
||||||
"-timestamp_start"
|
"-timestamp_start"
|
||||||
)
|
)
|
||||||
last_session = None
|
last_session = None
|
||||||
@ -298,7 +299,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
args=[last_session.pk],
|
args=[last_session.pk],
|
||||||
),
|
),
|
||||||
children=Popover(
|
children=Popover(
|
||||||
popover_content=last_session.purchase.first_edition.name,
|
popover_content=last_session.purchase.edition.name,
|
||||||
children=[
|
children=[
|
||||||
Button(
|
Button(
|
||||||
icon=True,
|
icon=True,
|
||||||
@ -306,9 +307,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
size="xs",
|
size="xs",
|
||||||
children=[
|
children=[
|
||||||
Icon("play"),
|
Icon("play"),
|
||||||
truncate(
|
truncate(f"{last_session.purchase.edition.name}"),
|
||||||
f"{last_session.purchase.first_edition.name}"
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -324,7 +323,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
NameWithPlatformIcon(
|
NameWithPlatformIcon(
|
||||||
name=session.purchase.name
|
name=session.purchase.name
|
||||||
if session.purchase.name
|
if session.purchase.name
|
||||||
else session.purchase.first_edition.name,
|
else session.purchase.edition.name,
|
||||||
platform=session.purchase.platform,
|
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 ""}",
|
||||||
@ -375,7 +374,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
"editions": editions,
|
"editions": editions,
|
||||||
"game": game,
|
"game": game,
|
||||||
"playrange": playrange,
|
"playrange": playrange,
|
||||||
"purchase_count": Purchase.objects.filter(editions__game=game).count(),
|
"purchase_count": Purchase.objects.filter(edition__game=game).count(),
|
||||||
"session_average_without_manual": round(
|
"session_average_without_manual": round(
|
||||||
safe_division(
|
safe_division(
|
||||||
total_hours_without_manual, int(session_count_without_manual)
|
total_hours_without_manual, int(session_count_without_manual)
|
||||||
|
@ -2,7 +2,7 @@ from datetime import datetime
|
|||||||
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, Q, 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.db.models.manager import BaseManager
|
||||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||||
@ -49,9 +49,7 @@ def use_custom_redirect(
|
|||||||
@login_required
|
@login_required
|
||||||
def stats_alltime(request: HttpRequest) -> HttpResponse:
|
def stats_alltime(request: HttpRequest) -> HttpResponse:
|
||||||
year = "Alltime"
|
year = "Alltime"
|
||||||
this_year_sessions = Session.objects.all().prefetch_related(
|
this_year_sessions = Session.objects.all().select_related("purchase__edition")
|
||||||
Prefetch("purchase__editions")
|
|
||||||
)
|
|
||||||
this_year_sessions_with_durations = this_year_sessions.annotate(
|
this_year_sessions_with_durations = this_year_sessions.annotate(
|
||||||
duration=ExpressionWrapper(
|
duration=ExpressionWrapper(
|
||||||
F("timestamp_end") - F("timestamp_start"),
|
F("timestamp_end") - F("timestamp_start"),
|
||||||
@ -60,10 +58,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||||||
)
|
)
|
||||||
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
|
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
|
||||||
this_year_games = Game.objects.filter(
|
this_year_games = Game.objects.filter(
|
||||||
editions__purchase__session__in=this_year_sessions
|
edition__purchase__session__in=this_year_sessions
|
||||||
).distinct()
|
).distinct()
|
||||||
this_year_games_with_session_counts = this_year_games.annotate(
|
this_year_games_with_session_counts = this_year_games.annotate(
|
||||||
session_count=Count("editions__purchase__session"),
|
session_count=Count("edition__purchase__session"),
|
||||||
)
|
)
|
||||||
game_highest_session_count = this_year_games_with_session_counts.order_by(
|
game_highest_session_count = this_year_games_with_session_counts.order_by(
|
||||||
"-session_count"
|
"-session_count"
|
||||||
@ -80,7 +78,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
this_year_purchases = Purchase.objects.all()
|
this_year_purchases = Purchase.objects.all()
|
||||||
this_year_purchases_with_currency = this_year_purchases.select_related("editions")
|
this_year_purchases_with_currency = this_year_purchases.select_related("edition")
|
||||||
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
|
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
|
||||||
date_refunded=None
|
date_refunded=None
|
||||||
)
|
)
|
||||||
@ -129,11 +127,11 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||||||
total_spent = this_year_spendings["total_spent"] or 0
|
total_spent = this_year_spendings["total_spent"] or 0
|
||||||
|
|
||||||
games_with_playtime = (
|
games_with_playtime = (
|
||||||
Game.objects.filter(editions__purchase__session__in=this_year_sessions)
|
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_playtime=Sum(
|
total_playtime=Sum(
|
||||||
F("editions__purchase__session__duration_calculated")
|
F("edition__purchase__session__duration_calculated")
|
||||||
+ F("editions__purchase__session__duration_manual")
|
+ F("edition__purchase__session__duration_manual")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.values("id", "name", "total_playtime")
|
.values("id", "name", "total_playtime")
|
||||||
@ -148,9 +146,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||||||
month["playtime"] = format_duration(month["playtime"], "%2.0H")
|
month["playtime"] = format_duration(month["playtime"], "%2.0H")
|
||||||
|
|
||||||
highest_session_average_game = (
|
highest_session_average_game = (
|
||||||
Game.objects.filter(editions__purchase__session__in=this_year_sessions)
|
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
||||||
.annotate(
|
.annotate(
|
||||||
session_average=Avg("editions__purchase__session__duration_calculated")
|
session_average=Avg("edition__purchase__session__duration_calculated")
|
||||||
)
|
)
|
||||||
.order_by("-session_average")
|
.order_by("-session_average")
|
||||||
.first()
|
.first()
|
||||||
@ -177,10 +175,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||||||
last_play_date = "N/A"
|
last_play_date = "N/A"
|
||||||
if this_year_sessions:
|
if this_year_sessions:
|
||||||
first_session = this_year_sessions.earliest()
|
first_session = this_year_sessions.earliest()
|
||||||
first_play_game = first_session.purchase.first_edition.game
|
first_play_game = first_session.purchase.edition.game
|
||||||
first_play_date = first_session.timestamp_start.strftime(dateformat)
|
first_play_date = first_session.timestamp_start.strftime(dateformat)
|
||||||
last_session = this_year_sessions.latest()
|
last_session = this_year_sessions.latest()
|
||||||
last_play_game = last_session.purchase.first_edition.game
|
last_play_game = last_session.purchase.edition.game
|
||||||
last_play_date = last_session.timestamp_start.strftime(dateformat)
|
last_play_date = last_session.timestamp_start.strftime(dateformat)
|
||||||
|
|
||||||
all_purchased_this_year_count = this_year_purchases_with_currency.count()
|
all_purchased_this_year_count = this_year_purchases_with_currency.count()
|
||||||
@ -229,7 +227,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||||||
else 0
|
else 0
|
||||||
),
|
),
|
||||||
"longest_session_game": (
|
"longest_session_game": (
|
||||||
longest_session.purchase.first_edition.game if longest_session else None
|
longest_session.purchase.edition.game if longest_session else None
|
||||||
),
|
),
|
||||||
"highest_session_count": (
|
"highest_session_count": (
|
||||||
game_highest_session_count.session_count
|
game_highest_session_count.session_count
|
||||||
@ -268,7 +266,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
return HttpResponseRedirect(reverse("stats_alltime"))
|
return HttpResponseRedirect(reverse("stats_alltime"))
|
||||||
this_year_sessions = Session.objects.filter(
|
this_year_sessions = Session.objects.filter(
|
||||||
timestamp_start__year=year
|
timestamp_start__year=year
|
||||||
).prefetch_related("purchase__editions")
|
).select_related("purchase__edition")
|
||||||
this_year_sessions_with_durations = this_year_sessions.annotate(
|
this_year_sessions_with_durations = this_year_sessions.annotate(
|
||||||
duration=ExpressionWrapper(
|
duration=ExpressionWrapper(
|
||||||
F("timestamp_end") - F("timestamp_start"),
|
F("timestamp_end") - F("timestamp_start"),
|
||||||
@ -277,12 +275,12 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
)
|
)
|
||||||
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
|
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
|
||||||
this_year_games = Game.objects.filter(
|
this_year_games = Game.objects.filter(
|
||||||
edition__purchases__session__in=this_year_sessions
|
edition__purchase__session__in=this_year_sessions
|
||||||
).distinct()
|
).distinct()
|
||||||
this_year_games_with_session_counts = this_year_games.annotate(
|
this_year_games_with_session_counts = this_year_games.annotate(
|
||||||
session_count=Count(
|
session_count=Count(
|
||||||
"edition__purchases__session",
|
"edition__purchase__session",
|
||||||
filter=Q(edition__purchases__session__timestamp_start__year=year),
|
filter=Q(edition__purchase__session__timestamp_start__year=year),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
game_highest_session_count = this_year_games_with_session_counts.order_by(
|
game_highest_session_count = this_year_games_with_session_counts.order_by(
|
||||||
@ -300,7 +298,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
this_year_purchases = Purchase.objects.filter(date_purchased__year=year)
|
this_year_purchases = Purchase.objects.filter(date_purchased__year=year)
|
||||||
this_year_purchases_with_currency = this_year_purchases.prefetch_related("editions")
|
this_year_purchases_with_currency = this_year_purchases.select_related("edition")
|
||||||
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
|
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
|
||||||
date_refunded=None
|
date_refunded=None
|
||||||
).exclude(ownership_type=Purchase.DEMO)
|
).exclude(ownership_type=Purchase.DEMO)
|
||||||
@ -337,7 +335,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
|
|
||||||
purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year)
|
purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year)
|
||||||
purchases_finished_this_year_released_this_year = (
|
purchases_finished_this_year_released_this_year = (
|
||||||
purchases_finished_this_year.filter(editions__year_released=year).order_by(
|
purchases_finished_this_year.filter(edition__year_released=year).order_by(
|
||||||
"date_finished"
|
"date_finished"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -351,11 +349,11 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
total_spent = this_year_spendings["total_spent"] or 0
|
total_spent = this_year_spendings["total_spent"] or 0
|
||||||
|
|
||||||
games_with_playtime = (
|
games_with_playtime = (
|
||||||
Game.objects.filter(edition__purchases__session__in=this_year_sessions)
|
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_playtime=Sum(
|
total_playtime=Sum(
|
||||||
F("edition__purchases__session__duration_calculated")
|
F("edition__purchase__session__duration_calculated")
|
||||||
+ F("edition__purchases__session__duration_manual")
|
+ F("edition__purchase__session__duration_manual")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.values("id", "name", "total_playtime")
|
.values("id", "name", "total_playtime")
|
||||||
@ -370,9 +368,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
month["playtime"] = format_duration(month["playtime"], "%2.0H")
|
month["playtime"] = format_duration(month["playtime"], "%2.0H")
|
||||||
|
|
||||||
highest_session_average_game = (
|
highest_session_average_game = (
|
||||||
Game.objects.filter(edition__purchases__session__in=this_year_sessions)
|
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
||||||
.annotate(
|
.annotate(
|
||||||
session_average=Avg("edition__purchases__session__duration_calculated")
|
session_average=Avg("edition__purchase__session__duration_calculated")
|
||||||
)
|
)
|
||||||
.order_by("-session_average")
|
.order_by("-session_average")
|
||||||
.first()
|
.first()
|
||||||
@ -403,10 +401,10 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
last_play_game = None
|
last_play_game = None
|
||||||
if this_year_sessions:
|
if this_year_sessions:
|
||||||
first_session = this_year_sessions.earliest()
|
first_session = this_year_sessions.earliest()
|
||||||
first_play_game = first_session.purchase.first_edition.game
|
first_play_game = first_session.purchase.edition.game
|
||||||
first_play_date = first_session.timestamp_start.strftime(dateformat)
|
first_play_date = first_session.timestamp_start.strftime(dateformat)
|
||||||
last_session = this_year_sessions.latest()
|
last_session = this_year_sessions.latest()
|
||||||
last_play_game = last_session.purchase.first_edition.game
|
last_play_game = last_session.purchase.edition.game
|
||||||
last_play_date = last_session.timestamp_start.strftime(dateformat)
|
last_play_date = last_session.timestamp_start.strftime(dateformat)
|
||||||
|
|
||||||
all_purchased_this_year_count = this_year_purchases_with_currency.count()
|
all_purchased_this_year_count = this_year_purchases_with_currency.count()
|
||||||
@ -423,7 +421,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
),
|
),
|
||||||
"total_games": this_year_played_purchases.count(),
|
"total_games": this_year_played_purchases.count(),
|
||||||
"total_2023_games": this_year_played_purchases.filter(
|
"total_2023_games": this_year_played_purchases.filter(
|
||||||
editions__year_released=year
|
edition__year_released=year
|
||||||
).count(),
|
).count(),
|
||||||
"top_10_games_by_playtime": top_10_games_by_playtime,
|
"top_10_games_by_playtime": top_10_games_by_playtime,
|
||||||
"year": year,
|
"year": year,
|
||||||
@ -434,16 +432,16 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
"spent_per_game": int(
|
"spent_per_game": int(
|
||||||
safe_division(total_spent, this_year_purchases_without_refunded_count)
|
safe_division(total_spent, this_year_purchases_without_refunded_count)
|
||||||
),
|
),
|
||||||
"all_finished_this_year": purchases_finished_this_year.prefetch_related(
|
"all_finished_this_year": purchases_finished_this_year.select_related(
|
||||||
"editions"
|
"edition"
|
||||||
).order_by("date_finished"),
|
).order_by("date_finished"),
|
||||||
"all_finished_this_year_count": purchases_finished_this_year.count(),
|
"all_finished_this_year_count": purchases_finished_this_year.count(),
|
||||||
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.prefetch_related(
|
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.select_related(
|
||||||
"editions"
|
"edition"
|
||||||
).order_by("date_finished"),
|
).order_by("date_finished"),
|
||||||
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(),
|
"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.prefetch_related(
|
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.select_related(
|
||||||
"editions"
|
"edition"
|
||||||
).order_by("date_finished"),
|
).order_by("date_finished"),
|
||||||
"total_sessions": this_year_sessions.count(),
|
"total_sessions": this_year_sessions.count(),
|
||||||
"unique_days": unique_days["dates"],
|
"unique_days": unique_days["dates"],
|
||||||
@ -473,7 +471,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
else 0
|
else 0
|
||||||
),
|
),
|
||||||
"longest_session_game": (
|
"longest_session_game": (
|
||||||
longest_session.purchase.first_edition.game if longest_session else None
|
longest_session.purchase.edition.game if longest_session else None
|
||||||
),
|
),
|
||||||
"highest_session_count": (
|
"highest_session_count": (
|
||||||
game_highest_session_count.session_count
|
game_highest_session_count.session_count
|
||||||
|
@ -13,8 +13,9 @@ from django.template.loader import render_to_string
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice
|
from common.components import A, Button, Icon, LinkedNameWithPlatformIcon
|
||||||
from common.time import dateformat
|
from common.time import dateformat
|
||||||
|
from common.utils import format_float_or_int
|
||||||
from games.forms import PurchaseForm
|
from games.forms import PurchaseForm
|
||||||
from games.models import Edition, Purchase
|
from games.models import Edition, Purchase
|
||||||
from games.views.general import use_custom_redirect
|
from games.views.general import use_custom_redirect
|
||||||
@ -48,6 +49,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
|||||||
"Name",
|
"Name",
|
||||||
"Type",
|
"Type",
|
||||||
"Price",
|
"Price",
|
||||||
|
"Currency",
|
||||||
"Infinite",
|
"Infinite",
|
||||||
"Purchased",
|
"Purchased",
|
||||||
"Refunded",
|
"Refunded",
|
||||||
@ -58,9 +60,14 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
|||||||
],
|
],
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
LinkedPurchase(purchase),
|
LinkedNameWithPlatformIcon(
|
||||||
|
name=purchase.edition.name,
|
||||||
|
game_id=purchase.edition.game.pk,
|
||||||
|
platform=purchase.platform,
|
||||||
|
),
|
||||||
purchase.get_type_display(),
|
purchase.get_type_display(),
|
||||||
PurchasePrice(purchase),
|
format_float_or_int(purchase.price),
|
||||||
|
purchase.price_currency,
|
||||||
purchase.infinite,
|
purchase.infinite,
|
||||||
purchase.date_purchased.strftime(dateformat),
|
purchase.date_purchased.strftime(dateformat),
|
||||||
(
|
(
|
||||||
@ -169,7 +176,7 @@ def add_purchase(request: HttpRequest, edition_id: int = 0) -> HttpResponse:
|
|||||||
|
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
context["title"] = "Add New Purchase"
|
context["title"] = "Add New Purchase"
|
||||||
# context["script_name"] = "add_purchase.js"
|
context["script_name"] = "add_purchase.js"
|
||||||
return render(request, "add_purchase.html", context)
|
return render(request, "add_purchase.html", context)
|
||||||
|
|
||||||
|
|
||||||
@ -185,7 +192,7 @@ def edit_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
|
|||||||
context["title"] = "Edit Purchase"
|
context["title"] = "Edit Purchase"
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
context["purchase_id"] = str(purchase_id)
|
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)
|
return render(request, "add_purchase.html", context)
|
||||||
|
|
||||||
|
|
||||||
@ -196,12 +203,6 @@ def delete_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
|
|||||||
return redirect("list_purchases")
|
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
|
@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)
|
||||||
|
@ -47,10 +47,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
|||||||
| Q(device__name__icontains=search_string)
|
| Q(device__name__icontains=search_string)
|
||||||
| Q(device__type__icontains=search_string)
|
| Q(device__type__icontains=search_string)
|
||||||
)
|
)
|
||||||
try:
|
last_session = sessions.latest()
|
||||||
last_session = sessions.latest()
|
|
||||||
except Session.DoesNotExist:
|
|
||||||
last_session = None
|
|
||||||
page_obj = None
|
page_obj = None
|
||||||
if int(limit) != 0:
|
if int(limit) != 0:
|
||||||
paginator = Paginator(sessions, limit)
|
paginator = Paginator(sessions, limit)
|
||||||
@ -97,7 +94,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
|||||||
args=[last_session.pk],
|
args=[last_session.pk],
|
||||||
),
|
),
|
||||||
children=Popover(
|
children=Popover(
|
||||||
popover_content=last_session.purchase.first_edition.name,
|
popover_content=last_session.purchase.edition.name,
|
||||||
children=[
|
children=[
|
||||||
Button(
|
Button(
|
||||||
icon=True,
|
icon=True,
|
||||||
@ -106,15 +103,13 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
|||||||
children=[
|
children=[
|
||||||
Icon("play"),
|
Icon("play"),
|
||||||
truncate(
|
truncate(
|
||||||
f"{last_session.purchase.first_edition.name}"
|
f"{last_session.purchase.edition.name}"
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
if last_session
|
|
||||||
else "",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -131,8 +126,8 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
|||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
LinkedNameWithPlatformIcon(
|
LinkedNameWithPlatformIcon(
|
||||||
name=session.purchase.first_edition.name,
|
name=session.purchase.edition.name,
|
||||||
game_id=session.purchase.first_edition.game.pk,
|
game_id=session.purchase.edition.game.pk,
|
||||||
platform=session.purchase.platform,
|
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 ""}",
|
||||||
|
2
poetry.lock
generated
2
poetry.lock
generated
@ -1155,4 +1155,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "b5bb46a6591964aec145637cd9a412a681f2cc5e7e4fdd6fd9ecb0fe8724b8e3"
|
content-hash = "f70c0aa9de08a0d7310b1379b1c5452b1a2597e57e170c9906ac85b256ec0506"
|
||||||
|
@ -33,7 +33,6 @@ django-cotton = "^1.2.1"
|
|||||||
django-q2 = "^1.7.4"
|
django-q2 = "^1.7.4"
|
||||||
croniter = "^5.0.1"
|
croniter = "^5.0.1"
|
||||||
requests = "^2.32.3"
|
requests = "^2.32.3"
|
||||||
pyyaml = "^6.0.2"
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user