From b41d9752095ca2a5d199889e703027e163a6f225 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Tue, 20 Aug 2024 11:38:26 +0200
Subject: [PATCH] Add status-based handling of notifications

---
 .../migrations/0018_add_lessoneventalarm.py   |  3 +-
 aleksis/apps/chronos/models.py                | 58 ++++++++++++++++---
 .../lesson_event_notification_description.txt |  1 +
 .../lesson_event_notification_title.txt       |  1 +
 4 files changed, 54 insertions(+), 9 deletions(-)
 create mode 100644 aleksis/apps/chronos/templates/chronos/lesson_event_notification_description.txt
 create mode 100644 aleksis/apps/chronos/templates/chronos/lesson_event_notification_title.txt

diff --git a/aleksis/apps/chronos/migrations/0018_add_lessoneventalarm.py b/aleksis/apps/chronos/migrations/0018_add_lessoneventalarm.py
index b813aa84..dd44633e 100644
--- a/aleksis/apps/chronos/migrations/0018_add_lessoneventalarm.py
+++ b/aleksis/apps/chronos/migrations/0018_add_lessoneventalarm.py
@@ -8,7 +8,7 @@ class Migration(migrations.Migration):
 
     dependencies = [
         ('chronos', '0017_optional_slot_number'),
-        ('core', '0066_alter_freebusy_options_and_more'),
+        ('core', '0066_add_calendar_alarm'),
     ]
 
     operations = [
@@ -16,6 +16,7 @@ class Migration(migrations.Migration):
             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')),
+                ('status', models.CharField(choices=[('c', 'Created'), ('e', 'Edited'), ('d', 'Deleted')], default='c', max_length=1, verbose_name='Status')),
             ],
             options={
                 'verbose_name': 'Lesson event alarm',
diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 25a16143..98a3ef9e 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -1359,10 +1359,10 @@ class LessonEvent(CalendarEvent):
     @property
     def all_teachers(self: LessonEvent) -> list[Person]:
         """Get list of all teachers for this lesson event."""
-        all_teachers = list(self.teachers.all())
+        all_teachers = self.teachers.all()
         if self.amends:
-            all_teachers += list(self.amends.teachers.all())
-        return all_teachers
+            all_teachers = all_teachers.union(self.amends.teachers.all())
+        return list(all_teachers)
 
     @property
     def group_names(self: LessonEvent) -> str:
@@ -1589,11 +1589,26 @@ class LessonEvent(CalendarEvent):
         return objs
 
     def save(self, *args, **kwargs):
+        adding_status = self._state.adding
+
         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})
+            if adding_status:
+                # TODO: allow for generating multiple alarms for one event
+                alarm = LessonEventAlarm(event=self, send_notifications=True)
+                alarm.save()
+            else:
+                alarms = LessonEventAlarm.objects.filter(event=self, status="c")
+                for alarm in alarms:
+                    if alarm.notifications.filter(sent=True).exists():
+                        follow_up_alarm = LessonEventAlarm(
+                            event=self, send_notifications=True, status="e"
+                        )
+                        follow_up_alarm.save()
+                    else:
+                        alarm.update_or_create_notifications()
 
     class Meta:
         verbose_name = _("Lesson Event")
@@ -1603,11 +1618,16 @@ class LessonEvent(CalendarEvent):
 class LessonEventAlarm(CalendarAlarm):
     """Alarm model for lesson events."""
 
+    STATUS_CHOICES = {"c": _("Created"), "e": _("Edited"), "d": _("Deleted")}
+
+    status = models.CharField(
+        verbose_name=_("Status"), max_length=1, choices=STATUS_CHOICES, default="c"
+    )
+
     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?
+    def value_trigger(self, request: HttpRequest | None = None) -> datetime | timedelta:
         if "fixed_time_relative" in get_site_preferences()["chronos__alarm_trigger_mode"]:
             return (
                 self.event.datetime_start
@@ -1618,15 +1638,37 @@ class LessonEventAlarm(CalendarAlarm):
             )
         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_title(self, request: HttpRequest | None = None) -> str:
+        return render_to_string(
+            "chronos/lesson_event_notification_title.txt",
+            {
+                "event": self.event,
+                "event_title": LessonEvent.value_title(self.event, request),
+                "status": self.STATUS_CHOICES[self.status].lower(),
+            },
+        )
+
     def value_notification_description(self, request: HttpRequest | None = None) -> str:
-        return _("bliblablubb")
+        # FIXME: In some (?) cases, this is incomplete (e.g. room names are missing)
+        return render_to_string(
+            "chronos/lesson_event_notification_description.txt",
+            {"event": self.event, "status": self.STATUS_CHOICES[self.status].lower()},
+        )
+
+    def value_notification_icon(self, request: HttpRequest | None = None) -> str:
+        return "calendar-remove-outline" if self.event.cancelled else "calendar-alert-outline"
+
+    # TODO: how to get fitting link (from vue-router)?
+    # TODO: timetable overview page needs to be fitted so that CW is specified
+    # def value_notification_link(self, request: HttpRequest | None = None) -> str:
+    #     raise NotImplementedError()
 
     class Meta:
         verbose_name = _("Lesson event alarm")
diff --git a/aleksis/apps/chronos/templates/chronos/lesson_event_notification_description.txt b/aleksis/apps/chronos/templates/chronos/lesson_event_notification_description.txt
new file mode 100644
index 00000000..308e1677
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/lesson_event_notification_description.txt
@@ -0,0 +1 @@
+{% load i18n %}{% with rooms=event.room_names_with_amends comment=event.comment %}{% trans "Groups" %}: {{ event.group_names|default:"–" }}{% if event.subject or event.amends and event.amends.subject %} · {% trans "Subject" %}: {{ event.subject_name_with_amends }}{% endif %} · {% trans "Teachers" %}: {{ event.teacher_names_with_amends|default:"–" }}{% if rooms %} · {% trans "Rooms" %}: {{ rooms }}{% endif %}{% if comment %} · {{ comment }}{% endif %}{% endwith %}
diff --git a/aleksis/apps/chronos/templates/chronos/lesson_event_notification_title.txt b/aleksis/apps/chronos/templates/chronos/lesson_event_notification_title.txt
new file mode 100644
index 00000000..298f4052
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/lesson_event_notification_title.txt
@@ -0,0 +1 @@
+{% load i18n %}{% trans "Lesson" %}{% if event.amends %} {% if event.cancelled %}{% trans "cancellation" %}{% else %}{% trans "substitution" %}{% endif %}{% endif %} {{ status }}: {{ event.datetime_start|date:"SHORT_DATETIME_FORMAT" }} - {{ event.datetime_end|date:"SHORT_DATETIME_FORMAT" }} · {{ event.group_names }}
-- 
GitLab