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
+59
View File
@@ -0,0 +1,59 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
ENV/
env/
.venv/
# Django
*.log
local_settings.py
db.sqlite3
media/
# Static files
staticfiles/
# Environment
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Testing
.coverage
htmlcov/
.pytest_cache/
# Misc
*.bak
View File
+9
View File
@@ -0,0 +1,9 @@
from django.apps import AppConfig
class AuthAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'auth_app'
def ready(self):
import auth_app.signals # noqa
@@ -0,0 +1,34 @@
# Generated by Django 4.2.29 on 2026-03-19 13:24
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserPreferences',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('preferred_locale', models.CharField(default='de', max_length=10)),
('default_view', models.CharField(default='list', max_length=10)),
('wrestlers_view', models.CharField(default='list', max_length=10)),
('wrestlers_items_per_page', models.IntegerField(default=10)),
('trainers_view', models.CharField(default='list', max_length=10)),
('trainers_items_per_page', models.IntegerField(default=10)),
('exercises_view', models.CharField(default='list', max_length=10)),
('exercises_items_per_page', models.IntegerField(default=10)),
('trainings_view', models.CharField(default='list', max_length=10)),
('trainings_items_per_page', models.IntegerField(default=10)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='preferences', to=settings.AUTH_USER_MODEL)),
],
),
]
@@ -0,0 +1,33 @@
# Generated by Django 4.2.29 on 2026-03-19 13:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth_app', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='userpreferences',
name='exercises_filters',
field=models.JSONField(blank=True, default=dict),
),
migrations.AddField(
model_name='userpreferences',
name='trainers_filters',
field=models.JSONField(blank=True, default=dict),
),
migrations.AddField(
model_name='userpreferences',
name='trainings_filters',
field=models.JSONField(blank=True, default=dict),
),
migrations.AddField(
model_name='userpreferences',
name='wrestlers_filters',
field=models.JSONField(blank=True, default=dict),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 4.2.29 on 2026-03-19 14:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth_app', '0002_userpreferences_exercises_filters_and_more'),
]
operations = [
migrations.AddField(
model_name='userpreferences',
name='homework_filters',
field=models.JSONField(blank=True, default=dict),
),
migrations.AddField(
model_name='userpreferences',
name='homework_items_per_page',
field=models.IntegerField(default=10),
),
migrations.AddField(
model_name='userpreferences',
name='homework_view',
field=models.CharField(default='list', max_length=10),
),
]
@@ -0,0 +1,25 @@
# Generated by Django 4.2.29 on 2026-03-20 14:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('clubs', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('auth_app', '0003_userpreferences_homework_filters_and_more'),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_profiles', to='clubs.club')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
]
+34
View File
@@ -0,0 +1,34 @@
from django.db import models
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
club = models.ForeignKey('clubs.Club', on_delete=models.SET_NULL, null=True, blank=True, related_name='user_profiles')
def __str__(self):
return f"{self.user.username} Profile"
class UserPreferences(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='preferences')
preferred_locale = models.CharField(max_length=10, default='de')
default_view = models.CharField(max_length=10, default='list')
wrestlers_view = models.CharField(max_length=10, default='list')
wrestlers_items_per_page = models.IntegerField(default=10)
wrestlers_filters = models.JSONField(default=dict, blank=True)
trainers_view = models.CharField(max_length=10, default='list')
trainers_items_per_page = models.IntegerField(default=10)
trainers_filters = models.JSONField(default=dict, blank=True)
exercises_view = models.CharField(max_length=10, default='list')
exercises_items_per_page = models.IntegerField(default=10)
exercises_filters = models.JSONField(default=dict, blank=True)
trainings_view = models.CharField(max_length=10, default='list')
trainings_items_per_page = models.IntegerField(default=10)
trainings_filters = models.JSONField(default=dict, blank=True)
homework_view = models.CharField(max_length=10, default='list')
homework_items_per_page = models.IntegerField(default=10)
homework_filters = models.JSONField(default=dict, blank=True)
def __str__(self):
return f"{self.user.username} Preferences"
+64
View File
@@ -0,0 +1,64 @@
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import UserPreferences
class UserSerializer(serializers.ModelSerializer):
club_id = serializers.SerializerMethodField()
club_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'club_id', 'club_name']
read_only_fields = ['id']
def get_club_id(self, obj):
if hasattr(obj, 'profile') and obj.profile and obj.profile.club:
return obj.profile.club.id
return None
def get_club_name(self, obj):
if hasattr(obj, 'profile') and obj.profile and obj.profile.club:
return obj.profile.club.name
return None
class LoginSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField(write_only=True)
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=8)
password_confirm = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['username', 'email', 'password', 'password_confirm', 'first_name', 'last_name']
def validate_email(self, value):
if User.objects.filter(email=value).exists():
raise serializers.ValidationError('A user with this email already exists')
return value
def validate(self, attrs):
if attrs['password'] != attrs['password_confirm']:
raise serializers.ValidationError({'password_confirm': 'Passwords do not match'})
return attrs
def create(self, validated_data):
validated_data.pop('password_confirm')
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data.get('email', ''),
password=validated_data['password'],
first_name=validated_data.get('first_name', ''),
last_name=validated_data.get('last_name', ''),
)
return user
class UserPreferencesSerializer(serializers.ModelSerializer):
class Meta:
model = UserPreferences
fields = '__all__'
+10
View File
@@ -0,0 +1,10 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import UserProfile
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
+98
View File
@@ -0,0 +1,98 @@
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes, throttle_classes
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth import authenticate
from .models import UserPreferences
from .serializers import LoginSerializer, RegisterSerializer, UserSerializer, UserPreferencesSerializer
class AuthRateThrottle(AnonRateThrottle):
rate = '5/minute'
@api_view(['POST'])
@permission_classes([AllowAny])
@throttle_classes([AuthRateThrottle])
def login(request):
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
user = authenticate(
username=serializer.validated_data['username'],
password=serializer.validated_data['password']
)
if user:
refresh = RefreshToken.for_user(user)
return Response({
'access': str(refresh.access_token),
'refresh': str(refresh),
'user': UserSerializer(user).data
})
return Response(
{'detail': 'Invalid credentials'},
status=status.HTTP_401_UNAUTHORIZED
)
return Response({'detail': serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([AllowAny])
@throttle_classes([AuthRateThrottle])
def register(request):
serializer = RegisterSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
refresh = RefreshToken.for_user(user)
return Response({
'access': str(refresh.access_token),
'refresh': str(refresh),
'user': UserSerializer(user).data
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([AllowAny])
@throttle_classes([AuthRateThrottle])
def refresh_token(request):
refresh_token = request.data.get('refresh')
if not refresh_token:
return Response(
{'detail': 'Refresh token required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
refresh = RefreshToken(refresh_token)
return Response({
'access': str(refresh.access_token),
})
except Exception:
return Response(
{'detail': 'Invalid refresh token'},
status=status.HTTP_401_UNAUTHORIZED
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def me(request):
return Response(UserSerializer(request.user).data)
@api_view(['GET', 'PATCH'])
@permission_classes([IsAuthenticated])
def user_preferences(request):
if request.method == 'GET':
prefs, _ = UserPreferences.objects.get_or_create(user=request.user)
serializer = UserPreferencesSerializer(prefs)
return Response(serializer.data)
elif request.method == 'PATCH':
prefs, _ = UserPreferences.objects.get_or_create(user=request.user)
serializer = UserPreferencesSerializer(prefs, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=400)
View File
+12
View File
@@ -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']
+6
View File
@@ -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'
))
+29
View File
@@ -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'],
},
),
]
+16
View File
@@ -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
+14
View File
@@ -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()
+30
View File
@@ -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")
+17
View File
@@ -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']
+12
View File
@@ -0,0 +1,12 @@
import unfold
from unfold.admin import ModelAdmin as UnfoldModelAdmin
from django.contrib import admin
from .models import Exercise
@admin.register(Exercise)
class ExerciseAdmin(UnfoldModelAdmin):
list_display = ['name', 'category', 'exercise_type', 'is_active']
list_filter = ['category', 'exercise_type', 'is_active']
search_fields = ['name', 'description']
readonly_fields = ['created_at', 'updated_at']
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ExercisesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'exercises'
@@ -0,0 +1,33 @@
# 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='Exercise',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('category', models.CharField(choices=[('warmup', 'Warm-up'), ('kraft', 'Strength'), ('technik', 'Technique'), ('ausdauer', 'Endurance'), ('spiele', 'Games'), ('cool_down', 'Cool-down')], max_length=20)),
('exercise_type', models.CharField(choices=[('reps', 'Repetitions'), ('time', 'Time-based')], default='reps', max_length=10)),
('description', models.TextField(blank=True)),
('default_value', models.CharField(blank=True, help_text='Default reps or seconds', max_length=50)),
('media', models.FileField(blank=True, null=True, upload_to='exercises/media/')),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['category', 'name'],
'indexes': [models.Index(fields=['category'], name='exercises_e_categor_eda76d_idx')],
},
),
]
@@ -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'),
('exercises', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='exercise',
name='club',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='exercises', to='clubs.club'),
),
migrations.AddIndex(
model_name='exercise',
index=models.Index(fields=['club'], name='exercises_e_club_id_06152b_idx'),
),
]
@@ -0,0 +1,19 @@
# 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 = [
('exercises', '0002_exercise_club_and_more'),
]
operations = [
migrations.AlterField(
model_name='exercise',
name='media',
field=models.FileField(blank=True, null=True, upload_to='exercises/media/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'mp4', 'webp'])]),
),
]
+43
View File
@@ -0,0 +1,43 @@
from django.db import models
from django.core.validators import FileExtensionValidator
class Exercise(models.Model):
CATEGORY_CHOICES = [
('warmup', 'Warm-up'),
('kraft', 'Strength'),
('technik', 'Technique'),
('ausdauer', 'Endurance'),
('spiele', 'Games'),
('cool_down', 'Cool-down'),
]
TYPE_CHOICES = [
('reps', 'Repetitions'),
('time', 'Time-based'),
]
name = models.CharField(max_length=200)
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
exercise_type = models.CharField(max_length=10, choices=TYPE_CHOICES, default='reps')
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='exercises', null=True, blank=True)
description = models.TextField(blank=True)
default_value = models.CharField(max_length=50, blank=True, help_text='Default reps or seconds')
media = models.FileField(
upload_to='exercises/media/',
null=True, blank=True,
validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'mp4', '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 = ['category', 'name']
indexes = [
models.Index(fields=['category']),
models.Index(fields=['club']),
]
def __str__(self):
return f"{self.name} ({self.get_category_display()})"
+25
View File
@@ -0,0 +1,25 @@
from rest_framework import serializers
from .models import Exercise
class ExerciseSerializer(serializers.ModelSerializer):
category_display = serializers.CharField(source='get_category_display', read_only=True)
class Meta:
model = Exercise
fields = '__all__'
read_only_fields = ['created_at', 'updated_at']
def validate_name(self, value):
queryset = Exercise.objects.filter(name__iexact=value)
if self.instance:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise serializers.ValidationError("Eine Übung mit diesem Namen existiert bereits.")
return value
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)
+33
View File
@@ -0,0 +1,33 @@
from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APITestCase
from rest_framework import status
from exercises.models import Exercise
class ExerciseAPITest(APITestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser2', password='testpass123')
self.client.force_authenticate(user=self.user)
def test_list_exercises(self):
response = self.client.get('/api/v1/exercises/')
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND])
def test_create_exercise(self):
data = {
'name': 'Push-ups',
'category': 'kraft',
'exercise_type': 'reps',
'default_value': '20'
}
response = self.client.post('/api/v1/exercises/', data)
self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_404_NOT_FOUND, status.HTTP_405_METHOD_NOT_ALLOWED])
def test_filter_by_category(self):
Exercise.objects.create(name='Squat', category='kraft', exercise_type='reps', default_value='10')
Exercise.objects.create(name='Run', category='ausdauer', exercise_type='time', default_value='5')
response = self.client.get('/api/v1/exercises/?category=kraft')
if response.status_code == 200:
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['name'], 'Squat')
+17
View File
@@ -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 Exercise
from .serializers import ExerciseSerializer
from wrestleDesk.pagination import StandardResultsSetPagination
class ExerciseViewSet(viewsets.ModelViewSet):
queryset = Exercise.objects.all()
serializer_class = ExerciseSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['category', 'exercise_type', 'is_active']
search_fields = ['name', 'description']
ordering_fields = ['name', 'category', 'created_at']
View File
+73
View File
@@ -0,0 +1,73 @@
import unfold
from unfold.admin import ModelAdmin as UnfoldModelAdmin
from django.contrib import admin
from .models import Homework, HomeworkExerciseItem, HomeworkAssignment, HomeworkAssignmentItem, HomeworkStatus, TrainingHomework, TrainingHomeworkExercise, TrainingHomeworkAssignment
class HomeworkExerciseItemInline(admin.TabularInline):
model = HomeworkExerciseItem
extra = 1
raw_id_fields = ['exercise']
@admin.register(Homework)
class HomeworkAdmin(UnfoldModelAdmin):
list_display = ['title', 'club', 'due_date', 'is_active', 'exercise_count']
list_filter = ['is_active', 'club']
search_fields = ['title', 'description']
readonly_fields = ['created_at', 'updated_at']
raw_id_fields = ['club']
inlines = [HomeworkExerciseItemInline]
def exercise_count(self, obj):
return obj.exercise_items.count()
exercise_count.short_description = 'Exercises'
class HomeworkAssignmentItemInline(admin.TabularInline):
model = HomeworkAssignmentItem
extra = 0
raw_id_fields = ['exercise']
readonly_fields = ['completion_date']
@admin.register(HomeworkAssignment)
class HomeworkAssignmentAdmin(UnfoldModelAdmin):
list_display = ['wrestler', 'homework', 'club', 'due_date', 'is_completed_display']
list_filter = ['club']
search_fields = ['wrestler__first_name', 'wrestler__last_name', 'homework__title']
readonly_fields = ['created_at', 'updated_at']
raw_id_fields = ['homework', 'wrestler', 'club']
inlines = [HomeworkAssignmentItemInline]
def is_completed_display(self, obj):
return obj.is_completed
is_completed_display.short_description = 'Completed'
is_completed_display.boolean = True
@admin.register(HomeworkExerciseItem)
class HomeworkExerciseItemAdmin(UnfoldModelAdmin):
list_display = ['homework', 'exercise', 'reps', 'time_minutes', 'order']
list_filter = ['homework']
raw_id_fields = ['homework', 'exercise']
@admin.register(HomeworkStatus)
class HomeworkStatusAdmin(UnfoldModelAdmin):
list_display = ['homework', 'wrestler', 'is_completed', 'completion_date']
list_filter = ['is_completed']
raw_id_fields = ['homework', 'wrestler']
@admin.register(TrainingHomework)
class TrainingHomeworkAdmin(UnfoldModelAdmin):
list_display = ['id', 'training', 'created_at']
list_select_related = ['training']
@admin.register(TrainingHomeworkAssignment)
class TrainingHomeworkAssignmentAdmin(UnfoldModelAdmin):
list_display = ['id', 'wrestler', 'training', 'is_completed', 'created_at']
list_filter = ['is_completed', 'created_at']
list_select_related = ['wrestler', 'training']
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class HomeworkConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'homework'
@@ -0,0 +1,60 @@
# 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 = [
('exercises', '0001_initial'),
('wrestlers', '0001_initial'),
('clubs', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Homework',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', models.TextField(blank=True)),
('target_group', models.CharField(choices=[('kids', 'Kids'), ('youth', 'Youth'), ('adults', 'Adults'), ('all', 'All')], default='all', max_length=20)),
('due_date', models.DateField(blank=True, null=True)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('club', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='homework_assignments', to='clubs.club')),
('exercises', models.ManyToManyField(related_name='homework_assignments', to='exercises.exercise')),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='HomeworkStatus',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_completed', models.BooleanField(default=False)),
('completion_date', models.DateField(blank=True, null=True)),
('notes', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('homework', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='statuses', to='homework.homework')),
('wrestler', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='homework_statuses', to='wrestlers.wrestler')),
],
options={
'unique_together': {('homework', 'wrestler')},
},
),
migrations.AddIndex(
model_name='homework',
index=models.Index(fields=['target_group'], name='homework_ho_target__66652f_idx'),
),
migrations.AddIndex(
model_name='homework',
index=models.Index(fields=['due_date'], name='homework_ho_due_dat_5d3fcb_idx'),
),
]
@@ -0,0 +1,126 @@
# Generated by Django 4.2.29 on 2026-03-19 14:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wrestlers', '0001_initial'),
('clubs', '0001_initial'),
('exercises', '0001_initial'),
('homework', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='HomeworkAssignment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('due_date', models.DateField(blank=True, null=True)),
('notes', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='HomeworkAssignmentItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_completed', models.BooleanField(default=False)),
('completion_date', models.DateField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='HomeworkExerciseItem',
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)),
],
options={
'ordering': ['homework', 'order'],
},
),
migrations.RemoveIndex(
model_name='homework',
name='homework_ho_target__66652f_idx',
),
migrations.RemoveField(
model_name='homework',
name='exercises',
),
migrations.RemoveField(
model_name='homework',
name='target_group',
),
migrations.AlterField(
model_name='homework',
name='club',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='homework_templates', to='clubs.club'),
),
migrations.AddIndex(
model_name='homework',
index=models.Index(fields=['club'], name='homework_ho_club_id_126bad_idx'),
),
migrations.AddField(
model_name='homeworkexerciseitem',
name='exercise',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='homework_items', to='exercises.exercise'),
),
migrations.AddField(
model_name='homeworkexerciseitem',
name='homework',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercise_items', to='homework.homework'),
),
migrations.AddField(
model_name='homeworkassignmentitem',
name='assignment',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='homework.homeworkassignment'),
),
migrations.AddField(
model_name='homeworkassignmentitem',
name='exercise',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignment_items', to='exercises.exercise'),
),
migrations.AddField(
model_name='homeworkassignment',
name='club',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='homework_assignments', to='clubs.club'),
),
migrations.AddField(
model_name='homeworkassignment',
name='homework',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='homework.homework'),
),
migrations.AddField(
model_name='homeworkassignment',
name='wrestler',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='homework_assignments', to='wrestlers.wrestler'),
),
migrations.AlterUniqueTogether(
name='homeworkexerciseitem',
unique_together={('homework', 'exercise')},
),
migrations.AlterUniqueTogether(
name='homeworkassignmentitem',
unique_together={('assignment', 'exercise')},
),
migrations.AddIndex(
model_name='homeworkassignment',
index=models.Index(fields=['wrestler'], name='homework_ho_wrestle_ddebdf_idx'),
),
migrations.AddIndex(
model_name='homeworkassignment',
index=models.Index(fields=['due_date'], name='homework_ho_due_dat_12e964_idx'),
),
migrations.AlterUniqueTogether(
name='homeworkassignment',
unique_together={('homework', 'wrestler')},
),
]
@@ -0,0 +1,21 @@
# Generated by Django 4.2.29 on 2026-03-20 14:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('homework', '0002_homeworkassignment_homeworkassignmentitem_and_more'),
]
operations = [
migrations.AddIndex(
model_name='homeworkassignmentitem',
index=models.Index(fields=['assignment', 'is_completed'], name='homework_ho_assignm_791a15_idx'),
),
migrations.AddIndex(
model_name='homeworkassignmentitem',
index=models.Index(fields=['completion_date'], name='homework_ho_complet_55c380_idx'),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 4.2.29 on 2026-03-20 14:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('homework', '0003_add_assignment_item_indexes'),
]
operations = [
migrations.AlterField(
model_name='homeworkexerciseitem',
name='reps',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='homeworkexerciseitem',
name='time_minutes',
field=models.PositiveIntegerField(blank=True, null=True),
),
]
@@ -0,0 +1,67 @@
# Generated by Django 4.2.29 on 2026-03-22 12:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wrestlers', '0002_alter_wrestler_license_scan_alter_wrestler_photo'),
('clubs', '0001_initial'),
('trainings', '0005_training_club_and_more'),
('exercises', '0003_alter_exercise_media'),
('homework', '0004_alter_homeworkexerciseitem_reps_and_more'),
]
operations = [
migrations.CreateModel(
name='TrainingHomework',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('training', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='homework_assignments', to='trainings.training')),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='TrainingHomeworkExercise',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reps', models.PositiveIntegerField(blank=True, null=True)),
('time_minutes', models.PositiveIntegerField(blank=True, null=True)),
('order', models.IntegerField(default=0)),
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_homework_items', to='exercises.exercise')),
('training_homework', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercises', to='homework.traininghomework')),
],
options={
'ordering': ['training_homework', 'order'],
'unique_together': {('training_homework', 'exercise')},
},
),
migrations.CreateModel(
name='TrainingHomeworkAssignment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('notes', models.TextField(blank=True)),
('is_completed', models.BooleanField(default=False)),
('completion_date', models.DateField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('club', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='training_homework_assignments', to='clubs.club')),
('training_homework', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='homework.traininghomework')),
('wrestler', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_homework_assignments', to='wrestlers.wrestler')),
],
options={
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['wrestler'], name='homework_tr_wrestle_63f0d7_idx'), models.Index(fields=['is_completed'], name='homework_tr_is_comp_a157f2_idx'), models.Index(fields=['club'], name='homework_tr_club_id_4648ff_idx')],
'unique_together': {('training_homework', 'wrestler')},
},
),
migrations.AddIndex(
model_name='traininghomework',
index=models.Index(fields=['training'], name='homework_tr_trainin_950ce2_idx'),
),
]
@@ -0,0 +1,66 @@
# Generated by Django 4.2.29 on 2026-03-23 06:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wrestlers', '0002_alter_wrestler_license_scan_alter_wrestler_photo'),
('exercises', '0003_alter_exercise_media'),
('trainings', '0005_training_club_and_more'),
('homework', '0005_traininghomework_traininghomeworkexercise_and_more'),
]
operations = [
migrations.CreateModel(
name='TrainingHomeworkExerciseItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reps', models.PositiveIntegerField(blank=True, null=True)),
('time_minutes', models.PositiveIntegerField(blank=True, null=True)),
('order', models.IntegerField(default=0)),
('is_completed', models.BooleanField(default=False)),
],
options={
'ordering': ['assignment', 'order'],
},
),
migrations.AlterUniqueTogether(
name='traininghomeworkassignment',
unique_together=set(),
),
migrations.AddField(
model_name='traininghomeworkassignment',
name='training',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='homework_assignments', to='trainings.training'),
),
migrations.AlterField(
model_name='traininghomework',
name='training',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='homework_legacy', to='trainings.training'),
),
migrations.AlterUniqueTogether(
name='traininghomeworkassignment',
unique_together={('training', 'wrestler')},
),
migrations.AddIndex(
model_name='traininghomeworkassignment',
index=models.Index(fields=['training'], name='homework_tr_trainin_048980_idx'),
),
migrations.AddField(
model_name='traininghomeworkexerciseitem',
name='assignment',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercises', to='homework.traininghomeworkassignment'),
),
migrations.AddField(
model_name='traininghomeworkexerciseitem',
name='exercise',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_homework_exercises', to='exercises.exercise'),
),
migrations.RemoveField(
model_name='traininghomeworkassignment',
name='training_homework',
),
]
+188
View File
@@ -0,0 +1,188 @@
from django.db import models
class Homework(models.Model):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='homework_templates', null=True, blank=True)
due_date = models.DateField(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 = ['-created_at']
indexes = [
models.Index(fields=['due_date']),
models.Index(fields=['club']),
]
def __str__(self):
return self.title
class HomeworkExerciseItem(models.Model):
homework = models.ForeignKey(Homework, on_delete=models.CASCADE, related_name='exercise_items')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='homework_items')
reps = models.PositiveIntegerField(null=True, blank=True)
time_minutes = models.PositiveIntegerField(null=True, blank=True)
order = models.IntegerField(default=0)
class Meta:
ordering = ['homework', 'order']
unique_together = ['homework', 'exercise']
def __str__(self):
return f"{self.homework.title} - {self.exercise.name}"
class HomeworkAssignment(models.Model):
homework = models.ForeignKey(Homework, on_delete=models.CASCADE, related_name='assignments')
wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='homework_assignments')
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='homework_assignments', null=True, blank=True)
due_date = models.DateField(null=True, blank=True)
notes = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ['homework', 'wrestler']
ordering = ['-created_at']
indexes = [
models.Index(fields=['wrestler']),
models.Index(fields=['due_date']),
]
def __str__(self):
return f"{self.wrestler} - {self.homework.title}"
def clean(self):
from wrestlers.models import Wrestler
if self.wrestler and self.homework and self.wrestler.club_id != self.homework.club_id:
from django.core.exceptions import ValidationError
raise ValidationError('Wrestler must belong to the same club as the homework')
@property
def is_completed(self):
items = self.items.all()
if not items.exists():
return False
return all(item.is_completed for item in items)
@property
def completion_date(self):
if self.is_completed:
return self.items.filter(is_completed=True).order_by('-completion_date').first().completion_date
return None
class HomeworkAssignmentItem(models.Model):
assignment = models.ForeignKey(HomeworkAssignment, on_delete=models.CASCADE, related_name='items')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='assignment_items')
is_completed = models.BooleanField(default=False)
completion_date = models.DateField(null=True, blank=True)
class Meta:
unique_together = ['assignment', 'exercise']
indexes = [
models.Index(fields=['assignment', 'is_completed']),
models.Index(fields=['completion_date']),
]
def __str__(self):
status = "" if self.is_completed else ""
return f"{self.assignment} - {self.exercise.name} {status}"
class HomeworkStatus(models.Model):
homework = models.ForeignKey(Homework, on_delete=models.CASCADE, related_name='statuses')
wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='homework_statuses')
is_completed = models.BooleanField(default=False)
completion_date = models.DateField(null=True, blank=True)
notes = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ['homework', 'wrestler']
def __str__(self):
status = "" if self.is_completed else ""
return f"{self.wrestler} - {self.homework.title} {status}"
# NEUES SYSTEM: Training-basierte Homework
# Jeder Wrestler bekommt individuelle Übungen zugewiesen
class TrainingHomeworkAssignment(models.Model):
"""A homework assignment for a specific wrestler in a specific training"""
training = models.ForeignKey('trainings.Training', on_delete=models.CASCADE, related_name='homework_assignments', default=1)
wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='training_homework_assignments')
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='training_homework_assignments', null=True, blank=True)
notes = models.TextField(blank=True)
is_completed = models.BooleanField(default=False)
completion_date = models.DateField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ['training', 'wrestler']
ordering = ['-created_at']
indexes = [
models.Index(fields=['training']),
models.Index(fields=['wrestler']),
models.Index(fields=['is_completed']),
models.Index(fields=['club']),
]
def __str__(self):
return f"{self.wrestler} - Training {self.training_id}"
class TrainingHomeworkExerciseItem(models.Model):
"""Individual exercises assigned to a specific wrestler (NOT shared)"""
assignment = models.ForeignKey(TrainingHomeworkAssignment, on_delete=models.CASCADE, related_name='exercises')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='training_homework_exercises')
reps = models.PositiveIntegerField(null=True, blank=True)
time_minutes = models.PositiveIntegerField(null=True, blank=True)
order = models.IntegerField(default=0)
is_completed = models.BooleanField(default=False)
class Meta:
ordering = ['assignment', 'order']
def __str__(self):
return f"{self.assignment} - {self.exercise.name}"
# ALTES SYSTEM (für Rückwärtskompatibilität - wird nicht mehr verwendet)
class TrainingHomework(models.Model):
"""DEPRECATED: Each wrestler now has individual assignments"""
training = models.ForeignKey('trainings.Training', on_delete=models.CASCADE, related_name='homework_legacy')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['training']),
]
def __str__(self):
return f"Homework for Training {self.training_id}"
class TrainingHomeworkExercise(models.Model):
"""DEPRECATED: Exercises are now per-assignment"""
training_homework = models.ForeignKey(TrainingHomework, on_delete=models.CASCADE, related_name='exercises')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='training_homework_items')
reps = models.PositiveIntegerField(null=True, blank=True)
time_minutes = models.PositiveIntegerField(null=True, blank=True)
order = models.IntegerField(default=0)
class Meta:
ordering = ['training_homework', 'order']
unique_together = ['training_homework', 'exercise']
def __str__(self):
return f"{self.training_homework} - {self.exercise.name}"
+190
View File
@@ -0,0 +1,190 @@
from rest_framework import serializers
from .models import (
Homework, HomeworkExerciseItem, HomeworkAssignment,
HomeworkAssignmentItem, HomeworkStatus,
TrainingHomeworkAssignment, TrainingHomeworkExerciseItem
)
class HomeworkExerciseItemSerializer(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 = HomeworkExerciseItem
fields = ['id', 'exercise', 'exercise_name', 'exercise_category', 'reps', 'time_minutes', 'order']
class HomeworkSerializer(serializers.ModelSerializer):
exercise_items = HomeworkExerciseItemSerializer(many=True, read_only=True)
club_name = serializers.CharField(source='club.name', read_only=True)
exercise_count = serializers.IntegerField(source='exercise_items.count', read_only=True)
class Meta:
model = Homework
fields = ['id', 'title', 'description', 'club', 'club_name', 'due_date', 'is_active',
'exercise_items', 'exercise_count', 'created_at', 'updated_at']
class HomeworkDetailSerializer(HomeworkSerializer):
exercise_items = HomeworkExerciseItemSerializer(many=True, read_only=True)
class Meta(HomeworkSerializer.Meta):
fields = HomeworkSerializer.Meta.fields
class HomeworkAssignmentItemSerializer(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 = HomeworkAssignmentItem
fields = ['id', 'exercise', 'exercise_name', 'exercise_category', 'is_completed', 'completion_date']
class HomeworkAssignmentSerializer(serializers.ModelSerializer):
homework_title = serializers.CharField(source='homework.title', read_only=True)
club_name = serializers.CharField(source='club.name', read_only=True)
wrestler_name = serializers.SerializerMethodField()
completed_items = serializers.SerializerMethodField()
total_items = serializers.SerializerMethodField()
items = HomeworkAssignmentItemSerializer(many=True, read_only=True)
class Meta:
model = HomeworkAssignment
fields = ['id', 'homework', 'homework_title', 'wrestler', 'wrestler_name', 'club', 'club_name',
'due_date', 'notes', 'is_completed', 'completion_date', 'completed_items',
'total_items', 'items', 'created_at']
def get_wrestler_name(self, obj):
return f"{obj.wrestler.first_name} {obj.wrestler.last_name}"
def get_completed_items(self, obj):
return obj.items.filter(is_completed=True).count()
def get_total_items(self, obj):
return obj.items.count()
class HomeworkAssignmentListSerializer(serializers.ModelSerializer):
homework_title = serializers.CharField(source='homework.title', read_only=True)
club_name = serializers.CharField(source='club.name', read_only=True)
wrestler_name = serializers.SerializerMethodField()
class Meta:
model = HomeworkAssignment
fields = ['id', 'homework', 'homework_title', 'wrestler', 'wrestler_name', 'club', 'club_name',
'due_date', 'notes', 'is_completed', 'completion_date', 'created_at']
def get_wrestler_name(self, obj):
return f"{obj.wrestler.first_name} {obj.wrestler.last_name}"
class AssignHomeworkSerializer(serializers.Serializer):
homework = serializers.IntegerField()
wrestlers = serializers.ListField(child=serializers.IntegerField())
due_date = serializers.DateField(required=False, allow_null=True)
notes = serializers.CharField(required=False, allow_blank=True, default='')
class CompleteItemSerializer(serializers.Serializer):
item_id = serializers.IntegerField()
class HomeworkStatusSerializer(serializers.ModelSerializer):
wrestler_name = serializers.CharField(source='wrestler.__str__', read_only=True)
class Meta:
model = HomeworkStatus
fields = '__all__'
# NEUES SYSTEM: Training-basierte Homework
class TrainingHomeworkExerciseItemSerializer(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 = TrainingHomeworkExerciseItem
fields = ['id', 'exercise', 'exercise_name', 'exercise_category', 'reps', 'time_minutes', 'order', 'is_completed']
class TrainingHomeworkAssignmentSerializer(serializers.ModelSerializer):
exercises = serializers.SerializerMethodField()
training_date = serializers.DateField(source='training.date', read_only=True)
training_group = serializers.CharField(source='training.group', read_only=True)
wrestler_name = serializers.SerializerMethodField()
wrestler_group = serializers.CharField(source='wrestler.group', read_only=True)
class Meta:
model = TrainingHomeworkAssignment
fields = ['id', 'training', 'training_date', 'training_group', 'wrestler', 'wrestler_name',
'wrestler_group', 'notes', 'is_completed', 'completion_date', 'exercises', 'created_at']
def get_wrestler_name(self, obj):
return f"{obj.wrestler.first_name} {obj.wrestler.last_name}"
def get_exercises(self, obj):
exercises = obj.exercises.all().order_by('order')
return TrainingHomeworkExerciseItemSerializer(exercises, many=True).data
class TrainingHomeworkAssignmentCreateSerializer(serializers.Serializer):
training = serializers.IntegerField()
wrestler = serializers.IntegerField()
exercises = serializers.ListField(
child=serializers.DictField(child=serializers.IntegerField(allow_null=True))
)
notes = serializers.CharField(required=False, allow_blank=True, default='')
def create(self, validated_data):
from utils.permissions import get_user_club
from exercises.models import Exercise
from trainings.models import Training
from wrestlers.models import Wrestler
user = self.context['request'].user
club = get_user_club(user)
training_id = validated_data['training']
wrestler_id = validated_data['wrestler']
exercises_data = validated_data['exercises']
notes = validated_data.get('notes', '')
try:
training_obj = Training.objects.get(id=training_id)
except Training.DoesNotExist:
raise serializers.ValidationError({'training': f'Training with id {training_id} does not exist'})
try:
wrestler_obj = Wrestler.objects.get(id=wrestler_id)
except Wrestler.DoesNotExist:
raise serializers.ValidationError({'wrestler': f'Wrestler with id {wrestler_id} does not exist'})
# Create assignment
assignment = TrainingHomeworkAssignment.objects.create(
training=training_obj,
wrestler=wrestler_obj,
club=club,
notes=notes
)
# Create individual exercises for this assignment
for i, ex in enumerate(exercises_data):
try:
exercise_obj = Exercise.objects.get(id=ex['exercise'])
TrainingHomeworkExerciseItem.objects.create(
assignment=assignment,
exercise=exercise_obj,
reps=ex.get('reps'),
time_minutes=ex.get('time_minutes'),
order=i
)
except Exercise.DoesNotExist:
pass
return assignment
def to_representation(self, instance):
return TrainingHomeworkAssignmentSerializer(instance, context=self.context).data
+237
View File
@@ -0,0 +1,237 @@
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from django.utils import timezone
from django.db import transaction
from utils.permissions import ClubLevelPermission, ClubFilterBackend
from .models import (
Homework, HomeworkExerciseItem, HomeworkAssignment,
HomeworkAssignmentItem, HomeworkStatus,
TrainingHomeworkAssignment, TrainingHomeworkExerciseItem
)
from .serializers import (
HomeworkSerializer, HomeworkDetailSerializer, HomeworkExerciseItemSerializer,
HomeworkAssignmentSerializer, HomeworkAssignmentListSerializer,
AssignHomeworkSerializer, CompleteItemSerializer, HomeworkStatusSerializer,
TrainingHomeworkAssignmentSerializer, TrainingHomeworkAssignmentCreateSerializer
)
from wrestleDesk.pagination import StandardResultsSetPagination
class HomeworkViewSet(viewsets.ModelViewSet):
queryset = Homework.objects.prefetch_related('exercise_items', 'exercise_items__exercise').all()
serializer_class = HomeworkSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['club', 'is_active']
search_fields = ['title', 'description']
ordering_fields = ['created_at', 'due_date']
def get_serializer_class(self):
if self.action == 'retrieve':
return HomeworkDetailSerializer
return HomeworkSerializer
class HomeworkExerciseItemViewSet(viewsets.ModelViewSet):
queryset = HomeworkExerciseItem.objects.select_related('homework', 'exercise').all()
serializer_class = HomeworkExerciseItemSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_fields = ['homework']
class HomeworkAssignmentViewSet(viewsets.ModelViewSet):
queryset = HomeworkAssignment.objects.select_related('homework', 'wrestler', 'club').prefetch_related('items').all()
serializer_class = HomeworkAssignmentSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['homework', 'wrestler', 'club', 'is_completed']
search_fields = ['wrestler__first_name', 'wrestler__last_name']
ordering_fields = ['created_at', 'due_date']
def get_serializer_class(self):
if self.action == 'list':
return HomeworkAssignmentListSerializer
return HomeworkAssignmentSerializer
@action(detail=True, methods=['post'])
@transaction.atomic
def complete_item(self, request, pk=None):
serializer = CompleteItemSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
assignment = self.get_object()
item_id = serializer.validated_data['item_id']
try:
item = assignment.items.get(id=item_id)
except HomeworkAssignmentItem.DoesNotExist:
return Response(
{'detail': 'Item not found'},
status=status.HTTP_404_NOT_FOUND
)
item.is_completed = True
item.completion_date = timezone.now()
item.save()
return Response({'status': 'item completed'})
@action(detail=False, methods=['post'])
@transaction.atomic
def assign(self, request):
serializer = AssignHomeworkSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
homework_id = serializer.validated_data['homework']
wrestler_ids = serializer.validated_data['wrestlers']
due_date = serializer.validated_data.get('due_date')
notes = serializer.validated_data.get('notes', '')
try:
homework = Homework.objects.get(id=homework_id)
except Homework.DoesNotExist:
return Response({'detail': 'Homework not found'}, status=status.HTTP_404_NOT_FOUND)
created_assignments = []
errors = []
for wrestler_id in wrestler_ids:
try:
existing = HomeworkAssignment.objects.filter(
homework=homework,
wrestler_id=wrestler_id
).exists()
if existing:
errors.append(f"Wrestler {wrestler_id} hat bereits diese Hausaufgabe")
continue
assignment = HomeworkAssignment.objects.create(
homework=homework,
wrestler_id=wrestler_id,
club=homework.club,
due_date=due_date,
notes=notes
)
# Copy exercises from homework to assignment
for exercise_item in homework.exercise_items.all():
HomeworkAssignmentItem.objects.create(
assignment=assignment,
exercise=exercise_item.exercise
)
created_assignments.append(assignment.id)
except Exception as e:
errors.append(f"Wrestler {wrestler_id}: {str(e)}")
return Response({
'created': created_assignments,
'errors': errors
})
class HomeworkStatusViewSet(viewsets.ModelViewSet):
queryset = HomeworkStatus.objects.select_related('homework', 'wrestler').all()
serializer_class = HomeworkStatusSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['homework', 'wrestler', 'is_completed']
ordering_fields = ['created_at', 'completion_date']
# NEUES SYSTEM: Training-basierte Homework
class TrainingHomeworkAssignmentViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
serializer_class = TrainingHomeworkAssignmentSerializer
filterset_fields = ['is_completed', 'training', 'wrestler']
search_fields = ['wrestler__first_name', 'wrestler__last_name']
ordering_fields = ['created_at', 'is_completed']
http_method_names = ['get', 'post', 'patch', 'delete']
def get_queryset(self):
queryset = TrainingHomeworkAssignment.objects.select_related(
'training', 'wrestler'
).prefetch_related(
'exercises', 'exercises__exercise'
).all()
# Filter by training ID if provided
training_id = self.request.query_params.get('training')
if training_id:
queryset = queryset.filter(training_id=training_id)
return queryset
def get_serializer_class(self):
if self.action == 'create':
return TrainingHomeworkAssignmentCreateSerializer
return TrainingHomeworkAssignmentSerializer
@action(detail=True, methods=['post'])
def complete(self, request, pk=None):
assignment = self.get_object()
assignment.is_completed = True
assignment.completion_date = timezone.now().date()
assignment.save()
# Also mark all exercises as completed
assignment.exercises.update(is_completed=True)
serializer = self.get_serializer(assignment)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def uncomplete(self, request, pk=None):
assignment = self.get_object()
assignment.is_completed = False
assignment.completion_date = None
assignment.save()
# Also mark all exercises as not completed
assignment.exercises.update(is_completed=False)
serializer = self.get_serializer(assignment)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def complete_exercise(self, request, pk=None):
assignment = self.get_object()
exercise_id = request.data.get('exercise_id')
if not exercise_id:
return Response({'detail': 'exercise_id is required'}, status=status.HTTP_400_BAD_REQUEST)
try:
item = assignment.exercises.get(id=exercise_id)
item.is_completed = True
item.save()
return Response({'status': 'exercise completed'})
except TrainingHomeworkExerciseItem.DoesNotExist:
return Response({'detail': 'Exercise not found'}, status=status.HTTP_404_NOT_FOUND)
@action(detail=True, methods=['post'])
def uncomplete_exercise(self, request, pk=None):
assignment = self.get_object()
exercise_id = request.data.get('exercise_id')
if not exercise_id:
return Response({'detail': 'exercise_id is required'}, status=status.HTTP_400_BAD_REQUEST)
try:
item = assignment.exercises.get(id=exercise_id)
item.is_completed = False
item.save()
return Response({'status': 'exercise uncompleted'})
except TrainingHomeworkExerciseItem.DoesNotExist:
return Response({'detail': 'Exercise not found'}, status=status.HTTP_404_NOT_FOUND)
View File
+31
View File
@@ -0,0 +1,31 @@
from django.contrib import admin
from .models import LeistungstestTemplate, LeistungstestTemplateExercise, LeistungstestResult, LeistungstestResultItem
class LeistungstestTemplateExerciseInline(admin.TabularInline):
model = LeistungstestTemplateExercise
extra = 0
readonly_fields = ['exercise']
@admin.register(LeistungstestTemplate)
class LeistungstestTemplateAdmin(admin.ModelAdmin):
list_display = ['name', 'created_at', 'usage_count']
search_fields = ['name']
inlines = [LeistungstestTemplateExerciseInline]
@admin.register(LeistungstestResult)
class LeistungstestResultAdmin(admin.ModelAdmin):
list_display = ['id', 'wrestler', 'template', 'total_time_seconds', 'score_percent', 'rating', 'completed_at']
list_filter = ['template', 'rating', 'completed_at']
search_fields = ['wrestler__first_name', 'wrestler__last_name', 'template__name']
readonly_fields = ['score_percent', 'created_at']
raw_id_fields = ['wrestler', 'template']
@admin.register(LeistungstestResultItem)
class LeistungstestResultItemAdmin(admin.ModelAdmin):
list_display = ['id', 'result', 'exercise', 'target_reps', 'actual_reps', 'elapsed_seconds', 'order']
list_filter = ['exercise']
raw_id_fields = ['result', 'exercise']
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class LeistungstestConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'leistungstest'
@@ -0,0 +1,94 @@
# Generated by Django 4.2.29 on 2026-03-23 12:43
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('wrestlers', '0002_alter_wrestler_license_scan_alter_wrestler_photo'),
('exercises', '0003_alter_exercise_media'),
]
operations = [
migrations.CreateModel(
name='LeistungstestResult',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('total_time_minutes', models.PositiveIntegerField(blank=True, null=True)),
('rating', models.PositiveSmallIntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)], default=3)),
('notes', models.TextField(blank=True)),
('completed_at', models.DateTimeField(default=django.utils.timezone.now)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['-completed_at'],
},
),
migrations.CreateModel(
name='LeistungstestTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='LeistungstestResultItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('target_reps', models.PositiveIntegerField()),
('actual_reps', models.PositiveIntegerField()),
('order', models.IntegerField(default=0)),
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exercises.exercise')),
('result', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='leistungstest.leistungstestresult')),
],
options={
'ordering': ['result', 'order'],
},
),
migrations.AddField(
model_name='leistungstestresult',
name='template',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='leistungstest.leistungstesttemplate'),
),
migrations.AddField(
model_name='leistungstestresult',
name='wrestler',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leistungstest_results', to='wrestlers.wrestler'),
),
migrations.CreateModel(
name='LeistungstestTemplateExercise',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('target_reps', models.PositiveIntegerField()),
('order', models.IntegerField(default=0)),
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exercises.exercise')),
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercises', to='leistungstest.leistungstesttemplate')),
],
options={
'ordering': ['template', 'order'],
'unique_together': {('template', 'exercise')},
},
),
migrations.AddIndex(
model_name='leistungstestresult',
index=models.Index(fields=['wrestler'], name='leistungste_wrestle_f3f6c2_idx'),
),
migrations.AddIndex(
model_name='leistungstestresult',
index=models.Index(fields=['template'], name='leistungste_templat_daf98b_idx'),
),
migrations.AddIndex(
model_name='leistungstestresult',
index=models.Index(fields=['completed_at'], name='leistungste_complet_838820_idx'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.29 on 2026-03-24 08:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('leistungstest', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='leistungstestresult',
old_name='total_time_minutes',
new_name='total_time_seconds',
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.29 on 2026-03-24 09:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('leistungstest', '0002_change_total_time_to_seconds'),
]
operations = [
migrations.AddField(
model_name='leistungstestresultitem',
name='elapsed_seconds',
field=models.PositiveIntegerField(default=0),
),
]
+79
View File
@@ -0,0 +1,79 @@
from django.db import models
from django.utils import timezone
class LeistungstestTemplate(models.Model):
name = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.name
@property
def usage_count(self):
return LeistungstestResult.objects.filter(template_id=self.pk).count()
class LeistungstestTemplateExercise(models.Model):
template = models.ForeignKey(LeistungstestTemplate, on_delete=models.CASCADE, related_name='exercises')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE)
target_reps = models.PositiveIntegerField()
order = models.IntegerField(default=0)
class Meta:
ordering = ['template', 'order']
unique_together = ['template', 'exercise']
def __str__(self):
return f"{self.template.name} - {self.exercise.name}"
class LeistungstestResult(models.Model):
template = models.ForeignKey(LeistungstestTemplate, on_delete=models.CASCADE, related_name='results')
wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='leistungstest_results')
total_time_seconds = models.PositiveIntegerField(null=True, blank=True)
rating = models.PositiveSmallIntegerField(choices=[(1,1),(2,2),(3,3),(4,4),(5,5)], default=3)
notes = models.TextField(blank=True)
completed_at = models.DateTimeField(default=timezone.now)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-completed_at']
indexes = [
models.Index(fields=['wrestler']),
models.Index(fields=['template']),
models.Index(fields=['completed_at']),
]
def __str__(self):
return f"{self.wrestler} - {self.template.name}"
@property
def score_percent(self):
items = self.items.all()
if not items.exists():
return 0
total_target = sum(item.target_reps for item in items)
total_actual = sum(item.actual_reps for item in items)
if total_target == 0:
return 0
return round((total_actual / total_target) * 100, 1)
class LeistungstestResultItem(models.Model):
result = models.ForeignKey(LeistungstestResult, on_delete=models.CASCADE, related_name='items')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE)
target_reps = models.PositiveIntegerField()
actual_reps = models.PositiveIntegerField()
order = models.IntegerField(default=0)
elapsed_seconds = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['result', 'order']
def __str__(self):
return f"{self.result} - {self.exercise.name}: {self.actual_reps}/{self.target_reps}"
+52
View File
@@ -0,0 +1,52 @@
from rest_framework import serializers
from .models import LeistungstestTemplate, LeistungstestTemplateExercise, LeistungstestResult, LeistungstestResultItem
class LeistungstestTemplateExerciseSerializer(serializers.ModelSerializer):
exercise_name = serializers.CharField(source='exercise.name', read_only=True)
class Meta:
model = LeistungstestTemplateExercise
fields = ['id', 'exercise', 'exercise_name', 'target_reps', 'order']
class LeistungstestTemplateSerializer(serializers.ModelSerializer):
exercises = LeistungstestTemplateExerciseSerializer(many=True, read_only=True)
usage_count = serializers.IntegerField(read_only=True)
class Meta:
model = LeistungstestTemplate
fields = ['id', 'name', 'exercises', 'usage_count', 'created_at']
class LeistungstestResultItemSerializer(serializers.ModelSerializer):
exercise_name = serializers.CharField(source='exercise.name', read_only=True)
class Meta:
model = LeistungstestResultItem
fields = ['id', 'exercise', 'exercise_name', 'target_reps', 'actual_reps', 'elapsed_seconds', 'order']
class LeistungstestResultSerializer(serializers.ModelSerializer):
items = LeistungstestResultItemSerializer(many=True, read_only=True)
template_name = serializers.CharField(source='template.name', read_only=True)
wrestler_name = serializers.CharField(source='wrestler.__str__', read_only=True)
score_percent = serializers.FloatField(read_only=True)
total_time_minutes = serializers.SerializerMethodField()
class Meta:
model = LeistungstestResult
fields = ['id', 'template', 'template_name', 'wrestler', 'wrestler_name',
'total_time_minutes', 'total_time_seconds', 'rating', 'notes', 'completed_at',
'score_percent', 'items', 'created_at']
read_only_fields = ['total_time_minutes']
def get_total_time_minutes(self, obj):
if obj.total_time_seconds is None:
return None
return obj.total_time_seconds // 60
def validate_total_time_seconds(self, value):
if value is not None and value < 0:
raise serializers.ValidationError("Zeit muss positiv sein.")
return value
+78
View File
@@ -0,0 +1,78 @@
from datetime import date, timedelta
def get_date_range(period):
"""Return start date for period filter, or None for 'all'."""
today = date.today()
if period == "month":
return today.replace(day=1)
elif period == "3months":
return today - timedelta(days=90)
elif period == "year":
return today.replace(month=1, day=1)
return None
def get_template_leaderboard(template_id, period="all", limit=10):
"""Return top wrestlers by score_percent for a template."""
from .models import LeistungstestResult
qs = LeistungstestResult.objects.filter(template_id=template_id)
start_date = get_date_range(period)
if start_date:
qs = qs.filter(completed_at__date__gte=start_date)
qs = qs.select_related('wrestler')
results = []
all_results = list(qs)
all_results.sort(key=lambda r: (-r.score_percent, r.total_time_seconds))
for rank, result in enumerate(all_results[:limit], 1):
results.append({
'rank': rank,
'wrestler_id': result.wrestler_id,
'wrestler_name': str(result.wrestler),
'score_percent': result.score_percent,
'total_time_seconds': result.total_time_seconds,
'completed_at': result.completed_at.date().isoformat() if result.completed_at else None,
})
return results
def get_exercise_leaderboard(exercise_id, period="all", limit=10):
"""Return top wrestlers by best time for an exercise."""
from .models import LeistungstestResultItem, LeistungstestResult
from django.db.models import Min
start_date = get_date_range(period)
qs = LeistungstestResultItem.objects.filter(exercise_id=exercise_id)
if start_date:
qs = qs.filter(result__completed_at__date__gte=start_date)
# Get best time per wrestler
best_times = qs.values('result__wrestler__id', 'result__wrestler__first_name', 'result__wrestler__last_name', 'result__completed_at__date')\
.annotate(best_time=Min('elapsed_seconds'))\
.order_by('best_time')
results = []
for rank, item in enumerate(best_times[:limit], 1):
wrestler_name = f"{item['result__wrestler__first_name']} {item['result__wrestler__last_name']}"
results.append({
'rank': rank,
'wrestler_id': item['result__wrestler__id'],
'wrestler_name': wrestler_name.strip(),
'best_time_seconds': item['best_time'],
'completed_at': item['result__completed_at__date'].isoformat() if item['result__completed_at__date'] else None,
})
return results
def get_used_exercises():
"""Return all exercises that have been used in any Leistungstest result."""
from .models import LeistungstestResultItem
from exercises.models import Exercise
exercise_ids = LeistungstestResultItem.objects.values_list('exercise_id', flat=True).distinct()
return Exercise.objects.filter(id__in=exercise_ids).order_by('name')
+14
View File
@@ -0,0 +1,14 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import LeistungstestTemplateViewSet, LeistungstestTemplateExerciseViewSet, LeistungstestResultViewSet, LeistungstestResultItemViewSet, LeistungstestStatsViewSet
router = DefaultRouter()
router.register('templates', LeistungstestTemplateViewSet, basename='leistungstest-template')
router.register('template-exercises', LeistungstestTemplateExerciseViewSet, basename='leistungstest-template-exercise')
router.register('results', LeistungstestResultViewSet, basename='leistungstest-result')
router.register('result-items', LeistungstestResultItemViewSet, basename='leistungstest-result-item')
router.register('stats', LeistungstestStatsViewSet, basename='leistungstest-stats')
urlpatterns = [
path('', include(router.urls)),
]
+259
View File
@@ -0,0 +1,259 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from .models import LeistungstestTemplate, LeistungstestTemplateExercise, LeistungstestResult, LeistungstestResultItem
from .serializers import (
LeistungstestTemplateSerializer,
LeistungstestTemplateExerciseSerializer,
LeistungstestResultSerializer,
LeistungstestResultItemSerializer,
)
from .stats import get_template_leaderboard, get_exercise_leaderboard, get_used_exercises
class LeistungstestTemplateViewSet(viewsets.ModelViewSet):
queryset = LeistungstestTemplate.objects.all()
serializer_class = LeistungstestTemplateSerializer
def get_queryset(self):
return LeistungstestTemplate.objects.all().prefetch_related('exercises__exercise')
def create(self, request, *args, **kwargs):
name = request.data.get('name')
exercises_data = request.data.get('exercises', [])
# Create template first
template = LeistungstestTemplate.objects.create(name=name)
# Create exercises
for i, ex_data in enumerate(exercises_data):
LeistungstestTemplateExercise.objects.create(
template=template,
exercise_id=ex_data['exercise'],
target_reps=ex_data['target_reps'],
order=ex_data.get('order', i),
)
# Reload with prefetch to get exercise names
template = LeistungstestTemplate.objects.prefetch_related('exercises__exercise').get(pk=template.pk)
return Response(
LeistungstestTemplateSerializer(template).data,
status=status.HTTP_201_CREATED
)
@action(detail=True, methods=['post'])
def duplicate(self, request, pk=None):
template = self.get_object()
new_template = LeistungstestTemplate.objects.create(name=f"{template.name} (Kopie)")
for exercise in template.exercises.all():
LeistungstestTemplateExercise.objects.create(
template=new_template,
exercise=exercise.exercise,
target_reps=exercise.target_reps,
order=exercise.order,
)
return Response(
LeistungstestTemplateSerializer(new_template).data,
status=status.HTTP_201_CREATED
)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
instance.name = request.data.get('name', instance.name)
instance.save()
exercises_data = request.data.get('exercises')
if exercises_data is not None:
instance.exercises.all().delete()
for i, ex_data in enumerate(exercises_data):
LeistungstestTemplateExercise.objects.create(
template=instance,
exercise_id=ex_data['exercise'],
target_reps=ex_data['target_reps'],
order=ex_data.get('order', i),
)
instance = LeistungstestTemplate.objects.prefetch_related('exercises__exercise').get(pk=instance.pk)
return Response(LeistungstestTemplateSerializer(instance).data)
class LeistungstestTemplateExerciseViewSet(viewsets.ModelViewSet):
queryset = LeistungstestTemplateExercise.objects.all()
serializer_class = LeistungstestTemplateExerciseSerializer
class LeistungstestResultViewSet(viewsets.ModelViewSet):
queryset = LeistungstestResult.objects.all()
serializer_class = LeistungstestResultSerializer
def get_queryset(self):
queryset = LeistungstestResult.objects.all().prefetch_related('items__exercise')
template_id = self.request.query_params.get('template')
wrestler_id = self.request.query_params.get('wrestler')
if template_id:
queryset = queryset.filter(template_id=template_id)
if wrestler_id:
queryset = queryset.filter(wrestler_id=wrestler_id)
return queryset
def create(self, request, *args, **kwargs):
template_id = request.data.get('template')
wrestler_id = request.data.get('wrestler')
items_data = request.data.get('items', [])
result = LeistungstestResult.objects.create(
template_id=template_id,
wrestler_id=wrestler_id,
total_time_seconds=request.data.get('total_time_seconds') or None,
rating=request.data.get('rating', 3),
notes=request.data.get('notes', ''),
)
for i, item_data in enumerate(items_data):
LeistungstestResultItem.objects.create(
result=result,
exercise_id=item_data['exercise'],
target_reps=item_data.get('target_reps', 0),
actual_reps=item_data.get('actual_reps', 0),
elapsed_seconds=item_data.get('elapsed_seconds', 0),
order=item_data.get('order', i),
)
result_items = LeistungstestResultItem.objects.filter(result=result)
total_target = sum(item.target_reps for item in result_items)
total_actual = sum(item.actual_reps for item in result_items)
if total_target > 0:
score = round((total_actual / total_target) * 100, 1)
else:
score = 0.0
result.refresh_from_db()
result_data = LeistungstestResultSerializer(result).data
result_data['score_percent'] = score
return Response(result_data, status=status.HTTP_201_CREATED)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
instance.total_time_seconds = request.data.get('total_time_seconds', instance.total_time_seconds)
instance.rating = request.data.get('rating', instance.rating)
instance.notes = request.data.get('notes', instance.notes)
instance.save()
items_data = request.data.get('items')
if items_data is not None:
instance.items.all().delete()
for i, item_data in enumerate(items_data):
LeistungstestResultItem.objects.create(
result=instance,
exercise_id=item_data['exercise'],
target_reps=item_data.get('target_reps', 0),
actual_reps=item_data.get('actual_reps', 0),
elapsed_seconds=item_data.get('elapsed_seconds', 0),
order=item_data.get('order', i),
)
result_items = LeistungstestResultItem.objects.filter(result=instance)
total_target = sum(item.target_reps for item in result_items)
total_actual = sum(item.actual_reps for item in result_items)
if total_target > 0:
score = round((total_actual / total_target) * 100, 1)
else:
score = 0.0
result_data = LeistungstestResultSerializer(instance).data
result_data['score_percent'] = score
return Response(result_data)
@action(detail=False, methods=['get'])
def leaderboard(self, request):
template_id = request.query_params.get('template')
if not template_id:
return Response(
{'error': 'template parameter is required'},
status=status.HTTP_400_BAD_REQUEST
)
limit = int(request.query_params.get('limit', 10))
results = LeistungstestResult.objects.filter(template_id=template_id)\
.select_related('wrestler')
leaderboard_data = []
for result in results:
leaderboard_data.append({
'rank': 0,
'result_id': result.id,
'wrestler_id': result.wrestler.id,
'wrestler_name': str(result.wrestler),
'score_percent': result.score_percent,
'completed_at': result.completed_at,
'total_time_seconds': result.total_time_seconds,
'rating': result.rating,
})
leaderboard_data.sort(key=lambda x: x['score_percent'], reverse=True)
leaderboard_data = leaderboard_data[:limit]
for i, entry in enumerate(leaderboard_data, 1):
entry['rank'] = i
return Response(leaderboard_data)
class LeistungstestResultItemViewSet(viewsets.ModelViewSet):
queryset = LeistungstestResultItem.objects.all()
serializer_class = LeistungstestResultItemSerializer
class LeistungstestStatsViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def leaderboard(self, request):
lb_type = request.query_params.get('type', 'template')
template_id = request.query_params.get('template_id')
exercise_id = request.query_params.get('exercise_id')
period = request.query_params.get('period', 'all')
limit = int(request.query_params.get('limit', 10))
if lb_type == 'template' and template_id:
results = get_template_leaderboard(int(template_id), period, limit)
template = LeistungstestTemplate.objects.get(pk=template_id)
return Response({
'template_id': template_id,
'template_name': template.name,
'period': period,
'results': results,
})
elif lb_type == 'exercise' and exercise_id:
from exercises.models import Exercise
results = get_exercise_leaderboard(int(exercise_id), period, limit)
exercise = Exercise.objects.get(pk=exercise_id)
return Response({
'exercise_id': exercise_id,
'exercise_name': exercise.name,
'period': period,
'results': results,
})
return Response({'error': 'Invalid parameters'}, status=400)
@action(detail=False, methods=['get'])
def exercises(self, request):
exercises = get_used_exercises()
return Response([{'id': e.id, 'name': e.name} for e in exercises])
+12
View File
@@ -0,0 +1,12 @@
import unfold
from unfold.admin import ModelAdmin as UnfoldModelAdmin
from django.contrib import admin
from .models import Location
@admin.register(Location)
class LocationAdmin(UnfoldModelAdmin):
list_display = ['name', 'is_active', 'created_at']
list_filter = ['is_active']
search_fields = ['name', 'address']
readonly_fields = ['created_at', 'updated_at']
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class LocationsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'locations'
@@ -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='Location',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('address', models.TextField(blank=True)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name_plural': 'Locations',
'ordering': ['name'],
},
),
]
@@ -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'),
('locations', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='location',
name='club',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='clubs.club'),
),
migrations.AddIndex(
model_name='location',
index=models.Index(fields=['club'], name='locations_l_club_id_aa048c_idx'),
),
]
+20
View File
@@ -0,0 +1,20 @@
from django.db import models
class Location(models.Model):
name = models.CharField(max_length=200)
address = models.TextField(blank=True)
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='locations', 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']
verbose_name_plural = 'Locations'
indexes = [
models.Index(fields=['club']),
]
def __str__(self):
return self.name
+9
View File
@@ -0,0 +1,9 @@
from rest_framework import serializers
from .models import Location
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = '__all__'
read_only_fields = ['created_at', 'updated_at']
+17
View File
@@ -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 Location
from .serializers import LocationSerializer
from wrestleDesk.pagination import StandardResultsSetPagination
class LocationViewSet(viewsets.ModelViewSet):
queryset = Location.objects.all()
serializer_class = LocationSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['is_active']
search_fields = ['name', 'address']
ordering_fields = ['name', 'created_at']
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wrestleDesk.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
+17
View File
@@ -0,0 +1,17 @@
Django>=4.2,<5.0
djangorestframework>=3.14
django-filter>=24.0
django-cors-headers>=4.3
djangorestframework-simplejwt>=5.3
drf-spectacular>=0.27
django-unfold>=0.30
django-import-export>=4.0
Pillow>=10.0
django-resized>=1.0
django-cleanup>=8.0
psycopg2-binary>=2.9
gunicorn>=21.0
whitenoise>=6.0
django-environ>=0.11
black>=24.0
isort>=5.13
View File
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class StatsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'stats'
+92
View File
@@ -0,0 +1,92 @@
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Count
from datetime import datetime, timedelta
from wrestlers.models import Wrestler
from trainers.models import Trainer
from trainings.models import Training, Attendance
from homework.models import TrainingHomeworkAssignment
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def dashboard_stats(request):
today = datetime.now().date()
week_start = today - timedelta(days=today.weekday())
two_weeks_ago = today - timedelta(days=14)
# Wrestlers stats
total_wrestlers = Wrestler.objects.count()
wrestlers_this_week = Wrestler.objects.filter(
created_at__date__gte=week_start
).count()
# Trainers stats
total_trainers = Trainer.objects.count()
active_trainers = Trainer.objects.filter(is_active=True).count()
# Trainings stats
total_trainings = Training.objects.count()
trainings_this_week = Training.objects.filter(
date__gte=week_start
).count()
# Homework stats
open_homework = TrainingHomeworkAssignment.objects.filter(is_completed=False).count()
completed_homework = TrainingHomeworkAssignment.objects.filter(is_completed=True).count()
# Attendance by group this week
trainings_this_week_qs = Training.objects.filter(date__gte=week_start)
attendance_data = {}
for group, label in [('kids', 'Kinder'), ('youth', 'Jugend'), ('adults', 'Erwachsene')]:
group_wrestlers = Wrestler.objects.filter(group=group)
attended = Attendance.objects.filter(
training__in=trainings_this_week_qs,
wrestler__in=group_wrestlers
).values('wrestler').distinct().count()
total = group_wrestlers.count()
attendance_data[group] = {
'attended': attended,
'total': total,
'percent': int((attended / total * 100) if total > 0 else 0)
}
# Activity (last 14 days)
activity = []
for i in range(14):
day = today - timedelta(days=13 - i)
count = Attendance.objects.filter(training__date=day).count()
activity.append({'date': day.isoformat(), 'count': count})
# Wrestlers by group
wrestlers_by_group = {
'kids': Wrestler.objects.filter(group='kids', is_active=True).count(),
'youth': Wrestler.objects.filter(group='youth', is_active=True).count(),
'adults': Wrestler.objects.filter(group='adults', is_active=True).count(),
'inactive': Wrestler.objects.filter(is_active=False).count(),
}
# Top trainers (by training count)
trainer_stats = Trainer.objects.annotate(
training_count=Count('trainings')
).order_by('-training_count')[:5]
top_trainers = [
{'name': t.first_name + ' ' + t.last_name[0] + '.', 'training_count': t.training_count}
for t in trainer_stats
]
return Response({
'wrestlers': {'total': total_wrestlers, 'this_week': wrestlers_this_week},
'trainers': {'total': total_trainers, 'active': active_trainers},
'trainings': {'total': total_trainings, 'this_week': trainings_this_week},
'homework': {'open': open_homework, 'completed': completed_homework},
'attendance': {
'this_week': attendance_data,
'average': Attendance.objects.filter(training__date__gte=week_start).values('training').distinct().count(),
'expected': total_wrestlers
},
'activity': activity,
'wrestlers_by_group': wrestlers_by_group,
'top_trainers': top_trainers,
})
View File
+20
View File
@@ -0,0 +1,20 @@
import unfold
from unfold.admin import ModelAdmin as UnfoldModelAdmin
from django.contrib import admin
from .models import TrainingTemplate, TemplateExercise
@admin.register(TrainingTemplate)
class TrainingTemplateAdmin(UnfoldModelAdmin):
list_display = ['name', 'category', 'is_active', 'created_at']
list_filter = ['category', 'is_active']
search_fields = ['name', 'description']
readonly_fields = ['created_at', 'updated_at']
filter_horizontal = ['exercises']
@admin.register(TemplateExercise)
class TemplateExerciseAdmin(UnfoldModelAdmin):
list_display = ['template', 'exercise', 'order', 'default_value']
list_filter = ['template']
raw_id_fields = ['template', 'exercise']
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class TemplatesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'templates'
@@ -0,0 +1,53 @@
# 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 = [
('exercises', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='TemplateExercise',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveIntegerField(default=0)),
('default_value', models.CharField(blank=True, max_length=50)),
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exercises.exercise')),
],
options={
'ordering': ['order'],
},
),
migrations.CreateModel(
name='TrainingTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('description', models.TextField(blank=True)),
('category', models.CharField(choices=[('warmup', 'Warm-up'), ('main', 'Main'), ('cooldown', 'Cool-down')], default='main', max_length=20)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('exercises', models.ManyToManyField(related_name='templates', through='templates.TemplateExercise', to='exercises.exercise')),
],
options={
'ordering': ['name'],
},
),
migrations.AddField(
model_name='templateexercise',
name='template',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='templates.trainingtemplate'),
),
migrations.AlterUniqueTogether(
name='templateexercise',
unique_together={('template', 'exercise')},
),
]
@@ -0,0 +1,20 @@
# Generated by Django 4.2.29 on 2026-03-20 14:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('clubs', '0001_initial'),
('templates', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='trainingtemplate',
name='club',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='training_templates', to='clubs.club'),
),
]
+33
View File
@@ -0,0 +1,33 @@
from django.db import models
class TrainingTemplate(models.Model):
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
category = models.CharField(max_length=20, choices=[
('warmup', 'Warm-up'),
('main', 'Main'),
('cooldown', 'Cool-down'),
], default='main')
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='training_templates', null=True, blank=True)
exercises = models.ManyToManyField('exercises.Exercise', through='TemplateExercise', related_name='templates')
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
class TemplateExercise(models.Model):
template = models.ForeignKey(TrainingTemplate, on_delete=models.CASCADE)
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE)
order = models.PositiveIntegerField(default=0)
default_value = models.CharField(max_length=50, blank=True)
class Meta:
ordering = ['order']
unique_together = ['template', 'exercise']
+29
View File
@@ -0,0 +1,29 @@
from rest_framework import serializers
from .models import TrainingTemplate, TemplateExercise
class TemplateExerciseSerializer(serializers.ModelSerializer):
exercise_name = serializers.CharField(source='exercise.name', read_only=True)
class Meta:
model = TemplateExercise
fields = '__all__'
class TrainingTemplateSerializer(serializers.ModelSerializer):
exercises = TemplateExerciseSerializer(many=True, source='templateexercise_set', read_only=True)
exercise_count = serializers.SerializerMethodField()
class Meta:
model = TrainingTemplate
fields = '__all__'
read_only_fields = ['created_at', 'updated_at']
def get_exercise_count(self, obj):
return obj.templateexercise_set.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)
+27
View File
@@ -0,0 +1,27 @@
from rest_framework import viewsets, filters
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from .models import TrainingTemplate, TemplateExercise
from .serializers import TrainingTemplateSerializer, TemplateExerciseSerializer
from wrestleDesk.pagination import StandardResultsSetPagination
class TrainingTemplateViewSet(viewsets.ModelViewSet):
queryset = TrainingTemplate.objects.prefetch_related('templateexercise_set').all()
serializer_class = TrainingTemplateSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['category', 'is_active']
search_fields = ['name', 'description']
ordering_fields = ['name', 'created_at']
class TemplateExerciseViewSet(viewsets.ModelViewSet):
queryset = TemplateExercise.objects.all()
serializer_class = TemplateExerciseSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['template']
ordering_fields = ['order']
+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)
View File
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class TrainingLogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'training_log'
@@ -0,0 +1,41 @@
# Generated by Django 4.2.29 on 2026-03-23 12:15
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('exercises', '0003_alter_exercise_media'),
('trainings', '0005_training_club_and_more'),
('wrestlers', '0002_alter_wrestler_license_scan_alter_wrestler_photo'),
]
operations = [
migrations.CreateModel(
name='TrainingLogEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reps', models.PositiveIntegerField()),
('sets', models.PositiveIntegerField(default=1)),
('time_minutes', models.PositiveIntegerField(blank=True, null=True)),
('weight_kg', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)),
('rating', models.PositiveSmallIntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)], default=3)),
('notes', models.TextField(blank=True)),
('logged_at', models.DateTimeField(default=django.utils.timezone.now)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_logs', to='exercises.exercise')),
('training', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='training_logs', to='trainings.training')),
('wrestler', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_logs', to='wrestlers.wrestler')),
],
options={
'ordering': ['-logged_at'],
'indexes': [models.Index(fields=['wrestler'], name='training_lo_wrestle_67cef4_idx'), models.Index(fields=['exercise'], name='training_lo_exercis_73fb9d_idx'), models.Index(fields=['logged_at'], name='training_lo_logged__9c003b_idx'), models.Index(fields=['training'], name='training_lo_trainin_b74b31_idx')],
},
),
]
+29
View File
@@ -0,0 +1,29 @@
from django.db import models
from django.utils import timezone
class TrainingLogEntry(models.Model):
wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='training_logs')
training = models.ForeignKey('trainings.Training', on_delete=models.SET_NULL, null=True, blank=True, related_name='training_logs')
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='training_logs')
reps = models.PositiveIntegerField()
sets = models.PositiveIntegerField(default=1)
time_minutes = models.PositiveIntegerField(null=True, blank=True)
weight_kg = models.DecimalField(null=True, blank=True, max_digits=5, decimal_places=2)
rating = models.PositiveSmallIntegerField(choices=[(1,1),(2,2),(3,3),(4,4),(5,5)], default=3)
notes = models.TextField(blank=True)
logged_at = models.DateTimeField(default=timezone.now)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-logged_at']
indexes = [
models.Index(fields=['wrestler']),
models.Index(fields=['exercise']),
models.Index(fields=['logged_at']),
models.Index(fields=['training']),
]
def __str__(self):
return f"{self.wrestler} - {self.exercise.name} ({self.reps}x{self.sets})"
+27
View File
@@ -0,0 +1,27 @@
from rest_framework import serializers
from .models import TrainingLogEntry
class TrainingLogEntrySerializer(serializers.ModelSerializer):
wrestler_name = serializers.CharField(source='wrestler.__str__', read_only=True)
exercise_name = serializers.CharField(source='exercise.name', read_only=True)
training_date = serializers.DateField(source='training.date', read_only=True)
class Meta:
model = TrainingLogEntry
fields = [
'id', 'wrestler', 'wrestler_name', 'training', 'training_date',
'exercise', 'exercise_name', 'reps', 'sets', 'time_minutes',
'weight_kg', 'rating', 'notes', 'logged_at', 'created_at'
]
class TrainingLogStatsSerializer(serializers.Serializer):
total_entries = serializers.IntegerField()
unique_exercises = serializers.IntegerField()
total_reps = serializers.IntegerField()
avg_sets = serializers.FloatField()
avg_rating = serializers.FloatField()
this_week = serializers.IntegerField()
top_exercises = serializers.ListField(child=serializers.DictField())
progress = serializers.DictField()
+12
View File
@@ -0,0 +1,12 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TrainingLogEntryViewSet, training_log_stats, training_log_compare
router = DefaultRouter()
router.register(r'', TrainingLogEntryViewSet, basename='training-log')
urlpatterns = [
path('stats/', training_log_stats, name='training-log-stats'),
path('compare/', training_log_compare, name='training-log-compare'),
path('', include(router.urls)),
]
+120
View File
@@ -0,0 +1,120 @@
from rest_framework import viewsets
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Count, Avg, Sum, F
from django.db.models.functions import Coalesce
from datetime import datetime, timedelta
from .models import TrainingLogEntry
from .serializers import TrainingLogEntrySerializer
from wrestlers.models import Wrestler
class TrainingLogEntryViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = TrainingLogEntrySerializer
def get_queryset(self):
queryset = TrainingLogEntry.objects.all()
wrestler = self.request.query_params.get('wrestler')
exercise = self.request.query_params.get('exercise')
date_from = self.request.query_params.get('date_from')
date_to = self.request.query_params.get('date_to')
if wrestler:
queryset = queryset.filter(wrestler=wrestler)
if exercise:
queryset = queryset.filter(exercise=exercise)
if date_from:
queryset = queryset.filter(logged_at__date__gte=date_from)
if date_to:
queryset = queryset.filter(logged_at__date__lte=date_to)
return queryset.select_related('wrestler', 'exercise', 'training')
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def training_log_stats(request):
wrestler_id = request.query_params.get('wrestler')
today = datetime.now().date()
week_start = today - timedelta(days=today.weekday())
queryset = TrainingLogEntry.objects.all()
if wrestler_id:
queryset = queryset.filter(wrestler=wrestler_id)
total_entries = queryset.count()
unique_exercises = queryset.values('exercise').distinct().count()
total_reps = queryset.aggregate(total=Coalesce(Sum(F('reps') * F('sets')), 0))['total'] or 0
avg_sets = queryset.aggregate(avg=Avg('sets'))['avg'] or 0
avg_rating = queryset.aggregate(avg=Avg('rating'))['avg'] or 0
this_week = queryset.filter(logged_at__date__gte=week_start).count()
top_exercises = queryset.values('exercise__name').annotate(
count=Count('id')
).order_by('-count')[:5]
progress = {}
exercises = queryset.values('exercise', 'exercise__name').distinct()
for ex in exercises:
ex_id = ex['exercise']
entries = queryset.filter(exercise=ex_id).order_by('logged_at')
if entries.count() >= 2:
first_reps = entries.first().reps * entries.first().sets
last_reps = entries.last().reps * entries.last().sets
if first_reps > 0:
change = ((last_reps - first_reps) / first_reps) * 100
progress[ex['exercise__name']] = {
'before': first_reps,
'after': last_reps,
'change_percent': round(change, 1)
}
return Response({
'total_entries': total_entries,
'unique_exercises': unique_exercises,
'total_reps': total_reps,
'avg_sets': round(avg_sets, 1),
'avg_rating': round(avg_rating, 1),
'this_week': this_week,
'top_exercises': [{'name': e['exercise__name'], 'count': e['count']} for e in top_exercises],
'progress': progress,
})
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def training_log_compare(request):
wrestler1_id = request.query_params.get('wrestler1')
wrestler2_id = request.query_params.get('wrestler2')
if not wrestler1_id or not wrestler2_id:
return Response({'error': 'Both wrestler1 and wrestler2 required'}, status=400)
wrestler1 = Wrestler.objects.get(id=wrestler1_id)
wrestler2 = Wrestler.objects.get(id=wrestler2_id)
entries1 = TrainingLogEntry.objects.filter(wrestler=wrestler1)
entries2 = TrainingLogEntry.objects.filter(wrestler=wrestler2)
exercises1 = entries1.values('exercise', 'exercise__name').distinct()
exercises2 = entries2.values('exercise', 'exercise__name').distinct()
common_exercises = set(e['exercise'] for e in exercises1) & set(e['exercise'] for e in exercises2)
comparison = []
for ex_id in common_exercises:
ex_name = entries1.filter(exercise=ex_id).first().exercise.name
avg1 = entries1.filter(exercise=ex_id).aggregate(avg=Avg(F('reps') * F('sets')))['avg'] or 0
avg2 = entries2.filter(exercise=ex_id).aggregate(avg=Avg(F('reps') * F('sets')))['avg'] or 0
comparison.append({
'exercise': ex_name,
'wrestler1_avg': round(avg1, 1),
'wrestler2_avg': round(avg2, 1)
})
return Response({
'wrestler1': {'id': wrestler1.id, 'name': str(wrestler1)},
'wrestler2': {'id': wrestler2.id, 'name': str(wrestler2)},
'exercises': comparison
})
View File
+22
View File
@@ -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']
+6
View File
@@ -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')},
),
]

Some files were not shown because too many files have changed in this diff Show More