mirror of
https://github.com/wger-project/wger.git
synced 2025-12-21 13:20:46 -06:00
Automatic linting
This commit is contained in:
@@ -39,6 +39,7 @@ from wger.utils.units import (
|
||||
AbstractWeight,
|
||||
)
|
||||
from wger.weight.models import WeightEntry
|
||||
|
||||
# Local
|
||||
from .language import Language
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}'
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -148,7 +148,6 @@ def reverse_load_trophies(apps, schema_editor):
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('trophies', '0002_add_last_complete_weekend_date'),
|
||||
]
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user