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