From 39745c9aa8bb099e8bda1f4d775ed229dbe97be4 Mon Sep 17 00:00:00 2001
From: Rasmus Fuhse <fuhse@data-quest.de>
Date: Fri, 10 Jan 2025 12:13:24 +0000
Subject: [PATCH] Resolve "Studiengruppen erweitern" - Hauptbronch

Closes #3616

Merge request studip/studip!2509
---
 app/controllers/admin/tags.php                |  67 ++++
 app/controllers/course/connectedcourses.php   | 234 +++++++++++++
 .../course/connectedstudygroups.php           | 315 ++++++++++++++++++
 app/controllers/course/overview.php           |  11 +
 app/controllers/course/studygroup.php         | 209 ++++++++++--
 app/controllers/course/wizard.php             |  11 +-
 app/controllers/my_courses.php                |  34 +-
 app/controllers/my_studygroups.php            | 100 +++++-
 app/controllers/search/angebot.php            |  12 +
 app/controllers/search/globalsearch.php       |  28 ++
 app/controllers/search/studiengaenge.php      |  14 +-
 app/views/admin/tags/index.php                |  65 ++++
 app/views/admin/tags/view_objects.php         |  51 +++
 .../connectedcourses/_course_to_connect.php   |  21 ++
 app/views/course/connectedcourses/connect.php |  78 +++++
 app/views/course/connectedcourses/index.php   | 112 +++++++
 .../_studygroup_to_connect.php                |  21 ++
 .../course/connectedstudygroups/connect.php   |  79 +++++
 .../course/connectedstudygroups/index.php     | 110 ++++++
 app/views/course/overview/index.php           |   4 +
 app/views/course/studygroup/details.php       |  19 ++
 app/views/course/studygroup/edit.php          |  27 +-
 app/views/course/studygroup/widget.php        |  88 +++++
 app/views/course/wizard/step.php              |  14 +-
 .../steps/basicdata/index_studygroup.php      |   7 +
 .../course/wizard/steps/studygroups/index.php | 110 ++++++
 app/views/my_studygroups/_course.php          |  46 +--
 app/views/my_studygroups/index.php            |  17 +-
 app/views/my_studygroups/proposals.php        |  31 ++
 app/views/search/studiengaenge/verlauf.php    |  55 +++
 app/views/studygroup/browse.php               |  54 +--
 db/migrations/6.0.38_improved_studygroups.php | 305 +++++++++++++++++
 .../ConnectedcourseAdmission.class.php        | 121 +++++++
 .../connectedcourseadmission/rule.manifest    |   2 +
 .../templates/configure.php                   |   5 +
 .../templates/info.php                        |   1 +
 lib/classes/Avatar.php                        |   2 +-
 lib/classes/MyRealmModel.php                  |  95 +-----
 lib/classes/StudipController.php              |   6 +
 lib/classes/StudygroupModel.php               | 246 ++++++++++++--
 lib/classes/admission/AdmissionRule.php       |   2 +-
 lib/classes/admission/CourseSet.php           |  39 +++
 .../coursewizardsteps/BasicDataWizardStep.php |  50 ++-
 lib/classes/forms/MultiquicksearchInput.php   |  31 ++
 .../globalsearch/GlobalSearchStudygroups.php  | 315 ++++++++++++++++++
 .../searchtypes/StudygroupSearch.class.php    | 103 ++++++
 lib/cronjobs/expire_studygroups.class.php     |  53 +++
 lib/cronjobs/studygroup_expiration.php        |  97 ++++++
 lib/models/Course.php                         |  34 ++
 lib/models/StudiengangTeil.php                |  26 ++
 lib/models/StudygroupCourse.php               |  29 ++
 lib/models/StudygroupCourseProposal.php       |  36 ++
 lib/models/StudygroupStgteil.php              |  19 ++
 lib/models/Tag.php                            |  55 +++
 lib/models/TagRelation.php                    |  19 ++
 lib/modules/CoreAdmin.php                     |   5 +
 lib/modules/CoreStudygroupAdmin.php           |   1 +
 lib/modules/MyStudygroupsWidget.php           |  38 +++
 lib/modules/StudygroupWidget.php              |  39 +++
 lib/navigation/AdminNavigation.php            |   8 +-
 .../assets/javascripts/lib/global_search.js   |   7 +
 resources/assets/javascripts/lib/search.js    |   7 +
 resources/assets/stylesheets/scss/forms.scss  |   7 +
 .../assets/stylesheets/scss/studygroup.scss   | 104 ++++++
 resources/assets/stylesheets/scss/tables.scss |   6 +-
 resources/vue/base-components.js              |   1 +
 resources/vue/components/Multiquicksearch.vue |  96 ++++++
 resources/vue/components/MyCoursesTables.vue  |   5 -
 resources/vue/components/Quicksearch.vue      |   1 +
 templates/forms/multiquicksearch_input.php    |  17 +
 templates/forms/quicksearch_input.php         |   1 +
 templates/header.php                          |   6 +-
 templates/start/my_studygroups.php            |   3 +
 templates/start/studygroups.php               |   3 +
 .../_generated/JsonapiTesterActions.php       |   2 +-
 75 files changed, 3870 insertions(+), 222 deletions(-)
 create mode 100644 app/controllers/admin/tags.php
 create mode 100644 app/controllers/course/connectedcourses.php
 create mode 100644 app/controllers/course/connectedstudygroups.php
 create mode 100644 app/views/admin/tags/index.php
 create mode 100644 app/views/admin/tags/view_objects.php
 create mode 100644 app/views/course/connectedcourses/_course_to_connect.php
 create mode 100644 app/views/course/connectedcourses/connect.php
 create mode 100644 app/views/course/connectedcourses/index.php
 create mode 100644 app/views/course/connectedstudygroups/_studygroup_to_connect.php
 create mode 100644 app/views/course/connectedstudygroups/connect.php
 create mode 100644 app/views/course/connectedstudygroups/index.php
 create mode 100644 app/views/course/studygroup/widget.php
 create mode 100644 app/views/course/wizard/steps/studygroups/index.php
 create mode 100644 app/views/my_studygroups/proposals.php
 create mode 100644 db/migrations/6.0.38_improved_studygroups.php
 create mode 100644 lib/admissionrules/connectedcourseadmission/ConnectedcourseAdmission.class.php
 create mode 100644 lib/admissionrules/connectedcourseadmission/rule.manifest
 create mode 100644 lib/admissionrules/connectedcourseadmission/templates/configure.php
 create mode 100644 lib/admissionrules/connectedcourseadmission/templates/info.php
 create mode 100644 lib/classes/forms/MultiquicksearchInput.php
 create mode 100644 lib/classes/globalsearch/GlobalSearchStudygroups.php
 create mode 100644 lib/classes/searchtypes/StudygroupSearch.class.php
 create mode 100644 lib/cronjobs/expire_studygroups.class.php
 create mode 100644 lib/cronjobs/studygroup_expiration.php
 create mode 100644 lib/models/StudygroupCourse.php
 create mode 100644 lib/models/StudygroupCourseProposal.php
 create mode 100644 lib/models/StudygroupStgteil.php
 create mode 100644 lib/models/Tag.php
 create mode 100644 lib/models/TagRelation.php
 create mode 100644 lib/modules/MyStudygroupsWidget.php
 create mode 100644 lib/modules/StudygroupWidget.php
 create mode 100644 resources/vue/components/Multiquicksearch.vue
 create mode 100644 templates/forms/multiquicksearch_input.php
 create mode 100644 templates/start/my_studygroups.php
 create mode 100644 templates/start/studygroups.php

diff --git a/app/controllers/admin/tags.php b/app/controllers/admin/tags.php
new file mode 100644
index 00000000000..03819bd51d5
--- /dev/null
+++ b/app/controllers/admin/tags.php
@@ -0,0 +1,67 @@
+<?php
+
+class Admin_TagsController extends AuthenticatedController
+{
+    /**
+     * Common tasks for all actions.
+     */
+    public function before_filter(&$action, &$args)
+    {
+        parent::before_filter($action, $args);
+
+        $GLOBALS['perm']->check('root');
+        Navigation::activateItem('/admin/locations/tags');
+        PageLayout::setTitle(_('Schlagwortverwaltung'));
+    }
+
+    public function index_action()
+    {
+        Tag::deleteBySQL('LEFT JOIN `tags_relations` ON (`tags`.`id` = `tags_relations`.`tag_id`) WHERE `tags_relations`.`range_id` IS NULL');
+        $this->page = Request::int('page', 0);
+        $this->tags = Tag::findBySQL('1 ORDER BY `name` ASC LIMIT :offset, :limit', [
+            'offset' => $this->page * Config::get()->ENTRIES_PER_PAGE,
+            'limit' => Config::get()->ENTRIES_PER_PAGE
+        ]);
+        $this->all_tags = Tag::countBySql('1');
+    }
+
+    public function edit_action(Tag $tag)
+    {
+        PageLayout::setTitle(sprintf(_('Schlagwort „%s“ bearbeiten'), $tag->name));
+        $form = \Studip\Forms\Form::fromSORM(
+            $tag,
+            [
+                'legend' => _('Grunddaten'),
+                'fields' => [
+                    'name' => [
+                        'label' =>_('Name'),
+                        'validate' => function ($value) use ($tag) {
+                            $output = '';
+                            if ($value !== mb_strtolower($value)) {
+                                $output .= _('Schlagwörter sollen keine Großbuchstaben entahlten').' ';
+                            }
+                            foreach (['\n', '#', '|', ' '] as $forbidden) {
+                                if (str_contains($value, $forbidden)) {
+                                    $output .= _('Schlagwörter dürfen keine Zeilenumbrüche, Leerzeichen, Doppelkreuze (#) oder Pipe-Zeichen (|) enthalten.').' ';
+                                    break;
+                                }
+                            }
+                            if (Tag::findOneByName($value) && $value !== $tag->name) {
+                                $output .= _('Dieses Schlagwort ist schon vergeben.').' ';
+                            }
+                            return $output !== '' ? $output : true;
+                        }
+                    ],
+                    'active' => _('Aktiv')
+                ]
+            ]
+        )->autoStore()->setURL($this->indexURL());
+        $this->render_form($form);
+    }
+
+    public function view_objects_action(Tag $tag)
+    {
+        $this->tag = $tag;
+        PageLayout::setTitle(sprintf(_("Verknüpfte Objekte mit Schlagwort „%s“"), $tag->name));
+    }
+}
diff --git a/app/controllers/course/connectedcourses.php b/app/controllers/course/connectedcourses.php
new file mode 100644
index 00000000000..dcbc3ae87a0
--- /dev/null
+++ b/app/controllers/course/connectedcourses.php
@@ -0,0 +1,234 @@
+<?php
+
+class Course_ConnectedcoursesController extends AuthenticatedController
+{
+
+    public function before_filter(&$action, &$args)
+    {
+        parent::before_filter($action, $args);
+
+        if (!$GLOBALS['perm']->have_studip_perm('tutor', Context::getId())) {
+            throw new AccessDeniedException();
+        }
+    }
+
+    public function index_action()
+    {
+        Navigation::activateItem('/course/admin/connectedcourses');
+        $this->connected = StudygroupCourse::findBySQL(
+            'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses.course_id) WHERE studygroup_courses.studygroup_id = ? ORDER BY seminare.name ASC',
+            [
+                Context::getId()
+            ]
+        );
+        $this->proposals = StudygroupCourseProposal::findBySQL(
+            'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses_proposals.studygroup_id) WHERE studygroup_courses_proposals.studygroup_id = ? ORDER BY seminare.name ASC',
+            [
+                Context::getId()
+            ]
+        );
+        $this->buildSidebar();
+
+    }
+
+    public function connect_action($course_id = null)
+    {
+
+        Navigation::activateItem('/course/admin/connectedcourses');
+        PageLayout::setTitle(_('Veranstaltung suchen und zur Verknüpfung vorschlagen'));
+
+        if (Request::isPost() && (Request::option('course_id') || $course_id)) {
+            CSRFProtection::verifySecurityToken();
+            $course_id = $course_id ?? Request::option('course_id');
+            $status = StudygroupModel::proposeAsStudygroupTo(Context::get(), $course_id);
+            if ($status === 'connected') {
+                PageLayout::postSuccess(_('Veranstaltung wurde verknüpft.'));
+            }
+            if ($status === 'proposed') {
+                PageLayout::postSuccess(_('Vorschlag wurde eingereicht.'));
+            }
+            $this->redirect('course/connectedcourses/index');
+            return;
+        }
+
+        $connected = StudygroupCourse::findBySQL(
+            'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses.course_id) WHERE studygroup_courses.studygroup_id = ? ORDER BY seminare.name ASC',
+            [
+                Context::getId()
+            ]
+        );
+        $proposals = StudygroupCourseProposal::findBySQL(
+            'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses_proposals.studygroup_id) WHERE studygroup_courses_proposals.studygroup_id = ? ORDER BY seminare.name ASC',
+            [
+                Context::getId()
+            ]
+        );
+        $already_covered = array_map(function($c) { return $c->course_id; }, $connected);
+        $already_covered = $already_covered + array_map(function($c) { return $c->course_id; }, $proposals);
+
+
+
+
+        $studygroup_ids = [];
+        foreach (SemClass::getClasses() as $sem_class) {
+            if ($sem_class['studygroup_mode'] > 0) {
+                foreach ($sem_class->getSemTypes() as $sem_type) {
+                    $studygroup_ids[] = $sem_type['id'];
+                }
+            }
+        }
+        $this->my_courses = [];
+        if (!$GLOBALS['perm']->have_perm('admin')) {
+            $this->my_courses = Course::findBySQL('INNER JOIN `seminar_user` USING (`Seminar_id`)
+                    LEFT JOIN `semester_courses` ON (`seminare`.`Seminar_id` = `semester_courses`.`course_id`)
+                    WHERE `seminar_user`.`user_id` = :user_id
+                        AND `seminare`.`status` NOT IN (:studygroup_sem_types)
+                        AND (`semester_courses`.`semester_id` IS NULL OR `semester_courses`.`semester_id` = :semester_id)
+                        AND `seminare`.`Seminar_id` NOT IN (:ignore)
+                    ORDER BY `seminare`.`name` ASC ',
+                [
+                    'user_id' => User::findCurrent()->id,
+                    'studygroup_sem_types' => $studygroup_ids,
+                    'semester_id' => Request::get('semester_id') ?? Semester::findCurrent()->id,
+                    'ignore' => count($already_covered) ? $already_covered : ''
+                ]);
+            foreach ($this->my_courses as $my_course) {
+                $already_covered[] = $my_course->id;
+            }
+        }
+
+        if (Request::get('search') && Request::get('search') != 1) {
+            //do the search:
+            $query = SQLQuery::table('seminare')
+                ->where('search',
+                    '`name` LIKE :search OR `VeranstaltungsNummer` LIKE :search',
+                    ['search' => '%' . Request::get('search') . '%']
+                )
+                ->where(
+                    'studygroups',
+                    '`seminare`.`status` NOT IN (:sem_type_ids)',
+                    ['sem_type_ids' => $studygroup_ids]
+                )
+                ->groupBy('`seminare`.`Seminar_id`');
+            if (count($already_covered) > 0) {
+                $query->where(
+                    'ignore',
+                    '`seminare`.`Seminar_id` NOT IN (:ignore)',
+                    ['ignore' => $already_covered]
+                );
+            }
+            if (!empty(Request::get('semester_id'))) {
+                $query->join(
+                    'semester_courses',
+                    'semester_courses',
+                    '`semester_courses`.`course_id` = `seminare`.`Seminar_id`',
+                    'LEFT JOIN'
+                );
+                $query->where(
+                    'semester_id',
+                    'semester_courses.semester_id = :semester_id OR semester_courses.semester_id IS NULL',
+                    ['semester_id' => Request::get('semester_id')]
+                );
+            }
+            $this->searchresults = $query->fetchAll(Course::class);
+        } else {
+            //get up to 10 courses with a lot of members of the current studygroup:
+            $statement = DBManager::get()->prepare("
+                SELECT `seminare`.*
+                FROM `seminar_user`
+                    INNER JOIN `seminare` ON (`seminare`.`Seminar_id` = `seminar_user`.`Seminar_id`)
+                    INNER JOIN `seminar_user` AS `su2` ON (`su2`.`user_id` = `seminar_user`.`user_id` AND `su2`.`Seminar_id` = :course_id)
+                    LEFT JOIN `studygroup_courses` ON (`studygroup_courses`.`course_id` = `seminare`.`Seminar_id` AND `studygroup_courses`.`studygroup_id` = `su2`.`Seminar_id`)
+                WHERE `seminare`.`status` NOT IN (:studygroup_sem_types)
+                    AND `studygroup_courses`.`id` IS NULL
+                    AND `seminare`.`Seminar_id` NOT IN (:ignore)
+                GROUP BY `seminare`.`Seminar_id`
+                HAVING COUNT(`seminar_user`.`user_id`) > 1
+                ORDER BY COUNT(`seminar_user`.`user_id`) DESC
+                LIMIT 20
+            ");
+            $statement->execute([
+                'course_id' => Context::getId(),
+                'studygroup_sem_types' => $studygroup_ids,
+                'ignore' => count($already_covered) ? $already_covered : ''
+            ]);
+            $suggestions = $statement->fetchAll(PDO::FETCH_ASSOC);
+            $this->suggestions = array_map(function ($d) {
+                return Course::buildExisting($d);
+            }, $suggestions);
+        }
+
+
+    }
+
+    public function remove_action($course_id)
+    {
+        if (Request::isPost() && $course_id) {
+            CSRFProtection::verifySecurityToken();
+            StudygroupCourse::deleteBySQL('course_id = ? AND studygroup_id = ?', [
+                $course_id,
+                Context::getId()
+            ]);
+            PageLayout::postSuccess(_('Verknüpfung zu der Veranstaltung wurde aufgehoben.'));
+        }
+        $this->redirect('course/connectedcourses/index');
+    }
+
+    public function decline_action(StudygroupCourseProposal $proposal)
+    {
+        if (Request::isPost()) {
+            CSRFProtection::verifySecurityToken();
+            if ($GLOBALS['perm']->have_studip_perm('tutor', $proposal['course_id']) || $GLOBALS['perm']->have_studip_perm('tutor', $proposal['studygroup_id'])) {
+                if ($proposal['proposed_from'] === 'course') {
+                    PageLayout::postSuccess(_('Vorschlag wurde abgewiesen.'));
+                    $statement = DBManager::get()->prepare("
+                        SELECT `username`, `user_id`
+                        FROM `auth_user_md5`
+                            INNER JOIN `seminar_user` USING (`user_id`)
+                        WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent')
+                    ");
+                    $statement->execute([$proposal['course_id']]);
+                    $messaging = new messaging();
+
+                    foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) {
+                        setTempLanguage($user_data['user_id']);
+                        $messaging->insert_message(
+                            sprintf(
+                                _('Ihr Vorschlag, die Studiengruppe „%1$s“ mit der Veranstaltung „%2$s“ zu verknüpfen, wurde leider abgewiesen.'),
+                                Context::get()->getFullname(),
+                                $proposal->studygroup->getFullname()
+                            ),
+                            $user_data['username'],
+                            '____%system%____',
+                            '',
+                            '',
+                            '',
+                            '',
+                            _('Verknüpfungsvorschlag abgewiesen'),
+                            '',
+                            'normal',
+                            ['Studiengruppe']
+                        );
+                        restoreLanguage();
+                    }
+                } else {
+                    PageLayout::postSuccess(_('Vorschlag wurde zurückgezogen.'));
+                }
+                $proposal->delete();
+            }
+        }
+        $this->redirect('course/connectedcourses/index');
+    }
+
+    protected function buildSidebar()
+    {
+        $actions = new ActionsWidget();
+        $actions->addLink(
+            _('Verknüpfung vorschlagen'),
+            $this->url_for('course/connectedcourses/connect'),
+            Icon::create('add'),
+            ['data-dialog' => 1]
+        );
+        Sidebar::Get()->addWidget($actions);
+    }
+}
diff --git a/app/controllers/course/connectedstudygroups.php b/app/controllers/course/connectedstudygroups.php
new file mode 100644
index 00000000000..9ef6b920087
--- /dev/null
+++ b/app/controllers/course/connectedstudygroups.php
@@ -0,0 +1,315 @@
+<?php
+
+class Course_ConnectedstudygroupsController extends AuthenticatedController
+{
+
+    public function before_filter(&$action, &$args)
+    {
+        parent::before_filter($action, $args);
+
+        if (!$GLOBALS['perm']->have_studip_perm('tutor', Context::getId())) {
+            throw new AccessDeniedException();
+        }
+    }
+
+    public function index_action()
+    {
+        Navigation::activateItem('/course/admin/connectedstudygroups');
+        $this->connected = StudygroupCourse::findBySQL(
+            'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses.studygroup_id) WHERE studygroup_courses.course_id = ? ORDER BY seminare.name ASC',
+            [
+                Context::getId()
+            ]
+        );
+        $this->proposals = StudygroupCourseProposal::findBySQL(
+            'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses_proposals.studygroup_id) WHERE studygroup_courses_proposals.course_id = ? ORDER BY seminare.name ASC',
+            [
+                Context::getId()
+            ]
+        );
+        $this->buildSidebar();
+
+    }
+
+    public function connect_action($course_id = null)
+    {
+        Navigation::activateItem('/course/admin/connectedstudygroups');
+        PageLayout::setTitle(_('Studiengruppe suchen und verknüpfen'));
+        if (Request::isPost() && (Request::option('course_id') || $course_id)) {
+            CSRFProtection::verifySecurityToken();
+            $course_id = $course_id ?? Request::option('course_id');
+            $proposal = StudygroupCourseProposal::findOneBySQL('course_id = ? AND studygroup_id = ?', [
+                Context::getId(),
+                $course_id
+            ]);
+            if ($GLOBALS['perm']->have_studip_perm('tutor', $course_id) || $proposal['proposed_from'] === 'studygroup') {
+                $connection = StudygroupCourse::findOneBySQL('course_id = ? AND studygroup_id = ?', [
+                    Context::getId(),
+                    $course_id
+                ]);
+                if (!$connection) {
+                    $connection = new StudygroupCourse();
+                    $connection['course_id'] = Context::getId();
+                    $connection['studygroup_id'] = $course_id;
+                    $connection->store();
+                }
+                if ($proposal) {
+                    if ($proposal['proposed_from'] === 'studygroup') {
+                        $statement = DBManager::get()->prepare("
+                            SELECT `username`, `user_id`
+                            FROM `auth_user_md5`
+                                INNER JOIN `seminar_user` USING (`user_id`)
+                            WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent')
+                        ");
+                        $statement->execute([$course_id]);
+                        $messaging = new messaging();
+
+                        foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) {
+                            setTempLanguage($user_data['user_id']);
+                            $messaging->insert_message(
+                                sprintf(
+                                    _('Ihr Vorschlag, die Studiengruppe „%1$s“ mit der Veranstaltung „%2$s“ zu verknüpfen, wurde angenommen.'),
+                                    Context::get()->getFullname(),
+                                    Course::find($course_id)->getFullname()
+                                ),
+                                $user_data['username'],
+                                '____%system%____',
+                                '',
+                                '',
+                                '',
+                                '',
+                                _('Verknüpfungsvorschlag angenommen'),
+                                '',
+                                'normal',
+                                ['Studiengruppe']
+                            );
+                            restoreLanguage();
+                        }
+                    }
+                    $proposal->delete();
+                }
+                PageLayout::postSuccess(_('Veranstaltung wurde verknüpft.'));
+            } else {
+                //send message:
+                if (!$proposal) {
+                    $proposal = new StudygroupCourseProposal();
+                    $proposal['course_id'] = Context::getId();
+                    $proposal['studygroup_id'] = $course_id;
+                    $proposal['proposed_from'] = 'course';
+                    $proposal['user_id'] = User::findCurrent()->id;
+                    $proposal->store();
+
+                    $statement = DBManager::get()->prepare("
+                        SELECT `username`, `user_id`
+                        FROM `auth_user_md5`
+                            INNER JOIN `seminar_user` USING (`user_id`)
+                        WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent')
+                    ");
+                    $statement->execute([$course_id]);
+                    $messaging = new messaging();
+                    $oldbase = URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']);
+
+                    foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) {
+                        setTempLanguage($user_data['user_id']);
+                        $messaging->insert_message(
+                            sprintf(
+                                _('Es wurde vorgeschlagen, die Veranstaltung „%1$s“ mit Ihrer Studiengruppe „%2$s“ zu verknüpfen. Sie können den Vorschlag unter folgendem Link annehmen oder ablehnen:'),
+                                Context::get()->getFullname(),
+                                Course::find($course_id)->getFullname()
+                            )."\n\n".URLHelper::getURL('dispatch.php/course/connectedcourses/index', ['cid' => $course_id]),
+                            $user_data['username'],
+                            '____%system%____',
+                            '',
+                            '',
+                            '',
+                            '',
+                            _('Verknüpfung Ihrer Studiengruppe zu einer Veranstaltung'),
+                            '',
+                            'normal',
+                            ['Studiengruppe']
+                        );
+                        restoreLanguage();
+                    }
+
+                    URLHelper::setBaseURL($oldbase);
+                }
+                PageLayout::postSuccess(_('Antrag wurde gestellt.'));
+            }
+            $this->redirect('course/connectedstudygroups/index');
+            return;
+        }
+
+        $connected = StudygroupCourse::findBySQL(
+            'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses.studygroup_id) WHERE studygroup_courses.course_id = ? ORDER BY seminare.name ASC',
+            [
+                Context::getId()
+            ]
+        );
+        $proposals = StudygroupCourseProposal::findBySQL(
+            'INNER JOIN seminare ON (seminare.Seminar_id = studygroup_courses_proposals.course_id) WHERE studygroup_courses_proposals.course_id = ? ORDER BY seminare.name ASC', 
+            [
+                Context::getId()
+            ]
+        );
+        $already_covered = array_map(function ($c) {
+            return $c->course_id;
+        }, $connected);
+        $already_covered = $already_covered + array_map(function ($c) {
+            return $c->course_id;
+        }, $proposals);
+
+        $studygroup_ids = [];
+        foreach (SemClass::getClasses() as $sem_class) {
+            if ($sem_class['studygroup_mode'] > 0) {
+                foreach ($sem_class->getSemTypes() as $sem_type) {
+                    $studygroup_ids[] = $sem_type['id'];
+                }
+            }
+        }
+
+        if (Request::get('search') && Request::get('search') != 1) {
+            $query = SQLQuery::table('seminare')
+                ->where('search', '`name` LIKE :search', ['search' => '%' . Request::get('search') . '%'])
+                ->where(
+                    'studygroups',
+                    '`seminare`.`status` IN (:sem_type_ids)',
+                    ['sem_type_ids' => $studygroup_ids]
+                )
+                ->groupBy('`seminare`.`Seminar_id`');
+            if (count($already_covered) > 0) {
+                $query->where(
+                    'ignore',
+                    '`seminare`.`Seminar_id` NOT IN (:ignore)',
+                    ['ignore' => $already_covered]
+                );
+            }
+            if (!empty(Request::get('semester_id'))) {
+                $query->join(
+                    'semester_courses',
+                    'semester_courses',
+                    '`semester_courses`.`course_id` = `seminare`.`Seminar_id`',
+                    'LEFT JOIN'
+                );
+                $query->where(
+                    'semester_id',
+                    'semester_courses.semester_id = :semester_id OR semester_courses.semester_id IS NULL',
+                    ['semester_id' => Request::get('semester_id')]
+                );
+            }
+            $this->searchresults = $query->fetchAll(Course::class);
+        } else {
+
+            $this->my_studygroups = [];
+            if (!$GLOBALS['perm']->have_perm('admin')) {
+                $this->my_studygroups = Course::findBySQL('INNER JOIN `seminar_user` USING (`Seminar_id`)
+                    WHERE `seminar_user`.`user_id` = :user_id
+                        AND `seminare`.`status` IN (:studygroup_sem_types)
+                        AND `seminare`.`Seminar_id` NOT IN (:ignore)
+                    ORDER BY `seminare`.`name` ASC ',
+                    [
+                        'user_id' => User::findCurrent()->id,
+                        'studygroup_sem_types' => $studygroup_ids,
+                        'ignore' => count($already_covered) ? $already_covered : ''
+                    ]);
+                foreach ($this->my_studygroups as $my_studygroup) {
+                    $already_covered[] = $my_studygroup->id;
+                }
+            }
+
+            //get all studygroups with a lot of members in the current course:
+            $statement = DBManager::get()->prepare("
+                SELECT `seminare`.*
+                FROM `seminar_user`
+                    INNER JOIN `seminare` ON (`seminare`.`Seminar_id` = `seminar_user`.`Seminar_id`)
+                    LEFT JOIN `seminar_user` AS `su2` ON (`su2`.`user_id` = `seminar_user`.`user_id` AND `su2`.`Seminar_id` = :course_id)
+                    LEFT JOIN `studygroup_courses` ON (`studygroup_courses`.`studygroup_id` = `seminare`.`Seminar_id` AND `studygroup_courses`.`course_id` = `su2`.`Seminar_id`)
+                WHERE `seminare`.`status` IN (:studygroup_sem_types)
+                    AND `studygroup_courses`.`id` IS NULL
+                GROUP BY `seminare`.`Seminar_id`
+                HAVING COUNT(`seminar_user`.`user_id`) > 1
+                ORDER BY COUNT(`seminar_user`.`user_id`) DESC
+                LIMIT 20
+            ");
+            $statement->execute([
+                'course_id' => Context::getId(),
+                'studygroup_sem_types' => $studygroup_ids
+            ]);
+            $this->suggestions = $statement->fetchAll(PDO::FETCH_ASSOC);
+            $this->suggestions = array_map(function ($d) {
+                return Course::buildExisting($d);
+            }, $this->suggestions);
+        }
+    }
+
+    public function remove_action($course_id)
+    {
+        if (Request::isPost() && $course_id) {
+            CSRFProtection::verifySecurityToken();
+            $connection = StudygroupCourse::deleteBySQL('course_id = ? AND studygroup_id = ?', [
+                Context::getId(),
+                $course_id
+            ]);
+            PageLayout::postSuccess(_('Verknüpfung zu der Studiengruppe wurde aufgehoben.'));
+        }
+        $this->redirect('course/connectedstudygroups/index');
+    }
+
+    public function decline_action(StudygroupCourseProposal $proposal)
+    {
+        if (Request::isPost()) {
+            CSRFProtection::verifySecurityToken();
+            if ($GLOBALS['perm']->have_studip_perm('tutor', $proposal['course_id']) || $GLOBALS['perm']->have_studip_perm('tutor', $proposal['studygroup_id'])) {
+                if ($proposal['proposed_from'] === 'studygroup') {
+                    PageLayout::postSuccess(_('Vorschlag wurde abgewiesen.'));
+                    $statement = DBManager::get()->prepare("
+                        SELECT `username`, `user_id`
+                        FROM `auth_user_md5`
+                            INNER JOIN `seminar_user` USING (`user_id`)
+                        WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent')
+                    ");
+                    $statement->execute([$proposal['studygroup_id']]);
+                    $messaging = new messaging();
+
+                    foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) {
+                        setTempLanguage($user_data['user_id']);
+                        $messaging->insert_message(
+                            sprintf(
+                                _('Ihr Vorschlag, die Studiengruppe „%1$s“ mit der Veranstaltung „%2$s“ zu verknüpfen, wurde leider abgewiesen.'),
+                                $proposal->studygroup->getFullname(),
+                                Context::get()->getFullname()
+                            ),
+                            $user_data['username'],
+                            '____%system%____',
+                            '',
+                            '',
+                            '',
+                            '',
+                            _('Verknüpfungsvorschlag abgewiesen'),
+                            '',
+                            'normal',
+                            ['Studiengruppe']
+                        );
+
+                        restoreLanguage();
+                    }
+                } else {
+                    PageLayout::postSuccess(_('Vorschlag wurde zurückgezogen.'));
+                }
+                $proposal->delete();
+            }
+        }
+        $this->redirect('course/connectedstudygroups/index');
+    }
+
+    protected function buildSidebar()
+    {
+        $actions = new ActionsWidget();
+        $actions->addLink(
+            _('Studiengruppe verknüpfen'),
+            $this->url_for('course/connectedstudygroups/connect'),
+            Icon::create('add'),
+            ['data-dialog' => 1]
+        );
+        Sidebar::Get()->addWidget($actions);
+    }
+}
diff --git a/app/controllers/course/overview.php b/app/controllers/course/overview.php
index e1da00e303f..4313cdc71a1 100644
--- a/app/controllers/course/overview.php
+++ b/app/controllers/course/overview.php
@@ -108,6 +108,17 @@ class Course_OverviewController extends AuthenticatedController
             $this->avatar   = StudygroupAvatar::getAvatar($this->course_id);
         }
 
+        $connections = StudygroupCourse::countBySql(
+            "`studygroup_id` = :cid OR `course_id` = :cid", 
+            [
+                'cid' => $this->course_id
+            ]
+        );
+        if ($connections > 0) {
+            $response                   = $this->relay('course/studygroup/widget/' . $this->course_id);
+            $this->connectedstudygroups = $response->body;
+        }
+
         $this->plugins = PluginEngine::getPlugins(StandardPlugin::class, $this->course_id);
 
         $sidebar = Sidebar::get();
diff --git a/app/controllers/course/studygroup.php b/app/controllers/course/studygroup.php
index 981a152f2a5..c95c20e89d6 100644
--- a/app/controllers/course/studygroup.php
+++ b/app/controllers/course/studygroup.php
@@ -170,43 +170,168 @@ class Course_StudygroupController extends AuthenticatedController
      */
     public function edit_action()
     {
-        global $perm;
-
-        $id = Context::getId();
-
+        PageLayout::setTitle(Context::getHeaderLine() . ' - ' . _('Studiengruppe bearbeiten'));
+        Navigation::activateItem('/course/admin/main');
         PageLayout::setHelpKeyword('Basis.StudiengruppenBearbeiten');
 
-        // if we are permitted to edit the studygroup get some data...
-        if ($id && $perm->have_studip_perm('dozent', $id)) {
-            $this->course = Course::find($id);
+        $course = Context::get();
 
-            PageLayout::setTitle(Context::getHeaderLine() . ' - ' . _('Studiengruppe bearbeiten'));
-            Navigation::activateItem('/course/admin/main');
+        $expiration_date = CourseConfig::get($course->id)->STUDYGROUP_EXPIRATION_DATE;
+        if (!$expiration_date) {
+            $expiration_date = _('Unbegrenzt');
+        }
 
-            $this->course_id         = $id;
-            $this->sem_class         = $GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$this->course->status]['class']];
-            $this->tutors            = CourseMember::findByCourseAndStatus($this->course->id, 'tutor');
-            $this->founders          = StudygroupModel::getFounders($id);
+        $zugang_options = [
+            'all' => _('Offen für alle'),
+            'invite' => _('Auf Anfrage'),
+            'connectedcourse' => _('Für Mitglieder der zugehörigen Lehrveranstaltung')
+        ];
+        if (Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED) {
+            $zugang_options['invisible'] = _('Unsichtbar');
+        }
 
-            $actions = new ActionsWidget();
+        $form = \Studip\Forms\Form::fromSORM(Context::get(), [
+            'legend' => _('Grunddaten'),
+            'fields' => [
+                'name' => [
+                    'label' => _('Name'),
+                    'required' => true
+                ],
+                'beschreibung' => _('Beschreibung'),
+                'zugang' => [
+                    'label' => _('Zugang'),
+                    'type' => 'select',
+                    'options' => $zugang_options,
+                    'value' => function () use ($course) {
+                        $courseset = CourseSet::getSetForCourse($course->id);
+                        if ($courseset && $courseset->getId() === CourseSet::getConnectedcourseAdmissionSetId()) {
+                            return 'connectedcourse';
+                        } elseif (!$course->visible) {
+                            return 'invisible';
+                        } else {
+                            return $course->admission_prelim > 0 ? 'invite' : 'all';
+                        }
+                    },
+                    'store' => function ($value, $input) {
+                        $course = $input->getContextObject();
+                        switch ($value) {
+                            case 'connectedcourse':
+                                CourseSet::addCourseToSet(CourseSet::getConnectedcourseAdmissionSetId(), $course->id);
+                                $course->visible = 1;
+                                break;
+                            case 'invisible':
+                                CourseSet::removeCourseFromSet(CourseSet::getConnectedcourseAdmissionSetId(), $course->id);
+                                if (Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED) {
+                                    $course->visible = 0;
+                                    break;
+                                }
+                            case 'invite':
+                                CourseSet::removeCourseFromSet(CourseSet::getConnectedcourseAdmissionSetId(), $course->id);
+                                $course->visible = 1;
+                                $course->admission_prelim = 1;
+                                $course->admission_prelim_txt = _('Die Moderator:innen der Studiengruppe können Ihren Aufnahmewunsch bestätigen oder ablehnen. Erst nach Bestätigung erhalten Sie vollen Zugriff auf die Gruppe.');
+                                break;
+                            case 'all':
+                                CourseSet::removeCourseFromSet(CourseSet::getConnectedcourseAdmissionSetId(), $course->id);
+                                $course->visible = 1;
+                                $course->admission_prelim = 0;
+                                break;
+                        }
+                        $course->store();
+                    }
+                ]
+            ]
+        ])->addSORM(
+            Context::get(), [
+                'legend' => _('Erweiterte Einstellungen'),
+                'fields' => [
+                    'ablaufdatum' => [
+                        'label' => _('Ablaufdatum / Löschdatum'),
+                        'type' => 'datetimepicker',
+                        'value' => $expiration_date,
+                        'store' => function ($value) {
+                            CourseConfig::get(Context::getId())->store('STUDYGROUP_EXPIRATION_DATE', $value);
+                        }
+                    ],
+                    'tags' => [
+                        'label' => _('Schlagwörter'),
+                        'type' => 'multiquicksearch',
+                        'addlabel' => _('Schlagwort hinzufügen'),
+                        'value' => function () {
+                            $course = Context::get();
+                            $tags = Tag::getByRange($course->id, 'course');
+                            return array_map(function ($t) { return $t->name; }, $tags);
+                        },
+                        'searchtype' => (string) SQLSearch::get('SELECT `name`, `name` FROM `tags` WHERE `active` = 1 AND `name` LIKE :input', _('Schlagwort suchen')),
+                        'autocomplete' => true,
+                        'mapper' => function ($value, $obj) {
+                            $tags = [];
+                            foreach ($value as $name) {
+                                if ($name) {
+                                    if ($tag = Tag::findOneByName($name)) {
+                                        if ($tag->active) {
+                                            $tags[] = $tag;
+                                        }
+                                    } else {
+                                        $tag = new Tag();
+                                        $tag->name = $name;
+                                        $tag->store();
+                                        $tags[] = $tag;
+                                    }
+                                }
+                            }
+                            return $tags;
+                        },
+                        'store' => function ($tags, $input) {
+                            $course = $input->getContextObject();
+                            $tag_ids = [];
+                            foreach ($tags as $tag) {
+                                $tag_ids[] = $tag->id;
+                                $relation = TagRelation::findOneBySQL(
+                                    "`range_id` = :course_id AND `range_type` = 'course' AND `tag_id` = :tag_id", 
+                                    [
+                                        'tag_id'    => $tag->id,
+                                        'course_id' => $course->id
+                                    ]
+                                );
+                                if (!$relation) {
+                                    $relation = TagRelation::create([
+                                        'range_id'   => $course->id,
+                                        'range_type' => 'course',
+                                        'tag_id'     => $tag->id,
+                                    ]);
+                                }
+                            }
+                            TagRelation::deleteBySQL(
+                                "`range_id` = :course_id AND `range_type` = 'course' AND `tag_id` NOT IN (:ids)", 
+                                [
+                                    'ids'       => $tag_ids,
+                                    'course_id' => $course->id
+                                ]
+                            );
+                        }
+                    ]
+                ]
+            ]
+        )->setURL($this->editURL())
+            ->autoStore();
 
-            $actions->addLink(
-                _('Neue Studiengruppe anlegen'),
-                $this->url_for('course/wizard?studygroup=1'),
-                Icon::create('add')
-            );
+        $actions = new ActionsWidget();
 
-            $actions->addLink(
-                _('Diese Studiengruppe löschen'),
-                $this->deleteURL(),
-                Icon::create('trash')
-            );
+        $actions->addLink(
+            _('Neue Studiengruppe anlegen'),
+            $this->url_for('course/wizard?studygroup=1'),
+            Icon::create('add')
+        );
+        $actions->addLink(
+            _('Diese Studiengruppe löschen'),
+            $this->deleteURL(),
+            Icon::create('trash')
+        );
 
-            Sidebar::get()->addWidget($actions);
-        } // ... otherwise redirect us to the seminar
-        else {
-            $this->redirect(URLHelper::getURL('dispatch.php/course/go?to=' . $id));
-        }
+        Sidebar::get()->addWidget($actions);
+
+        $this->render_form($form);
     }
 
     /**
@@ -255,14 +380,20 @@ class Course_StudygroupController extends AuthenticatedController
                     $course->schreibzugriff = 1;
                     $course->visible        = 1;
 
-                    if (Request::get('groupaccess') == 'all') {
+                    $cs_id = CourseSet::getConnectedcourseAdmissionSetId();
+                    if (Request::get('groupaccess') === 'all') {
                         $course->admission_prelim = 0;
+                        CourseSet::removeCourseFromSet($cs_id, $id);
+                    } elseif(Request::get('groupaccess') === 'top-course') {
+                        CourseSet::addCourseToSet($cs_id, $id);
                     } else {
                         $course->admission_prelim = 1;
-                        if (Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED && Request::get('groupaccess') == 'invisible') {
+                        if (Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED && Request::get('groupaccess') === 'invisible') {
                             $course->visible = 0;
                         }
                         $course->admission_prelim_txt = _('Die für die Moderation zuständigen Personen der Studiengruppe können Ihren Aufnahmewunsch bestätigen oder ablehnen. Erst nach Bestätigung erhalten Sie vollen Zugriff auf die Gruppe.');
+
+                        CourseSet::removeCourseFromSet($cs_id, $id);
                     }
                     $course->store();
                 }
@@ -783,4 +914,20 @@ class Course_StudygroupController extends AuthenticatedController
         $this->avatar_url = $avatar->getURL(Avatar::NORMAL);
     }
 
+    public function widget_action($range_id)
+    {
+        if (get_class($this->parent_controller) === __CLASS__) {
+            throw new RuntimeException('widget_action must be relayed');
+        }
+        $this->course = Course::find($range_id);
+
+        if ($this->course->isStudygroup()) {
+            $sql = "INNER JOIN `seminare` ON (`seminare`.`Seminar_id` = `studygroup_courses`.`course_id`) WHERE `studygroup_id` = ? ORDER BY `seminare`.`name` ASC";
+        } else {
+            $sql = "INNER JOIN `seminare` ON (`seminare`.`Seminar_id` = `studygroup_courses`.`studygroup_id`) WHERE `course_id` = ? ORDER BY `seminare`.`name` ASC ";
+        }
+        $this->connections = StudygroupCourse::findBySQL($sql, [$range_id]);
+    }
+
+
 }
diff --git a/app/controllers/course/wizard.php b/app/controllers/course/wizard.php
index b4244b61bea..ad289edc5c3 100644
--- a/app/controllers/course/wizard.php
+++ b/app/controllers/course/wizard.php
@@ -64,7 +64,7 @@ class Course_WizardController extends AuthenticatedController
      */
     public function index_action()
     {
-        $this->redirect('course/wizard/step/0' . ($this->studygroup ? '?studygroup=1' : ''));
+        $this->redirect('course/wizard/step/0' . ($this->studygroup ? '?studygroup=1&stgteil_id='.Request::option('stgteil_id') : ''));
     }
 
     /**
@@ -93,7 +93,10 @@ class Course_WizardController extends AuthenticatedController
             // Add special studygroup flag to set values.
             $this->setStepValues(
                 get_class($step),
-                array_merge($this->getValues(get_class($step)), ['studygroup' => 1])
+                array_merge($this->getValues(get_class($step)), [
+                    'studygroup' => 1,
+                    'stgteil_id' => Request::option('stgteil_id')
+                ])
             );
         }
         $this->values = $this->getValues();
@@ -193,11 +196,13 @@ class Course_WizardController extends AuthenticatedController
                         }
                         // A studygroup has been created.
                         if (in_array($this->course->status, studygroup_sem_types())) {
+
                             $message = MessageBox::success(sprintf(
-                                _('Die Studien-/Arbeitsgruppe "%s" wurde angelegt. '
+                                _('Die Studien-/Arbeitsgruppe „%s“ wurde angelegt. '
                                 . 'Sie können sie direkt hier weiter verwalten.'),
                                 htmlReady($this->course->name)
                             ));
+
                             $target = $this->url_for('course/studygroup/edit', ['cid' => $this->course->id]);
 
                             // "Normal" course.
diff --git a/app/controllers/my_courses.php b/app/controllers/my_courses.php
index c7a6771aa6f..a8d9797eedb 100644
--- a/app/controllers/my_courses.php
+++ b/app/controllers/my_courses.php
@@ -704,17 +704,19 @@ class MyCoursesController extends AuthenticatedController
                         }
                     }
 
-                    $groups[] = [
-                        'id' => $_outer_index,
-                        'name' => (string) $sem_data[$_outer_index]['name'],
-                        'data' => [
-                            [
-                                'id' => md5($_outer_index),
-                                'label' => false,
-                                'ids' => array_keys($_courses),
+                    if ($_outer_index) {
+                        $groups[] = [
+                            'id' => $_outer_index,
+                            'name' => (string)$sem_data[$_outer_index]['name'],
+                            'data' => [
+                                [
+                                    'id' => md5($_outer_index),
+                                    'label' => false,
+                                    'ids' => array_keys($_courses),
+                                ],
                             ],
-                        ],
-                    ];
+                        ];
+                    }
                     $temp_courses = array_merge($temp_courses, $_courses);
                 } else {
                     $count = 1;
@@ -747,11 +749,13 @@ class MyCoursesController extends AuthenticatedController
                         $temp_courses = array_merge($temp_courses, $_courses);
                     }
 
-                    $groups[] = [
-                        'id' => $_outer_index,
-                        'name' => (string) $sem_data[$_outer_index]['name'],
-                        'data' => $_groups,
-                    ];
+                    if ($_outer_index) {
+                        $groups[] = [
+                            'id' => $_outer_index,
+                            'name' => (string)$sem_data[$_outer_index]['name'],
+                            'data' => $_groups,
+                        ];
+                    }
                 }
             }
         }
diff --git a/app/controllers/my_studygroups.php b/app/controllers/my_studygroups.php
index 579d47ecc4d..ac55cf40245 100644
--- a/app/controllers/my_studygroups.php
+++ b/app/controllers/my_studygroups.php
@@ -10,15 +10,28 @@ class MyStudygroupsController extends AuthenticatedController
         }
     }
 
-    public function index_action()
+    public function index_action($is_widget = false)
     {
         PageLayout::setHelpKeyword('Basis.MeineStudiengruppen');
         PageLayout::setTitle(_('Meine Studiengruppen'));
         URLHelper::removeLinkParam('cid');
 
-        $this->studygroups  = MyRealmModel::getStudygroups();
+        $this->is_widget    = (bool)$is_widget;
+        $this->studygroups  = StudygroupModel::getStudygroups();
         $this->nav_elements = MyRealmModel::calc_single_navigation($this->studygroups);
-        $this->set_sidebar();
+
+        // do not render sidebar if this is the widget
+        if (!$this->is_widget) {
+            $this->set_sidebar();
+        }
+    }
+
+    public function proposals_action()
+    {
+        PageLayout::setHelpKeyword('Basis.MeineStudiengruppen');
+        PageLayout::setTitle(_('Meine Studiengruppen'));
+        URLHelper::removeLinkParam('cid');
+        $this->proposed_studygroups = $this->proposeStudygroups();
     }
 
     public function set_sidebar()
@@ -44,4 +57,85 @@ class MyStudygroupsController extends AuthenticatedController
         }
         $sidebar->addWidget($actions);
     }
+
+    public function proposeStudygroups($user_id = null, $amount = 4)
+    {
+        $user_id ??= User::findCurrent()->id;
+        $cache_id = 'core/studygroups/proposals/' . $user_id;
+        $cache = \Studip\Cache\Factory::getCache();
+        $studygroup_ids = $cache->read($cache_id);
+        if ($studygroup_ids !== false) {
+            return  Course::findMany($studygroup_ids);
+        }
+
+        // Vorgeschlagen werden sollen Studiengruppen,
+        // a) in denen Personen sitzen, die auch in anderen Veranstaltungen sitzen, in denen der aktive Nutzer Mitglied ist
+        // b) die zu dem Studienbereich des Studierenden gehören
+        // c) die einfach neu sind
+        // und die zudem aktiv sind. Es wird eine Liste von 36 Studiengruppen gebaut, wovon drei alle 15 Minuten im Widget
+        // angezeigt werden.
+
+        $studygroup_sem_types = array_filter(
+            array_keys($GLOBALS['SEM_TYPE']),
+            function ($sem_type_id) {
+                return (bool) $GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$sem_type_id]['class']]['studygroup_mode'];
+            }
+        );
+
+        $statement = DBManager::get()->prepare("
+            SELECT `Seminar_id` FROM (
+                SELECT `seminare`.`Seminar_id`, COUNT(`seminar_user`.`user_id`) AS `count_colleages`
+                FROM  `seminar_user` AS `my_courses`
+                    LEFT JOIN `seminar_user` AS `my_colleages` ON (`my_colleages`.`Seminar_id` = `my_courses`.`Seminar_id`)
+                    LEFT JOIN `seminar_user` ON (`my_colleages`.`user_id` = `seminar_user`.`user_id`)
+                    LEFT JOIN `seminar_user` AS `am_i_connected` ON (`seminar_user`.`Seminar_id` = `am_i_connected`.`Seminar_id` AND `am_i_connected`.`user_id` = :me)
+                    LEFT JOIN `seminare` ON (`seminare`.`Seminar_id` = `seminar_user`.`Seminar_id`)
+                WHERE `seminare`.`status` IN (:studygroup_types)
+                    AND `am_i_connected`.`user_id` IS NULL
+                    AND `my_courses`.`user_id` = :me
+                GROUP BY `seminare`.`seminar_id`
+                ORDER BY `count_colleages` DESC
+                LIMIT 12
+            ) AS `colleages_groups`
+
+            UNION SELECT `Seminar_id` FROM (
+                SELECT `seminare`.`Seminar_id`
+                FROM `seminare`
+                    LEFT JOIN `seminar_user` AS `am_i_connected` ON (`am_i_connected`.`Seminar_id` = `seminare`.`Seminar_id` AND `am_i_connected`.`user_id` = :me)
+                    INNER JOIN `studygroup_stgteil` ON (`studygroup_stgteil`.`studygroup_id` = `seminare`.`Seminar_id`)
+                    INNER JOIN `mvv_stgteil` ON (`studygroup_stgteil`.`stgteil_id` = `mvv_stgteil`.`stgteil_id`)
+                    INNER JOIN `user_studiengang` ON (`user_studiengang`.`fach_id` = `mvv_stgteil`.`fach_id`)
+                    INNER JOIN `mvv_stg_stgteil` ON (`mvv_stg_stgteil`.`stgteil_id` = `mvv_stgteil`.`stgteil_id`)
+                    INNER JOIN `mvv_studiengang` ON (`mvv_studiengang`.`studiengang_id` = `mvv_stg_stgteil`.`studiengang_id`
+                                                         AND `mvv_studiengang`.`abschluss_id` = `user_studiengang`.`abschluss_id`)
+                WHERE `am_i_connected`.`user_id` IS NULL
+                    AND `seminare`.`status` IN (:studygroup_types)
+                    AND `user_studiengang`.`user_id` = :me
+                ORDER BY rand()
+                LIMIT 12
+            ) AS `same_studyarea_groups`
+
+            UNION SELECT `Seminar_id` FROM (
+                SELECT `seminare`.`Seminar_id`
+                FROM `seminare`
+                    LEFT JOIN `seminar_user` AS `am_i_connected` ON (`am_i_connected`.`Seminar_id` = `seminare`.`Seminar_id` AND `am_i_connected`.`user_id` = :me)
+                WHERE `am_i_connected`.`user_id` IS NULL
+                    AND `seminare`.`status` IN (:studygroup_types)
+                ORDER BY `seminare`.`mkdate` DESC
+                LIMIT 12
+            ) AS `new_groups`
+
+            GROUP BY `Seminar_id`
+            ORDER BY rand()
+            LIMIT :amount
+        ");
+        $statement->execute([
+            'studygroup_types' => $studygroup_sem_types,
+            'me' => $user_id,
+            'amount' => $amount
+        ]);
+        $group_ids = $statement->fetchAll(PDO::FETCH_COLUMN);
+        $cache->write($cache_id, $group_ids, 15 * 60);
+        return Course::findMany($group_ids);
+    }
 }
diff --git a/app/controllers/search/angebot.php b/app/controllers/search/angebot.php
index 0c0f402f866..4f749d4e2a4 100644
--- a/app/controllers/search/angebot.php
+++ b/app/controllers/search/angebot.php
@@ -179,4 +179,16 @@ class Search_AngebotController extends MVVController
         $this->content = $response->body;
         $this->render_template('shared/content', $this->layout);
     }
+
+    public function remove_studygroup_action($course_id, $stgteil_id)
+    {
+        CSRFProtection::verifyUnsafeRequest();
+        if (!$GLOBALS['perm']->have_studip_perm('tutor', $course_id) && !$GLOBALS['perm']->have_perm('admin')) {
+            throw new AccessDeniedException();
+        }
+        StudygroupStgteil::deleteBySQL('`studygroup_id` = ? AND `stgteil_id` = ?', [$course_id, $stgteil_id]);
+        PageLayout::postSuccess(_('Zuordnung wurde aufgehoben.'));
+        $stgteil = StudiengangTeil::find($stgteil_id);
+        $this->redirect('search/angebot/studiengang/'.$stgteil->studiengang[0]->id);
+    }
 }
diff --git a/app/controllers/search/globalsearch.php b/app/controllers/search/globalsearch.php
index b31663f7ed7..fd82103901b 100644
--- a/app/controllers/search/globalsearch.php
+++ b/app/controllers/search/globalsearch.php
@@ -107,6 +107,17 @@ class Search_GlobalsearchController extends AuthenticatedController
             ),
             'institute_filter'
         );
+
+        $filter_widget->addElement(
+            new SelectListElement(
+                _('Studiengang'),
+                'study_course',
+                $this->getStudyCourses(),
+                '',
+                ['id' => 'study_course_select']
+            ),
+            'study_course_filter'
+        );
     }
 
     /**
@@ -170,6 +181,23 @@ class Search_GlobalsearchController extends AuthenticatedController
         return $sem_classes;
     }
 
+    /**
+     * @return array
+     */
+    private function getStudyCourses()
+    {
+
+        $this->user = User::findCurrent();
+        $study_courses = [];
+
+        foreach ($this->user->studycourses as $usc)
+        {
+            $study_courses[] = $usc->studycourse->name;
+        }
+
+        return $study_courses;
+    }
+
     /**
      * Add some information on how to use the search.
      */
diff --git a/app/controllers/search/studiengaenge.php b/app/controllers/search/studiengaenge.php
index 8f08c329967..1745ba61a7a 100644
--- a/app/controllers/search/studiengaenge.php
+++ b/app/controllers/search/studiengaenge.php
@@ -200,12 +200,12 @@ class Search_StudiengaengeController extends MVVController
         $this->with_courses = Request::option('with_courses', $_SESSION['MVV_SEARCH_SEQUENCE_WITH_COURSES'] ?? null);
         $_SESSION['MVV_SEARCH_SEQUENCE_WITH_COURSES'] = $this->with_courses;
 
-        $studiengangTeil = StudiengangTeil::find($stgteil_id);
+        $this->studiengangTeil = StudiengangTeil::find($stgteil_id);
         $versionen = StgteilVersion::findByStgteil($stgteil_id, 'start', 'DESC')->filter(function ($version) {
             $public = $GLOBALS['MVV_STGTEILVERSION']['STATUS']['values'][$version->stat]['public'];
             return (bool) $public;
         });
-        if (!$studiengangTeil || count($versionen) === 0) {
+        if (!$this->studiengangTeil || count($versionen) === 0) {
             PageLayout::postInfo(_('Kein Verlaufsplan im gewählten Bereich verfügbar.'));
         } else {
             $version_id = Request::option('version', $this->sessGet('selected_version'));
@@ -307,26 +307,24 @@ class Search_StudiengaengeController extends MVVController
             if ($studiengang_id) {
                 if ($stgteil_bez_id) {
                     $this->stgTeilBez = StgteilBezeichnung::get($stgteil_bez_id);
-                    $this->breadcrumb->append([$this->stgTeilBez, $studiengangTeil], 'verlauf');
+                    $this->breadcrumb->append([$this->stgTeilBez, $this->studiengangTeil], 'verlauf');
                 } else {
-                    $this->breadcrumb->append($studiengangTeil, 'verlauf');
+                    $this->breadcrumb->append($this->studiengangTeil, 'verlauf');
                 }
                 $this->studiengang = Studiengang::get($studiengang_id);
             }
 
             $this->setVersionSelectWidget(
                 $versionen,
-                $this->action_url('verlauf', $studiengangTeil->id, $stgteil_bez_id, $studiengang_id)
+                $this->action_url('verlauf', $this->studiengangTeil->id, $stgteil_bez_id, $studiengang_id)
             );
 
             ksort($fachsemesterData);
             $this->fachsemesterData = $fachsemesterData;
             $this->abschnitteData = $abschnitteData;
             $this->versionen = $versionen;
-            // Augsburg
             // Ausgabe des Namens ohne Fach (dieses ist im Zusatz bereits enthalten)
-            // $this->studiengangTeilName = $studiengangTeil->getDisplayName(0);
-            $this->studiengangTeilName = $studiengangTeil->getDisplayName();
+            $this->studiengangTeilName = $this->studiengangTeil->getDisplayName();
 
             // add option widget to show only modules with courses in the
             // selected semester
diff --git a/app/views/admin/tags/index.php b/app/views/admin/tags/index.php
new file mode 100644
index 00000000000..4d5067af5e6
--- /dev/null
+++ b/app/views/admin/tags/index.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * @var Admin_TagsController $controller
+ * @var Tag[] $tags
+ * @var integer $all_tags
+ * @var integer $page
+ * */
+?>
+<table class="default">
+    <caption>
+        <?= _('Schlagwörter') ?>
+        <span class="actions">
+            <?= sprintf(_('%s Schlagwörter'), $all_tags) ?>
+        </span>
+    </caption>
+    <thead>
+        <tr>
+            <th><?= _('Schlagwort') ?></th>
+            <th><?= _('Verknüpfte Objekte') ?></th>
+            <th><?= _('Aktiv') ?></th>
+            <th class="actions">
+                <?= _('Aktion') ?>
+            </th>
+        </tr>
+    </thead>
+    <tbody>
+        <? foreach ($tags as $tag) : ?>
+        <tr>
+            <td>
+                <?= htmlReady($tag['name']) ?>
+            </td>
+            <td>
+                <a href="<?= URLHelper::getLink('dispatch.php/admin/tags/view_objects/'.$tag->id) ?>" data-dialog>
+                    <?= TagRelation::countBySql('`tag_id` = ?', [$tag->id]) ?>
+                </a>
+            </td>
+            <td>
+                <?= $tag['active']
+                    ? Icon::create('checkbox-checked', Icon::ROLE_INFO)
+                    : Icon::create('checkbox-unchecked', Icon::ROLE_INFO) ?>
+            </td>
+            <td class="actions">
+                <a href="<?= $controller->edit($tag) ?>" data-dialog>
+                    <?= Icon::create('edit') ?>
+                </a>
+            </td>
+        </tr>
+        <? endforeach ?>
+        <? if (count($tags) === 0) : ?>
+        <tr>
+            <td colspan="2">
+                <?= _('Noch keine Schlagwörter vorhanden.') ?>
+            </td>
+        </tr>
+        <? endif ?>
+    </tbody>
+
+    <tfoot>
+    <tr>
+        <td colspan="4" class="actions">
+            <?= Pagination::create($all_tags, $page)->asLinks() ?>
+        </td>
+    </tr>
+    </tfoot>
+</table>
diff --git a/app/views/admin/tags/view_objects.php b/app/views/admin/tags/view_objects.php
new file mode 100644
index 00000000000..0d90f661a6b
--- /dev/null
+++ b/app/views/admin/tags/view_objects.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * @var Admin_TagsController $controller
+ * @var Tag $tag
+ * */
+?>
+<table class="default">
+    <thead>
+        <tr>
+            <th><?= _('Objekt') ?></th>
+            <th><?= _('Typ') ?></th>
+            <th></th>
+        </tr>
+    </thead>
+    <tbody>
+        <? foreach ($tag->related_objects as $relation) : ?>
+        <tr>
+            <td>
+                <?
+                switch ($relation->range_type) {
+                    case 'course':
+                        $course = Course::find($relation->range_id);
+                        if ($course) {
+                            echo '<a href="'.URLHelper::getLink($course->isStudygroup() ? 'dispatch.php/course/studygroup/details/' . $relation->range_id : 'dispatch.php/course/details/index/' . $relation->range_id) . '">';
+                            echo htmlReady($course->getFullName());
+                            echo '</a>';
+                        } else {
+                            echo $relation->range_id;
+                        }
+                        break;
+                    default:
+                        echo $relation->range_id;
+                        break;
+                }
+                ?>
+            </td>
+            <td><?
+                switch ($relation->range_type) {
+                    case 'course':
+                        echo _('Veranstaltung');
+                        break;
+                    default:
+                        echo $relation->range_type;
+                        break;
+                }
+                ?></td>
+            <td></td>
+        </tr>
+        <? endforeach ?>
+    </tbody>
+</table>
diff --git a/app/views/course/connectedcourses/_course_to_connect.php b/app/views/course/connectedcourses/_course_to_connect.php
new file mode 100644
index 00000000000..023578546c2
--- /dev/null
+++ b/app/views/course/connectedcourses/_course_to_connect.php
@@ -0,0 +1,21 @@
+<tr>
+    <td>
+        <?= CourseAvatar::getAvatar($course->id)->getImageTag(Avatar::SMALL) ?>
+        <?= htmlReady($course->getFullName()) ?>
+    </td>
+    <td>
+        <? if ($course->start_semester) : ?>
+            <?= htmlReady($course->start_semester->name) ?>
+            <? if ($course->end_semester && $course->end_semester->id !== $course->start_semester->id) : ?>
+                - <?= htmlReady($course->end_semester->name) ?>
+            <? endif ?>
+        <? endif ?>
+    </td>
+    <td class="actions">
+        <?= Icon::create('add')->asInput([
+            'title' => _('Verknüpfung mit dieser Veranstaltung vorschlagen.'),
+            'formaction' => $controller->connectURL($course->id),
+            'formmethod' => 'post'
+        ]) ?>
+    </td>
+</tr>
diff --git a/app/views/course/connectedcourses/connect.php b/app/views/course/connectedcourses/connect.php
new file mode 100644
index 00000000000..4c6a2e54a7a
--- /dev/null
+++ b/app/views/course/connectedcourses/connect.php
@@ -0,0 +1,78 @@
+<form method="get"
+      action="<?= $controller->link_for('course/connectedcourses/connect', ['search' => 1]) ?>">
+    <?= CSRFProtection::tokenTag() ?>
+    <table class="default" style="margin-top: 20px;">
+        <caption>
+            <?= _('Lehrveranstaltungen') ?>
+            <span class="actions">
+                <? if (Request::get('search')) : ?>
+                        <select name="semester_id" aria-label="<?= _('Filtern Sie optional nach einem Semester') ?>">
+                            <option value=""><?= _('In Semester') ?></option>
+                            <? foreach (array_reverse(Semester::getAll()) as $semester) : ?>
+                                <option value="<?= htmlReady($semester->id) ?>"<?= $semester->id === Request::option('semester_id') ? ' selected' : '' ?>>
+                                     <?= htmlReady($semester->name) ?>
+                                </option>
+                            <? endforeach ?>
+                        </select>
+
+                        <input type="text"
+                               name="search"
+                               id="search_connectable_courses"
+                               autofocus
+                               placeholder="<?= _('Veranstaltung suchen ...') ?>"
+                               value="<?= htmlReady(Request::get('search') != 1 ? Request::get('search') : '') ?>">
+                        <?= Icon::create('search')->asInput([
+                            'title' => _('Suchen Sie nach beliebigen Veranstaltungen'),
+                            'data-dialog' => 1
+                        ]) ?>
+                        <a href="<?= $controller->connect() ?>" data-dialog title="<?= _('Suche schließen') ?>">
+                            <?= Icon::create('decline') ?>
+                        </a>
+                <? else : ?>
+                    <?= Icon::create('search')->asInput([
+                        'title' => _('Suchen Sie nach beliebigen Veranstaltungen'),
+                        'data-dialog' => 1,
+                        'formaction' => $controller->connectURL(['search' => 1])
+                    ]) ?>
+                <? endif ?>
+            </span>
+        </caption>
+        <thead>
+            <tr>
+                <th><?= _('Name') ?></th>
+                <th><?= _('Semester') ?></th>
+                <th class="actions"><?= _('Aktion') ?></th>
+            </tr>
+        </thead>
+        <tbody>
+        <? if (!Request::get('search') || Request::get('search') == 1) : ?>
+            <? if (count($my_courses) + count($suggestions) > 0) : ?>
+                <? foreach ($my_courses as $my_course) : ?>
+                    <?= $this->render_partial('course/connectedcourses/_course_to_connect', ['course' => $my_course]) ?>
+                <? endforeach ?>
+                <? foreach ($suggestions as $suggested_course) : ?>
+                    <?= $this->render_partial('course/connectedcourses/_course_to_connect', ['course' => $suggested_course]) ?>
+                <? endforeach ?>
+            <? else : ?>
+            <tr>
+                <td colspan="3">
+                    <?= _('Suchen Sie nach Veranstaltungen.') ?>
+                </td>
+            </tr>
+            <? endif ?>
+        <? else : ?>
+            <? if (isset($searchresults) && count($searchresults)) : ?>
+            <? foreach ($searchresults as $course) : ?>
+                <?= $this->render_partial('course/connectedcourses/_course_to_connect', ['course' => $course]) ?>
+            <? endforeach ?>
+            <? else : ?>
+                <tr>
+                    <td colspan="3">
+                        <?= _('Keine passenden Ergebnisse gefunden.') ?>
+                    </td>
+                </tr>
+            <? endif ?>
+        <? endif ?>
+        </tbody>
+    </table>
+</form>
diff --git a/app/views/course/connectedcourses/index.php b/app/views/course/connectedcourses/index.php
new file mode 100644
index 00000000000..fa8006bae9c
--- /dev/null
+++ b/app/views/course/connectedcourses/index.php
@@ -0,0 +1,112 @@
+<? if (count($connected) + count($proposals) > 0) : ?>
+    <? if (count($connected) > 0) : ?>
+        <form method="post">
+            <?= CSRFProtection::tokenTag() ?>
+            <table class="default">
+                <caption>
+                    <?= _('Verknüpfte Veranstaltungen') ?>
+                    <thead>
+                    <tr>
+                        <th><?= _('Name') ?></th>
+                        <th class="actions"><?= _('Aktion') ?></th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    <? foreach ($connected as $connection) : ?>
+                        <tr>
+                            <td>
+                                <a href="<?= URLHelper::getLink('dispatch.php/course/details/' . $connection['course_id']) ?>" target="_blank">
+                                    <?= CourseAvatar::getAvatar($connection['course_id'])->getImageTag(Avatar::SMALL) ?>
+                                    <?= htmlReady($connection->course->getFullName()) ?>
+                                </a>
+                            </td>
+                            <td class="actions">
+                                <?= Icon::create('trash')->asInput([
+                                    'title' => _('Verknüpfung aufheben'),
+                                    'data-confirm' => _('Wirklich die Zuweisung zu der Veranstaltung aufheben?'),
+                                    'formaction' => $controller->url_for('course/connectedcourses/remove/'.$connection['course_id'])
+                                ]) ?>
+                            </td>
+                        </tr>
+                    <? endforeach ?>
+                    </tbody>
+                </caption>
+            </table>
+        </form>
+    <? endif ?>
+
+    <? if (count($proposals) > 0) : ?>
+        <form method="post">
+            <?= CSRFProtection::tokenTag() ?>
+            <table class="default">
+                <caption>
+                    <?= _('Eingereichte Vorschläge') ?>
+                </caption>
+                <thead>
+                <tr>
+                    <th><?= _('Name') ?></th>
+                    <th><?= _('Vorgeschlagen von') ?></th>
+                    <th class="actions"><?= _('Aktion') ?></th>
+                </tr>
+                </thead>
+                <tbody>
+                <? foreach ($proposals as $proposal) : ?>
+                    <tr>
+                        <td>
+                            <a href="<?= URLHelper::getLink('dispatch.php/course/details/' . $connection['course_id']) ?>" target="_blank">
+                                <?= CourseAvatar::getAvatar($proposal['course_id'])->getImageTag(Avatar::SMALL) ?>
+                                <?= htmlReady($proposal->course->getFullName()) ?>
+                            </a>
+                        </td>
+                        <td>
+                            <?= htmlReady($proposal->user->getFullName()) ?>
+                        </td>
+                        <td class="actions">
+                            <? if ($proposal['proposed_from'] === 'course') : ?>
+                                <?= Icon::create('accept')->asInput([
+                                    'title'        => _('Vorschlag annehmen'),
+                                    'data-confirm' => _('Wirklich die Veranstaltung mit dieser Studiengruppe verknüpfen?'),
+                                    'formaction'   => $controller->connectURL($proposal['course_id'])
+                                ]) ?>
+                            <? endif ?>
+                            <? if ($proposal['proposed_from'] === 'course') : ?>
+                                <?= Icon::create('decline')->asInput([
+                                    'title'        => _('Vorschlag ablehnen'),
+                                    'data-confirm' => _('Wirklich den Vorschlag ablehnen?'),
+                                    'formaction'   => $controller->declineURL($proposal->id)
+                                ]) ?>
+                            <? else : ?>
+                                <?= Icon::create('decline')->asInput([
+                                    'title'        => _('Vorschlag zurückziehen'),
+                                    'data-confirm' => _('Wirklich den Vorschlag zurückziehen?'),
+                                    'formaction'   => $controller->declineURL($proposal->id)
+                                ]) ?>
+                            <? endif ?>
+                        </td>
+                    </tr>
+                <? endforeach ?>
+                </tbody>
+            </table>
+        </form>
+    <? endif ?>
+
+<? else : ?>
+
+    <div class="studip-contents-overview-teaser">
+        <div class="teaser-content">
+
+            <div>
+                <header><?= _('Verknüpfung zu Lehrveranstaltungen') ?></header>
+                <?= _('Verknüpfen Sie diese Studiengruppen mit Lehrveranstaltungen, mit deren Inhalten sich diese Studiengruppe beschäftigt. Dadurch machen Sie diese Studiengruppe sichtbarer für andere Teilnehmende der Veranstaltung.') ?>
+            </div>
+
+            <?= \Studip\LinkButton::create(
+                _('Verknüpfung zu Lehrveranstaltung vorschlagen'),
+                $controller->connect(),
+                ['data-dialog' => 1]
+             )?>
+        </div>
+    </div>
+
+<? endif ?>
+
diff --git a/app/views/course/connectedstudygroups/_studygroup_to_connect.php b/app/views/course/connectedstudygroups/_studygroup_to_connect.php
new file mode 100644
index 00000000000..d438cf1eafa
--- /dev/null
+++ b/app/views/course/connectedstudygroups/_studygroup_to_connect.php
@@ -0,0 +1,21 @@
+<tr>
+    <td>
+        <?= StudygroupAvatar::getAvatar($course->id)->getImageTag(Avatar::SMALL) ?>
+        <?= htmlReady($course->getFullName()) ?>
+    </td>
+    <td>
+        <? if ($course->start_semester) : ?>
+            <?= htmlReady($course->start_semester->name) ?>
+            <? if ($course->end_semester && $course->end_semester->id !== $course->start_semester->id) : ?>
+                - <?= htmlReady($course->end_semester->name) ?>
+            <? endif ?>
+        <? endif ?>
+    </td>
+    <td class="actions">
+        <?= Icon::create('add')->asInput([
+            'title' => _('Verknüpfung mit dieser Studiengruppe vorschlagen.'),
+            'formaction' => $controller->connectURL($course->id),
+            'formmethod' => 'post'
+        ]) ?>
+    </td>
+</tr>
diff --git a/app/views/course/connectedstudygroups/connect.php b/app/views/course/connectedstudygroups/connect.php
new file mode 100644
index 00000000000..9f7c0948f10
--- /dev/null
+++ b/app/views/course/connectedstudygroups/connect.php
@@ -0,0 +1,79 @@
+<form method="get" action="<?= $controller->connect(['search' => 1]) ?>">
+    <?= CSRFProtection::tokenTag() ?>
+    <table class="default" style="margin-top: 20px;">
+        <caption>
+            <?= _('Studiengruppen') ?>
+            <span class="actions">
+            <? if (Request::get('search') || Request::get('semester_id')) : ?>
+                <select name="semester_id" aria-label="<?= _('Filtern Sie optional nach einem Semester') ?>">
+                    <option value=""><?= _('In Semester') ?></option>
+                    <? foreach (array_reverse(Semester::getAll()) as $semester) : ?>
+                    <option value="<?= htmlReady($semester->id) ?>"<?= $semester->id === Request::option('semester_id') ? ' selected' : '' ?>><?= htmlReady($semester->name) ?></option>
+                    <? endforeach ?>
+                </select>
+
+                <input type="text"
+                       name="search"
+                       id="search_connectable_courses"
+                       autofocus
+                       placeholder="<?= _('Veranstaltung suchen ...') ?>"
+                       value="<?= htmlReady(Request::get('search') != 1 ? Request::get('search') : '') ?>">
+                <?= Icon::create('search')->asInput([
+                    'title' => _('Suchen Sie nach beliebigen Veranstaltungen'),
+                    'data-dialog' => 1
+                ]) ?>
+                <a href="<?= $controller->connect() ?>" data-dialog title="<?= _('Suche schließen') ?>">
+                    <?= Icon::create('decline') ?>
+                </a>
+            <? else : ?>
+                <form action="<?= $controller->connect(['search' => 1]) ?>"
+                      method="get"
+                      class="default"
+                      data-dialog>
+                    <?= Icon::create('search')->asInput([
+                        'title' => _('Suchen Sie nach beliebiger Studiengruppe'),
+                        'data-dialog' => 1
+                    ]) ?>
+                </form>
+            <? endif ?>
+            </span>
+        </caption>
+        <thead>
+            <tr>
+                <th><?= _('Name') ?></th>
+                <th><?= _('Semester') ?></th>
+                <th class="actions"><?= _('Aktion') ?></th>
+            </tr>
+        </thead>
+        <tbody>
+        <? if (!Request::get('search') || Request::get('search') == 1) : ?>
+            <? if (count($my_studygroups) + count($suggestions) > 0) : ?>
+                <? foreach ($my_studygroups as $my_course) : ?>
+                    <?= $this->render_partial('course/connectedstudygroups/_studygroup_to_connect', ['course' => $my_course]) ?>
+                <? endforeach ?>
+                <? foreach ($suggestions as $suggested_course) : ?>
+                    <?= $this->render_partial('course/connectedstudygroups/_studygroup_to_connect', ['course' => $suggested_course]) ?>
+                <? endforeach ?>
+            <? else : ?>
+            <tr>
+                <td colspan="3">
+                    <?= _('Suchen Sie nach Studiengruppen.') ?>
+                </td>
+            </tr>
+            <? endif ?>
+        <? else : ?>
+            <? if (isset($searchresults) && count($searchresults)) : ?>
+                <? foreach ($searchresults as $course) : ?>
+                    <?= $this->render_partial('course/connectedstudygroups/_studygroup_to_connect', ['course' => $course]) ?>
+                <? endforeach ?>
+            <? else : ?>
+            <tr>
+                <td colspan="3">
+                    <?= _('Keine passenden Ergebnisse gefunden.') ?>
+                </td>
+            </tr>
+            <? endif ?>
+        <? endif ?>
+        </tbody>
+    </table>
+</form>
diff --git a/app/views/course/connectedstudygroups/index.php b/app/views/course/connectedstudygroups/index.php
new file mode 100644
index 00000000000..4611b6cbe28
--- /dev/null
+++ b/app/views/course/connectedstudygroups/index.php
@@ -0,0 +1,110 @@
+<? if (count($connected) + count($proposals) > 0) : ?>
+    <? if (count($connected) > 0) : ?>
+        <form method="post">
+            <table class="default">
+                <caption>
+                    <?= _('Verknüpfte Studiengruppen') ?>
+                    <thead>
+                        <tr>
+                            <th><?= _('Name') ?></th>
+                            <th class="actions"><?= _('Aktion') ?></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <? foreach ($connected as $connection) : ?>
+                        <tr>
+                            <td>
+                                <a href="<?= URLHelper::getLink('dispatch.php/course/studygroup/details/' . $connection['studygroup_id'], [], true) ?>">
+                                    <?= StudygroupAvatar::getAvatar($connection['studygroup_id'])->getImageTag(Avatar::SMALL) ?>
+                                    <?= htmlReady($connection->studygroup->getFullName()) ?>
+                                </a>
+                            </td>
+                            <td class="actions">
+                                <?= CSRFProtection::tokenTag() ?>
+                                <?= Icon::create('trash')->asInput([
+                                    'title'        => _('Verknüpfung aufheben'),
+                                    'data-confirm' => _('Wirklich die Zuweisung zu der Studiengruppe aufheben?'),
+                                    'formaction'   => $controller->removeURL($connection['studygroup_id'])
+                                ]) ?>
+                            </td>
+                        </tr>
+                        <? endforeach ?>
+                    </tbody>
+                </caption>
+            </table>
+        </form>
+    <? endif ?>
+
+    <? if (count($proposals) > 0) : ?>
+        <form method="post">
+            <table class="default">
+                <?= CSRFProtection::tokenTag() ?>
+                <caption>
+                    <?= _('Eingereichte Vorschläge') ?>
+                </caption>
+                <thead>
+                    <tr>
+                        <th><?= _('Name') ?></th>
+                        <th><?= _('Vorgeschlagen von') ?></th>
+                        <th class="actions"><?= _('Aktion') ?></th>
+                    </tr>
+                </thead>
+                <tbody>
+                <? foreach ($proposals as $proposal) : ?>
+                    <tr>
+                        <td>
+                            <a href="<?= URLHelper::getLink('dispatch.php/course/studygroup/details/' . $proposal['studygroup_id']) ?>" target="_blank">
+                                <?= StudygroupAvatar::getAvatar($proposal['studygroup_id'])->getImageTag(Avatar::SMALL) ?>
+                                <?= htmlReady($proposal->studygroup->getFullName()) ?>
+                            </a>
+                        </td>
+                        <td>
+                            <?= htmlReady($proposal->user->getFullName()) ?>
+                        </td>
+                        <td class="actions">
+                            <? if ($proposal['proposed_from'] === 'studygroup') : ?>
+                                <?= Icon::create('accept')->asInput([
+                                    'title'        => _('Vorschlag annehmen'),
+                                    'data-confirm' => _('Wirklich die Studiengruppe mit dieser Veranstaltung verknüpfen?'),
+                                    'formaction'   => $controller->connectURL($proposal['studygroup_id'])
+                                ]) ?>
+                            <? endif ?>
+                            <? if ($proposal['proposed_from'] === 'studycourse') : ?>
+                                <?= Icon::create('decline')->asInput([
+                                    'title'        => _('Vorschlag ablehnen'),
+                                    'data-confirm' => _('Wirklich den Vorschlag ablehnen?'),
+                                    'formaction'   => $controller->declineURL($proposal->id)
+                                ]) ?>
+                            <? else : ?>
+                                <?= Icon::create('decline')->asInput([
+                                    'title'        => _('Vorschlag zurückziehen'),
+                                    'data-confirm' => _('Wirklich den Vorschlag zurückziehen?'),
+                                    'formaction'   => $controller->declineURL($proposal->id)
+                                ]) ?>
+                            <? endif ?>
+                        </td>
+                    </tr>
+                <? endforeach ?>
+                </tbody>
+            </table>
+        </form>
+    <? endif ?>
+<? else : ?>
+
+    <div class="studip-contents-overview-teaser">
+        <div class="teaser-content">
+
+            <div>
+                <header><?= _('Verknüpfung zu Studiengruppen') ?></header>
+                <?= _('Verknüpfen Sie Studiengruppen, die sich mit den Inhalten dieser Veranstaltung beschäftigen.') ?>
+            </div>
+
+    <?= \Studip\LinkButton::create(
+        _('Verknüpfung zu Studiengruppe vorschlagen'),
+        $controller->connect(),
+        ['data-dialog' => 1]
+    )?>
+        </div>
+    </div>
+
+<? endif ?>
diff --git a/app/views/course/overview/index.php b/app/views/course/overview/index.php
index e538a6f3605..03fc848001e 100644
--- a/app/views/course/overview/index.php
+++ b/app/views/course/overview/index.php
@@ -68,6 +68,10 @@ if (!empty($questionnaires)) {
     echo $questionnaires;
 }
 
+if (!empty($connectedstudygroups)) {
+    echo $connectedstudygroups;
+}
+
 // display plugins
 
 if (!empty($plugins)) {
diff --git a/app/views/course/studygroup/details.php b/app/views/course/studygroup/details.php
index 26a3ef5bcbc..7fbbc39dd78 100644
--- a/app/views/course/studygroup/details.php
+++ b/app/views/course/studygroup/details.php
@@ -12,6 +12,10 @@
         <dl style="margin: 0">
             <dt><?= _('Name der Studiengruppe') ?></dt>
             <dd><?= htmlReady($studygroup->name) ?></dd>
+            <? if ((string) $studygroup->Beschreibung): ?>
+                <dt><?= _('Beschreibung') ?></dt>
+                <dd><?= formatLinks($studygroup->Beschreibung) ?></dd>
+            <? endif; ?>
 
         <? if ((string) $studygroup->beschreibung): ?>
             <dt><?= _('Beschreibung') ?></dt>
@@ -33,6 +37,21 @@
     </section>
 </article>
 
+<? if (count($studygroup->tags) > 0) : ?>
+<article class="studip">
+    <header>
+        <h1><?= _('Schlagwörter') ?></h1>
+    </header>
+    <section>
+        <? foreach ($studygroup->tags as $tag) : ?>
+            <a href="<?= URLHelper::getLink('dispatch.php/studygroup/browse', ['q' => $tag['name']]) ?>">
+                <?= htmlReady('#'.$tag['name']) ?>
+            </a>
+        <? endforeach ?>
+    </section>
+</article>
+<? endif ?>
+
 <div class="hidden-medium-up">
 <? foreach ($sidebarActions as $action) : ?>
     <?= Studip\LinkButton::create($action->label, $action->url) ?>
diff --git a/app/views/course/studygroup/edit.php b/app/views/course/studygroup/edit.php
index e3189f746e5..3891e9446ce 100644
--- a/app/views/course/studygroup/edit.php
+++ b/app/views/course/studygroup/edit.php
@@ -4,7 +4,7 @@
     <?= CSRFProtection::tokenTag() ?>
     <fieldset>
         <legend>
-            <?= _('Studiengruppe bearbeiten') ?>
+            <?= _('Grunddaten') ?>
         </legend>
 
         <input type='submit' class="invisible" name="<?=_('Änderungen übernehmen') ?>" aria-hidden="true">
@@ -35,12 +35,37 @@
                 <option value="invisible" <? if (!$course->visible) echo 'selected'; ?> <? if (!Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED) echo 'disabled'; ?>>
                     <?= _('Unsichtbar') ?>
                 </option>
+            <? endif; ?>
+            <? if (true) : ?>
+                <? $courseset = CourseSet::getSetForCourse($sem_id) ?>
+                <option value="top-course"<?= $courseset && $courseset->getId() === CourseSet::getConnectedcourseAdmissionSetId() ? ' selected' : '' ?>>
+                    <?= _('Für Mitglieder der zugehörigen Lehrveranstaltung') ?>
+                </option>
+            <? endif ?>
+            <? if (true) : ?>
+                <? $courseset = CourseSet::getSetForCourse($sem_id) ?>
+                <option value="top-course"<?= $courseset && $courseset->getId() === CourseSet::getConnectedcourseAdmissionSetId() ? ' selected' : '' ?>>
+                    <?= _('Für Mitglieder der zugehörigen Lehrveranstaltung') ?>
+                </option>
             <? endif ?>
             </select>
         </label>
 
     </fieldset>
 
+    <fieldset>
+        <legend><?= _('Erweiterte Einstellungen') ?></legend>
+
+        <label>
+            <?= _('Ablaufdatum') ?>
+            <input type="text" name="expiration_date">
+        </label>
+
+        <label>
+            <?= _('Schlagwörter') ?>
+        </label>
+    </fieldset>
+
     <footer>
         <?= Studip\Button::createAccept(_('Übernehmen'), ['title' => _("Änderungen übernehmen")]); ?>
         <?= Studip\LinkButton::createCancel(_('Abbrechen'), URLHelper::getURL('dispatch.php/course/go')); ?>
diff --git a/app/views/course/studygroup/widget.php b/app/views/course/studygroup/widget.php
new file mode 100644
index 00000000000..fc0f133ea96
--- /dev/null
+++ b/app/views/course/studygroup/widget.php
@@ -0,0 +1,88 @@
+<article class="studip connectedcourses_widget">
+    <header>
+        <h1>
+
+            <? if ($course->isStudygroup()) : ?>
+                <?= Icon::create('seminar', Icon::ROLE_INFO)->asimg(['class' => "text-bottom"]) ?>
+                <?= _('Zugehörige Veranstaltung') ?>
+            <? else : ?>
+                <?= Icon::create('studygroup', Icon::ROLE_INFO)->asimg(['class' => "text-bottom"]) ?>
+                <?= _('Verknüpfte Studiengruppen') ?>
+            <? endif ?>
+        </h1>
+
+    </header>
+
+    <section>
+        <? if ($course->isStudygroup()) : ?>
+            <ul>
+            <? foreach ($connections as $connection) : ?>
+                <li>
+                    <? $link = $connection->course->isAccessibleToUser()
+                        ? URLHelper::getLink('seminar_main.php', ['auswahl' => $connection->course->id])
+                        : URLHelper::getLink('dispatch.php/course/details', ['cid' => $connection->course->id]) ?>
+                    <a href="<?= $link ?>">
+                        <?= htmlReady($connection->course->getFullname()) ?>
+                    </a>
+                </li>
+            <? endforeach ?>
+            </ul>
+        <? else : ?>
+            <table class="default">
+                <colgroup>
+                    <col style="width: 60px;">
+                </colgroup>
+                <thead>
+                    <tr>
+                        <th><?= _('Avatar') ?></th>
+                        <th><?= _('Name / Beschreibung') ?></th>
+                        <th><?= _('Mitglieder') ?></th>
+                        <th><?= _('Gründer:in') ?></th>
+                    </tr>
+                </thead>
+                <tbody>
+                <? foreach ($connections as $connection) : ?>
+                    <tr>
+                        <td>
+                            <? $link = $connection->studygroup->isAccessibleToUser()
+                                ? URLHelper::getLink('seminar_main.php', ['auswahl' => $connection->studygroup->id])
+                                : URLHelper::getLink('dispatch.php/course/studygroup/details/'.$connection->studygroup->id) ?>
+                            <a href="<?= $link ?>">
+                                <?= CourseAvatar::getAvatar($connection->studygroup->id)->getImageTag(Avatar::SMALL) ?>
+                            </a>
+                        </td>
+                        <td>
+                            <a href="<?= $link ?>">
+                                <?= htmlReady($connection->studygroup->getFullname()) ?>
+                            </a>
+                            <? if ($connection->studygroup->beschreibung) : ?>
+                            <div>
+                                <?= htmlReady($connection->studygroup->beschreibung) ?>
+                            </div>
+                            <? endif ?>
+                        </td>
+                        <td>
+                            <?= count($connection->studygroup->members) ?>
+                        </td>
+                        <td>
+                            <?
+                            $founders = $connection->studygroup->members->filter(function ($m) { return $m['status'] === 'dozent'; });
+                            foreach ($founders as $index => $founder) : ?>
+                                <? if ($index > 0) : ?>
+                                ,
+                                <? endif ?>
+                                <a href="<?= URLHelper::getLink('dispatch.php/profile', ['username' => $founder->user->username]) ?>">
+                                    <?= Avatar::getAvatar($founder->user->id)->getImageTag(Avatar::SMALL) ?>
+                                    <?= htmlReady($founder->user->getFullname()) ?>
+                                </a>
+                            <? endforeach ?>
+                        </td>
+                    </tr>
+                <? endforeach ?>
+                </tbody>
+            </table>
+        <? endif ?>
+
+    </section>
+
+</article>
diff --git a/app/views/course/wizard/step.php b/app/views/course/wizard/step.php
index 0fe1dfcdb09..214515a70ce 100644
--- a/app/views/course/wizard/step.php
+++ b/app/views/course/wizard/step.php
@@ -10,10 +10,14 @@
 ?>
 <? if ($content) : ?>
     <form class="default course-wizard-step-<?= $stepnumber ?>" action="<?= $controller->link_for('course/wizard/process', $stepnumber, $temp_id) ?>" method="post" data-secure>
-        <fieldset>
-        <?= $content ?>
-        </fieldset>
 
+        <? if (!$studygroup) : ?>
+            <fieldset>
+        <? endif; ?>
+        <?= $content ?>
+        <? if (!$studygroup) : ?>
+            </fieldset>
+        <? endif; ?>
         <footer data-dialog-button>
             <input type="hidden" name="step" value="<?= $stepnumber ?>">
         <? if (empty($first_step)): ?>
@@ -23,11 +27,15 @@
                 !empty($dialog) ? ['data-dialog' => 'size=50%'] : []
             ) ?>
         <? endif; ?>
+            <? if (!$studygroup) : ?>
             <?= Studip\Button::create(
                 _('Weiter'),
                 'next',
                 !empty($dialog) ? ['data-dialog' => 'size=50%'] : []
             ) ?>
+            <? else : ?>
+                <?= Studip\Button::createAccept(_('Studiengruppe anlegen'), 'create') ?>
+            <? endif; ?>
         </footer>
     </form>
 <? else : ?>
diff --git a/app/views/course/wizard/steps/basicdata/index_studygroup.php b/app/views/course/wizard/steps/basicdata/index_studygroup.php
index 9f75da56436..10368b97369 100644
--- a/app/views/course/wizard/steps/basicdata/index_studygroup.php
+++ b/app/views/course/wizard/steps/basicdata/index_studygroup.php
@@ -34,6 +34,12 @@
               rows="4"><?= htmlReady($values['description'] ?? '') ?></textarea>
 </label>
 
+<label class="col-3">
+    <?= _('Bezieht sich auf Lehrveranstaltung (optional)') ?>
+    <?= QuickSearch::get('lv_course_id', new StandardSearch('Seminar_id'))
+        ->defaultValue($values['lv_course_id'], $values['lv_course_id'] ? Course::find($values['lv_course_id'])->getFullname() : '')
+        ->render() ?>
+</label>
 
 <label class="col-3">
     <span class="required"><?= _('Zugang') ?></span>
@@ -74,6 +80,7 @@
 <input type="hidden" name="institute" value="<?= $values['institute'] ?>"/>
 <input type="hidden" name="start_semester" value="<?= htmlReady($values['start_semester']) ?>">
 <input type="hidden" name="studygroup" value="1"/>
+<input type="hidden" name="stgteil_id" value="<?= htmlReady($values['stgteil_id']) ?>"/>
 <?php foreach ($values['lecturers'] as $id => $assigned) : ?>
     <input type="hidden" name="lecturers[<?= $id ?>]" value="1"/>
 <?php endforeach ?>
diff --git a/app/views/course/wizard/steps/studygroups/index.php b/app/views/course/wizard/steps/studygroups/index.php
new file mode 100644
index 00000000000..b7327a9dc48
--- /dev/null
+++ b/app/views/course/wizard/steps/studygroups/index.php
@@ -0,0 +1,110 @@
+<fieldset>
+<legend>
+    <?= _('Grunddaten') ?>
+</legend>
+
+<label class="">
+    <span class="required"><?= _('Name') ?></span>
+    <input type="text" name="name" id="wizard-name" maxlength="254" value="<?= htmlReady($values['name'] ?? '') ?>" required>
+</label>
+
+<? if(count($types) > 1) : ?>
+    <label class="">
+        <span class="required"><?= _('Typ') ?></span>
+        <select name="coursetype" id="wizard-coursetype">
+            <?php foreach ($types as $class => $subtypes) : ?>
+                <optgroup label="<?= htmlReady($class) ?>">
+                    <?php foreach ($subtypes as $type) : ?>
+                        <option value="<?= $type['id'] ?>"<?= $type['id'] == $values['coursetype'] ? ' selected="selected"' : '' ?>>
+                            <?= htmlReady($type['name']) ?>
+                        </option>
+                    <?php endforeach ?>
+                </optgroup>
+            <?php endforeach ?>
+        </select>
+    </label>
+<? else : ?>
+    <? $type = array_values($types)[0]; ?>
+    <input type="hidden" name="coursetype" value="<?= htmlReady($type[0]['id']) ?>">
+<? endif ?>
+
+
+<label class="">
+    <?= _('Beschreibung') ?>
+    <textarea name="description" id="wizard-description"
+              rows="4"><?= htmlReady($values['description'] ?? '') ?></textarea>
+</label>
+
+
+<label class="">
+    <span class="required"><?= _('Zugang') ?></span>
+
+    <select name="access" id="wizard-access">
+        <option value="all"
+            <? if (isset($values['access']) && $values['access'] === 'all') echo 'selected'; ?>>
+            <?= _('offen für alle') ?>
+        </option>
+        <option value="invite"
+            <? if (isset($values['access']) && $values['access'] === 'invite') echo 'selected'; ?>>
+            <?= _('auf Anfrage') ?>
+        </option>
+        <?php if (Config::get()->STUDYGROUPS_INVISIBLE_ALLOWED) : ?>
+            <option value="invisible"
+                <? if (isset($values['access']) && $values['access'] === 'invisible') echo 'selected'; ?>>
+                <?= _('unsichtbar') ?>
+            </option>
+        <?php endif ?>
+    </select>
+</label>
+
+
+<label><span class="required"><?= _('Nutzungsbedingungen')?></span></label>
+
+<? if ($GLOBALS['perm']->have_perm('admin')) : ?>
+    <p style="font-weight: bold;">
+        <?= _('Ich habe die eingetragenen Personen darüber informiert, dass in Ihrem Namen eine Studiengruppe angelegt wird und versichere, dass Sie mit folgenden Nutzungsbedingungen einverstandenen sind:') ?>
+    </p>
+<? endif ?>
+<?= formatReady(Config::Get()->STUDYGROUP_TERMS) ?>
+
+<label>
+    <input type="checkbox" name="accept" id="wizard-accept" required>
+    <?= _('Einverstanden') ?>
+</label>
+</fieldset>
+
+<fieldset>
+    <legend>
+        <?= _('Erweiterte Einstellungen') ?>
+    </legend>
+
+    <label>
+        <?= _('Ablaufdatum / Löschdatum') ?>
+        <input type="text" aria-label="<?= _('Ablaufdatum / Löschdatum') ?>" title="<?= _('Ablaufdatum / Löschdatum') ?>"
+               data-date-picker
+               name="exp_date"
+               value="<?= date('d.m.Y H:i', time() + 86400 * 365 * 2) ?>"
+               class="hasDatePicker">
+    </label>
+
+    <label>
+        <?= _('Schlagwörter') ?>
+        <?= Studip\VueApp::create('Multiquicksearch')
+            ->withProps([
+                'name'         => 'tags[]',
+                'searchtype'   => (string) SQLSearch::get('SELECT `name`, `name` FROM `tags` WHERE `active` = 1 AND `name` LIKE :input', _('Schlagwort suchen')),
+                'autocomplete' => true,
+                'addlabel' => _('Schlagwort hinzufügen')
+            ])
+        ?>
+    </label>
+
+</fieldset>
+
+
+<input type="hidden" name="institute" value="<?= $values['institute'] ?>">
+<input type="hidden" name="studygroup" value="1">
+<input type="hidden" name="stgteil_id" value="<?= htmlReady($values['stgteil_id']) ?>">
+<?php foreach ($values['lecturers'] as $id => $assigned) : ?>
+    <input type="hidden" name="lecturers[<?= $id ?>]" value="1">
+<?php endforeach ?>
diff --git a/app/views/my_studygroups/_course.php b/app/views/my_studygroups/_course.php
index 37c081e14b2..e554ddb2a73 100644
--- a/app/views/my_studygroups/_course.php
+++ b/app/views/my_studygroups/_course.php
@@ -4,6 +4,7 @@
         <td>
             <?= StudygroupAvatar::getAvatar($group['seminar_id'])->getImageTag(Avatar::SMALL, ['title' => $group['name']]) ?>
         </td>
+
         <td style="text-align: left">
             <a href="<?= URLHelper::getLink('dispatch.php/course/go', ['to' => $group['seminar_id']]) ?>"
                 <?= $group['last_visitdate'] < $group['chdate'] ? 'style="color: red;"' : '' ?>>
@@ -21,6 +22,9 @@
                 <?= tooltipicon($infotext) ?>
             <? endif ?>
         </td>
+        <td data-sort-value="<?= $group['mkdate'] ?>">
+            <?= htmlReady(date('d.m.Y', $group['mkdate'])) ?>
+        </td>
         <td style="text-align: left; white-space: nowrap;">
             <? if (!empty($group['navigation'])) : ?>
                 <ul class="my-courses-navigation" style="flex-wrap: nowrap">
@@ -43,28 +47,30 @@
                 </ul>
             <? endif ?>
         </td>
-        <td style="text-align: right">
-            <? if (in_array($group["user_status"], ["dozent", "tutor"])) : ?>
-                <? $adminmodule = $group["sem_class"]->getAdminModuleObject(); ?>
-                <? if ($adminmodule) : ?>
-                    <? $adminnavigation = $adminmodule->getIconNavigation($group['seminar_id'], 0, $GLOBALS['user']->id); ?>
-                <? endif ?>
-                <? if ($adminnavigation) : ?>
-                    <a href="<?= URLHelper::getLink($adminnavigation->getURL(), ['cid' => $group['seminar_id']]) ?>">
-                        <?= $adminnavigation->getImage()->asImg($adminnavigation->getLinkAttributes())?>
+        <? if (!$is_widget) : ?>
+            <td style="text-align: right">
+                <? if (in_array($group["user_status"], ["dozent", "tutor"])) : ?>
+                    <? $adminmodule = $group["sem_class"]->getAdminModuleObject(); ?>
+                    <? if ($adminmodule) : ?>
+                        <? $adminnavigation = $adminmodule->getIconNavigation($group['seminar_id'], 0, $GLOBALS['user']->id); ?>
+                    <? endif ?>
+                    <? if ($adminnavigation) : ?>
+                        <a href="<?= URLHelper::getLink($adminnavigation->getURL(), ['cid' => $group['seminar_id']]) ?>">
+                            <?= $adminnavigation->getImage()->asImg($adminnavigation->getLinkAttributes())?>
+                        </a>
+                    <? endif ?>
+
+                <? elseif (!empty($group['binding'])) : ?>
+                    <a href="<?= URLHelper::getLink('', ['to' => $group['seminar_id'], 'cmd' => 'no_kill']) ?>">
+                        <?= Icon::create('door-leave', Icon::ROLE_INACTIVE)->asImg(['title' => _('Die Teilnahme ist bindend. Bitte wenden Sie sich an die Lehrenden.')]) ?>
+                    </a>
+                <?
+                else : ?>
+                    <a href="<?= URLHelper::getLink("dispatch.php/my_courses/decline/{$group['seminar_id']}", ['cmd' => 'suppose_to_kill']) ?>">
+                        <?= Icon::create('door-leave', Icon::ROLE_INACTIVE)->asImg(['title' => _('aus der Studiengruppe abmelden')]) ?>
                     </a>
                 <? endif ?>
-
-            <? elseif (!empty($group['binding'])) : ?>
-                <a href="<?= URLHelper::getLink('', ['to' => $group['seminar_id'], 'cmd' => 'no_kill']) ?>">
-                    <?= Icon::create('door-leave', Icon::ROLE_INACTIVE)->asImg(['title' => _('Die Teilnahme ist bindend. Bitte wenden Sie sich an die Lehrenden.')]) ?>
-                </a>
-            <?
-            else : ?>
-                <a href="<?= URLHelper::getLink("dispatch.php/my_courses/decline/{$group['seminar_id']}", ['cmd' => 'suppose_to_kill']) ?>">
-                    <?= Icon::create('door-leave', Icon::ROLE_INACTIVE)->asImg(['title' => _('aus der Studiengruppe abmelden')]) ?>
-                </a>
+            </td>
             <? endif ?>
-        </td>
     </tr>
 <? endforeach ?>
diff --git a/app/views/my_studygroups/index.php b/app/views/my_studygroups/index.php
index 6fd2d20f685..cfdd2e28102 100644
--- a/app/views/my_studygroups/index.php
+++ b/app/views/my_studygroups/index.php
@@ -1,5 +1,5 @@
 <? if (!empty($studygroups)) : ?>
-    <table class="default" id="my_seminars">
+    <table class="default sortable-table" id="my_seminars">
         <caption>
             <?= _('Meine Studiengruppen') ?>
         </caption>
@@ -7,20 +7,27 @@
             <col width="10px">
             <col width="25px">
             <col>
+            <col>
             <col width="<?= $nav_elements * 27 ?>px">
-            <col width="45px">
+            <? if (!$is_widget) : ?>
+                <col width="45px">
+            <? endif ?>
         </colgroup>
         <thead>
-            <tr>
+            <tr class="sortable" title="<?= _('Klicken, um die Sortierung zu ändern') ?>">
+
                 <th colspan="2" nowrap align="center">
                     <a href="<?= URLHelper::getLink('dispatch.php/my_courses/groups/all/true') ?>"
                        data-dialog="size=normal">
                         <?= Icon::create('group')->asImg(['title' => _('Gruppe ändern'), 'class' => 'middle']) ?>
                     </a>
                 </th>
-                <th><?= _('Name') ?></th>
+                <th data-sort="text"><?= _('Name') ?></th>
+                <th data-sort="digit"><?= _('gegründet') ?></th>
                 <th><?= _('Inhalt') ?></th>
-                <th></th>
+                <? if (!$is_widget) : ?>
+                    <th><?= _('Aktionen') ?></th>
+                <? endif ?>
             </tr>
         </thead>
         <?= $this->render_partial('my_studygroups/_course', compact('studygroups')) ?>
diff --git a/app/views/my_studygroups/proposals.php b/app/views/my_studygroups/proposals.php
new file mode 100644
index 00000000000..7347849e788
--- /dev/null
+++ b/app/views/my_studygroups/proposals.php
@@ -0,0 +1,31 @@
+<section class="studip-tiles">
+    <? foreach ($proposed_studygroups as $course) : ?>
+        <a href="<?= URLHelper::getLink('dispatch.php/course/studygroup/details/'.$course->id) ?>">
+            <div>
+                <?= StudygroupAvatar::getAvatar($course->id)->getImageTag(Avatar::MEDIUM) ?>
+                <div>
+                    <strong>
+                        <?= htmlReady($course->getFullname()) ?>
+                    </strong>
+                    <div>
+                        <?= sprintf(
+                                ngettext(
+                                    '1 Mitglied',
+                                    '%s Mitglieder',
+                                    count($course->members)
+                                ),
+                                count($course->members)
+                            ) ?>
+                    </div>
+                </div>
+            </div>
+            <? if (count($course->tags)) : ?>
+                <div>
+                    <? foreach ($course->tags as $tag) : ?>
+                        <?= '#'.htmlReady($tag->name) ?>
+                    <? endforeach ?>
+                </div>
+            <? endif ?>
+        </a>
+    <? endforeach ?>
+</section>
diff --git a/app/views/search/studiengaenge/verlauf.php b/app/views/search/studiengaenge/verlauf.php
index 4464e66c6e7..540f46eac5b 100644
--- a/app/views/search/studiengaenge/verlauf.php
+++ b/app/views/search/studiengaenge/verlauf.php
@@ -143,4 +143,59 @@
             <? endforeach ?>
         </tbody>
     </table>
+
+    <h2><?= _('Studentische Arbeitsgruppen') ?></h2>
+
+    <section class="studip-tiles">
+        <? foreach ($studiengangTeil->studygroups as $course) : ?>
+            <div>
+                <div class="with-action-menu">
+                    <div>
+                        <a href="<?= URLHelper::getLink('dispatch.php/course/studygroup/details/'.$course->id) ?>">
+                            <?= CourseAvatar::getAvatar($course->id)->getImageTag(Avatar::MEDIUM) ?>
+                        </a>
+                        <a href="<?= URLHelper::getLink('dispatch.php/course/studygroup/details/'.$course->id) ?>">
+                            <strong>
+                                <?= htmlReady($course->name) ?>
+                            </strong>
+                            <div>
+                                <?= sprintf(
+                                    ngettext(
+                                        '1 Mitglied',
+                                        '%s Mitglieder',
+                                        count($course->members)
+                                    ),
+                                    $course->members
+                                ) ?>
+                            </div>
+                        </a>
+                    </div>
+                    <? if ($GLOBALS['perm']->have_perm('admin')) : ?>
+                        <form method="post">
+                            <?= CSRFProtection::tokenTag() ?>
+                            <button class="undecorated"
+                               data-confirm="<?= sprintf(_('Wirklich diese Studiengruppe aus dem Studiengang %s entfernen?'), $studiengangTeilName) ?>"
+                               formaction="<?= $controller->remove_studygroup($course->id, $studiengangTeil->id) ?>">
+                                <?= Icon::create('trash') ?>
+                            </button>
+                        </form>
+                    <? endif ?>
+                </div>
+                <? if (count($course->tags)) : ?>
+                <div>
+                    <? foreach ($course->tags as $tag) : ?>
+                        <?= '#'.htmlReady($tag->name) ?>
+                    <? endforeach ?>
+                </div>
+                <? endif ?>
+            </div>
+        <? endforeach ?>
+
+        <a href="<?= URLHelper::getLink('dispatch.php/course/wizard', ['studygroup' => 1, 'stgteil_id' => $studiengangTeil->id] )?>">
+            <div>
+                <?= Icon::create('add')->asImg(50) ?>
+                <strong><?= _('Neue Studiengruppe erstellen') ?></strong>
+            </div>
+        </a>
+    </section>
 <? endif ?>
diff --git a/app/views/studygroup/browse.php b/app/views/studygroup/browse.php
index 6362d6af7ec..cf8fe3e0c04 100644
--- a/app/views/studygroup/browse.php
+++ b/app/views/studygroup/browse.php
@@ -18,16 +18,16 @@
 
 <?php
 $headers = [
-    'name'     => _('Name'),
-    'founded'  => _('gegründet'),
-    'member'   => _('Mitglieder'),
-    'founder'  => _('GründerIn'),
-    'ismember' => _('Mitglied'),
+    'name'              => _('Name'),
+    'tags'              => _('Schlagwörter'),
+    'last_activity'     => _('Letzte Aktivität'),
+    'member'            => _('Mitglieder'),
+    'founder'           => _('Gründer:in')
 ];
 ?>
 
 <? if ($anzahl > 0): ?>
-    <table class="default studygroup-browse">
+    <table class="default studygroup-browse sortable-table" data-sortlist="[[3, 1]]">
         <caption>
             <?= sprintf(ngettext('%u Studiengruppe', '%u Studiengruppen', $anzahl), $anzahl)?>
         </caption>
@@ -36,19 +36,30 @@ $headers = [
             <col>
             <col style="width: 10%">
             <col style="width: 10%">
-            <col style="width: 20%">
             <col style="width: 10%">
+            <col style="width: 20%">
         </colgroup>
         <thead>
             <tr class="sortable" title="<?= _('Klicken, um die Sortierung zu ändern') ?>">
                 <th class="nosort hidden-small-down"></th>
-            <? foreach ($headers as $key => $label): ?>
-                <th <? if ($sort_type === $key) echo 'class="sort' . $sort_order . '"'; ?>>
-                    <a href="<?= $controller->link_for("studygroup/browse/1/{$key}_" . ($sort_order === 'asc' ? 'desc' : 'asc'), compact('q', 'closed')) ?>">
-                        <?= htmlReady($label) ?>
-                    </a>
-                </th>
-            <? endforeach; ?>
+                <? foreach ($headers as $key => $label): ?>
+                    <? if ($key !== 'last_activity' && $key !== 'tags') : ?>
+                        <th <? if ($sort_type === $key) echo 'class="sort' . $sort_order . '"'; ?>>
+                            <a href="<?= $controller->link_for("studygroup/browse/1/{$key}_" . ($sort_order === 'asc' ? 'desc' : 'asc'), compact('q', 'closed')) ?>">
+                                <?= htmlReady($label) ?>
+                            </a>
+                        </th>
+                    <? elseif($key !== 'tags') : ?>
+                        <th data-sort="htmldata">
+                            <?= htmlReady($label) ?>
+                        </th>
+                    <? else : ?>
+                        <th>
+                            <?= htmlReady($label) ?>
+                        </th>
+                    <? endif; ?>
+                <? endforeach; ?>
+                <th></th>
             </tr>
         </thead>
         <tbody>
@@ -71,7 +82,15 @@ $headers = [
                             <? } ?>
                         </a>
                 </td>
-                <td><?= strftime('%x', $group['mkdate']) ?>
+                <td>
+                    <? foreach ($group['course']->tags as $tag) : ?>
+                        <a href="<?= $controller->browse(['q' => $tag['name']]) ?>">
+                            <?= htmlReady('#'.$tag['name']) ?>
+                        </a>
+                    <? endforeach ?>
+                </td>
+                <td data-sort-value="<?= htmlReady($group['last_visit_date']) ?>">
+                    <?= htmlReady(date('d.m.Y', $group['last_visit_date'])) ?>
                 </td>
                 <td align="center">
                     <?= StudygroupModel::countMembers($group['Seminar_id']) ?>
@@ -88,11 +107,6 @@ $headers = [
                         <br>
                     <? endforeach; ?>
                 </td>
-                <td align="center">
-                    <? if ($is_member) : ?>
-                        <?= Icon::create('person', Icon::ROLE_INACTIVE, ['title' => _('Sie sind Mitglied in dieser Gruppe')])->asImg() ?>
-                    <? endif; ?>
-                </td>
             </tr>
         <? endforeach; ?>
         </tbody>
diff --git a/db/migrations/6.0.38_improved_studygroups.php b/db/migrations/6.0.38_improved_studygroups.php
new file mode 100644
index 00000000000..2ed71248a32
--- /dev/null
+++ b/db/migrations/6.0.38_improved_studygroups.php
@@ -0,0 +1,305 @@
+<?php
+
+class ImprovedStudygroups extends Migration
+{
+
+    public function description()
+    {
+        return 'Improve studygroups.';
+    }
+
+    public function up()
+    {
+        DBManager::get()->exec("
+            CREATE TABLE `studygroup_courses` (
+                `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+                `studygroup_id` char(32) NOT NULL,
+                `course_id` char(32) DEFAULT NULL,
+                `mkdate` int(11) DEFAULT NULL,
+                PRIMARY KEY (`id`),
+                UNIQUE KEY `studygroup_id` (`studygroup_id`,`course_id`),
+                KEY `studygroup_id_2` (`studygroup_id`),
+                KEY `course_id` (`course_id`)
+            )
+        ");
+        DBManager::get()->exec("
+            CREATE TABLE `studygroup_courses_proposals` (
+                `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+                `studygroup_id` char(32) NOT NULL,
+                `course_id` char(32) NOT NULL,
+                `proposed_from` enum('course','studygroup') NOT NULL,
+                `user_id` char(32) DEFAULT NULL,
+                `mkdate` int(11) DEFAULT NULL,
+                PRIMARY KEY (`id`),
+                UNIQUE KEY `studygroup_id` (`studygroup_id`,`course_id`),
+                KEY `course_id` (`course_id`),
+                KEY `studygroup_id_2` (`studygroup_id`)
+            )
+        ");
+        DBManager::get()->exec("
+            CREATE TABLE `tags` (
+                `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+                `name` varchar(128) NOT NULL,
+                `active` tinyint(1) DEFAULT 1,
+                `chdate` int(11) DEFAULT NULL,
+                `mkdate` int(11) DEFAULT NULL,
+                PRIMARY KEY (`id`)
+            )
+        ");
+        DBManager::get()->exec("
+            CREATE TABLE `tags_relations` (
+                `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+                `tag_id` int(11) DEFAULT NULL,
+                `range_id` varchar(32) DEFAULT NULL,
+                `range_type` varchar(100) DEFAULT NULL,
+                `mkdate` int(11) DEFAULT NULL,
+                PRIMARY KEY (`id`),
+                KEY `tag_id` (`tag_id`),
+                KEY `range_id` (`range_id`),
+                KEY `range_type` (`range_type`)
+            )
+        ");
+        DBManager::get()->exec("
+            ALTER TABLE `seminare`
+            ADD COLUMN `expires` int(11) DEFAULT NULL
+        ");
+        DBManager::get()->exec("
+            CREATE TABLE `studygroup_stgteil` (
+                `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+                `studygroup_id` char(32) NOT NULL,
+                `stgteil_id` varchar(32) NULL NULL,
+                `mkdate` int(11) DEFAULT NULL,
+                PRIMARY KEY (`id`),
+                UNIQUE KEY `studygroup_id` (`studygroup_id`,`stgteil_id`),
+                KEY `studygroup_id_2` (`studygroup_id`),
+                KEY `stgteil_id` (`stgteil_id`)
+            )
+        ");
+        DBManager::get()->exec(
+            "INSERT IGNORE INTO `config`
+             (`field`, `type`, `range`, `value`, `section`, `description`, `mkdate`, `chdate`)
+             VALUES
+             (
+                 'STUDYGROUP_ON_STGTEIL_ENABLE', 'boolean', 'global', '1', 'studygroups', 'Are studygroups allowed to get attached to study course parts?',
+                 UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
+             )"
+        );
+        DBManager::get()->exec("
+            INSERT INTO `admissionrules` (`ruletype`, `active`, `mkdate`, `path`)
+            VALUES
+	            ('ConnectedcourseAdmission', 1, UNIX_TIMESTAMP(), 'lib/admissionrules/connectedcourseadmission');
+        ");
+
+
+        $statement = DBManager::get()->prepare("
+            SELECT *
+            FROM config
+            WHERE field = 'GLOBALSEARCH_MODULES'
+        ");
+        $statement->execute();
+        $config = $statement->fetch(PDO::FETCH_ASSOC);
+        $config['value'] = json_decode($config['value'], true);
+        $config['value']['GlobalSearchStudygroups'] = [
+            'order' => 15,
+            'active' => true,
+            'fulltext' => false
+        ];
+
+        //Adding to the global search:
+
+        $statement = DBManager::get()->prepare("
+            UPDATE config
+            SET `value` = :json
+            WHERE field = 'GLOBALSEARCH_MODULES'
+        ");
+        $statement->execute([
+            'json' => json_encode($config['value'])
+        ]);
+
+        $statement = DBManager::get()->prepare("
+            SELECT *
+            FROM config_values
+            WHERE field = 'GLOBALSEARCH_MODULES'
+        ");
+        $statement->execute();
+        $config = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($config) {
+            $config['value'] = json_decode($config['value'], true);
+            $config['value']['GlobalSearchStudygroups'] = [
+                'order' => 15,
+                'active' => true,
+                'fulltext' => true
+            ];
+
+            $statement = DBManager::get()->prepare("
+                UPDATE config_values
+                SET `value` = :json
+                WHERE field = 'GLOBALSEARCH_MODULES'
+            ");
+            $statement->execute([
+                'json' => json_encode($config['value'])
+            ]);
+        }
+
+        $db = DBManager::get();
+
+        // get position
+        $pos = $db->fetchColumn("SELECT MAX(navigationpos) + 1 FROM plugins WHERE plugintype = 'PortalPlugin'");
+
+        // install as portal plugin
+        $sql = "INSERT INTO plugins (pluginclassname, pluginname, plugintype, enabled, navigationpos) VALUES (?)";
+        $db->execute($sql, [['StudygroupWidget', 'StudygroupWidget', 'PortalPlugin', 'yes', $pos]]);
+
+        $sql = "INSERT INTO roles_plugins (roleid, pluginid)
+                SELECT roleid, ? FROM roles WHERE `system` = 'y' AND rolename != 'Nobody'";
+        $db->execute($sql, [$db->lastInsertId()]);
+
+
+        // Add default cron tasks and schedules
+        $new_job = [
+            'filename'    => 'lib/cronjobs/studygroup_expiration.class.php',
+            'class'       => 'StudygroupExpirationJob',
+            'priority'    => 'normal'
+        ];
+
+        $query = "INSERT IGNORE INTO `cronjobs_tasks`
+                    (`task_id`, `filename`, `class`, `active`)
+                  VALUES (:task_id, :filename, :class, 1)";
+        $task_statement = DBManager::get()->prepare($query);
+
+        $query = "INSERT IGNORE INTO `cronjobs_schedules`
+                    (`schedule_id`, `task_id`, `parameters`,
+                     `minute`, `hour`, `mkdate`, `chdate`,
+                     `last_result`)
+                  VALUES (:schedule_id, :task_id, '[]',
+                          :minute, :hour, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(),
+                          NULL)";
+        $schedule_statement = DBManager::get()->prepare($query);
+
+
+        $task_id = md5(uniqid('expirestudygroups', true));
+
+        $task_statement->execute([
+            ':task_id'  => $task_id,
+            ':filename' => $new_job['filename'],
+            ':class'    => $new_job['class'],
+        ]);
+
+        $schedule_id = md5(uniqid('schedule', true));
+        $schedule_statement->execute([
+            ':schedule_id' => $schedule_id,
+            ':task_id'     => $task_id,
+            ':hour'        => $new_job['hour'],
+            ':minute'      => $new_job['minute'],
+        ]);
+
+        // get position
+        $pos = $db->fetchColumn("SELECT MAX(navigationpos) + 1 FROM plugins WHERE plugintype = 'PortalPlugin'");
+
+        // install as portal plugin
+        $sql = "INSERT INTO plugins (pluginclassname, pluginname, plugintype, enabled, navigationpos) VALUES (?)";
+        $db->execute($sql, [['MyStudygroupsWidget', 'MyStudygroupsWidget', 'PortalPlugin', 'yes', $pos]]);
+
+        $sql = "INSERT INTO roles_plugins (roleid, pluginid)
+                SELECT roleid, ? FROM roles WHERE `system` = 'y' AND rolename != 'Nobody'";
+        $db->execute($sql, [$db->lastInsertId()]);
+    }
+
+    public function down()
+    {
+        $db = DBManager::get();
+
+        $plugin_id = $db->fetchColumn('SELECT pluginid FROM plugins WHERE pluginclassname = ?', ['MyStudygroupsWidget']);
+
+        $db->execute('DELETE FROM widget_default WHERE pluginid = ?', [$plugin_id]);
+        $db->execute('DELETE FROM widget_user WHERE pluginid = ?', [$plugin_id]);
+        $db->execute('DELETE FROM roles_plugins WHERE pluginid = ?', [$plugin_id]);
+        $db->execute('DELETE FROM plugins WHERE pluginid = ?', [$plugin_id]);
+
+        $db->exec("
+            DELETE `cronjobs_schedules`.* FROM `cronjobs_schedules`
+            INNER JOIN `cronjobs_tasks` USING (`task_id`)
+                   WHERE `cronjobs_tasks`.`class` = 'StudygroupExpirationJob'
+        ");
+        $db->exec("
+            DELETE FROM `cronjobs_tasks`
+            WHERE `cronjobs_tasks`.`class` = 'StudygroupExpirationJob'
+        ");
+
+        $plugin_id = $db->fetchColumn('SELECT pluginid FROM plugins WHERE pluginclassname = ?', ['StudygroupWidget']);
+
+        $db->execute('DELETE FROM widget_default WHERE pluginid = ?', [$plugin_id]);
+        $db->execute('DELETE FROM widget_user WHERE pluginid = ?', [$plugin_id]);
+        $db->execute('DELETE FROM roles_plugins WHERE pluginid = ?', [$plugin_id]);
+        $db->execute('DELETE FROM plugins WHERE pluginid = ?', [$plugin_id]);
+
+        $statement = DBManager::get()->prepare("
+            SELECT *
+            FROM config_values
+            WHERE field = 'GLOBALSEARCH_MODULES'
+        ");
+        $statement->execute();
+        $config = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($config) {
+            $config['value'] = json_decode($config['value'], true);
+            unset($config['value']['GlobalSearchStudygroups']);
+            $statement = DBManager::get()->prepare("
+                UPDATE config_values
+                SET `value` = :json
+                WHERE field = 'GLOBALSEARCH_MODULES'
+            ");
+            $statement->execute([
+                'json' => json_encode($config['value'])
+            ]);
+        }
+
+        $statement = DBManager::get()->prepare("
+            SELECT *
+            FROM config
+            WHERE field = 'GLOBALSEARCH_MODULES'
+        ");
+        $statement->execute();
+        $config = $statement->fetch(PDO::FETCH_ASSOC);
+        $config['value'] = json_decode($config['value'], true);
+        unset($config['value']['GlobalSearchStudygroups']);
+        $statement = DBManager::get()->prepare("
+            UPDATE config
+            SET `value` = :json
+            WHERE field = 'GLOBALSEARCH_MODULES'
+        ");
+        $statement->execute([
+            'json' => json_encode($config['value'])
+        ]);
+
+
+        DBManager::get()->exec("
+            DELETE FROM `admissionrules` WHERE `ruletype` = 'ConnectedcourseAdmission'
+        ");
+        DBManager::get()->exec("
+            DELETE `config`, `config_values`
+            FROM `config`
+                LEFT JOIN `config_values` USING (`field`)
+            WHERE `config`.`field` = 'STUDYGROUP_ON_STGTEIL_ENABLE'
+        ");
+        DBManager::get()->exec("
+            DROP TABLE `studygroup_stgteil`
+        ");
+        DBManager::get()->exec("
+            ALTER TABLE `seminare`
+            DROP COLUMN `expires`
+        ");
+        DBManager::get()->exec("
+            DROP TABLE `tags_relations`
+        ");
+        DBManager::get()->exec("
+            DROP TABLE `tags`
+        ");
+        DBManager::get()->exec("
+            DROP TABLE `studygroup_courses_proposals`
+        ");
+        DBManager::get()->exec("
+            DROP TABLE `studygroup_courses`
+        ");
+    }
+
+}
diff --git a/lib/admissionrules/connectedcourseadmission/ConnectedcourseAdmission.class.php b/lib/admissionrules/connectedcourseadmission/ConnectedcourseAdmission.class.php
new file mode 100644
index 00000000000..40f32acdbbf
--- /dev/null
+++ b/lib/admissionrules/connectedcourseadmission/ConnectedcourseAdmission.class.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * ConnectedAdmission.class.php
+ *
+ * Represents a rule for access only for members of connected courses.
+ *
+ * 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      Rasmus Fuhse <fuhse@data-quest.de>
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ */
+
+class ConnectedcourseAdmission extends AdmissionRule
+{
+    /**
+     * Standard constructor.
+     *
+     * @param  String ruleId
+     */
+    public function __construct($ruleId = '', $courseSetId = '')
+    {
+        parent::__construct($ruleId, $courseSetId);
+        $this->default_message = _('Die Anmeldung ist nur für Mitglieder der dazu gehörigen Lehrveranstaltung möglich.');
+    }
+
+
+    /**
+     * Gets some text that describes what this AdmissionRule (or respective
+     * subclass) does.
+     */
+    public static function getDescription()
+    {
+        return _('Diese Art von Anmelderegel erlaubt die Anmeldung an bestimmte Studiengruppen. Nur wer in einer verknüpften Lehrveranstaltung eingetragen ist, darf sich auch in die Studiengruppe anmelden.');
+    }
+
+    /**
+     * Return this rule's name.
+     */
+    public static function getName()
+    {
+        return _('Anmeldung nur über verknüpfte Lehrveranstaltung');
+    }
+
+    /**
+     * Gets the template that provides a configuration GUI for this rule.
+     *
+     * @return String
+     */
+    public function getTemplate()
+    {
+        $factory = new Flexi\Factory(dirname(__FILE__).'/templates/');
+        // Now open specific template for this rule and insert base template.
+        $tpl = $factory->open('configure');
+        $tpl->set_attribute('rule', $this);
+        return $tpl->render();
+    }
+
+    /**
+     * Internal helper function for loading rule definition from database.
+     */
+    public function load()
+    {
+
+    }
+
+    /**
+     * Does the current rule allow the given user to register as participant
+     * in the given course? Never happens here as admission is completely
+     * locked.
+     *
+     * @param  String userId
+     * @param  String courseId
+     * @return Array Any errors that occurred on admission.
+     */
+    public function ruleApplies($userId, $courseId)
+    {
+        $errors = [];
+        $statement = DBManager::get()->prepare("
+            SELECT 1
+            FROM `studygroup_courses`
+                INNER JOIN `seminar_user` ON (`seminar_user`.`Seminar_id` = `studygroup_courses`.`course_id`)
+            WHERE `studygroup_courses`.`studygroup_id` = :studygroup_id
+                AND `seminar_user`.`user_id` = :user_id
+            LIMIT 1
+        ");
+        $statement->execute([
+            'user_id' => $userId,
+            'studygroup_id' => $courseId
+        ]);
+        if (!$statement->fetch(PDO::FETCH_COLUMN)) {
+            $errors[] = $this->getMessage();
+        }
+        return $errors;
+    }
+
+    /**
+     * Helper function for storing data to DB.
+     */
+    public function store()
+    {
+
+    }
+
+    /**
+     * A textual description of the current rule.
+     *
+     * @return String
+     */
+    public function toString() {
+        $factory = new Flexi\Factory(dirname(__FILE__).'/templates/');
+        $tpl = $factory->open('info');
+        $tpl->set_attribute('rule', $this);
+        return $tpl->render();
+    }
+
+}
diff --git a/lib/admissionrules/connectedcourseadmission/rule.manifest b/lib/admissionrules/connectedcourseadmission/rule.manifest
new file mode 100644
index 00000000000..a86c2184daa
--- /dev/null
+++ b/lib/admissionrules/connectedcourseadmission/rule.manifest
@@ -0,0 +1,2 @@
+# ConnectedcourseAdmission
+classname=ConnectedcourseAdmission
diff --git a/lib/admissionrules/connectedcourseadmission/templates/configure.php b/lib/admissionrules/connectedcourseadmission/templates/configure.php
new file mode 100644
index 00000000000..4a60973d09b
--- /dev/null
+++ b/lib/admissionrules/connectedcourseadmission/templates/configure.php
@@ -0,0 +1,5 @@
+<h3><?= $rule->getName() ?></h3>
+<label for="message" class="caption">
+    <?= _('Nachricht bei fehlgeschlagener Anmeldung') ?>:
+</label>
+<textarea name="message" rows="4" cols="50"><?= $rule->getMessage() ?></textarea>
\ No newline at end of file
diff --git a/lib/admissionrules/connectedcourseadmission/templates/info.php b/lib/admissionrules/connectedcourseadmission/templates/info.php
new file mode 100644
index 00000000000..123c7bc906f
--- /dev/null
+++ b/lib/admissionrules/connectedcourseadmission/templates/info.php
@@ -0,0 +1 @@
+<?= _('Die Anmeldung ist gesperrt.') ?>
diff --git a/lib/classes/Avatar.php b/lib/classes/Avatar.php
index bb3a91564e7..b9586550c5c 100644
--- a/lib/classes/Avatar.php
+++ b/lib/classes/Avatar.php
@@ -509,7 +509,7 @@ class Avatar
 
         $output_file = $this->getCustomAvatarPath($size);
         $directory = dirname($output_file);
-        if (!is_dir($directory) && !mkdir($directory)) {
+        if (!is_dir($directory) && !@mkdir($directory)) {
             throw new Exception(_('Das Verzeichnis zum Speichern der Datei konnte nicht angelegt werden.'));
         }
 
diff --git a/lib/classes/MyRealmModel.php b/lib/classes/MyRealmModel.php
index 54252881a1d..f3ab44c975e 100644
--- a/lib/classes/MyRealmModel.php
+++ b/lib/classes/MyRealmModel.php
@@ -281,6 +281,14 @@ class MyRealmModel
         $children = [];
         $semester_assign = [];
 
+        $courses2 = $courses;
+        foreach ($courses2 as $course) {
+            foreach ($course->studygroups as $studygroup) {
+                $courses[] = $studygroup;
+            }
+        }
+        $courses = $courses2;
+
         foreach ($courses as $course) {
             // export object to array for simple handling
             $_course = $course->toArray($param_array);
@@ -326,9 +334,7 @@ class MyRealmModel
             if ($show_semester_name && count($course->semesters) !== 1 && !$course->getSemClass()['studygroup_mode']) {
                 $_course['name'] .= ' (' . $course->getTextualSemester() . ')';
             }
-            if ($course->parent_course) {
-                $_course['parent_course'] = $course->parent_course;
-            }
+            $_course['parent_course'] = $course->parent_course ?? null;
             $_course['is_group'] = $course->getSemClass()->isGroup();
             $_course['navigation'] = self::getAdditionalNavigations(
                 $_course['seminar_id'],
@@ -340,7 +346,7 @@ class MyRealmModel
 
             // add the the course to the correct semester
 
-            if (empty($_course['parent_course'])) {
+            if (empty($_course['parent_course']) && !$course->isStudygroup()) {
                 if ($course->isOpenEnded()) {
                     if ($current_semester_nr >= $min_sem_key && $current_semester_nr <= $max_sem_key) {
                         $sem_courses[$current_semester_nr][$course->id] = $_course;
@@ -357,9 +363,16 @@ class MyRealmModel
                         }
                     }
                 }
-            } else {
+            } elseif(!empty($_course['parent_course'])) {
                 $children[$_course['parent_course']][] = $_course;
             }
+            if ($course->isStudygroup()) {
+                foreach ($course->connectedcourses as $connectedcourse) {
+                    if ($GLOBALS['perm']->have_studip_perm('user', $course->id)) {
+                        $children[$connectedcourse->id][] = $_course;
+                    }
+                }
+            }
         }
 
         // Now sort children directly under their parent.
@@ -787,78 +800,6 @@ class MyRealmModel
         $sem_courses = $_tmp_courses;
     }
 
-    /**
-     * Retrieves all study groups for the current user.
-     *
-     * @returns array A two-dimensional array. The second dimension contains
-     *     data for each study group. Most fields of the Course model are
-     *     present in the second dimension and there are additional fields
-     *     like the colour (gruppe) or the start and end semester.
-     */
-    public static function getStudygroups()
-    {
-        $studygroup_sem_types = array_filter(
-            array_keys($GLOBALS['SEM_TYPE']),
-            function ($sem_type_id) {
-                return (bool) $GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$sem_type_id]['class']]['studygroup_mode'];
-            }
-        );
-        $studygroup_memberships = CourseMember::findBySQL(
-            'INNER JOIN `seminare` USING (`seminar_id`)
-            WHERE `seminar_user`.`user_id` = :me
-            AND `seminare`.`status` IN (:studygroup_semtypes)
-            GROUP BY `seminar_id`
-            ORDER BY `seminar_user`.`gruppe` ASC, `seminare`.`name` ASC',
-            [
-                'me' => User::findCurrent()->id,
-                'studygroup_semtypes' => $studygroup_sem_types
-            ]
-        );
-        $studygroups = [];
-        Course::findEachMany(
-            function ($studygroup) use (&$studygroups) {
-                $studygroups[$studygroup->id] = $studygroup;
-            },
-            array_map(
-                function ($membership) {
-                    return $membership->seminar_id;
-                },
-                $studygroup_memberships
-            )
-        );
-
-        $data_fields = 'name seminar_id visible veranstaltungsnummer status visible '
-                     . 'chdate admission_binding admission_prelim';
-        $studygroup_data = [];
-        foreach ($studygroup_memberships as $membership) {
-            if (!isset($studygroups[$membership->seminar_id])) {
-                continue;
-            }
-            $studygroup = $studygroups[$membership->seminar_id];
-            $visit_data = get_objects_visits([$studygroup->id], 0, null, null, $studygroup->tools->pluck('plugin_id'));
-            $data = $studygroup->toArray($data_fields);
-            $data['tools'] = $studygroup->tools;
-            $data['sem_class'] = $studygroup->getSemClass();
-            $data['start_semester'] = $studygroup->start_semester->name;
-            $data['end_semester'] = $studygroup->end_semester->name ?? '';
-            $data['obj_type'] = 'sem';
-            $data['user_status'] = $membership->status;
-            $data['gruppe'] = $membership->gruppe;
-            $data['visitdate'] = $visit_data[$studygroup->id][0]['visitdate'];
-            $data['last_visitdate'] = $visit_data[$studygroup->id][0]['last_visitdate'];
-            $data['navigation'] = self::getAdditionalNavigations(
-                $studygroup->id,
-                $data,
-                $data['sem_class'],
-                $GLOBALS['user']->id,
-                $visit_data[$studygroup->id]
-            );
-            $studygroup_data[$studygroup->id] = $data;
-        }
-
-        return $studygroup_data;
-    }
-
 
     /**
      * Calc nav elements to get the table-column-width
diff --git a/lib/classes/StudipController.php b/lib/classes/StudipController.php
index e0cfd0584c8..b7c55fdbb95 100644
--- a/lib/classes/StudipController.php
+++ b/lib/classes/StudipController.php
@@ -571,6 +571,11 @@ abstract class StudipController extends Trails\Controller
         );
     }
 
+    /**
+     * Renders a stud.ip form object.
+     *
+     * @param \Studip\Forms\Form   $form   the form that should be rendered.
+     */
     public function render_form(\Studip\Forms\Form $form)
     {
         \NotificationCenter::postNotification('FormWillRender', $form);
@@ -591,6 +596,7 @@ abstract class StudipController extends Trails\Controller
         $this->render_template($app->getTemplate(), $this->layout);
     }
 
+
     /**
      * relays current request to another controller and returns the response
      * the other controller is given all assigned properties, additional parameters are passed
diff --git a/lib/classes/StudygroupModel.php b/lib/classes/StudygroupModel.php
index 9fd44b3eeab..09cec1647d6 100644
--- a/lib/classes/StudygroupModel.php
+++ b/lib/classes/StudygroupModel.php
@@ -168,8 +168,9 @@ class StudygroupModel
      */
     public static function countGroups($search = null, $closed_groups = null)
     {
-        $conditions = ['status IN (?)'];
-        $parameters = [studygroup_sem_types()];
+        $conditions = ['status IN (:studygroup_sem_types)'];
+        $parameters['studygroup_sem_types'] = studygroup_sem_types();
+        $joins = '';
 
         // Only root may see hidden studygroups
         if (!$GLOBALS['perm']->have_perm('root')) {
@@ -178,8 +179,10 @@ class StudygroupModel
 
         // Search by name?
         if (isset($search)) {
-            $conditions[] = "Name LIKE CONCAT('%', ?, '%')";
-            $parameters[] = $search;
+            $joins = "LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = seminare.Seminar_id AND `tags_relations`.`range_type` = 'course')
+                    LEFT JOIN `tags` ON (`tags`.`id` = `tags_relations`.`tag_id` AND `tags`.`active` = 1) ";
+            $conditions[] = "(seminare.`Name` LIKE :search OR `tags`.`name` LIKE :search) ";
+            $parameters['search'] = '%' . $search . '%';
         }
 
         // Show closed groups
@@ -188,7 +191,9 @@ class StudygroupModel
         }
 
         return Course::countBySQL(
-            implode(' AND ', $conditions),
+            ($joins ? $joins.' WHERE ' : '') .
+            implode(' AND ', $conditions) .
+            ' GROUP BY Seminar_id ',
             $parameters
         );
     }
@@ -209,25 +214,27 @@ class StudygroupModel
             $elements_per_page = Config::get()->ENTRIES_PER_PAGE;
         }
 
-        $sql = "SELECT *
-                FROM seminare AS s";
+        $sql = "SELECT s.*
+                FROM seminare AS s
+                    LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = s.Seminar_id AND `tags_relations`.`range_type` = 'course')
+                    LEFT JOIN `tags` ON (`tags`.`id` = `tags_relations`.`tag_id` AND `tags`.`active` = 1) ";
         $sql_additional = '';
         $conditions = [];
         $parameters = [];
 
-        $conditions[] = 's.status IN (?)';
-        $parameters[] = studygroup_sem_types();
+        $conditions[] = 's.status IN (:studygroup_sem_types)';
+        $parameters['studygroup_sem_types'] = studygroup_sem_types();
 
         if (!$GLOBALS['perm']->have_perm('root')) {
             $conditions[] = 's.visible = 1';
         }
 
         if (isset($search)) {
-            $conditions[] = "Name LIKE CONCAT('%', ?, '%')";
-            $parameters[] = $search;
+            $conditions[] = "(s.`Name` LIKE :search OR `tags`.`name` LIKE :search) ";
+            $parameters['search'] = '%' . $search . '%';
         }
         if (isset($closed_groups) && !$closed_groups) {
-            $conditions[] = 'admission_prelim = 0';
+            $conditions[] = 's.admission_prelim = 0';
         }
 
         list($sort_by, $sort_order) = explode('_', $sort);
@@ -235,35 +242,37 @@ class StudygroupModel
 
         // add here the sortings
         if ($sort_by === 'name') {
-            $sort_by = 'Name';
+            $sort_by = 's.Name';
         } elseif ($sort_by === 'founded') {
-            $sort_by = 'mkdate';
+            $sort_by = 's.mkdate';
         } elseif ($sort_by === 'member') {
             $sort_by = 'members';
 
             $sql = "SELECT s.*, COUNT(su.user_id) AS members
                     FROM seminare AS s
-                    LEFT JOIN seminar_user AS su USING (Seminar_id)";
+                        LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = s.Seminar_id AND `tags_relations`.`range_type` = 'course')
+                        LEFT JOIN `tags` ON (`tags`.`id` = `tags_relations`.`tag_id` AND `tags`.`active` = 1)
+                        LEFT JOIN seminar_user AS su USING (Seminar_id)";
 
-            $sql_additional = 'GROUP BY s.Seminar_id';
         } elseif ($sort_by === 'founder') {
             $sort_by = "GROUP_CONCAT(aum.Nachname ORDER BY su.status, su.position, aum.Nachname, aum.Vorname SEPARATOR ',')";
 
             $sql = "SELECT s.*
                     FROM seminare AS s
-                    LEFT JOIN seminar_user AS su ON (s.Seminar_id = su.Seminar_id AND su.status = 'dozent')
-                    LEFT JOIN auth_user_md5 AS aum ON (su.user_id = aum.user_id)";
+                        LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = s.Seminar_id AND `tags_relations`.`range_type` = 'course')
+                        LEFT JOIN `tags` ON (`tags`.`id` = `tags_relations`.`tag_id` AND `tags`.`active` = 1) LEFT JOIN seminar_user AS su ON (s.Seminar_id = su.Seminar_id AND su.status = 'dozent')
+                        LEFT JOIN auth_user_md5 AS aum ON (su.user_id = aum.user_id)";
 
-            $sql_additional = 'GROUP BY s.Seminar_id';
         } elseif ($sort_by === 'ismember') {
             $sort_by = 'is_member';
 
             $sql = "SELECT s.*, COUNT(su.user_id) AS is_member
                     FROM seminare AS s
-                    LEFT JOIN seminar_user AS su ON s.Seminar_id = su.Seminar_id AND su.user_id = ?";
-            array_unshift($parameters, $GLOBALS['user']->id);
+                        LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = s.Seminar_id AND `tags_relations`.`range_type` = 'course')
+                        LEFT JOIN `tags` ON (`tags`.`id` = `tags_relations`.`tag_id` AND `tags`.`active` = 1)
+                        LEFT JOIN seminar_user AS su ON s.Seminar_id = su.Seminar_id AND su.user_id = :user_id";
+            $parameters['user_id'] = $GLOBALS['user']->id;
 
-            $sql_additional = 'GROUP BY s.Seminar_id';
         } elseif ($sort_by == 'access') {
             $sort_by = 'admission_prelim';
         } else {
@@ -274,13 +283,22 @@ class StudygroupModel
             $sql .= ' WHERE ' . implode(' AND ', $conditions);
         }
         $sql .= ' ' . $sql_additional;
+        $sql .= ' GROUP BY s.Seminar_id ';
         $sql .= " ORDER BY {$sort_by} {$sort_order}";
-        $sql .= ", name {$sort_order} LIMIT " . (int) $lower_bound . ',' . (int) $elements_per_page;
+        $sql .= ", s.`name` {$sort_order} LIMIT " . (int) $lower_bound . ',' . (int) $elements_per_page;
 
         $statement = DBManager::get()->prepare($sql);
         $statement->execute($parameters);
         $groups = $statement->fetchAll();
 
+        foreach ($groups as $key => $studygroup)
+        {
+            $visit_data = get_objects_visits([$studygroup['Seminar_id']], 0);
+            $studygroup['last_visit_date'] = $visit_data[$studygroup['Seminar_id']];
+            $groups[$key]['last_visit_date'] = $studygroup['last_visit_date'];
+            $groups[$key]['course'] = Course::buildExisting($studygroup);
+        }
+
         return $groups;
     }
 
@@ -562,4 +580,186 @@ class StudygroupModel
 
         return $msging->insert_message($message, $recipients, '', '', '', '1', '', $subject);
     }
+
+    /**
+     * @param Course $studygroup
+     * @param $course_id
+     * @return false|string
+     */
+    public static function proposeAsStudygroupTo(Course $studygroup, $course_id)
+    {
+        if (!$GLOBALS['perm']->have_studip_perm('tutor', $studygroup->id) && !$GLOBALS['perm']->have_studip_perm('tutor')) {
+            return false;
+        }
+        $proposal = StudygroupCourseProposal::findOneBySQL('course_id = ? AND studygroup_id = ?', [
+            $course_id,
+            $studygroup->id
+        ]);
+        if ($GLOBALS['perm']->have_studip_perm('tutor', $course_id) || $proposal['proposed_from'] === 'course') {
+            $connection = StudygroupCourse::findOneBySQL('course_id = ? AND studygroup_id = ?', [
+                $course_id,
+                $studygroup->id
+            ]);
+            if (!$connection) {
+                $connection = StudygroupCourse::create([
+                    'course_id' => $course_id,
+                    'studygroup_id' => $studygroup->id
+                ]);
+            }
+            if ($proposal) {
+                if ($proposal['proposed_from'] === 'course') {
+                    $statement = DBManager::get()->prepare("
+                            SELECT `username`, `user_id`
+                            FROM `auth_user_md5`
+                                INNER JOIN `seminar_user` USING (`user_id`)
+                            WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent')
+                        ");
+                    $statement->execute([$course_id]);
+                    $messaging = new messaging();
+
+                    foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) {
+                        setTempLanguage($user_data['user_id']);
+                        $messaging->insert_message(
+                            sprintf(
+                                _('Ihr Vorschlag, die Studiengruppe "%s" mit der Veranstaltung "%s" zu verknüpfen, wurde angenommen.'),
+                                $studygroup->getFullname(),
+                                Course::find($course_id)->getFullname()
+                            ),
+                            $user_data['username'],
+                            '____%system%____',
+                            '',
+                            '',
+                            '',
+                            '',
+                            _('Verknüpfungsvorschlag angenommen'),
+                            '',
+                            'normal',
+                            ['Studiengruppe']
+                        );
+                        restoreLanguage();
+                    }
+                }
+                $proposal->delete();
+            }
+            PageLayout::postSuccess(_('Veranstaltung wurde verknüpft.'));
+            return 'connected';
+        } else {
+            if (!$proposal) {
+                $proposal = StudygroupCourseProposal::create([
+                    'course_id' => $course_id,
+                    'studygroup_id' => $studygroup->id,
+                    'proposed_from' => 'studygroup',
+                    'user_id' => User::findCurrent()->id
+                ]);
+                //send message:
+                $statement = DBManager::get()->prepare("
+                        SELECT `username`, `user_id`
+                        FROM `auth_user_md5`
+                            INNER JOIN `seminar_user` USING (`user_id`)
+                        WHERE `seminar_user`.`Seminar_id` = ? AND `seminar_user`.`status` IN ('tutor', 'dozent')
+                    ");
+                $statement->execute([$course_id]);
+                $messaging = new messaging();
+                $oldbase = URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']);
+
+                foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $user_data) {
+                    setTempLanguage($user_data['user_id']);
+                    $messaging->insert_message(
+                        sprintf(
+                            _('Es wurde vorgeschlagen, die Studiengruppe „%1$s“ mit Ihrer Veranstaltung „%2$s“ zu verknüpfen. Sie können den Vorschlag unter folgendem Link annehmen oder ablehnen:'),
+                            $studygroup->getFullname(),
+                            Course::find($course_id)->getFullname()
+                        )."\n\n".URLHelper::getURL('dispatch.php/course/connectedstudygroups/index', ['cid' => $course_id]),
+                        $user_data['username'],
+                        '____%system%____',
+                        '',
+                        '',
+                        '',
+                        '',
+                        _('Verknüpfung Ihrer Veranstaltung zu einer Studiengruppe'),
+                        '',
+                        'normal',
+                        ['Studiengruppe']
+                    );
+                    restoreLanguage();
+                }
+                URLHelper::setBaseURL($oldbase);
+                return 'proposed';
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Retrieves all study groups for the current user.
+     *
+     * @returns array A two-dimensional array. The second dimension contains
+     *     data for each study group. Most fields of the Course model are
+     *     present in the second dimension and there are additional fields
+     *     like the colour (gruppe) or the start and end semester.
+     */
+    public static function getStudygroups()
+    {
+        $studygroup_sem_types = array_filter(
+            array_keys($GLOBALS['SEM_TYPE']),
+            function ($sem_type_id) {
+                return (bool) $GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$sem_type_id]['class']]['studygroup_mode'];
+            }
+        );
+        $studygroup_memberships = CourseMember::findBySQL(
+            'INNER JOIN `seminare` USING (`seminar_id`)
+            WHERE `seminar_user`.`user_id` = :me
+            AND `seminare`.`status` IN (:studygroup_semtypes)
+            GROUP BY `seminar_id`
+            ORDER BY `seminar_user`.`gruppe` ASC, `seminare`.`name` ASC',
+            [
+                'me' => User::findCurrent()->id,
+                'studygroup_semtypes' => $studygroup_sem_types
+            ]
+        );
+        $studygroups = [];
+        Course::findEachMany(
+            function ($studygroup) use (&$studygroups) {
+                $studygroups[$studygroup->id] = $studygroup;
+            },
+            array_map(
+                function ($membership) {
+                    return $membership->seminar_id;
+                },
+                $studygroup_memberships
+            )
+        );
+
+        $data_fields = 'name seminar_id visible veranstaltungsnummer duration_time status visible '
+            . 'chdate admission_binding admission_prelim';
+        $studygroup_data = [];
+        foreach ($studygroup_memberships as $membership) {
+            if (!isset($studygroups[$membership->seminar_id])) {
+                continue;
+            }
+            $studygroup = $studygroups[$membership->seminar_id];
+            $visit_data = get_objects_visits([$studygroup->id], 0, null, null, $studygroup->tools->pluck('plugin_id'));
+            $data = $studygroup->toArray($data_fields);
+            $data['tools'] = $studygroup->tools;
+            $data['sem_class'] = $studygroup->getSemClass();
+            $data['start_semester'] = $studygroup->start_semester->name;
+            $data['end_semester'] = $studygroup->end_semester->name ?? '';
+            $data['obj_type'] = 'sem';
+            $data['user_status'] = $membership->status;
+            $data['gruppe'] = $membership->gruppe;
+            $data['mkdate'] = $membership->mkdate;
+            $data['visitdate'] = $visit_data[$studygroup->id][0]['visitdate'];
+            $data['last_visitdate'] = $visit_data[$studygroup->id][0]['last_visitdate'];
+            $data['navigation'] = MyRealmModel::getAdditionalNavigations(
+                $studygroup->id,
+                $data,
+                $data['sem_class'],
+                $GLOBALS['user']->id,
+                $visit_data[$studygroup->id]
+            );
+            $studygroup_data[$studygroup->id] = $data;
+        }
+
+        return $studygroup_data;
+    }
 }
diff --git a/lib/classes/admission/AdmissionRule.php b/lib/classes/admission/AdmissionRule.php
index cff3e356e47..9855165f03a 100644
--- a/lib/classes/admission/AdmissionRule.php
+++ b/lib/classes/admission/AdmissionRule.php
@@ -57,6 +57,7 @@ abstract class AdmissionRule
                             'active' => (bool) $row['active'],
                         ];
                     } catch (Exception $e) {
+                        throw $e;
                     }
                 }
             );
@@ -521,5 +522,4 @@ abstract class AdmissionRule
         $this->id = md5(uniqid(get_class($this)));
         $this->courseSetId = null;
     }
-
 }
diff --git a/lib/classes/admission/CourseSet.php b/lib/classes/admission/CourseSet.php
index d93cfb0e2ac..81bd17f04ca 100644
--- a/lib/classes/admission/CourseSet.php
+++ b/lib/classes/admission/CourseSet.php
@@ -1150,6 +1150,45 @@ class CourseSet implements UserFilterRange
         return $locked_set_id;
     }
 
+    public static function getConnectedcourseAdmissionSetId()
+    {
+        $db = DBManager::get();
+        $locked_set_id = $db->fetchColumn("
+            SELECT `courseset_rule`.`set_id`
+            FROM `courseset_rule`
+                INNER JOIN `coursesets` USING (`set_id`)
+            WHERE `type` = 'ConnectedcourseAdmission'
+                AND `private` = 1
+                AND `user_id` = ''
+            LIMIT 1
+        ");
+        if (!$locked_set_id) {
+            $cs_insert = $db->prepare("
+                INSERT INTO coursesets (set_id, user_id, name, infotext, algorithm, private, mkdate, chdate)
+                VALUES (?, ?, ?, ?, '', ?, ?, ?)
+            ");
+            $cs_r_insert = $db->prepare("
+                INSERT INTO `courseset_rule` (`set_id`, `rule_id`, `type`, `mkdate`)
+                VALUES (?, ?, ?, UNIX_TIMESTAMP())
+            ");
+            $locked_insert = $db->prepare("
+                INSERT INTO `lockedadmissions` (`rule_id`, `message`, `mkdate`, `chdate`)
+                VALUES (?,'Die Anmeldung ist gesperrt', UNIX_TIMESTAMP(), UNIX_TIMESTAMP())
+            ");
+            $locked_set_id = md5(uniqid('coursesets_connected_course',1));
+            $name = 'Verknüpfte Veranstaltung (global)';
+            $cs_insert->execute([$locked_set_id,'',$name,'',1,time(),time()]);
+            $locked_rule_id = md5(uniqid('connectedcourse',1));
+            $locked_insert->execute([$locked_rule_id]);
+            $cs_r_insert->execute([
+                $locked_set_id,
+                $locked_rule_id,
+                'ConnectedcourseAdmission'
+            ]);
+        }
+        return $locked_set_id;
+    }
+
     public static function addCourseToSet($set_id, $course_id)
     {
         $db = DBManager::get();
diff --git a/lib/classes/coursewizardsteps/BasicDataWizardStep.php b/lib/classes/coursewizardsteps/BasicDataWizardStep.php
index 4b401ee1042..18892f0febc 100644
--- a/lib/classes/coursewizardsteps/BasicDataWizardStep.php
+++ b/lib/classes/coursewizardsteps/BasicDataWizardStep.php
@@ -30,7 +30,7 @@ class BasicDataWizardStep implements CourseWizardStep
         // Load template from step template directory.
         $factory = new Flexi\Factory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views/course/wizard/steps');
         if (!empty($values[__CLASS__]['studygroup'])) {
-            $tpl = $factory->open('basicdata/index_studygroup');
+            $tpl = $factory->open('studygroups/index');
             $values[__CLASS__]['lecturers'][$GLOBALS['user']->id] = 1;
         } else {
             $tpl = $factory->open('basicdata/index');
@@ -428,7 +428,7 @@ class BasicDataWizardStep implements CourseWizardStep
         $course->name = new I18NString($values['name'], $values['name_i18n'] ?? []);
         $course->veranstaltungsnummer = $values['number'] ?? null;
         $course->beschreibung = new I18NString($values['description'], $values['description_i18n'] ?? []);
-        $course->start_semester = Semester::find($values['start_semester']);
+        $course->start_semester = isset($values['start_semester']) ? Semester::find($values['start_semester']) : Semester::findCurrent();
         $course->institut_id = $values['institute'];
 
         $semclass = $course->getSemClass();
@@ -455,6 +455,7 @@ class BasicDataWizardStep implements CourseWizardStep
                     break;
             }
         }
+
         if (!$course->store()) {
             return false;
         }
@@ -506,6 +507,51 @@ class BasicDataWizardStep implements CourseWizardStep
             self::copyParticipantsAndGroups($course, $source_id, $copy_participants, $copy_groups, $copy_members);
         }
 
+        if (in_array($values['coursetype'], studygroup_sem_types())) {
+            if (!empty($values['lv_course_id'])) {
+                StudygroupModel::proposeAsStudygroupTo($course, $values['lv_course_id']);
+            }
+        }
+
+        if (!empty($values['exp_date'])) {
+            $exp_date = strtotime($values['exp_date']);
+            if ($exp_date) {
+                CourseConfig::get($course->id)->store('STUDYGROUP_EXPIRATION_DATE', $exp_date);
+            }
+        }
+        if (!empty($values['tags'])) {
+            foreach ($values['tags'] as $name) {
+                if ($tag = Tag::findOneByName($name)) {
+                    if (!$tag->active) {
+                        continue;
+                    }
+                } else {
+                    $tag = Tag::create(['name' => $name]);
+                }
+
+                $relation = TagRelation::findOneBySQL(
+                    "`range_id` = :course_id AND `range_type` = 'course' AND `tag_id` = :tag_id", 
+                    [
+                        'tag_id'    => $tag->id,
+                        'course_id' => $course->id
+                    ]
+                );
+                if (!$relation) {
+                    $relation = TagRelation::create([
+                        'range_id'   => $course->id,
+                        'range_type' => 'course',
+                        'tag_id'     => $tag->id
+                    ]);
+                }
+            }
+        }
+
+
+        if (!empty($values['stgteil_id'])) {
+            $studiengangteil = StudiengangTeil::find($values['stgteil_id']);
+            $studiengangteil->addStudygroup($course);
+        }
+
         return $course;
     }
 
diff --git a/lib/classes/forms/MultiquicksearchInput.php b/lib/classes/forms/MultiquicksearchInput.php
new file mode 100644
index 00000000000..c7a9a02fa29
--- /dev/null
+++ b/lib/classes/forms/MultiquicksearchInput.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Studip\Forms;
+
+class MultiquicksearchInput extends Input
+{
+    public function render()
+    {
+        $options = $this->extractOptionsFromAttributes($this->attributes);
+
+        $name = $this->name;
+        if (substr($name, -2) === '[]') {
+            $name .= substr($name, 0, -2);
+        }
+
+        $template = $GLOBALS['template_factory']->open('forms/multiquicksearch_input');
+        $template->title      = $this->title;
+        $template->name       = $name;
+        $template->value      = $this->getValue();
+        $template->id         = md5(uniqid());
+        $template->required   = $this->required;
+        $template->attributes = arrayToHtmlAttributes($this->attributes);
+        $template->options    = $options;
+        return $template->render();
+    }
+
+    public function getRequestValue()
+    {
+        return \Request::getArray($this->name);
+    }
+}
diff --git a/lib/classes/globalsearch/GlobalSearchStudygroups.php b/lib/classes/globalsearch/GlobalSearchStudygroups.php
new file mode 100644
index 00000000000..a9f13981b70
--- /dev/null
+++ b/lib/classes/globalsearch/GlobalSearchStudygroups.php
@@ -0,0 +1,315 @@
+<?php
+/**
+ * Global search module for study groups
+ *
+ * @author      Michaela Brückner <brueckner@data-quest.de>
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ * @since       6.0
+ */
+class GlobalSearchStudygroups extends GlobalSearchModule implements GlobalSearchFulltext
+{
+    /**
+     * Returns the displayname for this module
+     *
+     * @return string
+     */
+    public static function getName()
+    {
+        return _('Studiengruppen');
+    }
+
+    /**
+     * Returns the filters that are displayed in the sidebar of the global search.
+     *
+     * @return array Filters for this class.
+     */
+    public static function getFilters()
+    {
+        return ['semester', 'study_course'];
+    }
+
+    /**
+     * Transforms the search request into an sql statement, that provides the id (same as getId) as type and
+     * the object id, that is later passed to the filter.
+     *
+     * This function is required to make use of the mysql union parallelism
+     *
+     * @param string $search the input query string
+     * @param array $filter an array with search limiting filter information (e.g. 'category', 'semester', etc.)
+     * @return string SQL Query to discover elements for the search
+     */
+    public static function getSQL($search, $filter, $limit)
+    {
+        if (!$search) {
+            return null;
+        }
+        $search = str_replace(' ', '% ', $search);
+        $query = DBManager::get()->quote("%{$search}%");
+
+        $language_name = 'courses.`Name`';
+        $language_join = '';
+        if (I18N::isEnabled() && $_SESSION['_language'] !== I18NString::getDefaultLanguage()) {
+            $language_name = 'IFNULL(`i18n`.`value`, courses.`Name`)';
+            $language_join = "LEFT JOIN `i18n`
+                                ON `i18n`.`object_id` = courses.`Seminar_id`
+                                  AND `i18n`.`table` = 'seminare'
+                                  AND `i18n`.`field` = 'name'
+                                  AND `lang` = " . DBManager::get()->quote($_SESSION['_language']);
+        }
+
+        $visibility = '';
+        $seminaruser = '';
+        $semester_join = '';
+        $institute_condition = '';
+        $seminar_type_condition = '';
+        $semester_condition = '';
+
+        // visibility
+        //if (!$GLOBALS['perm']->have_perm('admin')) {
+            $visibility = "courses.`visible` = 1 AND ";
+            $seminaruser = " AND EXISTS (
+                SELECT 1 FROM `seminar_user`
+                WHERE `seminar_id` = `courses`.`Seminar_id`
+                    AND `user_id` = " . DBManager::get()->quote($GLOBALS['user']->id) . "
+            ) ";
+        //}
+
+        // generate SQL for the given sidebar filter (semester, institute, seminar_type)
+        if ($filter['category'] === self::class || $filter['category'] === 'show_all_categories') {
+            if (!empty($filter['semester'])) {
+                if ($filter['semester'] === 'future') {
+                    $semester = Semester::findCurrent();
+                    $next_semester = Semester::findNext();
+
+                    $semester_ids = [$semester->id];
+                    if ($next_semester) {
+                        $semester_ids[] = $next_semester->id;
+                    }
+                } else {
+                    $semester = Semester::findByTimestamp($filter['semester']);
+                    $semester_ids = [$semester->id];
+                }
+                $semester_join = "LEFT JOIN semester_courses ON (courses.Seminar_id = semester_courses.course_id) ";
+                $semester_condition = "
+                    AND (
+                        semester_courses.semester_id IS NULL OR semester_courses.semester_id IN (" . join(',', array_map([DBManager::get(), 'quote'], $semester_ids)) . ")
+                    ) ";
+            }
+            $seminar_type_condition = " AND `courses`.`status` = '99' ";
+        }
+
+        $tags_join = "LEFT JOIN tags_relations ON courses.Seminar_id = tags_relations.range_id LEFT JOIN tags on tags_relations.tag_id = tags.id";
+        $tags_name = "tags.name";
+
+        $sql = "SELECT SQL_CALC_FOUND_ROWS courses.`Seminar_id`,
+                       {$language_name} AS `Name`,
+                       courses.`VeranstaltungsNummer`, courses.`status`,
+                       {$tags_name}  AS `Tag`
+                FROM `seminare` AS courses
+                {$language_join}
+                JOIN `seminar_user` u ON (u.`Seminar_id` = courses.`Seminar_id` AND u.`status` = 'dozent')
+                JOIN `auth_user_md5` a ON (a.`user_id` = u.`user_id`)
+                {$semester_join}
+                {$tags_join}
+                WHERE {$visibility}
+                    (
+                        {$language_name} LIKE {$query}
+                        OR {$tags_name} LIKE {$query}
+                        OR courses.`VeranstaltungsNummer` LIKE {$query}
+                        OR CONCAT(a.`Nachname`, ', ', a.`Vorname`, ' ', a.`Nachname`) LIKE {$query}
+                    )
+                {$seminaruser}
+                {$institute_condition}
+                {$seminar_type_condition}
+                {$semester_condition}
+                GROUP BY courses.Seminar_id";
+
+        if (Config::get()->IMPORTANT_SEMNUMBER) {
+            $sql .= ", courses.`VeranstaltungsNummer`";
+        }
+
+        $sql .= ", `Name`";
+        $sql .= " LIMIT " . $limit;
+
+
+        return $sql;
+    }
+
+    /**
+     * Returns an array of information for the found element. Following informations (key: description) are necessary
+     *
+     * - name: The name of the object
+     * - url: The url to send the user to when he clicks the link
+     *
+     * Additional informations are:
+     *
+     * - additional: Subtitle for the hit
+     * - expand: Url if the user further expands the search
+     * - img: Avatar for the
+     *
+     * @param array $data
+     * @param string $search
+     * @return array
+     */
+    public static function filter($data, $search)
+    {
+        $course = Course::buildExisting($data);
+        $turnus_string = implode(' ', $course->getAllDatesInSemester()->toStringArray());
+        //Shorten, if string too long (add link for details.php)
+        if (mb_strlen($turnus_string) > 70) {
+            $turnus_string = htmlReady(mb_substr($turnus_string, 0, mb_strpos(mb_substr($turnus_string, 70, mb_strlen($turnus_string)), ',') + 71));
+            $turnus_string .= ' ... <a href="' . URLHelper::getURL("dispatch.php/course/details/index/{$course->id}") . '">(' . _('mehr') . ')</a>';
+        } else {
+            $turnus_string = htmlReady($turnus_string);
+        }
+        $lecturers = $course->getMembersWithStatus('dozent');
+        $semester = $course->start_semester;
+
+        // If you are not root, perhaps not all available subcourses are visible.
+        $visibleChildren = $course->children;
+        if (!$GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM)) {
+            $visibleChildren = $visibleChildren->filter(function($c) {
+                return $c->visible;
+            });
+        }
+        $result_children = [];
+        foreach($visibleChildren as $child) {
+            $result_children[] = self::filter($child, $search);
+        }
+
+        //admission state
+        $admission_state = "";
+        if (Config::get()->COURSE_SEARCH_SHOW_ADMISSION_STATE) {
+            switch (self::getStatusCourseAdmission($course->id,
+                $course->admission_prelim)) {
+                case 1:
+                    $admission_state = Icon::create(
+                        'decline-circle',
+                        Icon::ROLE_STATUS_YELLOW,
+                        tooltip2(_('Eingeschränkter Zugang'))
+                    )->asImg();
+                    break;
+                case 2:
+                    $admission_state = Icon::create(
+                        'decline-circle',
+                        Icon::ROLE_STATUS_RED,
+                        tooltip2(_('Kein Zugang'))
+                    )->asImg();
+                    break;
+                default:
+                    $admission_state = Icon::create(
+                        'check-circle',
+                        Icon::ROLE_STATUS_GREEN,
+                        tooltip2(_('Uneingeschränkter Zugang'))
+                    )->asImg();
+            }
+        }
+
+        $tags = array_map(function ($t) {
+            return '#' . $t->name;
+        }, $course->tags->getArrayCopy());
+
+        $result = [
+            'id'            => $course->id,
+            'number'        => self::mark($course->veranstaltungsnummer, $search),
+            'name'          => self::mark($course->getFullName(), $search),
+            'url'           => URLHelper::getURL("dispatch.php/course/details/index/{$course->id}", [], true),
+            'date'          => htmlReady($semester->short_name),
+            'dates'         => $turnus_string,
+            'has_children'  => count($course->children) > 0,
+            'children'      => $result_children,
+            'additional'    => implode(', ',
+                array_filter(
+                    array_map(
+                        function ($lecturer, $index) use ($search, $course) {
+                            if ($index < 3) {
+                                return self::mark($lecturer->getUserFullname(), $search);
+                            } else if ($index == 3) {
+                                return '... (' . _('mehr') . ')';
+                            }
+                        },
+                        $lecturers,
+                        array_keys($lecturers)
+                    )
+                )
+            ),
+            'expand'            => self::getSearchURL($search),
+            'admission_state'   => $admission_state,
+            'found_tag'         => self::mark(implode(' ', $tags), $search)
+        ];
+        if ($course->getSemClass()->offsetGet('studygroup_mode')) {
+            $avatar = StudygroupAvatar::getAvatar($course->id);
+        } else {
+            $avatar = CourseAvatar::getAvatar($course->id);
+        }
+        $result['img'] = $avatar->getUrl(Avatar::MEDIUM);
+        return $result;
+    }
+
+    /**
+     * Enables fulltext (MATCH AGAINST) search by creating the corresponding indices.
+     */
+    public static function enable()
+    {
+        DBManager::get()->exec("ALTER TABLE `seminare` ADD FULLTEXT INDEX globalsearch (`VeranstaltungsNummer`, `Name`)");
+        DBManager::get()->exec("ALTER TABLE `sem_types` ADD FULLTEXT INDEX globalsearch (`Name`)");
+    }
+
+    /**
+     * Disables fulltext (MATCH AGAINST) search by removing the corresponding indices.
+     */
+    public static function disable()
+    {
+        DBManager::get()->exec("DROP INDEX globalsearch ON `seminare`");
+        DBManager::get()->exec("DROP INDEX globalsearch ON `sem_types`");
+    }
+
+    /**
+     * Returns the URL that can be called for a full search.
+     *
+     * @param string $searchterm what to search for?
+     * @return string URL to the full search, containing the searchterm and the category
+     */
+    public static function getSearchURL($searchterm): string
+    {
+        return URLHelper::getURL('dispatch.php/search/globalsearch', [
+            'q'        => $searchterm,
+            'category' => self::class
+        ]);
+    }
+
+    /**
+     * Returns the admission status for a course.
+     *
+     * @param string $seminar_id Id of the course
+     * @param bool   $prelim     State of preliminary setting
+     * @return int
+     */
+    public static function getStatusCourseAdmission($seminar_id, $prelim): int
+    {
+        $sql = "SELECT COUNT(`type`) AS `types`,
+                       SUM(IF(`type` = 'LockedAdmission', 1, 0)) AS `type_locked`
+                FROM `seminar_courseset`
+	            INNER JOIN `courseset_rule` USING (`set_id`)
+	            WHERE `seminar_id` = ?
+                GROUP BY `set_id`";
+
+        $stmt = DBManager::get()->prepare($sql);
+        $stmt->execute([$seminar_id]);
+        $result = $stmt->fetch();
+
+        if (!empty($result['types'])) {
+            if ($result['type_locked']) {
+                return 2;
+            }
+            return 1;
+        }
+
+        if ($prelim) {
+            return 1;
+        }
+        return 0;
+    }
+
+}
diff --git a/lib/classes/searchtypes/StudygroupSearch.class.php b/lib/classes/searchtypes/StudygroupSearch.class.php
new file mode 100644
index 00000000000..9bfef6ca73c
--- /dev/null
+++ b/lib/classes/searchtypes/StudygroupSearch.class.php
@@ -0,0 +1,103 @@
+<?php
+# Lifter010: TODO
+/**
+ * StudygroupSearch.class.php
+ * class to add Studygroup search to Quicksearch
+ *
+ * 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      Michaela Brückner <brueckner@data-quest>
+ * @copyright   2024 Stud.IP Core-Group
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Stud.IP
+ */
+
+class StudygroupSearch extends SearchType
+{
+
+    /**
+     * title of the search like "search for courses" or just "courses"
+     * @return string
+     */
+    public function getTitle(): string
+    {
+        return _('Studiengruppen suchen');
+    }
+
+    /**
+     * Returns the results to a given keyword. To get the results is the
+     * job of this routine and it does not even need to come from a database.
+     * The results should be an array in the form
+     * array (
+     *   array($key, $name),
+     *   array($key, $name),
+     *   ...
+     * )
+     * where $key is an identifier like user_id and $name is a displayed text
+     * that should appear to represent that ID.
+     * @param keyword: string
+     * @param array $contextual_data an associative array with more variables
+     * @param int $limit maximum number of results (default: all)
+     * @param int $offset return results starting from this row (default: 0)
+     * @return array
+     */
+    public function getResults($keyword, $contextual_data = [], $limit = PHP_INT_MAX, $offset = 0): array
+    {
+        $search_helper = new StudipSemSearchHelper();
+        $search_helper->setParams(
+            [
+                'quick_search' => $keyword,
+                'qs_choose' => $contextual_data['search_sem_qs_choose'] ?? 'all',
+                'sem' => $contextual_data['search_sem_sem'] ?? 'all',
+                'category' => $contextual_data['search_sem_category'] ?? null,
+                'scope_choose' => $contextual_data['search_sem_scope_choose'] ?? null,
+                'range_choose' => $contextual_data['search_sem_range_choose'] ?? null,
+            ],
+            !(is_object($GLOBALS['perm'])
+                && $GLOBALS['perm']->have_perm(
+                    Config::Get()->SEM_VISIBILITY_PERM)));
+        $search_helper->doSearch();
+        $result = $search_helper->getSearchResultAsArray();
+
+        if (empty($result)) {
+            return [];
+        }
+
+        $query = "SELECT s.Seminar_id, CONCAT_WS(' ', s.VeranstaltungsNummer, s.name, CONCAT(' (',
+            IF(semester_courses.semester_id IS NULL,  '" . _('unbegrenzt') . "',
+                IF(COUNT(DISTINCT semester_courses.semester_id) > 1, CONCAT_WS(' - ', (SELECT start_semester.name FROM `semester_data` AS start_semester WHERE start_semester.semester_id = semester_courses.semester_id ORDER BY `beginn` ASC LIMIT 1), (SELECT end_semester.name FROM `semester_data` AS end_semester WHERE end_semester.semester_id = semester_courses.semester_id ORDER BY `beginn` DESC LIMIT 1)), sem1.name)), ')')) AS Name
+                   FROM seminare AS s
+                   LEFT JOIN semester_courses ON (semester_courses.course_id = s.Seminar_id)
+                   LEFT JOIN `semester_data` sem1 ON (semester_courses.semester_id = sem1.semester_id)
+                   LEFT JOIN seminar_user AS su ON (su.Seminar_id = s.Seminar_id AND su.status='dozent')
+                   LEFT JOIN auth_user_md5 USING (user_id)
+                   WHERE s.Seminar_id IN (?)
+                   GROUP BY s.Seminar_id";
+        if (Config::get()->IMPORTANT_SEMNUMBER) {
+            $query .= " ORDER BY s.VeranstaltungsNummer, s.Name";
+        } else {
+            $query .= " ORDER BY s.Name";
+        }
+        $statement = DBManager::get()->prepare($query);
+        $statement->execute([
+            array_slice($result, $offset, $limit) ?: ''
+        ]);
+        return $statement->fetchAll(PDO::FETCH_NUM);
+    }
+
+
+    /**
+     * Returns the path to this file, so that this class can be autoloaded and is
+     * always available when necessary.
+     * Should be: "return __file__;"
+     *
+     * @return string   path to this file
+     */
+    public function includePath(): string
+    {
+        return studip_relative_path(__FILE__);
+    }
+}
diff --git a/lib/cronjobs/expire_studygroups.class.php b/lib/cronjobs/expire_studygroups.class.php
new file mode 100644
index 00000000000..3ad9b934696
--- /dev/null
+++ b/lib/cronjobs/expire_studygroups.class.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * expire_studygroups.class.php - Removes studygroups that have reached their expiration date
+ * and also notifies about upcoming expirations to the founders of the studygroups.
+ *
+ * @author Rasmus Fuhse <fuhse@data-quest.de>
+ * @access public
+ * @since  6.0
+ */
+
+class ExpireStudygroups extends CronJob
+{
+
+    public static function getName()
+    {
+        return _('Studiengruppen abräumen');
+    }
+
+    public static function getDescription()
+    {
+        return _('Löscht ablaufende Studiengruppen und benachrichtigt einen Monat vor Ablauf der Studiengruppe über die Löschung.');
+    }
+
+    public function execute($last_result, $parameters = [])
+    {
+        $statement = DBManager::get()->prepare("
+            SELECT `seminare`.*
+            FROM `seminare`
+                INNER JOIN `config_values` ON (`config_values`.`range_id` = `seminare`.`Seminar_id` AND `config_values`.`field` = 'STUDYGROUP_EXPIRATION_DATE')
+            WHERE `config_values`.`value` >= UNIX_TIMESTAMP()
+        ");
+        $statement->execute();
+        while ($course = Course::buildExisting($statement->fetch(PDO::FETCH_ASSOC))) {
+            $course->delete();
+        }
+
+        //now the notifications
+        $messaging = new messaging();
+        $statement = DBManager::get()->prepare("
+            SELECT `seminare`.*
+            FROM `seminare`
+                INNER JOIN `config_values` ON (`config_values`.`range_id` = `seminare`.`Seminar_id` AND `config_values`.`field` = 'STUDYGROUP_EXPIRATION_DATE')
+            WHERE `config_values`.`value` >= UNIX_TIMESTAMP() - 86400 * 31
+                AND `config_values`.`value` < UNIX_TIMESTAMP() - 86400 * 30
+        ");
+        $statement->execute([
+            'last_time' => $last_result
+        ]);
+        while ($course = Course::buildExisting($statement->fetch(PDO::FETCH_ASSOC))) {
+
+        }
+    }
+}
diff --git a/lib/cronjobs/studygroup_expiration.php b/lib/cronjobs/studygroup_expiration.php
new file mode 100644
index 00000000000..25ba288b6aa
--- /dev/null
+++ b/lib/cronjobs/studygroup_expiration.php
@@ -0,0 +1,97 @@
+<?php
+/**
+ * studygroup_expiration.php - Delete expired study groups
+ *
+ * @author Rasmus Fuhse <fuhse@data-quest.de>
+ * @author Michaela Brückner <brueckner@data-quest.de>
+ * @access public
+ * @since  6.0
+ */
+
+require_once 'lib/classes/CronJob.php';
+
+class StudygroupExpirationJob extends CronJob
+{
+    /**
+     * Returns the name of the cronjob.
+     */
+    public static function getName()
+    {
+        return _('Studiengruppen aufräumen');
+    }
+
+    /**
+     * Returns the description of the cronjob.
+     */
+    public static function getDescription()
+    {
+        return _('Studiengruppen, die abgelaufen sind, werden gelöscht. Zusätzlich werden Gruppengründer:innen benachrichtigt, wenn ihre Studiengruppen in einem Monat ablaufen.');
+    }
+
+    /**
+     * Return the paremeters for this cronjob.
+     *
+     * @return Array Parameters.
+     */
+    public static function getParameters()
+    {
+        return [];
+    }
+
+    /**
+     * Executes the cronjob.
+     *
+     * @param mixed $last_result What the last execution of this cronjob
+     *                           returned.
+     * @param Array $parameters Parameters for this cronjob instance which
+     *                          were defined during scheduling.
+     */
+    public function execute($last_result, $parameters = [])
+    {
+        $statement = DBManager::get()->prepare("
+            SELECT `range_id`
+            FROM `config_values`
+            WHERE `field` = 'STUDYGROUP_EXPIRATION_DATE'
+                AND `value` > 0 AND `value` < UNIX_TIMESTAMP()
+        ");
+        $statement->execute();
+        while ($course_id = $statement->fetch(PDO::FETCH_COLUMN)) {
+            $course = Course::find($course_id);
+            $course->delete();
+        }
+
+        //now the notifications
+        $messaging = new messaging();
+        $statement = DBManager::get()->prepare("
+            SELECT `seminare`.*
+            FROM `seminare`
+                INNER JOIN `config_values` ON (`config_values`.`range_id` = `seminare`.`Seminar_id` AND `config_values`.`field` = 'STUDYGROUP_EXPIRATION_DATE')
+            WHERE `config_values`.`value` >= UNIX_TIMESTAMP() - 86400 * 31
+                AND `config_values`.`value` < UNIX_TIMESTAMP() - 86400 * 30
+        ");
+        $statement->execute([
+            'last_time' => $last_result
+        ]);
+        while ($course = Course::buildExisting($statement->fetch(PDO::FETCH_ASSOC))) {
+            foreach ($course->getTeachers() as $course_member) {
+                setTempLanguage($course_member->user_id);
+                $message = sprintf(
+                    _('Ihre Studiengruppe %s wird in einem Monat ablaufen und dann automatisch gelöscht werden. Falls Sie die Studiengruppe noch benötigen, ändern Sie in der Verwaltung der Studiengruppe das Ablaufdatum.'),
+                    $course->getFullName()
+                );
+                $subject = _('Ablauf Ihrer Studiengruppe');
+                $messaging->insert_message(
+                    $message,
+                    $course_member->user->username,
+                    '____%system%____',
+                    '',
+                    '',
+                    '',
+                    '',
+                    $subject
+                );
+                restoreLanguage();
+            }
+        }
+    }
+}
diff --git a/lib/models/Course.php b/lib/models/Course.php
index f07440ccb85..4fc32e3bb75 100644
--- a/lib/models/Course.php
+++ b/lib/models/Course.php
@@ -47,6 +47,7 @@
  * @property int $admission_disable_waitlist_move database column
  * @property int $completion database column
  * @property string|null $parent_course database column
+ * @property string|null $expires database column
  * @property SimpleORMapCollection|CourseTopic[] $topics has_many CourseTopic
  * @property SimpleORMapCollection|CourseDate[] $dates has_many CourseDate
  * @property SimpleORMapCollection|CourseExDate[] $ex_dates has_many CourseExDate
@@ -247,6 +248,20 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
             'assoc_foreign_key' => 'parent_course',
             'order_by'          => 'GROUP BY seminar_id ORDER BY VeranstaltungsNummer, Name'
         ];
+        $config['has_and_belongs_to_many']['studygroups'] = [
+            'class_name'        => Course::class,
+            'thru_table'        => 'studygroup_courses',
+            'thru_key'          => 'course_id',
+            'thru_assoc_key'    => 'studygroup_id',
+            'order_by'          => 'ORDER BY VeranstaltungsNummer, Name'
+        ];
+        $config['has_and_belongs_to_many']['connectedcourses'] = [
+            'class_name'        => Course::class,
+            'thru_table'        => 'studygroup_courses',
+            'thru_key'          => 'studygroup_id',
+            'thru_assoc_key'    => 'course_id',
+            'order_by'          => 'ORDER BY VeranstaltungsNummer, Name'
+        ];
         $config['has_many']['tools'] = [
             'class_name'        => ToolActivation::class,
             'assoc_foreign_key' => 'range_id',
@@ -270,6 +285,25 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
             'assoc_foreign_key' => 'range_id',
             'on_delete'  => 'delete',
         ];
+        $config['has_and_belongs_to_many']['tags'] = [
+            'class_name' => Tag::class,
+            'thru_table' => 'tags_relations',
+            'thru_key' => 'range_id',
+            'thru_assoc_key' => 'tag_id',
+            'order_by'    => 'ORDER BY `name` ASC',
+            'on_delete'  => 'delete',
+            'on_store'   => 'store',
+        ];
+        $config['has_many']['studygroup_proposals'] = [
+            'class_name'        => StudygroupCourseProposal::class,
+            'assoc_foreign_key' => 'studygroup_id',
+            'on_delete'         => 'delete',
+        ];
+        $config['has_many']['course_proposals'] = [
+            'class_name'        => StudygroupCourseProposal::class,
+            'assoc_foreign_key' => 'course_id',
+            'on_delete'         => 'delete',
+        ];
 
         $config['default_values']['lesezugriff'] = 1;
         $config['default_values']['schreibzugriff'] = 1;
diff --git a/lib/models/StudiengangTeil.php b/lib/models/StudiengangTeil.php
index 13bf7ce37a4..b4a71ebefdb 100644
--- a/lib/models/StudiengangTeil.php
+++ b/lib/models/StudiengangTeil.php
@@ -70,6 +70,15 @@ class StudiengangTeil extends ModuleManagementModelTreeItem
             'on_delete' => 'delete',
             'on_store' => 'store'
         ];
+        $config['has_and_belongs_to_many']['studygroups'] = [
+            'class_name'     => Course::class,
+            'thru_table'     => 'studygroup_stgteil',
+            'thru_key'       => 'stgteil_id',
+            'thru_assoc_key' => 'studygroup_id',
+            'order_by'       => 'ORDER BY `name` ASC',
+            'on_delete'      => 'delete',
+            'on_store'       => 'store',
+        ];
 
 
         $config['additional_fields']['count_versionen']['get'] =
@@ -479,4 +488,21 @@ class StudiengangTeil extends ModuleManagementModelTreeItem
         }, self::getAssignedFachbereiche('name', 'ASC', ['mvv_stgteil.stgteil_id' => $this->getId()]));
     }
 
+    public function addStudygroup(Course $course)
+    {
+        if (in_array($course->status, studygroup_sem_types())) {
+            if (!StudygroupStgteil::findOneBySQL('`studygroup_id` = ? AND `stgteil_id` = ?', [$course->id, $this->id])) {
+                $connection = StudygroupStgteil::create([
+                    'studygroup_id' => $course->id,
+                    'stgteil_id' => $this->id
+                ]);
+            }
+        }
+    }
+
+    public function removeStudygroup(Course $course)
+    {
+        StudygroupStgteil::deleteBySQL('`studygroup_id` = ? AND `stgteil_id` = ?', [$course->id, $this->id]);
+    }
+
 }
diff --git a/lib/models/StudygroupCourse.php b/lib/models/StudygroupCourse.php
new file mode 100644
index 00000000000..14450acc2a6
--- /dev/null
+++ b/lib/models/StudygroupCourse.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @license GPL2 or any later version
+ *
+ * @property string $id alias column for tag_hash
+ * @property string $studygroup_id database column
+ * @property string $course_id database column
+ * @property int $mkdate database column
+ *
+ */
+class StudygroupCourse extends SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'studygroup_courses';
+        $config['belongs_to']['course'] = [
+            'class_name'        => Course::class,
+            'foreign_key'       => 'course_id',
+            'assoc_foreign_key' => 'seminar_id',
+        ];
+        $config['belongs_to']['studygroup'] = [
+            'class_name'        => Course::class,
+            'foreign_key'       => 'studygroup_id',
+            'assoc_foreign_key' => 'seminar_id',
+        ];
+        parent::configure($config);
+    }
+}
diff --git a/lib/models/StudygroupCourseProposal.php b/lib/models/StudygroupCourseProposal.php
new file mode 100644
index 00000000000..b746fbcbf79
--- /dev/null
+++ b/lib/models/StudygroupCourseProposal.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @license GPL2 or any later version
+ *
+ * @property string $id alias column for tag_hash
+ * @property string $studygroup_id database column
+ * @property string $course_id database column
+ * @property string $proposed_from database column 'course' or 'studygroup'
+ * @property string $user_id database column
+ * @property int $mkdate database column
+ *
+ */
+class StudygroupCourseProposal extends SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'studygroup_courses_proposals';
+        $config['belongs_to']['course'] = [
+            'class_name'        => Course::class,
+            'foreign_key'       => 'course_id',
+            'assoc_foreign_key' => 'seminar_id',
+        ];
+        $config['belongs_to']['studygroup'] = [
+            'class_name'        => Course::class,
+            'foreign_key'       => 'studygroup_id',
+            'assoc_foreign_key' => 'seminar_id',
+        ];
+        $config['belongs_to']['user'] = [
+            'class_name'        => User::class,
+            'foreign_key'       => 'user_id'
+        ];
+        parent::configure($config);
+    }
+
+}
diff --git a/lib/models/StudygroupStgteil.php b/lib/models/StudygroupStgteil.php
new file mode 100644
index 00000000000..7a0063eb685
--- /dev/null
+++ b/lib/models/StudygroupStgteil.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL2 or any later version
+ *
+ * @property string $id alias column for tag_hash
+ * @property string $studygroup_id database column
+ * @property string $stgteil_id database column
+ * @property int $mkdate database column
+ *
+ */
+class StudygroupStgteil extends SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'studygroup_stgteil';
+        parent::configure($config);
+    }
+}
diff --git a/lib/models/Tag.php b/lib/models/Tag.php
new file mode 100644
index 00000000000..8614e5a7572
--- /dev/null
+++ b/lib/models/Tag.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @license GPL2 or any later version
+ *
+ * @property string $id alias column for tag_hash
+ * @property string $name database column
+ * @property int $active database column
+ * @property int $chdate database column
+ * @property int $mkdate database column
+ *
+ */
+class Tag extends SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'tags';
+        $config['has_many']['related_objects'] = [
+            'class_name' => TagRelation::class,
+            'assoc_foreign_key' => 'tag_id',
+            'order_by' => 'ORDER BY `range_type` ASC, `range_id` ASC',
+            'on_store' => 'store',
+            'on_delete' => 'delete'
+        ];
+        parent::configure($config);
+    }
+
+    public static function isActive($tag_name)
+    {
+        $tag_name = self::normalizeName($tag_name);
+        $tag = static::findOneByName($tag_name);
+        return $tag === false || $tag['active'] > 0;
+    }
+
+    public static function getByRange($range_id, $range_type)
+    {
+        return Tag::findBySQL('INNER JOIN `tags_relations` ON (`tags_relations`.`tag_id` = `tags`.`id`)
+            WHERE `tags_relations`.`range_id` = :range_id
+                AND `tags_relations`.`range_type` = :range_type AND `tags`.`active` = 1 ORDER BY `tags`.`name` ASC', [
+                    'range_id' => $range_id,
+                    'range_type' => $range_type
+        ]);
+    }
+
+    public static function normalizeName($name)
+    {
+        $name = mb_strtolower($name);
+        $name = str_replace(
+            [' ', "\n", '|', '#'],
+            ['-', '-',  '-', ''],
+            $name
+        );
+        return $name;
+    }
+}
diff --git a/lib/models/TagRelation.php b/lib/models/TagRelation.php
new file mode 100644
index 00000000000..168b7d3f676
--- /dev/null
+++ b/lib/models/TagRelation.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @license GPL2 or any later version
+ *
+ * @property string $id alias column for tag_hash
+ * @property int $tag_id database column
+ * @property string $range_id database column
+ * @property string $range_type database column
+ * @property int $mkdate database column
+ */
+class TagRelation extends SimpleORMap
+{
+    protected static function configure($config = [])
+    {
+        $config['db_table'] = 'tags_relations';
+        parent::configure($config);
+    }
+}
diff --git a/lib/modules/CoreAdmin.php b/lib/modules/CoreAdmin.php
index 4c0cc3c1fcd..aded3aeb8dc 100644
--- a/lib/modules/CoreAdmin.php
+++ b/lib/modules/CoreAdmin.php
@@ -93,6 +93,11 @@ class CoreAdmin extends CorePlugin implements StudipModule
             $item->setDescription(_('Vorlagen zur Erhebung weiterer Angaben von Teilnehmenden auswählen.'));
             $navigation->addSubNavigation('additional_data', $item);
 
+            $item = new Navigation(_('Verknüpfte Studiengruppen'), 'dispatch.php/course/connectedstudygroups');
+            $item->setImage(Icon::create('studygroup'));
+            $item->setDescription(_('Studiengruppen verknüpfen bzw. verwalten'));
+            $navigation->addSubNavigation('connectedstudygroups', $item);
+
         }  // endif modules only seminars
 
         if (Config::get()->VOTE_ENABLE) {
diff --git a/lib/modules/CoreStudygroupAdmin.php b/lib/modules/CoreStudygroupAdmin.php
index acf59737330..5c74996eca9 100644
--- a/lib/modules/CoreStudygroupAdmin.php
+++ b/lib/modules/CoreStudygroupAdmin.php
@@ -39,6 +39,7 @@ class CoreStudygroupAdmin extends CorePlugin implements StudipModule
         $navigation->addSubNavigation('contentmodules', new Navigation(_('Werkzeuge'), "dispatch.php/course/contentmodules?cid={$course_id}"));
         $navigation->addSubNavigation('main', new Navigation(_('Verwaltung'), "dispatch.php/course/studygroup/edit/?cid={$course_id}"));
         $navigation->addSubNavigation('avatar', new Navigation(_(' Studiengruppenbild'), "dispatch.php/course/studygroup/avatar?cid={$course_id}"));
+        $navigation->addSubNavigation('connectedcourses', new Navigation(_('Verknüpfte Veranstaltungen'), "dispatch.php/course/connectedcourses?cid={$course_id}"));
 
         if (!$GLOBALS['perm']->have_perm('admin') && Config::get()->VOTE_ENABLE) {
             $item = new Navigation(_('Fragebögen'), 'dispatch.php/questionnaire/courseoverview');
diff --git a/lib/modules/MyStudygroupsWidget.php b/lib/modules/MyStudygroupsWidget.php
new file mode 100644
index 00000000000..170941914b1
--- /dev/null
+++ b/lib/modules/MyStudygroupsWidget.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * My Study group widget. Displays a list of own study groups
+ *
+ * @author
+ * @license GPL2 or any later version
+ * @since   Stud.IP 6.0
+ */
+class MyStudygroupsWidget extends CorePlugin implements PortalPlugin
+{
+    public function getPluginName()
+    {
+        return _('Meine Studiengruppen');
+    }
+
+    public function getMetadata()
+    {
+        return [
+            'description' => _('Dieses Widget zeigt eine Liste Ihrer Studiengruppen an.')
+        ];
+    }
+
+    public function getPortalTemplate()
+    {
+        $template = $GLOBALS['template_factory']->open('start/my_studygroups');
+
+        $controller = app(\Trails\Dispatcher::class)->load_controller('my_studygroups');
+        $response = $controller->relayWithRedirect('my_studygroups/index/true');
+        $template->content = $response->body;
+
+        $navigation = new Navigation('', 'dispatch.php/course/wizard?studygroup=1');
+        $navigation->setImage(Icon::create('add', Icon::ROLE_CLICKABLE, ['title' => _('Neue Studiengruppe anlegen')]));
+        $navigation->setLinkAttributes(['data-dialog' => 'reload-on-close']);
+        $template->icons = [$navigation];
+
+        return $template;
+    }
+}
diff --git a/lib/modules/StudygroupWidget.php b/lib/modules/StudygroupWidget.php
new file mode 100644
index 00000000000..beddd4f777f
--- /dev/null
+++ b/lib/modules/StudygroupWidget.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Study group widget. Displays a list of possibly interesting study groups
+ *
+ * @author
+ * @license GPL2 or any later version
+ * @since   Stud.IP 6.0
+ */
+
+class StudygroupWidget extends CorePlugin implements PortalPlugin
+{
+    public function getPluginName()
+    {
+        return _('Für dich vorgeschlagene Studiengruppen');
+    }
+
+    public function getMetadata()
+    {
+        return [
+            'description' => _('Dieses Widget zeigt eine Liste von Vorschlägen interessanter Studiengruppen an.')
+        ];
+    }
+
+    public function getPortalTemplate()
+    {
+        $template = $GLOBALS['template_factory']->open('start/studygroups');
+
+        $controller = app(\Trails\Dispatcher::class)->load_controller('my_studygroups');
+        $response = $controller->relayWithRedirect('my_studygroups/proposals');
+        $template->proposals = $response->body;
+
+        $navigation = new Navigation('', 'dispatch.php/course/wizard?studygroup=1');
+        $navigation->setImage(Icon::create('add', Icon::ROLE_CLICKABLE, ['title' => _('Neue Studiengruppe anlegen')]));
+        $navigation->setLinkAttributes(['data-dialog' => 'reload-on-close']);
+        $template->icons = [$navigation];
+
+        return $template;
+    }
+}
diff --git a/lib/navigation/AdminNavigation.php b/lib/navigation/AdminNavigation.php
index 62796d7abcf..fe31b72015e 100644
--- a/lib/navigation/AdminNavigation.php
+++ b/lib/navigation/AdminNavigation.php
@@ -169,7 +169,13 @@ class AdminNavigation extends Navigation
                     'dispatch.php/admin/login_style'
                 )
             );
-
+            $navigation->addSubNavigation(
+                'tags',
+                new Navigation(
+                    _('Schlagwortverwaltung'),
+                    'dispatch.php/admin/tags/index'
+                )
+            );
         }
 
         $this->addSubNavigation('locations', $navigation);
diff --git a/resources/assets/javascripts/lib/global_search.js b/resources/assets/javascripts/lib/global_search.js
index 19154a1b0e8..4c46c775bf7 100644
--- a/resources/assets/javascripts/lib/global_search.js
+++ b/resources/assets/javascripts/lib/global_search.js
@@ -131,6 +131,13 @@ const GlobalSearch = {
                             .appendTo(details);
                     }
 
+                    // Details: Descriptional text
+                    if (result.found_tag !== null) {
+                        $('<div class="globalsearch-result-description">')
+                            .html(result.found_tag)
+                            .appendTo(details);
+                    }
+
                     // Details: Additional information
                     if (result.additional !== null) {
                         $('<div class="globalsearch-result-additional">')
diff --git a/resources/assets/javascripts/lib/search.js b/resources/assets/javascripts/lib/search.js
index ff3a98d8c46..5c0e3e84b11 100644
--- a/resources/assets/javascripts/lib/search.js
+++ b/resources/assets/javascripts/lib/search.js
@@ -245,6 +245,13 @@ const Search = {
                 .appendTo(details);
         }
 
+        // Details: Tags
+        if (result.found_tag !== null) {
+            $('<div class="search-result-description">')
+                .html(result.found_tag)
+                .appendTo(details);
+        }
+
         if (result.dates !== null) {
             $('<div class="search-result-dates">')
                 .html(result.dates)
diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss
index 0a707c8f17c..5c8f3a4d68c 100644
--- a/resources/assets/stylesheets/scss/forms.scss
+++ b/resources/assets/stylesheets/scss/forms.scss
@@ -562,6 +562,13 @@ form.default {
             margin-top: 0.5ex;
         }
     }
+    .multiquicksearch > li {
+        display: flex;
+        align-items: center;
+        a.delete_item {
+            margin-left: 5px;
+        }
+    }
 }
 
 form.narrow {
diff --git a/resources/assets/stylesheets/scss/studygroup.scss b/resources/assets/stylesheets/scss/studygroup.scss
index 8eb0c4ce81b..18cde0008b1 100644
--- a/resources/assets/stylesheets/scss/studygroup.scss
+++ b/resources/assets/stylesheets/scss/studygroup.scss
@@ -52,3 +52,107 @@ ul.studygroup-gallery {
     }
   }
 }
+
+.connectedcourses {
+    .teaser {
+        font-size: 24px;
+    }
+    .connectedstudygroups-empty-background {
+        @include empty-placeholder-image('network2', false);
+    }
+    footer {
+        background-color: var(--content-color-20);
+        border-top: 1px solid var(--brand-color-darker);
+        clear: both;
+        padding: 0;
+        height: 58px;
+    }
+}
+
+.studip-tiles {
+    display: flex;
+    align-items: stretch;
+    flex-wrap: wrap;
+    > * {
+        display: flex;
+        flex-direction: column;
+        width: 270px;
+        border: 1px solid var(--content-color-20);
+        padding: 10px;
+        margin-bottom: 10px;
+        align-items: stretch;
+        margin-right: 10px;
+        > * {
+            display: flex;
+            flex-direction: row;
+            margin-bottom: 10px;
+
+            &.with-action-menu {
+                justify-content: space-between;
+                > * {
+                    margin-right: 0px;
+                    &:first-child {
+                        display: flex;
+                        flex-direction: row;
+                        > * {
+                            margin-right: 10px;
+                        }
+                    }
+                }
+            }
+            .actions {
+                text-align: right;
+            }
+            &:last-child {
+                margin-bottom: 0px;
+            }
+            > * {
+                margin-right: 10px;
+            }
+        }
+    }
+}
+
+.studip-contents-overview-teaser {
+    max-width: 782px;
+    background-color: var(--content-color-20);
+    background-image: url('#{$image-path}/courseware-keyvisual-negative.svg');
+    background-repeat: no-repeat;
+    background-size: 196px;
+    background-position-y: 50%;
+    background-position-x: 24px;
+    padding: 24px;
+    margin-bottom: 10px;
+
+    .teaser-content {
+        padding-left: 220px;
+
+        header {
+            font-size: 1.5em;
+            margin-bottom: 0.5em;
+        }
+    }
+}
+
+.responsive-display {
+    .cw-contents-overview-teaser {
+        max-width: 782px;
+        background-size: 60%;
+        background-position-y: 24px;
+        background-position-x: 50%;
+        padding: 24px;
+        margin-bottom: 10px;
+
+        .teaser-content {
+            padding-top: 28%;
+            padding-left: 0;
+            text-align: justify;
+
+            header {
+                font-size: 1.5em;
+                margin: 1em 0 0.5em 0;
+                text-align: center;
+            }
+        }
+    }
+}
diff --git a/resources/assets/stylesheets/scss/tables.scss b/resources/assets/stylesheets/scss/tables.scss
index 2c05362995c..32a26c29038 100644
--- a/resources/assets/stylesheets/scss/tables.scss
+++ b/resources/assets/stylesheets/scss/tables.scss
@@ -623,10 +623,14 @@ table.default {
         font-size: $font-size-base;
         border-left: 1px solid var(--color--table-border);
         margin-bottom: -2px;
-        min-height: 26px;
+        min-height: 30px;
         padding-bottom: 3px;
         padding-left: 0.5em;
         padding-top: 4px;
+        input[type=text] {
+            width: auto; // otherwise it can be 100%, so there is no space left for other parts of the actions-area
+            max-height: 30px;
+        }
     }
 
     td.actions, th.actions {
diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js
index d9bb1b79aff..25246429e5a 100644
--- a/resources/vue/base-components.js
+++ b/resources/vue/base-components.js
@@ -10,6 +10,7 @@ const BaseComponents = {
     EditableList: defineAsyncComponent(() => import('./components/EditableList.vue')),
     FileUpload: defineAsyncComponent(() => import('./components/form_inputs/FileUpload.vue')),
     I18nTextarea: defineAsyncComponent(() => import("./components/I18nTextarea.vue")),
+    Multiquicksearch: defineAsyncComponent(() => import('./components/Multiquicksearch.vue')),
     Multiselect: defineAsyncComponent(() => import('./components/Multiselect.vue')),
     MyCoursesColouredTable: defineAsyncComponent(() => import('./components/form_inputs/MyCoursesColouredTable.vue')),
     Quicksearch: defineAsyncComponent(() => import('./components/Quicksearch.vue')),
diff --git a/resources/vue/components/Multiquicksearch.vue b/resources/vue/components/Multiquicksearch.vue
new file mode 100644
index 00000000000..19d8068a462
--- /dev/null
+++ b/resources/vue/components/Multiquicksearch.vue
@@ -0,0 +1,96 @@
+<template>
+    <div>
+        <ul class="clean multiquicksearch">
+            <li v-for="(item, index) in items" :key="index">
+                <quicksearch :name="name"
+                             :searchtype="searchtype"
+                             :autocomplete="autocomplete"
+                             :modelValue="autocomplete ? item.item_name : item.item_id"
+                             :needle="item.item_name"
+                             :ref="'qs_' + index"
+                             @update:modelValue="(new_id, new_item_name) => editItem(new_id, new_item_name, index)"></quicksearch>
+                <a href="" class="delete_item" @click.prevent="deleteItem(index)">
+                    <studip-icon shape="trash" class="text-bottom"></studip-icon>
+                </a>
+            </li>
+        </ul>
+        <a href="#" @click.prevent="addItem">
+            <studip-icon shape="add" class="text-bottom"></studip-icon>
+            {{ addlabel }}
+        </a>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'multiquicksearch',
+    inheritAttrs: false,
+    props: {
+        name: {
+            type: String,
+            required: false
+        },
+        value: {
+            type: Object,
+            required: false,
+            default: []
+        },
+        searchtype: {
+            type: String,
+            required: true
+        },
+        autocomplete: {
+            type: Boolean,
+            required: false,
+            default: false
+        },
+        addlabel: {
+            type: String,
+            required: false,
+            default: ""
+        }
+    },
+    data () {
+        return {
+            items: []
+        };
+    },
+    mounted () {
+        for (let i in this.value) {
+            this.items.push({
+                item_id: this.autocomplete ? this.value[i] : i,
+                item_name: this.value[i]
+            });
+        }
+    },
+    watch: {
+        items: {
+            handler(newValue, oldValue) {
+                let new_val = {};
+                for (let i in newValue) {
+                    new_val[newValue[i].item_id] = newValue[i].item_name;
+                }
+                this.$emit('update:modelValue', new_val);
+            },
+            deep: true
+        }
+    },
+    methods: {
+        addItem: function () {
+            this.items.push({
+                item_id: '',
+                item_name: ''
+            });
+        },
+        editItem: function (item_id, item_name, index) {
+            this.items[index].item_id = item_id;
+            this.items[index].item_name = item_name;
+        },
+        deleteItem: function (index) {
+            if (this.items.length > 0) {
+                this.items.splice(index, 1);
+            }
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/MyCoursesTables.vue b/resources/vue/components/MyCoursesTables.vue
index e3b70ea59fb..30417351871 100644
--- a/resources/vue/components/MyCoursesTables.vue
+++ b/resources/vue/components/MyCoursesTables.vue
@@ -141,11 +141,6 @@ export default {
                 if (!this.isChild(course)) {
                     courses.push(course);
                 }
-                if (this.isParent(course)) {
-                    this.getCourses(course.children).forEach(c => {
-                        courses.push(c);
-                    });
-                }
             });
 
             return courses;
diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/Quicksearch.vue
index dcdd31dfb73..1a61513a77f 100644
--- a/resources/vue/components/Quicksearch.vue
+++ b/resources/vue/components/Quicksearch.vue
@@ -8,6 +8,7 @@
                :name="autocomplete ? name : null"
                v-model="inputValue"
                autocomplete="off"
+               ref="text_input"
                @blur="reset()"
                @keydown.up="selectUp"
                @keydown.down="selectDown"
diff --git a/templates/forms/multiquicksearch_input.php b/templates/forms/multiquicksearch_input.php
new file mode 100644
index 00000000000..acb28ae2b20
--- /dev/null
+++ b/templates/forms/multiquicksearch_input.php
@@ -0,0 +1,17 @@
+<div class="formpart" data-form-input-for="<?= htmlReady($name) ?>">
+    <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>">
+        <span class="textlabel">
+            <?= htmlReady($this->title) ?>
+        </span>
+        <? if ($this->required) : ?>
+            <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+        <? endif ?>
+    </label>
+    <multiquicksearch name="<?= htmlReady($name) ?>"
+                 v-model="<?= htmlReady($name) ?>"
+                 <?= ($required ? 'required aria-required="true"' : '') ?>
+                 :value="<?= htmlReady(json_encode($value)) ?>"
+                 id="<?= $id ?>"
+                 <?= $attributes ?>>
+    </multiquicksearch>
+</div>
diff --git a/templates/forms/quicksearch_input.php b/templates/forms/quicksearch_input.php
index 6fbaff15b0f..46680a1f766 100644
--- a/templates/forms/quicksearch_input.php
+++ b/templates/forms/quicksearch_input.php
@@ -10,6 +10,7 @@
     <span>
         <quicksearch value="<?= htmlReady($value) ?>"
                      name="<?= htmlReady($name) ?>"
+                     @update:modelValue="(new_id, new_item_name) => { console.log(new_id); this.<?= htmlReady($name) ?> = new_id; }"
                      id="<?= $id ?>"
                      <?= ($this->required ? 'required aria-required="true"' : '') ?>
                      <?= $attributes ?>>
diff --git a/templates/header.php b/templates/header.php
index fda7471171e..2f1d50992f4 100644
--- a/templates/header.php
+++ b/templates/header.php
@@ -317,7 +317,11 @@ if ($navigation) {
                         <? endif ?>
                     <? endif ?>
                     <? if (Context::isCourse()) : ?>
-                        <?= CourseAvatar::getAvatar(Context::get()->id)->getImageTag(Avatar::NORMAL, ['class' => 'context-avatar']) ?>
+                        <? if (Context::get()->isStudygroup()) : ?>
+                            <?= StudygroupAvatar::getAvatar(Context::getId())->getImageTag(Avatar::NORMAL, ['class' => 'context-avatar']) ?>
+                        <? else : ?>
+                            <?= CourseAvatar::getAvatar(Context::getId())->getImageTag(Avatar::NORMAL, ['class' => 'context-avatar']) ?>
+                        <? endif ?>
                         <span class="course-type"><?= htmlReady(Context::get()->getFullName('type')) ?>:</span> <span class="course-name"><?= htmlReady(Context::get()->getFullName('name')) ?></span>
                         <? if ($GLOBALS['user']->config->SHOWSEM_ENABLE && !Context::get()->isOpenEnded()): ?>
                             <span class="course-semester">(<?= htmlReady(Context::get()->getTextualSemester()) ?>)</span>
diff --git a/templates/start/my_studygroups.php b/templates/start/my_studygroups.php
new file mode 100644
index 00000000000..f2770cb03c4
--- /dev/null
+++ b/templates/start/my_studygroups.php
@@ -0,0 +1,3 @@
+<article class="studip">
+    <?= $content ?>
+</article>
diff --git a/templates/start/studygroups.php b/templates/start/studygroups.php
new file mode 100644
index 00000000000..aa7b6e61480
--- /dev/null
+++ b/templates/start/studygroups.php
@@ -0,0 +1,3 @@
+<article class="studip">
+    <?= $proposals ?>
+</article>
diff --git a/tests/_support/_generated/JsonapiTesterActions.php b/tests/_support/_generated/JsonapiTesterActions.php
index 9cf61187d47..df933073ab4 100644
--- a/tests/_support/_generated/JsonapiTesterActions.php
+++ b/tests/_support/_generated/JsonapiTesterActions.php
@@ -1,4 +1,4 @@
-<?php  //[STAMP] 9a0bbbffd781acee72848c128782d162
+<?php  //[STAMP] d235e554f01db85e147592e96c21bd04
 // phpcs:ignoreFile
 namespace _generated;
 
-- 
GitLab