From 8a0f5ac78a278c660810c2f673ffc2044edbcd35 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 25 Apr 2026 19:43:38 +0000 Subject: [PATCH] Autosave: 20260425-194338 --- core/__pycache__/forms.cpython-311.pyc | Bin 3261 -> 5642 bytes core/__pycache__/views.cpython-311.pyc | Bin 28404 -> 31291 bytes core/forms.py | 56 ++++++++++++++++ core/templates/core/event_form.html | 88 ++++++++++++++++++++----- core/views.py | 53 +++++++++++++++ 5 files changed, 182 insertions(+), 15 deletions(-) diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index f805a3e52973ac562c5e541f8ec2827b55da3c17..f417e72949a53c04f06df6d7546fd9ee8537b76a 100644 GIT binary patch literal 5642 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 delta 961 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 H8 delta 755 zcmYL_O-vI(6vy}Nw!7`NP!w_7QromNKry(2z(EDXgOX_ch=C|dg6vcaBE{Jj)byYU z@v8M*FcLM!V1iW+>&=+R!2=$YD#nW#F2;i-LW=QVVw|Y~C-Z)J?>{r|y?HOzU~Lgf zAD5J<0$Yzhz90B?AY1B%T()Kjn$Q798BfArtYQ3q59={wn(y1iX$CC7dIdayB z>(+OB(;*H1B`?y=C^0KgxZP2Qhr74fX(HtmaK>MOneG;uwrAjhg2!D|AmZ$PO%w>` z<3W#<5eHrCiE=g!^ys>8Jr;zFKKosI2PNL>qQ!@LcjH{P?Hun+w!)iNy z44@Qmo~gx2qiSnRqF32ds<2L}!90_9KUHk+>%;=tY~}jq0Bktg-zZBwvmKxHKlE=Dp$;xOIaCf8 z4jMbajdzSzQ1FuxR5vj>%gkKm#Vw7riY#B(xwmQsQ)l?e(_&j130dU)ZeHzvqFOFz zCou;m4L^QO)T_nf9Ivgwz?kH!WG)SGY{Pp)J~_nP1+ZQYEdvB`dN?FUnDPy*cf+3n z!k8QJ$pxmE6laIR*7C?S*lSrVT6bv_pd4SBA<%Hi^s|}%Hc!Dmt1EQ_qI`Cg|AF#$ zX|WAni}Lhzj8D84ZKDxaGmF_4%z|V!jQ$a0f>?GfI8ieXkvv2eAhIHQL0%CURwY5K L+E7|Bapl8bQS`wZ 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)