From 1728bc517691b3d0dde5ccdf0c0631ff1b855b6d Mon Sep 17 00:00:00 2001 From: Marcus Eibrink-Lunzenauer Date: Tue, 23 Jan 2024 07:41:52 +0000 Subject: Courseware Aufgaben erweitern (StEP3286) Merge request studip/studip!2445 --- app/controllers/course/courseware.php | 11 +- .../5.5.12_add_dates_to_cw_task_groups.php | 35 + db/migrations/5.5.12_new_external_pages.php | 578 ----------------- db/migrations/5.5.24_new_external_pages.php | 578 +++++++++++++++++ lib/classes/JsonApi/RouteMap.php | 7 + .../JsonApi/Routes/Courseware/Authority.php | 33 +- .../Routes/Courseware/Rel/SolversOfTaskGroup.php | 207 ++++++ .../JsonApi/Routes/Courseware/TaskGroupsCreate.php | 25 +- .../JsonApi/Routes/Courseware/TaskGroupsDelete.php | 38 ++ .../JsonApi/Routes/Courseware/TaskGroupsUpdate.php | 99 +++ .../JsonApi/Routes/Courseware/TasksIndex.php | 7 +- .../JsonApi/Routes/Courseware/TasksUpdate.php | 65 +- lib/classes/JsonApi/SchemaMap.php | 10 +- lib/classes/JsonApi/Schemas/Courseware/Task.php | 2 + .../JsonApi/Schemas/Courseware/TaskGroup.php | 3 + lib/models/Courseware/Task.php | 52 +- lib/models/Courseware/TaskGroup.php | 76 ++- lib/models/Statusgruppen.php | 13 + .../assets/javascripts/bootstrap/application.js | 2 +- .../assets/javascripts/bootstrap/consultations.js | 2 +- .../assets/javascripts/bootstrap/copyable_links.js | 2 +- .../assets/javascripts/bootstrap/data_secure.js | 2 +- resources/assets/javascripts/bootstrap/forms.js | 2 +- .../assets/javascripts/bootstrap/multi_select.js | 2 +- .../assets/javascripts/bootstrap/mvv_difflog.js | 2 +- resources/assets/javascripts/bootstrap/raumzeit.js | 2 +- .../assets/javascripts/bootstrap/resources.js | 2 +- .../bootstrap/studip_helper_attributes.js | 2 +- resources/assets/javascripts/chunks/tablesorter.js | 2 +- resources/assets/javascripts/chunks/vue.js | 2 +- .../cke/studip-a11y-dialog/a11y-dialog.js | 2 +- .../javascripts/cke/studip-a11y-dialog/ui.js | 2 +- .../cke/studip-quote/StudipBlockQuote.js | 2 +- .../assets/javascripts/cke/wiki-link/formview.js | 2 +- resources/assets/javascripts/cke/wiki-link/ui.js | 2 +- resources/assets/javascripts/init.js | 2 +- resources/assets/javascripts/jquery-bundle.js | 4 +- resources/assets/javascripts/lib/admission.js | 2 +- .../assets/javascripts/lib/big_image_handler.js | 2 +- resources/assets/javascripts/lib/calendar.js | 2 +- resources/assets/javascripts/lib/dialog.js | 2 +- resources/assets/javascripts/lib/files.js | 2 +- resources/assets/javascripts/lib/folders.js | 2 +- resources/assets/javascripts/lib/forum.js | 2 +- resources/assets/javascripts/lib/gettext.js | 95 --- resources/assets/javascripts/lib/gettext.ts | 114 ++++ resources/assets/javascripts/lib/instschedule.js | 2 +- resources/assets/javascripts/lib/jsupdater.js | 2 +- resources/assets/javascripts/lib/lightbox.js | 2 +- resources/assets/javascripts/lib/messages.js | 2 +- .../assets/javascripts/lib/multi_person_search.js | 2 +- resources/assets/javascripts/lib/multi_select.js | 2 +- resources/assets/javascripts/lib/oer.js | 2 +- resources/assets/javascripts/lib/overlapping.js | 4 +- resources/assets/javascripts/lib/overlay.js | 2 +- resources/assets/javascripts/lib/qr_code.js | 2 +- resources/assets/javascripts/lib/questionnaire.js | 2 +- resources/assets/javascripts/lib/quick_search.js | 2 +- resources/assets/javascripts/lib/raumzeit.js | 2 +- resources/assets/javascripts/lib/register.js | 2 +- resources/assets/javascripts/lib/resources.js | 2 +- resources/assets/javascripts/lib/schedule.js | 2 +- resources/assets/javascripts/lib/tour.js | 2 +- resources/assets/javascripts/lib/user_filter.js | 2 +- resources/assets/javascripts/mvv.js | 2 +- .../studip-jquery.multi-select.tweaks.js | 2 +- resources/assets/javascripts/studip-ui.js | 2 +- resources/vue-gettext.d.ts | 17 + resources/vue/components/StudipDate.vue | 27 + .../courseware/CoursewareDashboardStudents.vue | 481 -------------- .../courseware/CoursewareDashboardTasks.vue | 264 -------- .../courseware/CoursewareTasksDialogDistribute.vue | 682 -------------------- resources/vue/components/courseware/TasksApp.vue | 34 - .../layouts/CoursewareCollapsibleBox.vue | 3 +- .../courseware/tasks/AddFeedbackDialog.vue | 48 ++ .../tasks/CoursewareDashboardStudents.vue | 222 +++++++ .../courseware/tasks/CoursewareDashboardTasks.vue | 264 ++++++++ .../tasks/CoursewareTasksDialogDistribute.vue | 709 +++++++++++++++++++++ .../courseware/tasks/EditFeedbackDialog.vue | 60 ++ .../courseware/tasks/PagesTaskGroupsIndex.vue | 34 + .../courseware/tasks/PagesTaskGroupsShow.vue | 224 +++++++ .../components/courseware/tasks/RenewalDialog.vue | 79 +++ .../vue/components/courseware/tasks/TaskGroup.vue | 84 +++ .../courseware/tasks/TaskGroupTaskItem.vue | 118 ++++ .../tasks/TaskGroupsAddSolversDialog.vue | 224 +++++++ .../courseware/tasks/TaskGroupsDeleteDialog.vue | 33 + .../tasks/TaskGroupsModifyDeadlineDialog.vue | 117 ++++ .../courseware/tasks/task-groups-helper.js | 31 + .../widgets/CoursewareTasksActionWidget.vue | 49 +- resources/vue/components/stock-images/colors.js | 2 +- resources/vue/components/stock-images/filters.js | 2 +- resources/vue/courseware-index-app.js | 2 + resources/vue/courseware-tasks-app.js | 50 +- resources/vue/mixins/courseware/task-helper.js | 6 +- resources/vue/store/AdminCoursesStore.js | 2 +- .../store/courseware/courseware-tasks.module.js | 75 ++- .../vue/store/courseware/courseware.module.js | 8 +- tsconfig.json | 8 +- webpack.common.js | 1 + webpack.dev.js | 8 +- 100 files changed, 3823 insertions(+), 2291 deletions(-) create mode 100644 db/migrations/5.5.12_add_dates_to_cw_task_groups.php delete mode 100644 db/migrations/5.5.12_new_external_pages.php create mode 100644 db/migrations/5.5.24_new_external_pages.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/Rel/SolversOfTaskGroup.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/TaskGroupsDelete.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/TaskGroupsUpdate.php delete mode 100644 resources/assets/javascripts/lib/gettext.js create mode 100644 resources/assets/javascripts/lib/gettext.ts create mode 100644 resources/vue-gettext.d.ts create mode 100644 resources/vue/components/StudipDate.vue delete mode 100644 resources/vue/components/courseware/CoursewareDashboardStudents.vue delete mode 100644 resources/vue/components/courseware/CoursewareDashboardTasks.vue delete mode 100644 resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue delete mode 100644 resources/vue/components/courseware/TasksApp.vue create mode 100644 resources/vue/components/courseware/tasks/AddFeedbackDialog.vue create mode 100644 resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue create mode 100644 resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue create mode 100644 resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue create mode 100644 resources/vue/components/courseware/tasks/EditFeedbackDialog.vue create mode 100644 resources/vue/components/courseware/tasks/PagesTaskGroupsIndex.vue create mode 100644 resources/vue/components/courseware/tasks/PagesTaskGroupsShow.vue create mode 100644 resources/vue/components/courseware/tasks/RenewalDialog.vue create mode 100644 resources/vue/components/courseware/tasks/TaskGroup.vue create mode 100644 resources/vue/components/courseware/tasks/TaskGroupTaskItem.vue create mode 100644 resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue create mode 100644 resources/vue/components/courseware/tasks/TaskGroupsDeleteDialog.vue create mode 100644 resources/vue/components/courseware/tasks/TaskGroupsModifyDeadlineDialog.vue create mode 100644 resources/vue/components/courseware/tasks/task-groups-helper.js diff --git a/app/controllers/course/courseware.php b/app/controllers/course/courseware.php index af7d0e9..c1421f4 100644 --- a/app/controllers/course/courseware.php +++ b/app/controllers/course/courseware.php @@ -79,11 +79,16 @@ class Course_CoursewareController extends CoursewareController } } - public function tasks_action(): void + public function tasks_action($route = null): void { - global $perm, $user; - $this->is_teacher = $perm->have_studip_perm('tutor', Context::getId(), $user->id); + $this->is_teacher = $GLOBALS['perm']->have_studip_perm( + 'tutor', + Context::getId(), + $GLOBALS['user']->id + ); + Navigation::activateItem('course/courseware/tasks'); + PageLayout::setTitle(_('Courseware: Aufgaben')); $this->setTasksSidebar(); } diff --git a/db/migrations/5.5.12_add_dates_to_cw_task_groups.php b/db/migrations/5.5.12_add_dates_to_cw_task_groups.php new file mode 100644 index 0000000..aba5ea9 --- /dev/null +++ b/db/migrations/5.5.12_add_dates_to_cw_task_groups.php @@ -0,0 +1,35 @@ +exec( + "ALTER TABLE `cw_task_groups` + ADD `start_date` INT NOT NULL AFTER `title`, + ADD `end_date` INT NOT NULL AFTER `start_date`" + ); + $dbm->exec('UPDATE `cw_task_groups` SET `start_date`=`mkdate`'); + $dbm->exec( + 'UPDATE `cw_task_groups` AS tg SET tg.`end_date` = ( SELECT MAX(t.`submission_date`) FROM `cw_tasks` t WHERE t.`task_group_id` = tg.`id` )' + ); + $dbm->exec('ALTER TABLE `cw_tasks` DROP `submission_date`'); + } + + public function down() + { + $dbm = \DBManager::get(); + $dbm->exec("ALTER TABLE `cw_tasks` ADD `submission_date` int(11) NOT NULL AFTER `solver_type`"); + $dbm->exec('UPDATE `cw_tasks` AS t INNER JOIN cw_task_groups tg ON t.`task_group_id` = tg.`id` SET t.`submission_date` = tg.`end_date`'); + $dbm->exec( + 'ALTER TABLE `cw_task_groups` + DROP `start_date`, + DROP `end_date`' + ); + } +} diff --git a/db/migrations/5.5.12_new_external_pages.php b/db/migrations/5.5.12_new_external_pages.php deleted file mode 100644 index dec973f..0000000 --- a/db/migrations/5.5.12_new_external_pages.php +++ /dev/null @@ -1,578 +0,0 @@ -exec("CREATE TABLE `extern_pages_configs` ( - `config_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `range_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `type` varchar(50) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `description` tinytext COLLATE utf8mb4_unicode_ci NOT NULL, - `conf` text COLLATE utf8mb4_unicode_ci NOT NULL, - `template` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, - `author_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `editor_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, - `mkdate` int(11) UNSIGNED NOT NULL DEFAULT '0', - `chdate` int(11) UNSIGNED NOT NULL DEFAULT '0', - PRIMARY KEY (`config_id`), - KEY `range_id` (`range_id`), - KEY `type` (`type`)) - "); - - $this->migrate_configurations(); - - $description = 'Allgemeine Fehlermeldung,die auf der Webseite ausgegeben wird, auf der der Inhalt der ' - . 'externe Seite angezeigt werden soll. Diese Meldung wird ausgegeben, ' - . 'wenn z.B. das Template fehlerhaft ist.'; - $message = 'Ein Fehler ist aufgetreten. Die Inhalte können nicht angezeigt werden.'; - DBManager::get()->exec( - "INSERT IGNORE INTO `config` - (`field`, `value`, `type`, `range`, - `section`, - `mkdate`, `chdate`, - `description`) - VALUES - ('EXTERN_PAGES_ERROR_MESSAGE', '{$message}', 'string', 'global', - 'external_pages', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), - '{$description}')" - ); - - $db->exec( - "DELETE FROM `config` WHERE `field` = 'EXTERN_ALLOW_ACCESS_WITHOUT_CONFIG'" - ); - $db->exec( - "DELETE FROM `config` WHERE `field` = 'EXTERN_SRI_ENABLE'" - ); - $db->exec( - "DELETE FROM `config` WHERE `field` = 'EXTERN_SRI_ENABLE_BY_ROOT'" - ); - $db->exec("DROP TABLE `extern_config`"); - - } - - public function down() - { - $db = \DBManager::get(); - - $db->exec( - "DELETE FROM `config` WHERE `config_id` = 'EXTERN_PAGES_ERROR_MESSAGE'" - ); - $db->exec('DROP TABLE IF EXISTS `extern_pages_configs`'); - - $db->exec("CREATE TABLE `extern_config` ( - `config_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL DEFAULT '', - `range_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL DEFAULT '', - `config_type` int(4) NOT NULL DEFAULT '0', - `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `is_standard` tinyint(1) UNSIGNED NOT NULL DEFAULT '0', - `config` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, - `mkdate` int(11) UNSIGNED NOT NULL DEFAULT '0', - `chdate` int(11) UNSIGNED NOT NULL DEFAULT '0', - PRIMARY KEY (`config_id`,`range_id`)) - "); - - $configs = [ - 'EXTERN_ALLOW_ACCESS_WITHOUT_CONFIG' => - 'Free access to external pages (without the need of a configuration), independent of SRI settings above', - 'EXTERN_SRI_ENABLE' => - 'Allow the usage of SRI-interface (Stud.IP Remote Include)', - 'EXTERN_SRI_ENABLE_BY_ROOT' => - 'Only root allows the usage of SRI-interface for specific institutes' - ]; - foreach ($configs as $field => $description) { - $db->exec( - "INSERT IGNORE INTO `config` - (`field`, `value`, `type`, `range`, - `section`, - `mkdate`, `chdate`, - `description`) - VALUES - ({$field}, '0', 'boolean', 'global', - 'global', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), - '{$description}')" - ); - } - - } - - private function migrate_configurations() - { - $skip_text = 'Die Konfiguration (ID: %s, TYP: %s, NAME: %s) kann nicht migriert werden. '; - $db = DBManager::get(); - $db->fetchAll('SELECT * FROM `extern_config`', [], - function ($config) use ($skip_text) - { - // skip non template based configs - if ($config['config_type'] < 9) { - $this->announce($skip_text . 'Die Seite ist nicht Template-basiert.', - $config['config_id'], $config['config_type'], $config['name']); - return; - } - $new_conf = new ExternPageConfig($config['config_id']); - $new_conf->range_id = $config['range_id']; - $new_conf->name = $config['name']; - $new_conf->mkdate = $config['mkdate']; - $new_conf->chdate = time(); - $old_data = json_decode($config['config']); - $new_data = [ - 'language' => 'de_DE', - 'escaping' => 'htmlReady', - // this is used to show a hint that this configuration has still to be fixed after the migration - 'not_fixed_after_migration' => 1 - ]; - if (isset($old_data->Main->language) && strlen((string) $old_data->Main->language) < 5) { - $new_data['language'] = $old_data->Main->language; - } - switch ($config['config_type']) { - // Persons - case 9: - $new_conf->type = 'Persons'; - // The new sort fields are different, so match as good as possible. - // Use "nachname" as default. - if (isset($old_data->Main->sort)) { - asort($old_data->Main->sort, SORT_NUMERIC); - if (key($old_data->Main->sort) === '3') { - $new_data['sort'] = 'email'; - } else { - $new_data['sort'] = 'nachname'; - } - } - if (isset($old_data->Main->grouping) && $old_data->Main->grouping === '1') { - $new_data['grouping'] = true; - } - $new_data['groupsvisible'] = []; - if (isset($old_data->Main->groupsvisible)) { - foreach ($old_data->Main->groupsvisible as $groupsvisible) { - $new_data['groupsvisible'][$groupsvisible] = 1; - } - } - $new_data['groupsalias'] = []; - if (isset($old_data->Main->groupsalias)) { - $new_data['groupsalias'] = $old_data->Main->groupsalias; - } - $replace = [ - 'PERSONS' => 'GROUPS', - 'PERSON' => 'PERSONS', - 'GROUP' => '' - ]; - $template = $this->replaceSubparts( - $replace, - $this->replaceDatafieldMarkers($old_data->TemplateGeneric) - ); - $replace = [ - 'IMAGE-URL-NORMAL' => 'IMAGE_URL_NORMAL', - 'IMAGE-URL-MEDIUM' => 'IMAGE_URL_MEDIUM', - 'IMAGE-URL-SMALL' => 'IMAGE_URL_SMALL', - 'HOMEPAGE-HREF' => 'HOMEPAGE_URL' - ]; - $template = $this->replaceMarkers($replace, $template); - $replace = [ - 'PERSONS' => '' - ]; - $new_conf->template = $this->replaceAllOldMarkers( - $this->replaceNoMarkerSubparts($replace, $template) - ); - break; - // Download - case 10: - $new_conf->type = 'Download'; - $sort_fields = [ - 'filetype', - 'name', - '', - 'mkdate', - 'size', - 'author' - ]; - if (isset($old_data->Main->sort)) { - arsort($old_data->Main->sort, SORT_NUMERIC); - $new_data['sort'] = $sort_fields[key($old_data->Main->sort)]; - } - $new_data['subfolders'] = 1; - $template = $this->replaceDatafieldMarkers($old_data->TemplateGeneric); - $replace = [ - 'FILE_NAME' => 'NAME', - 'FILE_FILE-NAME' => 'NAME', - 'FILE_SIZE' => 'SIZE', - 'FILE_DESCRIPTION' => 'DESCRIPTION', - 'FILE_UPLOAD-DATE' => 'UPLOAD_DATE', - 'FILE_HREF' => 'URL', - 'FILE_ICON-HREF' => 'ICON_URL' - ]; - $template = $this->replaceMarkers($replace, $template); - $replace = [ - 'DOWNLOAD' => '', - 'FILE' => '', - 'FILES' => 'FILES' - ]; - $template = $this->replaceSubparts($replace, $template); - $replace = [ - 'FILES' => '' - ]; - $new_conf->template = $this->replaceAllOldMarkers( - $this->replaceNoMarkerSubparts($replace, $template) - ); - break; - // news - case 11: - $this->announce($skip_text . - 'Die Ausgabe von Ankündigungen an Einrichtungen wird nicht mehr unterstützt.', - $config['config_id'], $config['config_type'], $config['name']); - return; - // courses - case 12: - $new_conf->type = 'Courses'; - $new_data['groupby'] = $old_data->Main->grouping; - $new_data['startsem'] = $this->convertSemesters($old_data->Main->semstart); - $new_data['semcount'] = $old_data->Main->semrange; - $new_data['semswitch'] = $old_data->Main->semswitch; - $new_data['participating'] = $old_data->Main->allseminars; - $new_data['semtypes'] = (array) $old_data->ReplaceTextSemType->visibility; - $new_data['studyareas'] = (array) $old_data->SelectSubjectAreas->subjectareasselected; - $replace = [ - 'START_SEMESTER' => 'START_SEMESTER_NAME', - 'LECTURES-SUBSTITUTE-GROUPED-BY' => 'GROUPED_BY', - 'GROUP' => 'NAME', - 'SEMTYPE' => 'SEMTYPE_NAME', - 'UNAME' => 'USERNAME' - ]; - $template = $this->replaceMarkers( - $replace, - $this->replaceDatafieldMarkers($old_data->TemplateGeneric) - ); - $replace = [ - 'LECTURES' => '', - 'GROUP' => 'GROUPED_COURSES', - 'LECTURE' => 'COURSES', - 'LECTURERS' => 'LECTURERS' - ]; - $template = $this->replaceSubparts($replace, $template); - $replace = [ - 'LECTURES' => 'COURSES' - ]; - $new_conf->template = $this->replaceAllOldMarkers( - $this->replaceNoMarkerSubparts($replace, $template) - ); - break; - // course details - case 13: - $new_conf->type = 'CourseDetails'; - $new_data['rangepathlevel'] = $old_data->Main->rangepathlevel; - // combine templates - $pattern = [ - '/(\{%\s+|###)NEWS(###|\s+%})/', - '/(\{%\s+|###)STUDIP-DATA(###|\s+%})/' - ]; - $replacement = [ - $old_data->TemplateNews->template, - $old_data->TemplateStudipData->template - ]; - $template = preg_replace( - $pattern, - $replacement, - $this->replaceDatafieldMarkers($old_data->TemplateLectureData) - ); - $replace = [ - 'MISC' => 'MISCELLANEOUS', - 'LEISTUNGSNACHWEIS' => 'CERTIFICATE', - 'UNAME' => 'USERNAME', - 'TUTOR_FULLNAME' => 'FULLNAME', - 'TUTOR_LASTNAME' => 'LASTNAME', - 'TUTOR_FIRSTNAME' => 'FIRSTNAME', - 'TUTOR_TITLEFRONT' => 'TITLEFRONT', - 'TUTOR_TITLEREAR' => 'TITLEREAR', - 'TUTOR_UNAME' => 'USERNAME', - 'NEWS_TOPIC' => 'TOPIC', - 'NEWS_BODY' => 'BODY', - 'NEWS_DATE' => 'DATE', - 'PRELIM-DISCUSSION' => 'PRELIM_DISCUSSION', - 'FIRST-MEETING' => 'FIRST_MEETING', - 'HOME-INST-NAME' => 'HOME_INST_NAME', - 'COUNT-USER' => 'COUNT_USER', - 'INVOLVED-INSTITUTE_NAME' => 'NAME' - ]; - $template = $this->replaceMarkers($replace, $template); - $replace = [ - 'LECTUREDETAILS' => '', - 'LECTURER' => '', - 'LECTURERS' => 'LECTURERS', - 'TUTOR' => '', - 'TUTORS' => 'TUTORS', - 'NEWS' => '', - 'ALL-NEWS' => 'NEWS', - 'SINGLE-NEWS' => '', - 'STUDIP-DATA' => '', - 'RANGE-PATHES' => 'RANGE_PATHS', - 'RANGE-PATH' => '', - 'MODULES' => 'MODULES', - 'MODULE' => '', - 'INVOLVED-INSTITUTES' => 'INVOLVED_INSTITUTES', - 'INVOLVED-INSTITUTE' => '', - ]; - $template = $this->replaceSubparts($replace, $template); - $replace = [ - 'NEWS' => '' - ]; - $new_conf->template = $this->replaceAllOldMarkers( - $this->replaceNoMarkerSubparts($replace, $template) - ); - break; - // person details - case 14: - $new_conf->type = 'PersonDetails'; - $new_data['defaultaddr'] = $old_data->Main->defaultaddr; - $new_data['startsem'] = $this->convertSemesters($old_data->PersondetailsLectures->semstart); - $new_data['semcount'] = $old_data->PersondetailsLectures->semrange; - $new_data['semswitch'] = $old_data->PersondetailsLectures->semswitch; - $new_data['semclass'] = $old_data->PersondetailsLectures->semclass; - // combine templates - $pattern = [ - '/(\{%\s+|###)LECTURES(###|\s+%})/', - '/(\{%\s+|###)NEWS(###|\s+%})/', - '/(\{%\s+|###)APPOINTMENTS(###|\s+%})/', - '/(\{%\s+|###)OWNCATEGORIES(###|\s+%})/' - ]; - $replacement = [ - $old_data->TemplateLectures->template, - $old_data->TemplateNews->template, - $old_data->TemplateAppointments->template, - $old_data->TemplateOwnCategories->template - ]; - $template = preg_replace( - $pattern, - $replacement, - $this->replaceDatafieldMarkers($old_data->TemplateMain) - ); - $replace = [ - 'IMAGE-HREF' => 'IMAGE_URL_NORMAL', - 'INST-NAME' => 'NAME', - 'SINGLE-INST-NAME' => 'NAME', - 'SINGLE-INST-STREET' => 'STREET', - 'SINGLE-INST-ZIPCODE' => 'ZIPCODE', - 'SINGLE-INST-EMAIL' => 'EMAIL', - 'SINGLE-INST-ROOM' => 'MEMBER_ROOM', - 'SINGLE-INST-PHONE' => 'MEMBER_PHONE', - 'SINGLE-INST-FAX' => 'MEMBER_FAX', - 'SINGLE-INST-HREF' => 'HOMEPAGE', - 'SINGLE-INST-OFFICE-HOURS' => 'MEMBER_OFFICEHOURS', - 'NEWS_TOPIC' => 'TOPIC', - 'NEWS_BODY' => 'BODY', - 'NEWS_DATE' => 'DATE', - 'LIST-START' => 'APPOINTMENTS_START', - 'LIST-END' => 'APPOINTMENTS_END', - 'REPETITION' => 'RECURRENCE', - 'BEGIN' => 'START', - 'OWNCATEGORY_TITLE' => 'TITLE', - 'OWNCATEGORY_CONTENT' => 'CONTENT', - 'OFFICE-HOURS' => 'OFFICEHOURS', - 'HOMEPAGE-HREF' => 'HOMEPAGE_URL' - ]; - $template = $this->replaceMarkers($replace, $template); - $replace = [ - 'PERSONDETAILS' => '', - 'SINGLE-INST' => '', - 'SEMESTER' => '', - 'NEWS' => '', - 'SINGLE-NEWS' => '', - 'APPOINTMENTS' => '', - 'SINGLE-APPOINTMENT' => '', - 'ALL-INST' => 'INSTITUTES', - 'LECTURES' => 'SEMESTERS', - 'LECTURE' => 'LECTURES', - 'ALL-NEWS' => 'NEWS', - 'ALL-APPOINTMENTS' => 'APPOINTMENTS', - 'OWNCATEGORIES' => 'OWNCATEGORIES', - 'OWNCATEGORY' => '' - ]; - $template = $this->replaceSubparts($replace, $template); - $replace = [ - 'NEWS' => '', - 'APPOINTMENTS' => '' - ]; - $new_conf->template = $this->replaceAllOldMarkers( - $this->replaceNoMarkerSubparts($replace, $template) - ); - break; - // sembrowser - case 15: - $this->announce($skip_text . - 'Der Seminarbrowser wird nicht mehr unterstützt.', - $config['config_id'], $config['config_type'], $config['name']); - return; - // persbrowser - case 16: - $new_conf->type = 'PersBrowse'; - $new_data['instperms'] = $old_data->Main->instperms; - $new_data['onlylecturers'] = $old_data->Main->onlylecturers; - $new_data['institutes'] = $old_data->SelectInstitutes->institutesselected; - // combine templates - $pattern = [ - '/(\{%\s+|###)LISTCHARACTERS(###|\s+%})/', - '/(\{%\s+|###)LISTINSTITUTES(###|\s+%})/', - '/(\{%\s+|###)LISTPERSONS(###|\s+%})/' - ]; - $replacement = [ - $old_data->TemplateListCharacters->template, - $old_data->TemplateListInstitutes->template, - $old_data->TemplateListPersons->template - ]; - $template = preg_replace( - $pattern, - $replacement, - $old_data->TemplateMain->template - ); - $new_data['language'] = $old_data->Main->language; - $replace = [ - 'CHARACTER_COUNT_USER' => 'CHARACTER_COUNT', - 'INSTITUTE_NAME' => 'NAME', - 'INSTITUTE_COUNT_USER' => 'COUNT_USERS' - ]; - $template = $this->replaceMarkers($replace, $template); - - $replace = [ - 'PERS_BROWSER' => '', - 'CHARACTER' => '', - 'LIST_CHARACTERS' => 'CHARACTERS', - 'INSTITUTE' => '', - 'LIST_INSTITUTES' => 'INSTITUTES', - 'LIST_PERSONS' => '', - 'PERSON' => '', - 'PERSONS' => 'PERSONS' - ]; - $template = $this->replaceSubparts($replace, $template); - - $replace = [ - 'PERSONS' => '' - ]; - $new_conf->template = $this->replaceAllOldMarkers( - $this->replaceNoMarkerSubparts($replace, $template) - ); - } - $new_conf->conf = json_encode($new_data); - $new_conf->store(); - }); - } - - private function replaceMarkers(array $marker_replacements, $template) - { - foreach ($marker_replacements as $old_marker => $new_marker) { - $patterns = [ - '/###' . $old_marker . '###/', - '/\{%(\s.*)' . $old_marker . '(.*\s)%}/' - ]; - $replacements = [ - '###' . $new_marker . '###', - '{%${1}' . $new_marker . '${2}%}' - ]; - $template = preg_replace($patterns, $replacements, $template); - }; - return $template; - } - - private function replaceSubparts(array $marker_replacements, $template) - { - foreach ($marker_replacements as $old_marker => $new_marker) { - $patterns = [ - '//', - '/\{%\s+foreach.+' . $old_marker . '(\s.+)%}/', - '//' - ]; - if ($new_marker === '') { - $replacements = ['', '', '']; - } else { - $replacements = [ - '{% foreach ' . $new_marker . ' %}', - '{% foreach ' . $new_marker . '${1}%}', - '{% endforeach %}' - ]; - } - $template = preg_replace($patterns, $replacements, $template); - } - return $template; - - } - - private function replaceAllOldMarkers($template) - { - return preg_replace( - '/###(([A-Z0-9_-]+)|(DATAFIELD_[0-9a-f]+))###/', - '{% ${1} %}', - $template); - } - - private function replaceNoMarkerSubparts(array $markers, $template) - { - foreach ($markers as $marker => $replace) { - $patterns = [ - '//', - '//' - ]; - $new_marker = $replace !== '' ? $replace : $marker; - $replacements = [ - '{% if count(' . $new_marker . ') == 0 %}', - '{% endif %}' - ]; - $template = preg_replace($patterns, $replacements, $template); - } - return $template; - } - - /** - * Replaces all old datafield markers with new ones. - * - * @param object $template Array with template and datafield_ids. - * @return array|string|string[] - */ - private function replaceDatafieldMarkers($template) - { - if (isset($template->genericdatafields)) { - $search = []; - $replace = []; - $key = 1; - foreach ($template->genericdatafields as $df_id) { - $search[] = 'DATAFIELD_' . $key++; - $replace[] = 'DATAFIELD_' . $df_id; - } - return str_replace($search, $replace, $template->template); - } - return $template->template; - } - - private function convertSemesters($old_semester_config) - { - $semester_mapping = [ - 'previous' => 'previous', - 'current' => 'current', - 'next' => 'next' - ]; - $key = 1; - foreach (Semester::findAllVisible(false) as $sem) { - $semester_mapping[$key++] = $sem['semester_id']; - } - return $semester_mapping[$old_semester_config] ?? ''; - } - - private function insertConfig($field, $message, $description) - { - DBManager::get()->exec( - "INSERT IGNORE INTO `config` - (`field`, `value`, `type`, `range`, - `section`, - `mkdate`, `chdate`, - `description`) - VALUES - ({$field}, '{$message}', 'string', 'global', - 'external_pages', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), - '{$description}')" - ); - } -} diff --git a/db/migrations/5.5.24_new_external_pages.php b/db/migrations/5.5.24_new_external_pages.php new file mode 100644 index 0000000..dec973f --- /dev/null +++ b/db/migrations/5.5.24_new_external_pages.php @@ -0,0 +1,578 @@ +exec("CREATE TABLE `extern_pages_configs` ( + `config_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `range_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `type` varchar(50) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `description` tinytext COLLATE utf8mb4_unicode_ci NOT NULL, + `conf` text COLLATE utf8mb4_unicode_ci NOT NULL, + `template` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `author_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `editor_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `mkdate` int(11) UNSIGNED NOT NULL DEFAULT '0', + `chdate` int(11) UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (`config_id`), + KEY `range_id` (`range_id`), + KEY `type` (`type`)) + "); + + $this->migrate_configurations(); + + $description = 'Allgemeine Fehlermeldung,die auf der Webseite ausgegeben wird, auf der der Inhalt der ' + . 'externe Seite angezeigt werden soll. Diese Meldung wird ausgegeben, ' + . 'wenn z.B. das Template fehlerhaft ist.'; + $message = 'Ein Fehler ist aufgetreten. Die Inhalte können nicht angezeigt werden.'; + DBManager::get()->exec( + "INSERT IGNORE INTO `config` + (`field`, `value`, `type`, `range`, + `section`, + `mkdate`, `chdate`, + `description`) + VALUES + ('EXTERN_PAGES_ERROR_MESSAGE', '{$message}', 'string', 'global', + 'external_pages', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), + '{$description}')" + ); + + $db->exec( + "DELETE FROM `config` WHERE `field` = 'EXTERN_ALLOW_ACCESS_WITHOUT_CONFIG'" + ); + $db->exec( + "DELETE FROM `config` WHERE `field` = 'EXTERN_SRI_ENABLE'" + ); + $db->exec( + "DELETE FROM `config` WHERE `field` = 'EXTERN_SRI_ENABLE_BY_ROOT'" + ); + $db->exec("DROP TABLE `extern_config`"); + + } + + public function down() + { + $db = \DBManager::get(); + + $db->exec( + "DELETE FROM `config` WHERE `config_id` = 'EXTERN_PAGES_ERROR_MESSAGE'" + ); + $db->exec('DROP TABLE IF EXISTS `extern_pages_configs`'); + + $db->exec("CREATE TABLE `extern_config` ( + `config_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL DEFAULT '', + `range_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL DEFAULT '', + `config_type` int(4) NOT NULL DEFAULT '0', + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `is_standard` tinyint(1) UNSIGNED NOT NULL DEFAULT '0', + `config` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `mkdate` int(11) UNSIGNED NOT NULL DEFAULT '0', + `chdate` int(11) UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (`config_id`,`range_id`)) + "); + + $configs = [ + 'EXTERN_ALLOW_ACCESS_WITHOUT_CONFIG' => + 'Free access to external pages (without the need of a configuration), independent of SRI settings above', + 'EXTERN_SRI_ENABLE' => + 'Allow the usage of SRI-interface (Stud.IP Remote Include)', + 'EXTERN_SRI_ENABLE_BY_ROOT' => + 'Only root allows the usage of SRI-interface for specific institutes' + ]; + foreach ($configs as $field => $description) { + $db->exec( + "INSERT IGNORE INTO `config` + (`field`, `value`, `type`, `range`, + `section`, + `mkdate`, `chdate`, + `description`) + VALUES + ({$field}, '0', 'boolean', 'global', + 'global', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), + '{$description}')" + ); + } + + } + + private function migrate_configurations() + { + $skip_text = 'Die Konfiguration (ID: %s, TYP: %s, NAME: %s) kann nicht migriert werden. '; + $db = DBManager::get(); + $db->fetchAll('SELECT * FROM `extern_config`', [], + function ($config) use ($skip_text) + { + // skip non template based configs + if ($config['config_type'] < 9) { + $this->announce($skip_text . 'Die Seite ist nicht Template-basiert.', + $config['config_id'], $config['config_type'], $config['name']); + return; + } + $new_conf = new ExternPageConfig($config['config_id']); + $new_conf->range_id = $config['range_id']; + $new_conf->name = $config['name']; + $new_conf->mkdate = $config['mkdate']; + $new_conf->chdate = time(); + $old_data = json_decode($config['config']); + $new_data = [ + 'language' => 'de_DE', + 'escaping' => 'htmlReady', + // this is used to show a hint that this configuration has still to be fixed after the migration + 'not_fixed_after_migration' => 1 + ]; + if (isset($old_data->Main->language) && strlen((string) $old_data->Main->language) < 5) { + $new_data['language'] = $old_data->Main->language; + } + switch ($config['config_type']) { + // Persons + case 9: + $new_conf->type = 'Persons'; + // The new sort fields are different, so match as good as possible. + // Use "nachname" as default. + if (isset($old_data->Main->sort)) { + asort($old_data->Main->sort, SORT_NUMERIC); + if (key($old_data->Main->sort) === '3') { + $new_data['sort'] = 'email'; + } else { + $new_data['sort'] = 'nachname'; + } + } + if (isset($old_data->Main->grouping) && $old_data->Main->grouping === '1') { + $new_data['grouping'] = true; + } + $new_data['groupsvisible'] = []; + if (isset($old_data->Main->groupsvisible)) { + foreach ($old_data->Main->groupsvisible as $groupsvisible) { + $new_data['groupsvisible'][$groupsvisible] = 1; + } + } + $new_data['groupsalias'] = []; + if (isset($old_data->Main->groupsalias)) { + $new_data['groupsalias'] = $old_data->Main->groupsalias; + } + $replace = [ + 'PERSONS' => 'GROUPS', + 'PERSON' => 'PERSONS', + 'GROUP' => '' + ]; + $template = $this->replaceSubparts( + $replace, + $this->replaceDatafieldMarkers($old_data->TemplateGeneric) + ); + $replace = [ + 'IMAGE-URL-NORMAL' => 'IMAGE_URL_NORMAL', + 'IMAGE-URL-MEDIUM' => 'IMAGE_URL_MEDIUM', + 'IMAGE-URL-SMALL' => 'IMAGE_URL_SMALL', + 'HOMEPAGE-HREF' => 'HOMEPAGE_URL' + ]; + $template = $this->replaceMarkers($replace, $template); + $replace = [ + 'PERSONS' => '' + ]; + $new_conf->template = $this->replaceAllOldMarkers( + $this->replaceNoMarkerSubparts($replace, $template) + ); + break; + // Download + case 10: + $new_conf->type = 'Download'; + $sort_fields = [ + 'filetype', + 'name', + '', + 'mkdate', + 'size', + 'author' + ]; + if (isset($old_data->Main->sort)) { + arsort($old_data->Main->sort, SORT_NUMERIC); + $new_data['sort'] = $sort_fields[key($old_data->Main->sort)]; + } + $new_data['subfolders'] = 1; + $template = $this->replaceDatafieldMarkers($old_data->TemplateGeneric); + $replace = [ + 'FILE_NAME' => 'NAME', + 'FILE_FILE-NAME' => 'NAME', + 'FILE_SIZE' => 'SIZE', + 'FILE_DESCRIPTION' => 'DESCRIPTION', + 'FILE_UPLOAD-DATE' => 'UPLOAD_DATE', + 'FILE_HREF' => 'URL', + 'FILE_ICON-HREF' => 'ICON_URL' + ]; + $template = $this->replaceMarkers($replace, $template); + $replace = [ + 'DOWNLOAD' => '', + 'FILE' => '', + 'FILES' => 'FILES' + ]; + $template = $this->replaceSubparts($replace, $template); + $replace = [ + 'FILES' => '' + ]; + $new_conf->template = $this->replaceAllOldMarkers( + $this->replaceNoMarkerSubparts($replace, $template) + ); + break; + // news + case 11: + $this->announce($skip_text . + 'Die Ausgabe von Ankündigungen an Einrichtungen wird nicht mehr unterstützt.', + $config['config_id'], $config['config_type'], $config['name']); + return; + // courses + case 12: + $new_conf->type = 'Courses'; + $new_data['groupby'] = $old_data->Main->grouping; + $new_data['startsem'] = $this->convertSemesters($old_data->Main->semstart); + $new_data['semcount'] = $old_data->Main->semrange; + $new_data['semswitch'] = $old_data->Main->semswitch; + $new_data['participating'] = $old_data->Main->allseminars; + $new_data['semtypes'] = (array) $old_data->ReplaceTextSemType->visibility; + $new_data['studyareas'] = (array) $old_data->SelectSubjectAreas->subjectareasselected; + $replace = [ + 'START_SEMESTER' => 'START_SEMESTER_NAME', + 'LECTURES-SUBSTITUTE-GROUPED-BY' => 'GROUPED_BY', + 'GROUP' => 'NAME', + 'SEMTYPE' => 'SEMTYPE_NAME', + 'UNAME' => 'USERNAME' + ]; + $template = $this->replaceMarkers( + $replace, + $this->replaceDatafieldMarkers($old_data->TemplateGeneric) + ); + $replace = [ + 'LECTURES' => '', + 'GROUP' => 'GROUPED_COURSES', + 'LECTURE' => 'COURSES', + 'LECTURERS' => 'LECTURERS' + ]; + $template = $this->replaceSubparts($replace, $template); + $replace = [ + 'LECTURES' => 'COURSES' + ]; + $new_conf->template = $this->replaceAllOldMarkers( + $this->replaceNoMarkerSubparts($replace, $template) + ); + break; + // course details + case 13: + $new_conf->type = 'CourseDetails'; + $new_data['rangepathlevel'] = $old_data->Main->rangepathlevel; + // combine templates + $pattern = [ + '/(\{%\s+|###)NEWS(###|\s+%})/', + '/(\{%\s+|###)STUDIP-DATA(###|\s+%})/' + ]; + $replacement = [ + $old_data->TemplateNews->template, + $old_data->TemplateStudipData->template + ]; + $template = preg_replace( + $pattern, + $replacement, + $this->replaceDatafieldMarkers($old_data->TemplateLectureData) + ); + $replace = [ + 'MISC' => 'MISCELLANEOUS', + 'LEISTUNGSNACHWEIS' => 'CERTIFICATE', + 'UNAME' => 'USERNAME', + 'TUTOR_FULLNAME' => 'FULLNAME', + 'TUTOR_LASTNAME' => 'LASTNAME', + 'TUTOR_FIRSTNAME' => 'FIRSTNAME', + 'TUTOR_TITLEFRONT' => 'TITLEFRONT', + 'TUTOR_TITLEREAR' => 'TITLEREAR', + 'TUTOR_UNAME' => 'USERNAME', + 'NEWS_TOPIC' => 'TOPIC', + 'NEWS_BODY' => 'BODY', + 'NEWS_DATE' => 'DATE', + 'PRELIM-DISCUSSION' => 'PRELIM_DISCUSSION', + 'FIRST-MEETING' => 'FIRST_MEETING', + 'HOME-INST-NAME' => 'HOME_INST_NAME', + 'COUNT-USER' => 'COUNT_USER', + 'INVOLVED-INSTITUTE_NAME' => 'NAME' + ]; + $template = $this->replaceMarkers($replace, $template); + $replace = [ + 'LECTUREDETAILS' => '', + 'LECTURER' => '', + 'LECTURERS' => 'LECTURERS', + 'TUTOR' => '', + 'TUTORS' => 'TUTORS', + 'NEWS' => '', + 'ALL-NEWS' => 'NEWS', + 'SINGLE-NEWS' => '', + 'STUDIP-DATA' => '', + 'RANGE-PATHES' => 'RANGE_PATHS', + 'RANGE-PATH' => '', + 'MODULES' => 'MODULES', + 'MODULE' => '', + 'INVOLVED-INSTITUTES' => 'INVOLVED_INSTITUTES', + 'INVOLVED-INSTITUTE' => '', + ]; + $template = $this->replaceSubparts($replace, $template); + $replace = [ + 'NEWS' => '' + ]; + $new_conf->template = $this->replaceAllOldMarkers( + $this->replaceNoMarkerSubparts($replace, $template) + ); + break; + // person details + case 14: + $new_conf->type = 'PersonDetails'; + $new_data['defaultaddr'] = $old_data->Main->defaultaddr; + $new_data['startsem'] = $this->convertSemesters($old_data->PersondetailsLectures->semstart); + $new_data['semcount'] = $old_data->PersondetailsLectures->semrange; + $new_data['semswitch'] = $old_data->PersondetailsLectures->semswitch; + $new_data['semclass'] = $old_data->PersondetailsLectures->semclass; + // combine templates + $pattern = [ + '/(\{%\s+|###)LECTURES(###|\s+%})/', + '/(\{%\s+|###)NEWS(###|\s+%})/', + '/(\{%\s+|###)APPOINTMENTS(###|\s+%})/', + '/(\{%\s+|###)OWNCATEGORIES(###|\s+%})/' + ]; + $replacement = [ + $old_data->TemplateLectures->template, + $old_data->TemplateNews->template, + $old_data->TemplateAppointments->template, + $old_data->TemplateOwnCategories->template + ]; + $template = preg_replace( + $pattern, + $replacement, + $this->replaceDatafieldMarkers($old_data->TemplateMain) + ); + $replace = [ + 'IMAGE-HREF' => 'IMAGE_URL_NORMAL', + 'INST-NAME' => 'NAME', + 'SINGLE-INST-NAME' => 'NAME', + 'SINGLE-INST-STREET' => 'STREET', + 'SINGLE-INST-ZIPCODE' => 'ZIPCODE', + 'SINGLE-INST-EMAIL' => 'EMAIL', + 'SINGLE-INST-ROOM' => 'MEMBER_ROOM', + 'SINGLE-INST-PHONE' => 'MEMBER_PHONE', + 'SINGLE-INST-FAX' => 'MEMBER_FAX', + 'SINGLE-INST-HREF' => 'HOMEPAGE', + 'SINGLE-INST-OFFICE-HOURS' => 'MEMBER_OFFICEHOURS', + 'NEWS_TOPIC' => 'TOPIC', + 'NEWS_BODY' => 'BODY', + 'NEWS_DATE' => 'DATE', + 'LIST-START' => 'APPOINTMENTS_START', + 'LIST-END' => 'APPOINTMENTS_END', + 'REPETITION' => 'RECURRENCE', + 'BEGIN' => 'START', + 'OWNCATEGORY_TITLE' => 'TITLE', + 'OWNCATEGORY_CONTENT' => 'CONTENT', + 'OFFICE-HOURS' => 'OFFICEHOURS', + 'HOMEPAGE-HREF' => 'HOMEPAGE_URL' + ]; + $template = $this->replaceMarkers($replace, $template); + $replace = [ + 'PERSONDETAILS' => '', + 'SINGLE-INST' => '', + 'SEMESTER' => '', + 'NEWS' => '', + 'SINGLE-NEWS' => '', + 'APPOINTMENTS' => '', + 'SINGLE-APPOINTMENT' => '', + 'ALL-INST' => 'INSTITUTES', + 'LECTURES' => 'SEMESTERS', + 'LECTURE' => 'LECTURES', + 'ALL-NEWS' => 'NEWS', + 'ALL-APPOINTMENTS' => 'APPOINTMENTS', + 'OWNCATEGORIES' => 'OWNCATEGORIES', + 'OWNCATEGORY' => '' + ]; + $template = $this->replaceSubparts($replace, $template); + $replace = [ + 'NEWS' => '', + 'APPOINTMENTS' => '' + ]; + $new_conf->template = $this->replaceAllOldMarkers( + $this->replaceNoMarkerSubparts($replace, $template) + ); + break; + // sembrowser + case 15: + $this->announce($skip_text . + 'Der Seminarbrowser wird nicht mehr unterstützt.', + $config['config_id'], $config['config_type'], $config['name']); + return; + // persbrowser + case 16: + $new_conf->type = 'PersBrowse'; + $new_data['instperms'] = $old_data->Main->instperms; + $new_data['onlylecturers'] = $old_data->Main->onlylecturers; + $new_data['institutes'] = $old_data->SelectInstitutes->institutesselected; + // combine templates + $pattern = [ + '/(\{%\s+|###)LISTCHARACTERS(###|\s+%})/', + '/(\{%\s+|###)LISTINSTITUTES(###|\s+%})/', + '/(\{%\s+|###)LISTPERSONS(###|\s+%})/' + ]; + $replacement = [ + $old_data->TemplateListCharacters->template, + $old_data->TemplateListInstitutes->template, + $old_data->TemplateListPersons->template + ]; + $template = preg_replace( + $pattern, + $replacement, + $old_data->TemplateMain->template + ); + $new_data['language'] = $old_data->Main->language; + $replace = [ + 'CHARACTER_COUNT_USER' => 'CHARACTER_COUNT', + 'INSTITUTE_NAME' => 'NAME', + 'INSTITUTE_COUNT_USER' => 'COUNT_USERS' + ]; + $template = $this->replaceMarkers($replace, $template); + + $replace = [ + 'PERS_BROWSER' => '', + 'CHARACTER' => '', + 'LIST_CHARACTERS' => 'CHARACTERS', + 'INSTITUTE' => '', + 'LIST_INSTITUTES' => 'INSTITUTES', + 'LIST_PERSONS' => '', + 'PERSON' => '', + 'PERSONS' => 'PERSONS' + ]; + $template = $this->replaceSubparts($replace, $template); + + $replace = [ + 'PERSONS' => '' + ]; + $new_conf->template = $this->replaceAllOldMarkers( + $this->replaceNoMarkerSubparts($replace, $template) + ); + } + $new_conf->conf = json_encode($new_data); + $new_conf->store(); + }); + } + + private function replaceMarkers(array $marker_replacements, $template) + { + foreach ($marker_replacements as $old_marker => $new_marker) { + $patterns = [ + '/###' . $old_marker . '###/', + '/\{%(\s.*)' . $old_marker . '(.*\s)%}/' + ]; + $replacements = [ + '###' . $new_marker . '###', + '{%${1}' . $new_marker . '${2}%}' + ]; + $template = preg_replace($patterns, $replacements, $template); + }; + return $template; + } + + private function replaceSubparts(array $marker_replacements, $template) + { + foreach ($marker_replacements as $old_marker => $new_marker) { + $patterns = [ + '//', + '/\{%\s+foreach.+' . $old_marker . '(\s.+)%}/', + '//' + ]; + if ($new_marker === '') { + $replacements = ['', '', '']; + } else { + $replacements = [ + '{% foreach ' . $new_marker . ' %}', + '{% foreach ' . $new_marker . '${1}%}', + '{% endforeach %}' + ]; + } + $template = preg_replace($patterns, $replacements, $template); + } + return $template; + + } + + private function replaceAllOldMarkers($template) + { + return preg_replace( + '/###(([A-Z0-9_-]+)|(DATAFIELD_[0-9a-f]+))###/', + '{% ${1} %}', + $template); + } + + private function replaceNoMarkerSubparts(array $markers, $template) + { + foreach ($markers as $marker => $replace) { + $patterns = [ + '//', + '//' + ]; + $new_marker = $replace !== '' ? $replace : $marker; + $replacements = [ + '{% if count(' . $new_marker . ') == 0 %}', + '{% endif %}' + ]; + $template = preg_replace($patterns, $replacements, $template); + } + return $template; + } + + /** + * Replaces all old datafield markers with new ones. + * + * @param object $template Array with template and datafield_ids. + * @return array|string|string[] + */ + private function replaceDatafieldMarkers($template) + { + if (isset($template->genericdatafields)) { + $search = []; + $replace = []; + $key = 1; + foreach ($template->genericdatafields as $df_id) { + $search[] = 'DATAFIELD_' . $key++; + $replace[] = 'DATAFIELD_' . $df_id; + } + return str_replace($search, $replace, $template->template); + } + return $template->template; + } + + private function convertSemesters($old_semester_config) + { + $semester_mapping = [ + 'previous' => 'previous', + 'current' => 'current', + 'next' => 'next' + ]; + $key = 1; + foreach (Semester::findAllVisible(false) as $sem) { + $semester_mapping[$key++] = $sem['semester_id']; + } + return $semester_mapping[$old_semester_config] ?? ''; + } + + private function insertConfig($field, $message, $description) + { + DBManager::get()->exec( + "INSERT IGNORE INTO `config` + (`field`, `value`, `type`, `range`, + `section`, + `mkdate`, `chdate`, + `description`) + VALUES + ({$field}, '{$message}', 'string', 'global', + 'external_pages', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), + '{$description}')" + ); + } +} diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index d4d5bbb..4f44165 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -501,6 +501,13 @@ class RouteMap $group->get('/courseware-task-groups/{id}', Routes\Courseware\TaskGroupsShow::class); $group->post('/courseware-task-groups', Routes\Courseware\TaskGroupsCreate::class); + $group->patch('/courseware-task-groups/{id}', Routes\Courseware\TaskGroupsUpdate::class); + $group->delete('/courseware-task-groups/{id}', Routes\Courseware\TaskGroupsDelete::class); + $this->addRelationship( + $group, + '/courseware-task-groups/{id}/relationships/solvers', + Routes\Courseware\Rel\SolversOfTaskGroup::class + ); $group->get('/courseware-task-feedback/{id}', Routes\Courseware\TaskFeedbackShow::class); $group->post('/courseware-task-feedback', Routes\Courseware\TaskFeedbackCreate::class); diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php index 88eb3df..2acf83e 100644 --- a/lib/classes/JsonApi/Routes/Courseware/Authority.php +++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php @@ -23,7 +23,13 @@ use User; use Course; /** + * @SuppressWarnings(PHPMD.CamelCaseParameterName) + * @SuppressWarnings(PHPMD.CamelCaseVariableName) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.StaticAccess) + * @SuppressWarnings(PHPMD.Superglobals) * @SuppressWarnings(PHPMD.TooManyMethods) * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ @@ -306,6 +312,16 @@ class Authority return $resource['lecturer_id'] === $user->id; } + public static function canUpdateTaskGroup(User $user, TaskGroup $resource): bool + { + return self::canCreateTasks($user, $resource->target); + } + + public static function canDeleteTaskGroup(User $user, TaskGroup $resource): bool + { + return self::canUpdateTaskGroup($user, $resource); + } + public static function canShowTask(User $user, Task $resource): bool { return self::canUpdateTask($user, $resource); @@ -332,6 +348,11 @@ class Authority return self::canCreateTasks($user, $resource->structural_element) && !$resource->userIsASolver($user); } + public static function canRenewTask(User $user, Task $resource): bool + { + return self::canDeleteTask($user, $resource); + } + public static function canCreateTaskFeedback(User $user, Task $resource): bool { return self::canCreateTasks($user, $resource->structural_element); @@ -352,7 +373,6 @@ class Authority return self::canCreateTaskFeedback($user, $resource); } - public static function canIndexStructuralElementComments(User $user, StructuralElement $resource) { return self::canShowStructuralElement($user, $resource); @@ -407,7 +427,8 @@ class Authority public static function canShowStructuralElementFeedback(User $user, StructuralElementFeedback $resource) { - return $resource->user_id === $user->id || self::canUpdateStructuralElement($user, $resource->structural_element); + return $resource->user_id === $user->id || + self::canUpdateStructuralElement($user, $resource->structural_element); } public static function canDeleteStructuralElementFeedback(User $user, StructuralElementFeedback $resource) @@ -415,7 +436,6 @@ class Authority return self::canUpdateStructuralElementFeedback($user, $resource); } - public static function canShowTemplate(User $user, Template $resource) { // templates are for everybody, aren't they? @@ -430,7 +450,7 @@ class Authority public static function canCreateTemplate(User $user) { - return $GLOBALS['perm']->have_perm('admin'); + return $GLOBALS['perm']->have_perm('admin', $user->id); } public static function canUpdateTemplate(User $user, Template $resource) @@ -490,7 +510,7 @@ class Authority if ($user->id === $range->id) { return true; } - return $GLOBALS['perm']->have_studip_perm('tutor', $range->id ,$user->id); + return $GLOBALS['perm']->have_studip_perm('tutor', $range->id, $user->id); } public static function canSortUnit(User $user, \Range $range): bool @@ -518,7 +538,6 @@ class Authority return $request_user->id === $user->id; } - public static function canShowClipboard(User $user, Clipboard $resource): bool { return $resource->user_id === $user->id; @@ -541,7 +560,7 @@ class Authority } else { $structural_element = $resource->getStructuralElement(); } - + return $structural_element->canEdit($user); } diff --git a/lib/classes/JsonApi/Routes/Courseware/Rel/SolversOfTaskGroup.php b/lib/classes/JsonApi/Routes/Courseware/Rel/SolversOfTaskGroup.php new file mode 100644 index 0000000..2ab5ffa --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/Rel/SolversOfTaskGroup.php @@ -0,0 +1,207 @@ +getSolvers(); + $total = count($solvers); + + return $this->getPaginatedIdentifiersResponse(array_slice($solvers, ...$this->getOffsetAndLimit()), $total); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function addToRelationship(Request $request, $related) + { + $this->createTaskFor( + $related, + array_filter($this->validateSolvers($related, $this->validate($request)), function ($solver) use ( + $related + ) { + return !$related->findTaskBySolver($solver); + }) + ); + + return $this->getCodeResponse(204); + } + + protected function findRelated(array $args) + { + $related = TaskGroup::find($args['id']); + if (!$related) { + throw new RecordNotFoundException(); + } + + return $related; + } + + protected function authorize(Request $request, $resource) + { + switch ($request->getMethod()) { + case 'GET': + return Authority::canShowTaskGroup($this->getUser($request), $resource); + case 'POST': + return Authority::canUpdateTaskGroup($this->getUser($request), $resource); + + default: + return false; + } + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function getRelationshipSelfLink($resource, $schema, $userData) + { + return $schema->getRelationshipSelfLink($resource, TaskGroupSchema::REL_SOLVERS); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function getRelationshipRelatedLink($resource, $schema, $userData) + { + return $schema->getRelationshipRelatedLink($resource, TaskGroupSchema::REL_SOLVERS); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + + $data = self::arrayGet($json, 'data'); + + if (!is_array($data)) { + return 'Document´s `data` must be an array.'; + } + + foreach ($data as $item) { + if (!in_array(self::arrayGet($item, 'type'), [UserSchema::TYPE, StatusGroupSchema::TYPE])) { + return 'Wrong `type` in document´s `data`.'; + } + + if (!self::arrayGet($item, 'id')) { + return 'Missing `id` of document´s `data`.'; + } + } + } + + private function validateSolvers(TaskGroup $taskGroup, iterable $json): iterable + { + if (!$taskGroup->course) { + return []; + } + $solvers = []; + foreach ($json['data'] as $item) { + $solver = $this->findSolver($item); + if (!$solver) { + throw new RecordNotFoundException(); + } + if (!$this->validateSolver($taskGroup, $solver)) { + throw new UnprocessableEntityException(); + } + $solvers[] = $solver; + } + return $solvers; + } + + /** + * @return Statusgruppen|User|null + */ + private function findSolver($json) + { + switch ($json['type']) { + case 'status-groups': + return Statusgruppen::find($json['id']); + case 'users': + return User::find($json['id']); + } + return null; + } + + /** + * @param Statusgruppen|User $solver + * + * @SuppressWarnings(PHPMD.Superglobals) + */ + private function validateSolver(TaskGroup $taskGroup, $solver): bool + { + if ($solver instanceof User) { + return $GLOBALS['perm']->have_studip_perm('autor', $taskGroup->course->id, $solver->id); + } + if ($solver instanceof Statusgruppen) { + return $taskGroup->course->id === $solver->range_id; + } + + return false; + } + + /** + * @param array $solvers + */ + private function createTaskFor(TaskGroup $taskGroup, $solvers): void + { + $template = $this->getTaskTemplate($taskGroup); + if (!$template) { + throw new RuntimeException(); + } + + foreach ($solvers as $solver) { + $task = Task::build([ + 'task_group_id' => $taskGroup->id, + 'solver_id' => $solver->id, + 'solver_type' => $this->getSolverType($solver), + ]); + + $taskElement = $template->copy($taskGroup->lecturer, $taskGroup->target, 'task'); + $taskElement->title = $taskGroup->title; + $taskElement->store(); + + $task['structural_element_id'] = $taskElement->id; + $task->store(); + } + } + + private function getTaskTemplate(TaskGroup $taskGroup): StructuralElement + { + return StructuralElement::find($taskGroup->task_template_id); + } + + /** + * @param User|Statusgruppen $solver + */ + private function getSolverType($solver): string + { + $solverTypes = [\User::class => 'autor', \Statusgruppen::class => 'group']; + + return $solverTypes[get_class($solver)]; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php index 28c4e9c..f7357a4 100644 --- a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php @@ -65,14 +65,20 @@ class TaskGroupsCreate extends JsonApiController if (!self::arrayHas($json, 'data.attributes.title')) { return 'Missing `title` attribute.'; } - if (!self::arrayHas($json, 'data.attributes.submission-date')) { - return 'Missing `submission-date` attribute.'; + if (!self::arrayHas($json, 'data.attributes.start-date')) { + return 'Missing `start-date` attribute.'; } - $submissionDate = self::arrayGet($json, 'data.attributes.submission-date'); - if (!self::isValidTimestamp($submissionDate)) { - return '`submission-date` is not an ISO 8601 timestamp.'; + $startDate = self::arrayGet($json, 'data.attributes.start-date'); + if (!self::isValidTimestamp($startDate)) { + return '`start-date` is not an ISO 8601 timestamp.'; + } + if (!self::arrayHas($json, 'data.attributes.end-date')) { + return 'Missing `end-date` attribute.'; + } + $endDate = self::arrayGet($json, 'data.attributes.end-date'); + if (!self::isValidTimestamp($endDate)) { + return '`end-date` is not an ISO 8601 timestamp.'; } - if (!self::arrayHas($json, 'data.relationships.target')) { return 'Missing `target` relationship.'; } @@ -165,8 +171,8 @@ class TaskGroupsCreate extends JsonApiController $target = $this->getTargetFromJson($json); $solverMayAddBlocks = self::arrayGet($json, 'data.attributes.solver-may-add-blocks', ''); - $submissionDate = self::arrayGet($json, 'data.attributes.submission-date', ''); - $submissionDate = self::fromISO8601($submissionDate); + $startDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.start-date', '')); + $endDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.end-date', '')); $title = self::arrayGet($json, 'data.attributes.title', ''); /** @var TaskGroup $taskGroup */ @@ -177,6 +183,8 @@ class TaskGroupsCreate extends JsonApiController 'task_template_id' => $taskTemplate->getId(), 'solver_may_add_blocks' => $solverMayAddBlocks, 'title' => $title, + 'start_date' => $startDate->getTimestamp(), + 'end_date' => $endDate->getTimestamp(), ]); foreach ($solvers as $solver) { @@ -184,7 +192,6 @@ class TaskGroupsCreate extends JsonApiController 'task_group_id' => $taskGroup->getId(), 'solver_id' => $solver->getId(), 'solver_type' => $this->getSolverType($solver), - 'submission_date' => $submissionDate->getTimestamp(), ]); // copy task template diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsDelete.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsDelete.php new file mode 100644 index 0000000..2faf778 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsDelete.php @@ -0,0 +1,38 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + $resource->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsUpdate.php new file mode 100644 index 0000000..8662b71 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsUpdate.php @@ -0,0 +1,99 @@ +validate($request, $resource); + $user = $this->getUser($request); + if (!Authority::canUpdateTaskGroup($user, $resource)) { + throw new AuthorizationFailedException(); + } + + $process = $this->update($resource, $json); + + return $this->getContentResponse($process); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + * + * @param array $json + * @param mixed $data + * + * @return string|void + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (TaskGroupSchema::TYPE !== self::arrayGet($json, 'data.type')) { + return 'Invalid `type` of document´s `data`.'; + } + + if (!self::arrayHas($json, 'data.attributes.start-date')) { + return 'Missing `start-date` attribute.'; + } + $startDate = self::arrayGet($json, 'data.attributes.start-date'); + if (!self::isValidTimestamp($startDate)) { + return '`start-date` is not an ISO 8601 timestamp.'; + } + + if (!self::arrayHas($json, 'data.attributes.end-date')) { + return 'Missing `end-date` attribute.'; + } + $endDate = self::arrayGet($json, 'data.attributes.end-date'); + if (!self::isValidTimestamp($endDate)) { + return '`end-date` is not an ISO 8601 timestamp.'; + } + + if (self::fromISO8601($startDate) > self::fromISO8601($endDate)) { + return '`start-date` is later than `end-date`'; + } + } + + private function update(TaskGroup $taskGroup, array $json): TaskGroup + { + $startDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.start-date')); + $endDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.end-date')); + + $taskGroup->start_date = $startDate->getTimestamp(); + $taskGroup->end_date = $endDate->getTimestamp(); + + $taskGroup->store(); + + return $taskGroup; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php b/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php index f0b2ce9..26a021c 100644 --- a/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php +++ b/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php @@ -77,9 +77,10 @@ class TasksIndex extends JsonApiController } } - private function findTasksByCourse(\Course $course): \SimpleCollection + private function findTasksByCourse(\Course $course, bool $showNotYetActive = true): \SimpleCollection { - $taskGroups = TaskGroup::findBySQL('seminar_id = ?', [$course->getId()]); + $whereClause = $showNotYetActive ? 'seminar_id = ?' : 'start_date <= UNIX_TIMESTAMP() AND seminar_id = ?'; + $taskGroups = TaskGroup::findBySQL($whereClause, [$course->getId()]); $tasks = []; foreach ($taskGroups as $taskGroup) { @@ -98,7 +99,7 @@ class TasksIndex extends JsonApiController }) ->pluck('id'); - return $this->findTasksByCourse($course)->filter(function ($task) use ($user, $groupIds) { + return $this->findTasksByCourse($course, false)->filter(function ($task) use ($user, $groupIds) { return ('autor' === $task['solver_type'] && $task['solver_id'] === $user->getId()) || ('group' === $task['solver_type'] && in_array($task['solver_id'], $groupIds)); }); diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php index 3728dba..33b51ad 100644 --- a/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php @@ -13,6 +13,8 @@ use Psr\Http\Message\ServerRequestInterface as Request; /** * Update one Task. + * + * @SuppressWarnings(PHPMD.StaticAccess) */ class TasksUpdate extends JsonApiController { @@ -32,7 +34,8 @@ class TasksUpdate extends JsonApiController throw new RecordNotFoundException(); } $json = $this->validate($request, $resource); - if (!Authority::canUpdateTask($user = $this->getUser($request), $resource)) { + $user = $this->getUser($request); + if (!Authority::canUpdateTask($user, $resource)) { throw new AuthorizationFailedException(); } $resource = $this->updateTask($user, $resource, $json); @@ -66,53 +69,35 @@ class TasksUpdate extends JsonApiController private function updateTask(\User $user, Task $resource, array $json): Task { - if (Authority::canDeleteTask($user, $resource)) { - if (self::arrayHas($json, 'data.attributes.renewal')) { - $newRenewalState = self::arrayGet($json, 'data.attributes.renewal'); - if ('declined' === $newRenewalState) { - $resource->renewal = $newRenewalState; - } - if ('granted' === $newRenewalState && self::arrayHas($json, 'data.attributes.renewal-date')) { - $renewalDate = self::arrayGet($json, 'data.attributes.renewal-date', ''); - $renewalDate = self::fromISO8601($renewalDate); + if (Authority::canRenewTask($user, $resource)) { + return $this->renewTask($resource, $json); + } - $resource->renewal = $newRenewalState; - $resource->renewal_date = $renewalDate->getTimestamp(); - } - } - } else { - if (self::arrayHas($json, 'data.attributes.submitted')) { - $newSubmittedState = self::arrayGet($json, 'data.attributes.submitted'); - if ($this->canSubmit($resource, $newSubmittedState)) { - $resource->submitted = $newSubmittedState; - if ('pending' === $resource->renewal) { - $resource->renewal = ''; - } - } - } - if (self::arrayHas($json, 'data.attributes.renewal')) { - $newRenewalState = self::arrayGet($json, 'data.attributes.renewal'); - if ('pending' === $newRenewalState) { - $resource->renewal = $newRenewalState; - } - } + if (self::arrayGet($json, 'data.attributes.submitted') === true && $resource->canSubmit()) { + $resource->submitTask(); } - $resource->store(); + if (self::arrayGet($json, 'data.attributes.renewal') === 'pending') { + $resource->requestRenewal(); + } return $resource; } - private function canSubmit(Task $resource, string $newSubmittedState): bool + private function renewTask(Task $resource, array $json): Task { - $now = time(); - if (1 === (int) $resource->submitted || !$newSubmittedState) { - return false; - } - if ('granted' === $resource->renewal) { - return $now <= $resource->renewal_date; - } else { - return $now <= $resource->submission_date; + switch (self::arrayGet($json, 'data.attributes.renewal')) { + case 'declined': + $resource->declineRenewalRequest(); + break; + + case 'granted': + $resource->grantRenewalRequest( + self::fromISO8601(self::arrayGet($json, 'data.attributes.renewal-date')) + ); + break; } + + return $resource; } } diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index dd74bc9..71aadf7 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -59,17 +59,17 @@ class SchemaMap \Courseware\Clipboard::class => Schemas\Courseware\Clipboard::class, \Courseware\Container::class => Schemas\Courseware\Container::class, \Courseware\Instance::class => Schemas\Courseware\Instance::class, + \Courseware\PublicLink::class => Schemas\Courseware\PublicLink::class, \Courseware\StructuralElement::class => Schemas\Courseware\StructuralElement::class, \Courseware\StructuralElementComment::class => Schemas\Courseware\StructuralElementComment::class, \Courseware\StructuralElementFeedback::class => Schemas\Courseware\StructuralElementFeedback::class, - \Courseware\Unit::class => Schemas\Courseware\Unit::class, - \Courseware\UserDataField::class => Schemas\Courseware\UserDataField::class, - \Courseware\UserProgress::class => Schemas\Courseware\UserProgress::class, \Courseware\Task::class => Schemas\Courseware\Task::class, - \Courseware\TaskGroup::class => Schemas\Courseware\TaskGroup::class, \Courseware\TaskFeedback::class => Schemas\Courseware\TaskFeedback::class, + \Courseware\TaskGroup::class => Schemas\Courseware\TaskGroup::class, \Courseware\Template::class => Schemas\Courseware\Template::class, - \Courseware\PublicLink::class => Schemas\Courseware\PublicLink::class, + \Courseware\Unit::class => Schemas\Courseware\Unit::class, + \Courseware\UserDataField::class => Schemas\Courseware\UserDataField::class, + \Courseware\UserProgress::class => Schemas\Courseware\UserProgress::class, ]; } } diff --git a/lib/classes/JsonApi/Schemas/Courseware/Task.php b/lib/classes/JsonApi/Schemas/Courseware/Task.php index a0605e6..81c7a0d 100644 --- a/lib/classes/JsonApi/Schemas/Courseware/Task.php +++ b/lib/classes/JsonApi/Schemas/Courseware/Task.php @@ -2,6 +2,8 @@ namespace JsonApi\Schemas\Courseware; +use Courseware\Task as TaskModel; +use JsonApi\Routes\Courseware\Authority as CoursewareAuthority; use JsonApi\Schemas\SchemaProvider; use Neomerx\JsonApi\Contracts\Schema\ContextInterface; use Neomerx\JsonApi\Schema\Link; diff --git a/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php b/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php index 12dbc6c..c950671 100644 --- a/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php +++ b/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php @@ -3,6 +3,7 @@ namespace JsonApi\Schemas\Courseware; use Courseware\StructuralElement; +use Courseware\TaskGroup as TaskGroupModel; use JsonApi\Schemas\SchemaProvider; use Neomerx\JsonApi\Contracts\Schema\ContextInterface; use Neomerx\JsonApi\Schema\Identifier; @@ -35,6 +36,8 @@ class TaskGroup extends SchemaProvider return [ 'solver-may-add-blocks' => (bool) $resource['solver_may_add_blocks'], 'title' => (string) $resource->title, + 'start-date' => date('c', $resource['start_date']), + 'end-date' => date('c', $resource['end_date']), 'mkdate' => date('c', $resource['mkdate']), 'chdate' => date('c', $resource['chdate']), ]; diff --git a/lib/models/Courseware/Task.php b/lib/models/Courseware/Task.php index 3a68d3e..d409676 100644 --- a/lib/models/Courseware/Task.php +++ b/lib/models/Courseware/Task.php @@ -31,7 +31,9 @@ use User; * @property \Statusgruppen $group belongs_to \Statusgruppen * @property \Course $course belongs_to \Course * @property TaskFeedback|null $task_feedback belongs_to TaskFeedback - * @property mixed $solver additional field + * @property-read \User|\Statusgruppen|null $solver additional field + * + * @SuppressWarnings(PHPMD.StaticAccess) */ class Task extends \SimpleORMap { @@ -80,6 +82,10 @@ class Task extends \SimpleORMap 'get' => 'getSolver', 'set' => false, ]; + $config['additional_fields']['submission_date'] = [ + 'get' => 'getSubmissionDate', + 'set' => false, + ]; parent::configure($config); } @@ -171,6 +177,11 @@ class Task extends \SimpleORMap return null; } + public function getSubmissionDate(): int + { + return $this->task_group['end_date']; + } + public function getTaskProgress(): float { $children = $this->structural_element->findDescendants(); @@ -185,6 +196,45 @@ class Task extends \SimpleORMap return $progress * 100; } + public function canSubmit(): bool + { + return !$this->submitted + && time() <= ('granted' === $this->renewal ? $this->renewal_date : $this->submission_date); + } + + public function submitTask(): void + { + $this->submitted = 1; + if ('pending' === $this->renewal) { + $this->renewal = ''; + } + $this->store(); + } + + public function isRenewed(): bool + { + return $this->renewal === 'granted'; + } + + public function requestRenewal(): void + { + $this->renewal = 'pending'; + $this->store(); + } + + public function declineRenewalRequest(): void + { + $this->renewal = 'declined'; + $this->store(); + } + + public function grantRenewalRequest(\DateTime $renewalDate): void + { + $this->renewal = 'granted'; + $this->renewal_date = $renewalDate->getTimestamp(); + $this->store(); + } + private function getStructuralElementProgress(StructuralElement $structural_element): float { $containers = Container::findBySQL('structural_element_id = ?', [intval($structural_element->id)]); diff --git a/lib/models/Courseware/TaskGroup.php b/lib/models/Courseware/TaskGroup.php index 092edf6..6902cb3 100644 --- a/lib/models/Courseware/TaskGroup.php +++ b/lib/models/Courseware/TaskGroup.php @@ -2,6 +2,8 @@ namespace Courseware; +use DBManager; +use Statusgruppen; use User; /** @@ -19,11 +21,17 @@ use User; * @property int $task_template_id database column * @property int $solver_may_add_blocks database column * @property string $title database column + * @property int $start_date database column + * @property int $end_date database column * @property int $mkdate database column * @property int $chdate database column * @property \SimpleORMapCollection|Task[] $tasks has_many Task * @property \User $lecturer belongs_to \User * @property \Course $course belongs_to \Course + * @property \Courseware\StructuralElement $target belongs_to Courseware\StructuralElement + * @property \SimpleORMapCollection $tasks has_many Courseware\Task + * + * @SuppressWarnings(PHPMD.StaticAccess) */ class TaskGroup extends \SimpleORMap implements \PrivacyObject { @@ -41,6 +49,11 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject 'foreign_key' => 'seminar_id', ]; + $config['belongs_to']['target'] = [ + 'class_name' => StructuralElement::class, + 'foreign_key' => 'target_id', + ]; + $config['has_many']['tasks'] = [ 'class_name' => Task::class, 'assoc_foreign_key' => 'task_group_id', @@ -52,6 +65,22 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject parent::configure($config); } + /** + * Export available data of a given user into a storage object + * (an instance of the StoredUserData class) for that user. + * + * @param StoredUserData $storage object to store data into + */ + public static function exportUserData(\StoredUserData $storage) + { + $task_groups = DBManager::get()->fetchAll('SELECT * FROM cw_task_groups WHERE lecturer_id = ?', [ + $storage->user_id, + ]); + if ($task_groups) { + $storage->addTabularData(_('Courseware Aufgaben'), 'cw_task_groups', $task_groups); + } + } + public function getSolvers(): iterable { $solvers = $this->tasks->pluck('solver'); @@ -60,20 +89,45 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject } /** - * Export available data of a given user into a storage object - * (an instance of the StoredUserData class) for that user. + * Returns all submitters of this TaskGroup. * - * @param StoredUserData $storage object to store data into + * @returns iterable all the submitters of this TaskGroup. */ - public static function exportUserData(\StoredUserData $storage) + public function getSubmitters(): iterable { - $task_groups = \DBManager::get()->fetchAll( - 'SELECT * FROM cw_task_groups WHERE lecturer_id = ?', - [$storage->user_id] + return DBManager::get()->fetchAll( + 'SELECT solver_id, solver_type FROM cw_tasks WHERE task_group_id = ? AND submitted = 1', + [$this->getId()], + function ($row) { + switch ($row['solver_type']) { + case 'autor': + return \User::find($row['solver_id']); + case 'group': + return \Statusgruppen::find($row['solver_id']); + } + } ); - if ($task_groups) { - $storage->addTabularData(_('Courseware Aufgaben'), 'cw_task_groups', $task_groups); - } - } + + /** + * Returns the task of this TaskGroup given to $solver. + * + * @param User|Statusgruppen $solver + * + * @return Task|null + */ + public function findTaskBySolver($solver) + { + $row = DBManager::get()->fetchOne( + 'SELECT id FROM cw_tasks WHERE task_group_id = ? AND solver_id = ? AND solver_type = ?', + [ + $this->getId(), + $solver->getId(), + $solver instanceof User ? 'autor' : 'group', + ] + ); + + return empty($row) ? null : Task::find($row['id']); + } + } diff --git a/lib/models/Statusgruppen.php b/lib/models/Statusgruppen.php index e586110..e0d2575 100644 --- a/lib/models/Statusgruppen.php +++ b/lib/models/Statusgruppen.php @@ -740,4 +740,17 @@ class Statusgruppen extends SimpleORMap implements PrivacyObject } } } + + /** + * Checks if a user is a member of a group. + * + * @param string $user_id The user id + * @return boolean true if user is a member of this group + * + * @SuppressWarnings(PHPMD.StaticAccess) + */ + public static function isMemberOf(string $gruppenId, string $userId): bool + { + return StatusgruppeUser::countBySql('statusgruppe_id = ? AND user_id = ?', [$gruppenId, $userId]) !== 0; + } } diff --git a/resources/assets/javascripts/bootstrap/application.js b/resources/assets/javascripts/bootstrap/application.js index c831261..a9f53df 100644 --- a/resources/assets/javascripts/bootstrap/application.js +++ b/resources/assets/javascripts/bootstrap/application.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; import eventBus from "../lib/event-bus.ts"; /* ------------------------------------------------------------------------ diff --git a/resources/assets/javascripts/bootstrap/consultations.js b/resources/assets/javascripts/bootstrap/consultations.js index dec8f4a..ef79d9c 100644 --- a/resources/assets/javascripts/bootstrap/consultations.js +++ b/resources/assets/javascripts/bootstrap/consultations.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; $(document).on('click', '.consultation-delete-check:not(.ignore)', event => { const form = $(event.target).closest('form'); diff --git a/resources/assets/javascripts/bootstrap/copyable_links.js b/resources/assets/javascripts/bootstrap/copyable_links.js index d3675ed..521eae4 100644 --- a/resources/assets/javascripts/bootstrap/copyable_links.js +++ b/resources/assets/javascripts/bootstrap/copyable_links.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; $(document).on('click', 'a.copyable-link', function (event) { event.preventDefault(); diff --git a/resources/assets/javascripts/bootstrap/data_secure.js b/resources/assets/javascripts/bootstrap/data_secure.js index a1a5ac7..1b3b7a1 100644 --- a/resources/assets/javascripts/bootstrap/data_secure.js +++ b/resources/assets/javascripts/bootstrap/data_secure.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; /** * Secure forms or form elements by displaying a warning on page unload if diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js index bbc3d8a..1f4937d 100644 --- a/resources/assets/javascripts/bootstrap/forms.js +++ b/resources/assets/javascripts/bootstrap/forms.js @@ -1,4 +1,4 @@ -import { $gettext, $gettextInterpolate } from '../lib/gettext.js'; +import { $gettext, $gettextInterpolate } from '../lib/gettext'; // Allow fieldsets to collapse $(document).on( diff --git a/resources/assets/javascripts/bootstrap/multi_select.js b/resources/assets/javascripts/bootstrap/multi_select.js index 9e817b8..5996bd7 100644 --- a/resources/assets/javascripts/bootstrap/multi_select.js +++ b/resources/assets/javascripts/bootstrap/multi_select.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; import eventBus from "../lib/event-bus.ts"; eventBus.on('studip:set-locale', () => { diff --git a/resources/assets/javascripts/bootstrap/mvv_difflog.js b/resources/assets/javascripts/bootstrap/mvv_difflog.js index f21c368..8ade918 100644 --- a/resources/assets/javascripts/bootstrap/mvv_difflog.js +++ b/resources/assets/javascripts/bootstrap/mvv_difflog.js @@ -1,4 +1,4 @@ -import { $gettext, $gettextInterpolate } from '../lib/gettext.js'; +import { $gettext, $gettextInterpolate } from '../lib/gettext'; STUDIP.domReady(() => { $('del.diffdel').each(function() { diff --git a/resources/assets/javascripts/bootstrap/raumzeit.js b/resources/assets/javascripts/bootstrap/raumzeit.js index 2140497..241105b 100644 --- a/resources/assets/javascripts/bootstrap/raumzeit.js +++ b/resources/assets/javascripts/bootstrap/raumzeit.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; STUDIP.Dialog.handlers.header['X-Raumzeit-Update-Times'] = function(json) { var info = $.parseJSON(json); diff --git a/resources/assets/javascripts/bootstrap/resources.js b/resources/assets/javascripts/bootstrap/resources.js index 388f475..25582d4 100644 --- a/resources/assets/javascripts/bootstrap/resources.js +++ b/resources/assets/javascripts/bootstrap/resources.js @@ -1,4 +1,4 @@ -import {$gettext} from '../lib/gettext.js'; +import {$gettext} from '../lib/gettext'; STUDIP.ready(function () { //Event definitions: diff --git a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js index 8f96dcb..c106de3 100644 --- a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js +++ b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; /** * This file provides a set of global handlers. diff --git a/resources/assets/javascripts/chunks/tablesorter.js b/resources/assets/javascripts/chunks/tablesorter.js index 9cc8b0d..047c7ce 100644 --- a/resources/assets/javascripts/chunks/tablesorter.js +++ b/resources/assets/javascripts/chunks/tablesorter.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js' +import { $gettext } from '../lib/gettext' import "tablesorter/dist/js/jquery.tablesorter" import "tablesorter/dist/js/extras/jquery.tablesorter.pager.min.js" diff --git a/resources/assets/javascripts/chunks/vue.js b/resources/assets/javascripts/chunks/vue.js index cf95ed3..b98cc27 100644 --- a/resources/assets/javascripts/chunks/vue.js +++ b/resources/assets/javascripts/chunks/vue.js @@ -3,7 +3,7 @@ import Vuex from 'vuex'; import Router from "vue-router"; import eventBus from '../lib/event-bus.ts'; import GetTextPlugin from 'vue-gettext'; -import { getLocale, getVueConfig } from '../lib/gettext.js'; +import { getLocale, getVueConfig } from '../lib/gettext'; import PortalVue from 'portal-vue'; import BaseComponents from '../../../vue/base-components.js'; import BaseDirectives from "../../../vue/base-directives.js"; diff --git a/resources/assets/javascripts/cke/studip-a11y-dialog/a11y-dialog.js b/resources/assets/javascripts/cke/studip-a11y-dialog/a11y-dialog.js index 814e931..41195b0 100644 --- a/resources/assets/javascripts/cke/studip-a11y-dialog/a11y-dialog.js +++ b/resources/assets/javascripts/cke/studip-a11y-dialog/a11y-dialog.js @@ -1,6 +1,6 @@ import { Plugin } from '@ckeditor/ckeditor5-core'; import { add } from '@ckeditor/ckeditor5-utils/src/translation-service'; -import { $gettext } from '../../lib/gettext.js'; +import { $gettext } from '../../lib/gettext'; import A11YDialogEditing from './editing.js'; import A11YDialogUI from './ui.js'; diff --git a/resources/assets/javascripts/cke/studip-a11y-dialog/ui.js b/resources/assets/javascripts/cke/studip-a11y-dialog/ui.js index a2d207f..f80d703 100644 --- a/resources/assets/javascripts/cke/studip-a11y-dialog/ui.js +++ b/resources/assets/javascripts/cke/studip-a11y-dialog/ui.js @@ -1,6 +1,6 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; import { Plugin } from '@ckeditor/ckeditor5-core'; -import { $gettext } from '../../lib/gettext.js'; +import { $gettext } from '../../lib/gettext'; const a11yIcon = ''; diff --git a/resources/assets/javascripts/cke/studip-quote/StudipBlockQuote.js b/resources/assets/javascripts/cke/studip-quote/StudipBlockQuote.js index e50f8c6..0cd43e9 100644 --- a/resources/assets/javascripts/cke/studip-quote/StudipBlockQuote.js +++ b/resources/assets/javascripts/cke/studip-quote/StudipBlockQuote.js @@ -1,6 +1,6 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; -import { $gettext } from '../../lib/gettext.js'; +import { $gettext } from '../../lib/gettext'; import { Command, icons } from '@ckeditor/ckeditor5-core'; const divideIcon = diff --git a/resources/assets/javascripts/cke/wiki-link/formview.js b/resources/assets/javascripts/cke/wiki-link/formview.js index 8d82e25..8a1525a 100644 --- a/resources/assets/javascripts/cke/wiki-link/formview.js +++ b/resources/assets/javascripts/cke/wiki-link/formview.js @@ -12,7 +12,7 @@ import { addListToDropdown, } from '@ckeditor/ckeditor5-ui'; import { FocusTracker, KeystrokeHandler, Collection, Rect, isVisible } from '@ckeditor/ckeditor5-utils'; -import { $gettext } from '../../lib/gettext.js'; +import { $gettext } from '../../lib/gettext'; export default class WikiLinkFormView extends View { constructor(locale) { diff --git a/resources/assets/javascripts/cke/wiki-link/ui.js b/resources/assets/javascripts/cke/wiki-link/ui.js index a8e5f89..dba6b82 100644 --- a/resources/assets/javascripts/cke/wiki-link/ui.js +++ b/resources/assets/javascripts/cke/wiki-link/ui.js @@ -1,7 +1,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core'; import { createDropdown } from '@ckeditor/ckeditor5-ui'; import WikiLinkFormView from './formview.js'; -import { $gettext } from '../../lib/gettext.js'; +import { $gettext } from '../../lib/gettext'; const wikiIcon = ''; diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js index e824775..8981e95 100644 --- a/resources/assets/javascripts/init.js +++ b/resources/assets/javascripts/init.js @@ -77,7 +77,7 @@ import Table from './lib/table.js'; import TableOfContents from './lib/table-of-contents.js'; import Tooltip from './lib/tooltip.js'; import Tour from './lib/tour.js'; -import * as Gettext from './lib/gettext.js'; +import * as Gettext from './lib/gettext'; import UserFilter from './lib/user_filter.js'; import wysiwyg from './lib/wysiwyg.js'; import ScrollToTop from './lib/scroll_to_top.js'; diff --git a/resources/assets/javascripts/jquery-bundle.js b/resources/assets/javascripts/jquery-bundle.js index bdee32d..bd16422 100644 --- a/resources/assets/javascripts/jquery-bundle.js +++ b/resources/assets/javascripts/jquery-bundle.js @@ -1,6 +1,6 @@ import 'expose-loader?exposes[]=$&exposes[]=jQuery!jquery'; -import { setLocale } from './lib/gettext.js'; +import { setLocale } from './lib/gettext'; import 'jquery-ui/ui/widget.js'; import 'jquery-ui/ui/position.js'; @@ -76,7 +76,7 @@ import 'blueimp-file-upload/js/jquery.iframe-transport.js'; import './jquery/autoresize.jquery.min.js'; -import { $gettext } from './lib/gettext.js'; +import { $gettext } from './lib/gettext'; // Create jQuery "plugin" that just reverses the elements' order. This is // neccessary since the navigation is built and afterwards, we need to diff --git a/resources/assets/javascripts/lib/admission.js b/resources/assets/javascripts/lib/admission.js index 7cf8c88..df62bbe 100644 --- a/resources/assets/javascripts/lib/admission.js +++ b/resources/assets/javascripts/lib/admission.js @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------ * Anmeldeverfahren und -sets * ------------------------------------------------------------------------ */ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; import Dialog from './dialog.js'; const Admission = { diff --git a/resources/assets/javascripts/lib/big_image_handler.js b/resources/assets/javascripts/lib/big_image_handler.js index 5130997..55e9b38 100644 --- a/resources/assets/javascripts/lib/big_image_handler.js +++ b/resources/assets/javascripts/lib/big_image_handler.js @@ -18,7 +18,7 @@ * @license GPL2 or any later version * @since Stud.IP 3.4 */ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; var pixelRatio = window.devicePixelRatio || 1, dataAttribute = 'big-image-handled'; diff --git a/resources/assets/javascripts/lib/calendar.js b/resources/assets/javascripts/lib/calendar.js index 2f1cd67..2d995b5 100644 --- a/resources/assets/javascripts/lib/calendar.js +++ b/resources/assets/javascripts/lib/calendar.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; import eventBus from "./event-bus.ts"; eventBus.on('studip:set-locale', () => { diff --git a/resources/assets/javascripts/lib/dialog.js b/resources/assets/javascripts/lib/dialog.js index 8c22d1c..b5cab54 100644 --- a/resources/assets/javascripts/lib/dialog.js +++ b/resources/assets/javascripts/lib/dialog.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; import parseOptions from './parse_options.js'; import extractCallback from './extract_callback.js'; import Overlay from './overlay.js'; diff --git a/resources/assets/javascripts/lib/files.js b/resources/assets/javascripts/lib/files.js index 7b628f6..d05112d 100644 --- a/resources/assets/javascripts/lib/files.js +++ b/resources/assets/javascripts/lib/files.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; import Dialog from './dialog.js'; import FilesTable from '../../../vue/components/FilesTable.vue'; diff --git a/resources/assets/javascripts/lib/folders.js b/resources/assets/javascripts/lib/folders.js index ced430d..6cd23c5 100644 --- a/resources/assets/javascripts/lib/folders.js +++ b/resources/assets/javascripts/lib/folders.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; import Dialog from './dialog.js'; const Folders = { diff --git a/resources/assets/javascripts/lib/forum.js b/resources/assets/javascripts/lib/forum.js index c2f0d7c..385ec12 100644 --- a/resources/assets/javascripts/lib/forum.js +++ b/resources/assets/javascripts/lib/forum.js @@ -1,4 +1,4 @@ -import { $gettext } from "./gettext.js"; +import { $gettext } from "./gettext"; import eventBus from "./event-bus.ts"; eventBus.on('studip:set-locale', () => { diff --git a/resources/assets/javascripts/lib/gettext.js b/resources/assets/javascripts/lib/gettext.js deleted file mode 100644 index 5742466..0000000 --- a/resources/assets/javascripts/lib/gettext.js +++ /dev/null @@ -1,95 +0,0 @@ -import { translate } from 'vue-gettext'; -import defaultTranslations from '../../../../locale/de/LC_MESSAGES/js-resources.json'; -import eventBus from './event-bus.ts'; - -const DEFAULT_LANG = 'de_DE'; -const DEFAULT_LANG_NAME = 'Deutsch'; - -const state = getInitialState(); - -const $gettext = translate.gettext.bind(translate); -const $ngettext = translate.ngettext.bind(translate); -const $gettextInterpolate = translate.gettextInterpolate.bind(translate); - -export { $gettext, $ngettext, $gettextInterpolate, translate, getLocale, setLocale, getVueConfig }; - -function getLocale() { - return state.locale; -} - -async function setLocale(locale = getInitialLocale()) { - if (!(locale in getInstalledLanguages())) { - throw new Error('Invalid locale: ' + locale); - } - - state.locale = locale; - if (state.translations[state.locale] === null) { - const translations = await getTranslations(state.locale); - state.translations[state.locale] = translations; - } - - translate.initTranslations(state.translations, { - getTextPluginMuteLanguages: [DEFAULT_LANG], - getTextPluginSilent: false, - language: state.locale, - silent: false, - }); - - eventBus.emit('studip:set-locale', state.locale); -} - -function getVueConfig() { - const availableLanguages = Object.entries(getInstalledLanguages()).reduce((memo, [lang, { name }]) => { - memo[lang] = name; - - return memo; - }, {}); - - return { - availableLanguages, - defaultLanguage: DEFAULT_LANG, - muteLanguages: [DEFAULT_LANG], - silent: false, - translations: state.translations, - }; -} - -function getInitialState() { - const translations = Object.entries(getInstalledLanguages()).reduce((memo, [lang]) => { - memo[lang] = lang === DEFAULT_LANG ? defaultTranslations : null; - - return memo; - }, {}); - - return { - locale: DEFAULT_LANG, - translations, - }; -} - -function getInitialLocale() { - for (const [lang, { selected }] of Object.entries(getInstalledLanguages())) { - if (selected) { - return lang; - } - } - - return DEFAULT_LANG; -} - -function getInstalledLanguages() { - return window?.STUDIP?.INSTALLED_LANGUAGES ?? { [DEFAULT_LANG]: { name: DEFAULT_LANG_NAME, selected: true } }; -} - -async function getTranslations(locale) { - try { - const language = locale.split(/[_-]/)[0]; - const translation = await import(`../../../../locale/${language}/LC_MESSAGES/js-resources.json`); - - return translation; - } catch (exception) { - console.error('Could not load locale: "' + locale + '"', exception); - - return {}; - } -} diff --git a/resources/assets/javascripts/lib/gettext.ts b/resources/assets/javascripts/lib/gettext.ts new file mode 100644 index 0000000..23daaaa --- /dev/null +++ b/resources/assets/javascripts/lib/gettext.ts @@ -0,0 +1,114 @@ +import { translate } from 'vue-gettext'; +import * as defaultTranslations from '../../../../locale/de/LC_MESSAGES/js-resources.json'; +import eventBus from './event-bus'; + +interface StringDict { + [key: string]: string; +} + +interface InstalledLanguage { + name: string; + selected: boolean; +} + +interface InstalledLanguages { + [key: string]: InstalledLanguage; +} + +type TranslationDict = StringDict; + +interface TranslationDicts { + [key: string]: TranslationDict | null; +} + +const DEFAULT_LANG = 'de_DE'; +const DEFAULT_LANG_NAME = 'Deutsch'; + +const state = getInitialState(); + +const $gettext = translate.gettext.bind(translate); +const $ngettext = translate.ngettext.bind(translate); +const $gettextInterpolate = translate.gettextInterpolate.bind(translate); + +export { $gettext, $ngettext, $gettextInterpolate, translate, getLocale, setLocale, getVueConfig }; + +function getLocale() { + return state.locale; +} + +async function setLocale(locale = getInitialLocale()) { + if (!(locale in getInstalledLanguages())) { + throw new Error('Invalid locale: ' + locale); + } + + state.locale = locale; + if (state.translations[state.locale] === null) { + const translations: TranslationDict = await getTranslations(state.locale); + state.translations[state.locale] = translations; + } + + translate.initTranslations(state.translations, { + getTextPluginMuteLanguages: [DEFAULT_LANG], + getTextPluginSilent: false, + language: state.locale, + silent: false, + }); + + eventBus.emit('studip:set-locale', state.locale); +} + +function getVueConfig() { + const availableLanguages = Object.entries(getInstalledLanguages()).reduce((memo, [lang, { name }]) => { + memo[lang] = name; + + return memo; + }, {} as StringDict); + + return { + availableLanguages, + defaultLanguage: DEFAULT_LANG, + muteLanguages: [DEFAULT_LANG], + silent: false, + translations: state.translations, + }; +} + +function getInitialState() { + const translations: TranslationDicts = Object.entries(getInstalledLanguages()).reduce((memo, [lang]) => { + memo[lang] = lang === DEFAULT_LANG ? defaultTranslations : null; + + return memo; + }, {} as TranslationDicts); + + return { + locale: DEFAULT_LANG, + translations, + }; +} + +function getInitialLocale() { + for (const [lang, { selected }] of Object.entries(getInstalledLanguages())) { + if (selected) { + return lang; + } + } + + return DEFAULT_LANG; +} + +function getInstalledLanguages(): InstalledLanguages { + return window?.STUDIP?.INSTALLED_LANGUAGES ?? { [DEFAULT_LANG]: { name: DEFAULT_LANG_NAME, selected: true } }; +} + +async function getTranslations(locale: string): Promise { + try { + const language = locale.split(/[_-]/)[0]; + const translation = await import(`../../../../locale/${language}/LC_MESSAGES/js-resources.json`); + + return translation; + } catch (exception) { + console.error('Could not load locale: "' + locale + '"', exception); + + return {}; + } +} diff --git a/resources/assets/javascripts/lib/instschedule.js b/resources/assets/javascripts/lib/instschedule.js index af438c2..d925bfb 100644 --- a/resources/assets/javascripts/lib/instschedule.js +++ b/resources/assets/javascripts/lib/instschedule.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; import Dialog from './dialog.js'; const Instschedule = { diff --git a/resources/assets/javascripts/lib/jsupdater.js b/resources/assets/javascripts/lib/jsupdater.js index 7888f29..5069af0 100644 --- a/resources/assets/javascripts/lib/jsupdater.js +++ b/resources/assets/javascripts/lib/jsupdater.js @@ -10,7 +10,7 @@ * * Refer to the according function definitions for further info. * ------------------------------------------------------------------------ */ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; import Dialog from './dialog.js'; let active = false; diff --git a/resources/assets/javascripts/lib/lightbox.js b/resources/assets/javascripts/lib/lightbox.js index 134cfca..09bfda2 100644 --- a/resources/assets/javascripts/lib/lightbox.js +++ b/resources/assets/javascripts/lib/lightbox.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; import Dialog from './dialog.js'; function sprintf(string) { diff --git a/resources/assets/javascripts/lib/messages.js b/resources/assets/javascripts/lib/messages.js index dbb2793..7ce5328 100644 --- a/resources/assets/javascripts/lib/messages.js +++ b/resources/assets/javascripts/lib/messages.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; import Markup from './markup.js'; const Messages = { diff --git a/resources/assets/javascripts/lib/multi_person_search.js b/resources/assets/javascripts/lib/multi_person_search.js index f5ba046..b876bc9 100644 --- a/resources/assets/javascripts/lib/multi_person_search.js +++ b/resources/assets/javascripts/lib/multi_person_search.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; const MultiPersonSearch = { init: function() { diff --git a/resources/assets/javascripts/lib/multi_select.js b/resources/assets/javascripts/lib/multi_select.js index b4abeb9..6c1b387 100644 --- a/resources/assets/javascripts/lib/multi_select.js +++ b/resources/assets/javascripts/lib/multi_select.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; /** * Turns a select-box into an easy to use multiple select-box diff --git a/resources/assets/javascripts/lib/oer.js b/resources/assets/javascripts/lib/oer.js index 112d155..17f0186 100644 --- a/resources/assets/javascripts/lib/oer.js +++ b/resources/assets/javascripts/lib/oer.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; const OER = { periodicalPushData: function () { diff --git a/resources/assets/javascripts/lib/overlapping.js b/resources/assets/javascripts/lib/overlapping.js index 73ab32f..a6aa4b8 100644 --- a/resources/assets/javascripts/lib/overlapping.js +++ b/resources/assets/javascripts/lib/overlapping.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; const Overlapping = { @@ -91,4 +91,4 @@ const Overlapping = { } }; -export default Overlapping; \ No newline at end of file +export default Overlapping; diff --git a/resources/assets/javascripts/lib/overlay.js b/resources/assets/javascripts/lib/overlay.js index 52d1c94..ffe8ed1 100644 --- a/resources/assets/javascripts/lib/overlay.js +++ b/resources/assets/javascripts/lib/overlay.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; const Overlay = { delay: 300, diff --git a/resources/assets/javascripts/lib/qr_code.js b/resources/assets/javascripts/lib/qr_code.js index 3db4fd8..ada0cb3 100644 --- a/resources/assets/javascripts/lib/qr_code.js +++ b/resources/assets/javascripts/lib/qr_code.js @@ -1,4 +1,4 @@ -import { $gettext } from "./gettext.js"; +import { $gettext } from "./gettext"; import Dialog from "./dialog.js"; const QRCode = { diff --git a/resources/assets/javascripts/lib/questionnaire.js b/resources/assets/javascripts/lib/questionnaire.js index 8fbbbb0..2bca8c6 100644 --- a/resources/assets/javascripts/lib/questionnaire.js +++ b/resources/assets/javascripts/lib/questionnaire.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; import md5 from 'md5'; //import html2canvas from "html2canvas"; //import {jsPDF} from "jspdf"; diff --git a/resources/assets/javascripts/lib/quick_search.js b/resources/assets/javascripts/lib/quick_search.js index 806debd..627bffa 100644 --- a/resources/assets/javascripts/lib/quick_search.js +++ b/resources/assets/javascripts/lib/quick_search.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; /* ------------------------------------------------------------------------ * QuickSearch inputs diff --git a/resources/assets/javascripts/lib/raumzeit.js b/resources/assets/javascripts/lib/raumzeit.js index 5cd5e55..c28dbae 100644 --- a/resources/assets/javascripts/lib/raumzeit.js +++ b/resources/assets/javascripts/lib/raumzeit.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; const Raumzeit = { disableBookableRooms: function(icon) { diff --git a/resources/assets/javascripts/lib/register.js b/resources/assets/javascripts/lib/register.js index da81132..de7b666 100644 --- a/resources/assets/javascripts/lib/register.js +++ b/resources/assets/javascripts/lib/register.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; const register = { re_username: null, diff --git a/resources/assets/javascripts/lib/resources.js b/resources/assets/javascripts/lib/resources.js index 2375eee..9acb2e3 100644 --- a/resources/assets/javascripts/lib/resources.js +++ b/resources/assets/javascripts/lib/resources.js @@ -1,4 +1,4 @@ -import { $gettext } from '../lib/gettext.js'; +import { $gettext } from '../lib/gettext'; class Resources { diff --git a/resources/assets/javascripts/lib/schedule.js b/resources/assets/javascripts/lib/schedule.js index b7c9d37..f3e5123 100644 --- a/resources/assets/javascripts/lib/schedule.js +++ b/resources/assets/javascripts/lib/schedule.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; import Calendar from './calendar.js'; import Dialog from './dialog.js'; diff --git a/resources/assets/javascripts/lib/tour.js b/resources/assets/javascripts/lib/tour.js index b93be07..8094b2b 100644 --- a/resources/assets/javascripts/lib/tour.js +++ b/resources/assets/javascripts/lib/tour.js @@ -1,4 +1,4 @@ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; /* ------------------------------------------------------------------------ * Stud.IP Tour diff --git a/resources/assets/javascripts/lib/user_filter.js b/resources/assets/javascripts/lib/user_filter.js index 450af34..25b8488 100644 --- a/resources/assets/javascripts/lib/user_filter.js +++ b/resources/assets/javascripts/lib/user_filter.js @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------ * Bedingungen zur Auswahl von Stud.IP-Nutzern * ------------------------------------------------------------------------ */ -import { $gettext } from './gettext.js'; +import { $gettext } from './gettext'; import Dialog from './dialog.js'; const UserFilter = { diff --git a/resources/assets/javascripts/mvv.js b/resources/assets/javascripts/mvv.js index 12d2653..a339624 100644 --- a/resources/assets/javascripts/mvv.js +++ b/resources/assets/javascripts/mvv.js @@ -1,4 +1,4 @@ -import { $gettext } from './lib/gettext.js'; +import { $gettext } from './lib/gettext'; jQuery(function ($) { $(document).on('click', 'a.mvv-load-in-new-row', function () { diff --git a/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js b/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js index 2462ff9..adde0ce 100644 --- a/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js +++ b/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js @@ -1,4 +1,4 @@ -import { $gettext } from './lib/gettext.js'; +import { $gettext } from './lib/gettext'; /** diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js index 60a3cfa..f581295 100644 --- a/resources/assets/javascripts/studip-ui.js +++ b/resources/assets/javascripts/studip-ui.js @@ -1,4 +1,4 @@ -import { $gettext } from './lib/gettext.js'; +import { $gettext } from './lib/gettext'; import eventBus from "./lib/event-bus.ts"; /** diff --git a/resources/vue-gettext.d.ts b/resources/vue-gettext.d.ts new file mode 100644 index 0000000..b3f4c66 --- /dev/null +++ b/resources/vue-gettext.d.ts @@ -0,0 +1,17 @@ +declare module "vue-gettext" { + import GettextPlugin from 'vue-gettext'; + + declare namespace translate { + function getTranslation(msgid: any, n?: number, context?: any, defaultPlural?: any, language?: string): any; + function gettext(msgid: any, language?: string): any; + function pgettext(context: any, msgid: any, language?: string): any; + function ngettext(msgid: any, plural: any, n: any, language?: string): any; + function npgettext(context: any, msgid: any, plural: any, n: any, language?: string): any; + function initTranslations(translations: any, config: any): void; + const gettextInterpolate: any; + } + + export { translate }; + + export default GettextPlugin; +} diff --git a/resources/vue/components/StudipDate.vue b/resources/vue/components/StudipDate.vue new file mode 100644 index 0000000..2e30b9d --- /dev/null +++ b/resources/vue/components/StudipDate.vue @@ -0,0 +1,27 @@ + + + diff --git a/resources/vue/components/courseware/CoursewareDashboardStudents.vue b/resources/vue/components/courseware/CoursewareDashboardStudents.vue deleted file mode 100644 index bac31a6..0000000 --- a/resources/vue/components/courseware/CoursewareDashboardStudents.vue +++ /dev/null @@ -1,481 +0,0 @@ -