feat(models.py): add EventOccurrence model

This commit is contained in:
2026-03-17 11:56:06 +01:00
parent fa06fb3854
commit 357f5aea26

View File

@@ -1,7 +1,10 @@
from datetime import date, datetime, timedelta
from django import forms from django import forms
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.db import models from django.db import models
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
from django.utils import timezone
from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase from taggit.models import TaggedItemBase
@@ -9,8 +12,8 @@ from wagtail.admin.panels import FieldPanel
from wagtail.fields import RichTextField, StreamField from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page from wagtail.models import Page
from wagtail.models.copying import ParentalManyToManyField 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.edit_handlers import NativeColorPanel
from wagtail_color_panel.fields import ColorField
class EmptyPage(Page): class EmptyPage(Page):
@@ -95,8 +98,49 @@ class EventPage(Page):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="+", 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 # TODO: use google maps here
location = models.CharField(max_length=255, blank=True) location = models.CharField(max_length=255, blank=True)
@@ -112,16 +156,107 @@ class EventPage(Page):
related_name="hosted_events", related_name="hosted_events",
help_text="Select users who will be listed as hosts of this event.", 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, User,
related_name="signed_up_events", related_name="event_occurrences_signed_up",
blank=True, 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 @property
def attendees_count(self): def attendees_count(self):
return self.signed_up_users.count() # pyright: ignore[reportAttributeAccessIssue] return self.signed_up_users.count()
@property @property
def is_past(self): def is_past(self):
@@ -129,26 +264,7 @@ class EventPage(Page):
return self.end < timezone.now() return self.end < timezone.now()
def _user_signed_up(self, user): def user_signed_up(self, user):
if not user.is_authenticated: if not user.is_authenticated:
return False return False
return self.signed_up_users.filter(id=user.id).exists()
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),
]