diff --git a/wger/core/models/profile.py b/wger/core/models/profile.py index c73524d42..39451653a 100644 --- a/wger/core/models/profile.py +++ b/wger/core/models/profile.py @@ -39,6 +39,7 @@ from wger.utils.units import ( AbstractWeight, ) from wger.weight.models import WeightEntry + # Local from .language import Language diff --git a/wger/trophies/api/views.py b/wger/trophies/api/views.py index 566c7d256..d46e7834e 100644 --- a/wger/trophies/api/views.py +++ b/wger/trophies/api/views.py @@ -88,7 +88,7 @@ class TrophyViewSet(viewsets.ReadOnlyModelViewSet): return queryset.filter(is_hidden=False) @extend_schema( - summary="Get trophy progress", + summary='Get trophy progress', description=""" Return all trophies with progress information for the current user. diff --git a/wger/trophies/apps.py b/wger/trophies/apps.py index f76363369..ab3ef9d91 100644 --- a/wger/trophies/apps.py +++ b/wger/trophies/apps.py @@ -23,4 +23,4 @@ class TrophiesConfig(AppConfig): verbose_name = 'Trophies' def ready(self): - import wger.trophies.signals # noqa: F401 + import wger.trophies.signals # noqa: F401 diff --git a/wger/trophies/checkers/base.py b/wger/trophies/checkers/base.py index f952c8044..613b9850f 100644 --- a/wger/trophies/checkers/base.py +++ b/wger/trophies/checkers/base.py @@ -56,7 +56,9 @@ class BaseTrophyChecker(ABC): Lazy-load the user's statistics. """ if self._statistics is None: + # wger from wger.trophies.models import UserStatistics + self._statistics, _ = UserStatistics.objects.get_or_create(user=self.user) return self._statistics diff --git a/wger/trophies/checkers/date_based.py b/wger/trophies/checkers/date_based.py index 84e68d77d..6fa43d922 100644 --- a/wger/trophies/checkers/date_based.py +++ b/wger/trophies/checkers/date_based.py @@ -68,7 +68,9 @@ class DateBasedChecker(BaseTrophyChecker): # For other dates, we need to query the workout sessions # This is done in the statistics service when updating + # wger from wger.manager.models import WorkoutSession + return WorkoutSession.objects.filter( user=self.user, date__month=month, @@ -92,7 +94,9 @@ class DateBasedChecker(BaseTrophyChecker): return 'N/A' # Convert to month name + # Standard Library import calendar + month_name = calendar.month_name[month] return f'{month_name} {day}' diff --git a/wger/trophies/checkers/streak.py b/wger/trophies/checkers/streak.py index d6de7a5fa..a5a3bbc23 100644 --- a/wger/trophies/checkers/streak.py +++ b/wger/trophies/checkers/streak.py @@ -45,10 +45,7 @@ class StreakChecker(BaseTrophyChecker): return False # Check both current streak and longest streak (in case they achieved it before) target = self.get_target_value() - return ( - self.statistics.current_streak >= target - or self.statistics.longest_streak >= target - ) + return self.statistics.current_streak >= target or self.statistics.longest_streak >= target def get_progress(self) -> float: """Get progress as percentage of streak achieved.""" diff --git a/wger/trophies/management/commands/evaluate_trophies.py b/wger/trophies/management/commands/evaluate_trophies.py index d1a3cdb20..fc5b1b5ad 100644 --- a/wger/trophies/management/commands/evaluate_trophies.py +++ b/wger/trophies/management/commands/evaluate_trophies.py @@ -79,9 +79,7 @@ class Command(BaseCommand): # Validate that at least one option is provided if not username and not trophy_id and not evaluate_all: - raise CommandError( - 'Please specify --user, --trophy, or --all. See help for details.' - ) + raise CommandError('Please specify --user, --trophy, or --all. See help for details.') # Case 1: Evaluate for a specific user if username: @@ -105,9 +103,7 @@ class Command(BaseCommand): if verbosity >= 1: self.stdout.write( - self.style.SUCCESS( - f'\nEvaluation complete: {len(awarded)} trophy(ies) awarded' - ) + self.style.SUCCESS(f'\nEvaluation complete: {len(awarded)} trophy(ies) awarded') ) # Case 2: Evaluate a specific trophy for all users (or force re-evaluation) @@ -120,9 +116,7 @@ class Command(BaseCommand): trophy_ids = [trophy_id] if verbosity >= 1: - self.stdout.write( - f'Evaluating trophy "{trophy.name}" for all users' - ) + self.stdout.write(f'Evaluating trophy "{trophy.name}" for all users') except Trophy.DoesNotExist: raise CommandError(f'Trophy with ID {trophy_id} does not exist') else: diff --git a/wger/trophies/management/commands/load_trophies.py b/wger/trophies/management/commands/load_trophies.py index c1cb0c74d..fee11214b 100644 --- a/wger/trophies/management/commands/load_trophies.py +++ b/wger/trophies/management/commands/load_trophies.py @@ -163,25 +163,19 @@ class Command(BaseCommand): updated_count += 1 if verbosity >= 2: - self.stdout.write( - self.style.SUCCESS(f'✓ Updated trophy: {name}') - ) + self.stdout.write(self.style.SUCCESS(f'✓ Updated trophy: {name}')) else: skipped_count += 1 if verbosity >= 2: - self.stdout.write( - self.style.WARNING(f'- Skipped existing trophy: {name}') - ) + self.stdout.write(self.style.WARNING(f'- Skipped existing trophy: {name}')) else: # Create new trophy Trophy.objects.create(**trophy_data) created_count += 1 if verbosity >= 2: - self.stdout.write( - self.style.SUCCESS(f'+ Created trophy: {name}') - ) + self.stdout.write(self.style.SUCCESS(f'+ Created trophy: {name}')) # Summary if verbosity >= 1: diff --git a/wger/trophies/management/commands/recalculate_statistics.py b/wger/trophies/management/commands/recalculate_statistics.py index 50624f31a..4da4af6cc 100644 --- a/wger/trophies/management/commands/recalculate_statistics.py +++ b/wger/trophies/management/commands/recalculate_statistics.py @@ -75,9 +75,7 @@ class Command(BaseCommand): # Validate that at least one option is provided if not username and not recalculate_all: - raise CommandError( - 'Please specify --user or --all. See help for details.' - ) + raise CommandError('Please specify --user or --all. See help for details.') # Case 1: Recalculate for a specific user if username: @@ -104,9 +102,7 @@ class Command(BaseCommand): ) if verbosity >= 1: - self.stdout.write( - self.style.SUCCESS(f'\nRecalculation complete for {username}') - ) + self.stdout.write(self.style.SUCCESS(f'\nRecalculation complete for {username}')) # Case 2: Recalculate for all users elif recalculate_all: @@ -138,9 +134,7 @@ class Command(BaseCommand): processed += 1 if verbosity >= 2: - self.stdout.write( - self.style.SUCCESS(f'✓ Processed: {user.username}') - ) + self.stdout.write(self.style.SUCCESS(f'✓ Processed: {user.username}')) elif verbosity >= 1 and processed % 100 == 0: self.stdout.write(f' Processed {processed}/{total_users} users...') @@ -148,9 +142,7 @@ class Command(BaseCommand): errors += 1 if verbosity >= 1: self.stdout.write( - self.style.ERROR( - f'✗ Error processing {user.username}: {str(e)}' - ) + self.style.ERROR(f'✗ Error processing {user.username}: {str(e)}') ) if verbosity >= 1: diff --git a/wger/trophies/migrations/0001_initial.py b/wger/trophies/migrations/0001_initial.py index 7551c3fa3..8388d0ee9 100644 --- a/wger/trophies/migrations/0001_initial.py +++ b/wger/trophies/migrations/0001_initial.py @@ -9,7 +9,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ @@ -20,18 +19,101 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Trophy', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID' + ), + ), ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ('name', models.CharField(help_text='The name of the trophy', max_length=100, verbose_name='Name')), - ('description', models.TextField(blank=True, default='', help_text='A description of how to earn this trophy', verbose_name='Description')), - ('image', models.ImageField(blank=True, null=True, upload_to=wger.trophies.models.trophy.trophy_image_upload_path, verbose_name='Image')), - ('trophy_type', models.CharField(choices=[('time', 'Time-based'), ('volume', 'Volume-based'), ('count', 'Count-based'), ('sequence', 'Sequence-based'), ('date', 'Date-based'), ('other', 'Other')], default='other', help_text='The type of criteria used to evaluate this trophy', max_length=20, verbose_name='Trophy type')), - ('checker_class', models.CharField(help_text='The Python class path used to check if this trophy is earned', max_length=255, verbose_name='Checker class')), - ('checker_params', models.JSONField(blank=True, default=dict, help_text='JSON parameters passed to the checker class', verbose_name='Checker parameters')), - ('is_hidden', models.BooleanField(default=False, help_text='If true, this trophy is hidden until earned', verbose_name='Hidden')), - ('is_progressive', models.BooleanField(default=False, help_text='If true, this trophy shows progress towards completion', verbose_name='Progressive')), - ('is_active', models.BooleanField(default=True, help_text='If false, this trophy cannot be earned', verbose_name='Active')), - ('order', models.PositiveIntegerField(default=0, help_text='Display order of the trophy', verbose_name='Order')), + ( + 'name', + models.CharField( + help_text='The name of the trophy', max_length=100, verbose_name='Name' + ), + ), + ( + 'description', + models.TextField( + blank=True, + default='', + help_text='A description of how to earn this trophy', + verbose_name='Description', + ), + ), + ( + 'image', + models.ImageField( + blank=True, + null=True, + upload_to=wger.trophies.models.trophy.trophy_image_upload_path, + verbose_name='Image', + ), + ), + ( + 'trophy_type', + models.CharField( + choices=[ + ('time', 'Time-based'), + ('volume', 'Volume-based'), + ('count', 'Count-based'), + ('sequence', 'Sequence-based'), + ('date', 'Date-based'), + ('other', 'Other'), + ], + default='other', + help_text='The type of criteria used to evaluate this trophy', + max_length=20, + verbose_name='Trophy type', + ), + ), + ( + 'checker_class', + models.CharField( + help_text='The Python class path used to check if this trophy is earned', + max_length=255, + verbose_name='Checker class', + ), + ), + ( + 'checker_params', + models.JSONField( + blank=True, + default=dict, + help_text='JSON parameters passed to the checker class', + verbose_name='Checker parameters', + ), + ), + ( + 'is_hidden', + models.BooleanField( + default=False, + help_text='If true, this trophy is hidden until earned', + verbose_name='Hidden', + ), + ), + ( + 'is_progressive', + models.BooleanField( + default=False, + help_text='If true, this trophy shows progress towards completion', + verbose_name='Progressive', + ), + ), + ( + 'is_active', + models.BooleanField( + default=True, + help_text='If false, this trophy cannot be earned', + verbose_name='Active', + ), + ), + ( + 'order', + models.PositiveIntegerField( + default=0, help_text='Display order of the trophy', verbose_name='Order' + ), + ), ('created', models.DateTimeField(auto_now_add=True)), ('updated', models.DateTimeField(auto_now=True)), ], @@ -44,19 +126,108 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserStatistics', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('total_weight_lifted', models.DecimalField(decimal_places=2, default=0, help_text='Cumulative weight lifted in kg', max_digits=12, verbose_name='Total weight lifted')), - ('total_workouts', models.PositiveIntegerField(default=0, help_text='Total number of workout sessions completed', verbose_name='Total workouts')), - ('current_streak', models.PositiveIntegerField(default=0, help_text='Current consecutive days with workouts', verbose_name='Current streak')), - ('longest_streak', models.PositiveIntegerField(default=0, help_text='Longest consecutive days with workouts', verbose_name='Longest streak')), - ('last_workout_date', models.DateField(blank=True, help_text='Date of the most recent workout', null=True, verbose_name='Last workout date')), - ('earliest_workout_time', models.TimeField(blank=True, help_text='Earliest time a workout was started', null=True, verbose_name='Earliest workout time')), - ('latest_workout_time', models.TimeField(blank=True, help_text='Latest time a workout was started', null=True, verbose_name='Latest workout time')), - ('weekend_workout_streak', models.PositiveIntegerField(default=0, help_text='Consecutive weekends with workouts on both Saturday and Sunday', verbose_name='Weekend workout streak')), - ('last_inactive_date', models.DateField(blank=True, help_text='Last date before the current activity period began', null=True, verbose_name='Last inactive date')), - ('worked_out_jan_1', models.BooleanField(default=False, help_text='Whether user has ever worked out on January 1st', verbose_name='Worked out on January 1st')), + ( + 'id', + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID' + ), + ), + ( + 'total_weight_lifted', + models.DecimalField( + decimal_places=2, + default=0, + help_text='Cumulative weight lifted in kg', + max_digits=12, + verbose_name='Total weight lifted', + ), + ), + ( + 'total_workouts', + models.PositiveIntegerField( + default=0, + help_text='Total number of workout sessions completed', + verbose_name='Total workouts', + ), + ), + ( + 'current_streak', + models.PositiveIntegerField( + default=0, + help_text='Current consecutive days with workouts', + verbose_name='Current streak', + ), + ), + ( + 'longest_streak', + models.PositiveIntegerField( + default=0, + help_text='Longest consecutive days with workouts', + verbose_name='Longest streak', + ), + ), + ( + 'last_workout_date', + models.DateField( + blank=True, + help_text='Date of the most recent workout', + null=True, + verbose_name='Last workout date', + ), + ), + ( + 'earliest_workout_time', + models.TimeField( + blank=True, + help_text='Earliest time a workout was started', + null=True, + verbose_name='Earliest workout time', + ), + ), + ( + 'latest_workout_time', + models.TimeField( + blank=True, + help_text='Latest time a workout was started', + null=True, + verbose_name='Latest workout time', + ), + ), + ( + 'weekend_workout_streak', + models.PositiveIntegerField( + default=0, + help_text='Consecutive weekends with workouts on both Saturday and Sunday', + verbose_name='Weekend workout streak', + ), + ), + ( + 'last_inactive_date', + models.DateField( + blank=True, + help_text='Last date before the current activity period began', + null=True, + verbose_name='Last inactive date', + ), + ), + ( + 'worked_out_jan_1', + models.BooleanField( + default=False, + help_text='Whether user has ever worked out on January 1st', + verbose_name='Worked out on January 1st', + ), + ), ('last_updated', models.DateTimeField(auto_now=True, verbose_name='Last updated')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='trophy_statistics', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ( + 'user', + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name='trophy_statistics', + to=settings.AUTH_USER_MODEL, + verbose_name='User', + ), + ), ], options={ 'verbose_name': 'User statistics', @@ -66,12 +237,58 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserTrophy', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('earned_at', models.DateTimeField(auto_now_add=True, help_text='When the trophy was earned', verbose_name='Earned at')), - ('progress', models.FloatField(default=0.0, help_text='Progress towards earning the trophy (0-100)', validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(100.0)], verbose_name='Progress')), - ('is_notified', models.BooleanField(default=False, help_text='Whether the user has been notified about earning this trophy', verbose_name='Notified')), - ('trophy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_trophies', to='trophies.trophy', verbose_name='Trophy')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='earned_trophies', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ( + 'id', + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID' + ), + ), + ( + 'earned_at', + models.DateTimeField( + auto_now_add=True, + help_text='When the trophy was earned', + verbose_name='Earned at', + ), + ), + ( + 'progress', + models.FloatField( + default=0.0, + help_text='Progress towards earning the trophy (0-100)', + validators=[ + django.core.validators.MinValueValidator(0.0), + django.core.validators.MaxValueValidator(100.0), + ], + verbose_name='Progress', + ), + ), + ( + 'is_notified', + models.BooleanField( + default=False, + help_text='Whether the user has been notified about earning this trophy', + verbose_name='Notified', + ), + ), + ( + 'trophy', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='user_trophies', + to='trophies.trophy', + verbose_name='Trophy', + ), + ), + ( + 'user', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='earned_trophies', + to=settings.AUTH_USER_MODEL, + verbose_name='User', + ), + ), ], options={ 'verbose_name': 'User trophy', diff --git a/wger/trophies/migrations/0002_add_last_complete_weekend_date.py b/wger/trophies/migrations/0002_add_last_complete_weekend_date.py index 96a478484..1efe35882 100644 --- a/wger/trophies/migrations/0002_add_last_complete_weekend_date.py +++ b/wger/trophies/migrations/0002_add_last_complete_weekend_date.py @@ -4,7 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('trophies', '0001_initial'), ] @@ -13,6 +12,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='userstatistics', name='last_complete_weekend_date', - field=models.DateField(blank=True, help_text='Date of the last Saturday where both Sat and Sun had workouts', null=True, verbose_name='Last complete weekend date'), + field=models.DateField( + blank=True, + help_text='Date of the last Saturday where both Sat and Sun had workouts', + null=True, + verbose_name='Last complete weekend date', + ), ), ] diff --git a/wger/trophies/migrations/0003_load_initial_trophies.py b/wger/trophies/migrations/0003_load_initial_trophies.py index 403e394c0..67be18aa0 100644 --- a/wger/trophies/migrations/0003_load_initial_trophies.py +++ b/wger/trophies/migrations/0003_load_initial_trophies.py @@ -148,7 +148,6 @@ def reverse_load_trophies(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ('trophies', '0002_add_last_complete_weekend_date'), ] diff --git a/wger/trophies/services/__init__.py b/wger/trophies/services/__init__.py index 9418bfd88..aa30c8cc5 100644 --- a/wger/trophies/services/__init__.py +++ b/wger/trophies/services/__init__.py @@ -14,7 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +# Local from .statistics import UserStatisticsService from .trophy import TrophyService + __all__ = ['UserStatisticsService', 'TrophyService'] diff --git a/wger/trophies/services/statistics.py b/wger/trophies/services/statistics.py index 14d511ea1..91c881f5f 100644 --- a/wger/trophies/services/statistics.py +++ b/wger/trophies/services/statistics.py @@ -200,7 +200,10 @@ class UserStatisticsService: # Update workout times if session has time info if session and session.time_start: - if stats.earliest_workout_time is None or session.time_start < stats.earliest_workout_time: + if ( + stats.earliest_workout_time is None + or session.time_start < stats.earliest_workout_time + ): stats.earliest_workout_time = session.time_start if stats.latest_workout_time is None or session.time_start > stats.latest_workout_time: stats.latest_workout_time = session.time_start @@ -413,12 +416,8 @@ class UserStatisticsService: sunday = saturday + datetime.timedelta(days=1) # Check if both days have workouts - has_saturday = WorkoutSession.objects.filter( - user=stats.user, date=saturday - ).exists() - has_sunday = WorkoutSession.objects.filter( - user=stats.user, date=sunday - ).exists() + has_saturday = WorkoutSession.objects.filter(user=stats.user, date=saturday).exists() + has_sunday = WorkoutSession.objects.filter(user=stats.user, date=sunday).exists() if has_saturday and has_sunday: # This weekend is complete diff --git a/wger/trophies/services/trophy.py b/wger/trophies/services/trophy.py index 783028c8c..69d220c99 100644 --- a/wger/trophies/services/trophy.py +++ b/wger/trophies/services/trophy.py @@ -73,7 +73,9 @@ class TrophyService: # Get all active trophies the user hasn't earned earned_trophy_ids = UserTrophy.objects.filter(user=user).values_list('trophy_id', flat=True) - unevaluated_trophies = Trophy.objects.filter(is_active=True).exclude(id__in=earned_trophy_ids) + unevaluated_trophies = Trophy.objects.filter(is_active=True).exclude( + id__in=earned_trophy_ids + ) awarded = [] for trophy in unevaluated_trophies: @@ -115,7 +117,9 @@ class TrophyService: if checker.check(): return cls.award_trophy(user, trophy, progress=100.0) except Exception as e: - logger.error(f'Error checking trophy {trophy.name} for user {user.id}: {e}', exc_info=True) + logger.error( + f'Error checking trophy {trophy.name} for user {user.id}: {e}', exc_info=True + ) return None @@ -157,9 +161,7 @@ class TrophyService: List of UserTrophy instances """ return list( - UserTrophy.objects.filter(user=user) - .select_related('trophy') - .order_by('-earned_at') + UserTrophy.objects.filter(user=user).select_related('trophy').order_by('-earned_at') ) @classmethod @@ -184,8 +186,7 @@ class TrophyService: # Get user's earned trophies earned = { - ut.trophy_id: ut - for ut in UserTrophy.objects.filter(user=user).select_related('trophy') + ut.trophy_id: ut for ut in UserTrophy.objects.filter(user=user).select_related('trophy') } for trophy in trophies: diff --git a/wger/trophies/signals.py b/wger/trophies/signals.py index 5309d5ff9..0cf54e134 100644 --- a/wger/trophies/signals.py +++ b/wger/trophies/signals.py @@ -53,9 +53,12 @@ def _trigger_trophy_evaluation(user_id: int): evaluate_user_trophies_task.delay(user_id) except Exception: # Celery not available - evaluate synchronously - from wger.trophies.services import TrophyService + # Django from django.contrib.auth.models import User + # wger + from wger.trophies.services import TrophyService + try: user = User.objects.get(id=user_id) TrophyService.evaluate_all_trophies(user) @@ -107,7 +110,10 @@ def workout_log_deleted(sender, instance, **kwargs): try: UserStatisticsService.handle_workout_deletion(instance.user) except Exception as e: - logger.error(f'Error updating statistics after deletion for user {instance.user_id}: {e}', exc_info=True) + logger.error( + f'Error updating statistics after deletion for user {instance.user_id}: {e}', + exc_info=True, + ) @receiver(post_save, sender=WorkoutSession) @@ -155,4 +161,7 @@ def workout_session_deleted(sender, instance, **kwargs): try: UserStatisticsService.handle_workout_deletion(instance.user) except Exception as e: - logger.error(f'Error updating statistics after session deletion for user {instance.user_id}: {e}', exc_info=True) + logger.error( + f'Error updating statistics after session deletion for user {instance.user_id}: {e}', + exc_info=True, + ) diff --git a/wger/trophies/tests/test_api.py b/wger/trophies/tests/test_api.py index 34b42796e..3c92087cb 100644 --- a/wger/trophies/tests/test_api.py +++ b/wger/trophies/tests/test_api.py @@ -123,7 +123,14 @@ class TrophyAPITestCase(WgerTestCase): data = response.json() # Check all expected fields are present - expected_fields = ['id', 'name', 'description', 'trophy_type', 'is_hidden', 'is_progressive'] + expected_fields = [ + 'id', + 'name', + 'description', + 'trophy_type', + 'is_hidden', + 'is_progressive', + ] for field in expected_fields: self.assertIn(field, data) @@ -157,7 +164,9 @@ class TrophyAPITestCase(WgerTestCase): ) # Should not be allowed - self.assertIn(response.status_code, [status.HTTP_403_FORBIDDEN, status.HTTP_405_METHOD_NOT_ALLOWED]) + self.assertIn( + response.status_code, [status.HTTP_403_FORBIDDEN, status.HTTP_405_METHOD_NOT_ALLOWED] + ) class UserTrophyAPITestCase(WgerTestCase): @@ -223,7 +232,7 @@ class UserTrophyAPITestCase(WgerTestCase): defaults={ 'total_workouts': 1, 'total_weight_lifted': Decimal('2500'), - } + }, ) def test_list_user_trophies_authenticated(self): @@ -239,7 +248,9 @@ class UserTrophyAPITestCase(WgerTestCase): self.client.logout() response = self.client.get(reverse('user-trophy-list')) - self.assertIn(response.status_code, [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]) + self.assertIn( + response.status_code, [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN] + ) def test_user_only_sees_own_trophies(self): """Test users only see their own earned trophies""" @@ -369,7 +380,9 @@ class UserTrophyAPITestCase(WgerTestCase): ) # Should not be allowed - self.assertIn(response.status_code, [status.HTTP_403_FORBIDDEN, status.HTTP_405_METHOD_NOT_ALLOWED]) + self.assertIn( + response.status_code, [status.HTTP_403_FORBIDDEN, status.HTTP_405_METHOD_NOT_ALLOWED] + ) def test_trophy_progress_display_format(self): """Test progress display includes current and target values""" diff --git a/wger/trophies/tests/test_integration.py b/wger/trophies/tests/test_integration.py index 9c02885d1..40ba8ebc6 100644 --- a/wger/trophies/tests/test_integration.py +++ b/wger/trophies/tests/test_integration.py @@ -44,7 +44,9 @@ class TrophyIntegrationTestCase(WgerTestCase): self.user = User.objects.get(username='admin') # Set recent login to avoid being skipped by should_skip_user + # Django from django.utils import timezone + self.user.last_login = timezone.now() self.user.save() @@ -85,7 +87,9 @@ class TrophyIntegrationTestCase(WgerTestCase): def test_first_workout_earns_beginner_trophy(self): """Test that completing first workout earns Beginner trophy""" # Create user statistics - stats, _ = UserStatistics.objects.get_or_create(user=self.user, defaults={'total_workouts': 0}) + stats, _ = UserStatistics.objects.get_or_create( + user=self.user, defaults={'total_workouts': 0} + ) # Verify no trophies earned yet self.assertEqual(UserTrophy.objects.filter(user=self.user).count(), 0) @@ -109,7 +113,7 @@ class TrophyIntegrationTestCase(WgerTestCase): defaults={ 'total_workouts': 10, 'total_weight_lifted': Decimal('4999'), - } + }, ) # Evaluate - should not earn yet @@ -137,7 +141,7 @@ class TrophyIntegrationTestCase(WgerTestCase): 'total_workouts': 30, 'current_streak': 29, 'last_workout_date': datetime.date.today(), - } + }, ) # Evaluate - should not earn yet (only 29 days) @@ -165,7 +169,7 @@ class TrophyIntegrationTestCase(WgerTestCase): 'total_workouts': 1, # Qualifies for Beginner 'total_weight_lifted': Decimal('5000'), # Qualifies for Lifter 'current_streak': 30, # Qualifies for Unstoppable - } + }, ) # Evaluate all trophies @@ -186,7 +190,7 @@ class TrophyIntegrationTestCase(WgerTestCase): user=self.user, defaults={ 'total_weight_lifted': Decimal('2500'), # 50% of 5000kg - } + }, ) # Get progress for all trophies @@ -194,8 +198,7 @@ class TrophyIntegrationTestCase(WgerTestCase): # Find Lifter trophy progress lifter_progress = next( - (p for p in progress_list if p['trophy'].id == self.lifter_trophy.id), - None + (p for p in progress_list if p['trophy'].id == self.lifter_trophy.id), None ) self.assertIsNotNone(lifter_progress) @@ -211,7 +214,7 @@ class TrophyIntegrationTestCase(WgerTestCase): user=self.user, defaults={ 'total_workouts': 1, - } + }, ) # Evaluate and earn Beginner trophy @@ -227,7 +230,9 @@ class TrophyIntegrationTestCase(WgerTestCase): # Should not award Beginner again (already earned) self.assertEqual(len(awarded2), 0) - self.assertEqual(UserTrophy.objects.filter(user=self.user, trophy=self.beginner_trophy).count(), 1) + self.assertEqual( + UserTrophy.objects.filter(user=self.user, trophy=self.beginner_trophy).count(), 1 + ) def test_statistics_service_updates_correctly(self): """Test that statistics service updates all fields correctly""" @@ -277,7 +282,7 @@ class TrophyIntegrationTestCase(WgerTestCase): user=self.user, defaults={ 'total_workouts': 1, - } + }, ) # Deactivate the Beginner trophy @@ -300,7 +305,9 @@ class TrophyIntegrationTestCase(WgerTestCase): UserStatistics.objects.get_or_create(user=user2, defaults={'total_workouts': 1}) # Set recent login for both + # Django from django.utils import timezone + self.user.last_login = timezone.now() self.user.save() user2.last_login = timezone.now() @@ -314,7 +321,9 @@ class TrophyIntegrationTestCase(WgerTestCase): self.assertEqual(results['trophies_awarded'], 2) # Verify both users have the trophy - self.assertTrue(UserTrophy.objects.filter(user=self.user, trophy=self.beginner_trophy).exists()) + self.assertTrue( + UserTrophy.objects.filter(user=self.user, trophy=self.beginner_trophy).exists() + ) self.assertTrue(UserTrophy.objects.filter(user=user2, trophy=self.beginner_trophy).exists()) def test_complete_user_journey(self): @@ -327,7 +336,7 @@ class TrophyIntegrationTestCase(WgerTestCase): 'total_weight_lifted': Decimal('100'), 'current_streak': 1, 'last_workout_date': datetime.date.today(), - } + }, ) awarded = TrophyService.evaluate_all_trophies(self.user) diff --git a/wger/trophies/tests/test_services.py b/wger/trophies/tests/test_services.py index e853ad52b..5f3e3b2e3 100644 --- a/wger/trophies/tests/test_services.py +++ b/wger/trophies/tests/test_services.py @@ -62,7 +62,9 @@ class UserStatisticsServiceTestCase(WgerTestCase): def test_get_or_create_returns_existing(self): """Test get_or_create returns existing statistics""" - existing, _ = UserStatistics.objects.get_or_create(user=self.user, defaults={'total_workouts': 5}) + existing, _ = UserStatistics.objects.get_or_create( + user=self.user, defaults={'total_workouts': 5} + ) stats = UserStatisticsService.get_or_create_statistics(self.user) @@ -94,7 +96,7 @@ class UserStatisticsServiceTestCase(WgerTestCase): defaults={ 'total_workouts': 10, 'total_weight_lifted': Decimal('5000'), - } + }, ) stats = UserStatisticsService.handle_workout_deletion(self.user) @@ -258,8 +260,7 @@ class TrophyServiceTestCase(WgerTestCase): # Find the progressive trophy in the list prog_trophy_data = next( - (p for p in progress_list if p['trophy'].id == progressive_trophy.id), - None + (p for p in progress_list if p['trophy'].id == progressive_trophy.id), None ) self.assertIsNotNone(prog_trophy_data) self.assertEqual(prog_trophy_data['progress'], 50.0) diff --git a/wger/urls.py b/wger/urls.py index 132e3a840..badaaadd0 100644 --- a/wger/urls.py +++ b/wger/urls.py @@ -25,6 +25,7 @@ from django.contrib.sitemaps.views import ( sitemap, ) from django.urls import path + # Third Party from django_email_verification import urls as email_urls from drf_spectacular.views import (