* @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 class Course_StatusgroupsController extends AuthenticatedController { public function before_filter(&$action, &$args) { parent::before_filter($action, $args); 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 = $GLOBALS['perm']->have_studip_perm('dozent', $this->course_id); $this->is_tutor = $GLOBALS['perm']->have_studip_perm('tutor', $this->course_id); $this->is_autor = $GLOBALS['perm']->have_studip_perm('autor', $this->course_id); // 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 = $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; } else { $groupdata['load'] = false; } if (!$this->is_tutor && $g->userMayJoin($GLOBALS['user']->id)) { $joinable = true; } else { $joinable = false; } $groupdata['joinable'] = $joinable; $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]) ]); $this->ungrouped_count = $ungrouped_count[0]; if ($this->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' => $this->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; } else { $this->nogroupmembers = []; } // 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( _('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('add') )->asDialog('size=auto'); } if (Config::get()->EXPORT_ENABLE) { $export = new ExportWidget(); $export->addLink( _('Als Excel-Datei exportieren'), URLHelper::getURL('dispatch.php/course/statusgroups/export', [ 'course_id' => $this->course_id, 'format' => 'xlsx', ]), Icon::create('export') ); $export->addLink( _('Als CSV-Datei exportieren'), URLHelper::getURL('dispatch.php/course/statusgroups/export', [ 'course_id' => $this->course_id, 'format' => 'csv', ]), Icon::create('export') ); $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_1down') ); } $sidebar->addWidget($actions); } /** * */ public function export_action() { $export_format = Request::get('format'); if ($export_format !== 'csv' && $export_format !== 'xlsx') { throw new Exception('Wrong format'); } $course = Course::findCurrent(); $header = [ _('Gruppe'), _('geschlecht'), _('Titel'), _('Vorname'), _('Nachname'), _('Titel nachgestellt'), _('Nutzername'), _('Privatadresse'), _('Privatnr.'), _('E-Mail'), _('Anmeldedatum'), _('Matrikelnummer'), _('Studiengänge') ]; $groups = Statusgruppen::findBySeminar_id($this->course_id); $result = []; if ($groups) { $assigned_with_group = []; foreach ($groups as $group) { foreach ($group->members->orderBy('nachname,vorname') as $member) { $assigned_with_group[$member->user_id] = true; $result[] = $member->getExportData(); } } $members = $course->members->filter(function($group_member) use ($assigned_with_group) { return !array_key_exists($group_member->user_id, $assigned_with_group); })->orderBy('nachname,vorname'); foreach ($members as $member) { $data = ['gruppe' => _('keiner Funktion oder Gruppe zugeordnet')] + $member->getExportData(); unset($data['status']); unset($data['position']); $result[$member->user_id] = $data; } } $filename = FileManager::cleanFileName(_('Gruppenliste') . ' ' . $this->course_title . '.' . $export_format); $this->render_spreadsheet($header, $result, $export_format, $filename); } /** * Fetches the members of the given group. * * @param String $group_id the statusgroup to get members for. */ public function getgroup_action($group_id) { $this->sort_by = ''; if ($group_id != 'nogroup') { $this->group = Statusgruppen::find($group_id); $this->members = []; $this->invisible = 0; if (!empty($this->group->members)) { //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])) { CourseMember::insertCourseMember($this->course_id, $a, User::find($a)->perms == 'user' ? 'user' : 'autor'); } $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. */ $selfassign = Request::int('selfassign', 0); $start_time = 0; $end_time = 0; if ($selfassign) { $end_time = strtotime(Request::get('selfassign_end', 'now')); $start_time = strtotime(Request::get('selfassign_start', 'now')); if ($end_time <= $start_time) { $end_time = 0; } } $position = null; if (Statusgruppen::exists($group_id)) { $position = Statusgruppen::find($group_id)->position; } // Exclusive entry makes sense only when selfassign is set in general. if ($selfassign !== 0) { $selfassign += Request::int('exclusive', 0); // Selfassign is not set but exclusive selfassign or some timeframe -> show warning message } else if ( Request::bool('exclusive') || $start_time || $end_time ) { PageLayout::postWarning(_('Einstellungen zum Eintrag in eine Gruppe oder zum Eintragszeitraum können ' . 'nur gespeichert werden, wenn der Selbsteintrag aktiviert ist.')); } $group = Statusgruppen::createOrUpdate( $group_id, Request::get('name'), $position, $this->course_id, Request::int('size', 0), $selfassign, $start_time, $end_time, Request::bool('makefolder', false), Request::getArray('dates'), Request::bool('blubber', false) ); $group->description = trim(Request::get('description')) ?: null; $group->store(); 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) )); } $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++) { 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::bool('makefolder', false) ); $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( 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::bool('makefolder', false) ); // 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) { $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 ($room = $c->getMostBookedRoom()) { $name .= ' (' . $room->name . ')'; } else { $room = $c->getMostUsedFreetextRoomName(); if ($room) { $name .= ' (' . $room . ')'; } } $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::bool('makefolder', false) ); // 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(); // Get room name and append to group title. if ($room = $d->getRoomName()) { $name .= ' (' . $room . ')'; } $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::bool('makefolder', false) ); $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( 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::bool('makefolder', false) ); $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')) { $groups = Request::getArray('groups'); if ($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 'copy': $this->keepRequest(); $this->redirect($this->copyURL()); return; 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, $g->hasFolder(), null, $g->hasBlubber() ); } 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, $g->hasFolder(), null, $g->hasBlubber() ); } 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)) . '
' . 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)) . '
' . 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'); $course = Course::find($this->course_id); $removed_users = []; $errors = []; User::findEachMany( function (User $user) use ($course, &$errors, &$removed_users) { try { $course->deleteMember($user, true); $removed_users[] = $user->getFullName(); } catch (Exception $e) { $errors[] = $e->getMessage(); } }, array_column($members, 'user_id') ); if (!empty($errors)) { PageLayout::postError( _('Die folgenden Fehler traten beim Austragen von Personen auf:'), $errors ); } if (!empty($removed_users)) { if (count($removed_users) <= 5) { PageLayout::postSuccess( _('Die folgenden Personen wurden ausgetragen:'), $removed_users ); } else { PageLayout::postSuccess( _('%u Personen wurden ausgetragen.'), count($removed_users) ); } } $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'); } $groups = $group->course->statusgruppen ->filter(fn(Statusgruppen $grp) => $grp->id !== $group->id) ->getArrayCopy(); array_splice($groups, $index, 0, [$group]); foreach ($groups as $i => $g) { $g->position = $i; $g->store(); } $this->render_nothing(); } 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); } public function details_action(Statusgruppen $group): void { $course = Course::findCurrent(); if ($course->id !== $group->range_id) { throw new AccessDeniedException(); } PageLayout::setTitle(sprintf( _('Personen der Gruppe %s'), $group->name )); $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( '%s', URLHelper::getLink('dispatch.php/course/go', ['to' => $target_course_id], true), htmlReady($target_course->getFullName()) ), )); $this->redirect($this->indexURL()); } }