diff --git a/src/api/views/schedule.py b/src/api/views/schedule.py
index 3fd9bba054813a374fb2c50154be195a3fde42ba..d9c2c433d19ec4fcf9886839942d900244fd8676 100644
--- a/src/api/views/schedule.py
+++ b/src/api/views/schedule.py
@@ -127,7 +127,7 @@ class EventSchedule(ConferenceSlugMixin, APIView):
     permission_classes = [IsApiUserOrReadOnly]
 
     def get(self, request, pk, format=None, **kwargs):
-        event = Event.objects.accessible_by_user(conference=self.conference, user=self.request.user).get(pk=pk)
+        event = Event.objects.associated_with_user(conference=self.conference, user=self.request.user, show_public=True).get(pk=pk)
         return Response(ScheduleEncoder().encode_event(event, self.conference.timezone))
 
     def post(self, request, pk, format=None, **kwargs):
diff --git a/src/core/models/events.py b/src/core/models/events.py
index 2ae99c5c7132ea747d1cbc0c66d991261aad8e9d..bc1439f10660859c8d9f5911f2003e29d0e558ae 100644
--- a/src/core/models/events.py
+++ b/src/core/models/events.py
@@ -1,7 +1,7 @@
 import logging
 import random
 from datetime import timedelta
-from re import compile
+from re import compile as re_compile
 from typing import Any
 from uuid import UUID, uuid4
 
@@ -11,7 +11,7 @@ from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
 from django.db import models
-from django.db.models import Q
+from django.db.models import Q, QuerySet
 from django.forms import DurationField
 from django.utils import timezone
 from django.utils.dateparse import parse_datetime
@@ -20,19 +20,18 @@ from django.utils.text import slugify
 from django.utils.timezone import is_aware, make_aware
 from django.utils.translation import gettext_lazy as _
 
-from core.models.tags import TagItem
+from core.fields import ConferenceReference
+from core.markdown import compile_translated_markdown_fields, store_relationships
+from core.models.assemblies import Assembly
+from core.models.base_managers import ConferenceManagerMixin
+from core.models.conference import Conference, ConferenceMember, ConferenceTrack
+from core.models.rooms import Room
+from core.models.shared import BackendMixin
+from core.models.tags import TaggedItemMixin, TagItem
+from core.models.users import PlatformUser
+from core.utils import str2bool, str2timedelta
 
-from ..fields import ConferenceReference
-from ..markdown import compile_translated_markdown_fields, store_relationships
-from ..utils import str2bool, str2timedelta
-from .assemblies import Assembly
-from .conference import Conference, ConferenceMember, ConferenceTrack
-from .rooms import Room
-from .shared import BackendMixin
-from .tags import TaggedItemMixin
-from .users import PlatformUser
-
-SIMPLE_TIME_RE = compile(r'(.*\s)?(\d+:\d+)$')
+SIMPLE_TIME_RE = re_compile(r'(.*\s)?(\d+:\d+)$')
 
 logger = logging.getLogger(__name__)
 
@@ -60,76 +59,21 @@ class EventDurationField(models.DurationField):
         return super().formfield(**defaults)
 
 
-class EventManager(models.Manager):
-    def accessible_by_user(self, user: PlatformUser, conference: Conference):
-        assert user is not None
-        assert conference is not None
-
-        qs = self.get_queryset().filter(conference=conference)
-
-        # crude hack because Django makes it that hard to customize AnonymousUser
-        if user is None or not user.is_authenticated:
-            user = PlatformUser.get_anonymous_user()
-
-        # for global staff apply no more limits
-        if user.is_staff or user.is_superuser:
-            return qs
-
-        # see if we can return all events (i.e. the user is marked as "staff")
-        if user.is_authenticated:
-            try:
-                member = ConferenceMember.objects.get(conference=conference, user=user)
-                if member.is_staff:
-                    return qs
-            except ConferenceMember.DoesNotExist:
-                pass
-
-        # for everybody else, only show public events
-        return qs.filter(is_public=True)
-
-    def conference_accessible(self, conference: Conference):
-        return (
-            self.get_queryset()
-            .filter(conference=conference, is_public=True)
-            .filter(
-                Q(assembly__state_assembly__in=Assembly.PUBLIC_STATES)
-                | Q(assembly__state_channel__in=Assembly.PUBLIC_STATES)
-                | Q(kind=Event.Kind.SELF_ORGANIZED)
-            )
-        )
-
-    def manageable_by_user(self, user: PlatformUser, conference: Conference, staff_can_manage: bool = True):
-        assert user is not None
-        assert conference is not None
-
-        qs = self.get_queryset().filter(conference=conference)
-
-        # crude hack because Django makes it that hard to customize AnonymousUser
-        if user is None or not user.is_authenticated:
-            return qs.none()
+class EventManager(ConferenceManagerMixin['Event']):
+    staff_permissions = ['assembly_team', 'scheduleadmin']
+    conference_filter = 'assembly__conference'
+    assembly_filter = 'assembly'
 
-        # for global staff apply no more limits
-        if staff_can_manage and (user.is_staff or user.is_superuser):
-            return qs
-
-        # see if we can return all events (i.e. the user is marked as "staff")
-        if staff_can_manage:
-            try:
-                member = ConferenceMember.objects.get(conference=conference, user=user)
-                if member.is_staff:
-                    return qs
-            except ConferenceMember.DoesNotExist:
-                pass
+    def apply_public_filter(self, queryset: 'QuerySet[Event]', member: ConferenceMember | None = None) -> 'QuerySet[Event]':
+        return queryset.filter(
+            (Q(assembly__state_assembly__in=Assembly.PUBLIC_STATES) | Q(assembly__state_channel__in=Assembly.PUBLIC_STATES) | Q(kind=Event.Kind.SELF_ORGANIZED))
+            & Q(is_public=True)
+        )
 
-        # for everybody else, only show public events
-        manageable_assemblies_qs = Assembly.objects.manageable_by_user(conference=conference, user=user).filter(
-            members__member=user, members__can_manage_assembly=True
+    def apply_manage_filter(self, queryset: 'QuerySet[Event]', member: ConferenceMember) -> 'QuerySet[Event]':
+        return queryset.filter(
+            Q(assembly__members__member=member, assembly__members__can_manage_assembly=True) | Q(owner=member.user, kind=Event.Kind.SELF_ORGANIZED)
         )
-        # TODO: remove work around when implementing ConferenceManagerMixin
-        # Filter out events belonging to the SoS Assembly
-        if not staff_can_manage:
-            manageable_assemblies_qs.exclude(id=conference.self_organized_sessions_assembly_id)
-        return qs.filter(Q(kind=Event.Kind.SELF_ORGANIZED, owner=user) | Q(assembly_id__in=manageable_assemblies_qs.values_list('id', flat=True)))
 
 
 class Event(TaggedItemMixin, BackendMixin, models.Model):
diff --git a/src/plainui/views/events.py b/src/plainui/views/events.py
index 0b1535e385925ebe14240a34e57923300e1e82a0..962b253a153d235c85a991bc585d796081dc9d20 100644
--- a/src/plainui/views/events.py
+++ b/src/plainui/views/events.py
@@ -188,7 +188,7 @@ class SosList(ConferenceRequiredMixin, FilteredListView):
         context['events_upcoming'] = event_filter(self.request.user, self.conf, kinds=[Event.Kind.SELF_ORGANIZED], calendar_mode=False, upcoming=True)
         context['events'] = event_filter(self.request.user, self.conf, kinds=[Event.Kind.SELF_ORGANIZED], calendar_mode=False)
         context['is_favorite_events'] = session_get_favorite_events(self.request.session, self.request.user)
-        context['manageable_events'] = Event.objects.manageable_by_user(self.request.user, self.conf).only('id')
+        context['manageable_events'] = Event.objects.manageable_by_user(self.conf, user=self.request.user).only('id')
 
         context['report_info'] = {'enabled': True}