--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

6
course_calendar/admin.py Normal file
View File

@@ -0,0 +1,6 @@
from django.contrib import admin
# Register your models here.
# from .forms import EventForm
#
# admin.site.register(EventForm)

5
course_calendar/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class CourseCalendarConfig(AppConfig):
name = 'course_calendar'

21
course_calendar/forms.py Normal file
View File

@@ -0,0 +1,21 @@
from django.forms import fields as form_fields
from django.forms.widgets import ColorInput
from wagtail.admin.forms import WagtailAdminModelForm
from wagtail_color_panel.edit_handlers import NativeColorPanel
from course_calendar.models import CalendarEvent
class EventForm(WagtailAdminModelForm):
class Meta:
model = CalendarEvent
fields = "__all__"
widgets = {
"color": ColorInput(),
"start": form_fields.DateTimeInput(attrs={"type": "datetime-local"}),
"end": form_fields.DateTimeInput(attrs={"type": "datetime-local"}),
"recurrence_start_time": form_fields.TimeInput(attrs={"type": "time"}),
"recurrence_end_time": form_fields.TimeInput(attrs={"type": "time"}),
"recurrence_end_date": form_fields.DateInput(attrs={"type": "date"}),
}

View File

@@ -0,0 +1,72 @@
# Generated by Django 6.0.3 on 2026-03-30 09:43
import django.db.models.deletion
import modelcluster.contrib.taggit
import wagtail.fields
import wagtail_color_panel.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
('wagtailimages', '0027_image_description'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CalendarEvent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('color', wagtail_color_panel.fields.ColorField(default='#1c398e', max_length=7)),
('start', models.DateTimeField(blank=True, help_text='Start date and time of the singular event (ignored if recurrence is enabled)', null=True)),
('end', models.DateTimeField(blank=True, help_text='End date and time of the singular event (ignored if recurrence is enabled)', null=True)),
('recurrence_enabled', models.BooleanField(default=False, help_text='Enable automatic recurrence for this event')),
('recurrence_days_of_week', models.JSONField(blank=True, default=list, help_text='Days of the week for recurrence (e.g., [0,3] for Mon & Thu)')),
('recurrence_start_time', models.TimeField(blank=True, help_text='Start time for each occurrence', null=True)),
('recurrence_end_time', models.TimeField(blank=True, help_text='End time for each occurrence', null=True)),
('recurrence_repeat_until', models.DateField(blank=True, help_text='Repeat until this date (inclusive)', null=True)),
('recurrence_endless', models.BooleanField(default=False, help_text="If enabled, recurrence will not end (ignore 'repeat until' date)")),
('location', models.CharField(blank=True, max_length=255)),
('max_attendees', models.PositiveIntegerField(blank=True, help_text='Maximum number of attendees. Leave blank for unlimited.', null=True)),
('description', wagtail.fields.RichTextField(blank=True)),
('hosts', models.ManyToManyField(help_text='Select users who will be listed as hosts of this event.', related_name='hosted_events', to=settings.AUTH_USER_MODEL)),
('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
],
),
migrations.CreateModel(
name='EventOccurrence',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start', models.DateTimeField()),
('end', models.DateTimeField()),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='occurrences', to='course_calendar.calendarevent')),
('signed_up_users', models.ManyToManyField(blank=True, help_text='Users who have signed up for this occurrence.', related_name='event_occurrences_signed_up', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['start'],
},
),
migrations.CreateModel(
name='EventTag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tagged_items', to='course_calendar.calendarevent')),
('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_items', to='taggit.tag')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='calendarevent',
name='tags',
field=modelcluster.contrib.taggit.ClusterTaggableManager(blank=True, help_text='A comma-separated list of tags.', through='course_calendar.EventTag', to='taggit.Tag', verbose_name='Tags'),
),
]

View File

179
course_calendar/models.py Normal file
View File

@@ -0,0 +1,179 @@
from datetime import datetime, timedelta
import logging
from django.contrib.auth.models import User
from django.db import models
from django.forms.widgets import CheckboxSelectMultiple
from django.utils import timezone
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase
from wagtail.admin.panels import FieldPanel, ObjectList
from wagtail.fields import RichTextField
from wagtail_color_panel.edit_handlers import NativeColorPanel
from wagtail_color_panel.fields import ColorField
class EventTag(TaggedItemBase):
content_object = models.ForeignKey(
"course_calendar.CalendarEvent",
related_name="tagged_items",
on_delete=models.CASCADE,
)
class CalendarEvent(models.Model):
title = models.CharField(max_length=255)
tags = ClusterTaggableManager(through=EventTag, 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 = models.ManyToManyField(
User,
related_name="hosted_events",
help_text="Select users who will be listed as hosts of this event.",
)
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)
class EventOccurrence(models.Model):
event = models.ForeignKey(
CalendarEvent, 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

@@ -0,0 +1,9 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import CalendarEvent
@receiver(post_save, sender=CalendarEvent)
def generate_event_occurrences_on_save(sender, instance, created, **kwargs):
instance.generate_occurrences()

View File

@@ -0,0 +1,24 @@
{% extends "wagtailadmin/base.html" %}
{% load static i18n %}
{% block titletag %}
{% trans "Add Event" %}
{% endblock titletag %}
{% block content %}
{{ form.media }}
{% include "wagtailadmin/shared/header.html" with title="Events" icon="calendar" %}
<div style="margin: auto 72px;">
<h2>Add event</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="button button-primary">Save</button>
</form>
</div>
{% endblock content %}

View File

@@ -0,0 +1,27 @@
{% extends "wagtailadmin/base.html" %}
{% load static i18n %}
{% block titletag %}
{% trans "Events" %}
{% endblock titletag %}
{% block content %}
{% include "wagtailadmin/shared/header.html" with title="Events" icon="calendar" %}
<div>
<ul>
{% for event in events %}
<li>
{{ event | pprint }}
</li>
{% endfor %}
</ul>
</div>
<a href="{% url 'admin_add_event' %}" class="button">{% trans "Add event" %}</a>
{% endblock content %}

View File

@@ -0,0 +1,144 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Course Calendar" %}{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.20/index.global.min.js" integrity="sha256-sQEgS6I+FEeOlX4oTVi7qW/HMRAh0O6vifpeZXIMRsg=" crossorigin="anonymous"></script>
<script>
function showModal(occurrenceId) {
// get event's url
const eventApiUrl = `/calendar/api/calendar/occurrences/${occurrenceId}`;
const eventApi = fetch(eventApiUrl)
.then(response => response.json())
.then(data => {
eventUrl = `/calendar/occurrence/${occurrenceId}/`;
const modal = document.createElement('div');
modal.classList.add('fixed', 'inset-0', 'flex', 'items-center', 'justify-center', 'z-50', 'shadow-lg', 'overflow-auto');
modal.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; // semi-transparent background
const modalContent = document.createElement('div');
modalContent.classList.add('bg-white', 'rounded-lg', 'p-6', 'max-w-3xl', 'relative', 'overflow-auto', 'max-h-[80vh]');
modalContent.innerHTML = '<p class="text-gray-500">{% trans "Loading..." %}</p>';
if (eventUrl) {
fetch(eventUrl)
.then(response => response.text())
.then(html => {
modalContent.innerHTML = html;
})
.catch(error => {
modalContent.innerHTML = '<p class="text-red-500">Failed to load event details.</p>';
});
modal.appendChild(modalContent);
document.body.appendChild(modal);
// Close modal when clicking outside modalContent
modal.addEventListener('click', function(e) {
if (e.target === modal) {
document.body.removeChild(modal);
// remove ?modal=occurrenceId from the url
const url = new URL(window.location);
url.searchParams.delete('modal');
window.history.pushState({}, '', url);
}
});
// Close modal on ESC key
document.addEventListener('keydown', function escHandler(e) {
if (e.key === 'Escape') {
if (document.body.contains(modal)) {
document.body.removeChild(modal);
}
document.removeEventListener('keydown', escHandler);
// remove ?modal=occurrenceId from the url
const url = new URL(window.location);
url.searchParams.delete('modal');
window.history.pushState({}, '', url);
}
});
const closeButton = document.createElement('button');
closeButton.classList.add('absolute', 'top-2', 'right-2', 'text-red-500', 'cursor-pointer', 'hover:text-gray-700', 'text-5xl', 'translate-x-[-50%]');
closeButton.innerHTML = '&times;';
closeButton.addEventListener('click', function() {
if (document.body.contains(modal)) {
document.body.removeChild(modal);
}
// remove ?modal=occurrenceId from the url
const url = new URL(window.location);
url.searchParams.delete('modal');
window.history.pushState({}, '', url);
});
modal.appendChild(closeButton);
// add ?modal=occurrenceId to the url
const url = new URL(window.location);
url.searchParams.set('modal', occurrenceId);
window.history.pushState({}, '', url);
}
return data;
})
.catch(error => {
console.error('Error fetching event details:', error);
return null;
});
}
// if ?modal=event_id is in the url, open the modal for that event
const urlParams = new URLSearchParams(window.location.search);
const modalEventId = urlParams.get('modal');
if (modalEventId) {
showModal(modalEventId);
}
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
height: "auto",
initialView: 'timeGridWeek',
firstDay: 1,
nowIndicator: true,
views: {
timeGridWeek: {
titleFormat: { month: 'long', year: 'numeric' },
slotLabelFormat: { hour: '2-digit', minute: '2-digit' },
slotMinTime: '08:00:00',
slotMaxTime: '22:00:00',
},
listWeek: {
titleFormat: { month: 'long', year: 'numeric' },
listDaySideFormat: false,
},
},
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'timeGridWeek,listMonth',
},
locale: "{% get_current_language as LANGUAGE_CODE %}{{ LANGUAGE_CODE }}",
events: "/calendar/api/calendar/occurrences/",
eventClick: function(info) {
// prevent default navigation
info.jsEvent.preventDefault();
const eventUrl = info.event.url;
if (eventUrl) {
showModal(info.event.id);
}
},
});
calendar.render();
});
</script>
{% endblock %}
{% block content %}
<div class="overflow-auto">
<div id="calendar" class="min-w-xl"></div>
</div>
{% endblock %}

View File

@@ -0,0 +1,62 @@
{% 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>

3
course_calendar/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

36
course_calendar/urls.py Normal file
View File

@@ -0,0 +1,36 @@
from django.urls import path
from . import views
urlpatterns = [
path("", views.calendar, name="calendar_view"),
path(
"occurrence/<int:occurrence_id>/",
views.occurrence_detail,
name="occurrence_detail",
),
path(
"api/calendar/events/",
views.get_all_events,
name="get_all_events",
),
path(
"api/calendar/occurrences/",
views.get_calendar_occurrences,
name="get_calendar_occurrences",
),
path(
"api/calendar/occurrences/<int:occurrence_id>/",
views.get_calendar_occurrence,
name="get_calendar_occurrence",
),
path(
"occurrence/<int:occurrence_id>/signup/",
views.occurrence_signup,
name="occurrence_signup",
),
path(
"occurrence/<int:occurrence_id>/signout/",
views.occurrence_signout,
name="occurrence_signout",
),
]

156
course_calendar/views.py Normal file
View File

@@ -0,0 +1,156 @@
from django import urls
from django.http import JsonResponse
from django.shortcuts import redirect, render
from course_calendar.forms import EventForm
from .models import CalendarEvent, EventOccurrence
def calendar(request):
return render(request, "calendar.html")
def admin_events_dashboard(request):
return render(
request,
"admin_events_dashboard.html",
context={
"events": EventOccurrence.objects.select_related("event").all(),
},
)
def admin_add_event(request):
if request.method == "POST":
form = EventForm(request.POST)
if form.is_valid():
print("CLEANED DATA:", form.cleaned_data)
event = form.save()
print("Event created:", event.__dict__)
# Save many-to-many data if present
if hasattr(form, "save_m2m"):
form.save_m2m()
return redirect("admin_events_dashboard")
else:
print("FORM ERRORS:", form.errors)
form = EventForm()
return render(request, "admin_add_event.html", {"form": form})
def occurrence_detail(request, occurrence_id):
occ = (
EventOccurrence.objects.select_related("event").filter(id=occurrence_id).first()
)
if not occ:
return redirect("calendar")
event = occ.event.specific
return render(
request,
"occurrence_detail.html",
{
"occurrence": occ,
"event": event,
"user_signed_up": occ.user_signed_up(request.user),
},
)
def get_all_events(request):
events = EventOccurrence.objects.select_related("event").all()
events_list = []
for occ in events:
event = occ.event.specific
events_list.append(
{
"id": occ.id,
"event_id": event.id,
"title": event.title,
"start": occ.start,
"end": occ.end,
"location": event.location,
"url": event.url,
"color": "#666666" if occ.is_past else event.color,
"tags": list(event.tags.values_list("name", flat=True)),
}
)
return JsonResponse(events_list, safe=False)
def get_calendar_occurrences(request):
# get occurrences from database (EventOccurrence model)
start = request.GET.get("start")
end = request.GET.get("end")
occurrences = EventOccurrence.objects.filter(
start__gte=start, end__lte=end
).select_related("event")
events_list = []
for occ in occurrences:
event = occ.event.specific
events_list.append(
{
"id": occ.id,
"event_id": event.id,
"title": event.title,
"start": occ.start,
"end": occ.end,
"location": event.location,
"url": event.url,
"color": "#666666" if occ.is_past else event.color,
"tags": list(event.tags.values_list("name", flat=True)),
}
)
return JsonResponse(events_list, safe=False)
def get_calendar_occurrence(request, occurrence_id):
occ = (
EventOccurrence.objects.select_related("event").filter(id=occurrence_id).first()
)
if not occ:
return JsonResponse({"error": "Occurrence not found"}, status=404)
event = occ.event.specific
event_dict = {
"id": occ.id,
"event_id": event.id,
"title": event.title,
"start": occ.start,
"end": occ.end,
"location": event.location,
"url": event.url,
"color": "#666666" if occ.is_past else event.color,
"tags": list(event.tags.values_list("name", flat=True)),
"attendees_count": occ.attendees_count,
}
return JsonResponse(event_dict)
def occurrence_signup(request, occurrence_id):
if not request.user.is_authenticated:
return redirect("login")
occ = EventOccurrence.objects.filter(id=occurrence_id).first()
if not occ:
return redirect("calendar")
occ.signed_up_users.add(request.user)
occ.save()
# redirect to calendar page with ?modal=occurrence_id to show modal with event details
return redirect(urls.reverse("calendar") + f"?modal={occurrence_id}")
def occurrence_signout(request, occurrence_id):
if not request.user.is_authenticated:
return redirect("login")
occ = EventOccurrence.objects.filter(id=occurrence_id).first()
if not occ:
return redirect("calendar")
occ.signed_up_users.remove(request.user)
occ.save()
return redirect(urls.reverse("calendar") + f"?modal={occurrence_id}")

View File

@@ -0,0 +1,27 @@
from wagtail.admin.menu import MenuItem
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler
from wagtail import hooks
from django.urls import path, reverse
from . import views
@hooks.register("register_admin_urls")
def register_events_form_url():
return [
path("events/", views.admin_events_dashboard, name="admin_events_dashboard"),
path(
"events/add/",
views.admin_add_event,
name="admin_add_event",
),
]
@hooks.register("register_admin_menu_item")
def register_admin_chat_menu_item():
return MenuItem(
"Event Calendar", reverse("admin_events_dashboard"), icon_name="calendar"
)