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
+32
View File
@@ -0,0 +1,32 @@
import unfold
from unfold.admin import ModelAdmin as UnfoldModelAdmin
from django.contrib import admin
from .models import Wrestler
@admin.register(Wrestler)
class WrestlerAdmin(UnfoldModelAdmin):
list_display = ['first_name', 'last_name', 'club', 'group', 'weight_category', 'is_active']
list_filter = ['group', 'is_active', 'club', 'gender']
search_fields = ['first_name', 'last_name', 'license_number']
readonly_fields = ['created_at', 'updated_at', 'calculate_age']
raw_id_fields = ['club']
fieldsets = (
('Personal Info', {
'fields': ('first_name', 'last_name', 'club', 'group', 'gender', 'date_of_birth')
}),
('Wrestling Info', {
'fields': ('weight_category', 'weight_kg')
}),
('License & Documents', {
'fields': ('license_number', 'license_expiry', 'photo', 'license_scan')
}),
('Status', {
'fields': ('is_active', 'notes')
}),
('Metadata', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class WrestlersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'wrestlers'
@@ -0,0 +1,42 @@
# 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='Wrestler',
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)),
('group', models.CharField(choices=[('kids', 'Kids'), ('youth', 'Youth'), ('adults', 'Adults')], max_length=20)),
('date_of_birth', models.DateField(blank=True, null=True)),
('gender', models.CharField(choices=[('m', 'Male'), ('f', 'Female')], default='m', max_length=10)),
('weight_category', models.CharField(blank=True, max_length=50)),
('weight_kg', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
('license_number', models.CharField(blank=True, max_length=100)),
('license_expiry', models.DateField(blank=True, null=True)),
('photo', models.ImageField(blank=True, null=True, upload_to='wrestlers/photos/')),
('license_scan', models.FileField(blank=True, null=True, upload_to='wrestlers/licenses/')),
('is_active', models.BooleanField(default=True)),
('notes', models.TextField(blank=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='wrestlers', to='clubs.club')),
],
options={
'ordering': ['last_name', 'first_name'],
'indexes': [models.Index(fields=['club', 'group'], name='wrestlers_w_club_id_bb7ef9_idx'), models.Index(fields=['is_active'], name='wrestlers_w_is_acti_985f60_idx')],
},
),
]
@@ -0,0 +1,24 @@
# Generated by Django 4.2.29 on 2026-03-22 12:17
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wrestlers', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='wrestler',
name='license_scan',
field=models.FileField(blank=True, null=True, upload_to='wrestlers/licenses/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'pdf', 'webp'])]),
),
migrations.AlterField(
model_name='wrestler',
name='photo',
field=models.ImageField(blank=True, null=True, upload_to='wrestlers/photos/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'webp'])]),
),
]
+58
View File
@@ -0,0 +1,58 @@
from django.db import models
from django.core.validators import FileExtensionValidator
class Wrestler(models.Model):
GROUP_CHOICES = [
('kids', 'Kids'),
('youth', 'Youth'),
('adults', 'Adults'),
]
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='wrestlers')
group = models.CharField(max_length=20, choices=GROUP_CHOICES)
date_of_birth = models.DateField(null=True, blank=True)
gender = models.CharField(max_length=10, choices=[('m', 'Male'), ('f', 'Female')], default='m')
weight_category = models.CharField(max_length=50, blank=True)
weight_kg = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
license_number = models.CharField(max_length=100, blank=True)
license_expiry = models.DateField(null=True, blank=True)
photo = models.ImageField(
upload_to='wrestlers/photos/',
null=True, blank=True,
validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'webp'])]
)
license_scan = models.FileField(
upload_to='wrestlers/licenses/',
null=True, blank=True,
validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'pdf', 'webp'])]
)
is_active = models.BooleanField(default=True)
notes = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['last_name', 'first_name']
indexes = [
models.Index(fields=['club', 'group']),
models.Index(fields=['is_active']),
]
def __str__(self):
return f"{self.first_name} {self.last_name}"
def calculate_age(self):
if self.date_of_birth:
from datetime import date
today = date.today()
return today.year - self.date_of_birth.year - (
(today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)
)
return None
+15
View File
@@ -0,0 +1,15 @@
from rest_framework import serializers
from .models import Wrestler
class WrestlerSerializer(serializers.ModelSerializer):
club_name = serializers.CharField(source='club.name', read_only=True)
age = serializers.SerializerMethodField()
class Meta:
model = Wrestler
fields = '__all__'
read_only_fields = ['created_at', 'updated_at', 'age']
def get_age(self, obj):
return obj.calculate_age()
+67
View File
@@ -0,0 +1,67 @@
from django.test import TestCase
from datetime import date
from clubs.models import Club
from wrestlers.models import Wrestler
class WrestlerModelTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.club = Club.objects.create(name="Test Club")
cls.wrestler = Wrestler.objects.create(
first_name="Max",
last_name="Mustermann",
club=cls.club,
group="youth"
)
def test_wrestler_creation(self):
self.assertEqual(self.wrestler.first_name, "Max")
self.assertEqual(self.wrestler.last_name, "Mustermann")
self.assertEqual(self.wrestler.group, "youth")
self.assertTrue(self.wrestler.is_active)
def test_wrestler_str(self):
self.assertEqual(str(self.wrestler), "Max Mustermann")
def test_wrestler_ordering(self):
w2 = Wrestler.objects.create(
first_name="Anna",
last_name="Schmidt",
club=self.club,
group="kids"
)
wrestlers = list(Wrestler.objects.all())
self.assertEqual(wrestlers[0].last_name, "Mustermann")
self.assertEqual(wrestlers[1].last_name, "Schmidt")
def test_calculate_age_with_birthdate(self):
self.wrestler.date_of_birth = date(2015, 1, 15)
self.wrestler.save()
age = self.wrestler.calculate_age()
self.assertIsNotNone(age)
self.assertIsInstance(age, int)
def test_calculate_age_without_birthdate(self):
self.wrestler.date_of_birth = None
self.wrestler.save()
self.assertIsNone(self.wrestler.calculate_age())
def test_wrestler_club_relationship(self):
self.assertEqual(self.wrestler.club.name, "Test Club")
self.assertEqual(self.club.wrestlers.count(), 1)
def test_wrestler_default_active(self):
new_wrestler = Wrestler.objects.create(
first_name="New",
last_name="Wrestler",
club=self.club,
group="kids"
)
self.assertTrue(new_wrestler.is_active)
def test_wrestler_group_choices(self):
self.assertEqual(self.wrestler.group, "youth")
self.wrestler.group = "adults"
self.wrestler.save()
self.assertEqual(self.wrestler.group, "adults")
+56
View File
@@ -0,0 +1,56 @@
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 Wrestler
from .serializers import WrestlerSerializer
from wrestleDesk.pagination import StandardResultsSetPagination
class WrestlerViewSet(viewsets.ModelViewSet):
queryset = Wrestler.objects.select_related('club').all()
serializer_class = WrestlerSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['group', 'is_active', 'gender']
search_fields = ['first_name', 'last_name', 'license_number']
ordering_fields = ['last_name', 'first_name', 'created_at']
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
club = self.request.query_params.get('club')
if club and club != 'all':
queryset = queryset.filter(club_id=club)
return queryset
@action(detail=False, methods=['get'])
def available_for_training(self, request):
"""
Returns ALL wrestlers without club filtering.
Used when selecting wrestlers for a training session
where wrestlers from other clubs may attend.
"""
queryset = Wrestler.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)
group = request.query_params.get('group')
if group:
queryset = queryset.filter(group=group)
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)