diff --git a/home/models.py b/home/models.py index 37c1593..ab6f7c3 100644 --- a/home/models.py +++ b/home/models.py @@ -1,7 +1,10 @@ +from datetime import date, datetime, timedelta + from django import forms from django.contrib.auth.models import Group, User from django.db import models from django.forms import CheckboxSelectMultiple +from django.utils import timezone from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.fields import ParentalKey from taggit.models import TaggedItemBase @@ -9,8 +12,8 @@ from wagtail.admin.panels import FieldPanel from wagtail.fields import RichTextField, StreamField from wagtail.models import Page from wagtail.models.copying import ParentalManyToManyField -from wagtail_color_panel.fields import ColorField from wagtail_color_panel.edit_handlers import NativeColorPanel +from wagtail_color_panel.fields import ColorField class EmptyPage(Page): @@ -95,8 +98,49 @@ class EventPage(Page): on_delete=models.SET_NULL, related_name="+", ) - start = models.DateTimeField() - end = models.DateTimeField() + + start = models.DateTimeField( + blank=True, + null=True, + help_text="Start date and time of the singular event (ignored if recurrence is enabled)", + ) + end = models.DateTimeField( + blank=True, + null=True, + help_text="End date and time of the singular event (ignored if recurrence is enabled)", + ) + + # Recurrence fields + recurrence_enabled = models.BooleanField( + default=False, help_text="Enable automatic recurrence for this event" + ) + RECURRENCE_DAYS = [ + (0, "Monday"), + (1, "Tuesday"), + (2, "Wednesday"), + (3, "Thursday"), + (4, "Friday"), + (5, "Saturday"), + (6, "Sunday"), + ] + recurrence_days_of_week = models.JSONField( + default=list, + blank=True, + help_text="Days of the week for recurrence (e.g., [0,3] for Mon & Thu)", + ) + recurrence_start_time = models.TimeField( + null=True, blank=True, help_text="Start time for each occurrence" + ) + recurrence_end_time = models.TimeField( + null=True, blank=True, help_text="End time for each occurrence" + ) + recurrence_repeat_until = models.DateField( + null=True, blank=True, help_text="Repeat until this date (inclusive)" + ) + recurrence_endless = models.BooleanField( + default=False, + help_text="If enabled, recurrence will not end (ignore 'repeat until' date)", + ) # TODO: use google maps here location = models.CharField(max_length=255, blank=True) @@ -112,16 +156,107 @@ class EventPage(Page): related_name="hosted_events", help_text="Select users who will be listed as hosts of this event.", ) - signed_up_users = ParentalManyToManyField( + + def get_context(self, request): + context = super().get_context(request) + # Occurrence-specific context should be handled in views/templates + return context + + def generate_occurrences(self, days_ahead=30): + """ + Generate EventOccurrence objects for this event based on recurrence settings. + For endless recurrence, generate up to days_ahead into the future. + """ + now = timezone.now() + if not self.recurrence_enabled: + # if recurrence is not enabled, ensure there's at least one occurrence for the specified start/end + if self.occurrences.exists(): + occurrence = self.occurrences.first() + if occurrence.start != self.start or occurrence.end != self.end: + occurrence.start = self.start + occurrence.end = self.end + occurrence.save(update_fields=["start", "end"]) + else: + EventOccurrence.objects.create( + event=self, start=self.start, end=self.end + ) + return + + # Determine the date range + start_date = now.date() + if self.recurrence_endless: + end_date = start_date + timedelta(days=days_ahead) + elif self.recurrence_repeat_until: + end_date = self.recurrence_repeat_until + else: + end_date = start_date + timedelta(days=days_ahead) + + days_of_week = self.recurrence_days_of_week or [] + start_time = self.recurrence_start_time or datetime.min.time() + end_time = self.recurrence_end_time or datetime.min.time() + + for single_date in ( + start_date + timedelta(n) for n in range((end_date - start_date).days + 1) + ): + if days_of_week and single_date.weekday() not in days_of_week: + continue + start_dt = datetime.combine(single_date, start_time) + end_dt = datetime.combine(single_date, end_time) + start_dt = timezone.make_aware(start_dt) + end_dt = timezone.make_aware(end_dt) + # If an occurrence exists, update its start/end if needed, but keep signed_up_users + occurrence = self.occurrences.filter(start=start_dt, end=end_dt).first() + if occurrence: + if occurrence.start != start_dt or occurrence.end != end_dt: + occurrence.start = start_dt + occurrence.end = end_dt + occurrence.save(update_fields=["start", "end"]) + else: + EventOccurrence.objects.create(event=self, start=start_dt, end=end_dt) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.generate_occurrences() + + content_panels = Page.content_panels + [ + FieldPanel("tags"), + NativeColorPanel("color"), + FieldPanel("image"), + FieldPanel("location"), + FieldPanel("max_attendees"), + FieldPanel("hosts", widget=CheckboxSelectMultiple), + FieldPanel("description"), + FieldPanel("start"), + FieldPanel("end"), + FieldPanel("recurrence_enabled"), + FieldPanel("recurrence_days_of_week"), + FieldPanel("recurrence_start_time"), + FieldPanel("recurrence_end_time"), + FieldPanel("recurrence_repeat_until"), + FieldPanel("recurrence_endless"), + ] + + +class EventOccurrence(models.Model): + event = ParentalKey(EventPage, related_name="occurrences", on_delete=models.CASCADE) + start = models.DateTimeField() + end = models.DateTimeField() + signed_up_users = models.ManyToManyField( User, - related_name="signed_up_events", + related_name="event_occurrences_signed_up", blank=True, - help_text="Users who have signed up for this event.", + help_text="Users who have signed up for this occurrence.", ) + class Meta: + ordering = ["start"] + + def __str__(self): + return f"{self.event.title} ({self.start} - {self.end})" + @property def attendees_count(self): - return self.signed_up_users.count() # pyright: ignore[reportAttributeAccessIssue] + return self.signed_up_users.count() @property def is_past(self): @@ -129,26 +264,7 @@ class EventPage(Page): return self.end < timezone.now() - def _user_signed_up(self, user): + def user_signed_up(self, user): if not user.is_authenticated: return False - - return self.signed_up_users.filter(id__in=[user.id]).exists() # pyright: ignore[reportAttributeAccessIssue] - - def get_context(self, request): - context = super().get_context(request) - context["user_signed_up"] = self._user_signed_up(request.user) - return context - - content_panels = Page.content_panels + [ - FieldPanel("tags"), - NativeColorPanel("color"), - FieldPanel("image"), - FieldPanel("start"), - FieldPanel("end"), - FieldPanel("location"), - FieldPanel("max_attendees"), - FieldPanel("hosts", widget=CheckboxSelectMultiple), - FieldPanel("description"), - FieldPanel("signed_up_users", read_only=True, widget=CheckboxSelectMultiple), - ] + return self.signed_up_users.filter(id=user.id).exists()