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:
@@ -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']
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user