diff --git a/calendars/__pycache__/forms.cpython-313.pyc b/calendars/__pycache__/forms.cpython-313.pyc index 2ae9b99..8af97fd 100644 Binary files a/calendars/__pycache__/forms.cpython-313.pyc and b/calendars/__pycache__/forms.cpython-313.pyc differ diff --git a/calendars/__pycache__/models.cpython-313.pyc b/calendars/__pycache__/models.cpython-313.pyc index 0e5eafd..e1c44e0 100644 Binary files a/calendars/__pycache__/models.cpython-313.pyc and b/calendars/__pycache__/models.cpython-313.pyc differ diff --git a/calendars/__pycache__/urls.cpython-313.pyc b/calendars/__pycache__/urls.cpython-313.pyc index 149e62e..c81d853 100644 Binary files a/calendars/__pycache__/urls.cpython-313.pyc and b/calendars/__pycache__/urls.cpython-313.pyc differ diff --git a/calendars/__pycache__/views.cpython-313.pyc b/calendars/__pycache__/views.cpython-313.pyc index 7bacaaa..a9417b5 100644 Binary files a/calendars/__pycache__/views.cpython-313.pyc and b/calendars/__pycache__/views.cpython-313.pyc differ diff --git a/calendars/forms.py b/calendars/forms.py index ceb3f5a..7150976 100644 --- a/calendars/forms.py +++ b/calendars/forms.py @@ -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) diff --git a/calendars/migrations/0003_game_opened_for_teams.py b/calendars/migrations/0003_game_opened_for_teams.py new file mode 100644 index 0000000..69c84af --- /dev/null +++ b/calendars/migrations/0003_game_opened_for_teams.py @@ -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'), + ), + ] diff --git a/calendars/migrations/__pycache__/0003_game_opened_for_teams.cpython-313.pyc b/calendars/migrations/__pycache__/0003_game_opened_for_teams.cpython-313.pyc new file mode 100644 index 0000000..f84d418 Binary files /dev/null and b/calendars/migrations/__pycache__/0003_game_opened_for_teams.cpython-313.pyc differ diff --git a/calendars/models.py b/calendars/models.py index 5ec7b39..a19db7f 100644 --- a/calendars/models.py +++ b/calendars/models.py @@ -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') diff --git a/calendars/templates/calendars/open_game.html b/calendars/templates/calendars/open_game.html new file mode 100644 index 0000000..215e075 --- /dev/null +++ b/calendars/templates/calendars/open_game.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+

Open Game for Other Teams

+
+
+

Select teams to open the game "{{ game.title }}" for.

+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+
+
+
+{% endblock %} diff --git a/calendars/urls.py b/calendars/urls.py index 873c0cb..f87647a 100644 --- a/calendars/urls.py +++ b/calendars/urls.py @@ -9,4 +9,5 @@ urlpatterns = [ path('event//', views.EventUpdateView.as_view(), name='event-update'), path('event//delete/', views.EventDeleteView.as_view(), name='event-delete'), path('participation////', views.manage_participation, name='manage-participation'), + path('game//open/', views.open_game, name='open-game'), ] diff --git a/calendars/views.py b/calendars/views.py index c6e113f..340192d 100644 --- a/calendars/views.py +++ b/calendars/views.py @@ -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') \ No newline at end of file + 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}) \ No newline at end of file diff --git a/dashboard/__pycache__/views.cpython-313.pyc b/dashboard/__pycache__/views.cpython-313.pyc index 541f399..d9b5c18 100644 Binary files a/dashboard/__pycache__/views.cpython-313.pyc and b/dashboard/__pycache__/views.cpython-313.pyc differ diff --git a/dashboard/templates/dashboard/dashboard.html b/dashboard/templates/dashboard/dashboard.html index e78447a..9f89bf6 100644 --- a/dashboard/templates/dashboard/dashboard.html +++ b/dashboard/templates/dashboard/dashboard.html @@ -18,6 +18,9 @@
Edit Delete + {% 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 %} + Open Game + {% endif %} +
Player Participation ({{ item.accepted_count }}/{{ item.required_players }})
diff --git a/dashboard/views.py b/dashboard/views.py index 29beb00..2ed8004 100644 --- a/dashboard/views.py +++ b/dashboard/views.py @@ -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) \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index 6630a75..94aeee3 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/docs/traceback/trace6.log b/docs/traceback/trace6.log new file mode 100644 index 0000000..66322c7 --- /dev/null +++ b/docs/traceback/trace6.log @@ -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 "", line 1387, in _gcd_import + File "", line 1360, in _find_and_load + File "", line 1331, in _find_and_load_unlocked + File "", line 935, in _load_unlocked + File "", line 1026, in exec_module + File "", line 488, in _call_with_frames_removed + File "/home/mnagel/Projekte/baseball_organisator/baseball_organisator/urls.py", line 25, in + 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 "", line 1387, in _gcd_import + File "", line 1360, in _find_and_load + File "", line 1331, in _find_and_load_unlocked + File "", line 935, in _load_unlocked + File "", line 1026, in exec_module + File "", line 488, in _call_with_frames_removed + File "/home/mnagel/Projekte/baseball_organisator/calendars/urls.py", line 2, in + from . import views + File "/home/mnagel/Projekte/baseball_organisator/calendars/views.py", line 7, in + from .forms import EventForm, TrainingForm, GameForm + File "/home/mnagel/Projekte/baseball_organisator/calendars/forms.py", line 1, in + class OpenGameForm(forms.Form): + ^^^^^ +NameError: name 'forms' is not defined. Did you mean: 'format'? + diff --git a/docs/traceback/trace7.log b/docs/traceback/trace7.log new file mode 100644 index 0000000..db95b21 --- /dev/null +++ b/docs/traceback/trace7.log @@ -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 + diff --git a/templates/base.html b/templates/base.html index 79edd80..140cc5d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -14,6 +14,9 @@ .event-generic { border-left: 5px solid #6c757d; /* gray */ } + .support-game { + border: 2px solid #ffc107; /* yellow */ + }