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