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
+13
View File
@@ -0,0 +1,13 @@
import unfold
from unfold.admin import ModelAdmin as UnfoldModelAdmin
from django.contrib import admin
from .models import Trainer
@admin.register(Trainer)
class TrainerAdmin(UnfoldModelAdmin):
list_display = ['first_name', 'last_name', 'club', 'email', 'is_active']
list_filter = ['is_active', 'club']
search_fields = ['first_name', 'last_name', 'email']
readonly_fields = ['created_at', 'updated_at']
raw_id_fields = ['club']
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class TrainersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'trainers'
@@ -0,0 +1,34 @@
# 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 = [
('clubs', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Trainer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100)),
('last_name', models.CharField(max_length=100)),
('email', models.EmailField(blank=True, max_length=254)),
('phone', models.CharField(blank=True, max_length=50)),
('photo', models.ImageField(blank=True, null=True, upload_to='trainers/photos/')),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('club', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trainers', to='clubs.club')),
],
options={
'ordering': ['last_name', 'first_name'],
},
),
]
+29
View File
@@ -0,0 +1,29 @@
from django.db import models
from django.core.validators import FileExtensionValidator
class Trainer(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='trainers')
email = models.EmailField(blank=True)
phone = models.CharField(max_length=50, blank=True)
photo = models.ImageField(
upload_to='trainers/photos/',
null=True, blank=True,
validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'webp'])]
)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['last_name', 'first_name']
def __str__(self):
return f"{self.first_name} {self.last_name}"
# Note: Name uniqueness per club is not enforced at the database level.
# If the same trainer works for multiple clubs, they would appear once per club.
# If unique names per club are required, add: unique_together = ['first_name', 'last_name', 'club']
+11
View File
@@ -0,0 +1,11 @@
from rest_framework import serializers
from .models import Trainer
class TrainerSerializer(serializers.ModelSerializer):
club_name = serializers.CharField(source='club.name', read_only=True)
class Meta:
model = Trainer
fields = '__all__'
read_only_fields = ['created_at', 'updated_at']
+43
View File
@@ -0,0 +1,43 @@
from rest_framework import viewsets, filters
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from utils.permissions import ClubFilterBackend, ClubLevelPermission
from .models import Trainer
from .serializers import TrainerSerializer
from wrestleDesk.pagination import StandardResultsSetPagination
class TrainerViewSet(viewsets.ModelViewSet):
queryset = Trainer.objects.select_related('club').all()
serializer_class = TrainerSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['club', 'is_active']
search_fields = ['first_name', 'last_name', 'email']
ordering_fields = ['last_name', 'first_name', 'created_at']
@action(detail=False, methods=['get'])
def available_for_training(self, request):
"""
Returns ALL trainers without club filtering.
Used when selecting trainers for a training session
where trainers from other clubs may attend.
"""
queryset = Trainer.objects.select_related('club').filter(is_active=True)
search = request.query_params.get('search')
if search:
queryset = queryset.filter(first_name__icontains=search) | queryset.filter(last_name__icontains=search)
queryset = queryset.order_by('last_name', 'first_name')
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)