feat: Implementierung von Phase 5 (Fortgeschrittene Funktionen und Backend) und Fehlerbehebungen

This commit is contained in:
Matthias Nagel 2025-10-01 09:24:59 +02:00
parent ea8439e616
commit 51bf727885
33 changed files with 344 additions and 11 deletions

Binary file not shown.

View File

@ -1,3 +1,13 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser, AbsencePeriod
# Register your models here.
class CustomUserAdmin(UserAdmin):
model = CustomUser
list_display = ['email', 'username', 'team', 'is_staff']
fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('team',)}),
)
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(AbsencePeriod)

View File

@ -1,6 +1,8 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
def ready(self):
import accounts.signals

View File

@ -26,3 +26,12 @@ class CustomUserChangeForm(forms.ModelForm):
class Meta:
model = CustomUser
fields = ('username', 'first_name', 'last_name', 'email', 'birth_date', 'player_number', 'team')
class PlayerCreationForm(forms.ModelForm):
parent1_email = forms.EmailField(required=False)
parent2_email = forms.EmailField(required=False)
birth_date = forms.DateField(input_formats=['%d.%m.%Y'], widget=forms.DateInput(format='%d.%m.%Y'))
class Meta:
model = CustomUser
fields = ('username', 'first_name', 'last_name', 'email', 'birth_date', 'player_number', 'team')

View File

@ -0,0 +1,24 @@
# Generated by Django 5.2.6 on 2025-10-01 07:05
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_customuser_team'),
]
operations = [
migrations.CreateModel(
name='AbsencePeriod',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_date', models.DateField()),
('end_date', models.DateField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='absence_periods', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -30,4 +30,9 @@ class InvitationCode(models.Model):
return True
def __str__(self):
return self.code
return self.code
class AbsencePeriod(models.Model):
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='absence_periods')
start_date = models.DateField()
end_date = models.DateField()

19
accounts/signals.py Normal file
View File

@ -0,0 +1,19 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import AbsencePeriod
from calendars.models import Event, EventParticipation
@receiver(post_save, sender=AbsencePeriod)
def handle_absence_period(sender, instance, **kwargs):
user = instance.user
events_in_period = Event.objects.filter(
team=user.team,
start_time__date__gte=instance.start_date,
start_time__date__lte=instance.end_date
)
for event in events_in_period:
EventParticipation.objects.update_or_create(
user=user,
event=event,
defaults={'status': 'rejected'}
)

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<h2>Logout</h2>
<p>Are you sure you want to log out?</p>
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit">Log Out</button>
</form>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<h2>Create Player</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Create Player</button>
</form>
{% endblock %}

View File

@ -4,7 +4,14 @@
<title>Baseball Organisator</title>
</head>
<body>
{% if user.is_authenticated %}
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit">Log Out</button>
</form>
{% endif %}
{% block content %}
{% endblock %}
</body>
</html>
</html>

View File

@ -5,7 +5,8 @@ from django.contrib.auth import views as auth_views
urlpatterns = [
path('invitation/', views.invitation_code_view, name='invitation_code'),
path('register/', views.register_view, name='register'),
path('login/', auth_views.LoginView.as_view(template_name='accounts/login.html'), name='login'),
path('login/', views.MyLoginView.as_view(template_name='accounts/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('profile/', views.edit_profile, name='edit_profile'),
path('player/add/', views.PlayerCreateView.as_view(), name='player-add'),
]

View File

@ -1,7 +1,12 @@
from django.shortcuts import render, redirect
from .forms import InvitationCodeForm, CustomUserCreationForm, CustomUserChangeForm
from .models import InvitationCode
from django.urls import reverse_lazy
from django.views.generic.edit import CreateView
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth import views as auth_views
from .forms import InvitationCodeForm, CustomUserCreationForm, CustomUserChangeForm, PlayerCreationForm
from .models import CustomUser, InvitationCode
import uuid
def invitation_code_view(request):
if request.method == 'POST':
@ -52,4 +57,40 @@ def edit_profile(request):
return redirect('edit_profile') # Or wherever you want to redirect
else:
form = CustomUserChangeForm(instance=request.user)
return render(request, 'accounts/edit_profile.html', {'form': form})
return render(request, 'accounts/edit_profile.html', {'form': form})
class HeadCoachCheckMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_superuser or self.request.user.coached_teams.exists()
class PlayerCreateView(LoginRequiredMixin, HeadCoachCheckMixin, CreateView):
model = CustomUser
form_class = PlayerCreationForm
template_name = 'accounts/player_form.html'
success_url = reverse_lazy('dashboard')
def form_valid(self, form):
# Create player user
player = form.save(commit=False)
player.is_active = False # Player can only login after using invitation code
player.save()
# Create invitation code for player
InvitationCode.objects.create(code=str(uuid.uuid4()), user=player)
# Create parent users and invitation codes
for email_field in ['parent1_email', 'parent2_email']:
email = form.cleaned_data.get(email_field)
if email:
parent_user = CustomUser.objects.filter(email=email).first()
if not parent_user:
parent_user = CustomUser.objects.create(email=email, username=email, is_active=False)
InvitationCode.objects.create(code=str(uuid.uuid4()), user=parent_user)
return redirect(self.success_url)
class MyLoginView(auth_views.LoginView):
def get(self, request, *args, **kwargs):
if request.user.is_authenticated:
return redirect('dashboard')
return super().get(request, *args, **kwargs)

Binary file not shown.

View File

@ -0,0 +1,11 @@
from django.contrib import admin
def has_permission(self, request):
if request.user.is_active and request.user.is_staff:
if request.user.is_superuser:
return True
if hasattr(request.user, 'administered_clubs') and request.user.administered_clubs.exists():
return True
return False
admin.site.has_permission = has_permission.__get__(admin.site, admin.AdminSite)

View File

@ -16,6 +16,7 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import path, include
import baseball_organisator.admin
urlpatterns = [
path('admin/', admin.site.urls),

View File

@ -1,7 +1,8 @@
from django.contrib import admin
from .models import Event, Training, Game, GameResult
from .models import Event, Training, Game, GameResult, EventParticipation
admin.site.register(Event)
admin.site.register(Training)
admin.site.register(Game)
admin.site.register(GameResult)
admin.site.register(GameResult)
admin.site.register(EventParticipation)

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.6 on 2025-10-01 07:05
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('calendars', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='EventParticipation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('attending', 'Attending'), ('rejected', 'Rejected'), ('maybe', 'Maybe')], default='maybe', max_length=20)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='calendars.event')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'event')},
},
),
]

View File

@ -34,4 +34,12 @@ class GameResult(models.Model):
inning_results = models.CharField(max_length=255, help_text="Comma-separated scores per inning, e.g., '1-0,0-2,3-1'")
def __str__(self):
return f"Result for {self.game}"
return f"Result for {self.game}"
class EventParticipation(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
status = models.CharField(max_length=20, choices=[('attending', 'Attending'), ('rejected', 'Rejected'), ('maybe', 'Maybe')], default='maybe')
class Meta:
unique_together = ('user', 'event')

Binary file not shown.

29
docs/phase5.md Normal file
View File

@ -0,0 +1,29 @@
## Phase 5: Fortgeschrittene Funktionen und Backend
Diese Phase umfasst komplexere Features wie die Spieleranlage, die Abwesenheitsfunktion und das Backend-Management.
Schritt 1: Spieleranlage-Workflow
Implementiere in den accounts-Views einen Workflow für Headcoaches, um neue Spieler anzulegen.
Die Logik muss die automatische Generierung von Einladungscodes für den Spieler und optional für bis zu zwei Elternteile umfassen.
Die Überprüfung des Alters und die automatische Zuweisung eines Standard-Geburtsdatums muss implementiert werden.
Schritt 2: Abwesenheitsfunktion implementieren
Erstelle in accounts/models.py ein Modell AbsencePeriod mit Feldern start_date, end_date und einer Verknüpfung zum User.
Implementiere in den Views die Logik, die alle Termine in diesem Zeitraum automatisch auf "abgelehnt" setzt.
Schritt 3: Django-Admin-Bereich anpassen
Konfiguriere den Django-Admin-Bereich, sodass nur Club-Admins und der Superuser darauf zugreifen können.
Registriere die erstellten Modelle im Admin-Bereich, um die Verwaltung zu erleichtern.
Schritt 4: User-Migration und Logik für Rollen-Änderungen
Schreibe die Migrationsdateien für alle erstellten Modelle.
Implementiere die Logik für den Wechsel der Rollen, z. B. wenn ein Spieler über 18 Jahre alt wird.

70
docs/traceback/trace2.log Normal file
View File

@ -0,0 +1,70 @@
Exception in thread django-main-thread:
Traceback (most recent call last):
File "/usr/lib64/python3.13/threading.py", line 1043, in _bootstrap_inner
self.run()
~~~~~~~~^^
File "/usr/lib64/python3.13/threading.py", line 994, in run
self._target(*self._args, **self._kwargs)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/autoreload.py", line 64, in wrapper
fn(*args, **kwargs)
~~^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/management/commands/runserver.py", line 134, in inner_run
self.check(**check_kwargs)
~~~~~~~~~~^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/management/base.py", line 492, in check
all_issues = checks.run_checks(
app_configs=app_configs,
...<2 lines>...
databases=databases,
)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/checks/registry.py", line 89, in run_checks
new_errors = check(app_configs=app_configs, databases=databases)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/checks/urls.py", line 16, in check_url_config
return check_resolver(resolver)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/checks/urls.py", line 26, in check_resolver
return check_method()
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/urls/resolvers.py", line 531, in check
for pattern in self.url_patterns:
^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/functional.py", line 47, in __get__
res = instance.__dict__[self.name] = self.func(instance)
~~~~~~~~~^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/urls/resolvers.py", line 718, in url_patterns
patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
^^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/utils/functional.py", line 47, in __get__
res = instance.__dict__[self.name] = self.func(instance)
~~~~~~~~~^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/urls/resolvers.py", line 711, in urlconf_module
return import_module(self.urlconf_name)
File "/usr/lib64/python3.13/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/home/mnagel/Projekte/baseball_organisator/baseball_organisator/urls.py", line 23, in <module>
path('accounts/', include('accounts.urls')),
~~~~~~~^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/urls/conf.py", line 39, in include
urlconf_module = import_module(urlconf_module)
File "/usr/lib64/python3.13/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/home/mnagel/Projekte/baseball_organisator/accounts/urls.py", line 2, in <module>
from . import views
File "/home/mnagel/Projekte/baseball_organisator/accounts/views.py", line 2, in <module>
class MyLoginView(auth_views.LoginView):
^^^^^^^^^^
NameError: name 'auth_views' is not defined

47
docs/traceback/trace3.log Normal file
View File

@ -0,0 +1,47 @@
[01/Oct/2025 07:16:54] "GET / HTTP/1.1" 302 0
[01/Oct/2025 07:17:22] "GET / HTTP/1.1" 200 540
[01/Oct/2025 07:17:27] "POST /accounts/logout/ HTTP/1.1" 200 3420
[01/Oct/2025 07:17:31] "GET / HTTP/1.1" 302 0
[01/Oct/2025 07:17:31] "GET /accounts/login/?next=/ HTTP/1.1" 200 727
[01/Oct/2025 07:17:37] "POST /accounts/login/?next=/ HTTP/1.1" 302 0
[01/Oct/2025 07:17:37] "GET / HTTP/1.1" 200 540
[01/Oct/2025 07:17:49] "GET /accounts/player/add/ HTTP/1.1" 200 2330
Internal Server Error: /accounts/player/add/
Traceback (most recent call last):
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/views/generic/base.py", line 105, in view
return self.dispatch(request, *args, **kwargs)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/contrib/auth/mixins.py", line 73, in dispatch
return super().dispatch(request, *args, **kwargs)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/contrib/auth/mixins.py", line 135, in dispatch
return super().dispatch(request, *args, **kwargs)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/views/generic/base.py", line 144, in dispatch
return handler(request, *args, **kwargs)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/views/generic/edit.py", line 182, in post
return super().post(request, *args, **kwargs)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/views/generic/edit.py", line 151, in post
return self.form_valid(form)
~~~~~~~~~~~~~~~^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/accounts/views.py", line 85, in form_valid
parent_user, created = CustomUser.objects.get_or_create(email=email, defaults={'username': email, 'is_active': False})
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/db/models/manager.py", line 87, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/db/models/query.py", line 946, in get_or_create
return self.get(**kwargs), False
~~~~~~~~^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/db/models/query.py", line 636, in get
raise self.model.MultipleObjectsReturned(
...<5 lines>...
)
accounts.models.CustomUser.MultipleObjectsReturned: get() returned more than one CustomUser -- it returned 2!
[01/Oct/2025 07:18:48] "POST /accounts/player/add/ HTTP/1.1" 500 112859