--wip-- [skip ci]

This commit is contained in:
2026-04-02 10:13:19 +02:00
parent dd936473d8
commit 9c71dc1e47
26 changed files with 650 additions and 343 deletions

View File

@@ -0,0 +1,46 @@
# Generated by Django 6.0.3 on 2026-03-30 09:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('home', '0022_blogindexpage_blogpage'),
]
operations = [
migrations.RemoveField(
model_name='eventpage',
name='hosts',
),
migrations.RemoveField(
model_name='eventpage',
name='image',
),
migrations.RemoveField(
model_name='eventpage',
name='page_ptr',
),
migrations.RemoveField(
model_name='eventpage',
name='tags',
),
migrations.RemoveField(
model_name='eventpagetag',
name='content_object',
),
migrations.RemoveField(
model_name='eventpagetag',
name='tag',
),
migrations.DeleteModel(
name='EventOccurrence',
),
migrations.DeleteModel(
name='EventPage',
),
migrations.DeleteModel(
name='EventPageTag',
),
]

View File

@@ -4,11 +4,8 @@ from .pages import (
CoursePage,
CourseModulePage,
ModuleLessonPage,
EventPage,
)
from .event_occurrence import EventOccurrence
from .chat_message import ChatMessage
@@ -18,7 +15,5 @@ __all__ = [
"CoursePage",
"CourseModulePage",
"ModuleLessonPage",
"EventPage",
"EventOccurrence",
"ChatMessage",
]

View File

@@ -1,38 +0,0 @@
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone
from modelcluster.fields import ParentalKey
from .pages import EventPage
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="event_occurrences_signed_up",
blank=True,
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()
@property
def is_past(self):
return self.end < timezone.now()
def user_signed_up(self, user):
if not user.is_authenticated:
return False
return self.signed_up_users.filter(id=user.id).exists()

View File

@@ -1,21 +1,12 @@
from datetime import datetime, timedelta
from django.conf import settings
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import Group
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
from wagtail import blocks
from wagtail.admin.panels import FieldPanel
from wagtail.fields import RichTextField, StreamField
from wagtail.images.blocks import ImageBlock
from wagtail.models import Page
from wagtail.models.copying import ParentalManyToManyField
from wagtail_color_panel.edit_handlers import NativeColorPanel
from wagtail_color_panel.fields import ColorField
from purchase.models import CoursePurchase
@@ -230,163 +221,3 @@ class ModuleLessonPage(Page):
),
]
parent_page_types = ["home.CourseModulePage"]
class EventPageTag(TaggedItemBase):
content_object = ParentalKey(
"home.EventPage",
related_name="tagged_items",
on_delete=models.CASCADE,
)
class EventPage(Page):
tags = ClusterTaggableManager(through=EventPageTag, blank=True)
color = ColorField(default="#1c398e")
image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
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)
max_attendees = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Maximum number of attendees. Leave blank for unlimited.",
)
description = RichTextField(blank=True)
hosts = ParentalManyToManyField(
User,
related_name="hosted_events",
help_text="Select users who will be listed as hosts of this event.",
)
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.
"""
from .event_occurrence import EventOccurrence
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)
if self.live:
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"),
]

View File

@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% load static i18n wagtailcore_tags wagtailimages_tags %}
{% block title %}{% trans "Blog" %}{% endblock title %}
{% block body_class %}template-courseindex{% endblock body_class %}
{% block content %}
<h1 class="text-3xl font-bold mb-6 text-center">{% trans "Blog" %}</h1>
<div class="flex flex-wrap -mx-4">
{% for post in posts %}
<div class="w-full md:w-1/2 lg:w-1/3 px-4 mb-8">
<a href="{{ post.url }}" class="block bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
{% if post.blog_image %}
{% image post.blog_image original alt=post.title class="w-full h-auto" %}
{% endif %}
<div class="p-4">
<h2 class="text-xl font-semibold">{{ post.title }}</h2>
<p class="text-gray-600">{{ post.excerpt|truncatewords:20 }}</p>
</div>
</a>
</div>
{% empty %}
<p class="text-center text-gray-600 italic">{% trans "No blog posts yet." %}</p>
{% endfor %}
</div>
{% endblock content %}

View File

View File

@@ -1,62 +0,0 @@
{% load static i18n wagtailcore_tags wagtailimages_tags %}
<h2 class="text-3xl font-bold mb-4">{{ page.title }}</h2>
{% if page.image %}
{% image page.image original alt=page.title class="w-full max-h-64 rounded-lg mb-3 object-cover" %}
{% endif %}
<div class="flex items-center gap-2 text-gray-600 mb-2 text-sm">
<i class="fi fi-br-clock-three leading-0"></i>
<span>{{ page.start|date }}, {{ page.start|time }} {{ page.end|date }}, {{ page.end|time }}</span>
</div>
<div class="flex items-center gap-2 text-gray-600 mb-6 text-sm {% if page.attendees_count >= page.max_attendees and page.max_attendees is not None and page.max_attendees != 0 %}text-red-600{% endif %}">
<i class="fi fi-br-user leading-0"></i>
<span>
{{ page.attendees_count }}{% if page.max_attendees is not None and page.max_attendees != 0 %} / {{ page.max_attendees }}{% endif %}
</span>
</div>
<!-- sign up button -->
<div class="not-prose my-4">
{% if page.is_past %}
<div class="p-4 bg-gray-100 border-l-4 border-gray-500 text-gray-700">
<p>{% trans "This event has already ended. Please check our calendar for upcoming events." %}</p>
</div>
{% elif user_signed_up %}
<div class="p-4 bg-green-100 border-l-4 border-green-500 text-green-700">
<p>{% trans "You are signed up for this event. We look forward to seeing you there!" %}</p>
<!-- cancel button -->
<a href="{% url 'occurrence_signout' page.id %}" class="mt-4 inline-block bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition">
{% trans "Cancel Sign Up" %}
</a>
</div>
{% elif not user.is_authenticated %}
{# If the user is not authenticated, we can prompt them to log in or sign up. #}
<div class="p-4 bg-blue-100 border-l-4 border-blue-500 text-blue-700">
<p>{% trans "You need to be logged in to sign up for this event. Please log in or sign up to reserve your spot." %}</p>
<a href="{% url 'account_login' %}?next={% url 'calendar' %}?modal={{ page.id }}" class="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">
{% trans "Login" %}
</a>
<a href="{% url 'account_signup' %}?next={% url 'calendar' %}?modal={{ page.id }}" class="mt-4 inline-block bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition ml-2">
{% trans "Sign Up" %}
</a>
</div>
{% elif page.attendees_count >= page.max_attendees and page.max_attendees is not None and page.max_attendees != 0 %}
<div class="p-4 bg-red-100 border-l-4 border-red-500 text-red-700">
<p>{% trans "This event is fully booked. Please check back later for any cancellations." %}</p>
</div>
{% else %}
<div class="p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700">
<p>{% trans "You are not signed up for this event. Please sign up to reserve your spot." %}</p>
<a href="{% url 'occurrence_signup' page.id %}" class="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">
{% trans "Sign Up for Event" %}
</a>
</div>
{% endif %}
</div>
<div class="prose">
{{ page.description | richtext }}
</div>