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,12 @@
|
||||
import unfold
|
||||
from unfold.admin import ModelAdmin as UnfoldModelAdmin
|
||||
from django.contrib import admin
|
||||
from .models import Club
|
||||
|
||||
|
||||
@admin.register(Club)
|
||||
class ClubAdmin(UnfoldModelAdmin):
|
||||
list_display = ['name', 'short_name', 'is_active', 'created_at']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['name', 'short_name']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ClubsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'clubs'
|
||||
@@ -0,0 +1,171 @@
|
||||
import random
|
||||
from datetime import date, timedelta
|
||||
from django.core.management.base import BaseCommand
|
||||
from clubs.models import Club
|
||||
from wrestlers.models import Wrestler
|
||||
from trainers.models import Trainer
|
||||
from locations.models import Location
|
||||
from exercises.models import Exercise
|
||||
from templates.models import TrainingTemplate, TemplateExercise
|
||||
from trainings.models import Training, Attendance
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Populates database with comprehensive sample data for testing'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('Creating comprehensive sample data...')
|
||||
|
||||
# Create 3 clubs
|
||||
clubs = [
|
||||
Club.objects.create(name='KSV Wiesental', short_name='KSV', is_active=True),
|
||||
Club.objects.create(name='RSV Mannheim', short_name='RSV', is_active=True),
|
||||
Club.objects.create(name='AV Germering', short_name='AVG', is_active=True),
|
||||
]
|
||||
|
||||
# 3 Trainers (one per club)
|
||||
trainers = [
|
||||
Trainer.objects.create(first_name='Max', last_name='Mueller', club=clubs[0], email='max.mueller@ksv.de', is_active=True),
|
||||
Trainer.objects.create(first_name='Anna', last_name='Schmidt', club=clubs[1], email='anna.schmidt@rsv.de', is_active=True),
|
||||
Trainer.objects.create(first_name='Tom', last_name='Bauer', club=clubs[2], email='tom.bauer@avg.de', is_active=True),
|
||||
]
|
||||
|
||||
# 30 Wrestlers (10 per club, distributed across groups)
|
||||
first_names_m = ['Felix', 'Leon', 'Paul', 'Lukas', 'Jonas', 'Max', 'Tim', 'David', 'Kevin', 'Marco', 'Stefan', 'Daniel', 'Florian', 'Tobias', 'Christian']
|
||||
first_names_f = ['Emma', 'Sophie', 'Marie', 'Laura', 'Lena', 'Anna', 'Lisa', 'Sarah', 'Julia', 'Laura', 'Nina', 'Lea', 'Laura', 'Lena', 'Laura']
|
||||
last_names = ['Mueller', 'Schmidt', 'Weber', 'Fischer', 'Klein', 'Bauer', 'Wolf', 'Schulz', 'Neumann', 'Hoffmann', 'Koch', 'Becker', 'Richter', 'Wagner', 'Weiss']
|
||||
|
||||
wrestlers = []
|
||||
for i in range(30):
|
||||
club_idx = i % 3
|
||||
group = random.choice(['kids', 'kids', 'youth', 'youth', 'youth', 'adults', 'adults'])
|
||||
gender = random.choice(['m', 'f'])
|
||||
first_name = random.choice(first_names_m if gender == 'm' else first_names_f)
|
||||
last_name = random.choice(last_names)
|
||||
year_offset = {'kids': 8, 'youth': 4, 'adults': 0}[group]
|
||||
dob = date(2010 + year_offset + random.randint(-2, 2), random.randint(1, 12), random.randint(1, 28))
|
||||
weight = random.uniform(25, 90)
|
||||
|
||||
w = Wrestler.objects.create(
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
club=clubs[club_idx],
|
||||
group=group,
|
||||
date_of_birth=dob,
|
||||
gender=gender,
|
||||
weight_kg=round(weight, 1),
|
||||
is_active=True
|
||||
)
|
||||
wrestlers.append(w)
|
||||
|
||||
# 3 Locations (one per club)
|
||||
locations = [
|
||||
Location.objects.create(name='Sporthalle Wiesental', address='Ringstrasse 15, 79541 Wiesental', is_active=True),
|
||||
Location.objects.create(name='Sportzentrum Mannheim', address='Friedrich-Ebert-Strasse 88, 68159 Mannheim', is_active=True),
|
||||
Location.objects.create(name='Turnhalle Germering', address='Augsburger Strasse 45, 82110 Germering', is_active=True),
|
||||
]
|
||||
|
||||
# 15 Exercises
|
||||
exercises_data = [
|
||||
{'name': 'Springseil', 'category': 'warmup', 'exercise_type': 'time', 'default_value': '60'},
|
||||
{'name': 'Armkreisen', 'category': 'warmup', 'exercise_type': 'time', 'default_value': '60'},
|
||||
{'name': 'Beinspätze', 'category': 'warmup', 'exercise_type': 'time', 'default_value': '60'},
|
||||
{'name': 'Liegestuetze', 'category': 'kraft', 'exercise_type': 'reps', 'default_value': '15'},
|
||||
{'name': 'Kniebeugen', 'category': 'kraft', 'exercise_type': 'reps', 'default_value': '20'},
|
||||
{'name': 'Sit-ups', 'category': 'kraft', 'exercise_type': 'reps', 'default_value': '25'},
|
||||
{'name': 'Plank', 'category': 'kraft', 'exercise_type': 'time', 'default_value': '60'},
|
||||
{'name': 'Doppelbeintechnik', 'category': 'technik', 'exercise_type': 'reps', 'default_value': '10'},
|
||||
{'name': 'Ausputzer', 'category': 'technik', 'exercise_type': 'reps', 'default_value': '8'},
|
||||
{'name': 'Beinsäge', 'category': 'technik', 'exercise_type': 'reps', 'default_value': '12'},
|
||||
{'name': 'Armheber', 'category': 'technik', 'exercise_type': 'reps', 'default_value': '10'},
|
||||
{'name': 'Tempolauf', 'category': 'ausdauer', 'exercise_type': 'time', 'default_value': '180'},
|
||||
{'name': 'Seilspringen Station', 'category': 'ausdauer', 'exercise_type': 'time', 'default_value': '120'},
|
||||
{'name': 'Fangspiel', 'category': 'spiele', 'exercise_type': 'time', 'default_value': '300'},
|
||||
{'name': 'Dehnung', 'category': 'cool_down', 'exercise_type': 'time', 'default_value': '180'},
|
||||
]
|
||||
exercises = [Exercise.objects.create(**data) for data in exercises_data]
|
||||
|
||||
# 6 Templates (2 per group)
|
||||
templates = []
|
||||
for idx, (group, group_name) in enumerate([('kids', 'Kids'), ('youth', 'Youth'), ('adults', 'Adults')]):
|
||||
for t_idx in range(2):
|
||||
t = TrainingTemplate.objects.create(
|
||||
name=f'{group_name} Training {"A" if t_idx == 0 else "B"}',
|
||||
description=f'Standard training for {group_name.lower()} group - Part {"A" if t_idx == 0 else "B"}',
|
||||
category='main',
|
||||
is_active=True
|
||||
)
|
||||
templates.append(t)
|
||||
|
||||
# Kids Training A: warmup + kraft + spiele
|
||||
for i, ex in enumerate([exercises[0], exercises[3], exercises[5], exercises[13]]):
|
||||
TemplateExercise.objects.create(template=templates[0], exercise=ex, order=i, default_value=ex.default_value)
|
||||
|
||||
# Kids Training B: warmup + technik + spiele
|
||||
for i, ex in enumerate([exercises[1], exercises[7], exercises[9], exercises[13]]):
|
||||
TemplateExercise.objects.create(template=templates[1], exercise=ex, order=i, default_value=ex.default_value)
|
||||
|
||||
# Youth Training A: warmup + technik + kraft
|
||||
for i, ex in enumerate([exercises[0], exercises[3], exercises[4], exercises[7], exercises[8]]):
|
||||
TemplateExercise.objects.create(template=templates[2], exercise=ex, order=i, default_value=ex.default_value)
|
||||
|
||||
# Youth Training B: technik + ausdauer
|
||||
for i, ex in enumerate([exercises[7], exercises[8], exercises[9], exercises[11], exercises[14]]):
|
||||
TemplateExercise.objects.create(template=templates[3], exercise=ex, order=i, default_value=ex.default_value)
|
||||
|
||||
# Adults Training A: warmup + kraft + technik
|
||||
for i, ex in enumerate([exercises[0], exercises[3], exercises[4], exercises[6], exercises[7], exercises[14]]):
|
||||
TemplateExercise.objects.create(template=templates[4], exercise=ex, order=i, default_value=ex.default_value)
|
||||
|
||||
# Adults Training B: kraft + ausdauer + technik
|
||||
for i, ex in enumerate([exercises[3], exercises[5], exercises[7], exercises[11], exercises[12], exercises[14]]):
|
||||
TemplateExercise.objects.create(template=templates[5], exercise=ex, order=i, default_value=ex.default_value)
|
||||
|
||||
# 30 Trainings spread over 3 months (past, present, future)
|
||||
today = date.today()
|
||||
start_date = today - timedelta(days=90) # 3 months ago
|
||||
|
||||
trainings = []
|
||||
group_map = {'kids': [0, 1], 'youth': [2, 3], 'adults': [4, 5]}
|
||||
|
||||
for week_offset in range(15): # ~2 trainings per week over 3 months
|
||||
for day_offset in [0, 3]: # Monday and Thursday
|
||||
training_date = start_date + timedelta(days=week_offset * 7 + day_offset)
|
||||
if training_date > today + timedelta(days=14):
|
||||
continue # Don't create trainings too far in future
|
||||
|
||||
group = random.choice(['kids', 'youth', 'adults'])
|
||||
template = random.choice(group_map[group])
|
||||
location = random.choice(locations)
|
||||
is_completed = training_date < today
|
||||
|
||||
t = Training.objects.create(
|
||||
date=training_date,
|
||||
start_time='17:00' if group == 'kids' else '18:30' if group == 'youth' else '19:30',
|
||||
end_time='18:30' if group == 'kids' else '20:00' if group == 'youth' else '21:00',
|
||||
location=location,
|
||||
group=group,
|
||||
is_completed=is_completed,
|
||||
notes=f'Training für {group}'
|
||||
)
|
||||
t.trainers.set([random.choice(trainers)])
|
||||
trainings.append(t)
|
||||
|
||||
# Create attendances for completed trainings
|
||||
for training in trainings:
|
||||
if training.is_completed:
|
||||
group_wrestlers = [w for w in wrestlers if w.group == training.group]
|
||||
num_attendees = min(len(group_wrestlers), random.randint(3, 8))
|
||||
for w in random.sample(group_wrestlers, num_attendees):
|
||||
Attendance.objects.create(training=training, wrestler=w)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'Created: {Club.objects.count()} clubs, '
|
||||
f'{Trainer.objects.count()} trainers, '
|
||||
f'{Wrestler.objects.count()} wrestlers, '
|
||||
f'{Location.objects.count()} locations, '
|
||||
f'{Exercise.objects.count()} exercises, '
|
||||
f'{TrainingTemplate.objects.count()} templates, '
|
||||
f'{Training.objects.count()} trainings, '
|
||||
f'{Attendance.objects.count()} attendances'
|
||||
))
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 4.2.29 on 2026-03-19 09:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Club',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('short_name', models.CharField(blank=True, max_length=50)),
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='clubs/logos/')),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Club(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
short_name = models.CharField(max_length=50, blank=True)
|
||||
logo = models.ImageField(upload_to='clubs/logos/', null=True, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -0,0 +1,14 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Club
|
||||
|
||||
|
||||
class ClubSerializer(serializers.ModelSerializer):
|
||||
wrestler_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Club
|
||||
fields = '__all__'
|
||||
read_only_fields = ['created_at', 'updated_at']
|
||||
|
||||
def get_wrestler_count(self, obj):
|
||||
return obj.wrestlers.filter(is_active=True).count()
|
||||
@@ -0,0 +1,30 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
from clubs.models import Club
|
||||
from auth_app.models import UserProfile
|
||||
|
||||
|
||||
class ClubAPITest(APITestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username='testuser', password='testpass123')
|
||||
self.club = Club.objects.create(name='Test Club')
|
||||
UserProfile.objects.create(user=self.user, club=self.club)
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_list_clubs(self):
|
||||
response = self.client.get('/api/v1/clubs/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_create_club(self):
|
||||
initial_count = Club.objects.count()
|
||||
data = {'name': 'Test Club API', 'short_name': 'TC'}
|
||||
response = self.client.post('/api/v1/clubs/', data)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Club.objects.count(), initial_count + 1)
|
||||
self.assertEqual(Club.objects.last().name, 'Test Club API')
|
||||
|
||||
def test_club_str(self):
|
||||
club = Club.objects.create(name="API Test Club")
|
||||
self.assertEqual(str(club), "API Test Club")
|
||||
@@ -0,0 +1,17 @@
|
||||
from rest_framework import viewsets, filters
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from .models import Club
|
||||
from .serializers import ClubSerializer
|
||||
from wrestleDesk.pagination import StandardResultsSetPagination
|
||||
|
||||
|
||||
class ClubViewSet(viewsets.ModelViewSet):
|
||||
queryset = Club.objects.all()
|
||||
serializer_class = ClubSerializer
|
||||
pagination_class = StandardResultsSetPagination
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['is_active']
|
||||
search_fields = ['name', 'short_name']
|
||||
ordering_fields = ['name', 'created_at']
|
||||
Reference in New Issue
Block a user