<?php /** * grouping.php - grouping of courses * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * @author Thomas Hackl <thomas.hackl@uni-passau.de> * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * @category Stud.IP * @package admin */ require_once 'lib/export/export_studipdata_func.inc.php'; // Funktionne für den Export require_once 'lib/export/export_linking_func.inc.php'; /** * @addtogroup notifications * * Adding or removing a course to a group triggers a CourseDidAddToGroup or * CourseDidRemoveFromGroup notification. The parent and the child course IDs * are transmitted as subjects of the notification. */ class Course_GroupingController extends AuthenticatedController { /** * common tasks for all actions */ public function before_filter(&$action, &$args) { parent::before_filter($action, $args); checkObject(); $this->course = Course::findCurrent(); // Allow only tutor and upwards if (!$GLOBALS['perm']->have_studip_perm('tutor', $this->course->id)) { throw new AccessDeniedException(_('Sie haben leider nicht die notwendige Berechtigung für diese Aktion.')); } } /** * This course belongs to a parent or can be assigned to one. */ public function parent_action() { PageLayout::setTitle($this->course->getFullname() . ' - ' . _('Zuordnung zu Hauptveranstaltung')); Navigation::activateItem('/course/admin/parent'); $this->parent = $this->course->parent; // Prepare context for MyCoursesSearch... if ($GLOBALS['perm']->have_perm('root')) { $parameters = [ 'semtypes' => SemType::getNonGroupingSemTypes(), 'exclude' => [$this->course->parent_course ?: ''], 'semesters' => [$this->course->start_semester->id], ]; } elseif ($GLOBALS['perm']->have_perm('admin')) { $parameters = [ 'semtypes' => SemType::getNonGroupingSemTypes(), 'institutes' => array_map(function ($i) { return $i['Institut_id']; }, Institute::getMyInstitutes()), 'exclude' => [$this->course->parent_course ?: ''], 'semesters' => [$this->course->start_semester->id], ]; } else { $parameters = [ 'userid' => $GLOBALS['user']->id, 'semtypes' => SemType::getNonGroupingSemTypes(), 'exclude' => [$this->course->parent_course ?: ''], 'semesters' => [$this->course->start_semester->id], ]; } // Provide search object for finding groupable courses. $find = MyCoursesSearch::get('Seminar_id', $GLOBALS['perm']->get_perm(), $parameters); $this->search = QuickSearch::get('parent', $find)->setInputClass('target-seminar'); } /** * This course can be a parent with one or more children. */ public function children_action() { PageLayout::setTitle($this->course->getFullname() . ' - ' . _('Unterveranstaltungen')); Navigation::activateItem('/course/admin/children'); $this->children = $this->course->children; $excluded_course_ids = array_merge( [$this->course->id], $this->children->pluck('id') ); // Prepare context for MyCoursesSearch... if ($GLOBALS['perm']->have_perm('root')) { $parameters = [ 'semtypes' => array_merge(studygroup_sem_types(), SemType::getGroupingSemTypes()), 'exclude' => $excluded_course_ids, 'semesters' => [$this->course->start_semester->id], ]; } else if ($GLOBALS['perm']->have_perm('admin')) { $parameters = [ 'semtypes' => array_merge(studygroup_sem_types(), SemType::getGroupingSemTypes()), 'institutes' => array_map(function ($i) { return $i['Institut_id']; }, Institute::getMyInstitutes()), 'exclude' => $excluded_course_ids, 'semesters' => [$this->course->start_semester->id], ]; } else { $parameters = [ 'userid' => $GLOBALS['user']->id, 'semtypes' => array_merge(studygroup_sem_types(), SemType::getGroupingSemTypes()), 'exclude' => $excluded_course_ids, 'semesters' => [$this->course->start_semester->id] ]; } // Provide search object for finding groupable courses. $find = MyCoursesSearch::get('Seminar_id', $GLOBALS['perm']->get_perm(), $parameters); $this->search = QuickSearch::get('child', $find)->setInputClass('target-seminar'); if ($GLOBALS['perm']->have_perm(Config::get()->SEM_CREATE_PERM)) { $sidebar = Sidebar::get(); $actions = new ActionsWidget(); $actions->addLink( _('Unterveranstaltungen anlegen'), $this->url_for('course/grouping/create_children'), Icon::create('add', 'clickable') )->asDialog('size=auto'); $sidebar->addWidget($actions); } } /** * Show a list of all members, grouped by child course. */ public function members_action() { PageLayout::setTitle(sprintf( '%s - %s', Course::findCurrent()->getFullname(), _('Teilnehmende in Unterveranstaltungen') )); Navigation::activateItem('course/members/children'); $this->courses = SimpleCollection::createFromArray( Course::findByParent_Course( $this->course->id, 'ORDER BY ' . (Config::get()->IMPORTANT_SEMNUMBER ? 'veranstaltungsnummer, name' : 'name') ) ); if (count($this->course->children) > 0) { $query = "SELECT DISTINCT s.`user_id` FROM `seminar_user` s WHERE s.`Seminar_id` = :parent AND NOT EXISTS ( SELECT `user_id` FROM `seminar_user` WHERE `user_id` = s.`user_id` AND `Seminar_id` IN (:children) ) LIMIT 1"; $this->parentOnly = DBManager::get()->fetchFirst($query, [ 'parent' => $this->course->id, 'children' => $this->course->children->pluck('seminar_id'), ]); } else { $this->parentOnly = true; } // Write message to all participants. $sidebar = Sidebar::get(); $actions = new ActionsWidget(); $actions->addLink( _('Nachricht an alle Teilnehmenden schreiben'), $this->url_for('messages/write', [ 'filter' => 'all', 'course_id' => $this->course->id, 'default_subject' => '[' . $this->course->getFullname() . ']', ]), Icon::create('mail', 'clickable') )->asDialog('size=auto'); $sidebar->addWidget($actions); // Export all participants. if (Config::get()->EXPORT_ENABLE) { $widget = new ExportWidget(); // create csv-export link $csvExport = export_link( $this->course->id, 'person', htmlReady(sprintf( '%s %s', get_title_for_status('autor', 2), $this->course->getFullname() )), 'csv', 'csv-teiln', '', _('Teilnehmendenliste als CSV-Dokument exportieren'), 'passthrough' ); $widget->addLinkFromhtmL( $csvExport, Icon::create('file-office', 'clickable') ); // create csv-export link $rtfExport = export_link( $this->course->id, 'person', htmlReady(sprintf( '%s %s', get_title_for_status('autor', 2), $this->course->getFullname() )), 'rtf', 'rtf-teiln', '', _('Teilnehmendenliste als rtf-Dokument exportieren'), 'passthrough' ); $widget->addLinkFromHTML( $rtfExport, Icon::create('file-text', 'clickable') ); $sidebar->addWidget($widget); } } /** * Shows members of given child course. * @param $course_id */ public function child_course_members_action($course_id) { $this->child = Course::find($course_id); } /** * Collect users which are only in parent course and not in any child. */ public function parent_only_members_action() { if (count($this->course->children) > 0) { $childrens_users = DBManager::get()->fetchFirst( "SELECT DISTINCT `user_id` FROM `seminar_user` WHERE `Seminar_id` IN (:children)", ['children' => $this->course->children->pluck('seminar_id')] ); $this->parentOnly = $this->course->members->findBy('user_id', $childrens_users, '!='); } else { $this->parentOnly = $this->course->members; } } /** * Batch actions, like message sending, moving or removing for several members at once. */ public function action_action() { CSRFProtection::verifyUnsafeRequest(); if (Request::submitted('single_action')) { list($course_id, $permission) = explode('-', Request::get('single_action')); $selected = Request::getArray('members'); $users = SimpleORMapCollection::createFromArray( User::findMany($selected[$course_id][$permission]) ); switch (Request::option('selected_single_action_' . $course_id . '_' . $permission)) { case 'message': $this->redirect($this->url_for('messages/write', [ 'rec_uname' => $users->pluck('username'), 'default_subject' => '[' . Course::find($course_id)->getFullname() . ']', ])); break; case 'move': $this->redirect($this->url_for('course/grouping/move_members_target', $course_id, [ 'users' => $selected[$course_id][$permission], ])); break; case 'remove': $this->flash['users'] = $selected[$course_id][$permission]; $this->relocate('course/grouping/remove_members', $course_id); break; default: $this->relocate('course/grouping/members'); } } elseif (Request::submitted('courses_action')) { $this->flash['action'] = Request::option('action'); $this->flash['courses'] = Request::getArray('courses'); $this->redirect($this->url_for('course/grouping/find_members_to_add')); } else { $this->relocate('course/grouping/members'); } } /** * Select a course to move selected persons to. * @param string $source_id id of source course * @param string $user_id optional id of single user to move */ public function move_members_target_action($source_id, $user_id = '') { PageLayout::setTitle(_('Personen verschieben')); $this->source_id = $source_id; $this->users = $user_id ? [$user_id] : Request::getArray('users'); $this->targets = count($this->course->children) > 0 ? $this->course->children->findBy('id', $source_id, '!=') : new SimpleORMapCollection(); } /** * Move members to another cours * @param string $source_id The course to move members from. */ public function move_members_action($source_id) { CSRFProtection::verifyUnsafeRequest(); $source = Seminar::getInstance($source_id); $target = Seminar::getInstance(Request::option('target')); $success = 0; $fail = 0; foreach (Request::getArray('users') as $user) { $m = CourseMember::find([$source_id, $user]); $status = $m->status; if ($source->deleteMember($user)) { $target->addMember($user, $status); $success += 1; } else { $fail += 1; } } if ($success > 0) { PageLayout::postSuccess(sprintf(_('%u Personen wurden verschoben.'), $success)); } if ($fail > 0) { PageLayout::postError(sprintf(_('%u Personen konnten nicht verschoben werden.'), $fail)); } $this->relocate('course/grouping/members'); } /** * Removes selected members from given course. * @param string $course_id the course to remove members from */ public function remove_members_action($course_id, $user_id = null) { $s = Seminar::getInstance($course_id); $success = 0; $fail = 0; $users = $user_id ? [$user_id] : $this->flash['users']; foreach ($users as $user) { if ($s->deleteMember($user)) { $success += 1; } else { $fail += 1; } } if ($success > 0) { PageLayout::postSuccess(sprintf(_('%u Personen wurden entfernt.'), $success)); } if ($fail > 0) { PageLayout::postError(sprintf(_('%u Personen konnten nicht entfernt werden.'), $fail)); } $this->relocate('course/grouping/members'); } /** * Select people to add to the given courses. */ public function find_members_to_add_action() { switch ($this->flash['action']) { case 'add_dozent': $this->permission = 'dozent'; $title = get_title_for_status('dozent', 2, $this->course->status); $perms = ['dozent']; break; case 'add_deputy': $this->permission = 'deputy'; $title = _('Vertretung/en'); $perms = ['dozent']; break; case 'add_tutor': $this->permission = 'tutor'; $title = get_title_for_status('tutor', 2, $this->course->status); $perms = ['tutor', 'dozent']; break; case 'add_autor': default: $this->permission = 'autor'; $title = get_title_for_status('autor', 2, $this->course->status); $perms = ['autor', 'tutor', 'dozent']; break; } PageLayout::setTitle(sprintf(_('%s hinzufügen'), $title)); $this->courses = $this->flash['courses']; $searchtype = new PermissionSearch( 'user', sprintf(_('%s suchen'), $title), 'user_id', ['permission' => $perms, 'exclude_user' => []] ); $this->search = QuickSearch::get('user_id', $searchtype) ->withoutButton() ->setInputStyle('width: 75%') ->fireJSFunctionOnSelect('STUDIP.Members.addPersonToSelection'); } /** * Assign a (new) parent to the current course. */ public function assign_parent_action() { if ($parent = Request::option('parent')) { $this->course->parent_course = $parent; NotificationCenter::postNotification('CourseWillAddToGroup', $this->course->id, $parent); if ($this->course->store()) { $this->sync_users($parent, $this->course->id); NotificationCenter::postNotification('CourseDidAddToGroup', $this->course->id, $parent); StudipLog::log('SEM_ADD_TO_GROUP', $this->course->id, $parent, null, null, $GLOBALS['user']->id); PageLayout::postSuccess(_('Die Hauptveranstaltung wurde zugeordnet.')); } else { PageLayout::postError(_('Die Hauptveranstaltung konnte nicht zugeordnet werden.')); } } else { PageLayout::postError(_('Bitte geben Sie eine Veranstaltung an, zu der zugeordnet werden soll.')); } $this->relocate('course/grouping/parent'); } /** * Remove this courses' current parent. */ public function unassign_parent_action() { CSRFProtection::verifyUnsafeRequest(); $parent = $this->course->parent_course; $this->course->parent_course = null; NotificationCenter::postNotification('CourseWillRemoveFromGroup', $this->course->id, $parent); if ($this->course->store()) { NotificationCenter::postNotification('CourseDidRemoveFromGroup', $this->course->id, $parent); StudipLog::log('SEM_DEL_FROM_GROUP', $this->course->id, $parent, null, null, $GLOBALS['user']->id); PageLayout::postSuccess(_('Die Zuordnung zur Hauptveranstaltung wurde entfernt.')); } else { PageLayout::postError(_('Die Zuordnung zur Hauptveranstaltung konnte nicht entfernt werden.')); } $this->relocate('course/grouping/parent'); } /** * Assign a (new) child to the current course. */ public function assign_child_action() { CSRFProtection::verifyUnsafeRequest(); if ($child = Request::option('child')) { $child_course = Course::find($child); $child_course->parent_course = $this->course->id; NotificationCenter::postNotification('CourseWillAddToGroup', $child, $this->course->id); if ($child_course->store()) { $this->sync_users($this->course->id, $child); NotificationCenter::postNotification('CourseDidAddToGroup', $child, $this->course->id); StudipLog::log('SEM_ADD_TO_GROUP', $child, $this->course->id, null, null, $GLOBALS['user']->id); PageLayout::postSuccess(_('Die Unterveranstaltung wurde hinzugefügt.')); } else { PageLayout::postError(_('Die Unterveranstaltung konnte nicht hinzugefügt werden.')); } } else { PageLayout::postError(_('Bitte geben Sie eine Veranstaltung an, die als Unterveranstaltung hinzugefügt werden soll.')); } $this->relocate('course/grouping/children'); } /** * Remove the given child. * @param String $id The course ID to remove as child. */ public function unassign_child_action($id) { $child = Course::find($id); $child->parent_course = null; NotificationCenter::postNotification('CourseWillRemoveFromGroup', $child, $this->course->id); if ($child->store()) { NotificationCenter::postNotification('CourseDidRemoveFromGroup', $child, $this->course->id); StudipLog::log('SEM_DEL_FROM_GROUP', $id, $this->course->id, null, null, $GLOBALS['user']->id); PageLayout::postSuccess(_('Die Unterveranstaltung wurde entfernt.')); } else { PageLayout::postError(_('Die Unterveranstaltung konnte nicht entfernt werden.')); } $this->relocate('course/grouping/children'); } /** * Batch creation of several subcourses at once. */ public function create_children_action() { } /** * Add selected members to given courses * with the given permission level. */ public function add_members_action() { CSRFProtection::verifySecurityToken(); $fail = []; // Iterate over selected courses... foreach (Request::optionArray('courses') as $course) { $sem = Seminar::getInstance($course); // ... and selected users. foreach (Request::optionArray('users') as $user) { // Try to add deputies. if (Request::option('permission') == 'deputy') { // If not already deputy, create new entry. if (!Deputy::exists([$course, $user])) { $d = new Deputy(); $d->range_id = $course; $d->user_id = $user; // Error on storing. if (!$d->store()) { $fail[$sem->getFullname()][] = $user; // Check if new deputy was regular member before, remove entry. } else { $m = CourseMember::find([$course, $user]); // Could not delete old course membership, remove deputy entry. if ($m && !$m->delete()) { $d->delete(); $fail[$sem->getFullname()][] = $user; } } } // Add member with given permission. } elseif (!$sem->addMember($user, Request::option('permission'))) { $fail[$sem->getFullname()][] = $user; } } } if (count($fail) > 0) { PageLayout::postError( _('In folgenden Veranstaltungen sind Probleme beim Eintragen der gewünschten Personen aufgetreten:'), array_keys($fail) ); } else { PageLayout::postSuccess(ngettext( 'Die gewählte Person wurde eingetragen.', 'Die gewählten Personen wurden eingetragen.', count(Request::optionArray('users')) )); } $this->relocate('course/grouping/members'); } /** * Sychronizes members between parent and child course. * @param string $parent_id parent course ID * @param string $child_id child course ID */ private function sync_users($parent_id, $child_id) { $sem = Seminar::getInstance($parent_id); $csem = Seminar::getInstance($child_id); /* * Find users that are in current course but not in parent. */ $query = "SELECT u.`user_id` FROM `seminar_user` u WHERE u.`Seminar_id` = :course AND u.`status` = :status AND NOT EXISTS ( SELECT `user_id` FROM `seminar_user` WHERE `Seminar_id` = :parent AND `status` = :status AND `user_id` = u.`user_id` )"; $diff = DBManager::get()->prepare($query); /* * Before synchronizing the lecturers, we add all institutes * from child course. */ $sem->setInstitutes(array_merge($sem->getInstitutes(), $csem->getInstitutes())); /* * Synchronize all members (including lecturers, tutors * and deputies with parent course. */ foreach (words('user autor tutor dozent') as $permission) { $diff->execute([ 'course' => $child_id, 'status' => $permission, 'parent' => $parent_id ]); foreach ($diff->fetchFirst() as $user) { $sem->addMember($user, $permission); // Add default deputies of current user if applicable. if ($permission === 'dozent' && Config::get()->DEPUTIES_ENABLE && Config::get()->DEPUTIES_DEFAULTENTRY_ENABLE) { foreach (Deputy::findByRange_id($user) as $deputy) { if (!Deputy::exists([$parent_id, $deputy->user_id]) && !CourseMember::exists([$parent_id, $deputy->user_id])) { $d = new Deputy(); $d->range_id = $parent_id; $d->user_id = $user; $d->store(); } } } } } // Deputies. if (Config::get()->DEPUTIES_ENABLE) { foreach (Deputy::findByRange_id($child_id) as $deputy) { if (!Deputy::exists([$parent_id, $deputy->user_id]) && !CourseMember::exists([$parent_id, $deputy->user_id])) { $d = new Deputy(); $d->range_id = $parent_id; $d->user_id = $deputy->user_id; $d->store(); } } } } }