diff --git a/app/controllers/my_courses.php b/app/controllers/my_courses.php index c0dbeb3798a373aee5ec07d76a900954b1a78a91..996ad87a60440556ef686db226388637240fc1ed 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 0000000000000000000000000000000000000000..f191a530d8ab6af2f48af2ab7dbce619689412b5 --- /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 cf46134eb00085ba414bf6f51ae460bffbf331a2..c4fda2f33a1d456d290a26f49f2d633a93865225 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 3742fb5815d3059a27b5c17bdac90c6b4c315878..eac1d379f3daa393613009b638ecf1fd4a51301b 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 4e27ec7843c254ed72ca1a3f0e4c9bd4b75d90e7..829567d4ea94bf17e2d3e55fb1093cf064d61bd7 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 769d3152a4efafb6e3cf92772d9b1297c11bae84..40480251c4b2867d43430721446c5a27dde9382d 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 49bf7eda233652a59c2c74b7545d1d7f6b046dc1..3ced26d950f01878744a09f408b3fddda453d818 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 5abc1fb7b6b067a44b55c36a4b4c5c9a4915a829..788c053a21cec09ac29e945c43f36ff787afb391 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 203340b5ebbc9e5311363ce6de5a287d49ad07dc..39e11e739d02b73d9ffbca84d1cb6d3c3011fce7 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 a0900404e44c289626f93590a0a9b64696a016ca..08c03891bef1afb3e1139092aa5b5675e79ef36d 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; }, },