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