Merge Edition into Game #85
@ -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 Edition, Game, Purchase, Session
 | 
					from games.models import 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 = ""
 | 
				
			||||||
    edition_count = purchase.editions.count()
 | 
					    game_count = purchase.games.count()
 | 
				
			||||||
    popover_if_not_truncated = False
 | 
					    popover_if_not_truncated = False
 | 
				
			||||||
    if edition_count == 1:
 | 
					    if game_count == 1:
 | 
				
			||||||
        link_content += purchase.editions.first().name
 | 
					        link_content += purchase.games.first().name
 | 
				
			||||||
        popover_content = link_content
 | 
					        popover_content = link_content
 | 
				
			||||||
    if edition_count > 1:
 | 
					    if game_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"{edition_count} games"
 | 
					            link_content += f"{game_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>{edition.name}</li>" for edition in purchase.editions.all())}
 | 
					            {"".join(f"<li>{game.name}</li>" for game in purchase.games.all())}
 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
    icon = purchase.platform.icon if edition_count == 1 else "unspecified"
 | 
					    icon = purchase.platform.icon if game_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,34 +235,25 @@ 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 (
 | 
					    if (game_id != 0 or session_id != 0 or purchase_id != 0) and linkify:
 | 
				
			||||||
        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
 | 
				
			||||||
            edition = session.purchase.first_edition
 | 
					            game_id = session.game.pk
 | 
				
			||||||
            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)
 | 
				
			||||||
            edition = purchase.first_edition
 | 
					            game_id = purchase.games.first().pk
 | 
				
			||||||
            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 = edition.name if edition else game.name
 | 
					        name = game.name
 | 
				
			||||||
        platform = edition.platform if edition else None
 | 
					        platform = game.platform
 | 
				
			||||||
        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,7 +2,6 @@ from django.contrib import admin
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from games.models import (
 | 
					from games.models import (
 | 
				
			||||||
    Device,
 | 
					    Device,
 | 
				
			||||||
    Edition,
 | 
					 | 
				
			||||||
    ExchangeRate,
 | 
					    ExchangeRate,
 | 
				
			||||||
    Game,
 | 
					    Game,
 | 
				
			||||||
    Platform,
 | 
					    Platform,
 | 
				
			||||||
@ -15,6 +14,5 @@ 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, Edition, Game, Platform, Purchase, Session
 | 
					from games.models import Device, 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,11 +12,8 @@ autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"})
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SessionForm(forms.ModelForm):
 | 
					class SessionForm(forms.ModelForm):
 | 
				
			||||||
    # purchase = forms.ModelChoiceField(
 | 
					    game = forms.ModelChoiceField(
 | 
				
			||||||
    #     queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name")
 | 
					        queryset=Game.objects.order_by("sort_name"),
 | 
				
			||||||
    # )
 | 
					 | 
				
			||||||
    purchase = forms.ModelChoiceField(
 | 
					 | 
				
			||||||
        queryset=Purchase.objects.all(),
 | 
					 | 
				
			||||||
        widget=forms.Select(attrs={"autofocus": "autofocus"}),
 | 
					        widget=forms.Select(attrs={"autofocus": "autofocus"}),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,7 +26,7 @@ class SessionForm(forms.ModelForm):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        model = Session
 | 
					        model = Session
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
            "purchase",
 | 
					            "game",
 | 
				
			||||||
            "timestamp_start",
 | 
					            "timestamp_start",
 | 
				
			||||||
            "timestamp_end",
 | 
					            "timestamp_end",
 | 
				
			||||||
            "duration_manual",
 | 
					            "duration_manual",
 | 
				
			||||||
@ -39,7 +36,7 @@ class SessionForm(forms.ModelForm):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EditionChoiceField(forms.ModelMultipleChoiceField):
 | 
					class GameChoiceField(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})"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -57,19 +54,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 edition.
 | 
					        # to only include purchases of the selected game.
 | 
				
			||||||
        related_purchase_by_edition_url = reverse("related_purchase_by_edition")
 | 
					        related_purchase_by_game_url = reverse("related_purchase_by_game")
 | 
				
			||||||
        self.fields["editions"].widget.attrs.update(
 | 
					        self.fields["games"].widget.attrs.update(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                "hx-trigger": "load, click",
 | 
					                "hx-trigger": "load, click",
 | 
				
			||||||
                "hx-get": related_purchase_by_edition_url,
 | 
					                "hx-get": related_purchase_by_game_url,
 | 
				
			||||||
                "hx-target": "#id_related_purchase",
 | 
					                "hx-target": "#id_related_purchase",
 | 
				
			||||||
                "hx-swap": "outerHTML",
 | 
					                "hx-swap": "outerHTML",
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    editions = EditionChoiceField(
 | 
					    games = GameChoiceField(
 | 
				
			||||||
        queryset=Edition.objects.order_by("sort_name"),
 | 
					        queryset=Game.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"))
 | 
				
			||||||
@ -87,7 +84,7 @@ class PurchaseForm(forms.ModelForm):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        model = Purchase
 | 
					        model = Purchase
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
            "editions",
 | 
					            "games",
 | 
				
			||||||
            "platform",
 | 
					            "platform",
 | 
				
			||||||
            "date_purchased",
 | 
					            "date_purchased",
 | 
				
			||||||
            "date_refunded",
 | 
					            "date_refunded",
 | 
				
			||||||
@ -139,24 +136,14 @@ class GameModelChoiceField(forms.ModelChoiceField):
 | 
				
			|||||||
        return obj.sort_name
 | 
					        return obj.sort_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EditionForm(forms.ModelForm):
 | 
					class GameForm(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", "year_released", "wikidata"]
 | 
					        fields = ["name", "sort_name", "platform", "year_released", "wikidata"]
 | 
				
			||||||
        widgets = {"name": autofocus_input_widget}
 | 
					        widgets = {"name": autofocus_input_widget}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
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
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +0,0 @@
 | 
				
			|||||||
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()
 | 
					 | 
				
			||||||
							
								
								
									
										19
									
								
								games/migrations/0047_alter_edition_game.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								games/migrations/0047_alter_edition_game.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# 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'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										61
									
								
								games/migrations/0048_game_platform.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								games/migrations/0048_game_platform.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					# 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),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										17
									
								
								games/migrations/0049_alter_game_unique_together.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								games/migrations/0049_alter_game_unique_together.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					# 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')},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										35
									
								
								games/migrations/0050_session_game.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								games/migrations/0050_session_game.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					# 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),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										29
									
								
								games/migrations/0051_purchase_games.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								games/migrations/0051_purchase_games.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					# 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),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										17
									
								
								games/migrations/0052_remove_purchase_editions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								games/migrations/0052_remove_purchase_editions.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					# 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',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										16
									
								
								games/migrations/0053_delete_edition.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								games/migrations/0053_delete_edition.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					# 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',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										17
									
								
								games/migrations/0054_remove_session_purchase.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								games/migrations/0054_remove_session_purchase.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					# 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,10 +10,17 @@ 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
 | 
				
			||||||
@ -22,6 +29,17 @@ 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)
 | 
				
			||||||
@ -38,35 +56,6 @@ 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)
 | 
				
			||||||
@ -113,7 +102,8 @@ class Purchase(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    objects = PurchaseQueryset().as_manager()
 | 
					    objects = PurchaseQueryset().as_manager()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    editions = models.ManyToManyField(Edition, related_name="purchases", blank=True)
 | 
					    games = models.ManyToManyField(Game, 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
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@ -143,24 +133,26 @@ class Purchase(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def standardized_name(self):
 | 
					    def standardized_name(self):
 | 
				
			||||||
        return self.name if self.name else self.first_edition.name
 | 
					        return self.name if self.name else self.first_game.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def first_edition(self):
 | 
					    def first_game(self):
 | 
				
			||||||
        return self.editions.first()
 | 
					        return self.games.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.first_game.platform} version on {self.platform}"
 | 
				
			||||||
                if self.platform != self.first_edition.platform
 | 
					                if self.platform != self.first_game.platform
 | 
				
			||||||
                else self.platform
 | 
					                else self.platform
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            self.first_edition.year_released,
 | 
					            self.first_game.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.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
 | 
				
			||||||
@ -211,7 +203,14 @@ class Session(models.Model):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        get_latest_by = "timestamp_start"
 | 
					        get_latest_by = "timestamp_start"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE)
 | 
					    game = models.ForeignKey(
 | 
				
			||||||
 | 
					        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))
 | 
				
			||||||
@ -233,7 +232,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.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
 | 
					        return f"{str(self.game)} {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_edition",
 | 
					    source: "#id_games",
 | 
				
			||||||
    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_edition") {
 | 
					  if (event.target.id === "id_games") {
 | 
				
			||||||
    var idEditionValue = document.getElementById("id_edition").value;
 | 
					    var idEditionValue = document.getElementById("id_games").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_game", "#id_edition", "#id_platform"];
 | 
					const toggleableFields = ["#id_games", "#id_platform"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
toggleableFields.map((selector) => {
 | 
					toggleableFields.map((selector) => {
 | 
				
			||||||
  addToggleButton(document.querySelector(selector));
 | 
					  addToggleButton(document.querySelector(selector));
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +0,0 @@
 | 
				
			|||||||
<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 Edition" />
 | 
					               value="Submit & Create Purchase" />
 | 
				
			||||||
    </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.purchase.edition.game.id %}">
 | 
					                                       href="{% url 'view_game' session.game.id %}">
 | 
				
			||||||
                                        {{ session.purchase.edition.name }}
 | 
					                                        {{ session.game.name }}
 | 
				
			||||||
                                    </a>
 | 
					                                    </a>
 | 
				
			||||||
                                </span>
 | 
					                                </span>
 | 
				
			||||||
                            </td>
 | 
					                            </td>
 | 
				
			||||||
 | 
				
			|||||||
@ -57,10 +57,6 @@
 | 
				
			|||||||
                                <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>
 | 
				
			||||||
@ -102,10 +98,6 @@
 | 
				
			|||||||
                                <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_edition.game.id>
 | 
					    <c-gamelink :game_id=purchase.first_game.id>
 | 
				
			||||||
    {{ purchase.name }} ({{ purchase.first_edition.edition.name }} {{ purchase.get_type_display }})
 | 
					    {{ purchase.name }} ({{ purchase.first_game.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.first_game.id :name=purchase.first_game.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,10 +67,6 @@
 | 
				
			|||||||
                </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.editions.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.games.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,12 +19,20 @@
 | 
				
			|||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
        </a>
 | 
					        </a>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div>Price: {{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }} ({{ purchase.price | floatformat }} {{ purchase.price_currency }})</div>
 | 
					    <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 edition in purchase.editions.all %}
 | 
					        {% for game in purchase.games.all %}
 | 
				
			||||||
        <li><c-gamelink :game_id=edition.game.id :name=edition.name /></li>
 | 
					        <li><c-gamelink :game_id=game.id :name=game.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, edition, game, general, platform, purchase, session
 | 
					from games.views import device, game, general, platform, purchase, session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path("", general.index, name="index"),
 | 
					    path("", general.index, name="index"),
 | 
				
			||||||
@ -8,19 +8,6 @@ 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"),
 | 
				
			||||||
@ -39,6 +26,11 @@ 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,
 | 
				
			||||||
@ -75,20 +67,15 @@ urlpatterns = [
 | 
				
			|||||||
        name="refund_purchase",
 | 
					        name="refund_purchase",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
        "purchase/related-purchase-by-edition",
 | 
					        "purchase/related-purchase-by-game",
 | 
				
			||||||
        purchase.related_purchase_by_edition,
 | 
					        purchase.related_purchase_by_game,
 | 
				
			||||||
        name="related_purchase_by_edition",
 | 
					        name="related_purchase_by_game",
 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
    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-purchase/<int:purchase_id>",
 | 
					        "session/add/for-game/<int:game_id>",
 | 
				
			||||||
        session.add_session,
 | 
					        session.add_session,
 | 
				
			||||||
        name="add_session_for_purchase",
 | 
					        name="add_session_for_game",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
        "session/add/from-game/<int:session_id>",
 | 
					        "session/add/from-game/<int:session_id>",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,150 +0,0 @@
 | 
				
			|||||||
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 Edition, Game, Purchase, Session
 | 
					from games.models import Game, Purchase
 | 
				
			||||||
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_edition_for_game", kwargs={"game_id": game.id})
 | 
					                reverse("add_purchase_for_game", kwargs={"game_id": game.id})
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            return redirect("list_games")
 | 
					            return redirect("list_games")
 | 
				
			||||||
@ -158,21 +158,12 @@ 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 = Purchase.objects.filter(editions__game=game).order_by("date_purchased")
 | 
					    purchases = game.purchases.order_by("date_purchased")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sessions = Session.objects.prefetch_related("device").filter(
 | 
					    sessions = game.sessions
 | 
				
			||||||
        purchase__editions__game=game
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    session_count = sessions.count()
 | 
					    session_count = sessions.count()
 | 
				
			||||||
    session_count_without_manual = (
 | 
					    session_count_without_manual = game.sessions.without_manual().count()
 | 
				
			||||||
        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")
 | 
				
			||||||
@ -193,38 +184,6 @@ 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": [
 | 
				
			||||||
@ -255,9 +214,8 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sessions_all = Session.objects.filter(purchase__editions__game=game).order_by(
 | 
					    sessions_all = game.sessions.order_by("-timestamp_start")
 | 
				
			||||||
        "-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()
 | 
				
			||||||
@ -284,7 +242,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.game.name,
 | 
				
			||||||
                        children=[
 | 
					                        children=[
 | 
				
			||||||
                            Button(
 | 
					                            Button(
 | 
				
			||||||
                                icon=True,
 | 
					                                icon=True,
 | 
				
			||||||
@ -292,9 +250,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.game.name}"),
 | 
				
			||||||
                                        f"{last_session.purchase.first_edition.name}"
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                ],
 | 
					                                ],
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
@ -304,7 +260,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
 | 
				
			|||||||
                else "",
 | 
					                else "",
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        "columns": ["Edition", "Date", "Duration", "Actions"],
 | 
					        "columns": ["Game", "Date", "Duration", "Actions"],
 | 
				
			||||||
        "rows": [
 | 
					        "rows": [
 | 
				
			||||||
            [
 | 
					            [
 | 
				
			||||||
                NameWithIcon(
 | 
					                NameWithIcon(
 | 
				
			||||||
@ -354,11 +310,9 @@ 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": Purchase.objects.filter(editions__game=game).count(),
 | 
					        "purchase_count": game.purchases.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)
 | 
				
			||||||
@ -369,7 +323,6 @@ 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,13 +11,12 @@ 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 Edition, Game, Platform, Purchase, Session
 | 
					from games.models import 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(),
 | 
				
			||||||
@ -49,9 +48,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().prefetch_related(Prefetch("game"))
 | 
				
			||||||
        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"),
 | 
				
			||||||
@ -59,11 +56,9 @@ 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(sessions__in=this_year_sessions).distinct()
 | 
				
			||||||
        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("editions__purchase__session"),
 | 
					        session_count=Count("sessions"),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    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"
 | 
				
			||||||
@ -76,11 +71,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(
 | 
				
			||||||
        session__in=this_year_sessions
 | 
					        games__sessions__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("editions")
 | 
					    this_year_purchases_with_currency = this_year_purchases.select_related("games")
 | 
				
			||||||
    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 +124,10 @@ 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(sessions__in=this_year_sessions)
 | 
				
			||||||
        .annotate(
 | 
					        .annotate(
 | 
				
			||||||
            total_playtime=Sum(
 | 
					            total_playtime=Sum(
 | 
				
			||||||
                F("editions__purchase__session__duration_calculated")
 | 
					                F("sessions__duration_calculated") + F("sessions__duration_manual")
 | 
				
			||||||
                + F("editions__purchase__session__duration_manual")
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .values("id", "name", "total_playtime")
 | 
					        .values("id", "name", "total_playtime")
 | 
				
			||||||
@ -148,10 +142,8 @@ 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(sessions__in=this_year_sessions)
 | 
				
			||||||
        .annotate(
 | 
					        .annotate(session_average=Avg("sessions__duration_calculated"))
 | 
				
			||||||
            session_average=Avg("editions__purchase__session__duration_calculated")
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .order_by("-session_average")
 | 
					        .order_by("-session_average")
 | 
				
			||||||
        .first()
 | 
					        .first()
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@ -160,9 +152,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("purchase__platform__name")
 | 
					        this_year_sessions.values("game__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("purchase__platform__name"))
 | 
					        .annotate(platform_name=F("game__platform__name"))
 | 
				
			||||||
        .values("platform_name", "total_playtime")
 | 
					        .values("platform_name", "total_playtime")
 | 
				
			||||||
        .order_by("-total_playtime")
 | 
					        .order_by("-total_playtime")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@ -177,10 +169,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.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.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()
 | 
				
			||||||
@ -228,9 +220,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
 | 
				
			|||||||
            if longest_session
 | 
					            if longest_session
 | 
				
			||||||
            else 0
 | 
					            else 0
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        "longest_session_game": (
 | 
					        "longest_session_game": (longest_session.game if longest_session else None),
 | 
				
			||||||
            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
 | 
				
			||||||
@ -268,7 +258,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")
 | 
					    ).prefetch_related("game")
 | 
				
			||||||
    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"),
 | 
				
			||||||
@ -276,13 +266,11 @@ 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(sessions__in=this_year_sessions).distinct()
 | 
				
			||||||
        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(
 | 
				
			||||||
            "edition__purchases__session",
 | 
					            "sessions",
 | 
				
			||||||
            filter=Q(edition__purchases__session__timestamp_start__year=year),
 | 
					            filter=Q(sessions__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(
 | 
				
			||||||
@ -296,11 +284,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(
 | 
				
			||||||
        session__in=this_year_sessions
 | 
					        games__sessions__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("editions")
 | 
					    this_year_purchases_with_currency = this_year_purchases.prefetch_related("games")
 | 
				
			||||||
    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 +325,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(games__year_released=year).order_by(
 | 
				
			||||||
            "date_finished"
 | 
					            "date_finished"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@ -351,11 +339,10 @@ 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(sessions__in=this_year_sessions)
 | 
				
			||||||
        .annotate(
 | 
					        .annotate(
 | 
				
			||||||
            total_playtime=Sum(
 | 
					            total_playtime=Sum(
 | 
				
			||||||
                F("edition__purchases__session__duration_calculated")
 | 
					                F("sessions__duration_calculated") + F("sessions__duration_manual")
 | 
				
			||||||
                + F("edition__purchases__session__duration_manual")
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .values("id", "name", "total_playtime")
 | 
					        .values("id", "name", "total_playtime")
 | 
				
			||||||
@ -370,10 +357,8 @@ 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(sessions__in=this_year_sessions)
 | 
				
			||||||
        .annotate(
 | 
					        .annotate(session_average=Avg("sessions__duration_calculated"))
 | 
				
			||||||
            session_average=Avg("edition__purchases__session__duration_calculated")
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .order_by("-session_average")
 | 
					        .order_by("-session_average")
 | 
				
			||||||
        .first()
 | 
					        .first()
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@ -382,9 +367,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("purchase__platform__name")
 | 
					        this_year_sessions.values("game__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("purchase__platform__name"))
 | 
					        .annotate(platform_name=F("game__platform__name"))
 | 
				
			||||||
        .values("platform_name", "total_playtime")
 | 
					        .values("platform_name", "total_playtime")
 | 
				
			||||||
        .order_by("-total_playtime")
 | 
					        .order_by("-total_playtime")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@ -403,10 +388,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.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.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 +408,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
 | 
					            games__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,
 | 
				
			||||||
@ -435,15 +420,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(
 | 
				
			||||||
            "editions"
 | 
					            "games"
 | 
				
			||||||
        ).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(
 | 
				
			||||||
            "editions"
 | 
					            "games"
 | 
				
			||||||
        ).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(
 | 
				
			||||||
            "editions"
 | 
					            "games"
 | 
				
			||||||
        ).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"],
 | 
				
			||||||
@ -472,9 +457,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
 | 
				
			|||||||
            if longest_session
 | 
					            if longest_session
 | 
				
			||||||
            else 0
 | 
					            else 0
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        "longest_session_game": (
 | 
					        "longest_session_game": (longest_session.game if longest_session else None),
 | 
				
			||||||
            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 Edition, Purchase
 | 
					from games.models import Game, 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, edition_id: int = 0) -> HttpResponse:
 | 
					def add_purchase(request: HttpRequest, game_id: int = 0) -> HttpResponse:
 | 
				
			||||||
    context: dict[str, Any] = {}
 | 
					    context: dict[str, Any] = {}
 | 
				
			||||||
    initial = {"date_purchased": timezone.now()}
 | 
					    initial = {"date_purchased": timezone.now()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -149,19 +149,20 @@ def add_purchase(request: HttpRequest, edition_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_purchase", kwargs={"purchase_id": purchase.id}
 | 
					                        "add_session_for_game",
 | 
				
			||||||
 | 
					                        kwargs={"game_id": purchase.first_game.id},
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                return redirect("list_purchases")
 | 
					                return redirect("list_purchases")
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        if edition_id:
 | 
					        if game_id:
 | 
				
			||||||
            edition = Edition.objects.get(id=edition_id)
 | 
					            game = Game.objects.get(id=game_id)
 | 
				
			||||||
            form = PurchaseForm(
 | 
					            form = PurchaseForm(
 | 
				
			||||||
                initial={
 | 
					                initial={
 | 
				
			||||||
                    **initial,
 | 
					                    **initial,
 | 
				
			||||||
                    "edition": edition,
 | 
					                    "games": [game],
 | 
				
			||||||
                    "platform": edition.platform,
 | 
					                    "platform": game.platform,
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
@ -226,12 +227,14 @@ def finish_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
 | 
				
			|||||||
    return redirect("list_purchases")
 | 
					    return redirect("list_purchases")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def related_purchase_by_edition(request: HttpRequest) -> HttpResponse:
 | 
					def related_purchase_by_game(request: HttpRequest) -> HttpResponse:
 | 
				
			||||||
    edition_id = request.GET.get("edition")
 | 
					    games = request.GET.getlist("games")
 | 
				
			||||||
    if not edition_id:
 | 
					    if not games:
 | 
				
			||||||
        return HttpResponseBadRequest("Invalid edition_id")
 | 
					        return HttpResponseBadRequest("Invalid game_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(
 | 
				
			||||||
        edition_id=edition_id, type=Purchase.GAME
 | 
					        games__in=games, type=Purchase.GAME
 | 
				
			||||||
    ).order_by("edition__sort_name")
 | 
					    ).order_by("games__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 Purchase, Session
 | 
					from games.models import Game, 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")
 | 
					    sessions = Session.objects.order_by("-timestamp_start", "created_at")
 | 
				
			||||||
    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(purchase__edition__name__icontains=search_string)
 | 
					            Q(game__name__icontains=search_string)
 | 
				
			||||||
            | Q(purchase__edition__game__name__icontains=search_string)
 | 
					            | Q(game__name__icontains=search_string)
 | 
				
			||||||
            | Q(purchase__platform__name__icontains=search_string)
 | 
					            | Q(game__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.purchase.first_edition.name,
 | 
					                                    popover_content=last_session.game.name,
 | 
				
			||||||
                                    children=[
 | 
					                                    children=[
 | 
				
			||||||
                                        Button(
 | 
					                                        Button(
 | 
				
			||||||
                                            icon=True,
 | 
					                                            icon=True,
 | 
				
			||||||
@ -105,9 +105,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
 | 
				
			|||||||
                                            size="xs",
 | 
					                                            size="xs",
 | 
				
			||||||
                                            children=[
 | 
					                                            children=[
 | 
				
			||||||
                                                Icon("play"),
 | 
					                                                Icon("play"),
 | 
				
			||||||
                                                truncate(
 | 
					                                                truncate(f"{last_session.game.name}"),
 | 
				
			||||||
                                                    f"{last_session.purchase.first_edition.name}"
 | 
					 | 
				
			||||||
                                                ),
 | 
					 | 
				
			||||||
                                            ],
 | 
					                                            ],
 | 
				
			||||||
                                        )
 | 
					                                        )
 | 
				
			||||||
                                    ],
 | 
					                                    ],
 | 
				
			||||||
@ -191,13 +189,13 @@ def search_sessions(request: HttpRequest) -> HttpResponse:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse:
 | 
					def add_session(request: HttpRequest, game_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["purchase"] = last.purchase
 | 
					        initial["game"] = last.game
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if request.method == "POST":
 | 
					    if request.method == "POST":
 | 
				
			||||||
        form = SessionForm(request.POST or None, initial=initial)
 | 
					        form = SessionForm(request.POST or None, initial=initial)
 | 
				
			||||||
@ -205,12 +203,12 @@ def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse:
 | 
				
			|||||||
            form.save()
 | 
					            form.save()
 | 
				
			||||||
            return redirect("list_sessions")
 | 
					            return redirect("list_sessions")
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        if purchase_id:
 | 
					        if game_id:
 | 
				
			||||||
            purchase = Purchase.objects.get(id=purchase_id)
 | 
					            game = Game.objects.get(id=game_id)
 | 
				
			||||||
            form = SessionForm(
 | 
					            form = SessionForm(
 | 
				
			||||||
                initial={
 | 
					                initial={
 | 
				
			||||||
                    **initial,
 | 
					                    **initial,
 | 
				
			||||||
                    "purchase": purchase,
 | 
					                    "game": game,
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        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 Edition, Game, Platform, Purchase, Session
 | 
					from games.models import Game, Platform, Purchase, Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
 | 
					ZONEINFO = ZoneInfo(settings.TIME_ZONE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -21,10 +21,8 @@ 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(
 | 
				
			||||||
            edition=e,
 | 
					            games=[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),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -53,11 +51,6 @@ 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,14 +3,13 @@ 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 Edition, Game, Purchase, Session
 | 
					from games.models import Game, Purchase, Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
 | 
					ZONEINFO = ZoneInfo(settings.TIME_ZONE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,10 +21,8 @@ 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(
 | 
				
			||||||
            edition=e, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO)
 | 
					            game=g, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO)
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        p.save()
 | 
					        p.save()
 | 
				
			||||||
        s = Session(
 | 
					        s = Session(
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user