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:
Andrej Spielmann
2026-03-26 13:24:57 +01:00
commit 3fefc550fe
256 changed files with 38295 additions and 0 deletions
View File
+73
View File
@@ -0,0 +1,73 @@
import unfold
from unfold.admin import ModelAdmin as UnfoldModelAdmin
from django.contrib import admin
from .models import Homework, HomeworkExerciseItem, HomeworkAssignment, HomeworkAssignmentItem, HomeworkStatus, TrainingHomework, TrainingHomeworkExercise, TrainingHomeworkAssignment
class HomeworkExerciseItemInline(admin.TabularInline):
model = HomeworkExerciseItem
extra = 1
raw_id_fields = ['exercise']
@admin.register(Homework)
class HomeworkAdmin(UnfoldModelAdmin):
list_display = ['title', 'club', 'due_date', 'is_active', 'exercise_count']
list_filter = ['is_active', 'club']
search_fields = ['title', 'description']
readonly_fields = ['created_at', 'updated_at']
raw_id_fields = ['club']
inlines = [HomeworkExerciseItemInline]
def exercise_count(self, obj):
return obj.exercise_items.count()
exercise_count.short_description = 'Exercises'
class HomeworkAssignmentItemInline(admin.TabularInline):
model = HomeworkAssignmentItem
extra = 0
raw_id_fields = ['exercise']
readonly_fields = ['completion_date']
@admin.register(HomeworkAssignment)
class HomeworkAssignmentAdmin(UnfoldModelAdmin):
list_display = ['wrestler', 'homework', 'club', 'due_date', 'is_completed_display']
list_filter = ['club']
search_fields = ['wrestler__first_name', 'wrestler__last_name', 'homework__title']
readonly_fields = ['created_at', 'updated_at']
raw_id_fields = ['homework', 'wrestler', 'club']
inlines = [HomeworkAssignmentItemInline]
def is_completed_display(self, obj):
return obj.is_completed
is_completed_display.short_description = 'Completed'
is_completed_display.boolean = True
@admin.register(HomeworkExerciseItem)
class HomeworkExerciseItemAdmin(UnfoldModelAdmin):
list_display = ['homework', 'exercise', 'reps', 'time_minutes', 'order']
list_filter = ['homework']
raw_id_fields = ['homework', 'exercise']
@admin.register(HomeworkStatus)
class HomeworkStatusAdmin(UnfoldModelAdmin):
list_display = ['homework', 'wrestler', 'is_completed', 'completion_date']
list_filter = ['is_completed']
raw_id_fields = ['homework', 'wrestler']
@admin.register(TrainingHomework)
class TrainingHomeworkAdmin(UnfoldModelAdmin):
list_display = ['id', 'training', 'created_at']
list_select_related = ['training']
@admin.register(TrainingHomeworkAssignment)
class TrainingHomeworkAssignmentAdmin(UnfoldModelAdmin):
list_display = ['id', 'wrestler', 'training', 'is_completed', 'created_at']
list_filter = ['is_completed', 'created_at']
list_select_related = ['wrestler', 'training']
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class HomeworkConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'homework'
@@ -0,0 +1,60 @@
# Generated by Django 4.2.29 on 2026-03-19 09:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('exercises', '0001_initial'),
('wrestlers', '0001_initial'),
('clubs', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Homework',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', models.TextField(blank=True)),
('target_group', models.CharField(choices=[('kids', 'Kids'), ('youth', 'Youth'), ('adults', 'Adults'), ('all', 'All')], default='all', max_length=20)),
('due_date', models.DateField(blank=True, null=True)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('club', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='homework_assignments', to='clubs.club')),
('exercises', models.ManyToManyField(related_name='homework_assignments', to='exercises.exercise')),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='HomeworkStatus',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_completed', models.BooleanField(default=False)),
('completion_date', models.DateField(blank=True, null=True)),
('notes', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('homework', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='statuses', to='homework.homework')),
('wrestler', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='homework_statuses', to='wrestlers.wrestler')),
],
options={
'unique_together': {('homework', 'wrestler')},
},
),
migrations.AddIndex(
model_name='homework',
index=models.Index(fields=['target_group'], name='homework_ho_target__66652f_idx'),
),
migrations.AddIndex(
model_name='homework',
index=models.Index(fields=['due_date'], name='homework_ho_due_dat_5d3fcb_idx'),
),
]
@@ -0,0 +1,126 @@
# Generated by Django 4.2.29 on 2026-03-19 14:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wrestlers', '0001_initial'),
('clubs', '0001_initial'),
('exercises', '0001_initial'),
('homework', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='HomeworkAssignment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('due_date', models.DateField(blank=True, null=True)),
('notes', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='HomeworkAssignmentItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_completed', models.BooleanField(default=False)),
('completion_date', models.DateField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='HomeworkExerciseItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reps', models.IntegerField(blank=True, null=True)),
('time_minutes', models.IntegerField(blank=True, null=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['homework', 'order'],
},
),
migrations.RemoveIndex(
model_name='homework',
name='homework_ho_target__66652f_idx',
),
migrations.RemoveField(
model_name='homework',
name='exercises',
),
migrations.RemoveField(
model_name='homework',
name='target_group',
),
migrations.AlterField(
model_name='homework',
name='club',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='homework_templates', to='clubs.club'),
),
migrations.AddIndex(
model_name='homework',
index=models.Index(fields=['club'], name='homework_ho_club_id_126bad_idx'),
),
migrations.AddField(
model_name='homeworkexerciseitem',
name='exercise',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='homework_items', to='exercises.exercise'),
),
migrations.AddField(
model_name='homeworkexerciseitem',
name='homework',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercise_items', to='homework.homework'),
),
migrations.AddField(
model_name='homeworkassignmentitem',
name='assignment',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='homework.homeworkassignment'),
),
migrations.AddField(
model_name='homeworkassignmentitem',
name='exercise',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignment_items', to='exercises.exercise'),
),
migrations.AddField(
model_name='homeworkassignment',
name='club',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='homework_assignments', to='clubs.club'),
),
migrations.AddField(
model_name='homeworkassignment',
name='homework',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='homework.homework'),
),
migrations.AddField(
model_name='homeworkassignment',
name='wrestler',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='homework_assignments', to='wrestlers.wrestler'),
),
migrations.AlterUniqueTogether(
name='homeworkexerciseitem',
unique_together={('homework', 'exercise')},
),
migrations.AlterUniqueTogether(
name='homeworkassignmentitem',
unique_together={('assignment', 'exercise')},
),
migrations.AddIndex(
model_name='homeworkassignment',
index=models.Index(fields=['wrestler'], name='homework_ho_wrestle_ddebdf_idx'),
),
migrations.AddIndex(
model_name='homeworkassignment',
index=models.Index(fields=['due_date'], name='homework_ho_due_dat_12e964_idx'),
),
migrations.AlterUniqueTogether(
name='homeworkassignment',
unique_together={('homework', 'wrestler')},
),
]
@@ -0,0 +1,21 @@
# Generated by Django 4.2.29 on 2026-03-20 14:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('homework', '0002_homeworkassignment_homeworkassignmentitem_and_more'),
]
operations = [
migrations.AddIndex(
model_name='homeworkassignmentitem',
index=models.Index(fields=['assignment', 'is_completed'], name='homework_ho_assignm_791a15_idx'),
),
migrations.AddIndex(
model_name='homeworkassignmentitem',
index=models.Index(fields=['completion_date'], name='homework_ho_complet_55c380_idx'),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 4.2.29 on 2026-03-20 14:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('homework', '0003_add_assignment_item_indexes'),
]
operations = [
migrations.AlterField(
model_name='homeworkexerciseitem',
name='reps',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='homeworkexerciseitem',
name='time_minutes',
field=models.PositiveIntegerField(blank=True, null=True),
),
]
@@ -0,0 +1,67 @@
# Generated by Django 4.2.29 on 2026-03-22 12:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wrestlers', '0002_alter_wrestler_license_scan_alter_wrestler_photo'),
('clubs', '0001_initial'),
('trainings', '0005_training_club_and_more'),
('exercises', '0003_alter_exercise_media'),
('homework', '0004_alter_homeworkexerciseitem_reps_and_more'),
]
operations = [
migrations.CreateModel(
name='TrainingHomework',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('training', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='homework_assignments', to='trainings.training')),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='TrainingHomeworkExercise',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reps', models.PositiveIntegerField(blank=True, null=True)),
('time_minutes', models.PositiveIntegerField(blank=True, null=True)),
('order', models.IntegerField(default=0)),
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_homework_items', to='exercises.exercise')),
('training_homework', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercises', to='homework.traininghomework')),
],
options={
'ordering': ['training_homework', 'order'],
'unique_together': {('training_homework', 'exercise')},
},
),
migrations.CreateModel(
name='TrainingHomeworkAssignment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('notes', models.TextField(blank=True)),
('is_completed', models.BooleanField(default=False)),
('completion_date', models.DateField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('club', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='training_homework_assignments', to='clubs.club')),
('training_homework', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='homework.traininghomework')),
('wrestler', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_homework_assignments', to='wrestlers.wrestler')),
],
options={
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['wrestler'], name='homework_tr_wrestle_63f0d7_idx'), models.Index(fields=['is_completed'], name='homework_tr_is_comp_a157f2_idx'), models.Index(fields=['club'], name='homework_tr_club_id_4648ff_idx')],
'unique_together': {('training_homework', 'wrestler')},
},
),
migrations.AddIndex(
model_name='traininghomework',
index=models.Index(fields=['training'], name='homework_tr_trainin_950ce2_idx'),
),
]
@@ -0,0 +1,66 @@
# Generated by Django 4.2.29 on 2026-03-23 06:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wrestlers', '0002_alter_wrestler_license_scan_alter_wrestler_photo'),
('exercises', '0003_alter_exercise_media'),
('trainings', '0005_training_club_and_more'),
('homework', '0005_traininghomework_traininghomeworkexercise_and_more'),
]
operations = [
migrations.CreateModel(
name='TrainingHomeworkExerciseItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reps', models.PositiveIntegerField(blank=True, null=True)),
('time_minutes', models.PositiveIntegerField(blank=True, null=True)),
('order', models.IntegerField(default=0)),
('is_completed', models.BooleanField(default=False)),
],
options={
'ordering': ['assignment', 'order'],
},
),
migrations.AlterUniqueTogether(
name='traininghomeworkassignment',
unique_together=set(),
),
migrations.AddField(
model_name='traininghomeworkassignment',
name='training',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='homework_assignments', to='trainings.training'),
),
migrations.AlterField(
model_name='traininghomework',
name='training',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='homework_legacy', to='trainings.training'),
),
migrations.AlterUniqueTogether(
name='traininghomeworkassignment',
unique_together={('training', 'wrestler')},
),
migrations.AddIndex(
model_name='traininghomeworkassignment',
index=models.Index(fields=['training'], name='homework_tr_trainin_048980_idx'),
),
migrations.AddField(
model_name='traininghomeworkexerciseitem',
name='assignment',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercises', to='homework.traininghomeworkassignment'),
),
migrations.AddField(
model_name='traininghomeworkexerciseitem',
name='exercise',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_homework_exercises', to='exercises.exercise'),
),
migrations.RemoveField(
model_name='traininghomeworkassignment',
name='training_homework',
),
]
+188
View File
@@ -0,0 +1,188 @@
from django.db import models
class Homework(models.Model):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='homework_templates', null=True, blank=True)
due_date = models.DateField(null=True, blank=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['due_date']),
models.Index(fields=['club']),
]
def __str__(self):
return self.title
class HomeworkExerciseItem(models.Model):
homework = models.ForeignKey(Homework, on_delete=models.CASCADE, related_name='exercise_items')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='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 = ['homework', 'order']
unique_together = ['homework', 'exercise']
def __str__(self):
return f"{self.homework.title} - {self.exercise.name}"
class HomeworkAssignment(models.Model):
homework = models.ForeignKey(Homework, on_delete=models.CASCADE, related_name='assignments')
wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='homework_assignments')
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='homework_assignments', null=True, blank=True)
due_date = models.DateField(null=True, blank=True)
notes = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ['homework', 'wrestler']
ordering = ['-created_at']
indexes = [
models.Index(fields=['wrestler']),
models.Index(fields=['due_date']),
]
def __str__(self):
return f"{self.wrestler} - {self.homework.title}"
def clean(self):
from wrestlers.models import Wrestler
if self.wrestler and self.homework and self.wrestler.club_id != self.homework.club_id:
from django.core.exceptions import ValidationError
raise ValidationError('Wrestler must belong to the same club as the homework')
@property
def is_completed(self):
items = self.items.all()
if not items.exists():
return False
return all(item.is_completed for item in items)
@property
def completion_date(self):
if self.is_completed:
return self.items.filter(is_completed=True).order_by('-completion_date').first().completion_date
return None
class HomeworkAssignmentItem(models.Model):
assignment = models.ForeignKey(HomeworkAssignment, on_delete=models.CASCADE, related_name='items')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='assignment_items')
is_completed = models.BooleanField(default=False)
completion_date = models.DateField(null=True, blank=True)
class Meta:
unique_together = ['assignment', 'exercise']
indexes = [
models.Index(fields=['assignment', 'is_completed']),
models.Index(fields=['completion_date']),
]
def __str__(self):
status = "" if self.is_completed else ""
return f"{self.assignment} - {self.exercise.name} {status}"
class HomeworkStatus(models.Model):
homework = models.ForeignKey(Homework, on_delete=models.CASCADE, related_name='statuses')
wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='homework_statuses')
is_completed = models.BooleanField(default=False)
completion_date = models.DateField(null=True, blank=True)
notes = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ['homework', 'wrestler']
def __str__(self):
status = "" if self.is_completed else ""
return f"{self.wrestler} - {self.homework.title} {status}"
# NEUES SYSTEM: Training-basierte Homework
# Jeder Wrestler bekommt individuelle Übungen zugewiesen
class TrainingHomeworkAssignment(models.Model):
"""A homework assignment for a specific wrestler in a specific training"""
training = models.ForeignKey('trainings.Training', on_delete=models.CASCADE, related_name='homework_assignments', default=1)
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', 'wrestler']
ordering = ['-created_at']
indexes = [
models.Index(fields=['training']),
models.Index(fields=['wrestler']),
models.Index(fields=['is_completed']),
models.Index(fields=['club']),
]
def __str__(self):
return f"{self.wrestler} - Training {self.training_id}"
class TrainingHomeworkExerciseItem(models.Model):
"""Individual exercises assigned to a specific wrestler (NOT shared)"""
assignment = models.ForeignKey(TrainingHomeworkAssignment, on_delete=models.CASCADE, related_name='exercises')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='training_homework_exercises')
reps = models.PositiveIntegerField(null=True, blank=True)
time_minutes = models.PositiveIntegerField(null=True, blank=True)
order = models.IntegerField(default=0)
is_completed = models.BooleanField(default=False)
class Meta:
ordering = ['assignment', 'order']
def __str__(self):
return f"{self.assignment} - {self.exercise.name}"
# ALTES SYSTEM (für Rückwärtskompatibilität - wird nicht mehr verwendet)
class TrainingHomework(models.Model):
"""DEPRECATED: Each wrestler now has individual assignments"""
training = models.ForeignKey('trainings.Training', on_delete=models.CASCADE, related_name='homework_legacy')
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):
"""DEPRECATED: Exercises are now per-assignment"""
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}"
+190
View File
@@ -0,0 +1,190 @@
from rest_framework import serializers
from .models import (
Homework, HomeworkExerciseItem, HomeworkAssignment,
HomeworkAssignmentItem, HomeworkStatus,
TrainingHomeworkAssignment, TrainingHomeworkExerciseItem
)
class HomeworkExerciseItemSerializer(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 = HomeworkExerciseItem
fields = ['id', 'exercise', 'exercise_name', 'exercise_category', 'reps', 'time_minutes', 'order']
class HomeworkSerializer(serializers.ModelSerializer):
exercise_items = HomeworkExerciseItemSerializer(many=True, read_only=True)
club_name = serializers.CharField(source='club.name', read_only=True)
exercise_count = serializers.IntegerField(source='exercise_items.count', read_only=True)
class Meta:
model = Homework
fields = ['id', 'title', 'description', 'club', 'club_name', 'due_date', 'is_active',
'exercise_items', 'exercise_count', 'created_at', 'updated_at']
class HomeworkDetailSerializer(HomeworkSerializer):
exercise_items = HomeworkExerciseItemSerializer(many=True, read_only=True)
class Meta(HomeworkSerializer.Meta):
fields = HomeworkSerializer.Meta.fields
class HomeworkAssignmentItemSerializer(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 = HomeworkAssignmentItem
fields = ['id', 'exercise', 'exercise_name', 'exercise_category', 'is_completed', 'completion_date']
class HomeworkAssignmentSerializer(serializers.ModelSerializer):
homework_title = serializers.CharField(source='homework.title', read_only=True)
club_name = serializers.CharField(source='club.name', read_only=True)
wrestler_name = serializers.SerializerMethodField()
completed_items = serializers.SerializerMethodField()
total_items = serializers.SerializerMethodField()
items = HomeworkAssignmentItemSerializer(many=True, read_only=True)
class Meta:
model = HomeworkAssignment
fields = ['id', 'homework', 'homework_title', 'wrestler', 'wrestler_name', 'club', 'club_name',
'due_date', 'notes', 'is_completed', 'completion_date', 'completed_items',
'total_items', 'items', 'created_at']
def get_wrestler_name(self, obj):
return f"{obj.wrestler.first_name} {obj.wrestler.last_name}"
def get_completed_items(self, obj):
return obj.items.filter(is_completed=True).count()
def get_total_items(self, obj):
return obj.items.count()
class HomeworkAssignmentListSerializer(serializers.ModelSerializer):
homework_title = serializers.CharField(source='homework.title', read_only=True)
club_name = serializers.CharField(source='club.name', read_only=True)
wrestler_name = serializers.SerializerMethodField()
class Meta:
model = HomeworkAssignment
fields = ['id', 'homework', 'homework_title', 'wrestler', 'wrestler_name', 'club', 'club_name',
'due_date', 'notes', 'is_completed', 'completion_date', 'created_at']
def get_wrestler_name(self, obj):
return f"{obj.wrestler.first_name} {obj.wrestler.last_name}"
class AssignHomeworkSerializer(serializers.Serializer):
homework = serializers.IntegerField()
wrestlers = serializers.ListField(child=serializers.IntegerField())
due_date = serializers.DateField(required=False, allow_null=True)
notes = serializers.CharField(required=False, allow_blank=True, default='')
class CompleteItemSerializer(serializers.Serializer):
item_id = serializers.IntegerField()
class HomeworkStatusSerializer(serializers.ModelSerializer):
wrestler_name = serializers.CharField(source='wrestler.__str__', read_only=True)
class Meta:
model = HomeworkStatus
fields = '__all__'
# NEUES SYSTEM: Training-basierte Homework
class TrainingHomeworkExerciseItemSerializer(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 = TrainingHomeworkExerciseItem
fields = ['id', 'exercise', 'exercise_name', 'exercise_category', 'reps', 'time_minutes', 'order', 'is_completed']
class TrainingHomeworkAssignmentSerializer(serializers.ModelSerializer):
exercises = serializers.SerializerMethodField()
training_date = serializers.DateField(source='training.date', read_only=True)
training_group = serializers.CharField(source='training.group', read_only=True)
wrestler_name = serializers.SerializerMethodField()
wrestler_group = serializers.CharField(source='wrestler.group', read_only=True)
class Meta:
model = TrainingHomeworkAssignment
fields = ['id', 'training', 'training_date', 'training_group', 'wrestler', 'wrestler_name',
'wrestler_group', 'notes', 'is_completed', 'completion_date', 'exercises', 'created_at']
def get_wrestler_name(self, obj):
return f"{obj.wrestler.first_name} {obj.wrestler.last_name}"
def get_exercises(self, obj):
exercises = obj.exercises.all().order_by('order')
return TrainingHomeworkExerciseItemSerializer(exercises, many=True).data
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 utils.permissions 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', '')
try:
training_obj = Training.objects.get(id=training_id)
except Training.DoesNotExist:
raise serializers.ValidationError({'training': f'Training with id {training_id} does not exist'})
try:
wrestler_obj = Wrestler.objects.get(id=wrestler_id)
except Wrestler.DoesNotExist:
raise serializers.ValidationError({'wrestler': f'Wrestler with id {wrestler_id} does not exist'})
# Create assignment
assignment = TrainingHomeworkAssignment.objects.create(
training=training_obj,
wrestler=wrestler_obj,
club=club,
notes=notes
)
# Create individual exercises for this assignment
for i, ex in enumerate(exercises_data):
try:
exercise_obj = Exercise.objects.get(id=ex['exercise'])
TrainingHomeworkExerciseItem.objects.create(
assignment=assignment,
exercise=exercise_obj,
reps=ex.get('reps'),
time_minutes=ex.get('time_minutes'),
order=i
)
except Exercise.DoesNotExist:
pass
return assignment
def to_representation(self, instance):
return TrainingHomeworkAssignmentSerializer(instance, context=self.context).data
+237
View File
@@ -0,0 +1,237 @@
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from django.utils import timezone
from django.db import transaction
from utils.permissions import ClubLevelPermission, ClubFilterBackend
from .models import (
Homework, HomeworkExerciseItem, HomeworkAssignment,
HomeworkAssignmentItem, HomeworkStatus,
TrainingHomeworkAssignment, TrainingHomeworkExerciseItem
)
from .serializers import (
HomeworkSerializer, HomeworkDetailSerializer, HomeworkExerciseItemSerializer,
HomeworkAssignmentSerializer, HomeworkAssignmentListSerializer,
AssignHomeworkSerializer, CompleteItemSerializer, HomeworkStatusSerializer,
TrainingHomeworkAssignmentSerializer, TrainingHomeworkAssignmentCreateSerializer
)
from wrestleDesk.pagination import StandardResultsSetPagination
class HomeworkViewSet(viewsets.ModelViewSet):
queryset = Homework.objects.prefetch_related('exercise_items', 'exercise_items__exercise').all()
serializer_class = HomeworkSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['club', 'is_active']
search_fields = ['title', 'description']
ordering_fields = ['created_at', 'due_date']
def get_serializer_class(self):
if self.action == 'retrieve':
return HomeworkDetailSerializer
return HomeworkSerializer
class HomeworkExerciseItemViewSet(viewsets.ModelViewSet):
queryset = HomeworkExerciseItem.objects.select_related('homework', 'exercise').all()
serializer_class = HomeworkExerciseItemSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_fields = ['homework']
class HomeworkAssignmentViewSet(viewsets.ModelViewSet):
queryset = HomeworkAssignment.objects.select_related('homework', 'wrestler', 'club').prefetch_related('items').all()
serializer_class = HomeworkAssignmentSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['homework', 'wrestler', 'club', 'is_completed']
search_fields = ['wrestler__first_name', 'wrestler__last_name']
ordering_fields = ['created_at', 'due_date']
def get_serializer_class(self):
if self.action == 'list':
return HomeworkAssignmentListSerializer
return HomeworkAssignmentSerializer
@action(detail=True, methods=['post'])
@transaction.atomic
def complete_item(self, request, pk=None):
serializer = CompleteItemSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
assignment = self.get_object()
item_id = serializer.validated_data['item_id']
try:
item = assignment.items.get(id=item_id)
except HomeworkAssignmentItem.DoesNotExist:
return Response(
{'detail': 'Item not found'},
status=status.HTTP_404_NOT_FOUND
)
item.is_completed = True
item.completion_date = timezone.now()
item.save()
return Response({'status': 'item completed'})
@action(detail=False, methods=['post'])
@transaction.atomic
def assign(self, request):
serializer = AssignHomeworkSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
homework_id = serializer.validated_data['homework']
wrestler_ids = serializer.validated_data['wrestlers']
due_date = serializer.validated_data.get('due_date')
notes = serializer.validated_data.get('notes', '')
try:
homework = Homework.objects.get(id=homework_id)
except Homework.DoesNotExist:
return Response({'detail': 'Homework not found'}, status=status.HTTP_404_NOT_FOUND)
created_assignments = []
errors = []
for wrestler_id in wrestler_ids:
try:
existing = HomeworkAssignment.objects.filter(
homework=homework,
wrestler_id=wrestler_id
).exists()
if existing:
errors.append(f"Wrestler {wrestler_id} hat bereits diese Hausaufgabe")
continue
assignment = HomeworkAssignment.objects.create(
homework=homework,
wrestler_id=wrestler_id,
club=homework.club,
due_date=due_date,
notes=notes
)
# Copy exercises from homework to assignment
for exercise_item in homework.exercise_items.all():
HomeworkAssignmentItem.objects.create(
assignment=assignment,
exercise=exercise_item.exercise
)
created_assignments.append(assignment.id)
except Exception as e:
errors.append(f"Wrestler {wrestler_id}: {str(e)}")
return Response({
'created': created_assignments,
'errors': errors
})
class HomeworkStatusViewSet(viewsets.ModelViewSet):
queryset = HomeworkStatus.objects.select_related('homework', 'wrestler').all()
serializer_class = HomeworkStatusSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['homework', 'wrestler', 'is_completed']
ordering_fields = ['created_at', 'completion_date']
# NEUES SYSTEM: Training-basierte Homework
class TrainingHomeworkAssignmentViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
serializer_class = TrainingHomeworkAssignmentSerializer
filterset_fields = ['is_completed', 'training', 'wrestler']
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):
queryset = TrainingHomeworkAssignment.objects.select_related(
'training', 'wrestler'
).prefetch_related(
'exercises', 'exercises__exercise'
).all()
# Filter by training ID if provided
training_id = self.request.query_params.get('training')
if training_id:
queryset = queryset.filter(training_id=training_id)
return queryset
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()
# Also mark all exercises as completed
assignment.exercises.update(is_completed=True)
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()
# Also mark all exercises as not completed
assignment.exercises.update(is_completed=False)
serializer = self.get_serializer(assignment)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def complete_exercise(self, request, pk=None):
assignment = self.get_object()
exercise_id = request.data.get('exercise_id')
if not exercise_id:
return Response({'detail': 'exercise_id is required'}, status=status.HTTP_400_BAD_REQUEST)
try:
item = assignment.exercises.get(id=exercise_id)
item.is_completed = True
item.save()
return Response({'status': 'exercise completed'})
except TrainingHomeworkExerciseItem.DoesNotExist:
return Response({'detail': 'Exercise not found'}, status=status.HTTP_404_NOT_FOUND)
@action(detail=True, methods=['post'])
def uncomplete_exercise(self, request, pk=None):
assignment = self.get_object()
exercise_id = request.data.get('exercise_id')
if not exercise_id:
return Response({'detail': 'exercise_id is required'}, status=status.HTTP_400_BAD_REQUEST)
try:
item = assignment.exercises.get(id=exercise_id)
item.is_completed = False
item.save()
return Response({'status': 'exercise uncompleted'})
except TrainingHomeworkExerciseItem.DoesNotExist:
return Response({'detail': 'Exercise not found'}, status=status.HTTP_404_NOT_FOUND)