From 6bd82712910da58e599702be8de08f8d58091cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Wed, 29 Jan 2025 22:05:06 +0100 Subject: [PATCH] Remove Edition --- common/components.py | 37 ++--- games/admin.py | 2 - games/forms.py | 39 ++--- games/graphql/queries/__init__.py | 1 - games/graphql/queries/edition.py | 11 -- games/migrations/0048_game_platform.py | 5 +- games/migrations/0050_session_game.py | 35 ++++ games/migrations/0051_purchase_games.py | 29 ++++ .../0052_remove_purchase_editions.py | 17 ++ games/migrations/0053_delete_edition.py | 16 ++ .../0054_remove_session_purchase.py | 17 ++ games/models.py | 70 ++++---- games/static/js/add_purchase.js | 6 +- games/static/main.js | 2 +- games/templates/add_edition.html | 12 -- games/templates/add_game.html | 2 +- games/templates/list_sessions.html | 4 +- games/templates/navbar.html | 8 - games/templates/stats.html | 6 +- games/templates/view_game.html | 4 - games/templates/view_purchase.html | 16 +- games/urls.py | 35 ++-- games/views/edition.py | 150 ------------------ games/views/game.py | 69 ++------ games/views/general.py | 87 ++++------ games/views/purchase.py | 29 ++-- games/views/session.py | 26 ++- tests/test_paths_return_200.py | 11 +- tests/test_session_formatting.py | 7 +- 29 files changed, 286 insertions(+), 467 deletions(-) delete mode 100644 games/graphql/queries/edition.py create mode 100644 games/migrations/0050_session_game.py create mode 100644 games/migrations/0051_purchase_games.py create mode 100644 games/migrations/0052_remove_purchase_editions.py create mode 100644 games/migrations/0053_delete_edition.py create mode 100644 games/migrations/0054_remove_session_purchase.py delete mode 100644 games/templates/add_edition.html delete mode 100644 games/views/edition.py diff --git a/common/components.py b/common/components.py index cbd7d9f..287e2f2 100644 --- a/common/components.py +++ b/common/components.py @@ -9,7 +9,7 @@ from django.urls import NoReverseMatch, reverse from django.utils.safestring import SafeText, mark_safe 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] HTMLTag = str @@ -192,24 +192,24 @@ def LinkedPurchase(purchase: Purchase) -> SafeText: link = reverse("view_purchase", args=[int(purchase.id)]) link_content = "" popover_content = "" - edition_count = purchase.editions.count() + game_count = purchase.games.count() popover_if_not_truncated = False - if edition_count == 1: - link_content += purchase.editions.first().name + if game_count == 1: + link_content += purchase.games.first().name popover_content = link_content - if edition_count > 1: + if game_count > 1: if purchase.name: link_content += f"{purchase.name}" popover_content += f"

{purchase.name}


" else: - link_content += f"{edition_count} games" + link_content += f"{game_count} games" popover_if_not_truncated = True popover_content += f""" """ - icon = purchase.platform.icon if edition_count == 1 else "unspecified" + icon = purchase.platform.icon if game_count == 1 else "unspecified" if link_content == "": raise ValueError("link_content is empty!!") a_content = Div( @@ -235,36 +235,25 @@ def NameWithIcon( game_id: int = 0, session_id: int = 0, purchase_id: int = 0, - edition_id: int = 0, linkify: bool = True, emulated: bool = False, ) -> SafeText: create_link = False link = "" - edition = None platform = None - if ( - game_id != 0 or session_id != 0 or purchase_id != 0 or edition_id != 0 - ) and linkify: + if (game_id != 0 or session_id != 0 or purchase_id != 0) and linkify: create_link = True if session_id: session = Session.objects.get(pk=session_id) emulated = session.emulated - edition = session.purchase.first_edition - game_id = edition.game.pk + game_id = session.game.pk if purchase_id: purchase = Purchase.objects.get(pk=purchase_id) - edition = purchase.first_edition - game_id = purchase.edition.game.pk - if edition_id: - edition = Edition.objects.get(pk=edition_id) - game_id = edition.game.pk + game_id = purchase.games.first().pk if game_id: game = Game.objects.get(pk=game_id) - name = edition.name if edition else game.name - platform = edition.platform if edition else None - if game.platform: - platform = game.platform + name = game.name + platform = game.platform link = reverse("view_game", args=[int(game_id)]) content = Div( [("class", "inline-flex gap-2 items-center")], diff --git a/games/admin.py b/games/admin.py index 22faccd..dbfdec1 100644 --- a/games/admin.py +++ b/games/admin.py @@ -2,7 +2,6 @@ from django.contrib import admin from games.models import ( Device, - Edition, ExchangeRate, Game, Platform, @@ -15,6 +14,5 @@ admin.site.register(Game) admin.site.register(Purchase) admin.site.register(Platform) admin.site.register(Session) -admin.site.register(Edition) admin.site.register(Device) admin.site.register(ExchangeRate) diff --git a/games/forms.py b/games/forms.py index 78b9ace..4a1fe4b 100644 --- a/games/forms.py +++ b/games/forms.py @@ -2,7 +2,7 @@ from django import forms from django.urls import reverse 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_datetime_widget = forms.DateTimeInput( @@ -12,11 +12,8 @@ autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"}) class SessionForm(forms.ModelForm): - # purchase = forms.ModelChoiceField( - # queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name") - # ) - purchase = forms.ModelChoiceField( - queryset=Purchase.objects.all(), + game = forms.ModelChoiceField( + queryset=Game.objects.order_by("sort_name"), widget=forms.Select(attrs={"autofocus": "autofocus"}), ) @@ -29,7 +26,7 @@ class SessionForm(forms.ModelForm): } model = Session fields = [ - "purchase", + "game", "timestamp_start", "timestamp_end", "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: return f"{obj.sort_name} ({obj.platform}, {obj.year_released})" @@ -57,19 +54,19 @@ class PurchaseForm(forms.ModelForm): super().__init__(*args, **kwargs) # Automatically update related_purchase - - - - diff --git a/games/templates/add_game.html b/games/templates/add_game.html index cc0f0f1..dcbf459 100644 --- a/games/templates/add_game.html +++ b/games/templates/add_game.html @@ -5,7 +5,7 @@ + value="Submit & Create Purchase" /> diff --git a/games/templates/list_sessions.html b/games/templates/list_sessions.html index 6aae78d..646b1a5 100644 --- a/games/templates/list_sessions.html +++ b/games/templates/list_sessions.html @@ -36,8 +36,8 @@ - {{ session.purchase.edition.name }} + href="{% url 'view_game' session.game.id %}"> + {{ session.game.name }} diff --git a/games/templates/navbar.html b/games/templates/navbar.html index ff7de14..a869588 100644 --- a/games/templates/navbar.html +++ b/games/templates/navbar.html @@ -57,10 +57,6 @@ Game -
  • - Edition -
  • Platform @@ -102,10 +98,6 @@ Games
  • -
  • - Editions -
  • Platforms diff --git a/games/templates/stats.html b/games/templates/stats.html index 2af837e..38d3146 100644 --- a/games/templates/stats.html +++ b/games/templates/stats.html @@ -2,11 +2,11 @@ {% load static %} {% partialdef purchase-name %} {% if purchase.type != 'game' %} - - {{ purchase.name }} ({{ purchase.first_edition.edition.name }} {{ purchase.get_type_display }}) + + {{ purchase.name }} ({{ purchase.first_game.name }} {{ purchase.get_type_display }}) {% else %} - + {% endif %} {% endpartialdef %}
    diff --git a/games/templates/view_game.html b/games/templates/view_game.html index ee917e3..7c8ca45 100644 --- a/games/templates/view_game.html +++ b/games/templates/view_game.html @@ -67,10 +67,6 @@
    - Editions -
    - -
    Purchases diff --git a/games/templates/view_purchase.html b/games/templates/view_purchase.html index aa3290f..a9a6aa6 100644 --- a/games/templates/view_purchase.html +++ b/games/templates/view_purchase.html @@ -3,7 +3,7 @@
    - {% if purchase.name %}{{ purchase.name }}{% else %}Unnamed purchase{% endif %} ({{ purchase.editions.count }} games) + {% if purchase.name %}{{ purchase.name }}{% else %}Unnamed purchase{% endif %} ({{ purchase.games.count }} games) -
    Price: {{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }} ({{ purchase.price | floatformat }} {{ purchase.price_currency }})
    +
    + Price: + {% if purchase.converted_price %} + {{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }} + {% else %} + None + {% endif %} + ({{ purchase.price | floatformat }} {{ purchase.price_currency }}) +

    Items:

      - {% for edition in purchase.editions.all %} -
    • + {% for game in purchase.games.all %} +
    • {% endfor %}
    diff --git a/games/urls.py b/games/urls.py index ccb398f..a791dd8 100644 --- a/games/urls.py +++ b/games/urls.py @@ -1,6 +1,6 @@ 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 = [ path("", general.index, name="index"), @@ -8,19 +8,6 @@ urlpatterns = [ path("device/delete/", device.delete_device, name="delete_device"), path("device/edit/", device.edit_device, name="edit_device"), path("device/list", device.list_devices, name="list_devices"), - path("edition/add", edition.add_edition, name="add_edition"), - path( - "edition/add/for-game/", - edition.add_edition, - name="add_edition_for_game", - ), - path("edition//edit", edition.edit_edition, name="edit_edition"), - path("edition/list", edition.list_editions, name="list_editions"), - path( - "edition//delete", - edition.delete_edition, - name="delete_edition", - ), path("game/add", game.add_game, name="add_game"), path("game//edit", game.edit_game, name="edit_game"), path("game//view", game.view_game, name="view_game"), @@ -39,6 +26,11 @@ urlpatterns = [ ), path("platform/list", platform.list_platforms, name="list_platforms"), path("purchase/add", purchase.add_purchase, name="add_purchase"), + path( + "purchase/add/for-game/", + purchase.add_purchase, + name="add_purchase_for_game", + ), path( "purchase//edit", purchase.edit_purchase, @@ -75,20 +67,15 @@ urlpatterns = [ name="refund_purchase", ), path( - "purchase/related-purchase-by-edition", - purchase.related_purchase_by_edition, - name="related_purchase_by_edition", - ), - path( - "purchase/add/for-edition/", - purchase.add_purchase, - name="add_purchase_for_edition", + "purchase/related-purchase-by-game", + purchase.related_purchase_by_game, + name="related_purchase_by_game", ), path("session/add", session.add_session, name="add_session"), path( - "session/add/for-purchase/", + "session/add/for-game/", session.add_session, - name="add_session_for_purchase", + name="add_session_for_game", ), path( "session/add/from-game/", diff --git a/games/views/edition.py b/games/views/edition.py deleted file mode 100644 index ffb0039..0000000 --- a/games/views/edition.py +++ /dev/null @@ -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) diff --git a/games/views/game.py b/games/views/game.py index 2f99716..7d46938 100644 --- a/games/views/game.py +++ b/games/views/game.py @@ -29,7 +29,7 @@ from common.time import ( ) from common.utils import safe_division, truncate 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 @@ -109,7 +109,7 @@ def add_game(request: HttpRequest) -> HttpResponse: game = form.save() if "submit_and_redirect" in request.POST: return HttpResponseRedirect( - reverse("add_edition_for_game", kwargs={"game_id": game.id}) + reverse("add_purchase_for_game", kwargs={"game_id": game.id}) ) else: return redirect("list_games") @@ -158,21 +158,12 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: ), 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( - purchase__editions__game=game - ) + sessions = game.sessions session_count = sessions.count() - session_count_without_manual = ( - Session.objects.without_manual().filter(purchase__editions__game=game).count() - ) + session_count_without_manual = game.sessions.without_manual().count() if sessions: 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") ) - 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] = { "columns": ["Name", "Type", "Date", "Price", "Actions"], "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( - "-timestamp_start" - ) + sessions_all = game.sessions.order_by("-timestamp_start") + last_session = None if sessions_all.exists(): last_session = sessions_all.latest() @@ -284,7 +242,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: args=[last_session.pk], ), children=Popover( - popover_content=last_session.purchase.first_edition.name, + popover_content=last_session.game.name, children=[ Button( icon=True, @@ -292,9 +250,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: size="xs", children=[ Icon("play"), - truncate( - f"{last_session.purchase.first_edition.name}" - ), + truncate(f"{last_session.game.name}"), ], ) ], @@ -304,7 +260,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: else "", ], ), - "columns": ["Edition", "Date", "Duration", "Actions"], + "columns": ["Game", "Date", "Duration", "Actions"], "rows": [ [ NameWithIcon( @@ -354,11 +310,9 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: } context: dict[str, Any] = { - "edition_count": editions.count(), - "editions": editions, "game": game, "playrange": playrange, - "purchase_count": Purchase.objects.filter(editions__game=game).count(), + "purchase_count": game.purchases.count(), "session_average_without_manual": round( safe_division( total_hours_without_manual, int(session_count_without_manual) @@ -369,7 +323,6 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: "sessions": sessions, "title": f"Game Overview - {game.name}", "hours_sum": total_hours, - "edition_data": edition_data, "purchase_data": purchase_data, "session_data": session_data, "session_page_obj": session_page_obj, diff --git a/games/views/general.py b/games/views/general.py index 4834046..5607f98 100644 --- a/games/views/general.py +++ b/games/views/general.py @@ -11,13 +11,12 @@ from django.urls import reverse from common.time import available_stats_year_range, dateformat, format_duration 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]: return { "game_available": Game.objects.exists(), - "edition_available": Edition.objects.exists(), "platform_available": Platform.objects.exists(), "purchase_available": Purchase.objects.exists(), "session_count": Session.objects.exists(), @@ -49,9 +48,7 @@ def use_custom_redirect( @login_required def stats_alltime(request: HttpRequest) -> HttpResponse: year = "Alltime" - this_year_sessions = Session.objects.all().prefetch_related( - Prefetch("purchase__editions") - ) + this_year_sessions = Session.objects.all().prefetch_related(Prefetch("game")) this_year_sessions_with_durations = this_year_sessions.annotate( duration=ExpressionWrapper( 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() - this_year_games = Game.objects.filter( - editions__purchase__session__in=this_year_sessions - ).distinct() + this_year_games = Game.objects.filter(sessions__in=this_year_sessions).distinct() 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( "-session_count" @@ -76,11 +71,11 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: .aggregate(dates=Count("date")) ) this_year_played_purchases = Purchase.objects.filter( - session__in=this_year_sessions + games__sessions__in=this_year_sessions ).distinct() 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( date_refunded=None ) @@ -129,11 +124,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: total_spent = this_year_spendings["total_spent"] or 0 games_with_playtime = ( - Game.objects.filter(editions__purchase__session__in=this_year_sessions) + Game.objects.filter(sessions__in=this_year_sessions) .annotate( total_playtime=Sum( - F("editions__purchase__session__duration_calculated") - + F("editions__purchase__session__duration_manual") + F("sessions__duration_calculated") + F("sessions__duration_manual") ) ) .values("id", "name", "total_playtime") @@ -148,10 +142,8 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: month["playtime"] = format_duration(month["playtime"], "%2.0H") highest_session_average_game = ( - Game.objects.filter(editions__purchase__session__in=this_year_sessions) - .annotate( - session_average=Avg("editions__purchase__session__duration_calculated") - ) + Game.objects.filter(sessions__in=this_year_sessions) + .annotate(session_average=Avg("sessions__duration_calculated")) .order_by("-session_average") .first() ) @@ -160,9 +152,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H") 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(platform_name=F("purchase__platform__name")) + .annotate(platform_name=F("game__platform__name")) .values("platform_name", "total_playtime") .order_by("-total_playtime") ) @@ -177,10 +169,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: last_play_date = "N/A" if this_year_sessions: first_session = this_year_sessions.earliest() - first_play_game = first_session.purchase.first_edition.game + first_play_game = first_session.game first_play_date = first_session.timestamp_start.strftime(dateformat) 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) all_purchased_this_year_count = this_year_purchases_with_currency.count() @@ -228,9 +220,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse: if longest_session else 0 ), - "longest_session_game": ( - longest_session.purchase.first_edition.game if longest_session else None - ), + "longest_session_game": (longest_session.game if longest_session else None), "highest_session_count": ( game_highest_session_count.session_count if game_highest_session_count @@ -268,7 +258,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: return HttpResponseRedirect(reverse("stats_alltime")) this_year_sessions = Session.objects.filter( timestamp_start__year=year - ).prefetch_related("purchase__editions") + ).prefetch_related("game") this_year_sessions_with_durations = this_year_sessions.annotate( duration=ExpressionWrapper( 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() - this_year_games = Game.objects.filter( - edition__purchases__session__in=this_year_sessions - ).distinct() + this_year_games = Game.objects.filter(sessions__in=this_year_sessions).distinct() this_year_games_with_session_counts = this_year_games.annotate( session_count=Count( - "edition__purchases__session", - filter=Q(edition__purchases__session__timestamp_start__year=year), + "sessions", + filter=Q(sessions__timestamp_start__year=year), ) ) 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")) ) this_year_played_purchases = Purchase.objects.filter( - session__in=this_year_sessions + games__sessions__in=this_year_sessions ).distinct() 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( date_refunded=None ).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_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" ) ) @@ -351,11 +339,10 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: total_spent = this_year_spendings["total_spent"] or 0 games_with_playtime = ( - Game.objects.filter(edition__purchases__session__in=this_year_sessions) + Game.objects.filter(sessions__in=this_year_sessions) .annotate( total_playtime=Sum( - F("edition__purchases__session__duration_calculated") - + F("edition__purchases__session__duration_manual") + F("sessions__duration_calculated") + F("sessions__duration_manual") ) ) .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") highest_session_average_game = ( - Game.objects.filter(edition__purchases__session__in=this_year_sessions) - .annotate( - session_average=Avg("edition__purchases__session__duration_calculated") - ) + Game.objects.filter(sessions__in=this_year_sessions) + .annotate(session_average=Avg("sessions__duration_calculated")) .order_by("-session_average") .first() ) @@ -382,9 +367,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H") 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(platform_name=F("purchase__platform__name")) + .annotate(platform_name=F("game__platform__name")) .values("platform_name", "total_playtime") .order_by("-total_playtime") ) @@ -403,10 +388,10 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: last_play_game = None if this_year_sessions: first_session = this_year_sessions.earliest() - first_play_game = first_session.purchase.first_edition.game + first_play_game = first_session.game first_play_date = first_session.timestamp_start.strftime(dateformat) 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) 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_2023_games": this_year_played_purchases.filter( - editions__year_released=year + games__year_released=year ).count(), "top_10_games_by_playtime": top_10_games_by_playtime, "year": year, @@ -435,15 +420,15 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: safe_division(total_spent, this_year_purchases_without_refunded_count) ), "all_finished_this_year": purchases_finished_this_year.prefetch_related( - "editions" + "games" ).order_by("date_finished"), "all_finished_this_year_count": purchases_finished_this_year.count(), "this_year_finished_this_year": purchases_finished_this_year_released_this_year.prefetch_related( - "editions" + "games" ).order_by("date_finished"), "this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(), "purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.prefetch_related( - "editions" + "games" ).order_by("date_finished"), "total_sessions": this_year_sessions.count(), "unique_days": unique_days["dates"], @@ -472,9 +457,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse: if longest_session else 0 ), - "longest_session_game": ( - longest_session.purchase.first_edition.game if longest_session else None - ), + "longest_session_game": (longest_session.game if longest_session else None), "highest_session_count": ( game_highest_session_count.session_count if game_highest_session_count diff --git a/games/views/purchase.py b/games/views/purchase.py index daa6424..c7962b7 100644 --- a/games/views/purchase.py +++ b/games/views/purchase.py @@ -16,7 +16,7 @@ from django.utils import timezone from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice from common.time import dateformat from games.forms import PurchaseForm -from games.models import Edition, Purchase +from games.models import Game, Purchase from games.views.general import use_custom_redirect @@ -138,7 +138,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse: @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] = {} 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: return HttpResponseRedirect( reverse( - "add_session_for_purchase", kwargs={"purchase_id": purchase.id} + "add_session_for_game", + kwargs={"game_id": purchase.first_game.id}, ) ) else: return redirect("list_purchases") else: - if edition_id: - edition = Edition.objects.get(id=edition_id) + if game_id: + game = Game.objects.get(id=game_id) form = PurchaseForm( initial={ **initial, - "edition": edition, - "platform": edition.platform, + "games": [game], + "platform": game.platform, } ) else: @@ -226,12 +227,14 @@ def finish_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse: return redirect("list_purchases") -def related_purchase_by_edition(request: HttpRequest) -> HttpResponse: - edition_id = request.GET.get("edition") - if not edition_id: - return HttpResponseBadRequest("Invalid edition_id") +def related_purchase_by_game(request: HttpRequest) -> HttpResponse: + games = request.GET.getlist("games") + if not games: + return HttpResponseBadRequest("Invalid game_id") + if isinstance(games, int) or isinstance(games, str): + games = [games] form = PurchaseForm() form.fields["related_purchase"].queryset = Purchase.objects.filter( - edition_id=edition_id, type=Purchase.GAME - ).order_by("edition__sort_name") + games__in=games, type=Purchase.GAME + ).order_by("games__sort_name") return render(request, "partials/related_purchase_field.html", {"form": form}) diff --git a/games/views/session.py b/games/views/session.py index b84e032..6f2f107 100644 --- a/games/views/session.py +++ b/games/views/session.py @@ -28,7 +28,7 @@ from common.time import ( ) from common.utils import truncate 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 @@ -37,13 +37,13 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse context: dict[Any, Any] = {} page_number = request.GET.get("page", 1) 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) if search_string != "": sessions = sessions.filter( - Q(purchase__edition__name__icontains=search_string) - | Q(purchase__edition__game__name__icontains=search_string) - | Q(purchase__platform__name__icontains=search_string) + Q(game__name__icontains=search_string) + | Q(game__name__icontains=search_string) + | Q(game__platform__name__icontains=search_string) | Q(device__name__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], ), children=Popover( - popover_content=last_session.purchase.first_edition.name, + popover_content=last_session.game.name, children=[ Button( icon=True, @@ -105,9 +105,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse size="xs", children=[ Icon("play"), - truncate( - f"{last_session.purchase.first_edition.name}" - ), + truncate(f"{last_session.game.name}"), ], ) ], @@ -191,13 +189,13 @@ def search_sessions(request: HttpRequest) -> HttpResponse: @login_required -def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse: +def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse: context = {} initial: dict[str, Any] = {"timestamp_start": timezone.now()} last = Session.objects.last() if last != None: - initial["purchase"] = last.purchase + initial["game"] = last.game if request.method == "POST": form = SessionForm(request.POST or None, initial=initial) @@ -205,12 +203,12 @@ def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse: form.save() return redirect("list_sessions") else: - if purchase_id: - purchase = Purchase.objects.get(id=purchase_id) + if game_id: + game = Game.objects.get(id=game_id) form = SessionForm( initial={ **initial, - "purchase": purchase, + "game": game, } ) else: diff --git a/tests/test_paths_return_200.py b/tests/test_paths_return_200.py index 08bfd53..a4927a0 100644 --- a/tests/test_paths_return_200.py +++ b/tests/test_paths_return_200.py @@ -10,7 +10,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings") django.setup() 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) @@ -21,10 +21,8 @@ class PathWorksTest(TestCase): pl.save() g = Game(name="The Test Game") g.save() - e = Edition(game=g, name="The Test Game Edition", platform=pl) - e.save() p = Purchase( - edition=e, + games=[e], platform=pl, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO), ) @@ -53,11 +51,6 @@ class PathWorksTest(TestCase): response = self.client.get(url) 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): url = reverse("add_purchase") response = self.client.get(url) diff --git a/tests/test_session_formatting.py b/tests/test_session_formatting.py index 4bc345e..df4de41 100644 --- a/tests/test_session_formatting.py +++ b/tests/test_session_formatting.py @@ -3,14 +3,13 @@ from datetime import datetime from zoneinfo import ZoneInfo import django -from django.db import models from django.test import TestCase os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings") django.setup() 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) @@ -22,10 +21,8 @@ class FormatDurationTest(TestCase): def test_duration_format(self): g = Game(name="The Test Game") g.save() - e = Edition(game=g, name="The Test Game Edition") - e.save() 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() s = Session(