Skip to content
Snippets Groups Projects
Commit d27dc18e authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch '209-reimplement-notification-system-on-base-of-new-calendar-system' into 'master'

Resolve "Reimplement notification system on base of new calendar system"

Closes #209

See merge request !360
parents 6f001a3e aca9672b
No related branches found
No related tags found
1 merge request!360Resolve "Reimplement notification system on base of new calendar system"
Pipeline #194747 failed
......@@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog`_,
and this project adheres to `Semantic Versioning`_.
Unreleased
----------
Added
~~~~~
* Calendar alarms and notifications on creating, changing and deleting lesson substitutions.
`4.0.0.dev9`_ - 2024-12-08
--------------------------
......
......@@ -65,7 +65,7 @@
</template>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #cancelled.field="{ attrs, on }">
<v-checkbox v-bind="attrs" v-on="on" />
<v-checkbox :false-value="false" v-bind="attrs" v-on="on" />
</template>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #comment.field="{ attrs, on }">
......@@ -154,8 +154,9 @@ export default {
},
methods: {
transformCreateData(item) {
let { cancelled, ...createItem } = item;
return {
...item,
...createItem,
amends: this.selectedEvent.meta.id,
datetimeStart: this.$toUTCISO(this.selectedEvent.startDateTime),
datetimeEnd: this.$toUTCISO(this.selectedEvent.endDateTime),
......
# 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', '0022_add_substitution_global_permission'),
('core', '0069_add_calendar_alarm'),
]
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')),
('status', models.CharField(choices=[('C', 'Created'), ('E', 'Edited'), ('D', 'Deleted')], default='C', max_length=1, verbose_name='Status')),
],
options={
'verbose_name': 'Lesson event alarm',
'verbose_name_plural': 'Lesson event alarms',
},
bases=('core.calendaralarm',),
),
]
......@@ -3,7 +3,7 @@ from __future__ import annotations
import itertools
from collections.abc import Iterable
from datetime import date, datetime
from datetime import date, datetime, timedelta
from typing import Any
from django.contrib.contenttypes.models import ContentType
......@@ -32,7 +32,7 @@ from aleksis.core.managers import (
from aleksis.core.mixins import (
GlobalPermissionModel,
)
from aleksis.core.models import CalendarEvent, Group, Person, Room
from aleksis.core.models import CalendarAlarm, CalendarEvent, Group, Person, Room
from aleksis.core.util.core_helpers import get_site_preferences, has_person
from aleksis.core.util.predicates import check_global_permission
......@@ -157,6 +157,12 @@ class ChronosGlobalPermissions(GlobalPermissionModel):
)
class LessonEventAlarmStatusChoices(models.TextChoices):
CREATED = "C", _("Created")
EDITED = "E", _("Edited")
DELETED = "D", _("Deleted")
class LessonEvent(CalendarEvent):
"""Calendar feed for lessons."""
......@@ -626,11 +632,127 @@ class LessonEvent(CalendarEvent):
return substitutions
def save(self, *args, **kwargs):
adding_status = self._state.adding
super().save(*args, **kwargs)
# Save alarm in lesson event alarm model
if self.amends:
if adding_status:
alarm = LessonEventAlarm(
event=self,
send_notifications=get_site_preferences()[
"chronos__send_substitution_notifications"
],
)
alarm.save()
else:
alarms = LessonEventAlarm.objects.filter(
event=self, status=LessonEventAlarmStatusChoices.CREATED
)
for alarm in alarms:
alarm.update_or_create_follow_up()
class Meta:
verbose_name = _("Lesson Event")
verbose_name_plural = _("Lesson Events")
@receiver(models.signals.m2m_changed, sender=LessonEvent.teachers.through)
def create_alarm_on_teachers_m2m_changed(
sender: LessonEvent.teachers.through,
instance: models.Model,
action: str,
reverse: bool,
model: models.Model,
pk_set: set | None = None,
**kwargs,
) -> None:
"""Ensures teachers of amending lessons are included in the respective alarm's recipients."""
alarms = LessonEventAlarm.objects.filter(
event=instance, status=LessonEventAlarmStatusChoices.CREATED
)
for alarm in alarms:
alarm.update_or_create_follow_up()
class LessonEventAlarm(CalendarAlarm):
"""Alarm model for lesson events."""
status = models.CharField(
verbose_name=_("Status"),
max_length=1,
choices=LessonEventAlarmStatusChoices,
default=LessonEventAlarmStatusChoices.CREATED,
)
def value_description(self, request: HttpRequest | None = None) -> str:
return LessonEvent.value_title(self.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
- 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_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": LessonEventAlarmStatusChoices(self.status).label.lower(),
},
)
def value_notification_description(self, request: HttpRequest | None = None) -> str:
return render_to_string(
"chronos/lesson_event_notification_description.txt",
{
"event": self.event,
"status": LessonEventAlarmStatusChoices(self.status).label.lower(),
},
)
def value_notification_icon(self, request: HttpRequest | None = None) -> str:
return "calendar-remove-outline" if self.event.cancelled else "calendar-alert-outline"
@property
def has_sent_notifications(self) -> bool:
return self.notifications.filter(sent=True).exists()
def update_or_create_follow_up(self):
if self.has_sent_notifications:
follow_up_alarm = LessonEventAlarm(
event=self.event,
send_notifications=get_site_preferences()[
"chronos__send_substitution_notifications"
],
status=LessonEventAlarmStatusChoices.EDITED,
)
follow_up_alarm.save()
elif get_site_preferences()["chronos__send_substitution_notifications"]:
self.update_or_create_notifications()
class Meta:
verbose_name = _("Lesson event alarm")
verbose_name_plural = _("Lesson event alarms")
class SupervisionEvent(LessonEvent):
"""Calendar feed for supervisions."""
......
from datetime import time
from datetime import time, timedelta
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
......@@ -8,6 +8,7 @@ from colorfield.widgets import ColorWidget
from dynamic_preferences.preferences import Section
from dynamic_preferences.types import (
BooleanPreference,
DurationPreference,
IntegerPreference,
ModelMultipleChoicePreference,
MultipleChoicePreference,
......@@ -124,6 +125,60 @@ class SupervisionEventFeedColor(StringPreference):
required = True
@site_preferences_registry.register
class SendSubstitutionNotifications(BooleanPreference):
section = chronos
name = "send_substitution_notifications"
default = True
verbose_name = _(
"Send notifications to affected teachers when substitution lessons are created or edited."
)
@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
@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?")
@site_preferences_registry.register
class DaysInCalendar(MultipleChoicePreference):
section = chronos
......
{% 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 %}
{% 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 }}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment