- 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
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 modelsbackend/homework/serializers.py- Add new serializersbackend/homework/views.py- Add new ViewSetbackend/homework/urls.py- Add new routesbackend/homework/admin.py- Register new models
Frontend (Modified)
frontend/src/lib/api.ts- Add new typesfrontend/src/app/(dashboard)/trainings/[id]/page.tsx- Complete redesignfrontend/src/app/(dashboard)/homework/page.tsx- Complete rewritefrontend/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:
- State for homework modal
- State for selected exercises (with reps/time)
- Fetch exercises for the modal
- POST to
/homework/training-assignments/on submit - 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
- Go to training detail page
- Add a participant
- Click homework button on participant
- Select exercises and assign
- Go to homework page and verify assignment appears
- Toggle completion status
- Check dashboard for homework count