diff --git a/app/controllers/calendar/calendar.php b/app/controllers/calendar/calendar.php
index 2e17ff2eec5c7162d6def7d42bb887e932a49cce..eecf90016a393a74bebc00c52ff41fa3020c00b4 100644
--- a/app/controllers/calendar/calendar.php
+++ b/app/controllers/calendar/calendar.php
@@ -1,636 +1,896 @@
 <?php
-/*
- * The controller for the personal calendar.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @since
- */
 
 class Calendar_CalendarController extends AuthenticatedController
 {
     public function before_filter(&$action, &$args)
     {
         parent::before_filter($action, $args);
-        PageLayout::setHelpKeyword('Basis.Terminkalender');
-        $this->settings = $GLOBALS['user']->cfg->CALENDAR_SETTINGS;
-        if ($this->settings['start'] < 0 || $this->settings['start'] > 23) {
-            $this->settings['start'] = 0;
-        }
-        if ($this->settings['end'] < 0 || $this->settings['end'] > 23) {
-            $this->settings['end'] = 23;
-        }
-        if (!in_array($this->settings['view'], ['day','week','month','year'])) {
-            $this->settings['view'] = 'week';
-        }
-        if (!is_array($this->settings)) {
-            $this->settings = Calendar::getDefaultUserSettings();
-        }
-        URLHelper::bindLinkParam('atime', $this->atime);
-        $this->atime = Request::int('atime', time());
-        $this->category = Request::int('category');
-        $this->last_view = Request::option('last_view',
-                $this->settings['view']);
-        $this->action = $action;
-        $this->restrictions = [
-            'STUDIP_CATEGORY'     => $this->category ?: null,
-            // hide events with status 3 (CalendarEvent::PARTSTAT_DECLINED)
-            'STUDIP_GROUP_STATUS' => !empty($this->settings['show_declined']) ? null : [0,1,2,5]
-        ];
-        if ($this->category) {
-            URLHelper::bindLinkParam('category', $this->category);
-        }
-
-        $this->range_id = '';
 
-        if (Config::get()->COURSE_CALENDAR_ENABLE
-            && !Request::get('self')
-            && Course::findCurrent()) {
-            $current_seminar = new Seminar(Course::findCurrent());
-            if ($current_seminar->getSlotModule('calendar') instanceOf CoreCalendar) {
-                $this->range_id = $current_seminar->id;
-                Navigation::activateItem('/course/calendar');
-            }
-        }
-        if (!$this->range_id) {
-            $this->range_id = Request::option('range_id', $GLOBALS['user']->id);
-            Navigation::activateItem('/calendar/calendar');
-            URLHelper::bindLinkParam('range_id', $this->range_id);
+        if (!Context::isCourse() && Navigation::hasItem('/calendar')) {
+            Navigation::activateItem('/calendar');
         }
-
-        URLHelper::bindLinkParam('last_view', $this->last_view);
     }
 
-    protected function createSidebar($active = null, $calendar = null)
-    {
-        $active = $active ?: $this->last_view;
-        $sidebar = Sidebar::Get();
-
-        $views = new ViewsWidget();
-        $views->addLink(_('Tag'), $this->url_for($this->base . 'day'))
-                ->setActive($active == 'day');
-        $views->addLink(_('Woche'), $this->url_for($this->base . 'week'))
-                ->setActive($active == 'week');
-        $views->addLink(_('Monat'), $this->url_for($this->base . 'month'))
-                ->setActive($active == 'month');
-        $views->addLink(_('Jahr'), $this->url_for($this->base . 'year'))
-                ->setActive($active == 'year');
-        $sidebar->addWidget($views);
-    }
 
-    protected function createSidebarFilter()
+    protected function buildSidebar($schedule = false)
     {
-        $tmpl_factory = $this->get_template_factory();
-
-        $filters = new SidebarWidget();
-        $filters->setTitle('Auswahl');
-
-        $tmpl = $tmpl_factory->open('calendar/single/_jump_to');
-        $tmpl->atime = $this->atime;
-        $tmpl->action = $this->action;
-        $tmpl->action_url = $this->url_for('calendar/single/jump_to');
-        $filters->addElement(new WidgetElement($tmpl->render()));
-
-        $tmpl = $tmpl_factory->open('calendar/single/_select_category');
-        $tmpl->action_url = $this->url_for();
-        $tmpl->category = $this->category;
-        $filters->addElement(new WidgetElement($tmpl->render()));
-        Sidebar::get()->addWidget($filters);
-
-        if (Config::get()->CALENDAR_GROUP_ENABLE
-                || Config::get()->COURSE_CALENDAR_ENABLE) {
-            $tmpl = $tmpl_factory->open('calendar/single/_select_calendar');
-            $tmpl->range_id = $this->range_id;
-            $tmpl->action_url = $this->url_for('calendar/group/switch');
-            $tmpl->view = $this->action;
-            $filters->addElement(new WidgetElement($tmpl->render()));
-
-            $settings = new OptionsWidget();
-            $settings->addCheckbox(
-                _('Abgelehnte Termine anzeigen'),
-                $this->settings['show_declined'] ?? false,
-                $this->url_for($this->base . 'show_declined', ['show_declined' => 1]),
-                $this->url_for($this->base . 'show_declined', ['show_declined' => 0])
+        $sidebar = Sidebar::get();
+
+        $actions = new ActionsWidget();
+        if ($schedule) {
+            $actions->addLink(
+                _('Neuer Eintrag'),
+                $this->url_for('calendar/calendar/add_schedule_entry'),
+                Icon::create('add'),
+                ['data-dialog' => 'size=default']
+            );
+        } else {
+            $actions->addLink(
+                _('Termin anlegen'),
+                $this->url_for('calendar/date/add'),
+                Icon::create('add'),
+                ['data-dialog' => 'size=auto']
             );
-            Sidebar::get()->addWidget($settings);
+        }
+
+        if (!$GLOBALS['perm']->have_perm('admin')) {
+            $actions->addLink(
+                _('Veranstaltung auswählen'),
+                $this->url_for('calendar/calendar/add_courses'),
+                Icon::create('add'),
+                ['data-dialog' => 'size=medium']
+            );
+        }
+        if (!$schedule) {
+            $actions->addLink(
+                _('Termine exportieren'),
+                $this->url_for('calendar/calendar/export'),
+                Icon::create('export'),
+                ['data-dialog' => 'size=auto']
+            );
+            $actions->addLink(
+                _('Termine importieren'),
+                $this->url_for('calendar/calendar/import'),
+                Icon::create('import'),
+                ['data-dialog' => 'size=auto']
+            );
+            $actions->addLink(
+                _('Kalender veröffentlichen'),
+                $this->url_for('calendar/calendar/publish'),
+                Icon::create('export'),
+                ['data-dialog' => 'size=auto']
+            );
+        }
+        if (!$schedule && Config::get()->CALENDAR_GROUP_ENABLE) {
+            $actions->addLink(
+                _('Kalender teilen'),
+                $this->url_for('calendar/calendar/share'),
+                Icon::create('share'),
+                ['data-dialog' => 'size=default']
+            );
+        }
+        $actions->addLink(
+            _('Drucken'),
+            'javascript:void(window.print());',
+            Icon::create('print')
+        );
+        $actions->addLink(
+            _('Einstellungen'),
+            $this->url_for('settings/calendar'),
+            Icon::create('settings'),
+            ['data-dialog' => 'size=auto;reload-on-close']
+        );
+        $sidebar->addWidget($actions);
+
+        if (!$schedule) {
+            $date = new DateSelectWidget();
+            $date->setDate(\Studip\Calendar\Helper::getDefaultCalendarDate());
+            $date->setCalendarControl(true);
+            $sidebar->addWidget($date);
         }
     }
 
-    public function index_action()
+    protected function getUserCalendarSlotSettings() : array
     {
-        // switch to the view the user has selected in his personal settings
-        $default_view = $this->settings['view'] ?: 'week';
-
-        // Remove cid
-        if (Request::option('self')) {
-            Context::close();
-
-            $this->redirect(URLHelper::getURL('dispatch.php/' . $this->base
-                . $default_view . '/' . $GLOBALS['user']->id, [], true));
-        } else {
-            $this->redirect(URLHelper::getURL('dispatch.php/' . $this->base
-                . $default_view));
-        }
+        return [
+            'day'        => \Studip\Calendar\Helper::getCalendarSlotDuration('day'),
+            'week'       => \Studip\Calendar\Helper::getCalendarSlotDuration('week'),
+            'day_group'  => \Studip\Calendar\Helper::getCalendarSlotDuration('day_group'),
+            'week_group' => \Studip\Calendar\Helper::getCalendarSlotDuration('week_group')
+        ];
     }
 
-    public function edit_action($range_id = null, $event_id = null)
+    public function index_action()
     {
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = new SingleCalendar($this->range_id);
-        $this->event = $this->calendar->getEvent($event_id);
-
-        if ($this->event->isNew()) {
-         //   $this->event = $this->calendar->getNewEvent();
-            if (Request::get('isdayevent')) {
-                $this->event->setStart(mktime(0, 0, 0, date('n', $this->atime),
-                        date('j', $this->atime), date('Y', $this->atime)));
-                $this->event->setEnd(mktime(23, 59, 59, date('n', $this->atime),
-                        date('j', $this->atime), date('Y', $this->atime)));
+        PageLayout::setTitle(_('Kalender'));
+
+        if (Request::isPost()) {
+            //In case the checkbox of the options widget is clicked, the resulting
+            //POST request must be catched here and result in a redirect.
+            CSRFProtection::verifyUnsafeRequest();
+            if (Request::bool('show_declined')) {
+                $this->redirect('calendar/calendar', ['show_declined' => '1']);
             } else {
-                $this->event->setStart($this->atime);
-                $this->event->setEnd($this->atime + 3600);
+                $this->redirect('calendar/calendar');
             }
-            $this->event->setAuthorId($GLOBALS['user']->id);
-            $this->event->setEditorId($GLOBALS['user']->id);
-            $this->event->setAccessibility('PRIVATE');
-            if (!Request::isXhr()) {
-                PageLayout::setTitle($this->getTitle($this->calendar, _('Neuer Termin')));
+            return;
+        }
+
+        if (!Context::isCourse() && Navigation::hasItem('/calendar/calendar')) {
+            Navigation::activateItem('/calendar/calendar');
+        }
+
+        $view = Request::get('view', 'single');
+        $group_view = false;
+        $timeline_view = false;
+        if (Config::get()->CALENDAR_GROUP_ENABLE) {
+            $group_view = in_array($view, ['group', 'timeline']);
+            $timeline_view = $view === 'timeline';
+        }
+
+        $calendar_owner = null;
+        $selected_group = null;
+        $user_id = Request::option('user_id', User::findCurrent()->id);
+        $group_id = Request::option('group_id');
+
+        if (Config::get()->CALENDAR_GROUP_ENABLE) {
+            if ($group_id) {
+                $selected_group = ContactGroup::find($group_id);
+                if ($selected_group->owner_id !== User::findCurrent()->id) {
+                    //Thou shalt not see the groups of others!
+                    throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!'));
+                }
+                $view = $view === 'timeline' ? 'timeline' : 'group';
+            } elseif ($user_id) {
+                $calendar_owner = User::getCalendarOwner($user_id);
+                $view = 'single';
             }
         } else {
-            // open read only events and course events not as form
-            // show information in dialog instead
-            if (!$this->event->havePermission(Event::PERMISSION_WRITABLE)
-                    || $this->event instanceof CourseEvent) {
-                if (!$this->event instanceof CourseEvent && $this->event->attendees->count() > 1) {
-                    if ($this->event->group_status) {
-                        $this->redirect($this->url_for('calendar/single/edit_status/' . implode('/',
-                            [$this->range_id, $this->event->event_id])));
-                    } else {
-                        $this->redirect($this->url_for('calendar/single/event/' . implode('/',
-                            [$this->range_id, $this->event->event_id])));
+            //Show the calendar of the current user.
+            $view = 'single';
+            $calendar_owner = User::findCurrent();
+        }
+
+        //Check for permissions:
+        $read_permissions = false;
+        $write_permissions = false;
+        if ($calendar_owner) {
+            $read_permissions  = $calendar_owner->isCalendarReadable();
+            $write_permissions = $calendar_owner->isCalendarWritable();
+        } elseif ($selected_group) {
+            //Count on how many group member calendars the current user has read or write permissions:
+            foreach ($selected_group->items as $item) {
+                if ($item->user) {
+                    if ($item->user->isCalendarReadable()) {
+                        $read_permissions = true;
+                    }
+                    if ($item->user->isCalendarWritable()) {
+                        $write_permissions = true;
                     }
-                } else {
-                    $this->redirect($this->url_for('calendar/single/event/' . implode('/',
-                            [$this->range_id, $this->event->event_id])));
                 }
-                return null;
-            }
-            if (!Request::isXhr()) {
-                PageLayout::setTitle($this->getTitle($this->calendar, _('Termin bearbeiten')));
+                if ($read_permissions && $write_permissions) {
+                    //We only need to determine one read and one write permission to set the relevant fullcalendar
+                    //properties. The action to add/edit a date determines in which calendars the current user
+                    //may write into.
+                    break;
+                }
             }
         }
+        if (!$read_permissions) {
+            throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!'));
+        }
 
-        if (Config::get()->CALENDAR_GROUP_ENABLE
-                && $this->calendar->getRange() == Calendar::RANGE_USER) {
-
-            if (Config::get()->CALENDAR_GRANT_ALL_INSERT) {
-                $search_obj = SQLSearch::get("SELECT DISTINCT auth_user_md5.user_id, "
-                    . "{$GLOBALS['_fullname_sql']['full_rev_username']} as fullname, "
-                    . "auth_user_md5.perms, auth_user_md5.username "
-                    . "FROM auth_user_md5 "
-                    . "LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id) "
-                    . 'WHERE auth_user_md5.user_id <> ' . DBManager::get()->quote($GLOBALS['user']->id)
-                    . ' AND (username LIKE :input OR Vorname LIKE :input '
-                    . "OR CONCAT(Vorname,' ',Nachname) LIKE :input "
-                    . "OR CONCAT(Nachname,' ',Vorname) LIKE :input "
-                    . "OR Nachname LIKE :input OR {$GLOBALS['_fullname_sql']['full_rev']} LIKE :input "
-                    . ") ORDER BY fullname ASC",
-                    _('Person suchen'), 'user_id');
-            } else {
-                $search_obj = SQLSearch::get("SELECT DISTINCT auth_user_md5.user_id, "
-                    . "{$GLOBALS['_fullname_sql']['full_rev_username']} as fullname, "
-                    . "auth_user_md5.perms, auth_user_md5.username "
-                    . "FROM calendar_user "
-                    . "LEFT JOIN auth_user_md5 ON calendar_user.owner_id = auth_user_md5.user_id "
-                    . "LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id) "
-                    . 'WHERE calendar_user.user_id = '
-                    . DBManager::get()->quote($GLOBALS['user']->id)
-                    . ' AND calendar_user.permission > ' . Event::PERMISSION_READABLE
-                    . ' AND auth_user_md5.user_id <> ' . DBManager::get()->quote($GLOBALS['user']->id)
-                    . ' AND (username LIKE :input OR Vorname LIKE :input '
-                    . "OR CONCAT(Vorname,' ',Nachname) LIKE :input "
-                    . "OR CONCAT(Nachname,' ',Vorname) LIKE :input "
-                    . "OR Nachname LIKE :input OR {$GLOBALS['_fullname_sql']['full_rev']} LIKE :input "
-                    . ") ORDER BY fullname ASC",
-                    _('Person suchen'), 'user_id');
+        $this->buildSidebar(false);
+
+        $sidebar = Sidebar::get();
+
+        if (Config::get()->CALENDAR_GROUP_ENABLE) {
+            if ($calendar_owner && $calendar_owner->id === User::findCurrent()->id) {
+                //The user is viewing their own calendar.
+                $options = new OptionsWidget();
+                $options->addCheckbox(
+                    _('Abgelehnte Termine anzeigen'),
+                    Request::bool('show_declined'),
+                    $this->url_for('calendar/calendar', ['show_declined' => '1']),
+                    $this->url_for('calendar/calendar')
+                );
+                $sidebar->addWidget($options);
             }
 
-            // SEMBBS
-            // Eintrag von Terminen bereits ab PERMISSION_READABLE
-            /*
-            $search_obj = new SQLSearch('SELECT DISTINCT auth_user_md5.user_id, '
-                . $GLOBALS['_fullname_sql']['full_rev'] . ' as fullname, username, perms '
-                . 'FROM calendar_user '
-                . 'LEFT JOIN auth_user_md5 ON calendar_user.owner_id = auth_user_md5.user_id '
-                . 'LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id) '
-                . 'WHERE calendar_user.user_id = '
-                . DBManager::get()->quote($GLOBALS['user']->id)
-                . ' AND calendar_user.permission >= ' . Event::PERMISSION_READABLE
-                . ' AND (username LIKE :input OR Vorname LIKE :input '
-                . "OR CONCAT(Vorname,' ',Nachname) LIKE :input "
-                . "OR CONCAT(Nachname,' ',Vorname) LIKE :input "
-                . 'OR Nachname LIKE :input OR '
-                . $GLOBALS['_fullname_sql']['full_rev'] . ' LIKE :input '
-                . ') ORDER BY fullname ASC',
-                _('Nutzer suchen'), 'user_id');
-            // SEMBBS
-             *
-             */
-
-
-            $this->quick_search = QuickSearch::get('user_id', $search_obj)
-                    ->fireJSFunctionOnSelect('STUDIP.Messages.add_adressee')
-                    ->withButton();
-
-      //      $default_selected_user = array($this->calendar->getRangeId());
-            $this->mps = MultiPersonSearch::get('add_adressees')
-                ->setLinkText(_('Mehrere Teilnehmende hinzufügen'))
-       //         ->setDefaultSelectedUser($default_selected_user)
-                ->setTitle(_('Mehrere Teilnehmende hinzufügen'))
-                ->setExecuteURL($this->url_for($this->base . 'edit'))
-                ->setJSFunctionOnSubmit('STUDIP.Messages.add_adressees')
-                ->setSearchObject($search_obj);
-            $owners = SimpleORMapCollection::createFromArray(
-                    CalendarUser::findByUser_id($this->calendar->getRangeId()))
-                    ->pluck('owner_id');
-            foreach (Calendar::getGroups($GLOBALS['user']->id) as $group) {
-                $this->mps->addQuickfilter(
-                    $group->name,
-                    $group->members->filter(
-                        function ($member) use ($owners) {
-                            if (in_array($member->user_id, $owners)) {
-                                return $member;
-                            }
-                        })->pluck('user_id')
+            //Check if the user has groups. If so, display a select widget to select a group.
+            $groups = ContactGroup::findBySQL(
+                'owner_id = :owner_id ORDER BY name ASC',
+                [
+                    'owner_id' => User::findCurrent()->id
+                ]
+            );
+            if ($groups) {
+                $available_groups = [];
+
+                //Check if the user has at least read permissions for the calendar of one user of one group:
+                foreach ($groups as $group) {
+                    foreach ($group->items as $item) {
+                        if ($item->user && $item->user->isCalendarReadable()) {
+                            $available_groups[] = $group;
+                            break 1;
+                        }
+                    }
+                }
+                if ($available_groups) {
+                    $group_select = new SelectWidget(
+                        _('Gruppe'),
+                        $this->url_for('calendar/calendar/index', ['view' => 'group']),
+                        'group_id'
+                    );
+                    $options = [
+                        '' => _('(bitte wählen)')
+                    ];
+                    foreach ($available_groups as $available_group) {
+                        $options[$available_group->id] = $available_group->name;
+                    }
+                    $group_select->setOptions($options);
+                    $group_select->setSelection($group_id);
+                    $sidebar->addWidget($group_select);
+                }
+            }
+            //Get all calendars where the user has access to:
+            $other_users = User::findBySql(
+                "INNER JOIN `contact` c
+                ON `auth_user_md5`.`user_id` = c.`owner_id`
+                WHERE c.`user_id` = :current_user_id
+                AND c.`calendar_permissions` <> ''
+                ORDER BY `auth_user_md5`.`Vorname` ASC, `auth_user_md5`.`Nachname` ASC",
+                ['current_user_id' => User::findCurrent()->id]
+            );
+            if ($other_users) {
+                $calendar_select = new SelectWidget(
+                    _('Kalender auswählen'),
+                    $this->url_for('calendar/calendar'),
+                    'user_id'
                 );
+                $select_options = [
+                    '' => _('(bitte wählen)'),
+                    User::findCurrent()->id => _('Eigener Kalender')
+                ];
+                foreach ($other_users as $user) {
+                    $select_options[$user->id] = $user->getFullName();
+                }
+                $calendar_select->setOptions($select_options, Request::get('user_id'));
+                $sidebar->addWidget($calendar_select);
             }
         }
 
-        $stored = false;
-        if (Request::submitted('store')) {
-            $stored = $this->storeEventData($this->event, $this->calendar);
+        if (Config::get()->CALENDAR_GROUP_ENABLE && $selected_group) {
+            $views = new ViewsWidget();
+            $views->setTitle(_('Kalenderansicht'));
+            $views->addLink(
+                _('Gruppenkalender'),
+                $this->url_for('calendar/calendar', ['view' => 'group', 'group_id' => $group_id])
+            )->setActive($view === 'group');
+            $views->addLink(
+                _('Zeitleiste'),
+                $this->url_for('calendar/calendar', ['view' => 'timeline', 'group_id' => $group_id])
+            )->setActive($view === 'timeline');
+            $sidebar->addWidget($views);
         }
 
-        if ($stored !== false) {
-            if ($stored === 0) {
-                if (Request::isXhr()) {
-                    header('X-Dialog-Close: 1');
-                    exit;
-                } else {
-                    PageLayout::postMessage(MessageBox::success(_('Der Termin wurde nicht geändert.')));
-                    $this->relocate('calendar/single/' . $this->last_view, ['atime' => $this->atime]);
+        $calendar_resources = [];
+        $calendar_group_title = '';
+        if ($group_view && $selected_group) {
+            //All users in the selected group that have granted read permissions to the user can be shown.
+            foreach ($selected_group->items as $item) {
+                if ($item->user && $item->user->isCalendarReadable()) {
+                    $calendar_resources[] = [
+                        'id' => $item->user_id,
+                        'title' => $item->user ? $item->user->getFullName() : '',
+                        'parent_name' => ''
+                    ];
                 }
-            } else {
-                PageLayout::postMessage(MessageBox::success(_('Der Termin wurde gespeichert.')));
-                $this->relocate('calendar/single/' . $this->last_view, ['atime' => $this->atime]);
             }
+            $calendar_group_title = $selected_group->name;
         }
 
-        $this->createSidebar('edit', $this->calendar);
-        $this->createSidebarFilter();
+        $fullcalendar_studip_urls = [];
+        if ($write_permissions) {
+            if ($calendar_owner) {
+                $fullcalendar_studip_urls['add'] = $this->url_for('calendar/date/add', ['user_id' => $calendar_owner->id]);
+            } elseif ($selected_group) {
+                $fullcalendar_studip_urls['add'] = $this->url_for('calendar/date/add', ['group_id' => $group->id]);
+            }
+        }
+
+        $calendar_settings = User::findCurrent()->getConfiguration()->CALENDAR_SETTINGS ?? [];
+
+        //Map calendar settings to fullcalendar settings:
+
+        $default_view = 'timeGridWeek';
+        if ($timeline_view) {
+            $default_view = 'resourceTimelineWeek';
+            if ($calendar_settings['view'] === 'day') {
+                $default_view = 'resourceTimelineDay';
+            }
+        } elseif (!empty($calendar_settings['view'])) {
+            if ($calendar_settings['view'] === 'day') {
+                $default_view = 'timeGridDay';
+            } elseif ($calendar_settings['view'] === 'month') {
+                $default_view = 'dayGridMonth';
+            }
+        }
+
+        $slot_durations = $this->getUserCalendarSlotSettings();
+
+        //Create the fullcalendar object:
+        $default_date = \Studip\Calendar\Helper::getDefaultCalendarDate();
+
+        $data_url_params = [];
+        if (Request::bool('show_declined')) {
+            $data_url_params['show_declined'] = '1';
+        }
+        if ($timeline_view) {
+            $data_url_params['timeline_view'] = '1';
+        }
+
+        $this->fullcalendar = Studip\Fullcalendar::create(
+            _('Kalender'),
+            [
+                'editable'    => $write_permissions,
+                'selectable'  => $write_permissions,
+                'studip_urls' => $fullcalendar_studip_urls,
+                'dialog_size' => 'auto',
+                'minTime'     => sprintf('%02u:00', $calendar_settings['start'] ?? 8),
+                'maxTime'     => sprintf('%02u:00', $calendar_settings['end'] ?? 20),
+                'defaultDate' => $default_date->format('Y-m-d'),
+                'allDaySlot'  => !$group_view,
+                'allDayText'  => '',
+                'header'      => [
+                    'left'   => (
+                        $timeline_view
+                            ? 'resourceTimelineWeek,resourceTimelineDay'
+                            : 'dayGridYear,dayGridMonth,timeGridWeek,timeGridDay'
+                    ),
+                    'right'  => 'prev,today,next'
+                ],
+                'weekNumbers' => true,
+                'views' => [
+                    'dayGridMonth' => [
+                        'eventTimeFormat' => ['hour' => 'numeric', 'minute' => '2-digit'],
+                        'displayEventEnd' => true
+                    ],
+                    'timeGridWeek' => [
+                        'columnHeaderFormat' => ['weekday' => 'short', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true],
+                        'weekends'           => $calendar_settings['type_week'] === 'LONG',
+                        'slotDuration'       => $slot_durations['week']
+                    ],
+                    'timeGridDay' => [
+                        'columnHeaderFormat' => ['weekday' => 'long', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true],
+                        'slotDuration'       => $slot_durations['day']
+                    ],
+                    'resourceTimelineWeek' => [
+                        'columnHeaderFormat' => ['weekday' => 'long', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true],
+                        'titleFormat'        => ['year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit'],
+                        'weekends'           => $calendar_settings['type_week'] === 'LONG',
+                        'slotDuration'       => $slot_durations['week_group']
+                    ],
+                    'resourceTimelineDay' => [
+                        'columnHeaderFormat' => ['weekday' => 'long', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true],
+                        'titleFormat'        => ['year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit'],
+                        'slotDuration'       => $slot_durations['day_group']
+                    ]
+                ],
+                'defaultView' => $default_view,
+                'timeGridEventMinHeight' => 20,
+                'eventSources' => [
+                    [
+                        'url' => $this->url_for(
+                            (
+                            $group_view
+                                ? 'calendar/calendar/calendar_group_data/' . $selected_group->id
+                                : 'calendar/calendar/calendar_data/' . $calendar_owner->id
+                            ),
+                            $data_url_params
+                        ),
+                        'method' => 'GET',
+                        'extraParams' => []
+                    ]
+                ],
+                'resources' => $calendar_resources,
+                'resourceLabelText' => $calendar_group_title
+            ]
+        );
     }
 
-    public function edit_status_action($range_id, $event_id)
+    public function course_action($course_id)
     {
-        global $user;
+        PageLayout::setTitle(_('Veranstaltungskalender'));
 
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = new SingleCalendar($this->range_id);
-        $this->event = $this->calendar->getEvent($event_id);
-        $stored = false;
-        $old_status = $this->event->group_status;
+        if (!$course_id || !Config::get()->CALENDAR_GROUP_ENABLE || !Config::get()->COURSE_CALENDAR_ENABLE) {
+            throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!'));
+        }
 
-        if (Request::submitted('store')) {
+        $course = Course::find($course_id);
+        if (!$course) {
+            throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!'));
+        }
 
-            if ($this->event->isNew()
-                || !Config::get()->CALENDAR_GROUP_ENABLE
-                || !$this->calendar->havePermission(Calendar::PERMISSION_OWN)
-                || !$this->calendar->getRange() == Calendar::RANGE_USER
-                || !$this->event->havePermission(Event::PERMISSION_READABLE)) {
-                throw new AccessDeniedException();
+        if (!$course->isVisibleForUser() || !$course->isCalendarReadable()) {
+            throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!'));
+        }
+
+        if (Navigation::hasItem('/course/calendar')) {
+            Navigation::activateItem('/course/calendar');
+        }
+
+        $sidebar = Sidebar::get();
+
+        $actions = new ActionsWidget();
+        $actions->addLink(
+            _('Termin anlegen'),
+            $this->url_for('calendar/date/add/course_' . $course->id),
+            Icon::create('add'),
+            ['data-dialog' => 'size=default']
+        );
+        $actions->addLink(
+            _('Drucken'),
+            'javascript:void(window.print());',
+            Icon::create('print')
+        );
+        $actions->addLink(
+            _('Einstellungen'),
+            $this->url_for('settings/calendar'),
+            Icon::create('settings'),
+            ['data-dialog' => 'reload-on-close']
+        );
+        $sidebar->addWidget($actions);
+
+        $date = new DateSelectWidget();
+        $date->setCalendarControl(true);
+        $sidebar->addWidget($date);
+
+        //Create the fullcalendar object:
+
+        $calendar_writable = $course->isCalendarWritable();
+        $calendar_settings = User::findCurrent()->getConfiguration()->CALENDAR_SETTINGS ?? [];
+        $slot_settings = $this->getUserCalendarSlotSettings();
+
+        $fullcalendar_studip_urls = [];
+        if ($calendar_writable) {
+            $fullcalendar_studip_urls['add'] = $this->url_for('calendar/date/add/course_' . $course->id);
+        }
+
+        $this->fullcalendar = Studip\Fullcalendar::create(
+            _('Veranstaltungskalender'),
+            [
+                'editable'    => $calendar_writable,
+                'selectable'  => $calendar_writable,
+                'studip_urls' => $fullcalendar_studip_urls,
+                'minTime'     => sprintf('%02u:00', $calendar_settings['start'] ?? 8),
+                'maxTime'     => sprintf('%02u:00', $calendar_settings['end'] ?? 20),
+                'allDaySlot'  => true,
+                'allDayText'  => '',
+                'header'      => [
+                    'left'    => 'dayGridYear,dayGridMonth,timeGridWeek,timeGridDay',
+                    'right'   => 'prev,today,next'
+                ],
+                'weekNumbers' => true,
+                'views'       => [
+                    'dayGridMonth' => [
+                        'eventTimeFormat' => ['hour' => 'numeric', 'minute' => '2-digit'],
+                        'displayEventEnd' => true
+                    ],
+                    'timeGridWeek' => [
+                        'columnHeaderFormat' => [ 'weekday' => 'short', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true ],
+                        'weekends'           => $calendar_settings['type_week'] === 'LONG',
+                        'slotDuration'       => $slot_settings['week']
+                    ],
+                    'timeGridDay'  => [
+                        'columnHeaderFormat' => [ 'weekday' => 'long', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true ],
+                        'slotDuration'       => $slot_settings['day']
+                    ]
+                ],
+                'defaultView'            => 'timeGridWeek',
+                'timeGridEventMinHeight' => 20,
+                'eventSources'           => [
+                    [
+                        'url'         => $this->url_for('calendar/calendar/calendar_data/course_' . $course->id),
+                        'method'      => 'GET',
+                        'extraParams' => []
+                    ]
+                ]
+            ]
+        );
+    }
+
+    public function calendar_data_action($range_and_id)
+    {
+        $range_and_id = explode('_', $range_and_id);
+        $range = '';
+        $range_id = '';
+        if (!empty($range_and_id[1])) {
+            $range = $range_and_id[0];
+            $range_id = $range_and_id[1];
+        }
+        if (!$range) {
+            //Show the personal calendar of the current user:
+            $range = 'user';
+            $range_id = User::findCurrent()->id;
+        }
+        $owner = null;
+        if (!$range_id) {
+            //Assume a user calendar. $range contains the user-ID.
+            $owner = User::getCalendarOwner($range);
+        } elseif ($range === 'user') {
+            $owner = User::getCalendarOwner($range_id);
+        } elseif ($range === 'course') {
+            $owner = Course::getCalendarOwner($range_id);
+        }
+
+        if (!$owner || !$owner->isCalendarReadable()) {
+            throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!'));
+        }
+
+        $begin = Request::getDateTime('start', \DateTime::RFC3339);
+        $end = Request::getDateTime('end', \DateTime::RFC3339);
+        if (!($begin instanceof \DateTime) || !($end instanceof \DateTime)) {
+            //No time range specified.
+            throw new InvalidArgumentException('Invalid parameters!');
+        }
+
+        $calendar_events = CalendarDateAssignment::getEvents(
+            $begin,
+            $end,
+            $owner->id,
+            ['PUBLIC', 'PRIVATE', 'CONFIDENTIAL'],
+            Request::bool('show_declined', false)
+        );
+
+        $result = [];
+
+        foreach ($calendar_events as $date) {
+            $event = $date->toEventData(User::findCurrent()->id);
+            $result[] = $event->toFullcalendarEvent();
+        }
+
+        if ($range === 'user') {
+            //Include course dates of courses that shall be displayed in the calendar:
+            $course_dates = CalendarCourseDate::getEvents($begin, $end, $owner->id);
+            foreach ($course_dates as $course_date) {
+                $event = $course_date->toEventData(User::findCurrent()->id);
+                $event->background_colour = '#ffffff';
+                $event->text_colour = '#000000';
+                $event->border_colour = '#000000';
+                $event->event_classes = [];
+                $result[] = $event->toFullcalendarEvent();
+            }
+            //Include relevant cancelled course dates:
+            $cancelled_course_dates = CalendarCourseExDate::getEvents($begin, $end, $owner->id);
+            foreach ($cancelled_course_dates as $cancelled_course_date) {
+                $event = $cancelled_course_date->toEventData(User::findCurrent()->id);
+                $event->background_colour = '#ffffff';
+                $event->text_colour = '#000000';
+                $event->border_colour = '#000000';
+                $event->event_classes = [];
+                $result[] = $event->toFullcalendarEvent();
             }
+        }
+        //At this point, everything went fine. We can save the beginning as default date
+        //if the current user is looking at their own calendar:
+        if ($owner instanceof User && $owner->id === User::findCurrent()->id) {
+            $_SESSION['calendar_date'] = $begin->format('Y-m-d');
+        }
+        $this->render_json($result);
+    }
 
-            $status = Request::int('status', 1);
-            if ($status > 0 && $status < 6) {
-                $this->event->group_status = $status;
-                $stored = $this->event->store();
+    public function calendar_group_data_action($group_id)
+    {
+        $begin = Request::getDateTime('start', \DateTime::RFC3339);
+        $end = Request::getDateTime('end', \DateTime::RFC3339);
+        $timeline_view = Request::bool('timeline_view', false);
+
+        if (!($begin instanceof \DateTime) || !($end instanceof \DateTime)) {
+            //No time range specified.
+            throw new InvalidArgumentException('Invalid parameters!');
+        }
+
+        $group = null;
+        $users = [];
+        if ($group_id) {
+            //Get the group first:
+            $group = ContactGroup::find($group_id);
+            if ($group->owner_id !== User::findCurrent()->id) {
+                throw new AccessDeniedException();
+            }
+            foreach ($group->items as $item) {
+                if ($item->user->isCalendarReadable()) {
+                    $users[] = $item->user;
+                }
+            }
+            if (!$users) {
+                //No user has granted read access to the calendar for the current user.
+                throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!'));
             }
+        }
 
-            if ($stored !== false) {
-                if ($stored === 0) {
-                    if (Request::isXhr()) {
-                        header('X-Dialog-Close: 1');
-                        exit;
-                    } else {
-                        PageLayout::postMessage(MessageBox::success(_('Der Teilnahmestatus wurde nicht geändert.')));
-                        $this->relocate('calendar/single/' . $this->last_view, ['atime' => $this->atime]);
-                    }
-                } else {
-                    // send message to organizer...
-                    if ($this->event->author_id != $user->id) {
-                        setTempLanguage($this->event->author_id);
-                        $message = new messaging();
-                        $msg_text = sprintf(_('%s hat den Terminvorschlag für "%s" am %s von %s auf %s geändert.'),
-                                get_fullname(), $this->event->getTitle(),
-                                strftime('%c', $this->event->getStart()),
-                                $this->event->toStringGroupStatus($old_status), $this->event->toStringGroupStatus());
-                        if ($status == CalendarEvent::PARTSTAT_DELEGATED) {
-                            $msg_text .= "\n"
-                                    . sprintf(_('Der Termin wird akzeptiert, aber %s nimmt nicht selbst am Termin teil.'),
-                                    get_fullname());
-                        }
-                        $subject = sprintf(_('Terminvorschlag am %s von %s %s'),
-                                strftime('%c', $this->event->getStart()), get_fullname(), $this->event->toStringGroupStatus());
-                        $msg_text .= "\n\n**" . _('Beginn') . ':** ';
-                        if ($this->event->isDayEvent()) {
-                            $msg_text .= strftime('%x ', $this->event->getStart());
-                            $msg_text .= _('ganztägig');
-                        } else {
-                            $msg_text .= strftime('%c', $this->event->getStart());
-                        }
-                        $msg_text .= "\n**" . _('Ende') . ':** ';
-                        if ($this->event->isDayEvent()) {
-                            $msg_text .= strftime('%x ', $this->event->getEnd());
-                        } else {
-                            $msg_text .= strftime('%c', $this->event->getEnd());
-                        }
-                        $msg_text .= "\n**" . _('Zusammenfassung') . ':** ' . $this->event->getTitle() . "\n";
-                        if ($event_data = $this->event->getDescription()) {
-                            $msg_text .= '**' . _('Beschreibung') . ":** $event_data\n";
-                        }
-                        if ($event_data = $this->event->toStringCategories()) {
-                            $msg_text .= '**' . _('Kategorie') . ":** $event_data\n";
-                        }
-                        if ($event_data = $this->event->toStringPriority()) {
-                            $msg_text .= '**' . _('Priorität') . ":** $event_data\n";
-                        }
-                        if ($event_data = $this->event->toStringAccessibility()) {
-                            $msg_text .= '**' . _('Zugriff') . ":** $event_data\n";
-                        }
-                        if ($event_data = $this->event->toStringRecurrence()) {
-                            $msg_text .= '**' . _('Wiederholung') . ":** $event_data\n";
-                        }
-                        $member = [];
-                        foreach ($this->event->attendees as $attendee) {
-                            if ($attendee->range_id == $this->event->getAuthorId()) {
-                                $member[] = $attendee->user->getFullName()
-                                    . ' ('. _('Organisator') . ')';
-                            } else {
-                                $member[] = $attendee->user->getFullName()
-                                        . ' (' . $this->event->toStringGroupStatus($attendee->group_status)
-                                        . ')';
-                            }
-                        }
-                        $msg_text .= '**' . _('Teilnehmende') . ':** ' . implode(', ', $member);
-                        $msg_text .= "\n\n" . _('Hier kommen Sie direkt zum Termin in Ihrem Kalender:') . "\n";
-                        $msg_text .= URLHelper::getURL('dispatch.php/calendar/single/edit/'
-                                . $this->event->getAuthorId() . '/' . $this->event->event_id);
-                        $message->insert_message(
-                                addslashes($msg_text),
-                                [get_username($this->event->getAuthorId())],
-                                $this->event->range_id,
-                                '', '', '', '', addslashes($subject));
-                        restoreLanguage();
+        $result = [];
+
+        foreach ($users as $user) {
+            $events = CalendarDateAssignment::getEvents($begin, $end, $user->id);
+            if ($events) {
+                foreach ($events as $event) {
+                    $data = $event->toEventData(User::findCurrent()->id);
+                    if (!$timeline_view) {
+                        $data->title = $user->getFullName();
                     }
-                    PageLayout::postMessage(MessageBox::success(_('Der Teilnahmestatus wurde gespeichert.')));
-                    $this->relocate('calendar/single/' . $this->last_view, ['atime' => $this->atime]);
+                    $result[] = $data->toFullcalendarEvent();
                 }
             }
         }
-
-        $this->createSidebar('edit', $this->calendar);
-        $this->createSidebarFilter();
+        $this->render_json($result);
     }
 
-    public function switch_action()
+    public function add_courses_action()
     {
-        $default_view = $this->settings['view'] ?: 'week';
-        $view = Request::option('last_view', $default_view);
-        $this->range_id = Request::option('range_id', $GLOBALS['user']->id);
-        $object_type = get_object_type($this->range_id);
-        switch ($object_type) {
-            case 'user':
-                URLHelper::addLinkParam('cid', '');
-                $this->redirect($this->url_for('calendar/single/'
-                        . $view . '/' . $this->range_id));
-                break;
-            case 'sem':
-            case 'inst':
-            case 'fak':
-                URLHelper::addLinkParam('cid', $this->range_id);
-                $this->redirect($this->url_for('calendar/single/'
-                        . $view . '/' . $this->range_id));
-                break;
-            case 'group':
-                URLHelper::addLinkParam('cid', '');
-                $this->redirect($this->url_for('calendar/group/'
-                        . $view . '/' . $this->range_id));
-                break;
+        $selected_semester_pseudo_id = Request::option('semester_id');
+        $this->selected_semesters_id = '';
+        $this->available_semester_data = [];
+        $semesters = Semester::getAll();
+        foreach ($semesters as $semester) {
+            $this->available_semester_data[$semester['id']] = [
+                'id'   => $semester['id'],
+                'name' => $semester['name']
+            ];
         }
-    }
+        $this->available_semester_data = array_reverse($this->available_semester_data);
 
-    public function jump_to_action()
-    {
-        $date = Request::get('jmp_date');
-        if ($date) {
-            $atime = strtotime($date . strftime(' %T', $this->atime));
+        if (!$selected_semester_pseudo_id) {
+            $selected_semester_pseudo_id = User::findCurrent()->getConfiguration()->MY_COURSES_SELECTED_CYCLE;
+            if (!Config::get()->MY_COURSES_ENABLE_ALL_SEMESTERS && $selected_semester_pseudo_id === 'all') {
+                $selected_semester_pseudo_id = 'next';
+            }
+            if (!$selected_semester_pseudo_id) {
+                $selected_semester_pseudo_id = Config::get()->MY_COURSES_DEFAULT_CYCLE;
+            }
+        }
+        if ($selected_semester_pseudo_id === 'next') {
+            $semester = Semester::findNext();
+            $this->selected_semester_id = $semester->id;
+        } elseif (in_array($selected_semester_pseudo_id, ['all', 'current'])) {
+            $semester = Semester::findCurrent();
+            $this->selected_semester_id = $semester->id;
+        } elseif ($selected_semester_pseudo_id === 'last') {
+            $semester = Semester::findPrevious();
+            $this->selected_semester_id = $semester->id;
         } else {
-            $atime = 'now';
+            $this->selected_semester_id = $selected_semester_pseudo_id ?? '';
+            if (!Semester::exists($this->selected_semesters_id)) {
+                $this->selected_semester_id = '';
+            }
+        }
+
+        $this->selected_course_ids = SimpleCollection::createFromArray(
+            CourseMember::findBySQL(
+                'user_id = :user_id AND bind_calendar = 1',
+                ['user_id' => User::findCurrent()->id]
+            )
+        )->pluck('seminar_id');
+
+        $this->semester_data = [];
+        $all_semesters = Semester::getAll();
+        foreach ($all_semesters as $semester) {
+            $data = [
+                'id' => $semester->id,
+                'name' => $semester->name,
+                'courses' => []
+            ];
+            $this->semester_data[] = $data;
+        }
+
+        if (Request::submitted('add')) {
+            CSRFProtection::verifyUnsafeRequest();
+
+            $course_ids = Request::getArray('courses_course_ids');
+            foreach ($course_ids as $course_id => $selected) {
+                $course_membership = CourseMember::findOneBySQL(
+                    'seminar_id = :course_id AND user_id = :user_id',
+                    [
+                        'course_id' => $course_id,
+                        'user_id'   => User::findCurrent()->id
+                    ]
+                );
+                if ($course_membership) {
+                    $course_membership->bind_calendar = $selected ? '1' : '0';
+                    $course_membership->store();
+                }
+            }
+            PageLayout::postSuccess(_('Die Zuordnung von Veranstaltungen zum Kalender wurde aktualisiert.'));
+            $this->redirect('calendar/calendar');
         }
-        $action = Request::option('action', 'week');
-        $this->range_id = $this->range_id ?: $GLOBALS['user']->id;
-        $this->redirect($this->url_for($this->base . $action,
-                ['atime' => $atime, 'range_id' => $this->range_id]));
     }
 
-    public function show_declined_action ()
+    public function export_action()
     {
-        $config = UserConfig::get($GLOBALS['user']->id);
-        $this->settings['show_declined'] = Request::int('show_declined') ? '1' : '0';
-     //   var_dump($this->settings); exit;
-        $config->store('CALENDAR_SETTINGS', $this->settings);
-        $action = Request::option('action', 'week');
-        $this->range_id = $this->range_id ?: $GLOBALS['user']->id;
-        $this->redirect($this->url_for($this->base . $action,
-                ['range_id' => $this->range_id]));
+        PageLayout::setTitle(_('Termine exportieren'));
+        $this->begin = new DateTimeImmutable();
+        $this->end = $this->begin->add(new DateInterval('P1Y'));
+        $this->dates_to_export = 'user';
+        if (Request::submitted('export')) {
+            CSRFProtection::verifyUnsafeRequest();
+            $this->begin = Request::getDateTime('begin', 'd.m.Y');
+            $this->end = Request::getDateTime('end', 'd.m.Y');
+            if ($this->begin >= $this->end) {
+                PageLayout::postError(_('Der Startzeitpunkt darf nicht nach dem Endzeitpunkt liegen!'));
+                return;
+            }
+            $this->dates_to_export = Request::get('dates_to_export');
+            if (!in_array($this->dates_to_export, ['user', 'course', 'all'])) {
+                PageLayout::postError(_('Bitte wählen Sie aus, welche Termine exportiert werden sollen!'));
+                return;
+            }
+            $ical = '';
+            $calendar_export = new ICalendarExport();
+            if ($this->dates_to_export === 'user') {
+                $ical = $calendar_export->exportCalendarDates(User::findCurrent()->id, $this->begin, $this->end);
+            } elseif ($this->dates_to_export === 'course') {
+                $ical = $calendar_export->exportCourseDates(User::findCurrent()->id, $this->begin, $this->end);
+                $ical .= $calendar_export->exportCourseExDates(User::findCurrent()->id, $this->begin, $this->end);
+            } elseif ($this->dates_to_export === 'all') {
+                $ical = $calendar_export->exportCalendarDates(User::findCurrent()->id, $this->begin, $this->end);
+                $ical .= $calendar_export->exportCourseDates(User::findCurrent()->id, $this->begin, $this->end);
+                $ical .= $calendar_export->exportCourseExDates(User::findCurrent()->id, $this->begin, $this->end);
+            }
+            $ical = $calendar_export->writeHeader() . $ical . $calendar_export->writeFooter();
+            $this->response->add_header('Content-Type', 'text/calendar;charset=utf-8');
+            $this->response->add_header('Content-Disposition', 'attachment; filename="studip.ics"');
+            $this->response->add_header('Content-Transfer-Encoding', 'binary');
+            $this->response->add_header('Pragma', 'public');
+            $this->response->add_header('Cache-Control', 'private');
+            $this->response->add_header('Content-Length', strlen($ical));
+            $this->render_text($ical);
+        }
     }
 
-    protected function storeEventData(CalendarEvent $event, SingleCalendar $calendar)
+    public function import_action() {}
+
+    public function import_file_action()
     {
-        $messages = [];
-        if (Request::int('isdayevent')) {
-            $dt_string = Request::get('start_date') . ' 00:00:00';
-        } else {
-            $dt_string = sprintf(
-                '%s %u:%02u',
-                Request::get('start_date'),
-                Request::int('start_hour'),
-                Request::int('start_minute')
-            );
-        }
-        $event->setStart($this->parseDateTime($dt_string));
-        if (Request::int('isdayevent')) {
-            $dt_string = Request::get('end_date') . ' 23:59:59';
-        } else {
-            $dt_string = sprintf(
-                '%s %u:%02u',
-                Request::get('end_date'),
-                Request::int('end_hour'),
-                Request::int('end_minute')
-            );
+        if (Request::submitted('import')) {
+            CSRFProtection::verifySecurityToken();
+            $range_id = Context::getId() ?? User::findCurrent()->id;
+            $calendar_import = new ICalendarImport($range_id);
+            $calendar_import->convertPublicToPrivate(Request::bool('import_as_private_imp'));
+            $calendar_import->import(file_get_contents($_FILES['importfile']['tmp_name']));
+            $import_count = $calendar_import->getCountEvents();
+            PageLayout::postSuccess(sprintf(
+                ngettext(
+                    'Ein Termin wurde importiert.',
+                    'Es wurden %u Termine importiert.',
+                    $import_count
+                ),
+                $import_count
+            ));
+            $this->redirect($this->url_for('calendar/calendar/'));
         }
-        $event->setEnd($this->parseDateTime($dt_string));
+    }
 
-        if (!$this->validate_datetime(sprintf('%02u:%02u',Request::int('start_hour'),Request::int('start_minute')))
-            || !$this->validate_datetime(sprintf('%02u:%02u',Request::int('end_hour'),Request::int('end_minute')))
-            ) {
-            $messages[] = _('Die Start- und/oder Endzeit ist ungültig!');
+    public function share_action()
+    {
+        PageLayout::setTitle(_('Kalender teilen'));
+        if (!Config::get()->CALENDAR_GROUP_ENABLE) {
+            throw new FeatureDisabledException();
         }
 
-        if ($event->getStart() > $event->getEnd()) {
-            $messages[] = _('Die Startzeit muss vor der Endzeit liegen.');
+        $calendar_contacts = Contact::findBySql(
+            "JOIN `auth_user_md5` USING (`user_id`)
+             WHERE `contact`.`owner_id` = :user_id
+               AND `contact`.`calendar_permissions` <> ''
+             ORDER BY `auth_user_md5`.`Vorname`, `auth_user_md5`.`Nachname`",
+            [
+                'user_id' => User::findCurrent()->id
+            ]
+        );
+        $user_data = [];
+        foreach ($calendar_contacts as $contact) {
+            $user_data[$contact->user_id] = [
+                'id' => $contact->user_id,
+                'name' => $contact->friend->getFullName(),
+                'write_permissions' => $contact->calendar_permissions === 'WRITE'
+            ];
         }
+        $this->selected_users_json = json_encode($user_data, JSON_FORCE_OBJECT);
+        $this->searchtype = new StandardSearch('user_id', ['simple_name' => true]);
 
-        $event->setTitle(Request::get('summary', ''));
-        $event->event->description = Request::get('description', '');
-        $event->setUserDefinedCategories(Request::get('categories', ''));
-        $event->event->location        = Request::get('location', '');
-        $event->event->category_intern = Request::int('category_intern', 1);
-        $event->setAccessibility(Request::option('accessibility', 'PRIVATE'));
-        $event->setPriority(Request::int('priority', 0));
+        if (Request::submitted('share')) {
+            CSRFProtection::verifyUnsafeRequest();
+            $selected_user_ids = Request::getArray('calendar_permissions', []);
+            $write_permissions = Request::getArray('calendar_write_permissions', []);
 
-        if (!$event->getTitle()) {
-            $messages[] = _('Es muss eine Zusammenfassung angegeben werden.');
-        }
+            //Add/update contacts with calendar permissions:
 
-        $rec_type = Request::option('recurrence', 'single');
-        $expire = Request::option('exp_c', 'never');
-        $rrule = [
-            'linterval' => null,
-            'sinterval' => null,
-            'wdays' => null,
-            'month' => null,
-            'day' => null,
-            'rtype' => 'SINGLE',
-            'count' => null,
-            'expire' => null
-        ];
-        if ($expire == 'count') {
-            $rrule['count'] = Request::int('exp_count', 10);
-        } else if ($expire == 'date') {
-            if (Request::isXhr()) {
-                $exp_date = Request::get('exp_date');
-            } else {
-                $exp_date = Request::get('exp_date');
-            }
-            $exp_date = $exp_date ?: strftime('%x', time());
-            $rrule['expire'] = $this->parseDateTime($exp_date . ' 12:00');
-        }
-        switch ($rec_type) {
-            case 'daily':
-                if (Request::option('type_daily', 'day') === 'day') {
-                    $rrule['linterval'] = Request::int('linterval_d', 1);
-                    $rrule['rtype'] = 'DAILY';
-                } else {
-                    $rrule['linterval'] = 1;
-                    $rrule['wdays'] = '12345';
-                    $rrule['rtype'] = 'WEEKLY';
+            foreach ($selected_user_ids as $user_id) {
+                $user = User::find($user_id);
+                if (!$user) {
+                    //No user? No contact!
+                    continue;
                 }
-                break;
-            case 'weekly':
-                $rrule['rtype'] = 'WEEKLY';
-                $rrule['linterval'] = Request::int('linterval_w', 1);
-                $rrule['wdays'] = implode('', Request::intArray('wdays',
-                        [strftime('%u', $event->getStart())]));
-                break;
-            case 'monthly':
-                $rrule['rtype'] = 'MONTHLY';
-                if (Request::option('type_m', 'day') === 'day') {
-                    $rrule['linterval'] = Request::int('linterval_m1', 1);
-                    $rrule['day'] = Request::int('day_m',
-                            strftime('%e', $event->getStart()));
-                } else {
-                    $rrule['linterval'] = Request::int('linterval_m2', 1);
-                    $rrule['sinterval'] = Request::int('sinterval_m', 1);
-                    $rrule['wdays'] = Request::int('wday_m',
-                            strftime('%u', $event->getStart()));
+                $contact = Contact::findOneBySql(
+                    'owner_id = :owner_id AND user_id = :user_id',
+                    [
+                        'owner_id' => User::findCurrent()->id,
+                        'user_id' => $user_id
+                    ]
+                );
+                if (!$contact) {
+                    $contact = new Contact();
+                    $contact->owner_id = User::findCurrent()->id;
+                    $contact->user_id = $user->id;
                 }
-                break;
-            case 'yearly':
-                $rrule['rtype'] = 'YEARLY';
-                $rrule['linterval'] = 1;
-                if (Request::option('type_y', 'day') === 'day') {
-                    $rrule['day'] = Request::int('day_y',
-                            strftime('%e', $event->getStart()));
-                    $rrule['month'] = Request::int('month_y1',
-                            date('n', $event->getStart()));
+                if (in_array($user->id, $write_permissions)) {
+                    $contact->calendar_permissions = 'WRITE';
                 } else {
-                    $rrule['sinterval'] = Request::int('sinterval_y', 1);
-                    $rrule['wdays'] = Request::int('wday_y',
-                            strftime('%u', $event->getStart()));
-                    $rrule['month'] = Request::int('month_y2',
-                            date('n', $event->getStart()));
+                    $contact->calendar_permissions = 'READ';
                 }
-                break;
-        }
-        if (sizeof($messages)) {
-            PageLayout::postMessage(MessageBox::error(_('Bitte Eingaben korrigieren'), $messages));
-            return false;
-        } else {
-            $event->setRecurrence($rrule);
-            $exceptions = array_diff(Request::getArray('exc_dates'),
-                    Request::getArray('del_exc_dates'));
-            $event->setExceptions($this->parseExceptions($exceptions));
-            // if this is a group event, store event in the calendars of each attendee
-            if (Config::get()->CALENDAR_GROUP_ENABLE) {
-                $attendee_ids = Request::optionArray('attendees');
-                return $calendar->storeEvent($event, $attendee_ids);
-            } else {
-                return $calendar->storeEvent($event);
+                $contact->store();
             }
-        }
-    }
 
-    /**
-     * Parses a string with exception dates from input form and returns an array
-     * with all dates as unix timestamp identified by an internally used pattern.
-     *
-     * @param string $exc_dates
-     * @return array An array of unix timestamps.
-     */
-    protected function parseExceptions($exc_dates) {
-        $matches = [];
-        $dates = [];
-        preg_match_all('%(\d{1,2})\h*([/.])\h*(\d{1,2})\h*([/.])\h*(\d{4})\s*%',
-                implode(' ', $exc_dates), $matches, PREG_SET_ORDER);
-        foreach ($matches as $match) {
-            if ($match[2] == '/') {
-                $dates[] = strtotime($match[1].'/'.$match[3].'/'.$match[5]);
+            //Revoke calendar permissions for all users that aren't in the list
+            //of selected users:
+            if ($selected_user_ids) {
+                $stmt = DBManager::get()->prepare(
+                    "UPDATE `contact` SET `calendar_permissions` = ''
+                    WHERE `owner_id` = :owner_id
+                    AND `user_id` NOT IN ( :user_ids )"
+                );
+                $stmt->execute([
+                    'owner_id' => User::findCurrent()->id,
+                    'user_ids' => $selected_user_ids
+                ]);
             } else {
-                $dates[] = strtotime($match[1].$match[2].$match[3].$match[4].$match[5]);
+                $stmt = DBManager::get()->prepare(
+                    "UPDATE `contact` SET `calendar_permissions` = ''
+                    WHERE `owner_id` = :owner_id"
+                );
+                $stmt->execute(['owner_id' => User::findCurrent()->id]);
             }
+
+            PageLayout::postSuccess(
+                _('Die Kalenderfreigaben wurden geändert.')
+            );
+            $this->response->add_header('X-Dialog-Close', '1');
         }
-        return $dates;
     }
 
-    /**
-     * Parses a string as date time in the format "j.n.Y H:i:s" and returns the
-     * corresponding unix time stamp.
-     *
-     * @param string $dt_string The date time string.
-     * @return int A unix time stamp
-     */
-    protected function parseDateTime($dt_string)
+    public function publish_action()
     {
-        $dt_array = date_parse_from_format('j.n.Y H:i:s', $dt_string);
-        return mktime($dt_array['hour'], $dt_array['minute'], $dt_array['second'],
-                $dt_array['month'], $dt_array['day'], $dt_array['year']);
-    }
+        $this->short_id = null;
+        if (Request::submitted('delete_id')) {
+            CSRFProtection::verifySecurityToken();
+            IcalExport::deleteKey(User::findCurrent()->id);
+            PageLayout::postSuccess(_('Die Adresse, unter der Ihre Termine abrufbar sind, wurde gelöscht'));
+        }
+
+        if (Request::submitted('new_id')) {
+            CSRFProtection::verifySecurityToken();
+            $this->short_id = IcalExport::setKey(User::findCurrent()->id);
+            PageLayout::postSuccess(_('Eine Adresse, unter der Ihre Termine abrufbar sind, wurde erstellt.'));
+        } else {
+            $this->short_id = IcalExport::getKeyByUser(User::findCurrent()->id);
+        }
 
+        $text = '';
+        if (Request::submitted('submit_email')) {
+            $email_reg_exp = '/^([-.0-9=?A-Z_a-z{|}~])+@([-.0-9=?A-Z_a-z{|}~])+\.[a-zA-Z]{2,6}$/i';
+            if (preg_match($email_reg_exp, Request::get('email')) !== 0) {
+                $subject = '[' .Config::get()->UNI_NAME_CLEAN . ']' . _('Exportadresse für Ihre Termine');
+                $text .= _('Diese Email wurde vom Stud.IP-System verschickt. Sie können auf diese Nachricht nicht antworten.') . "\n\n";
+                $text .= _('Über diese Adresse erreichen Sie den Export für Ihre Termine:') . "\n\n";
+                $text .= $GLOBALS['ABSOLUTE_URI_STUDIP'] . 'dispatch.php/ical/index/'
+                    . IcalExport::getKeyByUser(User::findCurrent()->id);
+                StudipMail::sendMessage(Request::get('email'), $subject, $text);
+                PageLayout::postSuccess(_('Die Adresse wurde verschickt!'));
+            } else {
+                PageLayout::postError(_('Bitte geben Sie eine gültige Email-Adresse an.'));
+            }
+            $this->short_id = IcalExport::getKeyByUser(User::findCurrent()->id);
+        }
+        PageLayout::setTitle(_('Kalender veröffentlichen'));
+    }
 }
diff --git a/app/controllers/calendar/contentbox.php b/app/controllers/calendar/contentbox.php
index 129fb59c38ba87af34dd7923d56bafa9f1014bf9..5d77ed4a21afa3f24f02b89aff1e6a36a6771f2f 100644
--- a/app/controllers/calendar/contentbox.php
+++ b/app/controllers/calendar/contentbox.php
@@ -24,6 +24,7 @@ class Calendar_ContentboxController extends StudipController
         $this->admin = false;
         $this->single = false;
         $this->userRange = false;
+        $this->course_range = false;
         $this->termine = [];
 
         // Fetch time if needed
@@ -36,6 +37,8 @@ class Calendar_ContentboxController extends StudipController
             $range_id = [$range_id];
         }
 
+        $this->titles = [];
+
         foreach ($range_id as $id) {
             switch (get_object_type($id, ['user', 'sem'])) {
                 case 'user':
@@ -44,6 +47,7 @@ class Calendar_ContentboxController extends StudipController
                     break;
                 case 'sem':
                     $this->parseSeminar($id);
+                    $this->course_range = true;
                     break;
             }
         }
@@ -79,93 +83,91 @@ class Calendar_ContentboxController extends StudipController
     private function parseSeminar($id)
     {
         $course = Course::find($id);
-        $dates = $course->getDatesWithExdates()->findBy('end_time', [$this->start, $this->start + $this->timespan], '><');
-        $this->termine = [];
-        foreach ($dates as $courseDate) {
-            // Build info
-            $info = [];
-            if (count($courseDate->dozenten) > 0) {
-                $info[_('Durchführende Lehrende')] = implode(', ', $courseDate->dozenten->getFullname());
-            }
-            if (count($courseDate->statusgruppen) > 0) {
-                $info[_('Beteiligte Gruppen')] = implode(', ', $courseDate->statusgruppen->getValue('name'));
-            }
-
-            // Store for view
-            $description = '';
-            if ($courseDate instanceof CourseExDate) {
-                $description = $courseDate->content;
-            } elseif ($courseDate->cycle instanceof SeminarCycleDate) {
-                $description = $courseDate->cycle->description;
+        $this->termine = $course->getDatesWithExdates()->findBy('end_time', [$this->start, $this->start + $this->timespan], '><');
+        foreach ($this->termine as $course_date) {
+            if ($this->course_range) {
+                //Display only date and time:
+                $this->titles[$course_date->id] = $course_date->getFullname('include-room');
+            } else {
+                //Include the course title:
+                $this->titles[$course_date->id] = $course_date->getFullname('verbose');
             }
-            $this->termine[] = [
-                'id'          => $courseDate->id,
-                'chdate'      => $courseDate->chdate,
-                'title'       => $courseDate->getFullname() . (count($courseDate->topics) > 0 ? ', ' . implode(', ', $courseDate->topics->getValue('title')) : ''),
-                'description' => $description,
-                'topics'      => $courseDate->topics->toArray('title description'),
-                'room'        => $courseDate->getRoomName(),
-                'info'        => $info
-            ];
         }
     }
 
     private function parseUser($id)
     {
-        $restrictions = $GLOBALS['user']->id === $id ? [] : ['CLASS' => 'PUBLIC'];
-        $events = SingleCalendar::getEventList(
-            $id,
-            $this->start,
-            $this->start + $this->timespan,
-            null,
-            $restrictions
-        );
+        $begin = new DateTime();
+        $begin->setTimestamp($this->start);
+        $end = new DateTime();
+        $end->setTimestamp($this->start + $this->timespan);
+
         $this->termine = [];
-        // Prepare termine
-        foreach ($events as $termin) {
-            // Exclude events that begin after the given time range
-            if ($termin->getStart() > $this->start + $this->timespan) {
+
+        if ($GLOBALS['user']->id === $id) {
+            //The current user is looking at their dates.
+            //Get course dates, too:
+            $relevant_courses = Course::findBySQL(
+                "JOIN `seminar_user` USING (`seminar_id`)
+                WHERE `user_id` = :user_id",
+                ['user_id' => $id]
+            );
+            foreach ($relevant_courses as $course) {
+                $course_dates = $course->getDatesWithExdates()->findBy('end_time', [$this->start, $this->start + $this->timespan], '><');
+                foreach ($course_dates as $course_date) {
+                    $this->titles[$course_date->id] = sprintf(
+                        '%1$s: %2$s',
+                        $course_date->course->name,
+                        $course_date->getFullname()
+                    );
+                    $this->termine[] = $course_date;
+                }
+            }
+        }
+
+        //Get personal dates:
+
+        $assignments = [];
+        if (User::findCurrent()->id === $id) {
+            $assignments = CalendarDateAssignment::getEvents($begin, $end, $id);
+        } else {
+            //Only show public events:
+            $assignments = CalendarDateAssignment::getEvents($begin, $end, $id, ['PUBLIC']);
+        }
+        foreach ($assignments as $assignment) {
+            //Exclude events that begin after the given time range:
+            if ($assignment->calendar_date->begin > $this->start + $this->timespan) {
                 continue;
             }
 
+            $title = '';
+
             // Adjust title
-            if (date('Ymd', $termin->getStart()) == date('Ymd')) {
-                $title = _('Heute') . date(', H:i', $termin->getStart());
+            if (date('Ymd', $assignment->calendar_date->begin) == date('Ymd')) {
+                $title = _('Heute') . date(', H:i', $assignment->calendar_date->begin);
             } else {
-                $title = mb_substr(strftime('%a', $termin->getStart()), 0, 2);
-                $title .= date('. d.m.Y, H:i', $termin->getStart());
+                $title = mb_substr(strftime('%a', $assignment->calendar_date->begin), 0, 2);
+                $title .= date('. d.m.Y, H:i', $assignment->calendar_date->begin);
             }
 
-            if ($termin->getStart() < $termin->getEnd()) {
-                if (date('Ymd', $termin->getStart()) < date('Ymd', $termin->getEnd())) {
-                    $title .= ' - ' . mb_substr(strftime('%a', $termin->getEnd()), 0, 2);
-                    $title .= date('. d.m.Y, H:i', $termin->getEnd());
+            if ($assignment->calendar_date->begin < $assignment->calendar_date->end) {
+                if (date('Ymd', $assignment->calendar_date->begin) < date('Ymd', $assignment->calendar_date->end)) {
+                    $title .= ' - ' . mb_substr(strftime('%a', $assignment->calendar_date->end), 0, 2);
+                    $title .= date('. d.m.Y, H:i', $assignment->calendar_date->end);
                 } else {
-                    $title .= ' - ' . date('H:i', $termin->getEnd());
+                    $title .= ' - ' . date('H:i', $assignment->calendar_date->end);
                 }
             }
 
-            if ($termin->getTitle()) {
-                $tmp_titel = mila($termin->getTitle()); //Beschneiden des Titels
-                $title .= ', ' . $tmp_titel;
+            if ($assignment->calendar_date->title) {
+                //Cut the title:
+                $tmp_title = mila($assignment->calendar_date->title);
+                $title .= ', ' . $tmp_title;
             }
+            $this->titles[$assignment->getObjectId()] = $title;
 
             // Store for view
-            $this->termine[] = [
-                'id'          => $termin->id,
-                'type'        => get_class($termin),
-                'range_id'    => $termin->range_id,
-                'event_id'    => $termin->event_id,
-                'chdate'      => $termin->chdate,
-                'title'       => $title,
-                'description' => $termin->getDescription(),
-                'room'        => $termin->getLocation(),
-                'info'        => [
-                    _('Kategorie')    => $termin->toStringCategories(),
-                    _('Priorität')    => $termin->toStringPriority(),
-                    _('Sichtbarkeit') => $termin->toStringAccessibility(),
-                    _('Wiederholung') => $termin->toStringRecurrence()]
-            ];
+            $this->termine[] = $assignment;
         }
     }
 }
diff --git a/app/controllers/calendar/date.php b/app/controllers/calendar/date.php
new file mode 100644
index 0000000000000000000000000000000000000000..32cf99cb2ee738945ddf9c6eecbba1228942746e
--- /dev/null
+++ b/app/controllers/calendar/date.php
@@ -0,0 +1,828 @@
+<?php
+
+class Calendar_DateController extends AuthenticatedController
+{
+    protected function getCategoryOptions()
+    {
+        if (empty($GLOBALS['PERS_TERMIN_KAT'])) {
+            return [];
+        }
+        $options = [];
+        foreach ($GLOBALS['PERS_TERMIN_KAT'] as $key => $data) {
+            $options[$key] = $data['name'];
+        }
+        if (!array_key_exists(255, $options)) {
+            $options[255] = _('Sonstige');
+        }
+        return $options;
+    }
+
+    protected function getCalendarOwner($range_and_id)
+    {
+        $range = '';
+        $range_id = '';
+        $range_and_id = explode('_', $range_and_id ?? []);
+        if (!empty($range_and_id[1])) {
+            $range = $range_and_id[0];
+            $range_id = $range_and_id[1];
+        }
+        if (!$range) {
+            //Show the personal calendar of the current user:
+            $range = 'user';
+            $range_id = $GLOBALS['user']->id;
+        }
+
+        $owner = null;
+        if (!$range_id) {
+            //Assume a user calendar. $range contains the user-ID.
+            $owner = User::getCalendarOwner($range);
+        } else {
+            if ($range === 'user') {
+                $owner = User::getCalendarOwner($range_id);
+            } elseif ($range === 'course') {
+                $owner = Course::getCalendarOwner($range_id);
+            }
+        }
+
+        if (!$owner || !$owner->isCalendarReadable()) {
+            throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!'));
+        }
+        return $owner;
+    }
+
+    /**
+     * A helper method to determine whether the current user may write the date.
+     *
+     * @return Studip\Calendar\Owner[] The owners in which the current user may add a date.
+     */
+    protected function getCalendarOwnersWithWriteAccess(?CalendarDate $date, ?Studip\Calendar\Owner $owner) : array
+    {
+        $result = [];
+        if ($owner instanceof Course) {
+            //For course calendars, only the course can be the owner.
+            $result[$owner->id] = $owner;
+            return $result;
+        }
+        if ($date) {
+            foreach ($date->calendars as $calendar) {
+                if ($calendar->user) {
+                    $result[$calendar->user->id] = $calendar->user;
+                } elseif ($calendar->course) {
+                    $result[$calendar->course->id] = $calendar->course;
+                }
+            }
+        } else {
+            if ($group_id = Request::get('group_id')) {
+                $group = ContactGroup::find($group_id);
+                if ($group) {
+                    foreach ($group->items as $item) {
+                        if ($item->user && $item->user->isCalendarWritable()) {
+                            $result[$item->user_id] = $item->user;
+                        }
+                    }
+                }
+            } elseif ($user_id = Request::get('user_id', $GLOBALS['user']->id)) {
+                $user = User::find($user_id);
+                if ($user && $user->isCalendarWritable()) {
+                    $result[$user->id] = $user;
+                }
+            }
+            if ($other_calendar_ids = Request::getArray('other_calendar_ids')) {
+                foreach ($other_calendar_ids as $other_calendar_id) {
+                    $user = User::find($other_calendar_id);
+                    if ($user && $user->isCalendarWritable()) {
+                        $result[$user->id] = $user;
+                    }
+                }
+            }
+        }
+        return $result;
+    }
+
+    public function index_action($date_id)
+    {
+        $this->date = CalendarDate::find($date_id);
+        if (!$this->date) {
+            PageLayout::postError(_('Der angegebene Termin wurde nicht gefunden.'));
+            return;
+        }
+        if (!$this->date->isVisible($GLOBALS['user']->id)) {
+            throw new AccessDeniedException();
+        }
+        PageLayout::setTitle(
+            sprintf(
+                _('%1$s (am %2$s von %3$s bis %4$s Uhr)'),
+                $this->date->title,
+                date('d.m.Y', $this->date->begin),
+                date('H:i', $this->date->begin),
+                date('H:i', $this->date->end)
+            )
+        );
+        $this->selected_date = '';
+        if ($this->date->repetition_type) {
+            $this->selected_date = Request::get('selected_date');
+        }
+        $this->calendar_assignments = CalendarDateAssignment::findBySql(
+            "INNER JOIN `auth_user_md5`
+            ON `calendar_date_assignments`.`range_id` = `auth_user_md5`.`user_id`
+            WHERE
+            `calendar_date_id` = :calendar_date_id",
+            ['calendar_date_id' => $this->date->id]
+        );
+        $this->participation_message = null;
+        $this->user_participation_status = '';
+        $this->all_assignments_writable = false;
+        $this->is_group_date = count($this->calendar_assignments) > 1;
+
+        if ($this->calendar_assignments) {
+            $writable_assignment_c = 0;
+            $more_than_one_assignment = count($this->calendar_assignments) > 1;
+            //Find the calendar assignment of the user and set the participation message
+            //according to the participation status.
+            foreach ($this->calendar_assignments as $index => $assignment) {
+                if ($assignment->range_id === $GLOBALS['user']->id && $this->is_group_date) {
+                    $this->user_participation_status = $assignment->participation;
+                    if ($assignment->participation === 'ACCEPTED') {
+                        $this->participation_message = MessageBox::info(_('Sie nehmen am Termin teil.'));
+                    } elseif ($assignment->participation === 'DECLINED') {
+                        $this->participation_message = MessageBox::info(_('Sie nehmen nicht am Termin teil.'));
+                    } elseif ($assignment->participation === 'ACKNOWLEDGED') {
+                        $this->participation_message = MessageBox::info(_('Sie haben den Termin zur Kenntnis genommen.'));
+                    } else {
+                        $this->participation_message = MessageBox::info(_('Sie haben keine Angaben zur Teilnahme gemacht.'));
+                    }
+                    if ($more_than_one_assignment) {
+                        $writable_assignment_c++;
+                    } else {
+                        //We don't need the users own assignment in the list of assignments
+                        //when there is only one assignment to the users own calendar.
+                        unset($this->calendar_assignments[$index]);
+
+                    }
+                } else {
+                    if ($assignment->isWritable($GLOBALS['user']->id)) {
+                        $writable_assignment_c++;
+                    }
+                }
+            }
+
+            $this->all_assignments_writable = $writable_assignment_c === count($this->calendar_assignments);
+
+            //Order all calendar assignments by type and name:
+            uasort($this->calendar_assignments, function ($a, $b) {
+                $compare_name = ($a->course instanceof Course && $b->course instanceof Course)
+                    || ($a->user instanceof User && $b->user instanceof User);
+                if ($compare_name) {
+                    $a_name = '';
+                    if ($a->course instanceof Course) {
+                        $a_name = $a->course->getFullname();
+                    } elseif ($a->user instanceof User) {
+                        $a_name = $a->user->getFullName();
+                    }
+                    $b_name = '';
+                    if ($b->course instanceof Course) {
+                        $b_name = $b->course->getFullname();
+                    } elseif ($b->user instanceof User) {
+                        $b_name = $b->user->getFullName();
+                    }
+                    if ($a_name < $b_name) {
+                        return -1;
+                    } elseif ($a_name > $b_name) {
+                        return 1;
+                    } else {
+                        return 0;
+                    }
+                } else {
+                    //Compare types.
+                    $a_is_course = $a->course instanceof Course;
+                    if ($a_is_course) {
+                        return -1;
+                    } else {
+                        //$b is a course:
+                        return 1;
+                    }
+                }
+            });
+        }
+    }
+
+    public function add_action($range_and_id = '')
+    {
+        PageLayout::setTitle(_('Termin anlegen'));
+
+        $owner = $this->getCalendarOwner($range_and_id);
+
+        $this->date = new CalendarDate();
+        if (Request::submitted('begin') && Request::submitted('end')) {
+            $this->date->begin = Request::get('begin');
+            $this->date->end = Request::get('end');
+            $this->date->repetition_end = $this->date->end;
+        } else {
+            $time = new DateTime();
+            $time = $time->add(new DateInterval('PT1H'));
+            $time->setTime(intval($time->format('H')), 0, 0);
+            $this->date->begin = $time->getTimestamp();
+            $time = $time->add(new DateInterval('PT30M'));
+            $this->date->end = $time->getTimestamp();
+            $this->date->repetition_end = $this->date->end;
+        }
+        if ($owner instanceof Course) {
+            $this->form_post_link = $this->link_for('calendar/date/add/course_' . $owner->id);
+        } else {
+            //Personal calendar or group calendar
+            $this->form_post_link = $this->link_for('calendar/date/add');
+        }
+        $this->handleForm('add', $owner);
+    }
+
+    public function edit_action($date_id)
+    {
+        PageLayout::setTitle(_('Termin bearbeiten'));
+
+        $this->date = CalendarDate::find($date_id);
+        if (!$this->date) {
+            throw new Exception(_('Der Termin wurde nicht gefunden!'));
+        }
+        //Set the repetition end date to the end of the date in case it isn't set:
+        if (!$this->date->repetition_end) {
+            $this->date->repetition_end = $this->date->end;
+        }
+
+        $this->form_post_link = $this->link_for('calendar/date/edit/' . $this->date->id);
+        $this->handleForm();
+    }
+
+    protected function handleForm($mode = 'edit', $owner = null)
+    {
+        $this->form_errors = [];
+        $this->calendar_assignment_items = [];
+
+        $this->writable_calendars = $this->getCalendarOwnersWithWriteAccess($mode === 'edit' ? $this->date : null, $owner);
+        if (!$this->writable_calendars) {
+            throw new AccessDeniedException();
+        }
+        $this->user_id  = Request::get('user_id', $owner->id ?? '');
+        $this->group_id = '';
+        if (!$owner) {
+            $this->group_id = Request::get('group_id');
+        }
+        $this->owner_id = $owner ? $owner->id : '';
+
+        $this->category_options = $this->getCategoryOptions();
+        $this->exceptions = [];
+
+        if (!$owner || !($owner instanceof Course)) {
+            $this->user_quick_search_type = null;
+            $this->multi_person_search = null;
+            if (Config::get()->CALENDAR_GROUP_ENABLE) {
+                if (Config::get()->CALENDAR_GRANT_ALL_INSERT) {
+                    $this->user_quick_search_type = new StandardSearch('user_id');
+                } else {
+                    //Only get those users where the current user has
+                    //write access to the calendar.
+                    $this->user_quick_search_type = new SQLSearch(
+                            "SELECT
+                            `auth_user_md5`.`user_id`, "
+                            . $GLOBALS['_fullname_sql']['full'] . " AS fullname
+                            FROM `auth_user_md5`
+                            INNER JOIN `contact`
+                            ON `auth_user_md5`.`user_id` = `contact`.`owner_id`
+                            INNER JOIN `user_info`
+                            ON `user_info`.`user_id` = `auth_user_md5`.`user_id`
+                            WHERE
+                            `auth_user_md5`.`user_id` <> " . DBManager::get()->quote($GLOBALS['user']->id) . "
+                            AND `contact`.`user_id` = " . DBManager::get()->quote($GLOBALS['user']->id) . "
+                            AND `contact`.`calendar_permissions` = 'WRITE'
+                            AND (
+                                `auth_user_md5`.`username` LIKE :input
+                                OR CONCAT(`auth_user_md5`.`Vorname`, ' ', `auth_user_md5`.`Nachname`) LIKE :input
+                                OR CONCAT(`auth_user_md5`.`Nachname`, ' ', `auth_user_md5`.`Vorname`) LIKE :input
+                                OR `auth_user_md5`.`Nachname` LIKE :input
+                                OR " . $GLOBALS['_fullname_sql']['full'] . " LIKE :input
+                            )
+                            GROUP BY `auth_user_md5`.`user_id`
+                            ORDER BY fullname ASC",
+                            _('Person suchen'),
+                            'user_id'
+                        );
+                }
+            }
+        }
+
+        if ($this->date->isNew()) {
+            if (!($owner instanceof Course)) {
+                //Assign the date to the calendar of the current user by default:
+                $user = User::findCurrent();
+                if ($user) {
+                    $this->calendar_assignment_items[] = [
+                        'value'     => $user->id,
+                        'name'      => $user->getFullName(),
+                        'deletable' => true
+                    ];
+                }
+            }
+        } else {
+            $exceptions = CalendarDateException::findBySql(
+                'calendar_date_id = :date_id ORDER BY `date` ASC',
+                ['date_id' => $this->date->id]
+            );
+            foreach ($exceptions as $exception) {
+                $this->exceptions[] = $exception->date;
+            }
+
+            $calendars_assignments = CalendarDateAssignment::findByCalendar_date_id($this->date->id);
+            foreach ($calendars_assignments as $assignment) {
+                $range_avatar = $assignment->getRangeAvatar();
+                $this->calendar_assignment_items[] = [
+                    'value'     => $assignment->range_id,
+                    'name'      => $assignment->getRangeName(),
+                    'deletable' => true
+                ];
+            }
+        }
+
+        $this->all_day_event = false;
+        if ($mode === 'add' && Request::get('all_day') === '1') {
+            $this->all_day_event = true;
+        } else {
+            $begin = new DateTime();
+            $begin->setTimestamp(intval($this->date->begin));
+            $end = new DateTime();
+            $end->setTimestamp(intval($this->date->end));
+            $duration = $end->diff($begin);
+            if ($duration->h === 23 && $duration->i === 59 && $duration->s === 59 && $begin->format('H:i:s') === '00:00:00') {
+                //The event starts at midnight and ends on 23:59:59. It is an all-day event.
+                $this->all_day_event = true;
+            }
+        }
+
+        if (!Request::isPost()) {
+            return;
+        }
+        if (Request::submitted('save')) {
+            CSRFProtection::verifyUnsafeRequest();
+
+            if ($this->date->isNew()) {
+                $this->date->author_id = $GLOBALS['user']->id;
+            }
+            $this->date->editor_id = $GLOBALS['user']->id;
+
+            $begin = Request::getDateTime('begin', 'd.m.Y H:i');
+            $end = Request::getDateTime('end', 'd.m.Y H:i');
+            if (Request::get('all_day') === '1') {
+                $this->all_day_event = true;
+                $begin->setTime(0,0,0);
+                $end = clone $begin;
+                $end->setTime(23,59,59);
+            }
+            $this->date->begin = $begin->getTimestamp();
+            $this->date->end = $end->getTimestamp();
+            if (!$this->date->begin) {
+                $this->form_errors[_('Beginn')] = _('Bitte geben Sie einen Startzeitpunkt ein.');
+            }
+            if (!$this->date->end && !$this->all_day_event) {
+                $this->form_errors[_('Ende')] = _('Bitte geben Sie einen Endzeitpunkt ein.');
+            }
+            if ($this->date->begin && $this->date->end && ($this->date->end < $this->date->begin)) {
+                $this->form_errors[_('Ende')] = _('Der Startzeitpunkt darf nicht nach dem Endzeitpunkt liegen!');
+            }
+
+            $this->date->title = Request::get('title');
+            if (!$this->date->title) {
+                $this->form_errors[_('Titel')] = _('Bitte geben Sie einen Titel ein.');
+            }
+
+            $this->date->access = Request::get('access');
+            if (!in_array($this->date->access, ['PUBLIC', 'CONFIDENTIAL', 'PRIVATE'])) {
+                $this->form_errors[_('Zugriff')] = _('Bitte wählen Sie einen Zugriffstyp aus.');
+            }
+
+            $this->date->description = Request::get('description');
+
+            $this->date->category = Request::get('category');
+            if (!in_array($this->date->category, array_keys($this->category_options))) {
+                $this->form_errors[_('Kategorie')] = _('Bitte wählen Sie eine gültige Kategorie aus.');
+            }
+
+            $this->date->user_category = Request::get('user_category');
+
+            $this->date->location = Request::get('location');
+
+            //Store the repetition information:
+
+            $this->date->clearRepetitionFields();
+            $this->date->repetition_type = Request::get('repetition_type', '');
+            if (!in_array($this->date->repetition_type, ['', 'DAILY', 'WEEKLY', 'WORKDAYS', 'MONTHLY', 'YEARLY'])) {
+                $this->form_errors[_('Wiederholung')] = _('Bitte wählen Sie ein gültiges Wiederholungsintervall aus.');
+            }
+            if ($this->date->repetition_type !== '') {
+                $this->date->interval = '';
+                if (in_array($this->date->repetition_type, ['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'])) {
+                    $this->date->interval = Request::get('repetition_interval');
+                }
+
+                if ($this->date->repetition_type === 'WEEKLY') {
+                    $dow = array_unique(Request::getArray('repetition_dow'));
+                    foreach ($dow as $day) {
+                        if ($day < 1 || $day > 7) {
+                            $this->form_errors[_('Wiederholung an bestimmtem Wochentag')] = _('Bitte wählen Sie einen Wochentag zwischen Montag und Sonntag aus.');
+                        }
+                    }
+                    $this->date->days = implode('', $dow);
+                } elseif ($this->date->repetition_type === 'WORKDAYS') {
+                    //Special case: The "WORKDAYS" repetition type is a shorthand type
+                    //for a weekly repetition from Monday to Friday.
+                    $this->date->repetition_type = 'WEEKLY';
+                    $this->date->days = '12345';
+                    $this->date->interval = '1';
+                } elseif ($this->date->repetition_type === 'MONTHLY') {
+                    $month_type = Request::get('repetition_month_type');
+                    if ($month_type === 'dom') {
+                        $this->date->offset = Request::get('repetition_dom');
+                    } elseif ($month_type === 'dow') {
+                        $this->date->days = Request::get('repetition_dow');
+                        $this->date->offset = Request::get('repetition_dow_week');
+                    }
+                } elseif ($this->date->repetition_type === 'YEARLY') {
+                    $month = Request::get('repetition_month');
+                    if ($month < 1 || $month > 12) {
+                        $this->form_errors[_('Monat')] = _('Bitte wählen Sie einen Monat zwischen Januar und Dezember aus.');
+                    }
+                    $this->date->month = $month;
+                    $month_type = Request::get('repetition_month_type');
+                    if ($month_type === 'dom') {
+                        $this->date->offset = Request::get('repetition_dom');
+                    } elseif ($month_type === 'dow') {
+                        $this->date->days = Request::get('repetition_dow');
+                        $this->date->offset = Request::get('repetition_dow_week');
+                    }
+                }
+
+                $end_type = Request::get('repetition_rep_end_type');
+                if ($end_type === 'end_date') {
+                    $end_date = Request::getDateTime('repetition_rep_end_date', 'd.m.Y');
+                    $end_date->setTime(23,59,59);
+                    $this->date->repetition_end = $end_date->getTimestamp();
+                } elseif ($end_type === 'end_count') {
+                    $this->date->number_of_dates = Request::get('repetition_number_of_dates');
+                } else {
+                    //Repetition never ends:
+                    $this->date->repetition_end = CalendarDate::NEVER_ENDING;
+                }
+            }
+
+            $assigned_calendar_ids = Request::getArray('assigned_calendar_ids');
+            if (!$assigned_calendar_ids || (count($assigned_calendar_ids) === 0)) {
+                $this->form_errors[_('Teilnehmende Personen')] = _('Der Termin ist keinem Kalender zugewiesen!');
+            }
+
+            if ($this->form_errors) {
+                return;
+            }
+
+            $stored = false;
+            if ($this->date->isDirty()) {
+                $stored = $this->date->store();
+            } else {
+                $stored = true;
+            }
+            if (!$stored) {
+                PageLayout::postError(
+                    _('Beim Speichern des Termins ist ein Fehler aufgetreten.')
+                );
+                return;
+            }
+
+            //Assign the calendar date to all writable calendars.
+
+            //Check the assigned calendar-IDs first if they are valid:
+            $valid_assigned_calendar_ids = [];
+            if (($owner instanceof Course)) {
+                //Set the course as calendar:
+                $allowed_calendar_ids = [$owner->id];
+            } else {
+                //Assign the date to the calendars of all the selected users:
+                $allowed_calendar_ids = [$GLOBALS['user']->id];
+                if (Config::get()->CALENDAR_GROUP_ENABLE) {
+                    $allowed_calendar_results = $this->user_quick_search_type->getResults('%%%%');
+                    foreach ($allowed_calendar_results as $result) {
+                        $allowed_calendar_ids[] = $result[0];
+                    }
+                }
+            }
+
+            foreach ($assigned_calendar_ids as $assigned_calendar_id) {
+                if (Course::exists($assigned_calendar_id) || User::exists($assigned_calendar_id)) {
+                    //Valid ID of an existing calendar (range-ID).
+                    if (in_array($assigned_calendar_id, $allowed_calendar_ids)) {
+                        //The calendar is writable.
+                        $valid_assigned_calendar_ids[] = $assigned_calendar_id;
+                    }
+                }
+            }
+            if (count($valid_assigned_calendar_ids) < 1) {
+                PageLayout::postError(
+                    _('Die Zuweisungen des Termins zu Kalendern sind ungültig!')
+                );
+                return;
+            }
+
+            //Remove the date from all user calendars that aren't in the array of writable calendars.
+            CalendarDateAssignment::deleteBySQL(
+                '`range_id` NOT IN ( :owner_ids ) AND `calendar_date_id` = :calendar_date_id',
+                ['owner_ids' => $allowed_calendar_ids, 'calendar_date_id' => $this->date->id]
+            );
+
+            //Now add the date to all selected calendars:
+            foreach($valid_assigned_calendar_ids as $assigned_calendar_id) {
+                $assignment = CalendarDateAssignment::findOneBySql(
+                    'range_id = :assigned_calendar_id AND calendar_date_id = :calendar_date_id',
+                    [
+                        'assigned_calendar_id' => $assigned_calendar_id,
+                        'calendar_date_id' => $this->date->id
+                    ]
+                );
+                if (!$assignment) {
+                    $assignment = new CalendarDateAssignment();
+                    $assignment->range_id = $assigned_calendar_id;
+                    $assignment->calendar_date_id = $this->date->id;
+                    $assignment->store();
+                }
+            }
+
+            //Clear all exceptions for the event and set them again:
+            CalendarDateException::deleteByCalendar_date_id($this->date->id);
+            $new_exceptions = Request::getArray('exceptions');
+            $stored_c = 0;
+            foreach ($new_exceptions as $exception) {
+                $date_parts = explode('-', $exception);
+                if (count($date_parts) === 3) {
+                    //Should be a valid date string.
+                    $e = new CalendarDateException();
+                    $e->calendar_date_id = $this->date->id;
+                    $e->date = $exception;
+                    if ($e->store()) {
+                        $stored_c++;
+                    }
+                }
+            }
+            if ($stored_c === count($new_exceptions)) {
+                PageLayout::postSuccess(_('Der Termin wurde gespeichert.'));
+            } else {
+                PageLayout::postWarning(_('Der Termin wurde gespeichert, aber nicht mit allen Terminausfällen!'));
+            }
+            if (Request::submitted('selected_date')) {
+                $selected_date = Request::getDateTime('selected_date');
+                if ($selected_date) {
+                    //Set the calendar default date to the previously selected date:
+                    $_SESSION['calendar_date'] = $selected_date->format('Y-m-d');
+                }
+            } else {
+                //Set the calendar default date to the beginning of the date:
+                $_SESSION['calendar_date'] = $begin->format('Y-m-d');
+            }
+            $this->response->add_header('X-Dialog-Close', '1');
+        }
+    }
+
+    public function move_action($date_id)
+    {
+        $this->date = CalendarDate::find($date_id);
+        if (!$this->date) {
+            throw new InvalidArgumentException(
+                _('Der angegebene Termin wurde nicht gefunden.')
+            );
+        }
+        if (!$this->date->isWritable($GLOBALS['user']->id)) {
+            throw new AccessDeniedException(
+                _('Sie sind nicht berechtigt, diesen Termin zu ändern.')
+            );
+        }
+
+        $this->begin = Request::getDateTime('begin', \DateTime::RFC3339);
+        $this->end   = Request::getDateTime('end', \DateTime::RFC3339);
+        if (!$this->begin || !$this->end) {
+            throw new InvalidArgumentException();
+        }
+
+        if ($this->date->repetition_type) {
+            PageLayout::setTitle(_('Verschieben eines Termins aus einer Terminserie'));
+            //Show the dialog to decide what shall be done with the repetition.
+            if (Request::submitted('move')) {
+                CSRFProtection::verifyUnsafeRequest();
+                $repetition_handling = Request::get('repetition_handling');
+                $store_old_date = false;
+                if ($repetition_handling === 'create_single_date') {
+                    //Create a new date with the new time range and then
+                    //create an exception for the old date.
+                    $new_date = new CalendarDate();
+                    $new_date->setData($this->date->toArray());
+                    $new_date->id = $new_date->getNewId();
+                    $new_date->unique_id = '';
+                    $new_date->begin = $this->begin->getTimestamp();
+                    $new_date->end = $this->end->getTimestamp();
+                    $new_date->author_id = $GLOBALS['user']->id;
+                    $new_date->editor_id = $GLOBALS['user']->id;
+                    $new_date->clearRepetitionFields();
+                    $new_date->store();
+                    foreach ($this->date->calendars as $calendar) {
+                        $new_date_calendar = new CalendarDateAssignment();
+                        $new_date_calendar->calendar_date_id = $new_date->id;
+                        $new_date_calendar->range_id = $calendar->range_id;
+                        $new_date_calendar->store();
+                    }
+                    $exception = CalendarDateException::findBySQL(
+                        '`calendar_date_id` = :calendar_date_id AND `date` = :date',
+                        [
+                            'calendar_date_id' => $this->date->id,
+                            'date' => $this->begin->format('Y-m-d')
+                        ]
+                    );
+                    if (!$exception) {
+                        $exception = new CalendarDateException();
+                        $exception->calendar_date_id = $this->date->id;
+                        $exception->date = $this->begin->format('Y-m-d');
+                        $exception->store();
+                    }
+                    $this->response->add_header('X-Dialog-Close', '1');
+                    return;
+                } elseif ($repetition_handling === 'change_times') {
+                    //Set the new time for begin and end:
+                    $date_begin = new DateTime();
+                    $date_begin->setTimestamp($this->date->begin);
+                    $date_begin->setTime(
+                        intval($this->begin->format('H')),
+                        intval($this->begin->format('i')),
+                        intval($this->begin->format('s'))
+                    );
+                    $this->date->begin = $date_begin->getTimestamp();
+                    $date_end = new DateTime();
+                    $date_end->setTimestamp($this->date->end);
+                    $date_end->setTime(
+                        intval($this->end->format('H')),
+                        intval($this->end->format('i')),
+                        intval($this->end->format('s'))
+                    );
+                    $this->date->end = $date_end->getTimestamp();
+
+                    //Set the editor-ID:
+                    $this->date->editor_id = $GLOBALS['user']->id;
+
+                    $store_old_date = true;
+                } elseif ($repetition_handling === 'change_all') {
+                    $this->date->begin = $this->begin->getTimestamp();
+                    if ($this->date->repetition_end && intval($this->date->repetition_end) != pow(2,31) - 1) {
+                        //The repetition end date is set to one specific date.
+                        //It must be recalculated from the end date.
+                        $old_end = new DateTime();
+                        $old_end->setTimestamp($this->date->end);
+                        $old_repetition_end = new DateTime();
+                        $old_repetition_end ->setTimestamp($this->date->repetition_end);
+                        $distance = $old_end->diff($old_repetition_end);
+                        $this->date->end = $this->end->getTimestamp();
+                        $new_repetition_end = clone $this->end;
+                        $new_repetition_end = $new_repetition_end->add($distance);
+                        $this->date->repetition_end = $new_repetition_end->getTimestamp();
+                    }
+                    $this->date->end = $this->end->getTimestamp();
+
+                    //Set the editor-ID:
+                    $this->date->editor_id = $GLOBALS['user']->id;
+
+                    $store_old_date = true;
+                } else {
+                    //Invalid choice.
+                    PageLayout::postError(_('Ungültige Auswahl!'));
+                    return;
+                }
+                if ($store_old_date) {
+                    $success = false;
+                    if ($this->date->isDirty()) {
+                        $success = $this->date->store();
+                    } else {
+                        $success = true;
+                    }
+                    if ($success) {
+                        $this->response->add_header('X-Dialog-Close', '1');
+                        $this->render_nothing();
+                    } else {
+                        throw new Exception(_('Der Termin konnte nicht gespeichert werden.'));
+                    }
+                }
+            }
+        } else {
+            //Set the new date and time directly.
+            $this->date->begin = $this->begin->getTimestamp();
+            $this->date->end = $this->end->getTimestamp();
+            //Set the editor-ID:
+            $this->date->editor_id = $GLOBALS['user']->id;
+
+            $success = false;
+            if ($this->date->isDirty()) {
+                $success = $this->date->store();
+            } else {
+                $success = true;
+            }
+            if ($success) {
+                $this->response->add_header('X-Dialog-Close', '1');
+                $this->render_nothing();
+            } else {
+                throw new Exception(_('Der Termin konnte nicht gespeichert werden.'));
+            }
+        }
+    }
+
+    public function delete_action($date_id)
+    {
+        PageLayout::setTitle(_('Termin löschen'));
+        $this->date = CalendarDate::find($date_id);
+        if (!$this->date) {
+            PageLayout::postError(
+                _('Der Termin wurde nicht gefunden!')
+            );
+            $this->render_nothing();
+        }
+        $this->date_has_repetitions = !empty($this->date->repetition_type);
+        $this->selected_date = null;
+        if ($this->date_has_repetitions) {
+            $this->selected_date = Request::getDateTime('selected_date');
+            if (!$this->selected_date) {
+                $this->selected_date = new DateTime();
+                $this->selected_date->setTimestamp($this->date->begin);
+            }
+        }
+        $this->repetition_handling  = Request::get('repetition_handling', 'create_exception');
+        if (Request::submitted('delete')) {
+            $delete_whole_date = false;
+            CSRFProtection::verifyUnsafeRequest();
+            if ($this->date_has_repetitions) {
+                if ($this->repetition_handling === 'create_exception') {
+                    $exception = new CalendarDateException();
+                    $exception->calendar_date_id = $this->date->id;
+                    $exception->date = $this->selected_date->format('Y-m-d');
+                    if ($exception->store()) {
+                        PageLayout::postSuccess(
+                            sprintf(
+                                _('Die Ausnahme am %s wurde der Terminserie hinzugefügt.'),
+                                $this->selected_date->format('d.m.Y')
+                            )
+                        );
+                        $this->response->add_header('X-Dialog-Close', '1');
+                        $this->render_nothing();
+                    } else {
+                        PageLayout::postError(
+                            sprintf(
+                                _('Die Ausnahme am %s konnte der Terminserie nicht hinzugefügt werden.'),
+                                $this->selected_date->format('d.m.Y')
+                            )
+                        );
+                    }
+                } elseif ($this->repetition_handling === 'delete_all') {
+                    $delete_whole_date = true;
+                }
+            } else {
+                $delete_whole_date = true;
+            }
+            if ($delete_whole_date) {
+                if ($this->date->delete()) {
+                    if ($this->date_has_repetitions) {
+                        PageLayout::postSuccess(_('Die Terminserie wurde gelöscht!'));
+                    } else {
+                        PageLayout::postSuccess(_('Der Termin wurde gelöscht!'));
+                    }
+                    $this->response->add_header('X-Dialog-Close', '1');
+                    $this->render_nothing();
+                } else {
+                    if ($this->date_has_repetitions) {
+                        PageLayout::postError(_('Die Terminserie konnte nicht gelöscht werden!'));
+                    } else {
+                        PageLayout::postError(_('Der Termin konnte nicht gelöscht werden!'));
+                    }
+                }
+            }
+        }
+    }
+
+    public function participation_action($date_id)
+    {
+        $this->calendar_assignment = CalendarDateAssignment::find([$GLOBALS['user']->id, $date_id]);
+        if (!$this->calendar_assignment) {
+            throw new AccessDeniedException();
+        }
+        CSRFProtection::verifyUnsafeRequest();
+
+        $participation = Request::get('participation');
+        if (!in_array($participation, ['', 'ACCEPTED', 'DECLINED', 'ACKNOWLEDGED'])) {
+            throw new InvalidArgumentException();
+        }
+
+        $this->calendar_assignment->participation = $participation;
+        if ($this->calendar_assignment->isDirty()) {
+            $this->calendar_assignment->store();
+            $this->calendar_assignment->sendParticipationStatus();
+        }
+        $this->response->add_header('X-Dialog-Close', '1');
+        PageLayout::postSuccess(_('Ihre Teilnahmestatus wurde geändert.'));
+        $this->render_nothing();
+    }
+}
diff --git a/app/controllers/calendar/group.php b/app/controllers/calendar/group.php
deleted file mode 100644
index f440a570cfbf6804049c1b1604ab4e4eb1338f86..0000000000000000000000000000000000000000
--- a/app/controllers/calendar/group.php
+++ /dev/null
@@ -1,336 +0,0 @@
-<?php
-
-require_once 'app/controllers/calendar/calendar.php';
-require_once 'app/controllers/authenticated_controller.php';
-
-class Calendar_GroupController extends Calendar_CalendarController
-{
-    public function before_filter(&$action, &$args)
-    {
-        $this->base = 'calendar/group/';
-        parent::before_filter($action, $args);
-    }
-
-    protected function createSidebar($active = 'week', $calendar = null)
-    {
-        parent::createSidebar($active, $calendar);
-        $sidebar = Sidebar::Get();
-        $actions = new ActionsWidget();
-        $actions->addLink(_('Termin anlegen'),
-                          $this->url_for('calendar/group/edit'),
-                          Icon::create('add'),
-            ['data-dialog' => 'size=auto']);
-        $actions->addLink(_('Kalender freigeben'),
-                $this->url_for('calendar/single/manage_access/' . $GLOBALS['user']->id,
-                               ['group_filter' => $this->range_id]),
-                          Icon::create('community'),
-                          ['id' => 'calendar-open-manageaccess',
-                                'data-dialog' => '', 'data-dialogname' => 'manageaccess']);
-        $sidebar->addWidget($actions);
-    }
-
-    protected function getTitle($group)
-    {
-        $title = sprintf(_('Terminkalender der Gruppe "%s"'), $group->name);
-        return $title;
-    }
-
-    public function index_action()
-    {
-        // switch to the view the user has selected in his personal settings
-        $default_view = $this->settings['view'] ?: 'week';
-        $this->redirect($this->url_for('calendar/group/' . $default_view));
-    }
-
-    public function edit_action($range_id = null, $event_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        // get group and the calendars of the members
-        // the first calendar is the calendar of the actual user
-        $this->calendar = new SingleCalendar($GLOBALS['user']->id);
-        $group = $this->getGroup($this->calendar);
-        $this->attendee_ids = [];
-        if ($group) {
-            $calendar_owners = CalendarUser::getOwners($GLOBALS['user']->id,
-                        Calendar::PERMISSION_WRITABLE)->pluck('owner_id');
-            $members = $group->members->pluck('user_id');
-            $user_id = Request::option('user_id');
-            $this->attendee_ids = array_intersect($calendar_owners, $members);
-            $this->attendee_ids[] = $GLOBALS['user']->id;
-            if ($user_id && in_array($user_id, $this->attendee_ids)) {
-                $this->attendee_ids = [$user_id];
-            }
-        }
-
-        $this->event = $this->calendar->getEvent($event_id);
-
-        if ($this->event->isNew()) {
-            $this->event = $this->calendar->getNewEvent();
-            if (Request::get('isdayevent')) {
-                $this->event->setStart(mktime(0, 0, 0, date('n', $this->atime),
-                        date('j', $this->atime), date('Y', $this->atime)));
-                $this->event->setEnd(mktime(23, 59, 59, date('n', $this->atime),
-                        date('j', $this->atime), date('Y', $this->atime)));
-            } else {
-                $this->event->setStart($this->atime);
-                $this->event->setEnd($this->atime + 3600);
-            }
-            $this->event->setAuthorId($GLOBALS['user']->id);
-            $this->event->setEditorId($GLOBALS['user']->id);
-            $this->event->setAccessibility('PRIVATE');
-            if ($this->attendee_ids) {
-                foreach ($this->attendee_ids as $attendee_id) {
-                    $attendee_event = clone $this->event;
-                    $attendee_event->range_id = $attendee_id;
-                    $this->attendees[] = $attendee_event;
-                }
-            }
-            if (!Request::isXhr()) {
-                PageLayout::setTitle($this->getTitle($this->calendar, _('Neuer Termin')));
-            }
-        } else {
-            // open read only events and course events not as form
-            // show information in dialog instead
-            if (!$this->event->havePermission(Event::PERMISSION_WRITABLE)
-                    || $this->event instanceof CourseEvent) {
-                $this->redirect($this->url_for('calendar/single/event/' . implode('/',
-                        [$this->range_id, $this->event->event_id])));
-                return null;
-            }
-            $this->attendees = $this->event->attendees;
-            if (!Request::isXhr()) {
-                PageLayout::setTitle($this->getTitle($this->calendar, _('Termin bearbeiten')));
-            }
-        }
-
-        if (Config::get()->CALENDAR_GROUP_ENABLE
-                && $this->calendar->getRange() == Calendar::RANGE_USER) {
-            $search_obj = new SQLSearch("SELECT auth_user_md5.user_id, {$GLOBALS['_fullname_sql']['full_rev']} as fullname, username, perms "
-                . "FROM calendar_user "
-                . "LEFT JOIN auth_user_md5 ON calendar_user.owner_id = auth_user_md5.user_id "
-                . "LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id) "
-                . 'WHERE calendar_user.user_id = '
-                . DBManager::get()->quote($GLOBALS['user']->id)
-                . ' AND calendar_user.permission > ' . Event::PERMISSION_READABLE
-                . ' AND (username LIKE :input OR Vorname LIKE :input '
-                . "OR CONCAT(Vorname,' ',Nachname) LIKE :input "
-                . "OR CONCAT(Nachname,' ',Vorname) LIKE :input "
-                . "OR Nachname LIKE :input OR {$GLOBALS['_fullname_sql']['full_rev']} LIKE :input "
-                . ") ORDER BY fullname ASC",
-                _('Nutzer suchen'), 'user_id');
-            $this->quick_search = QuickSearch::get('user_id', $search_obj)
-                    ->fireJSFunctionOnSelect('STUDIP.Messages.add_adressee');
-
-      //      $default_selected_user = array($this->calendar->getRangeId());
-            $this->mps = MultiPersonSearch::get('add_adressees')
-                ->setLinkText(_('Mehrere Teilnehmende hinzufügen'))
-       //         ->setDefaultSelectedUser($default_selected_user)
-                ->setTitle(_('Mehrere Teilnehmende hinzufügen'))
-                ->setExecuteURL($this->url_for($this->base . 'edit'))
-                ->setJSFunctionOnSubmit('STUDIP.Messages.add_adressees')
-                ->setSearchObject($search_obj);
-            $owners = SimpleORMapCollection::createFromArray(
-                    CalendarUser::findByUser_id($this->calendar->getRangeId()))
-                    ->pluck('owner_id');
-            foreach (Calendar::getGroups($GLOBALS['user']->id) as $group) {
-                $this->mps->addQuickfilter(
-                    $group->name,
-                    $group->members->filter(
-                        function ($member) use ($owners) {
-                            if (in_array($member->user_id, $owners)) {
-                                return $member;
-                            }
-                        })->pluck('user_id')
-                );
-            }
-        }
-
-        $stored = false;
-        if (Request::submitted('store')) {
-            $stored = $this->storeEventData($this->event, $this->calendar);
-        }
-
-        if ($stored !== false) {
-            // switch back to group context
-            $this->range_id = $group->getId();
-            if ($stored === 0) {
-                if (Request::isXhr()) {
-                    header('X-Dialog-Close: 1');
-                    exit;
-                } else {
-                    PageLayout::postSuccess(_('Der Termin wurde nicht geändert.'));
-                    $this->relocate('calendar/group/' . $this->last_view, ['atime' => $this->atime]);
-                }
-            } else {
-                PageLayout::postSuccess(_('Der Termin wurde gespeichert.'));
-                $this->relocate('calendar/group/' . $this->last_view, ['atime' => $this->atime]);
-            }
-        } else {
-            $this->createSidebar('edit', $this->calendar);
-            $this->createSidebarFilter();
-            $this->render_template('calendar/single/edit', $this->layout);
-        }
-    }
-
-    public function day_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        // get group and the calendars of the members
-        // the first calendar is the calendar of the actual user
-        $this->calendars[0] = SingleCalendar::getDayCalendar(
-                $GLOBALS['user']->id, $this->atime);
-        $group = $this->getGroup($this->calendars[0]);
-        foreach ($group->members as $member) {
-            $calendar = new SingleCalendar($member->user_id);
-            if ($calendar->havePermission(Calendar::PERMISSION_READABLE)) {
-                $this->calendars[] = SingleCalendar::getDayCalendar($calendar,
-                        $this->atime, null, $this->restrictions);
-            }
-        }
-
-        PageLayout::setTitle($this->getTitle($group)
-                . ' - ' . _('Tagesansicht'));
-        Navigation::activateItem('/calendar/calendar');
-
-        $this->last_view = 'day';
-
-        $this->createSidebar('day');
-        $this->createSidebarFilter();
-    }
-
-    /**
-     * Returns the Statusgruppe for the given calendar.
-     *
-     * @param SingleCalendar The calendar of the group owner.
-     * @return Statusgruppen The found group.
-     * @throws AccessDeniedException If the group does not exists or the owner
-     * of the calendar is not the owner of the group.
-     */
-    private function getGroup($calendar)
-    {
-        $group = Statusgruppen::find($this->range_id);
-        if (!$group) {
-            throw new AccessDeniedException();
-        }
-        // is the user the owner of this group
-        if ($group->range_id != $calendar->getRangeId()) {
-            // not the owner...
-            throw new AccessDeniedException();
-        }
-        return $group;
-    }
-
-    public function week_action($range_id = null)
-    {
-        $this->calendars = [];
-        $this->range_id = $range_id ?: $this->range_id;
-        $timestamp = mktime(12, 0, 0, date('n', $this->atime),
-                date('j', $this->atime), date('Y', $this->atime));
-        $monday = $timestamp - 86400 * (strftime('%u', $timestamp) - 1);
-        $day_count = $this->settings['type_week'] == 'SHORT' ? 5 : 7;
-        // one calendar for each day for the actual user
-        for ($i = 0; $i < $day_count; $i++) {
-            // one calendar holds the events of one day
-            $this->calendars[0][$i] =
-                    SingleCalendar::getDayCalendar($GLOBALS['user']->id,
-                            $monday + $i * 86400, null, $this->restrictions);
-        }
-        // check and get the group
-        $group = $this->getGroup($this->calendars[0][0]);
-        $n = 1;
-        foreach ($group->members as $member) {
-            $calendar = new SingleCalendar($member->user_id);
-            if ($calendar->havePermission(Calendar::PERMISSION_READABLE)) {
-                for ($i = 0; $i < $day_count; $i++) {
-                    $this->calendars[$n][$i] =
-                            SingleCalendar::getDayCalendar($member->user_id,
-                                $monday + $i * 86400, null, $this->restrictions);
-                }
-                $n++;
-            }
-        }
-
-        PageLayout::setTitle($this->getTitle($group)
-                . ' - ' . _('Wochenansicht'));
-        Navigation::activateItem('/calendar/calendar');
-
-        $this->last_view = 'week';
-
-        $this->createSidebar('week');
-        $this->createSidebarFilter();
-    }
-
-    public function month_action($range_id = null)
-    {
-        $this->calendars = [];
-        $this->range_id = $range_id ?: $this->range_id;
-        $month_start = mktime(12, 0, 0, date('n', $this->atime), 1, date('Y', $this->atime));
-        $month_end = mktime(12, 0, 0, date('n', $this->atime), date('t', $this->atime), date('Y', $this->atime));
-        $adow = strftime('%u', $month_start) - 1;
-        $cor = date('n', $this->atime) == 3 ? 1 : 0;
-        $this->first_day = $month_start - $adow * 86400;
-        $this->last_day = ((42 - ($adow + date('t', $this->atime))) % 7 + $cor) * 86400 + $month_end;
-        // one calendar each day for the actual user
-        for ($start_day = $this->first_day; $start_day <= $this->last_day; $start_day += 86400) {
-            $this->calendars[0][] = SingleCalendar::getDayCalendar(
-                    $GLOBALS['user']->id, $start_day, null, $this->restrictions);
-        }
-        // check and get the group
-        $group = $this->getGroup($this->calendars[0][0]);
-        $n = 1;
-        // get the calendars of the group members
-        foreach ($group->members as $member) {
-            $calendar = new SingleCalendar($member->user_id);
-            if ($calendar->havePermission(Calendar::PERMISSION_READABLE)) {
-                for ($start_day = $this->first_day; $start_day <= $this->last_day; $start_day += 86400) {
-                    $this->calendars[$n][] =
-                            SingleCalendar::getDayCalendar($member->user_id,
-                                    $start_day, null, $this->restrictions);
-                }
-                $n++;
-            }
-        }
-        PageLayout::setTitle($this->getTitle($group)
-                . ' - ' . _('Monatssicht'));
-        Navigation::activateItem('/calendar/calendar');
-
-        $this->last_view = 'month';
-
-        $this->createSidebar('month');
-        $this->createSidebarFilter();
-    }
-
-    public function year_action($range_id = null)
-    {
-        $this->calendars = [];
-        $this->count_lists = [];
-
-        $this->range_id = $range_id ?: $this->range_id;
-        $start = mktime(0, 0, 0, 1, 1, date('Y', $this->atime));
-        $end = mktime(23, 59, 59, 12, 31, date('Y', $this->atime));
-        $this->calendars[0] = new SingleCalendar(
-                $GLOBALS['user']->id, $start, $end);
-        $this->count_lists[0] = $this->calendars[0]->getListCountEvents();
-
-        // check and get the group
-        $group = $this->getGroup($this->calendars[0]);
-        $n = 1;
-        // get the calendars of the group members
-        foreach ($group->members as $member) {
-            $calendar = new SingleCalendar($member->user_id);
-            if ($calendar->havePermission(Calendar::PERMISSION_READABLE)) {
-                $this->calendars[$n] = $calendar->setStart($start)->setEnd($end);
-                $this->count_lists[$n] = $this->calendars[$n]->getListCountEvents();
-                $n++;
-            }
-        }
-
-        PageLayout::setTitle($this->getTitle($group)
-                . ' - ' . _('Jahresansicht'));
-        Navigation::activateItem("/calendar/calendar");
-
-        $this->last_view = 'year';
-        $this->createSidebar('year');
-        $this->createSidebarFilter();
-    }
-}
diff --git a/app/controllers/calendar/instschedule.php b/app/controllers/calendar/instschedule.php
deleted file mode 100644
index 452e71db0db701b3ac11cddd4ca05f7542928120..0000000000000000000000000000000000000000
--- a/app/controllers/calendar/instschedule.php
+++ /dev/null
@@ -1,190 +0,0 @@
-<?php
-# Lifter010: TODO
-
-/**
- * This controller displays an institute-calendar for seminars
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Till Glöggler <tgloeggl@uos.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @since      2.0
- */
-class Calendar_InstscheduleController extends AuthenticatedController
-{
-    /**
-     * this action is the main action of the schedule-controller, setting the environment for the timetable,
-     * accepting a comma-separated list of days.
-     *
-     * @param  string $days a list of an arbitrary mix of the numbers 0-6, separated with a comma (e.g. 1,2,3,4,5 (for Monday to Friday, the default))
-     */
-    function index_action($days = false)
-    {
-        if ($GLOBALS['perm']->have_perm('admin')) {
-            $inst_mode = true;
-        }
-        $my_schedule_settings = $GLOBALS['user']->cfg->SCHEDULE_SETTINGS;
-        // set the days to be displayed
-        if ($days === false) {
-            if (Request::getArray('days')) {
-                $this->days = array_keys(Request::getArray('days'));
-            } else {
-                $this->days = CalendarScheduleModel::getDisplayedDays($my_schedule_settings['glb_days']);
-            }
-        } else {
-            $this->days = explode(',', $days);
-        }
-
-        // try to find the correct institute-id
-        $institute_id = Request::option('institute_id', Context::getId());
-
-        if (!$institute_id) {
-            $institute_id = $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT;
-        }
-
-        if (!$institute_id || (in_array(get_object_type($institute_id), words('inst fak')) === false)) {
-            throw new Exception(sprintf(_('Kann Einrichtungskalendar nicht anzeigen!'
-                . 'Es wurde eine ungültige Instituts-Id übergeben (%s)!', $institute_id)));
-        }
-
-        // load semester-data and current semester
-        $this->semesters = Semester::findAllVisible(false);
-
-        if (Request::option('semester_id')) {
-            $this->current_semester = Semester::find(Request::option('semester_id'));
-        } else {
-            $this->current_semester = Semester::findCurrent();
-        }
-
-        $this->entries = (array)CalendarInstscheduleModel::getInstituteEntries($GLOBALS['user']->id,
-            $this->current_semester, 8, 20, $institute_id, $this->days);
-
-        Navigation::activateItem('/course/main/schedule');
-        PageLayout::setHelpKeyword('Basis.TerminkalenderStundenplan');
-        PageLayout::setTitle(Context::getHeaderLine().' - '._('Veranstaltungs-Stundenplan'));
-
-        $zoom = Request::int('zoom', 0);
-        $this->controller = $this;
-        $this->calendar_view = new CalendarWeekView($this->entries, 'instschedule');
-        $this->calendar_view->setHeight(40 + (20 * $zoom));
-        $this->calendar_view->setRange($my_schedule_settings['glb_start_time'], $my_schedule_settings['glb_end_time']);
-        $this->calendar_view->groupEntries();  // if enabled, group entries with same start- and end-date
-
-        URLHelper::addLinkParam('zoom', $zoom);
-        URLHelper::addLinkParam('semester_id', $this->current_semester['semester_id']);
-
-        $style_parameters = [
-            'whole_height' => $this->calendar_view->getOverallHeight(),
-            'entry_height' => $this->calendar_view->getHeight()
-        ];
-
-        $factory = new Flexi_TemplateFactory($this->dispatcher->trails_root . '/views');
-        PageLayout::addStyle($factory->render('calendar/stylesheet', $style_parameters));
-
-        if (Request::option('printview')) {
-            PageLayout::addStylesheet('print.css');
-
-            // remove all stylesheets that are not used for printing to have a more reasonable printing preview
-            PageLayout::addHeadElement('script', [], "$('head link[media=screen]').remove();");
-        } else {
-            PageLayout::addStylesheet('print.css', ['media' => 'print']);
-        }
-
-        Helpbar::Get()->addPlainText(_('Information'), _('Der Stundenplan zeigt die regelmäßigen Veranstaltungen dieser Einrichtung.'), Icon::create('info'));
-
-        $views = new ViewsWidget();
-        $views->addLink(_('klein'), URLHelper::getURL('', ['zoom' => 0]))->setActive($zoom == 0);
-        $views->addLink(_('mittel'), URLHelper::getURL('', ['zoom' => 2]))->setActive($zoom == 2);
-        $views->addLink(_('groß'), URLHelper::getURL('', ['zoom' => 4]))->setActive($zoom == 4);
-        $views->addLink(_('extra groß'), URLHelper::getURL('', ['zoom' => 7]))->setActive($zoom == 7);
-
-        Sidebar::Get()->addWidget($views);
-        $actions = new ActionsWidget();
-        $actions->addLink(_('Druckansicht'),
-            $this->url_for('calendar/instschedule/index/'. implode(',', $this->days),
-                ['printview'    => 'true',
-                 'semester_id'  => $this->current_semester['semester_id']]),
-            Icon::create('print'),
-            ['target' => '_blank']);
-
-        // Only admins should have the ability to change their schedule settings here - they have no other schedule
-        if ($GLOBALS['perm']->have_perm('admin')) {
-            $actions->addLink(_("Darstellung ändern"),
-                $this->url_for('calendar/schedule/settings'),
-                Icon::create('admin'),
-                ['data-dialog' => '']
-            );
-
-            // only show this setting if we have indeed a faculty where children might exist
-            if (Context::get()->isFaculty()) {
-                if ($GLOBALS['user']->cfg->MY_INSTITUTES_INCLUDE_CHILDREN) {
-                    $actions->addLink(_("Untergeordnete Institute ignorieren"),
-                        $this->url_for('calendar/instschedule/include_children/0'),
-                        Icon::create('checkbox-checked')
-                    );
-                } else {
-                    $actions->addLink(_("Untergeordnete Institute einbeziehen"),
-                        $this->url_for('calendar/instschedule/include_children/1'),
-                        Icon::create('checkbox-unchecked')
-                    );
-                }
-            }
-        }
-
-        Sidebar::Get()->addWidget($actions);
-        $semesterSelector = new SemesterSelectorWidget($this->url_for('calendar/instschedule'), 'semester_id', 'post');
-        $semesterSelector->includeAll(false);
-        Sidebar::Get()->addWidget($semesterSelector);
-
-    }
-
-    /**
-     * Returns an HTML fragment of a grouped entry in the schedule of an institute.
-     *
-     * @param string $start the start time of the group, e.g. "1000"
-     * @param string $end   the end time of the group, e.g. "1200"
-     * @param string $seminars  the IDs of the courses
-     * @param string $day  numeric day to show
-     *
-     * @return void
-     */
-    function groupedentry_action($start, $end, $seminars, $day)
-    {
-        $this->response->add_header('Content-Type', 'text/html; charset=utf-8');
-
-        // strucutre of an id: seminar_id-cycle_id
-        // we do not need the cycle id here, so we trash it.
-        $seminar_list = [];
-
-        foreach (explode(',', $seminars) as $seminar) {
-            $zw = explode('-', $seminar);
-            $this->seminars[$zw[0]] = Seminar::getInstance($zw[0]);
-        }
-
-        $this->start = mb_substr($start, 0, 2) .':'. mb_substr($start, 2, 2);
-        $this->end   = mb_substr($end, 0, 2) .':'. mb_substr($end, 2, 2);
-
-        $day_names  = [_("Montag"),_("Dienstag"),_("Mittwoch"),
-            _("Donnerstag"),_("Freitag"),_("Samstag"),_("Sonntag")];
-
-        $this->day   = $day_names[(int)$day];
-
-        $this->render_template('calendar/instschedule/_entry_details');
-    }
-
-    /**
-     * Toggle config setting to include children in schedule for the current faculty
-     *
-     * @param  int $include_childs  0 / false to exclude children 1 / true to include them
-     */
-    function include_children_action($include_childs)
-    {
-        $GLOBALS['user']->cfg->store('MY_INSTITUTES_INCLUDE_CHILDREN', $include_childs ? 1 : 0);
-
-        $this->redirect('calendar/instschedule/index');
-    }
-}
diff --git a/app/controllers/calendar/schedule.php b/app/controllers/calendar/schedule.php
index 6fb51c401e254c61e3ca8a7136352ee8f2e04650..bdd0f51c2b4597c49ce2c7a307dc11f4ecd55df8 100644
--- a/app/controllers/calendar/schedule.php
+++ b/app/controllers/calendar/schedule.php
@@ -144,7 +144,7 @@ class Calendar_ScheduleController extends AuthenticatedController
         ];
 
         $factory = new Flexi_TemplateFactory($this->dispatcher->trails_root . '/views');
-        PageLayout::addStyle($factory->render('calendar/stylesheet', $style_parameters), 'screen, print');
+        PageLayout::addStyle($factory->render('calendar/schedule/stylesheet', $style_parameters), 'screen, print');
 
         if (Request::option('printview')) {
             $this->calendar_view->setReadOnly();
@@ -243,11 +243,7 @@ class Calendar_ScheduleController extends AuthenticatedController
                 $this->render_template('calendar/schedule/_entry_course');
             } else if ($id) {
                 $entry_columns = CalendarScheduleModel::getScheduleEntries($GLOBALS['user']->id, 0, 0, $id);
-                $entries = [];
-                $entry_columns = array_pop($entry_columns);
-                if ($entry_columns) {
-                    $entries = $entry_columns->getEntries();
-                }
+                $entries = array_pop($entry_columns)->getEntries();
                 $this->show_entry = array_pop($entries);
                 $this->render_template('calendar/schedule/_entry_schedule');
             }
diff --git a/app/controllers/calendar/single.php b/app/controllers/calendar/single.php
deleted file mode 100644
index af802bf6d14395c29922fafec91e33724293561a..0000000000000000000000000000000000000000
--- a/app/controllers/calendar/single.php
+++ /dev/null
@@ -1,571 +0,0 @@
-<?php
-/*
- * This is the controller for the single calendar view
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- */
-
-require_once 'app/controllers/calendar/calendar.php';
-
-class Calendar_SingleController extends Calendar_CalendarController
-{
-    public function before_filter(&$action, &$args) {
-        $this->base = 'calendar/single/';
-        parent::before_filter($action, $args);
-    }
-
-    protected function createSidebar($active = null, $calendar = null)
-    {
-        parent::createSidebar($active, $calendar);
-        $sidebar = Sidebar::Get();
-        if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) {
-            $actions = new ActionsWidget();
-            $actions->addLink(
-                _('Termin anlegen'),
-                $this->url_for('calendar/single/edit'),
-                Icon::create('add'),
-                ['data-dialog' => 'size=auto']
-            );
-            if ($calendar->havePermission(Calendar::PERMISSION_OWN)) {
-                if (Config::get()->CALENDAR_GROUP_ENABLE) {
-                    $actions->addLink(
-                        _('Kalender freigeben'),
-                        $this->url_for('calendar/single/manage_access'),
-                        Icon::create('community'),
-                        [
-                            'id' => 'calendar-open-manageaccess',
-                            'data-dialog' => '',
-                            'data-dialogname' => 'manageaccess'
-                        ]
-                    );
-                }
-                $actions->addLink(
-                    _('Veranstaltungstermine'),
-                    $this->url_for('calendar/single/seminar_events'),
-                    Icon::create('seminar'),
-                    ['data-dialog' => 'size=auto']
-                );
-            }
-            $sidebar->addWidget($actions);
-        }
-        if ($calendar->havePermission(Calendar::PERMISSION_OWN)) {
-            $export = new ExportWidget();
-            $export->addLink(_('Termine exportieren'),
-                $this->url_for('calendar/single/export_calendar'),
-                Icon::create('download'),
-                ['data-dialog' => 'size=auto']
-            )->setActive($active == 'export_calendar');
-            $export->addLink(
-                _('Termine importieren'),
-                $this->url_for('calendar/single/import'),
-                Icon::create('upload'),
-                ['data-dialog' => 'size=auto']
-            )->setActive($active == 'import');
-            $export->addLink(
-                _('Kalender teilen'),
-                $this->url_for('calendar/single/share'),
-                Icon::create('group2'),
-                ['data-dialog' => 'size=auto']
-            )->setActive($active == 'share');
-            $sidebar->addWidget($export);
-        }
-    }
-
-    public function day_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = SingleCalendar::getDayCalendar($this->range_id,
-                $this->atime, null, $this->restrictions);
-
-        PageLayout::setTitle($this->getTitle($this->calendar, _('Tagesansicht')));
-
-        $this->last_view = 'day';
-
-        $this->createSidebar('day', $this->calendar);
-        $this->createSidebarFilter();
-    }
-
-    public function week_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $timestamp = mktime(12, 0, 0, date('n', $this->atime), date('j', $this->atime), date('Y', $this->atime));
-        $monday = $timestamp - 86400 * (strftime('%u', $timestamp) - 1);
-        $day_count = $this->settings['type_week'] == 'SHORT' ? 5 : 7;
-
-        $this->calendars = [];
-        for ($i = 0; $i < $day_count; $i++) {
-            $this->calendars[$i] =
-                SingleCalendar::getDayCalendar(
-                    $this->range_id,
-                    $monday + $i * 86400,
-                    null,
-                    $this->restrictions
-                );
-        }
-
-        PageLayout::setTitle($this->getTitle($this->calendars[0],  _('Wochenansicht')));
-
-        $this->last_view = 'week';
-
-        $this->createSidebar('week', $this->calendars[0]);
-        $this->createSidebarFilter();
-    }
-
-    public function month_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $month_start = mktime(12, 0, 0, date('n', $this->atime), 1, date('Y', $this->atime));
-        $month_end = mktime(12, 0, 0, date('n', $this->atime), date('t', $this->atime), date('Y', $this->atime));
-        $adow = strftime('%u', $month_start) - 1;
-        $cor = date('n', $this->atime) == 3 ? 1 : 0;
-        $this->first_day = $month_start - $adow * 86400;
-        $this->last_day = ((42 - ($adow + date('t', $this->atime))) % 7 + $cor) * 86400 + $month_end;
-        $this->calendars = [];
-        for ($start_day = $this->first_day; $start_day <= $this->last_day; $start_day += 86400) {
-            $this->calendars[] = SingleCalendar::getDayCalendar($this->range_id,
-                    $start_day, null, $this->restrictions);
-        }
-
-        PageLayout::setTitle($this->getTitle($this->calendars[0], _('Monatsansicht')));
-
-        $this->last_view = 'month';
-        $this->createSidebar('month', $this->calendars[0]);
-        $this->createSidebarFilter();
-    }
-
-    public function year_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $start = mktime(0, 0, 0, 1, 1, date('Y', $this->atime));
-        $end = mktime(23, 59, 59, 12, 31, date('Y', $this->atime));
-        $this->calendar = new SingleCalendar($this->range_id, $start, $end);
-        $this->count_list = $this->calendar->getListCountEvents(null, null,
-                $this->restrictions);
-
-        PageLayout::setTitle($this->getTitle($this->calendar, _('Jahresansicht')));
-
-        $this->last_view = 'year';
-        $this->createSidebar('year', $this->calendar);
-        $this->createSidebarFilter();
-    }
-
-    public function event_action($range_id = null, $event_id = null)
-    {
-        PageLayout::setTitle(_('Termindaten'));
-
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = new SingleCalendar($this->range_id);
-        $this->event = $this->calendar->getEvent($event_id);
-
-        $this->createSidebar('edit', $this->calendar);
-        $this->createSidebarFilter();
-    }
-
-    public function delete_action($range_id, $event_id)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = new SingleCalendar($this->range_id);
-        if ($this->calendar->deleteEvent($event_id, true)) {
-            PageLayout::postSuccess(_('Der Termin wurde gelöscht.'));
-        }
-        $this->redirect($this->url_for('calendar/single/' . $this->last_view));
-    }
-
-    public function delete_recurrence_action($range_id, $event_id, $atime)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $calendar = new SingleCalendar($this->range_id);
-        $event = $calendar->getEvent($event_id);
-        if ($event->getRecurrence('rtype') != 'SINGLE') {
-            $exceptions = $event->getExceptions();
-            $exceptions[] = $atime;
-            $event->setExceptions($exceptions);
-            if ($event->store() !== false) {
-                PageLayout::postSuccess(strftime(_('Termin am %x aus Serie gelöscht.'), $atime));
-            }
-        }
-        $this->redirect($this->url_for('calendar/single/' . $this->last_view));
-    }
-
-    public function export_event_action($event_id, $range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $calendar = new SingleCalendar($this->range_id);
-        $event = $calendar->getEvent($event_id);
-        if (!$event->isNew()) {
-            $calender_writer = new CalendarWriterICalendar();
-            $export = new CalendarExportFile($calender_writer);
-            $export->exportFromObjects($event);
-            $export->sendFile();
-        }
-        $this->render_nothing();
-    }
-
-    public function export_calendar_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = new SingleCalendar($this->range_id);
-
-        if (Request::submitted('export')) {
-            $calender_writer = new CalendarWriterICalendar();
-            $export = new CalendarExportFile($calender_writer);
-            if (Request::get('event_type') == 'user') {
-                $types = ['CalendarEvent'];
-            } else if (Request::get('event_type') == 'course') {
-                $types = ['CourseEvent', 'CourseCancelledEvent'];
-            } else {
-                $types = ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent'];
-            }
-            if (Request::get('export_time') == 'date') {
-                $exstart = $this->parseDateTime(Request::get('export_start'));
-                $exend = $this->parseDateTime(Request::get('export_end'));
-            } else {
-                $exstart = 0;
-                $exend = Calendar::CALENDAR_END;
-            }
-            $export->exportFromDatabase($this->calendar->getRangeId(), $exstart, $exend, $types);
-            $export->sendFile();
-            $this->render_nothing();
-            exit;
-        }
-
-        PageLayout::setTitle($this->getTitle($this->calendar, _('Termine exportieren')));
-
-        $this->createSidebar('export_calendar', $this->calendar);
-        $this->createSidebarFilter();
-    }
-
-    public function import_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = new SingleCalendar($this->range_id);
-
-        if ($this->calendar->havePermission(Calendar::PERMISSION_OWN)) {
-            if (Request::submitted('import')) {
-                CSRFProtection::verifySecurityToken();
-                $calender_parser = new CalendarParserICalendar();
-                $import = new CalendarImportFile($calender_parser, $_FILES['importfile']);
-                if (Request::get('import_as_private_imp')) {
-                    $import->changePublicToPrivate();
-                }
-                $import->importIntoDatabase($range_id);
-                $import_count = $import->getCount();
-                PageLayout::postMessage(MessageBox::success(
-                        sprintf('Es wurden %s Termine importiert.', $import_count)));
-                $this->redirect($this->url_for('calendar/single/' . $this->last_view));
-            }
-        }
-        PageLayout::setTitle($this->getTitle($this->calendar, _('Termine importieren')));
-        $this->createSidebar('import', $this->calendar);
-        $this->createSidebarFilter();
-    }
-
-    public function share_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = new SingleCalendar($this->range_id);
-
-        $this->short_id = null;
-        if ($this->calendar->havePermission(Calendar::PERMISSION_OWN)) {
-            if (Request::submitted('delete_id')) {
-                CSRFProtection::verifySecurityToken();
-                IcalExport::deleteKey($GLOBALS['user']->id);
-                PageLayout::postSuccess(_('Die Adresse, unter der Ihre Termine abrufbar sind, wurde gelöscht'));
-            }
-
-            if (Request::submitted('new_id')) {
-                CSRFProtection::verifySecurityToken();
-                $this->short_id = IcalExport::setKey($GLOBALS['user']->id);
-                PageLayout::postSuccess(_('Eine Adresse, unter der Ihre Termine abrufbar sind, wurde erstellt.'));
-            } else {
-                $this->short_id = IcalExport::getKeyByUser($GLOBALS['user']->id);
-            }
-
-            $text = "";
-            if (Request::submitted('submit_email')) {
-                $email_reg_exp = '/^([-.0-9=?A-Z_a-z{|}~])+@([-.0-9=?A-Z_a-z{|}~])+\.[a-zA-Z]{2,6}$/i';
-                if (preg_match($email_reg_exp, Request::get('email')) !== 0) {
-                    $subject = '[' .Config::get()->UNI_NAME_CLEAN . ']' . _('Exportadresse für Ihre Termine');
-                    $text .= _('Diese Email wurde vom Stud.IP-System verschickt. Sie können auf diese Nachricht nicht antworten.') . "\n\n";
-                    $text .= _('Über diese Adresse erreichen Sie den Export für Ihre Termine:') . "\n\n";
-                    $text .= $GLOBALS['ABSOLUTE_URI_STUDIP'] . 'dispatch.php/ical/index/'
-                            . IcalExport::getKeyByUser($GLOBALS['user']->id);
-                    StudipMail::sendMessage(Request::get('email'), $subject, $text);
-                    PageLayout::postSuccess(_('Die Adresse wurde verschickt!'));
-                } else {
-                    PageLayout::postError(_('Bitte geben Sie eine gültige Email-Adresse an.'));
-                }
-                $this->short_id = IcalExport::getKeyByUser($GLOBALS['user']->id);
-            }
-        }
-        PageLayout::setTitle($this->getTitle($this->calendar, _('Kalender teilen oder einbetten')));
-
-        $this->createSidebar('share', $this->calendar);
-        $this->createSidebarFilter();
-    }
-
-    public function manage_access_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = new SingleCalendar($this->range_id);
-
-        $all_calendar_users =
-                CalendarUser::getUsers($this->calendar->getRangeId());
-
-        $this->filter_groups = Statusgruppen::findByRange_id(
-                $this->calendar->getRangeId());
-
-        $this->users = [];
-        $this->group_filter_selected = Request::option('group_filter', 'list');
-        if ($this->group_filter_selected != 'list') {
-            $contact_group = Statusgruppen::find($this->group_filter_selected);
-            $calendar_users = [];
-            foreach ($contact_group->members as $member) {
-                $calendar_users[] = new CalendarUser([$this->calendar->getRangeId(), $member->user_id]);
-            }
-            $this->calendar_users = SimpleORMapCollection::createFromArray($calendar_users);
-        } else {
-            $this->group_filter_selected = 'list';
-            $this->calendar_users = $all_calendar_users;
-        }
-
-        $this->own_perms = [];
-        foreach ($this->calendar_users as $calendar_user) {
-            $other_user = CalendarUser::find([$calendar_user->user_id, $this->calendar->getRangeId()]);
-            if ($other_user) {
-                $this->own_perms[$calendar_user->user_id] = $other_user->permission;
-            } else {
-                $this->own_perms[$calendar_user->user_id] = Calendar::PERMISSION_FORBIDDEN;
-            }
-            $this->users[mb_strtoupper(SimpleCollection::translitLatin1($calendar_user->nachname[0]))][] = $calendar_user;
-        }
-
-        ksort($this->users);
-        $this->users = array_map(function ($g) {
-            return SimpleCollection::createFromArray($g)->orderBy('nachname, vorname');
-        }, $this->users);
-
-        $this->mps = MultiPersonSearch::get('calendar-manage_access')
-                ->setTitle(_('Personhinzufügen'))
-                ->setLinkText(_('Person hinzufügen'))
-                ->setDefaultSelectedUser($all_calendar_users->pluck('user_id'))
-                ->setJSFunctionOnSubmit('STUDIP.CalendarDialog.closeMps')
-                ->setExecuteURL($this->url_for('calendar/single/add_users/' . $this->calendar->getRangeId()))
-                ->setSearchObject(new StandardSearch('user_id'));
-
-        PageLayout::setTitle($this->getTitle($this->calendar, _('Kalender freigeben')));
-
-        $this->createSidebar('manage_access', $this->calendar);
-        $this->createSidebarFilter();
-    }
-
-    public function add_users_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = new SingleCalendar($this->range_id);
-        if (Request::isXhr()) {
-            $added_users = Request::optionArray('added_users');
-        } else {
-            $mps = MultiPersonSearch::load('calendar-manage_access');
-            $added_users = $mps->getAddedUsers();
-            $mps->clearSession();
-        }
-
-        $added = 0;
-        foreach ($added_users as $user_id) {
-            $user_to_add = User::find($user_id);
-            if ($user_to_add) {
-                $calendar_user = new CalendarUser(
-                        [$this->calendar->getRangeId(), $user_to_add->id]);
-                if ($calendar_user->isNew()) {
-                    $calendar_user->permission = Calendar::PERMISSION_READABLE;
-                    $added += $calendar_user->store();
-                }
-            }
-        }
-        if ($added) {
-            PageLayout::postSuccess(sprintf(
-                ngettext(
-                    'Eine Person wurde mit der Berechtigung zum Lesen des Kalenders hinzugefügt.',
-                    '%s Personen wurden mit der Berechtigung zum Lesen des Kalenders hinzugefügt.',
-                    $added
-                ),
-                $added
-            ));
-        }
-
-        if (Request::isXhr()) {
-            $this->response->add_header('X-Dialog-Close', 1);
-            $this->response->set_status(200);
-            $this->render_nothing();
-        } else {
-            $this->redirect($this->url_for('calendar/single/manage_access/' . $this->calendar->getRangeId()));
-        }
-    }
-
-    public function remove_user_action($range_id = null, $user_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $user_id = $user_id ?: Request::option('user_id');
-        $this->calendar = new SingleCalendar($this->range_id);
-        $calendar_user = new CalendarUser([$this->calendar->getRangeId(), $user_id]);
-        if (!$calendar_user->isNew()) {
-            $name = $calendar_user->user->getFullname();
-            $calendar_user->delete();
-        }
-        if (Request::isXhr()) {
-            $this->response->set_status(200);
-            $this->render_nothing();
-        } else {
-            PageLayout::postSuccess(sprintf(_('Person %s wurde entfernt.', htmlReady($name))));
-            $this->redirect($this->url_for('calendar/single/manage_access/' . $this->calendar->getRangeId()));
-        }
-    }
-
-    public function store_permissions_action($range_id = null)
-    {
-        $this->range_id = $range_id ?: $this->range_id;
-        $this->calendar = new SingleCalendar($this->range_id);
-
-        $deleted = 0;
-        $read = 0;
-        $write = 0;
-        $submitted_permissions = Request::intArray('perm');
-        foreach ($submitted_permissions as $user_id => $new_perm) {
-            $calendar_user = new CalendarUser([$this->calendar->getRangeId(), $user_id]);
-            if (!$calendar_user->isNew() && $new_perm == 1) {
-                $deleted += $calendar_user->delete();
-                $new_perm = 0;
-            }
-            if ($new_perm >= Calendar::PERMISSION_READABLE
-                    && $calendar_user->permission != $new_perm) {
-                $calendar_user->permission = $new_perm;
-                if ($calendar_user->store()) {
-                    if ($new_perm == Calendar::PERMISSION_READABLE) {
-                        $read++;
-                    } else {
-                        $write++;
-                    }
-                }
-            }
-        }
-        $sum = $deleted + $read + $write;
-        if ($sum) {
-            if ($deleted) {
-                $details[] = sprintf(ngettext('Einer Person wurde die Berechtigungen entzogen.',
-                        '%s Personen wurden die Berechtigungen entzogen.', $deleted), $deleted);
-            }
-            if ($read) {
-                $details[] = sprintf(ngettext('Eine Person wurde auf leseberechtigt gesetzt.',
-                        '%s Personen wurden auf leseberechtigt gesetzt.', $read), $read);
-            }
-            if ($write) {
-                $details[] = sprintf(ngettext('Eine Person wurde auf schreibberechtigt gesetzt.',
-                        '%s Personen wurden auf schreibberechtigt gesetzt.', $write), $write);
-            }
-            PageLayout::postSuccess(sprintf(
-                ngettext('Die Berechtigungen von einer Person wurde geändert.',
-                        'Die Berechtigungen von %s Personen wurden geändert.',
-                $sum), $sum),
-                $details
-            );
-        // no message if the group was changed
-        } else if (!Request::submitted('calendar_group_submit')) {
-            PageLayout::postSuccess(_('Es wurden keine Berechtigungen geändert.'));
-        }
-        $this->redirect($this->url_for(
-            'calendar/single/manage_access/' . $this->calendar->getRangeId(),
-            ['group_filter' => Request::option('group_filter', 'list')])
-        );
-    }
-
-    public function seminar_events_action($order_by = null, $order = 'asc')
-    {
-        $config_sem = $GLOBALS['user']->cfg->MY_COURSES_SELECTED_CYCLE;
-        if (!Config::get()->MY_COURSES_ENABLE_ALL_SEMESTERS && $config_sem == 'all') {
-            $config_sem = 'future';
-        }
-        $this->sem_data = Semester::findAllVisible();
-
-        $sem = $config_sem ?: Config::get()->MY_COURSES_DEFAULT_CYCLE;
-        if (Request::option('sem_select')) {
-            $sem = Request::get('sem_select', $sem);
-        }
-        if (!in_array($sem, words('future all last current')) && isset($sem)) {
-            Request::set('sem_select', $sem);
-        }
-        $this->group_field = 'sem_number';
-        $this->order_by = $order_by;
-        $this->config_sem_number = Config::get()->IMPORTANT_SEMNUMBER;
-        // Needed parameters for selecting courses
-        $params = [
-            'group_field'         => $this->group_field,
-            'order_by'            => $order_by,
-            'order'               => $order,
-            'studygroups_enabled' => false,
-            'deputies_enabled'    => false
-        ];
-
-        $this->sem_courses  = MyRealmModel::getPreparedCourses($sem, $params);
-        $semesters       = new SimpleCollection(Semester::getAll());
-        $this->sem       = $sem;
-        $this->semesters = $semesters->orderBy('beginn desc');
-
-        $this->bind_calendar = SimpleCollection::createFromArray(
-            CourseMember::findBySQL('user_id = ? AND bind_calendar = 1', [$GLOBALS['user']->id])
-        )->pluck('seminar_id');
-
-    }
-
-    public function store_selected_sem_action()
-    {
-        CSRFProtection::verifySecurityToken();
-        if (Request::submitted('store')) {
-            $selected_sems = Request::intArray('selected_sem');
-            $courses = SimpleORMapCollection::createFromArray(
-                    CourseMember::findBySQL('user_id = ? AND Seminar_id IN (?)',
-                    [$GLOBALS['user']->id, array_keys($selected_sems)]));
-            $courses->each(function ($a) use ($selected_sems) {
-                $a->bind_calendar = $selected_sems[$a->seminar_id];
-                $a->store();
-            });
-            PageLayout::postSuccess(_('Die Auswahl der Veranstaltungen wurde gespeichert.'));
-        }
-        $this->redirect($this->url_for('calendar/single/' . $this->last_view));
-    }
-
-    /**
-     * Retrieve the title of the calendar depending on calendar owner (range).
-     *
-     * @param SingleCalendar $calendar The calendar
-     * @param string $title_end Additional text
-     * @return string The complete title for the headline
-     */
-    protected function getTitle(SingleCalendar $calendar, $title_end)
-    {
-        $status = '';
-        if ($calendar->getRangeId() == $GLOBALS['user']->id) {
-            $title = _('Mein persönlicher Terminkalender');
-        } else {
-            if ($calendar->getRange() == Calendar::RANGE_USER) {
-            $title = sprintf(_('Terminkalender von %s'),
-                    $calendar->range_object->getFullname());
-            } else {
-                $title = Context::getHeaderLine();
-            }
-            if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) {
-                $status = ' (' . _('schreibberechtigt') . ')';
-            } else {
-                $status = ' (' . _('leseberechtigt') . ')';
-            }
-        }
-        return $title . ' - ' . $title_end . $status ;
-    }
-}
diff --git a/app/controllers/contact.php b/app/controllers/contact.php
index 0225149222cc7b116c88faa9088a77efeb5ed882..7dd2b0581b3544b92daec411f60508247e80d533 100644
--- a/app/controllers/contact.php
+++ b/app/controllers/contact.php
@@ -20,15 +20,15 @@ class ContactController extends AuthenticatedController
         parent::before_filter($action, $args);
 
         // Load statusgroups
-        $this->groups = SimpleCollection::createFromArray(Statusgruppen::findByRange_id(User::findCurrent()->id));
+        $this->groups = SimpleCollection::createFromArray(ContactGroup::findByOwner_id(User::findCurrent()->id));
 
         // Load requested group
         if (!empty($args[0])) {
-            $this->group = $this->groups->findOneBy('statusgruppe_id', $args[0]);
+            $this->group = $this->groups->findOneBy('id', $args[0]);
 
             //Check for cheaters
-            if ($this->group->range_id != User::findCurrent()->id) {
-                throw new AccessDeniedException;
+            if ($this->group->owner_id !== User::findCurrent()->id) {
+                throw new AccessDeniedException();
             }
         }
 
@@ -43,16 +43,17 @@ class ContactController extends AuthenticatedController
         // Check if we need to add contacts
         $mps      = MultiPersonSearch::load('contacts');
         $imported = 0;
-        foreach ($mps->getAddedUsers() as $userId) {
-            $user_to_add = User::find($userId);
+        foreach ($mps->getAddedUsers() as $user_id) {
+            $user_to_add = User::find($user_id);
             if ($user_to_add) {
                 $new_contact = [
                     'owner_id' => User::findCurrent()->id,
-                    'user_id'  => $user_to_add->id];
+                    'user_id'  => $user_to_add->id,
+                ];
                 if ($filter && $this->group) {
-                    $new_contact['group_assignments'][] = [
-                        'statusgruppe_id' => $this->group->id,
-                        'user_id'         => $user_to_add->id
+                    $new_contact['groups'][] = [
+                        'group_id' => $this->group->id,
+                        'user_id'  => $user_to_add->id,
                     ];
                 }
                 $imported += (bool)Contact::import($new_contact)->store();
@@ -74,7 +75,7 @@ class ContactController extends AuthenticatedController
 
         if ($filter) {
             $selected = $this->group;
-            $contacts = SimpleCollection::createFromArray(User::findMany($selected->members->pluck('user_id')));
+            $contacts = SimpleCollection::createFromArray(User::findMany($selected->items->pluck('user_id')));
         } else {
             $selected = false;
             $contacts = User::findCurrent()->contacts;
@@ -125,7 +126,7 @@ class ContactController extends AuthenticatedController
                     $contact = Contact::find([User::findCurrent()->id, User::findByUsername($contact_username)->id]);
                     if ($contact) {
                         if ($group) {
-                            $contact->group_assignments->unsetBy('statusgruppe_id', $group);
+                            $contact->groups->unsetBy('group_id', $group);
                             if ($contact->store()) {
                                 $removed_group_number++;
                             }
@@ -144,7 +145,7 @@ class ContactController extends AuthenticatedController
             $contact = Contact::find([User::findCurrent()->id, User::findByUsername(Request::username('user'))->id]);
             if ($contact) {
                 if ($group) {
-                    $contact->group_assignments->unsetBy('statusgruppe_id', $group);
+                    $contact->group_assignments->unsetBy('group_id', $group);
                     if ($contact->store()) {
                         PageLayout::postSuccess(_('Der Kontakt wurde aus der Gruppe entfernt.'));
                     }
@@ -161,8 +162,8 @@ class ContactController extends AuthenticatedController
     public function editGroup_action()
     {
         if (!$this->group) {
-            $this->group           = new Statusgruppen();
-            $this->group->range_id = User::findCurrent()->id;
+            $this->group           = new ContactGroup();
+            $this->group->owner_id = User::findCurrent()->id;
         }
         if (Request::submitted('store')) {
             CSRFProtection::verifyRequest();
diff --git a/app/controllers/ical.php b/app/controllers/ical.php
index 4ecdc13755fdef584fbb38a7c5739b2d744347c3..44ff2a1c97b8f1ec31f8c6e27492f1e7ac5bf136 100644
--- a/app/controllers/ical.php
+++ b/app/controllers/ical.php
@@ -51,17 +51,14 @@ class iCalController extends StudipController
             $GLOBALS['user'] = new Seminar_User($user_id);
             $GLOBALS['perm'] = new Seminar_Perm();
 
-            $extype = 'ALL_EVENTS';
-            $calender_writer = new CalendarWriterICalendar();
-            $export = new CalendarExport($calender_writer);
-            $export->exportFromDatabase($user_id, strtotime('-4 week'), 2114377200, 'ALL_EVENTS');
-
-            if ($GLOBALS['_calendar_error']->getMaxStatus(ErrorHandler::ERROR_CRITICAL)) {
-                $this->set_status(500);
-                $this->render_nothing();
-                return;
-            }
-            $content = join($export->getExport());
+            $end = DateTime::createFromFormat('U', '2114377200');
+            $start = new DateTime();
+            $start->modify('-4 week');
+            $ical_export = new ICalendarExport();
+            $ical = $ical_export->exportCalendarDates($user_id, $start, $end)
+                  . $ical_export->exportCourseDates($user_id, $start, $end)
+                  . $ical_export->exportCourseExDates($user_id, $start, $end);
+            $content = $ical_export->writeHeader() . $ical . $ical_export->writeFooter();
             if (mb_stripos($_SERVER['HTTP_USER_AGENT'], 'google-calendar') !== false) {
                 $content = str_replace(['CLASS:PRIVATE','CLASS:CONFIDENTIAL'], 'CLASS:PUBLIC', $content);
             }
diff --git a/app/controllers/institute/overview.php b/app/controllers/institute/overview.php
index f511bd47ef720df04459db73bd202f33943d52c0..66d55e1b42b31663c2d2bd50de6456f147217f40 100644
--- a/app/controllers/institute/overview.php
+++ b/app/controllers/institute/overview.php
@@ -144,10 +144,6 @@ class Institute_OverviewController extends AuthenticatedController
             $response = $this->relay('questionnaire/widget/' . $this->institute_id . '/institute');
             $this->questionnaires = $response->body;
         }
-
-        // Fetch dates
-        $response = $this->relay("calendar/contentbox/display/$this->institute_id/1210000");
-        $this->dates = $response->body;
     }
 
 }
diff --git a/app/controllers/institute/schedule.php b/app/controllers/institute/schedule.php
new file mode 100644
index 0000000000000000000000000000000000000000..1c5d091252b0c3f4e34a60915ab0318154333027
--- /dev/null
+++ b/app/controllers/institute/schedule.php
@@ -0,0 +1,179 @@
+<?php
+class Institute_ScheduleController extends AuthenticatedController
+{
+    public function before_filter(&$action, &$args)
+    {
+        parent::before_filter($action, $args);
+
+        if (Navigation::hasItem('/course/main')) {
+            Navigation::activateItem('/course/main');
+        }
+
+        if (!$GLOBALS['perm']->have_studip_perm('autor', Context::getId())) {
+            throw new AccessDeniedException();
+        }
+    }
+
+    public function index_action($institute_id)
+    {
+        PageLayout::setTitle(_('Veranstaltungs-Stundenplan'));
+
+        if (Navigation::hasItem('/course/main/schedule')) {
+            Navigation::activateItem('/course/main/schedule');
+        }
+
+        $semester = null;
+        if (Request::submitted('semester_id')) {
+            $semester = Semester::find(Request::option('semester_id'));
+        } else {
+            $semester = Semester::findCurrent();
+        }
+
+        $extra_params = [];
+        if ($semester) {
+            $extra_params['semester_id'] = $semester->id;
+        }
+
+        $sidebar = Sidebar::get();
+        $semester_widget = new SemesterSelectorWidget($this->url_for('institute/schedule/index/' . $institute_id));
+        if ($semester) {
+            $semester_widget->setSelection($semester->id);
+        }
+        $sidebar->addWidget($semester_widget);
+
+        $calendar_settings  = $GLOBALS['user']->cfg->CALENDAR_SETTINGS ?? [];
+        $week_slot_duration = \Studip\Calendar\Helper::getCalendarSlotDuration('week');
+
+        $this->fullcalendar = \Studip\Fullcalendar::create(
+            _('Veranstaltungs-Stundenplan'),
+            [
+                'minTime'    => '08:00',
+                'maxTime'    => '20:00',
+                'allDaySlot' => false,
+                'header'     => [
+                    'left'  => '',
+                    'right' => ''
+                ],
+                'views' => [
+                    'timeGridWeek' => [
+                        'columnHeaderFormat' => ['weekday' => 'long'],
+                        'weekends'           => $calendar_settings['type_week'] === 'LONG',
+                        'slotDuration'       => $week_slot_duration
+                    ]
+                ],
+                'defaultView' => 'timeGridWeek',
+                'defaultDate' => date('Y-m-d'),
+                'timeGridEventMinHeight' => 20,
+                'eventSources' => [
+                    [
+                        'url'         => $this->url_for('institute/schedule/data/' . $institute_id),
+                        'method'      => 'GET',
+                        'extraParams' => $extra_params
+                    ]
+                ]
+            ]
+        );
+    }
+
+
+    public function data_action($institute_id)
+    {
+        //Fullcalendar sets the week time range in which to put the course dates
+        //of the semester. Therefore, start and end are handled in here.
+        $begin = Request::getDateTime('start', \DateTime::RFC3339);
+        $end = Request::getDateTime('end', \DateTime::RFC3339);
+        if (!($begin instanceof DateTime) || !($end instanceof DateTime)) {
+            //No time range specified.
+            throw new InvalidArgumentException('Invalid parameters!');
+        }
+
+        $semester_id = Request::option('semester_id');
+        $semester = Semester::find($semester_id);
+        if (!$semester) {
+            $this->render_json([]);
+            return;
+        }
+
+        //Get all regular course dates for that semester:
+        $cycle_dates = SeminarCycleDate::findBySql(
+            'JOIN `termine` USING (`metadate_id`)
+             JOIN `seminare` USING (`seminar_id`)
+             JOIN `seminar_inst` USING (`seminar_id`)
+             WHERE `seminar_inst`.`institut_id` = :institute_id
+               AND (
+                 `termine`.`date` BETWEEN :begin AND :end
+                 OR `termine`.`end_time` BETWEEN :begin AND :end
+               )
+             GROUP BY `metadate_id`',
+            [
+                'institute_id' => $institute_id,
+                'begin'        => $semester->beginn,
+                'end'          => $semester->ende
+            ]
+        );
+
+        if (!$cycle_dates) {
+            $this->render_json([]);
+            return;
+        }
+
+        foreach ($cycle_dates as $cycle_date) {
+            //Calculate a fake begin and end that lies in the week
+            //fullcalendar has specified.
+            $fake_begin = clone $begin;
+            $fake_end = clone $begin;
+            if ($cycle_date->weekday > 1) {
+                $fake_begin = $fake_begin->add(new DateInterval('P' . ($cycle_date->weekday - 1) . 'D'));
+                $fake_end = $fake_end->add(new DateInterval('P' . ($cycle_date->weekday - 1) . 'D'));
+            }
+            $start_time_parts = explode(':', $cycle_date->start_time);
+            $end_time_parts = explode(':', $cycle_date->end_time);
+            $fake_begin->setTime(
+                (int) $start_time_parts[0],
+                (int) $start_time_parts[1],
+                (int) $start_time_parts[2]
+            );
+            $fake_end->setTime(
+                (int) $end_time_parts[0],
+                (int) $end_time_parts[1],
+                (int) $end_time_parts[2]
+            );
+
+            //Get the course colour:
+            $course_membership = CourseMember::findOneBySQL(
+                'seminar_id = :course_id AND user_id = :user_id',
+                [
+                    'course_id' => $cycle_date->seminar_id,
+                    'user_id' => $GLOBALS['user']->id
+                ]
+            );
+            $event_classes = [];
+            if ($course_membership) {
+                $event_classes[] = sprintf('course-color-%u', $course_membership->gruppe);
+            }
+
+            $event = new \Studip\Calendar\EventData(
+                $fake_begin,
+                $fake_end,
+                $cycle_date->course->getFullName(),
+                $event_classes,
+                '',
+                '',
+                false,
+                'SeminarCycleDate',
+                $cycle_date->id,
+                '',
+                '',
+                'course',
+                $cycle_date->seminar_id,
+                [
+                    'show' => $this->url_for('course/details', ['cid' => $cycle_date->seminar_id, 'link_to_course' => '1'])
+                ]
+            );
+
+            $result[] = $event->toFullcalendarEvent();
+        }
+
+        $this->render_json($result);
+    }
+}
diff --git a/app/controllers/settings/calendar.php b/app/controllers/settings/calendar.php
index 5704246cca1ff41528b955a121c12967bcc39110..b2f4f1c55af12a359cd431fd4689e49f09efeac1 100644
--- a/app/controllers/settings/calendar.php
+++ b/app/controllers/settings/calendar.php
@@ -34,7 +34,7 @@ class Settings_CalendarController extends Settings_SettingsController
         parent::before_filter($action, $args);
 
         PageLayout::setHelpKeyword('Basis.MyStudIPTerminkalender');
-        PageLayout::setTitle(_('Einstellungen des Terminkalenders anpassen'));
+        PageLayout::setTitle(_('Einstellungen des Kalenders anpassen'));
         Navigation::activateItem('/profile/settings/calendar_new');
     }
 
@@ -71,6 +71,11 @@ class Settings_CalendarController extends Settings_SettingsController
         ]);
 
         PageLayout::postSuccess(_('Ihre Einstellungen wurden gespeichert'));
-        $this->redirect('settings/calendar');
+        if (Request::isDialog()) {
+            $this->response->add_header('X-Dialog-Close', '1');
+            $this->render_nothing();
+        } else {
+            $this->redirect('settings/calendar');
+        }
     }
 }
diff --git a/app/views/calendar/calendar/add_courses.php b/app/views/calendar/calendar/add_courses.php
new file mode 100644
index 0000000000000000000000000000000000000000..cf3d282ca6498decfea1f89a5bd58a1aea415a21
--- /dev/null
+++ b/app/views/calendar/calendar/add_courses.php
@@ -0,0 +1,15 @@
+<form class="default" method="post" action="<?= $controller->link_for('calendar/calendar/add_courses') ?>">
+    <?= CSRFProtection::tokenTag() ?>
+    <fieldset class="simplevue">
+        <legend><?= _('Veranstaltungen für den Kalender auswählen') ?></legend>
+        <my-courses-coloured-table name="courses"
+                                   :selected_course_ids="<?= htmlReady(json_encode($selected_course_ids)) ?>"
+                                   :default_semester_id="'<?= htmlReady($selected_semester_id) ?>'"
+                                   :semester_data="<?= htmlReady(json_encode($available_semester_data)) ?>"
+        ></my-courses-coloured-table>
+    </fieldset>
+    <div data-dialog-button>
+        <?= \Studip\Button::create(_('Übernehmen'), 'add') ?>
+        <?= \Studip\LinkButton::createCancel(_('Abbrechen'), $controller->url_for('calendar/calendar')) ?>
+    </div>
+</form>
diff --git a/app/views/calendar/calendar/course.php b/app/views/calendar/calendar/course.php
new file mode 100644
index 0000000000000000000000000000000000000000..23602dbb62490dc895f4651d4220c631f8af4c01
--- /dev/null
+++ b/app/views/calendar/calendar/course.php
@@ -0,0 +1 @@
+<?= $fullcalendar ?>
diff --git a/app/views/calendar/calendar/export.php b/app/views/calendar/calendar/export.php
new file mode 100644
index 0000000000000000000000000000000000000000..3fa302eeab003393f1631589c0f3fb0ddfef7b09
--- /dev/null
+++ b/app/views/calendar/calendar/export.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * @var Calendar_CalendarController $controller
+ * @var string $user_id
+ * @var string $dates_to_export
+ * @var DateTimeImmutable $begin
+ * @var DateTimeImmutable $end
+ */
+?>
+<form class="default" method="post"
+      action="<?= $controller->link_for('calendar/calendar/export/' . $user_id) ?>">
+    <?= CSRFProtection::tokenTag() ?>
+    <fieldset>
+        <legend><?= _('Termine exportieren') ?></legend>
+        <label>
+            <?= _('Zu exportierende Termine') ?>
+            <select name="dates_to_export">
+                <option value="user"
+                    <?= $dates_to_export === 'user' ? 'selected' : '' ?>>
+                    <?= _('Persönliche Termine') ?>
+                </option>
+                <option value="course"
+                    <?= $dates_to_export === 'course' ? 'selected' : '' ?>>
+                    <?= _('Veranstaltungstermine') ?>
+                </option>
+                <option value="all"
+                    <?= $dates_to_export === 'all' ? 'selected' : '' ?>>
+                    <?= _('Alle Termine') ?>
+                </option>
+            </select>
+        </label>
+        <label>
+            <?= _('Startdatum') ?>
+            <input type="text" value="<?= htmlReady($begin->format('d.m.Y')) ?>" name="begin" data-date-picker>
+        </label>
+        <label>
+            <?= _('Enddatum') ?>
+            <input type="text" value="<?= htmlReady($end->format('d.m.Y')) ?>" name="end" data-date-picker>
+        </label>
+    </fieldset>
+    <div data-dialog-button>
+        <?= \Studip\Button::create(_('Exportieren'), 'export') ?>
+        <?= \Studip\Button::createCancel(_('Abbrechen')) ?>
+    </div>
+</form>
diff --git a/app/views/calendar/single/import.php b/app/views/calendar/calendar/import.php
similarity index 59%
rename from app/views/calendar/single/import.php
rename to app/views/calendar/calendar/import.php
index db1feb3feccc2ced0e70043bd85cc50f31e4748f..d91025a6078f938f15e1a402509dc7d151a14815 100644
--- a/app/views/calendar/single/import.php
+++ b/app/views/calendar/calendar/import.php
@@ -1,27 +1,30 @@
-<?
-use Studip\Button, Studip\LinkButton;
+<?php
+/**
+ * @var Calendar_CalendarController $controller
+ */
 ?>
-<form action="<?= $controller->link_for('calendar/single/import/' . $calendar->getRangeId(), ['atime' => $atime, 'last_view' => $last_view]) ?>" method="post" enctype="multipart/form-data" class="default">
+<form class="default"
+      method="post"
+      data-dialog="size=auto"
+      enctype="multipart/form-data"
+      action="<?= $controller->link_for('calendar/calendar/import_file/') ?>">
     <input type="hidden" name="studip_ticket" value="<?= get_ticket() ?>">
     <?= CSRFProtection::tokenTag() ?>
     <fieldset>
         <legend>
             <?= sprintf(_('Termine importieren')) ?>
         </legend>
-
         <label for="event-type">
             <input type="checkbox" name="import_privat" value="1" checked>
             <?= _('Öffentliche Termine als "privat" importieren') ?>
         </label>
-
-        <label class="file-upload">
+        <label>
             <span class="required"><?= _('Datei zum Importieren wählen') ?></span>
             <input required type="file" name="importfile" accept=".ics,.ifb,.iCal,.iFBf">
         </label>
     </fieldset>
-
     <footer data-dialog-button>
-        <?= Button::createAccept(_('Termine importieren'), 'import') ?>
-        <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view)) ?>
+        <?= \Studip\Button::create(_('Importieren'), 'import') ?>
+        <?= \Studip\Button::createCancel(_('Abbrechen')) ?>
     </footer>
 </form>
diff --git a/app/views/calendar/calendar/index.php b/app/views/calendar/calendar/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..23602dbb62490dc895f4651d4220c631f8af4c01
--- /dev/null
+++ b/app/views/calendar/calendar/index.php
@@ -0,0 +1 @@
+<?= $fullcalendar ?>
diff --git a/app/views/calendar/single/share.php b/app/views/calendar/calendar/publish.php
similarity index 87%
rename from app/views/calendar/single/share.php
rename to app/views/calendar/calendar/publish.php
index eef5ca3086e2aa66e1fdd40821320fb3abcd5dbc..71901b506ed2f099827aa05c502ade79f211db1b 100644
--- a/app/views/calendar/single/share.php
+++ b/app/views/calendar/calendar/publish.php
@@ -1,10 +1,5 @@
 <? use Studip\Button, Studip\LinkButton; ?>
-<? if (Request::isXhr()) : ?>
-    <? foreach (PageLayout::getMessages() as $messagebox) : ?>
-        <?= $messagebox ?>
-    <? endforeach ?>
-<? endif; ?>
-<form data-dialog="size=auto" action="<?= $controller->url_for('calendar/single/share/' . $calendar->getRangeId()) ?>" method="post" class="default">
+<form data-dialog="size=auto" action="<?= $controller->link_for('calendar/calendar/publish/') ?>" method="post" class="default">
     <input type="hidden" name="studip_ticket" value="<?= get_ticket() ?>">
     <?= CSRFProtection::tokenTag() ?>
 
@@ -54,7 +49,7 @@
         <? endif ?>
 
         <? if (!Request::isXhr()) : ?>
-            <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view)) ?>
+            <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/calendar')) ?>
         <? endif; ?>
     </footer>
 </form>
diff --git a/app/views/calendar/calendar/share.php b/app/views/calendar/calendar/share.php
new file mode 100644
index 0000000000000000000000000000000000000000..69663d260c2318829cced12434b29663bdbe0394
--- /dev/null
+++ b/app/views/calendar/calendar/share.php
@@ -0,0 +1,13 @@
+<form class="default" method="post"
+      action="<?= $controller->link_for('calendar/calendar/share') ?>"
+      data-dialog="reload-on-close">
+    <?= CSRFProtection::tokenTag() ?>
+    <fieldset class="simplevue">
+        <calendar-permissions-table name="calendar"
+                                    :selected_users="<?= htmlReady($selected_users_json ?? '{}') ?>"
+                                    searchtype="<?= htmlReady((string) $searchtype) ?>"></calendar-permissions-table>
+    </fieldset>
+    <div data-dialog-button>
+        <?= \Studip\Button::create(_('Aktualisieren'), 'share') ?>
+    </div>
+</form>
diff --git a/app/views/calendar/contentbox/_termin.php b/app/views/calendar/contentbox/_termin.php
index 3c3fcffc942cfc45c7ca25b7c2f53009be414e2c..ded7d0bc2289be1fca13770262e914f2e8d381df 100644
--- a/app/views/calendar/contentbox/_termin.php
+++ b/app/views/calendar/contentbox/_termin.php
@@ -1,53 +1,88 @@
-<article class="studip toggle <?= ContentBoxHelper::classes($termin['id']) ?>" id="<?= $termin['id'] ?>">
+<article class="studip toggle <?= ContentBoxHelper::classes($termin->getObjectId()) ?>"
+         id="<?= htmlReady($termin->getObjectId()) ?>">
     <header>
         <h1>
-            <a href="<?= ContentBoxHelper::href($termin['id']) ?>">
-                <?= Icon::create('date', 'inactive')->asImg(['class' => 'text-bottom']) ?>
-                <?= htmlReady($termin['title']) ?>
+            <a href="<?= ContentBoxHelper::href($termin->getObjectId()) ?>">
+                <?= Icon::create('date', Icon::ROLE_INACTIVE)->asImg(['class' => 'text-bottom']) ?>
+                <?= htmlReady($titles[$termin->getObjectId()] ?? $termin->getTitle()) ?>
             </a>
         </h1>
         <nav>
             <span>
-                <?= $termin['room'] ? _('Raum') . ': ' . formatLinks($termin['room']) : '' ?>
+                <?= $termin->getLocation() ? _('Raum') . ': ' . formatLinks($termin->getLocation()) : '' ?>
             </span>
-            <? if($admin && $isProfile && $termin['type'] === 'CalendarEvent'): ?>
-            <a href="<?= URLHelper::getLink('dispatch.php/calendar/single/edit/' . $termin['range_id'] . '/' . $termin['event_id'], ['source_page' => 'dispatch.php/profile']) ?>">
-                <?= Icon::create('edit', 'clickable')->asImg(['class' => 'text-bottom']) ?>
-            </a>
-            <? endif; ?>
+            <? if ($admin && $isProfile && $termin->getObjectClass() === 'CalendarDateAssignment') : ?>
+                <a href="<?= URLHelper::getLink('dispatch.php/calendar/calendar') ?>"
+                   title="<?= _('Zum Kalender') ?>">
+                    <?= Icon::create('schedule')->asImg(['class' => 'text-bottom']) ?>
+                </a>
+                <? if ($termin->calendar_date->isWritable($GLOBALS['user']->id)) : ?>
+                    <a href="<?= URLHelper::getLink('dispatch.php/calendar/date/edit/' . $termin->getPrimaryObjectId()) ?>"
+                       title="<?= _('Termin bearbeiten') ?>"
+                       data-dialog>
+                        <?= Icon::create('edit')->asImg(['class' => 'text-bottom']) ?>
+                    </a>
+                <? endif ?>
+            <? elseif (!$course_range && in_array($termin->getObjectClass(), ['CourseDate', 'CourseExDate'])) : ?>
+                <a href="<?= URLHelper::getLink('dispatch.php/course/dates', ['cid' => $termin->getPrimaryObjectId()]) ?>"
+                   title="<?= _('Zur Veranstaltung') ?>">
+                    <?= Icon::create('seminar')->asImg(['class'=> 'text-bottom']) ?>
+                </a>
+            <? endif ?>
         </nav>
     </header>
     <div>
-        <? $themen = $termin['topics'] ?? [] ?>
-        <? if ($termin['description'] || count($themen)) : ?>
-        <p><?= formatReady($termin['description']) ?></p>
-        <? if (count($themen)) : ?>
-            <? foreach ($themen as $thema) : ?>
-                <h3>
-                    <?= Icon::create('topic', Icon::ROLE_INFO)->asImg(20, ['class' => "text-bottom"]) ?>
-                    <?= htmlReady($thema['title']) ?>
-                </h3>
-                <div>
-                    <?= formatReady($thema['description']) ?>
-                </div>
-            <? endforeach ?>
-        <? endif ?>
+        <?
+        $themen = [];
+        if ($termin instanceof CourseDate) {
+            $themen = $termin->topics->toArray('title description');
+        }
+        $description = '';
+        if ($termin instanceof CourseExDate) {
+            $description = $termin->content;
+        } elseif ($termin instanceof CourseDate && isset($termin->cycle)) {
+            $description = $termin->cycle->description;
+        } else {
+            $description = $termin->getDescription();
+        }
+        ?>
+        <? if ($description || count($themen) > 0) : ?>
+            <p><?= formatReady($description) ?></p>
+            <? if (count($themen)) : ?>
+                <? foreach ($themen as $thema) : ?>
+                    <h3>
+                        <?= Icon::create('topic', Icon::ROLE_INFO)->asImg(20, ['class' => "text-bottom"]) ?>
+                        <?= htmlReady($thema['title']) ?>
+                    </h3>
+                    <div>
+                        <?= formatReady($thema['description']) ?>
+                    </div>
+                <? endforeach ?>
+            <? endif ?>
         <? else : ?>
             <?= _('Keine Beschreibung vorhanden') ?>
         <? endif ?>
         <ul class="list-csv" style="text-align: center;">
-        <? foreach($termin['info'] as $type => $info): ?>
-            <? if (trim($info)) : ?>
-                <li>
-                    <small>
-                    <? if (!is_numeric($type)): ?>
-                        <em><?= htmlReady($type) ?>:</em>
-                    <? endif; ?>
-                        <?= htmlReady(trim($info)) ?>
-                    </small>
-                </li>
-            <? endif ?>
-        <? endforeach; ?>
+            <? foreach ($termin->getAdditionalDescriptions() as $type => $info) : ?>
+                <? if (trim($info)) : ?>
+                    <li>
+                        <small>
+                            <? if (!is_numeric($type)): ?>
+                                <em><?= htmlReady($type) ?>:</em>
+                            <? endif; ?>
+                            <?= htmlReady(trim($info)) ?>
+                        </small>
+                    </li>
+                <? endif ?>
+            <? endforeach ?>
         </ul>
+        <? if (!$course_range && in_array($termin->getObjectClass(), [CourseDate::class, CourseExDate::class])) : ?>
+            <div>
+                <a href="<?= URLHelper::getLink('dispatch.php/course/dates', ['cid' => $termin->getPrimaryObjectId()]) ?>">
+                    <?= Icon::create('link-intern')->asImg(['class'=> 'text-bottom']) ?>
+                    <?= _('Zur Veranstaltung') ?>
+                </a>
+            </div>
+        <? endif ?>
     </div>
 </article>
diff --git a/app/views/calendar/contentbox/display.php b/app/views/calendar/contentbox/display.php
index 41498f4e19cce6f8e3762895d6cc8783c2f52e91..c0387fd98194edacc0c48dd69fa6aeb6ef916125 100644
--- a/app/views/calendar/contentbox/display.php
+++ b/app/views/calendar/contentbox/display.php
@@ -1,36 +1,35 @@
-<? if ($admin || $termine): ?>
-<article class="studip">
-    <header>
-        <h1>
-            <?= Icon::create('schedule', 'info')->asImg() ?>
-            <?= htmlReady($title) ?>
-        </h1>
-        <nav>
-    <? if ($admin): ?>
-        <? if ($isProfile): ?>
-        <a href="<?= URLHelper::getLink('dispatch.php/calendar/single/edit/', ['source_page' => 'dispatch.php/profile']) ?>">
-            <?= Icon::create('add', 'clickable')->asImg(['class' => 'text-bottom']) ?>
-        </a>
+<? if ($admin || $termine) : ?>
+    <article class="studip">
+        <header>
+            <h1>
+                <?= Icon::create('schedule', 'info')->asImg() ?>
+                <?= htmlReady($title) ?>
+            </h1>
+            <nav>
+                <? if ($admin) : ?>
+                    <? if ($isProfile) : ?>
+                        <a href="<?= URLHelper::getLink('dispatch.php/calendar/date/add') ?>"
+                           data-dialog="reload-on-close"
+                           title="<?= _('Neuen Termin anlegen') ?>">
+                            <?= Icon::create('add', 'clickable')->asImg(['class' => 'text-bottom']) ?>
+                        </a>
+                    <? else: ?>
+                        <a href="<?= URLHelper::getLink("dispatch.php/course/timesrooms", ['cid' => $range_id]) ?>"
+                           title="<?= _('Neuen Termin anlegen') ?>">
+                            <?= Icon::create('admin', 'clickable')->asImg(['class' => 'text-bottom']) ?>
+                        </a>
+                    <? endif ?>
+                <? endif ?>
+            </nav>
+        </header>
+        <? if ($termine) : ?>
+            <? foreach ($termine as $termin) : ?>
+                <?= $this->render_partial('calendar/contentbox/_termin.php', ['termin' => $termin, 'course_range' => $course_range]) ?>
+            <? endforeach ?>
         <? else: ?>
-        <a href="<?= URLHelper::getLink("dispatch.php/course/timesrooms", ['cid' => $range_id]) ?>">
-            <?= Icon::create('admin', 'clickable')->asImg(['class' => 'text-bottom']) ?>
-        </a>
-        <? endif; ?>
-    <? endif; ?>
-        </nav>
-    </header>
-  <? if ($termine): ?>
-    <? foreach ($termine as $termin): ?>
-        <?= $this->render_partial('calendar/contentbox/_termin.php', ['termin' => $termin]); ?>
-    <? endforeach; ?>
-<? else: ?>
-    <section>
-    <? if ($isProfile): ?>
-        <?= _('Es sind keine aktuellen Termine vorhanden. Um neue Termine zu erstellen, klicken Sie rechts auf das Plus.') ?>
-    <? else: ?>
-        <?= _('Es sind keine aktuellen Termine vorhanden. Um neue Termine zu erstellen, klicken Sie rechts auf die Zahnräder.') ?>
-    <? endif; ?>
-    </section>
-  <? endif; ?>
-</article>
-<? endif; ?>
+            <section>
+                <?= _('Es sind keine aktuellen Termine vorhanden. Zum Anlegen neuer Termine können Sie die Aktion „Neuen Termin anlegen“ benutzen.') ?>
+            </section>
+        <? endif ?>
+    </article>
+<? endif ?>
diff --git a/app/views/calendar/date/_add_edit_form.php b/app/views/calendar/date/_add_edit_form.php
new file mode 100644
index 0000000000000000000000000000000000000000..7c922e41f6191629e1811a58fe51cf5f13f77b7f
--- /dev/null
+++ b/app/views/calendar/date/_add_edit_form.php
@@ -0,0 +1,154 @@
+<form class="default new-calendar-date-form" method="post" action="<?= $form_post_link ?>"
+      data-dialog="reload-on-close">
+    <?= CSRFProtection::tokenTag() ?>
+
+    <? if ($return_path = Request::get('return_path')) : ?>
+        <input type="hidden" name="return_path" value="<?= htmlReady($return_path) ?>">
+    <? endif ?>
+
+    <? if ($user_id) : ?>
+        <input type="hidden" name="user_id" value="<?= htmlReady($user_id) ?>">
+    <? endif ?>
+    <? if ($group_id) : ?>
+        <input type="hidden" name="group_id" value="<?= htmlReady($group_id) ?>">
+    <? endif ?>
+
+    <article aria-live="assertive"
+             class="validation_notes studip">
+        <header>
+            <h1>
+                <?= Icon::create('info-circle', Icon::ROLE_INFO)->asImg(['class' => 'text-bottom validation_notes_icon']) ?>
+                <?= _('Hinweise zum Ausfüllen des Formulars') ?>
+            </h1>
+        </header>
+        <div class="required_note">
+            <div aria-hidden="true">
+                <?= _('Pflichtfelder sind mit Sternchen gekennzeichnet.') ?>
+            </div>
+            <div class="sr-only">
+                <?= _('Dieses Formular enthält Pflichtfelder.') ?>
+            </div>
+        </div>
+        <? if ($form_errors) : ?>
+            <div>
+                <?= _('Folgende Angaben müssen korrigiert werden, um das Formular abschicken zu können:') ?>
+                <ul>
+                    <? foreach ($form_errors as $field => $error) : ?>
+                        <li><?= htmlReady($field) ?>: <?= htmlReady($error) ?></li>
+                    <? endforeach ?>
+                </ul>
+            </div>
+        <? endif ?>
+    </article>
+
+    <fieldset>
+        <legend><?= _('Grunddaten') ?></legend>
+        <label class="studiprequired">
+            <?= _('Titel') ?>
+            <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+            <input type="text" name="title" required="required"
+                   value="<?= htmlReady($date->title) ?>">
+        </label>
+        <div class="hgroup">
+            <label class="studiprequired">
+                <?= _('Beginn') ?>
+                <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+                <input type="text" name="begin" class="begin-input" data-datetime-picker
+                       required="required" value="<?= date('d.m.Y H:i', $date->begin) ?>">
+            </label>
+            <label class="studiprequired">
+                <?= _('Ende') ?>
+                <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+                <input type="text" name="end" class="end-input" data-datetime-picker
+                       required="required" value="<?= date('d.m.Y H:i', $date->end) ?>">
+            </label>
+        </div>
+        <label>
+            <input type="checkbox" name="all_day" value="1" <?= $all_day_event ? 'checked' : '' ?>
+                   data-deactivates=".new-calendar-date-form input[name='end']">
+            <?= _('Ganztägiger Termin') ?>
+        </label>
+        <label>
+            <?= _('Zugriff') ?>
+            <div class="flex-row">
+                <select name="access">
+                    <option value="PUBLIC" <?= $date->access === 'PUBLIC' ? 'selected' : '' ?>>
+                        <?= _('Öffentlich zugänglich') ?>
+                    </option>
+                    <option value="PRIVATE" <?= $date->access === 'PRIVATE' ? 'selected' : '' ?>>
+                        <?= _('Privat') ?>
+                    </option>
+                    <option value="CONFIDENTIAL" <?= $date->access === 'CONFIDENTIAL' ? 'selected' : '' ?>>
+                        <?= _('Vertraulich') ?>
+                    </option>
+                </select>
+                <?= tooltipIcon(
+                    _('Öffentliche Termine sind systemweit sichtbar. Private Termine sind für Personen, denen der Kalender freigegeben wurde, sichtbar. Vertrauliche Termine sind hingegen nur für einen selbst sichtbar.')
+                ) ?>
+            </div>
+        </label>
+        <label>
+            <?= _('Beschreibung') ?>
+            <textarea name="description"><?= htmlReady($date->description) ?></textarea>
+        </label>
+        <label class="studiprequired">
+            <?= _('Kategorie') ?>
+            <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+            <select class="" name="category" required>
+                <? foreach ($category_options as $key => $option) : ?>
+                    <option value="<?= htmlReady($key) ?>" <?= $key === intval($date->category) ? 'selected' : '' ?>>
+                        <?= htmlReady($option) ?>
+                    </option>
+                <? endforeach ?>
+            </select>
+        </label>
+        <label>
+            <?= _('Eigene Kategorie') ?>
+            <input type="text" name="user_category" value="<?= htmlReady($date->user_category) ?>">
+        </label>
+        <label>
+            <?= _('Ort') ?>
+            <input type="text" name="location" value="<?= htmlReady($date->location) ?>">
+        </label>
+    </fieldset>
+    <fieldset class="simplevue">
+        <legend><?= _('Wiederholung') ?></legend>
+        <?= $date->getRepetitionInputHtml('repetition') ?>
+    </fieldset>
+    <fieldset class="simplevue">
+        <legend><?= _('Ausnahmen') ?></legend>
+        <date-list-input name="exceptions" :selected_dates="<?= htmlReady(json_encode($exceptions)) ?>"></date-list-input>
+    </fieldset>
+    <? if (Config::get()->CALENDAR_GROUP_ENABLE && $user_quick_search_type) : ?>
+        <fieldset class="simplevue">
+            <legend><?= _('Teilnehmende Personen') ?></legend>
+            <editable-list
+                name="assigned_calendar_ids"
+                quicksearch="<?= htmlReady($user_quick_search_type) ?>"
+                :items="<?= htmlReady(json_encode($calendar_assignment_items)) ?>"
+            ></editable-list>
+        </fieldset>
+    <? elseif ($calendar_assignment_items) : ?>
+       <? foreach ($calendar_assignment_items as $item) : ?>
+            <input type="hidden" name="assigned_calendar_ids[]" value="<?= htmlReady($item['value']) ?>">
+       <? endforeach ?>
+    <? elseif ($owner_id): ?>
+        <input type="hidden" name="assigned_calendar_ids[]" value="<?= htmlReady($owner_id) ?>">
+    <? endif ?>
+
+    <footer data-dialog-button>
+        <? if ($date->isNew()) : ?>
+            <?= \Studip\Button::create(_('Anlegen'), 'save') ?>
+        <? else : ?>
+            <?= \Studip\Button::create(_('Speichern'), 'save') ?>
+        <? endif ?>
+        <? if (!$date->isNew()) : ?>
+            <?= \Studip\LinkButton::create(
+                _('Löschen'),
+                $controller->url_for('calendar/date/delete/' . $date->id),
+                ['data-dialog' => 'reload-on-close']
+            ) ?>
+        <? endif ?>
+        <?= \Studip\LinkButton::createCancel(_('Abbrechen'), $controller->url_for('calendar/calendar')) ?>
+    </footer>
+</form>
diff --git a/app/views/calendar/date/add.php b/app/views/calendar/date/add.php
new file mode 100644
index 0000000000000000000000000000000000000000..29c94c216205d49355fc83fd634333b8796e9c9b
--- /dev/null
+++ b/app/views/calendar/date/add.php
@@ -0,0 +1,4 @@
+<?= $this->render_partial('calendar/date/_add_edit_form', [
+    'action_link' => $controller->link_for('calendar/date/add'),
+    'event' => null
+]) ?>
diff --git a/app/views/calendar/date/delete.php b/app/views/calendar/date/delete.php
new file mode 100644
index 0000000000000000000000000000000000000000..ccb982973d5700158800bff93a8fa942a3b8339c
--- /dev/null
+++ b/app/views/calendar/date/delete.php
@@ -0,0 +1,51 @@
+<form class="default" method="post" data-dialog="reload-on-close"
+      action="<?= $controller->link_for('calendar/date/delete/' . $date->id) ?>">
+    <?= CSRFProtection::tokenTag() ?>
+    <? if ($date_has_repetitions) : ?>
+        <input type="hidden" name="selected_date" value="<?= $selected_date->format('Y-m-d') ?>">
+        <fieldset>
+            <legend><?= _('Es handelt sich um einen Termin in einer Terminserie. Was möchten Sie tun?') ?></legend>
+            <label>
+                <input type="radio" name="repetition_handling" value="create_exception"
+                    <?= $repetition_handling === 'create_exception' ? 'checked' : '' ?>>
+                <?= sprintf(
+                    _('Am %s soll aus dem Einzeltermin eine Ausnahme der Terminserie werden.'),
+                    $selected_date->format('d.m.Y')
+                ) ?>
+            </label>
+            <label>
+                <input type="radio" name="repetition_handling" value="delete_all"
+                    <?= $repetition_handling === 'delete_all' ? 'checked' : '' ?>>
+                <?= _('Die gesamte Terminserie soll gelöscht werden.') ?>
+            </label>
+        </fieldset>
+    <? else : ?>
+        <?= MessageBox::warning(_('Soll der folgende Termin wirklich gelöscht werden?')) ?>
+    <? endif ?>
+    <fieldset>
+        <legend><?= _('Informationen') ?></legend>
+        <dl>
+            <dt><?= _('Zeitbereich') ?></dt>
+            <dd>
+                <?= htmlReady(date('d.m.Y H:i', $date->begin)) ?>
+                -
+                <?= htmlReady(date('d.m.Y H:i', $date->end)) ?>
+            </dd>
+            <dt><?= _('Titel') ?></dt>
+            <dd><?= htmlReady($date->title) ?></dd>
+            <? if ($date->description) : ?>
+                <dt><?= _('Beschreibung') ?></dt>
+                <dd><?= htmlReady($date->description) ?></dd>
+            <? endif ?>
+            <dt><?= _('Zugriff') ?></dt>
+            <dd><?= htmlReady($date->getAccessAsString()) ?></dd>
+            <? if ($date->repetition_type) : ?>
+                <dt><?= _('Wiederholung') ?></dt>
+                <dd><?= htmlReady($date->getRepetitionAsString()) ?></dd>
+            <? endif ?>
+        </dl>
+    </fieldset>
+    <div data-dialog-button>
+        <?= \Studip\Button::create(_('Löschen'), 'delete') ?>
+    </div>
+</form>
diff --git a/app/views/calendar/date/edit.php b/app/views/calendar/date/edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..6090dfcbf4e2bd27b4060b5b9e9b854973adf187
--- /dev/null
+++ b/app/views/calendar/date/edit.php
@@ -0,0 +1,4 @@
+<?= $this->render_partial('calendar/date/_add_edit_form', [
+    'action_link' => $controller->link_for('calendar/date/edit/' . $date->id),
+    'date' => $date
+]) ?>
diff --git a/app/views/calendar/date/index.php b/app/views/calendar/date/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..faec7c76cb7119d9b16f7c99724b868ba6577a1f
--- /dev/null
+++ b/app/views/calendar/date/index.php
@@ -0,0 +1,135 @@
+<? if ($participation_message) : ?>
+    <?= $participation_message ?>
+    <article class="studip">
+        <header><h1><?= _('Neuen Teilnahmestatus wählen') ?></h1></header>
+        <section>
+            <form class="default" method="post" data-dialog="reload-on-close"
+                  action="<?= $controller->link_for('calendar/date/participation/' . $date->id) ?>">
+                <?= CSRFProtection::tokenTag() ?>
+                <fieldset>
+                    <? if ($user_participation_status) : ?>
+                        <label>
+                            <input type="radio" name="participation" value=""
+                                   data-activates="button[name='update_participation']">
+                            <?= _('Abwartend') ?>
+                        </label>
+                    <? endif ?>
+                    <? if ($user_participation_status !== 'ACCEPTED') : ?>
+                        <label>
+                            <input type="radio" name="participation" value="ACCEPTED"
+                                   data-activates="button[name='update_participation']">
+                            <?= _('Angenommen') ?>
+                        </label>
+                    <? endif ?>
+                    <? if ($user_participation_status !== 'DECLINED') : ?>
+                        <label>
+                            <input type="radio" name="participation" value="DECLINED"
+                                   data-activates="button[name='update_participation']">
+                            <?= _('Abgelehnt') ?>
+                        </label>
+                    <? endif ?>
+                    <? if ($user_participation_status !== 'ACKNOWLEDGED') : ?>
+                        <label>
+                            <input type="radio" name="participation" value="ACKNOWLEDGED"
+                                   data-activates="button[name='update_participation']">
+                            <?= _('Angenommen (keine Teilnahme)') ?>
+                        </label>
+                    <? endif ?>
+                </fieldset>
+                <div data-dialog-button>
+                    <?= \Studip\Button::create(_('Teilnahmestatus ändern'), 'update_participation') ?>
+                </div>
+            </form>
+        </section>
+    </article>
+<? endif ?>
+<? if ($date->description) : ?>
+    <article class="studip">
+        <header><h1><?= _('Beschreibung') ?></h1></header>
+        <section><?= htmlReady($date->description) ?></section>
+    </article>
+<? endif ?>
+<article class="studip">
+    <header><h1><?= _('Informationen') ?></h1></header>
+    <section>
+        <dl>
+            <dt><?= _('Zeit') ?></dt>
+            <dd><?= date('d.m.Y H:i', $date->begin) ?> - <?= date('d.m.Y H:i', $date->end) ?></dd>
+            <dt><?= _('Kategorie') ?></dt>
+            <dd><?= htmlReady($date->getCategoryAsString()) ?></dd>
+            <dt><?= _('Zugriff') ?></dt>
+            <dd><?= htmlReady($date->getAccessAsString()) ?></dd>
+            <? if ($date->repetition_type) : ?>
+                <dt><?= _('Wiederholung') ?></dt>
+                <dd><?= htmlReady($date->getRepetitionAsString()) ?></dd>
+            <? endif ?>
+            <? if (
+                $date->author && $date->editor
+                && (
+                    ($date->author_id !== User::findCurrent()->id)
+                    || ($date->editor_id !== User::findCurrent()->id)
+                )
+            ) : ?>
+                <dt><?= _('Bearbeitung') ?></dt>
+                <dd>
+                    <? if ($date->author->id === $date->editor->id) : ?>
+                        <? if ($date->mkdate === $date->chdate) : ?>
+                            <?= sprintf(
+                                _('Erstellt von %s'),
+                                htmlReady($date->author->getFullName())
+                            ) ?>
+                        <? else : ?>
+                            <?= sprintf(
+                                _('Erstellt und zuletzt bearbeitet von %s'),
+                                htmlReady($date->author->getFullName())
+                            ) ?>
+                        <? endif ?>
+                    <? else : ?>
+                        <?= sprintf(
+                            _('Erstellt von %1$s, zuletzt bearbeitet von %2$s'),
+                            htmlReady($date->author->getFullName()),
+                            htmlReady($date->editor->getFullName())
+                        ) ?>
+                    <? endif ?>
+                </dd>
+            <? endif ?>
+        </dl>
+    </section>
+</article>
+<? if ($is_group_date) : ?>
+    <article class="studip">
+        <header><h1><?= _('Teilnahmen') ?></h1></header>
+        <section>
+            <table class="default">
+                <body>
+                    <? foreach ($calendar_assignments as $assignment) : ?>
+                        <tr>
+                            <td><?= htmlReady($assignment->getRangeName()) ?></td>
+                            <td><?= htmlReady($assignment->getParticipationAsString()) ?></td>
+                        </tr>
+                    <? endforeach ?>
+                </body>
+            </table>
+        </section>
+    </article>
+<? endif ?>
+<div data-dialog-button>
+    <? if ($date->isWritable(User::findCurrent()->id) && $all_assignments_writable) : ?>
+        <?
+        $button_params = [];
+        if ($selected_date) {
+            $button_params['selected_date'] = $selected_date;
+        }
+        ?>
+        <?= Studip\LinkButton::create(
+            _('Bearbeiten'),
+            $controller->url_for('calendar/date/edit/' . $date->id, array_merge($button_params, ['return_path' => '/calendar/calendar'])),
+            ['data-dialog' => 'size=auto;reload-on-close']
+        ) ?>
+        <?= \Studip\LinkButton::create(
+            _('Löschen'),
+            $controller->url_for('calendar/date/delete/' . $date->id, $button_params),
+            ['data-dialog' => 'reload-on-close']
+        ) ?>
+    <? endif ?>
+</div>
diff --git a/app/views/calendar/date/move.php b/app/views/calendar/date/move.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ea2b89a9ff26f2101059f919b570e2455c9e6d7
--- /dev/null
+++ b/app/views/calendar/date/move.php
@@ -0,0 +1,22 @@
+<?= MessageBox::info(_('Es handelt sich um einen Termin in einer Terminserie. Was möchten Sie tun?')) ?>
+<form class="default" method="post" data-dialog="reload-on-close"
+      action="<?= $controller->link_for('calendar/date/move/' . $date->id) ?>">
+    <?= CSRFProtection::tokenTag() ?>
+    <input type="hidden" name="begin" value="<?= htmlReady($begin->format(\DateTime::RFC3339)) ?>">
+    <input type="hidden" name="end" value="<?= htmlReady($end->format(\DateTime::RFC3339)) ?>">
+    <label>
+        <input type="radio" name="repetition_handling" value="create_single_date">
+        <?= _('Der Termin soll aus der Terminserie herausgelöst werden.') ?>
+    </label>
+    <label>
+        <input type="radio" name="repetition_handling" value="change_times">
+        <?= _('Start- und Enduhrzeit der gesamten Terminserie soll geändert werden.') ?>
+    </label>
+    <label>
+        <input type="radio" name="repetition_handling" value="change_all">
+        <?= _('Die gesamte Terminserie soll verschoben werden und erst am gewählten Datum beginnen.') ?>
+    </label>
+    <div data-dialog-button>
+        <?= \Studip\Button::create(_('Verschieben'), 'move') ?>
+    </div>
+</form>
diff --git a/app/views/calendar/group/_attendees.php b/app/views/calendar/group/_attendees.php
deleted file mode 100644
index 3fbe27eb3eee883d575e9347d5085777c580e484..0000000000000000000000000000000000000000
--- a/app/views/calendar/group/_attendees.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<fieldset class="collapsed">
-    <legend>
-        <?= _('Teilnehmende/n hinzufügen') ?>
-    </legend>
-
-    <a class="toggler">
-        <?
-        if ($event->attendees->count()) {
-            $count_attendees = $event->attendees->filter(
-                function ($att, $k) use ($calendar) {
-                    if ($att->range_id != $calendar->getRangeId()) {
-                        return $att;
-                    }
-                })->count();
-        } else {
-            $count_attendees = 0;
-        }
-        ?>
-        <? if ($count_attendees) : ?>
-            <? if ($count_attendees < $event->attendees->count()) : ?>
-                <?= sprintf(ngettext('(%s weitere/r Teilnehmende/r)', '(%s weitere Teilnehmende)', $count_attendees), $count_attendees) ?>
-            <? else : ?>
-                <?= sprintf(_('(%s Teilnehmende)'), $count_attendees) ?>
-            <? endif; ?>
-        <? endif; ?>
-    </a>
-
-    <div>
-        <label for="user_id_1"><h4><?= _('Teilnehmende') ?></h4></label>
-        <ul class="clean" id="adressees">
-            <li id="template_adressee" style="display: none;" class="adressee">
-                <input type="hidden" name="attendees[]" value="">
-                <span class="visual"></span>
-                <a class="remove_adressee"><?= Icon::create('trash', 'clickable')->asImg(16, ['class' => 'text-bottom']) ?></a>
-            </li>
-            <? if ($event->isNew()) : ?>
-            <li style="padding: 0px;" class="adressee">
-                <input type="hidden" name="attendees[]" value="<?= $event->owner->id ?>">
-                <span class="visual">
-                    <a href="<?= URLHelper::getLink('dispatch.php/profile', ['username' => $event->owner->username], true) ?>"><?= htmlReady($event->owner->getFullname()) ?></a>
-                </span>
-                <a class="remove_adressee"><?= Icon::create('trash', 'clickable')->asImg(16, ['class' => 'text-bottom']) ?></a>
-            </li>
-            <? endif; ?>
-            <? $group_status = [
-                CalendarEvent::PARTSTAT_TENTATIVE => _('Abwartend'),
-                CalendarEvent::PARTSTAT_ACCEPTED => _('Angenommen'),
-                CalendarEvent::PARTSTAT_DECLINED => _('Abgelehnt'),
-                CalendarEvent::PARTSTAT_DELEGATED => _('Angenommen (keine Teilnahme)'),
-                CalendarEvent::PARTSTAT_NEEDS_ACTION => ''] ?>
-            <? foreach ($event->attendees as $attendee) : ?>
-                <? if ($attendee->owner) : ?>
-                <li style="padding: 0px;" class="adressee">
-                    <input type="hidden" name="attendees[]" value="<?= htmlReady($attendee->owner->id) ?>">
-                    <span class="visual">
-                        <a href="<?= URLHelper::getLink('dispatch.php/profile', ['username' => $attendee->owner->username], true) ?>"><?= htmlReady($attendee->owner->getFullname()) ?></a>
-                        <? if ($event->havePermission(Event::PERMISSION_OWN, $attendee->owner->id)) : ?>
-                            (<?= _('Organisator') ?>)
-                        <? elseif ($group_status[$attendee->group_status]) : ?>
-                            (<?= $group_status[$attendee->group_status] ?>)
-                        <? endif; ?>
-                    </span>
-                    <a class="remove_adressee"><?= Icon::create('trash', 'clickable', ['title' => _('Teilnehmende/n entfernen')])->asImg(16, ['class' => 'text-bottom']) ?></a>
-                </li>
-                <? endif; ?>
-            <? endforeach ?>
-        </ul>
-
-        <section>
-            <?= $quick_search->render() ?>
-            <br clear="both">
-        </section>
-
-        <section>
-            <?= $mps->render(); ?>
-        </section>
-
-        <script>
-            STUDIP.MultiPersonSearch.init();
-        </script>
-    </div>
-</fieldset>
diff --git a/app/views/calendar/group/_tooltip.php b/app/views/calendar/group/_tooltip.php
deleted file mode 100644
index aa8a9b134efe64994487123edada388754bf4d1d..0000000000000000000000000000000000000000
--- a/app/views/calendar/group/_tooltip.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<? $events = $events ?: $calendar->events ?>
-<? if (sizeof($events)) : ?>
-<div class="calendar-tooltip tooltip-content">
-    <h3><?= htmlReady($calendar->range_object->getFullname('no_title')) ?></h3>
-    <? foreach ($events as $event) : ?>
-    <div>
-        <? if (date('Ymd', $event->getStart()) == date('Ymd', $event->getEnd())) : ?>
-            <? if ($event->isDayEvent()) : ?>
-                <?= strftime('%x ', $event->getStart()) . '(' . _('ganztägig') . ')' ?>
-            <? else : ?>
-                <?= strftime('%x %X', $event->getStart()) . strftime(' - %X', $event->getEnd()) ?>
-            <? endif; ?>
-        <? else : ?>
-            <? if ($event->isDayEvent()) : ?>
-                <?= strftime('%x', $event->getStart()) . strftime(' - %x', $event->getEnd()) . '(' . _('ganztägig') . ')' ?>
-            <? else : ?>
-                <?= strftime('%x %X', $event->getStart()) . strftime(' - %x %X', $event->getEnd()) ?>
-            <? endif; ?>
-        <? endif; ?>
-    </div>
-    <div>
-        <?= htmlReady($event->getTitle()) ?>
-    </div>
-    <hr>
-    <? endforeach; ?>
-</div>
-<? endif; ?>
diff --git a/app/views/calendar/group/_tooltip_year.php b/app/views/calendar/group/_tooltip_year.php
deleted file mode 100644
index edd81cad0a5efec791e8153b1cdd905dd5fa2ca8..0000000000000000000000000000000000000000
--- a/app/views/calendar/group/_tooltip_year.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<? $i = 0 ?>
-<? $html = '' ?>
-<? $list_day = date('Ymd', $aday) ?>
-<? foreach ($calendars as $calendar) : ?>
-    <? if ($count_lists[$i][$list_day]) : ?>
-        <? 
-        $html .= '<div>'
-                . sprintf(ngettext('%s hat 1 Termin', '%s hat %s Termine',
-                        count($count_lists[$i][$list_day])),
-                        $calendar->range_object->getFullname('no_title'),
-                        count($count_lists[$i][$list_day]))
-                . '</div>';
-        ?>
-    <? endif; ?>
-    <? $i++ ?>
-<? endforeach; ?>
-<? if ($html) : ?>
-<div class="calendar-tooltip tooltip-content">
-    <?= $html ?>
-</div>
-<? endif; ?>
diff --git a/app/views/calendar/group/day.php b/app/views/calendar/group/day.php
deleted file mode 100644
index a452320489ad2d264a05333058dc9cb56890af90..0000000000000000000000000000000000000000
--- a/app/views/calendar/group/day.php
+++ /dev/null
@@ -1,128 +0,0 @@
-<?
-$step_day = $settings['step_day_group'];
-$step = 3600 / $step_day;
-// add one cell for day events
-$cells = (($settings['end'] - $settings['start']) / (1 / $step)) + 2;
-$width1 = floor(90 / $cells);
-$width2 = 10 + (90 - $width1 * $cells);
-$start = $settings['start'] * 3600;
-$end = ($settings['end'] + 1) * 3600;
-?>
-<table id="main_content" style="width:100%; table-layout:fixed;">
-    <thead>
-        <tr>
-            <td style="text-align: center; width: 10%; height: 40px;">
-                <div style="text-align: left; width: 20%; display: inline-block; white-space: nowrap;">
-                    <a href="<?= $controller->url_for('calendar/group/day', ['atime' => $atime - 86400]) ?>">
-                        <?= Icon::create('arr_1left', 'clickable', ['title' => _('Einen Tag zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-                        <?= strftime(_('%x'), strtotime('-1 day', $calendars[0]->getStart())) ?>
-                    </a>
-                </div>
-                <div class="calhead" style="width: 50%; display: inline-block;">
-                    <?= strftime('%A, %e. %B %Y', $atime) ?>
-                    <div style="text-align: center; font-size: 12pt; color: #bbb; height: auto; overflow: visible; font-weight: bold;"><? $hd = holiday($atime); echo $holiday['name']; ?></div>
-                </div>
-                <div style="text-align: right; width: 20%; display: inline-block; white-space: nowrap;">
-                    <a href="<?= $controller->url_for('calendar/group/day', ['atime' => $atime + 86400]) ?>">
-                        <?= strftime(_('%x'), strtotime('+1 day', $calendars[0]->getStart())) ?>
-                        <?= Icon::create('arr_1right', 'clickable', ['title' => _('Einen Tag vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-                    </a>
-                </div>
-            </td>
-        </tr>
-    </thead>
-    <tbody>
-        <tr>
-            <td>
-                <div style="overflow:auto; width:100%;">
-                <table style="width: 100%;">
-                    <? $time = mktime(0, 0, 0, date('n', $atime), date('j', $atime), date('Y', $atime)); ?>
-                    <? if ($step_day < 3600) : $colsp = ' colspan="' . $step . '"'; endif ?>
-                    <tr>
-                        <td class="precol1w" nowrap="nowrap" style="text-align: center; width: <?= $width2 ?>%;">
-                            <?= _('Mitglied') ?>
-                        </td>
-                        <td class="precol1w" style="text-align: center; width: <?= $width1 ?>%">
-                            <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $calendars[0]->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $calendars[0]->getStart(), 'isdayevent' => '1']) ?>">
-                                <?= Icon::create('schedule', 'clickable')->asImg() ?>
-                            </a>
-                        </td>
-                        <? for ($i = $time + $start; $i < $time + $end; $i += $step_day) : ?>
-                            <? if (!($i % 3600)) : ?>
-                            <td<?= $colsp ?> class="precol1w" style="text-align: center;">
-                                <a data-dialog="size=auto" title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i]) ?>" class="calhead">
-                                    <?= date('G', $i) ?>
-                                </a>
-                            </td>
-                            <? endif ?>
-                        <? endfor ?>
-                    </tr>
-                    <? foreach ($calendars as $calendar) : ?>
-                        <? $adapted = $calendar->adapt_events($start, $end, $settings['step_day_group']); ?>
-                        <tr>
-                            <td style="width: <?= $width2 ?>%; white-space: nowrap;" class="month">
-                                <a class="calhead" href="<?= $controller->url_for('calendar/single/day/' . $calendar->getRangeId(), ['atime' => $atime,]); ?>">
-                                    <?= htmlReady(($calendar->havePermission(Calendar::PERMISSION_OWN) ? _('Eigener Kalender') : get_fullname($calendar->getRangeId(), 'no_title_short'))) ?>
-                                </a>
-                            </td>
-                            <? // display day events
-                            $js_events = []; ?>
-                            <? for ($i = 0; $i < sizeof($adapted['day_events']); $i++) :
-                                $js_events[] = $calendar->events[$adapted['day_map'][$i]];
-                            endfor; ?>
-                            <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?>
-                            <td <?= sizeof($js_events) ? 'data-tooltip ' : '' ?>style="width: <?= $width1 ?>%; text-align: right;" class="calendar-day-edit<?= sizeof($js_events) ? ' calendar-group-events' : ' lightmonth' ?>">
-                                <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $calendar, 'events' => $js_events]) ?>
-                                <a title="<?= strftime(_('Neuer Tagestermin am %x'), $calendar->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $calendar->getStart(), 'isdayevent' => '1', 'user_id' => $calendar->getRangeId()]) ?>">+</a>
-                            <? else : ?>
-                            <td <?= sizeof($js_events) ? 'data-tooltip ' : '' ?>style="width: <?= $width1 ?>%;" class="<?= sizeof($js_events) ? 'calendar-group-events' : 'lightmonth' ?>">
-                                <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $calendar, 'events' => $js_events]) ?>
-                            <? endif; ?>
-                            </td>
-                            <? $k = 0;
-                            for ($i = $start + $calendar->getStart(); $i < $end + $calendar->getStart(); $i += $step_day) :
-                                if (!($i % 3600)) {
-                                    $k++;
-                                }
-                                $js_events = [];
-                                ?>
-                                <? for ($j = 0; $j < sizeof($adapted['events']); $j++) : ?>
-                                    <? if (($adapted['events'][$j]->getStart() <= $i && $adapted['events'][$j]->getEnd() > $i) || ($adapted['events'][$j]->getStart() > $i && $adapted['events'][$j]->getStart() < $i + $step_day)) :
-                                        $js_events[] = $calendar->events[$adapted['map'][$j]];
-                                    endif; ?>
-                                <? endfor; ?>
-                                <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?>
-                                <td <?= sizeof($js_events) ? 'data-tooltip ' : '' ?>style="width: <?= $width1 ?>%; text-align: right;" class="calendar-day-edit<?= sizeof($js_events) ? ' calendar-group-events' : ' lightmonth' ?>">
-                                    <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $calendar, 'events' => $js_events]) ?>
-                                    <a title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i, 'user_id' => $calendar->getRangeId()]) ?>">+</a>
-                                <? else : ?>
-                                <td <?= sizeof($js_events) ? 'data-tooltip ' : '' ?> style="width: <?= $width1 ?>%; text-align: right;" class="<?= sizeof($js_events) ? 'calendar-group-events' : 'lightmonth' ?>">
-                                    <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $calendar, 'events' => $js_events]) ?>
-                                <? endif; ?>
-                            </td>
-                            <? endfor ?>
-                        </tr>
-                    <? endforeach; ?>
-                    <tr>
-                        <td style="width: <?= $width2 ?>%;" class="precol1w"> </td>
-                        <td width="<?= $width1 ?>%" class="precol1w" style="text-align: center;">
-                            <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $calendars[0]->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $calendars[0]->getStart(), 'isdayevent' => '1']) ?>">
-                                <?= Icon::create('schedule', 'clickable')->asImg() ?>
-                            </a>
-                        </td>
-                        <? for ($i = $time + $start; $i < $time + $end; $i += $step_day) : ?>
-                            <? if (!($i % 3600)) : ?>
-                            <td<?= $colsp ?> class="precol1w" style="text-align: center;">
-                                <a data-dialog="size=auto" title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i]) ?>" class="calhead">
-                                    <?= date('G', $i) ?>
-                                </a>
-                            </td>
-                            <? endif ?>
-                        <? endfor ?>
-                    </tr>
-                </table>
-                </div>
-            </td>
-        </tr>
-    </tbody>
-</table>
diff --git a/app/views/calendar/group/month.php b/app/views/calendar/group/month.php
deleted file mode 100644
index 67212dd7992683538bf65663a8e4f24c25de2cc8..0000000000000000000000000000000000000000
--- a/app/views/calendar/group/month.php
+++ /dev/null
@@ -1,110 +0,0 @@
-<? $month = $calendar->view; ?>
-<table class="calendar-month">
-    <thead>
-        <tr>
-            <td colspan="8" style="text-align: center; vertical-align: middle;">
-                <div style="text-align: left; display: inline-block; width: 33%; white-space: nowrap;">
-                    <a style="padding-right: 2em;" href="<?= $controller->url_for('calendar/group/month', ['atime' => mktime(12, 0, 0, date('n', $calendars[0][15]->getStart()), 15, date('Y', $calendars[0][15]->getStart()) - 1)]) ?>">
-                        <?= Icon::create('arr_2left', 'clickable', ['title' => _('Ein Jahr zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-                        <?= strftime('%B %Y', strtotime('-1 year', $calendars[0][15]->getStart())) ?>
-                    </a>
-                    <a href="<?= $controller->url_for('calendar/group/month', ['atime' => $calendars[0][0]->getStart() - 1]) ?>">
-                        <?= Icon::create('arr_1left', 'clickable', ['title' => _('Einen Monat zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-                        <?= strftime('%B %Y', strtotime('-1 month', $calendars[0][15]->getStart())) ?>
-                    </a>
-                </div>
-                <div class="calhead" style="text-align: center; display: inline-block; width: 33%;">
-                    <?= htmlReady(strftime("%B ", $calendars[0][15]->getStart())) .' '. date('Y', $calendars[0][15]->getStart()); ?>
-                </div>
-                <div style="text-align: right; display: inline-block; width: 33%; white-space: nowrap;">
-                    <a style="padding-right: 2em;" href="<?= $controller->url_for('calendar/group/month', ['atime' => strtotime('+1 month', $calendars[0][15]->getStart())]) ?>">
-                        <?= strftime('%B %Y', strtotime('+1 month', $calendars[0][15]->getStart())) ?>
-                        <?= Icon::create('arr_1right', 'clickable', ['title' => _('Einen Monat vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-                    </a>
-                    <a href="<?= $controller->url_for('calendar/group/month', ['atime' => mktime(12, 0, 0, date('n', $calendars[0][15]->getStart()), 15, date('Y', $calendars[0][15]->getEnd()) + 1)]) ?>">
-                        <?= strftime('%B %Y', strtotime('+1 year', $calendars[0][15]->getStart())) ?>
-                        <?= Icon::create('arr_2right', 'clickable', ['title' => _('Ein Jahr vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-                    </a>
-                </div>
-            </td>
-        </tr>
-        <tr class="calendar-month-weekdays">
-            <? $week_days = [39092400, 39178800, 39265200, 39351600, 39438000, 39524400, 39610800]; ?>
-            <? foreach ($week_days as $week_day) : ?>
-                <td class="precol1w">
-                    <?= strftime('%a', $week_day) ?>
-                </td>
-            <? endforeach; ?>
-            <td align="center" class="precol1w">
-                <?= _('Woche') ?>
-            </td>
-        </tr>
-    </thead>
-    <tbody>
-        <? for ($i = $first_day, $j = 0; $i <= $last_day; $i += 86400, $j++) : ?>
-            <? $aday = date('j', $i); ?>
-            <?
-            $class_day = '';
-            if (($aday - $j - 1 > 0) || ($j - $aday > 6)) {
-                $class_cell = 'lightmonth';
-                $class_day = 'light';
-            } elseif (date('Ymd', $i) == date('Ymd')) {
-                $class_cell = 'celltoday';
-            } else {
-                $class_cell = 'month';
-            }
-            $hday = holiday($i);
-
-            if ($j % 7 == 0) {
-                ?><tr><?
-            }
-            ?>
-            <td class="<?= $class_cell ?>">
-            <? if (($j + 1) % 7 == 0) : ?>
-                <a class="<?= $class_day . 'sday' ?>" href="<?= $controller->url_for('calendar/group/day/' . $this->range_id, ['atime' => $i]) ?>">
-                    <?= $aday ?>
-                </a>
-                <? if ($hday["name"] != "") : ?>
-                    <div style="color: #aaaaaa;" class="inday"><?= $hday['name'] ?></div>
-                <? endif; ?>
-                <? foreach($calendars as $user_calendars) : ?>
-                    <? $count = sizeof($user_calendars[$j]->events) ?>
-                    <? if ($count) : ?>
-                    <div data-tooltip="">
-                        <a class="inday calendar-event-text" href="<?= $controller->url_for('calendar/single/day/' . $user_calendars[$j]->getRangeId(), ['atime' => $user_calendars[$j]->getStart()]) ?>"><?= htmlReady($user_calendars[$j]->range_object->getFullname('no_title')) ?></a>
-                        <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $user_calendars[$j]]) ?>
-                    </div>
-                    <? endif; ?>
-                <? endforeach; ?>
-                </td>
-                    <td class="lightmonth calendar-month-week">
-                    <a style="font-weight: bold;" class="calhead" href="<?= $controller->url_for('calendar/group/week/' . $this->range_id, ['atime' => $i]) ?>"><?= strftime("%V", $i) ?></a>
-                    </td>
-                </tr>
-            <? else : ?>
-                <? $hday_class = ['day', 'day', 'shday', 'hday'] ?>
-                <? if ($hday['col']) : ?>
-                    <a class="<?= $class_day . $hday_class[$hday['col']] ?>" href="<?= $controller->url_for('calendar/group/day', ['atime' => $i]) ?>">
-                        <?= $aday ?>
-                    </a>
-                    <div style="color: #aaaaaa;" class="inday"><?= $hday['name'] ?></div>
-                <? else : ?>
-                    <a class="<?= $class_day . 'day' ?>" href="<?= $controller->url_for('calendar/single/day', ['atime' => $i]) ?>">
-                        <?= $aday ?>
-                    </a>
-                <? endif; ?>
-                <? foreach($calendars as $user_calendars) : ?>
-                    <? $count = sizeof($user_calendars[$j]->events) ?>
-                    <? if ($count) : ?>
-                    <div data-tooltip>
-                        <a class="inday calendar-event-text" href="<?= $controller->url_for('calendar/single/day/' . $user_calendars[$j]->getRangeId(), ['atime' => $user_calendars[$j]->getStart()]) ?>"><?= htmlReady($user_calendars[$j]->range_object->getFullname('no_title')) ?></a>
-                        <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $user_calendars[$j]]) ?>
-                    </div>
-                    <? endif; ?>
-                <? endforeach; ?>
-                </td>
-            <? endif; ?>
-        <? endfor; ?>
-        </tr>
-    </tbody>
-</table>
diff --git a/app/views/calendar/group/week.php b/app/views/calendar/group/week.php
deleted file mode 100644
index cdac0b5dadca1b4c284c84f3f1086930a04ebf2f..0000000000000000000000000000000000000000
--- a/app/views/calendar/group/week.php
+++ /dev/null
@@ -1,144 +0,0 @@
-<?
-$width1 = 0;
-$width2 = 0;
-$cols = ceil(($settings['end'] - $settings['start'] + 1) * 3600 / $settings['step_week_group']) + 1;
-$start = $settings['start'] * 3600;
-$end = ($settings['end'] + 1) * 3600;
-$wlength = count($calendars[0]) - 1;
-?>
-<table style="width: 100%">
-    <tr>
-        <td colspan="<?= $colspan_2 ?>" style="vertical-align: middle; text-align: center;">
-            <div style="text-align: left; width: 25%; display: inline-block; white-space: nowrap;">
-                <a href="<?= $controller->url_for('calendar/group/week', ['atime' => mktime(12, 0, 0, date('n', $atime), date('j', $atime) - 7, date('Y', $atime))]) ?>">
-                    <?= Icon::create('arr_1left', 'clickable', ['title' => _('Eine Woche zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-                    <?= sprintf(_('%u. Woche'), strftime('%V'), strtotime('-1 week', $atime)) ?>
-                </a>
-            </div>
-            <div style="display: inline-block; text-align: center;" class="calhead">
-                <? printf(_("%s. Woche vom %s bis %s"), strftime("%V", $calendars[0][0]->getStart()), strftime("%x", $calendars[0][0]->getStart()), strftime("%x", $calendars[0][$wlength]->getEnd())) ?>
-            </div>
-            <div style="width: 25%; text-align: right; display: inline-block; white-space: nowrap;">
-                <a href="<?= $controller->url_for('calendar/group/week', ['atime' => mktime(12, 0, 0, date('n', $atime), date('j', $atime) + 7, date('Y', $atime))]) ?>">
-                    <?= sprintf(_('%u. Woche'), strftime('%V', strtotime('+1 week', $atime))) ?>
-                    <?= Icon::create('arr_1right', 'clickable', ['title' => _('Eine Woche vor')])->asImg(16, ["style" => 'vertical-align: text-top;']) ?>
-                </a>
-            </div>
-        </td>
-    </tr>
-</table>
-<div style="overflow:auto; width:100%;">
-    <table id="main_content" style="width: 100%;">
-        <thead>
-            <tr>
-                <td width="<?= $width2 ?>%" class="precol1w"> </td>
-                <? $time = $calendars[0][0]->getStart(); ?>
-                <? for ($i = 0; $i <= $wlength; $i++) : ?>
-                    <td colspan="<?= $cols ?>" style="text-align: center;" class="precol1w">
-                        <a href="<?= $controller->url_for('calendar/group/day/' . $this->range_id, ['atime' => $time]) ?>" class="calhead">
-                            <?= strftime('%a', $time) . ' ' . date('d', $time) ?>
-                        </a>
-                    </td>
-                    <? $time += 86400; ?>
-                <? endfor ?>
-            </tr>
-            <tr>
-                <td width="<?= $width2 ?>%" class="precol1w" style="text-align: center;">
-                    <?= _('Mitglied') ?>
-                </td>
-                <? foreach ($calendars[0] as $day) : ?>
-                    <td class="precol1w" style="text-align: center; width: <?= $width1 ?>%;">
-                        <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $day->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $day->getStart(), 'isdayevent' => '1']) ?>">
-                            <?= Icon::create('schedule', 'clickable')->asImg() ?>
-                        </a>
-                    </td>
-                    <? for ($i = $day->getStart() + $start; $i < $day->getStart() + $end; $i += 3600 * ceil($settings['step_week_group'] / 3600)) : ?>
-                        <td colspan="<?= ceil(3600 / $settings['step_week_group']) ?>" class="precol2w" style="text-align: center;">
-                            <a data-dialog="size=auto" title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i]) ?>" class="calhead">
-                                <?= (date('G', $i) < 10 ? '&nbsp;' . date('G', $i) . '&nbsp;' : date('G', $i)) ?>
-                            </a>
-                        </td>
-                    <? endfor ?>
-                <? endforeach ?>
-            </tr>
-        </thead>
-        <tbody>
-        <? foreach ($calendars as $user_calendar) : ?>
-            <tr>
-                <td style="width: <?= $width2 ?>%; white-space: nowrap;" class="month">
-                    <a class="calhead" href="<?= $controller->url_for('calendar/single/week/' . $user_calendar[0]->getRangeId(), ['atime' => $atime]) ?>">
-                        <?= htmlReady($user_calendar[0]->havePermission(Calendar::PERMISSION_OWN) ? _('Eigener Kalender') : get_fullname($user_calendar[0]->getRangeId(), 'no_title_short')) ?>
-                    </a>
-                </td>
-                <? $k = 1; ?>
-                <? foreach ($user_calendar as $day) : ?>
-                    <?
-                    if (date('Ymd', $day->getStart()) == date('Ymd')) {
-                        $css_class = 'celltoday';
-                    } else {
-                        if ($k % 2) {
-                            $css_class = 'lightmonth';
-                        } else {
-                            $css_class = 'month';
-                        }
-                    }
-                    $k++;
-                    $adapted = $day->adapt_events($start, $end, $settings['step_week_group']);
-
-                    // display day events
-                    $js_events = []; ?>
-                    <? for ($i = 0; $i < count($adapted['day_events']); $i++) : ?>
-                        <? $js_events[] = $day->events[$adapted['day_map'][$i]]; ?>
-                    <? endfor; ?>
-                    <? if (count($js_events) && $day->havePermission(Calendar::PERMISSION_WRITABLE)) : ?>
-                        <td <?= count($js_events) ? 'data-tooltip ' : '' ?>style="text-align: right; width: <?= $width1 ?>%" class="calendar-day-edit <?= $css_class ?><?= count($js_events) ? ' calendar-group-events' : '' ?>">
-                        <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $day, 'events' => $js_events]) ?>
-                        <a title="<?= strftime(_('Neuer Tagestermin am %x'), $day->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $day->getStart(), 'isdayevent' => '1', 'user_id' => $day->getRangeId()]) ?>">+</a>
-                    <? else : ?>
-                        <td <?= count($js_events) ? 'data-tooltip ' : '' ?>style="text-align: right; width: <?= $width1 ?>%" class="<?= $css_class ?><?= count($js_events) ? ' calendar-group-events' : '' ?>">
-                        <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $day, 'events' => $js_events]) ?>
-                    <? endif;?>
-                    </td>
-
-                    <? for ($i = $start + $day->getStart(); $i < $end + $day->getStart(); $i += $settings['step_week_group']) : ?>
-                        <? $js_events = []; ?>
-                        <? for ($j = 0; $j < count($adapted['events']); $j++) : ?>
-                            <? if (($adapted['events'][$j]->getStart() <= $i && $adapted['events'][$j]->getEnd() > $i) || ($adapted['events'][$j]->getStart() > $i && $adapted['events'][$j]->getStart() < $i + $settings['step_week_group'])) : ?>
-                                <? $js_events[] = $day->events[$adapted['map'][$j]]; ?>
-                            <? endif ?>
-                        <? endfor ?>
-                        <? if ($day->havePermission(Calendar::PERMISSION_WRITABLE)) : ?>
-                            <td <?= count($js_events) ? 'data-tooltip ' : '' ?>style="text-align: right; width: <?= $width1 ?>%" class="calendar-day-edit <?= $css_class ?><?= count($js_events) ? ' calendar-group-events' : '' ?>">
-                                <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $day, 'events' => $js_events]) ?>
-                                <a title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i, 'user_id' => $day->getRangeId()]) ?>">+</a>
-                        <? else : ?>
-                            <td <?= count($js_events) ? 'data-tooltip ' : '' ?>style="text-align: right; width: <?= $width1 ?>%" class="<?= $css_class ?><?= count($js_events) ? ' calendar-group-events' : '' ?>">
-                                <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $day, 'events' => $js_events]) ?>
-                        <? endif; ?>
-                        </td>
-                    <? endfor; ?>
-                <? endforeach; ?>
-            </tr>
-        <? endforeach; ?>
-        </tbody>
-        <tfoot>
-            <tr>
-                <td style="width:<?= $width2 ?>%; text-align: center;" class="precol1"> </td>
-                <? foreach ($calendars[0] as $day) : ?>
-                    <td style="width:<?= $width1 ?>%; text-align: center;" class="precol1w">
-                        <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $day->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $atime, 'isdayevent' => '1']) ?>">
-                            <?= Icon::create('schedule', 'clickable')->asImg() ?>
-                        </a>
-                    </td>
-                    <? for ($i = $day->getStart() + $start; $i < $day->getStart() + $end; $i += 3600 * ceil($settings['step_week_group'] / 3600)) : ?>
-                        <td colspan="<?= ceil(3600 / $settings['step_week_group']) ?>" class="precol2w" style="text-align: center;">
-                            <a data-dialog="size=auto" title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i]) ?>" class="calhead">
-                                <?= (date('G', $i) < 10 ? '&nbsp;' . date('G', $i) . '&nbsp;' : date('G', $i)) ?>
-                            </a>
-                        </td>
-                    <? endfor; ?>
-                <? endforeach; ?>
-            </tr>
-        </tfoot>
-    </table>
-</div>
diff --git a/app/views/calendar/group/year.php b/app/views/calendar/group/year.php
deleted file mode 100644
index b07f7493c7cc7456b41145827b218652db9ed72c..0000000000000000000000000000000000000000
--- a/app/views/calendar/group/year.php
+++ /dev/null
@@ -1,122 +0,0 @@
-<table style="width: 100%;">
-    <tr>
-        <td colspan="3" style="text-align: center; vertical-align: middle;">
-            <div style="text-align: left; display: inline-block; width: 20%; white-space: nowrap;">
-                <a href="<?= $controller->url_for('calendar/group/year', ['atime' => strtotime('-1 year', $atime)]) ?>">
-                    <?= Icon::create('arr_2left', 'clickable', ['title' => _('Ein Jahr zurück')])->asImg(16, ['style' => 'vertical-align: text-bottom;']) ?>
-                    <?= strftime('%Y', strtotime('-1 year', $atime)) ?>
-                </a>
-            </div>
-            <div class="calhead" style="text-align: center; display: inline-block; width:50%;">
-                <?= date('Y', $calendars[0]->getStart()) ?>
-            </div>
-            <div style="text-align: right; display: inline-block; width: 20%; white-space: nowrap;">
-                <a href="<?= $controller->url_for('calendar/group/year', ['atime' => strtotime('+1 year', $atime)]) ?>">
-                    <?= strftime('%Y', strtotime('+1 year', $atime)) ?>
-                    <?= Icon::create('arr_2right', 'clickable', ['title' => _('Ein Jahr vor')])->asImg(16, ['style' => 'vertical-align: text-bottom;']) ?>
-                </a>
-            </div>
-        </td>
-    </tr>
-    <tr>
-        <td colspan="3" class="blank">
-            <table style="width: 100%;">
-            <? $days_per_month = [31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
-                if (date('L', $calendars[0]->getStart())) {
-                    $days_per_month[2]++;
-                }
-            ?>
-                <tr>
-            <? for ($i = 1; $i < 13; $i++) : ?>
-                    <?  $ts_month += ( $days_per_month[$i] - 1) * 86400; ?>
-                    <td style="text-align: center; width: 8%;">
-                        <a class="calhead" href="<?= $controller->url_for('calendar/group/month', ['atime' => $calendars[0]->getStart() + $ts_month]) ?>">
-                            <?= strftime('%B', $ts_month); ?>
-                        </a>
-                    </td>
-            <? endfor; ?>
-                </tr>
-            <? $now = date('Ymd'); ?>
-            <? for ($i = 1; $i < 32; $i++) : ?>
-                <tr>
-                <? for ($month = 1; $month < 13; $month++) : ?>
-                    <? $aday = mktime(12, 0, 0, $month, $i, date('Y', $calendars[0]->getStart())); ?>
-                    <? if ($i <= $days_per_month[$month]) : ?>
-                           <? $wday = date('w', $aday);
-                            // emphasize current day
-                            if (date('Ymd', $aday) == $now) {
-                                $day_class = ' class="celltoday"';
-                            } else if ($wday == 0 || $wday == 6) {
-                                $day_class = ' class="weekend"';
-                            } else {
-                                $day_class = ' class="weekday"';
-                            }
-                    ?>
-                            <? if ($month == 1) : ?>
-                                <td<?= $day_class ?> height="25">
-                            <? else : ?>
-                                <td<?= $day_class ?>>
-                            <? endif; ?>
-                            <? $tooltip = $this->render_partial('calendar/group/_tooltip_year',
-                                    ['aday' => $aday, 'calendars' => $calendars, 'count_lists' => $count_lists]) ?> 
-                            <? if (trim($tooltip)) : ?>
-                                <table style="width: 100%;">
-                                    <tr>
-                                        <td<?= $day_class ?>>
-                            <? endif; ?>
-                            <? $weekday = strftime('%a', $aday); ?>
-
-                            <? $hday = holiday($aday); ?>
-                            <? if ($hday['col'] == '1') : ?>
-                                <? if (date('w', $aday) == '0') : ?>
-                                    <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                    <? $count++; ?>
-                                <? else : ?>
-                                    <a style="font-weight:bold;" class="day" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                <? endif; ?>
-                            <? elseif ($hday['col'] == '2' || $hday['col'] == '3') : ?>
-                                <? if (date('w', $aday) == '0') : ?>
-                                    <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                    <? $count++; ?>
-                                <? else : ?>
-                                    <a style="font-weight:bold;" class="hday" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                <? endif; ?>
-                            <? else : ?>
-                                <? if (date('w', $aday) == '0') : ?>
-                                    <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                    <? $count++; ?>
-                                <? else : ?>
-                                    <a style="font-weight:bold;" class="day" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                <? endif; ?>
-                            <? endif; ?>
-                            <? if (trim($tooltip)) : ?>
-                                        </td>
-                                <td<?= $day_class ?> style="text-align: right;" data-tooltip="">
-                                    <?= Icon::create('date', 'clickable', ['title' => $event_count_txt])->asImg(16, ["alt" => $event_count_txt]); ?>
-                                    <?= $tooltip ?>
-                                </td>
-                            </tr>
-                        </table>
-                            <? endif; ?>
-                    </td>
-                <? else : ?>
-                    <td class="weekday"> </td>
-                <? endif; ?>
-            <? endfor; ?>
-            </tr>
-        <? endfor; ?>
-            <tr>
-            <? $ts_month = 0; ?>
-            <? for ($i = 1; $i < 13; $i++) : ?>
-                <? $ts_month += ( $days_per_month[$i] - 1) * 86400; ?>
-                <td align="center" width="8%">
-                    <a class="calhead" href="<?= $controller->url_for('calendar/group/month', ['atime' => $calendars[0]->getStart() + $ts_month]) ?>">
-                        <?= strftime('%B', $ts_month); ?>
-                    </a>
-                </td>
-            <? endfor; ?>
-            </tr>
-        </table>
-        </td>
-    </tr>
-</table>
diff --git a/app/views/calendar/instschedule/_entry_details.php b/app/views/calendar/instschedule/_entry_details.php
deleted file mode 100644
index 7fd4a38bd2bea1cd1f59def4476122bbd91e4dec..0000000000000000000000000000000000000000
--- a/app/views/calendar/instschedule/_entry_details.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<table class="default">
-    <caption>
-        <?= sprintf(_('Veranstaltungen mit regelmäßigen Zeiten am %s, %s Uhr'), htmlReady($day), htmlReady($start) .' - '. htmlReady($end)) ?>
-    </caption>
-    <colgroup>
-        <col width="15%">
-        <col width="85%">
-    </colgroup>
-    <thead>
-        <tr>
-            <th><?= _('Nummer') ?></th>
-            <th><?= _('Name') ?></th>
-        </tr>
-    </thead>
-    <tbody>
-        <? foreach ($seminars as $seminar) : ?>
-            <tr class="<?= TextHelper::cycle('table_row_odd', 'table_row_even')?>">
-                <td><?= htmlReady($seminar->getNumber()) ?></td>
-                <td>
-                    <a href="<?= URLHelper::getLink('dispatch.php/course/details/', ['sem_id' => $seminar->getId()]) ?>">
-                        <?= Icon::create('link-intern', 'clickable')->asImg() ?>
-                        <?= htmlReady($seminar->getName()) ?>
-                    </a>
-                </td>
-            </tr>
-        <? endforeach ?>
-    </tbody>
-</table>
-<br>
diff --git a/app/views/calendar/instschedule/index.php b/app/views/calendar/instschedule/index.php
deleted file mode 100644
index bcfa72ee721e708dea2d365abbe13aae1c0c5eb3..0000000000000000000000000000000000000000
--- a/app/views/calendar/instschedule/index.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<h1>
-    <?= htmlReady(Context::getHeaderLine()) ?>  <?= _("im") ?>
-    <?= htmlReady($current_semester['name']) ?>
-</h1>
-
-<? if (Request::get('show_settings')) : ?>
-    <div class="ui-widget-overlay" style="width: 100%; height: 100%; z-index: 1001;"></div>
-    <?= $this->render_partial('calendar/schedule/_dialog', [
-        'content_for_layout' =>  $this->render_partial('calendar/schedule/settings', [
-            'settings' => $my_schedule_settings]),
-            'title'    => _('Darstellung ändern')
-    ]) ?>
-<? endif ?>
-
-<?= $calendar_view->render() ?>
diff --git a/app/views/calendar/stylesheet.php b/app/views/calendar/schedule/stylesheet.php
similarity index 100%
rename from app/views/calendar/stylesheet.php
rename to app/views/calendar/schedule/stylesheet.php
diff --git a/app/views/calendar/single/_attendees.php b/app/views/calendar/single/_attendees.php
deleted file mode 100644
index c2ee568892e39934a5559d7a1d6bf8c9bcb6dbd7..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_attendees.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<? use Studip\Button, Studip\LinkButton; ?>
-<? $show_members = $event->attendees->findOneBy('range_id',
-        $calendar->getRangeId(), '!=') ?>
-<? // Entkommentieren, wenn Mitglieder eines Termins sichtbar sein
-   // sollen, auch wenn man nicht selbst Mitglied ist und ... ?>
-<? // $show_members_visiter = $event->attendees->findOneBy('range_id', $GLOBALS['user']->id) ?>
-<? // folgende Zeile auskommentieren (siehe _attendees.php). ?>
-<? $show_members_visiter = true; ?>
-<? if ($show_members && $show_members_visiter) : ?>
-    <? $group_status = [
-        CalendarEvent::PARTSTAT_TENTATIVE => _('Abwartend'),
-        CalendarEvent::PARTSTAT_ACCEPTED => _('Angenommen'),
-        CalendarEvent::PARTSTAT_DECLINED => _('Abgelehnt'),
-        CalendarEvent::PARTSTAT_DELEGATED => _('Angenommen (keine Teilnahme)'),
-        CalendarEvent::PARTSTAT_NEEDS_ACTION => ''] ?>
-    <div>
-        <b><?= _('Teilnehmende:') ?></b>
-        <?= implode(', ', $event->attendees->map(
-            function ($att) use ($event, $group_status) {
-                $profil_link = ObjectdisplayHelper::link($att->user);
-                if ($event->havePermission(Event::PERMISSION_OWN, $att->user->getId())) {
-                    $profil_link .= ' (' . _('Organisator') . ')';
-                } else {
-                    if ($group_status[$att->group_status]) {
-                        $profil_link .= ' (' . $group_status[$att->group_status] . ')';
-                    }
-                }
-                return $profil_link;
-            })); ?>
-    </div>
-<? endif; ?>
diff --git a/app/views/calendar/single/_calhead.php b/app/views/calendar/single/_calhead.php
deleted file mode 100644
index 0d3e777a55fb7fca6ac1452ebe1f43c52c75518f..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_calhead.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<div class="calhead" style="white-space: nowrap; position: relative;">
-    <label>
-        <?= $calLabel ?>
-        <?= Icon::create('arr_1down', 'clickable') ?>
-
-        <input type="text"
-               id="date-chooser"
-               value="<?= strftime('%F', $atime) ?>"
-               data-url="<?= $controller->url_for('calendar/single/' . $calType, ['atime' => '%ATIME%']) ?>"
-               style="width: 0.1px; height: 0.1px; opacity: 0; overflow: hidden; position: absolute; left: 50%; z-index: -1;">
-    </label>
-
-    <script>
-     jQuery('#date-chooser').datepicker({ dateFormat: 'yy-mm-dd', onSelect: function () { window.location = $(this).data('url').replace(encodeURI("%ATIME%"), Math.floor($(this).datepicker('getDate').valueOf() / 1000)) } })
-    </script>
-</div>
diff --git a/app/views/calendar/single/_calhead_label_day.php b/app/views/calendar/single/_calhead_label_day.php
deleted file mode 100644
index df4f8bcfab4b1b9d23996f136809ea9f41e8911d..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_calhead_label_day.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<span class="hidden-tiny-down"><?= strftime('%A, ', $atime) ?></span>
-<?= strftime('%d.%m.%Y', $atime) ?>
-<span class="hidden-medium-down" style="font-size: 12pt; color: #bbb; font-weight: bold;"><? if ($hd = holiday($atime)) echo $hd['name']; ?></span>
diff --git a/app/views/calendar/single/_calhead_label_week.php b/app/views/calendar/single/_calhead_label_week.php
deleted file mode 100644
index fc0a5446ab4fe0cbc740d4edb4fec8a49bcf8093..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_calhead_label_week.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<? printf(_("%s. Woche"), strftime("%V", $calendars[0]->getStart())) ?>
-<span class="hidden-large-up"><?= date('Y', $calendars[0]->getStart()) ?></span>
-<span class="hidden-medium-down"><? printf(_("vom %s bis %s"), strftime("%x", $calendars[0]->getStart()), strftime("%x", $calendars[$week_type - 1]->getStart())) ?></span>
diff --git a/app/views/calendar/single/_day.php b/app/views/calendar/single/_day.php
deleted file mode 100644
index 8b70fe9ffcf560af924fc2f385185d38b2413cbc..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_day.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?
-$at = date('G', $atime);
-if ($at >= $settings['start']
-        && $at <= $settings['end'] || !$atime) {
-    $start = $settings['start'] * 3600;
-    $end = $settings['end'] * 3600;
-} elseif ($at < $settings['start']) {
-    $start = 0;
-    $end = ($settings['start'] + 2) * 3600;
-} else {
-    $start = ($settings['end'] - 2) * 3600;
-    $end = 23 * 3600;
-}
-$em = $calendar->createEventMatrix($start, $end, $settings['step_day']);
-$max_columns = $em['max_cols'] ?: 1;
-?>
-
-<nav class="calendar-nav">
-    <span style="white-space: nowrap;">
-        <a href="<?= $controller->url_for('calendar/single/day', ['atime' => strtotime('-1 day', $atime)]) ?>">
-            <?= Icon::create('arr_1left', 'clickable', ['title' => _('Einen Tag zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-            <span class="hidden-tiny-down">
-                <?= strftime(_('%x'), strtotime('-1 day', $calendar->getStart())) ?>
-            </span>
-        </a>
-    </span>
-
-    <?
-    $calType = 'day';
-    $calLabel = $this->render_partial('calendar/single/_calhead_label_day');
-    ?>
-
-    <?= $this->render_partial('calendar/single/_calhead', compact('calendar', 'atime', 'calType', 'calLabel')) ?>
-
-    <span style="white-space: nowrap;">
-        <a href="<?= $controller->url_for('calendar/single/day', ['atime' => strtotime('+1 day', $atime)]) ?>">
-            <span class="hidden-tiny-down">
-                <?= strftime(_('%x'), strtotime('+1 day', $calendar->getStart())) ?>
-            </span>
-            <?= Icon::create('arr_1right', 'clickable', ['title' => _('Einen Tag vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-        </a>
-    </span>
-</nav>
-
-<table class="calendar-day">
-    <colgroup>
-        <col style="max-width: 2em; width: 2em;">
-        <? if ($settings['step_day'] < 3600) : ?>
-        <col style="max-width: 2em; width: 2em;">
-        <? $max_columns_head = $max_columns + 3 ?>
-        <? else : ?>
-        <? $max_columns_head = $max_columns + 2 ?>
-        <? endif; ?>
-        <col span="<?= $em['max_cols'] ?: '1' ?>" style="width: <?= 100 / ($em['max_cols'] ?: 1) ?>%">
-        <col style="max-width: 0.8em; width: 0.8em;">
-    </colgroup>
-    <thead>
-        <? if ($start > 0) : ?>
-        <tr>
-            <td align="center"<?= $settings['step_day'] < 3600 ? ' colspan="2"' : '' ?>>
-                <a href="<?= $controller->url_for('calendar/single/day', ['atime' => ($atime - (date('G', $atime) * 3600 - $start + 3600))]) ?>">
-                    <?= Icon::create('arr_1up', 'clickable', ['title' => _('Früher')])->asImg() ?>
-                </a>
-            </td>
-            <td colspan="<?= $max_columns + 1 ?>">
-            </td>
-        </tr>
-        <? endif; ?>
-    </thead>
-    <tbody>
-        <?= $this->render_partial('calendar/single/_day_table', ['start' => $start, 'end' => $end, 'em' => $em]) ?>
-    </tbody>
-    <tfoot>
-    <? if ($end / 3600 < 23) : ?>
-        <tr>
-            <td align="center"<?= $settings['step_day'] < 3600 ? ' colspan="2"' : '' ?>>
-                <a href="<?= $controller->url_for('calendar/single/day', ['atime' => ($atime + $end - date('G', $atime) * 3600 + 3600)]) ?>">
-                    <?= Icon::create('arr_1down', 'clickable', ['title' => _('Später')])->asImg() ?>
-                </a>
-            </td>
-            <td colspan="<?= $max_columns + 1 ?>">
-            </td>
-        </tr>
-    <? else : ?>
-        <tr>
-            <td>&nbsp;</td>
-        </tr>
-    <? endif ?>
-    </tfoot>
-</table>
diff --git a/app/views/calendar/single/_day_cell.php b/app/views/calendar/single/_day_cell.php
deleted file mode 100644
index 31845efc63c1ed45e388772932c6a7057b793580..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_day_cell.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<? $link_notset = true ?>
-<? $atime_new = $calendar->getStart() + $i * $step ?>
-<? if (empty($em['term'][$row])) : ?>
-    <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?>
-    <td class="calendar-day-edit <?= $class_cell ?>" <?= ($em['max_cols'] > 0 ? ' colspan="' . ($em['max_cols'] + 1) . '"' : '') ?>>
-        <a title="<?= strftime(_('Neuer Termin am %x, %R Uhr'), $atime_new) ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId(), ['atime' => $atime_new]) ?>">+</a>
-    </td>
-    <? else : ?>
-    <td class="calendar-day-edit <?= $class_cell ?>" <?= ($em['max_cols'] > 0 ? ' colspan="' . ($em['max_cols'] + 1) . '"' : '') ?>>
-    </td>
-    <? endif; ?>
-<? $link_notset = false ?>
-<? else : ?>
-    <? for ($j = 0; $j < $em['colsp'][$row]; $j++) : ?>
-        <? $event = $em['term'][$row][$j]; ?>
-        <? $mapped_event = $calendar->events[$em['mapping'][$row][$j]]; ?>
-        <? if (is_object($event)) : ?>
-            <td data-tooltip<?= ($em['cspan'][$row][$j] > 1 ? ' colspan="' . $em['cspan'][$row][$j] . '"' : '') ?><?= ($em['rows'][$row][$j] > 1 ? ' rowspan="' . $em['rows'][$row][$j] . '"' : '') ?> class="<?= $event instanceof CourseEvent ? 'calendar-course-category' : 'calendar-category' ?><?= $event->getCategory() ?> calendar-day-event">
-                <? if ($em['rows'][$row][$j] > 1) : ?>
-                    <div>
-                        <?= date('H.i-', $mapped_event->getStart()) . date('H.i', $mapped_event->getEnd()) ?>
-                    </div>
-                <? endif ?>
-                <div class="calendar-day-event-title">
-                    <a title="<?= _('Termin bearbeiten') ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId() . '/' . $event->event_id, ['atime' => $atime_new, 'evtype' => $event->getType()]) ?>"><?= htmlReady($event->getTitle()) ?></a>
-                    <?= $this->render_partial('calendar/single/_tooltip', ['event' => $mapped_event]) ?>
-                </div>
-            </td>
-        <? elseif ($event == '#') : ?>
-            <td class="<?= $class_cell ?>"<?= ($em['cspan'][$row][$j] > 1 ? ' colspan="' . $em['cspan'][$row][$j] . '"' : '') ?>>
-                <span class="inday">&nbsp;</span>
-            </td>
-        <? elseif ($event == '') : ?>
-            <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?>
-                <td class="calendar-day-edit <?= $class_cell ?>"<?= ($em['cspan'][$row][$j] > 1 ? ' colspan="' . $em['cspan'][$row][$j] . '"' : '') ?>>
-                    <a title="<?= strftime(_('Neuer Termin am %x, %R Uhr'), $atime_new) ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId(), ['atime' => $atime_new]) ?>">+</a>
-                </td>
-            <? else : ?>
-                <td class="calendar-day-edit <?= $class_cell ?>"<?= ($em['cspan'][$row][$j] > 1 ? ' colspan="' . $em['cspan'][$row][$j] . '"' : '') ?>></td>
-            <? endif ?>
-            <? $link_notset = false; ?>
-            <? break; ?>
-        <? endif ?>
-    <? endfor ?>
-<? endif ?>
-<? if ($link_notset) : ?>
-    <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?>
-        <td class="calendar-day-edit <?= $class_cell ?>">
-            <a title="<?= strftime(_('Neuer Termin am %x, %R Uhr'), $atime_new) ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId(), ['atime' => $atime_new]) ?>">+</a>
-        </td>
-    <? else : ?>
-        <td class="calendar-day-edit <?= $class_cell ?>"></td>
-    <? endif; ?>
-<? endif ?>
diff --git a/app/views/calendar/single/_day_dayevents.php b/app/views/calendar/single/_day_dayevents.php
deleted file mode 100644
index 397f0bfb7897c273fb7030a666fee519fecb5f38..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_day_dayevents.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<? if (isset($em['day_events']) && count($em['day_events']) > 0) : ?>
-    <td class="<?= $class_cell ?>" style="padding: 0px;" <?= (($em['max_cols'] > 0) ? ' colspan="' . ($em['max_cols']) . '"' : '') ?>>
-        <table style="width: 100%; border-spacing: 0;">
-        <? $i = 0; ?>
-        <? foreach ($em['day_events'] as $day_event) : ?>
-            <tr>
-                <? if ($day_event->getPermission() == Event::PERMISSION_CONFIDENTIAL) : ?>
-                <td class="calendar-category<?= $day_event->getCategory() ?>">
-                    <?= htmlReady($day_event->getTitle()) ?>
-                </td>
-                <? else : ?>
-                <td data-tooltip onclick="STUDIP.Dialog.fromElement(jQuery(this).children('a').first(), {size: 'auto'}); return false;" class="calendar-category<?= $day_event->getCategory() ?>">
-                    <?= $this->render_partial('calendar/single/_tooltip', ['event' => $calendar->events[$em['day_map'][$i]]]) ?>
-                    <a style="color:#fff;" data-dialog="size=auto" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId() . '/' . $day_event->event_id, ['isdayevent' => '1']) ?>"><?= htmlReady($day_event->getTitle()) ?></a>
-                </td>
-                <? endif; ?>
-            </tr>
-            <? $i++; ?>
-        <? endforeach ?>
-        </table>
-    </td>
-    <td class="calendar-day-edit <?= $class_cell ?>" onclick="STUDIP.Dialog.fromElement(jQuery(this).children('a').first(), {size: 'auto'}); return false;">
-        <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?>
-        <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $calendar->getStart()) ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId(),  ['atime' => $calendar->getStart(), 'isdayevent' => '1']) ?>">+</a>
-        <? endif; ?>
-    </td>
-<? else : ?>
-        <td class="calendar-day-edit <?= $class_cell ?>" <?= (($em['max_cols'] > 0) ? ' colspan="' . ($em['max_cols'] + 1) . '"' : '') ?>>
-            <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?>
-            <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $calendar->getStart()) ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId(),  ['atime' => $calendar->getStart(), 'isdayevent' => '1']) ?>">+</a>
-            <? endif; ?>
-        </td>
-<? endif; ?>
diff --git a/app/views/calendar/single/_day_table.php b/app/views/calendar/single/_day_table.php
deleted file mode 100644
index 1b0ebcd4a559b2ee5479804bb2114fec63beaa3e..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_day_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?
-if ($settings['step_day'] >= 3600) {
-    $rowspan_precol = '';
-} else {
-    $rowspan_precol = ' rowspan="' . 3600 / $settings['step_day'] . '"';
-}
-?>
-<tr>
-    <td class="precol1w" <?= $rowspan_precol ? ' colspan="2"' : '' ?>><?= _('Tag') ?></td>
-    <?= $this->render_partial('calendar/single/_day_dayevents', ['em' => $em]); ?>
-</tr>
-<? for ($i = $start / $settings['step_day']; $i < $end / $settings['step_day'] + 3600 / $settings['step_day']; $i++) : ?>
-<? $row = $i - $start / $settings['step_day']; ?>
-<tr>
-    <? if (($i * $settings['step_day']) % 3600 == 0) : ?>
-    <td class="precol1w" <?= $rowspan_precol ?>>
-        <?= $i / (3600 / $settings['step_day']) ?>
-    </td>
-    <? endif ?>
-    <? if ($settings['step_day'] % 3600 != 0) : ?>
-    <? $minute = ($row % (3600 / $settings['step_day'])) * ($settings['step_day'] / 60); ?>
-    <td class="precol2w" style="height: 20px; width: 1%; padding-right: 3px;">
-        <?= $minute ? $minute : '00' ?>
-    </td>
-    <? endif ?>
-    <?= $this->render_partial('calendar/single/_day_cell', ['events' => $calendar->events, 'start' => $start, 'em' => $em, 'row' => $row, 'i' => $i, 'step' => $settings['step_day']]); ?>
-</tr>
-<? endfor; ?>
\ No newline at end of file
diff --git a/app/views/calendar/single/_edit_status.php b/app/views/calendar/single/_edit_status.php
deleted file mode 100644
index b5f41aa19d2ab0afefac3d824ef4308603572399..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_edit_status.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<? use Studip\Button, Studip\LinkButton; ?>
-<form action="" method="post">
-    <div>
-        <b><?= _('Eigener Teilnahmestatus') ?>:</b>
-        <? $group_status = [
-            CalendarEvent::PARTSTAT_TENTATIVE => _('Abwartend'),
-            CalendarEvent::PARTSTAT_ACCEPTED => _('Angenommen'),
-            CalendarEvent::PARTSTAT_DECLINED => _('Abgelehnt'),
-            CalendarEvent::PARTSTAT_DELEGATED => _('Angenommen (keine Teilnahme)')] ?>
-        <ul>
-        <? foreach ($group_status as $value => $name) : ?>
-            <ul class="list-unstyled">
-                <label>
-                    <input type="radio" value="<?= $value ?>" name="status" <?= $value == $event->group_status ? ' checked' : '' ?>>
-                    <?= $name ?>
-                </label>
-            </li>
-        <? endforeach; ?>
-        </ul>
-    </div>
-    <div>
-        <? $author = $event->getAuthor() ?>
-        <? if ($author) : ?>
-            <?= sprintf(_('Eingetragen am: %s von %s'),
-            strftime('%x, %X', $event->mkdate),
-                htmlReady($author->getFullName('no_title'))) ?>
-        <? endif; ?>
-    </div>
-    <? if ($event->event->mkdate < $event->event->chdate) : ?>
-        <? $editor = $event->getEditor() ?>
-        <? if ($editor) : ?>
-        <div>
-            <?= sprintf(_('Zuletzt bearbeitet am: %s von %s'),
-                strftime('%x, %X', $event->chdate),
-                    htmlReady($editor->getFullName('no_title'))) ?>
-        </div>
-        <? endif; ?>
-    <? endif; ?>
-    <div style="text-align: center;" data-dialog-button>
-        <?= Button::create(_('Speichern'), 'store', ['title' => _('Termin speichern')]) ?>
-        <? if ($event->havePermission(Event::PERMISSION_DELETABLE)) : ?>
-        <?= LinkButton::create(_('Löschen'), $controller->url_for('calendar/single/delete/' . implode('/', $event->getId()))) ?>
-        <? endif; ?>
-        <? if (!Request::isXhr()) : ?>
-        <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view, [$event->getStart()])) ?>
-        <? endif; ?>
-    </div>
-</form>
diff --git a/app/views/calendar/single/_event_data.php b/app/views/calendar/single/_event_data.php
deleted file mode 100644
index da9a5feb3c408f81bb3fd57c9e095932cf353558..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_event_data.php
+++ /dev/null
@@ -1,100 +0,0 @@
-<? use Studip\LinkButton; ?>
-<div>
-    <h4><?= htmlReady($event->getTitle()) ?></h4>
-    <div>
-        <b><?= _('Beginn') ?>:</b> <?= strftime('%c', $event->getStart()) ?>
-    </div>
-    <div>
-        <b><?= _('Ende') ?>:</b> <?= strftime('%c', $event->getEnd()) ?>
-    </div>
-    <? if ($event->havePermission(Event::PERMISSION_READABLE)) : ?>
-        <? if ($event instanceof CourseEvent) : ?>
-        <div>
-            <b><?= _('Veranstaltung') ?>:</b>
-            <? if ($GLOBALS['perm']->have_studip_perm('user', $event->range_id)) : ?>
-            <a href="<?= URLHelper::getLink('dispatch.php/course/details/?cid=' . $event->range_id) ?>">
-            <? else : ?>
-            <a href="<?= URLHelper::getLink('dispatch.php/course/details/index/' . $event->range_id) ?>">
-            <? endif; ?>
-                <?= htmlReady($event->course->getFullname()) ?>
-            </a>
-        </div>
-        <? endif;?>
-        <? if ($text = $event->getDescription()) : ?>
-            <div>
-                <b><?= _('Beschreibung') ?>:</b> <?= htmlReady(mila($text, 50)) ?>
-            </div>
-        <? endif; ?>
-        <? if ($text = $event->toStringCategories()) : ?>
-            <div>
-                <b><?= _('Kategorie') ?>:</b> <?= htmlReady(mila($text, 50)) ?>
-            </div>
-        <? endif; ?>
-        <? if ($text = $event->getLocation()) : ?>
-            <div>
-                <b><?= _('Raum/Ort') ?>:</b> <?= htmlReady(mila($text, 50)) ?>
-            </div>
-        <? endif; ?>
-        <? if ($text = $event->toStringPriority()) : ?>
-            <div>
-                <b><?= _('Priorität') ?>:</b> <?= htmlReady(mila($text, 50)) ?>
-            </div>
-        <? endif; ?>
-        <? if ($text = $event->toStringAccessibility()) : ?>
-            <div>
-                <b><?= _('Zugriff') ?>:</b> <?= htmlReady(mila($text, 50)) ?>
-            </div>
-        <? endif; ?>
-        <? if ($text = $event->toStringRecurrence()) : ?>
-            <div>
-                <b><?= _('Wiederholung') ?>:</b> <?= htmlReady($text) ?>
-            </div>
-        <? endif; ?>
-        <? if ($event instanceof CalendarEvent) : ?>
-            <? if (Config::get()->CALENDAR_GROUP_ENABLE) : ?>
-                <?= $this->render_partial('calendar/single/_attendees.php') ?>
-                <? if ($calendar->havePermission(Calendar::PERMISSION_OWN)
-                        && $event->toStringGroupStatus()) : ?>
-                    <?= $this->render_partial('calendar/single/_edit_status') ?>
-                <? else : ?>
-                <div style="text-align: center;" data-dialog-button>
-                    <? if ($event->havePermission(Event::PERMISSION_DELETABLE)) : ?>
-                    <?= LinkButton::create(_('Löschen'), $controller->url_for('calendar/single/delete/' . implode('/', $event->getId()))) ?>
-                    <? endif; ?>
-                    <? if (!Request::isXhr()) : ?>
-                    <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view, [$event->getStart()])) ?>
-                    <? endif; ?>
-                </div>
-                <? endif; ?>
-            <? endif; ?>
-        <? else : ?>
-            <? // durchführende Lehrende ?>
-            <? $related_persons = $event->dozenten; ?>
-            <? if (sizeof($related_persons)) : ?>
-            <div>
-                <b><?= ngettext('Durchführende Lehrperson', 'Durchführende Lehrende', sizeof($related_persons)) ?>:</b>
-                <ul class="list-unstyled">
-                <? foreach ($related_persons as $related_person) : ?>
-                    <li>
-                        <?= ObjectdisplayHelper::link($related_person) ?>
-                    </li>
-                <? endforeach; ?>
-                </ul>
-            </div>
-            <? endif; ?>
-            <? // related groups ?>
-            <? $related_groups = $event->getRelatedGroups(); ?>
-            <? if (sizeof($related_groups)) : ?>
-            <div>
-                <b><?= _('Betroffene Gruppen') ?>:</b>
-                <?= htmlReady(implode(', ', $related_groups->pluck('name'))) ?>
-            </div>
-            <? endif; ?>
-            <? if (!Request::isXhr()) : ?>
-            <div style="text-align: center;" data-dialog-button>
-                <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view, [$event->getStart()])) ?>
-            </div>
-            <? endif; ?>
-        <? endif; ?>
-    <? endif; ?>
-</div>
diff --git a/app/views/calendar/single/_include_month.php b/app/views/calendar/single/_include_month.php
deleted file mode 100644
index 6942d2f02be67d5633ba655007b593af9ba21922..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_include_month.php
+++ /dev/null
@@ -1,135 +0,0 @@
-<? $now = mktime(12, 0, 0, date('n', time()), date('j', time()), date('Y', time())); ?>
-<table class="blank">
-    <tr>
-        <td style="text-align: center;">
-            <table style="width: 100%;">
-                <tr>
-                    <td colspan="8" style="vertical-align: top; text-align: center; white-space:nowrap;">
-                        <div style="float:left; width:15%;">
-                        <? if ($mod == 'NONAVARROWS') : ?>
-                            &nbsp;
-                        <? else : ?>
-                            <a href="<?= $controller->url_for($href, ['imt' => mktime(12, 0, 0, date('n', $imt), 1, date('Y', $imt) - 1)]) ?>">
-                               <?= Icon::create('arr_2left', 'clickable', ['title' => _('Ein Jahr zurück')])->asImg() ?>
-                            </a>
-                            <a href="<?= $controller->url_for($href, ['imt' => mktime(12, 0, 0, date('n', $imt) - 1, 1, date('Y', $imt))]) ?>">
-                                <?= Icon::create('arr_1left', 'clickable', ['title' => _('Einen Monat zurück')])->asImg() ?>
-                            </a>
-                        <? endif; ?>
-                        </div>
-                        <div class="precol1w" style="float:left; text-align:center; width:70%;">
-                            <?= sprintf("%s %s\n", strftime('%B', $imt), date('Y', $imt)) ?>
-                        </div>
-                        <div style="float:right; width:15%;">
-                        <? if ($mod == 'NONAVARROWS') : ?>
-                            &nbsp;
-                        <? else : ?>
-                            <a href="<?= $controller->url_for($href, ['imt' => mktime(12, 0, 0, date('n', $imt) + 1, 1, date('Y', $imt))]) ?>">
-                                <?= Icon::create('arr_1right', 'clickable', ['title' => _('Einen Monat vor')])->asImg() ?>
-                            </a>
-                            <a href="<?= $controller->url_for($href, ['imt' => mktime(12, 0, 0, date('n', $imt), 1, date('Y', $imt) + 1)]) ?>">
-                                <?= Icon::create('arr_2right', 'clickable', ['title' => _('Ein Jahr vor')])->asImg() ?>
-                            </a>
-                        <? endif; ?>
-                        </div>
-                    </td>
-                </tr>
-            </table>
-        </td>
-    </tr>
-    <tr>
-        <td class="blank">
-            <table class="blank">
-                <tr>
-                    <? $week_days = [39092400, 39178800, 39265200, 39351600, 39438000, 39524400, 39610800]; ?>
-                    <? foreach ($week_days as $week_day) : ?>
-                    <td align="center" class="precol2w" width="25">
-                        <?= strftime('%a', $week_day) ?>
-                    </td>
-                    <? endforeach; ?>
-                    <td class="precol2w" width="25"> </td>
-                </tr>
-            <? $adow = date('w', mktime(12, 0, 0, date('n', $imt), 1, date('Y', $imt))); ?>
-            <? if ($adow == 0) : ?>
-                <? $adow = 6; ?>
-            <? else : ?>
-                <? $adow--; ?>
-            <? endif; ?>
-            <? $first_day = mktime(12, 0, 0, date('n', $imt), 1, date('Y', $imt)) - $adow * 86400; ?>
-            <? $cor = 0; ?>
-            <? if (date('n', $imt) == 3) : ?>
-                <? $cor = 1; ?>
-            <? endif; ?>
-            <? $last_day = ((42 - ($adow + date('t', mktime(12, 0, 0, date('n', $imt), 1, date('Y', $imt))))) % 7 + $cor) * 86400
-            + mktime(12, 0, 0, date('n', $imt), date('t', $imt), date('Y', $imt)); ?>
-            <? for ($i = $first_day, $j = 0; $i <= $last_day; $i += 86400, $j++) : ?>
-                <?
-                $aday = date('j', $i);
-                $style = '';
-                if (($aday - $j - 1 > 0) || ($j - $aday > 6)) {
-                    $style = 'light';
-                }
-                $hday = holiday($i);
-                ?>
-                <? if (abs($now - $i) < 43199 && !($style == 'light')) : ?>
-                    <td class="celltoday" align="center" width="25" height="25">
-                <? elseif (date('m', $i) != date('n', $imt)) : ?>
-                    <td class="lightmonth" align="center" width="25" height="25">
-                <? else : ?>
-                    <td class="month" align="center" width="25" height="25">
-                <? endif; ?>
-                <? $js_inc = ''; ?>
-                <? if (!empty($js_include) && is_array($js_include)) : ?>
-                    <?
-                    $js_inc = " onClick=\"{$js_include['function']}(";
-                    if (sizeof($js_include['parameters'])) {
-                        $js_inc .= implode(", ", $js_include['parameters']) . ", ";
-                    }
-                    $js_inc .= "'" . date('m', $i) . "', '$aday', '" . date('Y', $i) . "')\"";
-                    ?>
-                <? endif; ?>
-                <? if (abs($atime - $i) < 43199) : ?>
-                    <? $aday = '<span class="current">'.$aday.'</span>' ?>
-                <? endif; ?>
-                <? if (($j + 1) % 7 == 0) : ?>
-                    <a class="<?= $style ?>sday" href="<?= $controller->url_for($href, ['atime' => $i]) ?>" <?= is_array($hday) ? tooltip($hday['name'] ?: '') : '' ?> <?= $js_inc ?>>
-                        <?= $aday ?>
-                    </a>
-                </td>
-                <td class="lightmonth" style="text-align: center; width: 25px; height: 25px;">
-                    <a href="<?= $controller->url_for('calendar/single/week/', ['atime' => $i]) ?>">
-                        <span class="kwmin"><?= strftime('%V', $i) ?></span>
-                    </a>
-                </td>
-            </tr>
-                <? else : ?>
-                    <? if (is_array($hday)) : ?>
-                        <? switch ($hday['col']) {
-                            case 1:
-                                ?><a class="<?= $style ?>day" href="<?= $controller->url_for($href, ['atime' => $i]) ?>" <?= tooltip($hday['name']) . $js_inc ?>>
-                                <?= $aday ?>
-                                </a><?
-                                break;
-                            case 2:
-                            case 3;
-                                ?><a class="<?= $style ?>hday" href="<?= $controller->url_for($href, ['atime' => $i]) ?>" <?= tooltip($hday['name']) . $js_inc ?>>
-                                <?= $aday ?>
-                                </a><?
-                                break;
-                            default:
-                                ?><a class="<?= $style ?>day" href="<?= $controller->url_for($href, ['atime' => $i]) ?>" <?= $js_inc ?>>
-                                <?= $aday ?>
-                                </a>
-                            <?}?>
-                    <? else : ?>
-                        <a class="<?= $style ?>day" href="<?= $controller->url_for($href, ['atime' => $i]) ?>" <?= $js_inc ?>>
-                            <?= $aday ?>
-                        </a>
-                    <? endif ?>
-                    </td>
-                <? endif; ?>
-            <? endfor; ?>
-            </table>
-        </td>
-    </tr>
-</table>
diff --git a/app/views/calendar/single/_jump_to.php b/app/views/calendar/single/_jump_to.php
deleted file mode 100644
index b5e96bb21dea10779aba54903930b5a6dcc07dc7..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_jump_to.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<form class="default" action="<?= $action_url ?>" method="post" name="jump_to">
-    <input type="hidden" name="action" value="<?= $action ?>">
-
-    <section class="hgroup">
-        <?= _('Gehe zu:') ?>
-        <input size="10" style="width: 16em;" type="text" id="jmp_date" name="jmp_date" type="text" value="<?= strftime('%x', $atime)?>">
-        <?= Icon::create('accept', 'clickable')->asInput(['class' => 'text-top']) ?>
-    </section>
-</form>
-
-<script>
-    jQuery('#jmp_date').datepicker();
-</script>
diff --git a/app/views/calendar/single/_select_calendar.php b/app/views/calendar/single/_select_calendar.php
deleted file mode 100644
index 7b2b94279f57c826e5207efea3908407a4e4f95f..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_select_calendar.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<form name="select_calendars" class="default" method="post" action="<?= htmlReady($action_url) ?>">
-
-    <section class="hgroup">
-        <?= _('Kalender') ?>
-        <select class="sidebar-selectlist submit-upon-select" style="width: 16em;" name="range_id">
-            <option value="<?= get_userid() ?>"<?= get_userid() === $range_id ? ' selected' : '' ?>>
-                    <?= _("Eigener Kalender") ?>
-            </option>
-            <? $groups = Calendar::getGroups($GLOBALS['user']->id); ?>
-            <? if (count($groups)) : ?>
-                <optgroup style="font-weight:bold;" label="<?= _('Gruppenkalender:') ?>">
-                <? foreach ($groups as $group) : ?>
-                    <option value="<?= $group->getId() ?>"<?= ($range_id == $group->getId() ? ' selected' : '') ?>>
-                         <?= htmlReady($group->name) ?>
-                    </option>
-                <? endforeach ?>
-                </optgroup>
-            <? endif; ?>
-            <? $calendar_users = CalendarUser::getOwners($GLOBALS['user']->id)->getArrayCopy(); ?>
-            <? usort($calendar_users, function ($a, $b) {
-                return strnatcmp($a->owner->Nachname, $b->owner->Nachname);
-            }); ?>
-            <? if (count($calendar_users)) : ?>
-                <optgroup style="font-weight:bold;" label="<?= _('Einzelkalender:') ?>">
-                <? foreach ($calendar_users as $calendar_user) : ?>
-                    <? if (!$calendar_user->owner) {
-                        continue;
-                    } ?>
-                    <option value="<?= $calendar_user->owner_id ?>"<?= ($range_id == $calendar_user->owner_id ? ' selected' : '') ?>>
-                        <?= htmlReady($calendar_user->owner->getFullname('full_rev')) ?>
-                    </option>
-                <? endforeach ?>
-                </optgroup>
-            <? endif ?>
-            <?/*
-                if ($GLOBALS['perm']->have_perm('dozent')) {
-                    $lecturers = Calendar::GetLecturers();
-                } else {
-                    $lecturers = array();
-                }
-                */
-                $lecturers = [];
-            ?>
-            <? if (count($lecturers)) : ?>
-                <optgroup style="font-weight:bold;" label="<?= _('Lehrendenkalender:') ?>">
-                <? foreach ($lecturers as $lecturer) : ?>
-                    <option value="<?= $lecturer['id'] ?>"<?= ($range_id == $lecturer['id'] ? ' selected' : '') ?>>
-                        <?= htmlReady(my_substr($lecturer['name'] . " ({$lecturer['username']})", 0, 30)) ?>
-                    </option>
-                <? endforeach ?>
-                </optgroup>
-            <? endif ?>
-            <? if (Config::get()->COURSE_CALENDAR_ENABLE) : ?>
-                <? $courses = Calendar::GetCoursesActivatedCalendar($GLOBALS['user']->id); ?>
-                <? if (count($courses)) : ?>
-                    <optgroup style="font-weight:bold;" label="<?= _('Veranstaltungskalender:') ?>">
-                    <? foreach ($courses as $course) : ?>
-                        <option value="<?= $course->id ?>"<?= ($range_id == $course->id ? ' selected' : '') ?>>
-                            <?= htmlReady($course->getFullname()) ?>
-                        </option>
-                    <? endforeach ?>
-                    </optgroup>
-                <? endif ?>
-                <? $insts = Calendar::GetInstituteActivatedCalendar($GLOBALS['user']->id); ?>
-                <? if (count($insts)) : ?>
-                    <optgroup style="font-weight:bold;" label="<?= _('Einrichtungskalender:') ?>">
-                    <? foreach ($insts as $inst_id => $inst_name) : ?>
-                        <option value="<?= $inst_id ?>"<?= ($range_id == $inst_id ? ' selected' : '') ?>>
-                            <?= htmlReady(my_substr($inst_name, 0, 30)); ?>
-                        </option>
-                    <? endforeach ?>
-                    </optgroup>
-                <? endif ?>
-            <? endif ?>
-        </select>
-
-        <input type="hidden" name="view" value="<?= $view ?>">
-        <?= Icon::create('accept', 'clickable')->asInput(['class' => "text-top"]) ?>
-    </section>
-</form>
diff --git a/app/views/calendar/single/_select_category.php b/app/views/calendar/single/_select_category.php
deleted file mode 100644
index fe86bdc04d74203c12f6f73808cba357a19f724c..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_select_category.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<form class="default" name="filter_categories" method="post" action="<?= $action_url ?>">
-
-    <section class="hgroup">
-        <?= _('Kategorie') ?>
-        <select class="sidebar-selectlist nested-select submit-upon-select" style="width: 16em;" name="category">
-            <option value=""><?= _('Alle Kategorien') ?></option>
-        <? foreach (Config::get()->getValue('PERS_TERMIN_KAT') as $key => $cat) : ?>
-            <option value="<?= $key ?>"<?= ($category == $key ? ' selected="selected"' : '') ?> class="calendar-category<?= $key ?>" data-color-class="calendar-category<?= $key ?>">
-                <?= htmlReady($cat['name']) ?>
-            </option>
-        <? endforeach; ?>
-        </select>
-
-        <?= Icon::create('accept', 'clickable')->asInput(['class' => "text-top"]) ?>
-    </section>
-</form>
diff --git a/app/views/calendar/single/_semester_filter.php b/app/views/calendar/single/_semester_filter.php
deleted file mode 100644
index 016f1e537b0958586eec93fddde1885f1170ec86..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_semester_filter.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<form data-dialog action="<?= $controller->url_for('calendar/single/seminar_events/')?>" class="default">
-    <section class="hgroup">
-        <label>
-            <?= _('Semesterfilter') ?>:
-            <select name="sem_select" class="submit-upon-select">
-                <option <?= ($sem == 'current' ? 'selected' : '')?> value="current"><?= _('Aktuelles Semester') ?></option>
-                <option <?= ($sem == 'future' ? 'selected' : '')?> value="future"><?= _('Aktuelles und nächstes Semester') ?></option>
-                <option <?= ($sem == 'last' ? 'selected' : '')?> value="last"><?= _('Aktuelles und letztes Semester') ?></option>
-                <option <?= ($sem == 'lastandnext' ? 'selected' : '')?> value="lastandnext"><?= _('Letztes, aktuelles, nächstes Semester') ?></option>
-                <? if (Config::get()->MY_COURSES_ENABLE_ALL_SEMESTERS) : ?>
-                    <option <?= ($sem == 'all' ? 'selected' : '')?> value="all"><?= _('Alle Semester') ?></option>
-                <? endif ?>
-
-                <? if (!empty($semesters)) : ?>
-                    <optgroup label="<?=_('Semester auswählen')?>">
-                    <? foreach ($semesters as $semester) :?>
-                        <option value="<?=$semester->id?>" <?= ($sem == $semester->id ? 'selected' : '')?>>
-                            <?= htmlReady($semester->name)?>
-                        </option>
-                    <? endforeach ?>
-                    </optgroup>
-                <? endif ?>
-            </select>
-        </label>
-    </section>
-
-    <noscript>
-        <?= \Studip\Button::createAccept(_('Auswählen'))?>
-    </noscript>
-</form>
diff --git a/app/views/calendar/single/_tooltip.php b/app/views/calendar/single/_tooltip.php
deleted file mode 100644
index 0979ec2c981f311a8567186a8fe97dada8a35896..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/_tooltip.php
+++ /dev/null
@@ -1,113 +0,0 @@
-<div class="calendar-tooltip tooltip-content">
-    <h4><?= htmlReady($event->getTitle()) ?></h4>
-    <div>
-        <b><?= _('Beginn') ?>:</b> <?= strftime('%c', $event->getStart()) ?>
-    </div>
-    <div>
-        <b><?= _('Ende') ?>:</b> <?= strftime('%c', $event->getEnd()) ?>
-    </div>
-    <? if ($event->havePermission(Event::PERMISSION_READABLE)) : ?>
-        <? if ($event instanceof CourseEvent) : ?>
-        <div>
-            <b><?= _('Veranstaltung') ?>:</b> <?= htmlReady($event->course->getFullname()) ?>
-        </div>
-        <? endif;?>
-        <? if ($text = $event->getDescription()) : ?>
-            <div>
-                <b><?= _('Beschreibung') ?>:</b> <?= htmlReady(mila($text, 50)) ?>
-            </div>
-        <? endif; ?>
-        <? if ($text = $event->toStringCategories()) : ?>
-            <div>
-                <b><?= _('Kategorie') ?>:</b> <?= htmlReady(mila($text, 50)) ?>
-            </div>
-        <? endif; ?>
-        <? if ($text = $event->getLocation()) : ?>
-            <div>
-                <b><?= _('Raum/Ort') ?>:</b> <?= htmlReady(mila($text, 50)) ?>
-            </div>
-        <? endif; ?>
-        <? if ($text = $event->toStringPriority()) : ?>
-            <div>
-                <b><?= _('Priorität') ?>:</b> <?= htmlReady(mila($text, 50)) ?>
-            </div>
-        <? endif; ?>
-        <? if ($text = $event->toStringAccessibility()) : ?>
-            <div>
-                <b><?= _('Zugriff') ?>:</b> <?= htmlReady(mila($text, 50)) ?>
-            </div>
-        <? endif; ?>
-        <? if ($text = $event->toStringRecurrence()) : ?>
-            <div>
-                <b><?= _('Wiederholung') ?>:</b> <?= htmlReady($text) ?>
-            </div>
-        <? endif; ?>
-    <? endif; ?>
-    <? if ($event->havePermission(Event::PERMISSION_READABLE)) : ?>
-        <? if ($event instanceof CalendarEvent
-                && Config::get()->CALENDAR_GROUP_ENABLE
-                && $calendar->getRange() == Calendar::RANGE_USER) : ?>
-            <? $group_status = [
-                    CalendarEvent::PARTSTAT_TENTATIVE => _('Abwartend'),
-                    CalendarEvent::PARTSTAT_ACCEPTED => _('Angenommen'),
-                    CalendarEvent::PARTSTAT_DECLINED => _('Abgelehnt'),
-                    CalendarEvent::PARTSTAT_DELEGATED => _('Angenommen (keine Teilnahme)'),
-                    CalendarEvent::PARTSTAT_NEEDS_ACTION => ''] ?>
-            <? $show_members = $event->attendees->findOneBy('range_id',
-                    $calendar->getRangeId(), '!=') ?>
-            <? // Entkommentieren, wenn Mitglieder eines Termins sichtbar sein
-               // sollen, auch wenn man nicht selbst Mitglied ist und ... ?>
-            <? // $show_members_visiter = $event->attendees->findOneBy('range_id', $GLOBALS['user']->id) ?>
-            <? // folgende Zeile auskommentieren (siehe _attendees.php). ?>
-            <? $show_members_visiter = true; ?>
-            <? if ($show_members && $show_members_visiter) : ?>
-            <div>
-                <b><?= _('Teilnehmende:') ?></b>
-                    <?= implode(', ', $event->attendees->map(
-                        function ($att) use ($event, $group_status) {
-                            if ($event->havePermission(Event::PERMISSION_OWN, $att->owner->id)) {
-                                $ret = htmlReady($att->owner->getFullname())
-                                    . ' (' . _('Organisator') . ')';
-                            } else {
-                                $ret = htmlReady($att->owner->getFullname());
-                                if ($group_status[$att->group_status]) {
-                                    $ret .= ' (' . $group_status[$att->group_status] . ')';
-                                }
-                            }
-                            return $ret;
-                        })); ?>
-            </div>
-            <? endif; ?>
-        <? endif; ?>
-        <? if ($event instanceof CourseEvent) : ?>
-            <? // durchführende Lehrende ?>
-            <? $related_persons = $event->dozenten; ?>
-            <? if (sizeof($related_persons)) : ?>
-            <div>
-                <b><?= ngettext('Durchführende Lehrperson', 'Durchführende Lehrende', sizeof($related_persons)) ?>:</b>
-                <ul class="list-unstyled">
-                <? foreach ($related_persons as $related_person) : ?>
-                    <li>
-                        <?= htmlReady($related_person->getFullName()) ?>
-                    </li>
-                <? endforeach; ?>
-                </ul>
-            </div>
-            <? endif; ?>
-            <? // related groups ?>
-            <? $related_groups = $event->getRelatedGroups(); ?>
-            <? if (sizeof($related_groups)) : ?>
-            <div>
-                <b><?= _('Betroffene Gruppen') ?>:</b>
-                <ul class="list-unstyled">
-                <? foreach ($related_groups as $group) : ?>
-                    <li>
-                        <?= htmlReady($group->name) ?>
-                    </li>
-                <? endforeach; ?>
-                </ul>
-            </div>
-            <? endif; ?>
-        <? endif; ?>
-    <? endif; ?>
-</div>
diff --git a/app/views/calendar/single/day.php b/app/views/calendar/single/day.php
deleted file mode 100644
index 448509cd1725294f021b86211ed3da707b9a24c0..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/day.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<div style="width: 100%; display: flex; flex-wrap: wrap;">
-    <div style="flex-grow:2; flex-basis: 60%;">
-        <?= $this->render_partial('calendar/single/_day'); ?>
-    </div>
-    <div class="hidden-medium-down" style="flex-grow:1; padding-left:1em;">
-        <? $imt = Request::int('imt', mktime(12, 0, 0, date('n', $atime) - 1, date('j', $atime), date('Y', $atime))) ?>
-        <?= $this->render_partial('calendar/single/_include_month', ['imt' => $imt, 'href' => '', 'mod' => '']) ?>
-        <? $imt = mktime(12, 0, 0, date('n', $imt) + 1, date('j', $imt), date('Y', $imt)) ?>
-        <?= $this->render_partial('calendar/single/_include_month', ['imt' => $imt, 'href' => '', 'mod' => 'NONAVARROWS']) ?>
-        <? $imt = mktime(12, 0, 0, date('n', $imt) + 1, date('j', $imt), date('Y', $imt)) ?>
-        <?= $this->render_partial('calendar/single/_include_month', ['imt' => $imt, 'href' => '', 'mod' => 'NONAVARROWS']) ?>
-    </div>
-</div>
diff --git a/app/views/calendar/single/edit.php b/app/views/calendar/single/edit.php
deleted file mode 100644
index c22f7a6d9caea7463221f52cc27d0104a3000821..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/edit.php
+++ /dev/null
@@ -1,454 +0,0 @@
-<? use Studip\Button, Studip\LinkButton; ?>
-<? if (Request::isXhr()) : ?>
-    <? foreach (PageLayout::getMessages() as $messagebox) : ?>
-        <?= $messagebox ?>
-    <? endforeach ?>
-<? endif; ?>
-<form data-dialog="" method="post" action="<?= $controller->url_for($base . 'edit/' . $range_id . '/' . $event->event_id) ?>" class="default collapsable">
-    <?= CSRFProtection::tokenTag() ?>
-
-    <fieldset>
-        <legend>
-            <? if ($event->isNew()) : ?>
-                <?= _('Neuen Termin anlegen') ?>
-            <? else : ?>
-                <?= _('Termin bearbeiten') ?>
-            <? endif; ?>
-        </legend>
-
-        <label class="hidden-tiny-down">
-            <input type="checkbox" name="isdayevent" value="1" <?= $event->isDayEvent() ? 'checked' : '' ?>
-                onChange="jQuery(this).closest('fieldset').find('input[size=\'2\']').prop('disabled', function (i,val) { return !val; });">
-            <?= _('Ganztägig') ?>
-        </label>
-
-        <section class="required">
-            <?= _('Beginn') ?>
-        </section>
-
-        <label class="col-3">
-            <?= _('Datum') ?>
-            <input type="text" name="start_date" id="start-date" value="<?= strftime('%x', $event->getStart()) ?>" size="12" required>
-        </label>
-
-        <label class="col-3">
-            <?= _('Uhrzeit') ?>
-
-            <div class="hgroup">
-                <input class="size-s no-hint"
-                       type="text"
-                       name="start_hour"
-                       value="<?= date('G', $event->getStart()) ?>"
-                       size="2"
-                       maxlength="2"<?= $event->isDayEvent() ? ' disabled' : '' ?>
-                       aria-label="Stunde">
-                :
-                <input class="size-s no-hint"
-                       type="text"
-                       name="start_minute"
-                       value="<?= date('i', $event->getStart()) ?>"
-                       size="2"
-                       maxlength="2"<?= $event->isDayEvent() ? ' disabled' : '' ?>
-                       aria-label="Minuten">
-            </div>
-        </label>
-
-        <section class="required">
-            <?= _('Ende') ?>
-        </section>
-
-        <label class="col-3">
-            <?= _('Datum') ?>
-            <input type="text" name="end_date" id="end-date" value="<?= strftime('%x', $event->getEnd()) ?>" size="12" required>
-        </label>
-
-        <label class="col-3">
-            <?= _('Uhrzeit') ?>
-
-            <div class="hgroup">
-                <input class="size-s no-hint"
-                       type="text"
-                       name="end_hour"
-                       value="<?= date('G', $event->getEnd()) ?>"
-                       size="2"
-                       aria-label="<?= _("Stunde") ?>"
-                       maxlength="2"<?= $event->isDayEvent() ? ' disabled' : '' ?>>
-                :
-                <input class="size-s no-hint"
-                       type="text"
-                       name="end_minute"
-                       value="<?= date('i', $event->getEnd()) ?>"
-                       size="2"
-                       aria-label="<?= _("Minuten") ?>"
-                       maxlength="2"<?= $event->isDayEvent() ? ' disabled' : '' ?>>
-            </div>
-        </label>
-
-        <label>
-            <span class="required">
-                <?= _('Zusammenfassung') ?>
-            </span>
-
-            <input type="text" size="50" name="summary" id="summary" value="<?= htmlReady($event->getTitle()) ?>">
-        </label>
-
-        <label>
-            <?= _('Beschreibung') ?>
-            <textarea rows="2" cols="40" id="description" name="description"><?= htmlReady($event->getDescription()) ?></textarea>
-        </label>
-
-        <label class="col-3">
-            <?= _('Kategorie') ?>
-            <select name="category_intern" id="category-intern" class="nested-select">
-            <? foreach ($GLOBALS['PERS_TERMIN_KAT'] as $key => $category) : ?>
-                <option value="<?= $key ?>" <?= $key == $event->getCategory() ? 'selected' : '' ?> class="calendar-category<?= $key ?>" data-color-class="calendar-category<?= $key ?>">
-                    <?= htmlReady($category['name']) ?>
-                </option>
-            <? endforeach; ?>
-            </select>
-        </label>
-
-        <label class="col-3">
-            <?= tooltipicon(_('Sie können beliebige Kategorien in das Freitextfeld eingeben. Trennen Sie einzelne Kategorien bitte durch ein Komma.')) ?>
-            <input type="text" name="categories" value="<?= htmlReady($event->getUserDefinedCategories()) ?>"
-                placeholder="<?= _('Eigener Kategoriename') ?>">
-        </label>
-
-        <label>
-            <?= _('Raum/Ort') ?>
-            <input type="text" name="location" id="location" value="<?= htmlReady($event->getLocation()) ?>">
-        </label>
-
-        <? if ($calendar->getPermissionByUser($GLOBALS['user']->id) == Calendar::PERMISSION_OWN) : ?>
-        <? $info = _('Private und vertrauliche Termine sind nur für Sie sichtbar.') ?>
-
-        <? /* SEMBBS nur private und vertrauliche Termine
-        <? $info = _('Private und vertrauliche Termine sind nur für Sie sichtbar. Öffentliche Termine werden auf ihrer internen Homepage auch anderen Nutzern bekanntgegeben.') ?>
-         *
-         */ ?>
-
-        <? elseif ($calendar->getRange() == Calendar::RANGE_SEM) : ?>
-        <? $info = _('In Veranstaltungskalendern können nur private Termine angelegt werden.') ?>
-        <? elseif ($calendar->getRange() == Calendar::RANGE_INST) : ?>
-        <? $info = _('In Einrichtungskalendern können nur private Termine angelegt werden.') ?>
-        <? else : ?>
-        <? $info = _('Im Kalender eines anderen Nutzers können Sie nur private oder vertrauliche Termine einstellen. Vertrauliche Termine sind nur für Sie und den Kalenderbesitzer sichtbar. Alle anderen sehen den Termin nur als Besetztzeit.') ?>
-        <? endif; ?>
-
-        <label for="accessibility">
-            <?= _('Zugriff') ?>
-            <?= tooltipicon($info) ?>
-
-            <select name="accessibility" id="accessibility" size="1">
-                <? foreach ($event->getAccessibilityOptions($calendar->getPermissionByUser($GLOBALS['user']->id)) as $key => $option) : ?>
-                <option value="<?= $key ?>"<?= $event->getAccessibility() == $key ? ' selected' : '' ?>><?= $option ?></option>
-                <? endforeach; ?>
-            </select>
-        </label>
-
-        <label>
-            <?= _('Priorität') ?>
-
-            <? $priority_names = [_('Keine Angabe'), _('Hoch'), _('Mittel'), _('Niedrig')] ?>
-            <select name="priority" id="priority" size="1">
-                <? foreach ($priority_names as $key => $priority) : ?>
-                <option value="<?= $key ?>"<?= $key == $event->getPriority() ? ' selected' : '' ?>><?= $priority ?></option>
-                <? endforeach; ?>
-            </select>
-        </label>
-
-        <? if (!$event->isNew() && Config::get()->CALENDAR_GROUP_ENABLE) : ?>
-            <section>
-                <? $author = $event->getAuthor() ?>
-                <? if ($author) : ?>
-                    <?= sprintf(_('Eingetragen am: %s von %s'),
-                    strftime('%x, %X', $event->mkdate),
-                        htmlReady($author->getFullName('no_title'))) ?>
-                <? endif; ?>
-            </section>
-            <? if ($event->event->mkdate < $event->event->chdate) : ?>
-                <? $editor = $event->getEditor() ?>
-                <? if ($editor) : ?>
-                <section>
-                    <?= sprintf(_('Zuletzt bearbeitet am: %s von %s'),
-                        strftime('%x, %X', $event->chdate),
-                            htmlReady($editor->getFullName('no_title'))) ?>
-                </section>
-                <? endif; ?>
-            <? endif; ?>
-        <? endif; ?>
-    </fieldset>
-
-
-    <fieldset class="collapsed">
-        <legend>
-            <?= _('Wiederholung') ?>
-            <? if ($event->getRecurrence('rtype') != 'SINGLE') : ?>
-                (<?= $event->toStringRecurrence() ?>)
-            <? endif ?>
-        </legend>
-
-        <h2><?= _('Wiederholungsart') ?></h2>
-
-        <section>
-            <? $linterval = $event->getRecurrence('linterval') ?: '1' ?>
-            <? $rec_type = $event->toStringRecurrence(true) ?>
-            <ul class="recurrences">
-                <li>
-                    <label class="rec-label">
-                        <input type="radio" class="rec-select" id="rec-none" name="recurrence" value="single"<?= $event->getRecurrence('rtype') == 'SINGLE' ? ' checked' : '' ?>>
-                        <?= _('Keine') ?>
-                        <?= tooltipIcon(_('Der Termin wird nicht wiederholt.')) ?>
-                    </label>
-                </li>
-                <li>
-                    <label class="rec-label">
-                        <input type="radio" class="rec-select" id="rec-daily" name="recurrence" value="daily"<?= $event->getRecurrence('rtype') == 'DAILY' ? ' checked' : '' ?>>
-                        <?= _('Täglich') ?>
-                    </label>
-
-                    <div class="rec-content" id="rec-content-daily">
-                        <div class="hgroup">
-                            <label>
-                                <input type="radio" name="type_daily" value="day"<?= in_array($rec_type, ['daily', 'xdaily']) ? ' checked' : '' ?>>
-                                <?= sprintf(_('Jeden %s. Tag'), '<input type="text" size="3" name="linterval_d" value="' . $linterval . '">') ?>
-                            </label>
-                        </div>
-
-                        <label>
-                            <input type="radio" name="type_daily" value="workday"<?= $rec_type == 'workdaily' ? ' checked' : '' ?>>
-                            <?= _('Jeden Werktag') ?>
-                        </label>
-                    </div>
-                </li>
-                <li>
-                    <? $wdays = [
-                        '1' => _('Montag'),
-                        '2' => _('Dienstag'),
-                        '3' => _('Mittwoch'),
-                        '4' => _('Donnerstag'),
-                        '5' => _('Freitag'),
-                        '6' => _('Samstag'),
-                        '7' => _('Sonntag')] ?>
-                    <label class="rec-label" for="rec-weekly">
-                        <input type="radio" class="rec-select" id="rec-weekly" name="recurrence" value="weekly"<?= $event->getRecurrence('rtype') == 'WEEKLY' ? ' checked' : '' ?>>
-                        <?= _('Wöchentlich') ?>
-                    </label>
-                    <div class="rec-content" id="rec-content-weekly">
-                        <div class="hgroup">
-                            <label>
-                                <?= sprintf(_('Jede %s. Woche am:'), '<input type="text" size="3" name="linterval_w" value="' . $linterval . '">') ?>
-                            </label>
-                        </div>
-                        <div>
-                            <? $aday = $event->getRecurrence('wdays') ?: date('N', $event->getStart()) ?>
-                            <? foreach ($wdays as $key => $wday) : ?>
-                            <label style="white-space: nowrap;">
-                                <input type="checkbox" name="wdays[]" value="<?= $key ?>"<?= mb_strpos((string) $aday, (string) $key) !== false ? ' checked' : '' ?>>
-                                <?= $wday ?>
-                            </label>
-                            <? endforeach; ?>
-                        </div>
-                    </div>
-                </li>
-                <li>
-                    <? $mdays = [
-                        '1' => _('Ersten'),
-                        '2' => _('Zweiten'),
-                        '3' => _('Dritten'),
-                        '4' => _('Vierten'),
-                        '5' => _('Letzten')] ?>
-                    <? $mdays_options = '' ?>
-                    <? $mday_selected = $event->getRecurrence('sinterval') ?>
-                    <? foreach ($mdays as $key => $mday) :
-                            $mdays_options .= '<option value="' . $key . '"';
-                            if ($key == $mday_selected) {
-                                $mdays_options .= ' selected';
-                            }
-                            $mdays_options .= '>' . $mday . '</option>';
-                    endforeach; ?>
-                    <? $wdays_options = '' ?>
-                    <? $wday_selected = $event->getRecurrence('wdays') ?: date('N', $event->getStart()) ?>
-                    <? foreach ($wdays as $key => $wday) :
-                            $wdays_options .= '<option value="' . $key . '"';
-                            if ($key == $wday_selected) {
-                                $wdays_options .= ' selected';
-                            }
-                            $wdays_options .= '>' . $wday . '</option>';
-                    endforeach; ?>
-
-                    <label class="rec-label" for="rec-monthly">
-                        <input type="radio" class="rec-select" id="rec-monthly" name="recurrence" value="monthly"<?= $event->getRecurrence('rtype') == 'MONTHLY' ? ' checked' : '' ?>>
-                        <?= _('Monatlich') ?>
-                    </label>
-                    <div class="rec-content" id="rec-content-monthly">
-                        <div class="hgroup">
-                            <label>
-                                <input type="radio" value="day" name="type_m"<?= in_array($rec_type, ['mday_monthly', 'mday_xmonthly']) ? ' checked' : '' ?>>
-                                <? $mday = $event->getRecurrence('day') ?: date('j', $event->getStart()) ?>
-                                <?= sprintf(_('Wiederholt am %s. jeden %s. Monat'),
-                                    '<input type="text" name="day_m" size="2" value="'
-                                    . $mday . '">',
-                                    '<input type="text" name="linterval_m1" size="3" value="'
-                                    . $linterval . '">') ?>
-                            </label>
-                        </div>
-                        <div class="hgroup">
-                            <label>
-                                <input type="radio" value="wday" name="type_m"<?= in_array($rec_type, ['xwday_xmonthly', 'lastwday_xmonthly', 'xwday_monthly', 'lastwday_monthly']) ? ' checked' : '' ?>>
-                                <?= sprintf(_('Jeden %s alle %s Monate'),
-                                    '<select size="1" name="sinterval_m">' . $mdays_options . '</select>'
-                                    . '<select size="1" name="wday_m">' . $wdays_options . '</select>',
-                                    '<input type="text" class="no-hint" size="3" maxlength="3" name="linterval_m2" value="'
-                                    . $linterval . '">')?>
-                            </label>
-                        </div>
-                    </div>
-                </li>
-                <li>
-                    <? $months = [
-                        '1' => _('Januar'),
-                        '2' => _('Februar'),
-                        '3' => _('März'),
-                        '4' => _('April'),
-                        '5' => _('Mai'),
-                        '6' => _('Juni'),
-                        '7' => _('Juli'),
-                        '8' => _('August'),
-                        '9' => _('September'),
-                        '10' => _('Oktober'),
-                        '11' => _('November'),
-                        '12' => _('Dezember')] ?>
-                    <? $months_options = '' ?>
-                    <? $month_selected = $event->getRecurrence('month') ?: date('n', $event->getStart()) ?>
-                    <? foreach ($months as $key => $month) :
-                            $months_options .= '<option value="' . $key . '"';
-                            if ($key == $month_selected) {
-                                $months_options .= ' selected';
-                            }
-                            $months_options .= '>' . $month . '</option>';
-                    endforeach; ?>
-
-                    <label class="rec-label" for="rec-yearly">
-                        <input type="radio" class="rec-select" id="rec-yearly" name="recurrence" value="yearly"<?= $event->getRecurrence('rtype') == 'YEARLY' ? ' checked' : '' ?>>
-                        <?= _('Jährlich') ?>
-                    </label>
-                    <div class="rec-content" id="rec-content-yearly">
-                        <div class="hgroup">
-                            <label>
-                                <input type="radio" value="day" name="type_y"<?= $rec_type == 'mday_month_yearly' ? ' checked' : '' ?>>
-                                <?= sprintf(_('Jeden %s. %s'),
-                                    '<input type="text" size="2" maxlength="2" name="day_y" value="'
-                                    . ($event->getRecurrence('day') ?: date('j', $event->getStart())) . '">',
-                                    '<select size="1" name="month_y1">' . $months_options . '</select>') ?>
-                            </label>
-                        </div>
-
-                        <div class="hgroup">
-                            <label>
-                                <input type="radio" value="wday" name="type_y"<?= in_array($rec_type, ['xwday_month_yearly', 'lastwday_month_yearly']) ? ' checked' : '' ?>>
-                                <?= sprintf(_('Jeden %s im %s'),
-                                    '<select size="1" name="sinterval_y">' . $mdays_options . '</select>'
-                                    . '<select size="1" name="wday_y">' . $wdays_options . '</select>',
-                                    '<select size="1" name="month_y2">' . $months_options . '</select>') ?>
-                            </label>
-                        </div>
-                    </div>
-                </li>
-            </ul>
-        </section>
-
-        <h2><?= _('Wiederholung endet') ?></h2>
-
-        <label>
-            <? $checked = (!$event->getRecurrence('expire') || $event->getRecurrence('expire') >= Calendar::CALENDAR_END) && !$event->getRecurrence('count') ?>
-            <input type="radio" name="exp_c" value="never"<?= $checked ? ' checked' : '' ?>>
-            <?= _('Nie') ?>
-        </label>
-
-        <? $checked = $event->getRecurrence('expire') && $event->getRecurrence('expire') < Calendar::CALENDAR_END && !$event->getRecurrence('count') ?>
-
-        <section class="hgroup">
-            <label>
-                <input type="radio" name="exp_c" value="date"<?= $checked ? ' checked' : '' ?>>
-                <? $exp_date = $event->getRecurrence('expire') != Calendar::CALENDAR_END ? $event->getRecurrence('expire') : $event->getEnd() ?>
-                <?= sprintf(_('Am %s'),
-                        '<input type="text" class="size-s" name="exp_date" id="exp-date" value="'
-                        . strftime('%x', $exp_date) . '">') ?>
-            </label>
-        </section>
-
-        <section class="hgroup">
-            <? $checked = $event->getRecurrence('count') ?>
-            <label>
-                <input type="radio" name="exp_c" value="count"<?= $checked ? ' checked' : '' ?>>
-                <? $exp_count = $event->getRecurrence('count') ?: '10' ?>
-                <?= sprintf(_('Nach %s Wiederholungen'),
-                        '<input type="text" size="5" name="exp_count" value="'
-                        . $exp_count . '">') ?>
-            </label>
-        </section>
-
-
-        <label for="exc-dates">
-            <?= _('Ausnahmen') ?>
-        </label>
-
-        <ul id="exc-dates">
-            <? $exceptions = $event->getExceptions(); ?>
-            <? sort($exceptions, SORT_NUMERIC); ?>
-            <? foreach ($exceptions as $exception) : ?>
-            <li>
-                <label class="undecorated">
-                    <input type="checkbox" name="del_exc_dates[]" value="<?= strftime('%d.%m.%Y', $exception) ?>" style="display: none;">
-                    <span><?= strftime('%x', $exception) ?><?= Icon::create('trash', 'clickable', ['title' => _('Ausnahme löschen')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?></span>
-                </label>
-                <input type="hidden" name="exc_dates[]" value="<?= strftime('%d.%m.%Y', $exception) ?>">
-            </li>
-            <? endforeach; ?>
-        </ul>
-
-        <div class="hgroup">
-            <input style="vertical-align: top; opacity: 0.8;"
-                   type="text" size="12" name="exc_date" id="exc-date" value=""
-                   placeholder="<?= _("Datum eingeben") ?>">
-            <span style="vertical-align: top;" onclick="STUDIP.CalendarDialog.addException(); return false;">
-                <?= Icon::create('add', 'clickable', ['title' => _('Ausnahme hinzufügen')])->asInput(['class' => 'text-bottom']) ?>
-            </span>
-        </div>
-    </fieldset>
-
-    <? if (Config::get()->CALENDAR_GROUP_ENABLE && $calendar->getRange() == Calendar::RANGE_USER) : ?>
-        <?= $this->render_partial('calendar/group/_attendees') ?>
-    <? endif; ?>
-
-    <footer data-dialog-button>
-        <?= Button::create(_('Speichern'), 'store', ['title' => _('Termin speichern')]) ?>
-
-        <? if (!$event->isNew()) : ?>
-        <? if ($event->getRecurrence('rtype') != 'SINGLE') : ?>
-        <?= LinkButton::create(_('Aus Serie löschen'), $controller->url_for('calendar/single/delete_recurrence/' . implode('/', $event->getId()) . '/' . $atime)) ?>
-        <? endif; ?>
-        <?= LinkButton::create(_('Löschen'), $controller->url_for('calendar/single/delete/' . implode('/', $event->getId()))) ?>
-        <? endif; ?>
-        <? if (!Request::isXhr()) : ?>
-        <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view, [$event->getStart()])) ?>
-        <? endif; ?>
-    </footer>
-</form>
-<script>
-    jQuery('#start-date').datepicker({
-        altField: '#end-date'
-    });
-    jQuery('#end-date').datepicker();
-    jQuery('#exp-date').datepicker();
-    jQuery('#exc-date').datepicker();
-
-    $('ul.recurrences input[type=radio][id^=rec]').bind('change', function() {
-        $('.rec-content').hide();
-
-        if ($(this).is(':checked')) {
-            $(this).parent().siblings('.rec-content').show();
-        }
-    })
-</script>
diff --git a/app/views/calendar/single/edit_status.php b/app/views/calendar/single/edit_status.php
deleted file mode 100644
index 3e3bfc3e6d99678b64e8aa165c6fb26d4d6f022e..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/edit_status.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<form action="<?= $controller->url_for($base . 'edit_status/' . $range_id . '/' . $event->event_id) ?>" method="post">
-    <?= $this->render_partial('calendar/single/_event_data') ?>
-</form>
diff --git a/app/views/calendar/single/event.php b/app/views/calendar/single/event.php
deleted file mode 100644
index e9d544f8199f2a6d8f06a51ed9055120bf7ba284..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/event.php
+++ /dev/null
@@ -1 +0,0 @@
-<?= $this->render_partial('calendar/single/_event_data') ?>
diff --git a/app/views/calendar/single/export_calendar.php b/app/views/calendar/single/export_calendar.php
deleted file mode 100644
index 66f86c5dbb6ea0259a053a54b03882211ad0388b..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/export_calendar.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<? use Studip\Button, Studip\LinkButton; ?>
-<? if (Request::isXhr()) : ?>
-    <? foreach (PageLayout::getMessages() as $messagebox) : ?>
-        <?= $messagebox ?>
-    <? endforeach ?>
-<? endif; ?>
-<form action="<?= $controller->url_for('calendar/single/export_calendar/' . $calendar->getRangeId(), ['atime' => $atime, 'last_view' => $last_view]) ?>" method="post" name="sync_form" id="calendar_sync" class="default">
-    <fieldset>
-        <legend>
-            <?= sprintf(_('Termine exportieren')) ?>
-        </legend>
-
-        <label for="event-type">
-            <?= _('Welche Termine sollen exportiert werden') ?>:
-
-            <select name="event_type" id="event-type" size="1">
-                <option value="user" selected><?= _('Nur eigene Termine') ?></option>
-                <option value="course"><?= _('Nur Veranstaltungs-Termine') ?></option>
-                <option value="all"><?= _('Alle Termine') ?></option>
-            </select>
-        </label>
-
-        <label>
-            <input type="radio" name="export_time" value="all" id="export-all" checked>
-            <?= _('Alle Termine exportieren') ?>
-        </label>
-
-        <label>
-                <input type="radio" name="export_time" value="date" id="export-date">
-                <?= _('Nur Termin in folgendem Zeitraum exportieren') ?>
-        </label>
-
-        <section class="hgroup">
-            <? $start = strtotime('now') ?>
-            <? $end = strtotime('+1 year') ?>
-            <input id="export-start" type="text" name="export_start" class="no-hint"
-                maxlength="10" class="hasDatepicker" value="<?= strftime('%x', $start) ?>">
-            <input id="export-end" type="text" name="export_end" class="no-hint"
-                maxlength="10" class="hasDatepicker" value="<?= strftime('%x', $end) ?>">
-        </section>
-    </fieldset>
-
-    <footer data-dialog-button>
-        <?= Button::createAccept(_('Termine exportieren'), 'export', ['title' => _('Termine exportieren')]) ?>
-
-        <? if (!Request::isXhr()) : ?>
-            <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view)) ?>
-        <? endif; ?>
-    </footer>
-</form>
-<script>
-    jQuery('#export-start').datepicker();
-    jQuery('#export-end').datepicker();
-</script>
diff --git a/app/views/calendar/single/manage_access.php b/app/views/calendar/single/manage_access.php
deleted file mode 100644
index 4f7be8f86b8e952f36d73493981b59ba15b50fae..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/manage_access.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<? if (Request::isXhr()) : ?>
-    <? foreach (PageLayout::getMessages() as $messagebox) : ?>
-        <?= $messagebox ?>
-    <? endforeach ?>
-<? endif; ?>
-<form id="calendar-manage-access" data-dialog="" method="post" action="<?= $controller->url_for('calendar/single/store_permissions/' . $calendar->getRangeId()) ?>">
-    <? CSRFProtection::tokenTag() ?>
-    <? $perms = [1 => _('Keine'), 2 => _('Lesen'), 4 => _('Schreiben')] ?>
-    <table class="default">
-        <caption>
-            <?= _('Bestehende Freigaben') ?>
-            <span class="actions" style="font-size: 0.8em;">
-                <label>
-                    <?= _('Auswahl') ?>:
-                    <select name="group_filter" size="1" class="submit-upon-select">
-                        <option value="list"<?= $group_filter_selected == 'list' ? ' selected' : '' ?>><?= _('Alle Personen anzeigen') ?></option>
-                        <? foreach ($filter_groups as $filter_group) : ?>
-                        <option value="<?= $filter_group->getId() ?>"<?= $group_filter_selected == $filter_group->getId() ? ' selected' : '' ?>><?= htmlReady($filter_group->name) ?></option>
-                        <? endforeach; ?>
-                    </select>
-                </label>
-                <?= Icon::create('accept', 'clickable')
-                      ->asInput([
-                        'id' => "calendar-group-submit",
-                        'name' => "calendar_group_submit",
-                        'class' => "text-top"]) ?>
-                <span style="padding-left: 1em;">
-                    <?= $mps->render() ?>
-                </span>
-                <script>
-                    STUDIP.MultiPersonSearch.init();
-                </script>
-            </span>
-        </caption>
-        <thead>
-            <tr>
-                <th>
-                    <?= _('Name') ?>
-                </th>
-                <th>
-                    <?= _('Berechtigung') ?>
-                </th>
-                <th>
-                    <?= _('Eigene Berechtigung') ?>
-                </th>
-                <th class="actions">
-                    <?= _('Aktionen') ?>
-                </th>
-            </tr>
-        </thead>
-        <tbody>
-            <? foreach ($users as $header => $usergroup): ?>
-                <tr id="letter_<?= $header ?>" class="calendar-user-head">
-                    <th colspan="4">
-                        <?= $header ?>
-                    </th>
-                </tr>
-                <? foreach ($usergroup as $user): ?>
-                    <tr id="contact_<?= $user->user_id ?>">
-                        <td>
-                            <?= ObjectdisplayHelper::avatarlink($user->user) ?>
-                        </td>
-                        <td style="white-space: nowrap;">
-                            <label>
-                                <input type="radio" name="perm[<?= $user->user_id ?>]" value="<?= Calendar::PERMISSION_FORBIDDEN ?>"
-                                       <?= $user->permission < Calendar::PERMISSION_READABLE ? ' checked' : '' ?>>
-                                <?= _('Keine') ?>
-                            </label>
-                            <label>
-                                <input type="radio" name="perm[<?= $user->user_id ?>]" value="<?= Calendar::PERMISSION_READABLE ?>"
-                                    <?= $user->permission == Calendar::PERMISSION_READABLE ? ' checked' : '' ?>>
-                                <?= _('Lesen') ?>
-                            </label>
-                            <label>
-                                <input type="radio" name="perm[<?= $user->user_id ?>]" value="<?= Calendar::PERMISSION_WRITABLE ?>"
-                                    <?= $user->permission == Calendar::PERMISSION_WRITABLE ? ' checked' : '' ?>>
-                                <?= _('Schreiben') ?>
-                            </label>
-                        </td>
-                        <td>
-                            <?= $perms[$own_perms[$user->user_id]] ?>
-                        </td>
-                        <td class="actions">
-                            <a title="<?= _('Benutzer entfernen') ?>" onClick="STUDIP.CalendarDialog.removeUser(this);" href="<?= $controller->url_for('calendar/single/remove_user/' . $calendar->getRangeId(), ['user_id' => $user->user_id]) ?>">
-                                <?= Icon::create('trash', 'clickable')->asImg() ?>
-                            </a>
-                        </td>
-                    </tr>
-                <? endforeach; ?>
-            <? endforeach; ?>
-        </tbody>
-    </table>
-    <div style="text-align: center;" data-dialog-button>
-        <?= Studip\Button::create(_('Speichern'), 'store') ?>
-        <? if (!Request::isXhr()) : ?>
-        <?= Studip\LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view)) ?>
-        <? endif; ?>
-    </div>
-</form>
diff --git a/app/views/calendar/single/month.php b/app/views/calendar/single/month.php
deleted file mode 100644
index da715f5e0fe531e93f8fb5cb236226f26f75b037..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/month.php
+++ /dev/null
@@ -1,109 +0,0 @@
-<nav class="calendar-nav" style="vertical-align: middle">
-    <span style="white-space: nowrap;">
-        <a class="hidden-medium-down" style="padding-right: 2em;" href="<?= $controller->url_for('calendar/single/month', ['atime' => strtotime('-1 year', $atime)]) ?>">
-            <?= Icon::create('arr_2left', 'clickable', ['title' => _('Ein Jahr zurück')])->asImg(['style' => 'vertical-align: text-top;']) ?>
-            <?= strftime('%B %Y', strtotime('-1 year', $atime)) ?>
-        </a>
-        <a class="hidden-tiny-down" href="<?= $controller->url_for('calendar/single/month', ['atime' => strtotime('-1 month', $atime)]) ?>">
-            <?= Icon::create('arr_1left', 'clickable', ['title' => _('Einen Monat zurück')])->asImg(['style' => 'vertical-align: text-top;']) ?>
-            <?= strftime('%B %Y', strtotime('-1 month', $atime)) ?>
-        </a>
-    </span>
-
-    <?
-    $calType = 'month';
-    $calLabel = htmlReady(strftime("%B ", $calendars[15]->getStart())) .' '. date('Y', $calendars[15]->getStart());
-    ?>
-
-    <?= $this->render_partial('calendar/single/_calhead', compact('atime', 'calType', 'calLabel')) ?>
-
-    <span style="text-align: right; white-space: nowrap;">
-        <a class="hidden-tiny-down" style="padding-right: 2em;" href="<?= $controller->url_for('calendar/single/month', ['atime' => strtotime('+1 month', $atime)]) ?>">
-            <?= strftime('%B %Y', strtotime('+1 month', $atime)) ?>
-            <?= Icon::create('arr_1right', 'clickable', ['title' => _('Einen Monat vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-        </a>
-        <a class="hidden-medium-down" href="<?= $controller->url_for('calendar/single/month', ['atime' => strtotime('+1 year', $atime)]) ?>">
-            <?= strftime('%B %Y', strtotime('+1 year', $atime)) ?>
-            <?= Icon::create('arr_2right', 'clickable', ['title' => _('Ein Jahr vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-        </a>
-    </span>
-</nav>
-
-<div class="table-scrollbox-horizontal">
-<table class="calendar-month">
-    <thead>
-        <tr class="calendar-month-weekdays">
-            <? $week_days = [39092400, 39178800, 39265200, 39351600, 39438000, 39524400, 39610800]; ?>
-            <? foreach ($week_days as $week_day) : ?>
-                <td class="precol1w">
-                    <?= strftime('%a', $week_day) ?>
-                </td>
-            <? endforeach; ?>
-            <td align="center" class="precol1w">
-                <?= _('Woche') ?>
-            </td>
-        </tr>
-    </thead>
-    <tbody>
-        <? for ($i = $first_day, $j = 0; $i <= $last_day; $i += 86400, $j++) : ?>
-            <? $aday = date('j', $i); ?>
-            <?
-            $class_day = '';
-            if (($aday - $j - 1 > 0) || ($j - $aday > 6)) {
-                $class_cell = 'lightmonth';
-                $class_day = 'light';
-            } elseif (date('Ymd', $i) == date('Ymd')) {
-                $class_cell = 'celltoday';
-            } else {
-                $class_cell = 'month';
-            }
-            $hday = holiday($i);
-
-            if ($j % 7 == 0) {
-                ?><tr><?
-            }
-            ?>
-            <td class="<?= $class_cell ?>">
-            <? if (($j + 1) % 7 == 0) : ?>
-                <a class="<?= $class_day . 'sday' ?>" href="<?= $controller->url_for('calendar/single/day', ['atime' => $i]) ?>">
-                    <?= $aday ?>
-                </a>
-                <? if (!empty($hday['name'])) : ?>
-                    <div style="color: #aaaaaa;" class="inday"><?= $hday['name'] ?></div>
-                <? endif; ?>
-                <? foreach ($calendars[$j]->events as $event) : ?>
-                    <div data-tooltip>
-                        <a data-dialog="size=auto" title="<?= _('Termin bearbeiten') ?>" class="inday <?= $event instanceof CourseEvent ? 'calendar-course-event-text' : 'calendar-event-text' ?><?= $event->getCategory() ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendars[$j]->getRangeId() . '/' . $event->event_id, ['atime' => $event->getStart()]) ?>"><?= htmlReady($event->getTitle()) ?></a>
-                        <?= $this->render_partial('calendar/single/_tooltip', ['event' => $event, 'calendar' => $calendars[$j]]) ?>
-                    </div>
-                <? endforeach; ?>
-                </td>
-                    <td class="lightmonth calendar-month-week">
-                    <a style="font-weight: bold;" class="calhead" href="<?= $controller->url_for('calendar/single/week', ['atime' => $i]) ?>"><?= strftime("%V", $i) ?></a>
-                    </td>
-                </tr>
-            <? else : ?>
-                <? $hday_class = ['day', 'day', 'shday', 'hday'] ?>
-                <? if (!empty($hday['col'])) : ?>
-                    <a class="<?= $class_day . $hday_class[$hday['col']] ?>" href="<?= $controller->url_for('calendar/single/day', ['atime' => $i]) ?>">
-                        <?= $aday ?>
-                    </a>
-                    <div style="color: #aaaaaa;" class="inday"><?= $hday['name'] ?></div>
-                <? else : ?>
-                    <a class="<?= $class_day . 'day' ?>" href="<?= $controller->url_for('calendar/single/day', ['atime' => $i]) ?>">
-                        <?= $aday ?>
-                    </a>
-                <? endif; ?>
-                <? foreach ($calendars[$j]->events as $event) : ?>
-                    <div data-tooltip>
-                        <a data-dialog="size=auto" title="<?= _('Termin bearbeiten') ?>" class="inday <?= $event instanceof CourseEvent ? 'calendar-course-event-text' : 'calendar-event-text' ?><?= $event->getCategory() ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendars[$j]->getRangeId() . '/' . $event->event_id, ['atime' => $event->getStart()]) ?>"><?= htmlReady($event->getTitle()) ?></a>
-                        <?= $this->render_partial('calendar/single/_tooltip', ['event' => $event, 'calendar' => $calendars[$j]]) ?>
-                    </div>
-                <? endforeach; ?>
-                </td>
-            <? endif; ?>
-        <? endfor; ?>
-        </tr>
-    </tbody>
-</table>
-</div>
diff --git a/app/views/calendar/single/seminar_events.php b/app/views/calendar/single/seminar_events.php
deleted file mode 100644
index 06ed5faefdeaad8a62a416704e43bfca81e70819..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/seminar_events.php
+++ /dev/null
@@ -1,104 +0,0 @@
-<? if (!empty($sem_courses)) : ?>
-    <?= $this->render_partial('calendar/single/_semester_filter') ?>
-    <? $_order = (!$order_by || $order == 'desc') ? 'asc' : 'desc' ?>
-    <form action="<?= $controller->url_for('calendar/single/store_selected_sem') ?>" method="post">
-        <?= CSRFProtection::tokenTag() ?>
-        <div id="my_seminars">
-            <? foreach ($sem_courses as $sem_key => $course_group) : ?>
-                <table class="default collapsable">
-                    <caption>
-                        <?= htmlReady($sem_data[$sem_key]['name'] ?? _('Unbekanntes Semester')) ?>
-                    </caption>
-                    <colgroup>
-                        <col width="7px">
-                        <col width="25px">
-                        <? if ($config_sem_number) : ?>
-                            <col width="10%">
-                        <? endif ?>
-                        <col>
-                        <col width="45px">
-                        <col width="10%">
-                    </colgroup>
-                    <thead>
-                    <tr class="sortable">
-                        <th></th>
-                        <th></th>
-                        <? if ($config_sem_number) : ?>
-                            <th class=<?= ($order_by == 'veranstaltungsnummer') ? ($order == 'desc') ? 'sortdesc' : 'sortasc' : '' ?>>
-                                <a href="<?= $controller->url_for(sprintf('my_courses/index/veranstaltungsnummer/%s', $_order)) ?>">
-                                    <?= _('Nr.') ?>
-                                </a>
-                            </th>
-                        <? endif ?>
-                        <th
-                            class=<?= ($order_by == 'name') ? ($order == 'desc') ? 'sortdesc' : 'sortasc' : '' ?>>
-                            <a href="<?= $controller->url_for(sprintf('calendar/single/seminar_events/name/%s', $_order)) ?>" data-dialog="size=auto">
-                                <?= _('Name') ?>
-                            </a>
-                        </th>
-                        <th></th>
-                        <th><?= _('Auswahl') ?></th>
-                    </tr>
-                    </thead>
-                <? foreach ($course_group as $course)  : ?>
-                    <? $sem_class = $course['sem_class']; ?>
-                    <tr>
-                        <td class="gruppe<?= $course['gruppe'] ?>"></td>
-                        <td>
-                            <? if ($sem_class['studygroup_mode']) : ?>
-                                <?= StudygroupAvatar::getAvatar($course['seminar_id'])->getImageTag(Avatar::SMALL, ['title' => $course['name']])
-                                ?>
-                            <? else : ?>
-                                <?= CourseAvatar::getAvatar($course['seminar_id'])->getImageTag(Avatar::SMALL, ['title' => $course['name']])
-                                ?>
-                            <? endif ?>
-                        </td>
-                        <? if($config_sem_number) :?>
-                            <td><?= $course['veranstaltungsnummer']?></td>
-                        <? endif?>
-                        <td style="text-align: left">
-                            <a href="<?= URLHelper::getLink('seminar_main.php', ['auswahl' => $course['seminar_id']]) ?>"
-                                <?= $course['visitdate'] <= $course['chdate'] ? 'style="color: red;"' : '' ?>>
-                                <?= htmlReady($course['name']) ?>
-                                <?= ($course['is_deputy'] ? ' ' . _('[Vertretung]') : '');?>
-                            </a>
-                            <? if ($course['visible'] == 0) : ?>
-                                <?= _('[versteckt]') ?>
-                            <? endif ?>
-                        </td>
-                        <td>
-                            <? if (!$sem_class['studygroup_mode']) : ?>
-                                <a data-dialog href="<?= $controller->url_for(sprintf('course/details/index/%s', $course['seminar_id'])) ?>">
-                                    <? $params = tooltip2(_('Veranstaltungsdetails')); ?>
-                                    <? $params['style'] = 'cursor: pointer'; ?>
-                                    <?= Icon::create('info-circle', 'inactive')->asImg(20, $params) ?>
-                                </a>
-                            <? else : ?>
-                                <?= Assets::img('blank.gif', ['width'  => 20, 'height' => 20]); ?>
-                            <? endif ?>
-                        </td>
-                        <td style="text-align: center;">
-                            <input type="hidden" name="selected_sem[<?= $course['seminar_id'] ?>]" value="0">
-                            <input type="checkbox" name="selected_sem[<?= $course['seminar_id'] ?>]" value="1"<?= in_array($course['seminar_id'], $bind_calendar) ? ' checked' : '' ?>>
-                        </td>
-                    </tr>
-                <? endforeach ?>
-                </table>
-            <? endforeach ?>
-        </div>
-        <div style="text-align: center;" data-dialog-button>
-            <?= Studip\Button::create(_('Speichern'), 'store') ?>
-            <? if (!Request::isXhr()) : ?>
-            <?= Studip\LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view)) ?>
-            <? endif; ?>
-        </div>
-    </form>
-<? else : ?>
-    <?= PageLayout::postMessage(MessageBox::info(_('Es wurden keine Veranstaltungen gefunden. Mögliche Ursachen:'), [
-        sprintf(_('Sie haben zur Zeit keine Veranstaltungen belegt, an denen Sie teilnehmen können.<br>Bitte nutzen Sie %s<b>Veranstaltung suchen / hinzufügen</b>%s um sich für Veranstaltungen anzumelden.'),'<a href="' . URLHelper::getLink('dispatch.php/search/courses') . '">', '</a>'),
-        _('In dem ausgewählten <b>Semester</b> wurden keine Veranstaltungen belegt.').'<br>'._('Wählen Sie links im <b>Semesterfilter</b> ein anderes Semester aus')
-    ]))?>
-<? endif ?>
-<? if (!empty($my_bosses) && is_array($my_bosses) && count($my_bosses)) : ?>
-    <?= $this->render_partial('my_courses/_deputy_bosses'); ?>
-<? endif ?>
diff --git a/app/views/calendar/single/week.php b/app/views/calendar/single/week.php
deleted file mode 100644
index 98f831773e53922bc82a98c595b0b56a1490893d..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/week.php
+++ /dev/null
@@ -1,199 +0,0 @@
-<?
-$at = date('G', $atime);
-if ($at >= $settings['start']
-    && $at <= $settings['end'] || !$atime) {
-    $start = $settings['start'];
-    $end = $settings['end'];
-} elseif ($at < $settings['start']) {
-    $start = 0;
-    $end = $settings['start'] + 2;
-} else {
-    $start = $settings['end'] - 2;
-    $end = 23;
-}
-$tab_arr = [];
-$max_columns = 0;
-$week_type = $settings['type_week'] == 'SHORT' ? 5 : 7;
-$rows = ($end - $start + 1) * 3600 / $settings['step_week'];
-
-for ($i = 0; $i < $week_type; $i++) {
-    $tab_arr[$i] = $calendars[$i]->createEventMatrix($start * 3600, $end * 3600, $settings['step_week']);
-    if ($tab_arr[$i]['max_cols']) {
-        $max_columns += ($tab_arr[$i]['max_cols'] + 1);
-    } else {
-        $max_columns++;
-    }
-}
-
-$rowspan = ceil(3600 / $settings['step_week']);
-$height = ' height="20"';
-
-if ($rowspan > 1) {
-    $colspan_1 = ' colspan="2"';
-    $colspan_2 = $max_columns + 4;
-    $width_daycols = 100 - (4 + $week_type) * 0.1;
-} else {
-    $colspan_1 = '';
-    $colspan_2 = $max_columns + 2;
-    $width_daycols = 100 - (2 + $week_type) * 0.1;
-}
-?>
-
-<nav class="calendar-nav" style="vertical-align: middle">
-    <span style="white-space: nowrap;">
-        <a href="<?= $controller->url_for('calendar/single/week', ['atime' => strtotime('-1 week', $atime)]) ?>">
-            <?= Icon::create('arr_1left', 'clickable', ['title' => _('Eine Woche zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-            <span class="hidden-tiny-down"><?= sprintf(_('%u. Woche'), strftime('%V', strtotime('-1 week', $atime))) ?></span>
-        </a>
-    </span>
-
-    <?
-    $calType = 'week';
-    $calLabel = $this->render_partial('calendar/single/_calhead_label_week', compact('week_type'));
-    ?>
-
-    <?= $this->render_partial('calendar/single/_calhead', compact('atime', 'calType', 'calLabel')) ?>
-
-    <span style="white-space: nowrap; text-align: right;">
-        <a href="<?= $controller->url_for('calendar/single/week', ['atime' => strtotime('+1 week', $atime)]) ?>">
-            <span class="hidden-tiny-down"><?= sprintf(_('%u. Woche'), strftime('%V', strtotime('+1 week', $atime))) ?></span>
-            <?= Icon::create('arr_1right', 'clickable', ['title' => _('Eine Woche vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-        </a>
-    </span>
-</nav>
-
-<table id="main_content" class="calendar-week">
-    <colgroup>
-        <col style="max-width: 1.5em; width: 1.5em;">
-        <? if ($rowspan > 1) : ?>
-            <col style="max-width: 1.5em; width: 1.5em;">
-        <? endif; ?>
-        <? for ($i = 0; $i < $week_type; $i++) : ?>
-            <? if ($tab_arr[$i]['max_cols'] > 0) : ?>
-                <? $event_cols = $tab_arr[$i]['max_cols'] ?: 1; ?>
-                <col span="<?= $event_cols ?>" style="width: <?= 100 / $week_type / $event_cols ?>%">
-                <col style="max-width: 0.9em; width: 0.9em;">
-            <? else : ?>
-                <col style="width: <?= 100 / $week_type ?>%">
-            <? endif; ?>
-        <? endfor; ?>
-        <col class="hidden-tiny-down" style="max-width: 1.5em; width: 1.5em;">
-        <? if ($rowspan > 1) : ?>
-            <col class="hidden-tiny-down" style="max-width: 1.5em; width: 1.5em;">
-        <? endif; ?>
-    </colgroup>
-    <thead>
-        <tr>
-            <td style="text-align: center; white-space: nowrap;" <?= $colspan_1 ?>>
-                <? if ($start > 0) : ?>
-                    <a href="<?= $controller->url_for('calendar/single/week', ['atime' => mktime($start - 1, 0, 0, date('n', $atime), date('j', $atime), date('Y', $atime))]) ?>">
-                        <?= Icon::create('arr_1up', 'clickable', ['title' => _('Früher')])->asImg() ?>
-                    </a>
-                <? endif ?>
-            </td>
-            <? // weekday and date as title for each column ?>
-            <? for ($i = 0; $i < $week_type; $i++) : ?>
-                <td style="text-align:center; font-weight:bold;"<?= ($tab_arr[$i]['max_cols'] > 0 ? ' colspan="' . ($tab_arr[$i]['max_cols'] + 1) . '"' : '' ) ?>>
-                    <a class="calhead" href="<?= $controller->url_for('calendar/single/day', ['atime' => $calendars[$i]->getStart()]) ?>">
-                        <span class="hidden-tiny-down"><?= strftime('%a', $calendars[$i]->getStart()) ?></span> <?= date('d', $calendars[$i]->getStart()) ?>
-                    </a>
-                    <? if ($holiday = holiday($calendars[$i]->getStart())) : ?>
-                        <div class="hidden-tiny-down" style="font-size:9pt; color:#bbb; height:auto; overflow:visible; font-weight:bold;"><?= $holiday['name'] ?></div>
-                    <? endif ?>
-                </td>
-            <? endfor ?>
-            <td style="text-align: center; white-space: nowrap;" <?= $colspan_1 ?>>
-                <? if ($start > 0) : ?>
-                    <a href="<?= $controller->url_for('calendar/single/week', ['atime' => mktime($start - 1, 0, 0, date('n', $calendars[0]->getStart()), date('j', $calendars[0]->getStart()), date('Y', $calendars[0]->getStart()))]) ?>">
-                        <?= Icon::create('arr_1up', 'clickable', ['title' => _('Früher')])->asImg() ?>
-                    </a>
-                <? endif ?>
-            </td>
-        </tr>
-    </thead>
-    <tbody>
-        <tr>
-            <? // Zeile mit Tagesterminen ausgeben ?>
-            <td class="precol1w"<?= $colspan_1 ?> height="20">
-                <?= _("Tag") ?>
-            </td>
-            <? for ($i = 0; $i < $week_type; $i++) : ?>
-                <?
-                if (date('Ymd', $calendars[$i]->getStart()) == date('Ymd')) {
-                    $class_cell = 'celltoday';
-                } else {
-                    $class_cell = '';
-                }
-                ?>
-                <?= $this->render_partial('calendar/single/_day_dayevents', ['em' => $tab_arr[$i], 'calendar' => $calendars[$i], 'class_cell' => $class_cell]) ?>
-            <? endfor ?>
-            <td class="precol1w"<?= $colspan_1 ?>>
-                <?= _('Tag') ?>
-            </td>
-        </tr>
-        <? $j = $start ?>
-        <? for ($i = 0; $i < $rows; $i++) : ?>
-            <tr>
-                <? if ($i % $rowspan == 0) : ?>
-                    <? if ($rowspan == 1) : ?>
-                        <td class="precol1w"<?= $height ?>><?= $j ?></td>
-                    <?  else : ?>
-                        <td class="precol1w" rowspan="<?= $rowspan ?>"><?= $j ?></td>
-                    <? endif ?>
-                <? endif ?>
-                <? if ($rowspan > 1) : ?>
-                    <? $minutes = (60 / $rowspan) * ($i % $rowspan); ?>
-                    <? if ($minutes == 0) : ?>
-                        <td class="precol2w"<?= $height ?>>00</td>
-                    <? else : ?>
-                        <td class="precol2w"<?= $height ?>><?= $minutes ?></td>
-                    <? endif ?>
-                <? endif ?>
-                <? for ($y = 0; $y < $week_type; $y++) : ?>
-                    <?
-                    if (date('Ymd', $calendars[$y]->getStart()) == date('Ymd')) {
-                        $class_cell = 'celltoday';
-                    } else {
-                        $class_cell = '';
-                    }
-                    ?>
-                    <?= $this->render_partial('calendar/single/_day_cell', ['calendar' => $calendars[$y], 'em' => $tab_arr[$y], 'row' => $i, 'start' => $start * 3600, 'i' => $i + ($start * 3600 / $settings['step_week']), 'step' => $settings['step_week'], 'class_cell' => $class_cell]); ?>
-                <? endfor ?>
-                <? if ($rowspan > 1) : ?>
-                    <? if ($minutes == 0) : ?>
-                        <td class="precol2w"<?= $height ?>>00</td>
-                    <? else : ?>
-                        <td class="precol2w"<?= $height ?>><?= $minutes ?></td>
-                    <? endif ?>
-                <? endif ?>
-                <? if (($i + 2) % $rowspan == 0) : ?>
-                    <? if ($rowspan == 1) : ?>
-                        <td class="precol1w"<?= $height ?>><?= $j ?></td>
-                    <?  else : ?>
-                        <td class="precol1w" rowspan="<?= $rowspan ?>"><?= $j ?></td>
-                    <? endif ?>
-                    <? $j = $j + ceil($settings['step_week'] / 3600); ?>
-                <? endif ?>
-            </tr>
-        <? endfor ?>
-    </tbody>
-    <tfoot>
-        <tr>
-            <td<?= $colspan_1 ?> style="text-align:center;">
-                <? if ($end < 23) : ?>
-                    <a href="<?= $controller->url_for('calendar/single/week', ['atime' => mktime($end + 1, 0, 0, date('n', $calendars[0]->getStart()), date('j', $calendars[0]->getStart()), date('Y', $calendars[0]->getStart()))]) ?>">
-                        <?= Icon::create('arr_1down', 'clickable', ['title' => _('Später')])->asImg() ?>
-                    </a>
-                <? endif ?>
-            </td>
-            <td colspan="<?= $max_columns ?>">&nbsp;</td>
-            <td<?= $colspan_1 ?> style="text-align:center;">
-                <? if ($end < 23) : ?>
-                    <a href="<?= $controller->url_for('calendar/single/week', ['atime' => mktime($end + 1, 0, 0, date('n', $calendars[0]->getStart()), date('j', $calendars[0]->getStart()), date('Y', $calendars[0]->getStart()))]) ?>">
-                        <?= Icon::create('arr_1down', 'clickable', ['title' => _('Später')])->asImg() ?>
-                    </a>
-                <? endif ?>
-            </td>
-        </tr>
-    </tfoot>
-</table>
diff --git a/app/views/calendar/single/year.php b/app/views/calendar/single/year.php
deleted file mode 100644
index 0ebafe1431e387cffe983578dd5a5ff4d976b13c..0000000000000000000000000000000000000000
--- a/app/views/calendar/single/year.php
+++ /dev/null
@@ -1,145 +0,0 @@
-<div class="calendar-single-year">
-
-    <nav class="calendar-nav" style="vertical-align: middle">
-        <span style="white-space: nowrap;">
-            <a href="<?= $controller->url_for('calendar/single/year', ['atime' => strtotime('-1 year', $atime)]) ?>">
-                <?= Icon::create('arr_2left', 'clickable', ['title' => _('Ein Jahr zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-                <?= strftime('%Y', strtotime('-1 year', $atime)) ?>
-            </a>
-        </span>
-
-        <?
-        $calType = 'year';
-        $calLabel = date('Y', $calendar->getStart());
-        ?>
-
-        <?= $this->render_partial('calendar/single/_calhead', compact('calendar', 'atime', 'calType', 'calLabel')) ?>
-
-        <span style="text-align: right; white-space: nowrap;">
-            <a href="<?= $controller->url_for('calendar/single/year', ['atime' => strtotime('+1 year', $atime)]) ?>">
-                <?= strftime('%Y', strtotime('+1 year', $atime)) ?>
-                <?= Icon::create('arr_2right', 'clickable', ['title' => _('Ein Jahr vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?>
-            </a>
-        </span>
-    </nav>
-
-    <div class="table-scrollbox-horizontal">
-        <table class="calendar-single-year--table" width="100%">
-            <? $days_per_month = [31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
-            if (date('L', $calendar->getStart())) {
-                $days_per_month[2]++;
-            }
-            ?>
-
-            <thead>
-                <tr>
-                    <? $ts_month = 0; ?>
-                    <? for ($i = 1; $i < 13; $i++) : ?>
-                        <?  $ts_month += ( $days_per_month[$i] - 1) * 86400; ?>
-                        <th align="center" width="8%">
-                            <a class="calhead" href="<?= $controller->url_for('calendar/single/month', ['atime' => $calendar->getStart() + $ts_month]) ?>">
-                                <b><?= strftime('%B', $ts_month); ?></b>
-                            </a>
-                        </th>
-                    <? endfor; ?>
-                </tr>
-            </thead>
-
-            <tbody>
-                <?
-                $now = date('Ymd');
-                $count = 0;
-                ?>
-                <? for ($i = 1; $i < 32; $i++) : ?>
-                    <tr>
-                        <? for ($month = 1; $month < 13; $month++) : ?>
-
-                            <? $aday = mktime(12, 0, 0, $month, $i, date('Y', $calendar->getStart())); ?>
-                            <? $iday = date('Ymd', $aday); ?>
-                            <? if ($i <= $days_per_month[$month]) : ?>
-                                <? $wday = date('w', $aday);
-                                // emphasize current day
-                                if (date('Ymd', $aday) == $now) {
-                                    $day_class = ' class="celltoday"';
-                                } else if ($wday == 0 || $wday == 6) {
-                                    $day_class = ' class="weekend"';
-                                } else {
-                                    $day_class = ' class="weekday"';
-                                }
-                                ?>
-
-                                <td <?= $day_class ?> <?= $month == 1 ? 'height="25"' : '' ?>>
-
-                                    <? if (isset($count_list[$iday]) && count($count_list[$iday])) : ?>
-                                        <table width="100%" cellspacing="0" cellpadding="0">
-                                            <tr>
-                                                <td<?= $day_class ?>>
-                                    <? endif; ?>
-
-                                    <? $weekday = strftime('%a', $aday); ?>
-
-                                    <span class="yday">
-                                        <? $hday = holiday($aday); ?>
-                                        <? if (is_array($hday)) : ?>
-                                            <? if ($hday['col'] == '1') : ?>
-                                                <? if (date('w', $aday) == '0') : ?>
-                                                    <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                                    <? $count++; ?>
-                                                <? else : ?>
-                                                    <a style="font-weight:bold;" class="day" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                                <? endif; ?>
-                                            <? elseif ($hday['col'] == '2' || $hday['col'] == '3') : ?>
-                                                <? if (date('w', $aday) == '0') : ?>
-                                                    <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                                    <? $count++; ?>
-                                                <? else : ?>
-                                                    <a style="font-weight:bold;" class="hday" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                                <? endif; ?>
-                                            <? endif ?>
-                                        <? else : ?>
-                                            <? if (date('w', $aday) == '0') : ?>
-                                                <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                                <? $count++; ?>
-                                            <? else : ?>
-                                                <a style="font-weight:bold;" class="day" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?>
-                                            <? endif; ?>
-                                        <? endif; ?>
-                                    </span>
-
-                                    <? if (isset($count_list[$iday]) && count($count_list[$iday])) : ?>
-                                        <? $event_count_txt = sprintf(ngettext('1 Termin', '%s Termine', count($count_list[$iday])), count($count_list[$iday])) ?>
-                                                </td>
-                                                <td<?= $day_class ?> align="right">
-                                                    <?= Icon::create('date', 'clickable', ['title' => $event_count_txt])->asImg(16, ["alt" => $event_count_txt]); ?>
-                                                </td>
-                                            </tr>
-                                        </table>
-                                    <? endif; ?>
-
-                                </td>
-
-                            <? else : ?>
-                                <td class="weekday"> </td>
-                            <? endif; ?>
-
-                        <? endfor; ?>
-                    </tr>
-                <? endfor; ?>
-            </tbody>
-
-            <tfoot>
-                <tr>
-                    <? $ts_month = 0; ?>
-                    <? for ($i = 1; $i < 13; $i++) : ?>
-                        <? $ts_month += ( $days_per_month[$i] - 1) * 86400; ?>
-                        <th align="center" width="8%">
-                            <a class="calhead" href="<?= $controller->url_for('calendar/single/month', ['atime' => $calendar->getStart() + $ts_month]) ?>">
-                                <b><?= strftime('%B', $ts_month); ?></b>
-                            </a>
-                        </th>
-                    <? endfor; ?>
-                </tr>
-            </tfoot>
-        </table>
-    </div>
-</div>
diff --git a/app/views/course/cancel_dates/index.php b/app/views/course/cancel_dates/index.php
index e427849a49c18eaf52ca19a37ca3fe21d81ec470..3c092bc90fb465d145b63143af904bb13d4e765a 100644
--- a/app/views/course/cancel_dates/index.php
+++ b/app/views/course/cancel_dates/index.php
@@ -12,7 +12,7 @@
 
         <label>
             <?= _('Kommentar') ?>
-            <?= tooltipIcon(_('Wenn Sie die nicht stattfindenden Termine mit einem Kommentar versehen, werden die Ausfalltermine im Ablaufplan weiterhin dargestellt und auch im Terminkalender eingeblendet.')) ?>
+            <?= tooltipIcon(_('Wenn Sie die nicht stattfindenden Termine mit einem Kommentar versehen, werden die Ausfalltermine im Ablaufplan weiterhin dargestellt und auch im Kalender angezeigt.')) ?>
             <textarea wrap="virtual" name="cancel_dates_comment" id="cancel_dates_comment"></textarea>
         </label>
         <label>
diff --git a/app/views/course/dates/details-edit.php b/app/views/course/dates/details-edit.php
index 2fec69cb07be8a5072abc8914be2d899cfe448f9..0f91d7226300369ad20c49fdbf69028ffc3574d7 100644
--- a/app/views/course/dates/details-edit.php
+++ b/app/views/course/dates/details-edit.php
@@ -1,4 +1,5 @@
-<form name="edit_termin" action='<?= $controller->url_for('course/dates/save_details/' . $date->id) ?>' method="post" class="default" data-termin-id="<?= htmlReady($date->id) ?>">
+<form name="edit_termin" action='<?= $controller->url_for('course/dates/save_details/' . $date->id) ?>' method="post" class="default" data-termin-id="<?= htmlReady($date->id) ?>"
+      data-course-id="<?= htmlReady(Context::getID()) ?>">
     <?= CSRFProtection::tokenTag() ?>
 
     <fieldset>
@@ -93,6 +94,17 @@
         </div>
     </fieldset>
 <? endif; ?>
+<? if (!empty($date->room_booking->resource)) : ?>
+    <? $room = $date->room_booking->resource->getDerivedClassInstance() ?>
+    <? if ($room instanceof Resource) : ?>
+        <fieldset>
+            <legend><?= _('Raum') ?></legend>
+            <section>
+                <?= htmlReady($room->getFullName()) ?>
+            </section>
+        </fieldset>
+    <? endif ?>
+<? endif ?>
 
 <? if (count($groups) > 0): ?>
     <fieldset>
@@ -158,6 +170,12 @@
                 ['data-dialog' => '']
             ) ?>
         <? endif ?>
+        <? if (Request::submitted('extra_buttons')) : ?>
+            <?= Studip\LinkButton::create(
+                _('Zur Veranstaltung'),
+                $controller->url_for('course/details', ['cid' => $date->range_id])
+            ) ?>
+        <? endif ?>
     </footer>
 </form>
 
diff --git a/app/views/course/dates/details.php b/app/views/course/dates/details.php
index 5a772682a6cd47696c6df232a54164a1d53ac7c3..96f7e2eb7da9dc89b935482c565e71959702af16 100644
--- a/app/views/course/dates/details.php
+++ b/app/views/course/dates/details.php
@@ -39,6 +39,17 @@
             </td>
         </tr>
     <? endif; ?>
+    <? if (!empty($date->room_booking->resource)) : ?>
+        <? $room = $date->room_booking->resource->getDerivedClassInstance() ?>
+        <? if ($room instanceof Resource) : ?>
+            <tr>
+                <td><strong><?= _('Raum') ?></strong></td>
+                <td>
+                    <?= htmlReady($room->getFullName()) ?>
+                </td>
+            </tr>
+        <? endif ?>
+    <? endif ?>
     <? if (count($date->statusgruppen) > 0): ?>
         <tr>
             <td><strong><?= _('Beteiligte Gruppen') ?></strong></td>
@@ -96,3 +107,8 @@
         STUDIP.Table.enhanceSortableTable($('#course_date_files'));
     </script>
 <? endif; ?>
+<? if (Request::bool('extra_buttons') && $GLOBALS['perm']->have_studip_perm('user', $course->id)) : ?>
+    <div data-dialog-button>
+        <?= \Studip\LinkButton::create(_('Zur Veranstaltung'), $controller->url_for('course/details', ['cid' => $course->id])) ?>
+    </div>
+<? endif ?>
diff --git a/app/views/course/details/index.php b/app/views/course/details/index.php
index 15cee6ca6cd142e9c1218b7eb6809a6c041bf520..583f715b934c41c4da7b189e63452dfe792ea6e3 100644
--- a/app/views/course/details/index.php
+++ b/app/views/course/details/index.php
@@ -522,4 +522,9 @@ if (!empty($mvv_tree)) : ?>
     <? endif ?>
     </footer>
 <? endif ?>
-<?= Feedback::getHTML($course->id, Course::class) ?>
+<? if (Request::bool('link_to_course') && $GLOBALS['perm']->have_studip_perm('autor', $course->id)) : ?>
+    <footer data-dialog-button>
+        <?= \Studip\LinkButton::create(_('Direkt zur Veranstaltung'), URLHelper::getURL('dispatch.php/course/overview', ['cid' => $course->id]))?>
+    </footer>
+<? endif ?>
+<?= Feedback::getHTML($course->id, 'Course'); ?>
diff --git a/app/views/course/timesrooms/_cancel_form.php b/app/views/course/timesrooms/_cancel_form.php
index eaadaf19d6b8a30899482f70084b30f3cb3be71f..ee59d8933f0dd4c0b4ecf8deb7b110f13e2a3f0a 100644
--- a/app/views/course/timesrooms/_cancel_form.php
+++ b/app/views/course/timesrooms/_cancel_form.php
@@ -6,7 +6,7 @@ if (isset($termin) && $termin instanceof CourseExDate) {
 }
 ?>
 <p>
-    <strong> <?= _('Wenn Sie die nicht stattfindenden Termine mit einem Kommentar versehen, werden die Ausfalltermine im Ablaufplan weiterhin dargestellt und auch im Terminkalender eingeblendet.') ?></strong>
+    <strong> <?= _('Wenn Sie die nicht stattfindenden Termine mit einem Kommentar versehen, werden die Ausfalltermine im Ablaufplan weiterhin dargestellt und auch im Kalender angezeigt.') ?></strong>
 </p>
 
 <label for="cancel_comment">
diff --git a/app/views/institute/overview/index.php b/app/views/institute/overview/index.php
index 3543d665eec66da0f9d481180c882033e11a89bc..cb347b002d5e2abe30894568ed2762a7f531eb8b 100644
--- a/app/views/institute/overview/index.php
+++ b/app/views/institute/overview/index.php
@@ -50,7 +50,6 @@
 </article>
 
 <?= $news ?>
-<?= $dates ?>
 <?= $evaluations ?>
 <?= $questionnaires ?>
 
diff --git a/app/views/institute/schedule/index.php b/app/views/institute/schedule/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..23602dbb62490dc895f4651d4220c631f8af4c01
--- /dev/null
+++ b/app/views/institute/schedule/index.php
@@ -0,0 +1 @@
+<?= $fullcalendar ?>
diff --git a/app/views/settings/calendar.php b/app/views/settings/calendar.php
index 6833a0a0f7ce597ebca354337ceb09fc8228ebfe..889120ad72af01ec907c516095630ba012ebbff2 100644
--- a/app/views/settings/calendar.php
+++ b/app/views/settings/calendar.php
@@ -4,8 +4,7 @@ use Studip\Button, Studip\LinkButton;
 $cal_views = [
     'day'   => _('Tagesansicht'),
     'week'  => _('Wochenansicht'),
-    'month' => _('Monatsansicht'),
-    'year'  => _('Jahresansicht'),
+    'month' => _('Monatsansicht')
 ];
 $cal_deletes = [
     12 => _('12 Monate nach Ablauf'),
@@ -27,18 +26,19 @@ $cal_step_weeks = [
 ];
 ?>
 
-<form method="post" action="<?= $controller->url_for('settings/calendar/store') ?>" class="default">
+<form method="post" action="<?= $controller->link_for('settings/calendar/store') ?>" class="default"
+    <?= Request::isDialog() ? 'data-dialog="reload-on-close"' : '' ?>>
     <input type="hidden" name="studip_ticket" value="<?= get_ticket() ?>">
     <?= CSRFProtection::tokenTag() ?>
 
     <fieldset>
         <legend>
-            <?= _('Einstellungen des Terminkalenders') ?>
+            <?= _('Einstellungen des Kalenders') ?>
         </legend>
 
         <label>
             <?= _('Startansicht') ?>
-            <select name="cal_view" id="cal_view" size="1">
+            <select name="cal_view" id="cal_view">
                 <? foreach ($cal_views as $index => $label): ?>
                     <option value="<?= $index ?>" <? if ($view == $index) echo 'selected'; ?>>
                         <?= $label ?>
@@ -48,15 +48,14 @@ $cal_step_weeks = [
         </label>
 
         <label>
-            <?= _('Wochenansicht') ?>
-            <select name="cal_type_week">
-                <option value="LONG"<?= $type_week == 'LONG' ? ' selected' : "" ?>>
-                    <?= _('7 Tage-Woche') ?>
-                </option>
-                <option value="SHORT"<?= $type_week == 'SHORT' ? ' selected' : "" ?>>
-                    <?= _('5 Tage-Woche') ?>
-                </option>
-            </select>
+            <input type="radio" name="cal_type_week" value="LONG"
+                <?= $type_week == 'LONG' ? 'checked' : "" ?>>
+            <?= _('Alle Wochentage in der Wochenansicht anzeigen.') ?>
+        </label>
+        <label>
+            <input type="radio" name="cal_type_week" value="SHORT"
+                <?= $type_week == 'SHORT' ? 'checked' : "" ?>>
+            <?= _('Nur Montag bis Freitag in der Wochenansicht anzeigen.') ?>
         </label>
     </fieldset>
 
@@ -65,34 +64,27 @@ $cal_step_weeks = [
             <?= _('Einzelterminkalender') ?>
         </legend>
 
-        <div>
-            <?= _('Zeitraum der Tages- und Wochenansicht') ?>
-            <section class="hgroup">
-                <label>
-                    <?= _("Von") ?>
-                    <select name="cal_start" aria-label="<?= _('Startzeit der Tages- und Wochenansicht') ?>" class="size-s">
-                        <? for ($i = 0; $i < 24; $i += 1): ?>
-                            <option value="<?= $i ?>" <? if ($start == $i) echo 'selected'; ?>>
-                                <?= sprintf('%02u:00', $i) ?>
-                            </option>
-                        <? endfor; ?>
-                    </select>
-                    <?= _("Uhr") ?>
-                </label>
-
-                <label>
-                    <?= _("Bis") ?>
-                    <select name="cal_end" aria-label="<?= _('Endzeit der Tages- und Wochenansicht') ?>" class="size-s">
-                        <? for ($i = 0; $i < 24; $i += 1): ?>
-                            <option value="<?= $i ?>" <? if ($end == $i) echo 'selected'; ?>>
-                                <?= sprintf('%02u:00', $i) ?>
-                            </option>
-                        <? endfor; ?>
-                    </select>
-                    <?= _("Uhr") ?>.
-                </label>
-            </section>
-        </div>
+        <label>
+            <?= _('Startuhrzeit') ?>
+            <select name="cal_start" aria-label="<?= _('Startzeit der Tages- und Wochenansicht') ?>" class="size-s">
+                <? for ($i = 0; $i < 24; $i += 1): ?>
+                    <option value="<?= $i ?>" <? if ($start == $i) echo 'selected'; ?>>
+                        <?= sprintf(_('%02u:00 Uhr'), $i) ?>
+                    </option>
+                <? endfor; ?>
+            </select>
+        </label>
+
+        <label>
+            <?= _('Enduhrzeit') ?>
+            <select name="cal_end" aria-label="<?= _('Endzeit der Tages- und Wochenansicht') ?>" class="size-s">
+                <? for ($i = 0; $i < 24; $i += 1): ?>
+                    <option value="<?= $i ?>" <? if ($end == $i) echo 'selected'; ?>>
+                        <?= sprintf(_('%02u:00 Uhr'), $i) ?>
+                    </option>
+                <? endfor; ?>
+            </select>
+        </label>
 
         <label>
             <?= _('Zeitintervall der Tagesansicht') ?>
@@ -153,7 +145,7 @@ $cal_step_weeks = [
     </fieldset>
 <? endif ?>
 
-    <footer>
+    <footer data-dialog-button>
         <? if (Request::option('atime')): ?>
             <input type="hidden" name="atime" value="<?= Request::option('atime') ?>">
         <? endif ?>
diff --git a/app/views/settings/general.php b/app/views/settings/general.php
index db1d01d3cc4a903517abf1d351c78f86071eba91..4bd543388973171c5cbced5bd4df62508a5fa35b 100644
--- a/app/views/settings/general.php
+++ b/app/views/settings/general.php
@@ -3,7 +3,7 @@ $start_pages = [
     '' => _('keine'),
      1 => _('Meine Veranstaltungen'),
      3 => _('Mein Stundenplan'),
-     5 => _('Mein Terminkalender'),
+     5 => _('Mein Kalender'),
      4 => _('Mein Adressbuch'),
      6 => _('Mein globaler Blubberstream'),
      7 => _('Mein Arbeitsplatz'),
diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist
index 3ef8fb60fa1ae0a05ff5a0ff896abcdaaeae0cfc..156224897e5ffbd5e370070c4f621a033e0807ef 100644
--- a/config/config.inc.php.dist
+++ b/config/config.inc.php.dist
@@ -205,23 +205,112 @@ $TERMIN_TYP[7]=array("name"=>_("Vorlesung"), "sitzung"=>1);
 // more types can be added here
 
 
-// Configure the categories for the personal calendar
-$PERS_TERMIN_KAT[1]=array("name"=>_("Sonstiges"));
-$PERS_TERMIN_KAT[2]=array("name"=>_("Sitzung"));
-$PERS_TERMIN_KAT[3]=array("name"=>_("Vorbesprechung"));
-$PERS_TERMIN_KAT[4]=array("name"=>_("Klausur"));
-$PERS_TERMIN_KAT[5]=array("name"=>_("Exkursion"));
-$PERS_TERMIN_KAT[6]=array("name"=>_("Sondersitzung"));
-$PERS_TERMIN_KAT[7]=array("name"=>_("Prüfung"));
-$PERS_TERMIN_KAT[8]=array("name"=>_("Telefonat"));
-$PERS_TERMIN_KAT[9]=array("name"=>_("Besprechung"));
-$PERS_TERMIN_KAT[10]=array("name"=>_("Verabredung"));
-$PERS_TERMIN_KAT[11]=array("name"=>_("Geburtstag"));
-$PERS_TERMIN_KAT[12]=array("name"=>_("Familie"));
-$PERS_TERMIN_KAT[13]=array("name"=>_("Urlaub"));
-$PERS_TERMIN_KAT[14]=array("name"=>_("Reise"));
-$PERS_TERMIN_KAT[15]=array("name"=>_("Vorlesung"));
-// more categories can be added here
+//Configuration for the date categories in the personal calendar:
+$PERS_TERMIN_KAT = [
+    '1' => [
+        'name'         => _('Sonstiges'),
+        'border_color' => '#682C8B',
+        'bgcolor'      => '#682C8B',
+        'fgcolor'      => '#ffffff'
+    ],
+    '2' => [
+        'name'         => _('Sitzung'),
+        'border_color' => '#B02E7C',
+        'bgcolor'      => '#B02E7C',
+        'fgcolor'      => '#000000'
+    ],
+    '3' => [
+        'name'         => _('Vorbesprechung'),
+        'border_color' => '#D60000',
+        'bgcolor'      => '#D60000',
+        'fgcolor'      => '#ffffff'
+    ],
+    '4' => [
+        'name'         => _('Klausur'),
+        'border_color' => '#F26E00',
+        'bgcolor'      => '#F26E00',
+        'fgcolor'      => '#000000'
+    ],
+    '5' => [
+        'name'         => _('Exkursion'),
+        'border_color' => '#FFBD33',
+        'bgcolor'      => '#FFBD33',
+        'fgcolor'      => '#000000'
+    ],
+    '6' => [
+        'name'         => _('Sondersitzung'),
+        'border_color' => '#6EAD10',
+        'bgcolor'      => '#6EAD10',
+        'fgcolor'      => '#000000'
+    ],
+    '7' => [
+        'name'         => _('Prüfung'),
+        'border_color' => '#008512',
+        'bgcolor'      => '#008512',
+        'fgcolor'      => '#000000'
+    ],
+    '8' => [
+        'name'         => _('Telefonat'),
+        'border_color' => '#129C94',
+        'bgcolor'      => '#129C94',
+        'fgcolor'      => '#000000'
+    ],
+    '9' => [
+        'name'         => _('Besprechung'),
+        'border_color' => '#A85D45',
+        'bgcolor'      => '#A85D45',
+        'fgcolor'      => '#000000'
+    ],
+    '10' => [
+        'name'         => _('Verabredung'),
+        'border_color' => '#A480B9',
+        'bgcolor'      => '#A480B9',
+        'fgcolor'      => '#000000'
+    ],
+    '11' => [
+        'name'         => _('Geburtstag'),
+        'border_color' => '#D082B0',
+        'bgcolor'      => '#D082B0',
+        'fgcolor'      => '#000000'
+    ],
+    '12' => [
+        'name'         => _('Familie'),
+        'border_color' => '#E76666',
+        'bgcolor'      => '#E76666',
+        'fgcolor'      => '#000000'
+    ],
+    '13' => [
+        'name'         => _('Urlaub'),
+        'border_color' => '#F7A866',
+        'bgcolor'      => '#F7A866',
+        'fgcolor'      => '#000000'
+    ],
+    '14' => [
+        'name'         => _('Reise'),
+        'border_color' => '#FFD785',
+        'bgcolor'      => '#FFD785',
+        'fgcolor'      => '#000000'
+    ],
+    '15' => [
+        'name'         => _('Vorlesung'),
+        'border_color' => '#A8CE70',
+        'bgcolor'      => '#A8CE70',
+        'fgcolor'      => '#000000'
+    ],
+    '16' => [
+        'name'         => _('Videokonferenz'),
+        'border_color' => '#8bbd40',
+        'bgcolor'      => '#8bbd40',
+        'fgcolor'      => '#000000'
+    ],
+    '255' => [
+        'name'         => _('Sonstige'),
+        'border_color' => '#A7ABAF',
+        'bgcolor'      => '#A7ABAF',
+        'fgcolor'      => '#000000'
+    ]
+    //More categories can be added here.
+];
 
 //preset for academic titles -  add further titles to the array, if necessary
 $TITLE_FRONT_TEMPLATE = array("","Prof.","Prof. Dr.","Dr.","PD Dr.","Dr. des.","Dr. med.","Dr. rer. nat.","Dr. forest.",
diff --git a/db/migrations/1.160_step_00283_update_calendar_settings.php b/db/migrations/1.160_step_00283_update_calendar_settings.php
index f54956bf19893a5b7ea3aacb98b9c1ed6b2ebb7c..82a15849c7ef3b8809e0b4c16a28da3b1dea4d4f 100644
--- a/db/migrations/1.160_step_00283_update_calendar_settings.php
+++ b/db/migrations/1.160_step_00283_update_calendar_settings.php
@@ -17,7 +17,17 @@ class Step00283UpdateCalendarSettings extends Migration {
             'showmonth' => 'month',
             'showyear' => 'year'];
         $res = DBManager::get()->query("SELECT user_id FROM `user_config` WHERE field = 'CALENDAR_SETTINGS'");
-        $default_settings = Calendar::getDefaultUserSettings();
+        $default_settings = [
+            'view'              => 'week',
+            'start'             => '9',
+            'end'               => '20',
+            'step_day'          => '900',
+            'step_week'         => '1800',
+            'type_week'         => 'LONG',
+            'step_week_group'   => '3600',
+            'step_day_group'    => '3600',
+            'show_declined'     => '0'
+        ];
         Config::get()->store('CALENDAR_SETTINGS', $default_settings);
         foreach ($res as $row) {
             $config = new UserConfig($row['user_id']);
diff --git a/db/migrations/5.4.1.1_alter_calendar_tables.php b/db/migrations/5.4.1.1_alter_calendar_tables.php
new file mode 100644
index 0000000000000000000000000000000000000000..b1f477b0c1c8374b83b430f55ad2566102ca7326
--- /dev/null
+++ b/db/migrations/5.4.1.1_alter_calendar_tables.php
@@ -0,0 +1,247 @@
+<?php
+
+
+class AlterCalendarTables extends Migration
+{
+    public function description()
+    {
+        return 'Alters the tables for the personal calendar and related tables.';
+    }
+
+
+    protected function migrateEventData()
+    {
+        $db = DBManager::get();
+
+        $db->exec("RENAME TABLE `event_data` TO calendar_dates");
+
+        //Move the content of the "day" column into the "offset" column
+        //which is still called "sinterval" at this point:
+        $db->exec(
+            "UPDATE `calendar_dates`
+            SET `sinterval` = `day`
+            WHERE `day` <> ''"
+        );
+
+        $db->exec(
+            "ALTER TABLE `calendar_dates`
+            DROP COLUMN `ts`,
+            DROP COLUMN `duration`,
+            DROP COLUMN `priority`,
+            DROP COLUMN `day`,
+            CHANGE COLUMN event_id id CHAR(32) COLLATE latin1_bin NOT NULL,
+            CHANGE COLUMN uid unique_id VARCHAR(255) UNIQUE NOT NULL,
+            CHANGE COLUMN start begin INT(11) NOT NULL DEFAULT 0,
+            CHANGE COLUMN end end INT(11) NOT NULL DEFAULT 0,
+            CHANGE COLUMN summary title VARCHAR(255) NOT NULL DEFAULT '',
+            CHANGE COLUMN class access ENUM('PUBLIC', 'PRIVATE', 'CONFIDENTIAL') COLLATE latin1_bin NOT NULL DEFAULT 'PRIVATE',
+            CHANGE COLUMN categories user_category VARCHAR(64) NULL DEFAULT '',
+            CHANGE COLUMN category_intern category TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
+            CHANGE COLUMN location location VARCHAR(255) NULL DEFAULT '',
+            CHANGE COLUMN linterval `interval` TINYINT(2) NULL DEFAULT 0,
+            CHANGE COLUMN sinterval `offset` TINYINT(2) NULL DEFAULT 0,
+            CHANGE COLUMN wdays days VARCHAR(7) NULL DEFAULT '',
+            CHANGE COLUMN rtype repetition_type ENUM('SINGLE', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY') DEFAULT 'SINGLE',
+            CHANGE COLUMN `count` number_of_dates SMALLINT(5) UNSIGNED NOT NULL DEFAULT '1',
+            CHANGE COLUMN `expire` repetition_end BIGINT(10) NOT NULL DEFAULT '0',
+            CHANGE COLUMN mkdate mkdate INT(11) UNSIGNED NOT NULL DEFAULT 0,
+            CHANGE COLUMN chdate chdate INT(11) UNSIGNED NOT NULL DEFAULT 0,
+            CHANGE COLUMN importdate import_date INT(11) NOT NULL DEFAULT 0"
+        );
+
+        $get_stmt = $db->prepare("SELECT `id`, `exceptions` FROM `calendar_dates`");
+        $exception_stmt = $db->prepare(
+            "INSERT INTO `calendar_date_exceptions`
+            (`calendar_date_id`, `date`, `mkdate`, `chdate`)
+            VALUES
+            (:calendar_date_id, :date, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())"
+        );
+        $get_stmt->execute();
+        while ($row = $get_stmt->fetch()) {
+            //Migrate exceptions:
+            $exceptions = explode(',', $row['exceptions'] ?? '');
+            foreach ($exceptions as $exception) {
+                $exception_stmt->execute([
+                    'calendar_date_id' => $row['id'],
+                    'date' => date('Y-m-d', intval(trim($exception)))
+                ]);
+            }
+        }
+
+        $db->exec(
+            "ALTER TABLE `calendar_dates` DROP COLUMN `exceptions`"
+        );
+    }
+
+
+    protected function migrateCalendarEvent()
+    {
+        $db = DBManager::get();
+
+        $db->exec(
+            "RENAME TABLE `calendar_event` TO calendar_date_assignments"
+        );
+
+        $db->exec(
+            "ALTER TABLE `calendar_date_assignments`
+            ADD COLUMN participation ENUM('', 'ACCEPTED', 'DECLINED', 'ACKNOWLEDGED') COLLATE latin1_bin NOT NULL DEFAULT '',
+            CHANGE COLUMN event_id calendar_date_id CHAR(32) COLLATE latin1_bin NOT NULL,
+            CHANGE COLUMN group_status old_group_status TINYINT(1) UNSIGNED NOT NULL DEFAULT '0',
+            CHANGE COLUMN mkdate mkdate INT(11) NOT NULL DEFAULT 0,
+            CHANGE COLUMN chdate chdate INT(11) NOT NULL DEFAULT 0"
+        );
+
+        $db->exec(
+            "UPDATE `calendar_date_assignments`
+            SET `participation` = IF (
+                `old_group_status` = '2',
+                'ACCEPTED',
+                IF (`old_group_status` = '3',
+                    'DECLINED',
+                    IF (`old_group_status` = '4',
+                        'ACKNOWLEDGED',
+                        ''
+                        )
+                    )
+                )"
+        );
+
+        $db->exec("ALTER TABLE `calendar_date_assignments` DROP COLUMN `old_group_status`");
+    }
+
+
+    protected function migrateCalendarUser()
+    {
+        //All entries from calendar_user are transferred to the contacts table
+        //which gets an extra column so that it can store the calendar access level.
+        $db = DBManager::get();
+
+        $db->exec(
+            "ALTER TABLE `contact`
+            CHANGE COLUMN mkdate mkdate INT(11) NOT NULL DEFAULT 0,
+            ADD COLUMN chdate INT(11) NOT NULL DEFAULT 0,
+            ADD COLUMN calendar_permissions ENUM('', 'READ', 'WRITE') COLLATE latin1_bin NOT NULL DEFAULT ''"
+        );
+
+        $db->exec(
+            "INSERT INTO `contact`
+            (`owner_id`, `user_id`, `calendar_permissions`, `mkdate`, `chdate`)
+            SELECT `owner_id`, `user_id`,
+                    IF(`permission` = '4', 'WRITE', IF(`permission` = '2', 'READ', '')) AS calendar_permissions,
+                   `mkdate`, `chdate`
+                   FROM `calendar_user`
+            ON DUPLICATE KEY UPDATE `calendar_permissions` = calendar_permissions"
+        );
+
+        $db->exec("DROP TABLE `calendar_user`");
+    }
+
+
+    protected function addContactGroups()
+    {
+        $db = DBManager::get();
+
+        $db->exec(
+            "CREATE TABLE IF NOT EXISTS `contact_groups` (
+                `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+                `name` VARCHAR(255) NOT NULL,
+                `owner_id` CHAR(32) COLLATE latin1_bin NOT NULL,
+                `old_group_id` CHAR(32) COLLATE latin1_bin NOT NULL,
+                `mkdate` INT(11) UNSIGNED NOT NULL DEFAULT 0,
+                `chdate` INT(11) UNSIGNED NOT NULL DEFAULT 0,
+                PRIMARY KEY(`id`)
+            )"
+        );
+        $db->exec(
+            "CREATE TABLE IF NOT EXISTS `contact_group_items` (
+                `group_id` BIGINT UNSIGNED NOT NULL,
+                `user_id` CHAR(32) COLLATE latin1_bin NOT NULL,
+                `mkdate` INT(11) UNSIGNED NOT NULL DEFAULT 0,
+                `chdate` INT(11) UNSIGNED NOT NULL DEFAULT 0,
+                 PRIMARY KEY(`group_id`, `user_id`)
+            )"
+        );
+
+        //Migrate entries from statusgruppen and statusgruppe_user:
+        $old_groups = $db->query(
+            "SELECT `statusgruppe_id`, `name`, `range_id`, `mkdate`, `chdate`
+            FROM `statusgruppen`
+            WHERE `range_id` IN (
+                SELECT `user_id` FROM `auth_user_md5`
+            )"
+        )->fetchAll(PDO::FETCH_ASSOC);
+
+        $new_group_stmt = $db->prepare(
+            "INSERT INTO `contact_groups`
+            (`name`, `owner_id`, `old_group_id`, `mkdate`, `chdate`)
+            VALUES (:name, :user_id, :old_group_id, :mkdate, :chdate)"
+        );
+
+        $group_member_stmt = $db->prepare(
+            "INSERT INTO `contact_group_items`
+            (`group_id`, `user_id`, `mkdate`, `chdate`)
+            SELECT `contact_groups`.`id` AS group_id, `user_id`, `statusgruppe_user`.`mkdate` as mkdate, `statusgruppe_user`.`mkdate` AS chdate
+            FROM `statusgruppe_user`
+            INNER JOIN `contact_groups`
+            ON `statusgruppe_user`.`statusgruppe_id` = `contact_groups`.`old_group_id`
+            WHERE `statusgruppe_id` = :old_group_id"
+        );
+        $old_member_delete_stmt = $db->prepare("DELETE FROM `statusgruppe_user` WHERE `statusgruppe_id` = :old_group_id");
+
+        foreach ($old_groups as $old_group) {
+            $new_group_stmt->execute([
+                'name'         => $old_group['name'],
+                'user_id'      => $old_group['range_id'],
+                'old_group_id' => $old_group['statusgruppe_id'],
+                'mkdate'       => $old_group['mkdate'],
+                'chdate'       => $old_group['chdate']
+            ]);
+            $group_member_stmt->execute([
+                'old_group_id' => $old_group['statusgruppe_id']
+            ]);
+            $old_member_delete_stmt->execute([
+                'old_group_id' => $old_group['statusgruppe_id']
+            ]);
+        }
+
+        //Delete old status groups:
+        $db->exec(
+            "DELETE FROM `statusgruppen` WHERE `range_id` IN (
+                SELECT `user_id` FROM `auth_user_md5`
+            )"
+        );
+
+        //Delete the old group ID:
+        $db->exec("ALTER TABLE `contact_groups` DROP COLUMN `old_group_id`");
+    }
+
+    protected function up()
+    {
+        $db = DBManager::get();
+
+        $db->exec(
+            "CREATE TABLE IF NOT EXISTS `calendar_date_exceptions` (
+            `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+            `calendar_date_id` CHAR(32) COLLATE latin1_bin NOT NULL,
+            `date` DATE NOT NULL,
+            `mkdate` INT(11) UNSIGNED NOT NULL DEFAULT 0,
+            `chdate` INT(11) UNSIGNED NOT NULL DEFAULT 0,
+            PRIMARY KEY (`id`)
+            )"
+        );
+
+        $this->migrateEventData();
+
+        $this->migrateCalendarEvent();
+
+        $this->migrateCalendarUser();
+
+        $this->addContactGroups();
+    }
+
+
+    protected function down()
+    {
+        //I see nothing, I hear nothing, I know nothing! NOTHING!!
+    }
+}
diff --git a/db/studip_default_data.sql b/db/studip_default_data.sql
index 96bb42b532233e0d408ab75a8c5065aba467bddc..df7ee8ba6b47fa93a763d4f842e03430044290d6 100644
--- a/db/studip_default_data.sql
+++ b/db/studip_default_data.sql
@@ -162,7 +162,7 @@ INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `c
 INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('BLUBBER_GLOBAL_MESSENGER_ACTIVATE', '1', 'boolean', 'global', 'global', 1591630778, 1591630778, 'Ist Blubber unter Community global aktiv? Blubber in Veranstaltungen wird über das Plugin Blubber aktiviert oder deaktiviert.');
 INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('BLUBBER_GLOBAL_THREAD_OPTOUT', '1', 'boolean', 'global', 'global', 1640797278, 1640797278, 'Gibt an, ob beim globalen Blubber Thread ein Opt-Out-Verfahren genutzt werden soll');
 INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CALENDAR_ENABLE', '1', 'boolean', 'global', 'calendar', 1293118059, 1293118059, 'Schaltet ein oder aus, ob der Kalender global verfügbar ist.');
-INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CALENDAR_GRANT_ALL_INSERT', '0', 'boolean', 'global', 'calendar', 1462287762, 1462287762, 'Ermöglicht das Eintragen von Terminen in alle Nutzerkalender, ohne Beachtung des Rechtesystems.');
+INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CALENDAR_GRANT_ALL_INSERT', '1', 'boolean', 'global', 'calendar', 1462287762, 1462287762, 'Ermöglicht das Eintragen von Terminen in alle Kalender der Nutzenden, ohne Beachtung des Rechtesystems.');
 INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CALENDAR_GROUP_ENABLE', '0', 'boolean', 'global', 'calendar', 1326799692, 1326799692, 'Schaltet die Gruppenterminkalender-Funktionen ein.');
 INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CALENDAR_SETTINGS', '{\"view\":\"week\",\"start\":\"9\",\"end\":\"20\",\"step_day\":\"900\",\"step_week\":\"1800\",\"type_week\":\"LONG\",\"step_week_group\":\"3600\",\"step_day_group\":\"3600\"}', 'array', 'user', '', 1403258015, 1403258015, 'persönliche Einstellungen des Kalenders');
 INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CONSULTATION_ALLOW_DOCENTS_RESERVING', '1', 'boolean', 'global', 'Terminvergabe', 1557244743, 1557244743, 'Lehrende können sich bei anderen Lehrenden anmelden');
diff --git a/lib/bootstrap-autoload.php b/lib/bootstrap-autoload.php
index 69910b24f39553b80681a48f5f432fff01728d55..6f3f4a713f2be5564a86948f3369518fdc9325b4 100644
--- a/lib/bootstrap-autoload.php
+++ b/lib/bootstrap-autoload.php
@@ -8,6 +8,7 @@ StudipAutoloader::register();
 
 // General classes folders
 StudipAutoloader::addAutoloadPath('lib/models');
+StudipAutoloader::addAutoloadPath('lib/models/calendar');
 StudipAutoloader::addAutoloadPath('lib/models/resources');
 StudipAutoloader::addAutoloadPath('lib/classes');
 StudipAutoloader::addAutoloadPath('lib/classes', 'Studip');
diff --git a/lib/calendar/CalendarColumn.class.php b/lib/calendar/CalendarColumn.class.php
index 3c071f1760855f764f45af7833d752bd2732b71f..cc3abea5310f239cb32e63f58932a79c4c407b97 100644
--- a/lib/calendar/CalendarColumn.class.php
+++ b/lib/calendar/CalendarColumn.class.php
@@ -13,6 +13,8 @@
  * @author      Rasmus Fuhse <fuhse@data-quest.de>
  * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
  * @category    Stud.IP
+ *
+ * @deprecated since Stud.IP 5.5
  */
 
 class CalendarColumn
diff --git a/lib/calendar/CalendarExport.class.php b/lib/calendar/CalendarExport.class.php
deleted file mode 100644
index c9c10f6aac8ccf869d9c8127387c5a7a2128bc69..0000000000000000000000000000000000000000
--- a/lib/calendar/CalendarExport.class.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?
-# Lifter002: TODO
-# Lifter007: TODO
-
-/**
- * CalendarExport.class.php
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @package     calendar
- */
-
-class CalendarExport
-{
-    protected $_writer;
-    protected $export;
-    private $count;
-
-    public function __construct(&$writer)
-    {
-        $this->_writer = $writer;
-    }
-
-    public function exportFromDatabase($range_id = null, $start = 0, $end = Calendar::CALENDAR_END, $event_types = null, $except = NULL)
-    {
-        global $_calendar_error, $user;
-
-        if (!$range_id) {
-            $range_id = $user->id;
-        }
-        $calendar = new SingleCalendar($range_id);
-
-        $this->_export($this->_writer->writeHeader());
-        $calendar->getEvents($event_types, $start, $end);
-
-        foreach ($calendar->events as $event) {
-            $this->_export($this->_writer->write($event));
-        }
-        $this->count = sizeof($calendar->events);
-
-        $this->_export($this->_writer->writeFooter());
-    }
-
-    public function exportFromObjects($events)
-    {
-        global $_calendar_error;
-
-        $this->_export($this->_writer->writeHeader());
-
-        $this->count = 0;
-        foreach ($events as $event) {
-            $this->_export($this->_writer->write($event));
-            $this->count++;
-        }
-
-        if (!sizeof($events)) {
-            $message = _('Es wurden keine Termine exportiert.');
-        } else {
-            $message = sprintf(ngettext('Es wurde 1 Termin exportiert', 'Es wurden %s Termine exportiert', sizeof($events)), sizeof($events));
-        }
-
-        $this->_export($this->_writer->writeFooter());
-    }
-
-    public function _export($exp)
-    {
-        if (!empty($exp)) {
-            $this->export[] = $exp;
-        }
-    }
-
-    public function getExport()
-    {
-        return $this->export;
-    }
-
-    public function getCount()
-    {
-        return $this->count;
-    }
-}
diff --git a/lib/calendar/CalendarExportException.class.php b/lib/calendar/CalendarExportException.class.php
deleted file mode 100644
index fbdabda7d47bceac93695b56535c8978ac594945..0000000000000000000000000000000000000000
--- a/lib/calendar/CalendarExportException.class.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-/**
- * CalendarExportException.php - indicates an error during
- * calendar export or import
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
-*/
-
-class CalendarExportException extends Exception
-{
-}
diff --git a/lib/calendar/CalendarExportFile.class.php b/lib/calendar/CalendarExportFile.class.php
deleted file mode 100644
index 1e0f3fb898fd8cacd9aa29af794bd96b83df329e..0000000000000000000000000000000000000000
--- a/lib/calendar/CalendarExportFile.class.php
+++ /dev/null
@@ -1,122 +0,0 @@
-<?
-# Lifter002: TODO
-# Lifter007: TODO
-
-/**
- * CalendarExportFile.class.php
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @package     calendar
- */
-
-class CalendarExportFile extends CalendarExport
-{
-    private $file_name = 'studip';
-    private $tmp_file_name;
-    private $path;
-
-    public function __construct(&$writer, $path = null, $file_name = null)
-    {
-        global $TMP_PATH;
-
-        parent::__construct($writer);
-
-        if (!$file_name) {
-            $this->tmp_file_name = $this->makeUniqueFilename();
-            $this->file_name .= '.' . $writer->getDefaultFileNameSuffix();
-        } else {
-            $this->file_name = $file_name;
-            $this->tmp_file_name = $file_name;
-        }
-
-        if (!$path) {
-            $this->path = $TMP_PATH . '/';
-        }
-
-        $this->_writer = $writer;
-    }
-
-    public function exportFromDatabase($range_id = null, $start = 0, $end = Calendar::CALENDAR_END, $event_types = null, $except = null)
-    {
-        $this->_createFile();
-        parent::exportFromDatabase($range_id, $start, $end, $event_types, $except);
-        $this->_closeFile();
-    }
-
-    public function exportFromObjects($events)
-    {
-        $this->_createFile();
-        parent::exportFromObjects($events);
-        $this->_closeFile();
-    }
-
-    public function sendFile()
-    {
-        if (file_exists($this->path . $this->tmp_file_name)) {
-            header('Location: ' . FileManager::getDownloadURLForTemporaryFile($this->tmp_file_name, $this->file_name));
-        } else {
-            throw new CalendarExportException(_('Die Export-Datei konnte nicht erstellt werden!'));
-        }
-    }
-
-    public function makeUniqueFileName()
-    {
-        return md5(uniqid(rand() . "Stud.IP Calendar"));
-    }
-
-    // returns file handle
-    public function getExport()
-    {
-        return $this->export;
-    }
-
-    public function getFileName()
-    {
-        return $this->file_name;
-    }
-
-    public function getTempFileName()
-    {
-        return $this->tmp_file_name;
-    }
-
-    public function _createFile()
-    {
-        if (!(is_dir($this->path))) {
-            if (!mkdir($this->path)) {
-                var_dump($this->path); exit;
-                throw new CalendarExportException(_('Das Export-Verzeichnis konnte nicht angelegt werden!'));
-            } else {
-                if (!chmod($this->path, 0777)) {
-                    throw new CalendarExportException(_('Die Zugriffsrechte auf das Export-Verzeichnis konnten nicht geändert werden!'));
-                }
-            }
-        }
-        if (file_exists($this->path . $this->tmp_file_name)) {
-            if (!unlink($this->path . $this->tmp_file_name)) {
-                throw new CalendarExportException(_('Eine bestehende Export-Datei konnte nicht gelöscht werden!'));
-            }
-        }
-        $this->export = fopen($this->path . $this->tmp_file_name, "wb");
-        if (!$this->export) {
-            throw new CalendarExportException(_("Die Export-Datei konnte nicht erstellt werden!"));
-        }
-    }
-
-    public function _export($exp)
-    {
-        fwrite($this->export, $exp);
-    }
-
-    public function _closeFile()
-    {
-        fclose($this->export);
-    }
-}
diff --git a/lib/calendar/CalendarImport.class.php b/lib/calendar/CalendarImport.class.php
deleted file mode 100644
index 803a58dd3f29ec7be3b2786941ddf35c9e256116..0000000000000000000000000000000000000000
--- a/lib/calendar/CalendarImport.class.php
+++ /dev/null
@@ -1,91 +0,0 @@
-<?
-# Lifter002: TODO
-# Lifter007: TODO
-
-/**
- * CalendarImport.class.php
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @package     calendar
- */
-
-class CalendarImport
-{
-
-    const IGNORE_ERRORS = 1;
-
-    protected $_parser;
-    private $data;
-    private $public_to_private = false;
-
-    public function __construct(CalendarParser &$parser, $data = null)
-    {
-        $this->_parser = $parser;
-        $this->data = $data;
-    }
-
-    public function getContent()
-    {
-        return $this->data;
-    }
-
-    public function importIntoDatabase($range_id, $ignore = CalendarImport::IGNORE_ERRORS)
-    {
-        $this->_parser->changePublicToPrivate($this->public_to_private);
-        if ($this->_parser->parseIntoDatabase($range_id, $this->getContent(), $ignore)) {
-            return true;
-        }
-
-        return false;
-    }
-
-    public function importIntoObjects($ignore = CalendarImport::IGNORE_ERRORS)
-    {
-        $this->_parser->changePublicToPrivate($this->public_to_private);
-        if ($this->_parser->parseIntoObjects($this->getContent(), $ignore)) {
-            return true;
-        }
-
-        return false;
-    }
-
-    public function getObjects()
-    {
-        return $objects =& $this->_parser->getObjects();
-    }
-
-    public function getCount()
-    {
-        return $this->_parser->getCount($this->getContent());
-    }
-
-    public function changePublicToPrivate($value = TRUE)
-    {
-        $this->public_to_private = $value;
-    }
-
-    public function getClientIdentifier()
-    {
-        if (!$client_identifier = $this->_parser->getClientIdentifier()) {
-            return $this->_parser->getClientIdentifier($this->getContent());
-        }
-        return $client_identifier;
-    }
-
-    public function setImportSem($do_import)
-    {
-        if ($do_import) {
-            $this->_parser->import_sem = true;
-        } else {
-            $this->_parser->import_sem = false;
-        }
-    }
-
-}
diff --git a/lib/calendar/CalendarImportFile.class.php b/lib/calendar/CalendarImportFile.class.php
deleted file mode 100644
index 3e2ba15c15a835e219fa27017ed9754d3c4cc8b6..0000000000000000000000000000000000000000
--- a/lib/calendar/CalendarImportFile.class.php
+++ /dev/null
@@ -1,141 +0,0 @@
-<?
-# Lifter002: TODO
-# Lifter007: TODO
-
-/**
- * CalendarImportFile.class.php
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @package     calendar
- */
-
-class CalendarImportFile extends CalendarImport
-{
-
-    private $file;
-    private $path;
-
-    /**
-     *
-     */
-    public function __construct(&$parser, $file, $path = '')
-    {
-        parent::__construct($parser);
-        $this->file = $file;
-        $this->path = $path;
-    }
-
-    /**
-     *
-     */
-    public function getContent()
-    {
-        $data = '';
-        if (!$file = @fopen($this->file['tmp_name'], 'rb')) {
-            throw new CalendarExportException(_("Die Import-Datei konnte nicht geöffnet werden!"));
-            return false;
-        }
-        if ($file) {
-            while (!feof($file)) {
-                $data .= fread($file, 1024);
-            }
-            fclose($file);
-        }
-        return $data;
-    }
-
-    /**
-     *
-     */
-    public function getFileName()
-    {
-        return $this->file['name'];
-    }
-
-    /**
-     *
-     */
-    public function getFileType()
-    {
-        return $this->_parser->getType();
-    }
-
-    /**
-     *
-     */
-    public function getFileSize()
-    {
-        if (file_exists($this->file['tmp_name'])) {
-            return filesize($this->file['tmp_name']);
-        }
-        return false;
-    }
-
-    /**
-     *
-     */
-    public function checkFile()
-    {
-        return true;
-    }
-
-    /**
-     *
-     */
-    public function importIntoDatabase($range_id, $ignore = CalendarImport::IGNORE_ERRORS)
-    {
-        if ($this->checkFile()) {
-            parent::importIntoDatabase($range_id, $ignore);
-            return true;
-        }
-        throw new CalendarExportException(_('Die Datei konnte nicht gelesen werden!'));
-        return false;
-    }
-
-    /**
-     *
-     */
-    public function importIntoObjects($ignore = CalendarImport::IGNORE_ERRORS)
-    {
-        global $_calendar_error;
-
-        if ($this->checkFile()) {
-            parent::importIntoObjects($ignore);
-            return true;
-        }
-        throw new CalendarExportException(_('Die Datei konnte nicht gelesen werden!'));
-    }
-
-    /**
-     *
-     */
-    public function deleteFile()
-    {
-        if (!unlink($this->file['tmp_name'])) {
-            throw new CalendarExportException(_("Die Datei konnte nicht gelöscht werden!"));
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     *
-     */
-    public function _getFileExtension()
-    {
-        $i = mb_strrpos($this->file['name'], '.');
-        if (!$i) {
-            return '';
-        }
-        $l = mb_strlen($this->file['name']) - $i;
-        $ext = mb_substr($this->file['name'], $i + 1, $l);
-        return $ext;
-    }
-}
diff --git a/lib/calendar/CalendarParser.class.php b/lib/calendar/CalendarParser.class.php
deleted file mode 100644
index 75780763f78c42186b45e50725d0724cb509907f..0000000000000000000000000000000000000000
--- a/lib/calendar/CalendarParser.class.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?
-# Lifter002: TODO
-# Lifter007: TODO
-
-/**
- * CalendarParser.class.php
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @package     calendar
- */
-
-class CalendarParser
-{
-    private $events = [];
-    protected $components;
-    private $type;
-    private $number_of_events;
-    protected $public_to_private = false;
-    protected $client_identifier;
-    private $time;
-    protected $import_sem = false;
-
-    public function __construct()
-    {
-        $this->client_identifier = '';
-    }
-
-    public function parse($data, $ignore = null)
-    {
-        foreach ($data as $properties) {
-            if ($this->public_to_private && $properties['CLASS'] == 'PUBLIC') {
-                $properties['CLASS'] = 'PRIVATE';
-            }
-            $properties['CATEGORIES'] = implode(', ', $properties['CATEGORIES']);
-            $this->components[] = $properties;
-        }
-    }
-
-    public function getCount($data)
-    {
-        return 0;
-    }
-
-    public function parseIntoDatabase($range_id, $data, $ignore)
-    {
-        if ($this->parseIntoObjects($range_id, $data, $ignore)) {
-            foreach ($this->events as $event) {
-                $event->store();
-            }
-            return true;
-        }
-
-        return false;
-    }
-
-    public function parseIntoObjects($range_id, $data, $ignore)
-    {
-        $this->time = time();
-        if ($this->parse($data, $ignore)) {
-            if (is_array($this->components)) {
-                foreach ($this->components as $component) {
-                    $calendar_event = CalendarEvent::findByUid($component['UID'], $range_id);
-                    if ($calendar_event) {
-                        $this->setProperties($calendar_event, $component);
-                        $calendar_event->setRecurrence($component['RRULE']);
-                        $this->events[] = $calendar_event;
-                    } else {
-                        $calendar_event = new CalendarEvent();
-                        $event = new EventData();
-                        $event->author_id = $GLOBALS['user']->id;
-                        $event->event_id = $event->getNewId();
-                        $event->uid = $component['UID'];
-                        $calendar_event->range_id = $range_id;
-                        $calendar_event->event_id = $event->event_id;
-                        $calendar_event->event = $event;
-                        $this->setProperties($calendar_event, $component);
-                        $calendar_event->setRecurrence($component['RRULE']);
-                        $this->events[] = $calendar_event;
-                    }
-                }
-            }
-            return true;
-        }
-        $message = _('Die Import-Daten konnten nicht verarbeitet werden!');
-
-        return false;
-    }
-
-    private function setProperties($calendar_event, $component)
-    {
-        $calendar_event->setStart($component['DTSTART']);
-        $calendar_event->setEnd($component['DTEND']);
-        $calendar_event->setTitle($component['SUMMARY']);
-        $calendar_event->event->description = $component['DESCRIPTION'];
-        $calendar_event->setAccessibility($component['CLASS']);
-        $calendar_event->setUserDefinedCategories($component['CATEGORIES']);
-        $calendar_event->event->category_intern = $component['STUDIP_CATEGORY'] ?: 1;
-        $calendar_event->setPriority($component['PRIORITY'] ?? 0);
-        $calendar_event->event->location = $component['LOCATION'];
-        $calendar_event->setExceptions($component['EXDATE']);
-        $calendar_event->event->mkdate = $component['CREATED'] ?? time();
-        $calendar_event->event->chdate = $component['LAST-MODIFIED'] ?: $component['CREATED'] ?? time();
-        $calendar_event->event->importdate = $this->time;
-    }
-
-    public function getType()
-    {
-        return $this->type;
-    }
-
-    public function &getObjects()
-    {
-        return $objects =& $this->events;
-    }
-
-    public function changePublicToPrivate($value = true)
-    {
-        $this->public_to_private = $value;
-    }
-
-    public function getClientIdentifier($data = null)
-    {
-        return $this->client_identifier;
-    }
-}
diff --git a/lib/calendar/CalendarView.class.php b/lib/calendar/CalendarView.class.php
index e3bfd0583272e47ccec0cf08630b0617b7fec46c..fc69127247a2fad95430364d794a4bba3c051929 100644
--- a/lib/calendar/CalendarView.class.php
+++ b/lib/calendar/CalendarView.class.php
@@ -38,6 +38,8 @@
  *  print $plan->render();
  *
  * @since      2.0
+ *
+ * @deprecated since Stud.IP 5.5
  */
 
 class CalendarView
@@ -213,7 +215,7 @@ class CalendarView
             'entry_height' => $this->getHeight()
         ];
         $factory = new Flexi_TemplateFactory(dirname(__file__).'/../../app/views');
-        PageLayout::addStyle($factory->render('calendar/stylesheet', $style_parameters));
+        PageLayout::addStyle($factory->render('calendar/schedule/stylesheet', $style_parameters));
 
         $template = $GLOBALS['template_factory']->open("calendar/calendar_view.php");
         $template->set_attribute("calendar_view", $this);
diff --git a/lib/calendar/CalendarWeekView.class.php b/lib/calendar/CalendarWeekView.class.php
index e0d0a6bb77b659a8a5a2feaf4ce3a9fef689ffa0..c1e0f24718ff2d9fb78b479fd045aa44fb3b2028 100644
--- a/lib/calendar/CalendarWeekView.class.php
+++ b/lib/calendar/CalendarWeekView.class.php
@@ -20,6 +20,8 @@
  * Kind of bean class for the calendar view.
  *
  * @since      2.0
+ *
+ * @deprecated since Stud.IP 5.5
  */
 
 class CalendarWeekView extends CalendarView
diff --git a/lib/calendar/CalendarWidgetView.php b/lib/calendar/CalendarWidgetView.php
index 946592cdf73f0d69c9359d14d274e29a1fe38890..df980ff3953e529e720c799566d975ed670d08f5 100644
--- a/lib/calendar/CalendarWidgetView.php
+++ b/lib/calendar/CalendarWidgetView.php
@@ -5,6 +5,8 @@
  * @author  Jan-Hendrik Willms <tleilax+studip@gmail.com>
  * @license GPL2 or any later version
  * @since   Stud.IP 3.4
+ *
+ * @deprecated since Stud.IP 5.5
  */
 class CalendarWidgetView extends CalendarWeekView
 {
diff --git a/lib/calendar/CalendarWriter.class.php b/lib/calendar/CalendarWriter.class.php
deleted file mode 100644
index 941c12386ac5aa5abff796b986a4ceed167768d2..0000000000000000000000000000000000000000
--- a/lib/calendar/CalendarWriter.class.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?
-# Lifter002: TODO
-# Lifter007: TODO
-
-/**
- * CalendarWriter.class.php
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @package     calendar
- */
-
-class CalendarWriter
-{
-    var $default_filename_suffix;
-    var $format;
-    var $client_identifier;
-
-    public function __construct()
-    {
-        // initialize error handler
-        $GLOBALS['_calendar_error'] = new ErrorHandler();
-    }
-
-    public function write(Event &$event)
-    {
-        return $event->properties;
-    }
-
-    public function writeHeader()
-    {
-    }
-
-    public function writeFooter()
-    {
-    }
-
-    public function getDefaultFilenameSuffix()
-    {
-        return $this->default_filename_suffix;
-    }
-
-    public function getFormat()
-    {
-        return $this->format;
-    }
-}
diff --git a/lib/calendar/CalendarWriterICalendar.class.php b/lib/calendar/CalendarWriterICalendar.class.php
deleted file mode 100644
index e65a89f98d52ebaf01c9341fb35ca4b8ebbf62ec..0000000000000000000000000000000000000000
--- a/lib/calendar/CalendarWriterICalendar.class.php
+++ /dev/null
@@ -1,629 +0,0 @@
-<?
-# Lifter002: TODO
-# Lifter007: TODO
-
-/**
- * CalendarWriterICalendar.class.php
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @package     calendar
- */
-
-define('CALENDAR_WEEKSTART', 'MO');
-
-class CalendarWriterICalendar extends CalendarWriter
-{
-    var $newline = "\r\n";
-
-    public function __construct()
-    {
-
-        parent::__construct();
-        $this->default_filename_suffix = "ics";
-        $this->format = "iCalendar";
-    }
-
-    public function writeHeader()
-    {
-
-        // Default values
-        $header = "BEGIN:VCALENDAR" . $this->newline;
-        $header .= "VERSION:2.0" . $this->newline;
-        if ($this->client_identifier) {
-            $header .= "PRODID:" . $this->client_identifier . $this->newline;
-        } else {
-            $host = $_SERVER['SERVER_NAME'] ?? parse_url($GLOBALS['ABSOLUTE_URI_STUDIP'], PHP_URL_HOST);
-            $header .= "PRODID:-//Stud.IP@{$host}//Stud.IP_iCalendar Library";
-            $header .= " //EN" . $this->newline;
-        }
-        $header .= "METHOD:PUBLISH" . $this->newline;
-
-        // time zone definition CET/CEST
-        $header .= 'CALSCALE:GREGORIAN' . $this->newline
-                   . 'BEGIN:VTIMEZONE' . $this->newline
-                   . 'TZID:Europe/Berlin' . $this->newline
-                   . 'BEGIN:DAYLIGHT' . $this->newline
-                   . 'TZOFFSETFROM:+0100' . $this->newline
-                   . 'TZOFFSETTO:+0200' . $this->newline
-                   . 'TZNAME:CEST' . $this->newline
-                   . 'DTSTART:19700329T020000' . $this->newline
-                   . 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3' . $this->newline
-                   . 'END:DAYLIGHT' . $this->newline
-                   . 'BEGIN:STANDARD' . $this->newline
-                   . 'TZOFFSETFROM:+0200' . $this->newline
-                   . 'TZOFFSETTO:+0100' . $this->newline
-                   . 'TZNAME:CET' . $this->newline
-                   . 'DTSTART:19701025T030000' . $this->newline
-                   . 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10' . $this->newline
-                   . 'END:STANDARD' . $this->newline
-                   . 'END:VTIMEZONE' . $this->newline;
-
-        return $header;
-    }
-
-    public function writeFooter()
-    {
-        return "END:VCALENDAR" . $this->newline;
-    }
-
-    /**
-     * Export this component as iCalendar format
-     *
-     * @param object $event The event to export.
-     * @return String iCalendar formatted data
-     */
-    public function write(Event &$event)
-    {
-
-        $match_pattern_1 = ['\\', '\n', ';', ','];
-        $replace_pattern_1 = ['\\\\', '\\n', '\;', '\,'];
-        $match_pattern_2 = ['\\', '\n', ';'];
-        $replace_pattern_2 = ['\\\\', '\\n', '\;'];
-        $exdate_time = 0;
-
-        $result = "BEGIN:VEVENT" . $this->newline;
-
-        foreach ($event->getProperties() as $name => $value) {
-            $params = [];
-            $params_str = '';
-
-            if ($name === 'SUMMARY') {
-                $value = $event->getTitle();
-            }
-            if ($value === '' || is_null($value)) {
-                continue;
-            }
-
-            switch ($name) {
-                // not supported event properties
-                case 'SEMNAME':
-                case 'EXPIRE':
-                case 'STUDIP_AUTHOR_ID':
-                case 'STUDIP_EDITOR_ID':
-                case 'STUDIP_ID':
-                case 'BEGIN':
-                case 'END':
-                case 'EVENT_TYPE':
-                case 'SEM_ID':
-                case 'STUDIP_GROUP_STATUS':
-                case 'STUDIP_CATEGORY':
-                    continue 2;
-
-                // text fields
-                case 'SUMMARY':
-                    $value = str_replace($match_pattern_1, $replace_pattern_1, $value);
-                    break;
-                case 'DESCRIPTION':
-                    $value = str_replace($match_pattern_1, $replace_pattern_1, $event->getDescription());
-                    break;
-                case 'LOCATION':
-                    $value = str_replace($match_pattern_1, $replace_pattern_1, $event->getLocation());
-                    break;
-
-                case 'CATEGORIES':
-                    $value = $this->_exportCategories($event);
-                    break;
-
-                // Date fields
-                case 'LAST-MODIFIED':
-                case 'CREATED':
-                case 'COMPLETED':
-                    $value = $this->_exportDateTime($value, true);
-                    break;
-
-                case 'DTSTAMP':
-                    $value = $this->_exportDateTime(time(), true);
-                    break;
-
-                case 'DTSTART':
-                    $exdate_time = $value;
-                case 'DTEND':
-                    if ($event->isDayEvent()) {
-                        $params['VALUE'] = 'DATE';
-                        $params_str = ';VALUE=DATE';
-                        $value++;
-                    }
-                case 'DUE':
-                case 'RECURRENCE-ID':
-                    if (array_key_exists('VALUE', $params)) {
-                        if ($params['VALUE'] == 'DATE') {
-                            $value = $this->_exportDate($value);
-                        } else {
-                            $value = $this->_exportDateTime($value);
-                            $params_str = ';TZID=Europe/Berlin';
-                        }
-                    } else {
-                        $value = $this->_exportDateTime($value);
-                        $params_str = ';TZID=Europe/Berlin';
-                    }
-                    break;
-
-                case 'EXDATE':
-                    if (array_key_exists('VALUE', $params)) {
-                        $value = $this->_exportExDate($value, $params['VALUE']);
-                    } else {
-                        $value = $this->_exportExDateTime($value, $exdate_time);
-                    }
-                    $params_str = ';TZID=Europe/Berlin';
-                    break;
-
-                case 'RDATE':
-                    if (array_key_exists('VALUE', $params)) {
-                        if ($params['VALUE'] == 'DATE') {
-                            $value = $this->_exportDate($value);
-                        } else if ($params['VALUE'] == 'PERIOD') {
-                            $value = $this->_exportPeriod($value);
-                        } else {
-                            $value = $this->_exportDateTime($value);
-                        }
-                    } else {
-                        $value = $this->_exportDateTime($value);
-                    }
-                    break;
-
-                case 'TRIGGER':
-                    if (array_key_exists('VALUE', $params)) {
-                        if ($params['VALUE'] == 'DATE-TIME') {
-                            $value = $this->_exportDateTime($value);
-                        } else if ($params['VALUE'] == 'DURATION') {
-                            $value = $this->_exportDuration($value);
-                        }
-                    } else {
-                        $value = $this->_exportDuration($value);
-                    }
-                    break;
-
-                // Duration fields
-                case 'DURATION':
-                    $value = $this->_exportDuration($value);
-                    break;
-
-                // Period of time fields
-                case 'FREEBUSY':
-                    $value_str = '';
-                    foreach ($value as $period) {
-                        $value_str .= empty($value_str) ? '' : ',';
-                        $value_str .= $this->_exportPeriod($period);
-                    }
-                    $value = $value_str;
-                    break;
-
-
-                // UTC offset fields
-                case 'TZOFFSETFROM':
-                case 'TZOFFSETTO':
-                    $value = $this->_exportUtcOffset($value);
-                    break;
-
-                // Integer fields
-                case 'PERCENT-COMPLETE':
-                    if ($event->getPermission() == Event::PERMISSION_CONFIDENTIAL)
-                        $value = '';
-                case 'REPEAT':
-                case 'SEQUENCE':
-                    $value = "$value";
-                    break;
-
-                case 'PRIORITY':
-                    if ($event->getPermission() == Event::PERMISSION_CONFIDENTIAL)
-                        $value = '0';
-                    else {
-                        switch ($value) {
-                            case 1:
-                                $value = '1';
-                                break;
-                            case 2:
-                                $value = '5';
-                                break;
-                            case 3:
-                                $value = '9';
-                                break;
-                            default:
-                                $value = '0';
-                        }
-                    }
-                    break;
-
-                // Geo fields
-                case 'GEO':
-                    if ($event->getPermission() == Event::PERMISSION_CONFIDENTIAL)
-                        $value = '';
-                    else
-                        $value = $value['latitude'] . ',' . $value['longitude'];
-                    break;
-
-                // Recursion fields
-                case 'EXRULE':
-                case 'RRULE':
-                    if ($event->getRecurrence('rtype') != 'SINGLE')
-                        $value = $this->_exportRecurrence($value);
-                    else
-                        continue 2;
-                    break;
-
-                case "UID":
-                    $value = "$value";
-            }
-            if ($name) {
-                $attr_string = "$name$params_str:$value";
-                $result .= $this->_foldLine($attr_string) . $this->newline;
-            }
-        }
-    //    if ($event->isGroupEvent()) {
-        if ($event instanceof CalendarEvent && $event->attendees->count() > 1) {
-            $result .= $this->_exportGroupEventProperties($event);
-        }
-        //  $result .= 'DTSTAMP:' . $this->_exportDateTime(time()) . $this->newline;
-        $result .= "END:VEVENT" . $this->newline;
-
-        return $result;
-    }
-
-    /**
-     * Export a UTC Offset field
-     *
-     * @param array $value
-     * @return String UTC offset field iCalendar formatted
-     */
-    public function _exportUtcOffset($value)
-    {
-        $offset = $value['ahead'] ? '+' : '-';
-        $offset .= sprintf('%02d%02d', $value['hour'], $value['minute']);
-        if (array_key_exists('second', $value)) {
-            $offset .= sprintf('%02d', $value['second']);
-        }
-
-        return $offset;
-    }
-
-    /**
-     * Export a Time Period field
-     *
-     * @param array $value
-     * @return String Period field iCalendar formatted
-     */
-    public function _exportPeriod($value)
-    {
-        $period = $this->_exportDateTime($value['start']);
-        $period .= '/';
-        if (array_key_exists('duration', $value)) {
-            $period .= $this->_exportDuration($value['duration']);
-        } else {
-            $period .= $this->_exportDateTime($value['end']);
-        }
-        return $period;
-    }
-
-    /**
-     * Export a DateTime field
-     *
-     * @param int $value Unix timestamp
-     * @return String Date and time (UTC) iCalendar formatted
-     */
-    public function _exportDateTime($value, $utc = false)
-    {
-
-//      $TZOffset  = 3600 * mb_substr(date('O', $value), 0, 3);
-//      $TZOffset += 60 * mb_substr(date('O', $value), 3, 2);
-        //transform local time in UTC
-        if ($utc) {
-            $value -= date('Z', $value);
-        }
-
-        return $this->_exportDate($value) . 'T' . $this->_exportTime($value, $utc);
-    }
-
-    /**
-     * Export a Time field
-     *
-     * @param int $value Unix timestamp
-     * @return String Time (UTC) iCalendar formatted
-     */
-    public function _exportTime($value, $utc = false)
-    {
-        $time = date("His", $value);
-        if ($utc) {
-            $time .= 'Z';
-        }
-
-        return $time;
-    }
-
-    /**
-     * Export a Date field
-     */
-    public function _exportDate($value)
-    {
-        return date("Ymd", $value);
-    }
-
-    /**
-     * Export a duration value
-     */
-    public function _exportDuration($value)
-    {
-        $duration = '';
-        if ($value < 0) {
-            $value *= - 1;
-            $duration .= '-';
-        }
-        $duration .= 'P';
-
-        $weeks = floor($value / (7 * 86400));
-        $value = $value % (7 * 86400);
-        if ($weeks) {
-            $duration .= $weeks . 'W';
-        }
-
-        $days = floor($value / (86400));
-        $value = $value % (86400);
-        if ($days) {
-            $duration .= $days . 'D';
-        }
-
-        if ($value) {
-            $duration .= 'T';
-
-            $hours = floor($value / 3600);
-            $value = $value % 3600;
-            if ($hours) {
-                $duration .= $hours . 'H';
-            }
-
-            $mins = floor($value / 60);
-            $value = $value % 60;
-            if ($mins) {
-                $duration .= $mins . 'M';
-            }
-
-            if ($value) {
-                $duration .= $value . 'S';
-            }
-        }
-
-        return $duration;
-    }
-
-    /**
-     * Export a recurrence rule
-     */
-    public function _exportRecurrence($value)
-    {
-        $rrule = [];
-        // the last day of week in a MONTHLY or YEARLY recurrence in the
-        // Stud.IP calendar is 5, in iCalendar it is -1
-        if ($value['sinterval'] == '5')
-            $value['sinterval'] = '-1';
-
-        if ($value['count'])
-            unset($value['expire']);
-
-        foreach ($value as $r_param => $r_value) {
-            if ($r_value) {
-                switch ($r_param) {
-                    case 'rtype':
-                        $rrule[] = 'FREQ=' . $r_value;
-                        break;
-                    case 'expire':
-                        // end of unix epoche (this is also the end of Stud.IP epoche ;-) )
-                        if ($r_value < Calendar::CALENDAR_END)
-                            $rrule[] = 'UNTIL=' . $this->_exportDateTime($r_value, true);
-                        break;
-                    case 'linterval':
-                        $rrule[] = 'INTERVAL=' . $r_value;
-                        break;
-                    case 'wdays':
-                        switch ($value['rtype']) {
-                            case 'WEEKLY':
-                                $rrule[] = 'BYDAY=' . $this->_exportWdays($r_value);
-                                break;
-                            // Some CUAs (e.g. Outlook) don't understand the nWDAY syntax
-                            // (where n is the nth ocurrence of the day in a given period of
-                            // time and WDAY is the day of week) the RRULE uses the BYSETPOS
-                            // rule.
-                            case 'MONTHLY':
-                            case 'YEARLY':
-                                $rrule[] = 'BYDAY=' . $value['sinterval'] . $this->_exportWdays($r_value);
-                                $rrule[] = 'BYDAY=' . $this->_exportWdays($r_value);
-                                // The Stud.IP calendar don't support multiple values in a
-                                // comma separated list.
-
-                                if ($value['sinterval'])
-                                    $rrule[] = 'BYSETPOS=' . $value['sinterval'];
-
-                                break;
-                        }
-                        break;
-                    case 'day':
-                        $rrule[] = 'BYMONTHDAY=' . $r_value;
-                        break;
-                    case 'month':
-                        $rrule[] = 'BYMONTH=' . $r_value;
-                        break;
-                    case 'count':
-                        $rrule[] = 'COUNT=' . $r_value;
-                        break;
-                }
-            }
-        }
-
-        if ($value['rtype'] == 'WEEKLY' && CALENDAR_WEEKSTART != 'MO') {
-            $rrule[] = 'WKST=' . CALENDAR_WEEKSTART;
-        }
-
-        return implode(';', $rrule);
-    }
-
-    /**
-     * Return the Stud.IP calendar wdays attribute of a event recurrence
-     */
-    public function _exportWdays($value)
-    {
-        $wdays_map = ['1' => 'MO', '2' => 'TU', '3' => 'WE', '4' => 'TH', '5' => 'FR',
-            '6' => 'SA', '7' => 'SU'];
-        $wdays = [];
-        preg_match_all('/(\d)/', $value, $matches);
-        foreach ($matches[1] as $match) {
-            $wdays[] = $wdays_map[$match];
-        }
-
-        return implode(',', $wdays);
-    }
-
-    public function _exportExDate($value, $param)
-    {
-        $exdates = [];
-        $date_times = explode(',', $value);
-        foreach ($date_times as $date_time) {
-            $exdates[] = $this->_exportDate($date_time);
-        }
-
-        return implode(',', $exdates);
-    }
-
-    public function _exportExDateTime($value, $param)
-    {
-        $exdates = [];
-        $date_times = explode(',', $value);
-        foreach ($date_times as $date_time) {
-            $exdates[] = $this->_exportDate($date_time) . 'T' . $this->_exportTime($param);
-        }
-
-        return implode(',', $exdates);
-    }
-
-    private function _exportGroupEventProperties(Event $event)
-    {
-        $organizer = User::find($event->getAuthorId());
-        if ($organizer) {
-            $properties = $this->_foldLine('ORGANIZER;CN="'
-                    . $organizer->getFullName()
-                    . '":mailto:' . $organizer->Email)
-                    . $this->newline;
-        } else {
-            $properties = $this->_foldLine('ORGANIZER;CN="'
-                    . _('unbekannt')
-                    . '":mailto:' . $GLOBALS['user']->email)
-                    . $this->newline;
-        }
-        foreach ($event->attendees as $event_member) {
-            if ($event->getAuthorId() == $event_member->user->id) {
-                if ($event_member->user) {
-                    $properties .= $this->_foldLine('ATTENDEE;'
-                            . 'ROLE=REQ-PARTICIPANT;'
-                            . 'CN="' . $event_member->user->getFullName()
-                            . '":mailto:' . $event_member->user->Email)
-                            . $this->newline;
-                } else {
-                    $properties = '';
-                    /*
-                    $properties .= $this->_foldLine('ATTENDEE;'
-                            . 'ROLE=REQ-PARTICIPANT;'
-                            . 'CN="' . _('unbekannt') . '"')
-                            . $this->newline;
-                     *
-                     */
-                }
-            } else {
-                if ($event_member->user) {
-                    switch ($event_member->group_status) {
-                        case CalendarEvent::PARTSTAT_ACCEPTED :
-                            $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT'
-                                . ';PARTSTAT=ACCEPTED';
-                            break;
-                        case CalendarEvent::PARTSTAT_DELEGATED :
-                            $attendee = 'ATTENDEE;ROLE=NON-PARTICIPANT'
-                                . ';PARTSTAT=ACCEPTED'
-                                . ';DELEGATED-TO="mailto:'
-                                . $this->getFacultyEmail($organizer->getId())
-                                . '"';
-                            break;
-                        case CalendarEvent::PARTSTAT_DECLINED :
-                            $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT'
-                                . ';PARTSTAT=DECLINED';
-                            break;
-                        default :
-                            $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT';
-                            $attendee .= ';PARTSTAT=TENTATIVE';
-                            $attendee .= ';RSVP=TRUE';
-
-                    }
-                    $attendee .= ';CN="' . $event_member->user->getFullName()
-                            . '":mailto:' . $event_member->user->Email;
-                    /*
-                } else {
-                    $attendee .= ';CN="' . _('unbekannt') . '"';
-                }
-                     *
-                     */
-                    $properties .= $this->_foldLine($attendee) . $this->newline;
-                }
-            }
-        }
-        return $properties;
-    }
-
-    public function getFacultyEmail($user_id)
-    {
-        $stmt = DBManager::get()->prepare('SELECT email FROM Institute i '
-                . 'LEFT JOIN user_inst ui USING(institut_id) '
-                . 'WHERE i.Institut_id = fakultaets_id AND user_id = ?');
-        $stmt->execute([$user_id]);
-        return $stmt->fetchColumn();
-    }
-
-    public function _exportCategories($event)
-    {
-        return implode(',' ,$event->toStringCategories(true));
-    }
-
-    /**
-     * Return the folded version of a line
-     */
-    public function _foldLine($line)
-    {
-        $line = preg_replace('/(\r\n|\n|\r)/', '\n', $line);
-        if (mb_strlen($line) > 75) {
-            $foldedline = '';
-            while ($line !== '') {
-                $maxLine = mb_substr($line, 0, 75);
-                $cutPoint = max(60, max(mb_strrpos($maxLine, ';'), mb_strrpos($maxLine, ':')) + 1);
-
-                $foldedline .= ( empty($foldedline)) ?
-                        mb_substr($line, 0, $cutPoint) :
-                        $this->newline . ' ' . mb_substr($line, 0, $cutPoint);
-
-                $line = (mb_strlen($line) <= $cutPoint) ? '' : mb_substr($line, $cutPoint);
-            }
-            return $foldedline;
-        }
-        return $line;
-    }
-}
diff --git a/lib/calendar/lib/CalendarError.class.php b/lib/calendar/lib/CalendarError.class.php
deleted file mode 100644
index f796e1a98f42927938ba494bd0f748bd140fc64b..0000000000000000000000000000000000000000
--- a/lib/calendar/lib/CalendarError.class.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?
-# Lifter002: TODO
-# Lifter007: TODO
-
-/**
- * Error.class.php
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @package     calendar
- */
-
-
-class CalendarError
-{
-
-    var $status;
-    var $message;
-    var $file;
-    var $line;
-
-    public function __construct($status, $message, $file = '', $line = '')
-    {
-        $this->status = $status;
-        $this->message = $message;
-        $this->file = $file;
-        $this->line = $line;
-    }
-
-    public function getStatus()
-    {
-        return $this->status;
-    }
-
-    public function getMessage()
-    {
-        return $this->message;
-    }
-
-    public function getFile()
-    {
-        return $this->file;
-    }
-
-    public function getLine()
-    {
-        return $this->line;
-    }
-
-}
diff --git a/lib/calendar/lib/ErrorHandler.class.php b/lib/calendar/lib/ErrorHandler.class.php
deleted file mode 100644
index 390b5d521c019845d70eddc583dbcb11de07e210..0000000000000000000000000000000000000000
--- a/lib/calendar/lib/ErrorHandler.class.php
+++ /dev/null
@@ -1,119 +0,0 @@
-<?
-# Lifter002: TODO
-# Lifter007: TODO
-
-/**
- * ErrorHandler.class.php
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @package     calendar
- */
-
-class ErrorHandler
-{
-    // this is the state of the error handling if no error has occured
-    const ERROR_NORMAL = 1;
-    // this is the state of the error handling  a message has to be displayed
-    const ERROR_MESSAGE = 2;
-    // this is the state of the error handling if something was going wrong but without data-loss
-    const ERROR_WARNING = 4;
-    // this is the state of the error handling if a critical error occured (maybe with data loss)
-    const ERROR_CRITICAL = 8;
-    // this is the state of the error handling if a fatal error occured and the execution of the process (e.g. import of events) was stopped
-    const ERROR_FATAL = 16;
-
-    var $errors;
-    var $status;
-
-    public function __construct()
-    {
-        $this->errors = [];
-        $this->status = ErrorHandler::ERROR_NORMAL;
-    }
-
-    public function getStatus($status = NULL)
-    {
-        if ($status === NULL)
-            return $this->status;
-
-        return $status & $this->status;
-    }
-
-    public function getMaxStatus($status)
-    {
-        if ($status <= $this->status)
-            return true;
-
-        return false;
-    }
-
-    public function getMinStatus($status)
-    {
-        if ($status >= $this->status)
-            return true;
-
-        return false;
-    }
-
-    public function getErrors($status = NULL)
-    {
-        if ($status === NULL)
-            return $this->errors;
-
-        return $this->errors[$status];
-    }
-
-    public function getAllErrors()
-    {
-        $status = [ErrorHandler::ERROR_FATAL, ErrorHandler::ERROR_CRITICAL, ErrorHandler::ERROR_WARNING,
-            ErrorHandler::ERROR_MESSAGE, ErrorHandler::ERROR_NORMAL];
-        $errors = [];
-        foreach ($status as $stat) {
-            if (is_array($this->errors[$stat])) {
-                $errors = array_merge($errors, $this->errors[$stat]);
-            }
-        }
-        return $errors;
-    }
-
-    public function nextError($status)
-    {
-        if(is_array($this->errors[$status])) {
-            return reset($this->errors[$status]);
-        }
-        return false;
-    }
-
-    public function throwError($status, $message, $file = '', $line = '')
-    {
-        $this->errors[$status][] = new CalendarError($status, $message, $file, $line);
-        $this->status |= $status;
-        reset($this->errors[$status]);
-        if ($status == ErrorHandler::ERROR_FATAL) {
-            echo '<b>';
-            while ($error = $this->nextError(ErrorHandler::ERROR_FATAL)) {
-                echo '<br />' . $error->getMessage();
-            }
-            echo '</b><br />';
-            page_close();
-            exit;
-        }
-    }
-
-    public function throwSingleError($index, $status, $message, $file = '', $line = '')
-    {
-        static $index_list = [];
-
-        if ($index_list[$index] != 1) {
-            $this->throwError($status, $message, $file, $line);
-            $index_list[$index] = 1;
-        }
-    }
-}
diff --git a/lib/classes/Event.class.php b/lib/classes/Event.class.php
deleted file mode 100644
index 6254884e16d30302608227a015681c6b66ea0080..0000000000000000000000000000000000000000
--- a/lib/classes/Event.class.php
+++ /dev/null
@@ -1,223 +0,0 @@
-<?php
-
-/* 
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
- */
-
-interface Event
-{
-    const PERMISSION_FORBIDDEN = 0;
-    const PERMISSION_CONFIDENTIAL = 1;
-    const PERMISSION_READABLE = 2;
-    const PERMISSION_DELETABLE = 3;
-    const PERMISSION_WRITABLE = 4;
-    const PERMISSION_OWN = 5;
-    
-    /**
-     * Returns a list of all categories the event belongs to.
-     * Returns an empty string if no permission.
-     *
-     * @return string All categories as list.
-     */
-    public function toStringCategories();
-
-    /**
-     * Returns an array that represents the recurrence rule for this event.
-     * If an index is given, returns only this field of the rule.
-     * 
-     * @return array|string The array with th recurrence rule or only one field.
-     */
-    public function getRecurrence($index = null);
-    
-    /**
-     * TODO Wird das noch benötigt?
-     */
-    public function getType();
-    
-    /**
-     * Returns the title of this event.
-     * If the user has not the permission Event::PERMISSION_READABLE,
-     * the title is "Keine Berechtigung.".
-     * 
-     * @return string 
-     */
-    public function getTitle();
-    
-    /**
-     * Returns the starttime as unix timestamp of this event.
-     *
-     * @return int The starttime of this event as a unix timestamp
-     */
-    public function getStart();
-    
-    /**
-     * Returns the endtime as unix timestamp of this event.
-     *
-     * @return int the endtime of this event as a unix timestamp
-     */
-    public function getEnd();
-    
-    /**
-     * Returns the duration of this event in seconds.
-     *
-     * @return int the duration of this event in seconds
-     */
-    function getDuration();
-    
-    /**
-     * Returns the location.
-     * Without permission or the location is not set an empty string is returned.
-     * 
-     * @return string The location
-     */
-    function getLocation();
-    
-    /**
-     * Returns the global uni id of this event.
-     * 
-     * @return string The global unique id.
-     */
-    public function getUid();
-    
-    /**
-     * Returns the description of the topic.
-     * If the user has no permission or the event has no topic
-     * or the topics have no descritopn an empty string is returned.
-     *
-     * @return String the description
-     */
-    function getDescription();
-    
-    /**
-     * Returns the index of the category.
-     * If the user has no permission, 255 is returned.
-     * 
-     * @see config/config.inc.php $TERMIN_TYP
-     * @return int The index of the category
-     */
-    public function getCategory();
-    
-    /**
-     * Returns the user id of the last editor.
-     * 
-     * @return null|int The editor id.
-     */
-    public function getEditorId();
-    
-    /**
-     * Returns whether the event is a all day event.
-     * 
-     * @return 
-     */
-    public function isDayEvent();
-    
-    /**
-     * Returns the accessibility of this event. The value is not influenced by
-     * the permission of the actual user.
-     * 
-     * According to RFC5545 the accessibility (property CLASS) is represented
-     * by the 3 values PUBLIC, PRIVATE and CONFIDENTIAL. In RFC5545 the default
-     * value is PUBLIC. In Stud.IP the default is PRIVATE.
-     * 
-     * @return string The accessibility as string.
-     */
-    function getAccessibility();
-    
-    /**
-     * Returns the unix timestamp of the last change.
-     *
-     * @access public
-     */
-    public function getChangeDate();
-    
-    /**
-     * Returns the date time the event was imported.
-     * 
-     * TODO not sure if we need this anymore
-     * 
-     * @return int Date time of import as unix timestamp:
-     */
-    function getImportDate();
-    
-    /**
-     * Returns all properties of this event.
-     * The name of the properties correspond to the properties of the
-     * iCalendar calendar data exchange format. There are a few properties with
-     * the suffix STUDIP_ which have no eqivalent in the iCalendar format.
-     * 
-     * DTSTART: The start date-time as unix timestamp.
-     * DTEND: The end date-time as unix timestamp.
-     * SUMMARY: The short description (title) that will be displayed in the views.
-     * DESCRIPTION: The long description.
-     * UID: The global unique id of this event.
-     * CLASS:
-     * CATEGORIES: A comma separated list of categories.
-     * PRIORITY: The priority.
-     * LOCATION: The location.
-     * EXDATE: A comma separated list of unix timestamps.
-     * CREATED: The creation date-time as unix timestamp.
-     * LAST-MODIFIED: The date-time of last modification as unix timestamp.
-     * DTSTAMP: The cration date-time of this instance of the event as unix
-     * timestamp.
-     * RRULE: All data for the recurrence rule for this event as array.
-     * EVENT_TYPE:
-     * 
-     * 
-     * @return array The properties of this event.
-     */
-    public function getProperties();
-    
-    /**
-     * Returns the value of property with given name.
-     * 
-     * @param type $name See CalendarEvent::getProperties() for accepted values.
-     * @return mixed The value of the property.
-     * @throws InvalidArgumentException
-     */
-    public function getProperty($name);
-    
-    public function havePermission($permission, $user_id = null);
-    
-    public function getPermission($user_id = null);
-    
-    /**
-     * Returns the priority in a human readable form.
-     * If the user has no permission an epmty string will be returned.
-     * 
-     * @return string The priority as a string.
-     */
-    public function toStringPriority();
-    
-    /**
-     * Returns the accessibilty in a human readable form.
-     * If the user has no permission an epmty string will be returned.
-     * 
-     * @return string The accessibility as string.
-     */
-    public function toStringAccessibility();
-    
-    /**
-     * Returns a string representation of the recurrence rule.
-     * If $only_type is true returns only the type of the recurrence.
-     *
-     * @param bool $only_type If true returns only the type of recurrence.
-     * @return string The recurrence rule - human readable
-     */
-    public function toStringRecurrence($only_type = false);
-    
-    /**
-     * Returns the author of this event as user object.
-     * 
-     * @return User|null User object.
-     */
-    public function getAuthor();
-    
-    /**
-     * Returns the editor of this event as user object.
-     * 
-     * @return User|null User object.
-     */
-    public function getEditor();
-}
\ No newline at end of file
diff --git a/lib/classes/Event.interface.php b/lib/classes/Event.interface.php
new file mode 100644
index 0000000000000000000000000000000000000000..23f092b33877822e53f113459073bf549652fb57
--- /dev/null
+++ b/lib/classes/Event.interface.php
@@ -0,0 +1,184 @@
+<?php
+
+/*
+ * Event.interface.php - An interface for calendar events.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ */
+
+
+/**
+ * The Event interface represents calendar events.
+ */
+interface Event
+{
+    /**
+     * Retrieves events that lie in a given time range.
+     *
+     * @param DateTime $begin The beginning of the time range.
+     *
+     * @param DateTime $end The end of the time range.
+     *
+     * @param string $range_id The range for which to get the events. This may be a user-ID,
+     *     course-ID or another kind of ID.
+     *
+     * @return Event[] An array with event objects.
+     */
+    public static function getEvents(DateTime $begin, DateTime $end, string $range_id) : array;
+
+    /**
+     * Returns the ID of the event. This is the ID that is only
+     * valid inside of Stud.IP.
+     *
+     * @return string The ID of the event object.
+     */
+    public function getObjectId() : string;
+
+    /**
+     * Returns the ID of the primary object where this object is linked to
+     * in a primary-secondary relationship where this object is a secondary object.
+     *
+     * Example: A course date is a secondary object and the course it belongs to
+     * is the primary object.
+     *
+     * @return string The ID of the primary object or an empty string if the
+     *     implementation of the Event interface is a class of primary objects.
+     */
+    public function getPrimaryObjectID() : string;
+
+    /**
+     * Returns the class of the Event implementation.
+     *
+     * @return string The class name of the Event instance.
+     */
+    public function getObjectClass() : string;
+
+    /**
+     * Returns the title of this event.
+     * If the user has not the permission Event::PERMISSION_READABLE,
+     * the title is "Keine Berechtigung.".
+     *
+     * @return string The title of the event.
+     */
+    public function getTitle() : string;
+
+    /**
+     * Returns the start time of the event.
+     *
+     * @return DateTime The start time of the event.
+     */
+    public function getBegin() : DateTime;
+
+    /**
+     * Returns the end time of the event.
+     *
+     * @return DateTime The end time of the event.
+     */
+    public function getEnd() : DateTime;
+
+    /**
+     * Returns the duration of the event.
+     *
+     * @return DateInterval The duration of the event.
+     */
+    public function getDuration() : DateInterval;
+
+    /**
+     * Returns the location where the event takes place, if applicable.
+     *
+     * @return string The location of the event.
+     */
+    public function getLocation() : string;
+
+    /**
+     * Returns the global unique id of the event.
+     *
+     * @return string The global unique id of the event.
+     */
+    public function getUniqueId() : string;
+
+    /**
+     * Returns the description of the event.
+     *
+     * @return string The description of the event.
+     */
+    public function getDescription() : string;
+
+    /**
+     * Returns additional descriptions of the Event object.
+     * These are specific for each implementation.
+     *
+     * @return array Additional descriptions for the Event implementation.
+     *     Each array key represents a heading for the description and the
+     *     value contains the description itself as plain text.
+     *     In case this is not applicable for the implementation,
+     *     an empty array is returned.
+     */
+    public function getAdditionalDescriptions() : array;
+
+    /**
+     * Returns whether the event is an all day event or not.
+     *
+     * @return bool True, if the event is an all day event, false otherwise.
+     */
+    public function isAllDayEvent() : bool;
+
+    /**
+     * Determines whether the specified user has write permissions for the event.
+     *
+     * @param string $user_id The user for which to check write permissions.
+     *
+     * @return bool True, if the user has write permissions, false otherwise.
+     */
+    public function isWritable(string $user_id) : bool;
+
+    /**
+     * Returns the creation date of the event.
+     *
+     * @return DateTime The creation date of the event.
+     */
+    public function getCreationDate() : DateTime;
+
+    /**
+     * Returns the modification date of the event.
+     *
+     * @return DateTime The modification date of the event.
+     */
+    public function getModificationDate() : DateTime;
+
+    /**
+     * Returns the import date of the event.
+     *
+     * @return DateTime The import date of the event.
+     */
+    public function getImportDate() : DateTime;
+
+    /**
+     * Returns the author of this event as user object.
+     *
+     * @return User|null The user object of the author of the event, if available.
+     */
+    public function getAuthor() : ?User;
+
+    /**
+     * Returns the editor of this event as user object.
+     *
+     * @return User|null The user object of the editor of the event, if available.
+     */
+    public function getEditor() : ?User;
+
+    /**
+     * Returns a JSON-encoded fullcalendar event object that represents the event.
+     *
+     * @param $user_id string The user for which to generate the fullcalendar event.
+     *
+     * @return \Studip\Calendar\EventData The EventData representation of the event.
+     */
+    public function toEventData(string $user_id) : \Studip\Calendar\EventData;
+}
diff --git a/lib/classes/IcalExport.php b/lib/classes/IcalExport.php
index f011c5e79fd8a4f16942ebaf31d62cb5570d5367..2bb16d5342bbfa9349b88fb18897394cb19b768a 100644
--- a/lib/classes/IcalExport.php
+++ b/lib/classes/IcalExport.php
@@ -50,18 +50,25 @@ class IcalExport
         while ($length--) {
             while (1) {
                 $rnd = rand(48, 122);
-                if ($rnd < 48)
+                if ($rnd < 48) {
                     continue;
-                if ($rnd > 57 && $rnd < 65)
+                }
+                if ($rnd > 57 && $rnd < 65) {
                     continue;
-                if ($rnd > 90 && $rnd < 97)
+                }
+                if ($rnd > 90 && $rnd < 97) {
                     continue;
-                if ($rnd > 122)
+                }
+                if ($rnd > 122) {
                     continue;
+                }
                 $char = chr($rnd);
-                if ($rejected[$char] > 1) {
+                if (isset($rejected[$char]) && $rejected[$char] > 1) {
                     continue;
                 }
+                if (!isset($rejected[$char])) {
+                    $rejected[$char] = 0;
+                }
                 $rejected[$char]++;
                 $ret .= $char;
                 break;
diff --git a/lib/classes/JsonApi/Routes/Events/CourseEventsIndex.php b/lib/classes/JsonApi/Routes/Events/CourseEventsIndex.php
index 0ee6ded0218906d0274ff88bd6442c26696d926e..250d1442427bb1135bf96fb4f571b2c6c0a73942 100644
--- a/lib/classes/JsonApi/Routes/Events/CourseEventsIndex.php
+++ b/lib/classes/JsonApi/Routes/Events/CourseEventsIndex.php
@@ -27,19 +27,15 @@ class CourseEventsIndex extends JsonApiController
             throw new AuthorizationFailedException();
         }
 
-        $dates = $course->dates->map(function ($courseDate) {
-            return new \CourseEvent($courseDate->id);
-        });
-        $exDates = $course->ex_dates->map(function ($courseDate) {
-            return new \CourseCancelledEvent($courseDate->id);
-        });
-
-        $allDates = array_merge($dates, $exDates);
-        usort($allDates, function ($date1, $date2) {
+        $all_dates = array_merge(
+            $course->dates->getArrayCopy(),
+            $course->ex_dates->getArrayCopy()
+        );
+        usort($all_dates, function ($date1, $date2) {
             return intval($date1->date) <=> intval($date2->date);
         });
         list($offset, $limit) = $this->getOffsetAndLimit();
 
-        return $this->getPaginatedContentResponse(array_slice($allDates, $offset, $limit), count($allDates));
+        return $this->getPaginatedContentResponse(array_slice($all_dates, $offset, $limit), count($all_dates));
     }
 }
diff --git a/lib/classes/JsonApi/Routes/Events/UserEventsIcal.php b/lib/classes/JsonApi/Routes/Events/UserEventsIcal.php
index c8080c46803add729bb99fc62535f270da4b4de9..c068e6e7b5c8ad0bb4d016cd826f4db8555a8f11 100644
--- a/lib/classes/JsonApi/Routes/Events/UserEventsIcal.php
+++ b/lib/classes/JsonApi/Routes/Events/UserEventsIcal.php
@@ -24,14 +24,14 @@ class UserEventsIcal extends NonJsonApiController
             throw new RecordNotFoundException();
         }
 
-        $writer = new \CalendarWriterICalendar();
-        $export = new \CalendarExport($writer);
-        $export->exportFromDatabase($observedUser->id, 0, 2114377200, ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent']);
-        if ($GLOBALS['_calendar_error']->getMaxStatus(\ErrorHandler::ERROR_CRITICAL)) {
-            throw new InternalServerError();
-        }
+        $end = \DateTime::createFromFormat('U', '2114377200');
+        $start = new \DateTime();
+        $ical_export = new \ICalendarExport();
+        $ical = $ical_export->exportCalendarDates($observedUser->id, $start, $end)
+              . $ical_export->exportCourseDates($observedUser->id, $start, $end)
+              . $ical_export->exportCourseExDates($observedUser->id, $start, $end);
+        $content = $ical_export->writeHeader() . $ical . $ical_export->writeFooter();
 
-        $content = implode($export->getExport());
         $response->getBody()->write($content);
 
         return $response->withHeader('Content-Type', 'text/calendar')
diff --git a/lib/classes/JsonApi/Routes/Events/UserEventsIndex.php b/lib/classes/JsonApi/Routes/Events/UserEventsIndex.php
index 353382bb687b3668abccb29e01c85e5447fe02ce..192d279bb45c4425c580c980559b99002394dfc2 100644
--- a/lib/classes/JsonApi/Routes/Events/UserEventsIndex.php
+++ b/lib/classes/JsonApi/Routes/Events/UserEventsIndex.php
@@ -38,7 +38,14 @@ class UserEventsIndex extends JsonApiController
         }
         $end = strtotime('+2 weeks', $start);
 
-        $list = \SingleCalendar::getEventList($user->id, $start, $end, $user->id);
+        //See the RecordNotFoundException above: This route only lets user
+        //retrieve the dates of their own calendar. So no permission check
+        //is needed.
+        $start_dt = new \DateTime();
+        $start_dt->setTimestamp($start);
+        $end_dt = new \DateTime();
+        $end_dt->setTimestamp($end);
+        $list = \CalendarDateAssignment::getEvents($start_dt, $end_dt, $user->id);
         list($offset, $limit) = $this->getOffsetAndLimit();
 
         return $this->getPaginatedContentResponse(
diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php
index 71aadf7124a99463ac5de3ca0e962e9c1d8c2d1f..a0d21a3af044e82490a1bf287c1cd5e33de4b968 100644
--- a/lib/classes/JsonApi/SchemaMap.php
+++ b/lib/classes/JsonApi/SchemaMap.php
@@ -18,15 +18,16 @@ class SchemaMap
             \BlubberStatusgruppeThread::class => Schemas\BlubberStatusgruppeThread::class,
             \BlubberThread::class => Schemas\BlubberThread::class,
 
-            \CalendarEvent::class => Schemas\CalendarEvent::class,
+            \CalendarDateAssignment::class => Schemas\CalendarEvent::class,
             \ConsultationBlock::class => Schemas\ConsultationBlock::class,
             \ConsultationBooking::class => Schemas\ConsultationBooking::class,
             \ConsultationSlot::class => Schemas\ConsultationSlot::class,
             \ConfigValue::class => Schemas\ConfigValue::class,
-            \CourseEvent::class => Schemas\CourseEvent::class,
             \ContentTermsOfUse::class => Schemas\ContentTermsOfUse::class,
             \Course::class => Schemas\Course::class,
             \CourseMember::class => Schemas\CourseMember::class,
+            \CourseDate::class => Schemas\CourseEvent::class,
+            \CourseExDate::class => Schemas\CourseEvent::class,
             \FeedbackElement::class => Schemas\FeedbackElement::class,
             \FeedbackEntry::class => Schemas\FeedbackEntry::class,
             \JsonApi\Models\ForumCat::class => Schemas\ForumCategory::class,
diff --git a/lib/classes/JsonApi/Schemas/CalendarEvent.php b/lib/classes/JsonApi/Schemas/CalendarEvent.php
index 7b348d5478d8a456eb38abafaf64196376372cf4..fc5a1d81a47b8de83cf22a9811fe655884e2f5e5 100644
--- a/lib/classes/JsonApi/Schemas/CalendarEvent.php
+++ b/lib/classes/JsonApi/Schemas/CalendarEvent.php
@@ -18,17 +18,15 @@ class CalendarEvent extends SchemaProvider
     public function getAttributes($resource, ContextInterface $context): iterable
     {
         return [
-            'title' => $resource->title,
-            'description' => $resource->getDescription(),
-            'start' => date('c', $resource->getStart()),
-            'end' => date('c', $resource->getEnd()),
-            'categories' => $resource->toStringCategories(true),
-            'location' => $resource->getLocation(),
-// TODO: 'is-canceled'    => $singledate->isHoliday() ?: false,
-
-            'mkdate' => date('c', $resource->mkdate),
-            'chdate' => date('c', $resource->chdate),
-            'recurrence' => $resource->getRecurrence(),
+            'title' => $resource->calendar_date->title,
+            'description' => $resource->calendar_date->description,
+            'start' => date('c', $resource->calendar_date->begin),
+            'end' => date('c', $resource->calendar_date->end),
+            'categories' => $resource->calendar_date->getCategoryAsString(),
+            'location' => $resource->calendar_date->location,
+            'mkdate' => date('c', $resource->calendar_date->mkdate),
+            'chdate' => date('c', $resource->calendar_date->chdate),
+            'recurrence' => $resource->calendar_date->getRepetitionAsString(),
         ];
     }
 
@@ -39,7 +37,9 @@ class CalendarEvent extends SchemaProvider
     {
         $relationships = [];
 
-        if ($owner = $resource->getOwner()) {
+        $owner = $resource->user ?? $resource->course;
+
+        if ($owner) {
             $link = $this->createLinkToResource($owner);
             $relationships = [
                 self::REL_OWNER => [self::RELATIONSHIP_LINKS => [Link::RELATED => $link], self::RELATIONSHIP_DATA => $owner],
diff --git a/lib/classes/JsonApi/Schemas/Course.php b/lib/classes/JsonApi/Schemas/Course.php
index 09f11754e4016148866e640446d57d5fe06d1867..29a0c376380e0f28c5d6c20fb340110c2edf27a9 100644
--- a/lib/classes/JsonApi/Schemas/Course.php
+++ b/lib/classes/JsonApi/Schemas/Course.php
@@ -388,4 +388,29 @@ class Course extends SchemaProvider
 
         return array_merge($relationships, [self::REL_STATUS_GROUPS => $relation]);
     }
+
+    /**
+     * @inheritdoc
+     */
+    public function hasResourceMeta($resource): bool
+    {
+        return true;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function getResourceMeta($resource)
+    {
+        $avatar = \CourseAvatar::getAvatar($resource->id);
+
+        return [
+            'avatar' => [
+                'small' => $avatar->getURL(\Avatar::SMALL),
+                'medium' => $avatar->getURL(\Avatar::MEDIUM),
+                'normal' => $avatar->getURL(\Avatar::NORMAL),
+            ],
+        ];
+    }
+
 }
diff --git a/lib/classes/JsonApi/Schemas/CourseEvent.php b/lib/classes/JsonApi/Schemas/CourseEvent.php
index 77f1d3157f40023c3359d25935c39227af381317..e90ae0d030307fe84aba334e4a8461a8d191941e 100644
--- a/lib/classes/JsonApi/Schemas/CourseEvent.php
+++ b/lib/classes/JsonApi/Schemas/CourseEvent.php
@@ -18,16 +18,16 @@ class CourseEvent extends SchemaProvider
     public function getAttributes($resource, ContextInterface $context): iterable
     {
         return [
-            'title' => $resource->title,
+            'title' => isset($resource->course) ? $resource->course->getFullName() : '',
             'description' => $resource->getDescription(),
-            'start' => date('c', $resource->getStart()),
-            'end' => date('c', $resource->getEnd()),
-            'categories' => array_filter($resource->toStringCategories(true)),
-            'location' => $resource->getLocation(),
-
+            'start' => date('c', $resource->date),
+            'end' => date('c', $resource->end_time),
+            'categories' => '',
+            'location' => $resource->raum ?? '',
+            'is-cancelled' => $resource instanceof \CourseExDate,
             'mkdate' => date('c', $resource->mkdate),
             'chdate' => date('c', $resource->chdate),
-            'recurrence' => $resource->getRecurrence(),
+            'recurrence' => isset($resource->cycle) ? $resource->cycle->toString() : '',
         ];
     }
 
diff --git a/lib/classes/JsonApi/Schemas/Semester.php b/lib/classes/JsonApi/Schemas/Semester.php
index 75aef03c96bb3af028733dbcbbcb8d8e79d3a3eb..f66f90ca62e88a0f7dd6bfc455ac236be98a4c94 100644
--- a/lib/classes/JsonApi/Schemas/Semester.php
+++ b/lib/classes/JsonApi/Schemas/Semester.php
@@ -23,6 +23,7 @@ class Semester extends SchemaProvider
             'start-of-lectures' => date('c', $semester->vorles_beginn),
             'end-of-lectures' => date('c', $semester->vorles_ende),
             'visible' => (bool) $semester->visible,
+            'is-current' => $semester->isCurrent(),
         ];
     }
 
diff --git a/lib/classes/Privacy.php b/lib/classes/Privacy.php
index cbeb0a114025ffcf6d13bf1cf6b56498b4077c1f..670f30234093b751dafee85e209acf21e56855ec 100644
--- a/lib/classes/Privacy.php
+++ b/lib/classes/Privacy.php
@@ -28,7 +28,7 @@ class Privacy
         ],
         'date' => [
             'CalendarEvent',
-            'EventData',
+            'CalendarDate',
             'CourseDate',
             'CourseExDate',
         ],
diff --git a/lib/classes/UserManagement.class.php b/lib/classes/UserManagement.class.php
index 1a49aed31240f6310df5b683fe8546dde6c91e12..7dd38eaa570d5020e530f86e4086d190b44b43a1 100644
--- a/lib/classes/UserManagement.class.php
+++ b/lib/classes/UserManagement.class.php
@@ -1169,16 +1169,6 @@ class UserManagement
             if ($count) {
                 $msg .= 'info§' . sprintf(_('%s Einträge aus den Terminen gelöscht.'), $count) . '§';
             }
-            // delete membership in group calendars
-            if (Config::get()->CALENDAR_GROUP_ENABLE) {
-                $count = CalendarUser::deleteBySQL(
-                    'owner_id = :user_id OR user_id = :user_id',
-                    [':user_id' => $user_id]
-                );
-                if ($count) {
-                    $msg .= 'info§' . sprintf(_('%s Verknüpfungen mit Gruppenterminkalendern gelöscht.'), $count) . '§';
-                }
-            }
         }
 
         // delete all messages send or received by this user
diff --git a/lib/classes/calendar/Calendar.php b/lib/classes/calendar/Calendar.php
deleted file mode 100644
index abb2ec793d6d9086fadc83a895fc3932e8335d4a..0000000000000000000000000000000000000000
--- a/lib/classes/calendar/Calendar.php
+++ /dev/null
@@ -1,187 +0,0 @@
-<?php
-/**
- * Calendar.class.php - Holds some additional functions and constants
- * related to the personal calendar.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @since       3.2
- */
-class Calendar
-{
-    /**
-     * The (positive) end of unix epche
-     */
-    const CALENDAR_END = 0x7FFFFFFF;
-
-    /**
-     * The user is the owner of the calendar.
-     */
-    const PERMISSION_OWN = 16;
-
-    /**
-     * The user has administrative access to the calendar.
-     * Means, he is not the owner but have the same rights.
-     * Not in use at the moment.
-     */
-    const PERMISSION_ADMIN = 8;
-
-    /**
-     * The user can add new events and edit existing events in the calendar.
-     * If the owner of the calendar has created an confidential event, the only
-     * information the user get is the start and end time. The event is shown as
-     * busy time in the views for him.
-     * If the user adds a confidential event, only he and the owner has full
-     * access to it. The event is shown as busy time to all other users.
-     */
-    const PERMISSION_WRITABLE = 4;
-
-    /**
-     * The user can read all information of all events, except events marked as
-     * confidential. These events are shown as busy times in the views.
-     * The user can not add new events nor edit existing events.
-     */
-    const PERMISSION_READABLE = 2;
-
-    /**
-     * The user is not allowed to get any information about the calendar.
-     * The user has no access to the calendar but he see public events on the
-     * profile of the owner.
-     */
-    const PERMISSION_FORBIDDEN = 1;
-
-    /**
-     * The calendar is related to one user. He is the owner of the calendar.
-     */
-    const RANGE_USER = 1;
-
-    /**
-     * The calendar is related to a group of users
-     * ("contact group" or Statusgruppe).
-     * Not used at the moment.
-     * The implemeted group functionality shows all personal calendars of the
-     * members of a contact group. It is not a shared calendar where all members
-     * have access to.
-     */
-    const RANGE_GROUP = 2;
-
-    /**
-     * The calendar is a module of a course or studygroup. All members with
-     * status author, tutor or dozent have write access (PERMISSION_WRITABLE).
-     * Users with local status user has only read access (PERMISSION_READABLE).
-     */
-    const RANGE_SEM = 3;
-
-    /**
-     * The calendar is a module of an institute or faculty. All members with
-     * status author, tutor or dozent have write access (PERMISSION_WRITABLE).
-     * Users with local status user has only read access (PERMISSION_READABLE).
-     */
-    const RANGE_INST = 4;
-
-    /**
-     * Retrieves all contact groups (statusgruppen) owned by the given user
-     * where at least one member has granted access to his calender for the user.
-     *
-     * @param string $user_id User id of the owner.
-     * @return type
-     */
-    public static function getGroups($user_id)
-    {
-        $groups = [];
-        $calendar_owners = CalendarUser::getOwners($user_id)->pluck('owner_id');
-        $sg_groups = SimpleORMapCollection::createFromArray(
-                Statusgruppen::findByRange_id($user_id))
-                ->orderBy('position')
-                ->pluck('statusgruppe_id');
-        if (sizeof($calendar_owners)) {
-            $sg_users = StatusgruppeUser::findBySQL(
-                    'statusgruppe_id IN(?) AND user_id IN(?)',
-                    [$sg_groups, $calendar_owners]);
-            foreach ($sg_users as $sg_user) {
-                $groups[$sg_user->group->id] = $sg_user->group;
-            }
-        }
-        return $groups;
-    }
-
-    public static function GetInstituteActivatedCalendar($user_id)
-    {
-
-        $ret = [];
-        Institute::findAndMapBySQL(function($i) use (&$ret) {
-                if ($i->isToolActive('CoreCalendar')) {
-                    $ret[$i->id] = $i->name;
-                }
-            },
-            "JOIN user_inst USING(Institut_id)
-            WHERE user_id = ? AND inst_perms IN ('admin','dozent','tutor','autor')
-            ORDER BY Name ASC",
-            [$user_id]
-            );
-        return $ret;
-    }
-
-    /**
-     *
-     * @param string $user_id
-     * @return array
-     */
-    public static function GetCoursesActivatedCalendar($user_id)
-    {
-        $courses_user = SimpleCollection::createFromArray(
-                CourseMember::findByUser($user_id));
-        $courses = $courses_user->filter(function ($c) {
-            if ($c->course->isToolActive('CoreCalendar')) {
-                return $c;
-            }
-        });
-        return $courses->pluck('course');
-    }
-
-    public static function GetLecturers()
-    {
-        $stmt = DBManager::get()->prepare("SELECT aum.username, "
-                . "CONCAT(aum.Nachname,', ',aum.vorname) as fullname, "
-                . "aum.user_id FROM auth_user_md5 aum WHERE perms = 'dozent' "
-                . "ORDER BY fullname");
-        $stmt->execute();
-        $lecturers = [];
-        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
-            if ($row['user_id'] != $GLOBALS['user']->id) {
-                $lecturers[] = ['name' => $row['fullname'],
-                    'username' => $row['username'], 'id' => $row['user_id']];
-            }
-        }
-        return $lecturers;
-    }
-
-    /**
-     * Returns an array of default user settings for the calendar or a specific
-     * value if the index is given.
-     *
-     * @param string $index Index of setting to get.
-     * @return string|array Array of settings or one setting
-     */
-    public static function getDefaultUserSettings($index = null)
-    {
-        $default = [
-            'view'              => 'week',
-            'start'             => '9',
-            'end'               => '20',
-            'step_day'          => '900',
-            'step_week'         => '1800',
-            'type_week'         => 'LONG',
-            'step_week_group'   => '3600',
-            'step_day_group'    => '3600',
-            'show_declined'     => '0'
-        ];
-        return (is_null($index) ? $default : $default[$index]);
-    }
-}
diff --git a/lib/classes/calendar/CalendarInstscheduleModel.php b/lib/classes/calendar/CalendarInstscheduleModel.php
deleted file mode 100644
index e99da9d17011ab2d6fe6805dfd7f29a9ef7d8b1a..0000000000000000000000000000000000000000
--- a/lib/classes/calendar/CalendarInstscheduleModel.php
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php
-# Lifter010: TODO
-
-/*
- * This class is the model for the institute-calendar for seminars
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Till Glöggler <tgloeggl@uos.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- */
-
-require_once __DIR__ . '/default_color_definitions.php';
-
-/**
- * Pseudo-namespace containing helper methods for the calendar of institutes.
- *
- * @since      2.0
- */
-class CalendarInstscheduleModel
-{
-    /**
-     * Returns a schedule entry for a course
-     *
-     * @param string  $seminar_id  the ID of the course
-     * @param string  $user_id     the ID of the user
-     * @param string  $cycle_id    optional; if given, specifies the ID of the entry
-     * @return array  an array containing the properties of the entry
-     */
-    static function getSeminarEntry($seminar_id, $user_id, $cycle_id = false)
-    {
-        $ret = [];
-
-        $sem = new Seminar($seminar_id);
-        foreach ($sem->getCycles() as $cycle) {
-            if (!$cycle_id || $cycle->getMetaDateID() == $cycle_id) {
-                $entry = [];
-
-                $entry['id'] = $seminar_id;
-                $entry['cycle_id'] = $cycle->getMetaDateId();
-                $entry['start_formatted'] = sprintf("%02d", $cycle->getStartStunde()) .':'
-                    . sprintf("%02d", $cycle->getStartMinute());
-                $entry['end_formatted'] = sprintf("%02d", $cycle->getEndStunde()) .':'
-                    . sprintf("%02d", $cycle->getEndMinute());
-
-                $entry['start']   = ((int)$cycle->getStartStunde() * 100) + ($cycle->getStartMinute());
-                $entry['end']     = ((int)$cycle->getEndStunde() * 100) + ($cycle->getEndMinute());
-                $entry['day']     = $cycle->getDay();
-                $entry['content'] = $sem->getNumber() . ' ' . $sem->getName();
-                $entry['url']     = URLHelper::getLink('dispatch.php/calendar/instschedule/entry/' . $seminar_id
-                                  . '/' . $cycle->getMetaDateId());
-                $entry['onClick'] = "function(id) { STUDIP.Instschedule.showSeminarDetails('$seminar_id', '"
-                                  . $cycle->getMetaDateId() ."'); }";
-
-                $entry['title']   = '';
-                $ret[] = $entry;
-            }
-        }
-
-        return $ret;
-    }
-
-
-    /**
-     * Returns an array of CalendarColumn's, containing the seminar-entries
-     * for the passed user (in the passed semester belonging to the passed institute)
-     * The start- and end-hour are used to constrain the returned
-     * entries to the passed time-period. The passed days constrain the entries
-     * to these.
-     *
-     * @param string  $user_id       the ID of the user
-     * @param array   $semester      an array containing the "beginn" of the semester
-     * @param int     $start_hour    the start hour
-     * @param int     $end_hour      the end hour
-     * @param string  $institute_id  the ID of the institute
-     * @param array   $days          the days to be displayed
-     *
-     * @return array  an array containing the entries
-     */
-    static function getInstituteEntries($user_id, $semester, $start_hour, $end_hour, $institute_id, $days)
-    {
-        // fetch seminar-entries, show invisible seminars if the user has enough perms
-        $visibility_perms = $GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM);
-
-        $inst_ids = [];
-        $institut = new Institute($institute_id);
-
-        if (!$institut->isFaculty() || $GLOBALS['user']->cfg->MY_INSTITUTES_INCLUDE_CHILDREN) {
-            // If the institute is not a faculty or the child insts are included,
-            // pick the institute IDs of the faculty/institute and of all sub-institutes.
-            $inst_ids[] = $institute_id;
-            if ($institut->isFaculty()) {
-                foreach ($institut->sub_institutes->pluck("Institut_id") as $institut_id) {
-                    $inst_ids[] = $institut_id;
-                }
-            }
-        } else {
-            // If the institute is a faculty and the child insts are not included,
-            // pick only the institute id of the faculty:
-            $inst_ids[] = $institute_id;
-        }
-
-        $stmt = DBManager::get()->prepare("SELECT * FROM seminare
-            LEFT JOIN seminar_inst ON (seminare.Seminar_id = seminar_inst.seminar_id)
-            LEFT JOIN semester_courses ON (semester_courses.course_id = seminare.Seminar_id)
-            WHERE seminar_inst.institut_id IN (:institute)
-                AND (start_time <= :begin AND (semester_courses.semester_id IS NULL OR semester_courses.semester_id = :semester_id))
-                     "
-                    . (!$visibility_perms ? " AND visible='1'" : ""));
-
-        $stmt->bindValue(':begin', $semester['beginn']);
-        $stmt->bindValue(':semester_id', $semester['id']);
-        $stmt->bindValue(':institute', $inst_ids, StudipPDO::PARAM_ARRAY);
-        $stmt->execute();
-
-        while ($entry = $stmt->fetch()) {
-            $seminars[$entry['Seminar_id']] = $entry;
-        }
-
-        if (is_array($seminars)) foreach ($seminars as $data) {
-            $entries = self::getSeminarEntry($data['Seminar_id'], $user_id);
-
-            foreach ($entries as $entry) {
-                unset($entry['url']);
-                $entry['onClick'] = 'function(id) { STUDIP.Instschedule.showInstituteDetails(id); }';
-                $entry['visible'] = 1;
-
-                if (($entry['start'] >= $start_hour * 100 && $entry['start'] <= $end_hour * 100
-                    || $entry['end'] >= $start_hour * 100 && $entry['end'] <= $end_hour * 100)) {
-
-                    $entry['color'] = DEFAULT_COLOR_SEM;
-
-                    $day_number = ($entry['day'] + 6) % 7;
-                    if (!isset($ret[$day_number])) {
-                        $ret[$day_number] = CalendarColumn::create($day_number);
-                    }
-                    $ret[$day_number]->addEntry($entry);
-                }
-            }
-        }
-
-        return CalendarScheduleModel::addDayChooser($ret, $days, 'instschedule');
-    }
-}
diff --git a/lib/classes/calendar/CalendarScheduleModel.php b/lib/classes/calendar/CalendarScheduleModel.php
index ff9bce8e754a1cee1cbe0a17893f8bb0a7db7784..468b7e23842143d45a486cc48dcc8023a6c1ea2e 100644
--- a/lib/classes/calendar/CalendarScheduleModel.php
+++ b/lib/classes/calendar/CalendarScheduleModel.php
@@ -20,6 +20,8 @@ require_once __DIR__ . '/default_color_definitions.php';
  * Pseudo-namespace containing helper methods for the schedule.
  *
  * @since      2.0
+ *
+ * @deprecated since Stud.IP 5.5
  */
 class CalendarScheduleModel
 {
diff --git a/lib/calendar/EventData.class.php b/lib/classes/calendar/EventData.class.php
similarity index 89%
rename from lib/calendar/EventData.class.php
rename to lib/classes/calendar/EventData.class.php
index 9ab4377d8317b1544e6f5d81b8a2f337ef30da79..82afe6f3b7751e53dd3357d35f771ff168b6856e 100644
--- a/lib/calendar/EventData.class.php
+++ b/lib/classes/calendar/EventData.class.php
@@ -22,6 +22,8 @@ class EventData
     public $view_urls;
     public $api_urls;
     public $icon;
+    public $border_colour;
+    public $all_day;
 
     public function __construct(
         \DateTime $begin,
@@ -39,7 +41,9 @@ class EventData
         string $range_id,
         Array $view_urls = [],
         Array $api_urls = [],
-        string $icon = ''
+        string $icon = '',
+        string $border_colour = '',
+        bool $all_day = false
     )
     {
         $this->begin = $begin;
@@ -58,6 +62,8 @@ class EventData
         $this->view_urls = $view_urls;
         $this->api_urls = $api_urls;
         $this->icon = $icon;
+        $this->border_colour = $border_colour ?: $background_colour;
+        $this->all_day = $all_day;
     }
 
 
@@ -71,10 +77,12 @@ class EventData
             'resourceId' => $this->range_id,
             'start' => $this->begin->format('Y-m-d\TH:i:s'),
             'end' => $this->end->format('Y-m-d\TH:i:s'),
+            'allDay' => $this->all_day,
             'title' => $this->title,
             'classNames' => $this->event_classes,
             'textColor' => $this->text_colour,
             'color' => $this->background_colour,
+            'borderColor' => $this->border_colour,
             'editable' => $this->editable,
             'studip_weekday_begin' => $this->begin->format('N'),
             'studip_weekday_end' => $this->end->format('N'),
diff --git a/lib/calendar/EventSource.interface.php b/lib/classes/calendar/EventSource.interface.php
similarity index 100%
rename from lib/calendar/EventSource.interface.php
rename to lib/classes/calendar/EventSource.interface.php
diff --git a/lib/classes/calendar/Helper.php b/lib/classes/calendar/Helper.php
new file mode 100644
index 0000000000000000000000000000000000000000..10294f42a99c6a52dd63879596ed4cea59557e03
--- /dev/null
+++ b/lib/classes/calendar/Helper.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Studip\Calendar;
+
+class Helper
+{
+    /**
+     * Retrieves the time slot duration in the calendar for a specified calendar type
+     * and either the current user or a specific user.
+     *
+     * @param string $calendar_type The calendar type for which to retrieve the slot duration.
+     *     Valid values: 'week', 'day', 'week_group' (week group calendar), 'week_day' (day group calendar).
+     *     Defaults to 'week'.
+     * @param string $user_id The user for which to retrieve the slot duration. Defaults to an
+     *     empty string which then in turn means the current users slot duration is retrieved.
+     *
+     * @return string The slot duration as a time string in the form HH:MM:SS.
+     */
+    public static function getCalendarSlotDuration(string $calendar_type = 'week', string $user_id = '') : string
+    {
+        $default_slot_duration = '00:30:00';
+
+        $user_config = new \UserConfig($user_id ?: $GLOBALS['user']->id);
+        $calendar_settings = $user_config->CALENDAR_SETTINGS;
+
+        if (
+            $calendar_type === 'week'
+            && !empty($calendar_settings['step_week'])
+        ) {
+            $step_week = (int) $calendar_settings['step_week'];
+            $hours = floor($step_week / 3600);
+            $minutes = round(($step_week - $hours * 3600) / 60);
+            return sprintf('%1$02u:%2$02u:00', $hours, $minutes);
+        } elseif (
+            $calendar_type === 'day'
+            && !empty($calendar_settings['step_day'])
+        ) {
+            $step_day = (int) $calendar_settings['step_day'];
+            $hours = floor($step_day / 3600);
+            $minutes = round(($step_day - $hours * 3600) / 60);
+            return sprintf('%1$02u:%2$02u:00', $hours, $minutes);
+        } elseif (
+            $calendar_type === 'week_group'
+            && !empty($calendar_settings['step_week_group'])
+        ) {
+            $step_week = (int) $calendar_settings['step_week_group'];
+            $hours = floor($step_week / 3600);
+            $minutes = round(($step_week - $hours * 3600) / 60);
+            return sprintf('%1$02u:%2$02u:00', $hours, $minutes);
+        } elseif (
+            $calendar_type === 'day_group'
+            && !empty($calendar_settings['step_day_group'])
+        ) {
+            $step_day = (int) $calendar_settings['step_day_group'];
+            $hours = floor($step_day / 3600);
+            $minutes = round(($step_day - $hours * 3600) / 60);
+            return sprintf('%1$02u:%2$02u:00', $hours, $minutes);
+        }
+
+        // An unknown slot type or no appropriate match before:
+        // Return the default duration.
+        return $default_slot_duration;
+    }
+
+
+    /**
+     * Retrieves the default calendar date by various methods.
+     *
+     * @return \DateTime The default date for the calendar.
+     *     This defaults to the current date if no other date
+     *     can be retrieved.
+     */
+    public static function getDefaultCalendarDate() : \DateTime
+    {
+        $default_date = new \DateTime();
+        if (\Request::submitted('date')) {
+            $date = \Request::getDateTime('date', 'Y-m-d');
+            if ($date instanceof \DateTime) {
+                $default_date = $date;
+                //Update the session value:
+                $_SESSION['calendar_date'] = $default_date->format('Y-m-d');
+            }
+        } elseif (\Request::submitted('semester_id')) {
+            //A semester-ID is set, but no specific date that would override it.
+            //Use the first lecture week of the semester as default date.
+            $semester_id = \Request::option('semester_id');
+            $semester = \Semester::find($semester_id);
+            if ($semester) {
+                $default_date->setTimestamp($semester->vorles_beginn);
+                //Update the session value:
+                $_SESSION['calendar_date'] = $default_date->format('Y-m-d');
+            }
+        } elseif (!empty($_SESSION['calendar_date'])) {
+            $date = \DateTime::createFromFormat(
+                'Y-m-d',
+                $_SESSION['calendar_date'],
+                $default_date->getTimezone()
+            );
+            if ($date instanceof \DateTime) {
+                $default_date = $date;
+            }
+        }
+        $default_date->setTime(0,0,0);
+
+        return $default_date;
+    }
+}
diff --git a/lib/classes/calendar/ICalendarExport.class.php b/lib/classes/calendar/ICalendarExport.class.php
new file mode 100644
index 0000000000000000000000000000000000000000..fba097446b36023a4d340df484db3a9abb3e0923
--- /dev/null
+++ b/lib/classes/calendar/ICalendarExport.class.php
@@ -0,0 +1,638 @@
+<?php
+/**
+ * ICalendarExport.class.php
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Peter Thienel <thienel@data-quest.de>
+ * @author      Moritz Strohm <strohm@data-quest.de>
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ * @since       5.5
+ */
+
+
+class ICalendarExport
+{
+     /**
+     * Line break used in iCalendar
+     */
+    const NEWLINE = "\r\n";
+
+    /**
+     * Default start of the week
+     */
+    const WEEKSTART = 'MO';
+
+    /**
+     * Holds the time (as unix timestamp) used for
+     * the timestamp in every exported iCalendar object.
+     *
+     * @var int $time
+     */
+    private $time = 0;
+
+    public function __construct()
+    {
+        $this->default_filename_suffix = "ics";
+        $this->format = "iCalendar";
+    }
+
+    public function exportCalendarDates(string $range_id, DateTimeInterface $start, DateTimeInterface $end): string
+    {
+        if ($this->time === 0) {
+            $this->time = time();
+        }
+        $dates = CalendarDate::findBySQL(
+            "LEFT JOIN `calendar_date_assignments`
+                ON `calendar_dates`.`id` = `calendar_date_assignments`.`calendar_date_id`
+            WHERE
+                `calendar_date_assignments`.`range_id` = :range_id
+                AND (
+                    (`calendar_dates`.`begin` <= :end
+                        AND `calendar_dates`.`end` >= :begin)
+                    OR (`calendar_dates`.`repetition_type` != 'SINGLE'
+                        AND (`calendar_dates`.`repetition_end` >= :end
+                            OR `calendar_dates`.`repetition_end` = 0)
+                    AND `calendar_dates`.`begin` < :end))",
+            [
+                ':range_id' => $range_id,
+                ':begin'    => $start->getTimestamp(),
+                ':end'      => $end->getTimestamp(),
+            ]
+        );
+        $ical = '';
+        foreach ($dates as $date) {
+            $ical .= $this->writeICalEvent($this->prepareCalendarDate($date));
+        }
+        return $ical;
+    }
+
+    public function exportCourseDates(string $user_id, DateTimeInterface $start, DateTimeInterface $end)
+    {
+        if ($this->time === 0) {
+            $this->time = time();
+        }
+        $dates = CourseDate::findBySql(
+            "LEFT JOIN `seminar_user`
+                ON `termine`.`range_id` = `seminar_user`.`Seminar_id`
+            WHERE
+                `seminar_user`.`user_id` = :user_id
+                AND `seminar_user`.`bind_calendar` = 1
+                AND (`termine`.`date` <= :end
+                    AND `termine`.`end_time` >= :begin)",
+            [
+                ':user_id'  => $user_id,
+                ':begin'    => $start->getTimestamp(),
+                ':end'      => $end->getTimestamp(),
+            ]
+        );
+        $ical = '';
+        foreach ($dates as $date) {
+            $ical .= $this->writeICalEvent($this->prepareCourseDate($date));
+        }
+        return $ical;
+    }
+
+    public function exportCourseExDates(string $user_id, DateTimeInterface $start, DateTimeInterface $end)
+    {
+        if ($this->time === 0) {
+            $this->time = time();
+        }
+        $dates = CourseExDate::findBySql(
+            "LEFT JOIN `seminar_user`
+                ON `ex_termine`.`range_id` = `seminar_user`.`Seminar_id`
+            WHERE
+                `seminar_user`.`user_id` = :user_id
+                AND `seminar_user`.`bind_calendar` = 1
+                AND (`ex_termine`.`date` <= :end
+                    AND `ex_termine`.`end_time` >= :begin)",
+            [
+                ':user_id'  => $user_id,
+                ':begin'    => $start->getTimestamp(),
+                ':end'      => $end->getTimestamp(),
+            ]
+        );
+        $ical = '';
+        foreach ($dates as $date) {
+            $ical .= $this->writeICalEvent($this->prepareCourseDate($date));
+        }
+        return $ical;
+    }
+
+    /**
+     * @param CalendarDate $date
+     * @return array
+     */
+    public function prepareCalendarDate(CalendarDate $date): array
+    {
+        $properties =
+            [
+                'SUMMARY'       => $date->title,
+                'DESCRIPTION'   => $date->description,
+                'LOCATION'      => $date->location,
+                'CATEGORIES'    => $date->getCategoryAsString(),
+                'LAST-MODIFIED' => $date->chdate,
+                'CREATED'       => $date->mkdate,
+                'DTSTAMP'       => $this->time,
+                'DTSTART'       => $date->begin,
+                'DTEND'         => $date->end,
+                'EXDATE'        => implode(',', $date->exceptions->pluck('date')),
+                'PRIORITY'      => 5,
+                'RRULE'         => [
+                    'type'      => $date->repetition_type,
+                    'offset'    => $date->offset,
+                    'interval'  => $date->interval,
+                    'days'      => $date->days,
+                    'count'     => $date->number_of_dates,
+                    'expire'    => $date->repetition_end,
+                    'month'     => $date->month
+                ]
+            ];
+        return $properties;
+    }
+
+    public function prepareCourseDate(CourseDate $date): array
+    {
+        $properties =
+            [
+                'SUMMARY'       => $date->course->getFullname(),
+                'DESCRIPTION'   => '',
+                'LOCATION'      => $date->getRoomName(),
+                'CATEGORIES'    => $GLOBALS['TERMIN_TYP'][$date->date_typ]['name'],
+                'LAST-MODIFIED' => $date->chdate,
+                'CREATED'       => $date->mkdate,
+                'DTSTAMP'       => $this->time,
+                'DTSTART'       => $date->date,
+                'DTEND'         => $date->end_time,
+                'PRIORITY'      => ''
+            ];
+        return $properties;
+    }
+
+    /**
+     * Returns an iCalendar header with a rudimentary time zone definition.
+     *
+     * @return string The iCalendar header.
+     */
+    public function writeHeader()
+    {
+        // Default values
+        $header = "BEGIN:VCALENDAR" . self::NEWLINE;
+        $header .= "VERSION:2.0" . self::NEWLINE;
+        if (isset($this->client_identifier)) {
+            $header .= "PRODID:" . $this->client_identifier . self::NEWLINE;
+        } else {
+            $server_name = $_SERVER['SERVER_NAME'] ?? 'unknown';
+
+            $header .= "PRODID:-//Stud.IP@{$server_name}//Stud.IP_iCalendar Library";
+            $header .= " //EN" . self::NEWLINE;
+        }
+        $header .= "METHOD:PUBLISH" . self::NEWLINE;
+
+        // time zone definition CET/CEST
+        $header .= 'CALSCALE:GREGORIAN' . self::NEWLINE
+            . 'BEGIN:VTIMEZONE' . self::NEWLINE
+            . 'TZID:Europe/Berlin' . self::NEWLINE
+            . 'BEGIN:DAYLIGHT' . self::NEWLINE
+            . 'TZOFFSETFROM:+0100' . self::NEWLINE
+            . 'TZOFFSETTO:+0200' . self::NEWLINE
+            . 'TZNAME:CEST' . self::NEWLINE
+            . 'DTSTART:19700329T020000' . self::NEWLINE
+            . 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3' . self::NEWLINE
+            . 'END:DAYLIGHT' . self::NEWLINE
+            . 'BEGIN:STANDARD' . self::NEWLINE
+            . 'TZOFFSETFROM:+0200' . self::NEWLINE
+            . 'TZOFFSETTO:+0100' . self::NEWLINE
+            . 'TZNAME:CET' . self::NEWLINE
+            . 'DTSTART:19701025T030000' . self::NEWLINE
+            . 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10' . self::NEWLINE
+            . 'END:STANDARD' . self::NEWLINE
+            . 'END:VTIMEZONE' .self::NEWLINE;
+
+        return $header;
+    }
+
+    /**
+     * Returns the footer.
+     *
+     * @return string
+     */
+    public function writeFooter()
+    {
+        return "END:VCALENDAR" . self::NEWLINE;
+    }
+
+    /**
+     * Export prepared calendar data as iCalendar.
+     *
+     * @param array $properties The event to export.
+     * @return string iCalendar formatted data
+     */
+    public function writeICalEvent(array $properties): string
+    {
+        $result = "BEGIN:VEVENT" . self::NEWLINE;
+
+        foreach ($properties as $name => $value) {
+            $params = [];
+            $params_str = '';
+            if ($value === '' || is_null($value)) {
+                continue;
+            }
+            switch ($name) {
+                // not supported event properties
+                case 'SEMNAME':
+                    continue 2;
+
+                // Text fields
+                case 'SUMMARY':
+                    $value = $this->quoteText($value);
+                    break;
+                case 'DESCRIPTION':
+                    $value = $this->quoteText($value);
+                    break;
+                case 'LOCATION':
+                    $value = $this->quoteText($value);
+                    break;
+                case 'CATEGORIES':
+                    $value = $this->quoteText($value);
+                    break;
+
+                // Date fields
+                case 'LAST-MODIFIED':
+                case 'CREATED':
+                case 'COMPLETED':
+                    $value = $this->_exportDateTime($value, true);
+                    break;
+
+                case 'DTSTAMP':
+                    $value = $this->_exportDateTime(time(), true);
+                    break;
+
+                case 'DTSTART':
+                    $exdate_time = $value;
+                case 'DTEND':
+                case 'DUE':
+                case 'RECURRENCE-ID':
+                    if (array_key_exists('VALUE', $params)) {
+                        if ($params['VALUE'] == 'DATE') {
+                            $value = $this->_exportDate($value);
+                        } else {
+                            $value = $this->_exportDateTime($value);
+                            $params_str = ';TZID=Europe/Berlin';
+                        }
+                    } else {
+                        $value = $this->_exportDateTime($value);
+                        $params_str = ';TZID=Europe/Berlin';
+                    }
+                    break;
+
+                case 'EXDATE':
+                    if (array_key_exists('VALUE', $params)) {
+                        $value = $this->exportExDate($value);
+                    } else {
+                        $value = $this->exportExDateTime($value);
+                    }
+                    $params_str = ';TZID=Europe/Berlin';
+                    break;
+
+                // Integer fields
+                case 'PERCENT-COMPLETE':
+                case 'REPEAT':
+                case 'SEQUENCE':
+                    $value = "$value";
+                    break;
+
+                case 'PRIORITY':
+                    switch ($value) {
+                        case 1:
+                            $value = '1';
+                            break;
+                        case 2:
+                            $value = '5';
+                            break;
+                        case 3:
+                            $value = '9';
+                            break;
+                        default:
+                            $value = '0';
+                    }
+                    break;
+
+                // Geo fields
+                case 'GEO':
+                        $value = $value['latitude'] . ',' . $value['longitude'];
+                    break;
+
+                // Recursion fields
+                case 'EXRULE':
+                case 'RRULE':
+                    if ($value['type'] !== 'SINGLE') {
+                        $value = $this->_exportRecurrence($value);
+                    }
+                    break;
+
+                case "UID":
+                    $value = "$value";
+            }
+            if ($name && !is_array($value)) {
+                $attr_string = $name . $params_str . ':' . $value;
+                $result .= $this->foldLine($attr_string) . self::NEWLINE;
+            }
+        }
+        if (isset($properties['GROUP_EVENT'])) {
+            $result .= $this->exportGroupEventProperties($properties['GROUP_EVENT']);
+        }
+        $result .= "END:VEVENT" . self::NEWLINE;
+
+        return $result;
+    }
+
+    /**
+     * Quotes some characters accordingly to iCalendar format.
+     *
+     * @param string $text The text to quote.
+     * @return string The quoted text.
+     */
+    public function quoteText(string $text): string
+    {
+        $match = ['\\', '\n', ';', ','];
+        $replace = ['\\\\', '\\n', '\;', '\,'];
+        return str_replace($match, $replace, $text);
+    }
+
+    /**
+     * Export a DateTime field
+     *
+     * @param int $value Unix timestamp
+     * @return String Date and time (UTC) iCalendar formatted
+     */
+    public function _exportDateTime($value, $utc = false)
+    {
+        $date_time = new DateTime();
+        $date_time->setTimestamp($value);
+        //transform local time in UTC
+        if ($utc) {
+            $tz_utc = new DateTimeZone('UTC');
+            $date_time->setTimezone($tz_utc);
+            return $date_time->format('Ymd\THis\Z');
+        }
+        return $date_time->format('Ymd\THis');
+    }
+
+    /**
+     * Export a Time field
+     *
+     * @param int $value Unix timestamp
+     * @return String Time (UTC) iCalendar formatted
+     */
+    public function _exportTime($value, $utc = false)
+    {
+        $time = date("His", $value);
+        if ($utc) {
+            $time .= 'Z';
+        }
+
+        return $time;
+    }
+
+    /**
+     * Export a Date field
+     */
+    public function _exportDate($value)
+    {
+        return date("Ymd", $value);
+    }
+
+    /**
+     * Export a recurrence rule
+     */
+    public function _exportRecurrence($value)
+    {
+        $rrule = [];
+        // the last day of week in a MONTHLY or YEARLY recurrence in the
+        // Stud.IP calendar is 5, in iCalendar it is -1
+        if ($value['offset'] == '5') {
+            $value['offset'] = '-1';
+        }
+
+        if ($value['count']) {
+            unset($value['expire']);
+        }
+
+        foreach ($value as $r_param => $r_value) {
+            if ($r_value) {
+                switch ($r_param) {
+                    case 'type':
+                        $rrule[] = 'FREQ=' . $r_value;
+                        break;
+                    case 'expire':
+                        if ($r_value < CalendarDate::NEVER_ENDING)
+                            $rrule[] = 'UNTIL=' . $this->_exportDateTime($r_value, true);
+                        break;
+                    case 'interval':
+                        $rrule[] = 'INTERVAL=' . $r_value;
+                        break;
+                    case 'days':
+                        switch ($value['type']) {
+                            case 'WEEKLY':
+                                $rrule[] = 'BYDAY=' . $this->_exportWdays($r_value);
+                                break;
+                            // Some CUAs (e.g. Outlook) don't understand the nWDAY syntax
+                            // (where n is the nth ocurrence of the day in a given period of
+                            // time and WDAY is the day of week) the RRULE uses the BYSETPOS
+                            // rule.
+                            case 'MONTHLY':
+                            case 'YEARLY':
+                                $rrule[] = 'BYDAY=' . $value['offset'] . $this->_exportWdays($r_value);
+                                $rrule[] = 'BYDAY=' . $this->_exportWdays($r_value);
+                                if ($value['offset']) {
+                                    $rrule[] = 'BYSETPOS=' . $value['offset'];
+                                }
+                                break;
+                        }
+                        break;
+                    case 'day':
+                        $rrule[] = 'BYMONTHDAY=' . $r_value;
+                        break;
+                    case 'month':
+                        $rrule[] = 'BYMONTH=' . $r_value;
+                        break;
+                    case 'count':
+                        $rrule[] = 'COUNT=' . $r_value;
+                        break;
+                }
+            }
+        }
+
+        if ($value['type'] === 'WEEKLY' && self::WEEKSTART != 'MO') {
+            $rrule[] = 'WKST=' . self::WEEKSTART;
+        }
+
+        return implode(';', $rrule);
+    }
+
+    /**
+     * Return the days from CalendarDate::days as attribute of a event recurrence.
+     *
+     * @param string $value
+     * @return string
+     */
+    public function _exportWdays(string $value): string
+    {
+        $wdays_map = ['1' => 'MO', '2' => 'TU', '3' => 'WE', '4' => 'TH', '5' => 'FR',
+            '6' => 'SA', '7' => 'SU'];
+        $wdays = [];
+        preg_match_all('/(\d)/', $value, $matches);
+        foreach ($matches[1] as $match) {
+            $wdays[] = $wdays_map[$match];
+        }
+        return implode(',', $wdays);
+    }
+
+    /**
+     * Formats dates of exception.
+     *
+     * @param string $value Unix timestamps as csv list.
+     * @return string The formatted Exceptions.
+     */
+    public function exportExDate(string $value): string
+    {
+        $exdates = [];
+        $date_times = explode(',', $value);
+        foreach ($date_times as $date_time) {
+            $exdates[] = $this->_exportDate($date_time);
+        }
+        return implode(',', $exdates);
+    }
+
+    /**
+     * Formats date times of exception.
+     *
+     * @param string $value Unix timestamps as csv list.
+     * @return string The formatted Exceptions.
+     */
+    public function exportExDateTime(string $value): string
+    {
+        $ex_dates = [];
+        $ex_date_times = explode(',', $value);
+        foreach ($ex_date_times as $ex_date_time) {
+            $date_time = new DateTime();
+            $date_time->setTimestamp($ex_date_time);
+            $ex_dates[] = $date_time->format('Ymd\THis');
+        }
+        return implode(',', $ex_dates);
+    }
+
+    /**
+     * Returns iCalendar group event properties if the date has mor than one attendee.
+     *
+     * @param CalendarDate $date The date object to extract the group data from.
+     * @return string The formatted group event properties.
+     */
+    private function exportGroupEventProperties(CalendarDate $date): string
+    {
+        if (!count($date->calendars)) {
+            return '';
+        }
+        $organizer = $date->author;
+        if ($organizer) {
+            $properties = $this->foldLine('ORGANIZER;CN="'
+                    . $organizer->getFullName()
+                    . '":mailto:' . $organizer->Email)
+                . self::NEWLINE;
+        } else {
+            $properties = $this->foldLine('ORGANIZER;CN="'
+                    . _('unbekannt')
+                    . '":mailto:' . $GLOBALS['user']->email)
+                . self::NEWLINE;
+        }
+        foreach ($date->calendars as $calendar) {
+            if ($date->author_id === $calendar->range_id) {
+                if ($calendar->user) {
+                    $properties .= $this->foldLine('ATTENDEE;'
+                            . 'ROLE=REQ-PARTICIPANT;'
+                            . 'CN="' . $calendar->user->getFullName()
+                            . '":mailto:' . $calendar->user->Email)
+                        . self::NEWLINE;
+                } else {
+                    $properties = '';
+                }
+            } else {
+                if ($calendar->user) {
+                    switch ($calendar->participation) {
+                        case 'ACCEPTED' :
+                            $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT'
+                                . ';PARTSTAT=ACCEPTED';
+                            break;
+                        case 'ACKNOWLEDGED' :
+                            $attendee = 'ATTENDEE;ROLE=NON-PARTICIPANT'
+                                . ';PARTSTAT=ACCEPTED'
+                                . ';DELEGATED-TO="mailto:'
+                                . $this->getFacultyEmail($organizer->id)
+                                . '"';
+                            break;
+                        case 'DECLINED' :
+                            $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT'
+                                . ';PARTSTAT=DECLINED';
+                            break;
+                        default :
+                            $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT';
+                            $attendee .= ';PARTSTAT=TENTATIVE';
+                            $attendee .= ';RSVP=TRUE';
+
+                    }
+                    $attendee .= ';CN="' . $calendar->user->getFullName()
+                        . '":mailto:' . $calendar->user->Email;
+                    $properties .= $this->foldLine($attendee) . self::NEWLINE;
+                }
+            }
+        }
+        return $properties;
+    }
+
+    /**
+     * @param string $user_id
+     * @return string
+     */
+    private function getFacultyEmail(string $user_id): string
+    {
+        $stmt = DBManager::get()->prepare('
+            SELECT `email`
+            FROM `Institute`
+            LEFT JOIN `user_inst` USING(`institut_id`)
+            WHERE `Institute`.`Institut_id` = `fakultaets_id`
+              AND `user_id` = ?');
+        $stmt->execute([$user_id]);
+        return $stmt->fetchColumn();
+    }
+
+    /**
+     * Returns the folded version of a text line.
+     *
+     * @param string $line
+     * @return string
+     */
+    private function foldLine(string $line): string
+    {
+        $line = preg_replace('/(\r\n|\n|\r)/', '\n', $line);
+        if (mb_strlen($line) > 75) {
+            $foldedline = '';
+            while ($line !== '') {
+                $maxLine = mb_substr($line, 0, 75);
+                $cutPoint = max(60, max(mb_strrpos($maxLine, ';'), mb_strrpos($maxLine, ':')) + 1);
+
+                $foldedline .= ( empty($foldedline)) ?
+                    mb_substr($line, 0, $cutPoint) :
+                    self::NEWLINE . ' ' . mb_substr($line, 0, $cutPoint);
+
+                $line = (mb_strlen($line) <= $cutPoint) ? '' : mb_substr($line, $cutPoint);
+            }
+            return $foldedline;
+        }
+        return $line;
+    }
+}
diff --git a/lib/calendar/CalendarParserICalendar.class.php b/lib/classes/calendar/ICalendarImport.class.php
similarity index 59%
rename from lib/calendar/CalendarParserICalendar.class.php
rename to lib/classes/calendar/ICalendarImport.class.php
index 241580af198eef066427216cc20587ad6d18b27b..e78696d58fe4254855fae850b779ede07b1c6010 100644
--- a/lib/calendar/CalendarParserICalendar.class.php
+++ b/lib/classes/calendar/ICalendarImport.class.php
@@ -1,91 +1,88 @@
-<?
-# Lifter002: TODO
-# Lifter007: TODO
-
-/**
- * CalendarParserICalendar.class.php
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @package     calendar
- */
-
-class CalendarParserICalendar extends CalendarParser
+<?php
+class ICalendarImport
 {
-    public $type = '';
-    protected $count = null;
+    private $range_id;
 
-    public function __construct()
+    private $count = 0;
+
+    private $dates = [];
+
+    private $import_time;
+
+    private $convert_to_private = false;
+
+    public function __construct($range_id)
     {
-        parent::__construct();
-        $this->type = 'iCalendar';
-        // initialize error handler
-        $GLOBALS['_calendar_error'] = new ErrorHandler();
+        $this->range_id = $range_id;
+        $this->import_time = time();
     }
 
-    public function getCount($data)
+    public function import($ical_data)
+    {
+        $this->parse($ical_data);
+    }
+
+    public function countEvents($ical_data)
     {
         $matches = [];
         if (is_null($this->count)) {
             // Unfold any folded lines
-            $data = preg_replace('/\x0D?\x0A[\x20\x09]/', '', $data);
-            preg_match_all('/(BEGIN:VEVENT(\r\n|\r|\n)[\W\w]*?END:VEVENT\r?\n?)/', $data, $matches);
+            $data = preg_replace('/\x0D?\x0A[\x20\x09]/', '', $ical_data);
+            preg_match_all('/(BEGIN:VEVENT(\r\n|\r|\n)[\W\w]*?END:VEVENT\r?\n?)/', $ical_data, $matches);
             $this->count = sizeof($matches[1]);
         }
 
         return $this->count;
     }
 
+    public function getCountEvents() : int
+    {
+        return (int) $this->count;
+    }
+
+    public function convertPublicToPrivate(bool $to_private = true) : void
+    {
+        $this->convert_to_private = $to_private;
+    }
+
     /**
      * Parse a string containing vCalendar data.
      *
      * @access private
-     * @param String $data  The data to parse
-     *
+     * @param string $data  The data to parse
      */
-    public function parse($data, $ignore = null)
+    public function parse(string $data)
     {
-        global $_calendar_error, $PERS_TERMIN_KAT;
-
         // match categories
         $studip_categories = [];
         $i = 1;
-        foreach ($PERS_TERMIN_KAT as $cat) {
+        foreach ($GLOBALS['PERS_TERMIN_KAT'] as $cat) {
             $studip_categories[mb_strtolower($cat['name'])] = $i++;
         }
 
         // Unfold any folded lines
         // the CR is optional for files imported from Korganizer (non-standard)
-        $data = preg_replace('/\x0D?\x0A[\x20\x09]/', '', $data);
+        $data = $this->unfoldLine($data);
 
         if (!preg_match('/BEGIN:VCALENDAR(\r\n|\r|\n)([\W\w]*)END:VCALENDAR\r?\n?/', $data, $matches)) {
-            $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Import-Datei ist keine gültige iCalendar-Datei!"));
-            return false;
+            throw new UnexpectedValueException();
         }
 
         // client identifier
-        if (!$this->_parseClientIdentifier($matches[2])) {
-            return false;
+        if (!$this->parseClientIdentifier($matches[2])) {
+            throw new UnexpectedValueException();
         }
 
         // All sub components
         if (!preg_match_all('/BEGIN:VEVENT(\r\n|\r|\n)([\w\W]*?)END:VEVENT(\r\n|\r|\n)/', $matches[2], $v_events)) {
-            $_calendar_error->throwError(ErrorHandler::ERROR_MESSAGE, _("Die importierte Datei enthält keine Termine."));
-            return true;
+            // _("Die importierte Datei enthält keine Termine.")
+            throw new UnexpectedValueException();
         }
 
         if ($this->count) {
             $this->count = 0;
         }
         foreach ($v_events[2] as $v_event) {
-            $properties['CLASS'] = 'PRIVATE';
-            // Parse the remain attributes
 
             if (preg_match_all('/(.*):(.*)(\r|\n)+/', $v_event, $matches)) {
                 $properties = [];
@@ -111,7 +108,7 @@ class CalendarParserICalendar extends CalendarParser
                         if ($params['ENCODING']) {
                             switch ($params['ENCODING']) {
                                 case 'QUOTED-PRINTABLE':
-                                    $value = $this->_qp_decode($value);
+                                    $value = $this->qp_decode($value);
                                     break;
 
                                 case 'BASE64':
@@ -139,7 +136,7 @@ class CalendarParserICalendar extends CalendarParser
                                     $categories[] = $category;
                                 } else if (!$properties['STUDIP_CATEGORY']) {
                                     $properties['STUDIP_CATEGORY']
-                                            = $studip_categories[mb_strtolower($category)];
+                                        = $studip_categories[mb_strtolower($category)];
                                 }
                             }
                             $properties[$tag] = implode(',', $categories);
@@ -147,12 +144,11 @@ class CalendarParserICalendar extends CalendarParser
 
                         // Date fields
                         case 'DCREATED': // vCalendar property name for "CREATED"
-                            $tag = "CREATED";
                         case 'DTSTAMP':
                         case 'COMPLETED':
                         case 'CREATED':
                         case 'LAST-MODIFIED':
-                            $properties[$tag] = $this->_parseDateTime($value);
+                            $properties[$tag] = $this->parseDateTime($value);
                             break;
 
                         case 'DTSTART':
@@ -162,30 +158,30 @@ class CalendarParserICalendar extends CalendarParser
                                 $check['DAY_EVENT'] = true;
                         case 'DUE':
                         case 'RECURRENCE-ID':
-                            $properties[$tag] = $this->_parseDateTime($value);
+                            $properties[$tag] = $this->parseDateTime($value);
                             break;
 
                         case 'RDATE':
                             if (array_key_exists('VALUE', $params)) {
                                 if ($params['VALUE'] == 'PERIOD') {
-                                    $properties[$tag] = $this->_parsePeriod($value);
+                                    $properties[$tag] = $this->parsePeriod($value);
                                 } else {
-                                    $properties[$tag] = $this->_parseDateTime($value);
+                                    $properties[$tag] = $this->parseDateTime($value);
                                 }
                             } else {
-                                $properties[$tag] = $this->_parseDateTime($value);
+                                $properties[$tag] = $this->parseDateTime($value);
                             }
                             break;
 
                         case 'TRIGGER':
                             if (array_key_exists('VALUE', $params)) {
                                 if ($params['VALUE'] == 'DATE-TIME') {
-                                    $properties[$tag] = $this->_parseDateTime($value);
+                                    $properties[$tag] = $this->parseDateTime($value);
                                 } else {
-                                    $properties[$tag] = $this->_parseDuration($value);
+                                    $properties[$tag] = $this->parseDuration($value);
                                 }
                             } else {
-                                $properties[$tag] = $this->_parseDuration($value);
+                                $properties[$tag] = $this->parseDuration($value);
                             }
                             break;
 
@@ -198,12 +194,12 @@ class CalendarParserICalendar extends CalendarParser
                             foreach ($values[1] as $value) {
                                 if (array_key_exists('VALUE', $params)) {
                                     if ($params['VALUE'] == 'DATE-TIME') {
-                                        $dates[] = $this->_parseDateTime($value);
+                                        $dates[] = $this->parseDateTime($value);
                                     } else if ($params['VALUE'] == 'DATE') {
-                                        $dates[] = $this->_parseDate($value);
+                                        $dates[] = $this->parseDate($value);
                                     }
                                 } else {
-                                    $dates[] = $this->_parseDateTime($value);
+                                    $dates[] = $this->parseDateTime($value);
                                 }
                             }
                             // some iCalendar exports (e.g. KOrganizer) use an EXDATE-entry for every
@@ -213,7 +209,7 @@ class CalendarParserICalendar extends CalendarParser
 
                         // Duration fields
                         case 'DURATION':
-                            $attibutes[$tag] = $this->_parseDuration($value);
+                            $attibutes[$tag] = $this->parseDuration($value);
                             break;
 
                         // Period of time fields
@@ -222,7 +218,7 @@ class CalendarParserICalendar extends CalendarParser
                             $periods = [];
                             preg_match_all('/,([^,]*)/', ',' . $value, $values);
                             foreach ($values[1] as $value) {
-                                $periods[] = $this->_parsePeriod($value);
+                                $periods[] = $this->parsePeriod($value);
                             }
 
                             $properties[$tag] = $periods;
@@ -231,11 +227,11 @@ class CalendarParserICalendar extends CalendarParser
                         // UTC offset fields
                         case 'TZOFFSETFROM':
                         case 'TZOFFSETTO':
-                            $properties[$tag] = $this->_parseUtcOffset($value);
+                            $properties[$tag] = $this->parseUtcOffset($value);
                             break;
 
                         case 'PRIORITY':
-                            $properties[$tag] = $this->_parsePriority($value);
+                            $properties[$tag] = $this->parsePriority($value);
                             break;
 
                         case 'CLASS':
@@ -269,7 +265,7 @@ class CalendarParserICalendar extends CalendarParser
                         // Recursion fields
                         case 'EXRULE':
                         case 'RRULE':
-                            $properties[$tag] = $this->_parseRecurrence($value);
+                            $properties[$tag] = $this->parseRecurrence($value);
                             break;
 
                         default:
@@ -279,20 +275,22 @@ class CalendarParserICalendar extends CalendarParser
                     }
                 }
 
-                if (!$properties['RRULE']['rtype'])
+                if (!$properties['RRULE']['rtype']) {
                     $properties['RRULE'] = ['rtype' => 'SINGLE'];
+                }
 
-                if (!$properties['LAST-MODIFIED'])
-                    $properties['LAST-MODIFIED'] = $properties['CREATED'];
+                if (!$properties['LAST-MODIFIED']) {
+                    $properties['LAST-MODIFIED'] = $properties['DTSTAMP'] ?: $properties['CREATED'] ?? time();
+                }
 
                 if (!$properties['DTSTART'] || ($properties['EXDATE'] && !$properties['RRULE'])) {
-                    $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!"));
-                    $this->count = 0;
-                    return false;
+                    // _("Die Datei ist keine gültige iCalendar-Datei!")
+                    throw new UnexpectedValueException();
                 }
 
-                if (!$properties['DTEND'])
+                if (!$properties['DTEND']) {
                     $properties['DTEND'] = $properties['DTSTART'];
+                }
 
                 // day events starts at 00:00:00 and ends at 23:59:59
                 if ($check['DAY_EVENT'])
@@ -300,7 +298,7 @@ class CalendarParserICalendar extends CalendarParser
 
                 // default: all imported events are set to private
                 if (!$properties['CLASS']
-                        || ($this->public_to_private && $properties['CLASS'] == 'PUBLIC')) {
+                    || ($this->convert_to_private && $properties['CLASS'] == 'PUBLIC')) {
                     $properties['CLASS'] = 'PRIVATE';
                 }
 
@@ -312,11 +310,10 @@ class CalendarParserICalendar extends CalendarParser
                  *
                  */
 
-                $this->components[] = $properties;
+                $this->createDateFromProperties($properties);
             } else {
-                $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!"));
-                $this->count = 0;
-                return false;
+                // _("Die Datei ist keine gültige iCalendar-Datei!")
+                throw new InvalidValuesException();
             }
             $this->count++;
         }
@@ -324,163 +321,210 @@ class CalendarParserICalendar extends CalendarParser
         return true;
     }
 
+    private function createDateFromProperties($properties)
+    {
+        $date = CalendarDate::findOneBySQL(
+            'LEFT JOIN `calendar_date_assignments`
+              ON `calendar_dates`.`id` = `calendar_date_assignments`.`calendar_date_id`
+            WHERE `calendar_dates`.`unique_id` = :uid
+              AND `calendar_date_assignments`.`range_id` = :range_id',
+            [
+                ':uid'      => $properties['UID'],
+                ':range_id' => $this->range_id
+            ]
+        );
+
+        if (!$date) {
+            $date = new CalendarDate();
+            $date->id = $date->getNewId();
+            $date->author_id = $this->range_id;
+            $date->editor_id = $this->range_id;
+            $range_date = new CalendarDateAssignment();
+            $range_date->range_id = $this->range_id;
+            $range_date->participation = '';
+            $date->calendars[] = $range_date;
+        }
+
+        $date->begin = $properties['DTSTART']->getTimestamp();
+        $date->end = $properties['DTEND']->getTimestamp();
+        $date->title = $properties['SUMMARY'];
+        $date->description = $properties['DESCRIPTION'];
+        $date->access = $properties['CLASS'] ?? 'PRIVATE';
+        $date->user_category = $properties['CATEGORIES'];
+        $date->category = $properties['STUDIP_CATEGORY'] ?: 1;
+        $date->priority = $properties['PRIORITY'] ?? '';
+        $date->location = $properties['LOCATION'];
+        if (is_array($properties['EXDATE'])) {
+            foreach ($properties['EXDATE'] as $exdate) {
+                $exception = new CalendarDateException();
+                $exception->date = $exdate->format('Y-m-d');
+                $date->exceptions[] = $exception;
+            }
+        }
+        $date->mkdate = $properties['CREATED'] ? $properties['CREATED']->getTimestamp() : time();
+        if (isset($properties['LAST-MODIFIED'])) {
+            $date->chdate = $properties['LAST-MODIFIED']->getTimestamp();
+        } else {
+            $date->chdate = $date->mkdate;
+        }
+        $date->import_date = $this->import_time;
+        $date->unique_id = $properties['UID'];
+
+        $this->setRecurrenceRule($date, $properties['RRULE']);
+        $date->store();
+    }
+
+    private function setRecurrenceRule(CalendarDate $date, $rrule)
+    {
+        $date->interval = $rrule['linterval'] ?? 1;
+        if (strlen($rrule['wdays'] ?? '')) {
+            $date->offset = $rrule['sinterval'] ?? 0;
+            $date->days = $rrule['wdays'] ?? null;
+        } else {
+            $date->offset = $rrule['day'] ?? 0;
+            $date->days = $rrule['sinterval'] ?? null;
+        }
+        $date->month = $rrule['month'] ?? null;
+        $date->repetition_type = $rrule['rtype'] ?? 'SINGLE';
+        $date->number_of_dates = $rrule['count'] ?? 1;
+        $date->repetition_end = $rrule['expire'] ?? 0;
+    }
+
+    private function unfoldLine($data)
+    {
+        return preg_replace('/\x0D?\x0A[\x20\x09]/', '', $data);
+    }
+
     /**
      * Parse a UTC Offset field
      */
-    private function _parseUtcOffset($text)
+    private function parseUtcOffset($offset_text)
     {
         $offset = 0;
-        if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $matches)) {
+        if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $offset_text, $matches)) {
             $offset += 3600 * intval($matches[2]);
             $offset += 60 * intval($matches[3]);
             $offset *= ( $matches[1] == '+' ? 1 : -1);
             if (array_key_exists(4, $matches)) {
                 $offset += intval($matches[4]);
             }
-            return $offset;
-        } else {
-            return false;
         }
+        return $offset;
     }
 
     /**
      * Parse a Time Period field
      */
-    private function _parsePeriod($text)
+    private function parsePeriod($period_text): array
     {
-        $matches = explode('/', $text);
+        $matches = explode('/', $period_text);
 
-        $start = $this->_parseDateTime($matches[0]);
+        $start = $this->parseDateTime($matches[0]);
 
-        if ($duration = $this->_parseDuration($matches[1])) {
+        if ($duration = $this->parseDuration($matches[1])) {
             return ['start' => $start, 'duration' => $duration];
-        } else if ($end = $this->_parseDateTime($matches[1])) {
+        } else if ($end = $this->parseDateTime($matches[1])) {
             return ['start' => $start, 'end' => $end];
         }
+        return [];
     }
 
     /**
      * Parse a DateTime field
      */
-    private function _parseDateTime($text)
+    private function parseDateTime(String $date_time)
     {
-        $dateParts = explode('T', $text);
-        if (count($dateParts) != 2 && !empty($text)) {
-            // not a date time field but may be just a date field
-            if (!$date = $this->_parseDate($text)) {
-                return $date;
-            }
-            $date = $this->_parseDate($text);
-            return mktime(0, 0, 0, $date['month'], $date['mday'], $date['year']);
+        $parts = explode('T', $date_time);
+        if (count($parts) != 2) {
+            // not a date time string but may be just a date string
+            $date = $this->parseDate($date_time);
+            return DateTimeImmutable::createFromFormat('YmdHis', implode('', $date) . '000000');
         }
 
-        if (!$date = $this->_parseDate($dateParts[0])) {
-            return $date;
-        }
-        if (!$time = $this->_parseTime($dateParts[1])) {
-            return $time;
-        }
+        $date = $this->parseDate($parts[0]);
+        $time = $this->parseTime($parts[1]);
 
         if ($time['zone'] == 'UTC') {
-            return gmmktime($time['hour'], $time['minute'], $time['second'], $date['month'], $date['mday'], $date['year']);
+            $time_zone = new DateTimeZone('UTC');
         } else {
-            return mktime($time['hour'], $time['minute'], $time['second'], $date['month'], $date['mday'], $date['year']);
+            $time_zone = new DateTimeZone('Europe/Berlin');
         }
+        return DateTimeImmutable::createFromFormat(
+            'YmdHis',
+            implode('', $date) . $time['hour'] . $time['minute'] . $time['second'],
+            $time_zone
+        );
     }
 
     /**
      * Parse a Time field
      */
-    private function _parseTime($text)
+    private function parseTime($time_text): array
     {
-        if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $matches)) {
-            $time['hour'] = intval($matches[1]);
-            $time['minute'] = intval($matches[2]);
-            $time['second'] = intval($matches[3]);
+        $matches = [];
+        if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $time_text, $matches)) {
+            $time['hour'] = $matches[1];
+            $time['minute'] = $matches[2];
+            $time['second'] = $matches[3];
             if (array_key_exists(4, $matches)) {
                 $time['zone'] = 'UTC';
             } else {
                 $time['zone'] = 'LOCAL';
             }
             return $time;
-        } else {
-            return false;
         }
+        throw new InvalidValuesException();
     }
 
     /**
      * Parse a Date field
      */
-    private function _parseDate($text)
+    private function parseDate($date_text): array
     {
-        if (mb_strlen(trim($text)) !== 8) {
-            return false;
+        $matches = [];
+        if (preg_match('/([0-9]{4})([0-9]{2})([0-9]{2})/', $date_text, $matches)) {
+            $date['year'] = $matches[1];
+            $date['month'] = $matches[2];
+            $date['mday'] = $matches[3];
+            return $date;
         }
-
-        $date['year'] = intval(mb_substr($text, 0, 4));
-        $date['month'] = intval(mb_substr($text, 4, 2));
-        $date['mday'] = intval(mb_substr($text, 6, 2));
-
-        return $date;
+        throw new InvalidValuesException();
     }
 
     /**
      * Parse a Duration Value field
      */
-    private function _parseDuration($text)
+    private function parseDuration($interval_text): DateInterval
     {
-        if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $matches)) {
-            // weeks
-            $duration = 7 * 86400 * intval($matches[3]);
-            if (count($matches) > 4) {
-                // days
-                $duration += 86400 * intval($matches[4]);
-            }
-            if (count($matches) > 5) {
-                // hours
-                $duration += 3600 * intval($matches[7]);
-                // mins
-                if (array_key_exists(8, $matches)) {
-                    $duration += 60 * intval($matches[8]);
-                }
-                // secs
-                if (array_key_exists(9, $matches)) {
-                    $duration += intval($matches[9]);
-                }
-            }
-            // sign
-            if ($matches[1] == "-") {
-                $duration *= - 1;
-            }
-
-            return $duration;
-        } else {
-            return false;
-        }
+        return new DateInterval($interval_text);
     }
 
-    private function _parsePriority($value)
+    private function parsePriority($value)
     {
         $value = intval($value);
         if ($value > 0 && $value < 5) {
-            return 1;
+            return 'HIGH';
         }
 
         if ($value == 5) {
-            return 2;
+            return 'MEDIUM';
         }
 
         if ($value > 5 && $value < 10) {
-            return 3;
+            return 'LOW';
         }
 
-        return 0;
+        return '';
     }
 
     /**
-     * Parse a Recurrence field
+     * Parse a recurrence rule.
+     *
+     * @param $text string The text of the recurrence rule.
+     * @return array The translated recurrence rule as array.
+     * @throws InvalidValuesException
      */
-    private function _parseRecurrence($text)
+    private function parseRecurrence($text): array
     {
         global $_calendar_error;
 
@@ -498,13 +542,14 @@ class CalendarParserICalendar extends CalendarParser
                                 $r_rule['rtype'] = trim($match[2]);
                                 break;
                             default:
-                                $_calendar_error->throwSingleError('parse', ErrorHandler::ERROR_WARNING, _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann."));
-                                break;
+                                throw new InvalidValuesException(
+                                    _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann.")
+                                );
                         }
                         break;
 
                     case 'UNTIL' :
-                        $r_rule['expire'] = $this->_parseDateTime($match[2]);
+                        $r_rule['expire'] = $this->parseDateTime($match[2]);
                         break;
 
                     case 'COUNT' :
@@ -520,22 +565,22 @@ class CalendarParserICalendar extends CalendarParser
                     case 'BYHOUR' :
                     case 'BYWEEKNO' :
                     case 'BYYEARDAY' :
-                        $_calendar_error->throwSingleError('parse', ErrorHandler::ERROR_WARNING, _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann."));
-                        break;
-
+                        throw new InvalidValuesException(
+                            _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann.")
+                        );
                     case 'BYDAY' :
-                        $byday = $this->_parseByDay($match[2]);
+                        $byday = $this->parseByDay($match[2]);
                         $r_rule['wdays'] = $byday['wdays'];
                         if ($byday['sinterval'])
                             $r_rule['sinterval'] = $byday['sinterval'];
                         break;
 
                     case 'BYMONTH' :
-                        $r_rule['month'] = $this->_parseByMonth($match[2]);
+                        $r_rule['month'] = $this->parseByMonth($match[2]);
                         break;
 
                     case 'BYMONTHDAY' :
-                        $r_rule['day'] = $this->_parseByMonthDay($match[2]);
+                        $r_rule['day'] = $this->parseByMonthDay($match[2]);
                         break;
 
                     case 'BYSETPOS':
@@ -551,7 +596,7 @@ class CalendarParserICalendar extends CalendarParser
         return $r_rule;
     }
 
-    private function _parseByDay($text)
+    private function parseByDay($text)
     {
         global $_calendar_error;
 
@@ -564,12 +609,15 @@ class CalendarParserICalendar extends CalendarParser
             $wdays .= $wdays_map[$match[2]];
             if ($match[1]) {
                 if (!$sinterval && ((int) $match[1]) > 0 || $match[1] == '-1') {
-                    if ($match[1] == '-1')
+                    if ($match[1] == '-1') {
                         $sinterval = '5';
-                    else
+                    } else {
                         $sinterval = $match[1];
+                    }
                 } else {
-                    $_calendar_error->throwSingleError('parse', ErrorHandler::ERROR_WARNING, _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann."));
+                    throw new InvalidValuesException(
+                        _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann.")
+                    );
                 }
             }
         }
@@ -577,42 +625,40 @@ class CalendarParserICalendar extends CalendarParser
         return $wdays ? ['wdays' => $wdays, 'sinterval' => $sinterval] : false;
     }
 
-    private function _parseByMonthDay($text)
+    private function parseByMonthDay($text)
     {
         $days = explode(',', $text);
-        if (sizeof($days) > 1 || ((int) $days[0]) < 0) {
+        if (count($days) > 1 || ((int) $days[0]) < 0) {
             return false;
         }
 
         return $days[0];
     }
 
-    private function _parseByMonth($text)
+    private function parseByMonth($text)
     {
         $months = explode(',', $text);
-        if (sizeof($months) > 1) {
+        if (count($months) > 1) {
             return false;
         }
 
         return $months[0];
     }
 
-    private function _qp_decode($value)
+    private function qp_decode($value)
     {
         return preg_replace_callback("/=([0-9A-F]{2})/", function ($m) {return chr(hexdec($m[1]));}, $value);
     }
 
-    private function _parseClientIdentifier(&$data)
+    private function parseClientIdentifier(&$data)
     {
         global $_calendar_error;
 
         if ($this->client_identifier == '') {
-            if (!preg_match('/PRODID((;[\W\w]*)*):([\W\w]+?)(\r\n|\r|\n)/', $data, $matches)) {
-                $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!"));
-                return false;
-            } elseif (!trim($matches[3])) {
-                $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!"));
-                return false;
+            if (!preg_match('/PRODID((;[\W\w]*)*):([\W\w]+?)(\r\n|\r|\n)/', $data, $matches)
+                || !trim($matches[3])) {
+                // _("Die Datei ist keine gültige iCalendar-Datei!")
+                throw new InvalidValuesException();
             } else {
                 $this->client_identifier = trim($matches[3]);
             }
@@ -623,7 +669,7 @@ class CalendarParserICalendar extends CalendarParser
     public function getClientIdentifier($data = null)
     {
         if (!is_null($data)) {
-            $this->_parseClientIdentifier($data);
+            $this->parseClientIdentifier($data);
         }
 
         return $this->client_identifier;
diff --git a/lib/classes/calendar/Owner.interface.php b/lib/classes/calendar/Owner.interface.php
new file mode 100644
index 0000000000000000000000000000000000000000..a7c2519ed350fcc697647e347b42bc28763a79e7
--- /dev/null
+++ b/lib/classes/calendar/Owner.interface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Studip\Calendar;
+
+/**
+ * The Studip\Calendar\Owner interface defines methods that classes whose instances own calendars
+ * shall implement to faciliate permission checks for that calendars.
+ */
+interface Owner
+{
+    /**
+     * Retrieves the Owner object for a specified owner-ID.
+     *
+     * @param string $owner_id The ID of the owner.
+     *
+     * @return Owner|null Either the Owner object if it can be found or null in case
+     *     it cannot be found.
+     */
+    public static function getCalendarOwner(string $owner_id) : ?Owner;
+
+    /**
+     * Determines whether the specified user has read permissions to the calendar.
+     *
+     * @param string|null $user_id The ID of the user for which to determine write permissions.
+     *                             Defaults to the current user if no user-ID is provided.
+     *
+     * @return bool True, if the user has read permissions, false otherwise.
+     */
+    public function isCalendarReadable(?string $user_id = null) : bool;
+
+    /**
+     * Determines whether the specified user has write permissions to the calendar.
+     *
+     * @param string|null $user_id The ID of the user for which to determine write permissions.
+     *                             Defaults to the current user if no user-ID is provided.
+     *
+     * @return bool True, if the user has write permissions, false otherwise.
+     */
+    public function isCalendarWritable(?string $user_id = null) : bool;
+}
diff --git a/lib/classes/calendar/SingleCalendar.php b/lib/classes/calendar/SingleCalendar.php
deleted file mode 100644
index 938db1a134a238d047ecbef3573995e7f14257dc..0000000000000000000000000000000000000000
--- a/lib/classes/calendar/SingleCalendar.php
+++ /dev/null
@@ -1,1488 +0,0 @@
-<?php
-/**
- * SingleCalendar.php - Model class for a calendar
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @since       3.2
- */
-
-class SingleCalendar
-{
-    /**
-     * This collection holds all Events in this calendar.
-     *
-     * @var SimpleORMapCollection Collection of Objects which inherits Event.
-     */
-    public $events;
-
-    /**
-     * The owner of this calendar.
-     *
-     * @var Object A Stud.IP object of type User, Institute or Course.
-     */
-    public $range_object;
-
-    public $type;
-
-    public $range;
-
-    /**
-     * The start of this calendar.
-     *
-     * @var int Unix timestamp.
-     */
-    public $start;
-
-    /**
-     * The end of this calendar.
-     *
-     * @var int Unix timestamp.
-     */
-    public $end;
-
-    public $ts;
-
-    public function __construct($range_id, $start = null, $end = null)
-    {
-        $this->setRangeObject($range_id);
-        $this->start = $start ?: 0;
-        $this->end = $end ?: Calendar::CALENDAR_END;
-        $this->events = new SimpleORMapCollection();
-        $this->events->setClassName('Event');
-        $this->type = get_class($this->range_object);
-    }
-
-    /**
-     * Sets the range object and checks whether the calendar is available.
-     *
-     * @param string $range_id The id of a course, institute or user.
-     * @throws AccessDeniedException
-     */
-    private function setRangeObject($range_id)
-    {
-        $this->range_object = get_object_by_range_id($range_id);
-        if (!is_object($this->range_object)) {
-            throw new AccessDeniedException();
-        }
-        $range_map = [
-            'User' => Calendar::RANGE_USER,
-            'Course' => Calendar::RANGE_SEM,
-            'Institute' => Calendar::RANGE_INST
-        ];
-        $this->range = $range_map[get_class($this->range_object)];
-        if ($this->range == Calendar::RANGE_INST
-                || $this->range == Calendar::RANGE_SEM) {
-
-            if (!$this->range_object->isToolActive('CoreCalendar')) {
-                throw new AccessDeniedException();
-            }
-        }
-    }
-
-    /**
-     * Returns all events of given class names between start and end.
-     * Returns events of all types if no class names are given.
-     *
-     * @param array|null $class_names The names of classes that implements Event.
-     * @param int $start The start date time.
-     * @param int $end The end date time.
-     * @return \SingleCalendar This calendar object.
-     * @throws InvalidArgumentException
-     */
-    public function getEvents($class_names = null, $start = null, $end = null)
-    {
-        $start = !is_null($start) ? $start : $this->start;
-        $end = !is_null($end) ? $end : $this->end;
-        if (!is_array($class_names)) {
-            $class_names = ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent', 'CourseMarkedEvent'];
-        }
-        $events = $this->events->getArrayCopy();
-        foreach ($class_names as $type) {
-            if (in_array('Event', class_implements($type))) {
-                $events = array_merge($events, $type::getEventsByInterval(
-                        $this->range_object->getId(), new DateTime('@' . $start),
-                        new DateTime('@' . $end))->getArrayCopy());
-            } else {
-                throw new InvalidArgumentException(sprintf('Class %s does not implements Event.', $type));
-            }
-        }
-        $this->events = SimpleORMapCollection::createFromArray($events, false);
-        $this->events->setClassName('Event');
-        return $this;
-    }
-
-    /**
-     * Stores the event in the calendars of all attendees.
-     *
-     * @param CalendarEvent $event The event to store.
-     * @param array $attendee_ids The user ids of the attendees.
-     * @return bool|int The number of stored events or false if an error occured.
-     */
-    public function storeEvent(CalendarEvent $event, $attendee_ids = [])
-    {
-        $attendee_ids = array_filter($attendee_ids, function($id) {
-            return trim($id) != '';
-        });
-        if (!$attendee_ids) {
-            $attendee_ids = [$GLOBALS['user']->id];
-        }
-        if (count($attendee_ids) === 1) {
-            if (!$this->havePermission(Calendar::PERMISSION_WRITABLE)) {
-                return false;
-            }
-            $is_new = $event->isNew();
-            $stored = $event->store();
-            if ($stored !== false && $this->getRange() == Calendar::RANGE_USER
-                    && $this->getRangeId() != $GLOBALS['user']->id) {
-                $this->sendStoreMessage($event, $is_new);
-            }
-            return $stored;
-        }
-
-        if (in_array($this->getRangeId(), $attendee_ids)) {
-            // set default status if the organizer is an attendee...
-            $event->group_status = CalendarEvent::PARTSTAT_TENTATIVE;
-        }
-        if ($event->isNew()) {
-            return $this->storeAttendeeEvents($event, $attendee_ids);
-        }
-
-        if (!$event->havePermission(Event::PERMISSION_WRITABLE)) {
-            return false;
-        }
-
-        return $this->storeAttendeeEvents($event, $attendee_ids);
-    }
-
-    /**
-     * Helper function for SingleCalendar::storeEvent().
-     *
-     * @param CalendarEvent $event The event to store.
-     * @param type $attendee_ids The user ids of the attendees.
-     * @return bool|int The number of stored events or false if an error occured.
-     */
-    private function storeAttendeeEvents(CalendarEvent $event, $attendee_ids)
-    {
-        $ret = 0;
-        $new_attendees = [];
-        $recipient_ids = [];
-        $is_new = false;
-        foreach ($attendee_ids as $attendee_id) {
-            if (trim($attendee_id)) {
-                $attendee_calendar = new SingleCalendar($attendee_id);
-
-                // SEMBBS
-                // Gruppentermine können ab Calendar::PERMISSION_READABLE angelegt werden
-                // if ($attendee_calendar->havePermission(Calendar::PERMISSION_READABLE)) {
-
-                if ($attendee_calendar->havePermission(Calendar::PERMISSION_WRITABLE)
-                        || Config::get()->CALENDAR_GRANT_ALL_INSERT) {
-                    $attendee_event = new CalendarEvent(
-                            [$attendee_calendar->getRangeId(), $event->event_id]);
-                    $attendee_event->event = $event->event;
-                    $is_new = $attendee_event->isNew();
-                    if ($is_new) {
-                        $attendee_event->group_status = $event->group_status;
-                    }
-                    $stored = $attendee_event->store();
-                    if ($stored !== false) {
-                        // send message if not own calendar
-                        if (!$attendee_calendar->havePermission(Calendar::PERMISSION_OWN)) {
-                            $recipient_ids[] = $attendee_event->range_id;
-                        }
-                        $new_attendees[] = $attendee_event->range_id;
-                        $ret += $stored;
-                    }
-                }
-            }
-        }
-        if (count($recipient_ids)) {
-            $this->sendStoreMessage($attendee_event, $is_new, $recipient_ids);
-        }
-
-        $events_delete = CalendarEvent::findBySQL('event_id = ? AND range_id NOT IN(?)',
-                [$event->event_id, $new_attendees]);
-        foreach ($events_delete as $event_delete) {
-            $calendar = new SingleCalendar($event_delete->range_id);
-            $calendar->deleteEvent($event_delete);
-        }
-        return $ret;
-    }
-
-    /**
-     * Sets the start date time by given unix timestamp.
-     *
-     * @param int $start Unix timestamp.
-     */
-    public function setStart($start)
-    {
-        $this->start = $start;
-        return $this;
-    }
-
-    /**
-     * Returns the start date time of this calendar as a unix timestamp.
-     *
-     * @return int Unix timestamp.
-     */
-    public function getStart()
-    {
-        return $this->start;
-    }
-
-    /**
-     * Sets the end date time by given unix timestamp.
-     *
-     * @param int $end Unix timestamp.
-     */
-    public function setEnd($end)
-    {
-        $this->end = $end;
-        return $this;
-    }
-
-    /**
-     * Returns the end date time of this calendar as a unix timestamp.
-     *
-     * @return int Unix timestamp.
-     */
-    public function getEnd()
-    {
-        return $this->end;
-    }
-
-    /**
-     * Returns a event by given $event_id. Returns a new event of type
-     * CalendarEvent with default data if the id is null or unknown.
-     * If $class_names is set, only these types of Object will be returned.
-     *
-     * @param string $event_id
-     * @param array $class_names Names of classes which inherits Event.
-     * @return Event|null The found event, a new CalendarEvent or null if no
-     * event other than a CalendarEvent was found.
-     */
-    public function getEvent($event_id = null, $class_names = null)
-    {
-        if (!is_array($class_names)) {
-            $class_names = ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent', 'CourseMarkedEvent'];
-        }
-        foreach ($class_names as $type) {
-            if ($type == 'CalendarEvent') {
-                $event = CalendarEvent::find([$this->getRangeId(), $event_id]);
-            } else {
-                $event = $type::find($event_id);
-            }
-            if ($event && $event->havePermission(Event::PERMISSION_READABLE)) {
-                return $event;
-            }
-        }
-        return $this->getNewEvent();
-    }
-
-    /**
-     * Creates a new event, sets some default data and returns it.
-     *
-     * @return \CalendarEvent The new event.
-     */
-    public function getNewEvent()
-    {
-        $event_data = new EventData();
-        $event_data->setId($event_data->getNewId());
-        $now = time();
-        $event_data->start = $now;
-        $event_data->end = $now + 3600;
-        $calendar_event = new CalendarEvent();
-        $calendar_event->setId([$this->getRangeId(), $event_data->getId()]);
-        $calendar_event->event = $event_data;
-        return $calendar_event;
-    }
-
-    /**
-     * Sorts all events by start time.
-     */
-    public function sortEvents()
-    {
-        $this->events->orderBy('start');
-    }
-
-    /**
-     * An alias for SingleCalendar::getRangeId().
-     *
-     * @see SingleCalendar::getRangeId()
-     */
-    public function getId()
-    {
-        return $this->getRangeId();
-    }
-
-    /**
-     * Returns the range id of this calendar.
-     * Possible range id are for objects of type user, inst, fak, group.
-     *
-     * @return string The range id.
-     */
-    public function getRangeId()
-    {
-        return $this->range_object->getId();
-    }
-
-    /**
-     * Returns the object range of this calendar.
-     *
-     * @return int The object range.
-     */
-    public function getRange()
-    {
-        return $this->range;
-    }
-
-    /**
-     * Returns the range object (user, course, institute) of this calendar.
-     *
-     * @return int The object range.
-     */
-    public function getRangeObject()
-    {
-        return $this->range_object;
-    }
-
-    /**
-     * Returns the permission of the given user for this calendar.
-     *
-     * @param string $user_id User id.
-     * @return int The calendar permission.
-     */
-    public function getPermissionByUser($user_id = null)
-    {
-        static $user_permission = [];
-
-        $user_id = $user_id ?: $GLOBALS['user']->id;
-        $id = $user_id . $this->getRangeId();
-        if (!empty($user_permission[$id])) {
-            return $user_permission[$id];
-        }
-        // own calendar
-        if ($this->range == Calendar::RANGE_USER
-                && $this->getRangeId() == $user_id) {
-            $user_permission[$id] = Calendar::PERMISSION_OWN;
-            return $user_permission[$id];
-        }
-        switch ($this->type) {
-            case 'User' :
-                // alle Lehrenden haben gegenseitig schreibenden Zugriff, ab dozent immer schreibenden Zugriff
-                /*
-                if ($GLOBALS['perm']->have_perm('dozent') && $GLOBALS['perm']->get_perm($this->range_object->getId()) == 'dozent') {
-                    return Calendar::PERMISSION_WRITABLE;
-                }
-                 *
-                 */
-                $cal_user = CalendarUser::find([$this->getRangeId(), $user_id]);
-                if ($cal_user) {
-                    switch ($cal_user->permission) {
-                        case 1 :
-                            $user_permission[$id] = Calendar::PERMISSION_FORBIDDEN;
-                            break;
-                        case 2 :
-                            $user_permission[$id] = Calendar::PERMISSION_READABLE;
-                            break;
-                        case 4 :
-                            $user_permission[$id] = Calendar::PERMISSION_WRITABLE;
-                            break;
-                        default :
-                            $user_permission[$id] = Calendar::PERMISSION_FORBIDDEN;
-                    }
-                } else {
-                    $user_permission[$id] = Calendar::PERMISSION_FORBIDDEN;
-                }
-                break;
-                /*
-            case 'group' :
-                $stmt = DBManager::get()->prepare('SELECT range_id FROM statusgruppen WHERE statusgruppe_id = ?');
-                $stmt->execute(array($range_id));
-                $result = $stmt->fetch(PDO::FETCH_ASSOC);
-                if ($result) {
-                    if ($result['range_id'] == $user_id) {
-                        return Calendar::PERMISSION_OWN;
-                    }
-                }
-                return Calendar::PERMISSION_FORBIDDEN;
-                 *
-                 */
-            case 'Course' :
-                switch ($GLOBALS['perm']->get_studip_perm($this->range_object->getId(), $user_id)) {
-                    case 'user' :
-                    case 'autor' :
-                        $user_permission[$id] = Calendar::PERMISSION_READABLE;
-                        break;
-                    case 'tutor' :
-                    case 'dozent' :
-                    case 'admin' :
-                    case 'root' :
-                        $user_permission[$id] = Calendar::PERMISSION_WRITABLE;
-                        break;
-                    default :
-                        $user_permission[$id] = Calendar::PERMISSION_FORBIDDEN;
-                }
-                break;
-            case 'Institute' :
-                switch ($GLOBALS['perm']->get_studip_perm($this->range_object->getId(), $user_id)) {
-                    case 'user' :
-                        $user_permission[$id] = Calendar::PERMISSION_READABLE;
-                        break;
-                    case 'autor' :
-                        $user_permission[$id] = Calendar::PERMISSION_READABLE;
-                        break;
-                    case 'tutor' :
-                    case 'dozent' :
-                    case 'admin' :
-                    case 'root' :
-                        $user_permission[$id] = Calendar::PERMISSION_WRITABLE;
-                        break;
-                    default :
-                        // readable for all
-                        $user_permission[$id] = Calendar::PERMISSION_READABLE;
-                }
-                break;
-            default :
-                $user_permission[$id] = Calendar::PERMISSION_FORBIDDEN;
-        }
-        return $user_permission[$id];
-    }
-
-    /**
-     * Returns whether the given user has at least the $permission to this calendar.
-     * It checks for the actual user if $user_id is null.
-     *
-     * @param int $permission An accepted calendar permission.
-     * @param string|null $user_id The id of the user.
-     * @return bool True if the user has at least the given permission.
-     */
-    public function havePermission($permission, $user_id = null)
-    {
-        $user_id = $user_id ?: $GLOBALS['user']->id;
-        return ($permission <= $this->getPermissionByUser($user_id));
-    }
-
-    /**
-     * Returns whether the given user has the $permission to this calendar.
-     * It checks for the actual user if $user_id is null.
-     *
-     * @param int $permission An accepted calendar permission.
-     * @param string|null $user_id The id of the user.
-     * @return bool True if the user has the given permission.
-     */
-    public function checkPermission($permission, $user_id = null)
-    {
-        $user_id = $user_id ?: $GLOBALS['user']->id;
-        return $permission == $this->getPermissionByUser($user_id);
-    }
-
-    /**
-     * Sends a message to the owner of the calendar that a new event was inserted
-     * or an old event was modified by another user.
-     *
-     * @param CalendarEvent $event The new or updated event.
-     * @param bool $is_new True if the event is new.
-     * @param null|array Array with user_ids of the recipients. If not set the
-     * owner of the given event is used as recipient.
-     */
-    protected function sendStoreMessage($event, $is_new, $recipient_ids = null)
-    {
-        $message = new messaging();
-        $event_data = '';
-
-        if ($is_new) {
-            $msg_text = sprintf(_("%s hat einen neuen Termin in Ihren Kalender eingetragen."), get_fullname());
-            $subject = strftime(_('Neuer Termin am %c'), $event->getStart());
-            $msg_text .= "\n\n**";
-        } else {
-            $msg_text = sprintf(_("%s hat einen Termin in Ihrem Kalender geändert."), get_fullname());
-            $subject = strftime(_('Termin am %c geändert'), $event->getStart());
-            $msg_text .= "\n\n**";
-        }
-        $msg_text .= _('Zeit') . ':' . '** ' . strftime(' %c - ', $event->getStart())
-                . strftime('%c', $event->getEnd()) . "\n**";
-        $msg_text .= _("Zusammenfassung") . ':** ' . $event->getTitle() . "\n";
-        if ($event_data = $event->getDescription()) {
-            $msg_text .= '**' . _('Beschreibung') . ":** $event_data\n";
-        }
-        if ($event_data = $event->toStringCategories()) {
-            $msg_text .= '**' . _('Kategorie') . ":** $event_data\n";
-        }
-        if ($event_data = $event->toStringPriority()) {
-            $msg_text .= '**' . _('Priorität') . ":** $event_data\n";
-        }
-        if ($event_data = $event->toStringAccessibility()) {
-            $msg_text .= '**' . _('Zugriff') . ":** $event_data\n";
-        }
-        if ($event_data = $event->toStringRecurrence()) {
-            $msg_text .= '**' . _('Wiederholung') . ":** $event_data\n";
-        }
-        if (Config::get()->CALENDAR_GROUP_ENABLE && $event->attendees->count()) {
-            $msg_text .= '**' . _("Teilnehmende") . ':** ';
-            $msg_text .= implode(', ', $event->attendees->map(
-                function ($att) use ($event) {
-                    $att_name = $att->user->getFullname();
-                    if ($event->havePermission(Event::PERMISSION_OWN, $att->user->getId())) {
-                        $att_name .= ' (' . _('Organisator') . ')';
-                    } else {
-                        if ($event->toStringGroupStatus()) {
-                            $att_name .= ' (' . $att->toStringGroupStatus() . ')';
-                        }
-                    }
-                    return $att_name;
-                }));
-            $msg_text .= "\n";
-        }
-        $msg_text .= "\n\n" . _('Hier kommen Sie direkt zum Termin in Ihrem Kalender:') . "\n";
-                $msg_text .= URLHelper::getURL('dispatch.php/calendar/single/edit/'
-                    . implode('/', $event->getId()), false);
-
-        $recipient_unames = is_array($recipient_ids)
-                ? array_map('get_username', $recipient_ids)
-                : [get_username($event->range_id)];
-
-        $message->insert_message($msg_text, $recipient_unames,
-                '____%system%____', '', '', '', '', $subject);
-    }
-
-
-    /**
-     * Deletes a calendar event with regard of the consultation component.
-     * Depending whether a CalenderEvent instance is related to a consultation booking
-     * or not, the deletion has to be done differently.
-     *
-     * @param CalendarEvent $event
-     * @return bool True on success, false on failure.
-     */
-    protected function deleteEventWithConsultation(CalendarEvent $event) : bool
-    {
-        // If the event belongs to a consultation booking, cancel the booking,
-        // so that it is available for others.
-        $booking = ConsultationBooking::findOneByStudent_event_id($event->event_id);
-        if ($booking) {
-            // Delete the event indirectly by cancelling the consultation booking:
-            $booking->cancel();
-            return true;
-        }
-
-        //Delete the consultation event from the consultation block.
-        $consultation_event = ConsultationEvent::findOneByEvent_id($event->event_id);
-        // Check if the slot is empty and delete it, if so.
-        if ($consultation_event && $consultation_event->slot) {
-            $consultation_event->slot->bookings->cancel();
-            $consultation_event->slot->delete();
-            return true;
-        }
-
-        // Delete the event
-        return $event->delete();
-    }
-
-    /**
-     * Deletes an event from this calendar.
-     *
-     * @param string|object $calendar_event The id of an event or an event object of type CalendarEvent.
-     * @param boolean $all If true all events of a group event will be deleted.
-     * @return boolean|int The number of deleted events. False if the event was not deleted.
-     */
-    public function deleteEvent($calendar_event, $all = false)
-    {
-        if (!is_object($calendar_event)) {
-            $calendar_event = CalendarEvent::find(
-                    [$this->getRangeId(), $calendar_event]);
-        }
-        if (!$calendar_event
-            || !is_a($calendar_event, 'CalendarEvent')
-            || !$calendar_event->havePermission(Event::PERMISSION_DELETABLE)) {
-            return false;
-        }
-        if ($this->havePermission(Calendar::PERMISSION_WRITABLE)
-                || $calendar_event->havePermission(Event::PERMISSION_OWN)) {
-
-            if (!($calendar_event
-                    && $calendar_event->havePermission(Event::PERMISSION_WRITABLE))) {
-                return false;
-            }
-
-            if (!is_a($calendar_event, 'CalendarEvent')) {
-                return false;
-            }
-
-            if ($this->getRange() == Calendar::RANGE_USER) {
-                $event_message = clone $calendar_event;
-                $author_id = $calendar_event->getAuthorId();
-                $deleted = $this->deleteEventWithConsultation($calendar_event);
-                if ($deleted && !$this->havePermission(Calendar::PERMISSION_OWN)) {
-                    $this->sendDeleteMessage($event_message);
-                }
-                if ($all && $deleted && $author_id == $this->getRangeId()) {
-                    CalendarEvent::findEachBySQL(function ($ce) use ($deleted) {
-                        $calendar = new SingleCalendar($ce->range_id);
-                        $deleted += $calendar->deleteEvent($ce);
-                    }, 'event_id = ?', [$event_message->event_id]);
-                }
-                return $deleted;
-            } else if ($this->getRange() == Calendar::RANGE_SEM) {
-                $deleted = $this->deleteEventWithConsultation($calendar_event);
-                return $deleted;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Sends a message to the owner of the calendar that this event was deleted
-     * by another user.
-     *
-     * @param CalendarEvent $event The deleted event.
-     * @param null|array Array with user_ids of the recipients. If not set the
-     * owner of the given event is used as recipient.
-     */
-    protected function sendDeleteMessage($event, $recipient_ids = null)
-    {
-        $message = new messaging();
-        $event_data = '';
-
-        $subject = strftime(_('Termin am %c gelöscht'), $event->getStart());
-        $msg_text = sprintf(_("%s hat folgenden Termin in Ihrem Kalender gelöscht:"), get_fullname());
-        $msg_text .= "\n\n";
-
-        $msg_text .= '**' . _('Zeit') . ':**' . strftime(' %c - ', $event->getStart())
-                . strftime('%c', $event->getEnd()) . "\n";
-        $msg_text .= '**' . _('Zusammenfassung') . ':** ' . $event->getTitle() . "\n";
-        if ($event_data = $event->getDescription()) {
-            $msg_text .= '**' . _('Beschreibung') . ":** $event_data\n";
-        }
-        if ($event_data = $event->toStringCategories()) {
-            $msg_text .= '**' . _('Kategorie') . ":** $event_data\n";
-        }
-        if ($event_data = $event->toStringPriority()) {
-            $msg_text .= '**' . _('Priorität') . ":** $event_data\n";
-        }
-        if ($event_data = $event->toStringAccessibility()) {
-            $msg_text .= '**' . _('Zugriff') . ":** $event_data\n";
-        }
-        if ($event_data = $event->toStringRecurrence()) {
-            $msg_text .= '**' . _('Wiederholung') . ":** $event_data\n";
-        }
-        $recipient_unames = is_array($recipient_ids)
-                ? array_map('get_username', $recipient_ids)
-                : [get_username($event->range_id)];
-        $message->insert_message($msg_text, $recipient_unames,
-                '____%system%____', '', '', '', '', $subject);
-    }
-
-    /**
-     * Returns an array of all events (with calculated recurrences)
-     * in the given time range.
-     *
-     * @param string $owner_id The user id of calendar owner.
-     * @param int $time A unix timestamp of this day.
-     * @param string $user_id The id of the user who gets access to the calendar (optional, default current user)
-     * @param array $restrictions An array with key value pairs of properties to filter the result (optional).
-     * @param array $class_names Array of class names. The class must implement Event (optional).
-     * @return array All events in the given time range (with calculated recurrences)
-     */
-    public static function getEventList($owner_id, $start, $end, $user_id = null,
-            $restrictions = null, $class_names = null)
-    {
-        $user_id = $user_id ?: $GLOBALS['user']->id;
-        $end_time = mktime(12, 0, 0, date('n', $end), date('j', $end), date('Y', $end));
-        $start_day = date('j', $start);
-        $events = [];
-        do {
-            $time = mktime(12, 0, 0, date('n', $start), $start_day, date('Y', $start));
-            $start_day++;
-            $day = self::getDayCalendar($owner_id, $time, $user_id, $restrictions, $class_names);
-            foreach ($day->events as $event) {
-                $event_key = implode('', (array) $event->getId()) . $event->getStart();
-                $events["$event_key"] = $event;
-            }
-        } while ($time <= $end_time);
-        return $events;
-    }
-
-    /**
-     * Returns a SingleCalendar object with all events of the given owner or
-     * SingleCalendar object for one day set by timestamp.
-     *
-     * @param string|SingleCalendar $owner The user id of calendar owner or a calendar object.
-     * @param int $time A unix timestamp of this day.
-     * @param string $user_id The id of the user who gets access to the calendar (optional, default current user)
-     * @param array $restrictions An array with key value pairs of properties to filter the result (optional).
-     * @param array $class_names Array of class names. The class must implement Event (optional).
-     * @return \SingleCalendar Calendar Object with all events of given day.
-     */
-    public static function getDayCalendar($owner, $time, $user_id = null,
-            $restrictions = null, $class_names = null)
-    {
-        $user_id = $user_id ?: $GLOBALS['user']->id;
-        if (!is_array($class_names)) {
-            $class_names = ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent', 'CourseMarkedEvent'];
-        }
-
-        $day = date('Y-m-d-', $time);
-        $start = DateTime::createFromFormat('Y-m-d-H:i:s', $day . '00:00:00');
-        $end = DateTime::createFromFormat('Y-m-d-H:i:s', $day . '23:59:59');
-        if (is_object($owner)) {
-            if ($owner instanceof SingleCalendar) {
-                $calendar = $owner;
-                $calendar->setStart($start->format('U'))->setEnd($end->format('U'));
-            } else {
-                throw new InvalidArgumentException('The owner must be a user id or an object of type SingleCalendar.');
-            }
-        } else {
-            $calendar = new SingleCalendar($owner,
-                    $start->format('U'), $end->format('U'));
-        }
-        $calendar->getEvents($class_names)->sortEvents();
-
-        $dow = date('w', $calendar->getStart());
-        $month = date('n', $calendar->getStart());
-        $year = date('Y', $calendar->getStart());
-        $events_created = [];
-
-        foreach ($calendar->events as $event) {
-            if (!$calendar->havePermission(Calendar::PERMISSION_READABLE, $user_id)
-                   && $event->getAccessibility() != 'PUBLIC') {
-                continue;
-            }
-            if (!$event->havePermission(Event::PERMISSION_CONFIDENTIAL, $user_id)) {
-                continue;
-            }
-            if (!SingleCalendar::checkRestriction($event, $restrictions)) {
-                continue;
-            }
-            $properties = $event->getProperties();
-            $ts = mktime(12, 0, 0, date('n', $calendar->start), date('j', $calendar->start), date('Y', $calendar->start));
-            $rep = $properties['RRULE'];
-            $duration = (int) ((mktime(12, 0, 0, date('n', $properties['DTEND']), date('j', $properties['DTEND']), date('Y', $properties['DTEND']))
-                    - mktime(12, 0, 0, date('n', $properties['DTSTART']), date('j', $properties['DTSTART']), date('Y', $properties['DTSTART'])))
-                    / 86400);
-            // single events or first event
-            if ($properties['DTSTART'] >= $calendar->getStart()
-                    && $properties['DTEND'] <= $calendar->getEnd()) {
-                self::createDayViewEvent($event, $properties['DTSTART'], $properties['DTEND'],
-                        $calendar->getStart(), $calendar->getEnd(), $events_created);
-            } elseif ($properties['DTSTART'] >= $calendar->getStart()
-                    && $properties['DTSTART'] <= $calendar->getEnd()) {
-                self::createDayViewEvent($event, $properties['DTSTART'], $properties['DTEND'],
-                        $calendar->getStart(), $calendar->getEnd(), $events_created);
-            } elseif ($properties['DTSTART'] < $calendar->getStart()
-                    && $properties['DTEND'] > $calendar->getEnd()) {
-                self::createDayViewEvent($event, $properties['DTSTART'], $properties['DTEND'],
-                        $calendar->getStart(), $calendar->getEnd(), $events_created);
-            } elseif ($properties['DTEND'] > $calendar->getStart()
-                    && $properties['DTEND'] <= $calendar->getEnd()) {
-                self::createDayViewEvent($event, $properties['DTSTART'], $properties['DTEND'],
-                        $calendar->getStart(), $calendar->getEnd(), $events_created);
-            }
-            switch ($rep['rtype']) {
-                case 'DAILY':
-                    /*
-                    if ($calendar->getEnd() > $rep['expire'] + $duration * 86400) {
-                        continue;
-                    }
-                     *
-                     */
-                    if ($end > $rep['expire'] + $duration * 86400) {
-                        continue 2;
-                    }
-                    $ts = $ts + (date('I', $rep['ts']) * 3600);
-                    $pos = (($ts - $rep['ts']) / 86400) % $rep['linterval'];
-                    $start = $ts - $pos * 86400;
-                    $end = $start + $duration * 86400;
-                    self::createDayViewEvent($event, $start, $end, $calendar->getStart(),
-                            $calendar->getEnd(), $events_created);
-                    break;
-                case 'WEEKLY':
-                    $rep['ts'] = $rep['ts'] + ((date('I', $rep['ts']) - date('I', $ts)) * 3600);
-                    for ($i = 0; $i < mb_strlen($rep['wdays']); $i++) {
-                        $pos = ((($ts - $dow * 86400) - $rep['ts']) / 86400
-                                - ($rep['wdays'][$i] - 1) + $dow)
-                                % ($rep['linterval'] * 7);
-                        $start = $ts - $pos * 86400;
-                        $end = $start + $duration * 86400;
-                        if ($start >= $properties['DTSTART'] && $start <= $ts && $end >= $ts) {
-                            self::createDayViewEvent($event, $start, $end,
-                                    $calendar->getStart(), $calendar->getEnd(), $events_created);
-                        }
-                    }
-                    break;
-                case 'MONTHLY':
-                    if ($rep['day']) {
-                        $lwst = mktime(12, 0, 0, $month
-                                - ((($year - date('Y', $rep['ts'])) * 12
-                                + ($month - date('n', $rep['ts']))) % $rep['linterval']),
-                                $rep['day'], $year);
-                        $hgst = $lwst + $duration * 86400;
-                        self::createDayViewEvent($event, $lwst, $hgst, $calendar->getStart(),
-                                $calendar->getEnd(), $events_created);
-                        break;
-                    }
-                    if ($rep['sinterval']) {
-                        $mon = $month - $rep['linterval'];
-                        do {
-                            $lwst = mktime(12, 0, 0, $mon
-                                    - ((($year - date('Y', $rep['ts'])) * 12
-                                    + ($mon - date('n', $rep['ts']))) % $rep['linterval']),
-                                    1, $year) + ($rep['sinterval'] - 1) * 604800;
-                            $aday = strftime('%u', $lwst);
-                            $lwst -= ( $aday - $rep['wdays']) * 86400;
-                            if ($rep['sinterval'] == 5) {
-                                if (date('j', $lwst) < 10) {
-                                    $lwst -= 604800;
-                                }
-                                if (date('n', $lwst) == date('n', $lwst + 604800)) {
-                                    $lwst += 604800;
-                                }
-                            } else {
-                                if ($aday > $rep['wdays']) {
-                                    $lwst += 604800;
-                                }
-                            }
-                            $hgst = $lwst + $duration * 86400;
-                            if ($ts >= $lwst && $ts <= $hgst) {
-                                self::createDayViewEvent($event, $lwst, $hgst,
-                                        $calendar->getStart(), $calendar->getEnd(), $events_created);
-                            }
-                            $mon += $rep['linterval'];
-                        } while ($lwst < $ts);
-                    }
-                    break;
-                case 'YEARLY':
-                    if ($ts < $rep['ts']) {
-                        break;
-                    }
-                    if ($rep['day']) {
-                        if (date('Y', $properties['DTEND']) - date('Y', $properties['DTSTART'])) {
-                            $lwst = mktime(12, 0, 0, $rep['month'], $rep['day'],
-                                    $year - (($year - date('Y', $rep['ts'])) % $rep['linterval'])
-                                    - $rep['linterval']);
-                            $hgst = $lwst + 86400 * $duration;
-                            if ($ts >= $lwst && $ts <= $hgst) {
-                                self::createDayViewEvent($event, $lwst, $hgst,
-                                        $calendar->getStart(), $calendar->getEnd(), $events_created);
-                                break;
-                            }
-                        }
-                        $lwst = mktime(12, 0, 0, $rep['month'], $rep['day'],
-                                $year - (($year - date('Y', $rep['ts'])) % $rep['linterval']));
-                        $hgst = $lwst + 86400 * $duration;
-                        self::createDayViewEvent($event, $lwst, $hgst, $calendar->getStart(),
-                                $calendar->getEnd(), $events_created);
-                        break;
-                    }
-                    $ayear = $year - 1;
-                    do {
-                        if ($rep['sinterval']) {
-                            $lwst = mktime(12, 0, 0, $rep['month'],
-                                    1 + ($rep['sinterval'] - 1) * 7, $ayear);
-                            $aday = strftime('%u', $lwst);
-                            $lwst -= ( $aday - $rep['wdays']) * 86400;
-                            if ($rep['sinterval'] == 5) {
-                                if (date('j', $lwst) < 10) {
-                                    $lwst -= 604800;
-                                }
-                                if (date('n', $lwst) == date('n', $lwst + 604800)) {
-                                    $lwst += 604800;
-                                }
-                            } elseif ($aday > $rep['wdays']) {
-                                $lwst += 604800;
-                            }
-                            $ayear++;
-                            $hgst = $lwst + $duration * 86400;
-                            if ($ts >= $lwst && $ts <= $hgst) {
-                                self::createDayViewEvent($event, $lwst, $hgst,
-                                        $calendar->getStart(), $calendar->getEnd(), $events_created);
-                            }
-                        }
-                    } while ($lwst < $ts);
-            }
-        }
-        $calendar->events->exchangeArray(array_values($events_created));
-        return $calendar;
-    }
-
-    /**
-     * Creates events for the day view.
-     *
-     * @param Event $event
-     * @param int $lwst
-     * @param int $hgst
-     * @param int $cl_start
-     * @param int $cl_end
-     * @param array $events_created
-     * @return boolean
-     */
-    private static function createDayViewEvent($event, $lwst, $hgst,
-            $cl_start, $cl_end, Array &$events_created)
-    {
-        $lwst = mktime(12, 0, 0, date('n', $lwst), date('j', $lwst), date('Y', $lwst));
-        $hgst = mktime(12, 0, 0, date('n', $hgst), date('j', $hgst), date('Y', $hgst));
-
-        // if this date is in the exceptions?
-        if ($event->getProperty('EXDATE')) {
-            $exdates = explode(',', $event->getProperty('EXDATE'));
-            foreach ($exdates as $exdate) {
-                if ($exdate > 0 && $exdate >= $lwst && $exdate <= $hgst) {
-                    return false;
-                }
-            }
-        }
-        // is event expired?
-        $rrule = $event->getRecurrence();
-        if ($rrule['rtype'] != 'SINGLE' && $rrule['expire'] > 0 && $rrule['expire'] < $hgst) {
-            return false;
-        }
-        $start = mktime(date('G', $event->getStart()), date('i', $event->getStart()),
-                date('s', $event->getStart()), date('n', $lwst), date('j', $lwst), date('Y', $lwst));
-        $end = mktime(date('G', $event->getEnd()), date('i', $event->getEnd()),
-                date('s', $event->getEnd()), date('n', $hgst), date('j', $hgst), date('Y', $hgst));
-
-        if (($start <= $cl_start && $end >= $cl_end)
-                || ($start >= $cl_start && $start < $cl_end)
-                || ($end > $cl_start && $end <= $cl_end)) {
-
-            $key = implode('', (array) $event->getId()) . $start;
-            if (empty($events_created[$key])) {
-                $new_event = clone $event;
-                $new_event->setStart($start);
-                $new_event->setEnd($end);
-                $events_created[$key] = $new_event;
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Returns an array with all days between start and end of this SingleCalendar.
-     * The keys are the timestamps of the days (12:00) and the values are number
-     * of events for a day.
-     *
-     * @param string $user_id Use the permissions of this user.
-     * @param array $restrictions
-     * @return array An array with year day as key and number of events per day as value.
-     */
-    public function getListCountEvents($class_names = null, $user_id = null, $restrictions = null)
-    {
-        if (!is_array($class_names)) {
-            $class_names = ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent', 'CourseMarkedEvent'];
-        }
-        $end = $this->getEnd();
-        $start = $this->getStart();
-        $year = date('Y', $start);
-        $end_ts = mktime(12, 0, 0, date('n', $end), date('j', $end), date('Y', $end));
-        $start_ts = mktime(12, 0, 0, date('n', $start), date('j', $start), date('Y', $start));
-        $this->getEvents($class_names)->sortEvents();
-        $daylist = [];
-        $this->ts = mktime(12, 0, 0, 1, 1, $year);
-        foreach ($this->events as $event) {
-            if (!$event->havePermission(Event::PERMISSION_CONFIDENTIAL, $user_id)) {
-                continue;
-            }
-            if (!SingleCalendar::checkRestriction($event, $restrictions)) {
-                continue;
-            }
-            $properties = $event->getProperties();
-
-            $rep = $properties['RRULE'];
-            $duration = (int) ((mktime(12, 0, 0, date('n', $properties['DTEND']), date('j', $properties['DTEND']), date('Y', $properties['DTEND']))
-                    - mktime(12, 0, 0, date('n', $properties['DTSTART']), date('j', $properties['DTSTART']), date('Y', $properties['DTSTART'])))
-                    / 86400);
-
-            // single event or first event
-            $lwst = mktime(12, 0, 0, date('n', $properties['DTSTART']), date('j', $properties['DTSTART']), date('Y', $properties['DTSTART']));
-            if ($start_ts > $lwst) {
-                $adate = $start_ts;
-            } else {
-                $adate = $lwst;
-            }
-            $hgst = $lwst + $duration * 86400;
-            while ($adate >= $start_ts && $adate <= $end_ts && $adate <= $hgst) {
-                $md_date = $adate - date('I', $adate) * 3600;
-                $this->countListEvent($properties, $md_date, $properties['DTSTART'], $properties['DTEND'], $daylist);
-                $adate += 86400;
-            }
-
-            switch ($rep['rtype']) {
-                case 'DAILY' :
-                    if ($rep['ts'] < $start) {
-                        // brauche den ersten Tag nach $start an dem dieser Termin wiederholt wird
-                        if ($rep['linterval'] == 1) {
-                            $adate = $this->ts;
-                        } else {
-                            $adate = $this->ts + ($rep['linterval'] - (($this->ts - $rep['ts']) / 86400)
-                                    % $rep['linterval']) * 86400;
-                        }
-                        while ($adate <= $end_ts && $adate >= $this->ts && $adate <= $rep['expire']) {
-                            $hgst = $adate + $duration * 86400;
-                            $md_date = $adate;
-                            while ($md_date <= $end_ts && $md_date >= $this->ts && $md_date <= $hgst) {
-                                $md_date -= 3600 * date('I', $md_date);
-                                $this->countListEvent($properties, $md_date, $adate, $hgst, $daylist);
-                                $md_date += 86400;
-                            }
-                            $adate += $rep['linterval'] * 86400;
-                        }
-                    } else {
-                        $adate = $rep['ts'];
-                    }
-                    while ($adate <= $end_ts && $adate >= $this->ts && $adate <= $rep['expire']) {
-                        $hgst = $adate + $duration * 86400;
-                        $md_date = $adate;
-                        while ($md_date <= $end_ts && $md_date >= $this->ts && $md_date <= $hgst) {
-                            $md_date += 3600 * date('I', $md_date);
-                            $this->countListEvent($properties, $md_date, $adate, $hgst, $daylist);
-                            $md_date += 86400;
-                        }
-                        $adate += $rep['linterval'] * 86400;
-                    }
-                    break;
-
-                case 'WEEKLY' :
-                    if ($properties['DTSTART'] >= $start && $properties['DTSTART'] <= $end) {
-                        $lwst = mktime(12, 0, 0, date('n', $properties['DTSTART']), date('j', $properties['DTSTART']), date('Y', $properties['DTSTART']));
-                        $hgst = $lwst + $duration * 86400;
-                        if ($rep['ts'] != $adate) {
-                            $wdate = $lwst;
-                            while ($wdate <= $end_ts && $wdate >= $start_ts && $wdate <= $hgst) {
-                              //  $md_date = $wdate - date('I', $wdate) * 3600;
-                                $this->countListEvent($properties, $wdate, $lwst, $hgst, $daylist);
-                                $wdate += 86400;
-                            }
-                        }
-                        $aday = strftime('%u', $lwst) - 1;
-                        for ($i = 0; $i < mb_strlen($rep['wdays']); $i++) {
-                            $awday = (int) mb_substr($rep['wdays'], $i, 1) - 1;
-                            if ($awday > $aday) {
-                                $lwst = $lwst + ($awday - $aday) * 86400;
-                                $hgst = $lwst + $duration * 86400;
-                                $wdate = $lwst;
-                                while ($wdate >= $start_ts && $wdate <= $end_ts && $wdate <= $hgst) {
-                                  //  $md_date = $wdate - date('I', $wdate) * 3600;
-                                    $this->countListEvent($properties, $wdate, $lwst, $hgst, $daylist);
-                                    $wdate += 86400;
-                                }
-                            }
-                        }
-                    }
-                    if ($rep['ts'] < $start) {
-                        $adate = $start_ts - (strftime('%u', $start_ts) - 1) * 86400;
-                        $adate += ( $rep['linterval'] - (($adate - $rep['ts']) / 604800)
-                                % $rep['linterval']) * 604800;
-                        $adate -= $rep['linterval'] * 604800;
-                    } else {
-                        $adate = $rep['ts'] + 604800 * $rep['linterval'];
-                    }
-
-                    while ($adate >= $properties['DTSTART'] && $adate <= $rep['expire'] && $adate <= $end) {
-                        // event is repeated on different week days
-                        for ($i = 0; $i < mb_strlen($rep['wdays']); $i++) {
-                            $awday = (int) $rep['wdays'][$i];
-                            $lwst = $adate + ($awday - 1) * 86400;
-                            $hgst = $lwst + $duration * 86400;
-                            if ($lwst < $start_ts) {
-                                $lwst = $start_ts;
-                            }
-                            $wdate = $lwst;
-                            while ($wdate >= $start_ts && $wdate <= $end_ts && $wdate <= $hgst) {
-                               // $md_date = $wdate - date('I', $wdate) * 3600;
-                                $this->countListEvent($properties, $wdate, $lwst, $hgst, $daylist);
-                                $wdate += 86400;
-                            }
-                        }
-                        $adate += 604800 * $rep['linterval'];
-                    }
-                    break;
-
-                case 'MONTHLY' :
-                    $bmonth = ($rep['linterval'] - ((($year - date('Y', $rep['ts'])) * 12)
-                            - date('n', $rep['ts'])) % $rep['linterval']) % $rep['linterval'];
-
-                    for ($amonth = $bmonth - $rep['linterval']; $amonth <= $bmonth; $amonth += $rep['linterval']) {
-                        if ($rep['ts'] < $start) {
-                            // is repeated at X. week day of X. month...
-                            if (!$rep['day']) {
-                                $lwst = mktime(12, 0, 0, $amonth
-                                                - ((($year - date('Y', $rep['ts'])) * 12
-                                                + ($amonth - date('n', $rep['ts']))) % $rep['linterval']), 1, $year)
-                                        + ($rep['sinterval'] - 1) * 604800;
-                                $aday = strftime('%u', $lwst);
-                                $lwst -= ( $aday - $rep['wdays']) * 86400;
-                                if ($rep['sinterval'] == 5) {
-                                    if (date('j', $lwst) < 10) {
-                                        $lwst -= 604800;
-                                    }
-                                    if (date('n', $lwst) == date('n', $lwst + 604800)) {
-                                        $lwst += 604800;
-                                    }
-                                } else {
-                                    if ($aday > $rep['wdays']) {
-                                        $lwst += 604800;
-                                    }
-                                }
-                            } else {
-                                // or at X. day of month ?
-                                $lwst = mktime(12, 0, 0, $amonth
-                                        - ((($year - date('Y', $rep['ts'])) * 12
-                                        + ($amonth - date('n', $rep['ts']))) % $rep['linterval']), $rep['day'], $year);
-                            }
-                        } else {
-                            // first recurrence
-                            $lwst = $rep['ts'];
-                            $lwst = mktime(12, 0, 0, $amonth
-                                        - ((($year - date('Y', $rep['ts'])) * 12
-                                        + ($amonth - date('n', $rep['ts']))) % $rep['linterval']), $rep['day'], $year);
-
-                        }
-                        $hgst = $lwst + $duration * 86400;
-                        $md_date = $lwst;
-                        // events last longer than one day
-                        while ($md_date >= $start_ts && $md_date <= $hgst && $md_date <= $end_ts) {
-                            $this->countListEvent($properties, $md_date, $lwst, $hgst, $daylist);
-                            $md_date += 86400;
-                        }
-                    }
-                    break;
-
-                case 'YEARLY' :
-                    for ($ayear = $year - 1; $ayear <= $year; $ayear++) {
-                        if ($rep['day']) {
-                            $lwst = mktime(12, 0, 0, $rep['month'], $rep['day'], $ayear);
-                            $hgst = $lwst + $duration * 86400;
-                            $wdate = $lwst;
-                            while ($hgst >= $start_ts && $wdate <= $hgst && $wdate <= $end_ts) {
-                                $this->countListEvent($properties, $wdate, $lwst, $hgst, $daylist);
-                                $wdate += 86400;
-                            }
-                        } else {
-                            if ($rep['ts'] < $start) {
-                                $adate = mktime(12, 0, 0, $rep['month'], 1, $ayear)
-                                        + ($rep['sinterval'] - 1) * 604800;
-                                $aday = strftime('%u', $adate);
-                                $adate -= ( $aday - $rep['wdays']) * 86400;
-                                if ($rep['sinterval'] == 5) {
-                                    if (date('j', $adate) < 10) {
-                                        $adate -= 604800;
-                                    }
-                                } elseif ($aday > $rep['wdays']) {
-                                    $adate += 604800;
-                                }
-                            } else {
-                                $adate = $rep['ts'];
-                            }
-                            $lwst = $adate;
-                            $hgst = $lwst + $duration * 86400;
-                            while ($hgst >= $start_ts && $adate <= $hgst && $adate <= $end_ts) {
-                                $this->countListEvent($properties, $adate, $lwst, $hgst, $daylist);
-                                $adate += 86400;
-                            }
-                        }
-                    }
-            }
-        }
-        return $daylist;
-    }
-
-    private function countListEvent($properties, $date, $lwst, $hgst, &$daylist)
-    {
-        if ($date < $this->getStart() || $date > $this->getEnd()) {
-            return false;
-        }
-        $lwst = mktime(12, 0, 0, date('n', $lwst), date('j', $lwst), date('Y', $lwst));
-        $hgst = mktime(12, 0, 0, date('n', $hgst), date('j', $hgst), date('Y', $hgst));
-
-        // if this date is in the exceptions return false
-        $exdates = explode(',', $properties['EXDATE']);
-        foreach ($exdates as $exdate) {
-            if ($exdate > 0 && $exdate >= $lwst && $exdate <= $hgst) {
-                return false;
-            }
-        }
-        // is event expired?
-        if ($properties['RRULE']['expire'] > 0
-                && $properties['RRULE']['expire'] <= $hgst) {
-            return false;
-        }
-        $idate = date('Ymd', $date);
-        $daylist["$idate"]["{$properties['STUDIP_ID']}"] =
-                $daylist["$idate"]["{$properties['STUDIP_ID']}"]
-                ? $daylist["$idate"]["{$properties['STUDIP_ID']}"]++ : 1;
-        return true;
-    }
-
-    /**
-     *
-     * TODO use filter instead
-     *
-     * @param Event $event
-     * @param array $restrictions
-     * @return boolean
-     */
-    public static function checkRestriction(Event $event, $restrictions)
-    {
-        $properties = $event->getProperties();
-        if (is_array($restrictions)) {
-            foreach ($restrictions as $property_name => $restriction) {
-                if (isset($properties[mb_strtoupper($property_name)])) {
-                    if (is_array($restriction)) {
-                        return in_array($properties[mb_strtoupper($property_name)], $restriction);
-                    } else if ($restriction != '') {
-                        return $properties[mb_strtoupper($property_name)] == $restriction;
-                    }
-                }
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Returns an array with all necessary informations to build the day view.
-     *
-     * @param type $start
-     * @param type $end
-     * @param type $step
-     * @param type $params
-     * @return type
-     */
-    public function createEventMatrix($start, $end, $step)
-    {
-        // correction of days where dst starts or ends
-        $dst_offset = (date('I', $this->getStart()) - date('I', $this->getEnd())) * 3600;
-        $start += $dst_offset;
-        $end += $dst_offset;
-
-        $term = [];
-        $em = $this->adapt_events($start, $end, $step);
-        $max_cols = 0;
-        $mapping = [];
-        // calculate maximum number of columns
-        $w = 0;
-        for ($i = $start / $step; $i < $end / $step + 3600 / $step; $i++) {
-            $col = 0;
-            $row = $i - $start / $step;
-            while ($w < sizeof($em['events']) && $em['events'][$w]->getStart() >= $this->getStart() + $i * $step
-            && $em['events'][$w]->getStart() < $this->getStart() + ($i + 1) * $step) {
-                $rows = ceil($em['events'][$w]->getDuration() / $step);
-                if ($rows < 1) {
-                    $rows = 1;
-                }
-                if (empty($term[$row])) {
-                    $term[$row] = [];
-                }
-                if (empty($term[$row][$col])) {
-                    $term[$row][$col] = '';
-                }
-                while ($term[$row][$col] != '' && $term[$row][$col] != '#') {
-                    $col++;
-                }
-                $term[$row][$col] = $em['events'][$w];
-                $mapping[$row][$col] = $em['map'][$w];
-
-                $count = $rows - 1;
-                for ($x = $row + 1; $x < $row + $rows; $x++) {
-                    for ($y = 0; $y <= $col; $y++) {
-                        if ($y == $col) {
-                            $term[$x][$y] = $count--;
-                        } elseif ($term[$x][$y] == '') {
-                            $term[$x][$y] = '#';
-                        }
-                    }
-                }
-                if ($max_cols < sizeof($term[$row])) {
-                    $max_cols = sizeof($term[$row]);
-                }
-                $w++;
-            }
-        }
-        $row_min = 0;
-        for ($i = $start / $step; $i < $end / $step + 3600 / $step; $i++) {
-            $row = $i - $start / $step;
-            $row_min = $row;
-            while ($this->maxValue($term[$row] ?? 0, $step) > 1) {
-                $row += $this->maxValue($term[$row] ?? 0, $step) - 1;
-            }
-            $size = 0;
-            for ($j = $row_min; $j <= $row; $j++) {
-                if (isset($term[$j]) && count($term[$j]) > $size) {
-                    $size = count($term[$j]);
-                }
-            }
-            for ($j = $row_min; $j <= $row; $j++) {
-                $colsp[$j] = $size;
-            }
-            $i = $row + $start / $step;
-        }
-        $rows = [];
-        $cspan = [];
-        for ($i = $start / $step; $i < $end / $step + 3600 / $step; $i++) {
-            $row = $i - $start / $step;
-            $cspan_0 = 0;
-            if (!empty($term[$row])) {
-                if ($colsp[$row] > 0) {
-                    $cspan_0 = (int) ($max_cols / $colsp[$row]);
-                }
-                for ($j = 0; $j < $colsp[$row]; $j++) {
-                    $sp = 0;
-                    $n = 0;
-                    if ($j + 1 == $colsp[$row]) {
-                        $cspan[$row][$j] = $cspan_0 + $max_cols % $colsp[$row];
-                    }
-                    if (is_object($term[$row][$j])) {
-                        // Wieviele Termine sind zum aktuellen Termin zeitgleich?
-                        $p = 0;
-                        $count = 0;
-                        while (array_key_exists($p, $em['events']) && ($aterm = $em['events'][$p])) {
-                            if ($aterm->getStart() >= $term[$row][$j]->getStart()
-                                    && $aterm->getStart() <= $term[$row][$j]->getEnd()) {
-                                $count++;
-                            }
-                            $p++;
-                        }
-                        if ($count == 0) {
-                            for ($n = $j + 1; $n < $colsp[$row]; $n++) {
-                                if (!is_int($term[$row][$n])) {
-                                    $sp++;
-                                } else {
-                                    break;
-                                }
-                            }
-                            $cspan[$row][$j] += $sp;
-                        }
-                        $rows[$row][$j] = ceil($term[$row][$j]->getDuration() / $step);
-                        if ($rows[$row][$j] < 1) {
-                            $rows[$row][$j] = 1;
-                        }
-                        if ($sp > 0) {
-                            for ($m = $row; $m < $rows + $row; $m++) {
-                                $colsp[$m] = $colsp[$m] - $sp + 1;
-                                $v = $j;
-                                while ($term[$m][$v] == '#') {
-                                    $term[$m][$v] = 1;
-                                }
-                            }
-                            $j = $n;
-                        }
-                    } elseif ($term[$row][$j] == '#') {
-                        $csp = 1;
-                        while ($term[$row][$j] == '#') {
-                            $csp += $cspan[$row][$j];
-                            $j++;
-                        }
-                        $cspan[$row][$j] = $csp;
-                    } elseif ($term[$row][$j] == '') {
-                        $cspan[$row][$j] = $max_cols - $j + 1;
-                    }
-                }
-            }
-        }
-        if ($max_cols < 1 && isset($em['day_events']) && count($em['day_events'])) {
-            $max_cols = 1;
-        }
-        $em['cspan'] = $cspan;
-        $em['rows'] = $rows;
-        $em['colsp'] = $colsp;
-        $em['term'] = $term;
-        $em['max_cols'] = $max_cols;
-        $em['mapping'] = $mapping;
-        return $em;
-    }
-
-    /**
-     * Returns max value of colspan in calendar tables for day view.
-     *
-     * @param Array $term Array with table cell content.
-     * @param int $st Seconds between each row in calendar table.
-     * @return int Max value of colspan.
-     */
-    private function maxValue($term, $st)
-    {
-        $max_value = 0;
-
-        if (is_array($term)) {
-            for ($i = 0; $i < count($term); $i++) {
-                if (is_object($term[$i])) {
-                    $max = ceil($term[$i]->getDuration() / $st);
-                } elseif ($term[$i] == '#') {
-                    continue;
-                } elseif ($term[$i] > $max_value) {
-                    $max = $term[$i];
-                }
-                if ($max > $max_value) {
-                    $max_value = $max;
-                }
-            }
-        }
-
-        return $max_value;
-    }
-
-    /**
-     * Returns array with events and other information to build calendar tables
-     * for day view.
-     *
-     * @param int $start Start time date as unix timestamp
-     * @param int $end End time date as unix timestamp
-     * @param int $step Seconds between each row in calendar table.
-     * @return Array Array with new calculated events and some other things.
-     */
-    public function adapt_events($start, $end, $step = 900)
-    {
-        $tmp_events = [];
-        $map_events = [];
-        $tmp_day_event = [];
-        $map_day_events = [];
-        for ($i = 0; $i < sizeof($this->events); $i++) {
-            $event = $this->events[$i];
-            if (($event->getEnd() > $this->getStart() + $start)
-                    && ($event->getStart() < $this->getStart() + $end + 3600)) {
-                $cloned_event = clone $event;
-                if ($event->isDayEvent()
-                        || ($event->getStart() <= $this->getStart()
-                        && $event->getEnd() >= $this->getEnd())) {
-                    $cloned_event->setStart($this->getStart());
-                    $cloned_event->setEnd($this->getEnd());
-                    $tmp_day_event[] = $cloned_event;
-                    $map_day_events[] = $i;
-                } else {
-                    $end_corr = $cloned_event->getEnd() % $step;
-                    if ($end_corr > 0) {
-                        $end_corr = $cloned_event->getEnd() + ($step - $end_corr);
-                        $cloned_event->setEnd($end_corr);
-                    }
-                    if ($cloned_event->getStart() < ($this->getStart() + $start)) {
-                        $cloned_event->setStart($this->getStart() + $start);
-                    }
-                    if ($cloned_event->getEnd() > ($this->getStart() + $end + 3600)) {
-                        $cloned_event->setEnd($this->getStart() + $end + 3600);
-                    }
-                    $tmp_events[$cloned_event->id . $cloned_event->getStart()] = $cloned_event;
-                    $map_events[$cloned_event->id . $cloned_event->getStart()] = $i;
-                }
-            }
-        }
-
-        uasort($tmp_events, function($a, $b) {return $a->start - $b->start;});
-        $map = [];
-        foreach (array_keys($tmp_events) as $key) {
-            $map[] = $map_events[$key];
-        }
-
-        return [
-            'events' => array_values($tmp_events),
-            'map' => $map,
-            'day_events' => $tmp_day_event,
-            'day_map' => $map_day_events];
-    }
-
-}
diff --git a/lib/classes/forms/SelectedRangesInput.php b/lib/classes/forms/SelectedRangesInput.php
new file mode 100644
index 0000000000000000000000000000000000000000..77b2db22cc357cf2225d1fef8cbc825434947b7e
--- /dev/null
+++ b/lib/classes/forms/SelectedRangesInput.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Studip\Forms;
+
+class SelectedRangesInput extends Input
+{
+    protected $selectable_items = [];
+
+    protected $selected_items = [];
+
+    protected $search_type = null;
+
+
+    public function render()
+    {
+        $template = $GLOBALS['template_factory']->open('forms/selected_ranges_input');
+        $template->name = $this->name;
+        $template->selectable_items = $this->selectable_items;
+        $template->selected_items = [];
+        foreach ($this->selected_items as $item) {
+            $item_data = [];
+            if ($item instanceof \Range) {
+                $item_data['name'] = $item->getFullname();
+                $item_data['id'] = $item->getRangeId();
+            } elseif ($item instanceof \StudipItem) {
+                $item_data['name'] = $item->getItemName();
+                $item_data['id'] = $item->id;
+            } elseif (
+                is_array($item)
+                && array_key_exists('name', $item)
+                && array_key_exists('id', $item)
+            ) {
+                $item_data['name'] = $item['name'];
+                $item_data['id'] = $item['id'];
+            }
+            if ($item_data) {
+                $template->selected_items[] = $item_data;
+            }
+        }
+        $template->searchtype = $this->search_type;
+        return $template->render();
+    }
+
+    public function getRequestValue()
+    {
+        return \Request::getArray($this->name);
+    }
+
+    public function setSelectedItems(array $items)
+    {
+        $this->selected_items = $items;
+    }
+
+    public function setSearchType(\SearchType $search_type)
+    {
+        $this->search_type = $search_type;
+    }
+}
diff --git a/lib/classes/searchtypes/StandardSearch.class.php b/lib/classes/searchtypes/StandardSearch.class.php
index fc3135a00f38917f8c872ac042ee70fc74003022..837a849c86689d36440da202d2c84581a73f923d 100644
--- a/lib/classes/searchtypes/StandardSearch.class.php
+++ b/lib/classes/searchtypes/StandardSearch.class.php
@@ -94,24 +94,36 @@ class StandardSearch extends SQLSearch
         switch ($this->search) {
             case "username":
                 $this->extendedLayout = true;
-                return "SELECT DISTINCT auth_user_md5.username, CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname, ' (', auth_user_md5.username, ')'), auth_user_md5.perms " .
-                        "FROM auth_user_md5 LEFT JOIN user_info ON (user_info.user_id = auth_user_md5.user_id) " .
+                $sql = "SELECT DISTINCT auth_user_md5.username";
+                if (empty($this->search_settings['simple_name'])) {
+                    $sql .= ", CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname, ' (', auth_user_md5.username, ')'), auth_user_md5.perms ";
+                } else {
+                    $sql .= ", CONCAT(auth_user_md5.Vorname, ' ', auth_user_md5.Nachname) ";
+                }
+                $sql .= "FROM auth_user_md5 LEFT JOIN user_info ON (user_info.user_id = auth_user_md5.user_id) " .
                         "LEFT JOIN user_visibility ON (user_visibility.user_id = auth_user_md5.user_id) " .
                         "WHERE (CONCAT(auth_user_md5.Vorname, ' ', auth_user_md5.Nachname) LIKE REPLACE(:input, ' ', '% ') " .
                             "OR CONCAT(auth_user_md5.Nachname, ' ', auth_user_md5.Vorname) LIKE REPLACE(:input, ' ', '% ') " .
                             "OR CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname) LIKE :input " .
                             "OR auth_user_md5.username LIKE :input) AND " . get_vis_query('auth_user_md5', 'search') .
                         " ORDER BY Nachname ASC, Vorname ASC";
+                return $sql;
             case "user_id":
                 $this->extendedLayout = true;
-                return "SELECT DISTINCT auth_user_md5.user_id, CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname, ' (', auth_user_md5.username, ')'), auth_user_md5.perms " .
-                        "FROM auth_user_md5 LEFT JOIN user_info ON (user_info.user_id = auth_user_md5.user_id) " .
+                $sql = "SELECT DISTINCT auth_user_md5.user_id";
+                if (empty($this->search_settings['simple_name'])) {
+                    $sql .= ", CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname, ' (', auth_user_md5.username, ')'), auth_user_md5.perms ";
+                } else {
+                    $sql .= ", CONCAT(auth_user_md5.Vorname, ' ', auth_user_md5.Nachname) ";
+                }
+                $sql .= "FROM auth_user_md5 LEFT JOIN user_info ON (user_info.user_id = auth_user_md5.user_id) " .
                     "LEFT JOIN user_visibility ON (user_visibility.user_id = auth_user_md5.user_id) " .
                     "WHERE (CONCAT(auth_user_md5.Vorname, ' ', auth_user_md5.Nachname) LIKE REPLACE(:input, ' ', '% ') " .
                             "OR CONCAT(auth_user_md5.Nachname, ' ', auth_user_md5.Vorname) LIKE REPLACE(:input, ' ', '% ') " .
                             "OR CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname) LIKE :input " .
                             "OR auth_user_md5.username LIKE :input) AND " . get_vis_query('auth_user_md5', 'search') .
                         " ORDER BY Nachname ASC, Vorname ASC";
+                return $sql;
             case "Seminar_id":
                 return "SELECT seminare.Seminar_id, CONCAT_WS(' ', seminare.VeranstaltungsNummer, seminare.Name,  ".$semester.") " .
                     "FROM seminare " .
diff --git a/lib/classes/sidebar/DateSelectWidget.php b/lib/classes/sidebar/DateSelectWidget.php
new file mode 100644
index 0000000000000000000000000000000000000000..df341658dbcb88fff817c73f70de17267acfdbd8
--- /dev/null
+++ b/lib/classes/sidebar/DateSelectWidget.php
@@ -0,0 +1,51 @@
+<?php
+
+class DateSelectWidget extends SidebarWidget
+{
+    protected $date = null;
+    protected $calendar_control = false;
+
+    public function __construct()
+    {
+        $this->template = 'sidebar/date-select-widget';
+        $this->date = new DateTime();
+        parent::__construct();
+    }
+
+    public function setCalendarControl(bool $calendar_control = false) : void
+    {
+        $this->calendar_control = $calendar_control;
+    }
+
+    public function setDate(DateTime $date) : void
+    {
+        $this->date = $date;
+    }
+
+    public function getDate() : ?DateTime
+    {
+        return $this->date;
+    }
+
+    public function getCalendarControlStatus() : bool
+    {
+        return $this->calendar_control;
+    }
+
+    public function render($variables = []) : string
+    {
+        $template = $GLOBALS['template_factory']->open($this->template);
+        $template->set_attributes($variables + $this->template_variables);
+        $template->set_attribute('title', _('Datum auswählen'));
+        $template->set_attribute('date', $this->date);
+        $template->set_attribute('calendar_control', $this->calendar_control);
+
+        if ($this->layout) {
+            $layout = $GLOBALS['template_factory']->open($this->layout);
+            $layout->layout_css_classes = $this->layout_css_classes;
+            $template->set_layout($layout);
+        }
+
+        return $template->render();
+    }
+}
diff --git a/lib/exceptions/FeatureDisabledException.php b/lib/exceptions/FeatureDisabledException.php
new file mode 100644
index 0000000000000000000000000000000000000000..16af0bf8f8a35f85c5ef6bc7d87d9bb8e3a395d8
--- /dev/null
+++ b/lib/exceptions/FeatureDisabledException.php
@@ -0,0 +1,11 @@
+<?php
+class FeatureDisabledException extends StudipException
+{
+    public function __construct($message = '', $code = 0, Exception $previous = null)
+    {
+        if (func_num_args() === 0) {
+            $message = _('Diese Funktion ist ausgeschaltet.');
+        }
+        parent::__construct($message, [], $code, $previous);
+    }
+}
diff --git a/lib/models/CalendarEvent.class.php b/lib/models/CalendarEvent.class.php
deleted file mode 100644
index 16ca91aab0047e882d430b35f292cdc3c80f836c..0000000000000000000000000000000000000000
--- a/lib/models/CalendarEvent.class.php
+++ /dev/null
@@ -1,1394 +0,0 @@
-<?php
-/**
- * EventRange.class.php - model class for table calendar_event
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @copyright   2014 Stud.IP Core-Group
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- *
- * @property array $id alias for pk
- * @property string $range_id database column
- * @property string $event_id database column
- * @property int $group_status database column
- * @property int $mkdate database column
- * @property int $chdate database column
- * @property SimpleORMapCollection|CalendarEvent[] $attendees has_many CalendarEvent
- * @property SimpleORMapCollection|ConsultationEvent[] $consultation_events has_many ConsultationEvent
- * @property User $user belongs_to User
- * @property Course $course belongs_to Course
- * @property Institute $institute belongs_to Institute
- * @property ConsultationBooking $consultation_booking belongs_to ConsultationBooking
- * @property EventData $event has_one EventData
- * @property mixed $type additional field
- * @property mixed $name additional field
- * @property mixed $author_id additional field
- * @property mixed $editor_id additional field
- * @property mixed $title additional field
- * @property mixed $start additional field
- * @property mixed $end additional field
- * @property-read mixed $owner additional field
- */
-class CalendarEvent extends SimpleORMap implements Event, PrivacyObject
-{
-    const PARTSTAT_TENTATIVE = 1;
-    const PARTSTAT_ACCEPTED = 2;
-    const PARTSTAT_DECLINED = 3;
-    const PARTSTAT_DELEGATED = 4;
-    const PARTSTAT_NEEDS_ACTION = 5;
-
-    protected static function configure($config = [])
-    {
-        $config['db_table'] = 'calendar_event';
-
-        $config['belongs_to']['user'] = [
-            'class_name'  => User::class,
-            'foreign_key' => 'range_id',
-        ];
-        $config['belongs_to']['course'] = [
-            'class_name'  => Course::class,
-            'foreign_key' => 'range_id',
-        ];
-        $config['belongs_to']['institute'] = [
-            'class_name'  => Institute::class,
-            'foreign_key' => 'range_id',
-        ];
-        $config['has_one']['event'] = [
-            'class_name'        => EventData::class,
-            'foreign_key'       => 'event_id',
-            'assoc_foreign_key' => 'event_id',
-            'on_delete'         => 'delete',
-            'on_store'          => 'store'
-        ];
-        $config['has_many']['attendees'] = [
-            'class_name'        => CalendarEvent::class,
-            'foreign_key'       => 'event_id',
-            'assoc_foreign_key' => 'event_id'
-        ];
-        $config['belongs_to']['consultation_booking'] = [
-            'class_name'        => ConsultationBooking::class,
-            'foreign_key'       => 'event_id',
-            'assoc_foreign_key' => 'student_event_id',
-        ];
-        $config['has_many']['consultation_events'] = [
-            'class_name'        => ConsultationEvent::class,
-            'foreign_key'       => 'event_id',
-            'assoc_foreign_key' => 'event_id',
-        ];
-        $config['additional_fields']['type'] = true;
-        $config['additional_fields']['name'] = true;
-        $config['additional_fields']['author_id'] = true;
-        $config['additional_fields']['editor_id'] = true;
-        $config['additional_fields']['title'] = true;
-        $config['additional_fields']['start'] = true;
-        $config['additional_fields']['end'] = true;
-        $config['additional_fields']['owner']['get'] = 'getOwner';
-
-        $config['registered_callbacks']['after_delete'][] = function ($event) {
-            if ($event->consultation_booking) {
-                $event->consultation_booking->student_event_id = null;
-                $event->consultation_booking->store();
-            }
-            $event->consultation_events->delete();
-        };
-
-        parent::configure($config);
-    }
-
-    private $properties = null;
-    private $permission_user_id = null;
-
-    /**
-     * Returns the owner of this event as an object of type User, Course
-     * or Institute.
-     *
-     * @return object
-     */
-    public function getOwner()
-    {
-        if ($this->user) {
-            return $this->user;
-        } else if ($this->course) {
-            return $this->course;
-        } else if ($this->institute) {
-            return $this->institute;
-        }
-        return null;
-    }
-
-    public static function deleteBySQL($where, $params = [])
-    {
-        $ret = parent::deleteBySQL($where, $params);
-        EventData::garbageCollect();
-        return $ret;
-    }
-
-    /**
-     * Finds calendar events by the uid of the event data.
-     *
-     * @param string $uid The global unique id of this event.
-     * @return null|CalendarEvent The calendar event, an array of calendar events or null.
-     */
-    public static function findByUid($uid, $range_id = null)
-    {
-        $event_data = EventData::findOneByuid($uid);
-        if ($event_data) {
-            if ($range_id) {
-                return self::find([$range_id, $event_data->getId()]);
-            }
-            return self::findByevent_id($event_data->getId());
-        }
-        return null;
-    }
-
-    /**
-     * Keeps the event data
-     */
-    public function __clone()
-    {
-        if (is_object($this->event)) {
-            $event = clone $this->event;
-            parent::__clone();
-            $this->event = $event;
-        } else {
-            parent::__clone();
-        }
-    }
-
-    /**
-     * Returns a list of all categories the event belongs to.
-     * Returns an empty string if no permission.
-     *
-     * @return string All categories as list.
-     */
-    public function toStringCategories($as_array = false)
-    {
-        global $PERS_TERMIN_KAT;
-
-        $categories = [];
-        if ($this->havePermission(Event::PERMISSION_READABLE,
-                $this->permission_user_id)) {
-            if ($this->event->categories) {
-                $categories = array_map('trim', explode(',', $this->event->categories));
-            }
-            if ($this->event->category_intern) {
-                array_unshift($categories,
-                        $PERS_TERMIN_KAT[$this->event->category_intern]['name']);
-            }
-        }
-        return $as_array ? $categories : implode(', ', $categories);
-    }
-
-    /**
-     * Returns the name of the group status.
-     * Returns an empty string status is unknown.
-     *
-     * @return string All categories as list.
-     */
-    public function toStringGroupStatus($status = null)
-    {
-        if (is_null($status)) {
-            $status = $this->group_status;
-        }
-        switch ($status) {
-            case CalendarEvent::PARTSTAT_TENTATIVE :
-                return _('Abwartend');
-            case CalendarEvent::PARTSTAT_ACCEPTED :
-                return _('Angenommen');
-            case CalendarEvent::PARTSTAT_DECLINED :
-                return _('Abgelehnt');
-            case CalendarEvent::PARTSTAT_DELEGATED :
-                return _('Angenommen (keine Teilnahme)');
-        }
-        return '';
-    }
-
-    /**
-     * Returns all values that defines a recurrence rule or a single value
-     * named by $index.
-     *
-     * @param string $index Name of the value to retrieve (optional).
-     * @return string|array The value(s) of the recurrence rule.
-     * @throws InvalidArgumentException
-     */
-    public function getRecurrence($index = null)
-    {
-        $recurrence = [
-            'ts' => $this->event->ts ?: mktime(12, 0, 0, date('n', $this->getStart()), date('j',
-                $this->getStart()), date('Y', $this->getStart())),
-            'linterval' => $this->event->linterval,
-            'sinterval' => $this->event->sinterval,
-            'wdays' => $this->event->wdays,
-            'month' => $this->event->month,
-            'day' => $this->event->day,
-            'rtype' => $this->event->rtype ?: 'SINGLE',
-            'duration' => $this->event->duration,
-            'count' => $this->event->count,
-            'expire' => $this->event->expire ?: Calendar::CALENDAR_END
-        ];
-        if ($index) {
-            if (in_array($index, array_keys($recurrence))) {
-                return $recurrence[$index];
-            } else {
-                throw new InvalidArgumentException('CalendarEvent::getRecurrence '
-                        . $index . ' is not a field in the recurrence rule.');
-            }
-        }
-        return $recurrence;
-    }
-
-    /**
-     *
-     * TODO should throw an exception if input values are wrong
-     *
-     * @param array $r_rule
-     * @return array|false The values of the recurrence rule.
-     */
-    function setRecurrence($r_rule)
-    {
-        $start = $this->getStart();
-        $end = $this->getEnd();
-        $duration = (int) ((mktime(12, 0, 0, date('n', $end),
-                date('j', $end), date('Y', $end))
-                - mktime(12, 0, 0, date('n', $start),
-                        date('j', $start), date('Y', $start))) / 86400);
-        if (!isset($r_rule['count'])) {
-            $r_rule['count'] = 0;
-        }
-
-        switch ($r_rule['rtype']) {
-            case 'SINGLE':
-                $ts = mktime(12, 0, 0, date('n', $start),
-                        date('j', $start), date('Y', $start));
-                $rrule = [$ts, 0, 0, '', 0, 0, 'SINGLE', $duration];
-                break;
-            case 'DAILY':
-                $r_rule['linterval'] = $r_rule['linterval'] ? intval($r_rule['linterval']) : 1;
-                $ts = mktime(12, 0, 0, date('n', $start),
-                        date('j', $start) + $r_rule['linterval'], date('Y', $start));
-                if ($r_rule['count']) {
-                    $r_rule['expire'] = mktime(23, 59, 59, date('n', $start), date('j', $start)
-                            + ($r_rule['count'] - 1) * $r_rule['linterval'], date('Y', $start));
-                }
-                $rrule = [$ts, $r_rule['linterval'], 0, '', 0, 0, 'DAILY', $duration];
-                break;
-            case 'WEEKLY':
-                $r_rule['linterval'] = $r_rule['linterval'] ? intval($r_rule['linterval']) : 1;
-                if (!$r_rule['wdays']) {
-                    $ts = mktime(12, 0, 0, date('n', $start), date('j', $start) +
-                            ($r_rule['linterval'] * 7 - (strftime('%u', $start) - 1)),
-                            date('Y', $start));
-                    if ($r_rule['count']) {
-                        $r_rule['expire'] = mktime(23, 59, 59, date('n', $start),
-                                date('j', $start) + ($r_rule['linterval'] * 7 * ($r_rule['count'] - 1)),
-                                date('Y', $start));
-                    }
-                    $rrule = [$ts, $r_rule['linterval'], 0, strftime('%u', $start),
-                        0, 0, 'WEEKLY', $duration];
-                } else {
-                    $ts = mktime(12, 0, 0, date('n', $start),
-                            date('j', $start) + (7 - (strftime('%u', $start) - 1))
-                            - ((strftime('%u', $start) <= substr($r_rule['wdays'], -1)) ? 7 : 0),
-                            date('Y', $start));
-
-                    if ($r_rule['count']) {
-                        $dt_ts = DateTime::createFromFormat('U', $ts);
-
-                        // max. length of selected week days must not exceed
-                        // number of recurrences
-                        $r_rule['wdays'] = substr($r_rule['wdays'], 0, $r_rule['count']);
-
-                        $start_wday = date('N', $start);
-                        $count_first_week = 0;
-                        for ($i = 0; $i < strlen($r_rule['wdays']); $i++) {
-                            if (isset($r_rule['wdays'][$i]) && $r_rule['wdays'][$i] >= $start_wday) {
-                                $count_first_week++;
-                            }
-                        }
-
-                        $count_first_week += (date('N', $start) < $r_rule['wdays'][0]) ? 1 : 0;
-
-                        $count_complete = $r_rule['count'] - $count_first_week;
-                        $weeks_max = floor($count_complete / strlen($r_rule['wdays']));
-
-                        $dt_expire = $dt_ts->add(new DateInterval('P' . ($weeks_max + 1) . 'W'));
-                        $count_last_week = $count_complete % strlen($r_rule['wdays']);
-                        if ($count_last_week && isset($r_rule['wdays'][$count_last_week - 1])) {
-                            $last_wday = $r_rule['wdays'][$count_last_week - 1];
-                            $dt_expire = $dt_expire->add(new DateInterval('P' . ($last_wday - 1) . 'D'));
-                        } else {
-                            $dt_expire = $dt_expire->sub(new DateInterval('P1D'));
-                        }
-
-                        $expire_ts = $dt_expire->format('U');
-                        $r_rule['expire'] = mktime(23, 59, 59, date('n', $expire_ts),
-                                date('j', $expire_ts), date('Y', $expire_ts));
-                    }
-                    $rrule = [$ts, $r_rule['linterval'], 0, $r_rule['wdays'],
-                        0, 0, 'WEEKLY', $duration];
-                }
-                break;
-            case 'MONTHLY':
-                if ($r_rule['month']) {
-                    return false;
-                }
-                $r_rule['linterval'] = $r_rule['linterval'] ? intval($r_rule['linterval']) : 1;
-                if (!$r_rule['day'] && !$r_rule['sinterval'] && !$r_rule['wdays']) {
-                    $amonth = date('n', $start) + $r_rule['linterval'];
-                    $ts = mktime(12, 0, 0, $amonth, date('j', $start), date('Y', $start));
-                    $rrule = [$ts, $r_rule['linterval'], 0, '', 0,
-                        date('j', $start), 'MONTHLY', $duration];
-                } else if (!$r_rule['sinterval'] && !$r_rule['wdays']) {
-                    if ($r_rule['day'] < date('j', $start)) {
-                        $amonth = date('n', $start) + $r_rule['linterval'];
-                    } else {
-                        $amonth = date('n', $start);
-                    }
-                    $ts = mktime(12, 0, 0, $amonth, $r_rule['day'], date('Y', $start));
-                    $rrule = [$ts, $r_rule['linterval'], 0, '', 0,
-                        $r_rule['day'], 'MONTHLY', $duration];
-                } else if (!$r_rule['day']) {
-                    $amonth = date('n', $start);
-                    $adate = mktime(12, 0, 0, $amonth, 1,
-                            date('Y', $start)) + ($r_rule['sinterval'] - 1) * 604800;
-                    $awday = strftime('%u', $adate);
-                    $adate -= ( $awday - $r_rule['wdays']) * 86400;
-                    if ($r_rule['sinterval'] == 5) {
-                        if (date('j', $adate) < 10) {
-                            $adate -= 604800;
-                        }
-                        if (date('n', $adate) == date('n', $adate + 604800)) {
-                            $adate += 604800;
-                        }
-                    } else if ($awday > $r_rule['wdays']) {
-                        $adate += 604800;
-                    }
-                    if (date('Ymd', $adate) < date('Ymd', $start)) {
-                        $amonth = date('n', $start) + $r_rule['linterval'];
-                        $adate = mktime(12, 0, 0, $amonth, 1,
-                                date('Y', $start)) + ($r_rule['sinterval'] - 1) * 604800;
-                        $awday = strftime('%u', $adate);
-                        $adate -= ( $awday - $r_rule['wdays']) * 86400;
-                        if ($r_rule['sinterval'] == 5) {
-                            if (date('j', $adate) < 10) {
-                                $adate -= 604800;
-                            }
-                            if (date('n', $adate) == date('n', $adate + 604800)) {
-                                $adate += 604800;
-                            }
-                        } else if ($awday > $r_rule['wdays']) {
-                            $adate += 604800;
-                        }
-                    }
-                    $ts = $adate;
-                    $rrule = [$ts, $r_rule['linterval'], $r_rule['sinterval'],
-                        $r_rule['wdays'], 0, 0, 'MONTHLY', $duration];
-                }
-
-                if ($r_rule['count']) {
-                    $r_rule['expire'] = mktime(23, 59, 59, date('n', $ts) + $r_rule['linterval']
-                            * ($r_rule['count'] - 1), date('j', $ts), date('Y', $ts));
-                }
-                break;
-            case 'YEARLY':
-                if (!$r_rule['month'] && !$r_rule['day'] && !$r_rule['sinterval'] && !$r_rule['wdays']) {
-                    $ts = mktime(12, 0, 0, date('n', $start),
-                            date('j', $start), date('Y', $start) + 1);
-                    $rrule = [$ts, 1, 0, '', date('n', $start),
-                        date('j', $start), 'YEARLY', $duration];
-                } else if (!$r_rule['sinterval'] && !$r_rule['wdays']) {
-                    if (!$r_rule['day']) {
-                        $r_rule['day'] = date('j', $start);
-                    }
-                    $ts = mktime(12, 0, 0, $r_rule['month'], $r_rule['day'],
-                            date('Y', $start));
-                    if ($ts <= mktime(12, 0, 0, date('n', $start), date('j', $start), date('Y', $start))) {
-                        $ts = mktime(12, 0, 0, $r_rule['month'], $r_rule['day'],
-                                date('Y', $start) + 1);
-                    }
-                    $rrule = [$ts, 1, 0, '', $r_rule['month'],
-                        $r_rule['day'], 'YEARLY', $duration];
-                } else if (!$r_rule['day']) {
-                    $ayear = date('Y', $start);
-                    do {
-                        $adate = mktime(12, 0, 0, $r_rule['month'],
-                                1 + ($r_rule['sinterval'] - 1) * 7, $ayear);
-                        $aday = strftime('%u', $adate);
-                        $adate -= ( $aday - $r_rule['wdays']) * 86400;
-                        if ($r_rule['sinterval'] == 5) {
-                            if (date('j', $adate) < 10) {
-                                $adate -= 604800;
-                            }
-                            if (date('n', $adate) == date('n', $adate + 604800)) {
-                                $adate += 604800;
-                            }
-                        } else if ($aday > $r_rule['wdays']) {
-                            $adate += 604800;
-                        }
-                        $ts = $adate;
-                        $ayear++;
-                    } while ($ts <= mktime(12, 0, 0, date('n', $start), date('j', $start), date('Y', $start)));
-                    $rrule = [$ts, 1, $r_rule['sinterval'], $r_rule['wdays'],
-                        $r_rule['month'], 0, 'YEARLY', $duration];
-                }
-
-                if ($r_rule['count']) {
-                    $r_rule['expire'] = mktime(23, 59, 59, date('n', $ts),
-                            date('j', $ts), date('Y', $ts) + $r_rule['count'] - 1);
-                }
-                break;
-            default :
-                $ts = mktime(12, 0, 0, date('n', $start),
-                        date('j', $start), date('Y', $start));
-                $rrule = [$ts, 0, 0, '', 0, 0, 'SINGLE', $duration];
-                $r_rule['count'] = 0;
-        }
-
-        if (!$r_rule['expire'] || $r_rule['expire'] > Calendar::CALENDAR_END) {
-            $r_rule['expire'] = Calendar::CALENDAR_END;
-        }
-        $this->event->ts = $rrule[0];
-        $this->event->linterval = $rrule[1];
-        $this->event->sinterval = $rrule[2];
-        $this->event->wdays = $rrule[3];
-        $this->event->month = $rrule[4];
-        $this->event->day = $rrule[5];
-        $this->event->rtype = $rrule[6];
-        $this->event->duration = $rrule[7];
-        $this->event->count = $r_rule['count'];
-        $this->event->expire = $r_rule['expire'];
-
-        return $r_rule;
-    }
-
-    /**
-     * Returns a string representation of the recurrence rule.
-     * If $only_type is true returns only the type of the recurrence.
-     *
-     * @param bool $only_type If true returns only the type of recurrence.
-     * @return string The recurrence rule - human readable
-     */
-    public function toStringRecurrence($only_type = false)
-    {
-        $rrule = $this->getRecurrence();
-        $replace = [_('Montag') . ', ', _('Dienstag') . ', ', _('Mittwoch') . ', ',
-            _('Donnerstag') . ', ', _('Freitag') . ', ', _('Samstag') . ', ', _('Sonntag') . ', '];
-        $search = ['1', '2', '3', '4', '5', '6', '7'];
-        $wdays = str_replace($search, $replace, $rrule['wdays']);
-        $wdays = mb_substr($wdays, 0, -2);
-
-        switch ($rrule['rtype']) {
-            case 'DAILY':
-                if ($rrule['linterval'] > 1) {
-                        $type = 'xdaily';
-                    $text = sprintf(_('Der Termin wird alle %s Tage wiederholt.'),
-                            $rrule['linterval']);
-                } else {
-                    $type = 'daily';
-                    $text = _('Der Termin wird täglich wiederholt');
-                }
-                break;
-            case 'WEEKLY':
-                if ($rrule['linterval'] > 1) {
-                    $type = 'xweek_wdaily';
-                    $text = sprintf(_('Der Termin wird alle %s Wochen am %s wiederholt.'),
-                            $rrule['linterval'], $wdays);
-                } else {
-                    if ($rrule['wdays'] = '12345') {
-                        $type = 'workdaily';
-                    } else {
-                        $type = 'wdaily';
-                    }
-                    $text = sprintf(_('Der Termin wird jeden %s wiederholt.'), $wdays);
-                }
-                break;
-            case 'MONTHLY':
-                if ($rrule['linterval'] > 1) {
-                    if ($rrule['day']) {
-                        $type = 'mday_xmonthly';
-                        $text = sprintf(_('Der Termin wird am %s. alle %s Monate wiederholt.'),
-                                $rrule['day'], $rrule['linterval']);
-                    } else {
-                        if ($rrule['sinterval'] != '5') {
-                            $type = 'xwday_xmonthly';
-                            $text = sprintf(_('Der Termin wird jeden %s. %s alle %s Monate wiederholt.'),
-                                    $rrule['sinterval'], $wdays, $rrule['linterval']);
-                        } else {
-                            $type = 'lastwday_xmonthly';
-                            $text = sprintf(_('Der Termin wird jeden letzten %s alle %s Monate wiederholt.'),
-                                    $wdays, $rrule['linterval']);
-                        }
-                    }
-                } else {
-                    if ($rrule['day']) {
-                        $type = 'mday_monthly';
-                        $text = sprintf(_('Der Termin wird am %s. jeden Monat wiederholt.'),
-                                $rrule['day'], $rrule['linterval']);
-                    } else {
-                        if ($rrule['sinterval'] != '5') {
-                            $type = 'xwday_monthly';
-                            $text = sprintf(_('Der Termin wird am %s. %s jeden Monat wiederholt.'),
-                                    $rrule['sinterval'], $wdays, $rrule['linterval']);
-                        } else {
-                            $type = 'lastwday_monthly';
-                            $text = sprintf(_('Der Termin wird jeden letzten %s jeden Monat wiederholt.'),
-                                    $wdays, $rrule['linterval']);
-                        }
-                    }
-                }
-                break;
-            case 'YEARLY':
-                $month_names = [_('Januar'), _('Februar'), _('März'), _('April'), _('Mai'),
-                    _('Juni'), _('Juli'), _('August'), _('September'), _('Oktober'),
-                    _('November'), _('Dezember')];
-                if ($rrule['day']) {
-                    $type = 'mday_month_yearly';
-                    $text = sprintf(_('Der Termin wird jeden %s. %s wiederholt.'),
-                            $rrule['day'], $month_names[$rrule['month'] - 1]);
-                } else {
-                    if ($rrule['sinterval'] != '5') {
-                        $type = 'xwday_month_yearly';
-                        $text = sprintf(_('Der Termin wird jeden %s. %s im %s wiederholt.'),
-                                $rrule['sinterval'], $wdays, $month_names[$rrule['month'] - 1]);
-                    } else {
-                        $type = 'lastwday_month_yearly';
-                        $text = sprintf(_('Der Termin wird jeden letzten %s im %s wiederholt.'),
-                                $wdays, $month_names[$rrule['month'] - 1]);
-                    }
-                }
-                break;
-            default:
-                $type = 'single';
-                $text = _('Der Termin wird nicht wiederholt.');
-        }
-        return $only_type ? $type : $text;
-    }
-
-    /**
-     * Returns the priority in a human readable form.
-     * If the user has no permission an epmty string will be returned.
-     *
-     * @return string The priority as a string.
-     */
-    public function toStringPriority()
-    {
-        if (!$this->havePermission(Event::PERMISSION_READABLE,
-                $this->permission_user_id)) {
-            return '';
-        }
-        switch ($this->event->priority) {
-            case 1:
-                return _('Hoch');
-            case 2:
-                return _('Mittel');
-            case 3:
-                return _('Niedrig');
-            default:
-                return _('Keine Angabe');
-        }
-    }
-
-    /**
-     * Returns the accessibilty in a human readable form.
-     * If the user has no permission an epmty string will be returned.
-     *
-     * @return string The accessibility as string.
-     */
-    public function toStringAccessibility()
-    {
-        if ($this->havePermission(Event::PERMISSION_READABLE,
-                $this->permission_user_id)) {
-            switch ($this->event->class) {
-                case 'PUBLIC':
-                    return _('Öffentlich');
-                case 'CONFIDENTIAL':
-                    return _('Vertraulich');
-                default:
-                    return _('Privat');
-            }
-        }
-        return '';
-    }
-
-    /**
-     * Returns the exceptions as array of unix timestamps.
-     *
-     * @return array Array of unix timestamps.
-     */
-    public function getExceptions()
-    {
-        $exceptions = [];
-        if (trim($this->event->exceptions)) {
-            $exceptions = explode(',', $this->event->exceptions);
-        }
-        return $exceptions;
-    }
-
-    /**
-     * Sets proper timestamps as exceptions for given unix timestamps.
-     *
-     * @param array $exceptions Array of exceptions as unix timestamps.
-     */
-    public function setExceptions($exceptions)
-    {
-        $exc = [];
-        if (is_array($exceptions)) {
-            $exc = array_map(function ($exception) {
-                $exception = intval($exception);
-                return mktime(12, 0, 0, date('n', $exception),
-                        date('j', $exception), date('Y', $exception));
-            }, $exceptions);
-        }
-        $this->event->exceptions = implode(',', $exc);
-    }
-
-    /**
-     * Returns the title of this event.
-     * If the user has not the permission Event::PERMISSION_READABLE,
-     * the title is "Keine Berechtigung.".
-     *
-     * @return string
-     */
-    public function getTitle()
-    {
-        if (!$this->havePermission(Event::PERMISSION_READABLE,
-                $this->permission_user_id)) {
-            return _('Keine Berechtigung.');
-        }
-        if ($this->event->summary == '') {
-            return _('Kein Titel');
-        }
-        return $this->event->summary;
-    }
-
-    /**
-     * Sets the title of this event.
-     *
-     * @param type $title The title of this event.
-     */
-    public function setTitle($title)
-    {
-        $this->event->summary = $title;
-    }
-
-    /**
-     * Returns the starttime as unix timestamp of this event.
-     *
-     * @return int The starttime of this event as a unix timestamp
-     */
-    public function getStart()
-    {
-        return $this->event->start;
-    }
-
-    /**
-     * Sets the start date time with given unix timestamp.
-     *
-     * @param string $timestamp Unix timestamp.
-     */
-    public function setStart($timestamp)
-    {
-        $this->event->start = $timestamp;
-    }
-
-    /**
-     * Returns the endtime as unix timestamp of this event.
-     *
-     * @return int the endtime of this event as a unix timestamp
-     */
-    public function getEnd()
-    {
-        return $this->event->end;
-    }
-
-    /**
-     * Sets the end date time by given unix timestamp.
-     *
-     * @param string $timestamp Unix timestamp.
-     */
-    public function setEnd($timestamp)
-    {
-        $this->event->end = $timestamp;
-    }
-
-    /**
-     * Returns the user id of the author.
-     *
-     * @return string User id of the author.
-     */
-    public function getAuthorId()
-    {
-        return $this->event->author_id;
-    }
-
-    /**
-     * Sets the author by given user id.
-     *
-     * @param string $author_id User id of the author.
-     */
-    public function setAuthorId($author_id)
-    {
-        $this->event->author_id = $author_id;
-    }
-
-    /**
-     * Sets the editor id by given user id.
-     *
-     * @param string $editor_id User id of the editor.
-     */
-    public function setEditorId($editor_id)
-    {
-        $this->event->editor_id = $editor_id;
-    }
-
-    /**
-     * Returns the duration of this event in seconds.
-     *
-     * @return int the duration of this event in seconds
-     */
-    function getDuration()
-    {
-        return $this->event->end - $this->event->start;
-    }
-
-    /**
-     * Returns the location.
-     * Without permission or the location is not set an empty string is returned.
-     *
-     * @return string The location
-     */
-    public function getLocation()
-    {
-        $location = '';
-        if ($this->havePermission(Event::PERMISSION_READABLE,
-                $this->permission_user_id)) {
-            if (trim($this->event->location) != '') {
-                $location = $this->event->location;
-            }
-        }
-        return $location;
-    }
-
-    /**
-     * Returns the global unique id of this event.
-     *
-     * @return string The global unique id.
-     */
-    public function getUid()
-    {
-        return $this->event->uid !== ''
-                ? $this->event->uid
-                : 'Stud.IP-' . $this->event_id . '@' . $_SERVER['SERVER_NAME'];
-    }
-
-    /**
-     * Returns the description of the topic.
-     * If the user has no permission or the event has no topic
-     * or the topics have no descritopn an empty string is returned.
-     *
-     * @return String the description
-     */
-    public function getDescription()
-    {
-        $description = '';
-        if ($this->havePermission(Event::PERMISSION_READABLE,
-                $this->permission_user_id)) {
-            $description = trim($this->event->description);
-        }
-        return $description;
-    }
-
-    /**
-     * Returns the index of the category.
-     * If the user has no permission, 255 is returned.
-     *
-     * @see config/config.inc.php $TERMIN_TYP
-     * @return int The index of the category
-     */
-    public function getCategory()
-    {
-        global $PERS_TERMIN_KAT;
-
-        $category = 0;
-        if ($this->havePermission(Event::PERMISSION_READABLE,
-                $this->permission_user_id)) {
-            if ($this->event->category_intern) {
-                $category = $this->event->category_intern;
-            }
-
-            if ($category == 0 && trim($this->event->categories)) {
-                $categories = [];
-                $i = 1;
-                foreach ($PERS_TERMIN_KAT as $pers_cat) {
-                    $categories[mb_strtolower($pers_cat['name'])] = $i++;
-                }
-                $cat_event = explode(',', $this->event->categories);
-                foreach ($cat_event as $cat) {
-                    $index = mb_strtolower(trim($cat));
-                    if ($categories[$index]) {
-                        $category = $categories[$index];
-                        break;
-                    }
-                }
-            }
-        } else {
-            $category = 255;
-        }
-        return $category;
-    }
-
-    /**
-     * Returns a csv list of categories. If no categories are stated or the user
-     * has no permission an empty string will be returned.
-     *
-     * @return string csv list of categories or empty string
-     */
-    public function getUserDefinedCategories()
-    {
-        if ($this->havePermission(Event::PERMISSION_READABLE,
-                $this->permission_user_id)) {
-            return trim((string) $this->event->categories);
-        }
-        return '';
-    }
-
-    /**
-     * Stores user defined categories as a csv list.
-     *
-     * @param array|string $categories An array or csv list of user defined categories.
-     */
-    public function setUserDefinedCategories($categories)
-    {
-        if (!is_array($categories)) {
-            $categories = explode(',', $categories);
-        }
-        $cat_list = implode(',', array_map('trim', $categories));
-        $this->event->categories = $cat_list;
-    }
-
-    /**
-     * Sets the accessibility (class). Possible classes are 'PUBLIC', 'PRIVATE'
-     * and 'CONFIDENTIAL'.
-     * If the given class is unknown, the event gets the class 'PRIVATE'.
-     *
-     * @param string $class The name of the class.
-     */
-    public function setAccessibility($class)
-    {
-        $class = mb_strtoupper($class);
-        if (in_array($class, ['PUBLIC', 'PRIVATE', 'CONFIDENTIAL'])) {
-            $this->event->class = $class;
-        } else {
-            $this->event->class = 'PRIVATE';
-        }
-    }
-
-    /**
-     * Sets the priority. Possible values are
-     * 0: not specified
-     * 1: high
-     * 2: middle
-     * 3: low
-     * Default is 0.
-     *
-     * @param int $priority The priority between 0 and 3.
-     */
-    public function setPriority($priority)
-    {
-        if ($priority >= 0 && $priority < 4)
-        {
-            $this->event->priority = $priority;
-        } else {
-            $this->event->priority = 0;
-        }
-    }
-
-    /**
-     * Returns the user id of the editor.
-     *
-     * @return string User id of the editor
-     */
-    public function getEditorId()
-    {
-        return $this->event->editor_id;
-    }
-
-    /**
-     * Returns whether this event is an all day event.
-     *
-     * @return boolean true if all day event
-     */
-    public function isDayEvent()
-    {
-        return (date('His', $this->getStart()) == '000000' &&
-        (date('His', $this->getEnd()) == '235959'
-        || date('His', $this->getEnd() - 1) == '235959'));
-    }
-
-    /**
-     * Returns the state of accessibility as string.
-     * Possible values:
-     * PUBLIC, PRIVATE, CONFIDENTIAL
-     * The default is CONFIDENTIAL.
-     *
-     * @return string
-     */
-    public function getAccessibility()
-    {
-        if ($this->event->class) {
-            return $this->event->class;
-        }
-        return 'CONFIDENTIAL';
-    }
-
-    /**
-     * Returns an array with options for accessibility depending on the permission
-     * of the given calendar permission.
-     *
-     * @param int $permission The calendar permission
-     * @return array The accessibility options.
-     */
-    public function getAccessibilityOptions($permission)
-    {
-        switch ($permission) {
-            case Calendar::PERMISSION_OWN :
-            case Calendar::PERMISSION_ADMIN :
-                $options = [
-                    // SEMBBS nur private und vertrauliche Termine
-                    'PUBLIC' => _('Öffentlich'),
-                    'PRIVATE' => _('Privat'),
-                    'CONFIDENTIAL' => _('Vertraulich')
-                ];
-                break;
-            case Calendar::PERMISSION_WRITABLE :
-                $options = [
-                    'PRIVATE' => _('Privat'),
-                    'CONFIDENTIAL' => _('Vertraulich')
-                ];
-                break;
-            default :
-                $options = [];
-        }
-        return $options;
-    }
-
-    /**
-     *
-     * @return type
-     */
-    public function getChangeDate()
-    {
-        return $this->event->chdate;
-    }
-
-    /**
-     *
-     */
-    public function getImportDate()
-    {
-        return $this->event->importdate;
-    }
-
-
-    /**
-     * Returns the object type this event belongs to.
-     * Possible values are 'user', 'sem', 'inst', 'fak'.
-     *
-     * @return string The object type.
-     */
-    public function getType()
-    {
-        return get_object_type($this->range_id, ['user', 'sem', 'inst', 'fak']);
-    }
-
-    /**
-     * Returns the priority:
-     * 0 means priority is not stated
-     * 1 means "high"
-     * 2 means "middle"
-     * 3 means "low"
-     * If the user has no permission it returns 0.
-     *
-     * @return int The priority.
-     */
-    public function getPriority()
-    {
-        if ($this->havePermission(Event::PERMISSION_READABLE,
-                $this->permission_user_id)) {
-            return $this->event->priority ?: 0;
-        }
-        return 0;
-    }
-
-    /**
-     * @return string
-     */
-    public function getName()
-    {
-        switch ($this->type) {
-            case 'user':
-                return (string) $this->user->getFullname();
-            case 'sem':
-                return (string) $this->course->name;
-            case 'inst':
-            case 'fak':
-                return (string) $this->institute->name;
-            default:
-                return '';
-        }
-    }
-
-    /**
-     * Returns all properties of this event.
-     * The name of the properties correspond to the properties of the
-     * iCalendar calendar data exchange format. There are a few properties with
-     * the suffix STUDIP_ which have no eqivalent in the iCalendar format.
-     *
-     * DTSTART: The start date-time as unix timestamp.
-     * DTEND: The end date-time as unix timestamp.
-     * SUMMARY: The short description (title) that will be displayed in the views.
-     * DESCRIPTION: The long description.
-     * UID: The global unique id of this event.
-     * CLASS:
-     * CATEGORIES: A comma separated list of categories.
-     * PRIORITY: The priority.
-     * LOCATION: The location.
-     * EXDATE: A comma separated list of unix timestamps.
-     * CREATED: The creation date-time as unix timestamp.
-     * LAST-MODIFIED: The date-time of last modification as unix timestamp.
-     * DTSTAMP: The cration date-time of this instance of the event as unix
-     * timestamp.
-     * RRULE: All data for the recurrence rule for this event as array.
-     * EVENT_TYPE:
-     *
-     *
-     * @return array The properties of this event.
-     */
-    public function getProperties()
-    {
-        if ($this->properties === null) {
-            $this->properties = [
-                'DTSTART' => $this->getStart(),
-                'DTEND' => $this->getEnd(),
-                'SUMMARY' => stripslashes($this->getTitle()),
-                'DESCRIPTION' => stripslashes($this->getDescription()),
-                'UID' => $this->getUid(),
-                'CLASS' => $this->getAccessibility(),
-                'CATEGORIES' => $this->toStringCategories(),
-                'STUDIP_CATEGORY' => $this->getCategory(),
-                'PRIORITY' => $this->getPriority(),
-                'LOCATION' => stripslashes($this->getLocation()),
-                'RRULE' => $this->getRecurrence(),
-                'EXDATE' => (string) $this->event->exceptions,
-                'CREATED' => $this->event->mkdate,
-                'LAST-MODIFIED' => $this->event->chdate,
-                'STUDIP_ID' => $this->event->getId(),
-                'DTSTAMP' => time(),
-                'EVENT_TYPE' => 'cal',
-                'STUDIP_AUTHOR_ID' => $this->event->author_id,
-                'STUDIP_EDITOR_ID' => $this->event->editor_id,
-                'STUDIP_GROUP_STATUS' => $this->group_status];
-        }
-        return $this->properties;
-    }
-
-    /**
-     * Returns the value of property with given name.
-     *
-     * @param type $name See CalendarEvent::getProperties() for accepted values.
-     * @return mixed The value of the property.
-     * @throws InvalidArgumentException
-     */
-    public function getProperty($name)
-    {
-        if ($this->properties === null) {
-            $this->getProperties();
-        }
-
-        if (isset($this->properties[$name])) {
-            return $this->properties[$name];
-        }
-        throw new InvalidArgumentException(get_class($this)
-                . ': Property ' . $name . ' does not exist.');
-    }
-
-    /**
-     * Returns all CalendarEvents in the given time range for the given range_id.
-     *
-     * @param string $range_id Id of Stud.IP object from type user, course, inst
-     * @param DateTime $start The start date time.
-     * @param DateTime $end The end date time.
-     * @return SimpleORMapCollection Collection of found CalendarEvents.
-     */
-    public static function getEventsByInterval($range_id, DateTime $start, DateTime $end)
-    {
-        $query = "SELECT *
-                  FROM calendar_event
-                  INNER JOIN event_data USING (event_id)
-                  WHERE range_id = :range_id
-                    AND (
-                        start BETWEEN :start AND :end
-                        OR (
-                            start <= :end
-                            AND CAST(expire AS SIGNED) + CAST(end AS SIGNED) - CAST(start AS SIGNED) >= :start
-                            AND rtype != 'SINGLE'
-                        )
-                        OR :start BETWEEN start AND end
-                    )
-                  ORDER BY start ASC";
-        $stmt = DBManager::get()->prepare($query);
-        $stmt->execute([
-            ':range_id' => $range_id,
-            ':start'    => $start->getTimestamp(),
-            ':end'      => $end->getTimestamp(),
-        ]);
-        $i = 0;
-        $event_collection = new SimpleORMapCollection();
-        $event_collection->setClassName('Event');
-        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
-            $event_collection[$i] = new CalendarEvent();
-            $event_collection[$i]->setData($row);
-            $event_collection[$i]->setNew(false);
-            $event = new EventData();
-            $event->setData($row);
-            $event->setNew(false);
-            $event_collection[$i]->event = $event;
-            $i++;
-        }
-        return $event_collection;
-    }
-
-    /**
-     * Sets the user_id to check his permission.
-     *
-     * @param string $user_id The id of the user.
-     */
-    public function setPermissionUser($user_id)
-    {
-        $this->permission_user_id = $user_id;
-    }
-
-    /**
-     * Checks the permission of the user previously set with
-     * CalendarEvent::setPermissisonUser or given by second argument.
-     * Returns true if the user have the at least the given permission.
-     *
-     * @param int $permission
-     * @param string $user_id
-     * @return boolean
-     */
-    public function havePermission($permission, $user_id = null)
-    {
-        $perm = $this->getPermission($user_id);
-        return $perm >= $permission;
-    }
-
-    /**
-     * Returns the permission of the given user or the user set by
-     * CalendarEvent::setPermssionUser previously.
-     *
-     * @staticvar array $permissions
-     * @param string $user_id The user's id.
-     * @return int The permission.
-     */
-    public function getPermission($user_id = null)
-    {
-        static $permissions = [];
-
-        if (is_null($user_id)) {
-            $user_id = $this->permission_user_id ?: $GLOBALS['user']->id;
-        }
-        if (empty($permissions[$user_id][$this->event_id])) {
-            if ($user_id == $this->event->author_id) {
-                $permissions[$user_id][$this->event_id] = Event::PERMISSION_OWN;
-            } else
-
-            // SEMBBS
-            // Admins dürfen alle Termine löschen
-            /*
-            if ($GLOBALS['perm']->have_perm('admin')) {
-                $permissions[$user_id][$this->event_id] = Event::PERMISSION_DELETABLE;
-            } else
-             *
-             */
-
-            if ($user_id == $this->range_id) {
-                if ($this->group_status) {
-                    $permissions[$user_id][$this->event_id] = Event::PERMISSION_READABLE;
-                } else {
-                    $permissions[$user_id][$this->event_id] = Event::PERMISSION_DELETABLE;
-                }
-            } else {
-                switch ($this->getType()) {
-                    case 'user':
-                        $permissions[$user_id][$this->event_id] =
-                            $this->getUserCalendarPermission($user_id);
-                        break;
-                    case 'sem':
-                        $permissions[$user_id][$this->event_id] =
-                            $this->getCourseCalendarPermission($user_id);
-                        break;
-                    case 'inst':
-                    case 'fak':
-                        $permissions[$user_id][$this->event_id] =
-                            $this->getInstituteCalendarPermission($user_id);
-                        break;
-                    default:
-                        $permissions[$user_id][$this->event_id] =
-                            Event::PERMISSION_FORBIDDEN;
-                }
-            }
-        }
-        return $permissions[$user_id][$this->event_id];
-    }
-
-    /**
-     * Get the user's permission for this event in the actual calendar.
-     *
-     * @param string $user_id The user id.
-     * @return int The permission.
-     */
-    private function getUserCalendarPermission($user_id)
-    {
-        $permission = Event::PERMISSION_FORBIDDEN;
-        $accessibility = $this->getAccessibility();
-        if ($this->user->id) {
-            if ($user_id != $this->user->id) {
-                if ($accessibility == 'PUBLIC') {
-                    $permission = Event::PERMISSION_READABLE;
-                }
-                $calendar_user = CalendarUser::find(
-                        [$this->user->getId(), $user_id]);
-                if ($calendar_user) {
-                    if ($accessibility == 'CONFIDENTIAL') {
-                        if ($this->event->calendars->findOneBy('range_id', $user_id)) {
-                            if ($calendar_user->permission == Calendar::PERMISSION_WRITABLE) {
-                                $permission = Event::PERMISSION_WRITABLE;
-                            } else {
-                                $permission = Event::PERMISSION_READABLE;
-                            }
-                        } else {
-                            $permission = Event::PERMISSION_CONFIDENTIAL;
-                        }
-                    } else {
-                        if ($calendar_user->permission == Calendar::PERMISSION_WRITABLE) {
-                            $permission = Event::PERMISSION_WRITABLE;
-                        } else {
-                            $permission = Event::PERMISSION_READABLE;
-                        }
-                    }
-                }
-            } else {
-                $permission = Event::PERMISSION_WRITABLE;
-            }
-        }
-        return $permission;
-    }
-
-    /**
-     * Get the user's permission for this event in the actual calendar if the
-     * owner is a course.
-     *
-     * @param string $user_id The user's id.
-     * @return int The permission.
-     */
-    private function getCourseCalendarPermission($user_id)
-    {
-        global $perm;
-
-        $permission = Event::PERMISSION_FORBIDDEN;
-        if ($this->course->id) {
-            $course_perm = $perm->get_studip_perm($this->course->id, $user_id);
-            switch ($course_perm) {
-                case 'user':
-                case 'autor':
-                    $permission = Event::PERMISSION_READABLE;
-                    break;
-                case 'tutor':
-                case 'dozent':
-                case 'admin':
-                    $permission = Event::PERMISSION_WRITABLE;
-                    break;
-                default:
-                    $permission = Event::PERMISSION_FORBIDDEN;
-            }
-        }
-        return $permission;
-    }
-
-    /**
-     * Get the user's permission for this event in the actual calendar if the
-     * owner is an institute.
-     *
-     * @param string $user_id The user's id.
-     * @return int The permssion.
-     */
-    private function getInstituteCalendarPermission($user_id)
-    {
-        global $perm;
-        $permission = Event::PERMISSION_FORBIDDEN;
-        if ($this->institute->id) {
-            $institute_perm = $perm->get_studip_perm($this->institute->id, $user_id);
-            switch ($institute_perm) {
-                case 'user';
-                case 'autor':
-                    $permission = Event::PERMISSION_READABLE;
-                    break;
-                case 'tutor':
-                case 'dozent':
-                case 'admin':
-                    $permission = Event::PERMISSION_WRITABLE;
-                    break;
-                default:
-                    $permission = Event::PERMISSION_FORBIDDEN;
-            }
-        }
-        return $permission;
-    }
-
-    /**
-     * Returns the user id of the event's author.
-     *
-     * @return string The user id of the author.
-     */
-    public function getAuthor()
-    {
-        return $this->event->author;
-    }
-
-    /**
-     * Returns teh user id of the event's last editor.
-     *
-     * @return string The uder id og the editor.
-     */
-    public function getEditor()
-    {
-        return $this->event->editor;
-    }
-
-    /**
-     * Export available data of a given user into a storage object
-     * (an instance of the StoredUserData class) for that user.
-     *
-     * @param StoredUserData $storage object to store data into
-     */
-    public static function exportUserData(StoredUserData $storage)
-    {
-        $sorm = CalendarEvent::findBySQL("range_id = ?", [$storage->user_id]);
-        if ($sorm) {
-            $field_data = [];
-            foreach ($sorm as $row) {
-                $field_data[] = $row->toRawArray();
-            }
-            if ($field_data) {
-                $storage->addTabularData(_('Kalender'), 'calendar_event', $field_data);
-            }
-        }
-    }
-
-}
diff --git a/lib/models/CalendarUser.class.php b/lib/models/CalendarUser.class.php
deleted file mode 100644
index f079ac67c881e4f79a6d626b74d82bf712c93df2..0000000000000000000000000000000000000000
--- a/lib/models/CalendarUser.class.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-/**
- * CalendarUser.class.php - Model for users with access to other users calendar.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @since       3.2
- *
- * @property array $id alias for pk
- * @property string $owner_id database column
- * @property string $user_id database column
- * @property int $permission database column
- * @property int $mkdate database column
- * @property int $chdate database column
- * @property User $user belongs_to User
- * @property User $owner has_one User
- * @property-read mixed $nachname additional field
- * @property-read mixed $vorname additional field
- */
-
-class CalendarUser extends SimpleORMap
-{
-    protected static function configure($config = [])
-    {
-        $config['db_table'] = 'calendar_user';
-
-        $config['has_one']['owner'] = [
-            'class_name' => User::class,
-            'foreign_key' => 'owner_id',
-            'assoc_foreign_key' => 'user_id'
-        ];
-        $config['belongs_to']['user'] = [
-            'class_name' => User::class,
-            'foreign_key' => 'user_id'
-        ];
-
-        $config['additional_fields']['nachname']['get'] = function ($cu) {
-            return $cu->user->nachname;
-        };
-        $config['additional_fields']['vorname']['get'] = function ($cu) {
-            return $cu->user->vorname;
-        };
-
-        parent::configure($config);
-    }
-
-    public function setPerm($permission)
-    {
-        if ($permission == Calendar::PERMISSION_READABLE) {
-            $this->permission = Calendar::PERMISSION_READABLE;
-        } else if ($permission == Calendar::PERMISSION_WRITABLE) {
-            $this->permission = Calendar::PERMISSION_WRITABLE;
-        } else {
-            throw new InvalidArgumentException(
-                'Calendar permission must be of type PERMISSION_READABLE or PERMISSION_WRITABLE.');
-        }
-    }
-
-    public static function getUsers($user_id, $permission = null)
-    {
-        $permission_array = [Calendar::PERMISSION_READABLE,
-                Calendar::PERMISSION_WRITABLE];
-        if (!$permission) {
-            $permission = $permission_array;
-        } else if (!in_array($permission, $permission_array)) {
-            throw new InvalidArgumentException(
-                'Calendar permission must be of type PERMISSION_READABLE or PERMISSION_WRITABLE.');
-        } else {
-            $permission = [$permission];
-        }
-        return SimpleORMapCollection::createFromArray(CalendarUser::findBySQL(
-                'owner_id = ? AND permission IN(?)',
-                [$user_id, $permission]));
-
-    }
-
-    public static function getOwners($user_id, $permission = null)
-    {
-        $permission_array = [Calendar::PERMISSION_READABLE,
-                Calendar::PERMISSION_WRITABLE];
-        if (!$permission) {
-            $permission = $permission_array;
-        } else if (!in_array($permission, $permission_array)) {
-            throw new InvalidArgumentException(
-                'Calendar permission must be of type PERMISSION_READABLE or PERMISSION_WRITABLE.');
-        } else {
-            $permission = [$permission];
-        }
-        $statement = DBManager::get()->prepare("
-            SELECT *
-            FROM calendar_user
-                INNER JOIN auth_user_md5 ON (auth_user_md5.user_id = calendar_user.owner_id)
-            WHERE calendar_user.user_id = :user_id
-                AND calendar_user.permission IN (:permission)
-            ORDER BY auth_user_md5.Nachname, auth_user_md5.Vorname
-        ");
-        $statement->execute([
-            'user_id' => $user_id,
-            'permission' => $permission
-        ]);
-        $calendar_users = [];
-        foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $data) {
-            $calendar_users[] = CalendarUser::buildExisting($data);
-        }
-        return SimpleORMapCollection::createFromArray($calendar_users);
-
-    }
-}
diff --git a/lib/models/ConsultationBooking.php b/lib/models/ConsultationBooking.php
index 06e6967c2a52a972afb98bd659a5fb3ef2823e8d..7782681fa45797208533089a43e2fedbfc9f94f7 100644
--- a/lib/models/ConsultationBooking.php
+++ b/lib/models/ConsultationBooking.php
@@ -16,7 +16,7 @@
  * @property int $chdate database column
  * @property ConsultationSlot $slot belongs_to ConsultationSlot
  * @property User $user belongs_to User
- * @property EventData|null $event has_one EventData
+ * @property CalendarDate $event has_one CalendarDate
  */
 class ConsultationBooking extends SimpleORMap implements PrivacyObject
 {
@@ -37,9 +37,9 @@ class ConsultationBooking extends SimpleORMap implements PrivacyObject
             'foreign_key' => 'user_id',
         ];
         $config['has_one']['event'] = [
-            'class_name'        => EventData::class,
+            'class_name'        => CalendarDate::class,
             'foreign_key'       => 'student_event_id',
-            'assoc_foreign_key' => 'event_id',
+            'assoc_foreign_key' => 'id',
             'on_delete'         => 'delete',
         ];
 
@@ -48,8 +48,8 @@ class ConsultationBooking extends SimpleORMap implements PrivacyObject
             setTempLanguage($booking->user_id);
 
             $event = $booking->slot->createEvent($booking->user);
-            $event->category_intern = 1;
-            $event->summary = sprintf(
+            $event->category = 1;
+            $event->title = sprintf(
                 _('Termin bei %s'),
                 $booking->slot->block->range->getFullName()
             );
diff --git a/lib/models/ConsultationEvent.php b/lib/models/ConsultationEvent.php
index 2665c6be1ba1dd19b004ec5c0ad6e134cff2fd9b..e0b6b5962cddb3a58ba8e8bba2d98e88db63d190 100644
--- a/lib/models/ConsultationEvent.php
+++ b/lib/models/ConsultationEvent.php
@@ -10,7 +10,7 @@
  * @property string $event_id database column
  * @property int $mkdate database column
  * @property ConsultationSlot $slot belongs_to ConsultationSlot
- * @property EventData $event has_one EventData
+ * @property CalendarDate $event belongs_to CalendarDate
  */
 class ConsultationEvent extends SimpleORMap
 {
@@ -23,9 +23,9 @@ class ConsultationEvent extends SimpleORMap
             'foreign_key' => 'slot_id',
         ];
         $config['has_one']['event'] = [
-            'class_name'        => EventData::class,
+            'class_name'        => CalendarDate::class,
             'foreign_key'       => 'event_id',
-            'assoc_foreign_key' => 'event_id',
+            'assoc_foreign_key' => 'id',
             'on_delete'         => 'delete',
         ];
 
diff --git a/lib/models/ConsultationSlot.php b/lib/models/ConsultationSlot.php
index b9881900b18b930117fa5425af98a88e9a9f7ee3..2a71fbd3a0d8beeb3a38b9e2cb1f3845dccf01b9 100644
--- a/lib/models/ConsultationSlot.php
+++ b/lib/models/ConsultationSlot.php
@@ -166,26 +166,24 @@ class ConsultationSlot extends SimpleORMap
      * Creates a Stud.IP calendar event relating to the slot.
      *
      * @param  User $user User object to create the event for
-     * @return EventData Created event
+     * @return CalendarDate Created event
      */
-    public function createEvent(User $user)
+    public function createEvent(User $user) : CalendarDate
     {
-        $event = new EventData();
-        $event->uid = $this->createEventId($user);
+        $event = new CalendarDate();
+        $event->unique_id = $this->createEventId($user);
         $event->author_id = $user->id;
         $event->editor_id = $user->id;
-        $event->start     = $this->start_time;
+        $event->begin     = $this->start_time;
         $event->end       = $this->end_time;
-        $event->class     = 'PRIVATE';
-        $event->priority  = 0;
+        $event->access    = 'PRIVATE';
         $event->location  = $this->block->room;
-        $event->rtype     = 'SINGLE';
+        $event->repetition_type = '';
         $event->store();
 
-        $calendar_event = new CalendarEvent();
-        $calendar_event->range_id     = $user->id;
-        $calendar_event->group_status = 0;
-        $calendar_event->event_id     = $event->id;
+        $calendar_event = new CalendarDateAssignment();
+        $calendar_event->range_id         = $user->id;
+        $calendar_event->calendar_date_id = $event->id;
         $calendar_event->store();
 
         return $event;
@@ -267,12 +265,12 @@ class ConsultationSlot extends SimpleORMap
             });
 
             if (count($bookings) > 0) {
-                $event->event->category_intern = 1;
+                $event->event->category = 1;
 
                 if (count($bookings) === 1) {
                     $booking = $bookings->first();
 
-                    $event->event->summary = sprintf(
+                    $event->event->title = sprintf(
                         _('Termin mit %s'),
                         $booking->user ? $booking->user->getFullName() : _('unbekannt')
                     );
@@ -288,9 +286,9 @@ class ConsultationSlot extends SimpleORMap
                     }));
                 }
             } else {
-                $event->event->category_intern = 9;
-                $event->event->summary         = _('Freier Termin');
-                $event->event->description     = _('Dieser Termin ist noch nicht belegt.');
+                $event->event->category    = 9;
+                $event->event->title       = _('Freier Termin');
+                $event->event->description = _('Dieser Termin ist noch nicht belegt.');
             }
 
             $event->event->store();
diff --git a/lib/models/Contact.class.php b/lib/models/Contact.class.php
index be095de1f12dda49c7ff31fae7fb32923272fdb5..44d8bcdd651517ff6a99ada3fa0d0ef66a84a4fb 100644
--- a/lib/models/Contact.class.php
+++ b/lib/models/Contact.class.php
@@ -10,9 +10,14 @@
  * @property string $owner_id database column
  * @property string $user_id database column
  * @property int|null $mkdate database column
+ * @property string $calendar_permissions database column
+ *     An enum with the possible values "", "READ" and "WRITE".
+ *     The empty string specifies that no calendar permissions are granted.
  * @property SimpleORMapCollection|StatusgruppeUser[] $group_assignments has_many StatusgruppeUser
  * @property User $owner belongs_to User
  * @property User $friend belongs_to User
+ * @property string $mkdate database column
+ * @property string $chdate database column
  */
 class Contact extends SimpleORMap
 {
@@ -29,15 +34,16 @@ class Contact extends SimpleORMap
             'foreign_key' => 'user_id'
         ];
 
-        $config['has_many']['group_assignments'] = [
-            'class_name'        => 'StatusgruppeUser',
+        $config['has_many']['groups'] = [
+            'class_name'        => ContactGroupItem::class,
             'assoc_func'        => 'findByContact',
             'foreign_key'       => function ($me) {
                 return [$me];
             },
-            'assoc_foreign_key' => function ($group, $params) {
-                $group->setValue('user_id', $params[0]->user_id);
-            },
+            'assoc_foreign_key' => function ($item, $params) {
+                //Nothing else here. But this has to be present
+                //so that storing a new contact works.
+             },
             'on_store'          => 'store',
             'on_delete'         => 'delete'
         ];
diff --git a/lib/models/ContactGroup.class.php b/lib/models/ContactGroup.class.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e60d3b5a5ca6185ed4377194c0b2ef5256fd3ad
--- /dev/null
+++ b/lib/models/ContactGroup.class.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * The ContactGroup class represents a contact group of a user.
+ * 
+ * This file is part of Stud.IP
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Moritz Strohm <strohm@data-quest.de>
+ * @copyright   2023
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ * @package     resources
+ * @since       5.5
+ *
+ * @property string $id The ID of the group.
+ * @property string $name Name of the group.
+ * @property string $owner_id The ID of the owner to whom the group belongs to.
+ * @property string $mkdate The creation date of the group.
+ * @property string $chdate The modification date of the group.
+ * @property User $owner The owner of the group.
+ * @property ContactGroupItem[]|SimpleORMapCollection $items The items (users) that belong to the group.
+ */
+class ContactGroup extends SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'contact_groups';
+        $config['belongs_to']['owner'] = [
+            'class_name'  => User::class,
+            'foreign_key' => 'owner_id'
+        ];
+        $config['has_many']['items'] = [
+            'class_name'        => ContactGroupItem::class,
+            'assoc_foreign_key' => 'group_id',
+            'on_store'          => 'store',
+            'on_delete'         => 'delete'
+        ];
+        parent::configure($config);
+    }
+}
diff --git a/lib/models/ContactGroupItem.class.php b/lib/models/ContactGroupItem.class.php
new file mode 100644
index 0000000000000000000000000000000000000000..0204d0a5a6578a1ba5407b10afeaa516f10595f4
--- /dev/null
+++ b/lib/models/ContactGroupItem.class.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * The ContactGroupItem class represents an item in a contact group.
+ * 
+ * This file is part of Stud.IP
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Moritz Strohm <strohm@data-quest.de>
+ * @copyright   2023
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ * @package     resources
+ * @since       5.5
+ *
+ * @property string $group_id The ID of the group.
+ * @property string $user_id The ID of the user that is inside the group.
+ * @property string $mkdate The creation date of the group.
+ * @property string $chdate The modification date of the group.
+ * @property ContactGroup $contact_group The group instance for the item.
+ * @property User $user The user instance for the item.
+ */
+class ContactGroupItem extends SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'contact_group_items';
+        $config['belongs_to']['contact_group'] = [
+            'class_name'  => ContactGroup::class,
+            'foreign_key' => 'group_id'
+        ];
+        $config['belongs_to']['user'] = [
+            'class_name'  => User::class,
+            'foreign_key' => 'user_id'
+        ];
+        parent::configure($config);
+    }
+
+    /**
+     * Finds and returns all group items for a contact.
+     *
+     * @param Contact $contact The contact for which to find all contact group items.
+     * @return ContactGroupItem[] All memberships of the contact.
+     */
+    public static function findByContact(Contact $contact): array
+    {
+        return self::findBySQL(
+            'JOIN `contact_groups`
+              ON (`contact_group_items`.`group_id` = `contact_groups`.`id`)
+             WHERE `contact_groups`.`owner_id` = :owner_id
+               AND `contact_group_items`.`user_id` = :user_id',
+            [
+                'owner_id' => $contact->owner_id,
+                'user_id' => $contact->user_id
+            ]
+        );
+    }
+}
diff --git a/lib/models/Course.class.php b/lib/models/Course.class.php
index ab582196a4d32faeeb36475b57f5bf83d5a22247..75dad332fe6fc52e6bad37cd1c7e7500e4f5e939 100644
--- a/lib/models/Course.class.php
+++ b/lib/models/Course.class.php
@@ -80,7 +80,7 @@
  * @property-read mixed $config additional field
  */
 
-class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, FeedbackRange
+class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, FeedbackRange, Studip\Calendar\Owner
 {
     protected static function configure($config = [])
     {
@@ -1081,4 +1081,40 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
     {
         return $this->getFullName();
     }
+
+    /**
+     * @inheritDoc
+     */
+    public static function getCalendarOwner(string $owner_id): ?\Studip\Calendar\Owner
+    {
+        return self::find($owner_id);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function isCalendarReadable(?string $user_id = null): bool
+    {
+        if ($user_id === null) {
+            $user_id = self::findCurrent()->id;
+        }
+
+        //Calendar read permissions are granted for all participants
+        //that have at least user permissions.
+        return $GLOBALS['perm']->have_studip_perm('user', $this->id, $user_id);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function isCalendarWritable(string $user_id = null): bool
+    {
+        if ($user_id === null) {
+            $user_id = self::findCurrent()->id;
+        }
+
+        //Calendar write permissions are granted for all participants
+        //that have autor permissions or higher.
+        return $GLOBALS['perm']->have_studip_perm('autor', $this->id, $user_id);
+    }
 }
diff --git a/lib/models/CourseCancelledEvent.class.php b/lib/models/CourseCancelledEvent.class.php
deleted file mode 100644
index 5fed9c53989651b85fa19fca9d27b94aab30adc3..0000000000000000000000000000000000000000
--- a/lib/models/CourseCancelledEvent.class.php
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-/**
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @copyright   2014 Stud.IP Core-Group
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- *
- * @property string $id alias for pk
- * @property string $termin_id database column
- * @property string $event_id alias column for termin_id
- * @property string $range_id database column
- * @property string $sem_id alias column for range_id
- * @property string $autor_id database column
- * @property string $author_id alias column for autor_id
- * @property string $content database column
- * @property string $ex_description alias column for content
- * @property int $date database column
- * @property int $start alias column for date
- * @property int $end_time database column
- * @property int $end alias column for end_time
- * @property int $mkdate database column
- * @property int $chdate database column
- * @property int $date_typ database column
- * @property int $category_intern alias column for date_typ
- * @property string|null $raum database column
- * @property string|null $metadate_id database column
- * @property string $resource_id database column
- * @property SimpleORMapCollection|Folder[] $folders has_many Folder
- * @property SimpleORMapCollection|RoomRequest[] $room_requests has_many RoomRequest
- * @property SimpleORMapCollection|ResourceRequestAppointment[] $resource_request_appointments has_many ResourceRequestAppointment
- * @property User $author belongs_to User
- * @property Course $course belongs_to Course
- * @property SeminarCycleDate|null $cycle belongs_to SeminarCycleDate
- * @property ResourceBooking $room_booking has_one ResourceBooking
- * @property SimpleORMapCollection|CourseTopic[] $topics has_and_belongs_to_many CourseTopic
- * @property SimpleORMapCollection|Statusgruppen[] $statusgruppen has_and_belongs_to_many Statusgruppen
- * @property SimpleORMapCollection|User[] $dozenten has_and_belongs_to_many User
- * @property-read mixed $location additional field
- * @property mixed $type additional field
- * @property-read mixed $name additional field
- * @property-read mixed $title additional field
- * @property-read mixed $editor_id additional field
- * @property-read mixed $uid additional field
- * @property-read mixed $summary additional field
- * @property-read mixed $description additional field
- */
-
-class CourseCancelledEvent extends CourseEvent
-{
-
-    protected static function configure($config = [])
-    {
-        $config['alias_fields']['ex_description'] = 'content';
-
-        if (!self::TableScheme('ex_termine')) {
-            throw new Exception('Cannot obtain table meta data for table "ex_termine"');
-        }
-
-        $config['db_fields'] = self::$schemes['ex_termine']['db_fields'];
-        $config['pk'] = self::$schemes['ex_termine']['pk'];
-
-        parent::configure($config);
-        self::$config['CourseCancelledEvent']['db_table'] = 'ex_termine';
-    }
-
-    /**
-     * Returns all CourseCancelledEvents in the given time range for the given range_id.
-     *
-     * @param string $user_id Id of Stud.IP object from type user, course, inst
-     * @param DateTime $start The start date time.
-     * @param DateTime $end The end date time.
-     * @return SimpleORMapCollection Collection of found CourseCancelledEvents.
-     */
-    public static function getEventsByInterval($user_id, DateTime $start, dateTime $end)
-    {
-        $stmt = DBManager::get()->prepare('SELECT ex_termine.* FROM seminar_user '
-                . 'INNER JOIN ex_termine ON seminar_id = range_id '
-                . 'WHERE ex_termine.content <> \'\' AND user_id = :user_id '
-                . 'AND date BETWEEN :start AND :end '
-                . "AND (IFNULL(metadate_id, '') = '' "
-                . 'OR metadate_id NOT IN ( '
-                . 'SELECT metadate_id FROM schedule_seminare '
-                . 'WHERE user_id = :user_id AND visible = 0) ) '
-                . 'ORDER BY date ASC');
-        $stmt->execute([
-            ':user_id' => $user_id,
-            ':start'   => $start->getTimestamp(),
-            ':end'     => $end->getTimestamp()
-        ]);
-        $event_collection = [];
-        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
-            $event = new CourseCancelledEvent();
-            $event->setData($row);
-            $event->setNew(false);
-            // related persons (dozenten) or groups
-            if (self::checkRelated($event, $user_id)) {
-                $event_collection[] = $event;
-            }
-        }
-        $event_collection = SimpleORMapCollection::createFromArray($event_collection, false);
-        $event_collection->setClassName('Event');
-        return $event_collection;
-    }
-
-    /**
-     * Returns the title of this event.
-     * The title of a course event is the name of the course or if a topic is
-     * assigned, the title of this topic. If the user has not the permission
-     * Event::PERMISSION_READABLE, the title is "Keine Berechtigung.".
-     *
-     * @return string
-     */
-    public function getTitle()
-    {
-        $title = parent::getTitle();
-        if ($this->havePermission(Event::PERMISSION_READABLE)) {
-            $title .= ' ' . _('(fällt aus)');
-        }
-        return $title;
-    }
-
-    /**
-     * Returns the index of the category.
-     * If the user has no permission, 255 is returned.
-     *
-     * TODO remove? use getStudipCategory instead?
-     *
-     * @see config/config.inc.php $TERMIN_TYP
-     * @return int The index of the category
-     */
-    public function getCategory()
-    {
-        return 255;
-    }
-
-    public function getDescription()
-    {
-        if ($this->havePermission(Event::PERMISSION_READABLE)) {
-            return $this->ex_description;
-        }
-        return '';
-    }
-
-}
diff --git a/lib/models/CourseDate.class.php b/lib/models/CourseDate.class.php
index 5ba3534df03a738b387c6b2136a5de846ef401d5..2a5b77e848a968425757a24ed75352933117fd42 100644
--- a/lib/models/CourseDate.class.php
+++ b/lib/models/CourseDate.class.php
@@ -34,7 +34,7 @@
  * @property SimpleORMapCollection|User[] $dozenten has_and_belongs_to_many User
  */
 
-class CourseDate extends SimpleORMap implements PrivacyObject
+class CourseDate extends SimpleORMap implements PrivacyObject, Event
 {
     const FORMAT_DEFAULT = 'default';
     const FORMAT_VERBOSE = 'verbose';
@@ -476,4 +476,190 @@ class CourseDate extends SimpleORMap implements PrivacyObject
             date('H:i', $this->end_time)
         );
     }
+
+    //Start of Event interface implementation.
+
+    public static function getEvents(DateTime $begin, DateTime $end, string $range_id): array
+    {
+        return self::findBySQL(
+            "JOIN `seminar_user`
+               ON `seminar_user`.`seminar_id` = `termine`.`range_id`
+             WHERE `seminar_user`.`user_id` = :user_id
+               AND `termine`.`date` BETWEEN :begin AND :end
+               AND (
+                   IFNULL(`termine`.`metadate_id`, '') = ''
+                   OR `termine`.`metadate_id` NOT IN (
+                       SELECT `metadate_id`
+                       FROM `schedule_seminare`
+                       WHERE `user_id` = :user_id
+                         AND `visible` = 0
+                 )
+             )
+             ORDER BY date",
+            [
+                'begin'   => $begin->getTimestamp(),
+                'end'     => $end->getTimestamp(),
+                'user_id' => $range_id
+            ]
+        );
+    }
+
+    //Event interface implementation:
+
+    public function getObjectId() : string
+    {
+        return (string) $this->id;
+    }
+
+    public function getPrimaryObjectID(): string
+    {
+        return $this->range_id;
+    }
+
+    public function getObjectClass(): string
+    {
+        return static::class;
+    }
+
+    public function getTitle(): string
+    {
+        return $this->course->name ?? '';
+    }
+
+    public function getBegin(): DateTime
+    {
+        $begin = new DateTime();
+        $begin->setTimestamp($this->date);
+        return $begin;
+    }
+
+    public function getEnd(): DateTime
+    {
+        $end = new DateTime();
+        $end->setTimestamp($this->end_time);
+        return $end;
+    }
+
+    public function getDuration(): DateInterval
+    {
+        $begin = $this->getBegin();
+        $end = $this->getEnd();
+        return $end->diff($begin);
+    }
+
+    public function getLocation(): string
+    {
+        return $this->raum ?? '';
+    }
+
+    public function getUniqueId(): string
+    {
+        return sprintf('Stud.IP-SEM-%1$s@%2$s', $this->id, $_SERVER['SERVER_NAME']);
+    }
+
+    public function getDescription(): string
+    {
+        $descriptions = $this->topics->map(function ($topic) {
+            $desc = $topic->title . "\n";
+            $desc .= $topic->description;
+
+            return $desc;
+        });
+        return implode("\n\n", $descriptions);
+    }
+
+    public function getAdditionalDescriptions(): array
+    {
+        $descriptions = [];
+        if (count($this->dozenten) > 0) {
+            $descriptions[_('Durchführende Lehrende')] = implode(', ', $this->dozenten->getFullname());
+        }
+        if (count($this->statusgruppen) > 0) {
+            $descriptions[_('Beteiligte Gruppen')] = implode(', ', $this->statusgruppen->getValue('name'));
+        }
+        return $descriptions;
+    }
+
+    public function isAllDayEvent(): bool
+    {
+        //Course dates are never all day events.
+        return false;
+    }
+
+    public function isWritable(string $user_id): bool
+    {
+        return $GLOBALS['perm']->have_studip_perm('dozent', $this->range_id, $user_id);
+    }
+
+    public function getCreationDate(): DateTime
+    {
+        $mkdate = new DateTime();
+        $mkdate->setTimestamp($this->mkdate);
+        return $mkdate;
+    }
+
+    public function getModificationDate(): DateTime
+    {
+        $chdate = new DateTime();
+        $chdate->setTimestamp($this->chdate);
+        return $chdate;
+    }
+
+    public function getImportDate(): DateTime
+    {
+        return $this->getCreationDate();
+    }
+
+    public function getAuthor(): ?User
+    {
+        return $this->author;
+    }
+
+    public function getEditor(): ?User
+    {
+        return null;
+    }
+
+    public function toEventData(string $user_id): \Studip\Calendar\EventData
+    {
+        $begin = new DateTime();
+        $begin->setTimestamp($this->date);
+        $end = new DateTime();
+        $end->setTimestamp($this->end_time);
+
+        $membership = CourseMember::findOneBySQL(
+            'seminar_id = :course_id AND user_id = :user_id',
+            ['course_id' => $this->range_id, 'user_id' => $user_id]
+        );
+        $class_names = [];
+        if ($membership) {
+            $class_names[] = sprintf('gruppe%u', $membership->status);
+        }
+        $studip_view_urls = [];
+        if ($GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user_id)) {
+            $studip_view_urls['show'] = URLHelper::getURL('dispatch.php/course/dates/details/' . $this->id, ['cid' => $this->range_id, 'extra_buttons' => '1']);
+        }
+
+        return new \Studip\Calendar\EventData(
+            $begin,
+            $end,
+            $this->getTitle(),
+            $class_names,
+            '#000000',
+            '#aaaaaa',
+            $this->isWritable($user_id),
+            CourseDate::class,
+            $this->id,
+            Course::class,
+            $this->range_id,
+            'course',
+            $this->range_id,
+            $studip_view_urls,
+            [],
+            'seminar',
+            'rgba(0,0,0,0)'
+        );
+    }
+
+    //End of Event interface implementation.
 }
diff --git a/lib/models/CourseEvent.class.php b/lib/models/CourseEvent.class.php
deleted file mode 100644
index fa68b136a1007d9edd891ef0717c78da56d970fb..0000000000000000000000000000000000000000
--- a/lib/models/CourseEvent.class.php
+++ /dev/null
@@ -1,596 +0,0 @@
-<?php
-/**
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @copyright   2014 Stud.IP Core-Group
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- *
- * @property string $id alias for pk
- * @property string $termin_id database column
- * @property string $event_id alias column for termin_id
- * @property string $range_id database column
- * @property string $sem_id alias column for range_id
- * @property string $autor_id database column
- * @property string $author_id alias column for autor_id
- * @property string $content database column
- * @property int $date database column
- * @property int $start alias column for date
- * @property int $end_time database column
- * @property int $end alias column for end_time
- * @property int $mkdate database column
- * @property int $chdate database column
- * @property int $date_typ database column
- * @property int $category_intern alias column for date_typ
- * @property string|null $raum database column
- * @property string|null $metadate_id database column
- * @property SimpleORMapCollection|Folder[] $folders has_many Folder
- * @property SimpleORMapCollection|RoomRequest[] $room_requests has_many RoomRequest
- * @property SimpleORMapCollection|ResourceRequestAppointment[] $resource_request_appointments has_many ResourceRequestAppointment
- * @property User $author belongs_to User
- * @property Course $course belongs_to Course
- * @property SeminarCycleDate|null $cycle belongs_to SeminarCycleDate
- * @property ResourceBooking $room_booking has_one ResourceBooking
- * @property SimpleORMapCollection|CourseTopic[] $topics has_and_belongs_to_many CourseTopic
- * @property SimpleORMapCollection|Statusgruppen[] $statusgruppen has_and_belongs_to_many Statusgruppen
- * @property SimpleORMapCollection|User[] $dozenten has_and_belongs_to_many User
- * @property-read mixed $location additional field
- * @property mixed $type additional field
- * @property-read mixed $name additional field
- * @property-read mixed $title additional field
- * @property-read mixed $editor_id additional field
- * @property-read mixed $uid additional field
- * @property-read mixed $summary additional field
- * @property-read mixed $description additional field
- */
-
-class CourseEvent extends CourseDate implements Event
-{
-    protected static function configure($config = [])
-    {
-        $config['alias_fields']['event_id'] = 'termin_id';
-        $config['alias_fields']['start'] = 'date';
-        $config['alias_fields']['end'] = 'end_time';
-        $config['alias_fields']['category_intern'] = 'date_typ';
-        $config['alias_fields']['author_id'] = 'autor_id';
-        $config['alias_fields']['sem_id'] = 'range_id';
-
-        $config['additional_fields']['location']['get'] = 'getRoomName';
-        $config['additional_fields']['type'] = true;
-        $config['additional_fields']['name']['get'] = function ($event) {
-            return $event->course->getFullname();
-        };
-        $config['additional_fields']['title']['get'] = 'getTitle';
-        $config['additional_fields']['editor_id']['get'] = function ($date) {
-            return null;
-        };
-        $config['additional_fields']['uid']['get'] = function ($date) {
-            $host = $_SERVER['SERVER_NAME'] ?? parse_url($GLOBALS['ABSOLUTE_URI_STUDIP'], PHP_URL_HOST);
-            return 'Stud.IP-SEM-' . $date->getId() . '@' . $host;
-        };
-        $config['additional_fields']['summary']['get'] = function ($date) {
-            return $date->course->name;
-        };
-        $config['additional_fields']['description']['get'] = function ($date) {
-            return '';
-        };
-        parent::configure($config);
-    }
-
-    private $properties = null;
-    private $permission_user_id = null;
-
-    public function __construct($id = null)
-    {
-        $this->permission_user_id = $GLOBALS['user']->id;
-        parent::__construct($id);
-    }
-
-    /**
-     * Returns all CourseEvents in the given time range for the given range_id.
-     *
-     * @param string $user_id Id of Stud.IP object from type user, course, inst
-     * @param DateTime $start The start date time.
-     * @param DateTime $end The end date time.
-     * @return SimpleORMapCollection Collection of found CourseEvents.
-     */
-    public static function getEventsByInterval($user_id, DateTime $start, dateTime $end)
-    {
-        $stmt = DBManager::get()->prepare('SELECT termine.* FROM seminar_user '
-                . 'INNER JOIN termine ON seminar_id = range_id '
-                . 'WHERE user_id = :user_id '
-                . 'AND bind_calendar = 1 '
-                . 'AND date BETWEEN :start AND :end '
-                . "AND (IFNULL(metadate_id, '') = '' "
-                . 'OR metadate_id NOT IN ( '
-                . 'SELECT metadate_id FROM schedule_seminare '
-                . 'WHERE user_id = :user_id AND visible = 0) ) '
-                . 'ORDER BY date ASC');
-        $stmt->execute([
-            ':user_id' => $user_id,
-            ':start'   => $start->getTimestamp(),
-            ':end'     => $end->getTimestamp()
-        ]);
-       $event_collection = [];
-       foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
-           $event = new CourseEvent();
-           $event->setData($row);
-           $event->setNew(false);
-           // related persons (dozenten) or groups
-           if (self::checkRelated($event, $user_id)) {
-               $event_collection[] = $event;
-           }
-        }
-        $event_collection = SimpleORMapCollection::createFromArray($event_collection, false);
-        $event_collection->setClassName('Event');
-        return $event_collection;
-    }
-
-    /**
-     * Checks if given user is the responsible lecturer or is member of a
-     * related group
-     *
-     * @global object $perm The globa perm object.
-     * @param CourseEvent $event The course event to check against.
-     * @param string $user_id The id of the user.
-     * @return boolean
-     */
-    protected static function checkRelated(CourseEvent $event, $user_id)
-    {
-        global $perm;
-
-        $check_related = false;
-        $permission = $perm->get_studip_perm($event->range_id, $user_id);
-        switch ($permission) {
-            case 'dozent' :
-                $related_persons = $event->dozenten->pluck('user_id');
-                if (sizeof($related_persons)) {
-                    if (in_array($user_id, $related_persons)) {
-                        $check_related = true;
-                    }
-                } else {
-                    $check_related = true;
-                }
-                break;
-            case 'tutor' :
-                $check_related = true;
-                break;
-            default :
-                $group_ids = $event->statusgruppen->pluck('statusgruppe_id');
-                if (sizeof($group_ids)) {
-                    $member = StatusgruppeUser::findBySQL(
-                            'statusgruppe_id IN(?) AND user_id = ?',
-                            [$group_ids, $user_id]);
-                    $check_related = sizeof($member) > 0;
-                } else {
-                    $check_related = true;
-                }
-        }
-        return $check_related;
-    }
-
-    /**
-     * Returns the name of the category.
-     *
-     * @return array|string the name of the category
-     */
-    public function toStringCategories($as_array = false)
-    {
-        $category = '';
-        if (
-            $this->havePermission(Event::PERMISSION_READABLE)
-            && !empty($GLOBALS['TERMIN_TYP'][$this->getCategory()])
-        ) {
-            $category = $GLOBALS['TERMIN_TYP'][$this->getCategory()]['name'];
-        }
-        return $as_array ? [$category] : $category;
-    }
-
-    /**
-     * Returns the id of the related course
-     *
-     * @return string The id of the related course.
-     */
-    public function getSeminarId()
-    {
-        if ($this->havePermission(Event::PERMISSION_READABLE)) {
-            return $this->sem_id;
-        }
-        return null;
-    }
-
-    /**
-     * Returns an array that represents the recurrence rule for this event.
-     * If an index is given, returns only this field of the rule.
-     *
-     * @return array|string The array with th recurrence rule or only one field.
-     */
-    public function getRecurrence($index = null)
-    {
-        $rep = ['ts' => 0, 'linterval' => 0, 'sinterval' => 0, 'wdays' => '',
-            'month' => 0, 'day' => 0, 'rtype' => 'SINGLE', 'duration' => 1];
-        return $index ? $rep[$index] : $rep;
-    }
-
-    /**
-     * Returns the name of the related course.
-     *
-     * @return string The name of the related course.
-     */
-    public function getSemName()
-    {
-        if ($this->havePermission(Event::PERMISSION_READABLE)) {
-            return $this->course->name;
-        }
-        return '';
-    }
-
-    /**
-     * TODO Wird das noch benötigt?
-     */
-    public function getType()
-    {
-        return 1;
-    }
-
-    /**
-     * Returns the title of this event.
-     * The title of a course event is the name of the course or if a topic is
-     * assigned, the title of this topic. If the user has not the permission
-     * Event::PERMISSION_READABLE, the title is "Keine Berechtigung.".
-     *
-     * @return string
-     */
-    public function getTitle()
-    {
-        $title = _('Keine Berechtigung.');
-        if ($this->havePermission(Event::PERMISSION_READABLE)) {
-            $description = $this->cycle ? trim($this->cycle->description) : '';
-            if (sizeof($this->topics)) {
-                $title = $this->course->name.": ".implode(', ', $this->topics->pluck('title'));
-            } else {
-                $title = $this->course->name;
-            }
-            $title = ($description ? $description . ', ' : '') . $title;
-        }
-        return $title;
-    }
-
-    /**
-     * Returns the starttime as unix timestamp of this event.
-     *
-     * @return int The starttime of this event as a unix timestamp.
-     */
-    public function getStart()
-    {
-        return $this->date;
-    }
-
-    /**
-     * Sets the start date time with given unix timestamp.
-     *
-     * @param string $timestamp Unix timestamp.
-     */
-    public function setStart($timestamp)
-    {
-        $this->date = $timestamp;
-    }
-
-    /**
-     * Returns the endtime of this event.
-     *
-     * @return int The endtime of this event as a unix timestamp.
-     */
-    public function getEnd()
-    {
-        return $this->end_time;
-    }
-
-    /**
-     * Sets the end date time by given unix timestamp.
-     *
-     * @param string $timestamp Unix timestamp.
-     */
-    public function setEnd($timestamp)
-    {
-        $this->end_time = $timestamp;
-    }
-
-    /**
-     * Returns the duration of this event in seconds.
-     *
-     * @return int the duration of this event in seconds
-     */
-    function getDuration()
-    {
-        return $this->end - $this->start;
-    }
-
-    /**
-     * Returns the location.
-     * Without permission or the location is not set an empty string is returned.
-     *
-     * @see ClendarDate::getRoomName()
-     * @return string The location
-     */
-    function getLocation()
-    {
-        $location = '';
-        if ($this->havePermission(Event::PERMISSION_READABLE)) {
-            $location = $this->getRoomName();
-        }
-        return $location;
-    }
-
-    /**
-     * Returns the global unique id of this event.
-     *
-     * @return string The global unique id.
-     */
-    public function getUid()
-    {
-        return $this->uid;
-    }
-
-    /**
-     * Returns the description of the topic.
-     * If the user has no permission or the event has no topic
-     * or the topics have no descritopn an empty string is returned.
-     *
-     * @return String the description
-     */
-    function getDescription()
-    {
-        $description = '';
-        if ($this->havePermission(Event::PERMISSION_READABLE)) {
-            $descriptions = $this->topics->map(function ($topic) {
-                $desc = $topic->title . "\n";
-                $desc .= $topic->description;
-            });
-            $description = implode("\n\n", $descriptions);
-        }
-        return $description;
-    }
-
-    /**
-     * Returns the Stud.IP build in category as integer value.
-     * If the user has no permission, 255 is returned.
-     *
-     * @See config.inc.php $PERS_TERMIN
-     * @return int the categories
-     */
-    public function getStudipCategory()
-    {
-        if ($this->havePermission(Event::PERMISSION_READABLE)) {
-            return $this->date_typ;
-        }
-        return 255;
-    }
-
-    /**
-     * Returns the index of the category.
-     * If the user has no permission, 255 is returned.
-     *
-     * TODO remove? use getStudipCategory instead?
-     *
-     * @see config/config.inc.php $TERMIN_TYP
-     * @return int The index of the category
-     */
-    public function getCategory()
-    {
-        if ($this->havePermission(Event::PERMISSION_READABLE)) {
-            return $this->date_typ;
-        }
-        return 255;
-    }
-
-    /**
-     * Returns the user id of the last editor.
-     * Since course events have no editor null is returned.
-     *
-     * @return null|int Returns always null.
-     */
-    public function getEditorId()
-    {
-        return null;
-    }
-
-    /**
-     * Returns whether the event is a all day event.
-     *
-     * @return
-     */
-    public function isDayEvent()
-    {
-        return (($this->end - $this->start) / 60 / 60) > 23;
-    }
-
-    /**
-     * Returns the accessibility of this event. The value is not influenced by
-     * the permission of the actual user.
-     *
-     * According to RFC5545 the accessibility (property CLASS) is represented
-     * by the 3 state PUBLIC, PRIVATE and CONFIDENTIAL
-     *
-     * TODO check this statement:
-     * An course event is always CONFIDENTIAL
-     *
-     * @return string The accessibility as string.
-     */
-    public function getAccessibility()
-    {
-        return 'CONFIDENTIAL';
-    }
-
-    /**
-     * Returns the unix timestamp of the last change.
-     *
-     * @access public
-     */
-    public function getChangeDate()
-    {
-        return $this->chdate;
-    }
-
-    /**
-     * Returns the date time the event was imported.
-     * Since course events are not imported normaly, returns the date time
-     * of creation.
-     *
-     * @return int Date time of import as unix timestamp:
-     */
-    public function getImportDate()
-    {
-        return $this->mkdate;
-    }
-
-    /**
-     * Returns all related groups.
-     *
-     * TODO remove, use direct access to field CourseDate::statusgruppen.
-     *
-     * @return SimpleORMapCollection The collection of statusgruppen.
-     */
-    public function getRelatedGroups()
-    {
-        return $this->statusgruppen;
-    }
-
-    public function getProperties()
-    {
-        if ($this->properties === null) {
-            $this->properties = [
-                'DTSTART' => $this->getStart(),
-                'DTEND' => $this->getEnd(),
-                'SUMMARY' => $this->getTitle(),
-                'DESCRIPTION' => $this->getDescription(),
-                'LOCATION' => $this->getLocation(),
-                'CATEGORIES' => $this->toStringCategories(),
-                'STUDIP_CATEGORY' => $this->getStudipCategory(),
-                'CREATED' => $this->mkdate,
-                'LAST-MODIFIED' => $this->chdate,
-                'STUDIP_ID' => $this->termin_id,
-                'SEM_ID' => $this->range_id,
-                'SEMNAME' => $this->course->name,
-                'CLASS' => 'CONFIDENTIAL',
-                'UID' => CourseEvent::getUid(),
-                'RRULE' => CourseEvent::getRecurrence(),
-                'EXDATE' => '',
-                'EVENT_TYPE' => 'sem',
-                'STATUS' => 'CONFIRMED',
-                'DTSTAMP' => time()];
-        }
-        return $this->properties;
-    }
-
-    /**
-     * Returns the value of property with given name.
-     *
-     * @param type $name See CalendarEvent::getProperties() for accepted values.
-     * @return mixed The value of the property.
-     * @throws InvalidArgumentException
-     */
-    public function getProperty($name)
-    {
-        if ($this->properties === null) {
-            $this->getProperties();
-        }
-
-        if (isset($this->properties[$name])) {
-            return $this->properties[$name];
-        }
-        throw new InvalidArgumentException(get_class($this)
-                . ': Property ' . $name . ' does not exist.');
-    }
-
-    public function setPermissionUser($user_id)
-    {
-        $this->permission_user_id = $user_id;
-    }
-
-    public function havePermission($permission, $user_id = null)
-    {
-        $perm = $this->getPermission($user_id);
-        return $perm >= $permission;
-    }
-
-    public function getPermission($user_id = null)
-    {
-        global $perm;
-
-        $user_id = $user_id ?: $this->permission_user_id;
-        $course_perm = $perm->get_studip_perm($this->range_id, $user_id);
-        $permission = Event::PERMISSION_FORBIDDEN;
-        switch ($course_perm) {
-            case 'tutor':
-            case 'dozent':
-                $permission = Event::PERMISSION_WRITABLE;
-                break;
-            case 'user':
-            case 'autor':
-                $permission = Event::PERMISSION_READABLE;
-                break;
-            default:
-                $permission = Event::PERMISSION_FORBIDDEN;
-        }
-
-        return $permission;
-    }
-
-    /**
-     * Course events have no priority so returns always an empty string.
-     *
-     * @return string The priority as a string.
-     */
-    public function toStringPriority()
-    {
-        return '';
-    }
-
-    /**
-     * Course events have no accessibility settings so returns always the
-     * an empty string.
-     *
-     * @return string The accessibility as string.
-     */
-    public function toStringAccessibility()
-    {
-        return '';
-    }
-
-    /**
-     * Returns a string representation of the recurrence rule.
-     * Since course events have no recurence defined it returns an empty string.
-     *
-     * @param bool $only_type If true returns only the type of recurrence.
-     * @return string The recurrence rule - human readable
-     */
-    public function toStringRecurrence($only_type = false)
-    {
-        return '';
-    }
-
-    /**
-     * Returns the author of this event as user object.
-     *
-     * @return User|null User object.
-     */
-    public function getAuthor()
-    {
-        return $this->author;
-    }
-
-    /**
-     * Course events have no editor so always null is returned.
-     *
-     * @return null
-     */
-    public function getEditor()
-    {
-        return null;
-    }
-}
diff --git a/lib/models/CourseExDate.class.php b/lib/models/CourseExDate.class.php
index 993767a37b8eb77c78b207423f9424488e0d002f..ce50e314dd332797c8d810ce20c7420654f7c549 100644
--- a/lib/models/CourseExDate.class.php
+++ b/lib/models/CourseExDate.class.php
@@ -33,7 +33,7 @@
  * @property-read mixed $room_request additional field
  */
 
-class CourseExDate extends SimpleORMap implements PrivacyObject
+class CourseExDate extends SimpleORMap implements PrivacyObject, Event
 {
     /**
      * Configures this model.
@@ -247,4 +247,176 @@ class CourseExDate extends SimpleORMap implements PrivacyObject
             date('H:i', $this->end_time)
         );
     }
+
+    //Start of Event interface implementation.
+
+    public static function getEvents(DateTime $begin, DateTime $end, string $range_id): array
+    {
+        return self::findBySQL(
+            "JOIN `seminar_user`
+               ON `seminar_user`.`seminar_id` = `ex_termine`.`range_id`
+             WHERE `seminar_user`.`user_id` = :user_id
+               AND `ex_termine`.`date` BETWEEN :begin AND :end
+               AND (
+                   IFNULL(`ex_termine`.`metadate_id`, '') = ''
+                   OR `ex_termine`.`metadate_id` NOT IN (
+                       SELECT `metadate_id`
+                       FROM `schedule_seminare`
+                       WHERE `user_id` = :user_id
+                         AND `visible` = 0
+                   )
+               )
+             ORDER BY date",
+            [
+                'begin'   => $begin->getTimestamp(),
+                'end'     => $end->getTimestamp(),
+                'user_id' => $range_id
+            ]
+        );
+    }
+
+    //Event interface implementation:
+
+    public function getObjectId() : string
+    {
+        return (string) $this->id;
+    }
+
+    public function getPrimaryObjectID(): string
+    {
+        return $this->range_id;
+    }
+
+    public function getObjectClass(): string
+    {
+        return static::class;
+    }
+
+    public function getTitle(): string
+    {
+        return sprintf('%s (fällt aus)', $this->course->name ?? '');
+    }
+
+    public function getBegin(): DateTime
+    {
+        $begin = new DateTime();
+        $begin->setTimestamp($this->date);
+        return $begin;
+    }
+
+    public function getEnd(): DateTime
+    {
+        $end = new DateTime();
+        $end->setTimestamp($this->end_time);
+        return $end;
+    }
+
+    public function getDuration(): DateInterval
+    {
+        $begin = $this->getBegin();
+        $end = $this->getEnd();
+        return $end->diff($begin);
+    }
+
+    public function getLocation(): string
+    {
+        return '';
+    }
+
+    public function getUniqueId(): string
+    {
+        return sprintf('Stud.IP-SEM-%1$s@%2$s', $this->id, $_SERVER['SERVER_NAME']);
+    }
+
+    public function getDescription(): string
+    {
+        return trim($this->getValue('content'));
+    }
+
+    public function getAdditionalDescriptions(): array
+    {
+        $descriptions = [];
+        if (count($this->dozenten) > 0) {
+            $descriptions[_('Durchführende Lehrende')] = implode(', ', $this->dozenten->getFullname());
+        }
+        if (count($this->statusgruppen) > 0) {
+            $descriptions[_('Beteiligte Gruppen')] = implode(', ', $this->statusgruppen->getValue('name'));
+        }
+        return $descriptions;
+    }
+
+    public function isAllDayEvent(): bool
+    {
+        //Course dates are never all day events.
+        return false;
+    }
+
+    public function isWritable(string $user_id): bool
+    {
+        return $GLOBALS['perm']->have_studip_perm('dozent', $this->range_id, $user_id);
+    }
+
+    public function getCreationDate(): DateTime
+    {
+        $mkdate = new DateTime();
+        $mkdate->setTimestamp($this->mkdate);
+        return $mkdate;
+    }
+
+    public function getModificationDate(): DateTime
+    {
+        $chdate = new DateTime();
+        $chdate->setTimestamp($this->chdate);
+        return $chdate;
+    }
+
+    public function getImportDate(): DateTime
+    {
+        return $this->getCreationDate();
+    }
+
+    public function getAuthor(): ?User
+    {
+        return $this->author;
+    }
+
+    public function getEditor(): ?User
+    {
+        return null;
+    }
+
+    public function toEventData(string $user_id): \Studip\Calendar\EventData
+    {
+        $begin = new DateTime();
+        $begin->setTimestamp($this->date);
+        $end = new DateTime();
+        $end->setTimestamp($this->end_time);
+
+        $studip_view_urls = [];
+        if ($GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user_id)) {
+            $studip_view_urls['show'] = URLHelper::getURL('dispatch.php/course/details', ['cid' => $this->range_id, 'link_to_course' => '1']);
+        }
+
+        return new \Studip\Calendar\EventData(
+            $begin,
+            $end,
+            $this->getTitle(),
+            [],
+            '#000000',
+            '#aaaaaa',
+            $this->isWritable($user_id),
+            CourseExDate::class,
+            $this->id,
+            Course::class,
+            $this->range_id,
+            'course',
+            $this->range_id,
+            $studip_view_urls,
+            [],
+            'seminar',
+            'rgba(0,0,0,0)'
+        );
+    }
+
+    //End of Event interface implementation.
 }
diff --git a/lib/models/CourseMarkedEvent.class.php b/lib/models/CourseMarkedEvent.class.php
deleted file mode 100644
index 96f030043d51b7be751b2abda4e0f218493173be..0000000000000000000000000000000000000000
--- a/lib/models/CourseMarkedEvent.class.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-/**
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @copyright   2015 Stud.IP Core-Group
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- *
- * @property string $id alias for pk
- * @property string $termin_id database column
- * @property string $event_id alias column for termin_id
- * @property string $range_id database column
- * @property string $sem_id alias column for range_id
- * @property string $autor_id database column
- * @property string $author_id alias column for autor_id
- * @property string $content database column
- * @property int $date database column
- * @property int $start alias column for date
- * @property int $end_time database column
- * @property int $end alias column for end_time
- * @property int $mkdate database column
- * @property int $chdate database column
- * @property int $date_typ database column
- * @property int $category_intern alias column for date_typ
- * @property string|null $raum database column
- * @property string|null $metadate_id database column
- * @property SimpleORMapCollection|Folder[] $folders has_many Folder
- * @property SimpleORMapCollection|RoomRequest[] $room_requests has_many RoomRequest
- * @property SimpleORMapCollection|ResourceRequestAppointment[] $resource_request_appointments has_many ResourceRequestAppointment
- * @property User $author belongs_to User
- * @property Course $course belongs_to Course
- * @property SeminarCycleDate|null $cycle belongs_to SeminarCycleDate
- * @property ResourceBooking $room_booking has_one ResourceBooking
- * @property SimpleORMapCollection|CourseTopic[] $topics has_and_belongs_to_many CourseTopic
- * @property SimpleORMapCollection|Statusgruppen[] $statusgruppen has_and_belongs_to_many Statusgruppen
- * @property SimpleORMapCollection|User[] $dozenten has_and_belongs_to_many User
- * @property-read mixed $location additional field
- * @property mixed $type additional field
- * @property-read mixed $name additional field
- * @property-read mixed $title additional field
- * @property-read mixed $editor_id additional field
- * @property-read mixed $uid additional field
- * @property-read mixed $summary additional field
- * @property-read mixed $description additional field
- */
-
-class CourseMarkedEvent extends CourseEvent
-{
-
-    protected static function configure($config= [])
-    {
-        parent::configure($config);
-    }
-
-    /**
-     * Returns all CourseMarkedEvents in the given time range for the given range_id.
-     *
-     * @param string $user_id Id of Stud.IP object from type user, course, inst
-     * @param DateTime $start The start date time.
-     * @param DateTime $end The end date time.
-     * @return SimpleORMapCollection Collection of found CourseMarkedEvents.
-     */
-    public static function getEventsByInterval($user_id, DateTime $start, dateTime $end)
-    {
-        $stmt = DBManager::get()->prepare('SELECT DISTINCT termine.* FROM schedule_seminare '
-                . 'INNER JOIN termine ON schedule_seminare.seminar_id = range_id '
-                . 'LEFT JOIN seminar_user ON seminar_user.seminar_id = range_id AND seminar_user.user_id= :user_id '
-                . 'WHERE schedule_seminare.user_id = :user_id AND schedule_seminare.visible = 1 '
-                . 'AND seminar_user.seminar_id IS NULL AND date BETWEEN :start AND :end '
-                . 'ORDER BY date ASC');
-        $stmt->execute([
-            ':user_id' => $user_id,
-            ':start'   => $start->getTimestamp(),
-            ':end'     => $end->getTimestamp()
-        ]);
-        $event_collection = [];
-        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
-            $event = new CourseMarkedEvent();
-            $event->setData($row);
-            $event->setNew(false);
-            $event_collection[] = $event;
-        }
-        $event_collection = SimpleORMapCollection::createFromArray($event_collection, false);
-        $event_collection->setClassName('Event');
-        return $event_collection;
-    }
-
-    public function getPermission($user_id = null)
-    {
-        return Event::PERMISSION_READABLE;
-    }
-
-    /**
-     * Returns the title of this event.
-     * The title of a course event is the name of the course or if a topic is
-     * assigned, the title of this topic. If the user has not the permission
-     * Event::PERMISSION_READABLE, the title is "Keine Berechtigung.".
-     *
-     * @return string
-     */
-    public function getTitle()
-    {
-        $title = $this->course->name;
-        $title .= ' ' . _('(vorgemerkt)');
-
-        return $title;
-    }
-
-    /**
-     * Returns the index of the category.
-     * If the user has no permission, 255 is returned.
-     *
-     * TODO remove? use getStudipCategory instead?
-     *
-     * @see config/config.inc.php $TERMIN_TYP
-     * @return int The index of the category
-     */
-    public function getCategory()
-    {
-        return 256;
-    }
-
-    public function getDescription()
-    {
-        return '';
-    }
-
-}
diff --git a/lib/models/EventData.class.php b/lib/models/EventData.class.php
deleted file mode 100644
index 453272d30fd7de6a29a2b46261d554145fb555cb..0000000000000000000000000000000000000000
--- a/lib/models/EventData.class.php
+++ /dev/null
@@ -1,144 +0,0 @@
-<?php
-/**
- * EventData.class.php - Model class for calendar events.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of
- * the License, or (at your option) any later version.
- *
- * @author      Peter Thienel <thienel@data-quest.de>
- * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
- * @category    Stud.IP
- * @since       3.2
- *
- * @property string $id alias column for event_id
- * @property string $event_id database column
- * @property string $author_id database column
- * @property string|null $editor_id database column
- * @property string $uid database column
- * @property int $start database column
- * @property int $end database column
- * @property string $summary database column
- * @property string|null $description database column
- * @property string $class database column
- * @property string|null $categories database column
- * @property int $category_intern database column
- * @property int $priority database column
- * @property string|null $location database column
- * @property int $ts database column
- * @property int|null $linterval database column
- * @property int|null $sinterval database column
- * @property string|null $wdays database column
- * @property int|null $month database column
- * @property int|null $day database column
- * @property string $rtype database column
- * @property int $duration database column
- * @property int|null $count database column
- * @property int $expire database column
- * @property string|null $exceptions database column
- * @property int $mkdate database column
- * @property int $chdate database column
- * @property int $importdate database column
- * @property SimpleORMapCollection|CalendarEvent[] $calendars has_many CalendarEvent
- * @property User $author belongs_to User
- * @property User|null $editor belongs_to User
- */
-
-class EventData extends SimpleORMap implements PrivacyObject
-{
-    protected static function configure($config = [])
-    {
-        $config['db_table'] = 'event_data';
-
-        $config['belongs_to']['author'] = [
-            'class_name'  => User::class,
-            'foreign_key' => 'author_id',
-        ];
-        $config['belongs_to']['editor'] = [
-            'class_name'  => User::class,
-            'foreign_key' => 'editor_id',
-        ];
-        $config['has_many']['calendars'] = [
-            'class_name'  => CalendarEvent::class,
-            'foreign_key' => 'event_id'
-        ];
-
-        $config['default_values']['linterval'] = 0;
-        $config['default_values']['sinterval'] = 0;
-
-        $config['registered_callbacks']['before_create'][] = 'cbDefaultValues';
-
-        parent::configure($config);
-
-    }
-
-    public function delete()
-    {
-        // do not delete until one calendar is left
-        if (sizeof($this->calendars) > 1) {
-            return false;
-        }
-        $calendars = $this->calendars;
-        $ret = parent::delete();
-        // only one calendar is left
-        if ($ret) {
-            $calendars->each(function($c) { $c->delete(); });
-        }
-        return $ret;
-    }
-
-    public static function garbageCollect()
-    {
-        DBManager::get()->query('DELETE event_data '
-                . 'FROM calendar_event LEFT JOIN event_data USING(event_id)'
-                . 'WHERE range_id IS NULL');
-    }
-
-    public function getDefaultValue($field)
-    {
-        if ($field == 'start') {
-            return time();
-        }
-        if ($field == 'end' && $this->content['start']) {
-            return $this->content['start'] + 3600;
-        }
-        if ($field == 'ts' && $this->content['start']) {
-            return mktime(12, 0, 0, date('n', $this->content['start']),
-                date('j', $this->content['start']), date('Y', $this->content['start']));
-        }
-        return parent::getDefaultValue($field);
-    }
-
-    protected function cbDefaultValues()
-    {
-        if (empty($this->content['uid'])) {
-            $this->content['uid'] = 'Stud.IP-' . $this->event_id . '@' . ($_SERVER['SERVER_NAME'] ?? parse_url($GLOBALS['ABSOLUTE_URI_STUDIP'],  PHP_URL_HOST));
-        }
-    }
-
-    /**
-     * Export available data of a given user into a storage object
-     * (an instance of the StoredUserData class) for that user.
-     *
-     * @param StoredUserData $storage object to store data into
-     */
-    public static function exportUserData(StoredUserData $storage)
-    {
-        $sorm = EventData::findThru($storage->user_id, [
-            'thru_table'        => 'calendar_event',
-            'thru_key'          => 'range_id',
-            'thru_assoc_key'    => 'event_id',
-            'assoc_foreign_key' => 'event_id',
-        ]);
-        if ($sorm) {
-            $field_data = [];
-            foreach ($sorm as $row) {
-                $field_data[] = $row->toRawArray();
-            }
-            if ($field_data) {
-                $storage->addTabularData(_('Kalender Einträge'), 'event_data', $field_data);
-            }
-        }
-    }
-}
diff --git a/lib/models/User.class.php b/lib/models/User.class.php
index 7f6a402f44f1a109afab29277ed33a90d6035d2f..5f1d6a0e3f08fdfce326e40f59acbe2ad9dcb444 100644
--- a/lib/models/User.class.php
+++ b/lib/models/User.class.php
@@ -80,7 +80,7 @@
  * @property mixed $lock_rule additional field
  * @property mixed $oercampus_description additional field
  */
-class User extends AuthUserMd5 implements Range, PrivacyObject
+class User extends AuthUserMd5 implements Range, PrivacyObject, Studip\Calendar\Owner
 {
     /**
      *
@@ -1541,4 +1541,56 @@ class User extends AuthUserMd5 implements Range, PrivacyObject
         return $this->config->EXPIRATION_DATE > 0
             && $this->config->EXPIRATION_DATE < time();
     }
+
+    /**
+     * @inheritDoc
+     */
+    public static function getCalendarOwner(string $owner_id): ?\Studip\Calendar\Owner
+    {
+        return self::find($owner_id);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function isCalendarReadable(?string $user_id = null): bool
+    {
+        if ($user_id === null) {
+            $user_id = self::findCurrent()->id;
+        }
+
+        if ($this->id === $user_id) {
+            //The owner can always read their own calendar.
+            return true;
+        }
+        return Contact::countBySql(
+            "`owner_id` = :this_user_id AND `user_id` = :other_user_id
+            AND `calendar_permissions` <> ''",
+            ['this_user_id' => $this->id, 'other_user_id' => $user_id]
+        ) > 0;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function isCalendarWritable(string $user_id = null): bool
+    {
+        if ($user_id === null) {
+            $user_id = self::findCurrent()->id;
+        }
+
+        if ($this->id === $user_id) {
+            //The owner can always write their own calendar.
+            return true;
+        }
+        if (Config::get()->CALENDAR_GRANT_ALL_INSERT) {
+            //All users can write in all users calendars.
+            return true;
+        }
+        return Contact::countBySql(
+                "`owner_id` = :this_user_id AND `user_id` = :other_user_id
+            AND `calendar_permissions` = 'WRITE'",
+            ['this_user_id' => $this->id, 'other_user_id' => $user_id]
+        ) > 0;
+    }
 }
diff --git a/lib/models/calendar/CalendarCourseDate.class.php b/lib/models/calendar/CalendarCourseDate.class.php
new file mode 100644
index 0000000000000000000000000000000000000000..49179bd579d1d808e47298f45f56209b2f7fc467
--- /dev/null
+++ b/lib/models/calendar/CalendarCourseDate.class.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * CalendarCourseDate is a specialisation of CourseDate for
+ * course dates that are displayed in the personal calendar.
+ */
+class CalendarCourseDate extends CourseDate
+{
+    public static function getEvents(DateTime $begin, DateTime $end, string $range_id): array
+    {
+        return parent::findBySQL(
+            "JOIN `seminar_user`
+               ON `seminar_user`.`seminar_id` = `termine`.`range_id`
+             WHERE `seminar_user`.`user_id` = :user_id
+               AND `seminar_user`.`bind_calendar` = '1'
+               AND `termine`.`date` BETWEEN :begin AND :end
+               AND (
+                   IFNULL(`termine`.`metadate_id`, '') = ''
+                   OR `termine`.`metadate_id` NOT IN (
+                       SELECT `metadate_id`
+                       FROM `schedule_seminare`
+                       WHERE `user_id` = :user_id
+                         AND `visible` = 0
+                   )
+               )
+               ORDER BY date",
+            [
+                'begin'   => $begin->getTimestamp(),
+                'end'     => $end->getTimestamp(),
+                'user_id' => $range_id
+            ]
+        );
+    }
+}
diff --git a/lib/models/calendar/CalendarCourseExDate.class.php b/lib/models/calendar/CalendarCourseExDate.class.php
new file mode 100644
index 0000000000000000000000000000000000000000..eb23aeb79d53964681157a1f9d63c9753791d774
--- /dev/null
+++ b/lib/models/calendar/CalendarCourseExDate.class.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * CalendarCourseExDate is a specialisation of CourseExDate for
+ * cancelled course dates that are displayed in the personal calendar.
+ */
+class CalendarCourseExDate extends CourseExDate
+{
+    public static function getEvents(DateTime $begin, DateTime $end, string $range_id): array
+    {
+        return parent::findBySQL(
+            "JOIN `seminar_user`
+               ON `seminar_user`.`seminar_id` = `ex_termine`.`range_id`
+             WHERE `seminar_user`.`user_id` = :user_id
+               AND `seminar_user`.`bind_calendar` = '1'
+               AND `ex_termine`.`date` BETWEEN :begin AND :end
+               AND `ex_termine`.`content` <> ''
+               AND (
+                   IFNULL(`ex_termine`.`metadate_id`, '') = ''
+                   OR `ex_termine`.`metadate_id` NOT IN (
+                       SELECT `metadate_id`
+                       FROM `schedule_seminare`
+                       WHERE `user_id` = :user_id
+                         AND `visible` = 0
+                 )
+             )
+             ORDER BY date",
+            [
+                'begin'   => $begin->getTimestamp(),
+                'end'     => $end->getTimestamp(),
+                'user_id' => $range_id
+            ]
+        );
+    }
+}
diff --git a/lib/models/calendar/CalendarDate.class.php b/lib/models/calendar/CalendarDate.class.php
new file mode 100644
index 0000000000000000000000000000000000000000..23ea8af3daa4137b8e1900a03c0bf43cdf989209
--- /dev/null
+++ b/lib/models/calendar/CalendarDate.class.php
@@ -0,0 +1,944 @@
+<?php
+/**
+ * CalendarDate.class.php - Model class for calendar dates.
+ *
+ * CalendarDate represents a date in the personal calendar.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Peter Thienel <thienel@data-quest.de>
+ * @author      Moritz Strohm <strohm@data-quest.de>
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ * @since       3.2
+ *
+ * @property string id database column
+ * @property string author_id database column
+ * @property string editor_id database column
+ * @property string unique_id database column
+ * @property string begin database column
+ * @property string end database column
+ * @property string title database column
+ * @property string description database column
+ * @property string access database column
+ * @property string user_category database column
+ * @property string category database column
+ * @property string location database column
+ * @property string interval database column
+ * @property string offset database column
+ * @property string days database column
+ * @property string month database column
+ * @property string day_offset database column
+ * @property string repetition_type database column
+ * @property string number_of_dates database column
+ * @property string repetition_end database column
+ * @property string mkdate database column
+ * @property string chdate database column
+ * @property string import_date database column
+ */
+class CalendarDate extends SimpleORMap implements PrivacyObject
+{
+    /**
+     * NEVER_ENDING represents the value of the repetition_end field for
+     * a date that never ends. The value is the result of computing
+     * 2 ^ 31 - 1.
+     *
+     * NOTE: This constant must be changed long before 2038-01-19 03:14:07 UTC
+     * or else dates that should end at some specific point in time may end
+     * never.
+     */
+    public const NEVER_ENDING = 2147483647;
+
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'calendar_dates';
+
+        $config['belongs_to']['author'] = [
+            'class_name'  => User::class,
+            'foreign_key' => 'author_id',
+        ];
+        $config['belongs_to']['editor'] = [
+            'class_name'  => User::class,
+            'foreign_key' => 'editor_id',
+        ];
+        $config['has_many']['calendars'] = [
+            'class_name'  => CalendarDateAssignment::class,
+            'assoc_foreign_key' => 'calendar_date_id',
+            'on_store'    => 'store',
+            'on_delete'   => 'delete'
+        ];
+        $config['has_many']['exceptions'] = [
+            'class_name'  => CalendarDateException::class,
+            'assoc_foreign_key' => 'calendar_date_id',
+            'on_store'    => 'store',
+            'on_delete'   => 'delete'
+        ];
+
+        $config['default_values']['interval'] = 0;
+        $config['default_values']['offset'] = 0;
+
+        $config['registered_callbacks']['before_store'][] = 'calculateExpiration';
+        $config['registered_callbacks']['after_store'][] = 'cbSendDateModificationMail';
+        $config['registered_callbacks']['before_store'][] = 'cbGenerateUniqueId';
+
+        parent::configure($config);
+
+    }
+
+    public function delete()
+    {
+        // do not delete until one calendar is left
+        if (count($this->calendars) > 1) {
+            return false;
+        }
+        $calendars = $this->calendars;
+        $ret = parent::delete();
+        // only one calendar is left
+        if ($ret) {
+            $calendars->delete();
+        }
+        return $ret;
+    }
+
+    public static function garbageCollect()
+    {
+        DBManager::get()->query(
+            'DELETE `calendar_dates`
+            FROM `calendar_date_assignments`
+            LEFT JOIN `calendar_dates` ON (`calendar_dates`.`id` = `calendar_date_assignments`.`calendar_date_id`)
+            WHERE `range_id` IS NULL'
+        );
+    }
+
+    /**
+     * @deprecated
+     */
+    public function getDefaultValue($field)
+    {
+        if ($field == 'begin') {
+            return time();
+        }
+        if ($field == 'end' && $this->content['begin']) {
+            return $this->content['begin'] + 3600;
+        }
+        return parent::getDefaultValue($field);
+    }
+
+    public function cbSendDateModificationMail()
+    {
+        $template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/');
+
+        foreach ($this->calendars as $calendar) {
+            if ($calendar->range_id === $this->editor_id) {
+                //The editor shall not get a mail about the changes they just made.
+                continue;
+            }
+            if (!$calendar->user) {
+                //Wrong range or not a user.
+                continue;
+            }
+            setTempLanguage($calendar->range_id);
+
+            $lang_path = getUserLanguagePath($calendar->range_id);
+            $template = $template_factory->open($lang_path . '/LC_MAILS/date_changed.php');
+            $template->set_attribute('date', $this);
+            $template->set_attribute('receiver', $calendar->user);
+            $template->set_attribute('receiver_date_assignment', $calendar);
+            $mail_text = $template->render();
+            Message::send(
+                '____%system%____',
+                [$calendar->user->username],
+                sprintf(_('Terminänderung durch %s'), $this->editor->getFullName()),
+                $mail_text
+            );
+
+            restoreLanguage();
+        }
+    }
+
+    /**
+     * Generates an unique id if it isn't present.
+     * @return void
+     */
+    public function cbGenerateUniqueId()
+    {
+        if (!$this->unique_id) {
+            $this->unique_id = 'Stud.IP-' . $this->id . '@' . ($_SERVER['SERVER_NAME'] ?? '');
+        }
+    }
+
+    /**
+     * TODO
+     *
+     * @param string $range_id
+     * @return bool
+     */
+    public function isVisible(string $range_id)
+    {
+        if (CalendarDateAssignment::exists([$range_id, $this->id])) {
+            //Users may see the dates in their calendar:
+            return true;
+        }
+
+        $assignments = CalendarDateAssignment::findByCalendar_date_id($this->id);
+        foreach ($assignments as $assignment) {
+            if ($assignment->course instanceof Course) {
+                if ($assignment->course->isCalendarReadable($range_id)) {
+                    return true;
+                }
+            } elseif ($assignment->user instanceof User) {
+                if ($assignment->user->isCalendarReadable($range_id)) {
+                    return true;
+                }
+            }
+        }
+
+        //In case the date is not in a calendar of the user or a course
+        //where the user has access to, it is only visible when it is public.
+        return $this->access === 'PUBLIC';
+    }
+
+
+    public function isWritable(string $range_id)
+    {
+        if (CalendarDateAssignment::exists([$range_id, $this->id])) {
+            //The date is in the calendar of the user/course
+            //and therefore, the user or course administrator (tutor, dozent)
+            //may change the date.
+            return true;
+        }
+
+        //Check contacts: Has the contact of the user that is represented by
+        //$range_id write permissions to all the calendars of all the users that
+        //are assigned to the date?
+
+        $contacts_with_write_permissions = Contact::countBySql(
+            "JOIN `calendar_date_assignments` cda
+               ON `contact`.`user_id` = cda.`range_id`
+            WHERE `contact`.`owner_id` = :current_range_id
+              AND `contact`.`calendar_permissions` = 'WRITE'
+              AND cda.`calendar_date_id` = :calendar_date_id
+              AND cda.`range_id` <> :current_range_id",
+            [
+                'calendar_date_id' => $this->id,
+                'current_range_id' => $range_id
+            ]
+        );
+        $other_participant_count = CalendarDateAssignment::countBySql(
+            "`calendar_date_id` = :calendar_date_id
+             AND `range_id` <> :current_range_id",
+            [
+                'calendar_date_id' => $this->id,
+                'current_range_id' => $range_id
+            ]
+        );
+
+        if ($contacts_with_write_permissions === $other_participant_count) {
+            //The user represented by $range_id has write permissions to all
+            //calendars of all the other users that are assigned to the date.
+            return true;
+        }
+
+        //NOTE: CALENDAR_GRANT_ALL_INSERT MUST NOT be regarded here, because it only
+        //defines the behavior when inserting calendar dates and not when modifying them.
+
+        //In case it is a course date, we must check if the user has write
+        //permissions from the course:
+        $course_assignments = CalendarDateAssignment::findBySql(
+            "JOIN `seminare`
+               ON `calendar_date_assignments`.`range_id` = `seminare`.`seminar_id`
+            WHERE `calendar_date_id` = :calendar_date_id",
+            ['calendar_date_id' => $this->id]
+        );
+        foreach ($course_assignments as $course_assignment) {
+            if ($course_assignment->course->calendarWritable($range_id)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Determines whether the date spans over one whole day. This means that the date takes
+     * place on one day from 0:00:00 to 23:59:59.
+     *
+     * @return bool True, if the date spans over the whole day, false otherwise.
+     */
+    public function isWholeDay() : bool
+    {
+        $begin = new DateTime();
+        $begin->setTimestamp($this->begin);
+        $end = new DateTime();
+        $end->setTimestamp($this->end);
+
+        if ($begin->format('Ymd') !== $end->format('Ymd')) {
+            //Beginning and end are on different days.
+            return false;
+        }
+        //If the beginning is on midnight and the end is one second before midnight of the next day,
+        //the date spans over the whole day.
+        return $begin->format('His') === '000000'
+            && $end->format('His') === '235959';
+    }
+
+
+    /**
+     * Calculates the value of the "expire" column in case the CalendarDate object
+     * has a repetition defined.
+     *
+     * @return void
+     */
+    public function calculateExpiration()
+    {
+        if (!in_array($this->repetition_type, ['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'])) {
+            //No repetition. Nothing to do.
+            return;
+        }
+        if ($this->number_of_dates > 1) {
+            //There is a certain amount of repetitions, so that the expiration date
+            //has to be calculated by that.
+            $expiration = new DateTime();
+            $expiration->setTimestamp($this->begin);
+            $interval_str = '';
+            if ($this->repetition_type === 'DAILY') {
+                $interval_str = sprintf('P%dD', ((int) $this->number_of_dates - 1) * $this->interval);
+            } elseif ($this->repetition_type === 'WEEKLY') {
+                $days_length = mb_strlen($this->days);
+                if ($days_length > 0) {
+                    $wday = $expiration->format('N');
+                    // set next weekday as first repetition
+                    $expiration->modify($this->getWeekdayName());
+
+                    $rep_offset = ($this->number_of_dates - 1) % $days_length;
+
+                    $rep_count = $this->number_of_dates - 1;
+
+                    $days_offset = floor($rep_count / $days_length) * 7 *
+                        $this->interval + $rep_offset - 1;
+                    $interval_str = sprintf('P%dD', $days_offset);
+                } else {
+                    $interval_str = sprintf('P%dW', ($this->number_of_dates - 1) * $this->interval);
+                }
+            } elseif ($this->repetition_type === 'MONTHLY') {
+                $interval_str = sprintf('P%dM', ($this->number_of_dates - 1) * $this->interval);
+            } elseif ($this->repetition_type === 'YEARLY') {
+                $interval_str = sprintf('P%dY', ($this->number_of_dates - 1) * $this->interval);
+            }
+            try {
+                $interval = new DateInterval($interval_str);
+                $expiration->add($interval);
+                $expiration->setTime(23, 59, 59);
+                $this->repetition_end = $expiration->getTimestamp();
+            } catch (Exception $e) {
+                //Nothing to do.
+            }
+        } elseif (!$this->repetition_end) {
+            //No expiration date is specified.
+            //This would mean that the event "never" expires.
+            $this->repetition_end = self::NEVER_ENDING;
+        }
+    }
+
+
+    /**
+     *
+     * Returns the DateInterval for the repetition of this calendar date.
+     *
+     * @return DateInterval|null The DateInterval for this calendar date or null
+     *     in case the date has no repetition.
+     * @throws Exception In case a DateInterval cannot be constructed.
+     */
+    public function getRepetitionInterval() : ?DateInterval
+    {
+        if ($this->repetition_type === 'DAILY') {
+            return new DateInterval(sprintf('P%uD', $this->interval));
+        } elseif ($this->repetition_type === 'WORKDAYS') {
+            return new DateInterval('P1W');
+        } elseif ($this->repetition_type === 'WEEKLY') {
+            return new DateInterval(sprintf('P%uW', $this->interval));
+        } elseif ($this->repetition_type === 'MONTHLY') {
+            return new DateInterval(sprintf('P%uM', $this->interval));
+        } elseif ($this->repetition_type === 'YEARLY') {
+            return new DateInterval(sprintf('P%uY', $this->interval));
+        }
+        //No repetition: no interval.
+        return null;
+    }
+
+
+    public function getRepetitionOffset() : ?DateInterval
+    {
+        if (!$this->offset) {
+            return null;
+        }
+
+        if ($this->repetition_type === 'MONTHLY') {
+            if ($this->days_offset) {
+                return new DateInterval(sprintf('P%1$uM%2$uD', $this->offset, $this->days_offset));
+            } else {
+                return new DateInterval(sprintf('P%uM', $this->offset));
+            }
+        } elseif ($this->repetition_type === 'YEARLY') {
+            return new DateInterval(sprintf('P%uM', $this->offset));
+        }
+        return null;
+    }
+
+
+    /**
+     * Export available data of a given user into a storage object
+     * (an instance of the StoredUserData class) for that user.
+     *
+     * @param StoredUserData $storage object to store data into
+     */
+    public static function exportUserData(StoredUserData $storage)
+    {
+        $sorm = self::findThru($storage->user_id, [
+            'thru_table'        => 'calendar_date_assignments',
+            'thru_key'          => 'range_id',
+            'thru_assoc_key'    => 'event_id',
+            'assoc_foreign_key' => 'event_id',
+        ]);
+        if ($sorm) {
+            $field_data = [];
+            foreach ($sorm as $row) {
+                $field_data[] = $row->toRawArray();
+            }
+            if ($field_data) {
+                $storage->addTabularData(_('Kalendereinträge'), 'calendar_dates', $field_data);
+            }
+        }
+    }
+
+
+    /**
+     * This is a helper method to set all the fields for date repetition to an empty string.
+     *
+     * @return void
+     */
+    public function clearRepetitionFields()
+    {
+        $this->repetition_type = '';
+        $this->interval = '';
+        $this->offset = '';
+        $this->days = '';
+        $this->month = '';
+        $this->number_of_dates = '1';
+        $this->repetition_end = '';
+    }
+
+    public function getAccessAsString() : string
+    {
+        if ($this->access === 'PUBLIC') {
+            return _('Öffentlich');
+        } elseif ($this->access === 'PRIVATE') {
+            return _('Privat');
+        } elseif ($this->access === 'CONFIDENTIAL') {
+            return _('Vertraulich');
+        } else {
+            return _('Keine Angabe');
+        }
+    }
+
+    public function getRepetitionAsString() : string
+    {
+        require_once 'lib/dates.inc.php';
+
+        $repetition_string = '';
+
+        if ($this->repetition_type === 'SINGLE') {
+            $repetition_string = _('Keine Wiederholung');
+        } elseif ($this->repetition_type === 'DAILY') {
+            if ($this->interval > 0) {
+                if ($this->interval == '1') {
+                    //Each day
+                    if ($this->number_of_dates > 1) {
+                        $repetition_string = sprintf(
+                            _('Täglich (%u Termine)'),
+                            $this->number_of_dates
+                        );
+                    } elseif ($this->repetition_end < CalendarDate::NEVER_ENDING) {
+                        $repetition_string = sprintf(
+                            _('Täglich bis zum %1$s'),
+                            date('d.m.Y', $this->repetition_end)
+                        );
+                    } else {
+                        $repetition_string = _('Täglich ohne Begrenzung');
+                    }
+                } else {
+                    //Every %u day
+                    if ($this->number_of_dates > 1) {
+                        $repetition_string = sprintf(
+                            _('Jeden %1$u. Tag (%2$u Termine)'),
+                            $this->interval,
+                            $this->number_of_dates
+                        );
+                    } elseif ($this->repetition_end < self::NEVER_ENDING) {
+                        $repetition_string = sprintf(
+                            _('Jeden %1$u. Tag bis zum %2$s'),
+                            $this->interval,
+                            date('d.m.Y', $this->repetition_end)
+                        );
+                    } else {
+                        $repetition_string = sprintf(
+                            _('Jeden %u. Tag ohne Begrenzung'),
+                            $this->interval
+                        );
+                    }
+                }
+            }
+        } elseif ($this->repetition_type === 'WEEKLY') {
+            $weekday_string = '';
+            if (strlen($this->days) > 1) {
+                //Multiple days
+                $days = [];
+                foreach (str_split($this->days) as $day_number) {
+                    if ($day_number == '7') {
+                        $day_number = '0';
+                    }
+                    $days[] = getWeekday($day_number, false);
+                }
+                $all_but_last_day = array_slice($days, 0, -1);
+                $weekday_string = sprintf(
+                    _('%1$s und %2$s'),
+                    implode(', ', $all_but_last_day),
+                    end($days)
+                );
+            } else {
+                //One day
+                $weekday_string = getWeekday($this->days[0], false);
+            }
+            if ($this->interval == '1') {
+                //Each week
+                if ($this->number_of_dates > 1) {
+                    $repetition_string = sprintf(
+                        ngettext('Einmal am folgenden %s', 'Jeden %1$s (%2$u Termine)', $this->number_of_dates - 1),
+                        $weekday_string,
+                        $this->number_of_dates
+                    );
+                } elseif ($this->repetition_end < self::NEVER_ENDING) {
+                    $repetition_string = sprintf(
+                        _('Jeden %1$s bis zum %2$s'),
+                        $weekday_string,
+                        date('d.m.Y', $this->repetition_end)
+                    );
+                } else {
+                    $repetition_string = sprintf(
+                        _('Jeden %s ohne Begrenzung'),
+                        $weekday_string
+                    );
+                }
+            } else {
+                //Every %u week
+                if ($this->number_of_dates > 1) {
+                    $repetition_string = sprintf(
+                        _('Jeden %1$u. %2$s (%3$u Termine)'),
+                        $this->interval,
+                        $weekday_string,
+                        $this->number_of_dates
+                    );
+                } elseif ($this->repetition_end < self::NEVER_ENDING) {
+                    $repetition_string = sprintf(
+                        _('Jeden %1$u. %2$s bis zum %3$s'),
+                        $this->interval,
+                        $weekday_string,
+                        date('d.m.Y', $this->repetition_end)
+                    );
+                } else {
+                    $repetition_string = sprintf(
+                        _('Jeden %1$u. %2$s ohne Begrenzung'),
+                        $this->interval,
+                        $weekday_string
+                    );
+                }
+            }
+        } elseif ($this->repetition_type === 'MONTHLY') {
+            if ($this->interval == '1') {
+                //Each month
+                if ($this->days) {
+                    if ($this->offset < 0) {
+                        //Repetition on one specific day of week in the last week.
+                        $repetition_string = sprintf(
+                            _('Jeden Monat am letzten %s'),
+                            getWeekday($this->days, false)
+                        );
+                    } else {
+                        //Repetition on one specific day of week in a specific week.
+                        $repetition_string = sprintf(
+                            _('Jeden Monat am %1$u. %2$s'),
+                            $this->offset,
+                            getWeekday($this->days, false)
+                        );
+                    }
+                } else {
+                    //Repetition on one specific day of month.
+                    $repetition_string = sprintf(
+                        _('Jeden Monat am %u. Tag'),
+                        $this->offset
+                    );
+                }
+            } else {
+                //Every %u month
+                if ($this->days) {
+                    if ($this->offset < 0) {
+                        //Repetition on one specific day of week on the last week.
+                        $repetition_string = sprintf(
+                            _('Jeden %1$u. Monat am letzten %2$s'),
+                            $this->interval,
+                            getWeekday($this->days, false)
+                        );
+                    } else {
+                        //Repetition on one specific day of week in a specific week.
+                        $repetition_string = sprintf(
+                            _('Jeden %1$u. Monat am %2$u. %3$s'),
+                            $this->interval,
+                            $this->offset,
+                            getWeekday($this->days, false)
+                        );
+                    }
+                } else {
+                    //Repetition on one specific day of month.
+                    $repetition_string = sprintf(
+                        _('Jeden %1$u. Monat am %2$u.'),
+                        $this->interval,
+                        $this->offset
+                    );
+                }
+            }
+        } elseif ($this->repetition_type === 'YEARLY') {
+            if ($this->interval == '1') {
+                //Each year
+                if ($this->days) {
+                    //Repetition on one specific day of week in a specific week
+                    //in a specific month.
+                    if ($this->offset < 0) {
+                        if ($this->number_of_dates > 1) {
+                            $repetition_string = sprintf(
+                                _('Jedes Jahr im %1$s am letzten %2$s (%3$u Termine)'),
+                                getMonthName($this->month, false),
+                                getWeekday($this->days, false),
+                                $this->number_of_dates
+                            );
+                        } elseif ($this->repetition_end < self::NEVER_ENDING) {
+                            $repetition_string = sprintf(
+                                _('Jedes Jahr im %1$s am letzten %2$s bis zum %3$s'),
+                                getMonthName($this->month, false),
+                                getWeekday($this->days, false),
+                                date('d.m.Y', $this->repetition_end)
+                            );
+                        } else {
+                            $repetition_string = sprintf(
+                                _('Jedes Jahr im %1$s am letzten %2$s ohne Begrenzung'),
+                                getMonthName($this->month, false),
+                                getWeekday($this->days, false)
+                            );
+                        }
+                    } else {
+                        if ($this->number_of_dates > 1) {
+                            $repetition_string = sprintf(
+                                _('Jedes Jahr im %1$s am %2$u. %3$s (%4$u Termine'),
+                                getMonthName($this->month, false),
+                                $this->offset,
+                                getWeekday($this->days, false),
+                                $this->number_of_dates
+                            );
+                        } elseif ($this->repetition_end < self::NEVER_ENDING) {
+                            $repetition_string = sprintf(
+                                _('Jedes Jahr im %1$s am %2$u. %3$s bis zum %4$s'),
+                                getMonthName($this->month, false),
+                                $this->offset,
+                                getWeekday($this->days, false),
+                                date('d.m.Y', $this->repetition_end)
+                            );
+                        } else {
+                            $repetition_string = sprintf(
+                                _('Jedes Jahr im %1$s am %2$u. %3$s ohne Begrenzung'),
+                                getMonthName($this->month, false),
+                                $this->offset,
+                                getWeekday($this->days, false)
+                            );
+                        }
+                    }
+                } else {
+                    //Repetition on one specific day of month.
+                    if ($this->number_of_dates > 1) {
+                        $repetition_string = sprintf(
+                            _('Jedes Jahr am %1$u. %2$s (%3$u Termine)'),
+                            $this->offset,
+                            getMonthName($this->month, false),
+                            $this->number_of_dates
+                        );
+                    } elseif ($this->repetition_end < self::NEVER_ENDING) {
+                        $repetition_string = sprintf(
+                            _('Jedes Jahr am %1$u. %2$s bis zum %3$s'),
+                            $this->offset,
+                            getMonthName($this->month, false),
+                            date('d.m.Y', $this->repetition_end)
+                        );
+                    } else {
+                        $repetition_string = sprintf(
+                            _('Jedes Jahr am %1$u. %2$s ohne Begrenzung'),
+                            $this->offset,
+                            getMonthName($this->month, false)
+                        );
+                    }
+                }
+            } else {
+                //Every %u years
+                if ($this->days) {
+                    //Repetition on one specific day of week in a specific week
+                    //in a specific month.
+                    if ($this->offset < 0) {
+                        if ($this->number_of_dates > 1) {
+                            $repetition_string = sprintf(
+                                _('Jedes %1$u. Jahr im %2$s am letzten %3$s (%4$u Termine)'),
+                                $this->interval,
+                                getMonthName($this->month, false),
+                                getWeekday($this->days, false),
+                                $this->number_of_dates
+                            );
+                        } elseif ($this->repetition_end < self::NEVER_ENDING) {
+                            $repetition_string = sprintf(
+                                _('Jedes %1$u. Jahr im %2$s am letzten %3$s bis zum %4$s'),
+                                $this->interval,
+                                getMonthName($this->month, false),
+                                getWeekday($this->days, false),
+                                date('d.m.Y', $this->repetition_end)
+                            );
+                        } else {
+                            $repetition_string = sprintf(
+                                _('Jedes %1$u. Jahr im %2$s am letzten %3$s ohne Begrenzung'),
+                                $this->interval,
+                                getMonthName($this->month, false),
+                                getWeekday($this->days, false)
+                            );
+                        }
+                    } else {
+                        if ($this->number_of_dates > 1) {
+                            $repetition_string = sprintf(
+                                _('Jedes %1$u. Jahr im %2$s am %3$u. %4$s (%5$u Termine)'),
+                                $this->interval,
+                                getMonthName($this->month, false),
+                                $this->offset,
+                                getWeekday($this->days, false),
+                                $this->number_of_dates
+                            );
+                        } elseif ($this->repetition_end < self::NEVER_ENDING) {
+                            $repetition_string = sprintf(
+                                _('Jedes %1$u. Jahr im %2$s am %3$u. %4$s bis zum %5$s'),
+                                $this->interval,
+                                getMonthName($this->month, false),
+                                $this->offset,
+                                getWeekday($this->days, false),
+                                date('d.m.Y', $this->repetition_end)
+                            );
+                        } else {
+                            $repetition_string = sprintf(
+                                _('Jedes %1$u. Jahr im %2$s am %3$u. %4$s ohne Begrenzung'),
+                                $this->interval,
+                                getMonthName($this->month, false),
+                                $this->offset,
+                                getWeekday($this->days, false)
+                            );
+                        }
+                    }
+                } else {
+                    //Repetition on one specific day of month.
+                    if ($this->number_of_dates > 1) {
+                        $repetition_string = sprintf(
+                            _('Jedes %1$u. Jahr am %2$u. %3$s (%4$u Termine)'),
+                            $this->interval,
+                            $this->offset,
+                            getMonthName($this->month, false),
+                            $this->number_of_dates
+                        );
+                    } elseif ($this->repetition_end < self::NEVER_ENDING) {
+                        $repetition_string = sprintf(
+                            _('Jedes %1$u. Jahr am %2$u. %3$s bis zum %4$s'),
+                            $this->interval,
+                            $this->offset,
+                            getMonthName($this->month, false),
+                            date('d.m.Y', $this->repetition_end)
+                        );
+                    } else {
+                        $repetition_string = sprintf(
+                            _('Jedes %1$u. Jahr am %2$u. %3$s ohne Begrenzung'),
+                            $this->interval,
+                            $this->offset,
+                            getMonthName($this->month, false)
+                        );
+                    }
+                }
+            }
+        }
+
+        return $repetition_string;
+    }
+
+
+    /**
+     * Creates the HTML for creating a repetition input Vue component instance
+     * and fills it with the values from the model.
+     *
+     * @param string $element_name The name of the element.
+     *
+     * @return string The HTML code for creating the repetition input vue instance.
+     */
+    public function getRepetitionInputHtml(string $element_name = 'repetition') : string
+    {
+        $repetition_end_type = '';
+        $repetition_end_date = '';
+        $repetition_dow = '[]';
+        $repetition_dow_week = '';
+
+        if ($this->isNew()) {
+            $repetition_end_date = htmlReady(date('d.m.Y', $this->end));
+            $repetition_dow = sprintf('["%s"]', date('N', $this->begin));
+            $repetition_dow_week = '1';
+        } else {
+
+            if ($this->repetition_end) {
+                $repetition_end_date = htmlReady(date('d.m.Y', $this->repetition_end));
+            } else {
+                //Provide a good default value in case the user wants to enable or change the repetition:
+                $repetition_end_date = htmlReady(date('d.m.Y', $this->end));
+            }
+            if ($this->days) {
+                $repetition_dow = json_encode(str_split($this->days));
+                $repetition_dow_week = $this->offset;
+            } else {
+                //The days field is not in use. Use the day of the beginning as a good default.
+                $repetition_dow = sprintf('["%s"]', date('N', $this->begin));
+                //Also set repetition_dow_week to 1 as a good default in case the user
+                //switches to the monthly repetition type where a specific day of week
+                //is selected instead of a specific day of month:
+                $repetition_dow_week = '1';
+            }
+
+            if ($this->number_of_dates > 1) {
+                $repetition_end_type = 'end_count';
+            } elseif ($this->repetition_end && intval($this->repetition_end) !== self::NEVER_ENDING) {
+                //The end date is at some certain date and not on the virtual "never" date.
+                $repetition_end_type = 'end_date';
+            }
+        }
+
+        $attributes = [
+            'name'                   => $element_name,
+            'default_date'           => $this->begin,
+            'repetition_type'        => $this->isNew() ? '' : $this->repetition_type,
+            'repetition_interval'    => $this->isNew() ? '1'  : $this->interval,
+            ':repetition_dow'        => $repetition_dow,
+            ':repetition_dow_week'   => $repetition_dow_week,
+            ':repetition_month'      => $this->isNew() ? date('m', $this->begin) : $this->month,
+            ':repetition_month_type' => $this->isNew() ? "'dom'" : ($this->days ? "'dow'" : "'dom'"),
+            ':repetition_dom'        => $this->isNew() ? date('d', $this->begin) : $this->offset,
+            ':repetition_end_type'   => sprintf("'%s'", $repetition_end_type),
+            ':number_of_dates'       => $this->isNew() ? '1' : $this->number_of_dates,
+            ':repetition_end_date'   => sprintf("'%s'", $repetition_end_date)
+        ];
+        return sprintf('<repetition-input %s></repetition-input>', arrayToHtmlAttributes($attributes));
+    }
+
+    public function getCategoryAsString() : string
+    {
+        if ($this->user_category) {
+            return $this->user_category;
+        }
+        return $GLOBALS['PERS_TERMIN_KAT'][$this->category]['name'] ?? '';
+    }
+
+    /**
+     * Returns the textual ordinal for the offset of a weekday from property offset
+     * or an empty string if offset is not set.
+     *
+     * @return string The textual ordinal.
+     */
+    public function getOrdinalName(): string
+    {
+        if (mb_strlen($this->offset)) {
+            $ordinal_array = [
+                '1' => 'first',
+                '2' => 'second',
+                '3' => 'third',
+                '4' => 'fourth',
+                '5' => 'fifth',
+                '-1' => 'last'
+            ];
+            return $ordinal_array[$this->offset];
+        }
+        return '';
+    }
+
+    /**
+     * Returns the short name of first weekday from property days or an
+     * empty string if days is not set.
+     *
+     * @param $offset int Offset of days.
+     * @return string Short name of weekday.
+     */
+    public function getWeekdayName(int $offset = 0): string
+    {
+        if (mb_strlen($this->days)) {
+            $wdays = [
+                '1' => 'mon',
+                '2' => 'tue',
+                '3' => 'wed',
+                '4' => 'thu',
+                '5' => 'fri',
+                '6' => 'sat',
+                '7' => 'sun'
+            ];
+            return $wdays[substr($this->days, $offset, 1)];
+        }
+        return '';
+    }
+
+    /**
+     * Returns a string representation of the access field.
+     *
+     * @return string A localised string of the access field.
+     */
+    public function getVisibilityAsString() : string
+    {
+        if ($this->access === 'PUBLIC') {
+            return _('Öffentlich');
+        } elseif ($this->access === 'CONFIDENTIAL') {
+            return _('Vertraulich');
+        } else {
+            return _('Privat');
+        }
+    }
+
+    /**
+     * Returns the names of the participants of the date. This also includes courses
+     * to which the date is assigned.
+     *
+     * @param string $user_id The user for which to generate the participant array.
+     *     The user with that ID is excluded from that list.
+     * @return array A list with the names of the participants of the date.
+     */
+    public function getParticipantsAsStringArray(string $user_id = '') : array
+    {
+        $participant_strings = [];
+        foreach ($this->calendars as $calendar) {
+            if ($calendar->range_id === $user_id) {
+                //Exclude the user for which to generate the list.
+                continue;
+            }
+            if ($calendar->course instanceof Course) {
+                $participant_strings[] = $calendar->course->getFullName();
+            } elseif ($calendar->user instanceof User) {
+                $participant_strings[] = $calendar->user->getFullName();
+            }
+        }
+
+        asort($participant_strings);
+
+        return $participant_strings;
+    }
+}
diff --git a/lib/models/calendar/CalendarDateAssignment.class.php b/lib/models/calendar/CalendarDateAssignment.class.php
new file mode 100644
index 0000000000000000000000000000000000000000..ba0f38720efa2583c76167b72824d1a0b694a547
--- /dev/null
+++ b/lib/models/calendar/CalendarDateAssignment.class.php
@@ -0,0 +1,714 @@
+<?php
+/**
+ * CalendarDateAssignment.class.php - Model class for calendar date assignments.
+ *
+ * CalendarDateAssignment represents the assignment of a calendar date
+ *  to a specific calendar. The calendar is represented by a range-ID
+ *  since it can be a personal calendar, course calendar or institute
+ *  calendar.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Moritz Strohm <strohm@data-quest.de>
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ * @since       5.5
+ *
+ * @property string range_id The range-ID for the assignment.
+ * @property string calendar_date_id The ID of the calendar date for the assignment.
+ * @property string participation The participation status of the receiver (range_id).
+ *     This column is an enum with the following values:
+ *     - empty string: Participation status is unknown.
+ *     - "ACCEPTED": The calendar owner accepted the date.
+ *     - "DECLINED": The calendar owner declined the date.
+ *     - "ACKNOWLEDGED": The calendar owner only acknowledged that the date exists
+ *           but doesn't necessarily participate in it.
+ * @property string mkdate The creation date of the assignment.
+ * @property string chdate The modification date of the assignment.
+ * @property CalendarDate|null calendar_date The associated calendar date object.
+ */
+class CalendarDateAssignment extends SimpleORMap implements Event
+{
+    /**
+     * @var bool This attribute allows the suppression of automatic mail sending
+     *     when storing or deleting the calendar date assignment.
+     *     By default, mails are sent.
+     */
+    public $suppress_mails = false;
+
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'calendar_date_assignments';
+
+        $config['belongs_to']['calendar_date'] = [
+            'class_name'  => CalendarDate::class,
+            'foreign_key' => 'calendar_date_id',
+            'assoc_func'  => 'find'
+        ];
+        $config['belongs_to']['user'] = [
+            'class_name'  => User::class,
+            'foreign_key' => 'range_id',
+            'assoc_func'  => 'find'
+        ];
+        $config['belongs_to']['course'] = [
+            'class_name'  => Course::class,
+            'foreign_key' => 'range_id',
+            'assoc_func'  => 'find'
+        ];
+
+        $config['registered_callbacks']['after_create'][] = 'cbSendNewDateMail';
+        $config['registered_callbacks']['after_delete'][] = 'cbSendDateDeletedMail';
+
+        parent::configure($config);
+    }
+
+
+    public function cbSendNewDateMail()
+    {
+        if ($this->suppress_mails) {
+            return;
+        }
+        if ($this->range_id === $this->calendar_date->editor_id) {
+            return;
+        }
+        if (!$this->calendar_date || !$this->user) {
+            //Wrong calendar range (not a user) or invalid data set.
+            return;
+        }
+
+        $template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/');
+
+        setTempLanguage($this->range_id);
+        $lang_path = getUserLanguagePath($this->range_id);
+        $template = $template_factory->open($lang_path . '/LC_MAILS/date_created.php');
+        $template->set_attribute('date', $this->calendar_date);
+        $template->set_attribute('receiver', $this->user);
+        $mail_text = $template->render();
+        Message::send(
+            '____%system%____',
+            [$this->user->username],
+            sprintf(_('%s hat einen Termin im Kalender eingetragen'), $this->calendar_date->editor->getFullName()),
+            $mail_text
+        );
+
+        restoreLanguage();
+    }
+
+    public function cbSendDateDeletedMail()
+    {
+        if ($this->suppress_mails) {
+            return;
+        }
+        if ($this->range_id === $this->calendar_date->editor_id) {
+            return;
+        }
+        if (!$this->calendar_date || !$this->user) {
+            //Wrong calendar range (not a user) or invalid data set.
+            return;
+        }
+
+        $template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/');
+
+        setTempLanguage($this->range_id);
+        $lang_path = getUserLanguagePath($this->range_id);
+        $template = $template_factory->open($lang_path . '/LC_MAILS/date_deleted.php');
+        $template->set_attribute('date', $this->calendar_date);
+        $template->set_attribute('receiver', $this->user);
+        $mail_text = $template->render();
+        Message::send(
+            '____%system%____',
+            [$this->user->username],
+            sprintf(_('%s hat einen Termin im Kalender gelöscht'), $this->calendar_date->editor->getFullName()),
+            $mail_text
+        );
+
+        restoreLanguage();
+    }
+
+    /**
+     * Sends the participation status of the calendar the date
+     * is assigned to. This is only done for user calendars
+     * and not for course calendars.
+     *
+     * @return void
+     */
+    public function sendParticipationStatus() : void
+    {
+        if (!($this->user instanceof User)) {
+            //The calendar date is assigned to a course calendar.
+            return;
+        }
+
+        if (!$this->participation || $this->participation === 'ACKNOWLEDGED') {
+            //Nothing shall be done in these two cases.
+            return;
+        }
+
+        if (empty($this->calendar_date->author->username)) {
+            //The calendar date has no author.
+            return;
+        }
+        if ($this->range_id === $this->calendar_date->author_id) {
+            //The author of the date changed their participation status.
+            //So they know what they did and do not have to be notified.
+            return;
+        }
+
+        $template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/');
+
+        setTempLanguage($this->range_id);
+        $lang_path = getUserLanguagePath($this->range_id);
+        $template = $template_factory->open($lang_path . '/LC_MAILS/date_participation.php');
+        $template->set_attribute('date_assignment', $this);
+        $mail_text = $template->render();
+
+        $subject = '';
+        if ($this->participation === 'ACCEPTED') {
+            $subject = sprintf(
+                _('%1$s hat Ihren Termin am %2$s angenommen'),
+                $this->user->getFullName(),
+                date('d.m.Y', $this->calendar_date->begin)
+            );
+        } elseif ($this->participation === 'DECLINED') {
+            $subject = sprintf(
+                _('%1$s hat Ihren Termin am %2$s abgelehnt'),
+                $this->user->getFullName(),
+                date('d.m.Y', $this->calendar_date->begin)
+            );
+        }
+
+        Message::send(
+            '____%system%____',
+            [$this->calendar_date->author->username],
+            $subject,
+            $mail_text
+        );
+
+        restoreLanguage();
+    }
+
+    /**
+     * Retrieves calendar dates inside a specified time range that are present in the calendar of a
+     * course or user. They can additionally be filtered by the access level and declined events
+     * can be filtered out, too.
+     *
+     * @param DateTime $begin The beginning of the time range.
+     *
+     * @param DateTime $end The end of the time range.
+     *
+     * @param string $range_id The ID of the course or user whose calendar dates shall be retrieved.
+     *
+     * @param array $access_levels The access level filter: Only include calendar dates that have one of the
+     *     access levels in the list.
+     *
+     * @param bool $with_declined Include declined calendar dates (true) or filter them out (false).
+     *     Defaults to false.
+     *
+     * @return CalendarDateAssignment[] A list of calendar date assignments in the time range that match the filters.
+     */
+    public static function getEvents(
+        DateTime $begin,
+        DateTime $end,
+        string $range_id,
+        array $access_levels = ['PUBLIC', 'PRIVATE', 'CONFIDENTIAL'],
+        bool $with_declined = false
+    ) : array
+    {
+        $begin->setTime(0, 0);
+        $end->setTime(23, 59, 59);
+
+        $sql = "JOIN `calendar_dates`
+            ON calendar_date_id = `calendar_dates`.`id`
+            WHERE
+            `calendar_date_assignments`.`range_id` = :range_id ";
+        if (!$with_declined) {
+            $sql .= "AND `calendar_date_assignments`.`participation` <> 'DECLINED' ";
+        }
+        $sql .= "AND (
+                `calendar_dates`.`begin` BETWEEN :begin AND :end
+                OR
+                (`calendar_dates`.`begin` <= :end AND `calendar_dates`.`repetition_type` <> ''
+                    AND `calendar_dates`.`repetition_end` > :begin)
+                OR
+                :begin BETWEEN `calendar_dates`.`begin` AND `calendar_dates`.`end`
+            )
+            AND
+            `access` IN ( :access_levels )
+            ORDER BY `calendar_dates`.`begin` ASC";
+
+        $events = self::findBySql($sql, [
+            'range_id'      => $range_id,
+            'begin'         => $begin->getTimestamp(),
+            'end'           => $end->getTimestamp(),
+            'access_levels' => $access_levels
+        ]);
+
+        $m_start = clone $begin;
+        $m_end = clone $end;
+        $events_created = [];
+        while ($m_start < $m_end) {
+
+            foreach ($events as $event) {
+                $e_start = clone $event->getBegin();
+                $e_end = clone $event->getEnd();
+                $e_expire = $event->getExpire();
+
+                // duration in full days
+                $duration = $event->getDurationDays();
+
+                $cal_start = DateTimeImmutable::createFromMutable($m_start);
+                $cal_end = DateTimeImmutable::createFromMutable($m_start)->setTime(23,59,59);
+                $cal_noon = $cal_start->setTime(12, 0);
+                // single events or first event
+                if (
+                    ($e_start >= $cal_start && $e_end <= $cal_end)
+                    || ($e_start >= $cal_start && $e_start <= $cal_end)
+                    || ($e_start < $cal_start && $e_end > $cal_end)
+                    || ($e_end > $cal_start && $e_start <= $cal_end)
+                ) {
+                    // exception for first event or single event
+                    if (!$event->calendar_date->exceptions->findOneBy('date', $cal_start->format('Y-m-d'))) {
+                        $events_created = array_merge($events_created, self::createRecurrentDate($event, $cal_noon));
+                    }
+                } elseif ($e_expire > $cal_start) {
+                    $events_created = array_merge($events_created, self::getRepetition($event, $cal_noon));
+                }
+            }
+
+            $m_start->modify('+1 day');
+        }
+
+        return $events_created;
+    }
+
+    private static function getRepetition(
+        CalendarDateAssignment $date,
+        DateTimeImmutable $cal_noon,
+        bool $calc_prev = true
+    ): array
+    {
+        $rep_dates = [];
+        $ts = $date->getNoonDate();
+        if ($cal_noon >= $ts) {
+            if ($date->isRepeatedAtDate($cal_noon)) {
+                $rep_dates = array_merge($rep_dates, self::createRecurrentDate($date, $cal_noon));
+            }
+            if ($calc_prev) {
+                $rep_noon = $cal_noon->modify(sprintf('-%s days', $date->getDurationDays()));
+                $rep_dates = array_merge(
+                    $rep_dates,
+                    self::getRepetition(
+                        $date,
+                        $rep_noon,
+                        false
+                    )
+                );
+            }
+        }
+        return $rep_dates;
+    }
+
+    private function isRepeatedAtDate(DateTimeImmutable $cal_date): bool
+    {
+        $ts = $this->getNoonDate();
+        $pos = 1;
+        switch ($this->getRepetitionType()) {
+            case 'DAILY':
+                $pos = $cal_date->diff($ts)->days % $this->calendar_date->interval;
+                break;
+            case 'WEEKLY':
+                $cal_ts = $cal_date->modify('monday this week noon');
+                if ($cal_date >= $this->getBegin()) {
+                    $pos = $cal_ts->diff($ts)->days % ($this->calendar_date->interval * 7);
+                    if (
+                        $pos === 0
+                        && strpos($this->calendar_date->days, $cal_date->format('N')) === false
+                    ) {
+                        $pos = 1;
+                    }
+                }
+                break;
+            case 'MONTHLY':
+                $cal_ts = $cal_date->modify('first day of this month noon');
+                $diff = $cal_ts->diff($ts);
+                $pos = ($diff->m + $diff->y * 12) % $this->calendar_date->interval;
+                if ($pos === 0) {
+                    if (strlen($this->calendar_date->days)) {
+                        $cal_ts_dom = $cal_ts->modify(sprintf('%s %s of this month noon',
+                            $this->calendar_date->getOrdinalName(),
+                            $this->calendar_date->getWeekdayName()));
+                        if ($cal_ts_dom != $cal_date->setTime(12, 0)) {
+                            $pos = 1;
+                        }
+                    } elseif ($this->calendar_date->offset !== $cal_date->format('j')) {
+                        $pos = 1;
+                    }
+                }
+                break;
+            case 'YEARLY':
+                $cal_ts = $cal_date->modify('first day of this year noon');
+                $diff = $cal_ts->diff($ts);
+                $pos = $diff->y % $this->calendar_date->interval;
+                if ($pos === 0) {
+                    if (strlen($this->calendar_date->days)) {
+                        $ts_doy = $ts->modify(sprintf('%s %s of %s-%s noon',
+                            $this->calendar_date->getOrdinalName(),
+                            $this->calendar_date->getWeekdayName(),
+                            $cal_date->format('Y'),
+                            $this->calendar_date->month));
+                        if ($ts_doy->format('n-j') !== $cal_date->format('n-j')) {
+                            $pos = 1;
+                        }
+                    } elseif (
+                        $cal_date->format('n-j') !== sprintf(
+                            '%s-%s',
+                            $this->calendar_date->month,
+                            $this->calendar_date->offset
+                        )
+                    ) {
+                        $pos = 1;
+                    }
+                }
+                break;
+            default:
+                $pos = 1;
+        }
+        //Also check for exceptions before returning:
+        return $pos === 0
+            && !$this->calendar_date->exceptions->findOneBy(
+                'date',
+                $cal_date->format('Y-m-d'));
+    }
+
+    private static function createRecurrentDate(
+        CalendarDateAssignment $date,
+        DateTimeImmutable $date_time
+    ) : array
+    {
+        $date_begin = $date->getBegin();
+        $date_end = $date->getEnd();
+
+        $rec_date = clone $date;
+        $time_begin = $date_begin->format('H:i:s');
+        $time_end = $date_end->format('H:i:s');
+
+        $rec_date_begin = $date_time->modify(sprintf('today %s', $time_begin));
+        $rec_date_end = $rec_date_begin->add($date->getDuration())->modify(sprintf('today %s', $time_end));
+
+        $rec_date->calendar_date->begin = $rec_date_begin->getTimestamp();
+        $rec_date->calendar_date->end = $rec_date_end->getTimestamp();
+        $index = $date->calendar_date->id . '_' . $rec_date_begin->getTimestamp();
+        return [$index => $rec_date];
+    }
+
+    //Event interface implementation:
+
+    public function getObjectId() : string
+    {
+        return (string)$this->id;
+    }
+
+    public function getPrimaryObjectID(): string
+    {
+        return $this->calendar_date_id;
+    }
+
+    public function getObjectClass(): string
+    {
+        return static::class;
+    }
+
+    public function getTitle() : string
+    {
+        return $this->calendar_date->title ?? '';
+    }
+
+    public function getBegin(): DateTime
+    {
+        $begin = new DateTime();
+        $begin->setTimestamp($this->calendar_date->begin ?? 0);
+        return $begin;
+    }
+
+    public function getEnd(): DateTime
+    {
+        $end = new DateTime();
+        $end->setTimestamp($this->calendar_date->end ?? 0);
+        return $end;
+    }
+
+    public function getDuration(): DateInterval
+    {
+        $begin = $this->getBegin();
+        $end = $this->getEnd();
+        return $begin->diff($end);
+    }
+
+    /**
+     * Returns the "extent" in days of this date.
+     *
+     * @return int The "extent" in days of this date.
+     */
+    public function getDurationDays(): int
+    {
+        return self::getExtent($this->getEnd(), $this->getBegin());
+    }
+
+    /**
+     * Returns the "extent" in days of this date.
+     * The extent is the number of days a date is displayed in a calendar.
+     *
+     * @return int The "extent" in days of this date.
+     */
+    public static function getExtent(DateTimeInterface $date_begin, DateTimeInterface $date_end): int
+    {
+        $days_duration = $date_end->diff($date_begin)->days;
+        if ($date_begin->format('His') > $date_end->format('His')) {
+            $days_duration += 1;
+        }
+        return $days_duration;
+    }
+
+    public function getLocation(): string
+    {
+        return $this->calendar_date->location ?? '';
+    }
+
+    public function getUniqueId(): string
+    {
+        return $this->calendar_date->unique_id ?? '';
+    }
+
+    public function getDescription(): string
+    {
+        return $this->calendar_date->description ?? '';
+    }
+
+    public function getAdditionalDescriptions(): array
+    {
+        return [
+            _('Kategorie')    => $this->calendar_date->getCategoryAsString(),
+            _('Sichtbarkeit') => $this->calendar_date->getVisibilityAsString(),
+            _('Wiederholung') => $this->calendar_date->getRepetitionAsString()
+        ];
+    }
+
+    public function isAllDayEvent(): bool
+    {
+        $begin = $this->getBegin();
+        if ($begin->format('His') != '000000') {
+            return false;
+        }
+        $duration = $this->getDuration();
+        return $duration->h === 23 && $duration->i === 59 && $duration->s === 59;
+    }
+
+    public function isWritable(string $user_id): bool
+    {
+        if ($this->calendar_date->author_id === $user_id) {
+            //The author may always modify one of their dates:
+            return true;
+        }
+        if ($this->calendar_date->isWritable($user_id)) {
+            //The date is writable.
+            return true;
+        }
+
+        //The user referenced by $user_id is not the author of the date.
+        //Check if they have write permissions to the calendar where the date is assigned to:
+        if ($this->user instanceof User) {
+            //It is a personal calendar. Check if the owner of the calendar has granted write permissions
+            //to the user:
+            return Contact::countBySQL(
+                "`owner_id` = :owner_id AND `user_id` = :user_id
+                AND `calendar_permissions` = 'WRITE'",
+                ['owner_id' => $this->range_id, 'user_id' => $user_id]
+            ) > 0;
+        } elseif ($this->course instanceof Course) {
+            //It is a course calendar.
+            return $GLOBALS['perm']->have_studip_perm('dozent', $this->range_id, $user_id);
+        }
+
+        //No write permissions are granted.
+        return false;
+    }
+
+    public function getCreationDate(): DateTime
+    {
+        $mkdate = new DateTime();
+        $mkdate->setTimestamp($this->calendar_date->mkdate ?? 0);
+        return $mkdate;
+    }
+
+    public function getModificationDate(): DateTime
+    {
+        $chdate = new DateTime();
+        $chdate->setTimestamp($this->calendar_date->chdate ?? 0);
+        return $chdate;
+    }
+
+    public function getImportDate(): DateTime
+    {
+        $import_date = new DateTime();
+        $import_date->setTimestamp($this->calendar_date->import_date ?? 0);
+        return $import_date;
+    }
+
+    public function getAuthor(): ?User
+    {
+        return $this->calendar_date->author ?? null;
+    }
+
+    public function getEditor(): ?User
+    {
+        return $this->calendar_date->editor ?? null;
+    }
+
+    /**
+     * TODO calculate end of repetition for different types of repetition
+     * @return float|int|object
+     */
+    public function getExpire()
+    {
+        if ($this->calendar_date->repetition_end > 0) {
+            $expire = $this->calendar_date->repetition_end;
+        } else {
+            $expire = CalendarDate::NEVER_ENDING;
+        }
+
+        $end = new DateTime();
+        $end->setTimestamp($expire);
+        return $end;
+    }
+
+    // TODO calculate ts for monthly and yearly repetition
+    public function getNoonDate()
+    {
+        $ts = DateTimeImmutable::createFromMutable($this->getBegin());
+        switch ($this->calendar_date->repetition_type) {
+            case 'DAILY':
+                return $ts->modify('noon');
+            case 'WEEKLY':
+                return  $ts->modify('monday this week noon');
+            case 'MONTHLY':
+                return $ts->modify('first day of this month noon');
+            case 'YEARLY':
+                return $ts->modify('first day of this year noon');
+            default:
+                return $ts;
+        }
+    }
+
+    /**
+     * Returns the type of repetition.
+     *
+     * @return string The type of repetition.
+     */
+    public function getRepetitionType(): string
+    {
+        return $this->calendar_date->repetition_type;
+    }
+
+    public function toEventData(string $user_id): \Studip\Calendar\EventData
+    {
+        $begin = $this->getBegin();
+        $end = $this->getEnd();
+        $duration = $this->getDuration();
+
+        $all_day = $begin->format('H:i:s') === '00:00:00'
+            && $duration->h === 23
+            && $duration->i === 59
+            && $duration->s === 59;
+
+
+        $hide_confidential_data = $this->calendar_date->access === 'CONFIDENTIAL'
+            && $user_id !== $this->calendar_date->author_id;
+
+        $event_classes = ['user-date'];
+
+        $text_colour = '#000000';
+        $background_colour = '#ffffff';
+        $border_colour = '#000000';
+        if (!$hide_confidential_data) {
+            if ($this->calendar_date->user_category) {
+                //The date belongs to a personal category that gets a grey colour.
+                $background_colour = '#a7abaf';
+                $border_colour     = '#a7abaf';
+            } else {
+                //The date belongs to a system category that has its own colours.
+                $text_colour = $GLOBALS['PERS_TERMIN_KAT'][$this->calendar_date->category]['fgcolor'] ?? $text_colour;
+                $background_colour = $GLOBALS['PERS_TERMIN_KAT'][$this->calendar_date->category]['bgcolor'] ?? $background_colour;
+                $border_colour = $GLOBALS['PERS_TERMIN_KAT'][$this->calendar_date->category]['border_color'] ?? $border_colour;
+                $event_classes[] = sprintf('user-date-category%d', $this->calendar_date->category);
+            }
+        }
+
+        $show_url_params = [];
+        if ($this->calendar_date->repetition_type) {
+            $show_url_params['selected_date'] = $begin->format('Y-m-d');
+        }
+
+        return new \Studip\Calendar\EventData(
+            $begin,
+            $end,
+            !$hide_confidential_data ? $this->getTitle() : '',
+            $event_classes,
+            $text_colour,
+            $background_colour,
+            $this->isWritable($user_id),
+            CalendarDateAssignment::class,
+            $this->id,
+            CalendarDate::class,
+            $this->calendar_date_id,
+            'user',
+            $this->range_id ?? '',
+            [
+                'show'   => URLHelper::getURL('dispatch.php/calendar/date/index/' . $this->calendar_date_id, $show_url_params)
+            ],
+            [
+                'resize_dialog' => URLHelper::getURL('dispatch.php/calendar/date/move/' . $this->calendar_date_id),
+                'move_dialog'   => URLHelper::getURL('dispatch.php/calendar/date/move/' . $this->calendar_date_id)
+            ],
+            $this->participation === 'DECLINED' ? 'decline-circle-full' : '',
+            $border_colour,
+            $all_day
+        );
+    }
+
+    public function getRangeName() : string
+    {
+        if ($this->course instanceof Course) {
+            return $this->course->getFullname();
+        } elseif ($this->user instanceof User) {
+            return $this->user->getFullName();
+        }
+        return '';
+    }
+
+    public function getRangeAvatar() : ?Avatar
+    {
+        if ($this->course instanceof Course) {
+            return CourseAvatar::getAvatar($this->range_id);
+        } elseif ($this->user instanceof User) {
+            return Avatar::getAvatar($this->range_id);
+        }
+        return null;
+    }
+
+    public function getParticipationAsString() : string
+    {
+        if ($this->participation === '') {
+            return _('Abwartend');
+        } elseif ($this->participation === 'ACKNOWLEDGED') {
+            return _('Angenommen (keine Teilnahme)');
+        } elseif ($this->participation === 'ACCEPTED') {
+            return _('Angenommen');
+        } elseif ($this->participation === 'DECLINED') {
+            return _('Abgelehnt');
+        }
+        return '';
+    }
+}
diff --git a/lib/models/calendar/CalendarDateException.class.php b/lib/models/calendar/CalendarDateException.class.php
new file mode 100644
index 0000000000000000000000000000000000000000..b53a728d819b53ff62365fdd16155dfd3f594a32
--- /dev/null
+++ b/lib/models/calendar/CalendarDateException.class.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * The CalendarDateException class represents one exception for a calendar date.
+ * 
+ * This file is part of Stud.IP
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Moritz Strohm <strohm@data-quest.de>
+ * @copyright   2023
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ * @package     resources
+ * @since       5.5
+ *
+ * @property string $id The ID of the exception.
+ * @property string $calendar_date_id The ID of the calendar date where the exception belongs to.
+ * @property string $date The date of the exception in the date format YYYY-MM-DD.
+ * @property string $mkdate The creation date of the exception.
+ * @property string $chdate The modification date of the exception.
+ * @property CalendarDate|null $calendar_date The associated calendar date object.
+ */
+class CalendarDateException extends SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'calendar_date_exceptions';
+
+        $config['belongs_to']['calendar_date'] = [
+            'class_name' => CalendarDate::class,
+            'foreign_key' => 'calendar_date_id',
+            'assoc_func' => 'find'
+        ];
+
+        parent::configure($config);
+    }
+}
diff --git a/lib/modules/CoreCalendar.class.php b/lib/modules/CoreCalendar.class.php
index c0df36736d70a53057fd99c0e33233784921bb4b..318a2ba7da89a9090d7a1a9d1262b9991e90f0bf 100644
--- a/lib/modules/CoreCalendar.class.php
+++ b/lib/modules/CoreCalendar.class.php
@@ -20,7 +20,7 @@ class CoreCalendar extends CorePlugin implements StudipModule
             return null;
         }
 
-        $navigation = new Navigation(_('Kalender'), "seminar_main.php?auswahl={$course_id}&redirect_to=dispatch.php/calendar/single/");
+        $navigation = new Navigation(_('Kalender'), URLHelper::getURL('dispatch.php/calendar/calendar/course/' . $course_id));
         $navigation->setImage(Icon::create('schedule', Icon::ROLE_CLICKABLE));
         return $navigation;
     }
@@ -34,7 +34,7 @@ class CoreCalendar extends CorePlugin implements StudipModule
             return null;
         }
 
-        $navigation = new Navigation(_('Kalender'), 'dispatch.php/calendar/single/');
+        $navigation = new Navigation(_('Kalender'), 'dispatch.php/calendar/calendar/course/' . $course_id);
         $navigation->setImage(Icon::create('schedule', Icon::ROLE_INFO_ALT));
         $navigation->setActiveImage(Icon::create('schedule', Icon::ROLE_INFO));
         return ['calendar' => $navigation];
@@ -49,10 +49,10 @@ class CoreCalendar extends CorePlugin implements StudipModule
             'summary' => _('Kalender'),
             'category' => _('Lehr- und Lernorganisation'),
             'icon' => Icon::create('schedule', Icon::ROLE_INFO),
-            'icon_clickable' => Icon::create('schedule', Icon::ROLE_CLICKABLE),
-            'displayname' => _('Planer'),
+            'displayname' => _('Kalender'),
         ];
     }
+
     public function isActivatableForContext(Range $context)
     {
         return Config::get()->CALENDAR_GROUP_ENABLE && $context->getRangeType() === 'course';
@@ -60,7 +60,6 @@ class CoreCalendar extends CorePlugin implements StudipModule
 
     public function getInfoTemplate($course_id)
     {
-        // TODO: Implement getInfoTemplate() method.
         return null;
     }
 }
diff --git a/lib/modules/CoreOverview.class.php b/lib/modules/CoreOverview.class.php
index af1b9604774109bdb10c2a610fb91bba0a819d3e..94005da3edd6fb4d09d8e92a81f1533e3d31c621 100644
--- a/lib/modules/CoreOverview.class.php
+++ b/lib/modules/CoreOverview.class.php
@@ -90,7 +90,7 @@ class CoreOverview extends CorePlugin implements StudipModule
         if ($object_type !== 'sem') {
             $navigation->addSubNavigation('info', new Navigation(_('Kurzinfo'), 'dispatch.php/institute/overview'));
             $navigation->addSubNavigation('courses', new Navigation(_('Veranstaltungen'), 'show_bereich.php?level=s&id='.$course_id));
-            $navigation->addSubNavigation('schedule', new Navigation(_('Veranstaltungs-Stundenplan'), 'dispatch.php/calendar/instschedule?cid='.$course_id));
+            $navigation->addSubNavigation('schedule', new Navigation(_('Veranstaltungs-Stundenplan'), 'dispatch.php/institute/schedule/index/' . $course_id));
 
             if ($GLOBALS['perm']->have_studip_perm('admin', $course_id)) {
                 $navigation->addSubNavigation('admin', new Navigation(_('Administration der Einrichtung'), 'dispatch.php/institute/basicdata/index?new_inst=TRUE'));
diff --git a/lib/modules/ScheduleWidget.php b/lib/modules/ScheduleWidget.php
index 339beb3f87fa45ec83ef5a86406a0845b0876891..0393176827de3d4a79bad079f6f58c13bfd8199f 100644
--- a/lib/modules/ScheduleWidget.php
+++ b/lib/modules/ScheduleWidget.php
@@ -39,15 +39,45 @@ class ScheduleWidget extends CorePlugin implements PortalPlugin
      */
     public function getPortalTemplate()
     {
-        $view = CalendarScheduleModel::getUserCalendarView(
-            $GLOBALS['user']->id,
-            false,
-            false,
-            $days = array(0,1,2,3,4)
+        $week_slot_duration = \Studip\Calendar\Helper::getCalendarSlotDuration('week');
+        $calendar_settings = $GLOBALS['user']->cfg->CALENDAR_SETTINGS ?? [];
+
+        $semester = Semester::findCurrent();
+        $fullcalendar = \Studip\Fullcalendar::create(
+            '',
+            [
+                'minTime' => '08:00',
+                'maxTime' => '20:00',
+                'allDaySlot' => false,
+                'header' => [
+                    'left' => '',
+                    'right' => ''
+                ],
+                'views' => [
+                    'timeGridWeek' => [
+                        'columnHeaderFormat' => ['weekday' => 'long'],
+                        'weekends'           => $calendar_settings['type_week'] === 'LONG',
+                        'slotDuration'       => $week_slot_duration
+                    ]
+                ],
+                'defaultView' => 'timeGridWeek',
+                'defaultDate' => date('Y-m-d'),
+                'timeGridEventMinHeight' => 20,
+                'eventSources' => [
+                    [
+                        'url' => URLHelper::getURL('dispatch.php/calendar/calendar/schedule_data'),
+                        'method' => 'GET',
+                        'extraParams' => [
+                            'semester_id' => $semester->id,
+                            'full_semester_time_range' => false
+                        ]
+                    ]
+                ]
+            ]
         );
 
         $template = $GLOBALS['template_factory']->open('shared/string');
-        $template->content = CalendarWidgetView::createFromWeekView($view)->render();
+        $template->content = $fullcalendar;
 
         return $template;
     }
diff --git a/lib/modules/TerminWidget.php b/lib/modules/TerminWidget.php
index c9cf85400ef29b36492743d5258b0f15356bcbde..773e6923948d73a028c2a0483d8b3a0877c7c89d 100644
--- a/lib/modules/TerminWidget.php
+++ b/lib/modules/TerminWidget.php
@@ -20,7 +20,7 @@ class TerminWidget extends CorePlugin implements PortalPlugin
     public function getMetadata()
     {
         return [
-            'description' => _('Mit diesem Widget haben Sie ihre aktuellen Termine im Überlick.')
+            'description' => _('Dieses Widget zeigt die eigenen aktuellen Termine an.')
         ];
     }
 
@@ -31,8 +31,9 @@ class TerminWidget extends CorePlugin implements PortalPlugin
         $template = $GLOBALS['template_factory']->open('shared/string');
         $template->content = $response->body;
 
-        $navigation = new Navigation('', 'dispatch.php/calendar/single/week', ['self' => true]);
-        $navigation->setImage(Icon::create('add', 'clickable', ["title" => _('Neuen Termin anlegen')]));
+        $navigation = new Navigation('', 'dispatch.php/calendar/date/add');
+        $navigation->setImage(Icon::create('add', Icon::ROLE_CLICKABLE, ['title' => _('Neuen Termin anlegen')]));
+        $navigation->setLinkAttributes(['data-dialog' => 'reload-on-close']);
         $template->icons = [$navigation];
 
         return $template;
diff --git a/lib/navigation/CalendarNavigation.php b/lib/navigation/CalendarNavigation.php
index 972f3f840e999a43cb65232556ccf82fe6697728..f201d86c78292201e6e4024aa5909ed7b417ff8f 100644
--- a/lib/navigation/CalendarNavigation.php
+++ b/lib/navigation/CalendarNavigation.php
@@ -20,25 +20,19 @@ class CalendarNavigation extends Navigation
      */
     public function __construct()
     {
-        global $perm;
-
-        parent::__construct(_('Planer'));
-
-        if (
-            isset($perm)
-            && !$perm->have_perm('admin')
-            && Config::get()->SCHEDULE_ENABLE
-        ) {
-            $planerinfo = _('Stundenplan');
-        } else {
-            $planerinfo = _('Termine');
+        $title = _('Kalender');
+        $main_url = URLHelper::getURL('dispatch.php/calendar/calendar');
+        if (!$GLOBALS['perm']->have_perm('admin') && Config::get()->SCHEDULE_ENABLE) {
+            $title = _('Stundenplan');
+            $main_url = URLHelper::getURL('dispatch.php/calendar/schedule');
         }
+        parent::__construct($title, $main_url);
 
-        $this->setImage(Icon::create('schedule', 'navigation', ["title" => $planerinfo]));
+        $this->setImage(Icon::create('schedule', 'navigation', ['title' => $title]));
     }
 
     /**
-     * Initialize the subnavigation of this item. This method
+     * Initialize the sub-navigation of this item. This method
      * is called once before the first item is added or removed.
      */
     public function initSubNavigation()
@@ -47,16 +41,13 @@ class CalendarNavigation extends Navigation
 
         parent::initSubNavigation();
 
-        // schedule
         if (!$perm->have_perm('admin') && Config::get()->SCHEDULE_ENABLE) {
             $navigation = new Navigation(_('Stundenplan'), 'dispatch.php/calendar/schedule');
             $this->addSubNavigation('schedule', $navigation);
         }
 
-        // calendar
-        $atime = $atime ? intval($atime) : Request::int($atime);
         if (Config::get()->CALENDAR_ENABLE) {
-            $navigation = new Navigation(_('Terminkalender'), 'dispatch.php/calendar/single', ['self' => 1]);
+            $navigation = new Navigation(_('Kalender'), 'dispatch.php/calendar/calendar');
             $this->addSubNavigation('calendar', $navigation);
         }
     }
diff --git a/lib/navigation/ProfileNavigation.php b/lib/navigation/ProfileNavigation.php
index ffff06cbeb40678dab0e7c1cb88c67f25f4e2444..50dcfec2d9be15ea866398994d3ed8913e4a8ece 100644
--- a/lib/navigation/ProfileNavigation.php
+++ b/lib/navigation/ProfileNavigation.php
@@ -103,7 +103,7 @@ class ProfileNavigation extends Navigation
                 $navigation->addSubNavigation('messaging', new Navigation(_('Nachrichten'), 'dispatch.php/settings/messaging'));
 
                 if (Config::get()->CALENDAR_ENABLE) {
-                    $navigation->addSubNavigation('calendar_new', new Navigation(_('Terminkalender'), 'dispatch.php/settings/calendar'));
+                    $navigation->addSubNavigation('calendar_new', new Navigation(_('Kalender'), 'dispatch.php/settings/calendar'));
                 }
 
                 if (!$perm->have_perm('admin') && Config::get()->MAIL_NOTIFICATION_ENABLE) {
diff --git a/lib/navigation/StartNavigation.php b/lib/navigation/StartNavigation.php
index bc938b693d37b70a46ddd6c7cc07deb5f1d82b38..913f3faad48e996a8834611a3e95c4837901926b 100644
--- a/lib/navigation/StartNavigation.php
+++ b/lib/navigation/StartNavigation.php
@@ -281,10 +281,10 @@ class StartNavigation extends Navigation
 
             $this->addSubNavigation('profile', $navigation);
 
-            $navigation = new Navigation(_('Mein Planer'));
+            $navigation = new Navigation(_('Kalender'));
 
             if (Config::get()->CALENDAR_ENABLE) {
-                $navigation->addSubNavigation('calendar', new Navigation(_('Terminkalender'), 'dispatch.php/calendar/single'));
+                $navigation->addSubNavigation('calendar', new Navigation(_('Kalender'), 'dispatch.php/calendar/calendar'));
             }
 
             if (Config::get()->SCHEDULE_ENABLE) {
diff --git a/lib/seminar_open.php b/lib/seminar_open.php
index 0ca99918017a24374a750c9ac62afb968618dad7..45c4df375b0354d79eddd086055f46411d24dba0 100644
--- a/lib/seminar_open.php
+++ b/lib/seminar_open.php
@@ -44,7 +44,7 @@ function startpage_redirect($page_code) {
             $jump_page = "dispatch.php/contact";
         break;
         case 5:
-            $jump_page = "dispatch.php/calendar/single";
+            $jump_page = "dispatch.php/calendar";
         break;
         case 6:
             // redirect to global blubberstream
diff --git a/locale/de/LC_MAILS/_date_information.php b/locale/de/LC_MAILS/_date_information.php
new file mode 100644
index 0000000000000000000000000000000000000000..5eab14d55df782a0d21bd0dc9b2f2d3fc170bfe2
--- /dev/null
+++ b/locale/de/LC_MAILS/_date_information.php
@@ -0,0 +1,28 @@
+*Zeiten:* <?= date('d.m.Y H:i', $date->begin) ?> - <?= date('d.m.Y H:i', $date->end) ?>
+
+*Titel:* <?= $date->title ?>
+
+<?= $date->description ?? '' ?>
+
+--
+
+<? if ($date->category) : ?>
+*Kategorie:* <?= $date->getCategoryAsString() ?>
+<? endif ?>
+
+*Zugriff:* <?= $date->getAccessAsString() ?>
+
+<? if ($date->repetition_type) : ?>
+*Wiederholung:* <?= $date->getRepetitionAsString() ?>
+<? endif ?>
+
+<? if (Config::get()->CALENDAR_GROUP_ENABLE && count($date->calendars) > 1) : ?>
+*Teilnehmende:*
+<? foreach($date->getParticipantsAsStringArray($receiver->user_id) as $participant_string) : ?>
+- <?= $participant_string ?>
+<? endforeach ?>
+<? endif ?>
+
+<? if ($receiver_date_assignment) : ?>
+**Ihre Teilnahme:** <?= $receiver_date_assignment->getParticipationAsString() ?>
+<? endif ?>
diff --git a/locale/de/LC_MAILS/date_changed.php b/locale/de/LC_MAILS/date_changed.php
new file mode 100644
index 0000000000000000000000000000000000000000..aa92b7fd0c32999610952f82b980cc8b441fa414
--- /dev/null
+++ b/locale/de/LC_MAILS/date_changed.php
@@ -0,0 +1,10 @@
+<?= $date->editor->getFullName() ?> hat einen Termin im Kalender geändert.
+
+<?= $this->render_partial(__DIR__ . '/_date_information', [
+    'date' => $date,
+    'receiver' => $receiver,
+]) ?>
+
+--
+
+Direkt zum Termin: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date->id) ?>
diff --git a/locale/de/LC_MAILS/date_created.php b/locale/de/LC_MAILS/date_created.php
new file mode 100644
index 0000000000000000000000000000000000000000..d323b944c799d7b1534ff98e9a7d681b32627366
--- /dev/null
+++ b/locale/de/LC_MAILS/date_created.php
@@ -0,0 +1,10 @@
+<?= $date->editor->getFullName() ?> hat einen Termin im Kalender eingetragen.
+
+<?= $this->render_partial(__DIR__ . '/_date_information', [
+    'date' => $date,
+    'receiver' => $receiver,
+]) ?>
+
+--
+
+Direkt zum Termin: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date->id) ?>
diff --git a/locale/de/LC_MAILS/date_deleted.php b/locale/de/LC_MAILS/date_deleted.php
new file mode 100644
index 0000000000000000000000000000000000000000..0ff6ef80a9d7bb33f02b0f5b1aba4c9f00de5f94
--- /dev/null
+++ b/locale/de/LC_MAILS/date_deleted.php
@@ -0,0 +1,6 @@
+<?= $date->editor->getFullName() ?> hat einen Termin im Kalender gelöscht.
+
+<?= $this->render_partial(__DIR__ . '/_date_information', [
+    'date' => $date,
+    'receiver' => $receiver,
+]) ?>
diff --git a/locale/de/LC_MAILS/date_participation.php b/locale/de/LC_MAILS/date_participation.php
new file mode 100644
index 0000000000000000000000000000000000000000..11b0fb29a20b4f97aa592e609adefa11b19f9df3
--- /dev/null
+++ b/locale/de/LC_MAILS/date_participation.php
@@ -0,0 +1,14 @@
+<? if ($date_assignment->participation === 'ACCEPTED') : ?>
+<?= $date_assignment->user->getFullName() ?> hat Ihren Termin angenommen.
+<? elseif ($date_assignment->participation === 'DECLINED') : ?>
+<?= $date_assignment->user->getFullName() ?> hat Ihren Termin abgelehnt.
+<? endif ?>
+
+<?= $this->render_partial(__DIR__ . '/_date_information', [
+    'date' => $date_assignment->calendar_date,
+    'receiver' => $date_assignment->calendar_date->author,
+]) ?>
+
+--
+
+Direkt zum Termin: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date_assignment->calendar_date->id) ?>
diff --git a/locale/en/LC_MAILS/_date_information.php b/locale/en/LC_MAILS/_date_information.php
new file mode 100644
index 0000000000000000000000000000000000000000..a0cb470287ca79ecbf607131c29e76773d9a7556
--- /dev/null
+++ b/locale/en/LC_MAILS/_date_information.php
@@ -0,0 +1,24 @@
+*Time:* <?= date('d.m.Y H:i', $date->begin) ?> - <?= date('d.m.Y H:i', $date->end) ?>
+
+*Title:* <?= $date->title ?>
+
+<?= $date->description ?? '' ?>
+
+--
+
+<? if ($date->category) : ?>
+*Category:* <?= $date->category ?>
+<? endif ?>
+
+*Access:* <?= $date->getAccessAsString() ?>
+
+<? if ($date->repetition_type) : ?>
+*Repetition:* <?= $date->getRepetitionAsString() ?>
+<? endif ?>
+
+<? if (Config::get()->CALENDAR_GROUP_ENABLE && count($date->calendars) > 1) : ?>
+*Participants:*
+<? foreach($date->getParticipantsAsStringArray($receiver->user_id) as $participant_string) : ?>
+- <?= $participant_string ?>
+<? endforeach ?>
+<? endif ?>
diff --git a/locale/en/LC_MAILS/date_changed.php b/locale/en/LC_MAILS/date_changed.php
new file mode 100644
index 0000000000000000000000000000000000000000..ca1d670f83e3da8b51c8288ffd9d4e78357ca17c
--- /dev/null
+++ b/locale/en/LC_MAILS/date_changed.php
@@ -0,0 +1,10 @@
+<?= $date->editor->getFullName() ?> has modified a date in the calendar.
+
+<?= $this->render_partial(__DIR__ . '/_date_information', [
+    'date' => $date,
+    'receiver' => $receiver,
+]) ?>
+
+--
+
+Go to date: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date->id) ?>
diff --git a/locale/en/LC_MAILS/date_created.php b/locale/en/LC_MAILS/date_created.php
new file mode 100644
index 0000000000000000000000000000000000000000..bc805aee6baa8e7d03e9035f211d53667d531de6
--- /dev/null
+++ b/locale/en/LC_MAILS/date_created.php
@@ -0,0 +1,10 @@
+<?= $date->editor->getFullName() ?> has entered a date in the calendar.
+
+<?= $this->render_partial(__DIR__ . '/_date_information', [
+    'date' => $date,
+    'receiver' => $receiver,
+]) ?>
+
+--
+
+Go to date: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date->id) ?>
diff --git a/locale/en/LC_MAILS/date_deleted.php b/locale/en/LC_MAILS/date_deleted.php
new file mode 100644
index 0000000000000000000000000000000000000000..b4bafd66c40828a77c9581b662356168940f43e0
--- /dev/null
+++ b/locale/en/LC_MAILS/date_deleted.php
@@ -0,0 +1,6 @@
+<?= $date->editor->getFullName() ?> has deleted a date in the calendar.
+
+<?= $this->render_partial(__DIR__ . '/_date_information', [
+    'date' => $date,
+    'receiver' => $receiver,
+]) ?>
diff --git a/locale/en/LC_MAILS/date_participation.php b/locale/en/LC_MAILS/date_participation.php
new file mode 100644
index 0000000000000000000000000000000000000000..f76113440a74a87726299b9bd17ceafc7427149a
--- /dev/null
+++ b/locale/en/LC_MAILS/date_participation.php
@@ -0,0 +1,14 @@
+<? if ($date_assignment->participation === 'ACCEPTED') : ?>
+<?= $date_assignment->user->getFullName() ?> has accepted your date.
+<? elseif ($date_assignment->participation === 'DECLINED') : ?>
+<?= $date_assignment->user->getFullName() ?> has declined your date.
+<? endif ?>
+
+<?= $this->render_partial(__DIR__ . '/_date_information', [
+    'date' => $date_assignment->calendar_date,
+    'receiver' => $date_assignment->calendar_date->author,
+]) ?>
+
+--
+
+Go to date: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date_assignment->calendar_date->id) ?>
diff --git a/resources/assets/javascripts/bootstrap/calendar_dialog.js b/resources/assets/javascripts/bootstrap/calendar_dialog.js
deleted file mode 100644
index ee5ab4c76c690a711d29ead7703511ddfdf09ea6..0000000000000000000000000000000000000000
--- a/resources/assets/javascripts/bootstrap/calendar_dialog.js
+++ /dev/null
@@ -1,11 +0,0 @@
-jQuery(document).on('click', 'td.calendar-day-edit, td.calendar-day-event', function(event) {
-    var elem = jQuery(this)
-        .find('a')
-        .first();
-    if (_.isString(elem.attr('href'))) {
-        STUDIP.Dialog.fromURL(elem.attr('href'), { title: elem.attr('title') });
-        event.preventDefault();
-    } else {
-        return false;
-    }
-});
diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js
index 1f4937d19c1710903bfff99130adacf767232a27..8f9e5fce6ab68f880befee27d6c1aacd69272dcb 100644
--- a/resources/assets/javascripts/bootstrap/forms.js
+++ b/resources/assets/javascripts/bootstrap/forms.js
@@ -427,6 +427,24 @@ STUDIP.ready(function () {
         });
     }
 
+    /*
+     * Form elements with the "simplevue" class are meant for forms that just need some vue components
+     * to do something fancy inside the form but which do not need the full functionality of the form builder.
+     */
+    let simple_vue_items = document.querySelectorAll('form .simplevue:not(.vueified)');
+    if (simple_vue_items.length > 0) {
+        STUDIP.Vue.load().then(({createApp}) => {
+            simple_vue_items.forEach(f => {
+                createApp({
+                    el: f,
+                    mounted() {
+                        this.$el.classList.add('vueified');
+                    }
+                });
+            });
+        });
+    }
+
     // Well, this is really nasty: Select2 can't determine the select
     // element's width if it is hidden (by itself or by its parent).
     // This is due to the fact that elements are not rendered when hidden
diff --git a/resources/assets/javascripts/bootstrap/fullcalendar.js b/resources/assets/javascripts/bootstrap/fullcalendar.js
index 62beaa9e9f2d7fde29ab018ebc63d9fcb138a876..44786d9d02eb20a1a66058719862954b438bfc58 100644
--- a/resources/assets/javascripts/bootstrap/fullcalendar.js
+++ b/resources/assets/javascripts/bootstrap/fullcalendar.js
@@ -44,4 +44,5 @@ STUDIP.ready(function () {
         });
     }
 
+    jQuery(document).on('change', '#date_select[data-calendar-control]', STUDIP.Fullcalendar.submitDatePicker);
 });
diff --git a/resources/assets/javascripts/bootstrap/resources.js b/resources/assets/javascripts/bootstrap/resources.js
index 25582d43bdf25c66c6484aec55ae8f2be1ff0c32..8c89b7f2e10475b238fef2613711c24440670c92 100644
--- a/resources/assets/javascripts/bootstrap/resources.js
+++ b/resources/assets/javascripts/bootstrap/resources.js
@@ -483,7 +483,7 @@ STUDIP.ready(function () {
             } else if ($(this).hasClass('fc-today-button')
                 || $(this).hasClass('fc-prev-button')
                 || $(this).hasClass('fc-next-button')) {
-                updateDateURL();
+                STUDIP.Fullcalendar.updateDateURL();
             }
         }
     );
@@ -594,71 +594,11 @@ STUDIP.ready(function () {
         $('.booking-plan-allday_view').attr('href', url.toString());
     }
 
-    function submitDatePicker() {
-        var picked = $('#booking-plan-jmpdate').val();
-        var iso_date_string = '';
-        if(picked) {
-            if (picked.includes('.')) {
-                let [day, month, year] = picked.split('.');
-                iso_date_string = year.padStart(4, "20") + '-' + month.padStart(2, "0") + '-' + day.padStart(2, "0");
-            } else if (picked.includes('/')) {
-                let [day, month, year] = picked.split('/');
-                iso_date_string = year.padStart(4, "20") + '-' + month.padStart(2, "0") + '-' + day.padStart(2, "0");
-            } else if (picked.includes('-')) {
-                iso_date_string = picked;
-            }
-        }
-        if (iso_date_string) {
-            $('*[data-resources-fullcalendar="1"]').each(function () {
-                this.calendar.gotoDate(iso_date_string);
-            });
-            updateDateURL();
-        }
-    }
-
-    function updateDateURL() {
-        let changedMoment;
-        $('[data-resources-fullcalendar="1"]').each(function () {
-            changedMoment = $(this)[0].calendar.getDate();
-        });
-        if (changedMoment) {
-            let changedDate = STUDIP.Fullcalendar.toRFC3339String(changedMoment).split('T')[0];
-            //Get the timestamp:
-            let timeStamp = changedMoment.getTime() / 1000;
-
-            $('a.resource-bookings-actions').each(function () {
-                const url = new URL(this.href);
-                url.searchParams.set('timestamp', timeStamp)
-                url.searchParams.set('defaultDate', changedDate)
-                this.href = url.toString();
-            });
-
-            // Now change the URL of the window.
-            const url = new URL(window.location.href);
-            url.searchParams.set('defaultDate', changedDate);
-
-            // Update url in history
-            history.pushState({}, null, url.toString());
-
-            // Adjust links accordingly
-            url.searchParams.delete('allday');
-            $('.booking-plan-std_view').attr('href', url.toString());
-
-            url.searchParams.set('allday', 1);
-            $('.booking-plan-allday_view').attr('href', url.toString());
-
-            // Update sidebar value
-            $('#booking-plan-jmpdate').val(changedMoment.toLocaleDateString('de-DE'));
-
-            //Store the date in the sessionStorage:
-            sessionStorage.setItem('booking_plan_date', changedDate)
-        }
-    }
 
     jQuery('#booking-plan-jmpdate').datepicker(
         {
             dateFormat: 'dd.mm.yy',
-            onClose: submitDatePicker
+            onClose: STUDIP.Fullcalendar.submitDatePicker
         }
     );
     jQuery('.resource-booking-time-fields input[type="date"]').datepicker(
diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js
index 37bec894a65f27309104f570e9fd3c4444f46759..bb03231a39358a1fc2b20a02a675631d48f17174 100644
--- a/resources/assets/javascripts/entry-base.js
+++ b/resources/assets/javascripts/entry-base.js
@@ -37,7 +37,6 @@ import "./bootstrap/multi_person_search.js"
 import "./bootstrap/skip_links.js"
 import "./bootstrap/i18n_input.js"
 import "./bootstrap/forms.js"
-import "./bootstrap/calendar_dialog.js"
 import "./bootstrap/drag_and_drop_upload.js"
 import "./bootstrap/admin_sem_classes.js"
 import "./bootstrap/cronjobs.js"
diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js
index 8981e950ef3b3e128a172e755fcda3354f3e5bc7..2d592be2ca452e85869394e4a1a26aed4730da90 100644
--- a/resources/assets/javascripts/init.js
+++ b/resources/assets/javascripts/init.js
@@ -14,13 +14,13 @@ import Blubber from './lib/blubber.js';
 import Browse from './lib/browse.js';
 import Cache from './lib/cache.js';
 import Calendar from './lib/calendar.js';
-import CalendarDialog from './lib/calendar_dialog.js';
 import Clipboard from './lib/clipboard.js';
 import Cookie from './lib/cookie.js';
 import CourseWizard from './lib/course_wizard.js';
 import { createURLHelper } from './lib/url_helper.ts';
 import CSS from './lib/css.js';
 import Dates from './lib/dates.js';
+import DateTime from './lib/datetime.js';
 import Dialog from './lib/dialog.js';
 import DragAndDropUpload from './lib/drag_and_drop_upload.js';
 import enrollment from './lib/enrollment.js';
@@ -31,6 +31,7 @@ import FilesDashboard from './lib/files_dashboard.js';
 import Folders from './lib/folders.js';
 import Forms from './lib/forms.js';
 import Forum from './lib/forum.js';
+import Fullcalendar from './lib/fullcalendar.js';
 import Fullscreen from './lib/fullscreen.js';
 import GlobalSearch from './lib/global_search.js';
 import HeaderMagic from './lib/header_magic.js';
@@ -101,11 +102,11 @@ window.STUDIP = _.assign(window.STUDIP || {}, {
     Browse,
     Cache,
     Calendar,
-    CalendarDialog,
     Cookie,
     CourseWizard,
     CSS,
     Dates,
+    DateTime,
     Dialog,
     DragAndDropUpload,
     enrollment,
@@ -116,6 +117,7 @@ window.STUDIP = _.assign(window.STUDIP || {}, {
     Folders,
     Forms,
     Forum,
+    Fullcalendar,
     Fullscreen,
     Gettext,
     GlobalSearch,
diff --git a/resources/assets/javascripts/lib/calendar_dialog.js b/resources/assets/javascripts/lib/calendar_dialog.js
deleted file mode 100644
index e42a1490dca9c475c5c4e7bf207cb7c7a869e2d7..0000000000000000000000000000000000000000
--- a/resources/assets/javascripts/lib/calendar_dialog.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import Dialog from './dialog.js';
-
-const CalendarDialog = {
-    closeMps: function(form) {
-        var added_users = [];
-        jQuery('#calendar-manage_access_selectbox option:selected').each(function() {
-            added_users[added_users.length] = jQuery(this).attr('value');
-        });
-        jQuery.ajax({
-            url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/calendar/single/add_users/',
-            data: {
-                added_users: added_users
-            },
-            type: 'post'
-        });
-        jQuery(form)
-            .closest('.ui-dialog-content')
-            .dialog('close');
-        Dialog.fromURL(jQuery('#calendar-open-manageaccess').attr('href'));
-        return false;
-    },
-
-    removeUser: function(element) {
-        var url = jQuery(element).attr('href');
-        jQuery(element).removeAttr('href');
-        jQuery.ajax({
-            url: url,
-            type: 'get',
-            success: function() {
-                var head_tr = jQuery(element)
-                    .closest('tr')
-                    .prev('.calendar-user-head');
-                jQuery(element)
-                    .closest('tr')
-                    .remove();
-                if (head_tr.nextUntil('.calendar-user-head').length === 0) {
-                    head_tr.remove();
-                }
-            }
-        });
-        return false;
-    },
-
-    addException: function() {
-        var exc_date = jQuery('#exc-date').val();
-        var exists = jQuery('#exc-dates input').is("input[value='" + exc_date + "']");
-        if (!exists) {
-            var compiled = _.template(
-                '<li><label>' +
-                    '<input type="checkbox" name="del_exc_dates[]" value="<%- excdate %>" style="display: none">' +
-                    '<span><%- excdate %><img src="' +
-                    STUDIP.ASSETS_URL +
-                    'images/icons/blue/trash.svg' +
-                    '"></span></label>' +
-                    '<input type="hidden" name="exc_dates[]" value="<%- excdate %>">' +
-                    '</li>'
-            );
-            jQuery('#exc-dates').append(compiled({ excdate: exc_date, link: '' }));
-        }
-        return false;
-    }
-};
-
-export default CalendarDialog;
diff --git a/resources/assets/javascripts/lib/dates.js b/resources/assets/javascripts/lib/dates.js
index 3be67c057582b271a547de18421c123e0a041092..ccb67a842221d01ff982d52d3ec30611300b7578 100644
--- a/resources/assets/javascripts/lib/dates.js
+++ b/resources/assets/javascripts/lib/dates.js
@@ -4,6 +4,7 @@ const Dates = {
             termin_id = $('#new_topic')
                 .closest('[data-termin-id]')
                 .data().terminId;
+        let course_id = jQuery('#new_topic').closest('[data-course-id]').data().courseId;
 
         if (!topic_name) {
             $('#new_topic').focus();
@@ -12,7 +13,8 @@ const Dates = {
 
         $.post(STUDIP.URLHelper.getURL('dispatch.php/course/dates/add_topic'), {
             title: topic_name,
-            termin_id: termin_id
+            termin_id: termin_id,
+            cid: course_id
         }).done(function(response) {
             if (response.li !== undefined) {
                 $('#new_topic')
diff --git a/resources/assets/javascripts/lib/datetime.js b/resources/assets/javascripts/lib/datetime.js
new file mode 100644
index 0000000000000000000000000000000000000000..d58fac85b84434a549075409f5ac974d92c7f4c4
--- /dev/null
+++ b/resources/assets/javascripts/lib/datetime.js
@@ -0,0 +1,61 @@
+import { $gettext, $gettextInterpolate } from "./gettext.ts";
+
+
+const DateTime = {
+    /**
+     * A helper method for padding strings with leading zeros.
+     * @param what The date to pad.
+     * @param length The length of the string to output.
+     * @returns {string} A padded version of $what.
+     */
+    pad(what, length = 2) {
+        return `00000000${what}`.substr(-length);
+    },
+
+    /**
+     * Returns an ISO representation of the specified Date object.
+     * in the format YYYY-MM-DD.
+     *
+     * @param date The Date object to format as ISO date.
+     * @returns {string} The ISO date string of the Date object.
+     */
+    getISODate(date) {
+        return date.getFullYear() + '-' + this.pad(date.getMonth() + 1) + '-' + date.getDate();
+    },
+
+    /**
+     * Returns a formatted version of the specified Date object
+     * in the Stud.IP date formatting.
+     *
+     * @param date The Date object to be formatted.
+     * @param relative_value Whether to return a relative time value (true)
+     *     or an absolute one (false). Defaults to false.
+     * @param date_only Whether to return the date only (true) or date and time (false).
+     *     Defaults to false. Only regarded when $relative_value is false.
+     * @returns {*|string} The date, formatted according to the Stud.IP format for dates.
+     */
+    getStudipDate(date, relative_value = false, date_only = false) {
+        if (relative_value) {
+            let now = Date.now();
+            if (now - date < 1 * 60 * 1000) {
+                return $gettext('Jetzt');
+            }
+            if (now - date < 2 * 60 * 60 * 1000) {
+                return $gettextInterpolate(
+                    $gettext('Vor %{ minutes } Minuten'),
+                    {minutes: Math.floor((now - date) / (1000 * 60))}
+                );
+            }
+            return this.pad(date.getHours()) + ':' + this.pad(date.getMinutes());
+        }
+
+        if (date_only) {
+            return this.pad(date.getDate()) + '.' + this.pad(date.getMonth() + 1) + '.' + date.getFullYear();
+        }
+
+        return this.pad(date.getDate()) + '.' + this.pad(date.getMonth() + 1) + '.' + date.getFullYear() + ' ' + this.pad(date.getHours()) + ':' + this.pad(date.getMinutes());
+    }
+};
+
+
+export default DateTime;
diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js
index ef891507daa392cf3cff8344ea43d266ea3ecc6d..b5bd78d49a709df1455c2772e1fd5bc7ba2f4ef7 100644
--- a/resources/assets/javascripts/lib/fullcalendar.js
+++ b/resources/assets/javascripts/lib/fullcalendar.js
@@ -162,6 +162,16 @@ class Fullcalendar
                     end: this.toRFC3339String(info.event.end)
                 }
             }).fail(info.revert);
+        } else if (info.event.extendedProps.studip_api_urls.resize_dialog) {
+            STUDIP.Dialog.fromURL(
+                info.event.extendedProps.studip_api_urls.resize_dialog,
+                {
+                    data: {
+                        begin: this.toRFC3339String(info.event.start),
+                        end: this.toRFC3339String(info.event.end)
+                    }
+                }
+            );
         }
     }
 
@@ -242,40 +252,80 @@ class Fullcalendar
 
         var drop_resource_id = info.newResource ? info.newResource.id : info.event.extendedProps.studip_range_id;
 
-        if (info.event.extendedProps.studip_api_urls.move) {
+        if (info.event.extendedProps.studip_api_urls.move || info.event.extendedProps.studip_api_urls.move_dialog) {
+            let move_dialog = info.event.extendedProps.studip_api_urls.move_dialog;
             if (info.event.allDay) {
-                $.post({
-                    async: false,
-                    url: info.event.extendedProps.studip_api_urls.move,
-                    data: {
-                        resource_id: drop_resource_id,
-                        begin: this.toRFC3339String(info.event.start.setHours(0,0,0)),
-                        end: this.toRFC3339String(info.event.start.setHours(23,59,59))
-                    }
-                }).fail(info.revert);
+                if (move_dialog) {
+                    STUDIP.Dialog.fromURL(
+                        move_dialog,
+                        {
+                            data: {
+                                resource_id: drop_resource_id,
+                                begin: this.toRFC3339String(info.event.start.setHours(0, 0, 0)),
+                                end: this.toRFC3339String(info.event.start.setHours(23, 59, 59))
+                            }
+                        }
+                    );
+                } else {
+                    jQuery.post({
+                        async: false,
+                        url: info.event.extendedProps.studip_api_urls.move,
+                        data: {
+                            resource_id: drop_resource_id,
+                            begin: this.toRFC3339String(info.event.start.setHours(0, 0, 0)),
+                            end: this.toRFC3339String(info.event.start.setHours(23, 59, 59))
+                        }
+                    }).fail(info.revert);
+                }
             } else if (info.event.end === null) {
-                var real_end = new Date();
+                let real_end = new Date();
                 real_end.setTime(info.event.start.getTime());
                 real_end.setHours(info.event.start.getHours()+2);
-                $.post({
-                    async: false,
-                    url: info.event.extendedProps.studip_api_urls.move,
-                    data: {
-                        resource_id: drop_resource_id,
-                        begin: this.toRFC3339String(info.event.start),
-                        end: this.toRFC3339String(real_end)
-                    }
-                }).fail(info.revert);
+                if (move_dialog) {
+                    STUDIP.Dialog.fromURL(
+                        move_dialog,
+                        {
+                            data: {
+                                resource_id: drop_resource_id,
+                                begin: this.toRFC3339String(info.event.start),
+                                end: this.toRFC3339String(real_end)
+                            }
+                        }
+                    );
+                } else {
+                    jQuery.post({
+                        async: false,
+                        url: info.event.extendedProps.studip_api_urls.move,
+                        data: {
+                            resource_id: drop_resource_id,
+                            begin: this.toRFC3339String(info.event.start),
+                            end: this.toRFC3339String(real_end)
+                        }
+                    }).fail(info.revert);
+                }
             } else {
-                $.post({
-                    async: false,
-                    url: info.event.extendedProps.studip_api_urls.move,
-                    data: {
-                        resource_id: drop_resource_id,
-                        begin: this.toRFC3339String(info.event.start),
-                        end: this.toRFC3339String(info.event.end)
-                    }
-                }).fail(info.revert);
+                if (move_dialog) {
+                    STUDIP.Dialog.fromURL(
+                        move_dialog,
+                        {
+                            data: {
+                                resource_id: drop_resource_id,
+                                begin: this.toRFC3339String(info.event.start),
+                                end: this.toRFC3339String(info.event.end)
+                            }
+                        }
+                    );
+                } else {
+                    jQuery.post({
+                        async: false,
+                        url: info.event.extendedProps.studip_api_urls.move,
+                        data: {
+                            resource_id: drop_resource_id,
+                            begin: this.toRFC3339String(info.event.start),
+                            end: this.toRFC3339String(info.event.end)
+                        }
+                    }).fail(info.revert);
+                }
             }
         }
     }
@@ -370,6 +420,12 @@ class Fullcalendar
             studip_functions: [],
             resourceAreaWidth: '20%',
             select (selectionInfo) {
+                let calendar_config = JSON.parse(selectionInfo.view.context.calendar.el.dataset.config);
+                let dialog_size = 'auto';
+                if (calendar_config.dialog_size !== undefined) {
+                    dialog_size = calendar_config.dialog_size;
+                }
+
                 if (!selectionInfo.view.viewSpec.options.editable || !selectionInfo.view.viewSpec.options.studip_urls) {
                     //The calendar isn't editable.
                     return;
@@ -380,15 +436,19 @@ class Fullcalendar
                             data: {
                                 begin: selectionInfo.start.getTime()/1000,
                                 end: selectionInfo.end.getTime()/1000,
-                                ressource_id: selectionInfo.resource.id
-                            }
+                                ressource_id: selectionInfo.resource.id,
+                                all_day: selectionInfo.allDay ? '1' : '0'
+                            },
+                            size: dialog_size
                         });
                     } else {
                         STUDIP.Dialog.fromURL(selectionInfo.view.viewSpec.options.studip_urls.add, {
                             data: {
                                 begin: selectionInfo.start.getTime()/1000,
-                                end: selectionInfo.end.getTime()/1000
-                            }
+                                end: selectionInfo.end.getTime()/1000,
+                                all_day: selectionInfo.allDay ? '1' : '0'
+                            },
+                            size: dialog_size
                         });
                     }
                 }
@@ -421,15 +481,33 @@ class Fullcalendar
                 if (extended_props.studip_view_urls === undefined) {
                     return;
                 }
+                let calendar_config = JSON.parse(eventClickInfo.view.context.calendar.el.dataset.config);
+                let dialog_size = 'auto';
+                if (calendar_config.dialog_size !== undefined) {
+                    //Use the configured default dialog size for the fullcalendar instance:
+                    dialog_size = calendar_config.dialog_size;
+                }
+                if (extended_props.dialog_size !== undefined) {
+                    //Use the dialog size of the event:
+                    dialog_size = extended_props.dialog_size;
+                }
                 if (!event.startEditable && extended_props.studip_view_urls.show) {
                     STUDIP.Dialog.fromURL(
-                        STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show)
-                    );
-                } else if (event.startEditable && extended_props.studip_view_urls.edit) {
-                    STUDIP.Dialog.fromURL(
-                        STUDIP.URLHelper.getURL(extended_props.studip_view_urls.edit),
-                        {'size': 'big'}
+                        STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show),
+                        {size: dialog_size}
                     );
+                } else if (event.startEditable) {
+                    if (extended_props.studip_view_urls.edit) {
+                        STUDIP.Dialog.fromURL(
+                            STUDIP.URLHelper.getURL(extended_props.studip_view_urls.edit),
+                            {size: dialog_size}
+                        );
+                    } else if (extended_props.studip_view_urls.show) {
+                        STUDIP.Dialog.fromURL(
+                            STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show),
+                            {size: dialog_size}
+                        );
+                    }
                 }
                 return false;
             },
@@ -612,6 +690,77 @@ class Fullcalendar
 
         return this.init(node, config);
     }
+
+    static submitDatePicker() {
+        let picked_date = jQuery('#booking-plan-jmpdate').val();
+        let booking_plan_datepicker = true;
+        if (!picked_date) {
+            //Not a booking plan date selector.
+            picked_date = jQuery('#date_select').val();
+            booking_plan_datepicker = false;
+        }
+        let iso_date_string = '';
+        if (picked_date) {
+            if (picked_date.includes('.')) {
+                let [day, month, year] = picked_date.split('.');
+                iso_date_string = year.padStart(4, '20') + '-' + month.padStart(2, '0') + '-' + day.padStart(2, '0');
+            } else if (picked_date.includes('/')) {
+                let [day, month, year] = picked_date.split('/');
+                iso_date_string = year.padStart(4, '20') + '-' + month.padStart(2, '0') + '-' + day.padStart(2, '0');
+            } else if (picked_date.includes('-')) {
+                iso_date_string = picked_date;
+            }
+        }
+        if (iso_date_string) {
+            jQuery('[data-fullcalendar="1"],[data-resources-fullcalendar="1"]').each(function () {
+                this.calendar.gotoDate(iso_date_string);
+            });
+            if (booking_plan_datepicker) {
+                Fullcalendar.updateDateURL();
+            }
+        }
+    }
+
+    static updateDateURL() {
+        let changedMoment;
+        jQuery('[data-fullcalendar="1"],[data-resources-fullcalendar="1"]').each(function () {
+            changedMoment = this.calendar.getDate();
+        });
+        if (changedMoment) {
+            let changed_date = STUDIP.Fullcalendar.toRFC3339String(changedMoment).split('T')[0];
+            //Get the timestamp:
+            let timestamp = changedMoment.getTime() / 1000;
+
+            jQuery('a.resource-bookings-actions').each(function () {
+                const url = new URL(this.href);
+                url.searchParams.set('timestamp', timestamp)
+                url.searchParams.set('defaultDate', changed_date)
+                this.href = url.toString();
+            });
+
+            // Now change the URL of the window.
+            const url = new URL(window.location.href);
+            url.searchParams.set('defaultDate', changed_date);
+
+            // Update url in history
+            history.pushState({}, null, url.toString());
+
+            // Adjust links accordingly
+            url.searchParams.delete('allday');
+            jQuery('.booking-plan-std_view').attr('href', url.toString());
+
+            url.searchParams.set('allday', 1);
+            jQuery('.booking-plan-allday_view').attr('href', url.toString());
+
+            // Update sidebar value
+            let element = jQuery('#booking-plan-jmpdate,#date_select').first();
+            element.val(changedMoment.toLocaleDateString('de-DE'));
+            if (element.is('#booking-plan-jmpdate')) {
+                //Store the date in the sessionStorage:
+                sessionStorage.setItem('booking_plan_date', changed_date);
+            }
+        }
+    }
 }
 
 export default Fullcalendar;
diff --git a/resources/assets/stylesheets/highcontrast.scss b/resources/assets/stylesheets/highcontrast.scss
index 6c822b17be500b1bae62685bcaed259bea251faa..47bfbf213b7cd63dfac2e54344f1ebc55ce6978b 100644
--- a/resources/assets/stylesheets/highcontrast.scss
+++ b/resources/assets/stylesheets/highcontrast.scss
@@ -515,26 +515,6 @@ form.default fieldset.collapsable.collapsed legend {
 }
 
 /* Stundenplan / Terminkalender */
-.celltoday {
-    background-color: $white;
-    border: 1px solid $black;
-
-    > a {
-        color: $black;
-        font-size: 1.5em;
-    }
-}
-a:link.calhead {
-    color: $contrast-blue;
-}
-
-.calhead label {
-    color: $contrast-blue !important;
-
-    &:hover {
-        text-decoration: underline;
-    }
-}
 
 a .hidden-tiny-down {
     color: $contrast-blue !important;
@@ -566,40 +546,6 @@ a .hidden-tiny-down {
 
 /* Calendar categories */
 
-span li.calendar-category1,
-ul li.calendar-category1,
-span li.calendar-category2,
-ul li.calendar-category2,
-span li.calendar-category3,
-ul li.calendar-category3,
-span li.calendar-category4,
-ul li.calendar-category4,
-span li.calendar-category5,
-ul li.calendar-category5,
-span li.calendar-category6,
-ul li.calendar-category6,
-span li.calendar-category7,
-ul li.calendar-category7,
-span li.calendar-category8,
-ul li.calendar-category8,
-span li.calendar-category9,
-ul li.calendar-category9,
-span li.calendar-category10,
-ul li.calendar-category10,
-span li.calendar-category11,
-ul li.calendar-category11,
-span li.calendar-category12,
-ul li.calendar-category12,
-span li.calendar-category13,
-ul li.calendar-category13,
-span li.calendar-category14,
-ul li.calendar-category14,
-span li.calendar-category15,
-ul li.calendar-category15 {
-    color: $black;
-
-}
-
 div.schedule_entry {
     dl {
         &.hover:hover { opacity: unset; }
@@ -772,244 +718,6 @@ div.schedule_entry {
     }
 }
 
-table.calendar-week,
-table.calendar-day {
-    tbody tr td {
-        &.calendar-day-event {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-day-event;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $calendar-day-event;
-        }
-        &.calendar-category1,
-        &.calendar-course-category5 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-1;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-1;
-        }
-        &.calendar-category2,
-        &.calendar-course-category1 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-2;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-2;
-        }
-        &.calendar-category3,
-        &.calendar-course-category2 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color:  $calendar-category-3;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-3;
-        }
-        &.calendar-category4,
-        &.calendar-course-category3 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-4;
-                overflow: hidden;
-                color: $black;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-4;
-        }
-        &.calendar-category5,
-        &.calendar-course-category4 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-5;
-                overflow: hidden;
-                color: $black;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-5;
-        }
-        &.calendar-category6,
-        &.calendar-course-category6 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-6;
-                overflow: hidden;
-                color: $black;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-6;
-        }
-        &.calendar-category7,
-        &.calendar-course-category8 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-7;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-7;
-        }
-        &.calendar-category8,
-        &.calendar-course-category9 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-8;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-8;
-        }
-        &.calendar-category9,
-        &.calendar-course-category10 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-9;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-9;
-        }
-        &.calendar-category10,
-        &.calendar-course-category11 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-10;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-10;
-        }
-        &.calendar-category11,
-        &.calendar-course-category12 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-11;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-11;
-        }
-        &.calendar-category12,
-        &.calendar-course-category13 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-12;
-                overflow: hidden;
-                color: $black;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-12;
-        }
-        &.calendar-category13,
-        &.calendar-course-category14 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-13;
-                overflow: hidden;
-                color: $black;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-13;
-        }
-        &.calendar-category14,
-        &.calendar-course-category15 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-14;
-                overflow: hidden;
-                color: $black;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-14;
-        }
-        &.calendar-category15,
-        &.calendar-course-category7 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-15;
-                overflow: hidden;
-                color: $black;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-15;
-        }
-        &.calendar-category255,
-        &.calendar-course-category255 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $calendar-category-255;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $calendar-category-255;
-        }
-        /* Termin von im Stundenplan vorgemerkter Kurs */
-        &.calendar-course-category256 {
-            a {
-                color: $contrast-blue !important;
-            }
-            div:first-child {
-                background-color: $contrast-blue-medium;
-                overflow: hidden;
-            }
-            background: $white;
-            border: solid 1px $contrast-blue-medium;
-        }
-    }
-}
-
-
-/* links */
-div.index_container div.index_main nav div.login_link a {
-    text-decoration: underline;
-
-    p {
-        color: $black !important;
-    }
-}
-
 /* underlined links only in main content,not in navigation */
 a,
 a:link,
diff --git a/resources/assets/stylesheets/less/calendar.less b/resources/assets/stylesheets/less/calendar.less
deleted file mode 100644
index edb2e570d157c28062deab09d3a0eed3267cd912..0000000000000000000000000000000000000000
--- a/resources/assets/stylesheets/less/calendar.less
+++ /dev/null
@@ -1,587 +0,0 @@
-// TODO: LESSify
-
-/* --- Styles fuer Terminkalender ------------------------------------------- */
-a.day {
-    font-weight: bold;
-}
-
-a.sday {
-    color: var(--red);
-    font-weight: bold;
-}
-
-a.hday {
-    color: var(--red-80);
-    font-weight: bold;
-}
-
-span.kwmin {
-    color: var(--dark-gray-color-80);
-    font-weight: bold;
-}
-
-a.lightday {
-    color: var(--base-color-40);
-    font-weight: bold;
-}
-
-a.lightsday {
-    color: var(--red-40);
-    font-weight: bold;
-}
-
-.inday {
-    font-size: 8pt;
-}
-
-.precol1w {
-    font-size: 12pt;
-    font-weight: bold;
-    color: var(--light-gray-color);
-    text-align: center;
-    vertical-align: top;
-}
-
-.precol2w {
-    font-size: 8pt;
-    font-weight: bold;
-    color: var(--light-gray-color);
-    text-align: center;
-}
-
-td.calhead, div.calhead {
-    font-size: 18pt;
-    font-weight: bold;
-    color: var(--light-gray-color);
-    text-align: center;
-}
-
-a:link.calhead {
-    color: var(--base-color-60);
-    white-space: nowrap;
-    font-weight: bold;
-}
-
-a:hover.calhead {
-    color: var(--red-60);
-}
-
-.calhead label {
-  cursor: pointer;
-  &:hover {
-    color: var(--base-color-40);
-  }
-
-  .media-breakpoint-small-down({
-    .button();
-
-    img {
-      padding-left: 0.5em;
-      vertical-align: middle;
-    }
-  })
-}
-
-.celltoday {
-    background-color: var(--red-20);
-}
-
-td.weekend {
-    background-color: var(--dark-gray-color-15);
-}
-
-td.weekday {
-    background-color: var(--dark-gray-color-5);
-}
-
-td.current {
-    padding: 2px;
-    border: 2px solid var(--red);
-}
-
-table.calendar-week, table.calendar-day {
-    border-spacing: 0;
-    table-layout: fixed;
-    td {
-        padding: 0;
-    }
-}
-
-table.calendar-month {
-    width: 100%;
-    tr td {
-        max-width: 90px;
-        min-width: 90px;
-        vertical-align: top;
-    }
-}
-
-td.month {
-    background-color: fadeout(darken(@dark-gray-color-15, 5), 30);
-    padding: 3px;
-    div {
-        width: 90%;
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
-    }
-}
-
-td.lightmonth {
-    background-color: fadeout(darken(@dark-gray-color-5, 5), 30);
-    padding: 3px;
-    div {
-        width: 90%;
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
-    }
-}
-
-table.calendar-month td.calendar-month-week {
-    text-align: center;
-    vertical-align: middle;
-    height: 80px;
-    width: 80px;
-}
-
-td.weekdayevents {
-    width: 90px;
-}
-
-nav.calendar-nav {
-  display: flex;
-  align-items: center;
-  padding-bottom: 1em;
-
-  > div {
-    flex: 1 1 auto;
-  }
-
-  .calhead {
-    color: var(--base-color);
-  }
-}
-
-.calendar-day-edit {
-    text-align: right;
-    font-size: 0.8em;
-}
-
-.calendar-week tbody tr, .calendar-day tbody tr {
-    transition: background-color 0.3s;
-    &:hover {
-        background-color: fadeout(@dark-gray-color-5, 40%);
-    }
-    & td {
-        padding: 3px;
-        border-bottom: 1px solid var(--dark-gray-color-5);
-    }
-}
-
-.calendar-day-event-title {
-    overflow: hidden;
-    text-overflow: ellipsis;
-    a {
-        color: var(--dark-gray-color);
-    }
-}
-
-.calendar-category-mixin(@color-bg) {
-    vertical-align: top;
-    font-size: 11px;
-    color: var(--white);
-    padding: 0;
-    /* necessary for All-day Events */
-    a {
-        color: contrast(@color-bg, black, white, 60%);
-        font-weight: 600;
-    }
-}
-
-.calendar-single-year {
-  .calendar-single-year--table {
-
-    > thead th {
-      min-width: 5em;
-      text-align: left;
-    }
-
-    .yday {
-      white-space: nowrap;
-    }
-  }
-}
-/* --- Coloring Styles for Personal TERMIN Categories ---------------------------------------------- */
-select, ul, span {
-    option, li, span, input[type="radio"] {
-        &.calendar-category1 {
-            color: @calendar-category-1;
-        }
-        &.calendar-category2 {
-            color: @calendar-category-2;
-        }
-        &.calendar-category3 {
-            color: @calendar-category-3;
-        }
-        &.calendar-category4 {
-            color: @calendar-category-4;
-        }
-        &.calendar-category5 {
-            color: @calendar-category-5;
-        }
-        &.calendar-category6 {
-            color: @calendar-category-6;
-        }
-        &.calendar-category7 {
-            color: @calendar-category-7;
-        }
-        &.calendar-category8 {
-            color: @calendar-category-8;
-        }
-        &.calendar-category9 {
-            color: @calendar-category-9;
-        }
-        &.calendar-category10 {
-            color: @calendar-category-10;
-        }
-        &.calendar-category11 {
-            color: @calendar-category-11;
-        }
-        &.calendar-category12 {
-            color: @calendar-category-12;
-        }
-        &.calendar-category13 {
-            color: @calendar-category-13;
-        }
-        &.calendar-category14 {
-            color: @calendar-category-14;
-        }
-        &.calendar-category15 {
-            color: @calendar-category-15;
-        }
-        &.calendar-category255 {
-            color: @calendar-category-255;
-        }
-    }
-}
-
-table.calendar-week, table.calendar-day {
-    &  tbody tr td {
-        &.calendar-day-event {
-            div:first-child {
-                background-color: @calendar-day-event;
-                overflow: hidden;
-            }
-            background: @calendar-day-event-aux;
-            border: solid 1px @calendar-day-event;
-            .calendar-category-mixin(@calendar-day-event-aux);
-        }
-        &.calendar-category1, &.calendar-course-category5 {
-            div:first-child {
-                background-color: @calendar-category-1;
-                overflow: hidden;
-            }
-            background: @calendar-category-1-aux;
-            border: solid 1px @calendar-category-1;
-            .calendar-category-mixin(@calendar-category-1-aux);
-        }
-        &.calendar-category2, &.calendar-course-category1 {
-            div:first-child {
-                background-color: @calendar-category-2;
-                overflow: hidden;
-            }
-            background: @calendar-category-2-aux;
-            border: solid 1px @calendar-category-2;
-            .calendar-category-mixin(@calendar-category-2-aux);
-        }
-        &.calendar-category3, &.calendar-course-category2 {
-            div:first-child {
-                background-color:  @calendar-category-3;
-                overflow: hidden;
-            }
-            background:  @calendar-category-3-aux;
-            border: solid 1px @calendar-category-3;
-            .calendar-category-mixin(@calendar-category-3-aux);
-        }
-        &.calendar-category4, &.calendar-course-category3 {
-            div:first-child {
-                background-color: @calendar-category-4;
-                overflow: hidden;
-            }
-            background: @calendar-category-4-aux;
-            border: solid 1px @calendar-category-4;
-            .calendar-category-mixin(@calendar-category-4-aux);
-        }
-        &.calendar-category5, &.calendar-course-category4 {
-            div:first-child {
-                background-color: @calendar-category-5;
-                overflow: hidden;
-            }
-            background: @calendar-category-5-aux;
-            border: solid 1px @calendar-category-5;
-            .calendar-category-mixin(@calendar-category-5-aux);
-        }
-        &.calendar-category6, &.calendar-course-category6 {
-            div:first-child {
-                background-color: @calendar-category-6;
-                overflow: hidden;
-            }
-            background: @calendar-category-6-aux;
-            border: solid 1px @calendar-category-6;
-            .calendar-category-mixin(@calendar-category-6-aux);
-        }
-        &.calendar-category7, &.calendar-course-category8 {
-            div:first-child {
-                background-color: @calendar-category-7;
-                overflow: hidden;
-            }
-            background: @calendar-category-7-aux;
-            border: solid 1px @calendar-category-7;
-            .calendar-category-mixin(@calendar-category-7-aux);
-        }
-        &.calendar-category8, &.calendar-course-category9 {
-            div:first-child {
-                background-color: @calendar-category-8;
-                overflow: hidden;
-            }
-            background: @calendar-category-8-aux;
-            border: solid 1px @calendar-category-8;
-            .calendar-category-mixin(@calendar-category-8-aux);
-        }
-        &.calendar-category9, &.calendar-course-category10 {
-            div:first-child {
-                background-color: @calendar-category-9;
-                overflow: hidden;
-            }
-            background: @calendar-category-9-aux;
-            border: solid 1px @calendar-category-9;
-            .calendar-category-mixin(@calendar-category-9-aux);
-        }
-        &.calendar-category10, &.calendar-course-category11 {
-            div:first-child {
-                background-color: @calendar-category-10;
-                overflow: hidden;
-            }
-            background: @calendar-category-10-aux;
-            border: solid 1px @calendar-category-10;
-            .calendar-category-mixin(@calendar-category-10-aux);
-        }
-        &.calendar-category11, &.calendar-course-category12 {
-            div:first-child {
-                background-color: @calendar-category-11;
-                overflow: hidden;
-            }
-            background: @calendar-category-11-aux;
-            border: solid 1px @calendar-category-11;
-            .calendar-category-mixin(@calendar-category-11-aux);
-        }
-        &.calendar-category12, &.calendar-course-category13 {
-            div:first-child {
-                background-color: @calendar-category-12;
-                overflow: hidden;
-            }
-            background: @calendar-category-12-aux;
-            border: solid 1px @calendar-category-12;
-            .calendar-category-mixin(@calendar-category-12-aux);
-        }
-        &.calendar-category13, &.calendar-course-category14 {
-            div:first-child {
-                background-color: @calendar-category-13;
-                overflow: hidden;
-            }
-            background: @calendar-category-13-aux;
-            border: solid 1px @calendar-category-13;
-            .calendar-category-mixin(@calendar-category-13-aux);
-        }
-        &.calendar-category14, &.calendar-course-category15 {
-            div:first-child {
-                background-color: @calendar-category-14;
-                overflow: hidden;
-            }
-            background: @calendar-category-14-aux;
-            border: solid 1px @calendar-category-14;
-            .calendar-category-mixin(@calendar-category-14-aux);
-        }
-        &.calendar-category15, &.calendar-course-category7 {
-            div:first-child {
-                background-color: @calendar-category-15;
-                overflow: hidden;
-            }
-            background: @calendar-category-15-aux;
-            border: solid 1px @calendar-category-15;
-            .calendar-category-mixin(@calendar-category-15-aux);
-        }
-        &.calendar-category255, &.calendar-course-category255 {
-            div:first-child {
-                background-color: @calendar-category-255;
-                overflow: hidden;
-            }
-            background: @calendar-category-255-aux;
-            border: solid 1px @calendar-category-255;
-            .calendar-category-mixin(@calendar-category-255-aux);
-        }
-        /* Termin von im Stundenplan vorgemerkter Kurs */
-        &.calendar-course-category256 {
-            div:first-child {
-                background-color: #2D2C64;
-                overflow: hidden;
-            }
-            background: mix(#2D2C64, #fff, 60%);
-            border: solid 1px #2D2C64;
-            .calendar-category-mixin(mix(#2D2C64, #fff, 60%));
-        }
-    }
-}
-
-a.calendar-event-text1,
-a.Calendar-course-event-text5 {
-    color: @calendar-category-1;
-}
-
-a.calendar-event-text2,
-a.Calendar-course-event-text1{
-    color: @calendar-category-2;
-}
-
-a.calendar-event-text3,
-a.Calendar-course-event-text2 {
-    color: @calendar-category-3;
-}
-
-a.calendar-event-text4,
-a.Calendar-course-event-text3 {
-    color: @calendar-category-4;
-}
-
-a.calendar-event-text5,
-a.Calendar-course-event-text4 {
-    color: @calendar-category-5;
-}
-
-a.calendar-event-text6,
-a.Calendar-course-event-text6 {
-    color: @calendar-category-6;
-}
-
-a.calendar-event-text7 {
-    color: @calendar-category-7;
-}
-
-a.calendar-event-text8 {
-    color: @calendar-category-8;
-}
-
-a.calendar-event-text9 {
-    color: @calendar-category-9;
-}
-
-a.calendar-event-text10 {
-    color: @calendar-category-10;
-}
-
-a.calendar-event-text11 {
-    color: @calendar-category-11;
-}
-
-a.calendar-event-text12 {
-    color: @calendar-category-12;
-}
-
-a.calendar-event-text13 {
-    color: @calendar-category-13;
-}
-
-a.calendar-event-text14 {
-    color: @calendar-category-14;
-}
-
-a.calendar-event-text15,
-a.Calendar-course-event-text7 {
-    color: @calendar-category-15;
-}
-
-a.calendar-event-text255,
-a.Calendar-course-event-text255 {
-    color: @calendar-category-255;
-}
-.calendar-tooltip {
-    display: none;
-    font-size: 0.8em;
-}
-
-.calendar-group-events {
-    background: linear-gradient(to right, var(--base-color-60), var(--content-color-60)) repeat-x var(--base-color-60);
-    border: solid 1px var(--base-gray);
-}
-
-#exc-dates {
-    padding: 2px;
-    list-style-type: none;
-    width: 7.5em;
-    min-height: 5em;
-    max-height: 10em;
-    overflow: auto;
-    border: 1px solid var(--dark-gray-color-60);
-
-    img {
-        vertical-align: text-top;
-    }
-    input:checked ~ span {
-        text-decoration: line-through;
-        opacity: 0.6;
-    }
-}
-
-/* --- Styles fuer TerminZeile ---------------------------------------------- */
-table.tabdaterow {
-    background-color: white;
-}
-
-td.tddaterowp {
-    border: 1px solid var(--white);
-    background-color: var(--dark-gray-color-10);
-    font-weight: bold;
-    color: var(--dark-green);
-    font-size: 8pt;
-}
-
-td.tddaterowpx {
-    border: 1px solid var(--active-color);
-    background-color: var(--dark-gray-color-10);
-    font-weight: bold;
-    color: var(--dark-green);
-    font-size: 8pt;
-}
-
-
-.recurrences {
-    width: 100%;
-    float: none;
-    list-style: none;
-    text-align: left;
-    position: relative;
-    padding: 0;
-    margin: 0;
-    li {
-        display: block;
-        width: 100%;
-    }
-    input.rec-select {
-    }
-    .rec-label {
-        cursor: pointer;
-    }
-    .rec-label:hover {
-    }
-    .rec-content {
-        display: none;
-        position: relative;
-        padding-left: 3em;
-    }
-    [id^="rec"]:checked + label.rec-label {
-    }
-    [id^="rec"]:checked ~ [id^="rec-content"] {
-    display: block;
-    }
-}
diff --git a/resources/assets/stylesheets/scss/calendar.scss b/resources/assets/stylesheets/scss/calendar.scss
new file mode 100644
index 0000000000000000000000000000000000000000..4ad94b885ce967c5d1fb052ff7343695503413df
--- /dev/null
+++ b/resources/assets/stylesheets/scss/calendar.scss
@@ -0,0 +1,135 @@
+.fc-body {
+    .fc-event {
+
+        background-color: #fff;
+        color: #000;
+        border-width: 2px;
+
+        &:hover {
+            color: #000;
+        }
+
+        &.course-color-0 {
+            border-color: $group-color-0;
+            background-color: lighten($group-color-0, 45%);
+
+            &:hover {
+                background-color: lighten($group-color-0, 50%);
+            }
+
+            .fc-time {
+                border-bottom: 1px solid $group-color-0;
+            }
+        }
+
+        &.course-color-1 {
+            border-color: $group-color-1;
+            background-color: lighten($group-color-1, 45%);
+
+            &:hover {
+                background-color: lighten($group-color-1, 50%);
+            }
+
+            .fc-time {
+                border-bottom: 1px solid $group-color-1;
+            }
+        }
+
+        &.course-color-2 {
+            border-color: $group-color-2;
+            background-color: lighten($group-color-2, 45%);
+
+            &:hover {
+                background-color: lighten($group-color-2, 50%);
+            }
+
+            .fc-time {
+                border-bottom: 1px solid $group-color-2;
+            }
+        }
+
+        &.course-color-3 {
+            border-color: $group-color-3;
+            background-color: lighten($group-color-3, 45%);
+
+            &:hover {
+                background-color: lighten($group-color-3, 50%);
+            }
+
+            .fc-time {
+                border-bottom: 1px solid $group-color-3;
+            }
+        }
+
+        &.course-color-4 {
+            border-color: $group-color-4;
+            background-color: lighten($group-color-4, 45%);
+
+            &:hover {
+                background-color: lighten($group-color-4, 50%);
+            }
+
+            .fc-time {
+                border-bottom: 1px solid $group-color-4;
+            }
+        }
+
+        &.course-color-5 {
+            border-color: $group-color-5;
+            background-color: lighten($group-color-5, 45%);
+
+            &:hover {
+                background-color: lighten($group-color-5, 50%);
+            }
+
+            .fc-time {
+                border-bottom: 1px solid $group-color-5;
+            }
+        }
+
+        &.course-color-6 {
+            border-color: $group-color-6;
+            background-color: lighten($group-color-6, 45%);
+
+            &:hover {
+                background-color: lighten($group-color-6, 50%);
+            }
+
+            .fc-time {
+                border-bottom: 1px solid $group-color-6;
+            }
+        }
+
+        &.course-color-7 {
+            border-color: $group-color-7;
+            background-color: lighten($group-color-7, 45%);
+
+            &:hover {
+                background-color: lighten($group-color-7, 50%);
+            }
+
+            .fc-time {
+                border-bottom: 1px solid $group-color-7;
+            }
+        }
+
+        &.course-color-8 {
+            border-color: $group-color-8;
+            background-color: lighten($group-color-8, 45%);
+
+            &:hover {
+                background-color: lighten($group-color-8, 50%);
+            }
+
+            .fc-time {
+                border-bottom: 1px solid $group-color-8;
+            }
+        }
+    }
+}
+
+
+/* special rule for the month view: do not underline the time */
+.fc-view.fc-dayGridMonth-view .fc-event .fc-time {
+    border: none;
+}
diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss
index 0b43f044afae4f571df093bacc3b504bacdf7ddd..fc3eb4b41f58d4feafa304c3733e720f73f2cd76 100644
--- a/resources/assets/stylesheets/scss/forms.scss
+++ b/resources/assets/stylesheets/scss/forms.scss
@@ -37,7 +37,7 @@ form.default {
         font-style: italic;
     }
 
-    input[type=date], input[type=email], input[type=number],
+    input[type=date], input[type=datetime-local], input[type=email], input[type=number],
     input[type=password], input[type=text], input[type=time], input[type=url], input[type=tel],
     textarea, select {
         box-sizing: border-box;
@@ -86,10 +86,14 @@ form.default {
         max-width: $max-width-m;
     }
 
-    input[type=date], input[type=number], input[type=time], input[type=tel]:not(.size-m)  {
+    input[type=date].hasDatepicker, input[type=date][data-date-picker], input[type=number], input[type=time], input[type=tel]:not(.size-m)  {
         max-width: $max-width-s;
     }
 
+    input[type=date]:not(.hasDatepicker, [data-date-picker]) {
+        max-width: $max-width-m;
+    }
+
     textarea {
         min-height: 6em;
     }
@@ -533,6 +537,17 @@ form.default {
             margin-left: 10px;
         }
     }
+
+    .input-with-icon {
+        input {
+            display: inline;
+            width: calc(100% - 24px);
+        }
+        img.icon {
+            height: 2em;
+            margin-top: 0.5ex;
+        }
+    }
 }
 
 form.narrow {
diff --git a/resources/assets/stylesheets/studip.less b/resources/assets/stylesheets/studip.less
index 1a2c7941b20ead9113e59a3541fb6cda30c111b7..aee6779a5c121fd7ab4f088824eaf5ea058f59f6 100644
--- a/resources/assets/stylesheets/studip.less
+++ b/resources/assets/stylesheets/studip.less
@@ -12,7 +12,6 @@
 @import "less/tables.less";
 @import "less/buttons.less";
 @import "less/messagebox.less";
-@import "less/calendar.less";
 @import "less/schedule.less";
 @import "less/files.less";
 
diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss
index b0e768ea172bf86327d5c2a7361450a94a4b1171..606fa0738d1153e25a3183516dc46bce96db9d7e 100644
--- a/resources/assets/stylesheets/studip.scss
+++ b/resources/assets/stylesheets/studip.scss
@@ -22,6 +22,7 @@
 @import "scss/blockquote.scss";
 @import "scss/blubber";
 @import "scss/buttons";
+@import "scss/calendar";
 @import "scss/clipboard";
 @import "scss/consultation";
 @import "scss/contacts";
diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js
index b8cf935e011761cc70d43b7fe7c5df31394bb7f8..2390bb9b1848a4648366640d49f87c49f2c71a01 100644
--- a/resources/vue/base-components.js
+++ b/resources/vue/base-components.js
@@ -1,6 +1,11 @@
+import CalendarPermissionsTable from "./components/form_inputs/CalendarPermissionsTable.vue";
+import DayOfWeekSelect from './components/form_inputs/DayOfWeekSelect.vue';
+import DateListInput from './components/form_inputs/DateListInput.vue';
 import Multiselect from './components/Multiselect.vue';
+import MyCoursesColouredTable from './components/form_inputs/MyCoursesColouredTable.vue';
 import EditableList from "./components/EditableList.vue";
 import Quicksearch from './components/Quicksearch.vue';
+import RepetitionInput from "./components/form_inputs/RepetitionInput.vue";
 import SidebarWidget from './components/SidebarWidget.vue';
 import StudipActionMenu from './components/StudipActionMenu.vue';
 import StudipAssetImg from './components/StudipAssetImg.vue';
@@ -10,6 +15,7 @@ import StudipFileSize from './components/StudipFileSize.vue';
 import StudipFolderSize from './components/StudipFolderSize.vue';
 import StudipIcon from './components/StudipIcon.vue';
 import RangeInput from './components/RangeInput.vue';
+import Datepicker from './components/Datepicker.vue';
 import Datetimepicker from './components/Datetimepicker.vue';
 import TextareaWithToolbar from './components/TextareaWithToolbar.vue';
 import I18nTextarea from "./components/I18nTextarea.vue";
@@ -23,14 +29,20 @@ import StudipSelect from './components/StudipSelect.vue';
 import StudipMultiPersonSearch from './components/StudipMultiPersonSearch.vue';
 
 const BaseComponents = {
+    CalendarPermissionsTable,
+    DayOfWeekSelect,
+    DateListInput,
     Multiselect,
+    MyCoursesColouredTable,
     EditableList,
     Quicksearch,
     RangeInput,
+    RepetitionInput,
     SidebarWidget,
     StudipActionMenu,
     StudipAssetImg,
     StudipDateTime,
+    Datepicker,
     Datetimepicker,
     StudipDialog,
     StudipFileSize,
diff --git a/resources/vue/components/Datepicker.vue b/resources/vue/components/Datepicker.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3db44ceb9712af4ae0c68d55f251ec29f1d62747
--- /dev/null
+++ b/resources/vue/components/Datepicker.vue
@@ -0,0 +1,76 @@
+<template>
+    <span>
+        <input type="hidden" :name="name" :value="value">
+        <input type="text"
+               ref="visibleInput"
+               class="visible_input"
+               @change="setUnixTimestamp"
+               v-bind="$attrs"
+               v-on="$listeners">
+    </span>
+</template>
+
+<script>
+export default {
+    name: "datepicker",
+    inheritAttrs: false,
+    props: {
+        name: {
+            type: String,
+            required: false
+        },
+        value: {
+            required: false
+        },
+        mindate: {
+            required: false
+        },
+        maxdate: {
+            required: false
+        }
+    },
+    methods: {
+        setUnixTimestamp () {
+            let formatted_date = this.$refs.visibleInput.value;
+            let date = formatted_date.match(/(\d+)/g);
+            date = new Date(`${date[2]}-${date[1]}-${date[0]} ${date[3]}:${date[4]}`);
+            this.$emit('input', Math.floor(date / 1000));
+        }
+    },
+    mounted () {
+        let value = !isNaN(parseInt(this.value, 10)) ? parseInt(this.value, 10) : this.value;
+        if (Number.isInteger(value)) {
+            let date = new Date(value * 1000);
+            let formatted_date =
+                (date.getDate() < 10 ? "0" : "") + date.getDate()
+                + "."
+                + (date.getMonth() < 9 ? "0" : "") + (date.getMonth() + 1)
+                + "."
+                + date.getFullYear();
+            this.$refs.visibleInput.value = formatted_date;
+        } else {
+            this.$refs.visibleInput.value = value;
+        }
+        let params = {
+            onSelect: () => {
+                this.setUnixTimestamp();
+            }
+        };
+        if (this.mindate) {
+            params.minDate = new Date(this.mindate * 1000)
+        }
+        if (this.maxdate) {
+            params.maxDate = new Date(this.maxdate * 1000)
+        }
+        $(this.$refs.visibleInput).datetimepicker(params);
+    },
+    watch: {
+        mindate (new_data, old_data) {
+            $(this.$refs.visibleInput).datetimepicker('option', 'minDate', new Date(new_data * 1000));
+        },
+        maxdate (new_data, old_data) {
+            $(this.$refs.visibleInput).datetimepicker('option', 'maxDate', new Date(new_data * 1000));
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/EditableList.vue b/resources/vue/components/EditableList.vue
index c76b40063dc5a2ad6ac8c65eaeb4a1649a0959d4..cf1716b63576accc5bac9aa56ccdb881d1661ec6 100644
--- a/resources/vue/components/EditableList.vue
+++ b/resources/vue/components/EditableList.vue
@@ -78,7 +78,7 @@ export default {
         return {
             resort: false, //this is just for triggering the computed property sortedItems to be sorted again
             preventChangeOfQuickselect: false,
-            allItems: this.items
+            allItems: this.items ?? []
         };
     },
     methods: {
@@ -159,8 +159,8 @@ export default {
                 if (a.icon === b.icon) {
                     return a.name.localeCompare(b.name);
                 } else {
-                    let a_icon = a.icon || '';
-                    let b_icon = b.icon || '';
+                    let a_icon = typeof a.icon === 'string' ? a.icon : '';
+                    let b_icon = typeof b.icon === 'string' ? b.icon : '';
                     if (this.category_order.indexOf(a_icon) > -1 && this.category_order.indexOf(b_icon) > -1) {
                         return this.category_order.indexOf(a_icon) < this.category_order.indexOf(b_icon) ? -1 : 1;
                     } else {
diff --git a/resources/vue/components/StudipDateTime.vue b/resources/vue/components/StudipDateTime.vue
index 1cf852ca1c2d6dfe7e040a752bb9f9985de53753..dfdc0c3f16a13d545f5a5d629a362ce23d8cbbc0 100644
--- a/resources/vue/components/StudipDateTime.vue
+++ b/resources/vue/components/StudipDateTime.vue
@@ -5,9 +5,6 @@
 </template>
 
 <script>
-    function pad(what, length = 2) {
-        return `00000000${what}`.substr(-length);
-    }
 
     export default {
         name: 'studip-date-time',
@@ -17,6 +14,11 @@
                 type: Boolean,
                 required: false,
                 default: false
+            },
+            date_only: {
+                type: Boolean,
+                required: false,
+                default: false
             }
         },
         computed: {
@@ -40,18 +42,8 @@
                     return `Should be integer: ${this.timestamp}`;
                 }
                 let date = new Date(this.timestamp * 1000);
-                let now = Date.now();
-                if (!force_absolute && this.relative && this.display_relative()) {
-                    if (now - date < 1 * 60 * 1000) {
-                        return this.$gettext('Jetzt');
-                    }
-                    if (now - date < 2 * 60 * 60 * 1000) {
-                        return this.$gettext('Vor %s Minuten').replace('%s', Math.floor((now - date) / (1000 * 60)));
-                    }
-                    return pad(date.getHours()) + ':' + pad(date.getMinutes());
-                } else {
-                    return pad(date.getDate()) + '.' + pad(date.getMonth() + 1) + '.' + date.getFullYear() + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes());
-                }
+                let relative_value = !force_absolute && this.relative && this.display_relative();
+                return STUDIP.DateTime.getStudipDate(date, relative_value, this.date_only);
             }
         },
         mounted: function () {
diff --git a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a0a76a2897acbd7e7f8c099c57932ced9d0b31ca
--- /dev/null
+++ b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
@@ -0,0 +1,86 @@
+<template>
+    <div class="formpart">
+        <quicksearch v-if="searchtype" :searchtype="searchtype" name="qs" @input="addContact"
+                     :placeholder="$gettext('Personen hinzufügen')"></quicksearch>
+        <table class="default">
+            <caption>{{ $gettext('Kontakte, mit denen der Kalender geteilt wird')}}</caption>
+            <thead>
+                <tr>
+                    <th>{{ $gettext('Name') }}</th>
+                    <th>{{ $gettext('Schreibzugriff') }}</th>
+                    <th class="actions">{{ $gettext('Nicht mehr teilen') }}</th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-if="this.users.length === 0">
+                    <td colspan="3">
+                        <studip-message-box type="info">
+                            {{ $gettext('Der Kalender wird mit keinem Kontakt geteilt.') }}
+                        </studip-message-box>
+                    </td>
+                </tr>
+                <tr v-for="user in this.users" :key="user.id">
+                    <td>
+                        <input type="hidden" :name="name + '_permissions[]'"
+                               :value="user.id">
+                        {{ user.name }}
+                    </td>
+                    <td>
+                        <input type="checkbox" :name="name + '_write_permissions[]'" :value="user.id"
+                               v-model="user.write_permissions"
+                               :aria-label="$gettextInterpolate(
+                                   $gettext('Schreibzugriff für %{name}'),
+                                   {name: user.name}
+                               )">
+                    </td>
+                    <td class="actions">
+                        <studip-icon shape="trash" aria-role="button" @click="removeContact(user.id)"
+                                     :title="$gettextInterpolate(
+                                         $gettext('Kalender nicht mehr mit %{name} teilen'),
+                                         {name: user.name}
+                                     )"></studip-icon>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</template>
+
+<script>
+import StudipMessageBox from "../StudipMessageBox.vue";
+
+export default {
+    name: "calendar-permissions-table",
+    components: {StudipMessageBox},
+    props: {
+        name: {
+            type: String,
+            required: true
+        },
+        selected_users: {
+            type: Object,
+            required: false,
+            default: () => {},
+        },
+        searchtype: {
+            type: String,
+            required: true,
+        }
+    },
+    data() {
+        return {
+            users: {...this.selected_users},
+        }
+    },
+    methods: {
+        addContact(user_id, name) {
+            this.$set(this.users, user_id, {id: user_id, name: name, write_permissions: false});
+        },
+        removeContact(user_id) {
+            if (this.users[user_id] !== undefined) {
+                this.$delete(this.users, user_id);
+            }
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/form_inputs/DateListInput.vue b/resources/vue/components/form_inputs/DateListInput.vue
new file mode 100644
index 0000000000000000000000000000000000000000..05f3c574e93cab6e348c6eff78f82c2e15dbf8f3
--- /dev/null
+++ b/resources/vue/components/form_inputs/DateListInput.vue
@@ -0,0 +1,98 @@
+<template>
+    <div class="formpart">
+        <div class="sr-only" aria-live="polite" ref="list_message_field"></div>
+        <ul>
+            <li v-for="date in selected_date_list" v-bind="selected_date_list" :key="date">
+                <input type="hidden" :name="input_name + '[]'" :value="getISODate(date)">
+                <studip-date-time :timestamp="Math.floor(date.getTime() / 1000)" :date_only="true"></studip-date-time>
+                <studip-icon shape="trash" :title="$gettext('Löschen')" @click="removeDate"
+                             class="enter-accessible" aria-role="button" tabindex="0"></studip-icon>
+            </li>
+        </ul>
+        <label>
+            {{ $gettext('Datum') }}
+            <div class="flex-row input-with-icon">
+                <input type="text" v-model="selected_date_value" ref="date_select_input">
+                <studip-icon shape="add" :title="$gettext('Hinzufügen')" @click="addDate"
+                             class="icon enter-accessible button undecorated" aria-role="button" tabindex="0"></studip-icon>
+            </div>
+        </label>
+    </div>
+</template>
+
+<script>
+import StudipDateTime from "../StudipDateTime.vue";
+import {$gettext, $gettextInterpolate} from "@/assets/javascripts/lib/gettext";
+
+export default {
+    name: "date-list-input",
+    components: {StudipDateTime},
+    props: {
+        name: {
+            type: String,
+            required: true
+        },
+        selected_dates: {
+            type: Array,
+            required: false,
+            default: () => [],
+        }
+    },
+    data () {
+        return {
+            selected_date_value: STUDIP.DateTime.getStudipDate(new Date(), false, true),
+            selected_date_list: this.selected_dates.map(date => new Date(date)),
+            input_name: this.name,
+        };
+    },
+    mounted() {
+
+        //Set up the datepicker for the date selector input:
+        let v = this;
+        jQuery(this.$refs.date_select_input).datepicker({
+            onSelect: () => {
+                this.selected_date_value = this.$refs.date_select_input.value;
+                this.addDate();
+            },
+        });
+    },
+    watch: {
+        selected_date_value(new_value) {
+            this.$emit('selected_date_value', new_value);
+        },
+        selected_date_list: {
+            handler (new_value) {
+                this.$emit('selected_date_list', new_value);
+            },
+            deep: true
+        }
+    },
+    methods: {
+        addDate() {
+            if (this.selected_date_value.length < 8) {
+                //Input too short.
+                return;
+            }
+            let date_parts = this.selected_date_value.split('.');
+            if (date_parts.length !== 3) {
+                //Incorrect input formatting.
+                return;
+            }
+            let reformatted_date = date_parts[2] + '-' + date_parts[1] + '-' + date_parts[0];
+            this.selected_date_list.push(new Date(reformatted_date));
+            this.$refs.list_message_field.innerText = $gettextInterpolate($gettext('Datum %{date} hinzugefügt'), {date: this.selected_date_value});
+        },
+        removeDate(date_key) {
+            if (date_key) {
+                let date = this.selected_date_list.at(date_key);
+                let formatted_date = STUDIP.DateTime.getStudipDate(date, false, true);
+                this.selected_date_list.splice(date_key, 1);
+                this.$refs.list_message_field.innerText = $gettextInterpolate($gettext('Datum %{date} entfernt'), {date: formatted_date});
+            }
+        },
+        getISODate(date) {
+            return STUDIP.DateTime.getISODate(date);
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/form_inputs/DayOfWeekSelect.vue b/resources/vue/components/form_inputs/DayOfWeekSelect.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c28ddb6db4a435f3c4dbb7c666ebe952dcd9eded
--- /dev/null
+++ b/resources/vue/components/form_inputs/DayOfWeekSelect.vue
@@ -0,0 +1,60 @@
+<template>
+    <select :name="name" v-model="selected_value">
+        <option v-if="with_indeterminate" value=""
+                :selected="!value">
+            {{ $gettext('Bitte wählen') }}
+        </option>
+        <option value="1" :selected="value == '1'">
+            {{ $gettext('Montag') }}
+        </option>
+        <option value="2" :selected="value == '2'">
+            {{ $gettext('Dienstag') }}
+        </option>
+        <option value="3" :selected="value == '3'">
+            {{ $gettext('Mittwoch') }}
+        </option>
+        <option value="4" :selected="value == '4'">
+            {{ $gettext('Donnerstag') }}
+        </option>
+        <option value="5" :selected="value == '5'">
+            {{ $gettext('Freitag') }}
+        </option>
+        <option value="6" :selected="value == '6'">
+            {{ $gettext('Samstag') }}
+        </option>
+        <option value="7" :selected="value == '7'">
+            {{ $gettext('Sonntag') }}
+        </option>
+    </select>
+</template>
+
+<script>
+export default {
+    name: "day-of-week-select",
+    props: {
+        name: {
+            type: String,
+            required: true
+        },
+        value: {
+            type: String,
+            required: false
+        },
+        with_indeterminate: {
+            type: Boolean,
+            required: false,
+            default: false,
+        }
+    },
+    data () {
+        return {
+            selected_value: this.value
+        };
+    },
+    watch: {
+        selected_value(new_value) {
+            this.$emit('selected_value', new_value);
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d2cc2f1aa3f38fb16a533ae08ac1bcfaf00715d4
--- /dev/null
+++ b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
@@ -0,0 +1,166 @@
+<template>
+    <div class="formpart">
+        <label v-if="with_semester_selector">
+            {{ $gettext('Semester') }}
+            <select :name="`${name}_semester_id`" v-model="semester_id">
+                <option v-for="semester in available_semesters"
+                        :value="semester.id"
+                        :key="semester.id"
+                >
+                    {{ semester.name }}
+                </option>
+            </select>
+        </label>
+
+        <table class="default mycourses">
+            <caption>{{ semesterName }}</caption>
+            <colgroup>
+                <col style="width: 7px">
+                <col style="width: 25px">
+                <col style="width: 70px">
+                <col>
+                <col>
+            </colgroup>
+            <thead>
+                <tr>
+                    <th></th>
+                    <th></th>
+                    <th>{{ $gettext('Nummer') }}</th>
+                    <th>{{ $gettext('Name') }}</th>
+                    <th class="actions">{{ $gettext('Auswahl') }}</th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="course of courses" :key="course.id">
+                    <td :class="`gruppe${course.group}`"></td>
+                    <td>
+                        <img :src="course.avatar_url" alt="" class="my-courses-avatar course-avatar-small">
+                    </td>
+                    <td>{{ course.number }}</td>
+                    <td>{{ course.name }}</td>
+                    <td class="actions">
+                        <input type="hidden" :name="`${name}_course_ids[${course.id}]`" value="0">
+                        <input type="checkbox" :name="`${name}_course_ids[${course.id}]`"
+                               value="1" :checked="selected_course_id_list.includes(course.id)"
+                               :title="$gettextInterpolate($gettext('%{course} auswählen'), {course: course.name})">
+                    </td>
+                </tr>
+                <tr v-if="loadedSemesters.includes(semester_id) && courses.length === 0">
+                    <td colspan="5">
+                        <studip-message-box>{{ $gettext('Im gewählten Semester stehen keine Veranstaltungen zur Auswahl zur Verfügung.') }}</studip-message-box>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</template>
+
+<script>
+import StudipMessageBox from "../StudipMessageBox.vue";
+
+export default {
+    name: 'my-courses-coloured-table',
+    components: {StudipMessageBox},
+    props: {
+        default_semester_id: {
+            type: String,
+            required: true,
+        },
+        selected_course_ids: {
+            type: Array,
+            required: false,
+            default: () => [],
+        },
+        name: {
+            type: String,
+            required: false,
+            default: 'selected_course_ids',
+        },
+        semester_data: {
+            type: Object,
+            required: false,
+            default: () => {},
+        }
+    },
+    data() {
+        //Retrieve all semesters, if the semester selector is present:
+        let semester_data = this.semester_data;
+        return {
+            available_semesters: semester_data,
+            semester_id: null,
+            semester_courses: Object.values(semester_data).reduce(
+                (carry, current) => {
+                    carry[current.id] = [];
+                    return carry;
+                },
+                {}
+            ),
+            selected_course_id_list: [...this.selected_course_ids],
+            with_semester_selector: Object.keys(semester_data).length > 0,
+            membershipGroups: {},
+            loadedSemesters: [],
+        };
+    },
+    created() {
+        this.semester_id = this.default_semester_id;
+
+        STUDIP.jsonapi.GET(`users/${STUDIP.USER_ID}/course-memberships`, {
+            data: {
+                'page[limit]': 1000,
+            }
+        }).done((response) => {
+            this.membershipGroups = Object.values(response.data).reduce(
+                (carry, current) => {
+                    carry[current.id.split('_')[0]] = current.attributes.group;
+                    return carry;
+                },
+                {}
+            );
+        })
+    },
+    methods: {
+        loadSemesterCourses(semester_id) {
+            if (this.loadedSemesters.includes(semester_id)) {
+                return;
+            }
+
+            // The courses have not yet been retrieved.
+            STUDIP.jsonapi.GET(`users/${STUDIP.USER_ID}/courses`, {
+                data: {
+                    'fields[courses]': 'id,course-number,title,course-type',
+                    'filter[semester]': semester_id,
+                    'include': 'memberships',
+                }
+            }).done((response) => {
+                this.semester_courses[semester_id] = response.data
+                    .filter(item => item.type === 'courses')
+                    .map(item => ({
+                        id: item.id,
+                        name: item.attributes.title,
+                        number: item.attributes['course-number'] ?? '',
+                        group: this.membershipGroups[item.id] ?? item.attributes['course-type'],
+                        avatar_url: item.meta.avatar.small,
+                    }));
+
+                this.loadedSemesters.push(semester_id);
+            });
+        }
+    },
+    computed: {
+        courses() {
+            return [...this.semester_courses[this.semester_id]].sort((a, b) => {
+                return a.name.localeCompare(b.name)
+                    || a.number.localeCompare(b.number);
+            });
+        },
+        semesterName() {
+            return this.available_semesters[this.semester_id].name ?? '';
+        },
+    },
+    watch: {
+        semester_id(current) {
+            this.loadSemesterCourses(current);
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/form_inputs/RepetitionInput.vue b/resources/vue/components/form_inputs/RepetitionInput.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ef53ffb8cd2dd3963b3aced7bc72ba3fe684819b
--- /dev/null
+++ b/resources/vue/components/form_inputs/RepetitionInput.vue
@@ -0,0 +1,364 @@
+<template>
+    <div class="formpart">
+        <section>
+            <label>{{ $gettext('Art der Wiederholung') }}
+                <select :name="name + '_type'" v-model="repetition_type_value">
+                    <option value="" :selected="!repetition_type_value">
+                        {{ $gettext('Keine Wiederholung') }}
+                    </option>
+                    <option value="DAILY" :selected="repetition_type_value === 'DAILY'">
+                        {{ $gettext('Tägliche Wiederholung') }}
+                    </option>
+                    <option value="WORKDAYS" :selected="repetition_type_value === 'WORKDAYS'">
+                        {{ $gettext('Wiederholung an jedem Werktag') }}
+                    </option>
+                    <option value="WEEKLY" :selected="repetition_type_value === 'WEEKLY'">
+                        {{ $gettext('Wöchentliche Wiederholung') }}
+                    </option>
+                    <option value="MONTHLY" :selected="repetition_type_value === 'MONTHLY'">
+                        {{ $gettext('Monatliche Wiederholung') }}
+                    </option>
+                    <option value="YEARLY" :selected="repetition_type_value === 'YEARLY'">
+                        {{ $gettext('Jährliche Wiederholung') }}
+                    </option>
+                </select>
+            </label>
+        </section>
+        <section v-if="repetition_type_value === 'DAILY'">
+            <label>
+                {{ $gettext('Abstand in Tagen') }}
+                <input type="number" min="1" :name="name + '_interval'"
+                       v-model="repetition_interval_value">
+            </label>
+        </section>
+        <section v-else-if="repetition_type_value === 'WEEKLY'">
+            <label>
+                {{ $gettext('Abstand in Wochen') }}
+                <input type="number" min="1" :name="name + '_interval'"
+                       v-model="repetition_interval_value">
+            </label>
+            <div>
+                <p>{{ $gettext('Wiederholung an bestimmten Wochentagen') }}</p>
+                <label>
+                    <input type="checkbox" :name="name + '_dow[]'"
+                           value="1" :checked="repetition_dow_value.includes('1')">
+                    {{ $gettext('Montag') }}
+                </label>
+                <label>
+                    <input type="checkbox" :name="name + '_dow[]'"
+                           value="2" :checked="repetition_dow_value.includes('2')">
+                    {{ $gettext('Dienstag') }}
+                </label>
+                <label>
+                    <input type="checkbox" :name="name + '_dow[]'"
+                           value="3" :checked="repetition_dow_value.includes('3')">
+                    {{ $gettext('Mittwoch') }}
+                </label>
+                <label>
+                    <input type="checkbox" :name="name + '_dow[]'"
+                           value="4" :checked="repetition_dow_value.includes('4')">
+                    {{ $gettext('Donnerstag') }}
+                </label>
+                <label>
+                    <input type="checkbox" :name="name + '_dow[]'"
+                           value="5" :checked="repetition_dow_value.includes('5')">
+                    {{ $gettext('Freitag') }}
+                </label>
+                <label>
+                    <input type="checkbox" :name="name + '_dow[]'"
+                           value="6" :checked="repetition_dow_value.includes('6')">
+                    {{ $gettext('Samstag') }}
+                </label>
+                <label>
+                    <input type="checkbox" :name="name + '_dow[]'"
+                           value="7" :checked="repetition_dow_value.includes('7')">
+                    {{ $gettext('Sonntag') }}
+                </label>
+            </div>
+        </section>
+        <section v-else-if="repetition_type_value === 'YEARLY'">
+            <label>
+                {{ $gettext('Abstand in Jahren') }}
+                <input type="number" min="1" :name="name + '_interval'"
+                       v-model="repetition_interval_value">
+            </label>
+            <label>
+                {{ $gettext('Art der jährlichen Wiederholung') }}
+                <select :name="name + '_month_type'"
+                        v-model="repetition_month_type_value">
+                    <option value="dom"
+                            :selected="repetition_month_type_value === 'dom'">
+                        {{ $gettext('Wiederholung an einem bestimmten Datum') }}
+                    </option>
+                    <option value="dow"
+                            :selected="repetition_month_type_value === 'dow'">
+                        {{ $gettext('Wiederholung an einem bestimmten Wochentag') }}
+                    </option>
+                </select>
+            </label>
+            <label v-if="repetition_month_type_value === 'dom'">
+                {{ $gettext('Tag') }}
+                <input type="number" :name="name + '_dom'" min="1" max="31" v-model="repetition_dom_value">
+            </label>
+            <label>
+                {{ $gettext('Monat') }}
+                <select :name="name + '_month'"
+                        v-model="repetition_month_value">
+                    <option value="1" :selected="repetition_month_value === 1">
+                        {{ $gettext('Januar') }}
+                    </option>
+                    <option value="2" :selected="repetition_month_value === 2">
+                        {{ $gettext('Februar') }}
+                    </option>
+                    <option value="3" :selected="repetition_month_value === 3">
+                        {{ $gettext('März') }}
+                    </option>
+                    <option value="4" :selected="repetition_month_value === 4">
+                        {{ $gettext('April') }}
+                    </option>
+                    <option value="5" :selected="repetition_month_value === 5">
+                        {{ $gettext('Mai') }}
+                    </option>
+                    <option value="6" :selected="repetition_month_value === 6">
+                        {{ $gettext('Juni') }}
+                    </option>
+                    <option value="7" :selected="repetition_month_value === 7">
+                        {{ $gettext('Juli') }}
+                    </option>
+                    <option value="8" :selected="repetition_month_value === 8">
+                        {{ $gettext('August') }}
+                    </option>
+                    <option value="9" :selected="repetition_month_value === 9">
+                        {{ $gettext('September') }}
+                    </option>
+                    <option value="10" :selected="repetition_month_value === 10">
+                        {{ $gettext('Oktober') }}
+                    </option>
+                    <option value="11" :selected="repetition_month_value === 11">
+                        {{ $gettext('November') }}
+                    </option>
+                    <option value="12" :selected="repetition_month_value === 12">
+                        {{ $gettext('Dezember') }}
+                    </option>
+                </select>
+            </label>
+        </section>
+        <section v-if="repetition_type_value === 'MONTHLY'">
+            <label>
+                {{ $gettext('Abstand in Monaten') }}
+                <input type="number" min="1" :name="name + '_interval'"
+                       v-model="repetition_interval_value">
+            </label>
+            <label>
+                {{ $gettext('Art der monatlichen Wiederholung') }}
+                <select :name="name + '_month_type'"
+                        v-model="repetition_month_type_value">
+                    <option value="dom" :selected="repetition_month_type_value === 'dom'">
+                        {{ $gettext('Wiederholung an einem bestimmten Tag des Monats') }}
+                    </option>
+                    <option value="dow" :selected="repetition_month_type_value === 'dow'">
+                        {{ $gettext('Wiederholung an einem bestimmten Wochentag') }}
+                    </option>
+                </select>
+            </label>
+        </section>
+        <section v-if="repetition_type_value === 'MONTHLY' && repetition_month_type_value === 'dom'">
+            <label>
+                {{ $gettext('Wiederholung am einem bestimmten Tag des Monats:') }}
+                <input type="number" min="1" :name="name + '_dom'"
+                       v-model="repetition_dom_value">
+            </label>
+        </section>
+        <section v-if="['MONTHLY', 'YEARLY'].includes(repetition_type_value) && repetition_month_type_value === 'dow'">
+            <label>
+                {{ $gettext('Wiederholung an einem bestimmten Wochentag:') }}
+                <day-of-week-select :name="name + '_dow'" v-model="repetition_dow_value[0]"
+                                    :with_indeterminate="true"></day-of-week-select>
+            </label>
+            <label>
+                {{ $gettext('Wann im Monat soll die Wiederholung stattfinden?') }}
+                <select :name="name + '_dow_week'">
+                    <option value="" :selected="!repetition_dow_week_value">
+                        {{ $gettext('Bitte wählen') }}
+                    </option>
+                    <option value="1" :selected="repetition_dow_week_value === 1">
+                        {{ $gettext('Am ersten gewählten Wochentag') }}
+                    </option>
+                    <option value="2" :selected="repetition_dow_week_value === 2">
+                        {{ $gettext('Am zweiten gewählten Wochentag') }}
+                    </option>
+                    <option value="3" :selected="repetition_dow_week_value === 3">
+                        {{ $gettext('Am dritten gewählten Wochentag') }}
+                    </option>
+                    <option value="4" :selected="repetition_dow_week_value === 4">
+                        {{ $gettext('Am vierten gewählten Wochentag') }}
+                    </option>
+                    <option value="-1" :selected="repetition_dow_week_value === -1">
+                        {{ $gettext('Am letzten gewählten Wochentag') }}
+                    </option>
+                </select>
+            </label>
+        </section>
+
+        <section v-if="repetition_type_value">
+            <label>
+                {{ $gettext('Ende der Wiederholung') }}
+                <select :name="name + '_rep_end_type'"
+                        v-model="repetition_end_type_value">
+                    <option value="" :selected="!repetition_end_type_value">
+                        {{ $gettext('Nie') }}
+                    </option>
+                    <option value="end_date" :selected="repetition_end_type_value === 'end_date'">
+                        {{ $gettext('An einem bestimmten Datum') }}
+                    </option>
+                    <option value="end_count" :selected="repetition_end_type_value === 'end_count'">
+                        {{ $gettext('Nach einer Anzahl von Terminen') }}
+                    </option>
+                </select>
+            </label>
+        </section>
+        <section v-if="repetition_end_type_value === 'end_date'">
+            <label>
+                {{ $gettext('Enddatum') }}
+                <input type="text" :name="name + '_rep_end_date'"
+                       data-date-picker v-model="repetition_end_date_value">
+            </label>
+        </section>
+        <section v-else-if="repetition_end_type_value === 'end_count'">
+            <label>
+                {{ $gettext('Anzahl der Termine') }}
+                <input type="number" min="1" :name="name + '_number_of_dates'"
+                       v-model="number_of_dates_value">
+            </label>
+        </section>
+    </div>
+</template>
+
+<script>
+export default {
+    name: "repetition-input",
+    props: {
+        name: {
+            type: String,
+            required: true
+        },
+        default_date: {
+            type: String,
+            required: true
+        },
+        repetition_type: {
+            type: String,
+            required: true
+        },
+        repetition_interval: {
+            type: String,
+            required: true
+        },
+        repetition_dow: {
+            type: Array,
+            required: true
+        },
+        repetition_dow_week: {
+            type: Number,
+            required: true
+        },
+        repetition_month: {
+            type: Number,
+            required: true
+        },
+        repetition_month_type: {
+            type: String,
+            required: false
+        },
+        repetition_dom: {
+            type: Number,
+            required: true
+        },
+        repetition_end_type: {
+            type: String,
+            required: false
+        },
+        repetition_end_date: {
+            type: String,
+            required: true
+        },
+        number_of_dates: {
+            type: Number,
+            required: true
+        }
+    },
+    data () {
+        return {
+            repetition_type_value: '',
+            repetition_interval_value: 1,
+            repetition_dow_value: [],
+            repetition_dow_week_value: 0,
+            repetition_month_type_value: '',
+            repetition_month_value: 0,
+            repetition_dom_value: 0,
+            repetition_end_type_value: '',
+            repetition_end_date_value: '',
+            number_of_dates_value: 0
+        };
+    },
+    mounted () {
+        this.repetition_type_value = this.repetition_type;
+        this.repetition_interval_value = this.repetition_interval;
+        this.repetition_dow_value = this.repetition_dow;
+        this.repetition_dow_week_value = this.repetition_dow_week;
+        if (this.repetition_month_type === undefined) {
+            this.repetition_month_type_value = this.repetition_dow.length > 0 ? 'dow' : 'dom';
+        } else {
+            this.repetition_month_type_value = this.repetition_month_type;
+        }
+
+        this.repetition_month_value = this.repetition_month;
+        this.repetition_dom_value = this.repetition_dom;
+        this.repetition_end_type_value = '';
+        if (this.repetition_end_type !== undefined) {
+            this.repetition_end_type_value = this.repetition_end_type;
+        } else if (this.number_of_dates > 1) {
+            this.repetition_end_type_value = 'end_count';
+        } else if (this.repetition_end_date) {
+            this.repetition_end_type_value = 'end_date';
+        }
+        this.repetition_end_date_value = this.repetition_end_date;
+        this.number_of_dates_value = this.number_of_dates;
+    },
+    watch: {
+        repetition_type_value(new_value) {
+            this.$emit('input_repetition_type', new_value);
+        },
+        repetition_interval_value(new_value) {
+            this.$emit('input_repetition_interval', new_value);
+        },
+        repetition_dow_value: {
+            handler(new_value) {
+                this.$emit('input_repetition_dow', new_value);
+            },
+            deep: true,
+        },
+        repetition_dow_week_value(new_value) {
+            this.$emit('input_repetition_dow_week', new_value);
+        },
+        repetition_month_type_value(new_value) {
+            this.$emit('input_repetition_month_type', new_value);
+        },
+        repetition_month_value(new_value) {
+            this.$emit('input_repetition_month', new_value);
+        },
+        repetition_dom_value(new_value) {
+            this.$emit('input_repetition_dom', new_value);
+        },
+        repetition_end_type_value(new_value) {
+            this.$emit('input_repetition_end_type', new_value);
+        },
+        repetition_end_date_value(new_value) {
+            this.$emit('input_repetition_end_date', new_value);
+        },
+        number_of_dates_value(new_value) {
+            this.$emit('input_number_of_dates', new_value);
+        }
+    }
+}
+</script>
diff --git a/templates/forms/date_list_input.php b/templates/forms/date_list_input.php
new file mode 100644
index 0000000000000000000000000000000000000000..2f759ad1d961099b66af2a2e46c1302b31885ac1
--- /dev/null
+++ b/templates/forms/date_list_input.php
@@ -0,0 +1,3 @@
+<date-list-input
+    v-model="<?= htmlReady($name) ?>"
+    <?= arrayToHtmlAttributes($vue_attributes) ?>></date-list-input>
diff --git a/templates/forms/selected_ranges_input.php b/templates/forms/selected_ranges_input.php
new file mode 100644
index 0000000000000000000000000000000000000000..03ebf5dd25930da248a9cb0917265b79c1523236
--- /dev/null
+++ b/templates/forms/selected_ranges_input.php
@@ -0,0 +1,7 @@
+<editable-list name="<?= htmlReady($this->name) ?>"
+               quicksearch="<?= htmlReady((string) $searchtype) ?>"
+               :items="<?= htmlReady(json_encode($selected_items)) ?>"
+               :selectable="<?= htmlReady(json_encode($selectable)) ?>"
+               :category_order="<?= htmlReady(json_encode($category_order)) ?>"
+               @input="output => <?= htmlReady($this->name) ?> = output">
+</editable-list>
diff --git a/templates/sidebar/date-select-widget.php b/templates/sidebar/date-select-widget.php
new file mode 100644
index 0000000000000000000000000000000000000000..a85c44d11b92d8dc58e67263f26313197a9ba14b
--- /dev/null
+++ b/templates/sidebar/date-select-widget.php
@@ -0,0 +1,13 @@
+<form method="post" name="date_select_form" class="default">
+    <input type="text" id="date_select"
+           name="date_select"
+           value="<?= $date->format('d.m.Y') ?>"
+           data-date-picker
+           <?
+           if ($calendar_control) {
+               echo 'data-calendar-control';
+           } else {
+               echo 'onchange="jQuery(this).closest(\'form\').submit()"';
+           }
+           ?>>
+</form>
diff --git a/tests/jsonapi/UserEventsIcalTest.php b/tests/jsonapi/UserEventsIcalTest.php
index 81230d457c65d13c44f6ccb6c5fd2ab55f7ebe4f..c679f498251c019c0c8cdfd8b6c91b8da6164d38 100644
--- a/tests/jsonapi/UserEventsIcalTest.php
+++ b/tests/jsonapi/UserEventsIcalTest.php
@@ -23,16 +23,18 @@ class UserEventsIcalTest extends \Codeception\Test\Unit
     {
         $credentials = $this->tester->getCredentialsForTestAutor();
 
-        $calendar = new \SingleCalendar($credentials['id']);
-        $event = $calendar->getNewEvent();
-        $event->setTitle('blypyp');
-
-        $oldUser = $GLOBALS['user'] ?? null;
-        $GLOBALS['user'] = \User::find($credentials['id']);
-
-        $calendar->storeEvent($event, [$credentials['id']]);
-
-        $GLOBALS['user'] = $oldUser;
+        $event = new \CalendarDate();
+        $event->setId($event->getNewId());
+        $now = time();
+        $event->begin = $now;
+        $event->end = $now + 3600;
+        $event->title = 'blypyp';
+        $event->store();
+        $calendar_date = new \CalendarDateAssignment();
+        $calendar_date->setId([$credentials['id'], $event->getId()]);
+        $calendar_date->calendar_date = $event;
+        $calendar_date->suppress_mails = true;
+        $calendar_date->store();
 
         $app = $this->tester->createApp($credentials, 'get', '/users/{id}/events.ics', UserEventsIcal::class);
 
diff --git a/tests/jsonapi/UserEventsIndexTest.php b/tests/jsonapi/UserEventsIndexTest.php
index 0941f09b664b45decb9b11032feb16716f395e34..ac0747183b6dca9a658f421811c79387c548df4f 100644
--- a/tests/jsonapi/UserEventsIndexTest.php
+++ b/tests/jsonapi/UserEventsIndexTest.php
@@ -55,14 +55,16 @@ class UserEventsIndexTest extends \Codeception\Test\Unit
 
     private function createEvent($credentials)
     {
-        $calendar = new \SingleCalendar($credentials['id']);
-        $event = $calendar->getNewEvent();
-
-        $oldUser = $GLOBALS['user'];
-        $GLOBALS['user'] = \User::find($credentials['id']);
-
-        $calendar->storeEvent($event, [$credentials['id']]);
-
-        $GLOBALS['user'] = $oldUser;
+        $event = new \CalendarDate();
+        $event->setId($event->getNewId());
+        $now = time();
+        $event->begin = $now;
+        $event->end = $now + 3600;
+        $event->store();
+        $calendar_date = new \CalendarDateAssignment();
+        $calendar_date->setId([$credentials['id'], $event->getId()]);
+        $calendar_date->calendar_date = $event;
+        $calendar_date->suppress_mails = true;
+        $calendar_date->store();
     }
 }
diff --git a/tests/jsonapi/_bootstrap.php b/tests/jsonapi/_bootstrap.php
index 61325efc82c9cbb0e0bb06a4024c2bf7b05e1c25..82ae54e514aa29dc2c319848c0d9865d855a7b4d 100644
--- a/tests/jsonapi/_bootstrap.php
+++ b/tests/jsonapi/_bootstrap.php
@@ -41,6 +41,7 @@ StudipAutoloader::register();
 
 // General classes folders
 StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/models');
+StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/models/calendar');
 StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/models/resources');
 StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/classes');
 StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/classes', 'Studip');