Initial commit: WrestleDesk full project
- Django backend with DRF (clubs, wrestlers, trainers, exercises, templates, trainings, homework, locations, leistungstest) - Next.js 16 frontend with React, Shadcn UI, Tailwind - JWT authentication - Full CRUD for all entities - Calendar view for trainings - Homework management system - Leistungstest tracking
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
from django.contrib import admin
|
||||
from .models import LeistungstestTemplate, LeistungstestTemplateExercise, LeistungstestResult, LeistungstestResultItem
|
||||
|
||||
|
||||
class LeistungstestTemplateExerciseInline(admin.TabularInline):
|
||||
model = LeistungstestTemplateExercise
|
||||
extra = 0
|
||||
readonly_fields = ['exercise']
|
||||
|
||||
|
||||
@admin.register(LeistungstestTemplate)
|
||||
class LeistungstestTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'created_at', 'usage_count']
|
||||
search_fields = ['name']
|
||||
inlines = [LeistungstestTemplateExerciseInline]
|
||||
|
||||
|
||||
@admin.register(LeistungstestResult)
|
||||
class LeistungstestResultAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'wrestler', 'template', 'total_time_seconds', 'score_percent', 'rating', 'completed_at']
|
||||
list_filter = ['template', 'rating', 'completed_at']
|
||||
search_fields = ['wrestler__first_name', 'wrestler__last_name', 'template__name']
|
||||
readonly_fields = ['score_percent', 'created_at']
|
||||
raw_id_fields = ['wrestler', 'template']
|
||||
|
||||
|
||||
@admin.register(LeistungstestResultItem)
|
||||
class LeistungstestResultItemAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'result', 'exercise', 'target_reps', 'actual_reps', 'elapsed_seconds', 'order']
|
||||
list_filter = ['exercise']
|
||||
raw_id_fields = ['result', 'exercise']
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LeistungstestConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'leistungstest'
|
||||
@@ -0,0 +1,94 @@
|
||||
# Generated by Django 4.2.29 on 2026-03-23 12:43
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wrestlers', '0002_alter_wrestler_license_scan_alter_wrestler_photo'),
|
||||
('exercises', '0003_alter_exercise_media'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LeistungstestResult',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('total_time_minutes', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('rating', models.PositiveSmallIntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)], default=3)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('completed_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-completed_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LeistungstestTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LeistungstestResultItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('target_reps', models.PositiveIntegerField()),
|
||||
('actual_reps', models.PositiveIntegerField()),
|
||||
('order', models.IntegerField(default=0)),
|
||||
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exercises.exercise')),
|
||||
('result', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='leistungstest.leistungstestresult')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['result', 'order'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='leistungstestresult',
|
||||
name='template',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='leistungstest.leistungstesttemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='leistungstestresult',
|
||||
name='wrestler',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leistungstest_results', to='wrestlers.wrestler'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LeistungstestTemplateExercise',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('target_reps', models.PositiveIntegerField()),
|
||||
('order', models.IntegerField(default=0)),
|
||||
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exercises.exercise')),
|
||||
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercises', to='leistungstest.leistungstesttemplate')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['template', 'order'],
|
||||
'unique_together': {('template', 'exercise')},
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='leistungstestresult',
|
||||
index=models.Index(fields=['wrestler'], name='leistungste_wrestle_f3f6c2_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='leistungstestresult',
|
||||
index=models.Index(fields=['template'], name='leistungste_templat_daf98b_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='leistungstestresult',
|
||||
index=models.Index(fields=['completed_at'], name='leistungste_complet_838820_idx'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.29 on 2026-03-24 08:14
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('leistungstest', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='leistungstestresult',
|
||||
old_name='total_time_minutes',
|
||||
new_name='total_time_seconds',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.29 on 2026-03-24 09:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('leistungstest', '0002_change_total_time_to_seconds'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='leistungstestresultitem',
|
||||
name='elapsed_seconds',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class LeistungstestTemplate(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def usage_count(self):
|
||||
return LeistungstestResult.objects.filter(template_id=self.pk).count()
|
||||
|
||||
|
||||
class LeistungstestTemplateExercise(models.Model):
|
||||
template = models.ForeignKey(LeistungstestTemplate, on_delete=models.CASCADE, related_name='exercises')
|
||||
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE)
|
||||
target_reps = models.PositiveIntegerField()
|
||||
order = models.IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ['template', 'order']
|
||||
unique_together = ['template', 'exercise']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.template.name} - {self.exercise.name}"
|
||||
|
||||
|
||||
class LeistungstestResult(models.Model):
|
||||
template = models.ForeignKey(LeistungstestTemplate, on_delete=models.CASCADE, related_name='results')
|
||||
wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='leistungstest_results')
|
||||
total_time_seconds = models.PositiveIntegerField(null=True, blank=True)
|
||||
rating = models.PositiveSmallIntegerField(choices=[(1,1),(2,2),(3,3),(4,4),(5,5)], default=3)
|
||||
notes = models.TextField(blank=True)
|
||||
completed_at = models.DateTimeField(default=timezone.now)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-completed_at']
|
||||
indexes = [
|
||||
models.Index(fields=['wrestler']),
|
||||
models.Index(fields=['template']),
|
||||
models.Index(fields=['completed_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.wrestler} - {self.template.name}"
|
||||
|
||||
@property
|
||||
def score_percent(self):
|
||||
items = self.items.all()
|
||||
if not items.exists():
|
||||
return 0
|
||||
total_target = sum(item.target_reps for item in items)
|
||||
total_actual = sum(item.actual_reps for item in items)
|
||||
if total_target == 0:
|
||||
return 0
|
||||
return round((total_actual / total_target) * 100, 1)
|
||||
|
||||
|
||||
class LeistungstestResultItem(models.Model):
|
||||
result = models.ForeignKey(LeistungstestResult, on_delete=models.CASCADE, related_name='items')
|
||||
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE)
|
||||
target_reps = models.PositiveIntegerField()
|
||||
actual_reps = models.PositiveIntegerField()
|
||||
order = models.IntegerField(default=0)
|
||||
elapsed_seconds = models.PositiveIntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ['result', 'order']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.result} - {self.exercise.name}: {self.actual_reps}/{self.target_reps}"
|
||||
@@ -0,0 +1,52 @@
|
||||
from rest_framework import serializers
|
||||
from .models import LeistungstestTemplate, LeistungstestTemplateExercise, LeistungstestResult, LeistungstestResultItem
|
||||
|
||||
|
||||
class LeistungstestTemplateExerciseSerializer(serializers.ModelSerializer):
|
||||
exercise_name = serializers.CharField(source='exercise.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = LeistungstestTemplateExercise
|
||||
fields = ['id', 'exercise', 'exercise_name', 'target_reps', 'order']
|
||||
|
||||
|
||||
class LeistungstestTemplateSerializer(serializers.ModelSerializer):
|
||||
exercises = LeistungstestTemplateExerciseSerializer(many=True, read_only=True)
|
||||
usage_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = LeistungstestTemplate
|
||||
fields = ['id', 'name', 'exercises', 'usage_count', 'created_at']
|
||||
|
||||
|
||||
class LeistungstestResultItemSerializer(serializers.ModelSerializer):
|
||||
exercise_name = serializers.CharField(source='exercise.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = LeistungstestResultItem
|
||||
fields = ['id', 'exercise', 'exercise_name', 'target_reps', 'actual_reps', 'elapsed_seconds', 'order']
|
||||
|
||||
|
||||
class LeistungstestResultSerializer(serializers.ModelSerializer):
|
||||
items = LeistungstestResultItemSerializer(many=True, read_only=True)
|
||||
template_name = serializers.CharField(source='template.name', read_only=True)
|
||||
wrestler_name = serializers.CharField(source='wrestler.__str__', read_only=True)
|
||||
score_percent = serializers.FloatField(read_only=True)
|
||||
total_time_minutes = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = LeistungstestResult
|
||||
fields = ['id', 'template', 'template_name', 'wrestler', 'wrestler_name',
|
||||
'total_time_minutes', 'total_time_seconds', 'rating', 'notes', 'completed_at',
|
||||
'score_percent', 'items', 'created_at']
|
||||
read_only_fields = ['total_time_minutes']
|
||||
|
||||
def get_total_time_minutes(self, obj):
|
||||
if obj.total_time_seconds is None:
|
||||
return None
|
||||
return obj.total_time_seconds // 60
|
||||
|
||||
def validate_total_time_seconds(self, value):
|
||||
if value is not None and value < 0:
|
||||
raise serializers.ValidationError("Zeit muss positiv sein.")
|
||||
return value
|
||||
@@ -0,0 +1,78 @@
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
def get_date_range(period):
|
||||
"""Return start date for period filter, or None for 'all'."""
|
||||
today = date.today()
|
||||
if period == "month":
|
||||
return today.replace(day=1)
|
||||
elif period == "3months":
|
||||
return today - timedelta(days=90)
|
||||
elif period == "year":
|
||||
return today.replace(month=1, day=1)
|
||||
return None
|
||||
|
||||
|
||||
def get_template_leaderboard(template_id, period="all", limit=10):
|
||||
"""Return top wrestlers by score_percent for a template."""
|
||||
from .models import LeistungstestResult
|
||||
|
||||
qs = LeistungstestResult.objects.filter(template_id=template_id)
|
||||
|
||||
start_date = get_date_range(period)
|
||||
if start_date:
|
||||
qs = qs.filter(completed_at__date__gte=start_date)
|
||||
|
||||
qs = qs.select_related('wrestler')
|
||||
|
||||
results = []
|
||||
all_results = list(qs)
|
||||
all_results.sort(key=lambda r: (-r.score_percent, r.total_time_seconds))
|
||||
for rank, result in enumerate(all_results[:limit], 1):
|
||||
results.append({
|
||||
'rank': rank,
|
||||
'wrestler_id': result.wrestler_id,
|
||||
'wrestler_name': str(result.wrestler),
|
||||
'score_percent': result.score_percent,
|
||||
'total_time_seconds': result.total_time_seconds,
|
||||
'completed_at': result.completed_at.date().isoformat() if result.completed_at else None,
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def get_exercise_leaderboard(exercise_id, period="all", limit=10):
|
||||
"""Return top wrestlers by best time for an exercise."""
|
||||
from .models import LeistungstestResultItem, LeistungstestResult
|
||||
from django.db.models import Min
|
||||
|
||||
start_date = get_date_range(period)
|
||||
|
||||
qs = LeistungstestResultItem.objects.filter(exercise_id=exercise_id)
|
||||
if start_date:
|
||||
qs = qs.filter(result__completed_at__date__gte=start_date)
|
||||
|
||||
# Get best time per wrestler
|
||||
best_times = qs.values('result__wrestler__id', 'result__wrestler__first_name', 'result__wrestler__last_name', 'result__completed_at__date')\
|
||||
.annotate(best_time=Min('elapsed_seconds'))\
|
||||
.order_by('best_time')
|
||||
|
||||
results = []
|
||||
for rank, item in enumerate(best_times[:limit], 1):
|
||||
wrestler_name = f"{item['result__wrestler__first_name']} {item['result__wrestler__last_name']}"
|
||||
results.append({
|
||||
'rank': rank,
|
||||
'wrestler_id': item['result__wrestler__id'],
|
||||
'wrestler_name': wrestler_name.strip(),
|
||||
'best_time_seconds': item['best_time'],
|
||||
'completed_at': item['result__completed_at__date'].isoformat() if item['result__completed_at__date'] else None,
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def get_used_exercises():
|
||||
"""Return all exercises that have been used in any Leistungstest result."""
|
||||
from .models import LeistungstestResultItem
|
||||
from exercises.models import Exercise
|
||||
|
||||
exercise_ids = LeistungstestResultItem.objects.values_list('exercise_id', flat=True).distinct()
|
||||
return Exercise.objects.filter(id__in=exercise_ids).order_by('name')
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import LeistungstestTemplateViewSet, LeistungstestTemplateExerciseViewSet, LeistungstestResultViewSet, LeistungstestResultItemViewSet, LeistungstestStatsViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('templates', LeistungstestTemplateViewSet, basename='leistungstest-template')
|
||||
router.register('template-exercises', LeistungstestTemplateExerciseViewSet, basename='leistungstest-template-exercise')
|
||||
router.register('results', LeistungstestResultViewSet, basename='leistungstest-result')
|
||||
router.register('result-items', LeistungstestResultItemViewSet, basename='leistungstest-result-item')
|
||||
router.register('stats', LeistungstestStatsViewSet, basename='leistungstest-stats')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
@@ -0,0 +1,259 @@
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from .models import LeistungstestTemplate, LeistungstestTemplateExercise, LeistungstestResult, LeistungstestResultItem
|
||||
from .serializers import (
|
||||
LeistungstestTemplateSerializer,
|
||||
LeistungstestTemplateExerciseSerializer,
|
||||
LeistungstestResultSerializer,
|
||||
LeistungstestResultItemSerializer,
|
||||
)
|
||||
from .stats import get_template_leaderboard, get_exercise_leaderboard, get_used_exercises
|
||||
|
||||
|
||||
class LeistungstestTemplateViewSet(viewsets.ModelViewSet):
|
||||
queryset = LeistungstestTemplate.objects.all()
|
||||
serializer_class = LeistungstestTemplateSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return LeistungstestTemplate.objects.all().prefetch_related('exercises__exercise')
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
name = request.data.get('name')
|
||||
exercises_data = request.data.get('exercises', [])
|
||||
|
||||
# Create template first
|
||||
template = LeistungstestTemplate.objects.create(name=name)
|
||||
|
||||
# Create exercises
|
||||
for i, ex_data in enumerate(exercises_data):
|
||||
LeistungstestTemplateExercise.objects.create(
|
||||
template=template,
|
||||
exercise_id=ex_data['exercise'],
|
||||
target_reps=ex_data['target_reps'],
|
||||
order=ex_data.get('order', i),
|
||||
)
|
||||
|
||||
# Reload with prefetch to get exercise names
|
||||
template = LeistungstestTemplate.objects.prefetch_related('exercises__exercise').get(pk=template.pk)
|
||||
|
||||
return Response(
|
||||
LeistungstestTemplateSerializer(template).data,
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def duplicate(self, request, pk=None):
|
||||
template = self.get_object()
|
||||
new_template = LeistungstestTemplate.objects.create(name=f"{template.name} (Kopie)")
|
||||
|
||||
for exercise in template.exercises.all():
|
||||
LeistungstestTemplateExercise.objects.create(
|
||||
template=new_template,
|
||||
exercise=exercise.exercise,
|
||||
target_reps=exercise.target_reps,
|
||||
order=exercise.order,
|
||||
)
|
||||
|
||||
return Response(
|
||||
LeistungstestTemplateSerializer(new_template).data,
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
instance = self.get_object()
|
||||
|
||||
instance.name = request.data.get('name', instance.name)
|
||||
instance.save()
|
||||
|
||||
exercises_data = request.data.get('exercises')
|
||||
if exercises_data is not None:
|
||||
instance.exercises.all().delete()
|
||||
for i, ex_data in enumerate(exercises_data):
|
||||
LeistungstestTemplateExercise.objects.create(
|
||||
template=instance,
|
||||
exercise_id=ex_data['exercise'],
|
||||
target_reps=ex_data['target_reps'],
|
||||
order=ex_data.get('order', i),
|
||||
)
|
||||
|
||||
instance = LeistungstestTemplate.objects.prefetch_related('exercises__exercise').get(pk=instance.pk)
|
||||
return Response(LeistungstestTemplateSerializer(instance).data)
|
||||
|
||||
|
||||
class LeistungstestTemplateExerciseViewSet(viewsets.ModelViewSet):
|
||||
queryset = LeistungstestTemplateExercise.objects.all()
|
||||
serializer_class = LeistungstestTemplateExerciseSerializer
|
||||
|
||||
|
||||
class LeistungstestResultViewSet(viewsets.ModelViewSet):
|
||||
queryset = LeistungstestResult.objects.all()
|
||||
serializer_class = LeistungstestResultSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = LeistungstestResult.objects.all().prefetch_related('items__exercise')
|
||||
|
||||
template_id = self.request.query_params.get('template')
|
||||
wrestler_id = self.request.query_params.get('wrestler')
|
||||
|
||||
if template_id:
|
||||
queryset = queryset.filter(template_id=template_id)
|
||||
if wrestler_id:
|
||||
queryset = queryset.filter(wrestler_id=wrestler_id)
|
||||
|
||||
return queryset
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
template_id = request.data.get('template')
|
||||
wrestler_id = request.data.get('wrestler')
|
||||
items_data = request.data.get('items', [])
|
||||
|
||||
result = LeistungstestResult.objects.create(
|
||||
template_id=template_id,
|
||||
wrestler_id=wrestler_id,
|
||||
total_time_seconds=request.data.get('total_time_seconds') or None,
|
||||
rating=request.data.get('rating', 3),
|
||||
notes=request.data.get('notes', ''),
|
||||
)
|
||||
|
||||
for i, item_data in enumerate(items_data):
|
||||
LeistungstestResultItem.objects.create(
|
||||
result=result,
|
||||
exercise_id=item_data['exercise'],
|
||||
target_reps=item_data.get('target_reps', 0),
|
||||
actual_reps=item_data.get('actual_reps', 0),
|
||||
elapsed_seconds=item_data.get('elapsed_seconds', 0),
|
||||
order=item_data.get('order', i),
|
||||
)
|
||||
|
||||
result_items = LeistungstestResultItem.objects.filter(result=result)
|
||||
total_target = sum(item.target_reps for item in result_items)
|
||||
total_actual = sum(item.actual_reps for item in result_items)
|
||||
|
||||
if total_target > 0:
|
||||
score = round((total_actual / total_target) * 100, 1)
|
||||
else:
|
||||
score = 0.0
|
||||
|
||||
result.refresh_from_db()
|
||||
|
||||
result_data = LeistungstestResultSerializer(result).data
|
||||
result_data['score_percent'] = score
|
||||
|
||||
return Response(result_data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
instance = self.get_object()
|
||||
|
||||
instance.total_time_seconds = request.data.get('total_time_seconds', instance.total_time_seconds)
|
||||
instance.rating = request.data.get('rating', instance.rating)
|
||||
instance.notes = request.data.get('notes', instance.notes)
|
||||
instance.save()
|
||||
|
||||
items_data = request.data.get('items')
|
||||
if items_data is not None:
|
||||
instance.items.all().delete()
|
||||
for i, item_data in enumerate(items_data):
|
||||
LeistungstestResultItem.objects.create(
|
||||
result=instance,
|
||||
exercise_id=item_data['exercise'],
|
||||
target_reps=item_data.get('target_reps', 0),
|
||||
actual_reps=item_data.get('actual_reps', 0),
|
||||
elapsed_seconds=item_data.get('elapsed_seconds', 0),
|
||||
order=item_data.get('order', i),
|
||||
)
|
||||
|
||||
result_items = LeistungstestResultItem.objects.filter(result=instance)
|
||||
total_target = sum(item.target_reps for item in result_items)
|
||||
total_actual = sum(item.actual_reps for item in result_items)
|
||||
|
||||
if total_target > 0:
|
||||
score = round((total_actual / total_target) * 100, 1)
|
||||
else:
|
||||
score = 0.0
|
||||
|
||||
result_data = LeistungstestResultSerializer(instance).data
|
||||
result_data['score_percent'] = score
|
||||
|
||||
return Response(result_data)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def leaderboard(self, request):
|
||||
template_id = request.query_params.get('template')
|
||||
if not template_id:
|
||||
return Response(
|
||||
{'error': 'template parameter is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
limit = int(request.query_params.get('limit', 10))
|
||||
|
||||
results = LeistungstestResult.objects.filter(template_id=template_id)\
|
||||
.select_related('wrestler')
|
||||
|
||||
leaderboard_data = []
|
||||
for result in results:
|
||||
leaderboard_data.append({
|
||||
'rank': 0,
|
||||
'result_id': result.id,
|
||||
'wrestler_id': result.wrestler.id,
|
||||
'wrestler_name': str(result.wrestler),
|
||||
'score_percent': result.score_percent,
|
||||
'completed_at': result.completed_at,
|
||||
'total_time_seconds': result.total_time_seconds,
|
||||
'rating': result.rating,
|
||||
})
|
||||
|
||||
leaderboard_data.sort(key=lambda x: x['score_percent'], reverse=True)
|
||||
leaderboard_data = leaderboard_data[:limit]
|
||||
for i, entry in enumerate(leaderboard_data, 1):
|
||||
entry['rank'] = i
|
||||
|
||||
return Response(leaderboard_data)
|
||||
|
||||
|
||||
class LeistungstestResultItemViewSet(viewsets.ModelViewSet):
|
||||
queryset = LeistungstestResultItem.objects.all()
|
||||
serializer_class = LeistungstestResultItemSerializer
|
||||
|
||||
|
||||
class LeistungstestStatsViewSet(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def leaderboard(self, request):
|
||||
lb_type = request.query_params.get('type', 'template')
|
||||
template_id = request.query_params.get('template_id')
|
||||
exercise_id = request.query_params.get('exercise_id')
|
||||
period = request.query_params.get('period', 'all')
|
||||
limit = int(request.query_params.get('limit', 10))
|
||||
|
||||
if lb_type == 'template' and template_id:
|
||||
results = get_template_leaderboard(int(template_id), period, limit)
|
||||
template = LeistungstestTemplate.objects.get(pk=template_id)
|
||||
return Response({
|
||||
'template_id': template_id,
|
||||
'template_name': template.name,
|
||||
'period': period,
|
||||
'results': results,
|
||||
})
|
||||
elif lb_type == 'exercise' and exercise_id:
|
||||
from exercises.models import Exercise
|
||||
results = get_exercise_leaderboard(int(exercise_id), period, limit)
|
||||
exercise = Exercise.objects.get(pk=exercise_id)
|
||||
return Response({
|
||||
'exercise_id': exercise_id,
|
||||
'exercise_name': exercise.name,
|
||||
'period': period,
|
||||
'results': results,
|
||||
})
|
||||
return Response({'error': 'Invalid parameters'}, status=400)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def exercises(self, request):
|
||||
exercises = get_used_exercises()
|
||||
return Response([{'id': e.id, 'name': e.name} for e in exercises])
|
||||
Reference in New Issue
Block a user