Files
WrestleDesk/docs/superpowers/plans/2026-03-22-homework-redesign-plan.md
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

Homework System Redesign - 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: Redesign homework system to assign exercises directly from training page without templates.

Architecture:

  • New models: TrainingHomework, TrainingHomeworkExercise, TrainingHomeworkAssignment
  • Training detail page redesigned with 2-column layout: participants | training homework
  • Homework button per participant opens modal to select exercises
  • Homework page shows all assignments grouped by training

Tech Stack: Django + DRF backend, Next.js + React frontend


File Structure

Backend (Modified)

  • backend/homework/models.py - Add new models
  • backend/homework/serializers.py - Add new serializers
  • backend/homework/views.py - Add new ViewSet
  • backend/homework/urls.py - Add new routes
  • backend/homework/admin.py - Register new models

Frontend (Modified)

  • frontend/src/lib/api.ts - Add new types
  • frontend/src/app/(dashboard)/trainings/[id]/page.tsx - Complete redesign
  • frontend/src/app/(dashboard)/homework/page.tsx - Complete rewrite
  • frontend/src/app/(dashboard)/dashboard/page.tsx - Add homework stats

Tasks

Task 1: Backend - New Models

Files:

  • Modify: backend/homework/models.py

  • Step 1: Add new models at end of file

class TrainingHomework(models.Model):
    training = models.ForeignKey('trainings.Training', on_delete=models.CASCADE, related_name='homework_assignments')
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['training']),
        ]

    def __str__(self):
        return f"Homework for Training {self.training_id}"


class TrainingHomeworkExercise(models.Model):
    training_homework = models.ForeignKey(TrainingHomework, on_delete=models.CASCADE, related_name='exercises')
    exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='training_homework_items')
    reps = models.PositiveIntegerField(null=True, blank=True)
    time_minutes = models.PositiveIntegerField(null=True, blank=True)
    order = models.IntegerField(default=0)

    class Meta:
        ordering = ['training_homework', 'order']
        unique_together = ['training_homework', 'exercise']

    def __str__(self):
        return f"{self.training_homework} - {self.exercise.name}"


class TrainingHomeworkAssignment(models.Model):
    training_homework = models.ForeignKey(TrainingHomework, on_delete=models.CASCADE, related_name='assignments')
    wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='training_homework_assignments')
    club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='training_homework_assignments', null=True, blank=True)
    notes = models.TextField(blank=True)
    is_completed = models.BooleanField(default=False)
    completion_date = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ['training_homework', 'wrestler']
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['wrestler']),
            models.Index(fields=['is_completed']),
            models.Index(fields=['club']),
        ]

    def __str__(self):
        return f"{self.wrestler} - Training {self.training_homework.training_id}"
  • Step 2: Run makemigrations

Run: cd backend && python manage.py makemigrations homework Expected: "Migrations for 'homework': homework/migrations/xxx_create_training_homework.py"

  • Step 3: Run migrate

Run: cd backend && python manage.py migrate Expected: "Applying homework.xxx_create_training_homework... OK"


Task 2: Backend - Serializers

Files:

  • Modify: backend/homework/serializers.py

  • Step 1: Add new serializers

Add at end of file:

class TrainingHomeworkExerciseSerializer(serializers.ModelSerializer):
    exercise_name = serializers.CharField(source='exercise.name', read_only=True)
    exercise_category = serializers.CharField(source='exercise.category', read_only=True)

    class Meta:
        model = TrainingHomeworkExercise
        fields = ['id', 'exercise', 'exercise_name', 'exercise_category', 'reps', 'time_minutes', 'order']


class TrainingHomeworkSerializer(serializers.ModelSerializer):
    exercises = TrainingHomeworkExerciseSerializer(many=True, read_only=True)
    training_date = serializers.DateField(source='training.date', read_only=True)
    training_group = serializers.CharField(source='training.group', read_only=True)

    class Meta:
        model = TrainingHomework
        fields = ['id', 'training', 'training_date', 'training_group', 'exercises', 'created_at']


class TrainingHomeworkAssignmentSerializer(serializers.ModelSerializer):
    training_homework_detail = TrainingHomeworkSerializer(source='training_homework', read_only=True)
    wrestler_name = serializers.SerializerMethodField()
    wrestler_group = serializers.CharField(source='wrestler.group', read_only=True)

    class Meta:
        model = TrainingHomeworkAssignment
        fields = ['id', 'training_homework', 'training_homework_detail', 'wrestler', 'wrestler_name', 'wrestler_group', 'notes', 'is_completed', 'completion_date', 'created_at']

    def get_wrestler_name(self, obj):
        return f"{obj.wrestler.first_name} {obj.wrestler.last_name}"


class TrainingHomeworkAssignmentCreateSerializer(serializers.Serializer):
    training = serializers.IntegerField()
    wrestler = serializers.IntegerField()
    exercises = serializers.ListField(
        child=serializers.DictField(child=serializers.IntegerField(allow_null=True))
    )
    notes = serializers.CharField(required=False, allow_blank=True, default='')

    def create(self, validated_data):
        from clubs.utils import get_user_club
        from exercises.models import Exercise
        from trainings.models import Training
        from wrestlers.models import Wrestler

        user = self.context['request'].user
        club = get_user_club(user)

        training_id = validated_data['training']
        wrestler_id = validated_data['wrestler']
        exercises_data = validated_data['exercises']
        notes = validated_data.get('notes', '')

        # Create or get TrainingHomework for this training
        training_obj = Training.objects.get(id=training_id)
        training_homework, _ = TrainingHomework.objects.get_or_create(training=training_obj)

        # Create assignment
        assignment = TrainingHomeworkAssignment.objects.create(
            training_homework=training_homework,
            wrestler_id=wrestler_id,
            club=club,
            notes=notes
        )

        # Add exercises
        for i, ex in enumerate(exercises_data):
            TrainingHomeworkExercise.objects.create(
                training_homework=training_homework,
                exercise_id=ex['exercise'],
                reps=ex.get('reps'),
                time_minutes=ex.get('time_minutes'),
                order=i
            )

        return assignment

Task 3: Backend - Views

Files:

  • Modify: backend/homework/views.py

  • Step 1: Add new ViewSet

Add at end of file:

class TrainingHomeworkAssignmentViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, ClubLevelPermission]
    filter_backends = [ClubFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    serializer_class = TrainingHomeworkAssignmentSerializer
    filterset_fields = ['is_completed']
    search_fields = ['wrestler__first_name', 'wrestler__last_name']
    ordering_fields = ['created_at', 'is_completed']
    http_method_names = ['get', 'post', 'patch', 'delete']

    def get_queryset(self):
        from clubs.utils import get_user_club
        club = get_user_club(self.request.user)
        return TrainingHomeworkAssignment.objects.filter(club=club).select_related(
            'training_homework', 'training_homework__training', 'wrestler'
        )

    def get_serializer_class(self):
        if self.action == 'create':
            return TrainingHomeworkAssignmentCreateSerializer
        return TrainingHomeworkAssignmentSerializer

    @action(detail=True, methods=['post'])
    def complete(self, request, pk=None):
        assignment = self.get_object()
        assignment.is_completed = True
        assignment.completion_date = timezone.now().date()
        assignment.save()
        serializer = self.get_serializer(assignment)
        return Response(serializer.data)

    @action(detail=True, methods=['post'])
    def uncomplete(self, request, pk=None):
        assignment = self.get_object()
        assignment.is_completed = False
        assignment.completion_date = None
        assignment.save()
        serializer = self.get_serializer(assignment)
        return Response(serializer.data)
  • Step 2: Import new serializers at top of file

Add to imports:

from .serializers import (
    # ... existing imports ...
    TrainingHomeworkAssignmentSerializer,
    TrainingHomeworkAssignmentCreateSerializer,
)

Task 4: Backend - URLs

Files:

  • Modify: backend/homework/urls.py

  • Step 1: Add new route

Add to urlpatterns:

router.register(r'training-assignments', views.TrainingHomeworkAssignmentViewSet, basename='training-assignment')

Task 5: Backend - Admin

Files:

  • Modify: backend/homework/admin.py

  • Step 1: Register new models

Add at end of file:

@admin.register(TrainingHomework)
class TrainingHomeworkAdmin(admin.ModelAdmin):
    list_display = ['id', 'training', 'created_at']
    list_select_related = ['training']


@admin.register(TrainingHomeworkAssignment)
class TrainingHomeworkAssignmentAdmin(admin.ModelAdmin):
    list_display = ['id', 'wrestler', 'training_homework', 'is_completed', 'created_at']
    list_filter = ['is_completed', 'created_at']
    list_select_related = ['wrestler', 'training_homework__training']

Task 6: Frontend - Types

Files:

  • Modify: frontend/src/lib/api.ts

  • Step 1: Add new interfaces

Add after existing homework interfaces:

export interface ITrainingHomeworkExercise {
  id: number
  exercise: number
  exercise_name: string
  exercise_category: string
  reps: number | null
  time_minutes: number | null
  order: number
}

export interface ITrainingHomework {
  id: number
  training: number
  training_date: string
  training_group: string
  exercises: ITrainingHomeworkExercise[]
  created_at: string
}

export interface ITrainingHomeworkAssignment {
  id: number
  training_homework: number
  training_homework_detail: ITrainingHomework
  wrestler: number
  wrestler_name: string
  wrestler_group: string
  notes: string
  is_completed: boolean
  completion_date: string | null
  created_at: string
}

Task 7: Frontend - Training Page Redesign

Files:

  • Modify: frontend/src/app/(dashboard)/trainings/[id]/page.tsx

This is a complete rewrite. Key changes:

  • 2-column layout: left = participants, right = training homework for this session

  • Each participant has a homework button (BookOpen icon)

  • Click opens modal to select exercises and assign

  • Step 1: Read current file (already done above)

  • Step 2: Replace the entire file content

Write new content with:

  1. State for homework modal
  2. State for selected exercises (with reps/time)
  3. Fetch exercises for the modal
  4. POST to /homework/training-assignments/ on submit
  5. 2-column layout below details section

Task 8: Frontend - Homework Page

Files:

  • Modify: frontend/src/app/(dashboard)/homework/page.tsx

Complete rewrite:

  • Fetch from /homework/training-assignments/
  • Group by training date
  • Filter: All / Open / Completed
  • Search by wrestler name
  • Each assignment shows wrestler, exercises, status
  • Toggle complete/uncomplete via API

Task 9: Frontend - Dashboard Stats

Files:

  • Modify: frontend/src/app/(dashboard)/dashboard/page.tsx

  • Step 1: Add homework count to stats

Add a new stat card showing count of incomplete assignments from /homework/training-assignments/?is_completed=false


Task 10: Build & Test

  • Step 1: Run backend migrate

Run: cd backend && python manage.py migrate

  • Step 2: Run frontend build

Run: cd frontend && npm run build

  • Step 3: Test manually
  1. Go to training detail page
  2. Add a participant
  3. Click homework button on participant
  4. Select exercises and assign
  5. Go to homework page and verify assignment appears
  6. Toggle completion status
  7. Check dashboard for homework count