From 8ba78ce50c8cf61ad2df91ffaa19952bb5f4fff9 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms Date: Thu, 27 Feb 2025 15:43:55 +0000 Subject: replace old colour group selector and notifications with vue app that uses the same underlying data as my courses, relocate my courses vue components and remove now obsolete functions from meine_seminare_func, fixes #5165, fixes #5201 Closes #5165 and #5201 Merge request studip/studip!3892 --- app/controllers/admin/courses.php | 1 - app/controllers/my_courses.php | 413 ++++----------------- app/controllers/my_institutes.php | 2 - app/controllers/settings/notification.php | 167 +++------ app/controllers/settings/settings.php | 6 + app/views/calendar/schedule/_colour_selector.php | 18 - app/views/calendar/schedule/course_info.php | 22 +- app/views/calendar/schedule/entry.php | 16 +- app/views/my_courses/group_selector.php | 20 - app/views/my_courses/groups.php | 77 ---- app/views/settings/notification.php | 102 ----- lib/classes/ModulesNotification.php | 4 +- lib/classes/MyCoursesHelper.php | 266 +++++++++++++ lib/classes/MyRealmModel.php | 1 - lib/meine_seminare_func.inc.php | 273 -------------- resources/assets/javascripts/bootstrap/settings.js | 49 --- resources/assets/javascripts/bootstrap/vue.js | 10 +- .../assets/stylesheets/scss/colour-selector.scss | 51 +++ resources/assets/stylesheets/scss/my_courses.scss | 67 ---- resources/assets/stylesheets/scss/schedule.scss | 12 - resources/assets/stylesheets/scss/tables.scss | 2 +- resources/assets/stylesheets/studip.scss | 1 + resources/vue/components/ColourSelector.vue | 54 +++ resources/vue/components/MyCourses.vue | 108 ------ resources/vue/components/MyCoursesColorPicker.vue | 101 ----- resources/vue/components/MyCoursesNavigation.vue | 29 -- .../vue/components/MyCoursesNewContentToggle.vue | 31 -- .../vue/components/MyCoursesSidebarSwitch.vue | 39 -- resources/vue/components/MyCoursesTables.vue | 199 ---------- resources/vue/components/MyCoursesTiles.vue | 293 --------------- .../components/my-courses/ColorGroupSelector.vue | 204 ++++++++++ .../vue/components/my-courses/ColorPicker.vue | 101 +++++ resources/vue/components/my-courses/MyCourses.vue | 108 ++++++ resources/vue/components/my-courses/Navigation.vue | 29 ++ .../vue/components/my-courses/NewContentToggle.vue | 31 ++ .../my-courses/NotificationConfiguration.vue | 218 +++++++++++ .../vue/components/my-courses/SidebarSwitch.vue | 39 ++ resources/vue/components/my-courses/TableView.vue | 198 ++++++++++ resources/vue/components/my-courses/TileView.vue | 291 +++++++++++++++ resources/vue/mixins/MyCoursesMixin.js | 338 +++++++++-------- 40 files changed, 1926 insertions(+), 2065 deletions(-) delete mode 100644 app/views/calendar/schedule/_colour_selector.php delete mode 100644 app/views/my_courses/group_selector.php delete mode 100644 app/views/my_courses/groups.php delete mode 100644 app/views/settings/notification.php create mode 100644 lib/classes/MyCoursesHelper.php delete mode 100644 lib/meine_seminare_func.inc.php create mode 100644 resources/assets/stylesheets/scss/colour-selector.scss create mode 100644 resources/vue/components/ColourSelector.vue delete mode 100644 resources/vue/components/MyCourses.vue delete mode 100644 resources/vue/components/MyCoursesColorPicker.vue delete mode 100644 resources/vue/components/MyCoursesNavigation.vue delete mode 100644 resources/vue/components/MyCoursesNewContentToggle.vue delete mode 100644 resources/vue/components/MyCoursesSidebarSwitch.vue delete mode 100644 resources/vue/components/MyCoursesTables.vue delete mode 100644 resources/vue/components/MyCoursesTiles.vue create mode 100644 resources/vue/components/my-courses/ColorGroupSelector.vue create mode 100644 resources/vue/components/my-courses/ColorPicker.vue create mode 100644 resources/vue/components/my-courses/MyCourses.vue create mode 100644 resources/vue/components/my-courses/Navigation.vue create mode 100644 resources/vue/components/my-courses/NewContentToggle.vue create mode 100644 resources/vue/components/my-courses/NotificationConfiguration.vue create mode 100644 resources/vue/components/my-courses/SidebarSwitch.vue create mode 100644 resources/vue/components/my-courses/TableView.vue create mode 100644 resources/vue/components/my-courses/TileView.vue diff --git a/app/controllers/admin/courses.php b/app/controllers/admin/courses.php index 0d65e13..b01cfa1 100644 --- a/app/controllers/admin/courses.php +++ b/app/controllers/admin/courses.php @@ -23,7 +23,6 @@ * @since 3.1 */ -require_once 'lib/meine_seminare_func.inc.php'; require_once 'lib/object.inc.php'; require_once 'lib/archiv.inc.php'; //for lastActivity in getCourses() method diff --git a/app/controllers/my_courses.php b/app/controllers/my_courses.php index a8d9797..3a422bf 100644 --- a/app/controllers/my_courses.php +++ b/app/controllers/my_courses.php @@ -23,15 +23,17 @@ * @category Stud.IP * @since 3.1 */ -require_once 'lib/meine_seminare_func.inc.php'; + +use DI\Attribute\Inject; + require_once 'lib/object.inc.php'; class MyCoursesController extends AuthenticatedController { - /** - * @var Callable - */ - private $performance_timer = null; + #[Inject] + private readonly MyCoursesHelper $helper; + + private ?Closure $performance_timer = null; public function before_filter(&$action, &$args) { @@ -84,13 +86,7 @@ class MyCoursesController extends AuthenticatedController $sem_key = $this->getSemesterKey(); $group_field = $this->getGroupField(); - $sem_courses = 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, - ]); + $sem_courses = $this->helper->getCourses($sem_key, $group_field); // Waiting list $this->waiting_list = MyRealmModel::getWaitingList($GLOBALS['user']->id); @@ -114,11 +110,11 @@ class MyCoursesController extends AuthenticatedController $this->setupSidebar($sem_key, $group_field, $this->check_for_new($sem_courses, $group_field)); - $this->vueApp = Studip\VueApp::create('MyCourses') + $this->vueApp = Studip\VueApp::create('my-courses/MyCourses') ->withStore( 'MyCoursesStore', 'mycourses', - $this->getMyCoursesData($sem_courses, $group_field) + $this->helper->getVueAppData($sem_courses, $group_field) ); } @@ -165,69 +161,24 @@ class MyCoursesController extends AuthenticatedController PageLayout::setTitle($this->title); - PageLayout::setHelpKeyword('Basis.VeranstaltungenOrdnen'); Navigation::activateItem('/browse/my_courses/list'); $this->current_semester = $sem ?: Semester::findCurrent()->semester_id; $this->semesters = Semester::findAllVisible(); - $group_field = $this->getGroupField(); - - $temp = MyRealmModel::getPreparedCourses('', [ - 'group_field' => $group_field, - 'order_by' => null, - 'order' => 'asc', - 'studygroups_enabled' => Config::get()->MY_COURSES_ENABLE_STUDYGROUPS, - 'deputies_enabled' => Config::get()->DEPUTIES_ENABLE, - ]); - - $groups = []; - $my_sem = []; - foreach ($temp as $courses) { - foreach ($courses as $course) { - $my_sem[$course['seminar_id']] = $course; - if ($group_field) { - fill_groups($groups, $course[$group_field], [ - 'seminar_id' => $course['seminar_id'], - 'sem_nr' => $course['veranstaltungsnummer'], - 'name' => $course['name'], - 'gruppe' => $course['gruppe'] - ]); - } - } - } - - if ($group_field == 'sem_number') { - correct_group_sem_number($groups, $my_sem); - } else { - add_sem_name($my_sem); - } - - sort_groups($group_field, $groups); - - // Ensure that a seminar is never in multiple groups - $sem_ids = []; - foreach ($groups as $group_id => $seminars) { - foreach ($seminars as $index => $seminar) { - if (in_array($seminar['seminar_id'], $sem_ids)) { - unset($seminars[$index]); - } else { - $sem_ids[] = $seminar['seminar_id']; - } - } - if (empty($seminars)) { - unset($groups[$group_id]); - } else { - $groups[$group_id] = $seminars; - } - } - $this->studygroups = $studygroups; - $this->groups = $groups; - $this->group_names = get_group_names($group_field, $groups); - $this->group_field = $group_field; - $this->my_sem = $my_sem; - $this->cid = Request::get('cid', ''); + $this->render_vue_app( + Studip\VueApp::create('my-courses/ColorGroupSelector') + ->withProps([ + 'store-url' => $this->store_groupsURL($studygroups), + 'cid' => Request::get('option', ''), + ]) + ->withStore( + 'MyCoursesStore', + 'mycoursesgroupselector', + $this->helper->createVueAppData(''), + ) + ); } /** @@ -246,33 +197,34 @@ class MyCoursesController extends AuthenticatedController Request::get('select_group_field', $GLOBALS['user']->cfg->MY_COURSES_GROUPING) ); $gruppe = Request::getArray('gruppe'); - if (!empty($gruppe)) { - foreach ($gruppe as $key => $value) { - $updated = CourseMember::findEachBySQL( - function (CourseMember $cm) use ($value) { - $cm->gruppe = $value; - $cm->store(); + + if (count($gruppe) > 0) { + CourseMember::findEachBySQL( + function (CourseMember $member) use (&$gruppe) { + $member->gruppe = $gruppe[$member->seminar_id]; + $member->store(); + + unset($gruppe[$member->seminar_id]); + }, + 'user_id = ? AND Seminar_id IN (?)', + [ + User::findCurrent()->id, + array_keys($gruppe) + ] + ); + + if (count($gruppe) > 0 && $deputies_enabled) { + Deputy::findEachBySQL( + function (Deputy $deputy) use ($gruppe) { + $deputy->gruppe = $gruppe[$deputy->range_id]; + $deputy->store(); }, - 'Seminar_id = ? AND user_id = ?', + 'user_id = ? AND range_id IN ?', [ - $key, - $GLOBALS['user']->id + User::findCurrent()->id, + array_keys($gruppe), ] ); - - if ($deputies_enabled && !$updated) { - Deputy::findEachBySQL( - function (Deputy $deputy) use ($value) { - $deputy->gruppe = $value; - $deputy->store(); - }, - 'range_id = ? AND user_id = ?', - [ - $key, - $GLOBALS['user']->id - ] - ); - } } } @@ -653,125 +605,9 @@ class MyCoursesController extends AuthenticatedController $sem_key = $this->getSemesterKey(); $group_field = $this->getGroupField(); - $sem_courses = 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, - ]); - - return $this->getMyCoursesData($sem_courses, $group_field); - } - - /** - * 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 - * } - * } - */ - private function getMyCoursesData(?array $sem_courses, string $group_field): 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); - } + $sem_courses = $this->helper->getCourses($sem_key, $group_field); - 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' => $GLOBALS['user']->id, - 'setConfig' => [ - 'allow_dozent_visibility' => Config::get()->ALLOW_DOZENT_VISIBILITY, - 'open_groups' => array_values($GLOBALS['user']->cfg->MY_COURSES_OPEN_GROUPS), - 'sem_number' => Config::get()->IMPORTANT_SEMNUMBER, - 'view_settings' => $GLOBALS['user']->cfg->MY_COURSES_VIEW_SETTINGS, - 'group_by' => $this->getGroupField(), - ], - ]; + return $this->helper->getVueAppData($sem_courses, $group_field); } /** @@ -942,137 +778,6 @@ class MyCoursesController extends AuthenticatedController ); } - 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']), - 'extra_navigation' => $extra_navigation, - ]; - } - - private function reduceNavigation($nav) - { - 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; - } - - private function sanitizeNavigations(array $courses) - { - // 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 convertIcon(Icon $icon) - { - return [ - 'role' => $icon->getRole(), - 'shape' => $icon->getShape(), - ]; - } - private function getSemesterKey() { $config_sem = $GLOBALS['user']->cfg->MY_COURSES_SELECTED_CYCLE; @@ -1104,7 +809,7 @@ class MyCoursesController extends AuthenticatedController { $group_field = $GLOBALS['user']->cfg->MY_COURSES_GROUPING; - $forced_grouping = in_array(Config::get()->MY_COURSES_FORCE_GROUPING, getValidGroupingFields()) + $forced_grouping = in_array(Config::get()->MY_COURSES_FORCE_GROUPING, $this->getValidGroupingFields()) ? Config::get()->MY_COURSES_FORCE_GROUPING : 'sem_number'; @@ -1112,7 +817,7 @@ class MyCoursesController extends AuthenticatedController $forced_grouping = 'sem_number'; } - if (!$group_field || !in_array($group_field, getValidGroupingFields())) { + if (!$group_field || !in_array($group_field, $this->getValidGroupingFields())) { $group_field = 'sem_number'; } @@ -1123,6 +828,24 @@ class MyCoursesController extends AuthenticatedController return $group_field === 'not_grouped' ? 'sem_number' : $group_field; } + private function getValidGroupingFields(): array + { + $valid = [ + 'not_grouped', + 'sem_number', + 'sem_tree_id', + 'sem_status', + 'gruppe', + 'dozent_id', + ]; + + if (LvgruppeSeminar::countBySql('1') > 0) { + $valid[] = 'mvv'; + } + + return $valid; + } + /** * Returns all valid textual semester entries like 'last', 'future' etc * diff --git a/app/controllers/my_institutes.php b/app/controllers/my_institutes.php index 6093e72..31e25a6 100644 --- a/app/controllers/my_institutes.php +++ b/app/controllers/my_institutes.php @@ -1,6 +1,4 @@ MAIL_NOTIFICATION_ENABLE) { + if (!Config::get()->getValue('MAIL_NOTIFICATION_ENABLE')) { $message = _('Die Benachrichtigungsfunktion wurde in den Systemeinstellungen nicht freigeschaltet.'); throw new AccessDeniedException($message); } @@ -49,153 +49,76 @@ class Settings_NotificationController extends Settings_SettingsController /** * Display the notification settings of a user. */ - public function index_action() + public function index_action(): void { - $group_field = 'sem_number'; $semesters = Semester::findAllVisible(); $seminars = MyRealmModel::getCourses( array_key_first($semesters), array_key_last($semesters), - ['deputies_enabled' => Config::get()->DEPUTIES_ENABLE] + ['deputies_enabled' => Config::get()->getValue('DEPUTIES_ENABLE')] ); - if (!count($seminars)) { + if (count($seminars) === 0) { $message = sprintf(_('Sie haben zur Zeit keine Veranstaltungen belegt. Bitte nutzen Sie %sVeranstaltung suchen / hinzufügen%s um sch für Veranstaltungen anzumdelden.'), '', ''); PageLayout::postInfo($message); $this->render_nothing(); return; } - $modules_notification = new ModulesNotification(); - $enabled_modules = $modules_notification->registered_notification_modules; - $groups = []; - $my_sem = []; - foreach ($seminars as $seminar) { - $su = CourseMember::find([$seminar->id, User::findCurrent()->id]); - - if (!$su && Config::get()->DEPUTIES_ENABLE) { - $su = Deputy::find([$seminar->id, User::findCurrent()->id]); - } - - if (!$su) { - continue; - } - - $my_sem[$seminar['Seminar_id']] = [ - 'obj_type' => "sem", - 'sem_nr' => $seminar->veranstaltungsnummer, - 'name' => $seminar['Name'], - 'visible' => $seminar['visible'], - 'gruppe' => $su->gruppe, - 'sem_status' => $seminar->status, - 'sem_number' => Semester::getIndexById($seminar->start_semester->id), - 'sem_number_end' => Semester::getIndexById($seminar->end_semester->id ?? '') ?: '-1', - ]; - if ($group_field) { - fill_groups($groups, Semester::getIndexById($seminar->start_semester->id), [ - 'seminar_id' => $seminar['Seminar_id'], - 'sem_nr' => $seminar->veranstaltungsnummer, - 'name' => $seminar['Name'], - 'gruppe' => $su->gruppe, - ]); - } - } - - correct_group_sem_number($groups, $my_sem); - - - sort_groups($group_field, $groups); - $group_names = get_group_names($group_field, $groups); - $notifications = $this->user->course_notifications; - $open = UserConfig::get($this->user->user_id)->MY_COURSES_OPEN_GROUPS; - $checked = []; - foreach ($groups as $group_id => $group_members) { - if (!in_array($group_id, $open)) { - continue; - } - foreach ($group_members as $member) { - $checked[$member['seminar_id']] = []; - foreach ($enabled_modules as $index => $module) { - $notify = $notifications->findOneBy('seminar_id', $member['seminar_id']); - $checked[$member['seminar_id']][$index] = $notify && in_array($index, $notify->notification_data->getArrayCopy()); - } - $checked[$member['seminar_id']]['all'] = count($enabled_modules) === count(array_filter($checked[$member['seminar_id']])); - } - } - - $this->modules = $enabled_modules; - $this->groups = $groups; - $this->group_names = $group_names; - $this->group_field = 'sem_number'; - $this->open = $open; - $this->seminars = $my_sem; - $this->notifications = $notifications; - $this->checked = $checked; + $this->render_vue_app( + Studip\VueApp::create('my-courses/NotificationConfiguration') + ->withProps([ + 'store-url' => $this->storeURL(), + 'modules' => collect( + app(ModulesNotification::class)->registered_notification_modules + )->map( + fn(array $module, int $id): array => array_merge($module, ['id' => $id]) + )->values(), + 'notifications' => collect($this->user->course_notifications)->reduce( + function (array $carry, CourseMemberNotification $notification): array { + $carry[$notification->seminar_id] = array_map('intval', $notification->notification_data->getArrayCopy()); + return $carry; + }, + [] + ), + ]) + ->withStore( + 'MyCoursesStore', + 'mycoursesnotificationstore', + app(MyCoursesHelper::class)->createVueAppData('') + ) + ); } /** * Stores the notification settings of a user. */ - public function store_action() + public function store_action(): void { - $this->check_ticket(); - foreach (Request::getArray('m_checked') as $course_id => $checked) { - unset($checked['empty']); - if (!count($checked)) { - CourseMemberNotification::deleteBySQL('user_id=? AND seminar_id=?', [$this->user->user_id, $course_id]); + CSRFProtection::verifyUnsafeRequest(); + + $course_ids = Request::optionArray('course_ids'); + $notifications = Request::getArray('notifications'); + + $changed = 0; + foreach ($course_ids as $course_id) { + if (!isset($notifications[$course_id])) { + $changed += CourseMemberNotification::deleteBySQL( + 'user_id=? AND seminar_id=?', + [$this->user->user_id, $course_id] + ); } else { $notify = new CourseMemberNotification([$this->user->user_id, $course_id]); - $notify->notification_data = array_keys($checked); - $notify->store(); + $notify->notification_data = $notifications[$course_id]; + $changed += $notify->store(); } } - PageLayout::postSuccess(_('Die Einstellungen wurden gespeichert.')); - $this->redirect('settings/notification'); - } - /** - * Opens a specific area. - * - * @param String $id Id of the area to be opened - */ - public function open_action($id) - { - $open = $this->config->MY_COURSES_OPEN_GROUPS; - if (!in_array($id, $open)) { - $open[] = $id; + if ($changed > 0) { + PageLayout::postSuccess(_('Die Einstellungen wurden gespeichert.')); } - $this->config->store('MY_COURSES_OPEN_GROUPS', $open); - - $this->redirect('settings/notification'); - } - - /** - * Closes a specific area. - * - * @param String $id Id of the area to be closed - */ - public function close_action($id) - { - $open = $this->config->MY_COURSES_OPEN_GROUPS; - $open = array_diff($open, [$id]); - $this->config->store('MY_COURSES_OPEN_GROUPS', $open); $this->redirect('settings/notification'); } - - public function module_icon($area) - { - $mapping = [ - 'documents' => 'files', - 'elearning_interface' => 'learnmodule', - 'scm' => 'infopage', - 'votes' => 'vote', - 'basic_data' => 'seminar', - 'participants' => 'persons', - 'plugins' => 'plugin', - ]; - - return $mapping[$area] ?: $area; - } } diff --git a/app/controllers/settings/settings.php b/app/controllers/settings/settings.php index 294b288..a2cd32e 100644 --- a/app/controllers/settings/settings.php +++ b/app/controllers/settings/settings.php @@ -15,6 +15,12 @@ require_once 'lib/messaging.inc.php'; +/** + * @property User $user + * @property bool $restricted + * @property UserConfig $config + * @property email_validation_class $validator + */ abstract class Settings_SettingsController extends AuthenticatedController { // Stores message which shall be send to the user via email diff --git a/app/views/calendar/schedule/_colour_selector.php b/app/views/calendar/schedule/_colour_selector.php deleted file mode 100644 index c9cfec1..0000000 --- a/app/views/calendar/schedule/_colour_selector.php +++ /dev/null @@ -1,18 +0,0 @@ - - $data) : ?> - - - id="colour-"> - - - diff --git a/app/views/calendar/schedule/course_info.php b/app/views/calendar/schedule/course_info.php index 42c8b39..2a86a31 100644 --- a/app/views/calendar/schedule/course_info.php +++ b/app/views/calendar/schedule/course_info.php @@ -14,17 +14,19 @@
- - - render_partial( - 'my_courses/group_selector', - [ - 'course_id' => $course->id, - 'selected_group_id' => $membership->gruppe + withProps([ + 'autofocus' => true, + 'colours' => collect()->range(0, 8)->map( + fn($group) => [ + 'id' => $group, + 'class' => 'gruppe' . $group, + 'label' => sprintf(_('Gruppe %u zuordnen'), $group + 1), ] - ) ?> - -
+ )->values(), + 'input-name' => 'gruppe[' . htmlReady($course->id) . ']', + 'model-value' => $membership->gruppe, + ]) ?>
diff --git a/app/views/calendar/schedule/entry.php b/app/views/calendar/schedule/entry.php index 6109041..fa563fd 100644 --- a/app/views/calendar/schedule/entry.php +++ b/app/views/calendar/schedule/entry.php @@ -10,14 +10,14 @@
- - - render_partial( - 'calendar/schedule/_colour_selector', - ['selected_colour_id' => $entry->colour_id] - ) ?> - -
+ withProps([ + 'autofocus' => true, + 'colours' => collect($GLOBALS['PERS_TERMIN_KAT'])->map( + fn($data, $id) => ['id' => $id, 'colour' => $data['bgcolor']] + )->values(), + 'model-value' => $entry->colour_id, + ]) ?>
diff --git a/app/views/my_courses/group_selector.php b/app/views/my_courses/group_selector.php deleted file mode 100644 index 128b7eb..0000000 --- a/app/views/my_courses/group_selector.php +++ /dev/null @@ -1,20 +0,0 @@ - - - - > - - - diff --git a/app/views/my_courses/groups.php b/app/views/my_courses/groups.php deleted file mode 100644 index ee9b964..0000000 --- a/app/views/my_courses/groups.php +++ /dev/null @@ -1,77 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - $group_members): ?> - > - - - - - - - - - - render_partial( - 'my_courses/group_selector', - [ - 'course_id' => $member['seminar_id'], - 'selected_group_id' => $my_sem[$member['seminar_id']]['gruppe'] - ] - ) ?> - - - - -
- - - ' . $group_names[$group_id][0]) ?> - - - - -
- - IMPORTANT_SEMNUMBER ? $my_sem[$member['seminar_id']]['veranstaltungsnummer'] : '') ?> - - - - - -
- -
-
- - url_for('my_courses/groups')) ?> -
-
-
diff --git a/app/views/settings/notification.php b/app/views/settings/notification.php deleted file mode 100644 index 1ee9888..0000000 --- a/app/views/settings/notification.php +++ /dev/null @@ -1,102 +0,0 @@ - -
- - - - - - - - - - - - - - - - $module): ?> - copyWithRole(Icon::ROLE_INFO); ?> - - - - - - - - $data): ?> - - - - - - $members): ?> - - - - - - - - - - - - $data): ?> - - - - - - - - -
- -
- asImg(['class' => 'middle', 'title' => $module['name']]) ?> -
- - - > - - > - -
- - > - asImg() ?> - - > - asImg() ?> - - - -
  - - IMPORTANT_SEMNUMBER ? htmlReady($seminars[$member['seminar_id']]['sem_nr']) : '' ?> - - - - - - - - > - - > -
-
- _('Änderungen übernehmen')]) ?> - url_for('settings/notification')) ?> -
-
diff --git a/lib/classes/ModulesNotification.php b/lib/classes/ModulesNotification.php index 96aa660..22586fb 100644 --- a/lib/classes/ModulesNotification.php +++ b/lib/classes/ModulesNotification.php @@ -77,9 +77,7 @@ class ModulesNotification $this->subject = _("Stud.IP Benachrichtigung"); } - - - public function getAllNotifications ($user_id = null) + public function getAllNotifications($user_id = null) { if ($user_id === null) { $user_id = $GLOBALS['user']->id; diff --git a/lib/classes/MyCoursesHelper.php b/lib/classes/MyCoursesHelper.php new file mode 100644 index 0000000..c4ef51b --- /dev/null +++ b/lib/classes/MyCoursesHelper.php @@ -0,0 +1,266 @@ +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' => $GLOBALS['user']->id, + 'setConfig' => [ + 'allow_dozent_visibility' => Config::get()->ALLOW_DOZENT_VISIBILITY, + 'open_groups' => array_values($GLOBALS['user']->cfg->MY_COURSES_OPEN_GROUPS), + 'sem_number' => Config::get()->IMPORTANT_SEMNUMBER, + 'view_settings' => $GLOBALS['user']->cfg->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']), + '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/lib/classes/MyRealmModel.php b/lib/classes/MyRealmModel.php index f3ab44c..dea3cb5 100644 --- a/lib/classes/MyRealmModel.php +++ b/lib/classes/MyRealmModel.php @@ -23,7 +23,6 @@ * @since 3.1 */ -require_once 'lib/meine_seminare_func.inc.php'; require_once 'lib/object.inc.php'; class MyRealmModel diff --git a/lib/meine_seminare_func.inc.php b/lib/meine_seminare_func.inc.php deleted file mode 100644 index 5313f8d..0000000 --- a/lib/meine_seminare_func.inc.php +++ /dev/null @@ -1,273 +0,0 @@ -getPath(' > '); - }; - } elseif ($group_field === 'sem_status') { - $mapper = function ($key): string { - $sem_type = $GLOBALS['SEM_TYPE'][$key]; - return "{$sem_type['name']} ({$GLOBALS['SEM_CLASS'][$sem_type['class']]['name']})"; - }; - } elseif ($group_field === 'no_grouped') { - $mapper = function (): string { - return _('keine Gruppierung'); - }; - } elseif ($group_field === 'gruppe') { - $groupcount = 0; - $mapper = function () use (&$groupcount): string { - $groupcount += 1; - return _('Gruppe') . " {$groupcount}"; - }; - } elseif ($group_field === 'dozent_id') { - $mapper = function ($key): string { - return get_fullname($key, 'no_title_short'); - }; - } elseif ($group_field === 'mvv') { - $mapper = function ($key): string { - $module = Modul::find($key); - return $module ? (string) $module->getDisplayName() : _('Keinem Modul zugeordnet'); - }; - } - - $result = []; - foreach (array_keys($groups) as $key) { - $result[$key] = $mapper($key); - } - return $result; -} - -/** - * - * @param string $group_field - * @param array $groups - */ -function sort_groups($group_field, &$groups) -{ - switch ($group_field) { - case 'sem_number': - krsort($groups, SORT_NUMERIC); - break; - - case 'gruppe': - ksort($groups, SORT_NUMERIC); - break; - - case 'sem_tree_id': - uksort($groups, function ($a, $b) { - $a_obj = StudipStudyArea::getNode($a); - $b_obj = StudipStudyArea::getNode($b); - return strcmp($a_obj->name, $b_obj->name); - }); - break; - - case 'sem_status': - uksort($groups, function ($a, $b) { - global $SEM_CLASS,$SEM_TYPE; - return strnatcasecmp( - $SEM_TYPE[$a]['name'] . ' (' . $SEM_CLASS[$SEM_TYPE[$a]['class']]['name'] . ')', - $SEM_TYPE[$b]['name'] . ' (' . $SEM_CLASS[$SEM_TYPE[$b]['class']]['name'] . ')' - ); - }); - break; - - case 'dozent_id': - uksort($groups, function ($a,$b) { - $replacements = ['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue']; - return strnatcasecmp( - str_replace(array_keys($replacements), array_values($replacements), mb_strtolower(get_fullname($a, 'no_title_short'))), - str_replace(array_keys($replacements), array_values($replacements), mb_strtolower(get_fullname($b, 'no_title_short'))) - ); - }); - break; - - case 'mvv': - uksort($groups, function ($a, $b): int { - $module_a = Modul::find($a); - $module_b = Modul::find($b); - - if (!$module_a) { - return 1; - } - if (!$module_b) { - return -1; - } - return strnatcasecmp($module_a->getDisplayName(), $module_b->getDisplayName()); - }); - break; - } - - foreach ($groups as $key => &$value) { - usort($value, function ($a, $b) { - if ($a['gruppe'] != $b['gruppe']) { - return (int)($a['gruppe'] - $b['gruppe']); - } else { - if (Config::get()->IMPORTANT_SEMNUMBER) { - return strnatcasecmp($a['sem_nr'] ?? '', $b['sem_nr'] ?? ''); - } else { - return strnatcmp($a['name'], $b['name']); - } - } - }); - } - return true; -} - -/** - * - * @param array $groups - * @param array $my_obj - */ -function correct_group_sem_number(&$groups, &$my_obj): bool -{ - if (!is_array($groups) || !is_array($my_obj)) { - return false; - } - - $current_semester = Semester::findCurrent(); - - $my_sem = array_filter( - $my_obj, - fn($values) => $values['obj_type'] === 'sem' - ); - - Course::findEachMany( - function (Course $course) use (&$groups, &$my_obj, $current_semester) { - if (count($course->semesters) === 1) { - return; - } - - $obj_data = $my_obj[$course->id]; - - if ( - $course->isOpenEnded() - && $course->start_semester->beginn < $current_semester->beginn - ) { - unset($groups[$obj_data['sem_number']][$course->id]); - - fill_groups($groups, Semester::getIndexById($current_semester->id), [ - 'seminar_id' => $course->id, - 'name' => $obj_data['name'], - 'gruppe' => $obj_data['gruppe'], - ]); - - if (count($groups[$obj_data['sem_number']]) === 0) { - unset($groups[$obj_data['sem_number']]); - } - } else { - $to_sem = $obj_data['sem_number_end']; - for ($i = $obj_data['sem_number']; $i <= $to_sem; ++$i){ - fill_groups($groups, $i, [ - 'seminar_id' => $course->id, - 'name' => $obj_data['name'], - 'gruppe' => $obj_data['gruppe'] - ]); - } - } - - if (User::findCurrent()->getConfiguration()->getValue('SHOWSEM_ENABLE')) { - $my_obj[$course->id]['name'] .= ' (' . $course->getTextualSemester() . ')'; - } - }, - array_keys($my_sem) - ); - - return true; -} - -/** - * - * @param mixed $my_obj - */ -function add_sem_name(&$my_obj): bool -{ - if ($GLOBALS['user']->cfg->getValue('SHOWSEM_ENABLE')) { - $sem_data = Semester::findAllVisible(); - if (is_array($my_obj)) { - foreach ($my_obj as $seminar_id => $values){ - if ($values['obj_type'] == 'sem' && $values['sem_number'] != $values['sem_number_end']){ - $sem_name = " (" . $sem_data[$values['sem_number']]['name'] . " - "; - $sem_name .= (($values['sem_number_end'] == -1) ? _("unbegrenzt") : $sem_data[$values['sem_number_end']]['name']) . ")"; - $my_obj[$seminar_id]['name'] .= $sem_name; - } else { - $my_obj[$seminar_id]['name'] .= " (" . $sem_data[$values['sem_number']]['name'] . ") "; - } - } - } - } - return true; -} - -/** - * - * @param array $groups - * @param string|null $group_key - * @param array $group_entry - * - * @return bool - */ -function fill_groups(array &$groups, ?string $group_key, array $group_entry): bool -{ - if (is_null($group_key)){ - $group_key = 'not_grouped'; - } - - if (!isset($groups[$group_key]) || !is_array($groups[$group_key])) { - $groups[$group_key] = []; - } - - $group_entry['name'] = str_replace( - ['ä', 'ö', 'ü'], - ['ae', 'oe', 'ue'], - mb_strtolower($group_entry['name']) - ); - if (!in_array($group_entry, $groups[$group_key])) { - $groups[$group_key][$group_entry['seminar_id']] = $group_entry; - return true; - } - - return false; -} - -/** - * This function returns all valid fields that may be used for course - * grouping in "My Courses". - * - * @return array All fields that may be specified for course grouping - */ -function getValidGroupingFields(): array -{ - $valid = [ - 'not_grouped', - 'sem_number', - 'sem_tree_id', - 'sem_status', - 'gruppe', - 'dozent_id', - ]; - - if (LvgruppeSeminar::countBySql('1') > 0) { - $valid[] = 'mvv'; - } - - return $valid; -} diff --git a/resources/assets/javascripts/bootstrap/settings.js b/resources/assets/javascripts/bootstrap/settings.js index caae5b2..2acaabe 100644 --- a/resources/assets/javascripts/bootstrap/settings.js +++ b/resources/assets/javascripts/bootstrap/settings.js @@ -32,55 +32,6 @@ STUDIP.domReady(() => { }); }); -// -$(document).on('change', '#settings-notifications :checkbox', function() { - var name = $(this).attr('name'); - - if (name === 'all[all]') { - $(this) - .closest('table') - .find(':checkbox') - .prop('checked', this.checked); - return; - } - - if (/all\[columns\]/.test(name)) { - var index = - $(this) - .closest('td') - .index() + 2; - $(this) - .closest('table') - .find('tbody td:nth-child(' + index + ') :checkbox') - .prop('checked', this.checked); - } else if (/all\[rows\]/.test(name)) { - $(this) - .closest('td') - .siblings() - .find(':checkbox') - .prop('checked', this.checked); - } - - $('.notification.settings tbody :checkbox[name^=all]').each(function() { - var other = $(this) - .closest('td') - .siblings() - .find(':checkbox'); - this.checked = other.filter(':not(:checked)').length === 0; - }); - - $('.notification.settings thead :checkbox').each(function() { - var index = - $(this) - .closest('td') - .index() + 2, - other = $(this) - .closest('table') - .find('tbody td:nth-child(' + index + ') :checkbox'); - this.checked = other.filter(':not(:checked)').length === 0; - }); -}); - $(document).on('input', '#new_password', function() { var message = $(this).data().message; if (this.validity.patternMismatch) { diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js index c0fe942..ac22629 100644 --- a/resources/assets/javascripts/bootstrap/vue.js +++ b/resources/assets/javascripts/bootstrap/vue.js @@ -46,9 +46,12 @@ STUDIP.ready(() => { const promises = [Promise.resolve()]; for (const [index, name] of Object.entries(config.stores)) { + promises.push( import(`../../../vue/store/${name}.js`).then(storeConfig => { - store.registerModule(index, storeConfig.default); + if (!store.hasModule(index)) { + store.registerModule(index, storeConfig.default); + } const dataElement = document.getElementById(`vue-store-data-${index}`); if (dataElement) { @@ -106,5 +109,10 @@ STUDIP.ready(() => { plugins.forEach(plugin => app.use(plugin, { store })) app.mount(node); + + const dialog = node.closest('.studip-dialog'); + if (dialog !== null) { + $(dialog).on('dialogclose', () => app.unmount()); + } }); }); diff --git a/resources/assets/stylesheets/scss/colour-selector.scss b/resources/assets/stylesheets/scss/colour-selector.scss new file mode 100644 index 0000000..0585f2a --- /dev/null +++ b/resources/assets/stylesheets/scss/colour-selector.scss @@ -0,0 +1,51 @@ +.colour-selector { + $padding: 2px; + + position: relative; + + background-clip: padding-box; + width: var(--icon-size-default); + + &.mycourses-group-selector { + border: 1px solid var(--color--table-border); + } + + input[type="radio"] { + @extend .sr-only; + + &:focus + label { + // Replicates the focus on the label + // @see https://css-tricks.com/copy-the-browsers-native-focus-styles/ + outline: 5px auto Highlight; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: 1px; + } + + &:checked + label { + // We need to insert a pseudo element since the above focus + // indicator would be lost + &::before { + content: ''; + display: inline-block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + outline: 2px solid var(--white); + outline-offset: -4px; + } + } + } + + label { + box-sizing: border-box; + cursor: pointer; + height: calc(var(--icon-size-default) + 2 * $padding); + margin: 0 !important; + padding: $padding; + position: relative; + text-indent: 0 !important; + width: calc(var(--icon-size-default) + 2 * $padding); + } +} diff --git a/resources/assets/stylesheets/scss/my_courses.scss b/resources/assets/stylesheets/scss/my_courses.scss index 2e26dee..9d34b5c 100644 --- a/resources/assets/stylesheets/scss/my_courses.scss +++ b/resources/assets/stylesheets/scss/my_courses.scss @@ -9,73 +9,6 @@ background: var(--white); } -form.default table.mycourses-group-selector { - table-layout: fixed; - width: unset; - - td { - width: 2em; - } -} - -form.default td.mycourses-group-selector, -table.colour-selector td.colour { - position: relative; - - background-clip: padding-box; - - &.mycourses-group-selector { - border: 1px solid fade-out($brand-color-lighter, 0.8); - } - - &.colour { - padding-left: 0.1em; - padding-right: 0.1em; - } - - input[type="radio"] { - @extend .sr-only; - - &:checked + label { - .group-number { - display: none; - } - .checked-icon { - display: inline; - } - } - } - - &:hover label { - .group-number { - display: none; - } - .checked-icon { - display: inline; - } - } - - label { - text-align: center; - font-size: large; - font-weight: bold; - cursor: pointer; - - background-color: var(--white); - margin-bottom: 0; - text-indent: 0; - - height: 1.2em; - - .group-number { - display: inline; - } - .checked-icon { - display: none; - } - } -} - $icon-padding: 3px; .my-courses-navigation { diff --git a/resources/assets/stylesheets/scss/schedule.scss b/resources/assets/stylesheets/scss/schedule.scss index c163f42..c2be9ed 100644 --- a/resources/assets/stylesheets/scss/schedule.scss +++ b/resources/assets/stylesheets/scss/schedule.scss @@ -3,18 +3,6 @@ form.default.schedule-entry { section.nowrap { flex-wrap: nowrap; } - - table.colour-selector td.colour { - label { - width: 1.5em; - height: 1.5em; - - .studip-icon { - height: 1.5em; - filter: drop-shadow(0 0 2px var(--white)) - } - } - } } .fc.schedule { diff --git a/resources/assets/stylesheets/scss/tables.scss b/resources/assets/stylesheets/scss/tables.scss index 788392d..f2978ba 100644 --- a/resources/assets/stylesheets/scss/tables.scss +++ b/resources/assets/stylesheets/scss/tables.scss @@ -214,7 +214,7 @@ td.tree-elbow-line, td.tree-elbow-end { background-color: var(--group-color-8) !important; } -#my_seminars, #settings-notifications { +#my_seminars { .gruppe0, .gruppe1, .gruppe2, .gruppe3, .gruppe4, .gruppe5, .gruppe6, .gruppe7, .gruppe9 { width: 1px; diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index 42ab072..6d1d16a 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -24,6 +24,7 @@ @import "scss/buttons"; @import "scss/calendar"; @import "scss/clipboard"; +@import "scss/colour-selector"; @import "scss/consultation"; @import "scss/contacts"; @import "scss/contentbar"; diff --git a/resources/vue/components/ColourSelector.vue b/resources/vue/components/ColourSelector.vue new file mode 100644 index 0000000..43520b3 --- /dev/null +++ b/resources/vue/components/ColourSelector.vue @@ -0,0 +1,54 @@ + + diff --git a/resources/vue/components/MyCourses.vue b/resources/vue/components/MyCourses.vue deleted file mode 100644 index 85369c7..0000000 --- a/resources/vue/components/MyCourses.vue +++ /dev/null @@ -1,108 +0,0 @@ - - - - - diff --git a/resources/vue/components/MyCoursesColorPicker.vue b/resources/vue/components/MyCoursesColorPicker.vue deleted file mode 100644 index 68eae5d..0000000 --- a/resources/vue/components/MyCoursesColorPicker.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - - - diff --git a/resources/vue/components/MyCoursesNavigation.vue b/resources/vue/components/MyCoursesNavigation.vue deleted file mode 100644 index e782c7a..0000000 --- a/resources/vue/components/MyCoursesNavigation.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/resources/vue/components/MyCoursesNewContentToggle.vue b/resources/vue/components/MyCoursesNewContentToggle.vue deleted file mode 100644 index 3ced26d..0000000 --- a/resources/vue/components/MyCoursesNewContentToggle.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/resources/vue/components/MyCoursesSidebarSwitch.vue b/resources/vue/components/MyCoursesSidebarSwitch.vue deleted file mode 100644 index 788c053..0000000 --- a/resources/vue/components/MyCoursesSidebarSwitch.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/resources/vue/components/MyCoursesTables.vue b/resources/vue/components/MyCoursesTables.vue deleted file mode 100644 index 3041735..0000000 --- a/resources/vue/components/MyCoursesTables.vue +++ /dev/null @@ -1,199 +0,0 @@ - - - - - diff --git a/resources/vue/components/MyCoursesTiles.vue b/resources/vue/components/MyCoursesTiles.vue deleted file mode 100644 index 355d24c..0000000 --- a/resources/vue/components/MyCoursesTiles.vue +++ /dev/null @@ -1,293 +0,0 @@ - - - - - - diff --git a/resources/vue/components/my-courses/ColorGroupSelector.vue b/resources/vue/components/my-courses/ColorGroupSelector.vue new file mode 100644 index 0000000..f017cc7 --- /dev/null +++ b/resources/vue/components/my-courses/ColorGroupSelector.vue @@ -0,0 +1,204 @@ + + + diff --git a/resources/vue/components/my-courses/ColorPicker.vue b/resources/vue/components/my-courses/ColorPicker.vue new file mode 100644 index 0000000..d2354c9 --- /dev/null +++ b/resources/vue/components/my-courses/ColorPicker.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/resources/vue/components/my-courses/MyCourses.vue b/resources/vue/components/my-courses/MyCourses.vue new file mode 100644 index 0000000..96a68fb --- /dev/null +++ b/resources/vue/components/my-courses/MyCourses.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/resources/vue/components/my-courses/Navigation.vue b/resources/vue/components/my-courses/Navigation.vue new file mode 100644 index 0000000..bea4845 --- /dev/null +++ b/resources/vue/components/my-courses/Navigation.vue @@ -0,0 +1,29 @@ + + + diff --git a/resources/vue/components/my-courses/NewContentToggle.vue b/resources/vue/components/my-courses/NewContentToggle.vue new file mode 100644 index 0000000..20b0bf0 --- /dev/null +++ b/resources/vue/components/my-courses/NewContentToggle.vue @@ -0,0 +1,31 @@ + + + diff --git a/resources/vue/components/my-courses/NotificationConfiguration.vue b/resources/vue/components/my-courses/NotificationConfiguration.vue new file mode 100644 index 0000000..0153f49 --- /dev/null +++ b/resources/vue/components/my-courses/NotificationConfiguration.vue @@ -0,0 +1,218 @@ + + + diff --git a/resources/vue/components/my-courses/SidebarSwitch.vue b/resources/vue/components/my-courses/SidebarSwitch.vue new file mode 100644 index 0000000..8742982 --- /dev/null +++ b/resources/vue/components/my-courses/SidebarSwitch.vue @@ -0,0 +1,39 @@ + + + diff --git a/resources/vue/components/my-courses/TableView.vue b/resources/vue/components/my-courses/TableView.vue new file mode 100644 index 0000000..e7f6650 --- /dev/null +++ b/resources/vue/components/my-courses/TableView.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/resources/vue/components/my-courses/TileView.vue b/resources/vue/components/my-courses/TileView.vue new file mode 100644 index 0000000..bd51562 --- /dev/null +++ b/resources/vue/components/my-courses/TileView.vue @@ -0,0 +1,291 @@ + + + + + + diff --git a/resources/vue/mixins/MyCoursesMixin.js b/resources/vue/mixins/MyCoursesMixin.js index fa28388..1ce95a2 100644 --- a/resources/vue/mixins/MyCoursesMixin.js +++ b/resources/vue/mixins/MyCoursesMixin.js @@ -1,185 +1,217 @@ import Responsive from '../../assets/javascripts/lib/responsive.js'; import { mapState, mapActions, mapGetters } from 'vuex'; -import MyCoursesNavigation from '../components/MyCoursesNavigation.vue'; - -export default { - components: { MyCoursesNavigation }, - data () { - return { - responsiveDisplay: false, - }; - }, - methods: { - ...mapActions('mycourses', [ - 'toggleOpenGroup', - 'updateConfigValue', - ]), - - getViewConfig(key) { - return this.getConfig( - 'view_settings', - this.responsiveDisplay ? 'responsive' : 'regular', - key - ); - }, - updateViewConfig(key, value) { - let config = this.getConfig('view_settings'); - config[this.responsiveDisplay ? 'responsive' : 'regular'][key] = value; - return this.updateConfigValue({ - key: 'view_settings', - value: config - }); +import Navigation from '../components/my-courses/Navigation.vue'; +import { $gettext } from "../../assets/javascripts/lib/gettext"; + +function createMixin(minimal = false) { + const result = { + data () { + return { + responsiveDisplay: false, + }; }, + methods: { + getCourseName(course) { + let name = course.name; + + // Include sem number + if ( + this.config?.sem_number + && !this.responsiveDisplay + ) { + name = `${course.number} ${name}`; + } - getCourseName(course, include_number = false) { - let name = course.name; - if (include_number) { - name = `${course.number} ${course.name}`; - } - return name.trim(); - }, + // Show deputy info + if (course.is_deputy) { + name = `${name} ${$gettext('[Vertretung]')}`; + } - urlFor(url, parameters, ignore_params) { - return STUDIP.URLHelper.getURL(url, parameters, ignore_params); - }, + return name; + }, + getCourseURL(course) { + return this.urlFor('dispatch.php/course/go', {to: course.id}, true); + }, - getCourses (ids) { - return ids.map(id => this.courses[id]); - }, + urlFor(url, parameters, ignore_params) { + return STUDIP.URLHelper.getURL(url, parameters, ignore_params); + }, - isParent (course) { - return course.children.length > 0 && course.children.every(childId => { - return this.courses[childId] !== undefined; - }); + getCourses (ids) { + return ids.map(id => this.courses[id]); + }, }, - isChild (course) { - return course.parent !== null && this.courses[course.parent] !== undefined; - }, - - getHiddenTooltip(course) { - let infotext = this.$gettext('Versteckte Veranstaltungen können über die Suchfunktionen nicht gefunden werden.'); - infotext += ' '; - if (course.is_teacher && this.getConfig('allow_dozent_visibility')) { - infotext += this.$gettext('Um die Veranstaltung sichtbar zu machen, wählen Sie den Punkt "Sichtbarkeit" im Administrationsbereich der Veranstaltung.'); - } else { - infotext += this.$gettext('Um die Veranstaltung sichtbar zu machen, wenden Sie sich an Administrierende.'); + computed: { + csrf() { + return STUDIP.CSRF_TOKEN; } - return infotext; }, - - getActionMenuForCourse(course, withColorPicker = false) { - let menu = []; - - if (!course.is_studygroup) { - menu.push({ - url: this.urlFor(`dispatch.php/course/details/index/${course.id}`, {from: this.urlFor('dispatch.php/my_courses/index')}), - label: this.$gettext('Veranstaltungsdetails'), - icon: 'info-circle', - attributes: { - 'data-dialog': '', - }, - }); + created() { + this.responsiveDisplay = Responsive.isResponsive(); + Responsive.media_query.addEventListener('change', () => { + this.responsiveDisplay = Responsive.isResponsive(); + }) + } + }; + + if (!minimal) { + result.components = { Navigation }; + + result.computed = { + ...result.computed, + + ...mapState('mycourses', [ + 'courses', + 'groups', + 'userid', + 'config', + ]), + ...mapGetters('mycourses', [ + 'isGroupOpen', + 'getConfig', + ]), + + numberOfNavElements () { + return Math.max( + ...Object.values(this.courses).map(course => { + const navigation = this.getNavigationForCourse(course, true); + return Object.values(navigation).length; + }) + ); } + }; - if (withColorPicker) { - // Color grouping - menu.push({ - emit: 'show-color-picker', - emitArguments: [course], - label: this.$gettext('Farbgruppierung ändern'), - icon: 'group4' - }); - } + result.methods = { + ...result.methods, - // Extra navigation? - if (!course.is_group) { - if (course.extra_navigation) { - menu.push(course.extra_navigation); - } else if (course.admission_binding) { + ...mapActions('mycourses', [ + 'toggleOpenGroup', + 'updateConfigValue', + ]), + + getActionMenuForCourse(course, withColorPicker = false) { + let menu = []; + + if (!course.is_studygroup) { menu.push({ - url: this.urlFor('dispatch.php/my_courses/decline_binding'), - label: this.$gettext('Aus der Veranstaltung austragen'), - icon: 'decline/door-leave', + url: this.urlFor(`dispatch.php/course/details/index/${course.id}`, {from: this.urlFor('dispatch.php/my_courses/index')}), + label: this.$gettext('Veranstaltungsdetails'), + icon: 'info-circle', attributes: { - title: this.$gettext('Die Teilnahme ist bindend. Bitte wenden Sie sich an die Lehrenden.'), + 'data-dialog': '', }, - disabled: true }); - } else { + } + + if (withColorPicker) { + // Color grouping menu.push({ - url: this.urlFor(`dispatch.php/my_courses/decline/${course.id}`, {cmd: 'suppose_to_kill'}), - label: this.$gettext('Aus der Veranstaltung austragen'), - icon: 'door-leave' + emit: 'show-color-picker', + emitArguments: [course], + label: this.$gettext('Farbgruppierung ändern'), + icon: 'group4' }); } - } - - return menu; - }, - getNavigationForCourse(course, gaps = false) { - let navigation = {}; - - Object.entries(course.navigation).forEach(([key, nav]) => { - if (!nav && !gaps) { - return; + // Extra navigation? + if (!course.is_group) { + if (course.extra_navigation) { + menu.push(course.extra_navigation); + } else if (course.admission_binding) { + menu.push({ + url: this.urlFor('dispatch.php/my_courses/decline_binding'), + label: this.$gettext('Aus der Veranstaltung austragen'), + icon: 'decline/door-leave', + attributes: { + title: this.$gettext('Die Teilnahme ist bindend. Bitte wenden Sie sich an die Lehrenden.'), + }, + disabled: true + }); + } else { + menu.push({ + url: this.urlFor(`dispatch.php/my_courses/decline/${course.id}`, {cmd: 'suppose_to_kill'}), + label: this.$gettext('Aus der Veranstaltung austragen'), + icon: 'door-leave' + }); + } } - if (this.getViewConfig('only_new') && !nav.important) { - return; + return menu; + }, + getHiddenTooltip(course) { + let infotext = this.$gettext('Versteckte Veranstaltungen können über die Suchfunktionen nicht gefunden werden.'); + infotext += ' '; + if (course.is_teacher && this.getConfig('allow_dozent_visibility')) { + infotext += this.$gettext('Um die Veranstaltung sichtbar zu machen, wählen Sie den Punkt "Sichtbarkeit" im Administrationsbereich der Veranstaltung.'); + } else { + infotext += this.$gettext('Um die Veranstaltung sichtbar zu machen, wenden Sie sich an Administrierende.'); } - - let result = nav ? Object.assign({}, nav) : false; - if (nav) { - if (nav.important) { - result.class = 'my-courses-navigation-important'; - result.icon.role = 'attention'; - result.icon.shape = result.icon.shape.replace(/^new\//, ''); - } else { - result.class = false; - result.icon.role = 'clickable'; + return infotext; + }, + getNavigationForCourse(course, gaps = false) { + let navigation = {}; + + Object.entries(course.navigation).forEach(([key, nav]) => { + if (!nav && !gaps) { + return; } - result.url = this.urlFor('dispatch.php/course/go', { - to: course.id, - redirect_to: result.url, - }); - } + if (this.getViewConfig('only_new') && !nav.important) { + return; + } - navigation[key] = result; - }); + let result = nav ? Object.assign({}, nav) : false; + if (nav) { + if (nav.important) { + result.class = 'my-courses-navigation-important'; + result.icon.role = 'attention'; + result.icon.shape = result.icon.shape.replace(/^new\//, ''); + } else { + result.class = false; + result.icon.role = 'clickable'; + } + + result.url = this.urlFor('dispatch.php/course/go', { + to: course.id, + redirect_to: result.url, + }); + } - return navigation; - }, - }, - - computed: { - ...mapState('mycourses', [ - 'courses', - 'groups', - 'userid', - 'config', - ]), - ...mapGetters('mycourses', [ - 'isGroupOpen', - 'getConfig', - ]), - - numberOfNavElements () { - return Math.max( - ...Object.values(this.courses).map(course => { - const navigation = this.getNavigationForCourse(course, true); - return Object.values(navigation).length; - }) - ); - } - }, + navigation[key] = result; + }); - created () { - this.responsiveDisplay = Responsive.isResponsive(); - Responsive.media_query.addListener(() => { - this.responsiveDisplay = Responsive.isResponsive(); - }) + return navigation; + }, + getViewConfig(key) { + return this.getConfig( + 'view_settings', + this.responsiveDisplay ? 'responsive' : 'regular', + key + ); + }, + isChild (course) { + return course.parent !== null && this.courses[course.parent] !== undefined; + }, + isParent (course) { + return course.children.length > 0 && course.children.every(childId => { + return this.courses[childId] !== undefined; + }); + }, + updateViewConfig(key, value) { + let config = this.getConfig('view_settings'); + config[this.responsiveDisplay ? 'responsive' : 'regular'][key] = value; + return this.updateConfigValue({ + key: 'view_settings', + value: config + }); + }, + }; } + + return result; } + +export default createMixin(); + +export { createMixin }; -- cgit v1.0