Automatic linting

This commit is contained in:
Github-actions
2025-12-14 11:18:18 +00:00
parent 88cbec70b5
commit 0bcc47a2d0
20 changed files with 346 additions and 107 deletions

View File

@@ -39,6 +39,7 @@ from wger.utils.units import (
AbstractWeight,
)
from wger.weight.models import WeightEntry
# Local
from .language import Language

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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}'

View File

@@ -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."""

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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',

View File

@@ -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',
),
),
]

View File

@@ -148,7 +148,6 @@ def reverse_load_trophies(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('trophies', '0002_add_last_complete_weekend_date'),
]

View File

@@ -14,7 +14,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Local
from .statistics import UserStatisticsService
from .trophy import TrophyService
__all__ = ['UserStatisticsService', 'TrophyService']

View File

@@ -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

View File

@@ -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:

View File

@@ -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,
)

View File

@@ -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"""

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 (