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)