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,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
|
||||
})
|
||||
Reference in New Issue
Block a user