feat(models.py): add EventOccurrence model
This commit is contained in:
172
home/models.py
172
home/models.py
@@ -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),
|
|
||||||
]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user