Files
WrestleDesk/docs/superpowers/plans/2026-03-23-leistungstest-implementation.md
T
Andrej Spielmann 3fefc550fe 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
2026-03-26 13:24:57 +01:00

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


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)