From 9c4acc181641fe862f7b98276c00dc0f681835d2 Mon Sep 17 00:00:00 2001
From: Jan-Hendrik Willms <tleilax+studip@gmail.com>
Date: Tue, 25 Jun 2024 12:46:33 +0000
Subject: [PATCH] allow copying of participants and groups when copying a
 course, fixes #3441

Closes #3441

Merge request studip/studip!2873
---
 app/controllers/course/statusgroups.php       | 158 ++++++++++----
 app/controllers/course/wizard.php             |  36 ++-
 app/views/course/statusgroups/copy.php        |  36 +++
 app/views/course/statusgroups/index.php       |  24 +-
 app/views/course/wizard/summary.php           |  36 ++-
 lib/classes/AuthenticatedController.php       |  12 +-
 lib/classes/QuickSearch.php                   |   2 +-
 .../coursewizardsteps/BasicDataWizardStep.php | 205 ++++++++++++++----
 lib/classes/searchtypes/StandardSearch.php    |   2 +-
 lib/models/Statusgruppen.php                  |  74 +++++--
 resources/assets/javascripts/lib/dialog.js    |   4 +
 11 files changed, 446 insertions(+), 143 deletions(-)
 create mode 100644 app/views/course/statusgroups/copy.php

diff --git a/app/controllers/course/statusgroups.php b/app/controllers/course/statusgroups.php
index 6f96299409f..e9b2c431aac 100644
--- a/app/controllers/course/statusgroups.php
+++ b/app/controllers/course/statusgroups.php
@@ -25,8 +25,6 @@ class Course_StatusgroupsController extends AuthenticatedController
     {
         parent::before_filter($action, $args);
 
-        global $perm;
-
         checkObject();
         checkObjectModule("participants");
 
@@ -92,7 +90,7 @@ class Course_StatusgroupsController extends AuthenticatedController
         $membercounts = array_column(DBManager::get()->fetchAll(
             "SELECT u.`statusgruppe_id`, COUNT(u.`user_id`) as membercount
                 FROM `statusgruppen` s
-    	            JOIN `statusgruppe_user` u USING (`statusgruppe_id`)
+                JOIN `statusgruppe_user` u USING (`statusgruppe_id`)
                 WHERE s.`range_id` = ?
                 GROUP BY `statusgruppe_id`
                 ORDER BY s.`position` ASC, s.`name` ASC",
@@ -253,6 +251,7 @@ class Course_StatusgroupsController extends AuthenticatedController
                     Icon::create('add')
                 )->asDialog('size=auto');
             }
+            
             if (Config::get()->EXPORT_ENABLE) {
                 $export = new ExportWidget();
                 $export->addLink(
@@ -297,6 +296,7 @@ class Course_StatusgroupsController extends AuthenticatedController
                 Icon::create('arr_1down')
             );
         }
+        
         $sidebar->addWidget($actions);
     }
 
@@ -587,16 +587,18 @@ class Course_StatusgroupsController extends AuthenticatedController
             $group_id,
             Request::get('name'),
             $position,
-            $this->course_id, Request::int('size', 0),
+            $this->course_id,
+            Request::int('size', 0),
             $selfassign,
-            Request::int('selfassign', 0) !== 0
+            $selfassign
                 ? strtotime(Request::get('selfassign_start', 'now'))
                 : 0,
-            Request::int('selfassign', 0) && Request::get('selfassign_end')
+            $selfassign && Request::get('selfassign_end')
                 ? strtotime(Request::get('selfassign_end'))
                 : 0,
-            Request::int('makefolder', 0),
-            Request::getArray('dates')
+            Request::bool('makefolder', false),
+            Request::getArray('dates'),
+            Request::bool('blubber', false)
         );
 
         $group->description = trim(Request::get('description')) ?: null;
@@ -614,22 +616,6 @@ class Course_StatusgroupsController extends AuthenticatedController
             ));
         }
 
-        $thread = BlubberStatusgruppeThread::findByStatusgruppe_id($group->id);
-        if (Request::get("blubber") && !$thread) {
-            $thread = new BlubberStatusgruppeThread();
-            $thread['context_type'] = "course";
-            $thread['context_id'] = $this->course_id;
-            $thread['user_id'] = $GLOBALS['user']->id;
-            $thread['external_contact'] = 0;
-            $thread['visible_in_stream'] = 1;
-            $thread['display_class'] = "BlubberStatusgruppeThread";
-            $thread['commentable'] = 1;
-            $thread['metadata'] = ['statusgruppe_id' => $group->id];
-            $thread->store();
-        } elseif(!Request::get("blubber") && $thread) {
-            $thread->delete();
-        }
-
         $this->relocate('course/statusgroups');
     }
 
@@ -842,13 +828,17 @@ class Course_StatusgroupsController extends AuthenticatedController
                 $numbering = Request::int('startnumber', 1);
             }
             for ($i = 0 ; $i < Request::int('number') ; $i++) {
-                Statusgruppen::createOrUpdate('', Request::get('prefix').' '.
-                    $numbering++,
-                    null, $this->course_id, Request::int('size', 0),
+                Statusgruppen::createOrUpdate(
+                    null,
+                    Request::get('prefix') . ' ' . $numbering++,
+                    null,
+                    $this->course_id,
+                    Request::int('size', 0),
                     Request::int('selfassign', 0) + Request::int('exclusive', 0),
                     strtotime(Request::get('selfassign_start', 'now')),
                     strtotime(Request::get('selfassign_end', 0)),
-                    Request::int('makefolder', 0));
+                    Request::bool('makefolder', false)
+                );
                 $counter++;
             }
 
@@ -868,12 +858,16 @@ class Course_StatusgroupsController extends AuthenticatedController
                     })->orderBy('priority');
 
                     foreach ($topics as $t) {
-                        $group = Statusgruppen::createOrUpdate('', _('Thema:') . ' ' . $t->title,
-                            null, $this->course_id, Request::int('size', 0),
+                        $group = Statusgruppen::createOrUpdate(
+                            null,
+                            _('Thema:') . ' ' . $t->title,
+                            null,
+                            $this->course_id,
+                            Request::int('size', 0),
                             Request::int('selfassign', 0) + Request::int('exclusive', 0),
                             strtotime(Request::get('selfassign_start', 'now')),
                             strtotime(Request::get('selfassign_end', 0)),
-                            Request::int('makefolder', 0)
+                            Request::bool('makefolder', false)
                         );
 
                         // Connect group to dates that are assigned to the given topic.
@@ -920,12 +914,17 @@ class Course_StatusgroupsController extends AuthenticatedController
                             }
                         }
 
-                        $group = Statusgruppen::createOrUpdate('', $name,
-                            null, $this->course_id, Request::int('size', 0),
+                        $group = Statusgruppen::createOrUpdate(
+                            null,
+                            $name,
+                            null,
+                            $this->course_id,
+                            Request::int('size', 0),
                             Request::int('selfassign', 0) + Request::int('exclusive', 0),
                             strtotime(Request::get('selfassign_start', 'now')),
                             strtotime(Request::get('selfassign_end', 0)),
-                            Request::int('makefolder', 0));
+                            Request::bool('makefolder', false)
+                        );
 
                         // Connect group to dates that are assigned to the given cycle.
                         foreach ($c->dates as $d) {
@@ -952,12 +951,17 @@ class Course_StatusgroupsController extends AuthenticatedController
                             $name .= ' (' . $room . ')';
                         }
 
-                        $group = Statusgruppen::createOrUpdate('', $name,
-                            $counter + 1, $this->course_id, Request::int('size', 0),
+                        $group = Statusgruppen::createOrUpdate(
+                            null,
+                            $name,
+                            $counter + 1,
+                            $this->course_id,
+                            Request::int('size', 0),
                             Request::int('selfassign', 0) + Request::int('exclusive', 0),
                             strtotime(Request::get('selfassign_start', 'now')),
                             strtotime(Request::get('selfassign_end', 0)),
-                            Request::int('makefolder', 0));
+                            Request::bool('makefolder', false)
+                        );
 
                         $d->statusgruppen->append($group);
                         $d->store();
@@ -974,12 +978,17 @@ class Course_StatusgroupsController extends AuthenticatedController
                         CourseMember::findByCourseAndStatus($this->course_id, 'dozent'))->orderBy('position');
 
                     foreach ($lecturers as $l) {
-                        Statusgruppen::createOrUpdate('', $l->getUserFullname('full'),
-                            null, $this->course_id, Request::int('size', 0),
+                        Statusgruppen::createOrUpdate(
+                            null,
+                            $l->getUserFullname('full'),
+                            null,
+                            $this->course_id,
+                            Request::int('size', 0),
                             Request::int('selfassign', 0) + Request::int('exclusive', 0),
                             strtotime(Request::get('selfassign_start', 'now')),
                             strtotime(Request::get('selfassign_end', 0)),
-                            Request::int('makefolder', 0));
+                            Request::bool('makefolder', false)
+                        );
                         $counter++;
                     }
 
@@ -1012,9 +1021,11 @@ class Course_StatusgroupsController extends AuthenticatedController
 
         // Actions for selected groups.
         if (Request::submitted('batch_groups')) {
-            if ($groups = Request::getArray('groups')) {
+            $groups = Request::getArray('groups');
+            if ($groups) {
                 $this->groups = SimpleCollection::createFromArray(
-                    Statusgruppen::findMany($groups))->orderBy('position, name');
+                    Statusgruppen::findMany($groups)
+                )->orderBy('position, name');
                 switch (Request::option('groups_action')) {
                     case 'edit_size':
                         PageLayout::setTitle(_('Gruppengröße bearbeiten'));
@@ -1097,6 +1108,10 @@ class Course_StatusgroupsController extends AuthenticatedController
                             )
                         );
                         break;
+                    case 'copy':
+                        $this->keepRequest();
+                        $this->redirect($this->copyURL());
+                        return;
                     case 'delete':
                         PageLayout::setTitle(_('Gruppe(n) löschen?'));
                         $this->askdelete = true;
@@ -1232,10 +1247,17 @@ class Course_StatusgroupsController extends AuthenticatedController
         }
 
         foreach ($groups as $g) {
-            Statusgruppen::createOrUpdate($g->id, $g->name,
-                $g->position, $this->course_id, $g->size,
-                $selfassign, $selfassign_start, $selfassign_end,
-                false);
+            Statusgruppen::createOrUpdate(
+                $g->id,
+                $g->name,
+                $g->position,
+                $this->course_id,
+                $g->size,
+                $selfassign,
+                $selfassign_start,
+                $selfassign_end,
+                false
+            );
         }
         PageLayout::postSuccess(_('Die Einstellungen der ausgewählten Gruppen wurden gespeichert.'));
         $this->relocate('course/statusgroups');
@@ -1503,4 +1525,48 @@ class Course_StatusgroupsController extends AuthenticatedController
 
         $this->group = $group;
     }
+
+    public function copy_action(): void
+    {
+        PageLayout::setTitle(_('Gruppen in andere Veranstaltung kopieren'));
+
+        $this->group_ids = Request::optionArray('groups');
+
+        $this->search = new MyCoursesSearch('Seminar_id', User::findCurrent()->perms, [
+            ':userid' => User::findCurrent()->id,
+            ':exclude' => [$this->course_id],
+        ]);
+
+        $this->response->add_header('X-Dialog-Size', 'medium');
+    }
+
+    public function do_copy_action(): void
+    {
+        if (!Request::isPost()) {
+            throw new MethodNotAllowedException();
+        }
+
+        $target_course_id = Request::option('course_id');
+        $target_course = Course::find($target_course_id);
+
+        BasicDataWizardStep::copyParticipantsAndGroups(
+            $target_course,
+            $this->course_id,
+            false,
+            true,
+            Request::bool('copy_members', false),
+            Request::optionArray('group_ids')
+        );
+
+        PageLayout::postSuccess(sprintf(
+            _('Die Gruppen wurden in die Veranstaltung %s kopiert.'),
+            sprintf(
+                '<a href="%s">%s</a>',
+                URLHelper::getLink('seminar_main.php', ['auswahl' => $target_course_id], true),
+                htmlReady($target_course->getFullName())
+            ),
+        ));
+
+        $this->redirect($this->indexURL());
+    }
 }
diff --git a/app/controllers/course/wizard.php b/app/controllers/course/wizard.php
index c1571ad04a1..b4244b61bea 100644
--- a/app/controllers/course/wizard.php
+++ b/app/controllers/course/wizard.php
@@ -20,17 +20,28 @@ class Course_WizardController extends AuthenticatedController
     /**
      * @var Array steps the wizard has to execute in order to create a new course.
      */
-    public $steps = [];
+    public array $steps = [];
 
     public function before_filter (&$action, &$args)
     {
         parent::before_filter($action, $args);
 
+        if ($GLOBALS['user']->perms === 'user') {
+            throw new AccessDeniedException();
+        }
+
         $this->dialog = Request::isXhr();
         $this->studygroup = Request::bool('studygroup', $this->flash['studygroup'] ?? false);
 
+        // Feels a bit hacky
+        $this->is_copy = isset($_SESSION['coursewizard'][$args[1] ?? '']['source_id']);
+
         if (!$this->studygroup) {
-            PageLayout::setTitle(_('Neue Veranstaltung anlegen'));
+            if ($this->is_copy) {
+                PageLayout::setTitle(_('Veranstaltung kopieren'));
+            } else {
+                PageLayout::setTitle(_('Neue Veranstaltung anlegen'));
+            }
 
             $navigation = new Navigation(_('Neue Veranstaltung anlegen'), 'dispatch.php/course/wizard');
             Navigation::addItem('/browse/my_courses/new_course', $navigation);
@@ -46,10 +57,6 @@ class Course_WizardController extends AuthenticatedController
         }
 
         $this->steps = CourseWizardStepRegistry::findBySQL("`enabled`=1 ORDER BY `number`");
-
-        if ($GLOBALS['user']->perms === 'user') {
-            throw new AccessDeniedException();
-        }
     }
 
     /**
@@ -132,7 +139,10 @@ class Course_WizardController extends AuthenticatedController
             }
         // The "create" button was clicked -> create course.
         } else if (Request::submitted('create')) {
-            $_SESSION['coursewizard'][$this->temp_id]['copy_basic_data'] = Request::submitted('copy_basic_data');
+            $_SESSION['coursewizard'][$this->temp_id]['copy_basic_data'] = Request::bool('copy_basic_data');
+            $_SESSION['coursewizard'][$this->temp_id]['copy_participants'] = Request::bool('copy_participants');
+            $_SESSION['coursewizard'][$this->temp_id]['copy_groups'] = Request::bool('copy_groups');
+            $_SESSION['coursewizard'][$this->temp_id]['copy_members'] = Request::bool('copy_members');
             if ($this->getValues()) {
                 // Batch creation of several courses at once.
                 if ($batch = Request::getArray('batchcreate')) {
@@ -141,7 +151,7 @@ class Course_WizardController extends AuthenticatedController
                     $failed = 0;
                     // Create given number of courses.
                     for ($i = 1 ; $i <= $batch['number'] ; $i++) {
-                        if ($newcourse = $this->createCourse($i == $batch['number'] ? true : false)) {
+                        if ($newcourse = $this->createCourse($i == $batch['number'])) {
                             // Add corresponding number/letter to name or number of newly created course.
                             if ($batch['add_number_to'] == 'name') {
                                 $newcourse->name .= ' ' . $numbering;
@@ -293,16 +303,20 @@ class Course_WizardController extends AuthenticatedController
      * Copy an existing course.
      */
     public function copy_action($id) {
-        if (!$GLOBALS['perm']->have_studip_perm('dozent', $id)
-            || LockRules::Check($id, 'seminar_copy')) {
-            throw new AccessDeniedException(_("Sie dürfen diese Veranstaltung nicht kopieren"));
+        if (
+            !$GLOBALS['perm']->have_studip_perm('dozent', $id)
+            || LockRules::Check($id, 'seminar_copy')
+        ) {
+            throw new AccessDeniedException(_('Sie dürfen diese Veranstaltung nicht kopieren'));
         }
+
         $course = Course::find($id);
         $values = [];
         for ($i = 0 ; $i < sizeof($this->steps) ; $i++) {
             $step = $this->getStep($i);
             $values = $step->copy($course, $values);
         }
+
         $values['source_id'] = $course->id;
         $this->initialize();
         $_SESSION['coursewizard'][$this->temp_id] = $values;
diff --git a/app/views/course/statusgroups/copy.php b/app/views/course/statusgroups/copy.php
new file mode 100644
index 00000000000..b43f52be22a
--- /dev/null
+++ b/app/views/course/statusgroups/copy.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * @var Course_StatusgroupsController $controller
+ * @var MyCoursesSearch $search
+ * @var string[] $group_ids
+ */
+?>
+<form action="<?= $controller->do_copy() ?>" method="post" class="default">
+    <?= addHiddenFields('group_ids', $group_ids) ?>
+
+    <fieldset>
+        <legend><?= _('Gruppen in andere Veranstaltung kopieren') ?></legend>
+
+        <label>
+            <?= _('Zielveranstaltung auswählen') ?>
+            <?= QuickSearch::get('course_id', $search)
+                ->setAttributes(['required' => ''])
+                ->setInputStyle('width:100%')
+                ->withButton()
+                ->render(); ?>
+        </label>
+
+        <label>
+            <input type="checkbox" name="copy_members" value="1">
+            <?= _('Inklusive aller zugeordneten Personen') ?>
+        </label>
+    </fieldset>
+
+    <footer data-dialog-button>
+        <?= Studip\Button::createAccept(_('Kopieren')) ?>
+        <?= Studip\LinkButton::createCancel(
+            _('Abbrechen'),
+            $controller->indexURL()
+        ) ?>
+    </footer>
+</form>
diff --git a/app/views/course/statusgroups/index.php b/app/views/course/statusgroups/index.php
index d420de24f85..73a70ce98ca 100644
--- a/app/views/course/statusgroups/index.php
+++ b/app/views/course/statusgroups/index.php
@@ -1,5 +1,17 @@
-<form action="<?= $controller->url_for('course/statusgroups/batch_action') ?>" method="post">
-<section class="contentbox course-statusgroups" <? if ($is_tutor && !$is_locked) echo 'data-sortable="' . $controller->url_for('course/statusgroups/order') . '"'; ?>>
+<?php
+/**
+ * @var Course_StatusgroupsController $controller
+ * @var bool $is_tutor
+ * @var bool $is_locked
+ * @var array $groups
+ * @var bool $open_groups
+ * @var string $order
+ * @var string $sort_by
+ * @var int $ungrouped_count
+ */
+?>
+<form action="<?= $controller->batch_action() ?>" method="post">
+<section class="contentbox course-statusgroups" <? if ($is_tutor && !$is_locked) echo 'data-sortable="' . $controller->order() . '"'; ?>>
     <header>
         <h1><?= _('Teilnehmende nach Gruppen') ?></h1>
     </header>
@@ -29,14 +41,18 @@
             </div>
             <div class="groupactions">
                 <label>
-                    <select name="groups_action" id="batch-groups-action" disabled>
+                    <select name="groups_action" id="batch-groups-action">
                         <option value="edit_size"><?= _('Gruppengröße bearbeiten') ?></option>
                         <option value="edit_selfassign"><?= _('Selbsteintrag bearbeiten') ?></option>
                         <option value="write_message"><?= _('Nachricht schreiben') ?></option>
+                        <option value="copy"><?= _('In andere Veranstaltung kopieren') ?></option>
                         <option value="delete"><?= _('Löschen') ?></option>
                     </select>
                 </label>
-                <?= Studip\Button::create(_('Ausführen'), 'batch_groups', ['data-dialog' => 'size=auto', 'disabled' => '', 'id' => 'batch-groups-submit']) ?>
+                <?= Studip\Button::create(_('Ausführen'), 'batch_groups', [
+                    'data-dialog' => 'size=auto',
+                    'id'          => 'batch-groups-submit',
+                ]) ?>
             </div>
         </footer>
     <?php endif ?>
diff --git a/app/views/course/wizard/summary.php b/app/views/course/wizard/summary.php
index ec5a646f417..6344e561c09 100644
--- a/app/views/course/wizard/summary.php
+++ b/app/views/course/wizard/summary.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * @var Course_WikiController $controller
+ * @var Course_WizardController $controller
  * @var int $stepnumber
  * @var string $temp_id
  * @var bool $dialog
@@ -24,18 +24,37 @@
     ) ?>
 <? endif ?>
 
-<? if ($source_course) : ?>
+<? if (isset($source_course)) : ?>
     <section>
-        <label>
-            <input type="checkbox" checked name="copy_basic_data" value="1">
+        <p>
             <?= sprintf(
-                _('Alle Grunddaten der Ursprungsveranstaltung (%s) kopieren'),
+                _('Folgende Daten der Ursprungsveranstaltung (%s) kopieren'),
                 sprintf(
                     '<a data-dialog href="%s">%s</a>',
                     URLHelper::getLink('dispatch.php/course/details', ['sem_id' => $source_course->id]),
                     htmlReady($source_course->getFullName())
                 )
             ) ?>
+        </p>
+
+        <label>
+            <input type="checkbox" checked name="copy_basic_data" value="1">
+            <?= _('Grunddaten') ?>
+        </label>
+
+        <label>
+            <input type="checkbox" name="copy_participants" value="1">
+            <?= _('Reguläre Teilnehmende') ?>
+        </label>
+
+        <label>
+            <input type="checkbox" name="copy_groups" value="1" data-activates="[name='copy_members']">
+            <?= _('Statusgruppen') ?>
+        </label>
+
+        <label>
+            <input type="checkbox" name="copy_members" value="1">
+            <?= _('Zugeordnete Teilnehmende der Statusgruppen') ?>
         </label>
     </section>
 <? endif ?>
@@ -50,9 +69,10 @@
 
     <footer data-dialog-button>
     <? if (isset($_SESSION['coursewizard'][$this->temp_id]['batchcreate'])) : ?>
-        <? foreach ($_SESSION['coursewizard'][$this->temp_id]['batchcreate'] as $key => $value) : ?>
-            <input type="hidden" name="batchcreate[<?= $key ?>]" value="<?= $value ?>">
-        <? endforeach ?>
+        <?= addHiddenFields(
+            'batchcreate',
+            $_SESSION['coursewizard'][$this->temp_id]['batchcreate']
+        ) ?>
     <? endif ?>
         <?= Studip\Button::create(_('Zurück'), 'back',
             $dialog ? ['data-dialog' => 'size=50%'] : []) ?>
diff --git a/lib/classes/AuthenticatedController.php b/lib/classes/AuthenticatedController.php
index e051ffa7156..6ed5c20bf28 100644
--- a/lib/classes/AuthenticatedController.php
+++ b/lib/classes/AuthenticatedController.php
@@ -1,13 +1,11 @@
 <?php
-/*
- * Copyright (C) 2009 - Marcus Lunzenauer <mlunzena@uos.de>
+/**
+ * @author Marcus Lunzenauer <mlunzena@uos.de>
+ * @copyright 2009 - Marcus Lunzenauer <mlunzena@uos.de>
+ * @license GPL2 or any later version
  *
- * 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.
+ * @property Trails_Flash $flash
  */
-
 class AuthenticatedController extends StudipController
 {
     protected $with_session = true;  //we do need to have a session for this controller
diff --git a/lib/classes/QuickSearch.php b/lib/classes/QuickSearch.php
index f3e18cc27ea..b6fcc1223d6 100644
--- a/lib/classes/QuickSearch.php
+++ b/lib/classes/QuickSearch.php
@@ -155,7 +155,7 @@ class QuickSearch
      * the searchfield will automatically search for persons, courses, workgroups, institutes and
      * you don't need to call the specialSearch-method.
      *
-     * @return object of type QuickSearch
+     * @return static
      */
     public static function get($name, $search = NULL)
     {
diff --git a/lib/classes/coursewizardsteps/BasicDataWizardStep.php b/lib/classes/coursewizardsteps/BasicDataWizardStep.php
index d47c4f3a341..c312972b4a6 100644
--- a/lib/classes/coursewizardsteps/BasicDataWizardStep.php
+++ b/lib/classes/coursewizardsteps/BasicDataWizardStep.php
@@ -249,7 +249,7 @@ class BasicDataWizardStep implements CourseWizardStep
             $values['tutors'] = [];
         }
 
-        list($lsearch, $tsearch)  = array_values($this->getSearch($values['coursetype'],
+        [$lsearch, $tsearch]  = array_values($this->getSearch($values['coursetype'],
             array_merge([$values['institute']], array_keys($values['participating'])),
             array_keys($values['lecturers']), array_keys($values['tutors'])));
         // Quicksearch for lecturers.
@@ -387,26 +387,28 @@ class BasicDataWizardStep implements CourseWizardStep
      * Stores the given values to the given course.
      *
      * @param Course $course the course to store values for
-     * @param Array $values values to set
-     * @return Course The course object with updated values.
+     * @param array  $values values to set
+     *
+     * @return Course|false The course object with updated values.
      */
     public function storeValues($course, $values)
     {
+        // Fetch settings from $values before it is overwritten
+        $source_id = $values['source_id'] ?? null;
+        $copy_basic_data = !empty($values['copy_basic_data']);
+        $copy_participants = !empty($values['copy_participants']);
+        $copy_groups = !empty($values['copy_groups']);
+        $copy_members = !empty($values['copy_members']);
+
         // We only need our own stored values here.
-        if (@$values['copy_basic_data'] === true) {
-            $source = Course::find($values['source_id']);
-        }
         $values = $values[__CLASS__];
         $seminar = new Seminar($course);
 
-        if (isset($source)) {
-            $course->setData($source->toArray('untertitel ort sonstiges art teilnehmer vorrausetzungen lernorga leistungsnachweis ects admission_turnout modules'));
-            foreach ($source->datafields as $one) {
-                $df = $one->getTypedDatafield();
-                if ($df->isEditable()) {
-                    $course->datafields->findOneBy('datafield_id', $one->datafield_id)->content = $one->content;
-                }
-            }
+        if ($copy_basic_data) {
+            $this->copyBasicData(
+                $course,
+                $source_id
+            );
         }
 
         $course->status = $values['coursetype'];
@@ -440,36 +442,39 @@ class BasicDataWizardStep implements CourseWizardStep
                     break;
             }
         }
-        if ($course->store()) {
-            StudipLog::log('SEM_CREATE', $course->id, null, 'Veranstaltung mit Assistent angelegt');
-            $institutes = [$values['institute']];
-            if (isset($values['participating']) && is_array($values['participating'])) {
-                $institutes = array_merge($institutes, array_keys($values['participating']));
-            }
-            $seminar->setInstitutes($institutes);
-            if (isset($values['lecturers']) && is_array($values['lecturers'])) {
-                foreach (array_keys($values['lecturers']) as $user_id) {
-                    $seminar->addMember($user_id, 'dozent');
-                }
-            }
-            if (isset($values['tutors']) && is_array($values['tutors'])) {
-                foreach (array_keys($values['tutors']) as $user_id) {
-                    $seminar->addMember($user_id, 'tutor');
-                }
+        if (!$course->store()) {
+            return false;
+        }
+
+        StudipLog::log('SEM_CREATE', $course->id, null, 'Veranstaltung mit Assistent angelegt');
+        $institutes = [$values['institute']];
+        if (isset($values['participating']) && is_array($values['participating'])) {
+            $institutes = array_merge($institutes, array_keys($values['participating']));
+        }
+        $seminar->setInstitutes($institutes);
+        if (isset($values['lecturers']) && is_array($values['lecturers'])) {
+            foreach (array_keys($values['lecturers']) as $user_id) {
+                $seminar->addMember($user_id, 'dozent');
             }
-            if (Config::get()->DEPUTIES_ENABLE && isset($values['deputies']) && is_array($values['deputies'])) {
-                foreach ($values['deputies'] as $d => $assigned) {
-                    Deputy::addDeputy($d, $course->id);
-                }
+        }
+        if (isset($values['tutors']) && is_array($values['tutors'])) {
+            foreach (array_keys($values['tutors']) as $user_id) {
+                $seminar->addMember($user_id, 'tutor');
             }
-            if ($semclass['admission_type_default'] == 3) {
-                $course_set_id = CourseSet::getGlobalLockedAdmissionSetId();
-                CourseSet::addCourseToSet($course_set_id, $course->id);
+        }
+        if (Config::get()->DEPUTIES_ENABLE && isset($values['deputies']) && is_array($values['deputies'])) {
+            foreach ($values['deputies'] as $d => $assigned) {
+                Deputy::addDeputy($d, $course->id);
             }
-            return $course;
-        } else {
-            return false;
         }
+        if ($semclass['admission_type_default'] == 3) {
+            $course_set_id = CourseSet::getGlobalLockedAdmissionSetId();
+            CourseSet::addCourseToSet($course_set_id, $course->id);
+        }
+
+        self::copyParticipantsAndGroups($course, $source_id, $copy_participants, $copy_groups, $copy_members);
+
+        return $course;
     }
 
     /**
@@ -477,7 +482,7 @@ class BasicDataWizardStep implements CourseWizardStep
      * to already given values. A good example are study areas which
      * are only needed for certain sem_classes.
      *
-     * @param Array $values values specified from previous steps
+     * @param array $values values specified from previous steps
      * @return bool Is the current step required for a new course?
      */
     public function isRequired($values)
@@ -488,7 +493,7 @@ class BasicDataWizardStep implements CourseWizardStep
     /**
      * Copy values for basic data wizard step from given course.
      * @param Course $course
-     * @param Array $values
+     * @param array $values
      */
     public function copy($course, $values)
     {
@@ -521,17 +526,17 @@ class BasicDataWizardStep implements CourseWizardStep
      * Fetches the default deputies for a given person if the necessary
      * config options are set.
      * @param $user_id user whose default deputies to get
-     * @return Array Default deputy user_ids.
+     * @return array Default deputy user_ids.
      */
     public function getDefaultDeputies($user_id)
     {
         if (Config::get()->DEPUTIES_ENABLE && Config::get()->DEPUTIES_DEFAULTENTRY_ENABLE) {
-            return Deputy::findDeputies($user_id)->map(function($deputy) {
+            return Deputy::findDeputies($user_id)->map(function (Deputy $deputy): array {
                 return ['id' => $deputy->user_id, 'name' => $deputy->getDeputyFullname()];
             });
-        } else {
-            return [];
         }
+
+        return [];
     }
 
     public function getSearch($course_type, $institute_ids, $exclude_lecturers = [],$exclude_tutors = [])
@@ -629,4 +634,112 @@ class BasicDataWizardStep implements CourseWizardStep
         return $values;
     }
 
+    private function copyBasicData(
+        Course $course,
+        string $source_id
+    ): void {
+        $source = Course::find($source_id);
+        $course->setData($source->toArray('untertitel ort sonstiges art teilnehmer vorrausetzungen lernorga leistungsnachweis ects admission_turnout modules'));
+        foreach ($source->datafields as $one) {
+            $df = $one->getTypedDatafield();
+            if ($df->isEditable()) {
+                $course->datafields->findOneBy('datafield_id', $one->datafield_id)->content = $one->content;
+            }
+        }
+    }
+
+    /**
+     * Copies participants and/or groups from one course to another.
+     */
+    public static function copyParticipantsAndGroups(
+        Course $course,
+        string $source_id,
+        bool $with_participants = true,
+        bool $with_groups = true,
+        bool $with_members = true,
+        bool|array $group_ids = false
+    ): void {
+        $source = Course::find($source_id);
+
+        if (!$with_participants && !$with_groups) {
+            return;
+        }
+
+        if ($with_participants || ($with_groups && $with_members)) {
+            $member_ids = false;
+            if (!$with_participants && $with_members) {
+                $member_ids = [];
+                $source->statusgruppen->filter(function (Statusgruppen $group) use ($group_ids): bool {
+                    return $group_ids === false
+                        || in_array($group->id, $group_ids);
+                })->each(function (Statusgruppen $group) use (&$member_ids): void {
+                    $group->members->each(function (StatusgruppeUser $member) use (&$member_ids): void {
+                        if (!in_array($member->user_id, $member_ids)) {
+                            $member_ids[] = $member->user_id;
+                        }
+                    });
+                });
+            }
+
+            $source->getMembersWithStatus(['user', 'autor', 'tutor'], true)
+                ->filter(function (CourseMember $member) use ($course, $member_ids): bool {
+                    return ($member_ids === false || in_array($member->user_id, $member_ids))
+                        && !CourseMember::exists([$course->id, $member->user_id]);
+                })->each(function (CourseMember $member) use ($course): void {
+                    CourseMember::insertCourseMember(
+                        $course->id,
+                        $member->user_id,
+                        $member->status,
+                    );
+                });
+        }
+
+        if (!$with_groups) {
+            return;
+        }
+
+        $source->statusgruppen->filter(function (Statusgruppen $group) use ($group_ids): bool {
+            return $group_ids === false
+                || in_array($group->id, $group_ids);
+        })->each(function (Statusgruppen $group) use ($course, $with_members, $group_ids): void {
+            $g = Statusgruppen::findOneBySQL(
+                'range_id = ? AND name = ?',
+                [$course->id, $group->name]
+            );
+
+            if (!$g) {
+                $g = Statusgruppen::createOrUpdate(
+                    null,
+                    $group->name,
+                    $group->position,
+                    $course->id,
+                    $group->size,
+                    $group->selfassign,
+                    $group->selfassign_start,
+                    $group->selfassign_end,
+                    $group->hasFolder(),
+                    null,
+                    $group->hasBlubber()
+                );
+            }
+
+            if (!$with_members) {
+                return;
+            }
+
+            $group->members->filter(function (StatusgruppeUser $member) use ($g): bool {
+                return !StatusgruppeUser::exists([$g->id, $member->user_id]);
+            })->each(function (StatusgruppeUser $member) use ($g): void {
+                StatusgruppeUser::create([
+                    'statusgruppe_id' => $g->id,
+                    ...$member->toArray([
+                        'user_id',
+                        'position',
+                        'visible',
+                        'inherit',
+                    ])
+                ]);
+            });
+        });
+    }
 }
diff --git a/lib/classes/searchtypes/StandardSearch.php b/lib/classes/searchtypes/StandardSearch.php
index a3f0f3b5474..6d37cbf0e7f 100644
--- a/lib/classes/searchtypes/StandardSearch.php
+++ b/lib/classes/searchtypes/StandardSearch.php
@@ -114,7 +114,7 @@ class StandardSearch extends SQLSearch
                 if (empty($this->search_settings['simple_name'])) {
                     $sql .= ", CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname, ' (', auth_user_md5.username, ')'), auth_user_md5.perms ";
                 } else {
-                    $sql .= ", CONCAT(auth_user_md5.Vorname, ' ', auth_user_md5.Nachname) ";
+                    $sql .= ", CONCAT(auth_user_md5.Vorname, ' ', auth_user_md5.Nachname) ";
                 }
                 $sql .= "FROM auth_user_md5 LEFT JOIN user_info ON (user_info.user_id = auth_user_md5.user_id) " .
                     "LEFT JOIN user_visibility ON (user_visibility.user_id = auth_user_md5.user_id) " .
diff --git a/lib/models/Statusgruppen.php b/lib/models/Statusgruppen.php
index 94c8847261d..6297f2c94ae 100644
--- a/lib/models/Statusgruppen.php
+++ b/lib/models/Statusgruppen.php
@@ -133,31 +133,35 @@ class Statusgruppen extends SimpleORMap implements PrivacyObject
     /**
      * Creates or updates a statusgroup.
      *
-     * @param string      $id                ID of an existing group or empty if new group
+     * @param string|null $id                ID of an existing group or empty if new group
      * @param string      $name              group name
-     * @param int         $position          position or null if automatic position after other groups
+     * @param int|null    $position          position or null if automatic position after other groups
      * @param string      $range_id          ID of the object this group belongs to
      * @param int         $size              max number of members or 0 if unlimited
-     * @param int         $selfassign        may users join this group by themselves?
+     * @param bool        $selfassign        may users join this group by themselves?
      * @param int         $selfassign_start  group joining is possible starting at ...
-     * @param int         $makefolder        create a document folder assigned to this group?
+     * @param int         $selfassign_end    group joining is possible until ...
+     * @param bool        $makefolder        create a document folder assigned to this group?
      * @param array|null  $dates             dates assigned to this group. Defaults to null which means already assigned
      *                                       dates are not changed.
+     * @param bool        $make_blubber      create a blubber thread for this group?
+     *
      * @return Statusgruppen The saved statusgroup.
      * @throws Exception
      */
     public static function createOrUpdate(
-        $id,
-        $name,
-        $position,
-        $range_id,
-        $size,
-        $selfassign,
-        $selfassign_start,
-        $selfassign_end,
-        $makefolder,
-        $dates = null
-    )
+        ?string $id,
+        string $name,
+        ?int $position,
+        string $range_id,
+        int $size,
+        bool $selfassign,
+        int $selfassign_start,
+        int $selfassign_end,
+        bool $makefolder,
+        ?array $dates = null,
+        bool $make_blubber = false
+    ): Statusgruppen
     {
         $group = new Statusgruppen($id);
 
@@ -176,12 +180,12 @@ class Statusgruppen extends SimpleORMap implements PrivacyObject
 
         $group->store();
 
-        /*
-         * Create document folder if requested (ID is needed here,
-         * so we do that after store()).
-         */
+        // Create document folder if requested (ID is needed here, so we do
+        // that after store()).
         $group->updateFolder($makefolder);
 
+        $group->updateBlubber($make_blubber);
+
         return $group;
     }
 
@@ -412,6 +416,38 @@ class Statusgruppen extends SimpleORMap implements PrivacyObject
         }
     }
 
+    /**
+     * Returns whether the group has an associated blubber thread.
+     */
+    public function hasBlubber(): bool
+    {
+        return (bool) BlubberStatusgruppeThread::findByStatusgruppe_id($this->id);
+    }
+
+    /**
+     * Delete or create blubber thread
+     *
+     * @param bool $set Whether to create a blubber thread or not; an existing
+     *                  blubber thread will be deleted if $set is false
+     */
+    public function updateBlubber(bool $set): void
+    {
+        if ($set && $this->hasBlubber()) {
+            BlubberStatusgruppeThread::create([
+                'context_type'      => 'course',
+                'context_id'        => $this->range_id,
+                'user_id'           => User::findCurrent()->id,
+                'external_contact'  => false,
+                'display_class'     => BlubberStatusgruppeThread::class,
+                'visible_in_stream' => true,
+                'commentable'       => true,
+                'metadata'          => ['statusgruppe_id' => $this->id],
+            ]);
+        } elseif (!$set) {
+            BlubberStatusgruppeThread::findByStatusgruppe_id($this->id)?->delete();
+        }
+    }
+
     /**
      * Finds CourseTopics assigned to this group via course dates.
      * @return array
diff --git a/resources/assets/javascripts/lib/dialog.js b/resources/assets/javascripts/lib/dialog.js
index 8f6e50adb0b..c602a29f849 100644
--- a/resources/assets/javascripts/lib/dialog.js
+++ b/resources/assets/javascripts/lib/dialog.js
@@ -173,6 +173,10 @@ Dialog.handlers.header['X-Title'] = function(title, options) {
 Dialog.handlers.header['X-No-Buttons'] = function(value, options) {
     options.buttons = false;
 };
+// Handler for HTTP header X-Dialog-Size: Adjust the size of the dialog
+Dialog.handlers.header['X-Dialog-Size'] = function (value, options) {
+    options.size = value;
+};
 
 // Creates a dialog from an anchor, a button or a form element.
 // Will update the dialog if it is already open
-- 
GitLab