diff --git a/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/slot.graphql b/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/slot.graphql
index 269faa53c9f3bb62ea2a5d138c778279089d433f..016045303cde040117ada2a4c1f35862a96cf747 100644
--- a/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/slot.graphql
+++ b/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/slot.graphql
@@ -152,8 +152,8 @@ mutation carryOverSlots(
   }
 }
 
-mutation copySlotsFromGrid($toTimeGrid: ID!, $fromTimeGrid: ID!) {
-  copySlotsFromGrid(timeGrid: $toTimeGrid, fromTimeGrid: $fromTimeGrid) {
+mutation copySlotsFromGrid($fromTimeGrid: ID!, $toTimeGrid: ID!) {
+  copySlotsFromGrid(fromTimeGrid: $fromTimeGrid, toTimeGrid: $toTimeGrid) {
     result {
       id
       model
diff --git a/aleksis/apps/lesrooster/managers.py b/aleksis/apps/lesrooster/managers.py
index 10703b00c271ed9d9be8e74b639c47aa0d76d390..2b96c5352498c83724979bd88d23d25d2f25a056 100644
--- a/aleksis/apps/lesrooster/managers.py
+++ b/aleksis/apps/lesrooster/managers.py
@@ -1,7 +1,14 @@
+from typing import TYPE_CHECKING
+
 from django.db.models import QuerySet
 
+from reversion import create_revision, set_comment
+
 from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations, DateRangeQuerySetMixin
 
+if TYPE_CHECKING:
+    from .models import BreakSlot, Lesson, Slot, Supervision, TimeGrid, ValidityRange
+
 
 class ValidityRangeQuerySet(QuerySet, DateRangeQuerySetMixin):
     """Custom query set for validity ranges."""
@@ -9,3 +16,133 @@ class ValidityRangeQuerySet(QuerySet, DateRangeQuerySetMixin):
 
 class ValidityRangeManager(AlekSISBaseManagerWithoutMigrations):
     """Manager for validity ranges."""
+
+    def copy_from_to(
+        self, source: ValidityRange, target: ValidityRange
+    ) -> dict[str, dict[any, any]]:
+        from .models import Lesson, Slot, Supervision, TimeGrid
+
+        time_grid_obj_map = TimeGrid.objects.copy_from_to(source, target)
+
+        slot_map = {}
+        for time_grid_source, time_grid_target in time_grid_obj_map.items():
+            slot_map |= Slot.objects.copy_from_to(time_grid_source, time_grid_target)
+
+        lesson_obj_map = Lesson.objects.copy_from_to(source, slot_map)
+
+        supervision_obj_map = Supervision.objects.copy_from_to(source, slot_map)
+
+        obj_map = dict(
+            time_grid=time_grid_obj_map,
+            slot=slot_map,
+            lesson=lesson_obj_map,
+            supervision=supervision_obj_map,
+        )
+        return obj_map
+
+
+class TimeGridManager(AlekSISBaseManagerWithoutMigrations):
+    """Manager for time grids."""
+
+    def copy_from_to(
+        self, source: ValidityRange, target: ValidityRange
+    ) -> list[tuple[TimeGrid, TimeGrid]]:
+        obj_map = []
+        with create_revision():
+            set_comment(f"Copy time grids from validity range {source.pk} to {target.pk}")
+
+            for time_grid in source.time_grids.all():
+                new_time_grid = self.create(validity_range=self, group=time_grid.group)
+
+                obj_map.append((time_grid, new_time_grid))
+
+        return obj_map
+
+
+class SlotManager(AlekSISBaseManagerWithoutMigrations):
+    """Manager for slots."""
+
+    def copy_from_to(
+        self, source: TimeGrid, target: TimeGrid
+    ) -> [dict[Slot, Slot], QuerySet[Slot]]:
+        obj_map = {}
+        with create_revision():
+            set_comment(f"Copy slots from time grid {source.pk} to {target.pk}")
+            slots = self.filter(time_grid=source)
+
+            update_or_created_objs = []
+
+            for slot in slots:
+                slot: BreakSlot | Slot
+                klass = slot.get_real_instance_class()
+
+                defaults = {
+                    "name": slot.name,
+                    "time_start": slot.time_start,
+                    "time_end": slot.time_end,
+                }
+
+                obj, __ = klass.objects.update_or_create(
+                    weekday=slot.weekday, time_grid=target, period=slot.period, defaults=defaults
+                )
+                obj_map[slot] = obj
+                update_or_created_objs.append(obj)
+
+            # Delete all slots in the time_grid that are not in the from_time_grid
+            objects_to_delete = self.filter(time_grid=target).exclude(id__in=update_or_created_objs)
+            objects_to_delete.delete()
+
+        return obj_map, objects_to_delete
+
+
+class LessonManager(AlekSISBaseManagerWithoutMigrations):
+    def copy_from_to(
+        self, source: ValidityRange, target_slot_map: dict[Slot, Slot]
+    ) -> dict[Lesson, Lesson]:
+        obj_map = {}
+
+        lessons = self.filter(slot_start__validity_range=source)
+        for lesson in lessons:
+            new_slot_start = target_slot_map.get(lesson.slot_start)
+            new_slot_end = target_slot_map.get(lesson.slot_end)
+            if not new_slot_start or not new_slot_end:
+                continue
+
+            new_lesson = self.create(
+                course=lesson.course,
+                slot_start=new_slot_start,
+                slot_end=new_slot_end,
+                recurrence=lesson.recurrence,
+                subject=lesson.subject,
+            )
+            new_lesson.rooms.set(lesson.rooms.all())
+            new_lesson.teachers.set(lesson.teachers.all())
+
+            obj_map[lesson] = new_lesson
+
+        return obj_map
+
+
+class SupervisionManager(AlekSISBaseManagerWithoutMigrations):
+    def copy_from_to(
+        self, source: ValidityRange, target_slot_map: dict[Slot, Slot]
+    ) -> dict[Supervision, Supervision]:
+        obj_map = {}
+
+        supervisions = self.filter(break_slot__validity_range=source)
+        for supervision in supervisions:
+            new_slot = target_slot_map.get(supervision.break_slot)
+            if not new_slot:
+                continue
+
+            new_supervision = self.create(
+                break_slot=supervision.break_slot,
+                recurrence=supervision.recurrence,
+                subject=supervision.subject,
+            )
+            new_supervision.rooms.set(supervision.rooms.all())
+            new_supervision.teachers.set(supervision.teachers.all())
+
+            obj_map[supervision] = new_supervision
+
+        return obj_map
diff --git a/aleksis/apps/lesrooster/models.py b/aleksis/apps/lesrooster/models.py
index e4007c2dcbff788061641dad73e8a4a8e67c7276..17554f2bb74b6a518a0b9b262affb8ef119429a1 100644
--- a/aleksis/apps/lesrooster/models.py
+++ b/aleksis/apps/lesrooster/models.py
@@ -20,7 +20,14 @@ from aleksis.apps.cursus.models import Course, Subject
 from aleksis.core.mixins import ExtensibleModel, ExtensiblePolymorphicModel, GlobalPermissionModel
 from aleksis.core.models import Group, Holiday, Person, Room, SchoolTerm
 
-from .managers import ValidityRangeManager, ValidityRangeQuerySet
+from .managers import (
+    LessonManager,
+    SlotManager,
+    SupervisionManager,
+    TimeGridManager,
+    ValidityRangeManager,
+    ValidityRangeQuerySet,
+)
 
 
 class ValidityRangeStatus(models.TextChoices):
@@ -97,6 +104,11 @@ class ValidityRange(ExtensibleModel):
                     )
                 )
 
+    def clear(self):
+        """Drop all data in this validity range."""
+        # Heads up: Other objects will be deleted through cascade
+        TimeGrid.objects.filter(validity_range=self).delete()
+
     def __str__(self) -> str:
         return self.name or f"{date_format(self.date_start)}–{date_format(self.date_end)}"
 
@@ -135,6 +147,8 @@ class TimeGrid(ExtensibleModel):
         null=True,
     )
 
+    objects = TimeGridManager()
+
     def __str__(self):
         if self.group:
             return f"{self.validity_range}: {self.group}"
@@ -178,6 +192,8 @@ class Slot(ExtensiblePolymorphicModel):
     time_start = models.TimeField(verbose_name=_("Start time"))
     time_end = models.TimeField(verbose_name=_("End time"))
 
+    objects = SlotManager()
+
     def __str__(self) -> str:
         if self.name:
             suffix = self.name
@@ -287,6 +303,8 @@ class Lesson(TeacherPropertiesMixin, RoomPropertiesMixin, ExtensibleModel):
         null=True,
     )
 
+    objects = LessonManager()
+
     def get_teachers(self) -> QuerySet[Person]:
         return self.teachers.all()
 
@@ -413,6 +431,8 @@ class Supervision(TeacherPropertiesMixin, RoomPropertiesMixin, ExtensibleModel):
         help_text=_("Leave empty for a single supervision."),
     )
 
+    objects = SupervisionManager()
+
     def get_teachers(self) -> QuerySet[Person]:
         return self.teachers.all()
 
diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py
index ca3170100b2298fdbc454068d1d5ab858bbc3f75..6586448340af5eb2e144bc5c78d2117535d37204 100644
--- a/aleksis/apps/lesrooster/schema/__init__.py
+++ b/aleksis/apps/lesrooster/schema/__init__.py
@@ -68,6 +68,7 @@ from .timebound_course_config import (
     TimeboundCourseConfigType,
 )
 from .validity_range import (
+    CopyDataFromValidityRangeMutation,
     ValidityRangeBatchDeleteMutation,
     ValidityRangeBatchPatchMutation,
     ValidityRangeCreateMutation,
@@ -293,8 +294,6 @@ class Mutation(graphene.ObjectType):
     create_timebound_course_config = TimeboundCourseConfigCreateMutation.Field()
     delete_timebound_course_config = TimeboundCourseConfigDeleteMutation.Field()
     update_timebound_course_configs = TimeboundCourseConfigBatchPatchMutation.Field()
-    carry_over_slots = CarryOverSlotsMutation.Field()
-    copy_slots_from_grid = CopySlotsFromDifferentTimeGridMutation.Field()
 
     create_validity_range = ValidityRangeCreateMutation.Field()
     delete_validity_range = ValidityRangeDeleteMutation.Field()
@@ -316,3 +315,7 @@ class Mutation(graphene.ObjectType):
     delete_supervision = SupervisionDeleteMutation.Field()
     delete_supervisions = SupervisionBatchDeleteMutation.Field()
     update_supervisions = SupervisionBatchPatchMutation.Field()
+
+    carry_over_slots = CarryOverSlotsMutation.Field()
+    copy_slots_from_grid = CopySlotsFromDifferentTimeGridMutation.Field()
+    copy_data_from_validty_range = CopyDataFromValidityRangeMutation.Field()
diff --git a/aleksis/apps/lesrooster/schema/slot.py b/aleksis/apps/lesrooster/schema/slot.py
index 6c2de6a501613bd058dd72a141912d644ee01b87..02a00e6f2caf04f5b0cc8577a4b1af4b030970f6 100644
--- a/aleksis/apps/lesrooster/schema/slot.py
+++ b/aleksis/apps/lesrooster/schema/slot.py
@@ -9,6 +9,7 @@ from graphene_django_cud.mutations import (
     DjangoCreateMutation,
 )
 from guardian.shortcuts import get_objects_for_user
+from reversion import create_revision, set_comment, set_user
 
 from aleksis.core.schema.base import (
     DeleteMutation,
@@ -18,7 +19,7 @@ from aleksis.core.schema.base import (
     PermissionsTypeMixin,
 )
 
-from ..models import BreakSlot, Slot, TimeGrid
+from ..models import Slot, TimeGrid
 
 slot_filters = {
     "id": ["exact"],
@@ -153,44 +154,26 @@ class CarryOverSlotsMutation(graphene.Mutation):
 
 class CopySlotsFromDifferentTimeGridMutation(graphene.Mutation):
     class Arguments:
-        time_grid = graphene.ID()
         from_time_grid = graphene.ID()
+        to_time_grid = graphene.ID()
 
     deleted = graphene.List(graphene.ID)
     result = graphene.List(SlotType)
 
     @classmethod
-    def mutate(cls, root, info, time_grid, from_time_grid):
+    def mutate(cls, root, info, from_time_grid, to_time_grid):
         if not info.context.user.has_perm("lesrooster.edit_slot_rule"):
             raise PermissionDenied()
 
-        time_grid = TimeGrid.objects.get(id=time_grid)
         from_time_grid = TimeGrid.objects.get(id=from_time_grid)
+        to_time_grid = TimeGrid.objects.get(id=to_time_grid)
 
-        # Check for each slot in the from_time_grid if it exists in the time_grid, if not, create it
-        slots = Slot.objects.filter(time_grid=from_time_grid)
-
-        result = []
-
-        for slot in slots:
-            slot: BreakSlot | Slot
-            klass = slot.get_real_instance_class()
-
-            defaults = {"name": slot.name, "time_start": slot.time_start, "time_end": slot.time_end}
-
-            result.append(
-                klass.objects.update_or_create(
-                    weekday=slot.weekday, time_grid=time_grid, period=slot.period, defaults=defaults
-                )[0].id
-            )
+        with create_revision():
+            set_user(info.context.user)
+            set_comment(f"Copy slots from time grid {from_time_grid.pk} to {to_time_grid.pk}")
+            __, deleted = Slot.objects.copy_from_to(from_time_grid, to_time_grid)
 
-        # Delete all slots in the time_grid that are not in the from_time_grid
-        objects_to_delete = Slot.objects.filter(time_grid=time_grid).exclude(id__in=result)
-        objects_to_delete.delete()
-
-        deleted = objects_to_delete.values_list("id", flat=True)
-
-        return CopySlotsFromDifferentTimeGridMutation(
-            deleted=deleted,
-            result=Slot.objects.filter(time_grid=time_grid).non_polymorphic(),
+        return cls(
+            deleted=deleted.values_list("id", flat=True),
+            result=Slot.objects.filter(time_grid=to_time_grid).non_polymorphic(),
         )
diff --git a/aleksis/apps/lesrooster/schema/validity_range.py b/aleksis/apps/lesrooster/schema/validity_range.py
index 040fc2778846be7d0e32354bd534fef0034b8880..7d439f2a41ab18aa11fdd651c74c06caf410e914 100644
--- a/aleksis/apps/lesrooster/schema/validity_range.py
+++ b/aleksis/apps/lesrooster/schema/validity_range.py
@@ -1,3 +1,5 @@
+from django.core.exceptions import PermissionDenied
+
 import graphene
 from graphene_django.types import DjangoObjectType
 from graphene_django_cud.mutations import (
@@ -6,6 +8,7 @@ from graphene_django_cud.mutations import (
     DjangoCreateMutation,
 )
 from guardian.shortcuts import get_objects_for_user
+from reversion import create_revision, set_comment, set_user
 
 from aleksis.core.schema.base import (
     DeleteMutation,
@@ -15,7 +18,7 @@ from aleksis.core.schema.base import (
     PermissionsTypeMixin,
 )
 
-from ..models import ValidityRange
+from ..models import TimeGrid, ValidityRange
 
 
 class ValidityRangeType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
@@ -81,3 +84,29 @@ class ValidityRangeBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatc
             "time_grids",
         )
         field_types = {"status": graphene.String()}
+
+
+class CopyDataFromValidityRangeMutation(graphene.Mutation):
+    class Arguments:
+        from_validty_range = graphene.ID()
+        to_validity_range = graphene.ID()
+
+    result = graphene.Boolean()
+
+    @classmethod
+    def mutate(cls, root, info, from_validity_range, to_validity_range):
+        if not info.context.user.has_perm("lesrooster.edit_slot_rule"):
+            raise PermissionDenied()
+
+        from_validity_range = TimeGrid.objects.get(id=from_validity_range)
+        to_validity_range = TimeGrid.objects.get(id=to_validity_range)
+
+        with create_revision():
+            set_user(info.context.user)
+            set_comment(
+                f"Copy data from validity range {from_validity_range.pk} to {to_validity_range.pk}"
+            )
+            to_validity_range.clear()
+            ValidityRange.objects.copy_from_to(from_validity_range, to_validity_range)
+
+        return cls(result=True)