- 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
13 KiB
Leistungstest Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Create Leistungstest (Performance Test) system with templates, assignment, results tracking, and leaderboard.
Architecture: Backend provides CRUD for templates and results + leaderboard endpoint. Frontend has 4 tabs: Vorlagen, Zuweisen, Ergebnisse, Leaderboard.
Tech Stack: Django REST Framework (backend), React/Next.js (frontend)
File Structure
Backend
- Create:
backend/leistungstest/__init__.py - Create:
backend/leistungstest/apps.py - Create:
backend/leistungstest/models.py - Create:
backend/leistungstest/serializers.py - Create:
backend/leistungstest/views.py - Create:
backend/leistungstest/urls.py - Modify:
backend/wrestleDesk/settings.py— add 'leistungstest' - Modify:
backend/wrestleDesk/urls.py— include leistungstest URLs
Frontend
- Create:
frontend/src/app/(dashboard)/leistungstest/page.tsx - Modify:
frontend/src/components/layout/sidebar.tsx— add Leistungstest nav item - Modify:
frontend/src/lib/api.ts— add interfaces
Tasks
Task 1: Create Leistungstest Backend App
- Step 1: Create directory and files
Create backend/leistungstest/ with __init__.py and apps.py
- Step 2: Add to INSTALLED_APPS
Add 'leistungstest' to INSTALLED_APPS in backend/wrestleDesk/settings.py
- Step 3: Create models
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 self.results.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_minutes = 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)
class Meta:
ordering = ['result', 'order']
def __str__(self):
return f"{self.result} - {self.exercise.name}: {self.actual_reps}/{self.target_reps}"
- Step 4: Create serializers
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', '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)
class Meta:
model = LeistungstestResult
fields = ['id', 'template', 'template_name', 'wrestler', 'wrestler_name',
'total_time_minutes', 'rating', 'notes', 'completed_at',
'score_percent', 'items', 'created_at']
class LeaderboardEntrySerializer(serializers.Serializer):
rank = serializers.IntegerField()
wrestler = serializers.DictField()
score_percent = serializers.FloatField()
rating = serializers.IntegerField()
time_minutes = serializers.IntegerField(allow_null=True)
- Step 5: Create views
from rest_framework import viewsets
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Max
from .models import LeistungstestTemplate, LeistungstestTemplateExercise, LeistungstestResult, LeistungstestResultItem
from .serializers import (
LeistungstestTemplateSerializer, LeistungstestResultSerializer
)
class LeistungstestTemplateViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
queryset = LeistungstestTemplate.objects.all()
serializer_class = LeistungstestTemplateSerializer
def perform_create(self, serializer):
template = serializer.save()
exercises_data = self.request.data.get('exercises', [])
for i, ex in enumerate(exercises_data):
LeistungstestTemplateExercise.objects.create(
template=template,
exercise_id=ex['exercise'],
target_reps=ex['target_reps'],
order=i
)
class LeistungstestTemplateExerciseViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
queryset = LeistungstestTemplateExercise.objects.all()
serializer_class = None
class LeistungstestResultViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = LeistungstestResultSerializer
def get_queryset(self):
queryset = LeistungstestResult.objects.all()
wrestler = self.request.query_params.get('wrestler')
template = self.request.query_params.get('template')
if wrestler:
queryset = queryset.filter(wrestler=wrestler)
if template:
queryset = queryset.filter(template=template)
return queryset.prefetch_related('items')
def perform_create(self, serializer):
result = serializer.save()
items_data = self.request.data.get('items', [])
for i, item in enumerate(items_data):
LeistungstestResultItem.objects.create(
result=result,
exercise_id=item['exercise'],
target_reps=item['target_reps'],
actual_reps=item['actual_reps'],
order=i
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def leaderboard(request):
template_id = request.query_params.get('template')
if not template_id:
return Response({'error': 'template is required'}, status=400)
template = LeistungstestTemplate.objects.get(id=template_id)
latest_results = LeistungstestResult.objects.filter(
template=template
).values('wrestler').annotate(
latest_date=Max('completed_at')
)
rankings = []
for entry in latest_results:
result = LeistungstestResult.objects.get(
template=template,
wrestler_id=entry['wrestler'],
completed_at=entry['latest_date']
)
wrestler = result.wrestler
rankings.append({
'rank': 0,
'wrestler': {'id': wrestler.id, 'name': str(wrestler)},
'score_percent': result.score_percent,
'rating': result.rating,
'time_minutes': result.total_time_minutes
})
rankings.sort(key=lambda x: (-x['score_percent'], x['time_minutes'] or 999))
for i, r in enumerate(rankings):
r['rank'] = i + 1
return Response({
'template': {'id': template.id, 'name': template.name},
'rankings': rankings
})
- Step 6: Create urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import LeistungstestTemplateViewSet, LeistungstestResultViewSet, leaderboard
router = DefaultRouter()
router.register(r'templates', LeistungstestTemplateViewSet, basename='leistungstest-template')
router.register(r'results', LeistungstestResultViewSet, basename='leistungstest-result')
urlpatterns = [
path('leaderboard/', leaderboard, name='leistungstest-leaderboard'),
path('', include(router.urls)),
]
- Step 7: Add URL to main urls.py
path('api/v1/leistungstest/', include('leistungstest.urls')),
- Step 8: Create migration
Run: cd backend && python manage.py makemigrations leistungstest
Task 2: Update Frontend API Types
- Step 1: Add interfaces to frontend/src/lib/api.ts
export interface ILeistungstestTemplateExercise {
id: number
exercise: number
exercise_name: string
target_reps: number
order: number
}
export interface ILeistungstestTemplate {
id: number
name: string
exercises: ILeistungstestTemplateExercise[]
usage_count: number
created_at: string
}
export interface ILeistungstestResultItem {
id: number
exercise: number
exercise_name: string
target_reps: number
actual_reps: number
order: number
}
export interface ILeistungstestResult {
id: number
template: number
template_name: string
wrestler: number
wrestler_name: string
total_time_minutes: number | null
rating: number
notes: string
completed_at: string
score_percent: number
items: ILeistungstestResultItem[]
created_at: string
}
export interface ILeaderboardEntry {
rank: number
wrestler: { id: number; name: string }
score_percent: number
rating: number
time_minutes: number | null
}
export interface ILeaderboard {
template: { id: number; name: string }
rankings: ILeaderboardEntry[]
}
Task 3: Create Leistungstest Page
- Step 1: Create page with 4 tabs
Create frontend/src/app/(dashboard)/leistungstest/page.tsx with:
-
Tab state (vorlagen | zuweisen | ergebnisse | leaderboard)
-
Vorlagen tab: Template list + create form
-
Zuweisen tab: Select wrestler/template, record results
-
Ergebnisse tab: Results table with filters + progress
-
Leaderboard tab: Rankings by template
-
Step 2: Add sidebar navigation
Add to sidebar.tsx:
{ name: "Leistungstest", href: "/leistungstest", icon: Trophy },
Import Trophy icon from lucide-react.
Testing
Backend
- Run:
cd backend && python manage.py check - Run:
python manage.py migrate - Test endpoint:
curl -H "Authorization: Bearer <token>" http://localhost:8000/api/v1/leistungstest/templates/
Frontend
- Run:
cd frontend && npm run lint - Visit: http://localhost:3000/leistungstest
Notes
- Wrestler/Template dropdowns use SelectValue with find() to show names not IDs
- Score = (sum actual_reps / sum target_reps) * 100
- Leaderboard shows latest result per wrestler for selected template
- Results table sorted by date (newest first)
- Star rating component with clickable stars (1-5)