Feat: Wiederkehrende Trainingsevents
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.
This commit is contained in:
parent
223dd65382
commit
d45fc54280
@ -12,9 +12,14 @@ class EventForm(forms.ModelForm):
|
||||
class TrainingForm(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'}))
|
||||
end_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'}), required=False)
|
||||
is_recurring = forms.BooleanField(required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}))
|
||||
recurrence_interval = forms.IntegerField(required=False, min_value=1, widget=forms.NumberInput(attrs={'class': 'form-control'}), help_text="Repeat every X days.")
|
||||
recurrence_end_date = forms.DateField(required=False, widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}))
|
||||
|
||||
class Meta:
|
||||
model = Training
|
||||
fields = ['title', 'description', 'start_time', 'end_time', 'location_address', 'team']
|
||||
fields = ['title', 'description', 'start_time', 'end_time', 'location_address', 'team', 'is_recurring', 'recurrence_interval', 'recurrence_end_date']
|
||||
|
||||
|
||||
class GameForm(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'}))
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.2.6 on 2025-11-22 09:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('calendars', '0005_game_number_of_innings_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='is_recurring',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='parent_event',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_events', to='calendars.event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='recurrence_end_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='recurrence_interval',
|
||||
field=models.PositiveIntegerField(blank=True, help_text='In days', null=True),
|
||||
),
|
||||
]
|
||||
@ -10,6 +10,12 @@ class Event(models.Model):
|
||||
location_address = models.CharField(max_length=255)
|
||||
maps_shortlink = models.URLField(blank=True, editable=False)
|
||||
team = models.ForeignKey('clubs.Team', on_delete=models.CASCADE, related_name='events')
|
||||
|
||||
# Fields for recurring events
|
||||
is_recurring = models.BooleanField(default=False)
|
||||
recurrence_interval = models.PositiveIntegerField(null=True, blank=True, help_text="In days")
|
||||
recurrence_end_date = models.DateField(null=True, blank=True)
|
||||
parent_event = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='child_events')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.location_address and not self.maps_shortlink:
|
||||
|
||||
@ -8,9 +8,28 @@
|
||||
<h2>Delete Event</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Are you sure you want to delete "{{ object.title }}"?</p>
|
||||
<p>Are you sure you want to delete the event: <strong>"{{ object.title }}"</strong> on <strong>{{ object.start_time|date:"d.m.Y" }}</strong>?</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if is_series %}
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h6">Deletion Scope</legend>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="delete_scope" id="delete_one" value="one" checked>
|
||||
<label class="form-check-label" for="delete_one">
|
||||
Delete only this event
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="delete_scope" id="delete_all" value="all">
|
||||
<label class="form-check-label" for="delete_all">
|
||||
Delete the entire series
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
<button type="submit" class="btn btn-danger">Confirm Delete</button>
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
|
||||
@ -1,40 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>{% if object %}Edit Event{% else %}Create Event{% endif %}</h2>
|
||||
<h2>{% if object %}Edit Event{% else %}Create Training{% endif %}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
|
||||
{% if object and field.name == 'start_time' %}
|
||||
<p>Current: {{ object.start_time|localize }}</p>
|
||||
{% endif %}
|
||||
{% if object and field.name == 'end_time' %}
|
||||
<p>Current: {{ object.end_time|localize }}</p>
|
||||
{% endif %}
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<small class="form-text text-muted">{{ field.help_text }}</small>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
<!-- Standard Fields -->
|
||||
<div class="mb-3">{{ form.title.label_tag }} {{ form.title }}</div>
|
||||
<div class="mb-3">{{ form.description.label_tag }} {{ form.description }}</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">{{ form.start_time.label_tag }} {{ form.start_time }}</div>
|
||||
<div class="col-md-6 mb-3">{{ form.end_time.label_tag }} {{ form.end_time }}</div>
|
||||
</div>
|
||||
<div class="mb-3">{{ form.location_address.label_tag }} {{ form.location_address }}</div>
|
||||
<div class="mb-3">{{ form.team.label_tag }} {{ form.team }}</div>
|
||||
|
||||
<!-- Recurrence Fields -->
|
||||
{% if 'is_recurring' in form.fields %}
|
||||
<hr>
|
||||
<div class="form-check mb-3">
|
||||
{{ form.is_recurring }}
|
||||
<label class="form-check-label" for="{{ form.is_recurring.id_for_label }}">{{ form.is_recurring.label }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div id="recurrence-options" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
{{ form.recurrence_interval.label_tag }}
|
||||
{{ form.recurrence_interval }}
|
||||
<small class="form-text text-muted">{{ form.recurrence_interval.help_text }}</small>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
{{ form.recurrence_end_date.label_tag }}
|
||||
{{ form.recurrence_end_date }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const recurringCheckbox = document.getElementById('{{ form.is_recurring.id_for_label }}');
|
||||
const recurrenceOptions = document.getElementById('recurrence-options');
|
||||
|
||||
function toggleRecurrenceOptions() {
|
||||
if (recurringCheckbox.checked) {
|
||||
recurrenceOptions.style.display = 'block';
|
||||
} else {
|
||||
recurrenceOptions.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (recurringCheckbox) {
|
||||
// Set initial state on page load
|
||||
toggleRecurrenceOptions();
|
||||
|
||||
// Add event listener
|
||||
recurringCheckbox.addEventListener('change', toggleRecurrenceOptions);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -65,6 +65,38 @@ class TrainingCreateView(LoginRequiredMixin, ManageableTeamsMixin, CreateView):
|
||||
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
|
||||
@ -98,6 +130,29 @@ class EventDeleteView(LoginRequiredMixin, CoachCheckMixin, DeleteView):
|
||||
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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user