diff --git a/aleksis/apps/alsijil/managers.py b/aleksis/apps/alsijil/managers.py
index 7350cf375a12205b7ebe56b9d5fd57bd045a2477..862e33a6d6a1cf0c8893359a65cb9a8ea6440da5 100644
--- a/aleksis/apps/alsijil/managers.py
+++ b/aleksis/apps/alsijil/managers.py
@@ -1,6 +1,13 @@
+from datetime import date, datetime
+from typing import Optional, Sequence, Union
+
 from django.db.models import QuerySet
+from django.db.models.query import Prefetch
 from django.db.models.query_utils import Q
 
+from calendarweek import CalendarWeek
+
+from aleksis.apps.chronos.managers import DateRangeQuerySetMixin
 from aleksis.core.managers import CurrentSiteManagerWithoutMigrations
 
 
@@ -42,3 +49,51 @@ class LessonDocumentationQuerySet(QuerySet):
     def not_empty(self):
         """Get all not empty lesson documentations."""
         return self.filter(~Q(topic="") | ~Q(group_note="") | ~Q(homework=""))
+
+
+class GroupRoleManager(CurrentSiteManagerWithoutMigrations):
+    pass
+
+
+class GroupRoleQuerySet(QuerySet):
+    def with_assignments(
+        self, time_ref: Union[date, CalendarWeek], groups: Sequence["Group"]
+    ) -> QuerySet:
+        from aleksis.apps.alsijil.models import GroupRoleAssignment
+
+        if isinstance(time_ref, CalendarWeek):
+            qs = GroupRoleAssignment.objects.in_week(time_ref)
+        else:
+            qs = GroupRoleAssignment.objects.on_day(time_ref)
+
+        qs = qs.for_groups(groups).distinct()
+        return self.prefetch_related(Prefetch("assignments", queryset=qs,))
+
+
+class GroupRoleAssignmentManager(CurrentSiteManagerWithoutMigrations):
+    pass
+
+
+class GroupRoleAssignmentQuerySet(DateRangeQuerySetMixin, QuerySet):
+    def within_dates(self, start: date, end: date):
+        """Filter for all role assignments within a date range."""
+        return self.filter(
+            Q(date_start__lte=end) & (Q(date_end__gte=start) | Q(date_end__isnull=True))
+        )
+
+    def at_time(self, when: Optional[datetime] = None):
+        """Filter for role assignments assigned at a certain point in time."""
+        now = when or datetime.now()
+
+        return self.on_day(now.date())
+
+    def for_groups(self, groups: Sequence["Group"]):
+        """Filter all role assignments for a sequence of groups."""
+        qs = self
+        for group in groups:
+            qs = qs.for_group(group)
+        return qs
+
+    def for_group(self, group: "Group"):
+        """Filter all role assignments for a group."""
+        return self.filter(Q(groups=group) | Q(groups__child_groups=group))
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 2335efe5f7e8b56a020198554dca9beaf0edaac5..e47e92a65846706ec0eb8e4d8d94689a941bb742 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -13,6 +13,10 @@ from aleksis.apps.alsijil.data_checks import (
     PersonalNoteOnHolidaysDataCheck,
 )
 from aleksis.apps.alsijil.managers import (
+    GroupRoleAssignmentManager,
+    GroupRoleAssignmentQuerySet,
+    GroupRoleManager,
+    GroupRoleQuerySet,
     LessonDocumentationManager,
     LessonDocumentationQuerySet,
     PersonalNoteManager,
@@ -242,6 +246,8 @@ class ExtraMark(ExtensibleModel):
 
 
 class GroupRole(ExtensibleModel):
+    objects = GroupRoleManager.from_queryset(GroupRoleQuerySet)()
+
     name = models.CharField(max_length=255, verbose_name=_("Name"))
     icon = models.CharField(max_length=50, blank=True, choices=ICONS, verbose_name=_("Icon"))
     colour = models.CharField(max_length=50, blank=True, choices=COLOURS, verbose_name=_("Colour"))
@@ -255,6 +261,8 @@ class GroupRole(ExtensibleModel):
 
 
 class GroupRoleAssignment(GroupPropertiesMixin, ExtensibleModel):
+    objects = GroupRoleAssignmentManager.from_queryset(GroupRoleAssignmentQuerySet)()
+
     role = models.ForeignKey(
         GroupRole,
         on_delete=models.CASCADE,
@@ -282,6 +290,13 @@ class GroupRoleAssignment(GroupPropertiesMixin, ExtensibleModel):
         date_end = date_format(self.date_end) if self.date_end else "?"
         return f"{self.role}: {self.person}, {date_format(self.date_start)}–{date_end}"
 
+    @property
+    def date_range(self) -> str:
+        if not self.date_end:
+            return f"{date_format(self.date_start)}–?"
+        else:
+            return f"{date_format(self.date_start)}–{date_format(self.date_end)}"
+
     class Meta:
         verbose_name = _("Group role assignment")
         verbose_name_plural = _("Group role assignments")
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 6519310b0657924e08aa94ab971acc7f5373d91a..f83f2cfac818b733c3724c2d358f3b20c26782f9 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -934,16 +934,7 @@ class AssignedGroupRolesView(PermissionRequiredMixin, DetailView):
         today = timezone.now().date()
         context["today"] = today
 
-        self.roles = GroupRole.objects.prefetch_related(
-            Prefetch(
-                "assignments",
-                queryset=GroupRoleAssignment.objects.filter(
-                    Q(date_start__lte=today) & (Q(date_end__gte=today) | Q(date_end__isnull=True))
-                )
-                .filter(Q(groups=self.object) | Q(groups__child_groups=self.object))
-                .distinct(),
-            )
-        )
+        self.roles = GroupRole.objects.with_assignments(today, [self.object])
         context["roles"] = self.roles
         assignments = (
             GroupRoleAssignment.objects.filter(