Skip to content
Snippets Groups Projects
Select Git revision
  • c030a40df2279ff4a5a6e6f5bdf234b443febde2
  • main default protected
  • 5.5 protected
  • atlantis
  • 5.3 protected
  • 5.0 protected
  • issue-23
  • issue8-seat-logging-and-export
  • ticket-216
  • tickets-215-216-241-242
10 results

StudipSemSearchHelper.class.php

Blame
  • Forked from Stud.IP / Stud.IP
    Source project has a limited visibility.
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    statusgroups.php 53.30 KiB
    <?php
    /**
     * StatusgroupController
     *
     * 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      Thomas Hackl <thomas.hackl@uni-passau.de>
     * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
     * @category    Stud.IP
     * @since       3.5
     */
    
    require_once 'lib/messaging.inc.php'; //Funktionen des Nachrichtensystems
    require_once 'lib/export/export_studipdata_func.inc.php'; // Funktionen für den Export
    require_once 'lib/export/export_linking_func.inc.php';
    
    class Course_StatusgroupsController extends AuthenticatedController
    {
        public function before_filter(&$action, &$args)
        {
            parent::before_filter($action, $args);
    
            global $perm;
    
            checkObject();
            checkObjectModule("participants");
    
            $course = Course::findCurrent();
            $this->course_id = $course->id;
            $this->course_title = $course->getFullname();
            $this->config = CourseConfig::get($this->course_id);
    
            // Check perms
            $this->is_dozent = $perm->have_studip_perm('dozent', $this->course_id);
            $this->is_tutor  = $perm->have_studip_perm('tutor', $this->course_id);
            $this->is_autor  = $perm->have_studip_perm('autor', $this->course_id);
    
            // Hide groups page?
            if (!$this->is_tutor && $this->config->COURSE_MEMBERGROUPS_HIDE) {
                throw new AccessDeniedException();
            }
    
            // Check lock rules
            $this->is_locked = LockRules::Check($this->course_id, 'groups');
            $this->is_participants_locked = LockRules::Check($this->course_id, 'participants');
    
            PageLayout::setTitle(sprintf('%s - %s', Course::findCurrent()->getFullname(), _('Gruppen')));
            PageLayout::addStyleSheet('studip-statusgroups.css');
            PageLayout::addScript('studip-statusgroups.js');
        }
    
        /**
         * Lists all available statusgroups.
         */
        public function index_action()
        {
            Navigation::activateItem('/course/members/statusgroups');
    
            if ($this->is_locked && $this->is_tutor) {
                $lockdata = LockRules::getObjectRule($this->course_id);
                if ($lockdata['description']) {
                    PageLayout::postInfo(formatLinks($lockdata['description']));
                }
            }
    
            // Sorting as given by Request parameters
            $this->sort_by = Request::option('sortby', 'nachname');
            $this->order = Request::option('order', 'desc');
            $this->sort_group = Request::get('sort_group', '');
            $this->open_groups = Request::get('open_groups');
    
            // Get all course members (needed for mkdate).
            $this->allmembers = SimpleCollection::createFromArray(
                CourseMember::findByCourse($this->course_id));
    
            // Find all statusgroups for this course.
            $groups = Statusgruppen::findBySeminar_id($this->course_id);
    
            /*
             * Check if the current user may join any group at all. This is needed
             * for deciding if a Sidebar action for joining a group will be
             * displayed.
             */
            $joinable = false;
    
            // Fetch membercounts (all at once for performance)
            $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`)
                    WHERE s.`range_id` = ?
                    GROUP BY `statusgruppe_id`
                    ORDER BY s.`position` ASC, s.`name` ASC",
                    [$this->course_id]),
                'membercount',
                'statusgruppe_id'
            );
    
    
            // Now build actual groups.
            $this->groups = [];
            foreach ($groups as $g) {
                $groupdata = [
                    'group' => $g,
                    'members' => [],
                    'membercount' => $membercounts[$g->id] ?: 0,
                    'invisible_users' => 0
                ];
    
                /*
                 * We only need to load members for a group that shall be sorted
                 * explicitly, as this group will be loaded at once and not via AJAX.
                 */
                if ($g->id == $this->sort_group || $this->open_groups) {
                    if ($this->sort_group == $g->id) {
                        $sorted = $this->sortMembers($g->members, $this->sort_by, $this->order);
                    } else {
                        $sorted = $sorted = $this->sortMembers($g->members);
                    }
    
                    foreach ($sorted as $member) {
                        //Note: $member is a StatusgruppeUser object.
                        //We must get the CourseMember object to correctly
                        //determine the visibility of the user.
                        $course_member = CourseMember::findOneBySql(
                            'user_id = :user_id AND seminar_id = :course_id',
                            ['user_id' => $member->user_id, 'course_id' => $member->group->range_id]
                        );
                        if ($this->is_tutor || ($member->user_id == $GLOBALS['user']->id) ||
                            ($course_member->visible != 'no')) {
                            $groupdata['members'][] = $member;
                        } else {
                            $groupdata['invisible_users']++;
                        }
                    }
                    $groupdata['load'] = true;
                }
    
                if (!$this->is_tutor && $g->userMayJoin($GLOBALS['user']->id)) {
                    $groupdata['joinable'] = true;
                    $joinable = true;
                }
    
                $this->groups[] = $groupdata;
            }
    
            /*
             * Get number of users that are in no group, this is needed
             * for displaying in group header.
             */
            $ungrouped_count = DBManager::get()->fetchFirst(
                "SELECT COUNT(s.`user_id`) FROM `seminar_user` s WHERE s.`Seminar_id` = :course AND NOT EXISTS (
                        SELECT u.`user_id` FROM `statusgruppe_user` u
                        WHERE u.`statusgruppe_id` IN (:groups) AND u.`user_id` = s.`user_id`)",
                [
                    'course' => $this->course_id,
                    'groups' => DBManager::get()->fetchFirst(
                        "SELECT `statusgruppe_id` FROM `statusgruppen` WHERE `range_id` = ?",
                        [$this->course_id])
                ]);
            $ungrouped_count = $ungrouped_count[0];
            if ($ungrouped_count > 0) {
                // Create dummy entry for "no group" users.
                $no_group = new StdClass();
                $no_group->id = 'nogroup';
                $no_group->name = _('keiner Gruppe zugeordnet');
                $no_group->size = 0;
                $no_group->selfassign = 0;
    
                $groupdata = [
                    'group' => $no_group,
                    'membercount' => $ungrouped_count,
                    'joinable' => false,
                    'invisible_users' => 0,
                    'members' => []
                ];
    
                $nogroupmembers = DBManager::get()->fetchFirst("SELECT user_id
                    FROM seminar_user
                    WHERE `Seminar_id` = :course AND NOT EXISTS (
                        SELECT `user_id` FROM `statusgruppe_user`
                        WHERE `statusgruppe_id` IN (:groups) AND `user_id` = seminar_user.`user_id`)",
                    [
                        'course' => $this->course_id,
                        'groups' => array_map(function ($g) { return $g->id; }, $groups)
                    ]);
    
    
                $this->nogroupmembers = $nogroupmembers;
    
                if ($this->sort_group == 'nogroup') {
                    $members = $this->allmembers->findby('user_id', $nogroupmembers);
                    $members = $this->sortMembers($members, $this->sort_by, $this->order);
                    $groupdata['load'] = true;
                } else {
                    $members = $this->allmembers->findby(
                        'user_id',
                        $this->nogroupmembers
                    );
                }
    
                if(!empty($members)) {
                    foreach ($members as $member) {
                        //Note: $member is a CourseMember object here.
                        if ($this->is_tutor || ($member->user_id == $GLOBALS['user']->id) ||
                            ($member->visible != 'no')) {
                            $groupdata['members'][] = $member;
                        } else {
                            $groupdata['invisible_users']++;
                        }
                    }
                }
                $this->groups[] = $groupdata;
            }
    
            // Prepare search object for MultiPersonSearch.
            $this->memberSearch = new PermissionSearch(
                'user',
                _('Personen suchen'),
                'user_id',
                [
                    'permission' => ['user', 'autor', 'tutor', 'dozent'],
                    'exclude_user' => []
                ]
            );
    
            /*
             * Setup sidebar.
             */
            $sidebar = Sidebar::get();
    
            $actions = new ActionsWidget();
            if ($this->is_tutor) {
                if (!$this->is_locked) {
                    $actions->addLink(
                        _('Neue Gruppe anlegen'),
                        $this->url_for('course/statusgroups/edit'),
                        Icon::create('add')
                    )->asDialog('size=auto');
                    $actions->addLink(
                        _('Mehrere Gruppen anlegen'),
                        $this->url_for('course/statusgroups/create_groups'),
                        Icon::create('group2+add')
                    )->asDialog('size=auto');
                }
                if (Config::get()->EXPORT_ENABLE) {
                    $export = new ExportWidget();
                    // create csv-export link
                    $csvExport = export_link($this->course_id, 'person',
                        sprintf('%s %s', _('Gruppenliste'), htmlReady($this->course_title)),
                        'csv', 'csv-gruppen', 'status',
                        _('Gruppen als CSV-Dokument exportieren'),
                        'passthrough');
                    $element = LinkElement::fromHTML($csvExport, Icon::create('file-office'));
                    $export->addElement($element);
    
                    // create rtf-export link
                    $rtfExport = export_link($this->course_id, 'person',
                        sprintf('%s %s', _('Gruppenliste'), htmlReady($this->course_title)),
                        'rtf', 'rtf-gruppen', 'status',
                        _('Gruppen als RTF-Dokument exportieren'),
                        'passthrough');
                    $element = LinkElement::fromHTML($rtfExport, Icon::create('file-text'));
                    $export->addElement($element);
    
                    $sidebar->addWidget($export);
                }
            // Current user may join at least one group => show sidebar action.
            } else if ($joinable) {
                $actions->addLink(
                    _('In eine Gruppe eintragen'),
                    $this->url_for('course/statusgroups/joinables'),
                    Icon::create('door-enter')
                )->asDialog('size=auto');
            }
    
            if ($this->open_groups) {
                $actions->addLink(
                    _('Alle Gruppen zuklappen'),
                    $this->url_for('course/statusgroups'),
                    Icon::create('arr_2up')
                );
            } else {
                $actions->addLink(
                    _('Alle Gruppen aufklappen'),
                    $this->url_for('course/statusgroups', ['open_groups' => '1']),
                    Icon::create('arr_2down')
                );
            }
            $sidebar->addWidget($actions);
    
            if ($this->is_tutor) {
                $options = $sidebar->addWidget(new OptionsWidget());
                $options->addCheckbox(
                    _('Diese Seite für Studierende verbergen'),
                    $this->config->COURSE_MEMBERGROUPS_HIDE,
                    $this->url_for('course/statusgroups/course_groups_hide/1'),
                    $this->url_for('course/statusgroups/course_groups_hide/0'),
                    ['title' => _('Über diese Option können Sie die Teilnehmendengruppenliste für Studierende der Veranstaltung unsichtbar machen')]
                );
    
            }
        }
    
        /**
         * Fetches the members of the given group.
         *
         * @param String $group_id the statusgroup to get members for.
         */
        public function getgroup_action($group_id)
        {
            if ($group_id != 'nogroup') {
                $this->group = Statusgruppen::find($group_id);
                $this->members = [];
                $this->invisible = 0;
                if (count($this->group->members) > 0) {
                    //Note: $members consists of StatusgruppeUser objects here.
                    $members = $this->sortMembers($this->group->members);
                    foreach ($members as $member) {
                        //Get the course member object to check the visibility
                        //in the course.
                        $course_member = CourseMember::findOneBySql(
                            'user_id = :user_id AND seminar_id = :course_id',
                            ['user_id' => $member->user_id, 'course_id' => $member->group->range_id]
                        );
                        if ($this->is_tutor || ($member->user_id == $GLOBALS['user']->id)
                            || ($course_member->visible != 'no')) {
                            $this->members[] = $member;
                        } else {
                            $this->invisible++;
                        }
                    }
                }
            } else {
                // Create dummy entry for "no group" users.
                $no_group = new StdClass();
                $no_group->id = 'nogroup';
                $no_group->name = _('keiner Gruppe zugeordnet');
                $no_group->size = 0;
                $no_group->selfassign = 0;
    
                $members = DBManager::get()->fetchAll("SELECT seminar_user.*,
                        aum.vorname,aum.nachname,aum.email,
                        aum.username,ui.title_front,ui.title_rear
                        FROM seminar_user
                        LEFT JOIN auth_user_md5 aum USING (user_id)
                        LEFT JOIN user_info ui USING (user_id)
                        WHERE `Seminar_id` = :course AND NOT EXISTS (
                            SELECT `user_id` FROM `statusgruppe_user`
                            WHERE `statusgruppe_id` IN (:groups) AND `user_id` = seminar_user.`user_id`)",
                    [
                        'course' => $this->course_id,
                        'groups' => DBManager::get()->fetchFirst(
                            "SELECT `statusgruppe_id` FROM `statusgruppen` WHERE `range_id` = ?",
                            [$this->course_id])
                    ], 'CourseMember::buildExisting');
    
                $members = new SimpleCollection($members);
                //Note: $members consists of CourseMember objects here.
                $members = $this->sortMembers($members);
                $this->invisible = 0;
                $this->members = [];
                foreach ($members as $member) {
                    if ($this->is_tutor || ($member->user_id == $GLOBALS['user']->id)
                        || ($member->visible != 'no')) {
                        $this->members[] = $member;
                    } else {
                        $this->invisible++;
                    }
                }
    
                $this->group = $no_group;
            }
        }
    
        /**
         * Provides extended info about a status group, like maximum number of
         * participants, selfassign, exclusive entry, selfassign start and end
         * times.
         *
         * @param String $group_id The group to show info for.
         */
        public function groupinfo_action($group_id)
        {
            $this->group = Statusgruppen::find($group_id);
    
            // Topics can be implicitly assigned via course dates.
            $this->topics = $this->group->findTopics();
    
            // Lecturers can be implicitly assigned via course dates.
            $this->lecturers = $this->group->findLecturers();
        }
    
        /**
         * Shows a list of all groups that can be joined by current user
         * and allows the user to select one.
         */
        public function joinables_action()
        {
            $this->joinables = SimpleCollection::createFromArray(
                Statusgruppen::findJoinableGroups($this->course_id, $GLOBALS['user']->id))
                ->orderBy('position asc, name asc');
        }
    
        /**
         * Adds selected persons to given group. user_ids to add come from a
         * MultiPersonSearch object which was triggered in group actions.
         *
         * @param String $group_id
         */
        public function add_member_action($group_id)
        {
            $g = Statusgruppen::find($group_id);
    
            // Get selected persons.
            $mp = MultiPersonSearch::load('add_statusgroup_member' . $group_id);
    
            $success = 0;
            $fail = 0;
    
            foreach ($mp->getAddedUsers() as $a) {
    
                if (!CourseMember::exists([$this->course_id, $a])) {
                    $m = new CourseMember();
                    $m->seminar_id = $this->course_id;
                    $m->user_id = $a;
                    $m->status = User::find($a)->perms == 'user' ? 'user' : 'autor';
                    $m->store();
                }
    
                $s = new StatusgruppeUser();
                $s->statusgruppe_id = $group_id;
                $s->user_id = $a;
                if ($s->store() !== false) {
                    $success++;
                } else {
                    $fail++;
                }
            }
    
            if ($success > 0 && $fail == 0) {
                PageLayout::postSuccess(sprintf(ngettext(
                    '%u Person wurde zu %s hinzugefügt.',
                    '%u Personen wurden zu %s hinzugefügt.',
                    $success), $success, htmlReady($g->name)
                ));
            } else if ($success > 0 && $fail > 0) {
                $successMsg = sprintf(ngettext(
                    '%u Person wurde zu %s hinzugefügt.',
                    '%u Personen wurden zu %s hinzugefügt.',
                    $success), $success, htmlReady($g->name)
                );
                $failMsg = sprintf(ngettext(
                    '%u Person konnte nicht zu %s hinzugefügt werden.',
                    '%u Personen konnten nicht zu %s hinzugefügt werden.',
                    $fail), $fail, htmlReady($g->name)
                );
                PageLayout::postWarning($successMsg . ' ' . $failMsg);
            } else if ($success == 0 && $fail > 0) {
                PageLayout::postError(sprintf(ngettext(
                    '%u Person konnte nicht zu %s hinzugefügt werden.',
                    '%u Personen konnten nicht zu %s hinzugefügt werden.',
                    $success), $success, htmlReady($g->name)
                ));
            }
    
            $this->relocate('course/statusgroups');
    
        }
    
        /**
         * Allows editing of a given statusgroup or creating a new one.
         * @param String $group_id ID of the group to edit
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function edit_action($group_id = '')
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            // Fetch group with given ID or create a new one.
            $this->group = new Statusgruppen($group_id);
    
            // Check if course has regular times.
            $this->cycles = SeminarCycleDate::findBySeminar_id($this->course_id);
    
            // Check if course has single dates, not belonging to a regular cycle.
            $dates = CourseDate::findBySeminar_id($this->course_id);
            $this->singledates = array_filter($dates, function ($d) {
                return !((bool) $d->metadate_id);
            });
        }
    
        /**
         * Saves changes to given statusgroup or creates a new entry.
         *
         * @param String $group_id ID of the group to edit
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function save_action($group_id = '')
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            CSRFProtection::verifyUnsafeRequest();
            /*
             * Check if a valid end time was given.
             */
            if (Request::int('selfassign', 0)) {
                $endtime = strtotime(Request::get('selfassign_end', 'now'));
                $starttime = strtotime(Request::get('selfassign_start', 'now'));
                if ($endtime <= $starttime) {
                    $endtime = 0;
                }
            }
            $position = Statusgruppen::find($group_id)->position;
            $group = Statusgruppen::createOrUpdate(
                $group_id,
                Request::get('name'),
                $position,
                $this->course_id, Request::int('size', 0),
                Request::int('selfassign', 0) + Request::int('exclusive', 0),
                strtotime(Request::get('selfassign_start', 'now')),
                Request::get('selfassign_end') ? strtotime(Request::get('selfassign_end')) : 0,
                Request::int('makefolder', 0),
                Request::getArray('dates')
            );
    
            if (!$group_id) {
                PageLayout::postSuccess(sprintf(
                    _('Die Gruppe "%s" wurde angelegt.'),
                    htmlReady($group->name)
                ));
            } else {
                PageLayout::postSuccess(sprintf(
                    _('Die Daten der Gruppe "%s" wurden gespeichert.'),
                    htmlReady($group->name)
                ));
            }
    
            $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');
        }
    
        /**
         * Deletes the given statusgroup.
         *
         * @param String $group_id ID of the group to delete
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function delete_action($group_id)
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            $group = Statusgruppen::find($group_id);
            $groupname = $group->name;
            $group->delete();
            PageLayout::postSuccess(sprintf(
                _('Die Gruppe "%s" wurde gelöscht.'),
                htmlReady($groupname)
            ));
            $this->relocate('course/statusgroups');
        }
    
        /**
         * Removes the given user from the given statusgroup.
         *
         * @param String $user_id user to remove
         * @param String $group_id affected group
         */
        public function delete_member_action($user_id, $group_id)
        {
            $g = Statusgruppen::find($group_id);
            if (!$this->is_tutor && ($user_id !== $GLOBALS['user']->id || !$g->userMayLeave($user_id))) {
                throw new AccessDeniedException();
            }
    
            $s = StatusgruppeUser::find([$group_id, $user_id]);
            $name = $s->user->getFullname();
            if ($s->delete()) {
                if ($user_id == $GLOBALS['user']->id) {
                    PageLayout::postSuccess(sprintf(
                        _('Sie wurden aus der Gruppe %s ausgetragen.'),
                        htmlReady($g->name)
                    ));
                } else {
                    PageLayout::postSuccess(sprintf(
                        _('%s wurde aus der Gruppe %s ausgetragen.'),
                        htmlReady($name),
                        htmlReady($g->name)
                    ));
                }
            } else {
                if ($user_id == $GLOBALS['user']->id) {
                    PageLayout::postError(sprintf(
                        _('Sie konnten nicht aus der Gruppe %s ausgetragen werden.'),
                        htmlReady($g->name)
                    ));
                } else {
                    PageLayout::postError(sprintf(
                        _('%s konnte nicht aus der Gruppe %s ausgetragen werden.'),
                        htmlReady($name),
                        htmlReady($g->name)
                    ));
                }
            }
            $this->relocate('course/statusgroups');
        }
    
        public function move_member_action($user_id, $group_id)
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            $this->source_group = $group_id;
    
            $this->members = [$user_id];
    
            // Find possible target groups.
            $this->target_groups = SimpleCollection::createFromArray(
                Statusgruppen::findByRange_id($this->course_id))
                ->orderBy('position, name')
                ->filter(function ($g) use ($group_id) { return $g->id != $group_id; });
        }
    
        /**
         * Provides the possibility to batch create several groups at once.
         *
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function create_groups_action()
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            // Check if course has regular times.
            $this->has_cycles = count(SeminarCycleDate::findBySeminar_id($this->course_id)) > 0;
    
            // Check if course has single dates, not belonging to a regular cycle.
            $dates = CourseDate::findBySeminar_id($this->course_id);
            $this->has_singledates = count(array_filter($dates, function ($d) {
                return !((bool) $d->metadate_id);
            })) > 0;
    
            // Check if course has topics.
            $topics = CourseTopic::findBySeminar_id($this->course_id);
            $paper_topics = array_filter($topics, function ($topic) {
                return $topic->paper_related;
            });
    
            $this->has_topics = count($topics) > 0;
            $this->has_paper_related_topics = count($paper_topics) > 0;
        }
    
        /**
         * Adds the current user to the given group.
         *
         * @throws AccessDeniedException if current user may not join the given group.
         */
        public function join_action($group_id = '')
        {
    
            // group_id can also be given per request.
            if (!$group_id) {
                CSRFProtection::verifyUnsafeRequest();
                $group_id = Request::option('target_group');
    
                // Safety check if no group_id at all.
                if (!$group_id) {
                    throw new Trails_Exception(400);
                }
            }
    
            $g = Statusgruppen::find($group_id);
    
            if (!$g->userMayJoin($GLOBALS['user']->id)) {
                throw new AccessDeniedException();
            }
    
            $s = new StatusgruppeUser();
            $s->user_id = $GLOBALS['user']->id;
            $s->statusgruppe_id = $group_id;
            if ($s->store()) {
                PageLayout::postSuccess(sprintf(
                    _('Sie wurden als Mitglied der Gruppe %s eingetragen.'),
                    htmlReady($g->name)
                ));
            } else {
                PageLayout::postError(sprintf(
                    _('Sie konnten nicht als Mitglied der Gruppe %s eingetragen werden.'),
                    htmlReady($g->name)
                ));
            }
    
            $this->relocate('course/statusgroups');
        }
    
        /**
         * Removes the current user from the given group.
         *
         * @throws AccessDeniedException if current user may not join the given group.
         */
        public function leave_action($group_id)
        {
            $g = Statusgruppen::find($group_id);
    
            if (!$g->userMayLeave($GLOBALS['user']->id)) {
                throw new AccessDeniedException();
            }
    
            $s = StatusgruppeUser::find([$group_id, $GLOBALS['user']->id]);
            if ($s->delete()) {
                PageLayout::postSuccess(sprintf(
                    _('Sie wurden aus der Gruppe %s ausgetragen.'),
                    htmlReady($g->name)
                ));
            } else {
                PageLayout::postError(sprintf(
                    _('Sie konnten nicht aus der Gruppe %s ausgetragen werden.'),
                    htmlReady($g->name)
                ));
            }
    
            $this->relocate('course/statusgroups');
        }
    
        /**
         * Batch creation of statusgroups according to given settings.
         *
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function batch_create_action()
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            CSRFProtection::verifyUnsafeRequest();
    
            $counter = 0;
    
            // Create a number of groups, sequentially named.
            if (Request::option('mode') == 'numbering') {
                if (Request::get('numbering_type') == 2) {
                    $numbering = 'A';
                } else {
                    $numbering = Request::int('startnumber', 1);
                }
                for ($i = 0 ; $i < Request::int('number') ; $i++) {
                    $group = Statusgruppen::createOrUpdate('', 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));
                    $counter++;
                }
    
            // Create groups by course metadata, like topics, dates or lecturers.
            } else if (Request::option('mode') == 'coursedata') {
                $mode = Request::option('createmode');
                switch ($mode) {
    
                    // Create groups per topic.
                    case 'topics':
                    case 'paper_related':
                        $topics = SimpleCollection::createFromArray(
                            CourseTopic::findBySeminar_id($this->course_id)
                        )->filter(function ($topic) use ($mode) {
                            return $mode !== 'paper_related'
                                || $topic->paper_related;
                        })->orderBy('priority');
    
                        foreach ($topics as $t) {
                            $group = Statusgruppen::createOrUpdate('', _('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)
                            );
    
                            // Connect group to dates that are assigned to the given topic.
                            $dates = CourseDate::findByIssue_id($t->id);
                            foreach ($dates as $d) {
                                $d->statusgruppen->append($group);
                                $d->store();
                            }
    
                            $counter++;
                        }
    
                        break;
    
                    // Create groups per (regular and irregular) dates.
                    case 'dates':
    
                        // Find regular cycles first and create corresponding groups.
                        $cycles = SimpleCollection::createFromArray(
                            SeminarCycleDate::findBySeminar_id($this->course_id));
    
                        foreach ($cycles as $c) {
                            $cd = new CycleData($c);
    
                            $name = $c->toString();
    
                            // Append description to group title if applicable.
                            if ($c->description) {
                                $name .= ' ' . mila($c->description, 30);
                            }
    
                            // Get name of most used room and append to group title.
                            if ($rooms = $cd->getPredominantRoom()) {
                                $room_keys = array_keys($rooms);
                                $room_name = DBManager::get()->fetchOne(
                                    "SELECT `name` FROM `resources` WHERE `id` = ?",
                                    [array_pop($room_keys)]);
                                $name .= ' (' . $room_name['name'] . ')';
                            } else {
                                $free_text_predominant_rooms = array_keys($cd->getFreeTextPredominantRoom());
                                $room = trim(array_pop($free_text_predominant_rooms));
                                if ($room) {
                                    $name .= ' (' . $room . ')';
                                }
                            }
    
                            $group = Statusgruppen::createOrUpdate('', $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));
    
                            // Connect group to dates that are assigned to the given cycle.
                            foreach ($c->dates as $d) {
                                $d->statusgruppen->append($group);
                                $d->store();
                            }
    
                            $counter++;
                        }
    
                        // Now find irregular dates and create groups.
                        $dates = CourseDate::findBySeminar_id($this->course_id);
                        $singledates = array_filter($dates, function ($d) { return !((bool) $d->metadate_id); });
                        foreach ($singledates as $d) {
                            $name = $d->getFullname();
    
                            // Append description to group title if applicable.
                            if ($d->description) {
                                $name .= ' ' . mila($d->description, 30);
                            }
    
                            // Get room name and append to group title.
                            if ($room = $d->getRoomName()) {
                                $name .= ' (' . $room . ')';
                            }
    
                            $group = Statusgruppen::createOrUpdate('', $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));
    
                            $d->statusgruppen->append($group);
                            $d->store();
    
                            $counter++;
                        }
    
                        break;
    
    
                    // Create groups per lecturer.
                    case 'lecturers':
                        $lecturers = SimpleCollection::createFromArray(
                            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),
                                Request::int('selfassign', 0) + Request::int('exclusive', 0),
                                strtotime(Request::get('selfassign_start', 'now')),
                                strtotime(Request::get('selfassign_end', 0)),
                                Request::int('makefolder', 0));
                            $counter++;
                        }
    
                        break;
                }
    
            }
    
            if ($counter > 0) {
                PageLayout::postSuccess(sprintf(
                    ngettext('Eine Gruppe wurde angelegt.', '%u Gruppen wurden angelegt.', $counter),
                    $counter)
                );
            }
    
            $this->relocate('course/statusgroups');
        }
    
        /**
         * Batch action for several groups or group members at once.
         *
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function batch_action_action()
        {
            // Non-tutors may not access this.
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            // Actions for selected groups.
            if (Request::submitted('batch_groups')) {
                if ($groups = Request::getArray('groups')) {
                    $this->groups = SimpleCollection::createFromArray(
                        Statusgruppen::findMany($groups))->orderBy('position, name');
                    switch (Request::option('groups_action')) {
                        case 'edit_size':
                            PageLayout::setTitle(_('Gruppengröße bearbeiten'));
                            $this->edit_size = true;
    
                            $sizes = [];
    
                            // Check for diverging values on all groups.
                            foreach ($this->groups as $group) {
                                $sizes[$group->size] = true;
                            }
    
                            // Get default group size
                            $this->size = max(array_keys($sizes));
    
                            break;
                        case 'edit_selfassign':
                            PageLayout::setTitle(_('Selbsteintrag bearbeiten'));
                            $this->edit_selfassign = true;
    
                            $selfassign = 0;
                            $exclusive = 0;
                            $selfassign_start = [];
                            $selfassign_end = [];
    
                            // Check for diverging values on all groups.
                            foreach ($this->groups as $group) {
                                if ((int)$group->selfassign == 1) {
                                    $selfassign++;
                                }
                                if ((int)$group->selfassign === 2) {
                                    $selfassign++;
                                    $exclusive++;
                                }
                                $selfassign_start[$group->selfassign_start] = true;
                                $selfassign_end[$group->selfassign_end] = true;
                            }
    
                            if ($selfassign > 0) {
                                $this->selfassign = true;
                            }
    
                            if ($exclusive > 0) {
                                $this->exclusive = true;
                            }
    
                            // Selfassign start time set for all selected groups?
                            if (count($selfassign_start) <= 1) {
                                // Just one entry, take it as value for all.
                                $selfassign_starts = array_keys($selfassign_start);
                                $start = array_pop($selfassign_starts);
                                $this->selfassign_start = $start ? date('d.m.Y H:i', $start) : '';
                            } else {
                                // Different entries, mark this.
                                $this->selfassign_start = -1;
                            }
    
                            // Selfassign end time set for all selected groups?
                            if (count($selfassign_end) <= 1) {
                                // Just one entry, take it as value for all.
                                $selfassign_ends = array_keys($selfassign_end);
                                $end = array_pop($selfassign_ends);
                                $this->selfassign_end = $end ? date('d.m.Y H:i', $end) : '';
                            } else {
                                // Different entries, mark this.
                                $this->selfassign_end = -1;
                            }
    
                            break;
                        case 'write_message':
                            //Send the group-IDs of the selected groups
                            //to messages/write:
                            $this->redirect(
                                URLHelper::getURL(
                                    'dispatch.php/messages/write',
                                    [
                                        'default_subject' => $this->course_title,
                                        'group_ids' => $groups
                                    ]
                                )
                            );
                            break;
                        case 'delete':
                            PageLayout::setTitle(_('Gruppe(n) löschen?'));
                            $this->askdelete = true;
                            break;
                        default:
                            $this->relocate('course/statusgroups');
                    }
                } else {
                    PageLayout::postError(_('Sie haben keine Gruppe ausgewählt.'));
                }
            // Actions for selected group members.
            } else if (Request::submitted('batch_members')) {
                // Which group is selected?
                $group_id = key(Request::getArray('batch_members'));
    
                // Get selected group members.
                $group = Request::getArray('group');
                $this->members = array_keys($group[$group_id]);
    
                // Get selected action for group members.
                $actions = Request::getArray('members_action');
                $action = $actions[$group_id];
    
                switch ($action) {
                    case 'move':
                    case 'copy':
                        if ($action === 'move') {
                            PageLayout::setTitle(_('Gruppenmitglieder verschieben'));
                            $this->movemembers = true;
                        } else {
                            PageLayout::setTitle(_('Gruppenmitglieder kopieren'));
                            $this->copymembers = true;
                        }
                        $this->source_group = $group_id;
                        // Find possible target groups.
                        $this->target_groups = SimpleCollection::createFromArray(
                            Statusgruppen::findByRange_id($this->course_id))
                            ->orderBy('position, name')
                            ->filter(function ($g) use ($group_id) { return $g->id != $group_id; });
                        break;
                    case 'delete':
                        PageLayout::setTitle(_('Gruppenmitglieder entfernen'));
                        $this->deletemembers = true;
                        $this->source_group = Statusgruppen::find($group_id);
                        break;
                    case 'cancel':
                        PageLayout::setTitle(_('Personen aus Veranstaltung austragen'));
                        $this->cancelmembers = true;
                        $this->source_group = Statusgruppen::find($group_id);
                        break;
                }
    
            }
        }
    
        /**
         * Deletes several groups at once.
         *
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function batch_delete_groups_action()
        {
            CSRFProtection::verifyUnsafeRequest();
    
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            Statusgruppen::findEachMany(
                function ($group) {
                    $group->delete();
                },
                Request::optionArray('groups')
            );
            Statusgruppen::reorderPositionsForRange($this->course_id);
    
            PageLayout::postSuccess(_('Die ausgewählten Gruppen wurden gelöscht.'));
            $this->relocate('course/statusgroups');
        }
    
        /**
         * Sets data for several groups at once.
         *
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function batch_save_groups_size_action()
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            CSRFProtection::verifyUnsafeRequest();
            $groups = Statusgruppen::findMany(Request::getArray('groups'));
    
            foreach ($groups as $g) {
                Statusgruppen::createOrUpdate($g->id, $g->name,
                    $g->position, $this->course_id,
                    Request::int('size', 0),
                    $g->selfassign, $g->selfassign_start, $g->selfassign_end,
                    false);
            }
            PageLayout::postSuccess(_('Die Einstellungen der ausgewählten Gruppen wurden gespeichert.'));
            $this->relocate('course/statusgroups');
        }
    
        /**
         * Sets data for several groups at once.
         *
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function batch_save_groups_selfassign_action()
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            CSRFProtection::verifyUnsafeRequest();
            $groups = Statusgruppen::findMany(Request::getArray('groups'));
    
            $selfassign = Request::int('selfassign', 0);
            $selfassign_start = 0;
            $selfassign_end = 0;
            if ($selfassign) {
                $selfassign += Request::int('exclusive', 0);
                $selfassign_start = strtotime(Request::get('selfassign_start', 'now'));
                $selfassign_end = strtotime(Request::get('selfassign_end', 0));
            }
    
            foreach ($groups as $g) {
                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');
        }
    
        /**
         * Moves selected group members to another group.
         *
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function batch_move_members_action()
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            CSRFProtection::verifyUnsafeRequest();
            $success = 0;
            $error = 0;
            $members = Request::getArray('members');
            foreach ($members as $m) {
    
                $stored = false;
    
                // Add user to target statusgroup (if not already in there).
                if (!StatusgruppeUser::exists([Request::option('target_group'), $m])) {
                    $s = new StatusgruppeUser();
                    $s->user_id = $m;
                    $s->statusgruppe_id = Request::option('target_group');
                    if ($s->store()) {
                        $stored = true;
                    }
                }
    
                // Delete old group membership.
                $source = Request::option('source');
                if ($stored) {
                    if ($source != 'nogroup') {
                        $old = StatusgruppeUser::find([$source, $m]);
                        if ($old->delete()) {
                            $success++;
                        } else {
                            $error++;
                        }
                    } else {
                        $success++;
                    }
                }
            }
            $groupname = Statusgruppen::find(Request::option('target_group'))->name;
    
            // Everything completed successfully => success message.
            if ($success && !$error) {
                PageLayout::postSuccess(sprintf(ngettext('%u Person wurde in die Gruppe %s verschoben.',
                    '%u Personen wurden in die Gruppe %s verschoben.',
                    $success), $success, htmlReady($groupname)));
    
            // Some entries worked, some didn't => warning message.
            } else if ($success && $error) {
                PageLayout::postWarning(
                    sprintf(ngettext('%u Person wurde in die Gruppe %s verschoben.',
                    '%u Personen wurden in die Gruppe %s verschoben.',
                    $success), $success, htmlReady($groupname)) . '<br>' .
                    sprintf(ngettext('%u Person konnte nicht in die Gruppe %s verschoben werden.',
                        '%u Personen konnten nicht in die Gruppe %s verschoben werden.',
                        $error), $error, htmlReady($groupname))
                );
    
            // All is lost => error message.
            } else if ($error) {
                PageLayout::postError(sprintf(ngettext('%u Person konnte nicht in die Gruppe %s verschoben werden.',
                    '%u Personen konnten nicht in die Gruppe %s verschoben werden.',
                    $error), $error, htmlReady($groupname)));
            }
    
            $this->relocate('course/statusgroups');
        }
    
        /**
         * Copies selected group members to another group.
         *
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function batch_copy_members_action()
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            CSRFProtection::verifyUnsafeRequest();
            $success = 0;
            $members = Request::optionArray('members');
            foreach ($members as $m) {
    
                // Add user to target statusgroup (if not already in there).
                if (!StatusgruppeUser::exists(array(Request::option('target_group'), $m))) {
                    $s = new StatusgruppeUser();
                    $s->user_id = $m;
                    $s->statusgruppe_id = Request::option('target_group');
                    if ($s->store()) {
                        $success++;
                    }
                }
            }
            $groupname = Statusgruppen::find(Request::option('target_group'))->name;
    
            // Everything completed successfully => success message.
            if ($success) {
                PageLayout::postSuccess(sprintf(ngettext('%u Person wurde in die Gruppe %s kopiert.',
                    '%u Personen wurden in die Gruppe %s kopiert.',
                    $success), $success, htmlReady($groupname)));
            }
    
            $this->relocate('course/statusgroups');
        }
    
        /**
         * Removes selected group members from given group.
         *
         * @param String $group_id group to remove members from.
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function batch_delete_members_action($group_id)
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            CSRFProtection::verifyUnsafeRequest();
            $success = 0;
            $error = 0;
            $members = Request::getArray('members');
            foreach ($members as $m) {
    
                // Remove user from target statusgroup.
                $s = StatusgruppeUser::find([$group_id, $m]);
                if ($s->delete()) {
                    $success++;
                } else {
                    $error++;
                }
    
            }
            $groupname = Statusgruppen::find($group_id)->name;
    
            // Everything completed successfully => success message.
            if ($success && !$error) {
                PageLayout::postSuccess(sprintf(ngettext('%u Person wurde aus der Gruppe %s entfernt.',
                    '%u Personen wurden aus der Gruppe %s entfernt.',
                    $success), $success, htmlReady($groupname)));
    
            // Some entries worked, some didn't => warning message.
            } else if ($success && $error) {
                PageLayout::postWarning(
                    sprintf(ngettext('%u Person wurde aus der Gruppe %s entfernt.',
                        '%u Personen wurden aus der Gruppe %s entfernt.',
                        $success), $success, htmlReady($groupname)) . '<br>' .
                    sprintf(ngettext('%u Person konnte nicht aus der Gruppe %s entfernt werden.',
                        '%u Personen konnten nicht aus der Gruppe %s entfernt werden.',
                        $error), $error, htmlReady($groupname))
                );
    
            // All is lost => error message.
            } else if ($error) {
                PageLayout::postError(sprintf(ngettext('%u Person konnte nicht aus der Gruppe %s entfernt werden.',
                    '%u Personen konnten nicht aus der Gruppe %s entfernt werden.',
                    $error), $error, htmlReady($groupname)));
            }
    
            $this->relocate('course/statusgroups');
        }
    
        /**
         * Removes selected group members from the course (cancels their admission).
         *
         * @throws AccessDeniedException if access not allowed with current permission level.
         */
        public function batch_cancel_members_action()
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            CSRFProtection::verifyUnsafeRequest();
    
            $members = Request::getArray('members');
            $model = new MembersModel($this->course_id, $this->course_title);
    
            $removed_names = $model->cancelSubscription($members);
    
            PageLayout::postSuccess(
                _('Die folgenden Personen wurden aus der Veranstaltung ausgetragen'),
                $removed_names
            );
    
            $this->relocate('course/statusgroups');
        }
    
        public function order_action()
        {
            if (!Request::isPost()) {
                throw new MethodNotAllowedException();
            }
    
            if (!$this->is_tutor || $this->is_locked) {
                throw new AccessDeniedException();
            }
    
            $id    = Request::option('id');
            $index = Request::int('index');
    
            $group = Statusgruppen::find($id);
            if (!$group || $group->range_id !== $this->course_id) {
                throw new Exception('Invalid group or group does not belong to course');
            }
    
            if ($group->position == $index) {
                return;
            }
    
            if ($group->position < $index) {
                $range = [$group->position, $index];
                $adjustment = -1;
            } else {
                $range = [$index, $group->position];
                $adjustment = 1;
            }
    
            Statusgruppen::findEachBySQL(
                function ($g) use ($adjustment) {
                    $g->position = $g->position + $adjustment;
                    $g->store();
                },
                'range_id = ? AND statusgruppe_id != ? AND position BETWEEN ? AND ?',
                [$this->course_id, $id, $range[0], $range[1]]
            );
    
            $group->position = $index;
            $group->store();
    
            $this->render_nothing();
        }
    
        public function course_groups_hide_action($state)
        {
            if (!$this->is_tutor) {
                throw new AccessDeniedException();
            }
    
            $this->config->store('COURSE_MEMBERGROUPS_HIDE', $state);
    
            $this->redirect('course/statusgroups');
        }
    
        private function sortMembers(SimpleCollection $members, $sort_by = null, $sort_dir = null)
        {
            $order = "nachname asc, vorname asc";
            if ($sort_by && $sort_dir) {
                $order = "{$sort_by} {$sort_dir}, {$order}";
            }
            return $members->orderBy($order);
    
        }
    }