diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index f805a3e..f417e72 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 148c612..e7615ff 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/forms.py b/core/forms.py index 97aca69..8bc2105 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,8 +1,21 @@ +from datetime import timedelta + from django import forms from .models import Event +WEEKDAY_CHOICES = [ + ('0', 'Monday'), + ('1', 'Tuesday'), + ('2', 'Wednesday'), + ('3', 'Thursday'), + ('4', 'Friday'), + ('5', 'Saturday'), + ('6', 'Sunday'), +] + + class EventForm(forms.ModelForm): start = forms.DateTimeField( input_formats=['%Y-%m-%dT%H:%M'], @@ -18,6 +31,25 @@ class EventForm(forms.ModelForm): format='%Y-%m-%dT%H:%M', ), ) + recurrence_start_date = forms.DateField( + required=False, + label='Repeat from', + widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + help_text='Optional: first date in the weekly recurrence window.', + ) + recurrence_end_date = forms.DateField( + required=False, + label='Repeat until', + widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + help_text='Optional: last date in the weekly recurrence window.', + ) + recurrence_weekday = forms.ChoiceField( + required=False, + label='Weekday', + choices=[('', 'Select a weekday')] + WEEKDAY_CHOICES, + widget=forms.Select(attrs={'class': 'form-select'}), + help_text='Optional: create one event each week on this weekday.', + ) class Meta: model = Event @@ -46,4 +78,28 @@ class EventForm(forms.ModelForm): end = cleaned_data.get('end') if start and end and end <= start: self.add_error('end', 'End time must be after the start time.') + + recurrence_start_date = cleaned_data.get('recurrence_start_date') + recurrence_end_date = cleaned_data.get('recurrence_end_date') + recurrence_weekday = cleaned_data.get('recurrence_weekday') + recurrence_requested = any([recurrence_start_date, recurrence_end_date, recurrence_weekday]) + + if recurrence_requested: + if not recurrence_start_date: + self.add_error('recurrence_start_date', 'Enter the first date for the recurring series.') + if not recurrence_end_date: + self.add_error('recurrence_end_date', 'Enter the last date for the recurring series.') + if not recurrence_weekday: + self.add_error('recurrence_weekday', 'Choose which weekday should repeat each week.') + + if recurrence_start_date and recurrence_end_date and recurrence_end_date < recurrence_start_date: + self.add_error('recurrence_end_date', 'The recurring series must end on or after the start date.') + + if recurrence_start_date and recurrence_end_date and recurrence_weekday: + weekday_index = int(recurrence_weekday) + days_until_first = (weekday_index - recurrence_start_date.weekday()) % 7 + first_occurrence = recurrence_start_date + timedelta(days=days_until_first) + if first_occurrence > recurrence_end_date: + self.add_error('recurrence_weekday', 'No selected weekday falls inside that recurrence date range.') + return cleaned_data diff --git a/core/templates/core/event_form.html b/core/templates/core/event_form.html index f90b3fe..29f2bf1 100644 --- a/core/templates/core/event_form.html +++ b/core/templates/core/event_form.html @@ -16,23 +16,81 @@
{% csrf_token %}
- {% for field in form %} -
- {% if field.name == 'is_published' %} -
- {{ field }} - - {% if field.help_text %}
{{ field.help_text }}
{% endif %} -
- {% else %} - - {{ field }} - {% if field.help_text %}
{{ field.help_text }}
{% endif %} - {% endif %} - {% for error in field.errors %}
{{ error }}
{% endfor %} +
+ + {{ form.name }} + {% if form.name.help_text %}
{{ form.name.help_text }}
{% endif %} + {% for error in form.name.errors %}
{{ error }}
{% endfor %} +
+
+ + {{ form.location }} + {% if form.location.help_text %}
{{ form.location.help_text }}
{% endif %} + {% for error in form.location.errors %}
{{ error }}
{% endfor %} +
+
+ + {{ form.start }} + {% if form.start.help_text %}
{{ form.start.help_text }}
{% endif %} + {% for error in form.start.errors %}
{{ error }}
{% endfor %} +
+
+ + {{ form.end }} + {% if form.end.help_text %}
{{ form.end.help_text }}
{% endif %} + {% for error in form.end.errors %}
{{ error }}
{% endfor %} +
+
+ + {{ form.event_url }} + {% if form.event_url.help_text %}
{{ form.event_url.help_text }}
{% endif %} + {% for error in form.event_url.errors %}
{{ error }}
{% endfor %} +
+
+ + {{ form.summary }} + {% if form.summary.help_text %}
{{ form.summary.help_text }}
{% endif %} + {% for error in form.summary.errors %}
{{ error }}
{% endfor %} +
+
+
+ {{ form.is_published }} + + {% if form.is_published.help_text %}
{{ form.is_published.help_text }}
{% endif %} +
+ {% for error in form.is_published.errors %}
{{ error }}
{% endfor %}
- {% endfor %}
+ + {% if form_mode == 'create' %} +
+
+

Repeat weekly (optional)

+

Use the same event details and time range to create one event per week within a date window.

+
+
+
+ + {{ form.recurrence_start_date }} + {% if form.recurrence_start_date.help_text %}
{{ form.recurrence_start_date.help_text }}
{% endif %} + {% for error in form.recurrence_start_date.errors %}
{{ error }}
{% endfor %} +
+
+ + {{ form.recurrence_end_date }} + {% if form.recurrence_end_date.help_text %}
{{ form.recurrence_end_date.help_text }}
{% endif %} + {% for error in form.recurrence_end_date.errors %}
{{ error }}
{% endfor %} +
+
+ + {{ form.recurrence_weekday }} + {% if form.recurrence_weekday.help_text %}
{{ form.recurrence_weekday.help_text }}
{% endif %} + {% for error in form.recurrence_weekday.errors %}
{{ error }}
{% endfor %} +
+
+
+ {% endif %} + {% if form.non_field_errors %}
{{ form.non_field_errors }}
{% endif %} diff --git a/core/views.py b/core/views.py index 80fb163..3e8f618 100644 --- a/core/views.py +++ b/core/views.py @@ -370,6 +370,25 @@ def event_import_restore(request): return redirect('event_dashboard') +def _build_weekly_recurrence_datetimes(start, end, recurrence_start_date, recurrence_end_date, recurrence_weekday): + tz = timezone.get_current_timezone() + local_start = timezone.localtime(start, tz) if timezone.is_aware(start) else start + start_time = local_start.time().replace(tzinfo=None) + duration = end - start + weekday_index = int(recurrence_weekday) + days_until_first = (weekday_index - recurrence_start_date.weekday()) % 7 + current_date = recurrence_start_date + timedelta(days=days_until_first) + occurrence_datetimes = [] + + while current_date <= recurrence_end_date: + occurrence_start = timezone.make_aware(datetime.combine(current_date, start_time), tz) + occurrence_end = occurrence_start + duration + occurrence_datetimes.append((occurrence_start, occurrence_end)) + current_date += timedelta(days=7) + + return occurrence_datetimes + + @login_required(login_url='login') def event_create(request): if not request.user.is_staff: @@ -378,6 +397,40 @@ def event_create(request): if request.method == 'POST': form = EventForm(request.POST) if form.is_valid(): + recurrence_start_date = form.cleaned_data.get('recurrence_start_date') + recurrence_end_date = form.cleaned_data.get('recurrence_end_date') + recurrence_weekday = form.cleaned_data.get('recurrence_weekday') + + if recurrence_start_date and recurrence_end_date and recurrence_weekday: + base_event = form.save(commit=False) + occurrence_datetimes = _build_weekly_recurrence_datetimes( + start=base_event.start, + end=base_event.end, + recurrence_start_date=recurrence_start_date, + recurrence_end_date=recurrence_end_date, + recurrence_weekday=recurrence_weekday, + ) + + created_events = [] + with transaction.atomic(): + for occurrence_start, occurrence_end in occurrence_datetimes: + event = Event( + name=base_event.name, + location=base_event.location, + start=occurrence_start, + end=occurrence_end, + event_url=base_event.event_url, + summary=base_event.summary, + is_published=base_event.is_published, + ) + event.full_clean() + event.save() + created_events.append(event) + + event_label = 'event' if len(created_events) == 1 else 'events' + messages.success(request, f'Created {len(created_events)} recurring {event_label} and added them to the calendar.') + return redirect('event_dashboard') + event = form.save() messages.success(request, 'Event saved and ready for the public calendar.') return redirect('event_dashboard_detail', slug=event.slug)