Compare commits
No commits in common. "6bd82712910da58e599702be8de08f8d58091cd6" and "33103daebc9dc03a0965de89ca29db593075d482" have entirely different histories.
6bd8271291
...
33103daebc
@ -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 Game, Purchase, Session
|
from games.models import Edition, Game, Purchase, Session
|
||||||
|
|
||||||
HTMLAttribute = tuple[str, str | int | bool]
|
HTMLAttribute = tuple[str, str | int | bool]
|
||||||
HTMLTag = str
|
HTMLTag = str
|
||||||
@ -192,24 +192,24 @@ 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 = ""
|
||||||
popover_content = ""
|
popover_content = ""
|
||||||
game_count = purchase.games.count()
|
edition_count = purchase.editions.count()
|
||||||
popover_if_not_truncated = False
|
popover_if_not_truncated = False
|
||||||
if game_count == 1:
|
if edition_count == 1:
|
||||||
link_content += purchase.games.first().name
|
link_content += purchase.editions.first().name
|
||||||
popover_content = link_content
|
popover_content = link_content
|
||||||
if game_count > 1:
|
if edition_count > 1:
|
||||||
if purchase.name:
|
if purchase.name:
|
||||||
link_content += f"{purchase.name}"
|
link_content += f"{purchase.name}"
|
||||||
popover_content += f"<h1>{purchase.name}</h1><br>"
|
popover_content += f"<h1>{purchase.name}</h1><br>"
|
||||||
else:
|
else:
|
||||||
link_content += f"{game_count} games"
|
link_content += f"{edition_count} games"
|
||||||
popover_if_not_truncated = True
|
popover_if_not_truncated = True
|
||||||
popover_content += f"""
|
popover_content += f"""
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-disc list-inside">
|
||||||
{"".join(f"<li>{game.name}</li>" for game in purchase.games.all())}
|
{"".join(f"<li>{edition.name}</li>" for edition in purchase.editions.all())}
|
||||||
</ul>
|
</ul>
|
||||||
"""
|
"""
|
||||||
icon = purchase.platform.icon if game_count == 1 else "unspecified"
|
icon = purchase.platform.icon if edition_count == 1 else "unspecified"
|
||||||
if link_content == "":
|
if link_content == "":
|
||||||
raise ValueError("link_content is empty!!")
|
raise ValueError("link_content is empty!!")
|
||||||
a_content = Div(
|
a_content = Div(
|
||||||
@ -235,25 +235,34 @@ def NameWithIcon(
|
|||||||
game_id: int = 0,
|
game_id: int = 0,
|
||||||
session_id: int = 0,
|
session_id: int = 0,
|
||||||
purchase_id: int = 0,
|
purchase_id: int = 0,
|
||||||
|
edition_id: int = 0,
|
||||||
linkify: bool = True,
|
linkify: bool = True,
|
||||||
emulated: bool = False,
|
emulated: bool = False,
|
||||||
) -> SafeText:
|
) -> SafeText:
|
||||||
create_link = False
|
create_link = False
|
||||||
link = ""
|
link = ""
|
||||||
|
edition = None
|
||||||
platform = None
|
platform = None
|
||||||
if (game_id != 0 or session_id != 0 or purchase_id != 0) and linkify:
|
if (
|
||||||
|
game_id != 0 or session_id != 0 or purchase_id != 0 or edition_id != 0
|
||||||
|
) and linkify:
|
||||||
create_link = True
|
create_link = True
|
||||||
if session_id:
|
if session_id:
|
||||||
session = Session.objects.get(pk=session_id)
|
session = Session.objects.get(pk=session_id)
|
||||||
emulated = session.emulated
|
emulated = session.emulated
|
||||||
game_id = session.game.pk
|
edition = session.purchase.first_edition
|
||||||
|
game_id = edition.game.pk
|
||||||
if purchase_id:
|
if purchase_id:
|
||||||
purchase = Purchase.objects.get(pk=purchase_id)
|
purchase = Purchase.objects.get(pk=purchase_id)
|
||||||
game_id = purchase.games.first().pk
|
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:
|
if game_id:
|
||||||
game = Game.objects.get(pk=game_id)
|
game = Game.objects.get(pk=game_id)
|
||||||
name = game.name
|
name = edition.name if edition else game.name
|
||||||
platform = game.platform
|
platform = edition.platform if edition else None
|
||||||
link = reverse("view_game", args=[int(game_id)])
|
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")],
|
||||||
|
@ -2,6 +2,7 @@ from django.contrib import admin
|
|||||||
|
|
||||||
from games.models import (
|
from games.models import (
|
||||||
Device,
|
Device,
|
||||||
|
Edition,
|
||||||
ExchangeRate,
|
ExchangeRate,
|
||||||
Game,
|
Game,
|
||||||
Platform,
|
Platform,
|
||||||
@ -14,5 +15,6 @@ admin.site.register(Game)
|
|||||||
admin.site.register(Purchase)
|
admin.site.register(Purchase)
|
||||||
admin.site.register(Platform)
|
admin.site.register(Platform)
|
||||||
admin.site.register(Session)
|
admin.site.register(Session)
|
||||||
|
admin.site.register(Edition)
|
||||||
admin.site.register(Device)
|
admin.site.register(Device)
|
||||||
admin.site.register(ExchangeRate)
|
admin.site.register(ExchangeRate)
|
||||||
|
@ -2,7 +2,7 @@ from django import forms
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.utils import safe_getattr
|
from common.utils import safe_getattr
|
||||||
from games.models import Device, Game, Platform, Purchase, Session
|
from games.models import Device, Edition, Game, Platform, Purchase, Session
|
||||||
|
|
||||||
custom_date_widget = forms.DateInput(attrs={"type": "date"})
|
custom_date_widget = forms.DateInput(attrs={"type": "date"})
|
||||||
custom_datetime_widget = forms.DateTimeInput(
|
custom_datetime_widget = forms.DateTimeInput(
|
||||||
@ -12,8 +12,11 @@ autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"})
|
|||||||
|
|
||||||
|
|
||||||
class SessionForm(forms.ModelForm):
|
class SessionForm(forms.ModelForm):
|
||||||
game = forms.ModelChoiceField(
|
# purchase = forms.ModelChoiceField(
|
||||||
queryset=Game.objects.order_by("sort_name"),
|
# queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name")
|
||||||
|
# )
|
||||||
|
purchase = forms.ModelChoiceField(
|
||||||
|
queryset=Purchase.objects.all(),
|
||||||
widget=forms.Select(attrs={"autofocus": "autofocus"}),
|
widget=forms.Select(attrs={"autofocus": "autofocus"}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,7 +29,7 @@ class SessionForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
model = Session
|
model = Session
|
||||||
fields = [
|
fields = [
|
||||||
"game",
|
"purchase",
|
||||||
"timestamp_start",
|
"timestamp_start",
|
||||||
"timestamp_end",
|
"timestamp_end",
|
||||||
"duration_manual",
|
"duration_manual",
|
||||||
@ -36,7 +39,7 @@ class SessionForm(forms.ModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class GameChoiceField(forms.ModelMultipleChoiceField):
|
class EditionChoiceField(forms.ModelMultipleChoiceField):
|
||||||
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})"
|
||||||
|
|
||||||
@ -54,19 +57,19 @@ class PurchaseForm(forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Automatically update related_purchase <select/>
|
# Automatically update related_purchase <select/>
|
||||||
# to only include purchases of the selected game.
|
# to only include purchases of the selected edition.
|
||||||
related_purchase_by_game_url = reverse("related_purchase_by_game")
|
related_purchase_by_edition_url = reverse("related_purchase_by_edition")
|
||||||
self.fields["games"].widget.attrs.update(
|
self.fields["editions"].widget.attrs.update(
|
||||||
{
|
{
|
||||||
"hx-trigger": "load, click",
|
"hx-trigger": "load, click",
|
||||||
"hx-get": related_purchase_by_game_url,
|
"hx-get": related_purchase_by_edition_url,
|
||||||
"hx-target": "#id_related_purchase",
|
"hx-target": "#id_related_purchase",
|
||||||
"hx-swap": "outerHTML",
|
"hx-swap": "outerHTML",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
games = GameChoiceField(
|
editions = EditionChoiceField(
|
||||||
queryset=Game.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"))
|
||||||
@ -84,7 +87,7 @@ class PurchaseForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
model = Purchase
|
model = Purchase
|
||||||
fields = [
|
fields = [
|
||||||
"games",
|
"editions",
|
||||||
"platform",
|
"platform",
|
||||||
"date_purchased",
|
"date_purchased",
|
||||||
"date_refunded",
|
"date_refunded",
|
||||||
@ -136,14 +139,24 @@ class GameModelChoiceField(forms.ModelChoiceField):
|
|||||||
return obj.sort_name
|
return obj.sort_name
|
||||||
|
|
||||||
|
|
||||||
class GameForm(forms.ModelForm):
|
class EditionForm(forms.ModelForm):
|
||||||
|
game = GameModelChoiceField(
|
||||||
|
queryset=Game.objects.order_by("sort_name"),
|
||||||
|
widget=IncludeNameSelect(attrs={"autofocus": "autofocus"}),
|
||||||
|
)
|
||||||
platform = forms.ModelChoiceField(
|
platform = forms.ModelChoiceField(
|
||||||
queryset=Platform.objects.order_by("name"), required=False
|
queryset=Platform.objects.order_by("name"), required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Edition
|
||||||
|
fields = ["game", "name", "sort_name", "platform", "year_released", "wikidata"]
|
||||||
|
|
||||||
|
|
||||||
|
class GameForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Game
|
model = Game
|
||||||
fields = ["name", "sort_name", "platform", "year_released", "wikidata"]
|
fields = ["name", "sort_name", "year_released", "wikidata"]
|
||||||
widgets = {"name": autofocus_input_widget}
|
widgets = {"name": autofocus_input_widget}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from .device import Query as DeviceQuery
|
from .device import Query as DeviceQuery
|
||||||
|
from .edition import Query as EditionQuery
|
||||||
from .game import Query as GameQuery
|
from .game import Query as GameQuery
|
||||||
from .platform import Query as PlatformQuery
|
from .platform import Query as PlatformQuery
|
||||||
from .purchase import Query as PurchaseQuery
|
from .purchase import Query as PurchaseQuery
|
||||||
|
11
games/graphql/queries/edition.py
Normal file
11
games/graphql/queries/edition.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import graphene
|
||||||
|
|
||||||
|
from games.graphql.types import Edition
|
||||||
|
from games.models import Game as EditionModel
|
||||||
|
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
editions = graphene.List(Edition)
|
||||||
|
|
||||||
|
def resolve_editions(self, info, **kwargs):
|
||||||
|
return EditionModel.objects.all()
|
@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 5.1.5 on 2025-01-29 17:01
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('games', '0046_session_emulated'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='edition',
|
|
||||||
name='game',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='editions', to='games.game'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,61 +0,0 @@
|
|||||||
# Generated by Django 5.1.5 on 2025-01-29 17:08
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
from games.models import Game
|
|
||||||
|
|
||||||
|
|
||||||
def copy_platform_to_game(apps, schema_editor):
|
|
||||||
single_edition_games = Game.objects.annotate(
|
|
||||||
num_editions=models.Count("editions")
|
|
||||||
).filter(num_editions=1)
|
|
||||||
multi_edition_games = Game.objects.annotate(
|
|
||||||
num_editions=models.Count("editions")
|
|
||||||
).filter(num_editions__gt=1)
|
|
||||||
for game in single_edition_games:
|
|
||||||
game.platform = game.editions.first().platform
|
|
||||||
game.save()
|
|
||||||
|
|
||||||
for game in multi_edition_games:
|
|
||||||
all_editions = game.editions.all()
|
|
||||||
for e in all_editions:
|
|
||||||
# game with this platform edition already exists
|
|
||||||
if game.platform == e.platform:
|
|
||||||
print(
|
|
||||||
f"Game '{game}' with ID '{game.pk}' already has edition with platform '{game.platform}', skipping creation."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"Game '{game}' with ID '{game.pk}' missing edition with platform '{e.platform}', creating..."
|
|
||||||
)
|
|
||||||
newgame = Game.objects.create(
|
|
||||||
name=e.name,
|
|
||||||
sort_name=e.sort_name,
|
|
||||||
platform=e.platform,
|
|
||||||
year_released=e.year_released,
|
|
||||||
)
|
|
||||||
print(f"Setting edition to a newly created game with id '{newgame.pk}'")
|
|
||||||
e.game = newgame
|
|
||||||
e.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("games", "0047_alter_edition_game"),
|
|
||||||
]
|
|
||||||
|
|
||||||
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.RunPython(copy_platform_to_game),
|
|
||||||
]
|
|
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.1.5 on 2025-01-29 17:34
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('games', '0048_game_platform'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='game',
|
|
||||||
unique_together={('name', 'platform', 'year_released')},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,35 +0,0 @@
|
|||||||
# Generated by Django 5.1.5 on 2025-01-29 17:48
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
from games.models import Session
|
|
||||||
|
|
||||||
|
|
||||||
def connect_session_to_game(apps, schema_editor):
|
|
||||||
for session in Session.objects.all():
|
|
||||||
game = session.purchase.first_edition.game
|
|
||||||
session.game = game
|
|
||||||
session.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("games", "0049_alter_game_unique_together"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="session",
|
|
||||||
name="game",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="sessions",
|
|
||||||
to="games.game",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RunPython(connect_session_to_game),
|
|
||||||
]
|
|
@ -1,29 +0,0 @@
|
|||||||
# Generated by Django 5.1.5 on 2025-01-29 18:03
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
from games.models import Purchase
|
|
||||||
|
|
||||||
|
|
||||||
def connect_purchase_to_game(apps, schema_editor):
|
|
||||||
for purchase in Purchase.objects.all():
|
|
||||||
game = purchase.first_edition.game
|
|
||||||
purchase.games.add(game)
|
|
||||||
purchase.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("games", "0050_session_game"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="purchase",
|
|
||||||
name="games",
|
|
||||||
field=models.ManyToManyField(
|
|
||||||
blank=True, related_name="purchases", to="games.game"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RunPython(connect_purchase_to_game),
|
|
||||||
]
|
|
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.1.5 on 2025-01-29 18:20
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('games', '0051_purchase_games'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='purchase',
|
|
||||||
name='editions',
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,16 +0,0 @@
|
|||||||
# Generated by Django 5.1.5 on 2025-01-29 19:21
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('games', '0052_remove_purchase_editions'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='Edition',
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.1.5 on 2025-01-29 19:21
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('games', '0053_delete_edition'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='session',
|
|
||||||
name='purchase',
|
|
||||||
),
|
|
||||||
]
|
|
@ -10,17 +10,10 @@ from common.time import format_duration
|
|||||||
|
|
||||||
|
|
||||||
class Game(models.Model):
|
class Game(models.Model):
|
||||||
class Meta:
|
|
||||||
unique_together = [["name", "platform", "year_released"]]
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
|
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
|
||||||
year_released = models.IntegerField(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)
|
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||||
platform = models.ForeignKey(
|
|
||||||
"Platform", on_delete=models.SET_DEFAULT, null=True, blank=True, default=None
|
|
||||||
)
|
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
session_average: float | int | timedelta | None
|
session_average: float | int | timedelta | None
|
||||||
@ -29,17 +22,6 @@ class Game(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if self.platform is None:
|
|
||||||
self.platform = get_sentinel_platform()
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def get_sentinel_platform():
|
|
||||||
return Platform.objects.get_or_create(
|
|
||||||
name="Unspecified", icon="unspecified", group="Unspecified"
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
|
|
||||||
class Platform(models.Model):
|
class Platform(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
@ -56,6 +38,35 @@ class Platform(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_sentinel_platform():
|
||||||
|
return Platform.objects.get_or_create(
|
||||||
|
name="Unspecified", icon="unspecified", group="Unspecified"
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
|
||||||
|
class Edition(models.Model):
|
||||||
|
class Meta:
|
||||||
|
unique_together = [["name", "platform", "year_released"]]
|
||||||
|
|
||||||
|
game = models.ForeignKey(Game, on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
|
||||||
|
platform = models.ForeignKey(
|
||||||
|
Platform, on_delete=models.SET_DEFAULT, 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)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.sort_name
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.platform is None:
|
||||||
|
self.platform = get_sentinel_platform()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseQueryset(models.QuerySet):
|
class PurchaseQueryset(models.QuerySet):
|
||||||
def refunded(self):
|
def refunded(self):
|
||||||
return self.filter(date_refunded__isnull=False)
|
return self.filter(date_refunded__isnull=False)
|
||||||
@ -102,8 +113,7 @@ class Purchase(models.Model):
|
|||||||
|
|
||||||
objects = PurchaseQueryset().as_manager()
|
objects = PurchaseQueryset().as_manager()
|
||||||
|
|
||||||
games = models.ManyToManyField(Game, related_name="purchases", blank=True)
|
editions = models.ManyToManyField(Edition, related_name="purchases", blank=True)
|
||||||
|
|
||||||
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
|
||||||
)
|
)
|
||||||
@ -133,26 +143,24 @@ class Purchase(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def standardized_name(self):
|
def standardized_name(self):
|
||||||
return self.name if self.name else self.first_game.name
|
return self.name if self.name else self.first_edition.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def first_game(self):
|
def first_edition(self):
|
||||||
return self.games.first()
|
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_game.platform} version on {self.platform}"
|
f"{self.first_edition.platform} version on {self.platform}"
|
||||||
if self.platform != self.first_game.platform
|
if self.platform != self.first_edition.platform
|
||||||
else self.platform
|
else self.platform
|
||||||
),
|
),
|
||||||
self.first_game.year_released,
|
self.first_edition.year_released,
|
||||||
self.get_ownership_type_display(),
|
self.get_ownership_type_display(),
|
||||||
]
|
]
|
||||||
return (
|
return f"{self.first_edition} ({', '.join(filter(None, map(str, additional_info)))})"
|
||||||
f"{self.first_game} ({', '.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
|
||||||
@ -203,14 +211,7 @@ class Session(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
get_latest_by = "timestamp_start"
|
get_latest_by = "timestamp_start"
|
||||||
|
|
||||||
game = models.ForeignKey(
|
purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE)
|
||||||
Game,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
default=None,
|
|
||||||
related_name="sessions",
|
|
||||||
)
|
|
||||||
timestamp_start = models.DateTimeField()
|
timestamp_start = models.DateTimeField()
|
||||||
timestamp_end = models.DateTimeField(blank=True, null=True)
|
timestamp_end = models.DateTimeField(blank=True, null=True)
|
||||||
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0))
|
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0))
|
||||||
@ -232,7 +233,7 @@ class Session(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
mark = ", manual" if self.is_manual() else ""
|
mark = ", manual" if self.is_manual() else ""
|
||||||
return f"{str(self.game)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
|
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
|
||||||
|
|
||||||
def finish_now(self):
|
def finish_now(self):
|
||||||
self.timestamp_end = timezone.now()
|
self.timestamp_end = timezone.now()
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
|
|
||||||
let syncData = [
|
let syncData = [
|
||||||
{
|
{
|
||||||
source: "#id_games",
|
source: "#id_edition",
|
||||||
source_value: "dataset.platform",
|
source_value: "dataset.platform",
|
||||||
target: "#id_platform",
|
target: "#id_platform",
|
||||||
target_value: "value",
|
target_value: "value",
|
||||||
@ -36,8 +36,8 @@ getEl("#id_type").onchange = () => {
|
|||||||
|
|
||||||
document.body.addEventListener("htmx:beforeRequest", function (event) {
|
document.body.addEventListener("htmx:beforeRequest", function (event) {
|
||||||
// Assuming 'Purchase1' is the element that triggers the HTMX request
|
// Assuming 'Purchase1' is the element that triggers the HTMX request
|
||||||
if (event.target.id === "id_games") {
|
if (event.target.id === "id_edition") {
|
||||||
var idEditionValue = document.getElementById("id_games").value;
|
var idEditionValue = document.getElementById("id_edition").value;
|
||||||
|
|
||||||
// Condition to check - replace this with your actual logic
|
// Condition to check - replace this with your actual logic
|
||||||
if (idEditionValue != "") {
|
if (idEditionValue != "") {
|
||||||
|
@ -36,7 +36,7 @@ function addToggleButton(targetNode) {
|
|||||||
targetNode.parentElement.appendChild(manualToggleButton);
|
targetNode.parentElement.appendChild(manualToggleButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleableFields = ["#id_games", "#id_platform"];
|
const toggleableFields = ["#id_game", "#id_edition", "#id_platform"];
|
||||||
|
|
||||||
toggleableFields.map((selector) => {
|
toggleableFields.map((selector) => {
|
||||||
addToggleButton(document.querySelector(selector));
|
addToggleButton(document.querySelector(selector));
|
||||||
|
12
games/templates/add_edition.html
Normal file
12
games/templates/add_edition.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<c-layouts.add>
|
||||||
|
<c-slot name="additional_row">
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<input type="submit"
|
||||||
|
name="submit_and_redirect"
|
||||||
|
value="Submit & Create Purchase" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</c-slot>
|
||||||
|
</c-layouts.add>
|
@ -5,7 +5,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="submit"
|
<input type="submit"
|
||||||
name="submit_and_redirect"
|
name="submit_and_redirect"
|
||||||
value="Submit & Create Purchase" />
|
value="Submit & Create Edition" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</c-slot>
|
</c-slot>
|
||||||
|
@ -36,8 +36,8 @@
|
|||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name relative align-top w-24 h-12 group">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name relative align-top w-24 h-12 group">
|
||||||
<span class="inline-block relative">
|
<span class="inline-block relative">
|
||||||
<a class="underline decoration-slate-500 sm:decoration-2 inline-block truncate max-w-20char group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4 group-hover:decoration-purple-900 group-hover:text-purple-100"
|
<a class="underline decoration-slate-500 sm:decoration-2 inline-block truncate max-w-20char group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4 group-hover:decoration-purple-900 group-hover:text-purple-100"
|
||||||
href="{% url 'view_game' session.game.id %}">
|
href="{% url 'view_game' session.purchase.edition.game.id %}">
|
||||||
{{ session.game.name }}
|
{{ session.purchase.edition.name }}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -57,6 +57,10 @@
|
|||||||
<a href="{% url 'add_game' %}"
|
<a href="{% url 'add_game' %}"
|
||||||
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Game</a>
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Game</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'add_edition' %}"
|
||||||
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Edition</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'add_platform' %}"
|
<a href="{% url 'add_platform' %}"
|
||||||
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platform</a>
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platform</a>
|
||||||
@ -98,6 +102,10 @@
|
|||||||
<a href="{% url 'list_games' %}"
|
<a href="{% url 'list_games' %}"
|
||||||
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Games</a>
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Games</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'list_editions' %}"
|
||||||
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Editions</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'list_platforms' %}"
|
<a href="{% url 'list_platforms' %}"
|
||||||
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platforms</a>
|
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platforms</a>
|
||||||
|
@ -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_game.id>
|
<c-gamelink :game_id=purchase.first_edition.game.id>
|
||||||
{{ purchase.name }} ({{ purchase.first_game.name }} {{ purchase.get_type_display }})
|
{{ purchase.name }} ({{ purchase.first_edition.edition.name }} {{ purchase.get_type_display }})
|
||||||
</c-gamelink>
|
</c-gamelink>
|
||||||
{% else %}
|
{% else %}
|
||||||
<c-gamelink :game_id=purchase.first_game.id :name=purchase.first_game.name />
|
<c-gamelink :game_id=purchase.first_edition.game.id :name=purchase.first_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">
|
||||||
|
@ -67,6 +67,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<c-h1 :badge="edition_count">Editions</c-h1>
|
||||||
|
<div class="mb-6">
|
||||||
|
<c-simple-table :rows=edition_data.rows :columns=edition_data.columns />
|
||||||
|
</div>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<c-h1 :badge="purchase_count">Purchases</c-h1>
|
<c-h1 :badge="purchase_count">Purchases</c-h1>
|
||||||
<c-simple-table :rows=purchase_data.rows :columns=purchase_data.columns />
|
<c-simple-table :rows=purchase_data.rows :columns=purchase_data.columns />
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-5 mb-3">
|
<div class="flex flex-col 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">{% if purchase.name %}{{ purchase.name }}{% else %}Unnamed purchase{% endif %}</span> <span class="text-slate-500 text-2xl">({{ purchase.games.count }} games)</span>
|
<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>
|
</span>
|
||||||
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
|
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
|
||||||
<a href="{% url 'edit_purchase' purchase.id %}">
|
<a href="{% url 'edit_purchase' purchase.id %}">
|
||||||
@ -19,20 +19,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>Price: {{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }} ({{ purchase.price | floatformat }} {{ purchase.price_currency }})</div>
|
||||||
Price:
|
|
||||||
{% if purchase.converted_price %}
|
|
||||||
{{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }}
|
|
||||||
{% else %}
|
|
||||||
None
|
|
||||||
{% endif %}
|
|
||||||
({{ purchase.price | floatformat }} {{ purchase.price_currency }})
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-base">Items:</h2>
|
<h2 class="text-base">Items:</h2>
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-disc list-inside">
|
||||||
{% for game in purchase.games.all %}
|
{% for edition in purchase.editions.all %}
|
||||||
<li><c-gamelink :game_id=game.id :name=game.name /></li>
|
<li><c-gamelink :game_id=edition.game.id :name=edition.name /></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from games.views import device, game, general, platform, purchase, session
|
from games.views import device, edition, game, general, platform, purchase, session
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", general.index, name="index"),
|
path("", general.index, name="index"),
|
||||||
@ -8,6 +8,19 @@ urlpatterns = [
|
|||||||
path("device/delete/<int:device_id>", device.delete_device, name="delete_device"),
|
path("device/delete/<int:device_id>", device.delete_device, name="delete_device"),
|
||||||
path("device/edit/<int:device_id>", device.edit_device, name="edit_device"),
|
path("device/edit/<int:device_id>", device.edit_device, name="edit_device"),
|
||||||
path("device/list", device.list_devices, name="list_devices"),
|
path("device/list", device.list_devices, name="list_devices"),
|
||||||
|
path("edition/add", edition.add_edition, name="add_edition"),
|
||||||
|
path(
|
||||||
|
"edition/add/for-game/<int:game_id>",
|
||||||
|
edition.add_edition,
|
||||||
|
name="add_edition_for_game",
|
||||||
|
),
|
||||||
|
path("edition/<int:edition_id>/edit", edition.edit_edition, name="edit_edition"),
|
||||||
|
path("edition/list", edition.list_editions, name="list_editions"),
|
||||||
|
path(
|
||||||
|
"edition/<int:edition_id>/delete",
|
||||||
|
edition.delete_edition,
|
||||||
|
name="delete_edition",
|
||||||
|
),
|
||||||
path("game/add", game.add_game, name="add_game"),
|
path("game/add", game.add_game, name="add_game"),
|
||||||
path("game/<int:game_id>/edit", game.edit_game, name="edit_game"),
|
path("game/<int:game_id>/edit", game.edit_game, name="edit_game"),
|
||||||
path("game/<int:game_id>/view", game.view_game, name="view_game"),
|
path("game/<int:game_id>/view", game.view_game, name="view_game"),
|
||||||
@ -26,11 +39,6 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path("platform/list", platform.list_platforms, name="list_platforms"),
|
path("platform/list", platform.list_platforms, name="list_platforms"),
|
||||||
path("purchase/add", purchase.add_purchase, name="add_purchase"),
|
path("purchase/add", purchase.add_purchase, name="add_purchase"),
|
||||||
path(
|
|
||||||
"purchase/add/for-game/<int:game_id>",
|
|
||||||
purchase.add_purchase,
|
|
||||||
name="add_purchase_for_game",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"purchase/<int:purchase_id>/edit",
|
"purchase/<int:purchase_id>/edit",
|
||||||
purchase.edit_purchase,
|
purchase.edit_purchase,
|
||||||
@ -67,15 +75,20 @@ urlpatterns = [
|
|||||||
name="refund_purchase",
|
name="refund_purchase",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"purchase/related-purchase-by-game",
|
"purchase/related-purchase-by-edition",
|
||||||
purchase.related_purchase_by_game,
|
purchase.related_purchase_by_edition,
|
||||||
name="related_purchase_by_game",
|
name="related_purchase_by_edition",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"purchase/add/for-edition/<int:edition_id>",
|
||||||
|
purchase.add_purchase,
|
||||||
|
name="add_purchase_for_edition",
|
||||||
),
|
),
|
||||||
path("session/add", session.add_session, name="add_session"),
|
path("session/add", session.add_session, name="add_session"),
|
||||||
path(
|
path(
|
||||||
"session/add/for-game/<int:game_id>",
|
"session/add/for-purchase/<int:purchase_id>",
|
||||||
session.add_session,
|
session.add_session,
|
||||||
name="add_session_for_game",
|
name="add_session_for_purchase",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"session/add/from-game/<int:session_id>",
|
"session/add/from-game/<int:session_id>",
|
||||||
|
150
games/views/edition.py
Normal file
150
games/views/edition.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from common.components import (
|
||||||
|
A,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
NameWithIcon,
|
||||||
|
PopoverTruncated,
|
||||||
|
)
|
||||||
|
from common.time import dateformat, local_strftime
|
||||||
|
from games.forms import EditionForm
|
||||||
|
from games.models import Edition, Game
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def list_editions(request: HttpRequest) -> HttpResponse:
|
||||||
|
context: dict[Any, Any] = {}
|
||||||
|
page_number = request.GET.get("page", 1)
|
||||||
|
limit = request.GET.get("limit", 10)
|
||||||
|
editions = Edition.objects.order_by("-created_at")
|
||||||
|
page_obj = None
|
||||||
|
if int(limit) != 0:
|
||||||
|
paginator = Paginator(editions, limit)
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
editions = page_obj.object_list
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"title": "Manage editions",
|
||||||
|
"page_obj": page_obj or None,
|
||||||
|
"elided_page_range": (
|
||||||
|
page_obj.paginator.get_elided_page_range(
|
||||||
|
page_number, on_each_side=1, on_ends=1
|
||||||
|
)
|
||||||
|
if page_obj
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"data": {
|
||||||
|
"header_action": A([], Button([], "Add edition"), url="add_edition"),
|
||||||
|
"columns": [
|
||||||
|
"Game",
|
||||||
|
"Name",
|
||||||
|
"Sort Name",
|
||||||
|
"Year",
|
||||||
|
"Wikidata",
|
||||||
|
"Created",
|
||||||
|
"Actions",
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
NameWithIcon(edition_id=edition.pk),
|
||||||
|
PopoverTruncated(
|
||||||
|
edition.name
|
||||||
|
if edition.game.name != edition.name
|
||||||
|
else "(identical)"
|
||||||
|
),
|
||||||
|
PopoverTruncated(
|
||||||
|
edition.sort_name
|
||||||
|
if edition.sort_name is not None
|
||||||
|
and edition.game.name != edition.sort_name
|
||||||
|
else "(identical)"
|
||||||
|
),
|
||||||
|
edition.year_released,
|
||||||
|
edition.wikidata,
|
||||||
|
local_strftime(edition.created_at, dateformat),
|
||||||
|
render_to_string(
|
||||||
|
"cotton/button_group.html",
|
||||||
|
{
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"href": reverse("edit_edition", args=[edition.pk]),
|
||||||
|
"slot": Icon("edit"),
|
||||||
|
"color": "gray",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": reverse(
|
||||||
|
"delete_edition", args=[edition.pk]
|
||||||
|
),
|
||||||
|
"slot": Icon("delete"),
|
||||||
|
"color": "red",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for edition in editions
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return render(request, "list_purchases.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def edit_edition(request: HttpRequest, edition_id: int = 0) -> HttpResponse:
|
||||||
|
edition = get_object_or_404(Edition, id=edition_id)
|
||||||
|
form = EditionForm(request.POST or None, instance=edition)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return redirect("list_editions")
|
||||||
|
|
||||||
|
context: dict[str, Any] = {"form": form, "title": "Edit edition"}
|
||||||
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def delete_edition(request: HttpRequest, edition_id: int) -> HttpResponse:
|
||||||
|
edition = get_object_or_404(Edition, id=edition_id)
|
||||||
|
edition.delete()
|
||||||
|
return redirect("list_editions")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def add_edition(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
||||||
|
context: dict[str, Any] = {}
|
||||||
|
if request.method == "POST":
|
||||||
|
form = EditionForm(request.POST or None)
|
||||||
|
if form.is_valid():
|
||||||
|
edition = form.save()
|
||||||
|
if "submit_and_redirect" in request.POST:
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
reverse(
|
||||||
|
"add_purchase_for_edition", kwargs={"edition_id": edition.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return redirect("index")
|
||||||
|
else:
|
||||||
|
if game_id:
|
||||||
|
game = get_object_or_404(Game, id=game_id)
|
||||||
|
form = EditionForm(
|
||||||
|
initial={
|
||||||
|
"game": game,
|
||||||
|
"name": game.name,
|
||||||
|
"sort_name": game.sort_name,
|
||||||
|
"year_released": game.year_released,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = EditionForm()
|
||||||
|
|
||||||
|
context["form"] = form
|
||||||
|
context["title"] = "Add New Edition"
|
||||||
|
context["script_name"] = "add_edition.js"
|
||||||
|
return render(request, "add_edition.html", context)
|
@ -29,7 +29,7 @@ from common.time import (
|
|||||||
)
|
)
|
||||||
from common.utils import safe_division, truncate
|
from common.utils import safe_division, truncate
|
||||||
from games.forms import GameForm
|
from games.forms import GameForm
|
||||||
from games.models import Game, Purchase
|
from games.models import Edition, Game, Purchase, Session
|
||||||
from games.views.general import use_custom_redirect
|
from games.views.general import use_custom_redirect
|
||||||
|
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ def add_game(request: HttpRequest) -> HttpResponse:
|
|||||||
game = form.save()
|
game = form.save()
|
||||||
if "submit_and_redirect" in request.POST:
|
if "submit_and_redirect" in request.POST:
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
reverse("add_purchase_for_game", kwargs={"game_id": game.id})
|
reverse("add_edition_for_game", kwargs={"game_id": game.id})
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return redirect("list_games")
|
return redirect("list_games")
|
||||||
@ -158,12 +158,21 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
),
|
),
|
||||||
to_attr="game_purchases",
|
to_attr="game_purchases",
|
||||||
)
|
)
|
||||||
|
editions = (
|
||||||
|
Edition.objects.filter(game=game)
|
||||||
|
.prefetch_related(game_purchases_prefetch)
|
||||||
|
.order_by("year_released")
|
||||||
|
)
|
||||||
|
|
||||||
purchases = game.purchases.order_by("date_purchased")
|
purchases = Purchase.objects.filter(editions__game=game).order_by("date_purchased")
|
||||||
|
|
||||||
sessions = game.sessions
|
sessions = Session.objects.prefetch_related("device").filter(
|
||||||
|
purchase__editions__game=game
|
||||||
|
)
|
||||||
session_count = sessions.count()
|
session_count = sessions.count()
|
||||||
session_count_without_manual = game.sessions.without_manual().count()
|
session_count_without_manual = (
|
||||||
|
Session.objects.without_manual().filter(purchase__editions__game=game).count()
|
||||||
|
)
|
||||||
|
|
||||||
if sessions:
|
if sessions:
|
||||||
playrange_start = local_strftime(sessions.earliest().timestamp_start, "%b %Y")
|
playrange_start = local_strftime(sessions.earliest().timestamp_start, "%b %Y")
|
||||||
@ -184,6 +193,38 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
format_duration(sessions.calculated_duration_unformatted(), "%2.1H")
|
format_duration(sessions.calculated_duration_unformatted(), "%2.1H")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
edition_data: dict[str, Any] = {
|
||||||
|
"columns": [
|
||||||
|
"Name",
|
||||||
|
"Year Released",
|
||||||
|
"Actions",
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
NameWithIcon(edition_id=edition.pk),
|
||||||
|
edition.year_released,
|
||||||
|
render_to_string(
|
||||||
|
"cotton/button_group.html",
|
||||||
|
{
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"href": reverse("edit_edition", args=[edition.pk]),
|
||||||
|
"slot": Icon("edit"),
|
||||||
|
"color": "gray",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"href": reverse("delete_edition", args=[edition.pk]),
|
||||||
|
"slot": Icon("delete"),
|
||||||
|
"color": "red",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for edition in editions
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
purchase_data: dict[str, Any] = {
|
purchase_data: dict[str, Any] = {
|
||||||
"columns": ["Name", "Type", "Date", "Price", "Actions"],
|
"columns": ["Name", "Type", "Date", "Price", "Actions"],
|
||||||
"rows": [
|
"rows": [
|
||||||
@ -214,8 +255,9 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions_all = game.sessions.order_by("-timestamp_start")
|
sessions_all = Session.objects.filter(purchase__editions__game=game).order_by(
|
||||||
|
"-timestamp_start"
|
||||||
|
)
|
||||||
last_session = None
|
last_session = None
|
||||||
if sessions_all.exists():
|
if sessions_all.exists():
|
||||||
last_session = sessions_all.latest()
|
last_session = sessions_all.latest()
|
||||||
@ -242,7 +284,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.game.name,
|
popover_content=last_session.purchase.first_edition.name,
|
||||||
children=[
|
children=[
|
||||||
Button(
|
Button(
|
||||||
icon=True,
|
icon=True,
|
||||||
@ -250,7 +292,9 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
size="xs",
|
size="xs",
|
||||||
children=[
|
children=[
|
||||||
Icon("play"),
|
Icon("play"),
|
||||||
truncate(f"{last_session.game.name}"),
|
truncate(
|
||||||
|
f"{last_session.purchase.first_edition.name}"
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -260,7 +304,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
else "",
|
else "",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
"columns": ["Game", "Date", "Duration", "Actions"],
|
"columns": ["Edition", "Date", "Duration", "Actions"],
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
NameWithIcon(
|
NameWithIcon(
|
||||||
@ -310,9 +354,11 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
}
|
}
|
||||||
|
|
||||||
context: dict[str, Any] = {
|
context: dict[str, Any] = {
|
||||||
|
"edition_count": editions.count(),
|
||||||
|
"editions": editions,
|
||||||
"game": game,
|
"game": game,
|
||||||
"playrange": playrange,
|
"playrange": playrange,
|
||||||
"purchase_count": game.purchases.count(),
|
"purchase_count": Purchase.objects.filter(editions__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)
|
||||||
@ -323,6 +369,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||||||
"sessions": sessions,
|
"sessions": sessions,
|
||||||
"title": f"Game Overview - {game.name}",
|
"title": f"Game Overview - {game.name}",
|
||||||
"hours_sum": total_hours,
|
"hours_sum": total_hours,
|
||||||
|
"edition_data": edition_data,
|
||||||
"purchase_data": purchase_data,
|
"purchase_data": purchase_data,
|
||||||
"session_data": session_data,
|
"session_data": session_data,
|
||||||
"session_page_obj": session_page_obj,
|
"session_page_obj": session_page_obj,
|
||||||
|
@ -11,12 +11,13 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from common.time import available_stats_year_range, dateformat, format_duration
|
from common.time import available_stats_year_range, dateformat, format_duration
|
||||||
from common.utils import safe_division
|
from common.utils import safe_division
|
||||||
from games.models import Game, Platform, Purchase, Session
|
from games.models import Edition, Game, Platform, Purchase, Session
|
||||||
|
|
||||||
|
|
||||||
def model_counts(request: HttpRequest) -> dict[str, bool]:
|
def model_counts(request: HttpRequest) -> dict[str, bool]:
|
||||||
return {
|
return {
|
||||||
"game_available": Game.objects.exists(),
|
"game_available": Game.objects.exists(),
|
||||||
|
"edition_available": Edition.objects.exists(),
|
||||||
"platform_available": Platform.objects.exists(),
|
"platform_available": Platform.objects.exists(),
|
||||||
"purchase_available": Purchase.objects.exists(),
|
"purchase_available": Purchase.objects.exists(),
|
||||||
"session_count": Session.objects.exists(),
|
"session_count": Session.objects.exists(),
|
||||||
@ -48,7 +49,9 @@ 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(Prefetch("game"))
|
this_year_sessions = Session.objects.all().prefetch_related(
|
||||||
|
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"),
|
||||||
@ -56,9 +59,11 @@ 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(sessions__in=this_year_sessions).distinct()
|
this_year_games = Game.objects.filter(
|
||||||
|
editions__purchase__session__in=this_year_sessions
|
||||||
|
).distinct()
|
||||||
this_year_games_with_session_counts = this_year_games.annotate(
|
this_year_games_with_session_counts = this_year_games.annotate(
|
||||||
session_count=Count("sessions"),
|
session_count=Count("editions__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"
|
||||||
@ -71,11 +76,11 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||||||
.aggregate(dates=Count("date"))
|
.aggregate(dates=Count("date"))
|
||||||
)
|
)
|
||||||
this_year_played_purchases = Purchase.objects.filter(
|
this_year_played_purchases = Purchase.objects.filter(
|
||||||
games__sessions__in=this_year_sessions
|
session__in=this_year_sessions
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
this_year_purchases = Purchase.objects.all()
|
this_year_purchases = Purchase.objects.all()
|
||||||
this_year_purchases_with_currency = this_year_purchases.select_related("games")
|
this_year_purchases_with_currency = this_year_purchases.select_related("editions")
|
||||||
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
|
||||||
)
|
)
|
||||||
@ -124,10 +129,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(sessions__in=this_year_sessions)
|
Game.objects.filter(editions__purchase__session__in=this_year_sessions)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_playtime=Sum(
|
total_playtime=Sum(
|
||||||
F("sessions__duration_calculated") + F("sessions__duration_manual")
|
F("editions__purchase__session__duration_calculated")
|
||||||
|
+ F("editions__purchase__session__duration_manual")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.values("id", "name", "total_playtime")
|
.values("id", "name", "total_playtime")
|
||||||
@ -142,8 +148,10 @@ 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(sessions__in=this_year_sessions)
|
Game.objects.filter(editions__purchase__session__in=this_year_sessions)
|
||||||
.annotate(session_average=Avg("sessions__duration_calculated"))
|
.annotate(
|
||||||
|
session_average=Avg("editions__purchase__session__duration_calculated")
|
||||||
|
)
|
||||||
.order_by("-session_average")
|
.order_by("-session_average")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
@ -152,9 +160,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||||||
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
|
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
|
||||||
|
|
||||||
total_playtime_per_platform = (
|
total_playtime_per_platform = (
|
||||||
this_year_sessions.values("game__platform__name")
|
this_year_sessions.values("purchase__platform__name")
|
||||||
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
|
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
|
||||||
.annotate(platform_name=F("game__platform__name"))
|
.annotate(platform_name=F("purchase__platform__name"))
|
||||||
.values("platform_name", "total_playtime")
|
.values("platform_name", "total_playtime")
|
||||||
.order_by("-total_playtime")
|
.order_by("-total_playtime")
|
||||||
)
|
)
|
||||||
@ -169,10 +177,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.game
|
first_play_game = first_session.purchase.first_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.game
|
last_play_game = last_session.purchase.first_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()
|
||||||
@ -220,7 +228,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||||||
if longest_session
|
if longest_session
|
||||||
else 0
|
else 0
|
||||||
),
|
),
|
||||||
"longest_session_game": (longest_session.game if longest_session else None),
|
"longest_session_game": (
|
||||||
|
longest_session.purchase.first_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
|
||||||
if game_highest_session_count
|
if game_highest_session_count
|
||||||
@ -258,7 +268,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("game")
|
).prefetch_related("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"),
|
||||||
@ -266,11 +276,13 @@ 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(sessions__in=this_year_sessions).distinct()
|
this_year_games = Game.objects.filter(
|
||||||
|
edition__purchases__session__in=this_year_sessions
|
||||||
|
).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(
|
||||||
"sessions",
|
"edition__purchases__session",
|
||||||
filter=Q(sessions__timestamp_start__year=year),
|
filter=Q(edition__purchases__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(
|
||||||
@ -284,11 +296,11 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
.aggregate(dates=Count("date"))
|
.aggregate(dates=Count("date"))
|
||||||
)
|
)
|
||||||
this_year_played_purchases = Purchase.objects.filter(
|
this_year_played_purchases = Purchase.objects.filter(
|
||||||
games__sessions__in=this_year_sessions
|
session__in=this_year_sessions
|
||||||
).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("games")
|
this_year_purchases_with_currency = this_year_purchases.prefetch_related("editions")
|
||||||
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)
|
||||||
@ -325,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 = 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(games__year_released=year).order_by(
|
purchases_finished_this_year.filter(editions__year_released=year).order_by(
|
||||||
"date_finished"
|
"date_finished"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -339,10 +351,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(sessions__in=this_year_sessions)
|
Game.objects.filter(edition__purchases__session__in=this_year_sessions)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_playtime=Sum(
|
total_playtime=Sum(
|
||||||
F("sessions__duration_calculated") + F("sessions__duration_manual")
|
F("edition__purchases__session__duration_calculated")
|
||||||
|
+ F("edition__purchases__session__duration_manual")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.values("id", "name", "total_playtime")
|
.values("id", "name", "total_playtime")
|
||||||
@ -357,8 +370,10 @@ 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(sessions__in=this_year_sessions)
|
Game.objects.filter(edition__purchases__session__in=this_year_sessions)
|
||||||
.annotate(session_average=Avg("sessions__duration_calculated"))
|
.annotate(
|
||||||
|
session_average=Avg("edition__purchases__session__duration_calculated")
|
||||||
|
)
|
||||||
.order_by("-session_average")
|
.order_by("-session_average")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
@ -367,9 +382,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
|
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
|
||||||
|
|
||||||
total_playtime_per_platform = (
|
total_playtime_per_platform = (
|
||||||
this_year_sessions.values("game__platform__name")
|
this_year_sessions.values("purchase__platform__name")
|
||||||
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
|
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
|
||||||
.annotate(platform_name=F("game__platform__name"))
|
.annotate(platform_name=F("purchase__platform__name"))
|
||||||
.values("platform_name", "total_playtime")
|
.values("platform_name", "total_playtime")
|
||||||
.order_by("-total_playtime")
|
.order_by("-total_playtime")
|
||||||
)
|
)
|
||||||
@ -388,10 +403,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.game
|
first_play_game = first_session.purchase.first_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.game
|
last_play_game = last_session.purchase.first_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()
|
||||||
@ -408,7 +423,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(
|
||||||
games__year_released=year
|
editions__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,
|
||||||
@ -420,15 +435,15 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
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.prefetch_related(
|
||||||
"games"
|
"editions"
|
||||||
).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.prefetch_related(
|
||||||
"games"
|
"editions"
|
||||||
).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.prefetch_related(
|
||||||
"games"
|
"editions"
|
||||||
).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"],
|
||||||
@ -457,7 +472,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||||||
if longest_session
|
if longest_session
|
||||||
else 0
|
else 0
|
||||||
),
|
),
|
||||||
"longest_session_game": (longest_session.game if longest_session else None),
|
"longest_session_game": (
|
||||||
|
longest_session.purchase.first_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
|
||||||
if game_highest_session_count
|
if game_highest_session_count
|
||||||
|
@ -16,7 +16,7 @@ from django.utils import timezone
|
|||||||
from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice
|
from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice
|
||||||
from common.time import dateformat
|
from common.time import dateformat
|
||||||
from games.forms import PurchaseForm
|
from games.forms import PurchaseForm
|
||||||
from games.models import Game, Purchase
|
from games.models import Edition, Purchase
|
||||||
from games.views.general import use_custom_redirect
|
from games.views.general import use_custom_redirect
|
||||||
|
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def add_purchase(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
def add_purchase(request: HttpRequest, edition_id: int = 0) -> HttpResponse:
|
||||||
context: dict[str, Any] = {}
|
context: dict[str, Any] = {}
|
||||||
initial = {"date_purchased": timezone.now()}
|
initial = {"date_purchased": timezone.now()}
|
||||||
|
|
||||||
@ -149,20 +149,19 @@ def add_purchase(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
|||||||
if "submit_and_redirect" in request.POST:
|
if "submit_and_redirect" in request.POST:
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
reverse(
|
reverse(
|
||||||
"add_session_for_game",
|
"add_session_for_purchase", kwargs={"purchase_id": purchase.id}
|
||||||
kwargs={"game_id": purchase.first_game.id},
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return redirect("list_purchases")
|
return redirect("list_purchases")
|
||||||
else:
|
else:
|
||||||
if game_id:
|
if edition_id:
|
||||||
game = Game.objects.get(id=game_id)
|
edition = Edition.objects.get(id=edition_id)
|
||||||
form = PurchaseForm(
|
form = PurchaseForm(
|
||||||
initial={
|
initial={
|
||||||
**initial,
|
**initial,
|
||||||
"games": [game],
|
"edition": edition,
|
||||||
"platform": game.platform,
|
"platform": edition.platform,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -227,14 +226,12 @@ def finish_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
|
|||||||
return redirect("list_purchases")
|
return redirect("list_purchases")
|
||||||
|
|
||||||
|
|
||||||
def related_purchase_by_game(request: HttpRequest) -> HttpResponse:
|
def related_purchase_by_edition(request: HttpRequest) -> HttpResponse:
|
||||||
games = request.GET.getlist("games")
|
edition_id = request.GET.get("edition")
|
||||||
if not games:
|
if not edition_id:
|
||||||
return HttpResponseBadRequest("Invalid game_id")
|
return HttpResponseBadRequest("Invalid edition_id")
|
||||||
if isinstance(games, int) or isinstance(games, str):
|
|
||||||
games = [games]
|
|
||||||
form = PurchaseForm()
|
form = PurchaseForm()
|
||||||
form.fields["related_purchase"].queryset = Purchase.objects.filter(
|
form.fields["related_purchase"].queryset = Purchase.objects.filter(
|
||||||
games__in=games, type=Purchase.GAME
|
edition_id=edition_id, type=Purchase.GAME
|
||||||
).order_by("games__sort_name")
|
).order_by("edition__sort_name")
|
||||||
return render(request, "partials/related_purchase_field.html", {"form": form})
|
return render(request, "partials/related_purchase_field.html", {"form": form})
|
||||||
|
@ -28,7 +28,7 @@ from common.time import (
|
|||||||
)
|
)
|
||||||
from common.utils import truncate
|
from common.utils import truncate
|
||||||
from games.forms import SessionForm
|
from games.forms import SessionForm
|
||||||
from games.models import Game, Session
|
from games.models import Purchase, Session
|
||||||
from games.views.general import use_custom_redirect
|
from games.views.general import use_custom_redirect
|
||||||
|
|
||||||
|
|
||||||
@ -37,13 +37,13 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
|||||||
context: dict[Any, Any] = {}
|
context: dict[Any, Any] = {}
|
||||||
page_number = request.GET.get("page", 1)
|
page_number = request.GET.get("page", 1)
|
||||||
limit = request.GET.get("limit", 10)
|
limit = request.GET.get("limit", 10)
|
||||||
sessions = Session.objects.order_by("-timestamp_start", "created_at")
|
sessions = Session.objects.order_by("-timestamp_start")
|
||||||
search_string = request.GET.get("search_string", search_string)
|
search_string = request.GET.get("search_string", search_string)
|
||||||
if search_string != "":
|
if search_string != "":
|
||||||
sessions = sessions.filter(
|
sessions = sessions.filter(
|
||||||
Q(game__name__icontains=search_string)
|
Q(purchase__edition__name__icontains=search_string)
|
||||||
| Q(game__name__icontains=search_string)
|
| Q(purchase__edition__game__name__icontains=search_string)
|
||||||
| Q(game__platform__name__icontains=search_string)
|
| Q(purchase__platform__name__icontains=search_string)
|
||||||
| Q(device__name__icontains=search_string)
|
| Q(device__name__icontains=search_string)
|
||||||
| Q(device__type__icontains=search_string)
|
| Q(device__type__icontains=search_string)
|
||||||
)
|
)
|
||||||
@ -97,7 +97,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.game.name,
|
popover_content=last_session.purchase.first_edition.name,
|
||||||
children=[
|
children=[
|
||||||
Button(
|
Button(
|
||||||
icon=True,
|
icon=True,
|
||||||
@ -105,7 +105,9 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
|||||||
size="xs",
|
size="xs",
|
||||||
children=[
|
children=[
|
||||||
Icon("play"),
|
Icon("play"),
|
||||||
truncate(f"{last_session.game.name}"),
|
truncate(
|
||||||
|
f"{last_session.purchase.first_edition.name}"
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -189,13 +191,13 @@ def search_sessions(request: HttpRequest) -> HttpResponse:
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse:
|
||||||
context = {}
|
context = {}
|
||||||
initial: dict[str, Any] = {"timestamp_start": timezone.now()}
|
initial: dict[str, Any] = {"timestamp_start": timezone.now()}
|
||||||
|
|
||||||
last = Session.objects.last()
|
last = Session.objects.last()
|
||||||
if last != None:
|
if last != None:
|
||||||
initial["game"] = last.game
|
initial["purchase"] = last.purchase
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = SessionForm(request.POST or None, initial=initial)
|
form = SessionForm(request.POST or None, initial=initial)
|
||||||
@ -203,12 +205,12 @@ def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
|||||||
form.save()
|
form.save()
|
||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
else:
|
else:
|
||||||
if game_id:
|
if purchase_id:
|
||||||
game = Game.objects.get(id=game_id)
|
purchase = Purchase.objects.get(id=purchase_id)
|
||||||
form = SessionForm(
|
form = SessionForm(
|
||||||
initial={
|
initial={
|
||||||
**initial,
|
**initial,
|
||||||
"game": game,
|
"purchase": purchase,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -10,7 +10,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
|||||||
django.setup()
|
django.setup()
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from games.models import Game, Platform, Purchase, Session
|
from games.models import Edition, Game, Platform, Purchase, Session
|
||||||
|
|
||||||
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
|
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
|
||||||
|
|
||||||
@ -21,8 +21,10 @@ class PathWorksTest(TestCase):
|
|||||||
pl.save()
|
pl.save()
|
||||||
g = Game(name="The Test Game")
|
g = Game(name="The Test Game")
|
||||||
g.save()
|
g.save()
|
||||||
|
e = Edition(game=g, name="The Test Game Edition", platform=pl)
|
||||||
|
e.save()
|
||||||
p = Purchase(
|
p = Purchase(
|
||||||
games=[e],
|
edition=e,
|
||||||
platform=pl,
|
platform=pl,
|
||||||
date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO),
|
date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO),
|
||||||
)
|
)
|
||||||
@ -51,6 +53,11 @@ class PathWorksTest(TestCase):
|
|||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_add_edition_returns_200(self):
|
||||||
|
url = reverse("add_edition")
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_add_purchase_returns_200(self):
|
def test_add_purchase_returns_200(self):
|
||||||
url = reverse("add_purchase")
|
url = reverse("add_purchase")
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
|
@ -3,13 +3,14 @@ from datetime import datetime
|
|||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import django
|
import django
|
||||||
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
||||||
django.setup()
|
django.setup()
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from games.models import Game, Purchase, Session
|
from games.models import Edition, Game, Purchase, Session
|
||||||
|
|
||||||
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
|
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
|
||||||
|
|
||||||
@ -21,8 +22,10 @@ class FormatDurationTest(TestCase):
|
|||||||
def test_duration_format(self):
|
def test_duration_format(self):
|
||||||
g = Game(name="The Test Game")
|
g = Game(name="The Test Game")
|
||||||
g.save()
|
g.save()
|
||||||
|
e = Edition(game=g, name="The Test Game Edition")
|
||||||
|
e.save()
|
||||||
p = Purchase(
|
p = Purchase(
|
||||||
game=g, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO)
|
edition=e, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO)
|
||||||
)
|
)
|
||||||
p.save()
|
p.save()
|
||||||
s = Session(
|
s = Session(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user