diff options
| author | Jan-Hendrik Willms <tleilax+studip@gmail.com> | 2025-10-16 16:20:32 +0200 |
|---|---|---|
| committer | Jan-Hendrik Willms <tleilax+studip@gmail.com> | 2025-10-16 16:32:36 +0200 |
| commit | a81c3ecd4ed4f67e25a166661bf7c1efa61202fd (patch) | |
| tree | cd0b6f8b184c2dd6968b114448c34ec95d62b2b7 | |
| parent | 9a815c1f667caa96f98479c17736b43a08b716fc (diff) | |
add option to always display sem number on my courses, fixes #5957
Closes #5957
Merge request studip/studip!4555
| -rw-r--r-- | lib/classes/MyCoursesHelper.php | 267 | ||||
| -rw-r--r-- | resources/vue/components/MyCoursesTables.vue | 10 | ||||
| -rw-r--r-- | resources/vue/components/MyCoursesTiles.vue | 6 | ||||
| -rw-r--r-- | resources/vue/mixins/MyCoursesMixin.js | 12 |
4 files changed, 284 insertions, 11 deletions
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 @@ +<?php +final class MyCoursesHelper +{ + public function createVueAppData(string $sem_key, string $group_field = 'sem_number'): array + { + return $this->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 @@ <colgroup> <col style="width: 7px"> <col style="width: 25px"> - <col style="width: 70px" v-if="getConfig('sem_number') && !responsiveDisplay"> + <col style="width: 70px" v-if="displaySemNumber"> <col> <col v-if="!responsiveDisplay" :style="{width: (2 * 5 + numberOfNavElements * (iconSize + 2 * 3 + 3) - 3) + 'px'}"> <col v-if="!responsiveDisplay" style="width: 24px"> @@ -18,7 +18,7 @@ </span> </th> <th></th> - <th v-if="getConfig('sem_number') && !responsiveDisplay" :class="getOrderClasses('number')"> + <th v-if="displaySemNumber" :class="getOrderClasses('number')"> <a href="#" @click.prevent="changeOrder('number')"> {{ $gettext('Nr.') }} </a> @@ -35,7 +35,7 @@ <tbody v-for="subgroup in group.data" :key="subgroup.id" :class="{collapsed: !isGroupOpen(subgroup)}"> <tr class="header-row" v-if="subgroup.label !== false"> <th style="white-space: nowrap; text-align: left"></th> - <th class="toggle-indicator" :colspan="(getConfig('sem_number') && !responsiveDisplay) ? 3 : 2"> + <th class="toggle-indicator" :colspan="displaySemNumber ? 3 : 2"> <a href="#" @click.prevent.stop="toggleOpenGroup(subgroup)">{{ subgroup.label }}</a> </th> <th v-if="!responsiveDisplay" class="dont-hide" colspan="2"></th> @@ -52,12 +52,12 @@ <td :class="{'subcourse-indented': isChild(course)}"> <span :style="{backgroundImage: `url(${course.avatar}`}" class="my-courses-avatar course-avatar-small" :title="course.name" alt=""></span> </td> - <td v-if="getConfig('sem_number') && !responsiveDisplay" :class="{'subcourse-indented': isChild(course)}"> + <td v-if="displaySemNumber" :class="{'subcourse-indented': isChild(course)}"> {{ course.number }} </td> <td :class="{'subcourse-indented': isChild(course)}"> <a :href="urlFor('seminar_main.php', {auswahl: course.id})"> - {{ getCourseName(course, getConfig('sem_number') && responsiveDisplay) }} + {{ getCourseName(course) }} <span v-if="course.is_deputy">{{ $gettext('[Vertretung]') }}</span> </a> <span v-if="course.is_hidden" class="course-hidden-info"> 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 @@ <section class="studip-grid"> <template v-for="course in getOrderedCourses(subgroup.ids)"> <div class="course-group-label" v-if="isParent(course)" :key="course.id"> - {{ getCourseName(course, getConfig('sem_number')) }} + {{ getCourseName(course) }} </div> <article class="studip-grid-element" :data-course-id="course.id" :class="getCourseCssClasses(course)" :key="course.id"> @@ -24,10 +24,10 @@ ></studip-action-menu> </span> - <a :href="urlFor('seminar_main.php', {auswahl: course.id})" class="tiles-grid-element-header-content" :title="getCourseName(course, getConfig('sem_number'))"> + <a :href="urlFor('seminar_main.php', {auswahl: course.id})" class="tiles-grid-element-header-content" :title="getCourseName(course)"> <span :style="{backgroundImage: `url(${course.avatar})`}" class="tiles-grid-element-header-image"></span> <span class="tiled-grid-element-header-title"> - {{ getCourseName(course, getConfig('sem_number')) }} + {{ getCourseName(course) }} <span v-if="course.is_deputy">{{ $gettext('[Vertretung]') }}</span> </span> diff --git a/resources/vue/mixins/MyCoursesMixin.js b/resources/vue/mixins/MyCoursesMixin.js index dcf73a6..811619e 100644 --- a/resources/vue/mixins/MyCoursesMixin.js +++ b/resources/vue/mixins/MyCoursesMixin.js @@ -32,9 +32,9 @@ export default { }); }, - getCourseName(course, include_number = false) { + getCourseName(course) { let name = course.name; - if (include_number) { + if (this.displaySemNumber) { name = `${course.number} ${course.name}`; } return name.trim(); @@ -165,7 +165,13 @@ export default { 'isGroupOpen', 'getConfig', ]), - + displaySemNumber() { + return this.getConfig('sem_number_always') + || ( + this.getConfig('sem_number') + && !this.responsiveDisplay + ); + }, numberOfNavElements () { return Math.max( ...Object.values(this.courses).map(course => { |
