From 3d43afa9a0ad008490ae794786c9f63165e7a432 Mon Sep 17 00:00:00 2001 From: infektyd Date: Mon, 2 Mar 2026 08:05:05 -0500 Subject: [PATCH 1/4] Fix IntegrityError during user deletion (#2232) When a user deletes their account, the cascade deletes their WorkoutLogs and WorkoutSessions. These deletions trigger post_delete signals that attempt to recalculate the user's statistics via `UserStatisticsService`. This results in a `get_or_create` query attempting to insert a new UserStatistics record for a user that is simultaneously being deleted, yielding an IntegrityError. This commit resolves the transaction failure by explicitly checking if the user still exists before creating their statistics record, and safely catching `User.DoesNotExist` within the trophy signal handlers. --- wger/trophies/services/statistics.py | 2 ++ wger/trophies/signals.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/wger/trophies/services/statistics.py b/wger/trophies/services/statistics.py index 2128953c7..b9238987b 100644 --- a/wger/trophies/services/statistics.py +++ b/wger/trophies/services/statistics.py @@ -57,6 +57,8 @@ def get_or_create_statistics(cls, user: User) -> UserStatistics: Returns: The UserStatistics instance """ + if not User.objects.filter(pk=user.pk).exists(): + raise User.DoesNotExist('User not found') stats, created = UserStatistics.objects.get_or_create(user=user) return stats diff --git a/wger/trophies/signals.py b/wger/trophies/signals.py index 55aafe330..ec0ea52e4 100644 --- a/wger/trophies/signals.py +++ b/wger/trophies/signals.py @@ -26,6 +26,7 @@ # Django from django.conf import settings +from django.contrib.auth.models import User from django.db.models.signals import ( post_delete, post_save, @@ -112,6 +113,8 @@ def workout_log_saved(sender, instance: WorkoutLog, created: bool, **kwargs): # Trigger trophy evaluation _trigger_trophy_evaluation(instance.user_id) + except User.DoesNotExist: + pass except Exception as e: logger.error(f'Error updating statistics for user {instance.user_id}: {e}', exc_info=True) @@ -128,6 +131,8 @@ def workout_log_deleted(sender, instance: WorkoutLog, **kwargs): try: UserStatisticsService.handle_workout_deletion(instance.user) + except User.DoesNotExist: + pass except Exception as e: logger.error( f'Error updating statistics after deletion for user {instance.user_id}: {e}', @@ -163,6 +168,8 @@ def workout_session_saved(sender, instance: WorkoutSession, created: bool, **kwa # Trigger trophy evaluation _trigger_trophy_evaluation(instance.user_id) + except User.DoesNotExist: + pass except Exception as e: logger.error(f'Error updating statistics for session {instance.id}: {e}', exc_info=True) @@ -179,6 +186,8 @@ def workout_session_deleted(sender, instance: WorkoutSession, **kwargs): try: UserStatisticsService.handle_workout_deletion(instance.user) + except User.DoesNotExist: + pass except Exception as e: logger.error( f'Error updating statistics after session deletion for user {instance.user_id}: {e}', From 134f33966ab065e6d4cdd747addab54011d9a345 Mon Sep 17 00:00:00 2001 From: infektyd Date: Mon, 2 Mar 2026 09:21:34 -0500 Subject: [PATCH 2/4] test: add unit test for UserStatisticsService.get_or_create_statistics handling deleted user Addresses review comment on PR #2233 --- wger/trophies/tests/test_services.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wger/trophies/tests/test_services.py b/wger/trophies/tests/test_services.py index 038ef06a5..e6679d347 100644 --- a/wger/trophies/tests/test_services.py +++ b/wger/trophies/tests/test_services.py @@ -106,6 +106,14 @@ def test_handle_workout_deletion(self): self.assertEqual(stats.total_weight_lifted, Decimal('0')) + def test_get_or_create_statistics_deleted_user(self): + """ + Test get_or_create raises DoesNotExist for deleted user + """ + with patch.object(User.objects, 'filter') as mock_filter: + mock_filter.return_value.exists.return_value = False + with self.assertRaises(User.DoesNotExist): + UserStatisticsService.get_or_create_statistics(self.user) class TrophyServiceTestCase(WgerTestCase): """ Test the TrophyService From ccb22e993bce1c59460b385c031c1007127fc127 Mon Sep 17 00:00:00 2001 From: infektyd Date: Mon, 2 Mar 2026 09:39:16 -0500 Subject: [PATCH 3/4] Add integration test for user deletion with trophy data per maintainer feedback Implement Roland'\''s suggestion: create workout session records (logs), invoke trophy services, verify model delete succeeds without IntegrityError. Added logger statements. No mock in test_services.py needed to remove. --- wger/core/tests/test_delete_user.py | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/wger/core/tests/test_delete_user.py b/wger/core/tests/test_delete_user.py index 17c232743..6ce12fa5c 100644 --- a/wger/core/tests/test_delete_user.py +++ b/wger/core/tests/test_delete_user.py @@ -21,6 +21,11 @@ # wger from wger.core.tests.base_testcase import WgerTestCase +from django.utils import timezone +from wger.trophies.services.trophy import TrophyService +from wger.trophies.services.statistics import UserStatisticsService +from wger.trophies.models import Trophy, UserStatistics, UserTrophy +from wger.manager.models import WorkoutSession logger = logging.getLogger(__name__) @@ -196,3 +201,40 @@ def test_delete_user_anonymous(self): Tests deleting the user account as an anonymous user """ self.delete_user(fail=True) +class UserDeleteTrophyIntegrationTestCase(WgerTestCase): + """ + Tests user deletion with trophy records and service invocation + """ + def test_delete_user_with_trophy_records(self): + """ + Adds sessions/records, calls trophy system, ensures delete works without IntegrityError + """ + logger.info("Testing user deletion after trophy system invocation") + user = User.objects.create_user( + username="trophyuser", + email="trophy@test.com", + password="testpass" + ) + session = WorkoutSession.objects.create( + user=user, + date=timezone.now().date() + ) + logger.info("Created WorkoutSession") + trophy = Trophy.objects.create( + name="DeleteTestTrophy", + trophy_type=0, + checker_class="workout_count_based", + checker_params={"count": 1}, + is_active=True + ) + stats = UserStatistics.objects.create( + user=user, + total_workouts=1 + ) + logger.info("Created Trophy and UserStatistics") + UserStatisticsService.update_statistics(user) + TrophyService.evaluate_all_trophies(user) + logger.info("Trophy services invoked") + user.delete() + logger.info("User deleted") + self.assertEqual(User.objects.filter(username="trophyuser").count(), 0) From d124deded148a156f77650d2d8a220da39aaf00e Mon Sep 17 00:00:00 2001 From: infektyd Date: Mon, 2 Mar 2026 09:58:00 -0500 Subject: [PATCH 4/4] tests: FIX UNIQUE constraint failed by using update_or_create for UserStatistics --- wger/core/tests/test_delete_user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wger/core/tests/test_delete_user.py b/wger/core/tests/test_delete_user.py index 6ce12fa5c..86fa78e79 100644 --- a/wger/core/tests/test_delete_user.py +++ b/wger/core/tests/test_delete_user.py @@ -227,11 +227,11 @@ def test_delete_user_with_trophy_records(self): checker_params={"count": 1}, is_active=True ) - stats = UserStatistics.objects.create( + UserStatistics.objects.update_or_create( user=user, - total_workouts=1 + defaults={"total_workouts": 1} ) - logger.info("Created Trophy and UserStatistics") + logger.info("Created Trophy and updated UserStatistics") UserStatisticsService.update_statistics(user) TrophyService.evaluate_all_trophies(user) logger.info("Trophy services invoked")