diff --git a/app/controllers/admin/courses.php b/app/controllers/admin/courses.php index edf529b4ce4e0232104cf5dcf4833c1c0b1e709a..19a577f00975beaa56be849547050172ffbe96d9 100644 --- a/app/controllers/admin/courses.php +++ b/app/controllers/admin/courses.php @@ -29,43 +29,6 @@ require_once 'lib/archiv.inc.php'; //for lastActivity in getCourses() method class Admin_CoursesController extends AuthenticatedController { - /** - * This helper method retrieves the values of datafields when - * the user started a search for courses matching a specific value of - * one or more datafields. - * This method also checks if a datafield is activated by the user - * and will reject any value for datafields that aren't activated by the user. - * - * @return Array Associative array, consisting of datafield names - * (as array keys) and values for those datafields. - */ - private function getDatafieldFilters() - { - //first get the active datafields of the user: - $userSelectedElements = $this->getActiveElements(); - $activeDatafields = $userSelectedElements['datafields'] ?? []; - - if (!$activeDatafields) { - return []; - } - - //Ok, we have a list of active datafields whose value may be searched for. - //We must check for the request parameters (df_$DATAFIELD_ID) - //and return their IDs with a value. - - $searchedDatafields = []; - - foreach ($activeDatafields as $activeField) { - $requestParamValue = Request::get('df_'.$activeField); - if ($requestParamValue) { - $searchedDatafields[$activeField] = $requestParamValue; - } - } - - return $searchedDatafields; - } - - /** * This method returns the appropriate widget for the given datafield. * @@ -79,6 +42,8 @@ class Admin_CoursesController extends AuthenticatedController //The current user is allowed to see this datafield. //Now we must distinguish between the different types of data fields: + $datafields_filters = $GLOBALS['user']->cfg->ADMIN_COURSES_DATAFIELDS_FILTERS; + $type = $datafield->type; if ($type == 'bool') { @@ -86,14 +51,15 @@ class Admin_CoursesController extends AuthenticatedController $checkboxWidget = new OptionsWidget($datafield->name); $checkboxWidget->addCheckbox( _('Feld gesetzt'), - Request::bool('df_'.$datafield->id, false), + Request::bool('df_'.$datafield->id, $datafields_filters[$datafield->id] ?? false), URLHelper::getURL( 'dispatch.php/admin/courses/index', ['df_'.$datafield->id => '1'] ), URLHelper::getURL( 'dispatch.php/admin/courses/index' - ) + ), + ['onclick' => "$(this).toggleClass(['options-checked', 'options-unchecked']); STUDIP.AdminCourses.App.changeFilter({'df_".$datafield->id."': $(this).hasClass('options-checked') ? 1 : 0}); return false;"] ); return $checkboxWidget; } elseif ($type == 'selectbox' || $type == 'radio' || $type == 'selectboxmultiple') { @@ -103,21 +69,27 @@ class Admin_CoursesController extends AuthenticatedController ))); if ($options) { - $options = array_merge( - [' ' => '(' . _('keine Auswahl') . ')'], - $options - ); - $selectWidget = new SelectWidget( $datafield->name, '?', 'df_' . $datafield->id ); - foreach($options as $option) { + $selectWidget->addElement( + new SelectElement( + '', + '(' . _('keine Auswahl') . ')' + ) + ); + foreach ($options as $option) { $selectWidget->addElement( - new SelectElement($option, $option, Request::get('df_'.$datafield->id) == $option) + new SelectElement( + $option, + $option, + Request::get('df_'.$datafield->id, $datafields_filters[$datafield->id] ?? null) === $option + ) ); } + $selectWidget->setOnSubmitHandler("STUDIP.AdminCourses.App.changeFilter({'df_".$datafield->id."': $(this).find('select').val()}); return false;"); return $selectWidget; } return null; @@ -127,8 +99,13 @@ class Admin_CoursesController extends AuthenticatedController $textWidget->setTitle($datafield->name); $textWidget->addNeedle( '', - 'df_'.$datafield->id + 'df_'.$datafield->id, + false, + null, + null, + $datafields_filters[$datafield->id] ); + $textWidget->setOnSubmitHandler("STUDIP.AdminCourses.App.changeFilter({'df_".$datafield->id."': $(this).find('input').val()}); return false;"); return $textWidget; } } @@ -144,7 +121,7 @@ class Admin_CoursesController extends AuthenticatedController * @param string courseTypeFilterConfig The selected value for the course type filter field, defaults to null. * @return null This method does not return any value. */ - private function buildSidebar($courseTypeFilterConfig = null) + private function buildSidebar() { /* Depending on the elements the user has selected @@ -153,6 +130,10 @@ class Admin_CoursesController extends AuthenticatedController */ $visibleElements = $this->getActiveElements(); + $this->sem_create_perm = in_array(Config::get()->SEM_CREATE_PERM, ['root', 'admin', 'dozent']) + ? Config::get()->SEM_CREATE_PERM + : 'dozent'; + $sidebar = Sidebar::get(); /* @@ -179,13 +160,13 @@ class Admin_CoursesController extends AuthenticatedController $this->setSemesterSelector(); } if (!empty($visibleElements['stgteil'])) { - $this->setStgteilSelector(); + Sidebar::Get()->addWidget($this->getStgteilSelector(), 'filter_stgteil'); } if (!empty($visibleElements['courseType'])) { - $this->setCourseTypeWidget($courseTypeFilterConfig); + $this->setCourseTypeWidget(); } if (!empty($visibleElements['teacher'])) { - $this->setTeacherWidget(); + Sidebar::Get()->addWidget($this->getTeacherWidget(), 'filter_teacher'); } //if there are datafields in the list, draw their input fields, too: @@ -213,7 +194,7 @@ class Admin_CoursesController extends AuthenticatedController //this shall be visible in every case: - $this->setActionsWidget($this->selected_action); + $this->setActionsWidget(); //actions: always visible, too @@ -296,20 +277,15 @@ class Admin_CoursesController extends AuthenticatedController $this->semester = Semester::find($GLOBALS['user']->cfg->MY_COURSES_SELECTED_CYCLE); } - if (Request::submitted('search')) { - $GLOBALS['user']->cfg->store('ADMIN_COURSES_SEARCHTEXT', Request::get('search')); - } if (Request::get('reset-search')) { $GLOBALS['user']->cfg->delete('ADMIN_COURSES_SEARCHTEXT'); } - if (Request::submitted('teacher_filter')) { - $GLOBALS['user']->cfg->store('ADMIN_COURSES_TEACHERFILTER', Request::option('teacher_filter')); - } PageLayout::setHelpKeyword('Basis.Veranstaltungen'); PageLayout::setTitle(_('Verwaltung von Veranstaltungen und Einrichtungen')); // Add admission functions. PageLayout::addScript('studip-admission.js'); + $this->max_show_courses = 500; } /** @@ -317,67 +293,457 @@ class Admin_CoursesController extends AuthenticatedController */ public function index_action() { - $this->sem_create_perm = in_array(Config::get()->SEM_CREATE_PERM, ['root', 'admin', 'dozent']) - ? Config::get()->SEM_CREATE_PERM - : 'dozent'; + $this->fields = $this->getViewFilters(); + $this->sortby = $GLOBALS['user']->cfg->MEINE_SEMINARE_SORT ?? 'name'; + $this->sortflag = $GLOBALS['user']->cfg->MEINE_SEMINARE_SORT_FLAG ?? 'ASC'; + + $this->buildSidebar(); + + PageLayout::addHeadElement('script', [ + 'type' => 'text/javascript', + ], sprintf( + 'window.AdminCoursesStoreData = %s;', + json_encode($this->getStoreData()) + )); + } + + private function getStoreData(): array + { + $configuration = User::findCurrent()->getConfiguration(); + + $institut_id = $configuration->MY_INSTITUTES_DEFAULT && $configuration->MY_INSTITUTES_DEFAULT !== 'all' + ? $configuration->MY_INSTITUTES_DEFAULT + : null; + + + return [ + 'setActivatedFields' => $this->getFilterConfig(), + 'setActionArea' => $configuration->MY_COURSES_ACTION_AREA ?? '1', + 'setFilter' => array_filter(array_merge( + $this->getDatafieldFilters(), + [ + 'institut_id' => $institut_id, + 'search' => $configuration->ADMIN_COURSES_SEARCHTEXT, + 'semester_id' => $configuration->MY_COURSES_SELECTED_CYCLE, + 'course_type' => $configuration->MY_COURSES_TYPE_FILTER, + 'stgteil' => $configuration->MY_COURSES_SELECTED_STGTEIL, + 'teacher_filter' => $configuration->ADMIN_COURSES_TEACHERFILTER, + ] + )), + ]; + } + + private function getDatafieldFilters(): array + { + $visibleElements = $this->getActiveElements(); + if (empty($visibleElements['datafields'])) { + return []; + } + + $datafields = DataField::getDataFields('sem'); + $config = $GLOBALS['user']->cfg->ADMIN_COURSES_DATAFIELDS_FILTERS; - // get courses only if institutes available - $this->actions = $this->getActions(); + $datafields = array_filter($datafields, function (Datafield $datafield) use ($visibleElements, $config) { + return in_array($datafield->id, $visibleElements['datafields']) + && isset($config[$datafield->id]); + }); - $config_my_course_type_filter = $GLOBALS['user']->cfg->MY_COURSES_TYPE_FILTER; + $result = []; + foreach ($datafields as $datafield) { + $result["df_{$datafield->id}"] = $config[$datafield->id]; + } + return $result; + } - // Get the view filter - $this->view_filter = $this->getFilterConfig(); + public function search_action() + { + $activeSidebarElements = $this->getActiveElements(); + if (Request::get('search')) { + $GLOBALS['user']->cfg->store('ADMIN_COURSES_SEARCHTEXT', Request::get('search')); + } else { + $GLOBALS['user']->cfg->delete('ADMIN_COURSES_SEARCHTEXT'); + } + if (Request::option('institut_id') && Request::option('institut_id') !== 'all') { + $GLOBALS['user']->cfg->store('MY_INSTITUTES_DEFAULT', Request::option('institut_id')); + } else { + $GLOBALS['user']->cfg->delete('MY_INSTITUTES_DEFAULT'); + } - if (Request::get('sortFlag')) { - $GLOBALS['user']->cfg->store('MEINE_SEMINARE_SORT_FLAG', Request::get('sortFlag') === 'asc' ? 'DESC' : 'ASC'); + if (Request::option('semester_id')) { + $GLOBALS['user']->cfg->store('MY_COURSES_SELECTED_CYCLE', Request::option('semester_id')); + } else { + $GLOBALS['user']->cfg->delete('MY_COURSES_SELECTED_CYCLE'); } - if (Request::option('sortby')) { - $GLOBALS['user']->cfg->store('MEINE_SEMINARE_SORT', Request::option('sortby')); + + if (Request::option('course_type') && Request::option('course_type') !== 'all') { + $GLOBALS['user']->cfg->store('MY_COURSES_TYPE_FILTER', Request::option('course_type')); + } else { + $GLOBALS['user']->cfg->delete('MY_COURSES_TYPE_FILTER'); } - $this->selected_action = $GLOBALS['user']->cfg->MY_COURSES_ACTION_AREA; - if (is_null($this->selected_action) || (!is_numeric($this->selected_action) && !class_exists($this->selected_action))) { - $this->selected_action = 1; + if (Request::option('stgteil')) { + $GLOBALS['user']->cfg->store('MY_COURSES_SELECTED_STGTEIL', Request::option('stgteil')); + } else { + $GLOBALS['user']->cfg->delete('MY_COURSES_SELECTED_STGTEIL'); } - $this->sortby = $GLOBALS['user']->cfg->MEINE_SEMINARE_SORT; - $this->sortFlag = $GLOBALS['user']->cfg->MEINE_SEMINARE_SORT_FLAG ?: 'ASC'; + if (Request::option('teacher_filter')) { + $GLOBALS['user']->cfg->store('ADMIN_COURSES_TEACHERFILTER', Request::option('teacher_filter')); + } else { + $GLOBALS['user']->cfg->delete('ADMIN_COURSES_TEACHERFILTER'); + } - $this->courses = $this->getCourses([ - 'sortby' => $this->sortby, - 'sortFlag' => $this->sortFlag, - 'view_filter' => $this->view_filter, - 'typeFilter' => $config_my_course_type_filter, - 'datafields' => $this->getDatafieldFilters() - ], Request::get('display') === 'all'); + $datafields_filters = $GLOBALS['user']->cfg->ADMIN_COURSES_DATAFIELDS_FILTERS; + foreach (DataField::getDataFields('sem') as $datafield) { + if ( + Request::get('df_'.$datafield->getId()) + && in_array($datafield->getId(), $activeSidebarElements['datafields']) + ) { + $datafields_filters[$datafield->getId()] = Request::get('df_'.$datafield->getId()); + } else { + unset($datafields_filters[$datafield->getId()]); + } + } + $GLOBALS['user']->cfg->store('ADMIN_COURSES_DATAFIELDS_FILTERS', $datafields_filters); - if (in_array('contents', $this->view_filter)) { - $this->nav_elements = MyRealmModel::calc_nav_elements([$this->courses]); + $filter = AdminCourseFilter::get(); + if (Request::option('course_id')) { //we have only one course and want to see if that course is part of the result set + $filter->query->where('course_id', 'seminare.Seminar_id = :course_id', ['course_id' => Request::option('course_id')]); } - // get all available teacher for infobox-filter - // filter by selected teacher - $_SESSION['MY_COURSES_LIST'] = array_map(function ($c, $id) { - return [ - 'Name' => $c['Name'], - 'Seminar_id' => $id + $count = $filter->countCourses(); + if ($count > $this->max_show_courses && !Request::submitted('without_limit')) { + $this->render_json([ + 'count' => $count + ]); + return; + } + $courses = AdminCourseFilter::get()->getCourses(); + + $data = [ + 'data' => [] ]; - }, array_values($this->courses), array_keys($this->courses)); + if (Request::submitted('activated_fields')) { + $GLOBALS['user']->cfg->store('MY_COURSES_ADMIN_VIEW_FILTER_ARGS', json_encode(Request::getArray('activated_fields'))); + } + $activated_fields = $this->getFilterConfig(); + + $GLOBALS['user']->cfg->store('MY_COURSES_ACTION_AREA', Request::option('action')); + foreach ($courses as $course) { + if ($course->parent_course && !Request::option('course_id')) { + continue; + } + $data['data'][] = $this->getCourseData($course, $activated_fields); + foreach ($course->children as $childcourse) { + $data['data'][] = $this->getCourseData($childcourse, $activated_fields); + } + } + $tf = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/app/views'); + switch ($GLOBALS['user']->cfg->MY_COURSES_ACTION_AREA) { + case 1: + case 2: + case 3: + case 4: + break; + case 8: //Sperrebenen + $template = $tf->open('admin/courses/lock_preselect'); + $template->course = $course; + $template->all_lock_rules = new SimpleCollection(array_merge( + [[ + 'name' => '--' . _('keine Sperrebene') . '--', + 'lock_id' => 'none' + ]], + LockRule::findAllByType('sem') + )); + $data['buttons_top'] = $template->render(); + $data['buttons_bottom'] = (string) \Studip\Button::createAccept(_('Sperrebenen'), 'locking_button', ['formaction' => URLHelper::getURL('dispatch.php/admin/courses/set_lockrule')]); + break; + case 9: //Sichtbarkeit + $data['buttons_top'] = '<label>'._('Alle auswählen').'<input type="checkbox" data-proxyfor=".course-admin td:last-child :checkbox"></label>'; + $data['buttons_bottom'] = (string) \Studip\Button::createAccept(_('Sichtbarkeit'), 'visibility_button', ['formaction' => URLHelper::getURL('dispatch.php/admin/courses/set_visibility')]); + break; + case 10: //Zusatzangaben + $template = $tf->open('admin/courses/aux_preselect'); + $template->course = $course; + $template->aux_lock_rules = AuxLockRule::findBySQL('1 ORDER BY name ASC'); + $data['buttons_top'] = $template->render(); + $data['buttons_bottom'] = (string) \Studip\Button::createAccept(_('Zusatzangaben'), 'aux_button', ['formaction' => URLHelper::getURL('dispatch.php/admin/courses/set_aux_lockrule')]); + break; + case 11: //Veranstaltung kopieren + break; + case 14: //Zugangsberechtigungen + break; + case 16: //Löschen + $data['buttons_top'] = '<label>'._('Alle auswählen').'<input type="checkbox" data-proxyfor=".course-admin td:last-child :checkbox"></label>'; + $data['buttons_bottom'] = (string) \Studip\Button::createAccept(_('Löschen'), 'deleting_button', ['formaction' => URLHelper::getURL('dispatch.php/course/archive/confirm')]); + break; + case 17: //Gesperrte Veranstaltungen + $data['buttons_top'] = '<label>'._('Alle auswählen').'<input type="checkbox" data-proxyfor=".course-admin td:last-child :checkbox"></label>'; + $data['buttons_bottom'] = (string) \Studip\Button::createAccept(_('Einstellungen speichern'), 'locking_button', ['formaction' => URLHelper::getURL('dispatch.php/admin/courses/set_locked')]); + break; + case 18: //Startsemester + break; + case 19: //LV-Gruppen + break; + case 20: //Notiz + break; + default: + foreach (PluginManager::getInstance()->getPlugins('AdminCourseAction') as $plugin) { + if ($GLOBALS['user']->cfg->MY_COURSES_ACTION_AREA === get_class($plugin)) { + $multimode = $plugin->useMultimode(); + if ($multimode) { + $data['buttons_top'] = '<label>'._('Alle auswählen').'<input type="checkbox" data-proxyfor=".course-admin td:last-child :checkbox"></label>'; + if ($multimode instanceof Flexi_Template) { + $data['buttons_bottom'] = $multimode->render(); + } elseif (is_string($multimode)) { + $data['buttons_bottom'] = (string) \Studip\Button::create($multimode, '', ['formaction' => $plugin->getAdminActionURL()]); + } else { + $data['buttons_bottom'] = (string) \Studip\Button::create(_('Speichern'), '', ['formaction' => $plugin->getAdminActionURL()]); + } + } + } + break; + } + } + if (!isset($data['buttons_top'])) { + $data['buttons_top'] = ''; + } + if (!isset($data['buttons_bottom'])) { + $data['buttons_bottom'] = ''; + } + $this->render_json($data); + } - $this->all_lock_rules = new SimpleCollection(array_merge( - [[ - 'name' => '--' . _("keine Sperrebene") . '--', - 'lock_id' => 'none' - ]], - LockRule::findAllByType('sem') - )); - $this->aux_lock_rules = AuxLockRule::findBySQL('1 ORDER BY name'); + protected function getCourseData(Course $course, $activated_fields) + { + $d = [ + 'id' => $course->id, + 'parent_course' => $course->parent_course + ]; + if (in_array('name', $activated_fields)) { + $params = tooltip2(_('Veranstaltungsdetails anzeigen')); + $params['style'] = 'cursor: pointer'; + $d['name'] = '<a href="'.URLHelper::getLink('seminar_main.php', ['auswahl' => $course->id]).'">' + . htmlReady($course->name) + .'</a> ' + .'<a href="'.URLHelper::getLink('dispatch.php/course/details/index/'. $course->id).'" data-dialog><button class="undecorated">'.Icon::create('info-circle', Icon::ROLE_INACTIVE)->asImg($params).'</button></a> ' + .(!$course->visible ? _('(versteckt)') : ''); + } + if (in_array('number', $activated_fields)) { + $d['number'] = '<a href="'.URLHelper::getLink('seminar_main.php', ['auswahl' => $course->id]).'">' + .$course->veranstaltungsnummer + .'</a>'; + } + if (in_array('avatar', $activated_fields)) { + $d['avatar'] = '<a href="'.URLHelper::getLink('seminar_main.php', ['auswahl' => $course->id]).'">' + .CourseAvatar::getAvatar($course->getId())->getImageTag(Avatar::SMALL, ['title' => $course->name]) + ."</a>"; + } + if (in_array('type', $activated_fields)) { + $semtype = $course->getSemType(); + $d['type'] = $semtype['name']; + } + if (in_array('room_time', $activated_fields)) { + $d['room_time'] = Seminar::GetInstance($course->id)->getDatesHTML([ + 'show_room' => true, + ]) ?: _('nicht angegeben'); + } + if (in_array('semester', $activated_fields)) { + $d['semester'] = $course->semester_text; + } + if (in_array('institute', $activated_fields)) { + $d['institute'] = $course->home_institut ? $course->home_institut->name : $course->institute; + } + if (in_array('requests', $activated_fields)) { + $d['requests'] = '<a href="'.URLHelper::getLink('dispatch.php/course/room_requests', ['cid' => $course->id]).'">'.count($course->room_requests)."</a>"; + } + if (in_array('teachers', $activated_fields)) { + $teachers = $this->getTeacher($course->id); + $teachers = array_map(function ($teacher) { + return '<a href="'.URLHelper::getLink('dispatch.php/profile', ['username' => $teacher['username']]) .'">'. htmlReady($teacher['fullname']).'</a>'; + }, $teachers); + $d['teachers'] = implode(', ', $teachers); + } + if (in_array('members', $activated_fields)) { + $d['members'] = '<a href="'.URLHelper::getLink('dispatch.php/course/members', ['cid' => $course->id]).'">' + .$course->getNumParticipants() + .'</a>'; + } + if (in_array('waiting', $activated_fields)) { + $d['waiting'] = '<a href="'.URLHelper::getLink('dispatch.php/course/members', ['cid' => $course->id]).'">' + .$course->getNumWaiting() + .'</a>'; + } + if (in_array('preliminary', $activated_fields)) { + $d['preliminary'] = '<a href="'.URLHelper::getLink('dispatch.php/course/members', ['cid' => $course->id]).'">' + .$course->getNumPrelimParticipants() + .'</a>'; + } + if (in_array('contents', $activated_fields)) { + $icons = []; + foreach ($course->tools as $tool) { + $module = $tool->getStudipModule(); + if ($module) { + $last_visit = object_get_visit($course->id, $module->getPluginId()); + $nav = $module->getIconNavigation($course->id, $last_visit, $GLOBALS['user']->id); + if (isset($nav) && $nav->isVisible(true)) { + $icons[] = $nav; + } + } + } + $d['contents'] = '<div class="icons"> + <ul class="my-courses-navigation">'; + + foreach ($icons as $icon) { + $d['contents'] .= '<li class="my-courses-navigation-item '. ($icon->getImage()->signalsAttention() ? 'my-courses-navigation-important' : '').'"> + <a href="'. URLHelper::getLink('seminar_main.php', ['auswahl' => $course->id, 'redirect_to' => $icon->getURL()]).'"'. ($icon->getTitle() ? ' title="'.htmlReady($icon->getTitle()).'"' : '') .'> + '. $icon->getImage()->asImg(20) .' + </a> + </li>'; + } + $d['contents'] .= '</ul></div>'; + } + if (in_array('last_activity', $activated_fields)) { + $d['last_activity'] = date('%x', lastActivity($course->id)); + } + foreach (PluginManager::getInstance()->getPlugins('AdminCourseContents') as $plugin) { + foreach ($plugin->adminAvailableContents() as $index => $label) { + if (in_array($plugin->getPluginId() . '_' . $index, $activated_fields)) { + $content = $plugin->adminAreaGetCourseContent($course, $index); + $d[$plugin->getPluginId()."_".$index] = $content instanceof Flexi_Template ? $content->render() : $content; + } + } + } + $tf = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'].'/app/views'); - //build the sidebar: - $this->buildSidebar($config_my_course_type_filter); + switch ($GLOBALS['user']->cfg->MY_COURSES_ACTION_AREA) { + case 1: + $d['action'] = (string) \Studip\LinkButton::create( + _('Grunddaten'), + URLHelper::getURL('dispatch.php/course/basicdata/view', ['cid' => $course->id]), + ['data-dialog' => '', 'role' => 'button'] + ); + break; + case 2: + $d['action'] = (string) \Studip\LinkButton::create( + _('Studienbereiche'), + URLHelper::getURL('dispatch.php/course/study_areas/show', ['cid' => $course->id, 'from' => 'admin/courses']), + ['data-dialog' => '', 'role' => 'button'] + ); + break; + case 3: + $d['action'] = (string) \Studip\LinkButton::create( + _('Zeiten/Räume'), + URLHelper::getURL('dispatch.php/course/timesrooms/index', ['cid' => $course->id, 'cmd' => 'applyFilter']), + ['data-dialog' => '', 'role' => 'button'] + ); + break; + case 4: + $d['action'] = (string) \Studip\LinkButton::create( + _('Raumanfragen'), + URLHelper::getURL('dispatch.php/course/room_requests/index', ['cid' => $course->id, 'origin' => 'admin_courses']), + ['data-dialog' => '', 'role' => 'button'] + ); + break; + case 8: //Sperrebenen + $template = $tf->open('admin/courses/lock'); + $template->course = $course; + $template->aux_lock_rules = AuxLockRule::findBySQL('1 ORDER BY name ASC'); + $template->all_lock_rules = new SimpleCollection(array_merge( + [[ + 'name' => '--' . _('keine Sperrebene') . '--', + 'lock_id' => 'none' + ]], + LockRule::findAllByType('sem') + )); + $d['action'] = $template->render(); + break; + case 9: //Sichtbarkeit + $d['action'] = '<input type="hidden" name="all_sem[]" value="'.htmlReady($course->id).'"><input type="checkbox" name="visibility['.$course->id.']" '.($course->visible ? ' checked ' : '').'value="1">'; + break; + case 10: //Zusatzangaben + $template = $tf->open('admin/courses/aux-select'); + $template->course = $course; + $template->aux_lock_rules = AuxLockRule::findBySQL('1 ORDER BY name ASC'); + $d['action'] = $template->render(); + break; + case 11: //Veranstaltung kopieren + $d['action'] = (string) \Studip\LinkButton::create( + _('Kopieren'), + URLHelper::getURL('dispatch.php/course/wizard/copy/' . $course->id), + ['data-dialog' => '', 'role' => 'button'] + ); + break; + case 14: //Zugangsberechtigungen + $d['action'] = (string) \Studip\LinkButton::create( + _('Zugangsberechtigungen'), + URLHelper::getURL('dispatch.php/course/admission', ['cid' => $course->id]), + ['data-dialog' => '', 'role' => 'button'] + ); + break; + case 16: //Löschen + $d['action'] = '<input type="checkbox" name="archiv_sem[]" value="'.htmlReady($course->id).'" aria-label="'.htmlReady(sprintf(_('Veranstaltung %s löschen'), $course->getFullName())).'">'; + break; + case 17: //Gesperrte Veranstaltungen + $cs = CourseSet::getSetForCourse($course->id); + if ($cs) { + $locked = $cs->getId() === CourseSet::getGlobalLockedAdmissionSetId(); + } else { + $locked = false; + } + $d['action'] = '<input type="hidden" name="all_sem[]" value="'.htmlReady($course->id).'"><input type="checkbox" name="admission_locked['.$course->getId().']" '.($locked ? 'checked' : '').' value="1" aria-label="'.htmlReady(sprintf(_('Veranstaltung %s sperren'), $course->getFullName())).'">'; + break; + case 18: //Startsemester + $d['action'] = (string) \Studip\LinkButton::create( + _('Startsemester'), + URLHelper::getURL('dispatch.php/course/timesrooms/editSemester', ['cid' => $course->id, 'origin' => 'admin_courses']), + ['data-dialog' => '', 'role' => 'button'] + ); + break; + case 19: //LV-Gruppen + $d['action'] = (string) \Studip\LinkButton::create( + _('LV-Gruppen'), + URLHelper::getURL('dispatch.php/course/lvgselector', ['cid' => $course->id, 'from' => 'admin/courses']), + ['data-dialog' => '', 'role' => 'button'] + ); + break; + case 20: //Notiz + $method = $course->config->COURSE_ADMIN_NOTICE ? 'createHasNotice' : 'createHasNoNotice'; + $d['action'] = (string) \Studip\LinkButton::$method( + _('Notiz'), + URLHelper::getURL('dispatch.php/admin/courses/notice/'.$course->id), + [ + 'data-dialog' => 'size=auto', + 'title' => $course->config->COURSE_ADMIN_NOTICE, + 'role' => 'button' + ] + ); + break; + default: + foreach (PluginManager::getInstance()->getPlugins('AdminCourseAction') as $plugin) { + if ($GLOBALS['user']->cfg->MY_COURSES_ACTION_AREA === get_class($plugin)) { + $output = $plugin->getAdminCourseActionTemplate($course->getId()); + $d['action'] = $output instanceof Flexi_Template ? $output->render() : (string) $output; + } + break; + } + } + $d['completion'] = $course->completion; + return $d; + } + /** + * This action just stores the new settings for sorting the table of courses. + * @return void + */ + public function sort_action() + { + if (Request::isPost()) { + $GLOBALS['user']->cfg->store('MEINE_SEMINARE_SORT', Request::get('sortby')); + $GLOBALS['user']->cfg->store('MEINE_SEMINARE_SORT_FLAG', Request::get('sortflag')); + } + $this->render_nothing(); } @@ -454,6 +820,18 @@ class Admin_CoursesController extends AuthenticatedController } } + public function get_stdgangteil_selector_action($institut_id) + { + $selector = $this->getStgteilSelector($institut_id); + $this->render_text($selector->render(['base_class' => 'sidebar'])); + } + + public function get_teacher_selector_action($institut_id) + { + $selector = $this->getTeacherWidget($institut_id); + $this->render_text($selector->render(['base_class' => 'sidebar'])); + } + /** * Export action @@ -463,15 +841,7 @@ class Admin_CoursesController extends AuthenticatedController $filter_config = Request::getArray('fields'); if (count($filter_config) > 0) { - $sortby = $GLOBALS['user']->cfg->getValue('MEINE_SEMINARE_SORT'); - $config_my_course_type_filter = $GLOBALS['user']->cfg->getValue('MY_COURSES_TYPE_FILTER'); - - $courses = $this->getCourses([ - 'sortby' => $sortby, - 'sortFlag' => 'asc', - 'typeFilter' => $config_my_course_type_filter, - 'view_filter' => $filter_config, - ], true); + $courses = AdminCourseFilter::get()->getCourses(); $view_filters = $this->getViewFilters(); @@ -506,37 +876,40 @@ class Admin_CoursesController extends AuthenticatedController } if (in_array('requests', $filter_config)) { - $row['requests'] = $course['requests']; + $row['requests'] = $course['room_requests']; } if (in_array('teachers', $filter_config)) { - $row['teachers'] = implode(', ', array_map(function ($d) { - return $d['fullname']; - }, $course['dozenten'])); + $row['teachers'] = implode( + ', ', + $course->teachers->map(function ($d) { + return $d->user->getFullName(); + }) + ); } if (in_array('members', $filter_config)) { - $row['members'] = $course['teilnehmer']; + $row['members'] = $course->getNumParticipants(); } if (in_array('waiting', $filter_config)) { - $row['waiting'] = $course['waiting']; + $row['waiting'] = $course->getNumWaiting(); } if (in_array('preliminary', $filter_config)) { - $row['preliminary'] = $course['prelim']; + $row['preliminary'] = $course->getNumPrelimParticipants(); } if (in_array('last_activity', $filter_config)) { - $row['last_activity'] = strftime('%x', $course['last_activity']); + $row['last_activity'] = strftime('%x', lastActivity($course->id)); } if (in_array('semester', $filter_config)) { - $row['semester'] = $course_model->getTextualSemester(); + $row['semester'] = $course->getTextualSemester(); } if (in_array('institute', $filter_config)) { - $row['institute'] = $course_model->home_institut ? (string) $course_model->home_institut['name'] : $course_model['institut_id']; + $row['institute'] = $course->home_institut ? (string) $course->home_institut['name'] : $course['institut_id']; } foreach (PluginManager::getInstance()->getPlugins('AdminCourseContents') as $plugin) { @@ -551,7 +924,7 @@ class Admin_CoursesController extends AuthenticatedController } } - $data[$course_id] = $row; + $data[$course->id] = $row; } $captions = []; @@ -581,51 +954,6 @@ class Admin_CoursesController extends AuthenticatedController } } - /** - * Set the selected institute or semester - */ - public function set_selection_action() - { - if (Request::option('institute')) { - $GLOBALS['user']->cfg->store('ADMIN_COURSES_TEACHERFILTER', null); - $GLOBALS['user']->cfg->store('MY_COURSES_SELECTED_STGTEIL', null); - $inst = explode('_', Request::option('institute')); - $GLOBALS['user']->cfg->store('MY_INSTITUTES_DEFAULT', $inst[0]); - - if (isset($inst[1]) && $inst[1] === 'withinst') { - $GLOBALS['user']->cfg->store('MY_INSTITUTES_INCLUDE_CHILDREN', 1); - } else { - $GLOBALS['user']->cfg->store('MY_INSTITUTES_INCLUDE_CHILDREN', 0); - } - - PageLayout::postSuccess(_('Die gewünschte Einrichtung wurde ausgewählt!')); - } - - if (Request::option('sem_select')) { - $GLOBALS['user']->cfg->store('MY_COURSES_SELECTED_CYCLE', Request::option('sem_select')); - if (Request::option('sem_select') !== "all") { - $sem_name = Semester::find(Request::option('sem_select'))->name; - PageLayout::postSuccess(sprintf(_('Das %s wurde ausgewählt'), htmlReady($sem_name))); - } else { - PageLayout::postSuccess(_('Semesterfilter abgewählt')); - } - } - - if (Request::option('stgteil_select')) { - $GLOBALS['user']->cfg->store('MY_COURSES_SELECTED_STGTEIL', Request::option('stgteil_select')); - if (Request::option('stgteil_select') !== "all") { - PageLayout::postSuccess(sprintf( - _('Der Studiengangteil %s wurde ausgewählt'), - htmlReady(StudiengangTeil::find(Request::option('stgteil_select'))->getDisplayName()) - )); - } else { - PageLayout::postSuccess(_('Studiengangteilfilter abgewählt')); - } - } - - $this->redirect('admin/courses/index'); - } - /** * Set the lockrules of courses @@ -785,9 +1113,9 @@ class Admin_CoursesController extends AuthenticatedController // force to pre selection if (Request::submitted('all')) { $value = Request::get('lock_sem_all'); - $value_forced = Request::int('aux_all_forced'); + $value_forced = Request::int('aux_all_forced', 0); } else { - $value_forced = $lock_sem_forced[$course_id]; + $value_forced = $lock_sem_forced[$course_id] ?? 0; } $course = Course::find($course_id); @@ -822,53 +1150,6 @@ class Admin_CoursesController extends AuthenticatedController } - /** - * Set the selected view filter and store the selection in configuration - */ - public function set_view_filter_action($filter = null, $state = true) - { - // store view filter in configuration - if (!is_null($filter)) { - $filters = $this->getFilterConfig(); - - if ($state) { - $filters = array_diff($filters, [$filter]); - } else { - $filters[] = $filter; - } - - $this->setFilterConfig($filters); - } - - $this->redirect('admin/courses/index'); - } - - /** - * Set the selected action type and store the selection in configuration - */ - public function set_action_type_action() - { - // select the action area - if (Request::option('action_area')) { - $GLOBALS['user']->cfg->store('MY_COURSES_ACTION_AREA', Request::option('action_area')); - PageLayout::postSuccess(_('Der Aktionsbereich wurde erfolgreich übernommen!')); - } - - $this->redirect('admin/courses/index'); - } - - /** - * Set the selected course type filter and store the selection in configuration - */ - public function set_course_type_action() - { - if (Request::option('course_type')) { - $GLOBALS['user']->cfg->store('MY_COURSES_TYPE_FILTER', Request::option('course_type')); - PageLayout::postSuccess(_('Der gewünschte Veranstaltungstyp wurde übernommen!')); - } - $this->redirect('admin/courses/index'); - } - /** * Marks a course as complete/incomplete. * @@ -904,11 +1185,9 @@ class Admin_CoursesController extends AuthenticatedController $course->config->store('COURSE_ADMIN_NOTICE', trim(Request::get('notice'))); if (Request::isXhr()) { - $this->response->add_header('X-Dialog-Notice', json_encode([ - 'id' => $course->id, - 'notice' => $course->config->COURSE_ADMIN_NOTICE, - ])); - $this->render_nothing(); + $this->response->add_header('X-Dialog-Execute', 'STUDIP.AdminCourses.App.reloadCourse'); + $this->response->add_header('X-Dialog-Close', '1'); + $this->render_text($course->id); } else { $this->redirect($this->indexURL("#course-{$course->id}")); } @@ -919,30 +1198,6 @@ class Admin_CoursesController extends AuthenticatedController $this->notice = $course->config->COURSE_ADMIN_NOTICE; } - public function get_subcourses_action($course_id) - { - // get courses only if institutes available - $this->actions = $this->getActions(); - - // Get the view filter - $this->view_filter = $this->getFilterConfig(); - - $this->selected_action = $GLOBALS['user']->cfg->MY_COURSES_ACTION_AREA; - if (is_null($this->selected_action) || (!is_numeric($this->selected_action) && !class_exists($this->selected_action))) { - $this->selected_action = 1; - } - - $this->courses = $this->getCourses([ - 'sortby' => $this->sortby, - 'sortFlag' => $this->sortFlag, - 'view_filter' => $this->view_filter, - 'datafields' => $this->getDatafieldFilters(), - 'parent_course' => $course_id - ]); - - $this->parent = $course_id; - - } /** * Return a specifically action or all available actions @@ -1090,6 +1345,7 @@ class Admin_CoursesController extends AuthenticatedController private function getViewFilters() { $views = [ + 'avatar' => _('Avatar'), 'number' => _('Nr.'), 'name' => _('Name'), 'type' => _('Veranstaltungstyp'), @@ -1301,9 +1557,9 @@ class Admin_CoursesController extends AuthenticatedController * Adds view filter to the sidebar * @param array $configs */ - private function setViewWidget($configs = []) + private function setViewWidget() { - $configs = $configs ?: []; + $configs = $this->getFilterConfig(); $checkbox_widget = new OptionsWidget(); $checkbox_widget->setTitle(_('Darstellungsfilter')); @@ -1312,7 +1568,9 @@ class Admin_CoursesController extends AuthenticatedController $checkbox_widget->addCheckbox( $label, $state, - $this->url_for('admin/courses/set_view_filter/' . $index . '/' . $state) + $this->url_for('admin/courses/set_view_filter/' . $index . '/' . $state), + null, + ['onclick' => "$(this).toggleClass(['options-checked', 'options-unchecked']); $(this).attr('aria-checked', $(this).hasClass('options-checked') ? 'true' : 'false'); STUDIP.AdminCourses.App.toggleActiveField('".$index."'); return false;"] ); } Sidebar::get()->addWidget($checkbox_widget, 'views'); @@ -1333,9 +1591,9 @@ class Admin_CoursesController extends AuthenticatedController if ($GLOBALS['perm']->have_perm('root') || (count($this->insts) > 1)) { $list->addElement(new SelectElement( - 'all', + '', $GLOBALS['perm']->have_perm('root') ? _('Alle') : _('Alle meine Einrichtungen'), - $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT === 'all'), + !$GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT), 'select-all' ); } @@ -1366,6 +1624,7 @@ class Admin_CoursesController extends AuthenticatedController ); } } + $list->setOnSubmitHandler("STUDIP.AdminCourses.changeFiltersDependendOnInstitute($(this).find('select').val()); return false;"); $sidebar->addWidget($list, 'filter_institute'); } @@ -1378,7 +1637,7 @@ class Admin_CoursesController extends AuthenticatedController $semesters = array_reverse(Semester::getAll()); $sidebar = Sidebar::Get(); $list = new SelectWidget(_('Semester'), $this->url_for('admin/courses/set_selection'), 'sem_select'); - $list->addElement(new SelectElement('all', _('Alle')), 'sem_select-all'); + $list->addElement(new SelectElement('', _('Alle')), 'sem_select-all'); foreach ($semesters as $semester) { $list->addElement(new SelectElement( $semester->id, @@ -1386,6 +1645,7 @@ class Admin_CoursesController extends AuthenticatedController $semester->id === $GLOBALS['user']->cfg->MY_COURSES_SELECTED_CYCLE ), 'sem_select-' . $semester->id); } + $list->setOnSubmitHandler("STUDIP.AdminCourses.App.changeFilter({semester_id: $(this).find('select').val()}); return false;"); $sidebar->addWidget($list, 'filter_semester'); } @@ -1393,12 +1653,18 @@ class Admin_CoursesController extends AuthenticatedController /** * Adds the studiengangteil selector to the sidebar */ - private function setStgteilSelector() + private function getStgteilSelector($institut_id = null) { - $stgteile = StudiengangTeil::getAllEnriched('fach_name','ASC', ['mvv_fach_inst.institut_id' => $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT]); - $sidebar = Sidebar::Get(); + $institut_id = $institut_id ?: $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT; + $stgteile = StudiengangTeil::getAllEnriched('fach_name', 'ASC', ['mvv_fach_inst.institut_id' => $institut_id]); $list = new SelectWidget(_('Studiengangteil'), $this->url_for('admin/courses/set_selection'), 'stgteil_select'); - $list->addElement(new SelectElement('all', _('Alle')), 'stgteil_select-all'); + if (!$institut_id || $institut_id === 'all') { + $list->addElement(new SelectElement('', _('Wählen Sie eine Einrichtung') ), 'stgteil_select-all'); + } elseif (count($stgteile) === 0) { + $list->addElement(new SelectElement('', _('Keine Studiengangteile zu der gewählten Einrichtung') ), 'stgteil_select-all'); + } else { + $list->addElement(new SelectElement('', _('Alle')), 'stgteil_select-all'); + } foreach ($stgteile as $stgteil) { $list->addElement(new SelectElement( $stgteil->id, @@ -1406,8 +1672,8 @@ class Admin_CoursesController extends AuthenticatedController $stgteil->id === $GLOBALS['user']->cfg->MY_COURSES_SELECTED_STGTEIL ), 'stgteil_select-' . $stgteil->id); } - - $sidebar->addWidget($list, 'filter_stgteil'); + $list->setOnSubmitHandler("STUDIP.AdminCourses.App.changeFilter({stgteil: $(this).find('select').val()}); return false;"); + return $list; } @@ -1415,15 +1681,21 @@ class Admin_CoursesController extends AuthenticatedController * Adds HTML-Selector to the sidebar * @param null $selected_action */ - private function setActionsWidget($selected_action = null) + private function setActionsWidget() { $actions = $this->getActions(); $sidebar = Sidebar::Get(); $list = new SelectWidget(_('Aktionsbereichauswahl'), $this->url_for('admin/courses/set_action_type'), 'action_area'); foreach ($actions as $index => $action) { - $list->addElement(new SelectElement($index, $action['name'], $selected_action == $index), 'action-aria-' . $index); + $list->addElement(new SelectElement( + $index, + $action['name'], + $GLOBALS['user']->cfg->MY_COURSES_ACTION_AREA == $index), + 'action-aria-' . $index + ); } + $list->setOnSubmitHandler("STUDIP.AdminCourses.App.changeActionArea($(this).find('select').val()); return false;"); $sidebar->addWidget($list, 'editmode'); } @@ -1433,12 +1705,12 @@ class Admin_CoursesController extends AuthenticatedController * @param string $selected * @param array $params */ - private function setCourseTypeWidget($selected = 'all') + private function setCourseTypeWidget() { $sidebar = Sidebar::get(); $this->url = $this->url_for('admin/courses/set_course_type'); $this->types = []; - $this->selected = $selected; + $this->selected = $GLOBALS['user']->cfg->MY_COURSES_TYPE_FILTER; $list = new SelectWidget( _('Veranstaltungstypfilter'), @@ -1446,7 +1718,7 @@ class Admin_CoursesController extends AuthenticatedController 'course_type' ); $list->addElement(new SelectElement( - 'all', _('Alle'), $selected === 'all' + '', _('Alle'), !$GLOBALS['user']->cfg->MY_COURSES_TYPE_FILTER ), 'course-type-all'); foreach ($GLOBALS['SEM_CLASS'] as $class_id => $class) { if ($class['studygroup_mode']) { @@ -1456,7 +1728,7 @@ class Admin_CoursesController extends AuthenticatedController $element = new SelectElement( $class_id, $class['name'], - $selected === (string)$class_id + $GLOBALS['user']->cfg->MY_COURSES_TYPE_FILTER === (string)$class_id ); $list->addElement( $element->setAsHeader(), @@ -1467,7 +1739,7 @@ class Admin_CoursesController extends AuthenticatedController $element = new SelectElement( $class_id . '_' . $id, $result['name'], - $selected === $class_id . '_' . $id + $GLOBALS['user']->cfg->MY_COURSES_TYPE_FILTER === $class_id . '_' . $id ); $list->addElement( $element->setIndentLevel(1), @@ -1475,6 +1747,7 @@ class Admin_CoursesController extends AuthenticatedController ); } } + $list->setOnSubmitHandler("STUDIP.AdminCourses.App.changeFilter({course_type: $(this).find('select').val()}); return false;"); $sidebar->addWidget($list, 'filter-course-type'); } @@ -1482,35 +1755,38 @@ class Admin_CoursesController extends AuthenticatedController * Returns a widget to selected a specific teacher * @param array $teachers */ - private function setTeacherWidget() + private function getTeacherWidget($institut_id = null) { - if (!$GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT || $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT === "all") { - return; - } + $institut_id = $institut_id ?: $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT; $teachers = DBManager::get()->fetchAll(" - SELECT auth_user_md5.*, user_info.* - FROM auth_user_md5 - LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id) - INNER JOIN user_inst ON (user_inst.user_id = auth_user_md5.user_id) - INNER JOIN Institute ON (Institute.Institut_id = user_inst.Institut_id) - WHERE (Institute.Institut_id = :institut_id OR Institute.fakultaets_id = :institut_id) - AND auth_user_md5.perms = 'dozent' - ORDER BY auth_user_md5.Nachname ASC, auth_user_md5.Vorname ASC - ", [ - 'institut_id' => $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT - ], - function ($data) { - $ret['user_id'] = $data['user_id']; - unset($data['user_id']); - $ret['fullname'] = User::build($data)->getFullName("full_rev"); - return $ret; - } + SELECT auth_user_md5.*, user_info.* + FROM auth_user_md5 + LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id) + INNER JOIN user_inst ON (user_inst.user_id = auth_user_md5.user_id) + INNER JOIN Institute ON (Institute.Institut_id = user_inst.Institut_id) + WHERE (Institute.Institut_id = :institut_id OR Institute.fakultaets_id = :institut_id) + AND auth_user_md5.perms = 'dozent' + ORDER BY auth_user_md5.Nachname ASC, auth_user_md5.Vorname ASC + ", [ + 'institut_id' => $institut_id + ], + function ($data) { + $ret['user_id'] = $data['user_id']; + unset($data['user_id']); + $ret['fullname'] = User::build($data)->getFullName("full_rev"); + return $ret; + } ); - $sidebar = Sidebar::Get(); $list = new SelectWidget(_('Lehrendenfilter'), $this->url_for('admin/courses/index'), 'teacher_filter'); - $list->addElement(new SelectElement('all', _('alle'), Request::get('teacher_filter') == 'all'), 'teacher_filter-all'); + if (!$institut_id || $institut_id === 'all') { + $list->addElement(new SelectElement('', _('Wählen Sie eine Einrichtung') ), 'teacher_filter-all'); + } elseif (count($teachers) === 0) { + $list->addElement(new SelectElement('', _('Keine Lehrenden in der gewählten Einrichtung') ), 'teacher_filter-all'); + } else { + $list->addElement(new SelectElement('', _('Alle')), 'teacher_filter-all'); + } foreach ($teachers as $teacher) { $list->addElement(new SelectElement( @@ -1519,8 +1795,8 @@ class Admin_CoursesController extends AuthenticatedController $GLOBALS['user']->cfg->ADMIN_COURSES_TEACHERFILTER === $teacher['user_id'] ), 'teacher_filter-' . $teacher['user_id']); } - - $sidebar->addWidget($list, 'filter_teacher'); + $list->setOnSubmitHandler("STUDIP.AdminCourses.App.changeFilter({teacher_filter: $(this).find('select').val()}); return false;"); + return $list; } /** @@ -1530,7 +1806,15 @@ class Admin_CoursesController extends AuthenticatedController { $sidebar = Sidebar::Get(); $search = new SearchWidget(URLHelper::getURL('dispatch.php/admin/courses')); - $search->addNeedle(_('Freie Suche'), 'search', true, null, null, $GLOBALS['user']->cfg->ADMIN_COURSES_SEARCHTEXT); + $search->addNeedle( + _('Freie Suche'), + 'search', + true, + null, + '', + $GLOBALS['user']->cfg->ADMIN_COURSES_SEARCHTEXT + ); + $search->setOnSubmitHandler("STUDIP.AdminCourses.App.changeFilter({search: $(this).find('input').val()}); return false;"); $sidebar->addWidget($search, 'filter_search'); } @@ -1556,7 +1840,9 @@ class Admin_CoursesController extends AuthenticatedController } if (!$config) { - $config = $this->setFilterConfig($available_filters); + $config = $this->setFilterConfig([ + 'number', 'name', 'semester', 'institute', 'teachers' + ]); } return $config; diff --git a/app/controllers/course/basicdata.php b/app/controllers/course/basicdata.php index baa817a0fd515fe85915d27774452a76b0fc1117..3bf10f730a6f88e7640d63686ca4a1125befae5c 100644 --- a/app/controllers/course/basicdata.php +++ b/app/controllers/course/basicdata.php @@ -524,7 +524,14 @@ class Course_BasicdataController extends AuthenticatedController } $this->flash['msg'] = $this->msg; $this->flash['open'] = Request::get("open"); - $this->redirect($this->url_for('course/basicdata/view/' . $sem->getId())); + if (Request::isDialog()) { + $this->response->add_header('X-Dialog-Close', 1); + $this->response->add_header('X-Dialog-Execute', 'STUDIP.AdminCourses.App.reloadCourse'); + $this->render_text($course_id); + } else { + $this->redirect($this->url_for('course/basicdata/view/' . $sem->getId())); + } + } public function add_member_action($course_id, $status = 'dozent') diff --git a/app/controllers/course/lvgselector.php b/app/controllers/course/lvgselector.php index 101bee0e042ebd1b31f5d4dd2f680abf086cfb3f..60b090e81b7e741ae31077c16e4f348cb9dff8f2 100644 --- a/app/controllers/course/lvgselector.php +++ b/app/controllers/course/lvgselector.php @@ -196,8 +196,13 @@ class Course_LvgselectorController extends AuthenticatedController $url = $this->action_url('index'); } - - $this->redirect($url); + if (Request::isDialog()) { + $this->response->add_header('X-Dialog-Close', 1); + $this->response->add_header('X-Dialog-Execute', 'STUDIP.AdminCourses.App.reloadCourse'); + $this->render_text($this->course_id); + } else { + $this->redirect($url); + } } /** diff --git a/app/controllers/course/study_areas.php b/app/controllers/course/study_areas.php index 3643309d4bc74d565d43ceb5114ff2eb4675c258..2993f569636a990b929fed99e4cb60c9dda56e1d 100644 --- a/app/controllers/course/study_areas.php +++ b/app/controllers/course/study_areas.php @@ -149,7 +149,13 @@ class Course_StudyAreasController extends AuthenticatedController } else { PageLayout::postError($msg); } - $this->redirect($url); + if (Request::isDialog()) { + $this->response->add_header('X-Dialog-Close', 1); + $this->response->add_header('X-Dialog-Execute', 'STUDIP.AdminCourses.App.reloadCourse'); + $this->render_text($this->course->id); + } else { + $this->redirect($url); + } } public function unassign() diff --git a/app/controllers/course/timesrooms.php b/app/controllers/course/timesrooms.php index 7e3d4596d4febb92e9ff40bda9f2c8d1e0c17cb8..7168bd7c99f5c2a6db8eb4386a8b1ba43dd3703b 100644 --- a/app/controllers/course/timesrooms.php +++ b/app/controllers/course/timesrooms.php @@ -245,6 +245,9 @@ class Course_TimesroomsController extends AuthenticatedController $this->checked_dates = $_SESSION['_checked_dates']; unset($_SESSION['_checked_dates']); } + if (Request::isDialog()) { + $this->response->add_header('X-Dialog-Execute', '{"func": "STUDIP.AdminCourses.App.reloadCourse", "payload": "'.$this->course->getId().'"}'); + } } /** @@ -314,6 +317,9 @@ class Course_TimesroomsController extends AuthenticatedController } $this->relocate(str_replace('_', '/', Request::option('origin'))); } + if (Request::isDialog()) { + $this->response->add_header('X-Dialog-Execute', '{"func": "STUDIP.AdminCourses.App.reloadCourse", "payload": "'.$this->course->getId().'"}'); + } } } diff --git a/app/controllers/resources/room_request.php b/app/controllers/resources/room_request.php index e2b476671d55e3c49e5ee475c53a20d91bf97cfc..03c65161229cf23249926d6c97c6ee2fc6fc8d88 100644 --- a/app/controllers/resources/room_request.php +++ b/app/controllers/resources/room_request.php @@ -1997,6 +1997,8 @@ class Resources_RoomRequestController extends AuthenticatedController if ($save_only) { // redirect to reload all infos and showing the most current ones $this->redirect('resources/room_request/resolve/' . $request_id); + } elseif (Request::isDialog()) { + $this->response->add_header('X-Dialog-Execute', '{"func": "STUDIP.AdminCourses.App.reloadCourse", "payload": "'.Context::get()->id.'"}'); } } diff --git a/app/views/admin/courses/_course.php b/app/views/admin/courses/_course.php deleted file mode 100644 index e12d31a16d3c60d102fb82019d458677ea4eb12a..0000000000000000000000000000000000000000 --- a/app/views/admin/courses/_course.php +++ /dev/null @@ -1,227 +0,0 @@ -<?php -/** - * Show course only if it has no parent course or the parent course is not - * part of the current view. Otherwise the current course will be listed - * as subcourse under its parent. - * - * @var array $values - * @var array $courses - * @var string $semid - * @var string $parent - * @var Admin_CoursesController $controller - * @var array $view_filter - * @var Semester $semester - * @var string $selected_action - */ -if (!$values['parent_course'] || !in_array($values['parent_course'], array_keys($courses))) : ?> - <?php - $course = Course::find($semid); - $children = []; - if ($GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$values['status']]['class']]['is_group']) { - $children = Course::findbyParent_Course($semid); - } - ?> - <tr id="course-<?= $semid ?>"<?= $parent ? ' class="subcourses subcourse-' . $parent . '"' : '' ?> data-course-id="<?= $semid ?>"> - <td> - <? if (Config::get()->ADMIN_COURSES_SHOW_COMPLETE): ?> - <? if ($GLOBALS['perm']->have_studip_perm('tutor', $semid)) : ?> - <a href="<?= $controller->toggle_complete($course) ?>" - class="course-completion" - data-course-completion="<?= $values['completion'] ?>" - title="<?= htmlReady($course->getCompetionLabel()) ?>" - aria-label="<?= _('Bearbeitungsstatus ändern') ?>"> - <?= _('Bearbeitungsstatus ändern') ?> - </a> - <? else : ?> - <?= $course->getCompletionIcon()->asImg(['title' => _('Bearbeitungsstatus kann nicht von Ihnen geändert werden.')]) ?> - <? endif ?> - <? else: ?> - <?= CourseAvatar::getAvatar($semid)->getImageTag(Avatar::SMALL, ['title' => trim($values['Name'])]) ?> - <? endif; ?> - </td> - <? if (in_array('number', $view_filter)) : ?> - <td> - <? if ($GLOBALS['perm']->have_studip_perm('autor', $semid)) : ?> - <a href="<?= URLHelper::getLink('seminar_main.php', ['auswahl' => $semid]) ?>"> - <? endif ?> - <?= htmlReady($values["VeranstaltungsNummer"]) ?> - <? if ($GLOBALS['perm']->have_studip_perm('autor', $semid)) : ?> - </a> - <? endif ?> - </td> - <? endif ?> - <? if (in_array('name', $view_filter)) : ?> - <td> - <? if ($GLOBALS['perm']->have_studip_perm("autor", $semid)) : ?> - <a href="<?= URLHelper::getLink('seminar_main.php', ['auswahl' => $semid]) ?>"> - <? endif ?> - <?= htmlReady($course->name) ?> - <? if ($GLOBALS['perm']->have_studip_perm("autor", $semid)) : ?> - </a> - <? endif ?> - <a data-dialog="buttons=false" href="<?= $controller->url_for(sprintf('course/details/index/%s', $semid)) ?>"> - <? $params = tooltip2(_("Veranstaltungsdetails anzeigen")); ?> - <? $params['style'] = 'cursor: pointer'; ?> - <?= Icon::create('info-circle', 'inactive')->asImg($params) ?> - </a> - <? if ($values["visible"] == 0) : ?> - <?= _("(versteckt)") ?> - <? endif ?> - <?php if (count($children) > 0) : ?> - <br> - <a href="" class="toggle-subcourses" data-get-subcourses-url="<?= $controller->url_for('admin/courses/get_subcourses', $semid) ?>"> - <?= Icon::create('add', 'clickable')->asImg(12) ?> - <?= Icon::create('remove', 'clickable', ['class' => 'hidden-js'])->asImg(12) ?> - <?= sprintf( - ngettext('%u Unterveranstaltung', '%u Unterveranstaltungen', - count($children)), - count($children)) ?> - </a> - <?php endif ?> - </td> - <? endif ?> - <? if (in_array('type', $view_filter)) : ?> - <td> - <?= htmlReady($GLOBALS['SEM_CLASS'][$GLOBALS['SEM_TYPE'][$values["status"]]["class"]]['name']) ?>: - <strong><?= htmlReady($GLOBALS['SEM_TYPE'][$values["status"]]["name"]) ?></strong> - </td> - <? endif ?> - <? if (in_array('room_time', $view_filter)) : ?> - <td class="raumzeit"> - <?= Seminar::GetInstance($semid)->getDatesHTML([ - 'semester_id' => $semester ? $semester->id : null, - 'show_room' => true, - ]) ?: _('nicht angegeben') ?> - </td> - <? endif ?> - <? if (in_array('semester', $view_filter)) : ?> - <td> - <?= htmlReady($course->semester_text) ?> - </td> - <? endif?> - <? if (in_array('institute', $view_filter)) : ?> - <td> - <?= htmlReady($course->home_institut ? $course->home_institut['name'] : $course['institute']) ?> - </td> - <? endif?> - <? if (in_array('requests', $view_filter)) : ?> - <td style="text-align: center;"> - <a title="<?=_('Raumanfragen')?>" href="<?= URLHelper::getLink('dispatch.php/course/room_requests', ['cid' => $semid])?>"> - <?= $values['requests'] ?> - </a> - </td> - <? endif ?> - <? if (in_array('teachers', $view_filter)) : ?> - <td> - <?= $this->render_partial_collection('my_courses/_dozent', $values['dozenten']) ?> - - </td> - <? endif ?> - <? if (in_array('members', $view_filter)) : ?> - <td style="text-align: center;"> - <a title="<?=_('Teilnehmende')?>" href="<?= URLHelper::getLink(count($children) > 0 ? 'dispatch.php/course/grouping/members' : 'dispatch.php/course/members', ['cid' => $semid]) ?>"> - <?= $values["teilnehmer"] ?> - </a> - </td> - <? endif ?> - <? if (in_array('waiting', $view_filter)) : ?> - <td style="text-align: center;"> - <a title="<?=_('Teilnehmende auf der Warteliste')?>" href="<?= URLHelper::getLink('dispatch.php/course/members', ['cid' => $semid])?>"> - <?= $values["waiting"] ?> - </a> - </td> - <? endif ?> - <? if (in_array('preliminary', $view_filter)) : ?> - <td style="text-align: center;"> - <a title="<?=_('Vorläufige Anmeldungen') ?>" href="<?= URLHelper::getLink('dispatch.php/course/members', ['cid' => $semid])?>"> - <?= $values['prelim'] ?> - </a> - </td> - <? endif ?> - <? if (in_array('contents', $view_filter)) : ?> - <td style="text-align: left; white-space: nowrap;"> - <? if (!empty($values['navigation'])) : ?> - <ul class="my-courses-navigation" style="flex-wrap: nowrap"> - <? foreach (MyRealmModel::array_rtrim($values['navigation']) as $key => $nav) : ?> - <? if ($nav instanceof Navigation && $nav->isVisible(true)) : ?> - <li class="my-courses-navigation-item <? if ($nav->getImage()->signalsAttention()) echo 'my-courses-navigation-important'; ?>"> - <a href="<?= - URLHelper::getLink('seminar_main.php', - ['auswahl' => $semid, - 'redirect_to' => $nav->getURL()]) ?>" <?= $nav->hasBadgeNumber() ? 'class="badge" data-badge-number="' . intval($nav->getBadgeNumber()) . '"' : '' ?>> - <?= $nav->getImage()->asImg(20, $nav->getLinkAttributes()) ?> - </a> - </li> - <? elseif (is_string($key)) : ?> - <li class="my-courses-navigation-item"> - <span class="empty-slot" style="width: 20px"></span> - </li> - <? endif ?> - <? endforeach ?> - </ul> - <? endif ?> - </td> - <? endif ?> - <? if (in_array('last_activity', $view_filter)) : ?> - <td style="text-align: center;"> - <span title="<?=_('Datum der letzten Aktivität in dieser Veranstaltung')?>"> - <?= htmlReady(date('d.m.Y', $values['last_activity'])); ?> - </span> - </td> - <? endif ?> - <? foreach (PluginManager::getInstance()->getPlugins("AdminCourseContents") as $plugin) : ?> - <? foreach ($plugin->adminAvailableContents() as $index => $label) : ?> - <? if (in_array($plugin->getPluginId()."_".$index, $view_filter)) : ?> - <td style="text-align: center;"> - <? $content = $plugin->adminAreaGetCourseContent($course, $index) ?> - <?= is_a($content, "Flexi_Template") ? $content->render() : $content ?> - </td> - <? endif ?> - <? endforeach ?> - <? endforeach ?> - <td class="actions"> - <? if (isset($actions[$selected_action]['partial']) && is_numeric($selected_action) && $GLOBALS['perm']->have_studip_perm('tutor', $semid)) : ?> - <?= $this->render_partial("admin/courses/{$actions[$selected_action]['partial']}", [ - 'course' => $course, - 'values' => $values, - 'action' => $actions[$selected_action], - ]) ?> - <? elseif (!is_numeric($selected_action)) : ?> - <? $plugin = PluginManager::getInstance()->getPlugin($selected_action) ?> - <? $template = $plugin->getAdminCourseActionTemplate($semid, $values) ?> - <? if ($template) : ?> - <?= $template->render() ?> - <? elseif ($GLOBALS['perm']->have_studip_perm('tutor', $semid)) : ?> - <?= - \Studip\LinkButton::create( - $actions[$selected_action]['title'], - URLHelper::getURL(sprintf($actions[$selected_action]['url'], $semid), - ($actions[$selected_action]['params'] ? $actions[$selected_action]['params'] : [])), - ($actions[$selected_action]['attributes'] ? $actions[$selected_action]['attributes'] : []) - ) ?> - <? endif ?> - <? elseif ($GLOBALS['perm']->have_studip_perm('tutor', $semid)) : ?> - <? $lockrules = [ - '2' => "sem_tree", - '3' => "room_time", - '11' => "seminar_copy", - '14' => "admission_type", - '16' => "seminar_archive", - '17' => "admission_type", - '18' => 'room_time' - ] ?> - <? if ($GLOBALS['perm']->have_studip_perm("admin", $semid) || !isset($lockrules[$selected_action]) || !LockRules::Check($semid, $lockrules[$selected_action])) : ?> - <?= - \Studip\LinkButton::create( - $actions[$selected_action]['title'], - URLHelper::getURL( - sprintf($actions[$selected_action]['url'], $semid), - $actions[$selected_action]['params'] ?? [] - ), - $actions[$selected_action]['attributes'] ?? [] - ) ?> - <? endif ?> - <? endif ?> - </td> - </tr> -<?php endif ?> diff --git a/app/views/admin/courses/aux-select.php b/app/views/admin/courses/aux-select.php index 950120e081f9062c782d90d617c1823934654f43..0b87029279fd43f9802508c71516660209e07a1d 100644 --- a/app/views/admin/courses/aux-select.php +++ b/app/views/admin/courses/aux-select.php @@ -10,7 +10,7 @@ --<?= _('keine Zusatzangaben') ?>-- </option> <? foreach ($aux_lock_rules as $rule) : ?> - <option value="<?= htmlReady($rule->id) ?>" <? if ($values['aux_lock_rule'] === $rule->id) echo 'selected'; ?>> + <option value="<?= htmlReady($rule->id) ?>" <? if ($course->aux_lock_rule === $rule->id) echo 'selected'; ?>> <?= htmlReady($rule->name) ?> </option> <? endforeach ?> @@ -18,6 +18,6 @@ <br> <label> <input type="checkbox" value="1" name="lock_sem_forced[<?= htmlReady($course->id) ?>]" - <?= $values['aux_lock_rule_forced'] ? 'checked' : '' ?>> + <?= $course->aux_lock_rule_forced ? 'checked' : '' ?>> <?=_('Erzwungen')?> </label> diff --git a/app/views/admin/courses/aux_preselect.php b/app/views/admin/courses/aux_preselect.php index 73a2036577e970c1d0451011a85345bb1c3db60f..d7fd801eb2aefaf26df7454829cc79d79c637565 100644 --- a/app/views/admin/courses/aux_preselect.php +++ b/app/views/admin/courses/aux_preselect.php @@ -21,4 +21,4 @@ <input type="checkbox" value="1" name="aux_all_forced"> <?=_('Erzwungen')?> </label> -<?= \Studip\Button::createAccept(_('Speichern'), 'all'); ?> +<?= \Studip\Button::createAccept(_('Speichern'), 'all', ['formaction' => URLHelper::getURL('dispatch.php/admin/courses/set_aux_lockrule')]); ?> diff --git a/app/views/admin/courses/get_subcourses.php b/app/views/admin/courses/get_subcourses.php deleted file mode 100644 index b2857ae7f45914932a0fb17386b1cbe4229ef2b4..0000000000000000000000000000000000000000 --- a/app/views/admin/courses/get_subcourses.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php -/** - * @var array $courses - * @var array $view_filter - * @var array $actions - * @var string $selected_action - * @var string $parent - */ -?> -<?php foreach ($courses as $semid => $values) : ?> - <?= $this->render_partial('admin/courses/_course', compact('semid', 'values', 'view_filter', 'actions', 'selected_action', 'parent', 'courses')) ?> -<?php endforeach; diff --git a/app/views/admin/courses/index.php b/app/views/admin/courses/index.php index 7145332b37f67035fc4b6b3b7f844fb1da613fc5..812790c615167d7b284fda551a21093367dc067e 100644 --- a/app/views/admin/courses/index.php +++ b/app/views/admin/courses/index.php @@ -2,19 +2,44 @@ /** * @var Admin_CoursesController $controller * @var int $count_courses + * @var Semester $semester + * @var array $fields + * @var array $activated_fields + * @var string $sortby + * @var string $sortflag + * @var array $activeSidebarElements + * @var int $max_show_courses */ + +$unsortable_fields = [ + 'avatar', + 'room_time', + 'contents' +]; ?> + <? if (empty($insts)): ?> <?= MessageBox::info(sprintf(_('Sie wurden noch keinen Einrichtungen zugeordnet. Bitte wenden Sie sich an einen der zuständigen %sAdministratoren%s.'), '<a href="' . URLHelper::getLink('dispatch.php/siteinfo/show') . '">', '</a>')) ?> -<? elseif (!empty($courses)): ?> - <?= $this->render_partial('admin/courses/courses.php', compact('courses')) ?> -<? elseif ($count_courses): ?> - <?= MessageBox::info(sprintf( - _('Es wurden %u Veranstaltungen gefunden. Grenzen Sie das Suchergebnis mit den Filtermöglichkeiten weiter ein, oder %slassen Sie sich alle Veranstaltungen anzeigen%s.'), - $count_courses, - '<a href="' . $controller->url_for('admin/courses', ['display' => 'all']) . '">', - '</a>' - )) ?> -<? else: ?> - <?= MessageBox::info(_('Ihre Suche ergab keine Treffer')) ?> +<? else : + + $attributes = [ + ':show-complete' => json_encode((bool) Config::get()->ADMIN_COURSES_SHOW_COMPLETE), + ':fields' => json_encode($fields), + ':unsortable-fields' => json_encode($unsortable_fields), + ':max-courses' => (int) $max_show_courses, + 'sort-by' => $sortby, + 'sort-flag' => $sortflag, + ]; +?> + <form method="post"> + <?= CSRFProtection::tokenTag() ?> + + <div class="admin-courses-vue-app course-admin" + is="AdminCourses" + v-cloak + ref="app" + <?= arrayToHtmlAttributes($attributes) ?> + ></div> + </form> + <? endif; ?> diff --git a/app/views/admin/courses/lock.php b/app/views/admin/courses/lock.php index 459b8f744fdad80fb1b545cea1de2b175f8bdc03..d364f89bc22e6827c975e85863f77d0abb1b475b 100644 --- a/app/views/admin/courses/lock.php +++ b/app/views/admin/courses/lock.php @@ -5,13 +5,13 @@ * @var Course $course */ ?> -<? $current_lock_rule = $all_lock_rules->findOneBy('lock_id', $values['lock_rule']); ?> +<? $current_lock_rule = $all_lock_rules->findOneBy('lock_id', $course->lock_rule); ?> <? if (!$GLOBALS['perm']->have_perm('root') && ($current_lock_rule['permission'] == 'admin' || $current_lock_rule['permission'] == 'root')) : ?> <?= htmlReady($current_lock_rule['name'])?> <? else : ?> <select name="lock_sem[<?= htmlReady($course->id) ?>]" style="max-width: 200px"> <? foreach ($all_lock_rules as $lock_rule): ?> - <option value="<?= $lock_rule['lock_id'] ?>" <?= $lock_rule['lock_id'] == $values['lock_rule'] ? 'selected' : '' ?>> + <option value="<?= $lock_rule['lock_id'] ?>" <?= $lock_rule['lock_id'] === $course->lock_rule ? 'selected' : '' ?>> <?= htmlReady($lock_rule['name']) ?> </option> <? endforeach; ?> diff --git a/app/views/admin/courses/lock_preselect.php b/app/views/admin/courses/lock_preselect.php index b9bfcef73fa04c6ce2d075ab5e4a04d44e28d6ea..433230e73ae1993d7335bd7f87423f2b25eff0d9 100644 --- a/app/views/admin/courses/lock_preselect.php +++ b/app/views/admin/courses/lock_preselect.php @@ -1,6 +1,6 @@ <?php /** - * @var array $values + * @var Course $course * @var SimpleCollection $all_lock_rules */ ?> @@ -8,11 +8,11 @@ <select name="lock_sem_all" style="max-width: 200px"> <? for ($i = 0; $i < count($all_lock_rules); $i++) : ?> <option value="<?= $all_lock_rules[$i]["lock_id"] ?>" - <?= ($all_lock_rules[$i]["lock_id"] == $values['lock_rule']) ? 'selected' : '' ?>> + <?= $all_lock_rules[$i]['lock_id'] === $course->lock_rule ? 'selected' : '' ?>> <?= htmlReady($all_lock_rules[$i]["name"]) ?> </option> <? endfor ?> </select> </label> -<?= \Studip\Button::createAccept(_('Zuweisen'), 'all'); ?> +<?= \Studip\Button::createAccept(_('Zuweisen'), 'all', ['formaction' => URLHelper::getURL('dispatch.php/admin/courses/set_lockrule')]); ?> diff --git a/app/views/admin/courses/sidebar.php b/app/views/admin/courses/sidebar.php index 92263f1946b7fbb603cc851fc8625a5cdf55befd..6b243caa42806b51258de36015185f934ba7c534 100644 --- a/app/views/admin/courses/sidebar.php +++ b/app/views/admin/courses/sidebar.php @@ -17,16 +17,16 @@ </label> <label> - <input name="instituteActive" type="checkbox" value="1" - <?= (!empty($userSelectedElements['institute'])) ? 'checked' : '' ?> + <input name="semesterActive" type="checkbox" value="1" + <?= !empty($userSelectedElements['semester']) ? 'checked' : '' ?> > - <?= _('Einrichtung'); ?> + <?= _('Semester'); ?> </label> <label> - <input name="semesterActive" type="checkbox" value="1" - <?= (!empty($userSelectedElements['semester'])) ? 'checked' : '' ?> + <input name="instituteActive" type="checkbox" value="1" + <?= !empty($userSelectedElements['institute']) ? 'checked' : '' ?> > - <?= _('Semester'); ?> + <?= _('Einrichtung'); ?> </label> <label> <input name="stgteilActive" type="checkbox" value="1" @@ -34,18 +34,18 @@ > <?= _('Studiengangteil'); ?> </label> - <label> - <input name="courseTypeActive" type="checkbox" value="1" - <?= (!empty($userSelectedElements['courseType'])) ? 'checked' : '' ?> - > - <?= _('Veranstaltungstypfilter'); ?> - </label> <label> <input name="teacherActive" type="checkbox" value="1" <?= (!empty($userSelectedElements['teacher'])) ? 'checked' : '' ?> - > + > <?= _('Lehrperson'); ?> </label> + <label> + <input name="courseTypeActive" type="checkbox" value="1" + <?= !empty($userSelectedElements['courseType']) ? 'checked' : '' ?> + > + <?= _('Veranstaltungstypfilter'); ?> + </label> <label> <input name="viewFilterActive" type="checkbox" value="1" <?= (!empty($userSelectedElements['viewFilter'])) ? 'checked' : '' ?> diff --git a/app/views/course/lvgselector/index.php b/app/views/course/lvgselector/index.php index ca9b46f14735c67f4828aeef49b7b796858c5ddb..925c1c4844efa3afc16e3a94abd94e2f191cb2ca 100644 --- a/app/views/course/lvgselector/index.php +++ b/app/views/course/lvgselector/index.php @@ -1,5 +1,7 @@ <? if (!$locked) : ?> - <form action="<?= $controller->link_for('course/lvgselector/index/' . $course_id, $url_params ?? []) ?>" method="post"> + <form action="<?= $controller->link_for('course/lvgselector/index/' . $course_id, $url_params ?? []) ?>" + <?= Request::isDialog() ? 'data-dialog' : '' ?> + method="post"> <? endif ?> <h1><?= _('Lehrveranstaltungsgruppen') ?></h1> <div id="assigned" data-ajax-url="<?= $ajax_url ?>" data-forward-url="<?= $no_js_url ?>"> diff --git a/app/views/course/study_areas/show.php b/app/views/course/study_areas/show.php index efec1b45025f976c0c0a39ffefb036cc84b568dc..5bf7a5af0460a0bd2774617f7200c3b5b63176b1 100644 --- a/app/views/course/study_areas/show.php +++ b/app/views/course/study_areas/show.php @@ -1,5 +1,7 @@ <? if (!$locked) : ?> - <form action="<?= $controller->url_for('course/study_areas/save/' . $course->id, $url_params) ?>" method="post"> + <form action="<?= $controller->link_for('course/study_areas/save/' . $course->id, $url_params) ?>" + <?= Request::isDialog() ? 'data-dialog' : '' ?> + method="post"> <? endif?> <?= $tree ?> <div style="text-align: center;"> diff --git a/db/migrations/5.4.9_add_datafields_filter_config.php b/db/migrations/5.4.9_add_datafields_filter_config.php new file mode 100644 index 0000000000000000000000000000000000000000..b44d108cde703b5f4a17edc25e322ca378bab1c8 --- /dev/null +++ b/db/migrations/5.4.9_add_datafields_filter_config.php @@ -0,0 +1,26 @@ +<?php + +class AddDatafieldsFilterConfig extends Migration +{ + protected function up() + { + $query = "INSERT IGNORE INTO `config` (`field`, `value`, `type`, `range`, `mkdate`, `chdate`, `description`) + VALUES (:name, :value, :type, :range, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :description)"; + + $statement = DBManager::get()->prepare($query); + $statement->execute([ + ':name' => 'ADMIN_COURSES_DATAFIELDS_FILTERS', + ':description' => 'Für Admins, Roots und DedicatedAdmins können hier die Datenfelder gespeichert werden, nach denen die Veranstaltungen gefiltert werden sollen.', + ':range' => 'user', + ':type' => 'array', + ':value' => '[]' + ]); + } + + protected function down() + { + DBManager::get()->prepare(' + DELETE FROM `config` WHERE `field` = "ADMIN_COURSES_DATAFIELDS_FILTERS" + '); + } +} diff --git a/lib/classes/AdminCourseFilter.class.php b/lib/classes/AdminCourseFilter.class.php index 7ec1c7bf6db189c8aa6aed3c9c50bdb6607fa882..68b96d2a8709b489ed10a6b0e6307c07e91ae61a 100644 --- a/lib/classes/AdminCourseFilter.class.php +++ b/lib/classes/AdminCourseFilter.class.php @@ -18,39 +18,17 @@ * public function addLectureshipFilter($event, $filter) * { * if ($GLOBALS['user']->cfg->getValue("LECTURESHIP_FILTER")) { - * $filter->settings['query']['joins']['lehrauftrag'] = array( - * 'join' => "INNER JOIN", - * 'on' => "seminare.Seminar_id = lehrauftrag.seminar_id" - * ); + * $filter->query->join('lehrauftrag', 'seminare.Seminar_id = lehrauftrag.seminar_id'); * } * } * - * Within this method you alter the public $filter->settings array, because this array - * describes entirely the big query for the admin-search. In our example above - * we simple add an INNER JOIN to filter for the course having an entry in - * the lehrauftrag table. - * - * Description of this array is as follows: - * - * $filter->settings['query'] : The main sql query as a prepared statement. - * $filter->settings['query']['select'] : An assoc array. $filter->settings['query']['select']['Number_of_teachers'] = "COUNT(DISTINCT dozenten.user_id)" - * will select the result of COUNT as the variable Number_of_teachers. - * $filter->settings['query']['joins'] : Example $filter->settings['query']['joins']['dozenten'] = array( - * 'join' => "INNER JOIN", //default value, else use "LEFT JOIN" - * 'table' => "seminar_user", //can me omitted if you don't want to use a table-alias - * 'on' => "dozenten.Seminar_id = seminare.Seminar_id AND dozenten.status = 'dozent'" - * ) - * if 'table' differs from the index, the index will be the alias of the table. - * So normally you don't need to name a table if you don't want it to be aliased. - * $filter->settings['query']['where'] : You might want to use the method $filter->where($sql, $parameter) instead. - * $filter->settings['query']['orderby'] : You might want to use $filter->orderBy($attribute, $flag = "ASC") instead. - * $filter->settings['parameter'] : An assoc array of parameter that will be passed to - * the prepared statement. + * Within this method you alter the public $filter->query object. That query object is of type SQLQuery. * */ class AdminCourseFilter { static protected $instance = null; + public $query = null; public $max_show_courses = 500; public $settings = []; @@ -68,288 +46,131 @@ class AdminCourseFilter } /** - * Constructor of the singleton-object. The settings might come from the session - * if $reset_settings is false. - * @param bool $reset_settings : should the session settings of the singleton be reset? - */ - public function __construct($reset_settings = false) - { - $this->initSettings(); - - if ($reset_settings) { - $this->resetSettings(); - } else { - $this->restoreSettings(); - } - } - - /** - * store settings in session - */ - public function storeSettings() - { - $_SESSION['AdminCourseFilter_settings'] = $this->settings; - } - - /** - * restore settings from session - */ - public function restoreSettings() - { - if ($_SESSION['AdminCourseFilter_settings']) { - $this->settings = $_SESSION['AdminCourseFilter_settings']; - } - } - - /** - * reset settings + * Constructor of the singleton-object. */ - public function resetSettings() + public function __construct() { $this->initSettings(); - unset($_SESSION['AdminCourseFilter_settings']); } - /** - * initialize settings - */ - public function initSettings() - { - $this->settings = []; - - $this->settings['query']['select'] = [ - 'Institut' => "Institute.Name", - 'teilnehmer' => "(SELECT COUNT(seminar_id) - FROM seminar_user - WHERE seminar_id = seminare.Seminar_id AND status != 'dozent' AND status != 'tutor')", - 'prelim' => "(SELECT COUNT(seminar_id) - FROM admission_seminar_user - WHERE seminar_id = seminare.Seminar_id AND status = 'accepted')", - 'waiting' => "(SELECT COUNT(seminar_id) - FROM admission_seminar_user - WHERE seminar_id = seminare.Seminar_id AND status = 'awaiting')", - 'requests' => "(SELECT COUNT(id) - FROM resource_requests - WHERE course_id = seminare.Seminar_id)", - 'course_set' => "(SELECT set_id FROM seminar_courseset WHERE seminar_id = seminare.Seminar_id LIMIT 1)" - ]; - $this->settings['query']['joins'] = [ - 'seminar_inst' => [ - 'join' => "INNER JOIN", - 'on' => "seminare.Seminar_id = seminar_inst.seminar_id" - ], - 'Institute' => [ - 'join' => "INNER JOIN", - 'on' => "seminar_inst.institut_id = Institute.Institut_id" - ], - 'sem_types' => [ - 'join' => "LEFT JOIN", - 'on' => "sem_types.id = seminare.status" - ], - 'sem_classes' => [ - 'join' => "LEFT JOIN", - 'on' => "sem_classes.id = sem_types.class" - ] - ]; - $this->settings['query']['where'] = []; - $this->settings['query']['orderby'] = Config::get()->IMPORTANT_SEMNUMBER ? "seminare.veranstaltungsnummer, seminare.name" : "seminare.name"; - } - - /** - * Adds a filter for all courses of the given semester. - * @param string $semester_id : ID of the given semester. - * @return AdminCourseFilter - * @throws Exception if semester_id does not exist - */ - public function filterBySemester($semester_id) - { - $semester = Semester::find($semester_id); - if (!$semester) { - throw new Exception("Das ausgewählte Semester scheint nicht zu existieren."); - } - $this->settings['query']['joins']['semester_courses'] = [ - 'join' => "LEFT JOIN", - 'on' => "semester_courses.course_id = seminare.Seminar_id" - ]; - $this->settings['query']['where']['semester'] = "(semester_courses.semester_id IS NULL OR semester_courses.semester_id = :semester_id)"; - $this->settings['parameter']['semester_beginn'] = $semester['beginn']; - $this->settings['parameter']['semester_id'] = $semester['id']; - return $this; - } - - /** - * Adds a filter for a sem_type or many sem_types if the parameter is an array. - * @param array|integer $type : id or ids of sem_types - * @return AdminCourseFilter - */ - public function filterByType($type) - { - if (is_array($type)) { - $this->settings['query']['where']['status'] = "seminare.status IN (:types)"; - $this->settings['parameter']['types'] = $type; + protected function initSettings() + { + $this->query = SQLQuery::table('seminare'); + $this->query->join('sem_types', 'sem_types', 'sem_types.id = seminare.status'); + $this->query->join('sem_classes', 'sem_classes', 'sem_classes.id = sem_types.class'); + $this->query->where("sem_classes.studygroup_mode = '0'"); + $this->query->groupBy('seminare.Seminar_id'); + + if ($GLOBALS['user']->cfg->ADMIN_COURSES_SEARCHTEXT) { + $this->query->join('teachers_su', 'seminar_user', "teachers_su.Seminar_id = seminare.Seminar_id AND teachers_su.status = 'dozent'"); + $this->query->join('teachers', 'auth_user_md5', 'teachers.user_id = teachers_su.user_id'); + $this->query->where( + 'search', + "(seminare.name LIKE :search OR seminare.VeranstaltungsNummer LIKE :search OR seminare.untertitel LIKE :search OR CONCAT(teachers.Vorname, ' ', teachers.Nachname) LIKE :search)", + ['search' => '%'.$GLOBALS['user']->cfg->ADMIN_COURSES_SEARCHTEXT.'%'] + ); + } + if (Request::option('course_id')) { + $this->query->where('course_id', 'seminare.Seminar_id = :course_id', ['course_id' => Request::option('course_id')]); + } + $inst_ids = []; + + if ( + !$GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT + || $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT === 'all' + ) { + $inst = new SimpleCollection(Institute::getMyInstitutes($GLOBALS['user']->id)); + $inst_ids = $inst->map(function ($a) { + return $a['Institut_id']; + }); } else { - $this->settings['query']['where']['status'] = "seminare.status = :type"; - $this->settings['parameter']['type'] = (int) $type; + //We must check, if the institute ID belongs to a faculty + //and has the string _i appended to it. + //In that case we must display the courses of the faculty + //and all its institutes. + //Otherwise we just display the courses of the faculty. + + $include_children = false; + $inst_id = $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT; + if (str_contains($inst_id, '_')) { + $inst_id = substr($inst_id, 0, strpos($inst_id, '_')); + $include_children = true; + } + $inst_ids[] = $inst_id; + + if ($include_children) { + $inst = Institute::find($inst_id); + if ($inst && $inst->isFaculty()) { + foreach ($inst->sub_institutes->pluck('Institut_id') as $institut_id) { + $inst_ids[] = $institut_id; + } + } + } } - return $this; - } - /** - * Adds a filter for an institut_id or many institut_ids if the parameter is an array. - * @param array|integer $institut_ids : id or ids of institutes - * @return AdminCourseFilter - */ - public function filterByInstitute($institut_ids) - { if (Config::get()->ALLOW_ADMIN_RELATED_INST) { $sem_inst = 'seminar_inst'; + $this->query->join('seminar_inst', 'seminar_inst', 'seminar_inst.seminar_id = seminare.Seminar_id'); } else { $sem_inst = 'seminare'; } - if (is_array($institut_ids)) { - $this->settings['query']['where']['institute'] = "$sem_inst.institut_id IN (:institut_ids)"; - $this->settings['parameter']['institut_ids'] = $institut_ids; - } else { - $this->settings['query']['where']['status'] = "$sem_inst.institut_id = :institut_id"; - $this->settings['parameter']['institut_id'] = (string) $institut_ids; + $this->query->where('seminar_inst', "$sem_inst.institut_id IN (:institut_ids)"); + $this->query->parameter('institut_ids', $inst_ids); + + if ($GLOBALS['user']->cfg->MY_COURSES_SELECTED_CYCLE) { + $this->query->join('semester_courses', 'semester_courses.course_id = seminare.Seminar_id'); + $this->query->where('semester_id', '(semester_courses.semester_id = :semester_id OR semester_courses.semester_id IS NULL)', [ + 'semester_id' => $GLOBALS['user']->cfg->MY_COURSES_SELECTED_CYCLE + ]); } - return $this; - } - /** - * Adds a filter for an stgteil_id or many stgteil_ids if the parameter is an array. - * @param array|integer $stgteil_ids : id or ids of stgteile - * @return AdminCourseFilter - */ - public function filterByStgTeil($stgteil_ids) - { - $this->settings['query']['joins']['mvv_lvgruppe_seminar'] = [ - 'join' => "LEFT JOIN", - 'table' => "mvv_lvgruppe_seminar", - 'on' => "mvv_lvgruppe_seminar.seminar_id = seminare.Seminar_id" - ]; - $this->settings['query']['joins']['mvv_lvgruppe_modulteil'] = [ - 'join' => "LEFT JOIN", - 'table' => "mvv_lvgruppe_modulteil", - 'on' => "mvv_lvgruppe_modulteil.lvgruppe_id = mvv_lvgruppe_seminar.lvgruppe_id" - ]; - $this->settings['query']['joins']['mvv_modulteil'] = [ - 'join' => "LEFT JOIN", - 'table' => "mvv_modulteil", - 'on' => "mvv_modulteil.modulteil_id = mvv_lvgruppe_modulteil.modulteil_id" - ]; - $this->settings['query']['joins']['mvv_stgteilabschnitt_modul'] = [ - 'join' => "LEFT JOIN", - 'table' => "mvv_stgteilabschnitt_modul", - 'on' => "mvv_stgteilabschnitt_modul.modul_id = mvv_modulteil.modul_id" - ]; - $this->settings['query']['joins']['mvv_stgteilabschnitt'] = [ - 'join' => "LEFT JOIN", - 'table' => "mvv_stgteilabschnitt", - 'on' => "mvv_stgteilabschnitt.abschnitt_id = mvv_stgteilabschnitt_modul.abschnitt_id" - ]; - $this->settings['query']['joins']['mvv_stgteilversion'] = [ - 'join' => "LEFT JOIN", - 'table' => "mvv_stgteilversion", - 'on' => "mvv_stgteilversion.version_id = mvv_stgteilabschnitt.version_id" - ]; + if ($GLOBALS['user']->cfg->MY_COURSES_TYPE_FILTER && $GLOBALS['user']->cfg->MY_COURSES_TYPE_FILTER !== 'all') { + if (str_contains(Request::option('course_type'), '_')) { + list($sem_class_id, $sem_type_id) = explode('_', $GLOBALS['user']->cfg->MY_COURSES_TYPE_FILTER); + $this->query->where('course_type', 'seminare.status = :course_type', ['course_type' => $sem_type_id]); + } else { + //sem class + $this->query->where('course_class', 'sem_types.class = :course_class', [ + 'course_class' => $GLOBALS['user']->cfg->MY_COURSES_TYPE_FILTER + ]); + } - if (is_array($stgteil_ids)) { - $this->settings['query']['where']['mvv_stgteilversion'] = "mvv_stgteilversion.stgteil_id IN (:stgteil_ids)"; - $this->settings['parameter']['stgteil_ids'] = $stgteil_ids; - } else { - $this->settings['query']['where']['mvv_stgteilversion'] = "mvv_stgteilversion.stgteil_id = :stgteil_id"; - $this->settings['parameter']['stgteil_id'] = (string) $stgteil_ids; } - return $this; - } - /** - * @param array|string $user_ids - * @return AdminCourseFilter - */ - public function filterByDozent($user_ids) - { - $this->settings['query']['joins']['dozenten'] = [ - 'join' => "INNER JOIN", - 'table' => "seminar_user", - 'on' => "dozenten.Seminar_id = seminare.Seminar_id AND dozenten.status = 'dozent'" - ]; - if (is_array($user_ids)) { - $this->settings['query']['where']['dozenten'] = "dozenten.user_id IN (:dozenten_ids)"; - $this->settings['parameter']['dozenten_ids'] = $user_ids; - } else { - $this->settings['query']['where']['dozenten'] = "dozenten.user_id = :dozenten_id"; - $this->settings['parameter']['dozenten_id'] = (string) $user_ids; + if ($GLOBALS['user']->cfg->MY_COURSES_SELECTED_STGTEIL) { + $this->query->join('mvv_lvgruppe_seminar', '`mvv_lvgruppe_seminar`.`seminar_id` = `seminare`.`Seminar_id`'); + $this->query->join('mvv_lvgruppe_modulteil', '`mvv_lvgruppe_modulteil`.`lvgruppe_id` = `mvv_lvgruppe_seminar`.`lvgruppe_id`'); + $this->query->join('mvv_modulteil', '`mvv_modulteil`.`modulteil_id` = `mvv_lvgruppe_modulteil`.`modulteil_id`'); + $this->query->join('mvv_modul', '`mvv_modul`.`modul_id` = `mvv_modulteil`.`modul_id`'); + $this->query->join('mvv_stgteilabschnitt_modul', '`mvv_stgteilabschnitt_modul`.`modul_id` = `mvv_modul`.`modul_id`'); + $this->query->join('mvv_stgteilabschnitt', '`mvv_stgteilabschnitt`.`abschnitt_id` = `mvv_stgteilabschnitt_modul`.`abschnitt_id`'); + $this->query->join('mvv_stgteilversion', '`mvv_stgteilversion`.`version_id` = `mvv_stgteilabschnitt`.`version_id`'); + $this->query->where('stgteil', 'mvv_stgteilversion.stgteil_id = :stgteil_id', [ + 'stgteil_id' => $GLOBALS['user']->cfg->MY_COURSES_SELECTED_STGTEIL + ]); } - return $this; - } - /** - * Adds a filter for a textstring, that can be the coursenumber, the name of the course - * or the last name of one of the dozenten. - * @param string $text the searchstring - * @return AdminCourseFilter - */ - public function filterBySearchstring($text) - { - $this->settings['query']['joins']['dozenten'] = [ - 'join' => "INNER JOIN", - 'table' => "seminar_user", - 'on' => "dozenten.Seminar_id = seminare.Seminar_id AND dozenten.status = 'dozent'" - ]; - $this->settings['query']['joins']['dozentendata'] = [ - 'join' => "INNER JOIN", - 'table' => "auth_user_md5", - 'on' => "dozenten.user_id = dozentendata.user_id" - ]; - $this->settings['query']['where']['search'] = "(CONCAT_WS(' ', seminare.VeranstaltungsNummer, seminare.name, seminare.Untertitel, dozentendata.Nachname) LIKE :search - OR CONCAT(dozentendata.Nachname, ', ', dozentendata.Vorname) LIKE :search - OR CONCAT_WS(' ', dozentendata.Vorname, dozentendata.Nachname) LIKE :search - OR dozentendata.Vorname LIKE :search - OR dozentendata.Nachname LIKE :search - )"; - $this->settings['parameter']['search'] = "%".$text."%"; + if ($GLOBALS['user']->cfg->ADMIN_COURSES_TEACHERFILTER) { + $this->query->join('teachers_su', 'seminar_user', "teachers_su.Seminar_id = seminare.Seminar_id AND teachers_su.status = 'dozent'"); + $this->query->where( + 'teacher_filter', + "teachers_su.user_id = :teacher_id", + ['teacher_id' => $GLOBALS['user']->cfg->ADMIN_COURSES_TEACHERFILTER] + ); + } - return $this; - } - /** - * @param string $attribute : column, name of the column, yb whcih we should order the results - * @param string $flag : "ASC" or "DESC for ascending order or descending order, - * @return AdminCourseFilter - * @throws Exception if $flag does not exist - */ - public function orderBy($attribute, $flag = 'ASC') - { - $flag = mb_strtoupper($flag); - if (!in_array($flag, words('ASC DESC'))) { - throw new Exception("Sortierreihenfolge undefiniert."); - } - if (in_array($attribute, words('VeranstaltungsNummer Name status teilnehmer waiting prelim requests completion start_time Institute.Name'))) { - $this->settings['query']['orderby'] = $attribute . ' ' . $flag; - } - return $this; - } - /** - * Adds a where filter. - * @param string $where any where condition like "sem_classes.overview = 'CoreOverview'" - * @param array $parameter an array of parameter that appear in the $where query. - * @param null|string $id an id of the where-query. Use this to possibly - * avoid double where conditions or allow deleting the condition - * by plugins if necessary. Can be omitted. - * @return AdminCourseFilter - */ - public function where($where, $parameter = [], $id = null) - { - if (!$id) { - $id = md5($where); + $datafields_filters = $GLOBALS['user']->cfg->ADMIN_COURSES_DATAFIELDS_FILTERS; + foreach ($datafields_filters as $datafield_id => $value) { + $this->query->join('de_'.$datafield_id, 'datafields_entries', 'de_'.$datafield_id.'.range_id = seminare.Seminar_id AND `de_'.$datafield_id.'`.datafield_id = :de_'.$datafield_id.'_id'); + $this->query->where('de_' . $datafield_id . '_contents', 'de_' . $datafield_id . '.`content` LIKE :de_' . $datafield_id . '_content', + [ + 'de_' . $datafield_id . '_id' => $datafield_id, + 'de_' . $datafield_id . '_content' => '%' . $value . '%' + ]); } - $this->settings['query']['where'][$id] = $where; - $this->settings['parameter'] = array_merge((array)($this->settings['parameter'] ?? []), $parameter); - return $this; } /** @@ -359,16 +180,10 @@ class AdminCourseFilter * Plugins may register at this event to fully alter this AdminCourseFilter-object and so the resultset. * @return array associative array with seminar_ids as keys and seminar-data-arrays as values. */ - public function getCourses($grouped = true) + public function getCourses() { NotificationCenter::postNotification("AdminCourseFilterWillQuery", $this); - if (empty($this->settings['query']['where'])) { - return []; - } - $statement = DBManager::get()->prepare($this->createQuery()); - $statement->execute($this->settings['parameter']); - $_SESSION['AdminCourseFilter_settings'] = $this->settings; - return $statement->fetchAll($grouped ? (PDO::FETCH_GROUP | PDO::FETCH_ASSOC) : PDO::FETCH_ASSOC); + return $this->query->fetchAll(Course::class); } /** @@ -377,10 +192,7 @@ class AdminCourseFilter public function countCourses() { NotificationCenter::postNotification("AdminCourseFilterWillQuery", $this); - if (empty($this->settings['query']['where'])) { - return 0; - } - return (int)DBManager::get()->fetchColumn($this->createQuery(true), $this->settings['parameter']); + return $this->query->count(); } /** @@ -400,54 +212,10 @@ class AdminCourseFilter $order = 'seminare.veranstaltungsnummer, seminare.name'; } if ($count_courses && $count_courses <= $this->max_show_courses) { - $settings = $this->settings; - $this->settings['query']['select'] = []; - $this->settings['query']['orderby'] = $order; - $ret = $this->getCourses(false); - $this->settings = $settings; - return $ret; + $this->query->orderBy($order); + return $this->getCourses(); } return []; } - /** - * Creates the sql-query from the $this->settings['query'] - * @param boolean $only_count : boolean - * @return string the big query - */ - public function createQuery($only_count = false) - { - if ($only_count) { - $select_query = "COUNT(DISTINCT seminare.Seminar_id) "; - } else { - $select_query = "seminare.* "; - foreach ((array) $this->settings['query']['select'] as $alias => $select) { - $select_query .= ", ".$select." AS ".$alias." "; - } - } - - $join_query = ""; - foreach ((array) $this->settings['query']['joins'] as $alias => $joininfo) { - $table = isset($joininfo['table']) ? $joininfo['table']." AS ".$alias : $alias; - $on = isset($joininfo['on']) ? " ON (".$joininfo['on'].")" : ""; - $join_query .= " ".(isset($joininfo['join']) ? $joininfo['join'] : "INNER JOIN")." ".$table.$on." "; - } - - $where_query = ""; - if (count($this->settings['query']['where']) > 0) { - $where_query .= implode(" AND ", $this->settings['query']['where']); - } - - $query = " - SELECT ".$select_query." - FROM seminare - ".$join_query." - ".($where_query ? "WHERE ".$where_query : ""); - if (!$only_count) { - $query .= " GROUP BY seminare.Seminar_id ORDER BY ".$this->settings['query']['orderby'].($this->settings['query']['orderby'] !== "seminare.name" ? ", seminare.name" : ""); - } - - return $query; - } - } diff --git a/lib/classes/SQLQuery.php b/lib/classes/SQLQuery.php index 3a09fd9568b814b74254861c62939eb1788c1b12..0442604ff6e2933bd411c9c5e8ac58e56254234c 100644 --- a/lib/classes/SQLQuery.php +++ b/lib/classes/SQLQuery.php @@ -283,20 +283,20 @@ class SQLQuery $on = isset($joindata['on']) ? " ON ({$joindata['on']})" : ''; $sql .= " " . (isset($joindata['join']) ? $joindata['join'] : 'INNER JOIN') . " {$table}{$on} "; } - if ($this->settings['where']) { + if (!empty($this->settings['where'])) { $sql .= "WHERE (" . implode(") AND (", $this->settings['where']) . ") "; } - if ($this->settings['groupby']) { + if (!empty($this->settings['groupby'])) { $sql .= "GROUP BY {$this->settings['groupby']} "; } - if ($this->settings['having']) { + if (!empty($this->settings['having'])) { $sql .= "HAVING (" . implode(") AND (", $this->settings['having']) . ") "; } - if ($this->settings['order']) { + if (!empty($this->settings['order'])) { $sql .= "ORDER BY {$this->settings['order']} "; } - if ($this->settings['limit']) { + if (!empty($this->settings['limit'])) { $sql .= "LIMIT ". (int) $this->settings['limit'][0] . ", " . (int) $this->settings['limit'][1] . " "; } return $sql; diff --git a/lib/classes/sidebar/OptionsWidget.php b/lib/classes/sidebar/OptionsWidget.php index 0cb805cef85965d76e94d724f269f92fbf1a6e21..a7931ce57852979e4fff1875e7dbb7d91ec0745b 100644 --- a/lib/classes/sidebar/OptionsWidget.php +++ b/lib/classes/sidebar/OptionsWidget.php @@ -34,8 +34,9 @@ class OptionsWidget extends ListWidget $toggle_url_off = isset($toggle_url_off) ? html_entity_decode($toggle_url_off) : null; $content = sprintf( - '<a href="%s" class="options-checkbox options-%s" %s>%s</a>', + '<a href="%s" role="checkbox" aria-checked="%s" class="options-checkbox options-%s" %s>%s</a>', htmlReady($state && $toggle_url_off !== null ? $toggle_url_off : $toggle_url), + $state ? 'true' : 'false', $state ? 'checked' : 'unchecked', arrayToHtmlAttributes($attributes), htmlReady($label) diff --git a/lib/classes/sidebar/SearchWidget.php b/lib/classes/sidebar/SearchWidget.php index 82aa26b8c9952b182e83b4a0365c0ce1feded0bb..16d577b62d8a15bc614f89de1bedf0eec6ad0f38 100644 --- a/lib/classes/sidebar/SearchWidget.php +++ b/lib/classes/sidebar/SearchWidget.php @@ -16,6 +16,7 @@ class SearchWidget extends SidebarWidget protected $filters = []; protected $method = 'get'; protected $id = null; + protected $onsubmit = null; /** * Constructor for the widget. @@ -105,6 +106,11 @@ class SearchWidget extends SidebarWidget return count($this->elements) + count($this->needles) + count($this->filters) > 0; } + public function setOnSubmitHandler($onsubmit) + { + $this->onsubmit = $onsubmit; + } + /** * Renders the widget. * @@ -167,6 +173,7 @@ class SearchWidget extends SidebarWidget $this->template_variables['filters'] = $this->filters; $this->template_variables['has_data'] = $this->hasData(); + $this->template_variables['onsubmit'] = $this->onsubmit; return parent::render($variables); } diff --git a/lib/classes/sidebar/SelectWidget.php b/lib/classes/sidebar/SelectWidget.php index 73971757b8a70a466cfa5dafc64ba1d36fe6db1e..772004cb78b643d8cdab88165bb065d05edf35c0 100644 --- a/lib/classes/sidebar/SelectWidget.php +++ b/lib/classes/sidebar/SelectWidget.php @@ -8,6 +8,9 @@ */ class SelectWidget extends SidebarWidget { + + protected $onsubmit = null; + /** * Constructs the widget by defining a special template. * @@ -125,6 +128,11 @@ class SelectWidget extends SidebarWidget } } + public function setOnSubmitHandler($onsubmit) + { + $this->onsubmit = $onsubmit; + } + /** * Renders the select widget * @param array $variables Additional vaiarbles @@ -141,6 +149,7 @@ class SelectWidget extends SidebarWidget } $this->template_variables['class'] .= ' submit-upon-select'; } + $this->template_variables['onsubmit'] = $this->onsubmit; return parent::render($variables); } diff --git a/lib/models/ConfigValue.php b/lib/models/ConfigValue.php index 829567d4ea94bf17e2d3e55fb1093cf064d61bd7..f5410c93a0c22698227d67e039620abfe542ca8f 100644 --- a/lib/models/ConfigValue.php +++ b/lib/models/ConfigValue.php @@ -43,7 +43,7 @@ class ConfigValue extends SimpleORMap ]; $config['registered_callbacks']['after_delete'][] = function (ConfigValue $value) { - if ($value->entry->type === 'i18n') { + if (isset($value->entry) && $value->entry->type === 'i18n') { $value->getTypedValue()->removeTranslations(); } }; diff --git a/lib/models/Course.class.php b/lib/models/Course.class.php index 86e9a471c53bc796be826dfc3c1036a28e25e351..e16df06e5f33b9191a650b363348b823ba541451 100644 --- a/lib/models/Course.class.php +++ b/lib/models/Course.class.php @@ -224,6 +224,9 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe $config['default_values']['schreibzugriff'] = 1; $config['default_values']['duration_time'] = 0; + $config['additional_fields']['teachers'] = [ + 'get' => 'getTeachers' + ]; $config['additional_fields']['end_time'] = true; $config['additional_fields']['start_semester'] = [ @@ -456,6 +459,13 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe } } + public function getTeachers() + { + return $this->members->filter(function ($m) { + return $m['status'] === 'dozent'; + }); + } + public function getFreeSeats() { $free_seats = $this->admission_turnout - $this->getNumParticipants(); diff --git a/resources/assets/javascripts/bootstrap/admin-courses.js b/resources/assets/javascripts/bootstrap/admin-courses.js index 3fa05110909e498b3786b6e53f72b7d61bf5bb89..06c4621ad636033d30f18a99668421a9e93d52ed 100644 --- a/resources/assets/javascripts/bootstrap/admin-courses.js +++ b/resources/assets/javascripts/bootstrap/admin-courses.js @@ -1,12 +1,25 @@ -STUDIP.Dialog.registerHeaderHandler('X-Dialog-Notice', json => { - json = JSON.parse(json); +STUDIP.domReady(() => { + const node = document.querySelector('.admin-courses-vue-app'); + if (!node) { + return; + } - $(`#course-${json.id} td.actions .button`) - .removeClass('has-notice has-no-notice') - .addClass(json.notice.length > 0 ? 'has-notice' : 'has-no-notice') - .attr('title', json.notice); + Promise.all([ + STUDIP.Vue.load(), + import('../../../vue/store/AdminCoursesStore.js').then((config) => config.default), + import('../../../vue/components/AdminCourses.vue').then((component) => component.default), + ]).then(([{ createApp, store }, storeConfig, AdminCourses]) => { + store.registerModule('admincourses', storeConfig); - STUDIP.Dialog.close(); + Object.entries(window.AdminCoursesStoreData ?? {}).forEach(([key, value]) => { + store.commit(`admincourses/${key}`, value); + }) - return false; + const vm = createApp({ + components: { AdminCourses }, + }); + vm.$mount(node); + + STUDIP.AdminCourses.App = vm.$refs.app; + }); }); diff --git a/resources/assets/javascripts/bootstrap/application.js b/resources/assets/javascripts/bootstrap/application.js index d673455f73319768be1cede0b839d14c2fbc80e6..c6ffe3522808be2c8541798cc8dc30a78383d568 100644 --- a/resources/assets/javascripts/bootstrap/application.js +++ b/resources/assets/javascripts/bootstrap/application.js @@ -331,24 +331,6 @@ STUDIP.domReady(function () { } }); -jQuery(document).on('click', '.course-admin td .course-completion', function () { - var href = $(this).attr('href'), - timeout = window.setTimeout(function () { - $(this).addClass('ajaxing'); - }.bind(this), 300); - - $.getJSON(href).done(function (response) { - clearTimeout(timeout); - - $(this).removeClass('ajaxing').attr({ - 'data-course-completion': response.state, - title: response.label - }); - }.bind(this)); - - return false; -}); - // Global handler: // Toggle a table element. The url of the link will be called, an ajax // indicator will be shown instead of the element and the whole table row diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js index 161b6d608af43e322f5d2644da9b91f54bb37e8c..3d9be08dc9083d848787b5c9e70ac03a60cdadb5 100644 --- a/resources/assets/javascripts/bootstrap/forms.js +++ b/resources/assets/javascripts/bootstrap/forms.js @@ -108,6 +108,7 @@ $(document) // select was opened by click if (!is_default && shouldSubmit) { $(this.form).submit(); + $('option', this).prop('defaultSelected', false).filter(':selected').prop('defaultSelected', true); return false; } }); diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js index a7d6f05f96bc590df3470a8aba7cc3d2a59d39dd..55276026788cc9d5629de4eca1ec8611f85b03bd 100644 --- a/resources/assets/javascripts/init.js +++ b/resources/assets/javascripts/init.js @@ -4,6 +4,7 @@ import Vue from './lib/studip-vue.js'; import ActionMenu from './lib/actionmenu.js'; import ActivityFeed from './lib/activityfeed.js'; import admin_sem_class from './lib/admin_sem_class.js'; +import AdminCourses from './lib/admin-courses.js'; import Admission from './lib/admission.js'; import Arbeitsgruppen from './lib/arbeitsgruppen.js'; import Archive from './lib/archive.js'; @@ -66,6 +67,7 @@ import Resources from './lib/resources.js'; import Responsive from './lib/responsive.js'; import RESTAPI, { api } from './lib/restapi.js'; import Schedule from './lib/schedule.js'; +import Screenreader from './lib/screenreader.js'; import Scroll from './lib/scroll.js'; import Search from './lib/search.js'; import Sidebar from './lib/sidebar.js'; @@ -90,6 +92,7 @@ window.STUDIP = _.assign(window.STUDIP || {}, { ActionMenu, ActivityFeed, admin_sem_class, + AdminCourses, Admission, api, Arbeitsgruppen, @@ -154,6 +157,7 @@ window.STUDIP = _.assign(window.STUDIP || {}, { RESTAPI, Schedule, Scroll, + Screenreader, Search, Sidebar, SkipLinks, diff --git a/resources/assets/javascripts/lib/admin-courses.js b/resources/assets/javascripts/lib/admin-courses.js new file mode 100644 index 0000000000000000000000000000000000000000..e6a3523db54835a9fbf559c2c80126b4049cc52a --- /dev/null +++ b/resources/assets/javascripts/lib/admin-courses.js @@ -0,0 +1,20 @@ +const AdminCourses = { + App: null, + changeFiltersDependendOnInstitute(institut_id) { + STUDIP.AdminCourses.App.changeFilter({ institut_id }); + //change Studiengangteil filter + $.get( + STUDIP.URLHelper.getURL('dispatch.php/admin/courses/get_stdgangteil_selector/' + institut_id) + ).done((widget) => { + $('select[name=stgteil_select]').closest('.sidebar-widget').replaceWith(widget); + }); + + //change Dozenten-Filter + $.get( + STUDIP.URLHelper.getURL('dispatch.php/admin/courses/get_teacher_selector/' + institut_id) + ).done((widget) => { + $('select[name=teacher_filter]').closest('.sidebar-widget').replaceWith(widget); + }); + } +}; +export default AdminCourses; diff --git a/resources/assets/javascripts/lib/screenreader.js b/resources/assets/javascripts/lib/screenreader.js new file mode 100644 index 0000000000000000000000000000000000000000..b8b9d098d468167a880a5e99d249f17075535e1b --- /dev/null +++ b/resources/assets/javascripts/lib/screenreader.js @@ -0,0 +1,8 @@ +class Screenreader +{ + static notify(text) { + $('#notes_for_screenreader').text(text); + } +} + +export default Screenreader; diff --git a/resources/assets/stylesheets/less/tables.less b/resources/assets/stylesheets/less/tables.less index c7b1f95c2627e4571919725d580eaf5214444ecc..eb576dd075bc97c3938163b32f1791c1569513dc 100644 --- a/resources/assets/stylesheets/less/tables.less +++ b/resources/assets/stylesheets/less/tables.less @@ -438,6 +438,23 @@ table.default { } } } + > tbody > tr.selected > td { + background-color: var(--yellow-20); + &:first-child { + position: relative; + &::before { + display: block; + content: ''; + position: absolute; + + top: 0; + bottom: 0; + left: 0; + width: 2px; + background-color: var(--light-gray-color); + } + } + } > tbody > tr.new > td { font-weight: bold; &:first-child { @@ -465,6 +482,9 @@ table.default { &:not(.nohover) > tbody:not(.nohover) > tr:not(.nohover):hover > td:not(.nohover) { background-color: fadeout(@light-gray-color, 80%); } + &:not(.nohover) > tbody:not(.nohover) > tr.selected:not(.nohover):hover > td:not(.nohover) { + background-color: var(--yellow-40); + } > tfoot { > tr > td { background-color: @content-color-20; diff --git a/resources/assets/stylesheets/scss/admin.scss b/resources/assets/stylesheets/scss/admin.scss index a2e5843a06a37ea23f604c9e736f70e9ef5ac4bc..d8ac4d30d6c47cb61d4473b43186a291cda179fb 100644 --- a/resources/assets/stylesheets/scss/admin.scss +++ b/resources/assets/stylesheets/scss/admin.scss @@ -151,4 +151,7 @@ fieldset.attribute_table { background-image: url("#{$image-path}/loading-indicator.svg"); } } + > tbody.loading > tr > td { + opacity: 0.5; + } } diff --git a/resources/vue/components/AdminCourses.vue b/resources/vue/components/AdminCourses.vue new file mode 100644 index 0000000000000000000000000000000000000000..8e8c5c84ea76a0bac0f79dd473227e9efe840b9f --- /dev/null +++ b/resources/vue/components/AdminCourses.vue @@ -0,0 +1,265 @@ +<template> + <table class="default"> + <caption> + {{ $gettext('Veranstaltungen') }} + <span class="actions" v-if="isLoading"> + <img :src="loadingIndicator" width="20" height="20" :title="$gettext('Daten werden geladen')"> + </span> + <span class="actions" v-else-if="coursesCount > 0"> + {{ coursesCount + ' ' + $gettext('Veranstaltungen') }} + </span> + </caption> + <thead> + <tr class="sortable"> + <th v-if="showComplete"> + <a + @click.prevent="changeSort('completion')" + class="course-completion" + :title="$gettext('Bearbeitungsstatus')" + > + {{ $gettext('Bearbeitungsstatus') }} + </a> + <studip-icon :shape="sort.direction === 'ASC' ? 'arr_1down' : 'arr_1up'" + v-if="sort.by === 'completion'" + class="text-bottom"></studip-icon> + </th> + <th v-for="activeField in sortedActivatedFields" :key="`field-${activeField}`"> + <a href="#" + @click.prevent="changeSort(activeField)" + :title="sort.by === activeField && sort.direction === 'ASC' ? $gettextInterpolate('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}) : (sort.by === activeField && sort.direction === 'DESC' ? $gettextInterpolate('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}) : $gettextInterpolate('Sortieren nach %{ field }', { field: fields[activeField]}))" + v-if="!unsortableFields.includes(activeField)" + > + {{ fields[activeField] }} + <studip-icon :shape="sort.direction === 'ASC' ? 'arr_1down' : 'arr_1up'" + v-if="sort.by === activeField" + class="text-bottom"></studip-icon> + </a> + <template v-else> + {{ fields[activeField] }} + </template> + </th> + <th class="actions" style="text-align: center;"> + {{ $gettext('Aktion') }} + </th> + </tr> + <tr v-if="buttons.top"> + <th v-html="buttons.top" style="text-align: right" :colspan="colspan"></th> + </tr> + </thead> + <tbody :class="{ loading: isLoading }"> + <tr v-for="course in sortedCourses" + :key="course.id" + :class="course.id === currentLine ? 'selected' : ''" + @click="currentLine = course.id"> + <td v-if="showComplete"> + <button :href="getURL('dispatch.php/admin/courses/toggle_complete/' + course.id)" + class="course-completion undecorated" + :data-course-completion="course.completion" + :title="(course.completion > 0 ? (course.completion == 1 ? $gettext('Veranstaltung in Bearbeitung.') : $gettext('Veranstaltung komplett.')) : $gettext('Veranstaltung neu.')) + ' ' + $gettext('Klicken zum Ändern des Status.')" + @click.prevent="toggleCompletionState(course.id)"> + {{ $gettext('Bearbeitungsstatus ändern') }} + </button> + </td> + <td v-for="active_field in sortedActivatedFields" :key="active_field"> + <div v-html="course[active_field]"></div> + <a v-if="active_field === 'name' && getChildren(course).length > 0" + @click.prevent="toggleOpenChildren(course.id)" + href=""> + <studip-icon :shape="open_children.indexOf(course.id) === -1 ? 'add' : 'remove'" class="text-bottom"></studip-icon> + {{ $gettextInterpolate( + $gettext('%{ n } Unterveranstaltungen'), + { n: getChildren(course).length } + ) }} + </a> + </td> + <td class="actions" v-html="course.action"> + </td> + </tr> + <tr v-if="sortedCourses.length === 0 && coursesCount <= 500 && displayLimitedLines && coursesLoaded"> + <td :colspan="colspan"> + {{ $gettext('Keine Ergebnisse') }} + </td> + </tr> + <tr v-if="coursesCount > maxCourses && displayLimitedLines"> + <td :colspan="colspan"> + {{ + $gettextInterpolate( + $gettext(`%{ n } Veranstaltungen entsprechen Ihrem Filter. Schränken Sie nach Möglichkeit die Filter weiter ein.`), + { n: coursesCount } + ) + }} + <a href="" @click.prevent="loadCourses({withoutLimit: true}); displayLimitedLines = false;"> + {{ $gettext('Alle anzeigen') }} + </a> + </td> + </tr> + <tr v-if="!coursesLoaded"> + <td :colspan="colspan"> + {{ $gettext('Daten werden geladen ...') }} + </td> + </tr> + </tbody> + <tfoot v-if="buttons.bottom"> + <tr> + <td v-html="buttons.bottom" style="text-align: right" :colspan="colspan"></td> + </tr> + </tfoot> + </table> + +</template> +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; + +export default { + name: 'AdminCourses', + props: { + maxCourses: Number, + showComplete: { + type: Boolean, + default: false, + }, + fields: Object, + unsortableFields: Array, + sortBy: String, + sortFlag: String, + }, + data() { + return { + sort: { + by: this.sortBy, + direction: this.sortFlag, + }, + currentLine: null, + displayLimitedLines: true, + open_children: [], + }; + }, + created() { + this.loadCourses(); + }, + computed: { + ...mapState('admincourses', [ + 'activatedFields', + 'buttons', + 'courses', + 'coursesCount', + 'coursesLoaded', + 'filters', + ]), + ...mapGetters('admincourses', [ + 'isLoading', + ]), + colspan () { + let colspan = this.activatedFields.length + 1; + if (this.showComplete) { + colspan += 1; + } + return colspan; + }, + sortedCourses() { + let maincourses = this.courses.filter(c => !c.parent_course); + maincourses = this.sortArray(maincourses); + + let sorted_courses = []; + let children = []; + for (let i in maincourses) { + sorted_courses.push(maincourses[i]); + if (this.open_children.indexOf(maincourses[i].id) !== -1) { + children = this.getChildren(maincourses[i]); + children = this.sortArray(children); + for (let k in children) { + sorted_courses.push(children[k]); + } + } + } + return sorted_courses; + }, + sortedActivatedFields() { + return Object.keys(this.fields).filter(f => this.activatedFields.includes(f)); + }, + loadingIndicator() { + return STUDIP.ASSETS_URL + 'images/loading-indicator.svg'; + } + }, + methods: { + ...mapActions('admincourses', [ + 'changeActionArea', + 'changeFilter', + 'loadCourses', + 'loadCourse', + 'toggleActiveField', + 'toggleCompletionState', + ]), + getChildren(course) { + return this.courses.filter(c => c.parent_course === course.id); + }, + toggleOpenChildren(course_id) { + if (!this.open_children.includes(course_id)) { + this.open_children.push(course_id); + } else { + this.open_children = this.open_children.filter(cid => cid !== course_id); + } + }, + changeSort(column) { + if (this.sort.by === column) { + this.sort.direction = this.sort.direction === 'ASC' ? 'DESC' : 'ASC'; + } else { + this.currentLine = null; + this.sort.direction = 'ASC'; + } + this.sort.by = column; + + $.post(STUDIP.URLHelper.getURL('dispatch.php/admin/courses/sort'), { + sortby: column, + sortflag: this.sort.direction, + }); + }, + sortArray (array) { + if (!array.length) { + return []; + } + let sortby = this.sort.by; + if (!this.activatedFields.includes(sortby) && sortby !== 'completion') { + return array; + } + + const striptags = function (text) { + if (typeof text === "string") { + return text.replace(/(<([^>]+)>)/gi, ""); + } else { + return text; + } + }; + + // Define sort direction by this factor + const directionFactor = this.sort.direction === 'ASC' ? 1 : -1; + + // Default sort function by string comparison of field + const collator = new Intl.Collator(String.locale, {numeric: true, sensitivity: 'base'}); + let sortFunction = function (a, b) { + return collator.compare(striptags(a[sortby]), striptags(b[sortby])); + }; + + let is_numeric = true; + for (let i in array) { + if (striptags(array[i][sortby]) && isNaN(striptags(array[i][sortby]))) { + is_numeric = false; + break; + } + } + if (is_numeric) { + sortFunction = function (a, b) { + return (striptags(a[sortby]) ? parseInt(striptags(a[sortby]), 10) : 0) + - (striptags(b[sortby]) ? parseInt(striptags(b[sortby]), 10) : 0); + }; + } + + // Actual sort on copy of array + return array.concat().sort((a, b) => directionFactor * sortFunction(a, b)); + }, + getURL(url, params = {}) { + return STUDIP.URLHelper.getURL(url, params); + }, + } +}; +</script> diff --git a/resources/vue/store/AdminCoursesStore.js b/resources/vue/store/AdminCoursesStore.js new file mode 100644 index 0000000000000000000000000000000000000000..e3df44ece1914c5dc680c460e90b576bad21757c --- /dev/null +++ b/resources/vue/store/AdminCoursesStore.js @@ -0,0 +1,170 @@ +import Screenreader from '../../assets/javascripts/lib/screenreader.js'; +import { $gettext } from '../../assets/javascripts/lib/gettext.js'; + +export default { + namespaced: true, + + state: () => ({ + actionArea: 1, + activatedFields: [], + buttons: { + top: '', + bottom: '', + }, + courses: [], + coursesCount: null, + coursesLoaded: false, + filters: {}, + loadingXhr: false, + }), + + getters: { + getCourseById: (state) => (courseId) => { + return state.courses.find((course) => course.id === courseId); + }, + isLoading(state) { + return state.loadingXhr !== null; + } + }, + + mutations: { + setActionArea(state, area) { + state.actionArea = area; + }, + setActivatedFields(state, fields) { + state.activatedFields = fields; + }, + setButtons(state, { top, bottom }) { + state.buttons.top = top ?? state.buttons.top; + state.buttons.bottom = bottom ?? state.buttons.bottom; + }, + setCourse(state, payload) { + state.courses = state.courses.filter(c => c.id !== payload.courseId); + if (payload.data) { + state.courses.push(payload.data); + } + }, + setCourses(state, courses, count = null) { + state.courses = courses; + state.coursesCount = count ?? courses.length; + }, + setCoursesLoaded(state, loaded = true) { + state.coursesLoaded = loaded; + }, + setFilter(state, filters) { + state.filters = { + ...state.filters, + ...filters, + }; + }, + }, + + actions: { + loadCourse({ commit, state }, courseId) { + $.getJSON(STUDIP.URLHelper.getURL('dispatch.php/admin/courses/search'), { + ...state.filters, + course_id: courseId, + action: state.actionArea, + }).done((response) => { + commit('setCourse', { + courseId, + data: response.data[0] ?? null + }); + }); + + }, + loadCourses({ commit, state }, {withoutLimit = false, withoutScreenreaderNotice = false} = {}) { + if (state.loadingXhr) { + state.loadingXhr.abort(); + } + + Screenreader.notify(''); + + let params = { + ...state.filters, + action: state.actionArea, + activated_fields: state.activatedFields, + without_limit: withoutLimit ? 1 : null, + }; + + // Remove empty items from params + params = Object.keys(params) + .filter((k) => params[k]) + .reduce((a, k) => ({ ...a, [k]: params[k] }), {}); + + let timeout = null; + if (!withoutScreenreaderNotice && !state.coursesLoaded) { + timeout = setTimeout(() => { + STUDIP.Screenreader.notify($gettext('Suche läuft.')); + }, 800); + } + + const xhr = $.ajax({ + type: 'GET', + url: STUDIP.URLHelper.getURL('dispatch.php/admin/courses/search'), + dataType: 'json', + data: params, + }); + xhr.done((response) => { + commit('setCoursesLoaded'); + + if (response.data === undefined) { + commit('setCourses', [], response.count); + } else { + commit('setCourses', response.data); + } + + commit('setButtons', { + top: response.buttons_top ?? null, + bottom: response.buttons_bottom ?? null, + }); + }).always(() => { + clearTimeout(timeout); + state.loadingXhr = null; + }); + + state.loadingXhr = xhr; + }, + changeActionArea({ commit, state, dispatch }, area) { + if (state.actionArea !== area) { + commit('setActionArea', area); + dispatch('loadCourses'); + } + }, + changeFilter({ commit, state, dispatch }, filters) { + const changed = Object.entries(filters).some(([key, value]) => { + return state.filters[key] === undefined || state.filters[key] !== value; + }); + if (changed) { + commit('setFilter', filters); + dispatch('loadCourses'); + } + }, + toggleActiveField({ commit, state, dispatch }, field) { + let fields = state.activatedFields; + if (fields.includes(field)) { + fields = fields.filter(f => f !== field); + } else { + fields.push(field); + } + + commit('setActivatedFields', fields); + dispatch('loadCourses'); + }, + toggleCompletionState({ commit, getters }, courseId) { + $.get( + STUDIP.URLHelper.getURL('dispatch.php/admin/courses/toggle_complete/' + courseId) + ).done((response) => { + const course = getters.getCourseById(courseId); + commit('setCourse', { + courseId, + data: { + ...course, + completion: response.state, + } + }); + }); + + } + } +} diff --git a/templates/layouts/base.php b/templates/layouts/base.php index 2869270fd51ef44883151a09a779ee8ed9abe4c0..67bb2b92f525947995ee2227ad3f992cc3e5f0b3 100644 --- a/templates/layouts/base.php +++ b/templates/layouts/base.php @@ -102,6 +102,7 @@ $lang_attr = str_replace('_', '-', $_SESSION['_language']); <?= $this->render_partial('footer', ['link_params' => $header_template->link_params]); ?> <?= SkipLinks::getHTML() ?> + <section class="sr-only" id="notes_for_screenreader" aria-live="polite"></section> </body> </html> <?php NotificationCenter::postNotification('PageDidRender', PageLayout::getBodyElementId()); diff --git a/templates/sidebar/search-widget.php b/templates/sidebar/search-widget.php index 13d6bed0ac6206243c4c8de99f93bdcfbc0384c5..6d870ba246a0e4c0d97bfe2f77df2e88367f8fd9 100644 --- a/templates/sidebar/search-widget.php +++ b/templates/sidebar/search-widget.php @@ -1,4 +1,8 @@ -<form action="<?= URLHelper::getLink($url) ?>" method="<?= $method ?>" <? if (isset($id)) printf('id="%s"', htmlReady($id)); ?> class="sidebar-search"> +<form action="<?= URLHelper::getLink($url) ?>" + method="<?= $method ?>" + <? if (isset($id)) printf('id="%s"', htmlReady($id)); ?> + <?= $onsubmit ? 'onsubmit="'.htmlReady($onsubmit).'"' : '' ?> + class="sidebar-search"> <? foreach ($url_params as $key => $value): ?> <?=addHiddenFields($key,$value)?> <? endforeach; ?> diff --git a/templates/sidebar/select-widget.php b/templates/sidebar/select-widget.php index 7c7b039f60ec29e2041cf8cb9ed1eb17f14ac707..b7aa596af208cb3a96699385df0c5132ba0eb336 100644 --- a/templates/sidebar/select-widget.php +++ b/templates/sidebar/select-widget.php @@ -1,9 +1,15 @@ -<form action="<?= URLHelper::getLink($url) ?>" method="<?= $method ?>"> +<form action="<?= URLHelper::getLink($url) ?>" + <?= $onsubmit ? 'onsubmit="'.htmlReady($onsubmit).'"' : '' ?> + method="<?= $method ?>"> <?= \SelectWidget::arrayToHiddenInput($params) ?> <?= (strtolower($method) == 'post') ? CSRFProtection::tokenTag() : ''; ?> - <select class="sidebar-selectlist <?= $class ?> <? if ($__is_nested): ?>nested-select<? endif; ?>" <? !empty($size) ? printf('size="%u"', $size) : '' ?> <?= !empty($attributes) ? arrayToHtmlAttributes($attributes) : '' ?> - name="<?= sprintf('%s%s', htmlReady($name), $multiple ? '[]' : '') ?>" <? if ($multiple) echo 'multiple'; ?> - aria-label="<?= htmlReady($title) ?>" <?= $dropdownAutoWidth ? 'data-dropdown-auto-width="1"' : '' ?>> + <select class="sidebar-selectlist <?= $class ?> <? if ($__is_nested): ?>nested-select<? endif; ?>" + <? !empty($size) ? printf('size="%u"', $size) : '' ?> + <?= !empty($attributes) ? arrayToHtmlAttributes($attributes) : '' ?> + name="<?= sprintf('%s%s', htmlReady($name), $multiple ? '[]' : '') ?>" + <? if ($multiple) echo 'multiple'; ?> + aria-label="<?= htmlReady($title) ?>" + <?= $dropdownAutoWidth ? 'data-dropdown-auto-width="1"' : '' ?>> <? foreach ($elements as $element): ?> <? if ($element instanceof SelectGroupElement && count($element->getElements()) > 0): ?> diff --git a/templates/sidebar/selector-widget.php b/templates/sidebar/selector-widget.php index 62f384cb9f0eeae4b72de68eb04089e45a6c7c89..f96e6b3227520d544986cf9072860875b664de24 100644 --- a/templates/sidebar/selector-widget.php +++ b/templates/sidebar/selector-widget.php @@ -1,4 +1,6 @@ -<form action="<?= URLHelper::getLink($url, [], true) ?>" method="<?= $method?>" class="selector-widget"> +<form action="<?= URLHelper::getLink($url, [], true) ?>" + method="<?= $method?>" + class="selector-widget"> <?= ($method == 'post' ? CSRFProtection::tokenTag() : '') ?> <select name="<?=htmlReady($name)?>" class="sidebar-selectlist submit-upon-select text-top" size="<?= (int) $size ?: 8 ?>" aria-label="<?= _("Wählen Sie ein Objekt aus. Sie gelangen dann zur neuen Seite.") ?>"> <? foreach ($elements as $element): ?>