feat: Implementierung der 'Spiel öffnen'-Funktionalität und Fehlerbehebungen

This commit is contained in:
Matthias Nagel 2025-10-01 14:35:31 +02:00
parent 84a37206fd
commit 0d5357793e
18 changed files with 301 additions and 5 deletions

View File

@ -1,5 +1,6 @@
from django import forms
from .models import Event, Training, Game
from clubs.models import Team
class EventForm(forms.ModelForm):
start_time = forms.DateTimeField(input_formats=['%d.%m.%Y %H:%M', '%Y-%m-%dT%H:%M'], widget=forms.DateTimeInput(format='%d.%m.%Y %H:%M', attrs={'type': 'datetime-local'}))
@ -21,3 +22,11 @@ class GameForm(forms.ModelForm):
class Meta:
model = Game
fields = ['title', 'description', 'start_time', 'end_time', 'location_address', 'team', 'opponent', 'meeting_minutes_before_game', 'season', 'min_players']
class OpenGameForm(forms.Form):
teams = forms.ModelMultipleChoiceField(queryset=Team.objects.none(), widget=forms.CheckboxSelectMultiple)
def __init__(self, *args, **kwargs):
club = kwargs.pop('club')
super().__init__(*args, **kwargs)
self.fields['teams'].queryset = Team.objects.filter(club=club)

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.6 on 2025-10-01 12:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('calendars', '0002_eventparticipation'),
('clubs', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='game',
name='opened_for_teams',
field=models.ManyToManyField(blank=True, related_name='opened_games', to='clubs.team'),
),
]

View File

@ -27,6 +27,7 @@ class Game(Event):
meeting_minutes_before_game = models.PositiveIntegerField(default=60)
season = models.CharField(max_length=255, blank=True)
min_players = models.PositiveIntegerField(default=9)
opened_for_teams = models.ManyToManyField('clubs.Team', related_name='opened_games', blank=True)
class GameResult(models.Model):
game = models.OneToOneField(Game, on_delete=models.CASCADE, related_name='result')

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2>Open Game for Other Teams</h2>
</div>
<div class="card-body">
<p>Select teams to open the game "{{ game.title }}" for.</p>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Open Game</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -9,4 +9,5 @@ urlpatterns = [
path('event/<int:pk>/', views.EventUpdateView.as_view(), name='event-update'),
path('event/<int:pk>/delete/', views.EventDeleteView.as_view(), name='event-delete'),
path('participation/<int:child_id>/<int:event_id>/<str:status>/', views.manage_participation, name='manage-participation'),
path('game/<int:game_id>/open/', views.open_game, name='open-game'),
]

View File

@ -4,8 +4,10 @@ from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.decorators import login_required
from django.urls import reverse_lazy
from .models import Event, Training, Game, EventParticipation
from .forms import EventForm, TrainingForm, GameForm
from .forms import EventForm, TrainingForm, GameForm, OpenGameForm
from accounts.models import CustomUser # Import CustomUser for manage_participation view
from django.utils import timezone
import datetime
def select_event_type(request):
return render(request, 'calendars/select_event_type.html')
@ -87,8 +89,51 @@ def manage_participation(request, child_id, event_id, status):
# Handle unauthorized access
return redirect('dashboard')
# Check for parallel events if accepting a support game
if status == 'attending' and hasattr(event, 'game') and child.team in event.game.opened_for_teams.all():
# A game's duration is defined as innings * 20 minutes + 1 hour travel.
# I will assume 9 innings for now.
game_duration = datetime.timedelta(minutes=(9 * 20 + 60))
event_start = event.start_time
event_end = event.start_time + game_duration
parallel_events = Event.objects.filter(
eventparticipation__user=child,
eventparticipation__status='attending'
).exclude(id=event.id)
for pe in parallel_events:
pe_duration = datetime.timedelta(minutes=(9 * 20 + 60)) # Assuming 9 innings for all games
pe_start = pe.start_time
pe_end = pe.start_time + pe_duration
# Check for overlap with a tolerance of +/- 2 hours
if (event_start < pe_end + datetime.timedelta(hours=2) and event_end > pe_start - datetime.timedelta(hours=2)):
# Handle parallel event conflict
return redirect('dashboard') # Or show an error message
participation, created = EventParticipation.objects.get_or_create(user=child, event=event)
participation.status = status
participation.save()
return redirect('dashboard')
@login_required
def open_game(request, game_id):
game = get_object_or_404(Game, id=game_id)
club = game.team.club
# Permission check: only head coach of the team's club can open the game
if not request.user.is_superuser and request.user not in club.administrators.all():
return redirect('dashboard')
if request.method == 'POST':
form = OpenGameForm(request.POST, club=club)
if form.is_valid():
teams = form.cleaned_data['teams']
game.opened_for_teams.add(*teams)
return redirect('dashboard')
else:
form = OpenGameForm(club=club)
return render(request, 'calendars/open_game.html', {'form': form, 'game': game})

View File

@ -18,6 +18,9 @@
<div class="list-group-item list-group-item-action flex-column align-items-start
{% if item.event.game %}
event-game
{% if user.team in item.event.game.opened_for_teams.all %}
support-game
{% endif %}
{% elif item.event.training %}
event-training
{% else %}
@ -38,6 +41,10 @@
<a href="{% url 'event-update' item.event.pk %}" class="btn btn-warning btn-sm">Edit</a>
<a href="{% url 'event-delete' item.event.pk %}" class="btn btn-danger btn-sm">Delete</a>
{% if item.event.game and item.days_until_event >= 0 and item.days_until_event < 7 and item.accepted_count < item.required_players and item.event.team.club.teams.count > 1 %}
<a href="{% url 'open-game' item.event.game.id %}" class="btn btn-info btn-sm">Open Game</a>
{% endif %}
<div class="mt-3">
<h6>Player Participation ({{ item.accepted_count }}/{{ item.required_players }})</h6>
<div style="column-count: 3;">

View File

@ -2,6 +2,7 @@ from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from calendars.models import Event, EventParticipation
from clubs.models import Team
from django.utils import timezone
@login_required
def dashboard(request):
@ -17,8 +18,11 @@ def dashboard(request):
assisted_teams = user.assisted_teams.all()
from itertools import chain
all_teams = list(set(chain(player_teams, coached_teams, assisted_teams)))
if all_teams:
events = Event.objects.filter(team__in=all_teams).select_related('game', 'training').prefetch_related('team__players', 'eventparticipation_set__user').order_by('start_time')
user_events = Event.objects.filter(team__in=all_teams)
opened_games = Event.objects.filter(game__opened_for_teams__in=all_teams)
events = (user_events | opened_games).distinct().select_related('game', 'training').prefetch_related('team__players', 'eventparticipation_set__user').order_by('start_time')
for event in events:
participations = event.eventparticipation_set.all()
@ -33,11 +37,14 @@ def dashboard(request):
status = participation_map.get(player.id, 'maybe')
player_participations.append({'player': player, 'status': status})
days_until_event = (event.start_time - timezone.now()).days
events_with_participation.append({
'event': event,
'accepted_count': accepted_count,
'required_players': required_players,
'player_participations': player_participations
'player_participations': player_participations,
'days_until_event': days_until_event
})
# Get children's events
@ -45,7 +52,10 @@ def dashboard(request):
for child in user.children.all():
child_events_list = []
if child.team:
child_events = Event.objects.filter(team=child.team).select_related('game', 'training').order_by('start_time')
child_user_events = Event.objects.filter(team=child.team)
child_opened_games = Event.objects.filter(game__opened_for_teams=child.team)
child_events = (child_user_events | child_opened_games).distinct().select_related('game', 'training').order_by('start_time')
for event in child_events:
participation, created = EventParticipation.objects.get_or_create(user=child, event=event)
child_events_list.append({'event': event, 'participation': participation})
@ -54,5 +64,6 @@ def dashboard(request):
context = {
'events_with_participation': events_with_participation,
'children_events': children_events,
'now': timezone.now()
}
return render(request, 'dashboard/dashboard.html', context)

Binary file not shown.

71
docs/traceback/trace6.log Normal file
View File

@ -0,0 +1,71 @@
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 25, in <module>
path('calendars/', include('calendars.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/calendars/urls.py", line 2, in <module>
from . import views
File "/home/mnagel/Projekte/baseball_organisator/calendars/views.py", line 7, in <module>
from .forms import EventForm, TrainingForm, GameForm
File "/home/mnagel/Projekte/baseball_organisator/calendars/forms.py", line 1, in <module>
class OpenGameForm(forms.Form):
^^^^^
NameError: name 'forms' is not defined. Did you mean: 'format'?

108
docs/traceback/trace7.log Normal file
View File

@ -0,0 +1,108 @@
Internal Server Error: /
Traceback (most recent call last):
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/smartif.py", line 180, in translate_token
op = OPERATORS[token]
~~~~~~~~~^^^^^^^
KeyError: '(item.event.start_time'
During handling of the above exception, another exception occurred:
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/contrib/auth/decorators.py", line 59, in _view_wrapper
return view_func(request, *args, **kwargs)
File "/home/mnagel/Projekte/baseball_organisator/dashboard/views.py", line 66, in dashboard
return render(request, 'dashboard/dashboard.html', context)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/shortcuts.py", line 25, in render
content = loader.render_to_string(template_name, context, request, using=using)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/loader.py", line 61, in render_to_string
template = get_template(template_name, using=using)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/loader.py", line 15, in get_template
return engine.get_template(template_name)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/backends/django.py", line 79, in get_template
return Template(self.engine.get_template(template_name), self)
~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/engine.py", line 177, in get_template
template, origin = self.find_template(template_name)
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/engine.py", line 159, in find_template
template = loader.get_template(name, skip=skip)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/loaders/cached.py", line 57, in get_template
template = super().get_template(template_name, skip)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/loaders/base.py", line 28, in get_template
return Template(
contents,
...<2 lines>...
self.engine,
)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 154, in __init__
self.nodelist = self.compile_nodelist()
~~~~~~~~~~~~~~~~~~~~~^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 196, in compile_nodelist
nodelist = parser.parse()
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 518, in parse
raise self.error(token, e)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 516, in parse
compiled_result = compile_func(self, token)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/loader_tags.py", line 299, in do_extends
nodelist = parser.parse()
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 518, in parse
raise self.error(token, e)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 516, in parse
compiled_result = compile_func(self, token)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/loader_tags.py", line 234, in do_block
nodelist = parser.parse(("endblock",))
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 518, in parse
raise self.error(token, e)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 516, in parse
compiled_result = compile_func(self, token)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/defaulttags.py", line 962, in do_if
nodelist = parser.parse(("elif", "else", "endif"))
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 518, in parse
raise self.error(token, e)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 516, in parse
compiled_result = compile_func(self, token)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/defaulttags.py", line 862, in do_for
nodelist_loop = parser.parse(
(
...<2 lines>...
)
)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 518, in parse
raise self.error(token, e)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 516, in parse
compiled_result = compile_func(self, token)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/defaulttags.py", line 962, in do_if
nodelist = parser.parse(("elif", "else", "endif"))
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 518, in parse
raise self.error(token, e)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 516, in parse
compiled_result = compile_func(self, token)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/defaulttags.py", line 961, in do_if
condition = TemplateIfParser(parser, bits).parse()
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/defaulttags.py", line 894, in __init__
super().__init__(*args, **kwargs)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/smartif.py", line 171, in __init__
mapped_tokens.append(self.translate_token(token))
~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/smartif.py", line 182, in translate_token
return self.create_var(token)
~~~~~~~~~~~~~~~^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/defaulttags.py", line 897, in create_var
return TemplateLiteral(self.template_parser.compile_filter(value), value)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 609, in compile_filter
return FilterExpression(token, self)
File "/home/mnagel/Projekte/baseball_organisator/venv/lib64/python3.13/site-packages/django/template/base.py", line 710, in __init__
raise TemplateSyntaxError(
...<2 lines>...
)
django.template.exceptions.TemplateSyntaxError: Could not parse the remainder: '(item.event.start_time' from '(item.event.start_time'
[01/Oct/2025 12:32:40] "GET / HTTP/1.1" 500 399373

View File

@ -14,6 +14,9 @@
.event-generic {
border-left: 5px solid #6c757d; /* gray */
}
.support-game {
border: 2px solid #ffc107; /* yellow */
}
</style>
</head>
<body>