From 31428640ab5e1690d034e52edd0a3bc577d9ef32 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Sat, 3 Aug 2024 17:33:01 +0200
Subject: [PATCH] Implement calendar alarms for LessonEvent

---
 .../migrations/0018_add_lessoneventalarm.py   | 26 ++++++++++
 aleksis/apps/chronos/models.py                | 42 +++++++++++++++-
 aleksis/apps/chronos/preferences.py           | 50 ++++++++++++++++++-
 3 files changed, 116 insertions(+), 2 deletions(-)
 create mode 100644 aleksis/apps/chronos/migrations/0018_add_lessoneventalarm.py

diff --git a/aleksis/apps/chronos/migrations/0018_add_lessoneventalarm.py b/aleksis/apps/chronos/migrations/0018_add_lessoneventalarm.py
new file mode 100644
index 00000000..b813aa84
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0018_add_lessoneventalarm.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.0.7 on 2024-08-03 15:21
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0017_optional_slot_number'),
+        ('core', '0066_alter_freebusy_options_and_more'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='LessonEventAlarm',
+            fields=[
+                ('calendaralarm_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.calendaralarm')),
+            ],
+            options={
+                'verbose_name': 'Lesson event alarm',
+                'verbose_name_plural': 'Lesson event alarms',
+            },
+            bases=('core.calendaralarm',),
+        ),
+    ]
diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 35f4e011..25a16143 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -68,7 +68,7 @@ from aleksis.core.mixins import (
     GlobalPermissionModel,
     SchoolTermRelatedExtensibleModel,
 )
-from aleksis.core.models import CalendarEvent, Group, Person, Room, SchoolTerm
+from aleksis.core.models import CalendarAlarm, CalendarEvent, Group, Person, Room, SchoolTerm
 from aleksis.core.util.core_helpers import get_site_preferences, has_person
 
 
@@ -1588,11 +1588,51 @@ class LessonEvent(CalendarEvent):
             return objs.for_person(request.user.person)
         return objs
 
+    def save(self, *args, **kwargs):
+        super().save(*args, **kwargs)
+
+        # Save alarm in lesson event alarm model
+        if self.amends:
+            alarm, created = LessonEventAlarm.objects.get_or_create(event=self, defaults={"send_notifications": True})
+
     class Meta:
         verbose_name = _("Lesson Event")
         verbose_name_plural = _("Lesson Events")
 
 
+class LessonEventAlarm(CalendarAlarm):
+    """Alarm model for lesson events."""
+
+    def value_description(self, request: HttpRequest | None = None) -> str:
+        return LessonEvent.value_title(self.event)
+
+    def value_trigger(self, request: HttpRequest | None = None) -> Union[datetime, timedelta]:
+        # question: allow for generating multiple alarms for one event?
+        if "fixed_time_relative" in get_site_preferences()["chronos__alarm_trigger_mode"]:
+            return (
+                self.event.datetime_start
+                - timedelta(days=get_site_preferences()["chronos__days_in_advance_alarms"])
+            ).replace(
+                hour=get_site_preferences()["chronos__fixed_time_alarms"].hour,
+                minute=get_site_preferences()["chronos__fixed_time_alarms"].minute,
+            )
+        elif "strictly_relative" in get_site_preferences()["chronos__alarm_trigger_mode"]:
+            return get_site_preferences()["chronos__time_in_advance_alarms"]
+    
+    def value_notification_sender(self, request: HttpRequest | None = None) -> str:
+        return _("Lesson notification")
+
+    def value_notification_recipients(self, request: HttpRequest | None = None) -> [Person]:
+        return self.event.all_teachers
+
+    def value_notification_description(self, request: HttpRequest | None = None) -> str:
+        return _("bliblablubb")
+
+    class Meta:
+        verbose_name = _("Lesson event alarm")
+        verbose_name_plural = _("Lesson event alarms")
+
+
 class SupervisionEvent(LessonEvent):
     """Calendar feed for supervisions."""
 
diff --git a/aleksis/apps/chronos/preferences.py b/aleksis/apps/chronos/preferences.py
index d2665321..ba435ec9 100644
--- a/aleksis/apps/chronos/preferences.py
+++ b/aleksis/apps/chronos/preferences.py
@@ -1,4 +1,4 @@
-from datetime import time
+from datetime import time, timedelta
 
 from django.utils.translation import gettext_lazy as _
 
@@ -6,8 +6,10 @@ from colorfield.widgets import ColorWidget
 from dynamic_preferences.preferences import Section
 from dynamic_preferences.types import (
     BooleanPreference,
+    DurationPreference,
     IntegerPreference,
     ModelMultipleChoicePreference,
+    MultipleChoicePreference,
     StringPreference,
     TimePreference,
 )
@@ -150,3 +152,49 @@ class SupervisionEventFeedColor(StringPreference):
     verbose_name = _("Supervision calendar feed color")
     widget = ColorWidget
     required = True
+
+
+@site_preferences_registry.register
+class AlarmTriggerMode(MultipleChoicePreference):
+    """Mode for computing the trigger property of alarms associated with lesson events."""
+
+    section = chronos
+    name = "alarm_trigger_mode"
+    default = ["fixed_time_relative", "strictly_relative"]
+    verbose_name = _("Trigger mode for lesson event alarms")
+    choices = (
+        (
+            "fixed_time_relative",
+            "Trigger alarm on a fixed time in a earlier day relative to the event's start",
+        ),
+        ("strictly_relative", "Trigger alarm on a time relative to the event's start"),
+    )
+    required = True
+
+
+# FIXME: hide the following preferences conditionally, depending on the trigger mode selected above
+@site_preferences_registry.register
+class DaysInAdvanceAlarms(IntegerPreference):
+    section = chronos
+    name = "days_in_advance_alarms"
+    default = 1
+    verbose_name = _("How many days in advance should lesson event alarms be sent?")
+    required = True
+
+
+@site_preferences_registry.register
+class FixedTimeAlarms(TimePreference):
+    section = chronos
+    name = "fixed_time_alarms"
+    default = time(17, 00)
+    verbose_name = _("Time for sending lesson event alarms")
+    required = True
+
+
+@site_preferences_registry.register
+class TimeInAdvanceAlarms(DurationPreference):
+    section = chronos
+    name = "time_in_advance_alarms"
+    default = timedelta(hours=24)
+    verbose_name = _("How much in advance should lesson event alarms be sent?")
+    required = True
-- 
GitLab