From a81c3ecd4ed4f67e25a166661bf7c1efa61202fd Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms Date: Thu, 16 Oct 2025 16:20:32 +0200 Subject: add option to always display sem number on my courses, fixes #5957 Closes #5957 Merge request studip/studip!4555 --- lib/classes/MyCoursesHelper.php | 267 +++++++++++++++++++++++++++ resources/vue/components/MyCoursesTables.vue | 10 +- resources/vue/components/MyCoursesTiles.vue | 6 +- resources/vue/mixins/MyCoursesMixin.js | 12 +- 4 files changed, 284 insertions(+), 11 deletions(-) create mode 100644 lib/classes/MyCoursesHelper.php diff --git a/lib/classes/MyCoursesHelper.php b/lib/classes/MyCoursesHelper.php new file mode 100644 index 0000000..714a6cb --- /dev/null +++ b/lib/classes/MyCoursesHelper.php @@ -0,0 +1,267 @@ +getVueAppData( + $this->getCourses($sem_key, $group_field), + $group_field + ); + } + + public function getCourses(string $sem_key, string $group_field = 'sem_number'): array + { + return MyRealmModel::getPreparedCourses($sem_key, [ + 'group_field' => $group_field, + 'order_by' => null, + 'order' => 'asc', + 'studygroups_enabled' => Config::get()->MY_COURSES_ENABLE_STUDYGROUPS, + 'deputies_enabled' => Config::get()->DEPUTIES_ENABLE, + ]); + } + + /** + * Get the data array for presenting the course list in Vue. + * + * @param array|null $sem_courses + * @param string $group_field + * @return array{ + * courses: array, + * groups: array, + * user_id: string, + * config: array{ + * allow_dozent_visibility: bool, + * open_groups: array, + * sem_number: bool, + * display_type: string, + * responsive_type: string, + * navigation_show_only_new: bool, + * group_by: string + * } + * } + */ + public function getVueAppData(?array $sem_courses, string $group_field = 'sem_number'): array + { + $sem_data = Semester::getAllAsArray(); + $temp_courses = []; + $groups = []; + + if (is_array($sem_courses)) { + foreach ($sem_courses as $_outer_index => $_outer) { + if ($group_field === 'sem_number') { + $_courses = []; + + foreach ($_outer as $course) { + $_courses[$course['seminar_id']] = $course; + if (!empty($course['children']) && is_array($course['children'])) { + foreach ($course['children'] as $child) { + $_courses[$child['seminar_id']] = $child; + } + } + } + + if ($_outer_index) { + $groups[] = [ + 'id' => $_outer_index, + 'name' => (string)$sem_data[$_outer_index]['name'], + 'data' => [ + [ + 'id' => md5($_outer_index), + 'label' => false, + 'ids' => array_keys($_courses), + ], + ], + ]; + } + $temp_courses = array_merge($temp_courses, $_courses); + } else { + $count = 1; + $_groups = []; + foreach ($_outer as $_inner_index => $_inner) { + $_courses = []; + + foreach ($_inner as $course) { + $_courses[$course['seminar_id']] = $course; + if (isset($course['children']) && is_array($course['children'])) { + foreach ($course['children'] as $child) { + $_courses[$child['seminar_id']] = $child; + } + } + } + + $label = $_inner_index; + if ($group_field === 'sem_tree_id' && !$label) { + $label = _('keine Zuordnung'); + } elseif ($group_field === 'gruppe') { + $label = _('Gruppe') . ' ' . $count++; + } + + $_groups[] = [ + 'id' => md5($_outer_index . $_inner_index), + 'label' => $label, + 'ids' => array_keys($_courses), + ]; + + $temp_courses = array_merge($temp_courses, $_courses); + } + + if ($_outer_index) { + $groups[] = [ + 'id' => $_outer_index, + 'name' => (string)$sem_data[$_outer_index]['name'], + 'data' => $_groups, + ]; + } + } + } + } + + return [ + 'setCourses' => $this->sanitizeNavigations(array_map([$this, 'convertCourse'], $temp_courses)), + 'setGroups' => $groups, + 'setUserId' => User::findCurrent()->id, + 'setConfig' => [ + 'allow_dozent_visibility' => Config::get()->getValue('ALLOW_DOZENT_VISIBILITY'), + 'open_groups' => array_values(User::findCurrent()->getConfiguration()->getValue('MY_COURSES_OPEN_GROUPS')), + 'sem_number' => Config::get()->getValue('IMPORTANT_SEMNUMBER'), + 'sem_number_always' => Config::get()->getValue('MY_COURSES_ALWAYS_SHOW_SEMNUM'), + 'view_settings' => User::findCurrent()->getConfiguration()->getValue('MY_COURSES_VIEW_SETTINGS'), + 'group_by' => $group_field, + ], + ]; + } + + private function sanitizeNavigations(array $courses): array + { + // Count occurences of slots + $counters = []; + foreach ($courses as $course) { + foreach ($course['navigation'] as $key => $value) { + if (!isset($counters[$key])) { + $counters[$key] = 0; + } + if ($value) { + $counters[$key] += 1; + } + } + } + + // Detect which slots are not set at all + $remove = array_keys(array_filter($counters, function ($counter) { + return !$counter; + })); + + // Set positions by predefined positions without the always empty slots + $positions = array_diff(array_keys(MyRealmModel::getDefaultModules()), $remove); + + // Get other positions based on count + arsort($counters); + foreach ($counters as $key => $count) { + if ($count && !in_array($key, $positions)) { + $positions[] = $key; + } + } + + // Sort and filter course navigations + return array_map( + function ($course) use ($positions) { + $course['navigation'] = array_filter($course['navigation'], function ($key) use ($positions) { + return in_array($key, $positions); + }, ARRAY_FILTER_USE_KEY); + uksort($course['navigation'], function ($a, $b) use ($positions) { + return array_search($a, $positions) - array_search($b, $positions); + }); + $course['navigation'] = array_values($course['navigation']); + return $course; + }, + $courses + ); + } + + private function convertCourse($course) + { + $is_teacher = in_array($course['user_status'], ['tutor', 'dozent']); + + $avatar = $course['sem_class']['studygroup_mode'] + ? StudygroupAvatar::getAvatar($course['seminar_id']) + : CourseAvatar::getAvatar($course['seminar_id']); + + $extra_navigation = false; + if ($is_teacher) { + $adminmodule = $course['sem_class']->getAdminModuleObject(); + if ($adminmodule) { + $adminnavigation = $adminmodule->getIconNavigation($course['seminar_id'], 0, $GLOBALS['user']->id); + $extra_navigation = [ + 'url' => URLHelper::getURL($adminnavigation->getURL(), ['cid' => $course['seminar_id']]), + 'icon' => $adminnavigation->getImage()->getShape(), + 'label' => $adminnavigation->getLinkAttributes()['title'] ?? _('Verwaltung'), + ]; + } + } + + return [ + 'id' => (string) $course['seminar_id'], + 'name' => (string) $course['name'], + 'number' => (string) $course['veranstaltungsnummer'], + 'group' => (int) $course['gruppe'], + 'admission_binding' => (bool) $course['admission_binding'], + 'children' => array_column($course['children'] ?? [], 'seminar_id'), + 'parent' => $course['parent_course'] ?? null, + + 'is_teacher' => in_array($course['user_status'], ['tutor', 'dozent']), + 'is_studygroup' => (bool) $course['sem_class']['studygroup_mode'], + 'is_hidden' => !$course['visible'], + 'is_deputy' => (bool) $course['is_deputy'], + 'is_group' => (bool) $course['is_group'], + + 'avatar' => $avatar->getURL(Avatar::MEDIUM), + + 'navigation' => $this->reduceNavigation($course['navigation'] ?? null), + 'extra_navigation' => $extra_navigation, + ]; + } + + private function reduceNavigation($nav): array + { + if (!$nav) { + return []; + } + + $result = []; + foreach (MyRealmModel::array_rtrim($nav) as $key => $n) { + if (!$n || !$n->isVisible(true)) { + $item = false; + } else { + $attr = $n->getLinkAttributes(); + if (empty($attr['title']) && $n->getImage()) { + $attr['title'] = (string) ($n->getImage()->getAttributes()['title'] ?? ''); + } + if (empty($attr['title'])) { + $attr['title'] = (string) $n->getTitle(); + } + $attr['title'] = (string) $attr['title']; + + $item = [ + 'url' => $n->getURL(), + 'icon' => $this->convertIcon($n->getImage()), + 'attr' => $attr, + 'important' => $n->getImage()->signalsAttention(), + ]; + } + $result[$key] = $item; + } + + return $result; + } + + /** + * @return array{role: string, shape: string} + */ + private function convertIcon(Icon $icon): array + { + return [ + 'role' => $icon->getRole(), + 'shape' => $icon->getShape(), + ]; + } +} diff --git a/resources/vue/components/MyCoursesTables.vue b/resources/vue/components/MyCoursesTables.vue index 5e0feec..23e1aba 100644 --- a/resources/vue/components/MyCoursesTables.vue +++ b/resources/vue/components/MyCoursesTables.vue @@ -5,7 +5,7 @@ - + @@ -18,7 +18,7 @@ - + {{ $gettext('Nr.') }} @@ -35,7 +35,7 @@ - + {{ subgroup.label }} @@ -52,12 +52,12 @@ - + {{ course.number }} - {{ getCourseName(course, getConfig('sem_number') && responsiveDisplay) }} + {{ getCourseName(course) }} {{ $gettext('[Vertretung]') }} diff --git a/resources/vue/components/MyCoursesTiles.vue b/resources/vue/components/MyCoursesTiles.vue index ad09ef5..2851187 100644 --- a/resources/vue/components/MyCoursesTiles.vue +++ b/resources/vue/components/MyCoursesTiles.vue @@ -11,7 +11,7 @@