From 7032b8c7c78f33c5e1065b314a1a07ddc32533c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Tue, 30 Dec 2025 13:24:09 +0100 Subject: [PATCH] Fix signals interfering with deleting a game with sessions --- games/signals.py | 10 +++++++++- tests/test_signals.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/test_signals.py diff --git a/games/signals.py b/games/signals.py index 9728354..fcd4894 100644 --- a/games/signals.py +++ b/games/signals.py @@ -20,7 +20,15 @@ def update_num_purchases(sender, instance, **kwargs): @receiver([post_save, post_delete], sender=Session) def update_game_playtime(sender, instance, **kwargs): - game = instance.game + # During cascade deletes the related Game may already have been removed. + # Use the FK id to look up the Game safely and bail out if it no longer exists. + game_pk = getattr(instance, "game_id", None) + if not game_pk: + return + game = Game.objects.filter(pk=game_pk).first() + if not game: + return + total_playtime = game.sessions.aggregate( total_playtime=Sum(F("duration_calculated") + F("duration_manual")) )["total_playtime"] diff --git a/tests/test_signals.py b/tests/test_signals.py new file mode 100644 index 0000000..2070dd6 --- /dev/null +++ b/tests/test_signals.py @@ -0,0 +1,39 @@ +import os +from datetime import datetime +from zoneinfo import ZoneInfo + +import django +from django.test import TestCase + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings") +django.setup() +from django.conf import settings + +from games.models import Game, Session + +ZONEINFO = ZoneInfo(settings.TIME_ZONE) + + +class SignalsTest(TestCase): + def test_deleting_game_with_sessions_does_not_raise(self): + # Create a game and attach a session to it + g = Game(name="Signal Test Game") + g.save() + + s = Session( + game=g, + timestamp_start=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO), + timestamp_end=datetime(2022, 9, 26, 17, 38, tzinfo=ZONEINFO), + ) + s.save() + + # Sanity checks before delete + self.assertTrue(Game.objects.filter(pk=g.pk).exists()) + self.assertEqual(g.sessions.count(), 1) + + # Deleting the game should not raise (signals run during cascade) + g.delete() + + # After deletion, the Game should be gone and no sessions remain + self.assertFalse(Game.objects.filter(pk=g.pk).exists()) + self.assertEqual(Session.objects.filter(pk=s.pk).count(), 0)