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
+22
View File
@@ -0,0 +1,22 @@
import unfold
from unfold.admin import ModelAdmin as UnfoldModelAdmin
from django.contrib import admin
from .models import Training, Attendance
@admin.register(Training)
class TrainingAdmin(UnfoldModelAdmin):
list_display = ['date', 'start_time', 'group', 'location', 'is_completed']
list_filter = ['group', 'is_completed', 'date']
search_fields = ['notes']
readonly_fields = ['created_at', 'updated_at']
raw_id_fields = ['location', 'template']
filter_horizontal = ['trainers']
date_hierarchy = 'date'
@admin.register(Attendance)
class AttendanceAdmin(UnfoldModelAdmin):
list_display = ['training', 'wrestler', 'created_at']
list_filter = ['training']
raw_id_fields = ['training', 'wrestler']
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class TrainingsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'trainings'
@@ -0,0 +1,61 @@
# 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 = [
('trainers', '0001_initial'),
('wrestlers', '0001_initial'),
('locations', '0001_initial'),
('templates', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Training',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('start_time', models.TimeField()),
('end_time', models.TimeField()),
('group', models.CharField(choices=[('kids', 'Kids'), ('youth', 'Youth'), ('adults', 'Adults'), ('all', 'All')], default='all', max_length=20)),
('notes', models.TextField(blank=True)),
('is_completed', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='trainings', to='locations.location')),
('template', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='trainings', to='templates.trainingtemplate')),
('trainers', models.ManyToManyField(blank=True, related_name='trainings', to='trainers.trainer')),
],
options={
'ordering': ['-date', '-start_time'],
},
),
migrations.CreateModel(
name='Attendance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_present', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('training', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attendances', to='trainings.training')),
('wrestler', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attendances', to='wrestlers.wrestler')),
],
),
migrations.AddIndex(
model_name='training',
index=models.Index(fields=['date'], name='trainings_t_date_81aa6e_idx'),
),
migrations.AddIndex(
model_name='training',
index=models.Index(fields=['group'], name='trainings_t_group_290df4_idx'),
),
migrations.AlterUniqueTogether(
name='attendance',
unique_together={('training', 'wrestler')},
),
]
@@ -0,0 +1,30 @@
# Generated by Django 4.2.29 on 2026-03-19 19:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('exercises', '0001_initial'),
('trainings', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='TrainingExercise',
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)),
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_exercises', to='exercises.exercise')),
('training', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_exercises', to='trainings.training')),
],
options={
'ordering': ['order', 'id'],
'unique_together': {('training', 'exercise')},
},
),
]
@@ -0,0 +1,17 @@
# Generated by Django 4.2.29 on 2026-03-20 07:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('trainings', '0002_trainingexercise'),
]
operations = [
migrations.RemoveField(
model_name='attendance',
name='is_present',
),
]
@@ -0,0 +1,23 @@
# Generated by Django 4.2.29 on 2026-03-20 14:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('trainings', '0003_remove_is_present'),
]
operations = [
migrations.AlterField(
model_name='trainingexercise',
name='reps',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='trainingexercise',
name='time_minutes',
field=models.PositiveIntegerField(blank=True, null=True),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 4.2.29 on 2026-03-20 14:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('clubs', '0001_initial'),
('trainings', '0004_alter_trainingexercise_reps_and_more'),
]
operations = [
migrations.AddField(
model_name='training',
name='club',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='trainings', to='clubs.club'),
),
migrations.AddIndex(
model_name='training',
index=models.Index(fields=['club'], name='trainings_t_club_id_eb404f_idx'),
),
]
+68
View File
@@ -0,0 +1,68 @@
from django.db import models
from django.core.exceptions import ValidationError
class Training(models.Model):
date = models.DateField()
start_time = models.TimeField()
end_time = models.TimeField()
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='trainings', null=True, blank=True)
location = models.ForeignKey('locations.Location', on_delete=models.SET_NULL, null=True, blank=True, related_name='trainings')
trainers = models.ManyToManyField('trainers.Trainer', related_name='trainings', blank=True)
template = models.ForeignKey('templates.TrainingTemplate', on_delete=models.SET_NULL, null=True, blank=True, related_name='trainings')
group = models.CharField(max_length=20, choices=[
('kids', 'Kids'),
('youth', 'Youth'),
('adults', 'Adults'),
('all', 'All'),
], default='all')
notes = models.TextField(blank=True)
is_completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-date', '-start_time']
indexes = [
models.Index(fields=['date']),
models.Index(fields=['group']),
models.Index(fields=['club']),
]
def __str__(self):
return f"{self.date} - {self.get_group_display()}"
def clean(self):
if self.start_time and self.end_time and self.end_time <= self.start_time:
raise ValidationError({'end_time': 'End time must be greater than start time.'})
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
class Attendance(models.Model):
training = models.ForeignKey(Training, on_delete=models.CASCADE, related_name='attendances')
wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='attendances')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['training', 'wrestler']
def __str__(self):
return f"{self.wrestler} - {self.training.date}"
class TrainingExercise(models.Model):
training = models.ForeignKey(Training, on_delete=models.CASCADE, related_name='training_exercises')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='training_exercises')
reps = models.PositiveIntegerField(null=True, blank=True)
time_minutes = models.PositiveIntegerField(null=True, blank=True)
order = models.IntegerField(default=0)
class Meta:
ordering = ['order', 'id']
unique_together = ['training', 'exercise']
def __str__(self):
return f"{self.training} - {self.exercise.name}"
+75
View File
@@ -0,0 +1,75 @@
from rest_framework import serializers
from .models import Training, Attendance, TrainingExercise
from utils.permissions import get_user_club
class AttendanceSerializer(serializers.ModelSerializer):
wrestler_name = serializers.SerializerMethodField()
wrestler_first_name = serializers.SerializerMethodField()
wrestler_last_name = serializers.SerializerMethodField()
wrestler_group = serializers.SerializerMethodField()
wrestler_club_name = serializers.SerializerMethodField()
class Meta:
model = Attendance
fields = ['id', 'training', 'wrestler', 'wrestler_name', 'wrestler_first_name', 'wrestler_last_name', 'wrestler_group', 'wrestler_club_name', 'created_at']
def get_wrestler_name(self, obj):
return str(obj.wrestler)
def get_wrestler_first_name(self, obj):
return obj.wrestler.first_name if hasattr(obj.wrestler, 'first_name') else None
def get_wrestler_last_name(self, obj):
return obj.wrestler.last_name if hasattr(obj.wrestler, 'last_name') else None
def get_wrestler_group(self, obj):
return obj.wrestler.group if hasattr(obj.wrestler, 'group') else None
def get_wrestler_club_name(self, obj):
return obj.wrestler.club.name if hasattr(obj.wrestler, 'club') and obj.wrestler.club else None
class TrainingExerciseSerializer(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 = TrainingExercise
fields = ['id', 'training', 'exercise', 'exercise_name', 'exercise_category', 'reps', 'time_minutes', 'order']
class TrainingSerializer(serializers.ModelSerializer):
location_name = serializers.CharField(source='location.name', read_only=True)
trainer_names = serializers.SerializerMethodField()
attendance_count = serializers.SerializerMethodField()
class Meta:
model = Training
fields = '__all__'
read_only_fields = ['created_at', 'updated_at']
def get_trainer_names(self, obj):
return [t.__str__() for t in obj.trainers.all()]
def get_attendance_count(self, obj):
return obj.attendances.count()
def create(self, validated_data):
request = self.context.get('request')
if request and hasattr(request.user, 'profile') and request.user.profile.club:
validated_data['club'] = request.user.profile.club
return super().create(validated_data)
class TrainingDetailSerializer(TrainingSerializer):
attendances = AttendanceSerializer(many=True, read_only=True)
exercise_count = serializers.SerializerMethodField()
class Meta(TrainingSerializer.Meta):
fields = ['id', 'date', 'start_time', 'end_time', 'club', 'location', 'location_name',
'trainers', 'trainer_names', 'template', 'group', 'notes', 'is_completed',
'created_at', 'updated_at', 'attendances', 'attendance_count', 'exercise_count']
def get_exercise_count(self, obj):
return obj.training_exercises.count()
+74
View File
@@ -0,0 +1,74 @@
from rest_framework import viewsets, filters
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from django_filters.rest_framework import DjangoFilterBackend
from .models import Training, Attendance, TrainingExercise
from .serializers import TrainingSerializer, TrainingDetailSerializer, AttendanceSerializer, TrainingExerciseSerializer
from wrestleDesk.pagination import StandardResultsSetPagination
class TrainingExerciseViewSet(viewsets.ModelViewSet):
queryset = TrainingExercise.objects.select_related('training', 'exercise').all()
serializer_class = TrainingExerciseSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['training']
ordering_fields = ['order', 'id']
class TrainingViewSet(viewsets.ModelViewSet):
queryset = Training.objects.select_related('location').prefetch_related('trainers', 'attendances').all()
serializer_class = TrainingSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['group', 'is_completed', 'date', 'location']
search_fields = ['notes']
ordering_fields = ['date', 'created_at']
def get_serializer_class(self):
if self.action == 'retrieve':
return TrainingDetailSerializer
return TrainingSerializer
def get_queryset(self):
queryset = super().get_queryset()
date_from = self.request.query_params.get('date_from')
date_to = self.request.query_params.get('date_to')
if date_from:
queryset = queryset.filter(date__gte=date_from)
if date_to:
queryset = queryset.filter(date__lte=date_to)
return queryset
class AttendanceViewSet(viewsets.ModelViewSet):
queryset = Attendance.objects.select_related('training', 'wrestler').all()
serializer_class = AttendanceSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['training', 'wrestler']
ordering_fields = ['created_at']
def list(self, request, *args, **kwargs):
training_id = self.request.query_params.get('training')
if not training_id:
return Response(
{'error': 'training query parameter is required'},
status=status.HTTP_400_BAD_REQUEST
)
return super().list(request, *args, **kwargs)
def get_queryset(self):
queryset = super().get_queryset()
if self.action == 'list':
training_id = self.request.query_params.get('training')
if training_id:
queryset = queryset.filter(training_id=training_id)
return queryset