From 2df806ab549ac7b7522fb3443e654d83b7095843 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Wed, 3 May 2023 10:00:38 +0000 Subject: [PATCH] complete separate regular and responsive view settings for my courses, fixes #2071 Closes #2071 Merge request studip/studip!1557 --- app/controllers/my_courses.php | 4 +- ...5.4.3_combine_my_courses_view_settings.php | 167 ++++++++++++++++++ .../ConfigValues/ConfigValuesUpdate.php | 6 +- .../Routes/ConfigValues/HelperTrait.php | 9 +- lib/models/ConfigValue.php | 10 ++ resources/vue/components/MyCourses.vue | 2 +- .../components/MyCoursesNewContentToggle.vue | 9 +- .../vue/components/MyCoursesSidebarSwitch.vue | 25 +-- resources/vue/mixins/MyCoursesMixin.js | 21 ++- resources/vue/store/MyCoursesStore.js | 23 +-- 10 files changed, 225 insertions(+), 51 deletions(-) create mode 100644 db/migrations/5.4.3_combine_my_courses_view_settings.php diff --git a/app/controllers/my_courses.php b/app/controllers/my_courses.php index c0dbeb3798a..996ad87a604 100644 --- a/app/controllers/my_courses.php +++ b/app/controllers/my_courses.php @@ -793,9 +793,7 @@ class MyCoursesController extends AuthenticatedController '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, - 'display_type' => $GLOBALS['user']->cfg->MY_COURSES_TILED_DISPLAY ? 'tiles' : 'tables', - 'responsive_type' => $GLOBALS['user']->cfg->MY_COURSES_TILED_DISPLAY_RESPONSIVE ? 'tiles' : 'tables', - 'navigation_show_only_new' => $GLOBALS['user']->cfg->MY_COURSES_SHOW_NEW_ICONS_ONLY, + 'view_settings' => $GLOBALS['user']->cfg->MY_COURSES_VIEW_SETTINGS, 'group_by' => $this->getGroupField(), ], ]; diff --git a/db/migrations/5.4.3_combine_my_courses_view_settings.php b/db/migrations/5.4.3_combine_my_courses_view_settings.php new file mode 100644 index 00000000000..f191a530d8a --- /dev/null +++ b/db/migrations/5.4.3_combine_my_courses_view_settings.php @@ -0,0 +1,167 @@ +<?php + +/** + * - MY_COURSES_TILED_DISPLAY + * - MY_COURSES_TILED_DISPLAY_RESPONSIVE + * - MY_COURSES_SHOW_NEW_ICONS_ONLY + */ +final class CombineMyCoursesViewSettings extends Migration +{ + const OLD_FIELDS = [ + 'MY_COURSES_SHOW_NEW_ICONS_ONLY' => false, + 'MY_COURSES_TILED_DISPLAY' => false, + 'MY_COURSES_TILED_DISPLAY_RESPONSIVE' => true, + ]; + + public function description() + { + return 'Combines the different view settings for my courses into a single configuration'; + } + + protected function up() + { + // Add new configuration + $query = "INSERT IGNORE INTO `config` ( + `field`, `value`, `type`, `range`, + `section`, `description`, + `mkdate`, `chdate` + ) VALUES ( + 'MY_COURSES_VIEW_SETTINGS', ?, 'array', 'user', + 'MeineVeranstaltungen', 'Konfiguration der Ansicht \"Meine Veranstaltungen\"', + UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + )"; + DBManager::get()->execute($query, [ + json_encode($this->convertOldConfig(['MY_COURSES_TILED_DISPLAY_RESPONSIVE' => true])) + ]); + + // Migrate old settings + $this->migrateOldConfigurations(); + + // Drop old configuration + $query = "DELETE `config`, `config_values` + FROM `config` + LEFT JOIN `config_values` USING (`field`) + WHERE `field` IN (?)"; + DBManager::get()->execute($query, [ + array_keys(self::OLD_FIELDS), + ]); + } + + protected function down() + { + // Restore old configuration + $query = "INSERT IGNORE INTO `config` ( + `field`, `value`, `type`, `range`, + `section`, `description`, + `mkdate`, `chdate` + ) VALUES ( + 'MY_COURSES_SHOW_NEW_ICONS_ONLY', 0, 'boolean', 'user', + 'MeineVeranstaltungen', 'Nur Icons für neue Inhalte sollen angezeigt werden', + UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + ), ( + 'MY_COURSES_TILED_DISPLAY', 0, 'boolean', 'user', + 'MeineVeranstaltungen', 'Hat die Kachelansicht unter \"Meine Veranstaltungen\" aktiviert', + UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + ), ( + 'MY_COURSES_TILED_DISPLAY_RESPONSIVE', 0, 'boolean', 'user', + 'MeineVeranstaltungen', 'Hat die Kachelansicht unter \"Meine Veranstaltungen\" aktiviert (responsiv)', + UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + )"; + DBManager::get()->exec($query); + + // Migrate new settings + $this->migrateNewConfigurations(); + + // Drop new configuration + $query = "DELETE `config`, `config_values` + FROM `config` + LEFT JOIN `config_values` USING (`field`) + WHERE `field` = 'MY_COURSES_VIEW_SETTINGS'"; + DBManager::get()->exec($query); + } + + private function migrateOldConfigurations(): void + { + $query = "SELECT `value` + FROM `config_values` + WHERE `range_id` = :user_id AND `field` = :field"; + $values_statement = DBManager::get()->prepare($query); + + $query = "INSERT IGNORE INTO `config_values` (`field`, `range_id`, `value`, `mkdate`, `chdate`) + VALUES ('MY_COURSES_VIEW_SETTINGS', :user_id, :value, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())"; + $insert_statement = DBManager::get()->prepare($query); + + $query = "SELECT DISTINCT `range_id` + FROM `config_values` + WHERE `field` IN (?)"; + $user_ids = DBManager::get()->fetchFirst($query, [ + array_keys(self::OLD_FIELDS), + ]); + foreach ($user_ids as $user_id) { + $values_statement->bindValue(':user_id', $user_id); + + $config = self::OLD_FIELDS; + foreach (array_keys(self::OLD_FIELDS) as $field) { + $values_statement->bindValue(':field', $field); + $values_statement->execute(); + + $config[$field] = $values_statement->fetchColumn(); + } + + $insert_statement->execute([ + ':user_id' => $user_id, + ':value' => json_encode($this->convertOldConfig($config)), + ]); + } + } + + private function convertOldConfig(array $config): array + { + return [ + 'regular' => [ + 'tiled' => (bool) $config['MY_COURSES_TILED_DISPLAY'], + 'only_new' => (bool) $config['MY_COURSES_SHOW_NEW_ICONS_ONLY'], + ], + 'responsive' => [ + 'tiled' => (bool) $config['MY_COURSES_TILED_DISPLAY_RESPONSIVE'], + 'only_new' => (bool) $config['MY_COURSES_SHOW_NEW_ICONS_ONLY'], + ], + ]; + } + + private function migrateNewConfigurations(): void + { + $query = "INSERT IGNORE INTO `config_values` (`field`, `range_id`, `value`, `mkdate`, `chdate`) + VALUES (:field, :user_id, :value, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())"; + $insert_statement = DBManager::get()->prepare($query); + + $query = "SELECT `range_id`, `value` + FROM `config_values` + WHERE `field` = 'MY_COURSES_VIEW_SETTINGS'"; + $statement = DBManager::get()->exec($query); + $statement->setFetchMode(PDO::FETCH_ASSOC); + foreach ($statement as $row) { + $config = json_decode($row['value'], true); + + $insert_statement->bindValue(':user_id', $row['user_id']); + + foreach ($this->convertNewConfig($config) as $field => $value) { + if ($value !== self::OLD_FIELDS[$field]) { + $insert_statement->bindValue(':field', $field); + $insert_statement->bindValue(':value', (int) $value); + $insert_statement->execute(); + } + } + } + } + + private function convertNewConfig(array $config): array + { + return [ + 'MY_COURSES_SHOW_NEW_ICONS_ONLY' => $config['regular']['only_new'] || $config['responsive']['only_new'], + 'MY_COURSES_TILED_DISPLAY' => $config['regular']['tiled'], + 'MY_COURSES_TILED_DISPLAY_RESPONSIVE' => $config['responsive']['tiled'], + ]; + } + +} diff --git a/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php b/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php index cf46134eb00..c4fda2f33a1 100644 --- a/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php +++ b/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php @@ -31,7 +31,11 @@ class ConfigValuesUpdate extends JsonApiController $resource = $this->findOrFakeConfigValue($range, $field); // TODO: zunächst kann diese Route nur Konfigurationseinstellungen vom Typ bool ändern - if ('boolean' !== $resource->entry['type'] && $resource->entry['field'] !== 'MY_COURSES_OPEN_GROUPS') { + if ( + 'boolean' !== $resource->entry['type'] + && $resource->entry['field'] !== 'MY_COURSES_OPEN_GROUPS' + && $resource->entry['field'] !== 'MY_COURSES_VIEW_SETTINGS' + ) { throw new NotImplementedException(); } diff --git a/lib/classes/JsonApi/Routes/ConfigValues/HelperTrait.php b/lib/classes/JsonApi/Routes/ConfigValues/HelperTrait.php index 3742fb5815d..eac1d379f3d 100644 --- a/lib/classes/JsonApi/Routes/ConfigValues/HelperTrait.php +++ b/lib/classes/JsonApi/Routes/ConfigValues/HelperTrait.php @@ -2,11 +2,12 @@ namespace JsonApi\Routes\ConfigValues; +use ConfigValue; use JsonApi\Errors\RecordNotFoundException; trait HelperTrait { - private function generateId(\ConfigValue $resource): string + private function generateId(ConfigValue $resource): string { return join('_', [$resource['range_id'], $resource['field']]); } @@ -29,10 +30,10 @@ trait HelperTrait return $range; } - private function findOrFakeConfigValue(?\Range $range, string $field) + private function findOrFakeConfigValue(?\Range $range, string $field): ConfigValue { // first search optimistically for this config value - if ($configValue = \ConfigValue::find([$field, $range->id])) { + if ($configValue = ConfigValue::find([$field, $range->id])) { return $configValue; } @@ -41,7 +42,7 @@ trait HelperTrait throw new RecordNotFoundException(); } - return \ConfigValue::build([ + return ConfigValue::build([ 'field' => $field, 'range_id' => $range->id, 'value' => $configEntry->value, diff --git a/lib/models/ConfigValue.php b/lib/models/ConfigValue.php index 4e27ec7843c..829567d4ea9 100644 --- a/lib/models/ConfigValue.php +++ b/lib/models/ConfigValue.php @@ -12,6 +12,16 @@ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * * @category Stud.IP + * + * @property array $id + * @property string $field + * @property string $range_id + * @property string $value + * @property int $mkdate + * @property int $chdate + * @property string $comment + * + * @property ConfigEntry $entry */ class ConfigValue extends SimpleORMap diff --git a/resources/vue/components/MyCourses.vue b/resources/vue/components/MyCourses.vue index 769d3152a4e..40480251c4b 100644 --- a/resources/vue/components/MyCourses.vue +++ b/resources/vue/components/MyCourses.vue @@ -55,7 +55,7 @@ export default { : MyCoursesTables; }, displayedType () { - return this.getConfig(this.viewConfig); + return this.getViewConfig('tiled') ? 'tiles' : 'table'; }, iconSize () { if (this.displayedType !== 'tiles' && !this.responsiveDisplay) { diff --git a/resources/vue/components/MyCoursesNewContentToggle.vue b/resources/vue/components/MyCoursesNewContentToggle.vue index 49bf7eda233..3ced26d950f 100644 --- a/resources/vue/components/MyCoursesNewContentToggle.vue +++ b/resources/vue/components/MyCoursesNewContentToggle.vue @@ -2,7 +2,7 @@ <ul class="widget-list widget-options"> <li> <a href="#" class="options-checkbox" :class="showNewContents ? 'options-checked' : 'options-unchecked'" @click.prevent="toggleNewContents"> - <translate>Nur neue Inhalte anzeigen</translate> + {{ $gettext('Nur neue Inhalte anzeigen') }} </a> </li> </ul> @@ -17,15 +17,12 @@ export default { mixins: [MyCoursesMixin], computed: { showNewContents () { - return this.getConfig('navigation_show_only_new'); + return this.getViewConfig('only_new'); }, }, methods: { toggleNewContents() { - this.updateConfigValue({ - key: 'navigation_show_only_new', - value: !this.getConfig('navigation_show_only_new'), - }).then(() => { + this.updateViewConfig('only_new', !this.showNewContents).then(() => { Sidebar.close(); }); }, diff --git a/resources/vue/components/MyCoursesSidebarSwitch.vue b/resources/vue/components/MyCoursesSidebarSwitch.vue index 5abc1fb7b6b..788c053a21c 100644 --- a/resources/vue/components/MyCoursesSidebarSwitch.vue +++ b/resources/vue/components/MyCoursesSidebarSwitch.vue @@ -1,13 +1,13 @@ <template> <ul class="widget-list widget-links sidebar-views"> <li :class="{ active: tableView }"> - <a href="#" @click.prevent="setTableView"> - <translate>Tabellarische Ansicht</translate> + <a href="#" @click.prevent="setTiledView(false)"> + {{ $gettext('Tabellarische Ansicht') }} </a> </li> <li :class="{ active: tilesView }"> - <a href="#" @click.prevent="setTilesView"> - <translate>Kachelansicht</translate> + <a href="#" @click.prevent="setTiledView(true)"> + {{ $gettext('Kachelansicht') }} </a> </li> </ul> @@ -22,24 +22,15 @@ export default { mixins: [MyCoursesMixin], computed: { tableView () { - return this.getConfig(this.viewConfig) === 'tables'; + return !this.getViewConfig('tiled'); }, tilesView () { - return this.getConfig(this.viewConfig) === 'tiles'; + return this.getViewConfig('tiled'); }, }, methods: { - setTableView () { - this.setView('tables'); - }, - setTilesView () { - this.setView('tiles'); - }, - setView (view) { - this.updateConfigValue({ - key: this.viewConfig, - value: view - }).then(() => { + setTiledView (state) { + this.updateViewConfig('tiled', state).then(() => { Sidebar.close(); }); } diff --git a/resources/vue/mixins/MyCoursesMixin.js b/resources/vue/mixins/MyCoursesMixin.js index 203340b5ebb..39e11e739d0 100644 --- a/resources/vue/mixins/MyCoursesMixin.js +++ b/resources/vue/mixins/MyCoursesMixin.js @@ -16,6 +16,22 @@ export default { '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 + }); + }, + getCourseName(course, include_number = false) { let name = course.name; if (include_number) { @@ -110,7 +126,7 @@ export default { return; } - if (this.getConfig('navigation_show_only_new') && !nav.important) { + if (this.getViewConfig('only_new') && !nav.important) { return; } @@ -150,9 +166,6 @@ export default { 'getConfig', ]), - viewConfig () { - return this.responsiveDisplay ? 'responsive_type' : 'display_type'; - }, numberOfNavElements () { return Math.max( ...Object.values(this.courses).map(course => { diff --git a/resources/vue/store/MyCoursesStore.js b/resources/vue/store/MyCoursesStore.js index a0900404e44..08c03891bef 100644 --- a/resources/vue/store/MyCoursesStore.js +++ b/resources/vue/store/MyCoursesStore.js @@ -1,17 +1,7 @@ const configMapping = { - display_type: value => { + view_settings: value => { return { - MY_COURSES_TILED_DISPLAY: value === 'tiles', - } - }, - responsive_type: value => { - return { - MY_COURSES_TILED_DISPLAY_RESPONSIVE: value === 'tiles', - } - }, - navigation_show_only_new: value => { - return { - MY_COURSES_SHOW_NEW_ICONS_ONLY: value, + MY_COURSES_VIEW_SETTINGS: value }; }, open_groups: value => { @@ -19,7 +9,6 @@ const configMapping = { MY_COURSES_OPEN_GROUPS: value, }; }, - }; export default { @@ -39,8 +28,12 @@ export default { } return state.config.open_groups.includes(group.id); }, - getConfig: (state) => (key) => { - return state.config[key]; + getConfig: (state) => (...keys) => { + let config = state.config; + for (const key of keys) { + config = config[key]; + } + return config; }, }, -- GitLab