Fügt die Funktionalität hinzu, wiederkehrende Trainingsevents zu erstellen,
zu verwalten und zu löschen. Ein Coach kann nun ein Training erstellen,
das sich alle X Tage bis zu einem bestimmten Enddatum wiederholt.
Wesentliche Änderungen:
- **Datenmodell ():** Das -Modell wurde um Felder
für die Wiederholung (, ,
) und zur Gruppierung von Serien ()
erweitert.
- **Formulare ():** Das Formular zur Erstellung von Trainings
wurde um die neuen Wiederholungsoptionen erweitert.
- **Views:**
- : Die Logik wurde erweitert, um beim Speichern
eines wiederkehrenden Events automatisch alle zukünftigen Instanzen
der Serie zu erstellen.
- : Bietet nun die Möglichkeit, entweder nur ein
einzelnes Event einer Serie oder die gesamte Serie zu löschen.
- **Templates:**
- : Enthält jetzt die neuen Formularfelder mit
JavaScript, um die Wiederholungsoptionen dynamisch ein- und
auszublenden.
- : Zeigt eine Auswahlmöglichkeit für den
Löschumfang an, wenn das Event Teil einer Serie ist.
- **Migration:** Eine neue Datenbankmigration wurde erstellt, um die
Änderungen am -Modell anzuwenden.
249 lines
10 KiB
Python
249 lines
10 KiB
Python
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
|
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, GameResult
|
|
from .forms import EventForm, TrainingForm, GameForm, OpenGameForm, GameResultForm
|
|
from accounts.models import CustomUser # Import CustomUser for manage_participation view
|
|
from clubs.models import Team
|
|
from django.utils import timezone
|
|
import datetime
|
|
|
|
def select_event_type(request):
|
|
return render(request, 'calendars/select_event_type.html')
|
|
|
|
def get_all_child_teams(parent_team):
|
|
"""
|
|
Iteratively gets all child teams for a given team.
|
|
"""
|
|
children = []
|
|
teams_to_check = list(parent_team.child_teams.all())
|
|
while teams_to_check:
|
|
team = teams_to_check.pop(0)
|
|
if team not in children:
|
|
children.append(team)
|
|
teams_to_check.extend(list(team.child_teams.all()))
|
|
return children
|
|
|
|
class ManageableTeamsMixin:
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
user = self.request.user
|
|
if not user.is_superuser:
|
|
coached_teams_qs = user.coached_teams.all()
|
|
|
|
expanded_coached_teams = list(coached_teams_qs)
|
|
for team in coached_teams_qs:
|
|
expanded_coached_teams.extend(get_all_child_teams(team))
|
|
|
|
assisted_teams_qs = user.assisted_teams.all()
|
|
|
|
# Combine and get unique teams
|
|
managed_teams_ids = {team.id for team in expanded_coached_teams} | {team.id for team in assisted_teams_qs}
|
|
|
|
form.fields['team'].queryset = Team.objects.filter(id__in=managed_teams_ids)
|
|
return form
|
|
|
|
class CoachCheckMixin(UserPassesTestMixin):
|
|
def test_func(self):
|
|
user = self.request.user
|
|
if user.is_superuser:
|
|
return True
|
|
team = self.get_object().team
|
|
return user == team.head_coach or user in team.assistant_coaches.all()
|
|
|
|
class EventCreateView(LoginRequiredMixin, ManageableTeamsMixin, CreateView):
|
|
model = Event
|
|
form_class = EventForm
|
|
template_name = 'calendars/event_form.html'
|
|
success_url = reverse_lazy('dashboard')
|
|
|
|
class TrainingCreateView(LoginRequiredMixin, ManageableTeamsMixin, CreateView):
|
|
model = Training
|
|
form_class = TrainingForm
|
|
template_name = 'calendars/event_form.html'
|
|
success_url = reverse_lazy('dashboard')
|
|
|
|
def form_valid(self, form):
|
|
is_recurring = form.cleaned_data.get('is_recurring')
|
|
interval = form.cleaned_data.get('recurrence_interval')
|
|
end_date = form.cleaned_data.get('recurrence_end_date')
|
|
|
|
if is_recurring and interval and end_date:
|
|
# Save the first event, which will be the parent
|
|
self.object = form.save()
|
|
|
|
# Get details from the parent event
|
|
start_time = self.object.start_time
|
|
duration = self.object.end_time - start_time if self.object.end_time else datetime.timedelta(hours=1)
|
|
|
|
current_start_time = start_time + datetime.timedelta(days=interval)
|
|
|
|
while current_start_time.date() <= end_date:
|
|
Training.objects.create(
|
|
title=self.object.title,
|
|
description=self.object.description,
|
|
start_time=current_start_time,
|
|
end_time=current_start_time + duration,
|
|
location_address=self.object.location_address,
|
|
team=self.object.team,
|
|
parent_event=self.object,
|
|
is_recurring=False # Child events are not themselves recurring
|
|
)
|
|
current_start_time += datetime.timedelta(days=interval)
|
|
|
|
return redirect(self.get_success_url())
|
|
else:
|
|
return super().form_valid(form)
|
|
|
|
class GameCreateView(LoginRequiredMixin, ManageableTeamsMixin, CreateView):
|
|
model = Game
|
|
form_class = GameForm
|
|
template_name = 'calendars/event_form.html'
|
|
success_url = reverse_lazy('dashboard')
|
|
|
|
class EventUpdateView(LoginRequiredMixin, CoachCheckMixin, UpdateView):
|
|
model = Event
|
|
template_name = 'calendars/event_form.html'
|
|
success_url = reverse_lazy('dashboard')
|
|
|
|
def get_object(self, queryset=None):
|
|
# Fetch the event and then check if it's a game or training to return the specific instance
|
|
obj = super().get_object(queryset)
|
|
if hasattr(obj, 'game'):
|
|
return obj.game
|
|
if hasattr(obj, 'training'):
|
|
return obj.training
|
|
return obj
|
|
|
|
def get_form_class(self):
|
|
# self.object is now the correct child instance because of the overridden get_object
|
|
if isinstance(self.object, Game):
|
|
return GameForm
|
|
if isinstance(self.object, Training):
|
|
return TrainingForm
|
|
return EventForm
|
|
|
|
class EventDeleteView(LoginRequiredMixin, CoachCheckMixin, DeleteView):
|
|
model = Event
|
|
template_name = 'calendars/event_confirm_delete.html'
|
|
success_url = reverse_lazy('dashboard')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
# An event is part of a series if it has a parent or is a parent
|
|
context['is_series'] = self.object.parent_event is not None or self.object.child_events.exists()
|
|
return context
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
delete_scope = request.POST.get('delete_scope')
|
|
|
|
if delete_scope == 'all':
|
|
# Identify the parent of the series
|
|
parent = self.object.parent_event if self.object.parent_event else self.object
|
|
|
|
# Delete all children and the parent itself
|
|
parent.child_events.all().delete()
|
|
parent.delete()
|
|
|
|
return redirect(self.success_url)
|
|
else:
|
|
# Default behavior: delete only the single event
|
|
return super().post(request, *args, **kwargs)
|
|
|
|
@login_required
|
|
def manage_participation(request, child_id, event_id, status):
|
|
child = get_object_or_404(CustomUser, id=child_id)
|
|
event = get_object_or_404(Event, id=event_id)
|
|
|
|
# Check if the logged-in user is a parent of the child
|
|
if request.user not in child.parents.all():
|
|
# 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})
|
|
|
|
@login_required
|
|
def record_results(request, game_id):
|
|
game = get_object_or_404(Game, id=game_id)
|
|
game_result, created = GameResult.objects.get_or_create(game=game)
|
|
|
|
if request.method == 'POST':
|
|
form = GameResultForm(request.POST, game=game, instance=game_result)
|
|
if form.is_valid():
|
|
inning_results = {}
|
|
for i in range(1, game.number_of_innings + 1):
|
|
inning_results[f'inning_{i}'] = {
|
|
'home': form.cleaned_data.get(f'inning_{i}_home'),
|
|
'guest': form.cleaned_data.get(f'inning_{i}_guest'),
|
|
}
|
|
game_result.inning_results = inning_results
|
|
game_result.save()
|
|
return redirect('dashboard')
|
|
else:
|
|
initial_data = {}
|
|
if game_result.inning_results:
|
|
for inning, scores in game_result.inning_results.items():
|
|
initial_data[f'{inning}_home'] = scores.get('home')
|
|
initial_data[f'{inning}_guest'] = scores.get('guest')
|
|
form = GameResultForm(game=game, instance=game_result, initial=initial_data)
|
|
|
|
form_fields_by_inning = []
|
|
for i in range(1, game.number_of_innings + 1):
|
|
form_fields_by_inning.append({
|
|
'inning': i,
|
|
'home': form[f'inning_{i}_home'],
|
|
'guest': form[f'inning_{i}_guest'],
|
|
})
|
|
|
|
return render(request, 'calendars/record_results.html', {'form': form, 'game': game, 'form_fields_by_inning': form_fields_by_inning})
|