--wip-- [skip ci]
This commit is contained in:
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
31
home/templates/home/blog_index_page.html
Normal file
31
home/templates/home/blog_index_page.html
Normal 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 %}
|
||||
|
||||
0
home/templates/home/blog_page.html
Normal file
0
home/templates/home/blog_page.html
Normal 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>
|
||||
Reference in New Issue
Block a user