diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 643ff17db9651a866c10dedddd59f1feb9d6f84f..5389b8747a7f76495756ad47fda11d9e410319cf 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,12 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Added
+~~~~~
+
+* New timetable interface based on calendar system.
+* [Dev] LessonEvent and SupervisionEvent basing on calendar system.
+
 `3.0.2`_ - 2023-09-10
 ---------------------
 
diff --git a/aleksis/apps/chronos/frontend/components/SelectTimetable.vue b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue
index 4f880bdc2bf7952017f7c92cd149e1cd5ad23c27..6e1931e71f89a0fc608ce6714866c5174592947d 100644
--- a/aleksis/apps/chronos/frontend/components/SelectTimetable.vue
+++ b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue
@@ -5,7 +5,7 @@ export default {
   name: "SelectTimetable",
   props: {
     value: {
-      type: String,
+      type: Object,
       required: false,
       default: null,
     },
diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py
index fae25d1817462aa3f0fd0babf619bf480cb65320..b61587a8867745914f158c3d419e7c48c148d908 100644
--- a/aleksis/apps/chronos/managers.py
+++ b/aleksis/apps/chronos/managers.py
@@ -816,7 +816,7 @@ class GroupPropertiesMixin:
         return sep.join([group.short_name for group in self.get_groups()])
 
     @property
-    def groups_to_show(self) -> models.QuerySet:
+    def groups_to_show(self) -> QuerySet[Group]:
         groups = self.get_groups()
         if (
             groups.count() == 1
@@ -869,13 +869,15 @@ class RoomPropertiesMixin:
 class LessonEventQuerySet(PolymorphicQuerySet):
     """Queryset with special query methods for lesson events."""
 
-    def for_teacher(self, teacher: Union[int, Person]):
+    def for_teacher(self, teacher: Union[int, Person]) -> "LessonEventQuerySet":
+        """Get all lesson events for a certain person as teacher (including amends)."""
         amended = self.filter(Q(amended_by__isnull=False) & (Q(teachers=teacher))).values_list(
             "amended_by__pk", flat=True
         )
         return self.filter(Q(teachers=teacher) | Q(pk__in=amended)).distinct()
 
-    def for_group(self, group: Union[int, Group]):
+    def for_group(self, group: Union[int, Group]) -> "LessonEventQuerySet":
+        """Get all lesson events for a certain group (including amends/as parent group)."""
         amended = self.filter(
             Q(amended_by__isnull=False) & (Q(groups=group) | Q(groups__parent_groups=group))
         ).values_list("amended_by__pk", flat=True)
@@ -883,19 +885,22 @@ class LessonEventQuerySet(PolymorphicQuerySet):
             Q(groups=group) | Q(groups__parent_groups=group) | Q(pk__in=amended)
         ).distinct()
 
-    def for_room(self, room: Union[int, Room]):
+    def for_room(self, room: Union[int, Room]) -> "LessonEventQuerySet":
+        """Get all lesson events for a certain room (including amends)."""
         amended = self.filter(Q(amended_by__isnull=False) & (Q(rooms=room))).values_list(
             "amended_by__pk", flat=True
         )
         return self.filter(Q(rooms=room) | Q(pk__in=amended)).distinct()
 
-    def for_course(self, course: Union[int, Course]):
+    def for_course(self, course: Union[int, Course]) -> "LessonEventQuerySet":
+        """Get all lesson events for a certain course (including amends)."""
         amended = self.filter(Q(amended_by__isnull=False) & (Q(course=course))).values_list(
             "amended_by__pk", flat=True
         )
         return self.filter(Q(course=course) | Q(pk__in=amended)).distinct()
 
-    def for_person(self, person: Union[int, Person]):
+    def for_person(self, person: Union[int, Person]) -> "LessonEventQuerySet":
+        """Get all lesson events for a certain person (as teacher/participant, including amends)."""
         amended = self.filter(
             Q(amended_by__isnull=False) & (Q(teachers=person) | Q(groups__members=person))
         ).values_list("amended_by__pk", flat=True)
@@ -903,17 +908,34 @@ class LessonEventQuerySet(PolymorphicQuerySet):
             Q(teachers=person) | Q(groups__members=person) | Q(pk__in=amended)
         ).distinct()
 
-    def related_to_person(self, person: Union[int, Person]):
+    def related_to_person(self, person: Union[int, Person]) -> "LessonEventQuerySet":
+        """Get all lesson events a certain person is allowed to see.
+
+        This includes all lesson events the person is assigned to as
+        teacher/participant/group owner/parent group owner,
+        including those amended.
+        """
         amended = self.filter(
             Q(amended_by__isnull=False)
-            & (Q(teachers=person) | Q(groups__members=person) | Q(groups__owners=person))
+            & (
+                Q(teachers=person)
+                | Q(groups__members=person)
+                | Q(groups__owners=person)
+                | Q(groups__parent_groups__owners=person)
+            )
         ).values_list("amended_by__pk", flat=True)
         return self.filter(
-            Q(teachers=person) | Q(groups__members=person) | Q(groups__owners=person)
+            Q(teachers=person)
+            | Q(groups__members=person)
+            | Q(groups__owners=person)
+            | Q(groups__parent_groups__owners=person)
+            | Q(pk__in=amended)
         ).distinct()
 
-    def not_amended(self):
+    def not_amended(self) -> "LessonEventQuerySet":
+        """Get all lesson events that are not amended."""
         return self.filter(amended_by__isnull=True)
 
-    def not_amending(self):
+    def not_amending(self) -> "LessonEventQuerySet":
+        """Get all lesson events that are not amending other events."""
         return self.filter(amends__isnull=True)
diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 60ffa45c6bfca6addfd0e32ecec2b6ace7a1e1ce..12df770fc553cc7bb969a5de1467a66d96dc2c70 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -11,10 +11,11 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import PermissionDenied, ValidationError
 from django.core.validators import MinValueValidator
 from django.db import models
-from django.db.models import Max, Min, Q
+from django.db.models import Max, Min, Q, QuerySet
 from django.db.models.functions import Coalesce
 from django.dispatch import receiver
 from django.forms import Media
+from django.http import HttpRequest
 from django.template.loader import render_to_string
 from django.urls import reverse
 from django.utils import timezone
@@ -1324,6 +1325,8 @@ class ChronosGlobalPermissions(GlobalPermissionModel):
 
 
 class LessonEvent(CalendarEvent):
+    """Calendar feed for lessons."""
+
     name = "lesson"
     verbose_name = _("Lessons")
 
@@ -1374,36 +1377,43 @@ class LessonEvent(CalendarEvent):
     )
 
     @property
-    def actual_groups(self: LessonEvent):
-        return self.groups.all() if self.amends else self.real_amends.groups.all()
+    def actual_groups(self: LessonEvent) -> QuerySet[Group]:
+        """Get list of the groups of this lesson event."""
+        return self.amends.groups.all() if self.amends else self.groups.all()
 
     @property
     def all_members(self: LessonEvent) -> list[Person]:
+        """Get list of all group members for this lesson event."""
         return list(itertools.chain(*[list(g.members.all()) for g in self.actual_groups]))
 
     @property
     def all_teachers(self: LessonEvent) -> list[Person]:
+        """Get list of all teachers for this lesson event."""
         all_teachers = list(self.teachers.all())
         if self.amends:
-            all_teachers += list(self.real_amends.teachers.all())
+            all_teachers += list(self.amends.teachers.all())
         return all_teachers
 
     @property
     def group_names(self: LessonEvent) -> str:
+        """Get comma-separated string with all group names."""
         return ", ".join([g.name for g in self.actual_groups])
 
     @property
     def teacher_names(self: LessonEvent) -> str:
+        """Get comma-separated string with all teacher names."""
         return ", ".join([t.full_name for t in self.teachers.all()])
 
     @property
     def room_names(self: LessonEvent) -> str:
+        """Get comma-separated string with all room names."""
         return ", ".join([r.name for r in self.rooms.all()])
 
     @property
     def room_names_with_amends(self: LessonEvent) -> str:
+        """Get comma-separated string with all room names (including amends)."""
         my_room_names = self.room_names
-        amended_room_names = self.real_amends.room_names if self.amends else ""
+        amended_room_names = self.amends.room_names if self.amends else ""
 
         if my_room_names and amended_room_names:
             return _("{} (instead of {})").format(my_room_names, amended_room_names)
@@ -1413,8 +1423,9 @@ class LessonEvent(CalendarEvent):
 
     @property
     def teacher_names_with_amends(self: LessonEvent) -> str:
+        """Get comma-separated string with all teacher names (including amends)."""
         my_teacher_names = self.teacher_names
-        amended_teacher_names = self.real_amends.teacher_names if self.amends else ""
+        amended_teacher_names = self.amends.teacher_names if self.amends else ""
 
         if my_teacher_names and amended_teacher_names:
             return _("{} (instead of {})").format(my_teacher_names, amended_teacher_names)
@@ -1424,8 +1435,9 @@ class LessonEvent(CalendarEvent):
 
     @property
     def subject_name_with_amends(self: LessonEvent) -> str:
+        """Get formatted subject name (including amends)."""
         my_subject = self.subject.name if self.subject else ""
-        amended_subject = self.real_amends.subject.name if self.amends else ""
+        amended_subject = self.amends.subject.name if self.amends and self.amends.subject else ""
 
         if my_subject and amended_subject:
             return _("{} (instead of {})").format(my_subject, amended_subject)
@@ -1435,26 +1447,21 @@ class LessonEvent(CalendarEvent):
             return my_subject
         return _("Lesson")
 
-    @property
-    def real_amends(self: LessonEvent) -> LessonEvent:
-        # FIXME THIS IS AWFUL SLOW
-        if self.amends:
-            return LessonEvent.objects.get(pk=self.amends.pk)
-        return self
-
     @classmethod
-    def value_title(cls, reference_object: LessonEvent, request) -> str:
-        """Get the title of the event."""
+    def value_title(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str:
+        """Get the title of the lesson event."""
         if reference_object.title:
             return reference_object.title
         elif reference_object.subject or (
-            reference_object.amends and reference_object.real_amends.subject
+            reference_object.amends and reference_object.amends.subject
         ):
             title = reference_object.subject_name_with_amends
-            if request.user.person in reference_object.teachers.all():
+            if request and request.user.person in reference_object.teachers.all():
                 title += " · " + reference_object.group_names
-            else:
+            elif request:
                 title += " · " + reference_object.teacher_names_with_amends
+            else:
+                title += f" · {reference_object.group_names} · {reference_object.teacher_names_with_amends}"
             if reference_object.rooms.all().exists():
                 title += " · " + reference_object.room_names_with_amends
             return title
@@ -1462,47 +1469,59 @@ class LessonEvent(CalendarEvent):
         return _("Lesson")
 
     @classmethod
-    def value_description(cls, reference_object: LessonEvent, request) -> str:
+    def value_description(
+        cls, reference_object: LessonEvent, request: HttpRequest | None = None
+    ) -> str:
+        """Get the description of the lesson event."""
         return render_to_string("chronos/lesson_event_description.txt", {"event": reference_object})
 
     @classmethod
-    def value_color(cls, reference_object: LessonEvent, request) -> str:
-        """Get the color of the event."""
+    def value_color(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str:
+        """Get the color of the lesson event."""
         if reference_object.cancelled:
             return "#eeeeee"
         if reference_object.subject:
             return reference_object.subject.colour_bg
-        if reference_object.amends and reference_object.real_amends.subject:
-            return reference_object.real_amends.subject.colour_bg
+        if reference_object.amends and reference_object.amends.subject:
+            return reference_object.amends.subject.colour_bg
         return super().value_color(reference_object, request)
 
     @classmethod
-    def value_attendee(cls, reference_object: LessonEvent, request) -> str:
-        """Get the attendees of the event."""
+    def value_attendee(
+        cls, reference_object: LessonEvent, request: HttpRequest | None = None
+    ) -> str:
+        """Get the attendees of the lesson event."""
+        # Only teachers due to privacy concerns
         attendees = [t.get_vcal_address(role="CHAIR") for t in reference_object.teachers.all()]
         return [a for a in attendees if a]
 
     @classmethod
-    def value_location(cls, reference_object: LessonEvent, request) -> str:
-        """Get the location of the event."""
-        return ", ".join([r.name for r in reference_object.rooms.all()])
+    def value_location(
+        cls, reference_object: LessonEvent, request: HttpRequest | None = None
+    ) -> str:
+        """Get the location of the lesson event."""
+        return reference_object.room_names_with_amends
 
     @classmethod
-    def value_status(cls, reference_object: LessonEvent, request) -> str:
-        """Get the status of the event."""
+    def value_status(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str:
+        """Get the status of the lesson event."""
         if reference_object.cancelled:
             return "CANCELLED"
         return "CONFIRMED"
 
     @classmethod
-    def value_meta(cls, reference_object: LessonEvent, request) -> str:
-        """Get the meta of the event."""
-        real_amends = reference_object.real_amends
+    def value_meta(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str:
+        """Get the meta of the lesson event.
+
+        These information will be primarly used in our own calendar frontend.
+        """
 
         return {
             "id": reference_object.id,
             "amended": bool(reference_object.amends),
-            "amends": cls.value_meta(real_amends, request) if reference_object.amends else None,
+            "amends": cls.value_meta(reference_object.amends, request)
+            if reference_object.amends
+            else None,
             "teachers": [
                 {
                     "id": t.pk,
@@ -1513,12 +1532,12 @@ class LessonEvent(CalendarEvent):
                 }
                 for t in reference_object.teachers.all()
             ],
-            "is_teacher": request.user.person in reference_object.all_teachers,
+            "is_teacher": request.user.person in reference_object.all_teachers if request else None,
             "groups": [
                 {"id": g.pk, "name": g.name, "short_name": g.short_name}
                 for g in reference_object.actual_groups
             ],
-            "is_member": request.user.person in reference_object.all_members,
+            "is_member": request.user.person in reference_object.all_members if request else None,
             "rooms": [
                 {"id": r.pk, "name": r.name, "short_name": r.short_name}
                 for r in reference_object.rooms.all()
@@ -1537,11 +1556,13 @@ class LessonEvent(CalendarEvent):
         }
 
     @classmethod
-    def get_objects(cls, request, params=None) -> Iterable:
+    def get_objects(
+        cls, request: HttpRequest | None = None, params: dict[str, any] | None = None
+    ) -> Iterable:
         """Return all objects that should be included in the calendar."""
         objs = super().get_objects(request, params).not_instance_of(SupervisionEvent)
 
-        if not has_person(request.user):
+        if request and not has_person(request.user):
             raise PermissionDenied()
 
         if params:
@@ -1557,7 +1578,7 @@ class LessonEvent(CalendarEvent):
             if not_amending:
                 objs = objs.not_amending()
 
-            if "own" in params:
+            if request and "own" in params:
                 if own:
                     objs = objs.for_person(request.user.person)
                 else:
@@ -1572,9 +1593,9 @@ class LessonEvent(CalendarEvent):
                     return objs.for_room(obj_id)
                 elif type_ == "COURSE":
                     return objs.for_course(obj_id)
-            elif "own" in params:
-                return objs
-        return objs.for_person(request.user.person)
+        if request:
+            return objs.for_person(request.user.person)
+        return objs
 
     class Meta:
         verbose_name = _("Lesson Event")
@@ -1582,25 +1603,31 @@ class LessonEvent(CalendarEvent):
 
 
 class SupervisionEvent(LessonEvent):
+    """Calendar feed for supervisions."""
+
     name = "supervision"
     verbose_name = _("Supervisions")
 
     objects = PolymorphicBaseManager.from_queryset(LessonEventQuerySet)()
 
     @classmethod
-    def value_title(cls, reference_object: LessonEvent, request) -> str:
+    def value_title(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str:
         """Get the title of the event."""
 
         return _("Supervision: {}").format(reference_object.room_names)
 
     @classmethod
-    def value_description(cls, reference_object: LessonEvent, request) -> str:
+    def value_description(
+        cls, reference_object: LessonEvent, request: HttpRequest | None = None
+    ) -> str:
         return render_to_string(
             "chronos/supervision_event_description.txt", {"event": reference_object}
         )
 
     @classmethod
-    def get_objects(cls, request, params=None) -> Iterable:
+    def get_objects(
+        cls, request: HttpRequest | None = None, params: dict[str, any] | None = None
+    ) -> Iterable:
         """Return all objects that should be included in the calendar."""
         objs = cls.objects.instance_of(cls)
         if params:
@@ -1614,4 +1641,6 @@ class SupervisionEvent(LessonEvent):
                     return objs.for_group(obj_id)
                 elif type_ == "ROOM":
                     return objs.for_room(obj_id)
-        return objs.for_person(request.user.person)
+        if request:
+            return objs.for_person(request.user.person)
+        return objs