diff options
| author | Michaela Brückner <brueckner@data-quest.de> | 2025-06-26 09:51:32 +0200 |
|---|---|---|
| committer | David Siegfried <david.siegfried@uni-vechta.de> | 2025-06-26 07:51:32 +0000 |
| commit | d87fff87cd24cfcd7ad72a02af251ad890cf646e (patch) | |
| tree | 20dc6e62a02b0ca1396998bcb90871ffa3994b79 | |
| parent | f385c70d09166f6a41cc49922510daa1ec3402b4 (diff) | |
Resolve "Veranstaltungs-Stundenplan: Anzeigefilter für Terminkachel einbauen"
Closes #5585
Merge request studip/studip!4209
| -rw-r--r-- | app/controllers/admin/courseplanning.php | 58 | ||||
| -rw-r--r-- | db/migrations/6.1.2_add_timetable_filters.php | 64 | ||||
| -rw-r--r-- | lib/classes/InstituteCalendarHelper.php | 30 | ||||
| -rw-r--r-- | lib/models/CourseDate.php | 13 | ||||
| -rw-r--r-- | resources/assets/javascripts/lib/fullcalendar.js | 21 | ||||
| -rw-r--r-- | resources/vue/apps/CoursePlanningTileFilter.vue | 66 |
6 files changed, 243 insertions, 9 deletions
diff --git a/app/controllers/admin/courseplanning.php b/app/controllers/admin/courseplanning.php index 81e90a5..100088e 100644 --- a/app/controllers/admin/courseplanning.php +++ b/app/controllers/admin/courseplanning.php @@ -72,6 +72,12 @@ class Admin_CourseplanningController extends AuthenticatedController Icon::create('file-pdf') ); + Sidebar::get()->getWidget('actions')->addLink( + _('Ansichtsoptionen'), + $this->tilefilterURL('overview'), + Icon::create('admin') + )->asDialog('size=400x350'); + $this->courses = $this->getFilteredCourses(); $this->events = InstituteCalendarHelper::getEvents( $this->courses, @@ -128,6 +134,12 @@ class Admin_CourseplanningController extends AuthenticatedController Icon::create('admin') )->asDialog('size=auto'); + Sidebar::get()->getWidget('actions')->addLink( + _('Ansichtsoptionen'), + $this->tilefilterURL('weekday', $day_of_week), + Icon::create('admin') + )->asDialog('size=400x350'); + $this->cal_date = $cal_date; $this->courses = $this->getFilteredCourses(); $this->events = InstituteCalendarHelper::getEvents($this->courses, $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT, $this->semester, $day_of_week); @@ -887,4 +899,50 @@ class Admin_CourseplanningController extends AuthenticatedController } $this->redirect('admin/courseplanning/index'); } + + /** + * Sets filters for the title on the date tile + */ + public function tilefilter_action($view = null, $weekday = null) + { + PageLayout::setTitle(_('Angezeigte Veranstaltungsdaten')); + + $config = User::findCurrent()->getConfiguration(); + + $this->render_vue_app( + Studip\VueApp::create('CoursePlanningTileFilter') + ->withProps([ + 'view' => $view, + 'weekday' => $weekday, + 'config' => [ + 'course_number' => (bool) $config->getValue('TIMETABLE_COURSE_NUMBER_VISIBLE'), + 'course_name' => (bool) $config->getValue('TIMETABLE_COURSE_NAME_VISIBLE'), + 'lecturers' => (bool) $config->getValue('TIMETABLE_LECTURERS_VISIBLE'), + 'rooms' => (bool) $config->getValue('TIMETABLE_ROOMS_VISIBLE'), + ] + ]) + ); + + $this->view = $view; + $this->weekday = $weekday; + $this->config = UserConfig::get($GLOBALS['user']->id); + } + + public function store_tilefilter_action($view = null, $weekday = null) + { + CSRFProtection::verifyUnsafeRequest(); + + $GLOBALS['user']->cfg->store('TIMETABLE_COURSE_NUMBER_VISIBLE', Request::bool('course_number')); + $GLOBALS['user']->cfg->store('TIMETABLE_COURSE_NAME_VISIBLE', Request::bool('course_name')); + $GLOBALS['user']->cfg->store('TIMETABLE_LECTURERS_VISIBLE', Request::bool('lecturers')); + $GLOBALS['user']->cfg->store('TIMETABLE_ROOMS_VISIBLE', Request::bool('rooms')); + + if ($view === 'overview') { + $this->redirect('admin/courseplanning/index'); + } elseif ($view === 'weekday') { + $this->redirect('admin/courseplanning/weekday/' . $weekday); + } + + PageLayout::postSuccess(_('Ihre Einstellungen wurden gespeichert.')); + } } diff --git a/db/migrations/6.1.2_add_timetable_filters.php b/db/migrations/6.1.2_add_timetable_filters.php new file mode 100644 index 0000000..14869b5 --- /dev/null +++ b/db/migrations/6.1.2_add_timetable_filters.php @@ -0,0 +1,64 @@ +<?php + +final class AddTimetableFilters extends Migration +{ + public function description() + { + return 'Adds config fields for filtering content on timetable tiles'; + } + + + public function up() + { + $query = "INSERT IGNORE INTO `config` ( + `field`, `value`, `type`, `range`, `section`, + `mkdate`, `chdate`, `description` + ) VALUES ( + :field, :value, :type, 'user', '', + UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :description + )"; + $statement = DBManager::get()->prepare($query); + + $statement->execute([ + ':field' => 'TIMETABLE_COURSE_NUMBER_VISIBLE', + ':value' => 1, + ':type' => 'int', + ':description' => 'Soll die Veranstaltungs-Nummer auf dem Stundenplaneintrag sichtbar sein?', + ]); + $statement->execute([ + ':field' => 'TIMETABLE_COURSE_NAME_VISIBLE', + ':value' => 1, + ':type' => 'int', + ':description' => 'Soll der Veranstaltungs-Titel auf dem Stundenplaneintrag sichtbar sein?', + ]); + $statement->execute([ + ':field' => 'TIMETABLE_LECTURERS_VISIBLE', + ':value' => 0, + ':type' => 'boolean', + ':description' => 'Sollen die Dozenten einer Veranstaltung auf dem Stundenplaneintrag sichtbar sein?', + ]); + $statement->execute([ + ':field' => 'TIMETABLE_ROOMS_VISIBLE', + ':value' => 0, + ':type' => 'boolean', + ':description' => 'Soll der Raum einer Veranstaltung auf dem Stundenplaneintrag sichtbar sein?', + ]); + + } + + public function down() + { + DBManager::get()->exec(" + DELETE `config`, `config_values` + FROM `config` + LEFT JOIN `config_values` USING (`field`) + WHERE `config`.`field` IN ( + 'TIMETABLE_COURSE_NUMBER_VISIBLE', + 'TIMETABLE_COURSE_NAME_VISIBLE', + 'TIMETABLE_LECTURERS_VISIBLE', + 'TIMETABLE_ROOMS_VISIBLE' + ) + "); + + } +} diff --git a/lib/classes/InstituteCalendarHelper.php b/lib/classes/InstituteCalendarHelper.php index 343bc76..13677ea 100644 --- a/lib/classes/InstituteCalendarHelper.php +++ b/lib/classes/InstituteCalendarHelper.php @@ -436,10 +436,22 @@ class InstituteCalendarHelper } } + $next_single_date = CourseDate::getNextDateByMetadate($cycle_date->metadate_id); + if ($next_single_date) { + $room_name = $next_single_date->getRoomName() ?: _('ohne Raumangabe'); + } + + $fields = [ + 'course_number' => UserConfig::get($GLOBALS['user']->id)->TIMETABLE_COURSE_NUMBER_VISIBLE ? ($course->veranstaltungsnummer ?: _('(keine VA-Nummer)')) : null, + 'course_name' => UserConfig::get($GLOBALS['user']->id)->TIMETABLE_COURSE_NAME_VISIBLE ? $course->getFullName('name') : null, + 'lecturers' => UserConfig::get($GLOBALS['user']->id)->TIMETABLE_LECTURERS_VISIBLE ? self::getLecturers($course) : null, + 'room' => UserConfig::get($GLOBALS['user']->id)->TIMETABLE_ROOMS_VISIBLE ? $room_name : null + ]; + $events[] = [ 'resourceId' => $resource_column, 'id' => $cycle_date->id, - 'title' => $name, + 'title' => empty($fields) ? $name : '', 'start' => $start, 'end' => $end, 'textColor' => $textcolor, @@ -456,6 +468,8 @@ class InstituteCalendarHelper 'tooltip' => self::getCycleInfos($course, $cycle_date), 'icon' => $is_start_editable ? '' : 'lock-locked', 'conform' => $conform, + // custom props (event.extendedProps) + 'content_fields' => $fields, ]; } }, array_keys($courses)); @@ -702,6 +716,20 @@ class InstituteCalendarHelper return $info_string; } + private static function getLecturers(Course $course): array + { + $dozenten = []; + $lecturers = ''; + foreach (CourseMember::findByCourseAndStatus($course->id, 'dozent') as $cmember) { + $dozenten[$cmember->user->user_id] = $cmember->user->getFullName(); + } + if ($dozenten) { + $lecturers .= implode(', ', $dozenten) . "\n"; + } + + return $lecturers; + } + public static function getBackgroundEvents($start = null) { $datetime = new DateTime(); diff --git a/lib/models/CourseDate.php b/lib/models/CourseDate.php index 0fb9e08..038a120 100644 --- a/lib/models/CourseDate.php +++ b/lib/models/CourseDate.php @@ -740,5 +740,18 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event ); } + /** + * returns the next single date within a given cycle + */ + public static function getNextDateByMetadate($metadate_id): ?self + { + return self::findOneBySQL( + "`metadate_id` = :metadate_id + AND `date` >= UNIX_TIMESTAMP() + ORDER BY `date`", + ['metadate_id' => $metadate_id] + ); + } + //End of Event interface implementation. } diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js index f4e2826..350c72f 100644 --- a/resources/assets/javascripts/lib/fullcalendar.js +++ b/resources/assets/javascripts/lib/fullcalendar.js @@ -579,14 +579,19 @@ class Fullcalendar if ($(info.view.context.calendar.el).hasClass('institute-plan')) { $(eventElement).attr('title', event.extendedProps.tooltip); - $(eventElement).find('.fc-title').html( - $('<div>').css({ - width: 'calc(100% - 21px)', - height: '100%', - wordBreak: 'break-word' - }).text(eventElement.text) - ); - $(eventElement).find('.fc-title').append( + if (event.extendedProps.content_fields) { + for (const [css_class, field] of Object.entries(event.extendedProps.content_fields)) { + $(eventElement).find('.fc-content').append( + $('<div>').css({ + width: 'calc(100% - 21px)', + height: '100%', + wordBreak: 'break-word' + }).text(field) + .addClass(css_class + ' fc-title') + ); + } + } + $(eventElement).find('.fc-content').append( $('<button class="event-colorpicker">').addClass(iconColor) ); } else { diff --git a/resources/vue/apps/CoursePlanningTileFilter.vue b/resources/vue/apps/CoursePlanningTileFilter.vue new file mode 100644 index 0000000..0ac69b1 --- /dev/null +++ b/resources/vue/apps/CoursePlanningTileFilter.vue @@ -0,0 +1,66 @@ +<template> + <form method="post" :action="storeURL" class="default"> + <input type="hidden" :name="csrf.name" :value="csrf.value"> + <input v-for="(_, key) in items" + :key="`input-${key}`" + type="hidden" + :name="key" + :value="checkboxes[key] ? 1 : 0" + > + + <fieldset> + <legend>{{ $gettext('Angezeigte Veranstaltungsdaten') }}</legend> + + <label v-for="(label, key) in items" :key="key"> + <input :name="key" + type="checkbox" + v-model="checkboxes[key]" + :disabled="isDisabled(key)" + > + {{ label }} + </label> + </fieldset> + + <footer data-dialog-button> + <button type="submit" class="accept button"> + {{ $gettext('Speichern') }} + </button> + </footer> + </form> +</template> +<script setup lang="ts"> +import { computed, reactive, unref } from "vue"; +import { $gettext } from "../../assets/javascripts/lib/gettext"; + +type ValidField = 'course_number' | 'course_name' | 'lecturers' | 'rooms'; + +const props = defineProps({ + view: [String, null], + weekday: [String, null], + config: Object, +}); + +const checkboxes = reactive({...unref(props.config)}); +if (!checkboxes.course_number && !checkboxes.course_name) { + checkboxes.course_name = true; +} + +const csrf = computed(() => window.STUDIP.CSRF_TOKEN); + +const items: Record<ValidField, string> = { + course_number: $gettext('Veranstaltungsnummer'), + course_name: $gettext('Veranstaltungstitel'), + lecturers: $gettext('Lehrende'), + rooms: $gettext('Raum'), +}; + +const storeURL = window.STUDIP.URLHelper.getURL(`dispatch.php/admin/courseplanning/store_tilefilter/${props.view}/${props.weekday}`, {}, true); + +function isDisabled(f: string): boolean { + const field = f as ValidField; + + return (field === 'course_number' && !checkboxes.course_name) + || (field === 'course_name' && !checkboxes.course_number); + +} +</script> |
