diff --git a/aleksis/apps/chronos/admin.py b/aleksis/apps/chronos/admin.py
index 9da4770d092663c4baa2c5a9e770677305ea0121..2ed048d96d6cdb40a440e22a4a3fcb421b330013 100644
--- a/aleksis/apps/chronos/admin.py
+++ b/aleksis/apps/chronos/admin.py
@@ -1,218 +1,9 @@
 # noqa
 
 from django.contrib import admin
-from django.utils.html import format_html
-
-from guardian.admin import GuardedModelAdmin
-
-from aleksis.core.models import Room
 
 from .models import (
-    Absence,
-    AbsenceReason,
     AutomaticPlan,
-    Break,
-    Event,
-    ExtraLesson,
-    Holiday,
-    Lesson,
-    LessonPeriod,
-    LessonSubstitution,
-    Subject,
-    Supervision,
-    SupervisionArea,
-    SupervisionSubstitution,
-    TimePeriod,
-    ValidityRange,
 )
-from .util.format import format_date_period, format_m2m
-
-
-def colour_badge(fg: str, bg: str, val: str):
-    html = """
-    <div style="
-        color: {};
-        background-color: {};
-        padding-top: 3px;
-        padding-bottom: 4px;
-        text-align: center;
-        border-radius: 3px;
-    ">{}</span>
-    """
-    return format_html(html, fg, bg, val)
-
-
-class AbsenceReasonAdmin(admin.ModelAdmin):
-    list_display = ("short_name", "name")
-    list_display_links = ("short_name", "name")
-
-
-admin.site.register(AbsenceReason, AbsenceReasonAdmin)
-
-
-class AbsenceAdmin(admin.ModelAdmin):
-    def start(self, obj):
-        return format_date_period(obj.date_start, obj.period_from)
-
-    def end(self, obj):
-        return format_date_period(obj.date_end, obj.period_to)
-
-    list_display = ("__str__", "reason", "start", "end")
-
-
-admin.site.register(Absence, AbsenceAdmin)
-
-
-class SupervisionInline(admin.TabularInline):
-    model = Supervision
-
-
-class BreakAdmin(admin.ModelAdmin):
-    list_display = ("__str__", "after_period", "before_period")
-    inlines = [SupervisionInline]
-
-
-admin.site.register(Break, BreakAdmin)
-
-
-class EventAdmin(admin.ModelAdmin):
-    def start(self, obj):
-        return format_date_period(obj.date_start, obj.period_from)
-
-    def end(self, obj):
-        return format_date_period(obj.date_end, obj.period_to)
-
-    def _groups(self, obj):
-        return format_m2m(obj.groups)
-
-    def _teachers(self, obj):
-        return format_m2m(obj.teachers)
-
-    def _rooms(self, obj):
-        return format_m2m(obj.rooms)
-
-    filter_horizontal = ("groups", "teachers", "rooms")
-    list_display = ("__str__", "_groups", "_teachers", "_rooms", "start", "end")
-
-
-admin.site.register(Event, EventAdmin)
-
-
-class ExtraLessonAdmin(admin.ModelAdmin):
-    def _groups(self, obj):
-        return format_m2m(obj.groups)
-
-    def _teachers(self, obj):
-        return format_m2m(obj.teachers)
-
-    list_display = ("week", "period", "subject", "_groups", "_teachers", "room")
-
-
-admin.site.register(ExtraLesson, ExtraLessonAdmin)
-
-
-class HolidayAdmin(admin.ModelAdmin):
-    list_display = ("title", "date_start", "date_end")
-
-
-admin.site.register(Holiday, HolidayAdmin)
-
-
-class LessonPeriodInline(admin.TabularInline):
-    model = LessonPeriod
-
-
-class LessonSubstitutionAdmin(admin.ModelAdmin):
-    list_display = ("lesson_period", "week", "date")
-    list_display_links = ("lesson_period", "week", "date")
-    filter_horizontal = ("teachers",)
-
-
-admin.site.register(LessonSubstitution, LessonSubstitutionAdmin)
-
-
-class LessonAdmin(admin.ModelAdmin):
-    def _groups(self, obj):
-        return format_m2m(obj.groups)
-
-    def _teachers(self, obj):
-        return format_m2m(obj.teachers)
-
-    filter_horizontal = ["teachers", "groups"]
-    inlines = [LessonPeriodInline]
-    list_filter = ("subject", "groups", "groups__parent_groups", "teachers")
-    list_display = ("_groups", "subject", "_teachers")
-
-
-admin.site.register(Lesson, LessonAdmin)
-
-
-class RoomAdmin(GuardedModelAdmin):
-    list_display = ("short_name", "name")
-    list_display_links = ("short_name", "name")
-
-
-admin.site.register(Room, RoomAdmin)
-
-
-class SubjectAdmin(admin.ModelAdmin):
-    def _colour(self, obj):
-        return colour_badge(
-            obj.colour_fg,
-            obj.colour_bg,
-            obj.short_name,
-        )
-
-    list_display = ("short_name", "name", "_colour")
-    list_display_links = ("short_name", "name")
-
-
-admin.site.register(Subject, SubjectAdmin)
-
-
-class SupervisionAreaAdmin(admin.ModelAdmin):
-    def _colour(self, obj):
-        return colour_badge(
-            obj.colour_fg,
-            obj.colour_bg,
-            obj.short_name,
-        )
-
-    list_display = ("short_name", "name", "_colour")
-    list_display_links = ("short_name", "name")
-    inlines = [SupervisionInline]
-
-
-admin.site.register(SupervisionArea, SupervisionAreaAdmin)
-
-
-class SupervisionSubstitutionAdmin(admin.ModelAdmin):
-    list_display = ("supervision", "date")
-
-
-admin.site.register(SupervisionSubstitution, SupervisionSubstitutionAdmin)
-
-
-class SupervisionAdmin(admin.ModelAdmin):
-    list_display = ("break_item", "area", "teacher")
-
-
-admin.site.register(Supervision, SupervisionAdmin)
-
-
-class TimePeriodAdmin(admin.ModelAdmin):
-    list_display = ("weekday", "period", "time_start", "time_end")
-    list_display_links = ("weekday", "period")
-
-
-admin.site.register(TimePeriod, TimePeriodAdmin)
-
-
-class ValidityRangeAdmin(admin.ModelAdmin):
-    list_display = ("__str__", "date_start", "date_end")
-    list_display_links = ("__str__", "date_start", "date_end")
-
-
-admin.site.register(ValidityRange, ValidityRangeAdmin)
 
 admin.site.register(AutomaticPlan)
diff --git a/aleksis/apps/chronos/apps.py b/aleksis/apps/chronos/apps.py
index 377cefd6c1a90969c1c6c650da75fad34d3f2ae5..8eeee15eb1aebf7170021a338a22ca7c0cc5db68 100644
--- a/aleksis/apps/chronos/apps.py
+++ b/aleksis/apps/chronos/apps.py
@@ -1,6 +1,3 @@
-from typing import Any, Optional
-
-import django.apps
 from django.db import transaction
 
 from reversion.signals import post_revision_commit
@@ -37,75 +34,3 @@ class ChronosConfig(AppConfig):
             transaction.on_commit(lambda: handle_new_revision.delay(revision.pk))
 
         post_revision_commit.connect(_handle_post_revision_commit, weak=False)
-
-    def _ensure_notification_task(self):
-        """Update or create the task for sending notifications."""
-        from django.conf import settings  # noqa
-
-        from celery import schedules
-        from django_celery_beat.models import CrontabSchedule, PeriodicTask
-
-        from aleksis.core.util.core_helpers import get_site_preferences
-
-        time_for_sending = get_site_preferences()["chronos__time_for_sending_notifications"]
-        active = get_site_preferences()["chronos__send_notifications_site"]
-
-        if active:
-            schedule = schedules.crontab(
-                minute=str(time_for_sending.minute), hour=str(time_for_sending.hour)
-            )
-            schedule = CrontabSchedule.from_schedule(schedule)
-            schedule.timezone = settings.TIME_ZONE
-            schedule.save()
-
-        possible_periodic_tasks = PeriodicTask.objects.filter(
-            task="chronos_send_notifications_for_next_day"
-        )
-
-        if not active:
-            possible_periodic_tasks.delete()
-
-        elif possible_periodic_tasks.exists():
-            task = possible_periodic_tasks[0]
-            for d_task in possible_periodic_tasks:
-                if d_task != task:
-                    d_task.delete()
-
-            if task.crontab != schedule:
-                task.interval, task.solar, task.clocked = None, None, None
-                task.crontab = schedule
-                task.save()
-
-        elif active:
-            PeriodicTask.objects.get_or_create(
-                task="chronos_send_notifications_for_next_day",
-                crontab=schedule,
-                defaults=dict(name="Send notifications for next day (automatic schedule)"),
-            )
-
-    def preference_updated(
-        self,
-        sender: Any,
-        section: Optional[str] = None,
-        name: Optional[str] = None,
-        old_value: Optional[Any] = None,
-        new_value: Optional[Any] = None,
-        **kwargs,
-    ) -> None:
-        if section == "chronos" and name in (
-            "send_notifications_site",
-            "time_for_sending_notifications",
-        ):
-            self._ensure_notification_task()
-
-    def post_migrate(
-        self,
-        app_config: django.apps.AppConfig,
-        verbosity: int,
-        interactive: bool,
-        using: str,
-        **kwargs,
-    ) -> None:
-        super().post_migrate(app_config, verbosity, interactive, using, **kwargs)
-        # Ensure that the notification task is created after setting up AlekSIS
-        self._ensure_notification_task()
diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py
index 6b2f4c3cc7a6ec190624e8db5c69c3edd3c818d6..e4387402ec83f8887e4d447962e99c0b8385e866 100644
--- a/aleksis/apps/chronos/managers.py
+++ b/aleksis/apps/chronos/managers.py
@@ -1,77 +1,13 @@
-from collections.abc import Iterable
-from datetime import date, datetime, timedelta
 from enum import Enum
-from typing import TYPE_CHECKING, Optional, Union
+from typing import Optional, Union
 
-from django.db import models
-from django.db.models import ExpressionWrapper, F, Func, Q, QuerySet, Value
-from django.db.models.fields import DateField
-from django.db.models.functions import Concat
+from django.db.models import Q
 
-from calendarweek import CalendarWeek
-
-from aleksis.apps.chronos.util.date import week_weekday_from_date, week_weekday_to_date
 from aleksis.apps.cursus.models import Course
 from aleksis.core.managers import (
-    AlekSISBaseManagerWithoutMigrations,
-    DateRangeQuerySetMixin,
     RecurrencePolymorphicQuerySet,
-    SchoolTermRelatedQuerySet,
 )
 from aleksis.core.models import Group, Person, Room
-from aleksis.core.util.core_helpers import get_site_preferences
-
-if TYPE_CHECKING:
-    from .models import Holiday, LessonPeriod, ValidityRange
-
-
-class ValidityRangeQuerySet(QuerySet, DateRangeQuerySetMixin):
-    """Custom query set for validity ranges."""
-
-
-class ValidityRangeRelatedQuerySet(QuerySet):
-    """Custom query set for all models related to validity ranges."""
-
-    def within_dates(self, start: date, end: date) -> "ValidityRangeRelatedQuerySet":
-        """Filter for all objects within a date range."""
-        return self.filter(validity__date_start__lte=end, validity__date_end__gte=start)
-
-    def in_week(self, wanted_week: CalendarWeek) -> "ValidityRangeRelatedQuerySet":
-        """Filter for all objects within a calendar week."""
-        return self.within_dates(wanted_week[0], wanted_week[6])
-
-    def on_day(self, day: date) -> "ValidityRangeRelatedQuerySet":
-        """Filter for all objects on a certain day."""
-        return self.within_dates(day, day)
-
-    def for_validity_range(self, validity_range: "ValidityRange") -> "ValidityRangeRelatedQuerySet":
-        return self.filter(validity=validity_range)
-
-    def for_current_or_all(self) -> "ValidityRangeRelatedQuerySet":
-        """Get all objects related to current validity range.
-
-        If there is no current validity range, it will return all objects.
-        """
-        from aleksis.apps.chronos.models import ValidityRange
-
-        current_validity_range = ValidityRange.current
-        if current_validity_range:
-            return self.for_validity_range(current_validity_range)
-        else:
-            return self
-
-    def for_current_or_none(self) -> Union["ValidityRangeRelatedQuerySet", None]:
-        """Get all objects related to current validity range.
-
-        If there is no current validity range, it will return `None`.
-        """
-        from aleksis.apps.chronos.models import ValidityRange
-
-        current_validity_range = ValidityRange.current
-        if current_validity_range:
-            return self.for_validity_range(current_validity_range)
-        else:
-            return None
 
 
 class TimetableType(Enum):
@@ -86,786 +22,6 @@ class TimetableType(Enum):
         return cls.__members__.get(s.upper())
 
 
-class LessonPeriodManager(AlekSISBaseManagerWithoutMigrations):
-    """Manager adding specific methods to lesson periods."""
-
-    def get_queryset(self):
-        """Ensure all related lesson data is loaded as well."""
-        return (
-            super()
-            .get_queryset()
-            .select_related(
-                "lesson",
-                "lesson__subject",
-                "period",
-                "room",
-                "lesson__validity",
-                "lesson__validity__school_term",
-            )
-            .prefetch_related(
-                "lesson__groups",
-                "lesson__groups__parent_groups",
-                "lesson__teachers",
-                "substitutions",
-            )
-        )
-
-
-class LessonSubstitutionManager(AlekSISBaseManagerWithoutMigrations):
-    """Manager adding specific methods to lesson substitutions."""
-
-    def get_queryset(self):
-        """Ensure all related lesson data is loaded as well."""
-        return (
-            super()
-            .get_queryset()
-            .select_related(
-                "lesson_period",
-                "lesson_period__lesson",
-                "lesson_period__lesson__subject",
-                "subject",
-                "lesson_period__period",
-                "room",
-                "lesson_period__room",
-            )
-            .prefetch_related(
-                "lesson_period__lesson__groups",
-                "lesson_period__lesson__groups__parent_groups",
-                "teachers",
-                "lesson_period__lesson__teachers",
-            )
-        )
-
-
-class SupervisionManager(AlekSISBaseManagerWithoutMigrations):
-    """Manager adding specific methods to supervisions."""
-
-    def get_queryset(self):
-        """Ensure all related data is loaded as well."""
-        return (
-            super()
-            .get_queryset()
-            .select_related(
-                "teacher",
-                "area",
-                "break_item",
-                "break_item__after_period",
-                "break_item__before_period",
-            )
-        )
-
-
-class SupervisionSubstitutionManager(AlekSISBaseManagerWithoutMigrations):
-    """Manager adding specific methods to supervision substitutions."""
-
-    def get_queryset(self):
-        """Ensure all related data is loaded as well."""
-        return (
-            super()
-            .get_queryset()
-            .select_related(
-                "teacher",
-                "supervision",
-                "supervision__teacher",
-                "supervision__area",
-                "supervision__break_item",
-                "supervision__break_item__after_period",
-                "supervision__break_item__before_period",
-            )
-        )
-
-
-class EventManager(AlekSISBaseManagerWithoutMigrations):
-    """Manager adding specific methods to events."""
-
-    def get_queryset(self):
-        """Ensure all related data is loaded as well."""
-        return (
-            super()
-            .get_queryset()
-            .select_related("period_from", "period_to")
-            .prefetch_related(
-                "groups",
-                "groups__school_term",
-                "groups__parent_groups",
-                "teachers",
-                "rooms",
-            )
-        )
-
-
-class ExtraLessonManager(AlekSISBaseManagerWithoutMigrations):
-    """Manager adding specific methods to extra lessons."""
-
-    def get_queryset(self):
-        """Ensure all related data is loaded as well."""
-        return (
-            super()
-            .get_queryset()
-            .select_related("room", "period", "subject")
-            .prefetch_related(
-                "groups",
-                "groups__school_term",
-                "groups__parent_groups",
-                "teachers",
-            )
-        )
-
-
-class BreakManager(AlekSISBaseManagerWithoutMigrations):
-    """Manager adding specific methods to breaks."""
-
-    def get_queryset(self):
-        """Ensure all related data is loaded as well."""
-        return super().get_queryset().select_related("before_period", "after_period")
-
-
-class WeekQuerySetMixin:
-    def annotate_week(self, week: Union[CalendarWeek]):
-        """Annotate all lessons in the QuerySet with the number of the provided calendar week."""
-        return self.annotate(
-            _week=models.Value(week.week, models.IntegerField()),
-            _year=models.Value(week.year, models.IntegerField()),
-        )
-
-    def alias_week(self, week: Union[CalendarWeek]):
-        """Add an alias to all lessons in the QuerySet with the number of the calendar week."""
-        return self.alias(
-            _week=models.Value(week.week, models.IntegerField()),
-            _year=models.Value(week.year, models.IntegerField()),
-        )
-
-
-class GroupByPeriodsMixin:
-    def group_by_periods(self, is_week: bool = False) -> dict:
-        """Group a QuerySet of objects with attribute period by period numbers and weekdays."""
-        per_period = {}
-        for obj in self:
-            period = obj.period.period
-            weekday = obj.period.weekday
-
-            if period not in per_period:
-                per_period[period] = [] if not is_week else {}
-
-            if is_week and weekday not in per_period[period]:
-                per_period[period][weekday] = []
-
-            if not is_week:
-                per_period[period].append(obj)
-            else:
-                per_period[period][weekday].append(obj)
-
-        return per_period
-
-
-class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin):
-    """Overrides default QuerySet to add specific methods for lesson data."""
-
-    # Overridden in the subclasses. Swaps the paths to the base lesson period
-    # and to any substitutions depending on whether the query is run on a
-    # lesson period or a substitution
-    _period_path = None
-    _subst_path = None
-
-    def within_dates(self, start: date, end: date):
-        """Filter for all lessons within a date range."""
-        return self.filter(
-            **{
-                self._period_path + "lesson__validity__date_start__lte": start,
-                self._period_path + "lesson__validity__date_end__gte": end,
-            }
-        )
-
-    def in_week(self, wanted_week: CalendarWeek):
-        """Filter for all lessons within a calendar week."""
-        return self.within_dates(
-            wanted_week[0] + timedelta(days=1) * (F(self._period_path + "period__weekday")),
-            wanted_week[0] + timedelta(days=1) * (F(self._period_path + "period__weekday")),
-        ).annotate_week(wanted_week)
-
-    def on_day(self, day: date):
-        """Filter for all lessons on a certain day."""
-        week, weekday = week_weekday_from_date(day)
-
-        return (
-            self.within_dates(day, day)
-            .filter(**{self._period_path + "period__weekday": weekday})
-            .annotate_week(week)
-        )
-
-    def at_time(self, when: Optional[datetime] = None):
-        """Filter for the lessons taking place at a certain point in time."""
-        now = when or datetime.now()
-        week, weekday = week_weekday_from_date(now.date())
-
-        return self.filter(
-            **{
-                self._period_path + "lesson__validity__date_start__lte": now.date(),
-                self._period_path + "lesson__validity__date_end__gte": now.date(),
-                self._period_path + "period__weekday": now.weekday(),
-                self._period_path + "period__time_start__lte": now.time(),
-                self._period_path + "period__time_end__gte": now.time(),
-            }
-        ).annotate_week(week)
-
-    def filter_participant(self, person: Union[Person, int]):
-        """Filter for all lessons a participant (student) attends."""
-        return self.filter(Q(**{self._period_path + "lesson__groups__members": person}))
-
-    def filter_group(self, group: Union[Group, int]):
-        """Filter for all lessons a group (class) regularly attends."""
-        if isinstance(group, int):
-            group = Group.objects.get(pk=group)
-
-        if group.parent_groups.all():
-            # Prevent to show lessons multiple times
-            return self.filter(Q(**{self._period_path + "lesson__groups": group}))
-        else:
-            return self.filter(
-                Q(**{self._period_path + "lesson__groups": group})
-                | Q(**{self._period_path + "lesson__groups__parent_groups": group})
-            )
-
-    def filter_groups(self, groups: Iterable[Group]) -> QuerySet:
-        """Filter for all lessons one of the groups regularly attends."""
-        return self.filter(
-            Q(**{self._period_path + "lesson__groups__in": groups})
-            | Q(**{self._period_path + "lesson__groups__parent_groups__in": groups})
-        )
-
-    def filter_teacher(self, teacher: Union[Person, int], is_smart: bool = True):
-        """Filter for all lessons given by a certain teacher."""
-        qs1 = self.filter(**{self._period_path + "lesson__teachers": teacher})
-        qs2 = self.filter(
-            **{
-                self._subst_path + "teachers": teacher,
-                self._subst_path + "week": F("_week"),
-                self._subst_path + "year": F("_year"),
-            }
-        )
-
-        if is_smart:
-            return qs1.union(qs2)
-        else:
-            return qs1
-
-    def filter_room(self, room: Union["Room", int], is_smart: bool = True):
-        """Filter for all lessons taking part in a certain room."""
-        qs1 = self.filter(**{self._period_path + "room": room})
-        qs2 = self.filter(
-            **{
-                self._subst_path + "room": room,
-                self._subst_path + "week": F("_week"),
-                self._subst_path + "year": F("_year"),
-            }
-        )
-
-        if is_smart:
-            return qs1.union(qs2)
-        else:
-            return qs1
-
-    def filter_from_type(
-        self, type_: TimetableType, obj: Union[Person, Group, "Room", int], is_smart: bool = True
-    ) -> Optional[models.QuerySet]:
-        """Filter lesson data for a group, teacher or room by provided type."""
-        if type_ == TimetableType.GROUP:
-            return self.filter_group(obj)
-        elif type_ == TimetableType.TEACHER:
-            return self.filter_teacher(obj, is_smart=is_smart)
-        elif type_ == TimetableType.ROOM:
-            return self.filter_room(obj, is_smart=is_smart)
-        else:
-            return None
-
-    def filter_from_person(self, person: Person) -> Optional[models.QuerySet]:
-        """Filter lesson data for a person."""
-        type_ = person.timetable_type
-
-        if type_ == TimetableType.TEACHER:
-            # Teacher
-
-            return self.filter_teacher(person)
-
-        elif type_ == TimetableType.GROUP:
-            # Student
-
-            return self.filter_participant(person)
-
-        else:
-            # If no student or teacher
-            return None
-
-    def daily_lessons_for_person(
-        self, person: Person, wanted_day: date
-    ) -> Optional[models.QuerySet]:
-        """Filter lesson data on a day by a person."""
-        if person.timetable_type is None:
-            return None
-
-        lesson_periods = self.on_day(wanted_day).filter_from_person(person)
-
-        return lesson_periods
-
-    def group_by_validity(self) -> dict["ValidityRange", list["LessonPeriod"]]:
-        """Group lesson periods by validity range as dictionary."""
-        lesson_periods_by_validity = {}
-        for lesson_period in self:
-            lesson_periods_by_validity.setdefault(lesson_period.lesson.validity, [])
-            lesson_periods_by_validity[lesson_period.lesson.validity].append(lesson_period)
-        return lesson_periods_by_validity
-
-    def next_lesson(
-        self, reference: "LessonPeriod", offset: Optional[int] = 1
-    ) -> Optional["LessonPeriod"]:
-        """Get another lesson in an ordered set of lessons.
-
-        By default, it returns the next lesson in the set. By passing the offset argument,
-        the n-th next lesson can be selected. By passing a negative number, the n-th
-        previous lesson can be selected.
-
-        This function will handle week, year and validity range changes automatically
-        if the queryset contains enough lesson data.
-        """
-        # Group lesson periods by validity to handle validity range changes correctly
-        lesson_periods_by_validity = self.group_by_validity()
-        validity_ranges = list(lesson_periods_by_validity.keys())
-
-        # List with lesson periods in the validity range of the reference lesson period
-        current_lesson_periods = lesson_periods_by_validity[reference.lesson.validity]
-        pks = [lesson_period.pk for lesson_period in current_lesson_periods]
-
-        # Position of the reference lesson period
-        index = pks.index(reference.id)
-
-        next_index = index + offset
-        if next_index > len(pks) - 1:
-            next_index %= len(pks)
-            week = reference._week + 1
-        elif next_index < 0:
-            next_index = len(pks) + next_index
-            week = reference._week - 1
-        else:
-            week = reference._week
-
-        # Check if selected week makes a year change necessary
-        year = reference._year
-        if week < 1:
-            year -= 1
-            week = CalendarWeek.get_last_week_of_year(year).week
-        elif week > CalendarWeek.get_last_week_of_year(year).week:
-            year += 1
-            week = 1
-
-        # Get the next lesson period in this validity range and it's date
-        # to check whether the validity range has to be changed
-        week = CalendarWeek(week=week, year=year)
-        next_lesson_period = current_lesson_periods[next_index]
-        next_lesson_period_date = week_weekday_to_date(week, next_lesson_period.period.weekday)
-
-        validity_index = validity_ranges.index(next_lesson_period.lesson.validity)
-
-        # If date of next lesson period is out of validity range (smaller) ...
-        if next_lesson_period_date < next_lesson_period.lesson.validity.date_start:
-            # ... we have to get the lesson period from the previous validity range
-            if validity_index == 0:
-                # There are no validity ranges (and thus no lessons)
-                # in the school term before this lesson period
-                return None
-
-            # Get new validity range and last lesson period of this validity range
-            new_validity = validity_ranges[validity_index - 1]
-            next_lesson_period = lesson_periods_by_validity[new_validity][-1]
-
-            # Build new week with the date from the new validity range/lesson period
-            week = CalendarWeek(
-                week=new_validity.date_end.isocalendar()[1], year=new_validity.date_end.year
-            )
-
-        # If date of next lesson period is out of validity range (larger) ...
-        elif next_lesson_period_date > next_lesson_period.lesson.validity.date_end:
-            # ... we have to get the lesson period from the next validity range
-            if validity_index >= len(validity_ranges) - 1:
-                # There are no validity ranges (and thus no lessons)
-                # in the school term after this lesson period
-                return None
-
-            # Get new validity range and first lesson period of this validity range
-            new_validity = validity_ranges[validity_index + 1]
-            next_lesson_period = lesson_periods_by_validity[new_validity][0]
-
-            # Build new week with the date from the new validity range/lesson period
-            week = CalendarWeek(
-                week=new_validity.date_start.isocalendar()[1], year=new_validity.date_start.year
-            )
-
-        # Do a new query here to be able to annotate the new week
-        return self.annotate_week(week).get(pk=next_lesson_period.pk)
-
-
-class LessonPeriodQuerySet(LessonDataQuerySet, GroupByPeriodsMixin):
-    """QuerySet with custom query methods for lesson periods."""
-
-    _period_path = ""
-    _subst_path = "substitutions__"
-
-
-class LessonSubstitutionQuerySet(LessonDataQuerySet):
-    """QuerySet with custom query methods for substitutions."""
-
-    _period_path = "lesson_period__"
-    _subst_path = ""
-
-    def within_dates(self, start: date, end: date):
-        """Filter for all substitutions within a date range."""
-        start_week = CalendarWeek.from_date(start)
-        end_week = CalendarWeek.from_date(end)
-        return self.filter(
-            week__gte=start_week.week,
-            week__lte=end_week.week,
-            year__gte=start_week.year,
-            year__lte=end_week.year,
-        ).filter(
-            Q(
-                week=start_week.week,
-                year=start_week.year,
-                lesson_period__period__weekday__gte=start.weekday(),
-            )
-            | Q(
-                week=end_week.week,
-                year=end_week.year,
-                lesson_period__period__weekday__lte=end.weekday(),
-            )
-            | (
-                ~Q(week=start_week.week, year=start_week.year)
-                & ~Q(week=end_week.week, year=end_week.year)
-            )
-        )
-
-    def in_week(self, wanted_week: CalendarWeek):
-        """Filter for all lessons within a calendar week."""
-        return self.filter(week=wanted_week.week, year=wanted_week.year).annotate_week(wanted_week)
-
-    def on_day(self, day: date):
-        """Filter for all lessons on a certain day."""
-        week, weekday = week_weekday_from_date(day)
-
-        return self.in_week(week).filter(lesson_period__period__weekday=weekday)
-
-    def at_time(self, when: Optional[datetime] = None):
-        """Filter for the lessons taking place at a certain point in time."""
-        now = when or datetime.now()
-
-        return self.on_day(now.date()).filter(
-            lesson_period__period__time_start__lte=now.time(),
-            lesson_period__period__time_end__gte=now.time(),
-        )
-
-    def affected_lessons(self):
-        """Return all lessons which are affected by selected substitutions."""
-        from .models import Lesson  # noaq
-
-        return Lesson.objects.filter(lesson_periods__substitutions__in=self).distinct()
-
-    def affected_teachers(self):
-        """Get affected teachers.
-
-        Return all teachers which are affected by
-        selected substitutions (as substituted or substituting).
-        """
-        return (
-            Person.objects.filter(
-                Q(lessons_as_teacher__in=self.affected_lessons()) | Q(lesson_substitutions__in=self)
-            )
-            .distinct()
-            .order_by("short_name")
-        )
-
-    def affected_groups(self):
-        """Return all groups which are affected by selected substitutions."""
-        return (
-            Group.objects.filter(lessons__in=self.affected_lessons())
-            .distinct()
-            .order_by("short_name")
-        )
-
-
-class DateRangeQuerySetMixin:
-    """QuerySet with custom query methods for models with date and period ranges.
-
-    Filterable fields: date_start, date_end, period_from, period_to
-    """
-
-    def within_dates(self, start: date, end: date):
-        """Filter for all events within a date range."""
-        return self.filter(date_start__lte=end, date_end__gte=start)
-
-    def in_week(self, wanted_week: CalendarWeek):
-        """Filter for all events within a calendar week."""
-        return self.within_dates(wanted_week[0], wanted_week[6])
-
-    def on_day(self, day: date):
-        """Filter for all events on a certain day."""
-        return self.within_dates(day, day)
-
-    def at_time(self, when: Optional[datetime] = None):
-        """Filter for the events taking place at a certain point in time."""
-        now = when or datetime.now()
-
-        return self.on_day(now.date()).filter(
-            period_from__time_start__lte=now.time(), period_to__time_end__gte=now.time()
-        )
-
-    def exclude_holidays(self, holidays: Iterable["Holiday"]) -> QuerySet:
-        """Exclude all objects which are in the provided holidays."""
-        q = Q()
-        for holiday in holidays:
-            q = q | Q(date_start__lte=holiday.date_end, date_end__gte=holiday.date_start)
-        return self.exclude(q)
-
-
-class AbsenceQuerySet(DateRangeQuerySetMixin, SchoolTermRelatedQuerySet):
-    """QuerySet with custom query methods for absences."""
-
-    def absent_teachers(self):
-        return Person.objects.filter(absences__in=self).distinct().order_by("short_name")
-
-    def absent_groups(self):
-        return Group.objects.filter(absences__in=self).distinct().order_by("short_name")
-
-    def absent_rooms(self):
-        return Person.objects.filter(absences__in=self).distinct().order_by("short_name")
-
-
-class HolidayQuerySet(QuerySet, DateRangeQuerySetMixin):
-    """QuerySet with custom query methods for holidays."""
-
-    def get_all_days(self) -> list[date]:
-        """Get all days included in the selected holidays."""
-        holiday_days = []
-        for holiday in self:
-            holiday_days += list(holiday.get_days())
-        return holiday_days
-
-
-class SupervisionQuerySet(ValidityRangeRelatedQuerySet, WeekQuerySetMixin):
-    """QuerySet with custom query methods for supervisions."""
-
-    def filter_by_weekday(self, weekday: int):
-        """Filter supervisions by weekday."""
-        return self.filter(
-            Q(break_item__before_period__weekday=weekday)
-            | Q(break_item__after_period__weekday=weekday)
-        )
-
-    def filter_by_teacher(self, teacher: Union[Person, int]):
-        """Filter for all supervisions given by a certain teacher."""
-        if self.count() > 0:
-            if hasattr(self[0], "_week"):
-                week = CalendarWeek(week=self[0]._week, year=self[0]._year)
-            else:
-                week = CalendarWeek.current_week()
-
-            dates = [week[w] for w in range(0, 7)]
-
-            return self.filter(
-                Q(substitutions__teacher=teacher, substitutions__date__in=dates)
-                | Q(teacher=teacher)
-            )
-
-        return self
-
-
-class TimetableQuerySet(models.QuerySet):
-    """Common query set methods for objects in timetables.
-
-    Models need following fields:
-    - groups
-    - teachers
-    - rooms (_multiple_rooms=True)/room (_multiple_rooms=False)
-    """
-
-    _multiple_rooms = True
-
-    def filter_participant(self, person: Union[Person, int]):
-        """Filter for all objects a participant (student) attends."""
-        return self.filter(Q(groups__members=person))
-
-    def filter_group(self, group: Union[Group, int]):
-        """Filter for all objects a group (class) attends."""
-        if isinstance(group, int):
-            group = Group.objects.get(pk=group)
-
-        if group.parent_groups.all():
-            # Prevent to show lessons multiple times
-            return self.filter(groups=group)
-        else:
-            return self.filter(Q(groups=group) | Q(groups__parent_groups=group))
-
-    def filter_groups(self, groups: Iterable[Group]) -> QuerySet:
-        """Filter for all objects one of the groups attends."""
-        return self.filter(Q(groups__in=groups) | Q(groups__parent_groups__in=groups)).distinct()
-
-    def filter_teacher(self, teacher: Union[Person, int]):
-        """Filter for all lessons given by a certain teacher."""
-        return self.filter(teachers=teacher)
-
-    def filter_room(self, room: Union["Room", int]):
-        """Filter for all objects taking part in a certain room."""
-        if self._multiple_rooms:
-            return self.filter(rooms=room)
-        else:
-            return self.filter(room=room)
-
-    def filter_from_type(
-        self, type_: TimetableType, obj: Union[Group, Person, "Room", int]
-    ) -> Optional[models.QuerySet]:
-        """Filter data for a group, teacher or room by provided type."""
-        if type_ == TimetableType.GROUP:
-            return self.filter_group(obj)
-        elif type_ == TimetableType.TEACHER:
-            return self.filter_teacher(obj)
-        elif type_ == TimetableType.ROOM:
-            return self.filter_room(obj)
-        else:
-            return None
-
-    def filter_from_person(self, person: Person) -> Optional[models.QuerySet]:
-        """Filter data by person."""
-        type_ = person.timetable_type
-
-        if type_ == TimetableType.TEACHER:
-            # Teacher
-
-            return self.filter_teacher(person)
-
-        elif type_ == TimetableType.GROUP:
-            # Student
-
-            return self.filter_participant(person)
-
-        else:
-            # If no student or teacher
-            return None
-
-
-class EventQuerySet(DateRangeQuerySetMixin, SchoolTermRelatedQuerySet, TimetableQuerySet):
-    """QuerySet with custom query methods for events."""
-
-    def annotate_day(self, day: date):
-        """Annotate all events in the QuerySet with the provided date."""
-        return self.annotate(_date=models.Value(day, models.DateField()))
-
-    def alias_day(self, day: date):
-        """Add an alias to all events in the QuerySet with the provided date."""
-        return self.alias(_date=models.Value(day, models.DateField()))
-
-
-class ExtraLessonQuerySet(TimetableQuerySet, SchoolTermRelatedQuerySet, GroupByPeriodsMixin):
-    """QuerySet with custom query methods for extra lessons."""
-
-    _multiple_rooms = False
-
-    def within_dates(self, start: date, end: date):
-        """Filter all extra lessons within a specific time range."""
-        return self.alias_day().filter(day__gte=start, day__lte=end)
-
-    def on_day(self, day: date):
-        """Filter all extra lessons on a day."""
-        return self.within_dates(day, day)
-
-    def _get_weekday_to_date(self):
-        """Get DB function to convert a weekday to a date."""
-        return ExpressionWrapper(
-            Func(
-                Concat(F("year"), F("week")),
-                Value("IYYYIW"),
-                output_field=DateField(),
-                function="TO_DATE",
-            )
-            + F("period__weekday"),
-            output_field=DateField(),
-        )
-
-    def annotate_day(self):
-        return self.annotate(day=self._get_weekday_to_date())
-
-    def alias_day(self):
-        return self.alias(day=self._get_weekday_to_date())
-
-    def exclude_holidays(self, holidays: Iterable["Holiday"]) -> QuerySet:
-        """Exclude all extra lessons which are in the provided holidays."""
-        q = Q()
-        for holiday in holidays:
-            q = q | Q(day__lte=holiday.date_end, day__gte=holiday.date_start)
-        return self.alias_day().exclude(q)
-
-
-class GroupPropertiesMixin:
-    """Mixin for common group properties.
-
-    Necessary method: `get_groups`
-    """
-
-    @property
-    def group_names(self, sep: Optional[str] = ", ") -> str:
-        return sep.join([group.short_name for group in self.get_groups()])
-
-    @property
-    def group_short_names(self, sep: Optional[str] = ", ") -> str:
-        return sep.join([group.short_name for group in self.get_groups()])
-
-    @property
-    def groups_to_show(self) -> QuerySet[Group]:
-        groups = self.get_groups()
-        if (
-            groups.count() == 1
-            and groups[0].parent_groups.all()
-            and get_site_preferences()["chronos__use_parent_groups"]
-        ):
-            return groups[0].parent_groups.all()
-        else:
-            return groups
-
-    @property
-    def groups_to_show_names(self, sep: Optional[str] = ", ") -> str:
-        return sep.join([group.short_name for group in self.groups_to_show])
-
-    @property
-    def groups_to_show_short_names(self, sep: Optional[str] = ", ") -> str:
-        return sep.join([group.short_name for group in self.groups_to_show])
-
-
-class TeacherPropertiesMixin:
-    """Mixin for common teacher properties.
-
-    Necessary method: `get_teachers`
-    """
-
-    @property
-    def teacher_names(self, sep: Optional[str] = ", ") -> str:
-        return sep.join([teacher.full_name for teacher in self.get_teachers()])
-
-    @property
-    def teacher_short_names(self, sep: str = ", ") -> str:
-        return sep.join([teacher.short_name for teacher in self.get_teachers()])
-
-
-class RoomPropertiesMixin:
-    """Mixin for common room properties.
-
-    Necessary method: `get_rooms`
-    """
-
-    @property
-    def room_names(self, sep: Optional[str] = ", ") -> str:
-        return sep.join([room.name for room in self.get_rooms()])
-
-    @property
-    def room_short_names(self, sep: str = ", ") -> str:
-        return sep.join([room.short_name for room in self.get_rooms()])
-
-
 class LessonEventQuerySet(RecurrencePolymorphicQuerySet):
     """Queryset with special query methods for lesson events."""
 
diff --git a/aleksis/apps/chronos/migrations/0001_initial.py b/aleksis/apps/chronos/migrations/0001_initial.py
index 758f099cade786ea7cbab3cb6a4616847fa2c2d6..cf9aea3066896d3724d8f7757a00cb136f3ffc80 100644
--- a/aleksis/apps/chronos/migrations/0001_initial.py
+++ b/aleksis/apps/chronos/migrations/0001_initial.py
@@ -116,8 +116,6 @@ class Migration(migrations.Migration):
             },
             bases=(
                 models.Model,
-                aleksis.apps.chronos.managers.GroupPropertiesMixin,
-                aleksis.apps.chronos.managers.TeacherPropertiesMixin,
             ),
             managers=[("objects", aleksis.core.managers.AlekSISBaseManager()),],
         ),
@@ -699,7 +697,7 @@ class Migration(migrations.Migration):
                 "verbose_name": "Extra lesson",
                 "verbose_name_plural": "Extra lessons",
             },
-            bases=(models.Model, aleksis.apps.chronos.managers.GroupPropertiesMixin),
+            bases=(models.Model,),
         ),
         migrations.CreateModel(
             name="Exam",
@@ -832,8 +830,6 @@ class Migration(migrations.Migration):
             },
             bases=(
                 models.Model,
-                aleksis.apps.chronos.managers.GroupPropertiesMixin,
-                aleksis.apps.chronos.managers.TeacherPropertiesMixin,
             ),
         ),
         migrations.AddField(
diff --git a/aleksis/apps/chronos/migrations/0004_substitution_extra_lesson_year.py b/aleksis/apps/chronos/migrations/0004_substitution_extra_lesson_year.py
index cb7383592d167f7d3aa4bf203aaa8cee503d7551..eeb02c4822fd609781ac8b02839511237114a0a2 100644
--- a/aleksis/apps/chronos/migrations/0004_substitution_extra_lesson_year.py
+++ b/aleksis/apps/chronos/migrations/0004_substitution_extra_lesson_year.py
@@ -1,8 +1,7 @@
 # Generated by Django 3.0.9 on 2020-08-13 14:06
 
 from django.db import migrations, models
-
-import aleksis.apps.chronos.util.date
+from django.utils import timezone
 
 
 def migrate_data(apps, schema_editor):
@@ -19,7 +18,7 @@ def migrate_data(apps, schema_editor):
         sub.save()
 
     for extra_lesson in ExtraLesson.objects.using(db_alias).all():
-        year = aleksis.apps.chronos.util.date.get_current_year()
+        year = timezone.now().year
         extra_lesson.year = year
         extra_lesson.save()
 
@@ -36,7 +35,7 @@ class Migration(migrations.Migration):
             model_name="extralesson",
             name="year",
             field=models.IntegerField(
-                default=aleksis.apps.chronos.util.date.get_current_year,
+                default=lambda: timezone.now().year,
                 verbose_name="Year",
             ),
         ),
@@ -44,7 +43,7 @@ class Migration(migrations.Migration):
             model_name="lessonsubstitution",
             name="year",
             field=models.IntegerField(
-                default=aleksis.apps.chronos.util.date.get_current_year,
+                default=lambda: timezone.now().year,
                 verbose_name="Year",
             ),
         ),
diff --git a/aleksis/apps/chronos/migrations/0018_check_new_models.py b/aleksis/apps/chronos/migrations/0018_check_new_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..144a1af5ce41b1313157ac45c75aa55facf4f224
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0018_check_new_models.py
@@ -0,0 +1,30 @@
+from django.db import migrations, models
+
+from django.apps import apps as global_apps
+
+def check_for_migration(apps, schema_editor):
+    if global_apps.is_installed('aleksis.apps.lesrooster'):
+        return
+
+    ValidityRange = apps.get_model('chronos', 'ValidityRange')
+    Subject = apps.get_model('chronos', 'Subject')
+    AbsenceReason = apps.get_model('chronos', 'AbsenceReason')
+    Absence = apps.get_model('chronos', 'Absence')
+    Holiday = apps.get_model('chronos', 'Holiday')
+    SupervisionArea = apps.get_model('chronos', 'SupervisionArea')
+
+    model_types = [ValidityRange, Subject, AbsenceReason, Absence, Holiday, SupervisionArea]
+
+    for model in model_types:
+        if model.objects.exists():
+            raise RuntimeError("You have legacy data. Please install AlekSIS-App-Lesrooster to migrate them.")
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0017_optional_slot_number'),
+    ]
+
+    operations = [
+        migrations.RunPython(check_for_migration),
+    ]
diff --git a/aleksis/apps/chronos/migrations/0019_remove_old_models.py b/aleksis/apps/chronos/migrations/0019_remove_old_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..f6668aabe3248e1717a2b08fb9cdf90f8a37a4c9
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0019_remove_old_models.py
@@ -0,0 +1,260 @@
+# Generated by Django 5.0.8 on 2024-08-14 13:08
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0018_check_new_models'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='absence',
+            name='group',
+        ),
+        migrations.RemoveField(
+            model_name='absence',
+            name='period_from',
+        ),
+        migrations.RemoveField(
+            model_name='absence',
+            name='period_to',
+        ),
+        migrations.RemoveField(
+            model_name='absence',
+            name='reason',
+        ),
+        migrations.RemoveField(
+            model_name='absence',
+            name='room',
+        ),
+        migrations.RemoveField(
+            model_name='absence',
+            name='school_term',
+        ),
+        migrations.RemoveField(
+            model_name='absence',
+            name='teacher',
+        ),
+        migrations.RemoveField(
+            model_name='break',
+            name='after_period',
+        ),
+        migrations.RemoveField(
+            model_name='break',
+            name='before_period',
+        ),
+        migrations.RemoveField(
+            model_name='break',
+            name='validity',
+        ),
+        migrations.RemoveField(
+            model_name='supervision',
+            name='break_item',
+        ),
+        migrations.RemoveField(
+            model_name='event',
+            name='groups',
+        ),
+        migrations.RemoveField(
+            model_name='event',
+            name='period_from',
+        ),
+        migrations.RemoveField(
+            model_name='event',
+            name='period_to',
+        ),
+        migrations.RemoveField(
+            model_name='event',
+            name='rooms',
+        ),
+        migrations.RemoveField(
+            model_name='event',
+            name='school_term',
+        ),
+        migrations.RemoveField(
+            model_name='event',
+            name='teachers',
+        ),
+        migrations.RemoveField(
+            model_name='exam',
+            name='lesson',
+        ),
+        migrations.RemoveField(
+            model_name='exam',
+            name='period_from',
+        ),
+        migrations.RemoveField(
+            model_name='exam',
+            name='period_to',
+        ),
+        migrations.RemoveField(
+            model_name='exam',
+            name='school_term',
+        ),
+        migrations.RemoveField(
+            model_name='extralesson',
+            name='exam',
+        ),
+        migrations.RemoveField(
+            model_name='extralesson',
+            name='groups',
+        ),
+        migrations.RemoveField(
+            model_name='extralesson',
+            name='period',
+        ),
+        migrations.RemoveField(
+            model_name='extralesson',
+            name='room',
+        ),
+        migrations.RemoveField(
+            model_name='extralesson',
+            name='school_term',
+        ),
+        migrations.RemoveField(
+            model_name='extralesson',
+            name='subject',
+        ),
+        migrations.RemoveField(
+            model_name='extralesson',
+            name='teachers',
+        ),
+        migrations.DeleteModel(
+            name='Holiday',
+        ),
+        migrations.RemoveField(
+            model_name='lesson',
+            name='groups',
+        ),
+        migrations.RemoveField(
+            model_name='lesson',
+            name='periods',
+        ),
+        migrations.RemoveField(
+            model_name='lesson',
+            name='subject',
+        ),
+        migrations.RemoveField(
+            model_name='lesson',
+            name='teachers',
+        ),
+        migrations.RemoveField(
+            model_name='lesson',
+            name='validity',
+        ),
+        migrations.RemoveField(
+            model_name='lessonperiod',
+            name='lesson',
+        ),
+        migrations.RemoveField(
+            model_name='lessonperiod',
+            name='period',
+        ),
+        migrations.RemoveField(
+            model_name='lessonperiod',
+            name='room',
+        ),
+        migrations.RemoveField(
+            model_name='lessonsubstitution',
+            name='lesson_period',
+        ),
+        migrations.RemoveField(
+            model_name='lessonsubstitution',
+            name='room',
+        ),
+        migrations.RemoveField(
+            model_name='lessonsubstitution',
+            name='subject',
+        ),
+        migrations.RemoveField(
+            model_name='lessonsubstitution',
+            name='teachers',
+        ),
+        migrations.RemoveField(
+            model_name='supervision',
+            name='area',
+        ),
+        migrations.RemoveField(
+            model_name='supervision',
+            name='teacher',
+        ),
+        migrations.RemoveField(
+            model_name='supervision',
+            name='validity',
+        ),
+        migrations.RemoveField(
+            model_name='supervisionsubstitution',
+            name='supervision',
+        ),
+        migrations.RemoveField(
+            model_name='supervisionsubstitution',
+            name='teacher',
+        ),
+        migrations.RemoveField(
+            model_name='timeperiod',
+            name='validity',
+        ),
+        migrations.RemoveField(
+            model_name='validityrange',
+            name='school_term',
+        ),
+        migrations.DeleteModel(
+            name='TimetableWidget',
+        ),
+        migrations.AlterModelOptions(
+            name='chronosglobalpermissions',
+            options={'managed': False, 'permissions': (('view_all_room_timetables', 'Can view all room timetables'), ('view_all_group_timetables', 'Can view all group timetables'), ('view_all_person_timetables', 'Can view all person timetables'), ('view_timetable_overview', 'Can view timetable overview'), ('view_substitutions', 'Can view substitutions table'))},
+        ),
+        migrations.AlterModelOptions(
+            name='supervisionevent',
+            options={'base_manager_name': 'objects'},
+        ),
+        migrations.DeleteModel(
+            name='AbsenceReason',
+        ),
+        migrations.DeleteModel(
+            name='Absence',
+        ),
+        migrations.DeleteModel(
+            name='Break',
+        ),
+        migrations.DeleteModel(
+            name='Event',
+        ),
+        migrations.DeleteModel(
+            name='Exam',
+        ),
+        migrations.DeleteModel(
+            name='ExtraLesson',
+        ),
+        migrations.DeleteModel(
+            name='Lesson',
+        ),
+        migrations.DeleteModel(
+            name='LessonPeriod',
+        ),
+        migrations.DeleteModel(
+            name='Subject',
+        ),
+        migrations.DeleteModel(
+            name='LessonSubstitution',
+        ),
+        migrations.DeleteModel(
+            name='SupervisionArea',
+        ),
+        migrations.DeleteModel(
+            name='Supervision',
+        ),
+        migrations.DeleteModel(
+            name='SupervisionSubstitution',
+        ),
+        migrations.DeleteModel(
+            name='TimePeriod',
+        ),
+        migrations.DeleteModel(
+            name='ValidityRange',
+        ),
+    ]
diff --git a/aleksis/apps/chronos/mixins.py b/aleksis/apps/chronos/mixins.py
deleted file mode 100644
index 8cb4dab29467236f950b5b9ff56f5fa7ab7da435..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/mixins.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from datetime import date
-from typing import Union
-
-from django.db import models
-from django.utils.translation import gettext as _
-
-from calendarweek import CalendarWeek
-
-from aleksis.apps.chronos.util.date import week_weekday_to_date
-from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations
-from aleksis.core.mixins import ExtensibleModel
-
-from .managers import ValidityRangeRelatedQuerySet
-
-
-class ValidityRangeRelatedExtensibleModel(ExtensibleModel):
-    """Add relation to validity range."""
-
-    objects = AlekSISBaseManagerWithoutMigrations.from_queryset(ValidityRangeRelatedQuerySet)()
-
-    validity = models.ForeignKey(
-        "chronos.ValidityRange",
-        on_delete=models.CASCADE,
-        related_name="+",
-        verbose_name=_("Linked validity range"),
-        null=True,
-        blank=True,
-    )
-
-    class Meta:
-        abstract = True
-
-
-class WeekRelatedMixin:
-    @property
-    def date(self) -> date:
-        period = self.lesson_period.period if hasattr(self, "lesson_period") else self.period
-        return week_weekday_to_date(self.calendar_week, period.weekday)
-
-    @property
-    def calendar_week(self) -> CalendarWeek:
-        return CalendarWeek(week=self.week, year=self.year)
-
-
-class WeekAnnotationMixin:
-    def annotate_week(self, week: CalendarWeek):
-        """Annotate this lesson with the number of the provided calendar week."""
-        self._week = week.week
-        self._year = week.year
-
-    @property
-    def week(self) -> Union[CalendarWeek, None]:
-        """Get annotated week as `CalendarWeek`.
-
-        Defaults to `None` if no week is annotated.
-        """
-        if hasattr(self, "_week"):
-            return CalendarWeek(week=self._week, year=self._year)
-        else:
-            return None
diff --git a/aleksis/apps/chronos/model_extensions.py b/aleksis/apps/chronos/model_extensions.py
index 8f43350ee5a5ab019bad41f9674737e414c9d2f3..457ef9ef8d0183bf5ddfa5c6799a7e4d14db20d5 100644
--- a/aleksis/apps/chronos/model_extensions.py
+++ b/aleksis/apps/chronos/model_extensions.py
@@ -1,142 +1,6 @@
-from datetime import date
-from typing import Optional, Union
-
-from django.dispatch import receiver
 from django.utils.translation import gettext_lazy as _
 
-from reversion.models import Revision
-
-from aleksis.core.models import Announcement, Group, Person
-from aleksis.core.util.core_helpers import get_site_preferences
-
-from .managers import TimetableType
-from .models import Lesson, LessonPeriod
-from .util.change_tracker import timetable_data_changed
-from .util.notifications import send_notifications_for_object
-
-
-@Person.property_
-def is_teacher(self):
-    """Check if the user has lessons as a teacher."""
-    return self.lesson_periods_as_teacher.exists()
-
-
-@Person.property_
-def timetable_type(self) -> Optional[TimetableType]:
-    """Return which type of timetable this user has."""
-    if self.is_teacher:
-        return TimetableType.TEACHER
-    elif self.primary_group:
-        return TimetableType.GROUP
-    else:
-        return None
-
-
-@Person.property_
-def timetable_object(self) -> Optional[Union[Group, Person]]:
-    """Return the object which has the user's timetable."""
-    type_ = self.timetable_type
-
-    if type_ == TimetableType.TEACHER:
-        return self
-    elif type_ == TimetableType.GROUP:
-        return self.primary_group
-    else:
-        return None
-
-
-@Person.property_
-def lessons_as_participant(self):
-    """Return a `QuerySet` containing all `Lesson`s this person participates in (as student).
-
-    .. note:: Only available when AlekSIS-App-Chronos is installed.
-
-    :Date: 2019-11-07
-    :Authors:
-        - Dominik George <dominik.george@teckids.org>
-    """
-    return Lesson.objects.filter(groups__members=self)
-
-
-@Person.property_
-def lesson_periods_as_participant(self):
-    """Return a `QuerySet` containing all `LessonPeriod`s this person participates in (as student).
-
-    .. note:: Only available when AlekSIS-App-Chronos is installed.
-
-    :Date: 2019-11-07
-    :Authors:
-        - Dominik George <dominik.george@teckids.org>
-    """
-    return LessonPeriod.objects.filter(lesson__groups__members=self)
-
-
-@Person.property_
-def lesson_periods_as_teacher(self):
-    """Return a `QuerySet` containing all `Lesson`s this person gives (as teacher).
-
-    .. note:: Only available when AlekSIS-App-Chronos is installed.
-
-    :Date: 2019-11-07
-    :Authors:
-        - Dominik George <dominik.george@teckids.org>
-    """
-    return LessonPeriod.objects.filter(lesson__teachers=self)
-
-
-@Person.method
-def lessons_on_day(self, day: date):
-    """Get all lessons of this person (either as participant or teacher) on the given day."""
-    qs = LessonPeriod.objects.on_day(day).filter_from_person(self)
-    if qs:
-        # This is a union queryset, so order by must be after the union.
-        return qs.order_by("period__period")
-    return None
-
-
-@Person.method
-def _adjacent_lesson(
-    self, lesson_period: "LessonPeriod", day: date, offset: int = 1
-) -> Union["LessonPeriod", None]:
-    """Get next/previous lesson of the person (either as participant or teacher) on the same day."""
-    daily_lessons = self.lessons_on_day(day)
-
-    if not daily_lessons:
-        return None
-
-    ids = list(daily_lessons.values_list("id", flat=True))
-
-    # Check if the lesson period is one of the person's lesson periods on this day
-    # and return None if it's not so
-    if lesson_period.pk not in ids:
-        return None
-
-    index = ids.index(lesson_period.pk)
-
-    if (offset > 0 and index + offset < len(ids)) or (offset < 0 and index >= -offset):
-        return daily_lessons[index + offset]
-    else:
-        return None
-
-
-@Person.method
-def next_lesson(self, lesson_period: "LessonPeriod", day: date) -> Union["LessonPeriod", None]:
-    """Get next lesson of the person (either as participant or teacher) on the same day."""
-    return self._adjacent_lesson(lesson_period, day)
-
-
-@Person.method
-def previous_lesson(self, lesson_period: "LessonPeriod", day: date) -> Union["LessonPeriod", None]:
-    """Get previous lesson of the person (either as participant or teacher) on the same day."""
-    return self._adjacent_lesson(lesson_period, day, offset=-1)
-
-
-def for_timetables(cls):
-    """Return all announcements that should be shown in timetable views."""
-    return cls.objects.all()
-
-
-Announcement.class_method(for_timetables)
+from aleksis.core.models import Group, Person
 
 # Dynamically add extra permissions to Group and Person models in core
 # Note: requires migrate afterwards
@@ -148,16 +12,3 @@ Person.add_permission(
     "view_person_timetable",
     _("Can view person timetable"),
 )
-
-
-@receiver(timetable_data_changed)
-def send_notifications(sender: Revision, **kwargs):
-    """Send notifications to users about the changes."""
-    if not get_site_preferences()["chronos__send_notifications_site"]:
-        return
-
-    for change in sender.changes.values():
-        if change.deleted:
-            continue
-
-        send_notifications_for_object(change.instance)
diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 3189999febab65467e7cd76ad6e273f438be089b..d2ca3cdd4c77e26f716c3a51af883fe1eb16cc75 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -2,1187 +2,41 @@
 from __future__ import annotations
 
 import itertools
-from collections.abc import Iterable, Iterator
-from datetime import date, datetime, time, timedelta
-from itertools import chain
+from collections.abc import Iterable
+from datetime import date
 from typing import Any
 
 from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.exceptions import PermissionDenied
 from django.core.validators import MinValueValidator
 from django.db import models
-from django.db.models import Max, Min, Q, QuerySet
-from django.db.models.functions import Coalesce
+from django.db.models import QuerySet
 from django.dispatch import receiver
 from django.http import HttpRequest
 from django.template.loader import render_to_string
-from django.urls import reverse
 from django.utils import timezone
-from django.utils.formats import date_format
-from django.utils.functional import classproperty
 from django.utils.translation import gettext_lazy as _
 
-from cache_memoize import cache_memoize
-from calendarweek.django import CalendarWeek, i18n_day_abbr_choices_lazy, i18n_day_name_choices_lazy
-from colorfield.fields import ColorField
-from model_utils import FieldTracker
 from reversion.models import Revision, Version
 
 from aleksis.apps.chronos.managers import (
-    AbsenceQuerySet,
-    BreakManager,
-    EventManager,
-    EventQuerySet,
-    ExtraLessonManager,
-    ExtraLessonQuerySet,
-    GroupPropertiesMixin,
-    HolidayQuerySet,
     LessonEventQuerySet,
-    LessonPeriodManager,
-    LessonPeriodQuerySet,
-    LessonSubstitutionManager,
-    LessonSubstitutionQuerySet,
     SupervisionEventQuerySet,
-    SupervisionManager,
-    SupervisionQuerySet,
-    SupervisionSubstitutionManager,
-    TeacherPropertiesMixin,
-    ValidityRangeQuerySet,
-)
-from aleksis.apps.chronos.mixins import (
-    ValidityRangeRelatedExtensibleModel,
-    WeekAnnotationMixin,
-    WeekRelatedMixin,
 )
 from aleksis.apps.chronos.util.change_tracker import _get_substitution_models, substitutions_changed
-from aleksis.apps.chronos.util.date import get_current_year
-from aleksis.apps.chronos.util.format import format_m2m
 from aleksis.apps.cursus import models as cursus_models
 from aleksis.apps.cursus.models import Course
 from aleksis.apps.resint.models import LiveDocument
 from aleksis.core.managers import (
-    AlekSISBaseManagerWithoutMigrations,
     RecurrencePolymorphicManager,
 )
 from aleksis.core.mixins import (
-    ExtensibleModel,
     GlobalPermissionModel,
-    SchoolTermRelatedExtensibleModel,
 )
-from aleksis.core.models import CalendarEvent, Group, Person, Room, SchoolTerm
+from aleksis.core.models import CalendarEvent, Group, Person, Room
 from aleksis.core.util.core_helpers import get_site_preferences, has_person
 
 
-class ValidityRange(ExtensibleModel):
-    """Validity range model.
-
-    This is used to link data to a validity range.
-    """
-
-    objects = AlekSISBaseManagerWithoutMigrations.from_queryset(ValidityRangeQuerySet)()
-
-    school_term = models.ForeignKey(
-        SchoolTerm,
-        on_delete=models.CASCADE,
-        verbose_name=_("School term"),
-        related_name="validity_ranges",
-    )
-    name = models.CharField(verbose_name=_("Name"), max_length=255, blank=True)
-
-    date_start = models.DateField(verbose_name=_("Start date"))
-    date_end = models.DateField(verbose_name=_("End date"))
-
-    @classmethod
-    @cache_memoize(3600)
-    def get_current(cls, day: date | None = None):
-        if not day:
-            day = timezone.now().date()
-        try:
-            return cls.objects.on_day(day).first()
-        except ValidityRange.DoesNotExist:
-            return None
-
-    @classproperty
-    def current(cls):
-        return cls.get_current()
-
-    def clean(self):
-        """Ensure there is only one validity range at each point of time."""
-        if self.date_end < self.date_start:
-            raise ValidationError(_("The start date must be earlier than the end date."))
-
-        if self.school_term and (
-            self.date_end > self.school_term.date_end
-            or self.date_start < self.school_term.date_start
-        ):
-            raise ValidationError(_("The validity range must be within the school term."))
-
-        qs = ValidityRange.objects.within_dates(self.date_start, self.date_end)
-        if self.pk:
-            qs = qs.exclude(pk=self.pk)
-        if qs.exists():
-            raise ValidationError(
-                _("There is already a validity range for this time or a part of this time.")
-            )
-
-    def __str__(self):
-        return self.name or f"{date_format(self.date_start)}–{date_format(self.date_end)}"
-
-    class Meta:
-        verbose_name = _("Validity range")
-        verbose_name_plural = _("Validity ranges")
-        constraints = [
-            models.UniqueConstraint(
-                fields=["school_term", "date_start", "date_end"], name="unique_dates_per_term"
-            ),
-        ]
-        indexes = [
-            models.Index(fields=["date_start", "date_end"], name="validity_date_start_date_end")
-        ]
-
-
-class TimePeriod(ValidityRangeRelatedExtensibleModel):
-    WEEKDAY_CHOICES = i18n_day_name_choices_lazy()
-    WEEKDAY_CHOICES_SHORT = i18n_day_abbr_choices_lazy()
-
-    weekday = models.PositiveSmallIntegerField(
-        verbose_name=_("Week day"), choices=i18n_day_name_choices_lazy()
-    )
-    period = models.PositiveSmallIntegerField(verbose_name=_("Number of period"))
-
-    time_start = models.TimeField(verbose_name=_("Start time"))
-    time_end = models.TimeField(verbose_name=_("End time"))
-
-    def __str__(self) -> str:
-        return f"{self.get_weekday_display()}, {self.period}."
-
-    @classmethod
-    def get_times_dict(cls) -> dict[int, tuple[datetime, datetime]]:
-        periods = {}
-        for period in cls.objects.for_current_or_all().all():
-            periods[period.period] = (period.time_start, period.time_end)
-
-        return periods
-
-    def get_date(self, week: CalendarWeek | None = None) -> date:
-        if isinstance(week, CalendarWeek):
-            wanted_week = week
-        else:
-            year = getattr(self, "_year", None) or date.today().year
-            week_number = getattr(self, "_week", None) or CalendarWeek().week
-
-            wanted_week = CalendarWeek(year=year, week=week_number)
-
-        return wanted_week[self.weekday]
-
-    def get_datetime_start(self, date_ref: CalendarWeek | int | date | None = None) -> datetime:
-        """Get datetime of lesson start in a specific week."""
-        day = date_ref if isinstance(date_ref, date) else self.get_date(date_ref)
-        return datetime.combine(day, self.time_start)
-
-    def get_datetime_end(self, date_ref: CalendarWeek | int | date | None = None) -> datetime:
-        """Get datetime of lesson end in a specific week."""
-        day = date_ref if isinstance(date_ref, date) else self.get_date(date_ref)
-        return datetime.combine(day, self.time_end)
-
-    @classmethod
-    def get_next_relevant_day(
-        cls, day: date | None = None, time: time | None = None, prev: bool = False
-    ) -> date:
-        """Return next (previous) day with lessons depending on date and time."""
-        if day is None:
-            day = timezone.now().date()
-
-        if time is not None and cls.time_max and not prev and time > cls.time_max:
-            day += timedelta(days=1)
-
-        cw = CalendarWeek.from_date(day)
-
-        if day.weekday() > cls.weekday_max:
-            if prev:
-                day = cw[cls.weekday_max]
-            else:
-                cw += 1
-                day = cw[cls.weekday_min]
-        elif day.weekday() < TimePeriod.weekday_min:
-            if prev:
-                cw -= 1
-                day = cw[cls.weekday_max]
-            else:
-                day = cw[cls.weekday_min]
-
-        return day
-
-    @classmethod
-    def get_relevant_week_from_datetime(cls, when: datetime | None = None) -> CalendarWeek:
-        """Return currently relevant week depending on current date and time."""
-        if not when:
-            when = timezone.now()
-
-        day = when.date()
-        time = when.time()
-
-        week = CalendarWeek.from_date(day)
-
-        if (cls.weekday_max and day.weekday() > cls.weekday_max) or (
-            cls.time_max and time > cls.time_max and day.weekday() == cls.weekday_max
-        ):
-            week += 1
-
-        return week
-
-    @classmethod
-    def get_prev_next_by_day(cls, day: date, url: str) -> tuple[str, str]:
-        """Build URLs for previous/next day."""
-        day_prev = cls.get_next_relevant_day(day - timedelta(days=1), prev=True)
-        day_next = cls.get_next_relevant_day(day + timedelta(days=1))
-
-        url_prev = reverse(url, args=[day_prev.year, day_prev.month, day_prev.day])
-        url_next = reverse(url, args=[day_next.year, day_next.month, day_next.day])
-
-        return url_prev, url_next
-
-    @classmethod
-    def from_period(cls, period: int, day: date) -> TimePeriod:
-        """Get `TimePeriod` object for a period on a specific date.
-
-        This will respect the relation to validity ranges.
-        """
-        return cls.objects.on_day(day).filter(period=period, weekday=day.weekday()).first()
-
-    @classproperty
-    @cache_memoize(3600)
-    def period_min(cls) -> int:
-        return (
-            cls.objects.for_current_or_all()
-            .aggregate(period__min=Coalesce(Min("period"), 1))
-            .get("period__min")
-        )
-
-    @classproperty
-    @cache_memoize(3600)
-    def period_max(cls) -> int:
-        return (
-            cls.objects.for_current_or_all()
-            .aggregate(period__max=Coalesce(Max("period"), 7))
-            .get("period__max")
-        )
-
-    @classproperty
-    @cache_memoize(3600)
-    def time_min(cls) -> time | None:
-        return cls.objects.for_current_or_all().aggregate(Min("time_start")).get("time_start__min")
-
-    @classproperty
-    @cache_memoize(3600)
-    def time_max(cls) -> time | None:
-        return cls.objects.for_current_or_all().aggregate(Max("time_end")).get("time_end__max")
-
-    @classproperty
-    @cache_memoize(3600)
-    def weekday_min(cls) -> int:
-        return (
-            cls.objects.for_current_or_all()
-            .aggregate(weekday__min=Coalesce(Min("weekday"), 0))
-            .get("weekday__min")
-        )
-
-    @classproperty
-    @cache_memoize(3600)
-    def weekday_max(cls) -> int:
-        return (
-            cls.objects.for_current_or_all()
-            .aggregate(weekday__max=Coalesce(Max("weekday"), 6))
-            .get("weekday__max")
-        )
-
-    @classproperty
-    @cache_memoize(3600)
-    def period_choices(cls) -> list[tuple[str | int, str]]:
-        """Build choice list of periods for usage within Django."""
-        time_periods = (
-            cls.objects.filter(weekday=cls.weekday_min)
-            .for_current_or_all()
-            .values("period", "time_start", "time_end")
-            .distinct()
-        )
-
-        period_choices = [("", "")] + [
-            (period, f"{period}.") for period in time_periods.values_list("period", flat=True)
-        ]
-
-        return period_choices
-
-    class Meta:
-        constraints = [
-            models.UniqueConstraint(
-                fields=["weekday", "period", "validity"], name="unique_period_per_range"
-            ),
-        ]
-        ordering = ["weekday", "period"]
-        indexes = [models.Index(fields=["time_start", "time_end"])]
-        verbose_name = _("Time period")
-        verbose_name_plural = _("Time periods")
-
-
-class Subject(ExtensibleModel):
-    short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True)
-    name = models.CharField(verbose_name=_("Long name"), max_length=255)
-
-    colour_fg = ColorField(verbose_name=_("Foreground colour"), blank=True)
-    colour_bg = ColorField(verbose_name=_("Background colour"), blank=True)
-
-    def __str__(self) -> str:
-        return f"{self.short_name} ({self.name})"
-
-    class Meta:
-        ordering = ["name", "short_name"]
-        verbose_name = _("Subject")
-        verbose_name_plural = _("Subjects")
-
-
-class Lesson(ValidityRangeRelatedExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
-    subject = models.ForeignKey(
-        "Subject",
-        on_delete=models.CASCADE,
-        related_name="lessons",
-        verbose_name=_("Subject"),
-    )
-    teachers = models.ManyToManyField(
-        "core.Person", related_name="lessons_as_teacher", verbose_name=_("Teachers")
-    )
-    periods = models.ManyToManyField(
-        "TimePeriod",
-        related_name="lessons",
-        through="LessonPeriod",
-        verbose_name=_("Periods"),
-    )
-    groups = models.ManyToManyField("core.Group", related_name="lessons", verbose_name=_("Groups"))
-
-    def get_year(self, week: int) -> int:
-        year = self.validity.date_start.year
-        if week < int(self.validity.date_start.strftime("%V")):
-            year += 1
-        return year
-
-    def get_calendar_week(self, week: int):
-        year = self.get_year(week)
-
-        return CalendarWeek(year=year, week=week)
-
-    def get_teachers(self) -> models.query.QuerySet:
-        """Get teachers relation."""
-        return self.teachers
-
-    @property
-    def _equal_lessons(self):
-        """Get all lesson periods with equal lessons in the whole school term."""
-
-        qs = Lesson.objects.filter(
-            subject=self.subject,
-            validity__school_term=self.validity.school_term,
-        )
-        for group in self.groups.all():
-            qs = qs.filter(groups=group)
-        return qs
-
-    def __str__(self):
-        return f"{format_m2m(self.groups)}, {self.subject.short_name}, {format_m2m(self.teachers)}"
-
-    class Meta:
-        ordering = ["validity__date_start", "subject"]
-        verbose_name = _("Lesson")
-        verbose_name_plural = _("Lessons")
-
-
-class LessonSubstitution(ExtensibleModel, TeacherPropertiesMixin, WeekRelatedMixin):
-    objects = LessonSubstitutionManager.from_queryset(LessonSubstitutionQuerySet)()
-
-    tracker = FieldTracker()
-
-    week = models.IntegerField(verbose_name=_("Week"), default=CalendarWeek.current_week)
-    year = models.IntegerField(verbose_name=_("Year"), default=get_current_year)
-
-    lesson_period = models.ForeignKey(
-        "LessonPeriod", models.CASCADE, "substitutions", verbose_name=_("Lesson period")
-    )
-
-    subject = models.ForeignKey(
-        "Subject",
-        on_delete=models.CASCADE,
-        related_name="lesson_substitutions",
-        null=True,
-        blank=True,
-        verbose_name=_("Subject"),
-    )
-    teachers = models.ManyToManyField(
-        "core.Person",
-        related_name="lesson_substitutions",
-        blank=True,
-        verbose_name=_("Teachers"),
-    )
-    room = models.ForeignKey(
-        "core.Room", models.CASCADE, null=True, blank=True, verbose_name=_("Room")
-    )
-
-    cancelled = models.BooleanField(default=False, verbose_name=_("Cancelled?"))
-    cancelled_for_teachers = models.BooleanField(
-        default=False, verbose_name=_("Cancelled for teachers?")
-    )
-
-    comment = models.TextField(verbose_name=_("Comment"), blank=True)
-
-    def clean(self) -> None:
-        if self.subject and self.cancelled:
-            raise ValidationError(_("Lessons can only be either substituted or cancelled."))
-
-    @property
-    def date(self):
-        week = CalendarWeek(week=self.week, year=self.year)
-        return week[self.lesson_period.period.weekday]
-
-    @property
-    def time_range(self) -> (timezone.datetime, timezone.datetime):
-        """Get the time range of this substitution."""
-        return timezone.datetime.combine(
-            self.date, self.lesson_period.period.time_start
-        ), timezone.datetime.combine(self.date, self.lesson_period.period.time_end)
-
-    def get_teachers(self):
-        return self.teachers
-
-    def __str__(self):
-        return f"{self.lesson_period}, {date_format(self.date)}"
-
-    class Meta:
-        ordering = [
-            "year",
-            "week",
-            "lesson_period__period__weekday",
-            "lesson_period__period__period",
-        ]
-        constraints = [
-            models.CheckConstraint(
-                check=~Q(cancelled=True, subject__isnull=False),
-                name="either_substituted_or_cancelled",
-            ),
-            models.UniqueConstraint(
-                fields=["lesson_period", "week", "year"], name="unique_period_per_week"
-            ),
-        ]
-        indexes = [
-            models.Index(fields=["week", "year"], name="substitution_week_year"),
-            models.Index(fields=["lesson_period"], name="substitution_lesson_period"),
-        ]
-        verbose_name = _("Lesson substitution")
-        verbose_name_plural = _("Lesson substitutions")
-
-
-class LessonPeriod(WeekAnnotationMixin, TeacherPropertiesMixin, ExtensibleModel):
-    label_ = "lesson_period"
-
-    objects = LessonPeriodManager.from_queryset(LessonPeriodQuerySet)()
-
-    lesson = models.ForeignKey(
-        "Lesson",
-        models.CASCADE,
-        related_name="lesson_periods",
-        verbose_name=_("Lesson"),
-    )
-    period = models.ForeignKey(
-        "TimePeriod",
-        models.CASCADE,
-        related_name="lesson_periods",
-        verbose_name=_("Time period"),
-    )
-
-    room = models.ForeignKey(
-        "core.Room",
-        models.CASCADE,
-        null=True,
-        related_name="lesson_periods",
-        verbose_name=_("Room"),
-    )
-
-    def get_substitution(self, week: CalendarWeek | None = None) -> LessonSubstitution:
-        wanted_week = week or self.week or CalendarWeek()
-
-        # We iterate over all substitutions because this can make use of
-        # prefetching when this model is loaded from outside, in contrast
-        # to .filter()
-        for substitution in self.substitutions.all():
-            if substitution.week == wanted_week.week and substitution.year == wanted_week.year:
-                return substitution
-        return None
-
-    def get_subject(self) -> Subject | None:
-        sub = self.get_substitution()
-        if sub and sub.subject:
-            return sub.subject
-        else:
-            return self.lesson.subject
-
-    def get_teachers(self) -> models.query.QuerySet:
-        sub = self.get_substitution()
-        if sub and sub.teachers.all():
-            return sub.teachers
-        else:
-            return self.lesson.teachers
-
-    def get_room(self) -> Room | None:
-        if self.get_substitution() and self.get_substitution().room:
-            return self.get_substitution().room
-        else:
-            return self.room
-
-    def get_groups(self) -> models.query.QuerySet:
-        return self.lesson.groups
-
-    @property
-    def group_names(self):
-        """Get group names as joined string."""
-        return self.lesson.group_names
-
-    @property
-    def group_short_names(self):
-        """Get group short names as joined string."""
-        return self.lesson.group_short_names
-
-    def __str__(self) -> str:
-        return f"{self.period}, {self.lesson}"
-
-    @property
-    def _equal_lesson_periods(self):
-        """Get all lesson periods with equal lessons in the whole school term."""
-
-        return LessonPeriod.objects.filter(lesson__in=self.lesson._equal_lessons)
-
-    @property
-    def next(self) -> LessonPeriod:  # noqa
-        """Get next lesson period of this lesson.
-
-        .. warning::
-            To use this property,  the provided lesson period must be annotated with a week.
-        """
-        return self._equal_lesson_periods.next_lesson(self)
-
-    @property
-    def prev(self) -> LessonPeriod:
-        """Get previous lesson period of this lesson.
-
-        .. warning::
-            To use this property,  the provided lesson period must be annotated with a week.
-        """
-        return self._equal_lesson_periods.next_lesson(self, -1)
-
-    def is_replaced_by_event(
-        self, events: Iterable[Event], groups: Iterable[Group] | None = None
-    ) -> bool:
-        """Check if this lesson period is replaced by an event."""
-        groups_of_event = set(chain(*[event.groups.all() for event in events]))
-
-        if groups:
-            # If the current group is a part of the event,
-            # there are no other lessons for the group.
-            groups = set(groups)
-            if groups.issubset(groups_of_event):
-                return True
-        else:
-            groups_lesson_period = set(self.lesson.groups.all())
-
-            # The lesson period isn't replacable if the lesson has no groups at all
-            if not groups_lesson_period:
-                return False
-
-            # This lesson period is replaced by an event ...
-            # ... if all groups of this lesson period are a part of the event ...
-            if groups_lesson_period.issubset(groups_of_event):
-                return True
-
-            all_parent_groups = set(
-                chain(*[group.parent_groups.all() for group in groups_lesson_period])
-            )
-            # ... or if all parent groups of this lesson period are a part of the event.
-            if all_parent_groups.issubset(groups_of_event):
-                return True
-
-    class Meta:
-        ordering = [
-            "lesson__validity__date_start",
-            "period__weekday",
-            "period__period",
-            "lesson__subject",
-        ]
-        indexes = [
-            models.Index(fields=["lesson", "period"], name="lesson_period_lesson_period"),
-            models.Index(fields=["room"], include=["lesson", "period"], name="lesson_period_room"),
-        ]
-        verbose_name = _("Lesson period")
-        verbose_name_plural = _("Lesson periods")
-
-
-class AbsenceReason(ExtensibleModel):
-    short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True)
-    name = models.CharField(verbose_name=_("Name"), blank=True, max_length=255)
-
-    def __str__(self):
-        if self.name:
-            return f"{self.short_name} ({self.name})"
-        else:
-            return self.short_name
-
-    class Meta:
-        verbose_name = _("Absence reason")
-        verbose_name_plural = _("Absence reasons")
-
-
-class Absence(SchoolTermRelatedExtensibleModel):
-    objects = AlekSISBaseManagerWithoutMigrations.from_queryset(AbsenceQuerySet)()
-
-    reason = models.ForeignKey(
-        "AbsenceReason",
-        on_delete=models.SET_NULL,
-        related_name="absences",
-        blank=True,
-        null=True,
-        verbose_name=_("Absence reason"),
-    )
-
-    teacher = models.ForeignKey(
-        "core.Person",
-        on_delete=models.CASCADE,
-        related_name="absences",
-        null=True,
-        blank=True,
-        verbose_name=_("Teacher"),
-    )
-    group = models.ForeignKey(
-        "core.Group",
-        on_delete=models.CASCADE,
-        related_name="absences",
-        null=True,
-        blank=True,
-        verbose_name=_("Group"),
-    )
-    room = models.ForeignKey(
-        "core.Room",
-        on_delete=models.CASCADE,
-        related_name="absences",
-        null=True,
-        blank=True,
-        verbose_name=_("Room"),
-    )
-
-    date_start = models.DateField(verbose_name=_("Start date"), null=True)
-    date_end = models.DateField(verbose_name=_("End date"), null=True)
-    period_from = models.ForeignKey(
-        "TimePeriod",
-        on_delete=models.CASCADE,
-        verbose_name=_("Start period"),
-        null=True,
-        related_name="+",
-    )
-    period_to = models.ForeignKey(
-        "TimePeriod",
-        on_delete=models.CASCADE,
-        verbose_name=_("End period"),
-        null=True,
-        related_name="+",
-    )
-    comment = models.TextField(verbose_name=_("Comment"), blank=True)
-
-    def __str__(self):
-        if self.teacher:
-            return str(self.teacher)
-        elif self.group:
-            return str(self.group)
-        elif self.room:
-            return str(self.room)
-        else:
-            return _("Unknown absence")
-
-    class Meta:
-        ordering = ["date_start"]
-        indexes = [models.Index(fields=["date_start", "date_end"])]
-        verbose_name = _("Absence")
-        verbose_name_plural = _("Absences")
-
-
-class Exam(SchoolTermRelatedExtensibleModel):
-    lesson = models.ForeignKey(
-        "Lesson",
-        on_delete=models.CASCADE,
-        related_name="exams",
-        verbose_name=_("Lesson"),
-    )
-
-    date = models.DateField(verbose_name=_("Date of exam"))
-    period_from = models.ForeignKey(
-        "TimePeriod",
-        on_delete=models.CASCADE,
-        verbose_name=_("Start period"),
-        related_name="+",
-    )
-    period_to = models.ForeignKey(
-        "TimePeriod",
-        on_delete=models.CASCADE,
-        verbose_name=_("End period"),
-        related_name="+",
-    )
-
-    title = models.CharField(verbose_name=_("Title"), max_length=255, blank=True)
-    comment = models.TextField(verbose_name=_("Comment"), blank=True)
-
-    class Meta:
-        ordering = ["date"]
-        indexes = [models.Index(fields=["date"])]
-        verbose_name = _("Exam")
-        verbose_name_plural = _("Exams")
-
-
-class Holiday(ExtensibleModel):
-    objects = AlekSISBaseManagerWithoutMigrations.from_queryset(HolidayQuerySet)()
-
-    title = models.CharField(verbose_name=_("Title"), max_length=255)
-    date_start = models.DateField(verbose_name=_("Start date"), null=True)
-    date_end = models.DateField(verbose_name=_("End date"), null=True)
-    comments = models.TextField(verbose_name=_("Comments"), blank=True)
-
-    def get_days(self) -> Iterator[date]:
-        delta = self.date_end - self.date_start
-        for i in range(delta.days + 1):
-            yield self.date_start + timedelta(days=i)
-
-    @classmethod
-    def on_day(cls, day: date) -> Holiday | None:
-        holidays = cls.objects.on_day(day)
-        if holidays.exists():
-            return holidays[0]
-        else:
-            return None
-
-    @classmethod
-    def in_week(cls, week: CalendarWeek) -> dict[int, Holiday | None]:
-        per_weekday = {}
-        holidays = Holiday.objects.in_week(week)
-
-        for weekday in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
-            holiday_date = week[weekday]
-            filtered_holidays = list(
-                filter(
-                    lambda h: holiday_date >= h.date_start and holiday_date <= h.date_end,
-                    holidays,
-                )
-            )
-            if filtered_holidays:
-                per_weekday[weekday] = filtered_holidays[0]
-
-        return per_weekday
-
-    def __str__(self):
-        return self.title
-
-    class Meta:
-        ordering = ["date_start"]
-        indexes = [models.Index(fields=["date_start", "date_end"])]
-        verbose_name = _("Holiday")
-        verbose_name_plural = _("Holidays")
-
-
-class SupervisionArea(ExtensibleModel):
-    short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True)
-    name = models.CharField(verbose_name=_("Long name"), max_length=255)
-    colour_fg = ColorField(default="#000000")
-    colour_bg = ColorField()
-
-    def __str__(self):
-        return f"{self.name} ({self.short_name})"
-
-    class Meta:
-        ordering = ["name"]
-        verbose_name = _("Supervision area")
-        verbose_name_plural = _("Supervision areas")
-
-
-class Break(ValidityRangeRelatedExtensibleModel):
-    objects = BreakManager()
-
-    short_name = models.CharField(verbose_name=_("Short name"), max_length=255)
-    name = models.CharField(verbose_name=_("Long name"), max_length=255)
-
-    after_period = models.ForeignKey(
-        "TimePeriod",
-        on_delete=models.CASCADE,
-        verbose_name=_("Time period after break starts"),
-        related_name="break_after",
-        blank=True,
-        null=True,
-    )
-    before_period = models.ForeignKey(
-        "TimePeriod",
-        on_delete=models.CASCADE,
-        verbose_name=_("Time period before break ends"),
-        related_name="break_before",
-        blank=True,
-        null=True,
-    )
-
-    @property
-    def weekday(self):
-        return self.after_period.weekday if self.after_period else self.before_period.weekday
-
-    @property
-    def after_period_number(self):
-        return self.after_period.period if self.after_period else self.before_period.period - 1
-
-    @property
-    def before_period_number(self):
-        return self.before_period.period if self.before_period else self.after_period.period + 1
-
-    @property
-    def time_start(self):
-        return self.after_period.time_end if self.after_period else None
-
-    @property
-    def time_end(self):
-        return self.before_period.time_start if self.before_period else None
-
-    @classmethod
-    def get_breaks_dict(cls) -> dict[int, tuple[datetime, datetime]]:
-        breaks = {}
-        for break_ in cls.objects.all():
-            breaks[break_.before_period_number] = break_
-
-        return breaks
-
-    def __str__(self):
-        return f"{self.name} ({self.short_name})"
-
-    class Meta:
-        ordering = ["after_period"]
-        indexes = [models.Index(fields=["after_period", "before_period"])]
-        verbose_name = _("Break")
-        verbose_name_plural = _("Breaks")
-        constraints = [
-            models.UniqueConstraint(
-                fields=["validity", "short_name"], name="unique_short_name_per_validity_break"
-            ),
-        ]
-
-
-class Supervision(ValidityRangeRelatedExtensibleModel, WeekAnnotationMixin):
-    objects = SupervisionManager.from_queryset(SupervisionQuerySet)()
-
-    area = models.ForeignKey(
-        SupervisionArea,
-        models.CASCADE,
-        verbose_name=_("Supervision area"),
-        related_name="supervisions",
-    )
-    break_item = models.ForeignKey(
-        Break, models.CASCADE, verbose_name=_("Break"), related_name="supervisions"
-    )
-    teacher = models.ForeignKey(
-        "core.Person",
-        models.CASCADE,
-        related_name="supervisions",
-        verbose_name=_("Teacher"),
-    )
-
-    def get_year(self, week: int) -> int:
-        year = self.validity.date_start.year
-        if week < int(self.validity.date_start.strftime("%V")):
-            year += 1
-        return year
-
-    def get_calendar_week(self, week: int):
-        year = self.get_year(week)
-
-        return CalendarWeek(year=year, week=week)
-
-    def get_substitution(self, week: CalendarWeek | None = None) -> SupervisionSubstitution | None:
-        wanted_week = week or self.week or CalendarWeek()
-        # We iterate over all substitutions because this can make use of
-        # prefetching when this model is loaded from outside, in contrast
-        # to .filter()
-        for substitution in self.substitutions.all():
-            for weekday in range(0, 7):
-                if substitution.date == wanted_week[weekday]:
-                    return substitution
-        return None
-
-    @property
-    def teachers(self):
-        return [self.teacher]
-
-    def __str__(self):
-        return f"{self.break_item}, {self.area}, {self.teacher}"
-
-    class Meta:
-        ordering = ["area", "break_item"]
-        verbose_name = _("Supervision")
-        verbose_name_plural = _("Supervisions")
-
-
-class SupervisionSubstitution(ExtensibleModel):
-    objects = SupervisionSubstitutionManager()
-
-    tracker = FieldTracker()
-
-    date = models.DateField(verbose_name=_("Date"))
-    supervision = models.ForeignKey(
-        Supervision,
-        models.CASCADE,
-        verbose_name=_("Supervision"),
-        related_name="substitutions",
-    )
-    teacher = models.ForeignKey(
-        "core.Person",
-        models.CASCADE,
-        related_name="substituted_supervisions",
-        verbose_name=_("Teacher"),
-    )
-
-    @property
-    def teachers(self):
-        return [self.teacher]
-
-    @property
-    def time_range(self) -> (timezone.datetime, timezone.datetime):
-        """Get the time range of this supervision substitution."""
-        return timezone.datetime.combine(
-            self.date,
-            self.supervision.break_item.time_start or self.supervision.break_item.time_end,
-        ), timezone.datetime.combine(
-            self.date,
-            self.supervision.break_item.time_end or self.supervision.break_item.time_start,
-        )
-
-    def __str__(self):
-        return f"{self.supervision}, {date_format(self.date)}"
-
-    class Meta:
-        ordering = ["date", "supervision"]
-        verbose_name = _("Supervision substitution")
-        verbose_name_plural = _("Supervision substitutions")
-
-
-class Event(SchoolTermRelatedExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
-    label_ = "event"
-
-    tracker = FieldTracker()
-
-    objects = EventManager.from_queryset(EventQuerySet)()
-
-    title = models.CharField(verbose_name=_("Title"), max_length=255, blank=True)
-
-    date_start = models.DateField(verbose_name=_("Start date"), null=True)
-    date_end = models.DateField(verbose_name=_("End date"), null=True)
-
-    period_from = models.ForeignKey(
-        "TimePeriod",
-        on_delete=models.CASCADE,
-        verbose_name=_("Start time period"),
-        related_name="+",
-    )
-    period_to = models.ForeignKey(
-        "TimePeriod",
-        on_delete=models.CASCADE,
-        verbose_name=_("End time period"),
-        related_name="+",
-    )
-
-    groups = models.ManyToManyField("core.Group", related_name="events", verbose_name=_("Groups"))
-    rooms = models.ManyToManyField("core.Room", related_name="events", verbose_name=_("Rooms"))
-    teachers = models.ManyToManyField(
-        "core.Person", related_name="events", verbose_name=_("Teachers")
-    )
-
-    def __str__(self):
-        if self.title:
-            return self.title
-        else:
-            return _("Event {pk}").format(pk=self.pk)
-
-    def get_period_min(self, day) -> int:
-        return (
-            TimePeriod.objects.on_day(day)
-            .aggregate(period__min=Coalesce(Min("period"), 1))
-            .get("period__min")
-        )
-
-    def get_period_max(self, day) -> int:
-        return (
-            TimePeriod.objects.on_day(day)
-            .aggregate(period__max=Coalesce(Max("period"), 7))
-            .get("period__max")
-        )
-
-    @property
-    def raw_period_from_on_day(self) -> TimePeriod:
-        """Get start period on the annotated day (as TimePeriod object).
-
-        If there is no date annotated, it will use the current date.
-        """
-        day = getattr(self, "_date", timezone.now().date())
-        if day != self.date_start:
-            return TimePeriod.from_period(self.get_period_min(day), day)
-        else:
-            return self.period_from
-
-    @property
-    def raw_period_to_on_day(self) -> TimePeriod:
-        """Get end period on the annotated day (as TimePeriod object).
-
-        If there is no date annotated, it will use the current date.
-        """
-        day = getattr(self, "_date", timezone.now().date())
-        if day != self.date_end:
-            return TimePeriod.from_period(self.get_period_max(day), day)
-        else:
-            return self.period_to
-
-    @property
-    def period_from_on_day(self) -> int:
-        """Get start period on the annotated day (as period number).
-
-        If there is no date annotated, it will use the current date.
-        """
-        return self.raw_period_from_on_day.period
-
-    @property
-    def period_to_on_day(self) -> int:
-        """Get end period on the annotated day (as period number).
-
-        If there is no date annotated, it will use the current date.
-        """
-        return self.raw_period_to_on_day.period
-
-    def get_start_weekday(self, week: CalendarWeek) -> int:
-        """Get start date of an event in a specific week."""
-        if self.date_start < week[TimePeriod.weekday_min]:
-            return TimePeriod.weekday_min
-        else:
-            return self.date_start.weekday()
-
-    def get_end_weekday(self, week: CalendarWeek) -> int:
-        """Get end date of an event in a specific week."""
-        if self.date_end > week[TimePeriod.weekday_max]:
-            return TimePeriod.weekday_max
-        else:
-            return self.date_end.weekday()
-
-    def annotate_day(self, day: date):
-        """Annotate event with the provided date."""
-        self._date = day
-
-    def get_groups(self) -> models.query.QuerySet:
-        """Get groups relation."""
-        return self.groups
-
-    def get_teachers(self) -> models.query.QuerySet:
-        """Get teachers relation."""
-        return self.teachers
-
-    @property
-    def time_range(self) -> (timezone.datetime, timezone.datetime):
-        """Get the time range of this event."""
-        return timezone.datetime.combine(
-            self.date_start, self.period_from.time_start
-        ), timezone.datetime.combine(self.date_end, self.period_to.time_end)
-
-    class Meta:
-        ordering = ["date_start"]
-        indexes = [
-            models.Index(
-                fields=["date_start", "date_end"],
-                include=["period_from", "period_to"],
-                name="event_date_start_date_end",
-            )
-        ]
-        verbose_name = _("Event")
-        verbose_name_plural = _("Events")
-
-
-class ExtraLesson(
-    GroupPropertiesMixin, TeacherPropertiesMixin, WeekRelatedMixin, SchoolTermRelatedExtensibleModel
-):
-    label_ = "extra_lesson"
-
-    tracker = FieldTracker()
-
-    objects = ExtraLessonManager.from_queryset(ExtraLessonQuerySet)()
-
-    week = models.IntegerField(verbose_name=_("Week"), default=CalendarWeek.current_week)
-    year = models.IntegerField(verbose_name=_("Year"), default=get_current_year)
-    period = models.ForeignKey(
-        "TimePeriod",
-        models.CASCADE,
-        related_name="extra_lessons",
-        verbose_name=_("Time period"),
-    )
-
-    subject = models.ForeignKey(
-        "Subject",
-        on_delete=models.CASCADE,
-        related_name="extra_lessons",
-        verbose_name=_("Subject"),
-    )
-    groups = models.ManyToManyField(
-        "core.Group", related_name="extra_lessons", verbose_name=_("Groups")
-    )
-    teachers = models.ManyToManyField(
-        "core.Person",
-        related_name="extra_lessons_as_teacher",
-        verbose_name=_("Teachers"),
-    )
-    room = models.ForeignKey(
-        "core.Room",
-        models.CASCADE,
-        null=True,
-        related_name="extra_lessons",
-        verbose_name=_("Room"),
-    )
-
-    comment = models.CharField(verbose_name=_("Comment"), blank=True, max_length=255)
-
-    exam = models.ForeignKey(
-        "Exam",
-        on_delete=models.CASCADE,
-        verbose_name=_("Related exam"),
-        related_name="extra_lessons",
-        blank=True,
-        null=True,
-    )
-
-    def __str__(self):
-        return f"{self.week}, {self.period}, {self.subject}"
-
-    def get_groups(self) -> models.query.QuerySet:
-        """Get groups relation."""
-        return self.groups
-
-    def get_teachers(self) -> models.query.QuerySet:
-        """Get teachers relation."""
-        return self.teachers
-
-    def get_subject(self) -> Subject:
-        """Get subject."""
-        return self.subject
-
-    @property
-    def time_range(self) -> (timezone.datetime, timezone.datetime):
-        """Get the time range of this extra lesson."""
-        return timezone.datetime.combine(
-            self.date, self.period.time_start
-        ), timezone.datetime.combine(self.date, self.period.time_end)
-
-    class Meta:
-        verbose_name = _("Extra lesson")
-        verbose_name_plural = _("Extra lessons")
-        indexes = [models.Index(fields=["week", "year"], name="extra_lesson_week_year")]
-
-
 class AutomaticPlan(LiveDocument):
     """Model for configuring automatically updated PDF substitution plans."""
 
@@ -1294,8 +148,7 @@ class ChronosGlobalPermissions(GlobalPermissionModel):
             ("view_all_group_timetables", _("Can view all group timetables")),
             ("view_all_person_timetables", _("Can view all person timetables")),
             ("view_timetable_overview", _("Can view timetable overview")),
-            ("view_lessons_day", _("Can view all lessons per day")),
-            ("view_supervisions_day", _("Can view all supervisions per day")),
+            ("view_substitutions", _("Can view substitutions table")),
         )
 
 
diff --git a/aleksis/apps/chronos/preferences.py b/aleksis/apps/chronos/preferences.py
index b3dc8362091693b313bda2d5f9493d8afa7e7701..a2bd26ef9eac55d0ddca38091619b3d008279803 100644
--- a/aleksis/apps/chronos/preferences.py
+++ b/aleksis/apps/chronos/preferences.py
@@ -16,7 +16,7 @@ from dynamic_preferences.types import (
 )
 
 from aleksis.core.models import GroupType
-from aleksis.core.registries import person_preferences_registry, site_preferences_registry
+from aleksis.core.registries import site_preferences_registry
 
 chronos = Section("chronos", verbose_name=_("Timetables"))
 
@@ -34,27 +34,6 @@ class UseParentGroups(BooleanPreference):
     )
 
 
-@person_preferences_registry.register
-class ShortenGroups(BooleanPreference):
-    section = chronos
-    name = "shorten_groups"
-    default = True
-    verbose_name = _("Shorten groups in timetable views")
-    help_text = _("If there are more groups than the set limit, they will be collapsed.")
-
-
-@site_preferences_registry.register
-class ShortenGroupsLimit(IntegerPreference):
-    section = chronos
-    name = "shorten_groups_limit"
-    default = 4
-    verbose_name = _("Limit of groups for shortening of groups")
-    help_text = _(
-        "If a user activates shortening of groups,"
-        "they will be collapsed if there are more groups than this limit."
-    )
-
-
 @site_preferences_registry.register
 class SubstitutionsRelevantDays(MultipleChoicePreference):
     """Relevant days which have substitution plans."""
@@ -110,44 +89,6 @@ class AffectedGroupsUseParentGroups(BooleanPreference):
     )
 
 
-@site_preferences_registry.register
-class DaysInAdvanceNotifications(IntegerPreference):
-    section = chronos
-    name = "days_in_advance_notifications"
-    default = 1
-    verbose_name = _("How many days in advance users should be notified about timetable changes?")
-
-
-@site_preferences_registry.register
-class TimeForSendingNotifications(TimePreference):
-    section = chronos
-    name = "time_for_sending_notifications"
-    default = time(17, 00)
-    verbose_name = _("Time for sending notifications about timetable changes")
-    required = True
-    help_text = _(
-        "This is only used for scheduling notifications "
-        "which doesn't affect the time period configured above. "
-        "All other notifications affecting the next days are sent immediately."
-    )
-
-
-@site_preferences_registry.register
-class SendNotifications(BooleanPreference):
-    section = chronos
-    name = "send_notifications_site"
-    default = True
-    verbose_name = _("Send notifications for current timetable changes")
-
-
-@person_preferences_registry.register
-class SendNotificationsPerson(BooleanPreference):
-    section = chronos
-    name = "send_notifications"
-    default = True
-    verbose_name = _("Send notifications for current timetable changes")
-
-
 @site_preferences_registry.register
 class GroupTypesTimetables(ModelMultipleChoicePreference):
     section = chronos
diff --git a/aleksis/apps/chronos/rules.py b/aleksis/apps/chronos/rules.py
index 18b7ac4ba8da17bac80a0b52b85b26e832f60533..c7d0065c6bc2c6180a0fa81188484472e6ed8af7 100644
--- a/aleksis/apps/chronos/rules.py
+++ b/aleksis/apps/chronos/rules.py
@@ -6,7 +6,7 @@ from aleksis.core.util.predicates import (
     has_person,
 )
 
-from .util.predicates import has_any_timetable_object, has_room_timetable_perm, has_timetable_perm
+from .util.predicates import has_any_timetable_object, has_timetable_perm
 
 # View timetable overview
 view_timetable_overview_predicate = has_person & (
@@ -14,55 +14,27 @@ view_timetable_overview_predicate = has_person & (
 )
 add_perm("chronos.view_timetable_overview_rule", view_timetable_overview_predicate)
 
-# View my timetable
-add_perm("chronos.view_my_timetable_rule", has_person)
-
 # View timetable
 view_timetable_predicate = has_person & has_timetable_perm
 add_perm("chronos.view_timetable_rule", view_timetable_predicate)
 
-# View all lessons per day
-view_lessons_day_predicate = has_person & has_global_perm("chronos.view_lessons_day")
-add_perm("chronos.view_lessons_day_rule", view_lessons_day_predicate)
 
 # Edit substition
 edit_substitution_predicate = has_person & (
-    has_global_perm("chronos.change_lessonsubstitution")
-    | has_object_perm("chronos.change_lessonsubstitution")
+    has_global_perm("chronos.change_lessonevent") | has_object_perm("chronos.change_lessonevent")
 )
 add_perm("chronos.edit_substitution_rule", edit_substitution_predicate)
 
 # Delete substitution
 delete_substitution_predicate = has_person & (
-    has_global_perm("chronos.delete_lessonsubstitution")
-    | has_object_perm("chronos.delete_lessonsubstitution")
+    has_global_perm("chronos.delete_lessonevent") | has_object_perm("chronos.delete_lessonevent")
 )
 add_perm("chronos.delete_substitution_rule", delete_substitution_predicate)
 
 # View substitutions
-view_substitutions_predicate = has_person & (has_global_perm("chronos.view_lessonsubstitution"))
+view_substitutions_predicate = has_person & (has_global_perm("chronos.view_substitutions"))
 add_perm("chronos.view_substitutions_rule", view_substitutions_predicate)
 
-# View all supervisions per day
-view_supervisions_day_predicate = has_person & has_global_perm("chronos.view_supervisions_day")
-add_perm("chronos.view_supervisions_day_rule", view_supervisions_day_predicate)
-
-# Edit supervision substitution
-edit_supervision_substitution_predicate = has_person & (
-    has_global_perm("chronos.change_supervisionsubstitution")
-)
-add_perm("chronos.edit_supervision_substitution_rule", edit_supervision_substitution_predicate)
-
-# Delete supervision substitution
-delete_supervision_substitution_predicate = has_person & (
-    has_global_perm("chronos.delete_supervisionsubstitution")
-)
-add_perm("chronos.delete_supervision_substitution_rule", delete_supervision_substitution_predicate)
-
-# View room (timetable)
-view_room_predicate = has_person & has_room_timetable_perm
-add_perm("chronos.view_room_rule", view_room_predicate)
-
 # View parent menu entry
 view_menu_predicate = has_person & (view_timetable_overview_predicate)
 add_perm("chronos.view_menu_rule", view_menu_predicate)
diff --git a/aleksis/apps/chronos/static/css/chronos/timetable_print.css b/aleksis/apps/chronos/static/css/chronos/timetable_print.css
deleted file mode 100644
index 343864ae919cb68ab0bb4571d1736fd87aa5a573..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/static/css/chronos/timetable_print.css
+++ /dev/null
@@ -1,34 +0,0 @@
-.timetable-plan .row,
-.timetable-plan .col {
-  display: flex;
-  padding: 0rem;
-}
-
-.timetable-plan .row {
-  margin-bottom: 0rem;
-}
-
-.lesson-card,
-.timetable-title-card {
-  margin: 0;
-  display: flex;
-  flex-grow: 1;
-  min-height: 40px;
-  box-shadow: none;
-  border: 1px solid black;
-  margin-right: -1px;
-  margin-top: -1px;
-  border-radius: 0px;
-  font-size: 11px;
-}
-.lesson-card .card-content > div {
-  padding: 1px;
-}
-
-.card .card-title {
-  font-size: 18px;
-}
-
-.timetable-title-card .card-content {
-  padding: 7px;
-}
diff --git a/aleksis/apps/chronos/static/js/chronos/date_select.js b/aleksis/apps/chronos/static/js/chronos/date_select.js
deleted file mode 100644
index 04a0cf0e6623c772c007ca8f4a85e41038f0c48c..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/static/js/chronos/date_select.js
+++ /dev/null
@@ -1,21 +0,0 @@
-var data = getJSONScript("datepicker_data");
-var activeDate = new Date(data.date);
-
-function updateDatepicker() {
-  $("#date").val(formatDate(activeDate));
-}
-
-function loadNew() {
-  window.location.href = data.dest + formatDateForDjango(activeDate);
-}
-
-function onDateChanged() {
-  activeDate = M.Datepicker.getInstance($("#date")).date;
-  loadNew();
-}
-
-$(document).ready(function () {
-  $("#date").change(onDateChanged);
-
-  updateDatepicker();
-});
diff --git a/aleksis/apps/chronos/static/js/chronos/week_select.js b/aleksis/apps/chronos/static/js/chronos/week_select.js
deleted file mode 100644
index 1854b17d284b4bcb8953a97a3e049d51821d3368..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/static/js/chronos/week_select.js
+++ /dev/null
@@ -1,21 +0,0 @@
-var data = getJSONScript("week_select");
-
-function goToCalendarWeek(cw, year) {
-  window.location.href = data.dest.replace("year", year).replace("cw", cw);
-}
-
-function onCalendarWeekChanged(where) {
-  goToCalendarWeek($(where).val(), data.year);
-}
-
-$(document).ready(function () {
-  $("#calendar-week-1").change(function () {
-    onCalendarWeekChanged("#calendar-week-1");
-  });
-  $("#calendar-week-2").change(function () {
-    onCalendarWeekChanged("#calendar-week-2");
-  });
-  $("#calendar-week-3").change(function () {
-    onCalendarWeekChanged("#calendar-week-3");
-  });
-});
diff --git a/aleksis/apps/chronos/templatetags/__init__.py b/aleksis/apps/chronos/templatetags/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/aleksis/apps/chronos/templatetags/common.py b/aleksis/apps/chronos/templatetags/common.py
deleted file mode 100644
index f3b4ca74d952a6f9fda5034360aa517f2fea475c..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/templatetags/common.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from django import template
-
-register = template.Library()
-
-
-class SetVarNode(template.Node):
-    def __init__(self, var_name, var_value):
-        self.var_name = var_name
-        self.var_value = var_value
-
-    def render(self, context):
-        try:
-            value = template.Variable(self.var_value).resolve(context)
-        except template.VariableDoesNotExist:
-            value = ""
-        context[self.var_name] = value
-
-        return ""
-
-
-@register.tag(name="set")
-def set_var(parser, token):
-    """Set var.
-
-    {% set some_var = '123' %}
-    """
-    parts = token.split_contents()
-    if len(parts) < 4:
-        raise template.TemplateSyntaxError(
-            "'set' tag must be of the form: {% set <var_name> = <var_value> %}"
-        )
-
-    return SetVarNode(parts[1], parts[3])
diff --git a/aleksis/apps/chronos/templatetags/week_helpers.py b/aleksis/apps/chronos/templatetags/week_helpers.py
deleted file mode 100644
index cdba9b3f014913a2525b3ca59bfc514dc0e98ef1..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/templatetags/week_helpers.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from datetime import date, datetime
-from typing import Optional, Union
-
-from django import template
-from django.db.models.query import QuerySet
-
-from aleksis.apps.chronos.util.date import CalendarWeek, week_period_to_date, week_weekday_to_date
-
-register = template.Library()
-
-
-@register.filter
-def week_start(week: CalendarWeek) -> date:
-    return week[0]
-
-
-@register.filter
-def week_end(week: CalendarWeek) -> date:
-    return week[-1]
-
-
-@register.filter
-def only_week(qs: QuerySet, week: Optional[CalendarWeek]) -> QuerySet:
-    wanted_week = week or CalendarWeek()
-    return qs.filter(week=wanted_week.week, year=wanted_week.year)
-
-
-@register.simple_tag
-def weekday_to_date(week: CalendarWeek, weekday: int) -> date:
-    return week_weekday_to_date(week, weekday)
-
-
-@register.simple_tag
-def period_to_date(week: CalendarWeek, period) -> date:
-    return week_period_to_date(week, period)
-
-
-@register.simple_tag
-def period_to_time_start(date_ref: Union[CalendarWeek, int, date], period) -> date:
-    return period.get_datetime_start(date_ref)
-
-
-@register.simple_tag
-def period_to_time_end(date_ref: Union[CalendarWeek, int, date], period) -> date:
-    return period.get_datetime_end(date_ref)
-
-
-@register.simple_tag
-def today() -> date:
-    return date.today()
-
-
-@register.simple_tag
-def now_datetime() -> datetime:
-    return datetime.now()
diff --git a/aleksis/apps/chronos/util/build.py b/aleksis/apps/chronos/util/build.py
index dd24a0452b93b3e34785e71f5ccdd5b58b908baa..66168ce2496758b6525c98fb31afbd5c686a84b5 100644
--- a/aleksis/apps/chronos/util/build.py
+++ b/aleksis/apps/chronos/util/build.py
@@ -1,385 +1,7 @@
-from collections import OrderedDict
 from datetime import date, datetime, time
-from typing import Union
 
-from django.apps import apps
-
-from calendarweek import CalendarWeek
-
-from aleksis.apps.chronos.managers import TimetableType
-from aleksis.apps.chronos.models import SupervisionEvent
-from aleksis.core.models import Group, Person, Room
-
-LessonPeriod = apps.get_model("chronos", "LessonPeriod")
-LessonEvent = apps.get_model("chronos", "LessonEvent")
-TimePeriod = apps.get_model("chronos", "TimePeriod")
-Break = apps.get_model("chronos", "Break")
-Supervision = apps.get_model("chronos", "Supervision")
-LessonSubstitution = apps.get_model("chronos", "LessonSubstitution")
-SupervisionSubstitution = apps.get_model("chronos", "SupervisionSubstitution")
-Event = apps.get_model("chronos", "Event")
-Holiday = apps.get_model("chronos", "Holiday")
-ExtraLesson = apps.get_model("chronos", "ExtraLesson")
-
-
-def build_timetable(
-    type_: Union[TimetableType, str],
-    obj: Union[Group, Room, Person],
-    date_ref: Union[CalendarWeek, date],
-    with_holidays: bool = True,
-):
-    needed_breaks = []
-
-    is_person = False
-    if type_ == "person":
-        is_person = True
-        type_ = obj.timetable_type
-
-    is_week = False
-    if isinstance(date_ref, CalendarWeek):
-        is_week = True
-
-    if type_ is None:
-        return None
-
-    # Get matching holidays
-    if is_week:
-        holidays_per_weekday = Holiday.in_week(date_ref) if with_holidays else {}
-    else:
-        holiday = Holiday.on_day(date_ref) if with_holidays else None
-
-    # Get matching lesson periods
-    lesson_periods = LessonPeriod.objects
-    lesson_periods = (
-        lesson_periods.select_related(None)
-        .select_related("lesson", "lesson__subject", "period", "room")
-        .only(
-            "lesson",
-            "period",
-            "room",
-            "lesson__subject",
-            "period__weekday",
-            "period__period",
-            "lesson__subject__short_name",
-            "lesson__subject__name",
-            "lesson__subject__colour_fg",
-            "lesson__subject__colour_bg",
-            "room__short_name",
-            "room__name",
-        )
-    )
-
-    if is_week:
-        lesson_periods = lesson_periods.in_week(date_ref)
-    else:
-        lesson_periods = lesson_periods.on_day(date_ref)
-
-    if is_person:
-        lesson_periods = lesson_periods.filter_from_person(obj)
-    else:
-        lesson_periods = lesson_periods.filter_from_type(type_, obj, is_smart=with_holidays)
-
-    # Sort lesson periods in a dict
-    lesson_periods_per_period = lesson_periods.group_by_periods(is_week=is_week)
-
-    # Get events
-    extra_lessons = ExtraLesson.objects
-    if is_week:
-        extra_lessons = extra_lessons.filter(week=date_ref.week, year=date_ref.year)
-    else:
-        extra_lessons = extra_lessons.on_day(date_ref)
-    if is_person:
-        extra_lessons = extra_lessons.filter_from_person(obj)
-    else:
-        extra_lessons = extra_lessons.filter_from_type(type_, obj)
-
-    extra_lessons = extra_lessons.only(
-        "week",
-        "year",
-        "period",
-        "subject",
-        "room",
-        "comment",
-        "period__weekday",
-        "period__period",
-        "subject__short_name",
-        "subject__name",
-        "subject__colour_fg",
-        "subject__colour_bg",
-        "room__short_name",
-        "room__name",
-    )
-
-    # Sort lesson periods in a dict
-    extra_lessons_per_period = extra_lessons.group_by_periods(is_week=is_week)
-
-    # Get events
-    events = Event.objects
-    events = events.in_week(date_ref) if is_week else events.on_day(date_ref)
-
-    events = events.only(
-        "id",
-        "title",
-        "date_start",
-        "date_end",
-        "period_from",
-        "period_to",
-        "period_from__weekday",
-        "period_from__period",
-        "period_to__weekday",
-        "period_to__period",
-    )
-
-    if is_person:
-        events_to_display = events.filter_from_person(obj)
-    else:
-        events_to_display = events.filter_from_type(type_, obj)
-
-    # Sort events in a dict
-    events_per_period = {}
-    events_for_replacement_per_period = {}
-    for event in events:
-        if is_week and event.date_start < date_ref[TimePeriod.weekday_min]:
-            # If start date not in current week, set weekday and period to min
-            weekday_from = TimePeriod.weekday_min
-            period_from_first_weekday = TimePeriod.period_min
-        else:
-            weekday_from = event.date_start.weekday()
-            period_from_first_weekday = event.period_from.period
-
-        if is_week and event.date_end > date_ref[TimePeriod.weekday_max]:
-            # If end date not in current week, set weekday and period to max
-            weekday_to = TimePeriod.weekday_max
-            period_to_last_weekday = TimePeriod.period_max
-        else:
-            weekday_to = event.date_end.weekday()
-            period_to_last_weekday = event.period_to.period
-
-        for weekday in range(weekday_from, weekday_to + 1):
-            if not is_week and weekday != date_ref.weekday():
-                # If daily timetable for person, skip other weekdays
-                continue
-
-            # If start day, use start period else use min period
-            period_from = (
-                period_from_first_weekday if weekday == weekday_from else TimePeriod.period_min
-            )
-
-            # If end day, use end period else use max period
-            period_to = period_to_last_weekday if weekday == weekday_to else TimePeriod.periox_max
-
-            for period in range(period_from, period_to + 1):
-                # The following events are possibly replacing some lesson periods
-                if period not in events_for_replacement_per_period:
-                    events_for_replacement_per_period[period] = {} if is_week else []
-
-                if is_week and weekday not in events_for_replacement_per_period[period]:
-                    events_for_replacement_per_period[period][weekday] = []
-
-                if not is_week:
-                    events_for_replacement_per_period[period].append(event)
-                else:
-                    events_for_replacement_per_period[period][weekday].append(event)
-
-                # and the following will be displayed in the timetable
-                if event in events_to_display:
-                    if period not in events_per_period:
-                        events_per_period[period] = {} if is_week else []
-
-                    if is_week and weekday not in events_per_period[period]:
-                        events_per_period[period][weekday] = []
-
-                    if not is_week:
-                        events_per_period[period].append(event)
-                    else:
-                        events_per_period[period][weekday].append(event)
-
-    if type_ == TimetableType.TEACHER:
-        # Get matching supervisions
-        week = CalendarWeek.from_date(date_ref) if not is_week else date_ref
-        supervisions = (
-            Supervision.objects.in_week(week)
-            .all()
-            .annotate_week(week)
-            .filter_by_teacher(obj)
-            .only(
-                "area",
-                "break_item",
-                "teacher",
-                "area",
-                "area__short_name",
-                "area__name",
-                "area__colour_fg",
-                "area__colour_bg",
-                "break_item__short_name",
-                "break_item__name",
-                "break_item__after_period__period",
-                "break_item__after_period__weekday",
-                "break_item__before_period__period",
-                "break_item__before_period__weekday",
-                "teacher__short_name",
-                "teacher__first_name",
-                "teacher__last_name",
-            )
-        )
-
-        if not is_week:
-            supervisions = supervisions.filter_by_weekday(date_ref.weekday())
-
-        supervisions_per_period_after = {}
-        for supervision in supervisions:
-            weekday = supervision.break_item.weekday
-            period_after_break = supervision.break_item.before_period_number
-
-            if period_after_break not in needed_breaks:
-                needed_breaks.append(period_after_break)
-
-            if is_week and period_after_break not in supervisions_per_period_after:
-                supervisions_per_period_after[period_after_break] = {}
-
-            if not is_week:
-                supervisions_per_period_after[period_after_break] = supervision
-            else:
-                supervisions_per_period_after[period_after_break][weekday] = supervision
-
-    # Get ordered breaks
-    breaks = OrderedDict(sorted(Break.get_breaks_dict().items()))
-
-    rows = []
-    for period, break_ in breaks.items():  # period is period after break
-        # Break
-        if type_ == TimetableType.TEACHER and period in needed_breaks:
-            row = {
-                "type": "break",
-                "after_period": break_.after_period_number,
-                "before_period": break_.before_period_number,
-                "time_start": break_.time_start,
-                "time_end": break_.time_end,
-            }
-
-            if is_week:
-                cols = []
-
-                for weekday in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
-                    col = None
-                    if (
-                        period in supervisions_per_period_after
-                        and weekday not in holidays_per_weekday
-                    ) and weekday in supervisions_per_period_after[period]:
-                        col = supervisions_per_period_after[period][weekday]
-                    cols.append(col)
-
-                row["cols"] = cols
-            else:
-                col = None
-                if period in supervisions_per_period_after and not holiday:
-                    col = supervisions_per_period_after[period]
-                row["col"] = col
-            rows.append(row)
-
-        # Period
-        if period <= TimePeriod.period_max:
-            row = {
-                "type": "period",
-                "period": period,
-                "time_start": break_.before_period.time_start,
-                "time_end": break_.before_period.time_end,
-            }
-
-            if is_week:
-                cols = []
-                for weekday in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
-                    # Skip this period if there are holidays
-                    if weekday in holidays_per_weekday:
-                        cols.append([])
-                        continue
-
-                    col = []
-
-                    events_for_this_period = (
-                        events_per_period[period].get(weekday, [])
-                        if period in events_per_period
-                        else []
-                    )
-                    events_for_replacement_for_this_period = (
-                        events_for_replacement_per_period[period].get(weekday, [])
-                        if period in events_for_replacement_per_period
-                        else []
-                    )
-                    lesson_periods_for_this_period = (
-                        lesson_periods_per_period[period].get(weekday, [])
-                        if period in lesson_periods_per_period
-                        else []
-                    )
-
-                    # Add lesson periods
-                    if lesson_periods_for_this_period:
-                        if events_for_replacement_for_this_period:
-                            # If there is a event in this period,
-                            # we have to check whether the actual lesson is taking place.
-
-                            for lesson_period in lesson_periods_for_this_period:
-                                replaced_by_event = lesson_period.is_replaced_by_event(
-                                    events_for_replacement_for_this_period,
-                                    [obj] if type_ == TimetableType.GROUP else None,
-                                )
-                                lesson_period.replaced_by_event = replaced_by_event
-                                if not replaced_by_event or (
-                                    replaced_by_event and type_ != TimetableType.GROUP
-                                ):
-                                    col.append(lesson_period)
-
-                        else:
-                            col += lesson_periods_for_this_period
-
-                    # Add extra lessons
-                    if period in extra_lessons_per_period:
-                        col += extra_lessons_per_period[period].get(weekday, [])
-
-                    # Add events
-                    col += events_for_this_period
-
-                    cols.append(col)
-
-                row["cols"] = cols
-            else:
-                col = []
-
-                # Skip this period if there are holidays
-                if holiday:
-                    continue
-
-                events_for_this_period = events_per_period.get(period, [])
-                events_for_replacement_for_this_period = events_for_replacement_per_period.get(
-                    period, []
-                )
-                lesson_periods_for_this_period = lesson_periods_per_period.get(period, [])
-
-                # Add lesson periods
-                if lesson_periods_for_this_period:
-                    if events_for_replacement_for_this_period:
-                        # If there is a event in this period,
-                        # we have to check whether the actual lesson is taking place.
-
-                        lesson_periods_to_keep = []
-                        for lesson_period in lesson_periods_for_this_period:
-                            if not lesson_period.is_replaced_by_event(
-                                events_for_replacement_for_this_period
-                            ):
-                                lesson_periods_to_keep.append(lesson_period)
-                        col += lesson_periods_to_keep
-                    else:
-                        col += lesson_periods_for_this_period
-
-                # Add events and extra lessons
-                col += extra_lessons_per_period.get(period, [])
-                col += events_for_this_period
-
-                row["col"] = col
-
-            rows.append(row)
-
-    return rows
+from aleksis.apps.chronos.models import LessonEvent, SupervisionEvent
+from aleksis.core.models import Group, Person
 
 
 def build_substitutions_list(wanted_day: date) -> tuple[list[dict], set[Person], set[Group]]:
@@ -433,23 +55,3 @@ def build_substitutions_list(wanted_day: date) -> tuple[list[dict], set[Person],
     rows.sort(key=lambda row: row["sort_a"] + row["sort_b"])
 
     return rows, affected_teachers, affected_groups
-
-
-def build_weekdays(
-    base: list[tuple[int, str]], wanted_week: CalendarWeek, with_holidays: bool = True
-) -> list[dict]:
-    if with_holidays:
-        holidays_per_weekday = Holiday.in_week(wanted_week)
-
-    weekdays = []
-    for key, name in base[TimePeriod.weekday_min : TimePeriod.weekday_max + 1]:
-        weekday = {
-            "key": key,
-            "name": name,
-            "date": wanted_week[key],
-        }
-        if with_holidays:
-            weekday["holiday"] = holidays_per_weekday[key] if key in holidays_per_weekday else None
-        weekdays.append(weekday)
-
-    return weekdays
diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py
index 8e70ceae4b383dfcd72eb3aad80afd12d2f08175..1dc23b62eabcf3f499ab6b5d03d4030b223dc7a8 100644
--- a/aleksis/apps/chronos/util/chronos_helpers.py
+++ b/aleksis/apps/chronos/util/chronos_helpers.py
@@ -2,8 +2,6 @@ from datetime import date, datetime, timedelta
 from typing import TYPE_CHECKING, Optional
 
 from django.db.models import Count, Q
-from django.http import HttpRequest, HttpResponseNotFound
-from django.shortcuts import get_object_or_404
 
 from guardian.core import ObjectPermissionChecker
 
@@ -11,13 +9,6 @@ from aleksis.core.models import Announcement, Group, Person, Room
 from aleksis.core.util.core_helpers import get_site_preferences
 from aleksis.core.util.predicates import check_global_permission
 
-from ..managers import TimetableType
-from ..models import (
-    LessonPeriod,
-    LessonSubstitution,
-    Supervision,
-    SupervisionSubstitution,
-)
 from .build import build_substitutions_list
 
 if TYPE_CHECKING:
@@ -26,42 +17,6 @@ if TYPE_CHECKING:
     User = get_user_model()  # noqa
 
 
-def get_el_by_pk(
-    request: HttpRequest,
-    type_: str,
-    pk: int,
-    prefetch: bool = False,
-    *args,
-    **kwargs,
-):
-    if type_ == TimetableType.GROUP.value:
-        return get_object_or_404(
-            Group.objects.prefetch_related("owners", "parent_groups") if prefetch else Group,
-            pk=pk,
-        )
-    elif type_ == TimetableType.TEACHER.value:
-        return get_object_or_404(Person, pk=pk)
-    elif type_ == TimetableType.ROOM.value:
-        return get_object_or_404(Room, pk=pk)
-    else:
-        return HttpResponseNotFound()
-
-
-def get_substitution_by_id(request: HttpRequest, id_: int, week: int):
-    lesson_period = get_object_or_404(LessonPeriod, pk=id_)
-    wanted_week = lesson_period.lesson.get_calendar_week(week)
-
-    return LessonSubstitution.objects.filter(
-        week=wanted_week.week, year=wanted_week.year, lesson_period=lesson_period
-    ).first()
-
-
-def get_supervision_substitution_by_id(request: HttpRequest, id_: int, date: datetime.date):
-    supervision = get_object_or_404(Supervision, pk=id_)
-
-    return SupervisionSubstitution.objects.filter(date=date, supervision=supervision).first()
-
-
 def get_teachers(user: "User"):
     """Get the teachers whose timetables are allowed to be seen by current user."""
     checker = ObjectPermissionChecker(user)
diff --git a/aleksis/apps/chronos/util/date.py b/aleksis/apps/chronos/util/date.py
deleted file mode 100644
index 9a2eb10592509b12f4e915a5e115b96d95ce939a..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/util/date.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from datetime import date
-
-from django.utils import timezone
-
-from calendarweek import CalendarWeek
-
-
-def week_weekday_from_date(when: date) -> tuple[CalendarWeek, int]:
-    """Return a tuple of week and weekday from a given date."""
-    return (CalendarWeek.from_date(when), when.weekday())
-
-
-def week_weekday_to_date(week: CalendarWeek, weekday: int) -> date:
-    """Return a date object for one day in a calendar week."""
-    return week[weekday]
-
-
-def week_period_to_date(week: CalendarWeek, period) -> date:
-    """Return the date of a lesson period in a given week."""
-    return period.get_date(week)
-
-
-def get_weeks_for_year(year: int) -> list[CalendarWeek]:
-    """Generate all weeks for one year."""
-    weeks = []
-
-    # Go for all weeks in year and create week list
-    current_week = CalendarWeek(year=year, week=1)
-
-    while current_week.year == year:
-        weeks.append(current_week)
-        current_week += 1
-
-    return weeks
-
-
-def get_current_year() -> int:
-    """Get current year."""
-    return timezone.now().year
diff --git a/aleksis/apps/chronos/util/format.py b/aleksis/apps/chronos/util/format.py
deleted file mode 100644
index 0dd640f3c6e892741eb81d8de61421681160c9c9..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/util/format.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from datetime import date
-from typing import TYPE_CHECKING
-
-from django.utils.formats import date_format
-
-if TYPE_CHECKING:
-    from ..models import TimePeriod
-
-
-def format_m2m(f, attr: str = "short_name") -> str:
-    """Join a attribute of all elements of a ManyToManyField."""
-    return ", ".join([getattr(x, attr) for x in f.all()])
-
-
-def format_date_period(day: date, period: "TimePeriod") -> str:
-    """Format date and time period."""
-    return f"{date_format(day)}, {period.period}."
diff --git a/aleksis/apps/chronos/util/js.py b/aleksis/apps/chronos/util/js.py
deleted file mode 100644
index 96612411ac9d4c25772c258f688706ab583b22cb..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/util/js.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from datetime import date, datetime, time
-
-
-def date_unix(value: date) -> int:
-    """Convert a date object to an UNIX timestamp."""
-    value = datetime.combine(value, time(hour=0, minute=0))
-    return int(value.timestamp()) * 1000
diff --git a/aleksis/apps/chronos/util/notifications.py b/aleksis/apps/chronos/util/notifications.py
deleted file mode 100644
index 847dc599ab47699ec4867d22822f81c8f1ec00ff..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/util/notifications.py
+++ /dev/null
@@ -1,210 +0,0 @@
-from datetime import datetime, timedelta
-from typing import Union
-from urllib.parse import urljoin
-
-from django.conf import settings
-from django.urls import reverse
-from django.utils import timezone
-from django.utils.formats import date_format
-from django.utils.translation import gettext_lazy as _
-from django.utils.translation import ngettext
-
-import zoneinfo
-
-from aleksis.core.models import Notification, Person
-from aleksis.core.util.core_helpers import get_site_preferences
-
-from ..models import Event, ExtraLesson, LessonSubstitution, SupervisionSubstitution
-
-
-def send_notifications_for_object(
-    instance: Union[ExtraLesson, LessonSubstitution, Event, SupervisionSubstitution],
-):
-    """Send notifications for a change object."""
-    recipients = []
-    if isinstance(instance, LessonSubstitution):
-        recipients += instance.lesson_period.lesson.teachers.all()
-        recipients += instance.teachers.all()
-        recipients += Person.objects.filter(
-            member_of__in=instance.lesson_period.lesson.groups.all()
-        )
-    elif isinstance(instance, (Event, ExtraLesson)):
-        recipients += instance.teachers.all()
-        recipients += Person.objects.filter(member_of__in=instance.groups.all())
-    elif isinstance(instance, SupervisionSubstitution):
-        recipients.append(instance.teacher)
-        recipients.append(instance.supervision.teacher)
-
-    description = ""
-    if isinstance(instance, LessonSubstitution):
-        # Date, lesson, subject
-        subject = instance.lesson_period.lesson.subject
-        day = instance.date
-        period = instance.lesson_period.period
-
-        if instance.cancelled:
-            description += (
-                _(
-                    "The {subject} lesson in the {period}. period on {day} has been cancelled."
-                ).format(subject=subject.name, period=period.period, day=date_format(day))
-                + " "
-            )
-        else:
-            description += (
-                _(
-                    "The {subject} lesson in the {period}. period "
-                    "on {day} has some current changes."
-                ).format(subject=subject.name, period=period.period, day=date_format(day))
-                + " "
-            )
-
-            if instance.teachers.all():
-                description += (
-                    ngettext(
-                        "The teacher {old} is substituted by {new}.",
-                        "The teachers {old} are substituted by {new}.",
-                        instance.teachers.count(),
-                    ).format(
-                        old=instance.lesson_period.lesson.teacher_names,
-                        new=instance.teacher_names,
-                    )
-                    + " "
-                )
-
-            if instance.subject:
-                description += (
-                    _("The subject is changed to {subject}.").format(subject=instance.subject.name)
-                    + " "
-                )
-
-            if instance.room:
-                description += (
-                    _("The lesson is moved from {old} to {new}.").format(
-                        old=instance.lesson_period.room.name,
-                        new=instance.room.name,
-                    )
-                    + " "
-                )
-
-        if instance.comment:
-            description += (
-                _("There is an additional comment: {comment}.").format(comment=instance.comment)
-                + " "
-            )
-
-    elif isinstance(instance, Event):
-        if instance.date_start != instance.date_end:
-            description += (
-                _(
-                    "There is an event that starts on {date_start}, {period_from}. period "
-                    "and ends on {date_end}, {period_to}. period:"
-                ).format(
-                    date_start=date_format(instance.date_start),
-                    date_end=date_format(instance.date_end),
-                    period_from=instance.period_from.period,
-                    period_to=instance.period_to.period,
-                )
-                + "\n"
-            )
-        else:
-            description += (
-                _(
-                    "There is an event on {date} from the "
-                    "{period_from}. period to the {period_to}. period:"
-                ).format(
-                    date=date_format(instance.date_start),
-                    period_from=instance.period_from.period,
-                    period_to=instance.period_to.period,
-                )
-                + "\n"
-            )
-
-        if instance.groups.all():
-            description += _("Groups: {groups}").format(groups=instance.group_names) + "\n"
-        if instance.teachers.all():
-            description += _("Teachers: {teachers}").format(teachers=instance.teacher_names) + "\n"
-        if instance.rooms.all():
-            description += (
-                _("Rooms: {rooms}").format(
-                    rooms=", ".join([room.name for room in instance.rooms.all()])
-                )
-                + "\n"
-            )
-    elif isinstance(instance, ExtraLesson):
-        description += (
-            _("There is an extra lesson on {date} in the {period}. period:").format(
-                date=date_format(instance.date),
-                period=instance.period.period,
-            )
-            + "\n"
-        )
-
-        if instance.groups.all():
-            description += _("Groups: {groups}").format(groups=instance.group_names) + "\n"
-        if instance.room:
-            description += _("Subject: {subject}").format(subject=instance.subject.name) + "\n"
-        if instance.teachers.all():
-            description += _("Teachers: {teachers}").format(teachers=instance.teacher_names) + "\n"
-        if instance.room:
-            description += _("Room: {room}").format(room=instance.room.name) + "\n"
-        if instance.comment:
-            description += _("Comment: {comment}.").format(comment=instance.comment) + "\n"
-    elif isinstance(instance, SupervisionSubstitution):
-        description += _(
-            "The supervision of {old} on {date} between the {period_from}. period "
-            "and the {period_to}. period in the area {area} is substituted by {new}."
-        ).format(
-            old=instance.supervision.teacher.full_name,
-            date=date_format(instance.date),
-            period_from=instance.supervision.break_item.after_period_number,
-            period_to=instance.supervision.break_item.before_period_number,
-            area=instance.supervision.area.name,
-            new=instance.teacher.full_name,
-        )
-
-    day = instance.date if hasattr(instance, "date") else instance.date_start
-
-    url = urljoin(
-        settings.BASE_URL,
-        reverse(
-            "my_timetable_by_date",
-            args=[day.year, day.month, day.day],
-        ),
-    )
-
-    dt_start, dt_end = instance.time_range
-    dt_start = dt_start.replace(tzinfo=zoneinfo.ZoneInfo(settings.TIME_ZONE))
-    dt_end = dt_end.replace(tzinfo=zoneinfo.ZoneInfo(settings.TIME_ZONE))
-
-    send_time = get_site_preferences()["chronos__time_for_sending_notifications"]
-    number_of_days = get_site_preferences()["chronos__days_in_advance_notifications"]
-
-    start_range = timezone.now().replace(hour=send_time.hour, minute=send_time.minute)
-    if timezone.now().time() > send_time:
-        start_range = start_range - timedelta(days=1)
-    end_range = start_range + timedelta(days=number_of_days)
-
-    if dt_start < start_range and dt_end < end_range:
-        # Skip this, because the change is in the past
-        return
-
-    if dt_start <= end_range and dt_end >= start_range:
-        # Send immediately
-        send_at = timezone.now()
-    else:
-        # Schedule for later
-        send_at = datetime.combine(
-            dt_start.date() - timedelta(days=number_of_days), send_time
-        ).replace(tzinfo=zoneinfo.ZoneInfo(settings.TIME_ZONE))
-
-    for recipient in recipients:
-        if recipient.preferences["chronos__send_notifications"]:
-            n = Notification(
-                recipient=recipient,
-                sender=_("Timetable"),
-                title=_("There are current changes to your timetable."),
-                description=description,
-                link=url,
-                send_at=send_at,
-            )
-            n.save()