From 74603117e50e764dfb0233d49cb99ffafaccac54 Mon Sep 17 00:00:00 2001 From: Rasmus Fuhse Date: Thu, 13 Jul 2023 13:14:56 +0000 Subject: Resolve "Restrukturierung der Veranstaltungsverwaltung inklusive Mehr-Seite" Closes #2440 Merge request studip/studip!1695 --- app/controllers/admin/plugin.php | 45 +++ app/controllers/course/basicdata.php | 58 ++- app/controllers/course/change_view.php | 2 +- app/controllers/course/contentmodules.php | 293 +++++++++++++++ app/controllers/course/plus.php | 399 --------------------- app/views/admin/plugin/edit_description.php | 1 + app/views/admin/plugin/index.php | 10 + app/views/course/contentmodules/index.php | 1 + app/views/course/contentmodules/info.php | 56 +++ app/views/course/contentmodules/rename.php | 24 ++ app/views/course/plus/edittool.php | 25 -- app/views/course/plus/index.php | 245 ------------- app/views/course/plus/sorttools.php | 15 - .../5.4.10_contentmodules_description.php | 47 +++ .../Routes/ConfigValues/ConfigValuesUpdate.php | 1 + lib/classes/JsonApi/Schemas/Course.php | 22 ++ lib/classes/SemClass.class.php | 8 +- lib/classes/forms/DatetimepickerInput.php | 9 + lib/classes/forms/Form.php | 26 +- lib/classes/forms/InfoInput.php | 20 ++ lib/classes/forms/Part.php | 16 +- lib/classes/forms/WysiwygInput.php | 30 ++ lib/classes/sidebar/OptionsWidget.php | 3 +- lib/models/Plugin.php | 25 ++ lib/models/ToolActivation.php | 2 +- lib/modules/Blubber.class.php | 4 +- lib/modules/ConsultationModule.class.php | 1 + lib/modules/CoreAdmin.class.php | 6 +- lib/modules/CoreCalendar.class.php | 1 + lib/modules/CoreDocuments.class.php | 3 +- lib/modules/CoreElearningInterface.class.php | 1 + lib/modules/CoreForum.class.php | 1 + lib/modules/CoreOverview.class.php | 5 +- lib/modules/CoreParticipants.class.php | 1 + lib/modules/CorePersonal.class.php | 1 + lib/modules/CoreSchedule.class.php | 1 + lib/modules/CoreScm.class.php | 1 + lib/modules/CoreStudygroupAdmin.class.php | 1 + lib/modules/CoreWiki.class.php | 3 +- lib/modules/CoursewareModule.class.php | 7 +- lib/modules/FeedbackModule.class.php | 1 + lib/modules/GradebookModule.class.php | 1 + lib/modules/IliasInterfaceModule.class.php | 1 + lib/modules/LtiToolModule.class.php | 1 + lib/navigation/CourseNavigation.php | 21 +- lib/plugins/core/CorePlugin.php | 35 ++ lib/plugins/core/StudIPPlugin.class.php | 39 ++ lib/plugins/engine/PluginManager.class.php | 11 +- lib/seminar_open.php | 13 - .../assets/javascripts/bootstrap/contentmodules.js | 40 +++ resources/assets/javascripts/bootstrap/forms.js | 13 +- resources/assets/javascripts/entry-base.js | 1 + resources/assets/javascripts/init.js | 2 - resources/assets/javascripts/lib/plus.js | 23 -- resources/assets/stylesheets/scss/forms.scss | 3 + resources/assets/stylesheets/scss/plus.scss | 79 ---- resources/assets/stylesheets/studip.scss | 1 - resources/vue/base-components.js | 2 + resources/vue/components/ContentModules.vue | 218 +++++++++++ resources/vue/components/ContentModulesControl.vue | 91 +++++ .../vue/components/ContentModulesEditTiles.vue | 165 +++++++++ .../vue/components/ContentmodulesEditTable.vue | 100 ++++++ resources/vue/components/Datetimepicker.vue | 8 +- resources/vue/components/I18nTextarea.vue | 2 + resources/vue/mixins/ContentModulesMixin.js | 125 +++++++ resources/vue/store/ContentModulesStore.js | 124 +++++++ templates/forms/form.php | 3 +- templates/forms/i18n_formatted_input.php | 1 + templates/forms/i18n_textarea_input.php | 1 + templates/forms/info_input.php | 8 + templates/forms/wysiwyg_input.php | 16 + 71 files changed, 1725 insertions(+), 844 deletions(-) create mode 100644 app/controllers/course/contentmodules.php delete mode 100644 app/controllers/course/plus.php create mode 100644 app/views/admin/plugin/edit_description.php create mode 100644 app/views/course/contentmodules/index.php create mode 100644 app/views/course/contentmodules/info.php create mode 100644 app/views/course/contentmodules/rename.php delete mode 100644 app/views/course/plus/edittool.php delete mode 100644 app/views/course/plus/index.php delete mode 100644 app/views/course/plus/sorttools.php create mode 100644 db/migrations/5.4.10_contentmodules_description.php create mode 100644 lib/classes/forms/InfoInput.php create mode 100644 lib/classes/forms/WysiwygInput.php create mode 100644 lib/models/Plugin.php create mode 100644 resources/assets/javascripts/bootstrap/contentmodules.js delete mode 100644 resources/assets/javascripts/lib/plus.js delete mode 100644 resources/assets/stylesheets/scss/plus.scss create mode 100644 resources/vue/components/ContentModules.vue create mode 100644 resources/vue/components/ContentModulesControl.vue create mode 100644 resources/vue/components/ContentModulesEditTiles.vue create mode 100644 resources/vue/components/ContentmodulesEditTable.vue create mode 100644 resources/vue/mixins/ContentModulesMixin.js create mode 100644 resources/vue/store/ContentModulesStore.js create mode 100644 templates/forms/info_input.php create mode 100644 templates/forms/wysiwyg_input.php diff --git a/app/controllers/admin/plugin.php b/app/controllers/admin/plugin.php index 7a87d8f..ef63517 100644 --- a/app/controllers/admin/plugin.php +++ b/app/controllers/admin/plugin.php @@ -571,4 +571,49 @@ class Admin_PluginController extends AuthenticatedController } } + public function edit_description_action(Plugin $plugin) + { + $this->plugin = PluginManager::getInstance()->getPluginById($plugin->getId()); + $this->metadata = $this->plugin->getMetadata(); + $this->form = \Studip\Forms\Form::fromSORM($plugin, [ + 'legend' => _('Pluginbeschreibung'), + 'fields' => [ + 'description' => [ + 'label' => _('Beschreibung'), + 'type' => 'i18n_formatted' + ], + 'manifest_info_de' => [ + 'label' => _('Standardbeschreibung des Plugins'), + 'type' => 'info', + 'value' => $this->metadata['descriptionlong'] ?? $this->metadata['description'], + 'if' => "STUDIPFORM_SELECTEDLANGUAGES.description === 'de_DE'" + ], + 'manifest_info_en' => [ + 'label' => sprintf(_('Standardbeschreibung des Plugins (%s)'), _('Englisch')), + 'type' => 'info', + 'value' => $this->metadata['descriptionlong_en'] ?? $this->metadata['description_en'], + 'if' => "STUDIPFORM_SELECTEDLANGUAGES.description === 'en_GB'" + ], + 'decription_mode' => [ + 'label' => _('Modus der neuen Beschreibung'), + 'type' => 'select', + 'options' => [ + 'add' => _('Hinzufügen zur Standardbeschreibung'), + 'override_description' => _('Standardbeschreibung überschreiben'), + 'replace_all' => _('Beschreibungsfenster komplett ersetzen durch Beschreibung') + ] + ], + 'highlight_until' => [ + 'label' => _('In Veranstaltungen bewerben bis (oder leer lassen)'), + 'type' => 'datetimepicker' + ], + 'highlight_text' => [ + 'label' => _('Bewerbungs-Infotext') + ] + ] + ])->autoStore() + //->setDebugMode(true) + ->setURL(URLHelper::getURL('dispatch.php/admin/plugin/index')); + } + } diff --git a/app/controllers/course/basicdata.php b/app/controllers/course/basicdata.php index b9ba67e..937ec0f 100644 --- a/app/controllers/course/basicdata.php +++ b/app/controllers/course/basicdata.php @@ -290,7 +290,8 @@ class Course_BasicdataController extends AuthenticatedController } //Daten sammeln: - $sem = Seminar::getInstance($this->course_id); + $course = Course::find($this->course_id); + $sem = new Seminar($course); $data = $sem->getData(); //Erster, zweiter und vierter Reiter des Akkordions: Grundeinstellungen @@ -365,10 +366,51 @@ class Course_BasicdataController extends AuthenticatedController $widget = new ActionsWidget(); + $sem_create_perm = in_array(Config::get()->SEM_CREATE_PERM, ['root','admin','dozent']) ? Config::get()->SEM_CREATE_PERM : 'dozent'; + if ($GLOBALS['perm']->have_perm($sem_create_perm)) { + if (!LockRules::check(Context::getId(), 'seminar_copy')) { + $widget->addLink( + _('Veranstaltung kopieren'), + $this->url_for( + 'course/wizard/copy/' . $this->course_id, + ['studip_ticket' => Seminar_Session::get_ticket()] + ), + Icon::create('seminar') + ); + } + } $widget->addLink(_('Bild ändern'), - $this->url_for('avatar/update/course', $course_id), - Icon::create('edit') + $this->url_for('avatar/update/course', $this->course_id), + Icon::create('edit') ); + if ($GLOBALS['perm']->have_perm('admin')) { + $is_locked = $course->lock_rule; + $widget->addLink( + _('Sperrebene ändern') . ' (' . ($is_locked ? _('gesperrt') : _('nicht gesperrt')) . ')', + $this->url_for( + 'course/management/lock', + ['studip_ticket' => Seminar_Session::get_ticket()] + ), + Icon::create('lock-' . ($is_locked ? 'locked' : 'unlocked')) + )->asDialog('size=auto'); + } + + if ( + (Config::get()->ALLOW_DOZENT_VISIBILITY || $GLOBALS['perm']->have_perm('admin')) + && !LockRules::Check($this->course_id, 'seminar_visibility') + ) { + $is_visible = $course->visible; + if ($course->isOpenEnded() || $course->end_semester->visible) { + $widget->addLink( + $is_visible ? _('Veranstaltung verstecken') : _('Veranstaltung sichtbar schalten'), + $this->url_for( + 'course/management/change_visibility', + ['studip_ticket' => Seminar_Session::get_ticket()] + ), + Icon::create('visibility-' . ($is_visible ? 'visible' : 'invisible')) + ); + } + } if ($this->deputies_enabled) { if (Deputy::isDeputy($GLOBALS['user']->id, $this->course_id)) { @@ -388,6 +430,16 @@ class Course_BasicdataController extends AuthenticatedController ); } } + if (Config::get()->ALLOW_DOZENT_DELETE || $GLOBALS['perm']->have_perm('admin')) { + $widget->addLink( + _('Veranstaltung löschen'), + $this->url_for( + 'course/archive/confirm', + ['studip_ticket' => Seminar_Session::get_ticket()] + ), + Icon::create('trash') + )->asDialog('size=auto'); + } $sidebar->addWidget($widget); if ($GLOBALS['perm']->have_studip_perm('admin', $this->course_id)) { $widget = new CourseManagementSelectWidget(); diff --git a/app/controllers/course/change_view.php b/app/controllers/course/change_view.php index 58cc995..156a68a 100644 --- a/app/controllers/course/change_view.php +++ b/app/controllers/course/change_view.php @@ -48,6 +48,6 @@ class Course_ChangeViewController extends AuthenticatedController public function reset_changed_view_action() { unset($_SESSION["seminar_change_view_{$this->course_id}"]); - $this->relocate('course/management'); + $this->relocate('course/contentmodules'); } } diff --git a/app/controllers/course/contentmodules.php b/app/controllers/course/contentmodules.php new file mode 100644 index 0000000..2e4907a --- /dev/null +++ b/app/controllers/course/contentmodules.php @@ -0,0 +1,293 @@ +sem = Context::get(); + $this->sem_class = $this->sem->getSemClass(); + } else { + $this->sem = Context::get(); + $this->sem_class = SemClass::getDefaultInstituteClass($this->sem['type']); + } + $this->modules = $this->getModules($this->sem); + + $this->highlighted_modules = []; + foreach ($this->modules as $module) { + if ($module['highlighted']) { + $this->highlighted_modules[] = $module['id']; + } + } + + if (Context::isCourse()) { + $actions = new ActionsWidget(); + + $actions->addLink( + _('Studierendenansicht simulieren'), + URLHelper::getURL('dispatch.php/course/change_view/set_changed_view'), + Icon::create('visibility-invisible') + ); + Sidebar::Get()->addWidget($actions); + } + + $views = Sidebar::Get()->addWidget(new ViewsWidget()); + $views->id = 'tool-view-switch'; + $views->addLink( + _('Kachelansicht'), + '#tiles' + )->setActive($GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY); + $views->addLink( + _('Tabellarische Ansicht'), + '#tabular' + )->setActive(!$GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY); + + $this->categories = []; + foreach ($this->modules as $i => $module) { + if ($module['category'] && !in_array($module['category'], $this->categories)) { + $this->categories[] = $module['category']; + } + if (!$module['category']) { + if (!in_array(_('Sonstige'), $this->categories)) { + $this->categories[] = _('Sonstige'); + } + $this->modules[$i]['category'] = _('Sonstige'); + } + } + sort($this->categories); + + $filter_widget = Sidebar::Get()->addWidget(new OptionsWidget()); + $filter_widget->id = 'tool-filter-category'; + $filter_widget->setTitle(_('Filter nach Kategorie')); + $filter_widget->addRadioButton( + _('Alle Kategorien'), + '#', + true + ); + foreach ($this->categories as $category) { + $filter_widget->addRadioButton( + $category, + '#' + ); + } + + if ( + Context::isCourse() + && $GLOBALS['perm']->have_studip_perm('admin', Context::getId()) + && !$this->sem_class['studygroup_mode'] + ) { + $widget = new CourseManagementSelectWidget(); + Sidebar::Get()->addWidget($widget); + } + + PageLayout::addHeadElement('script', [ + 'type' => 'text/javascript', + ], sprintf( + 'window.ContentModulesStoreData = %s;', + json_encode([ + 'setCategories' => $this->categories, + 'setHighlighted' => $this->highlighted_modules, + 'setModules' => array_values($this->modules), + 'setUserId' => User::findCurrent()->id, + 'setView' => $GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY ? 'tiles' : 'table', + ]) + )); + } + + public function trigger_action() + { + $context = Context::get(); + + $required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin'; + if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) { + throw new AccessDeniedException(); + } + if (Request::isPost()) { + if ($context->getRangeType() === 'course') { + $sem_class = $context->getSemClass(); + } else { + $sem_class = SemClass::getDefaultInstituteClass($context->type); + } + $moduleclass = Request::get('moduleclass'); + $active = Request::bool('active', false); + $module = new $moduleclass; + if ($module->isActivatableForContext($context)) { + PluginManager::getInstance()->setPluginActivated($module->getPluginId(), $context->getId(), $active); + } + if ($active) { + $active_tool = ToolActivation::find([$context->id, $module->getPluginId()]); + $default_position = array_search(get_class($module), $sem_class->getActivatedModules()); + if ($default_position !== false && $active_tool) { + $active_tool->position = $default_position; + $active_tool->store(); + } + } + //$this->redirect("course/contentmodules/trigger", ['cid' => $context->getId()]); + } + $template = $GLOBALS['template_factory']->open('tabs.php'); + $template->navigation = Navigation::getItem('/course'); + Navigation::getItem('/course/admin')->setActive(true); + $this->render_json([ + 'tabs' => $template->render(), + 'position' => $active_tool->position + ]); + } + + public function reorder_action() + { + $context = Context::get(); + + $required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin'; + if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) { + throw new AccessDeniedException(); + } + if (Request::isPost()) { + $position = 0; + foreach (Request::getArray('order') as $plugin_id) { + $tool = ToolActivation::find([$context->getId(), $plugin_id]); + $tool->position = $position++; + $tool->store(); + } + $this->redirect($this->reorderURL()); + return; + } + Navigation::getItem('/course/admin')->setActive(true); + $template = $GLOBALS['template_factory']->open('tabs.php'); + $template->navigation = Navigation::getItem('/course'); + $this->render_json([ + 'tabs' => $template->render() + ]); + } + + public function change_visibility_action() + { + if (!Request::isPost()) { + throw new AccessDeniedException(); + } + $context = Context::get(); + + $required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin'; + if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) { + throw new AccessDeniedException(); + } + $moduleclass = Request::get('moduleclass'); + $module = new $moduleclass; + + $active_tool = ToolActivation::find([$context->id, $module->getPluginId()]); + $metadata = $active_tool->metadata->getArrayCopy(); + if (Request::bool('visible')) { + unset($metadata['visibility']); + } else { + $metadata['visibility'] = 'tutor'; + } + $active_tool['metadata'] = $metadata; + $active_tool->store(); + + $this->render_json([ + 'visibility' => $active_tool->getVisibilityPermission() + ]); + } + + public function tiles_display_action() + { + if (Request::isPost()) { + $GLOBALS['user']->cfg->store( + 'CONTENTMODULES_TILED_DISPLAY', + Request::get('view') === 'tiles' + ); + } + $this->render_nothing(); + } + + public function rename_action($module_id) + { + $context = Context::get(); + + $required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin'; + if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) { + throw new AccessDeniedException(); + } + $this->module = PluginManager::getInstance()->getPluginById($module_id); + $this->metadata = $this->module->getMetadata(); + PageLayout::setTitle(_('Werkzeug umbenennen')); + $this->tool = ToolActivation::find([$context->id, $module_id]); + if (Request::isPost()) { + $metadata = $this->tool->metadata->getArrayCopy(); + if (!trim(Request::get('displayname')) || Request::submitted('delete')) { + unset($metadata['displayname']); + } else { + $metadata['displayname'] = trim(Request::get('displayname')); + } + $this->tool['metadata'] = $metadata; + $this->tool->store(); + $this->redirect('course/contentmodules/index'); + } + } + + public function info_action($plugin_id) + { + $this->plugin = PluginManager::getInstance()->getPluginById($plugin_id); + $this->metadata = $this->plugin->getMetadata(); + PageLayout::setTitle(sprintf(_('Informationen über %s'), $this->metadata['displayname'])); + } + + private function getModules(Range $context) + { + $list = []; + + foreach (PluginEngine::getPlugins('StudipModule') as $plugin) { + if (!$plugin->isActivatableForContext($context)) { + continue; + } + + if (!$this->sem_class->isModuleAllowed(get_class($plugin))) { + continue; + } + + $info = $plugin->getMetadata(); + + $plugin_id = $plugin->getPluginId(); + + $tool = ToolActivation::find([$context->getRangeId(), $plugin->getPluginId()]); + $toolname = $info['displayname'] ?? $plugin->getPluginname(); + if ($tool && $tool->metadata['displayname']) { + $displayname = $tool->getDisplayname() . ' (' . $toolname . ')'; + } else { + $displayname = $toolname; + } + $visibility = $tool ? $tool->getVisibilityPermission() : 'nobody'; + + $metadata = $plugin->getMetadata(); + $list[$plugin_id] = [ + 'id' => $plugin_id, + 'moduleclass' => get_class($plugin), + 'position' => $tool ? $tool->position : null, + 'toolname' => $toolname, + 'displayname' => $displayname, + 'visibility' => $visibility, + 'active' => (bool) $tool, + ]; + if ($metadata['icon_clickable']) { + $list[$plugin_id]['icon'] = $metadata['icon_clickable'] instanceof Icon + ? $metadata['icon_clickable']->asImagePath() + : Icon::create($plugin->getPluginURL().'/'.$metadata['icon_clickable'])->asImagePath(); + } elseif ($metadata['icon']) { + $list[$plugin_id]['icon'] = $metadata['icon'] instanceof Icon + ? $metadata['icon']->asImagePath() + : Icon::create($plugin->getPluginURL().'/'.$metadata['icon'])->asImagePath(); + } else { + $list[$plugin_id]['icon'] = null; + } + $list[$plugin_id]['summary'] = $metadata['summary']; + $list[$plugin_id]['mandatory'] = $this->sem_class->isModuleMandatory(get_class($plugin)); + $list[$plugin_id]['highlighted'] = (bool) $plugin->isHighlighted(); + $list[$plugin_id]['highlight_text'] = $plugin->getHighlightText(); + $list[$plugin_id]['category'] = $metadata['category']; + } + + return $list; + } +} diff --git a/app/controllers/course/plus.php b/app/controllers/course/plus.php deleted file mode 100644 index 1a5ac26..0000000 --- a/app/controllers/course/plus.php +++ /dev/null @@ -1,399 +0,0 @@ - - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - */ - -use Studip\Button, Studip\LinkButton; - -class Course_PlusController extends AuthenticatedController -{ - - public function before_filter(&$action, &$args) - { - parent::before_filter($action, $args); - $id = Context::get()->getId(); - $object_type = Context::getType(); - if (!$id || !$GLOBALS['perm']->have_studip_perm($object_type === 'course' ? 'tutor' : 'admin', $id)) { - throw new AccessDeniedException(); - } - Navigation::activateItem('/course/modules'); - - if ($object_type === 'course') { - $this->sem = Context::get(); - $this->sem_class = $this->sem->getSemClass(); - } else { - $this->sem = Context::get(); - $this->sem_class = SemClass::getDefaultInstituteClass($this->sem['type']); - } - PageLayout::setTitle(_("Mehr Funktionen")); - } - - public function index_action() - { - - PageLayout::setTitle($this->sem->getFullname() . " - " . PageLayout::getTitle()); - PageLayout::addSqueezePackage('statusgroups'); //sortier css - - $this->setupSidebar(); - $this->available_modules = $this->getSortedList($this->sem); - - if (Request::submitted('deleteContent')) { - $this->deleteContent($this->available_modules); - } - } - - public function trigger_action() - { - $context = Context::get(); - - if (!$GLOBALS['perm']->have_studip_perm($context->getRangeType() === 'course' ? 'tutor' : 'admin', $context->getId())) { - throw new AccessDeniedException(); - } - if (Request::isPost()) { - if ($context->getRangeType() === 'course') { - $sem_class = $context->getSemClass(); - } else { - $sem_class = SemClass::getDefaultInstituteClass($context->type); - } - $moduleclass = Request::get("moduleclass"); - $active = Request::int("active", 0); - $module = new $moduleclass; - if ($module->isActivatableForContext($context)) { - PluginManager::getInstance()->setPluginActivated($module->getPluginId(), $context->getId(), $active); - if (Context::isCourse()) { - if ($active) { - StudipLog::log('PLUGIN_ENABLE', Context::getId(), $module->getPluginId(), $GLOBALS['user']->id); - NotificationCenter::postNotification('PluginDidActivate', Context::getId(), $module->getPluginId()); - } else { - StudipLog::log('PLUGIN_DISABLE', Context::getId(), $module->getPluginId(), $GLOBALS['user']->id); - NotificationCenter::postNotification('PluginDidDeactivate', Context::getId(), $module->getPluginId()); - } - } - } - if ($active) { - $default_position = array_search(get_class($module), $sem_class->getActivatedModules()); - if ($default_position !== false) { - $active_tool = ToolActivation::find([$context->getId(), $module->getPluginId()]); - if ($active_tool) { - $active_tool->position = $default_position; - $active_tool->store(); - } - } - } - $this->redirect("course/plus/trigger", ['cid' => $context->getId()]); - } else { - $template = $GLOBALS['template_factory']->open("tabs.php"); - $template->navigation = Navigation::getItem("/course"); - $this->render_json([ - 'tabs' => $template->render() - ]); - } - } - - public function sorttools_action() - { - PageLayout::setTitle(_('Reihenfolge der Werkzeuge ändern')); - if (Request::submitted('order')) { - CSRFProtection::verifyUnsafeRequest(); - $plugin_id = explode('_', Request::get('id'))[1]; - $newpos = Request::get('index') + 1; - if ($this->sem->tools->findOneBy('plugin_id', $plugin_id)) { - $oldpos = $this->sem->tools->findOneBy('plugin_id', $plugin_id)->position; - if ($oldpos < $newpos) { - $this->sem->tools->findBy('position', $newpos, '>')->each(function ($p) { - $p->position++; - }); - $this->sem->tools->findOneBy('plugin_id', $plugin_id)->position = $newpos + 1; - } else { - $this->sem->tools->findBy('position', $newpos, '>=')->each(function ($p) { - $p->position++; - }); - $this->sem->tools->findOneBy('plugin_id', $plugin_id)->position = $newpos; - } - $this->sem->tools->orderBy('position asc')->each(function ($p) {static $pos = 0; $p->position = $pos++;}); - $this->sem->tools->store(); - $this->render_nothing(); - return; - } - } - - } - - public function edittool_action($plugin) - { - PageLayout::setTitle(_('Optionen des Werkzeugs ändern')); - $id = explode('_', $plugin)[1]; - $this->tool = ToolActivation::find([$this->sem->id, $id]); - if (!$this->tool) { - $this->render_nothing(); - return; - } - if (Request::submitted('save')) { - CSRFProtection::verifyUnsafeRequest(); - $displayname = trim(Request::get('displayname')); - if ($displayname !== $this->tool->getDisplayname()) { - if (strlen($displayname)) { - $this->tool->metadata['displayname'] = $displayname; - } else { - unset($this->tool->metadata['displayname']); - } - - } - if (Request::get('permission') === 'tutor') { - $this->tool->metadata['visibility'] = 'tutor'; - } else { - unset($this->tool->metadata['visibility']); - } - if ($this->tool->store()) { - PageLayout::postSuccess(_('Die Einstellungen wurden gespeichert.')); - } - $this->redirect($this->action_url('index')); - } - } - - - private function deleteContent($plugmodlist) - { - $name = Request::get('name'); - - foreach ($plugmodlist as $key => $val) { - if (array_key_exists($name, $val)) { - if ($val[$name]['type'] == 'plugin') { - $class = PluginEngine::getPlugin(get_class($val[$name]['object'])); - $displayname = $class->getPluginName(); - } - } - } - - if (Request::submitted('check')) { - if (method_exists($class, 'deleteContent')) { - $class->deleteContent(); - } else { - PageLayout::postMessage(MessageBox::info(_("Das Plugin/Modul enthält keine Funktion zum Löschen der Inhalte."))); - } - } else { - PageLayout::postMessage(MessageBox::info(sprintf(_("Sie beabsichtigen die Inhalte von %s zu löschen."), htmlReady($displayname)) - . "
" . _("Wollen Sie die Inhalte wirklich löschen?") . "
" - . LinkButton::createAccept(_('Ja'), URLHelper::getURL("?deleteContent=true&check=true&name=" . $name)) - . LinkButton::createCancel(_('Nein')))); - } - } - - private function setupSidebar() - { - - $plusconfig = UserConfig::get($GLOBALS['user']->id)->PLUS_SETTINGS; - - if (!isset($_SESSION['plus'])) { - if (isset($plusconfig['course_plus'])){ - $usr_conf = $plusconfig['course_plus']; - - $_SESSION['plus']['Kategorie']['Lehr- und Lernorganisation'] = $usr_conf['Kategorie']['Lehr- und Lernorganisation']; - $_SESSION['plus']['Kategorie']['Kommunikation und Zusammenarbeit'] = $usr_conf['Kategorie']['Kommunikation und Zusammenarbeit']; - $_SESSION['plus']['Kategorie']['Inhalte und Aufgabenstellungen'] = $usr_conf['Kategorie']['Inhalte und Aufgabenstellungen']; - $_SESSION['plus']['Kategorie']['Sonstiges'] = $usr_conf['Kategorie']['Sonstiges']; - - foreach ($usr_conf['Kategorie'] as $key => $val){ - if(!array_key_exists($key, $_SESSION['plus']['Kategorie'])){ - $_SESSION['plus']['Kategorie'][$key] = $val; - } - } - - $_SESSION['plus']['View'] = $usr_conf['View']; - $_SESSION['plus']['displaystyle'] = $usr_conf['displaystyle']; - - } else { - $_SESSION['plus']['Kategorie']['Lehr- und Lernorganisation'] = 1; - $_SESSION['plus']['Kategorie']['Kommunikation und Zusammenarbeit'] = 1; - $_SESSION['plus']['Kategorie']['Inhalte und Aufgabenstellungen'] = 1; - $_SESSION['plus']['Kategorie']['Sonstiges'] = 1; - $_SESSION['plus']['View'] = 'openall'; - $_SESSION['plus']['displaystyle'] = 'category'; - } - } - - if(isset($_SESSION['plus']['Kategorielist'])){ - foreach ($_SESSION['plus']['Kategorie'] as $key => $val){ - if(!array_key_exists($key, $_SESSION['plus']['Kategorielist']) && $key != 'Sonstiges'){ - unset($_SESSION['plus']['Kategorie'][$key]); - } - } - } - if (Request::get('mode') !== null) { - $_SESSION['plus']['View'] = Request::get('mode'); - } - if (Request::get('displaystyle') !== null) { - $_SESSION['plus']['displaystyle'] = Request::get('displaystyle'); - } - - $sidebar = Sidebar::get(); - - $widget = new OptionsWidget(); - $widget->setTitle(_('Kategorien')); - - foreach ($_SESSION['plus']['Kategorie'] as $key => $val) { - - if (Request::get(md5('cat_' . $key)) !== null) { - $_SESSION['plus']['Kategorie'][$key] = Request::get(md5('cat_' . $key)); - } - - if ($_SESSION['plus']['displaystyle'] == 'alphabetical') { - $_SESSION['plus']['Kategorie'][$key] = 1; - } - - if ($key == 'Sonstiges') { - continue; - } - $widget->addCheckbox( - $key, - $_SESSION['plus']['Kategorie'][$key], - URLHelper::getURL('?', [md5('cat_' . $key) => 1, 'displaystyle' => 'category']), - URLHelper::getURL('?', [md5('cat_' . $key) => 0, 'displaystyle' => 'category']) - ); - - } - - $widget->addCheckbox( - _('Sonstiges'), - $_SESSION['plus']['Kategorie']['Sonstiges'], - URLHelper::getURL('?', [md5('cat_Sonstiges') => 1, 'displaystyle' => 'category']), - URLHelper::getURL('?', [md5('cat_Sonstiges') => 0, 'displaystyle' => 'category']) - ); - - $sidebar->addWidget($widget, 'Kategorien'); - - $widget = new ActionsWidget(); - $widget->setTitle(_('Ansichten')); - - if ($_SESSION['plus']['View'] === 'openall') { - $widget->addLink( - _('Alles zuklappen'), - URLHelper::getURL('?', ['mode' => 'closeall']), - Icon::create('assessment') - ); - } else { - $widget->addLink( - _('Alles aufklappen'), - URLHelper::getURL('?', ['mode' => 'openall']), - Icon::create('assessment') - ); - } - - if ($_SESSION['plus']['displaystyle'] === 'category') { - $widget->addLink( - _('Alphabetische Anzeige ohne Kategorien'), - URLHelper::getURL('?', ['displaystyle' => 'alphabetical']), - Icon::create('assessment') - ); - } else { - $widget->addLink( - _('Anzeige nach Kategorien'), - URLHelper::getURL('?', ['displaystyle' => 'category']), - Icon::create('assessment') - ); - } - - $sidebar->addWidget($widget, 'ansicht'); - - $actions = new ActionsWidget(); - $actions->addLink( - _('Werkzeugreihenfolge ändern'), - $this->action_url('sorttools'), - Icon::create('arr_2down') - )->asDialog('size=500;reload-on-close'); - - $sidebar->addWidget($actions, 'aktion'); - - - unset($_SESSION['plus']['Kategorielist']); - $plusconfig['course_plus'] = $_SESSION['plus']; - UserConfig::get($GLOBALS['user']->id)->store('PLUS_SETTINGS', $plusconfig); - } - - private function getSortedList(Range $context) - { - - $list = []; - $cat_index = []; - - foreach (PluginEngine::getPlugins('StudipModule') as $plugin) { - if (!$plugin->isActivatableForContext($context)) { - continue; - } - - - - if (!$this->sem_class->isModuleMandatory(get_class($plugin)) - && $this->sem_class->isModuleAllowed(get_class($plugin)) - ) { - - $info = $plugin->getMetadata(); - - $indcat = isset($info['category']) ? $info['category'] : 'Sonstiges'; - if (!array_key_exists($indcat, $cat_index)) { - array_push($cat_index, $indcat); - } - $plugin_id = 'plugin_' . $plugin->getPluginId(); - $tool = ToolActivation::find([$context->getRangeId(), $plugin->getPluginId()]); - $displayname = $info['displayname'] ?? $plugin->getPluginname(); - if ($tool && $tool->metadata['displayname']) { - $displayname .= ' (' .$tool->getDisplayname() . ')'; - } - $visibility = $tool && $tool->metadata['visibility'] ? $tool->metadata['visibility'] : 'autor'; - - if ($_SESSION['plus']['displaystyle'] != 'category') { - - - $list['Funktionen von A-Z'][$plugin_id]['object'] = $plugin; - $list['Funktionen von A-Z'][$plugin_id]['type'] = 'plugin'; - $list['Funktionen von A-Z'][$plugin_id]['moduleclass'] = get_class($plugin); - $list['Funktionen von A-Z'][$plugin_id]['sorter'] = mb_strtolower($displayname); - $list['Funktionen von A-Z'][$plugin_id]['displayname'] = $displayname; - $list['Funktionen von A-Z'][$plugin_id]['visibility'] = $visibility; - } else { - - $cat = isset($info['category']) ? $info['category'] : 'Sonstiges'; - - if (!isset($_SESSION['plus']['Kategorie'][$cat])) { - $_SESSION['plus']['Kategorie'][$cat] = 1; - } - - $list[$cat][$plugin_id]['object'] = $plugin; - $list[$cat][$plugin_id]['moduleclass'] = get_class($plugin); - $list[$cat][$plugin_id]['type'] = 'plugin'; - $list[$cat][$plugin_id]['sorter'] = mb_strtolower($displayname); - $list[$cat][$plugin_id]['displayname'] = $displayname; - $list[$cat][$plugin_id]['visibility'] = $visibility; - } - } - } - - $sortedcats['Lehr- und Lernorganisation'] = []; - $sortedcats['Kommunikation und Zusammenarbeit'] = []; - $sortedcats['Inhalte und Aufgabenstellungen'] = []; - - foreach ($list as $cat_key => $cat_val) { - uasort($cat_val, function ($a, $b) {return strcmp($a['sorter'], $b['sorter']);}); - $list[$cat_key] = $cat_val; - if ($cat_key != 'Sonstiges') { - $sortedcats[$cat_key] = $list[$cat_key]; - } - } - - if (isset($list['Sonstiges'])) { - $sortedcats['Sonstiges'] = $list['Sonstiges']; - } - - - $_SESSION['plus']['Kategorielist'] = array_flip($cat_index); - - return $sortedcats; - } - -} diff --git a/app/views/admin/plugin/edit_description.php b/app/views/admin/plugin/edit_description.php new file mode 100644 index 0000000..45a48d1 --- /dev/null +++ b/app/views/admin/plugin/edit_description.php @@ -0,0 +1 @@ +render() ?> diff --git a/app/views/admin/plugin/index.php b/app/views/admin/plugin/index.php index 1d51d8d..c2a2034 100644 --- a/app/views/admin/plugin/index.php +++ b/app/views/admin/plugin/index.php @@ -104,6 +104,16 @@ use Studip\Button, Studip\LinkButton; _('Zugriffsrechte bearbeiten'), Icon::create('edit', 'clickable', ['title' => _('Zugriffsrechte bearbeiten')]) ) ?> + addLink( + $controller->url_for('admin/plugin/edit_description/' . $pluginid), + _('Beschreibung und Hervorhebung'), + Icon::create('infopage', Icon::ROLE_CLICKABLE, ['title' => _('Beschreibung und Hervorhebung')]), + ['data-dialog' => 'size=big'] + ); + } + ?> addLink( diff --git a/app/views/course/contentmodules/index.php b/app/views/course/contentmodules/index.php new file mode 100644 index 0000000..af8f3e1 --- /dev/null +++ b/app/views/course/contentmodules/index.php @@ -0,0 +1 @@ +
diff --git a/app/views/course/contentmodules/info.php b/app/views/course/contentmodules/info.php new file mode 100644 index 0000000..63374e3 --- /dev/null +++ b/app/views/course/contentmodules/info.php @@ -0,0 +1,56 @@ +getDescriptionMode() === 'replace_all') : ?> + getPluginDescription()) ?> + +
+
+
+
+ + asImg(100) ?> +
+
+

getPluginName()) ?>

+ + + +
+
+
+ + +
    + +
  • + +
  • + +
+ +
+ getPluginDescription()) ?> +
+
+ + + +
+ diff --git a/app/views/course/contentmodules/rename.php b/app/views/course/contentmodules/rename.php new file mode 100644 index 0000000..c7f4ef7 --- /dev/null +++ b/app/views/course/contentmodules/rename.php @@ -0,0 +1,24 @@ +
+
+ + + +
+ +
+
+
+ + + + +
+
diff --git a/app/views/course/plus/edittool.php b/app/views/course/plus/edittool.php deleted file mode 100644 index 124d2b4..0000000 --- a/app/views/course/plus/edittool.php +++ /dev/null @@ -1,25 +0,0 @@ -
- -
- - - -
- - -
-
- -
- -
-
diff --git a/app/views/course/plus/index.php b/app/views/course/plus/index.php deleted file mode 100644 index adf58b2..0000000 --- a/app/views/course/plus/index.php +++ /dev/null @@ -1,245 +0,0 @@ - - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - */ - -use Studip\Button; - -?> - -
- - - - - - $pluginlist) : ?> - - - - - $val) : ?> - isActivated(); - $info = $plugin->getMetadata(); - - //Checkbox - $anchor = 'p_' . $plugin->getPluginId(); - $cb_disabled = ''; - $cb_checked = $plugin_activated ? "checked" : ""; - - $pluginname = $val['displayname']; - $url = $plugin->isCorePlugin() ? $GLOBALS['ABSOLUTE_URI_STUDIP'] : $plugin->getPluginURL(); - $pluginvisibility = $val['visibility']; - } - ?> - - - - - - - - - - - - -
- -
- -
- - onClick="STUDIP.Plus.setModule.call(this);"> -
- - -
-
- - - - "> - - asImg(['class' => 'plugin_icon text-bottom', 'alt' => '']) ?> - - - - - - - - - - - - - - - - - - -
- - setContext($pluginname); - $actionMenu->addLink( - $controller->action_url('edittool/' . $key), - _('Optionen bearbeiten'), - Icon::create('edit'), - ['data-dialog' => 'size=auto'] - ); - if (method_exists($plugin, 'deleteContent')) { - $actionMenu->addLink( - $controller->action_url('index', ['deleteContent' => 1, 'name' => $key]), - _('Inhalte löschen'), - Icon::create('trash') - ); - } - ?> -
- render() ?> -
- -
- - -
- -
- -
    - -
  • - -
- - - -

- -

- - - -

- - - - - -

- - -

- - - - -

- - - - ... - - -
-
- -
- -
-
diff --git a/app/views/course/plus/sorttools.php b/app/views/course/plus/sorttools.php deleted file mode 100644 index 153e7c9..0000000 --- a/app/views/course/plus/sorttools.php +++ /dev/null @@ -1,15 +0,0 @@ -
-tools): ?> - tools as $tool): ?> - getStudipModule()) continue; ?> -
-
- -

getDisplayName()) ?>

-
-
- - -
- - diff --git a/db/migrations/5.4.10_contentmodules_description.php b/db/migrations/5.4.10_contentmodules_description.php new file mode 100644 index 0000000..266be87 --- /dev/null +++ b/db/migrations/5.4.10_contentmodules_description.php @@ -0,0 +1,47 @@ +exec($query); + + $query = "INSERT IGNORE INTO `config` (`field`, `value`, `type`, `range`, `mkdate`, `chdate`, `description`) + VALUES (:name, :value, :type, :range, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :description)"; + + $statement = DBManager::get()->prepare($query); + $statement->execute([ + ':name' => 'CONTENTMODULES_TILED_DISPLAY', + ':description' => 'Bevorzugt ein Nutzer eine Kachelansicht auf der Werkzeugseite in den Veranstaltungen oder lieber eine Tabelle?', + ':range' => 'user', + ':type' => 'boolean', + ':value' => '1' + ]); + } + + protected function down() + { + $query = "ALTER TABLE `plugins` + DROP COLUMN `description`, + DROP COLUMN `highlight_until`, + DROP COLUMN `highlight_text`"; + DBManager::get()->exec($query); + + $query = "DELETE FROM `config_values` + WHERE `field` = 'CONTENTMODULES_TILED_DISPLAY' "; + DBManager::get()->exec($query); + $query = "DELETE FROM `config` + WHERE `field` = 'CONTENTMODULES_TILED_DISPLAY' "; + DBManager::get()->exec($query); + } +} diff --git a/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php b/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php index c4fda2f..c45f3a4 100644 --- a/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php +++ b/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php @@ -33,6 +33,7 @@ class ConfigValuesUpdate extends JsonApiController // TODO: zunächst kann diese Route nur Konfigurationseinstellungen vom Typ bool ändern if ( 'boolean' !== $resource->entry['type'] + && $resource->entry['field'] !== 'CONTENTMODULES_TILED_DISPLAY' && $resource->entry['field'] !== 'MY_COURSES_OPEN_GROUPS' && $resource->entry['field'] !== 'MY_COURSES_VIEW_SETTINGS' ) { diff --git a/lib/classes/JsonApi/Schemas/Course.php b/lib/classes/JsonApi/Schemas/Course.php index acc302e..09f1175 100644 --- a/lib/classes/JsonApi/Schemas/Course.php +++ b/lib/classes/JsonApi/Schemas/Course.php @@ -27,6 +27,7 @@ class Course extends SchemaProvider const REL_START_SEMESTER = 'start-semester'; const REL_STATUS_GROUPS = 'status-groups'; const REL_WIKI_PAGES = 'wiki-pages'; + const REL_TOOLS = 'tools'; public function getId($course): ?string { @@ -82,6 +83,7 @@ class Course extends SchemaProvider $relationships = $this->getSemTypeRelationship($relationships, $course, $includeList); $relationships = $this->getStatusGroupsRelationship($relationships, $course, $includeList); $relationships = $this->getWikiPagesRelationship($relationships, $course, $includeList); + $relationships = $this->getToolsRelationship($relationships, $course, $includeList); return $relationships; } @@ -301,6 +303,26 @@ class Course extends SchemaProvider /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ + private function getToolsRelationship( + array $relationships, + \Course $course, + $includeData + ) { + $relation = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($course, self::REL_TOOLS), + ] + ]; + if (in_array(self::REL_TOOLS, $includeData)) { + $relation[self::RELATIONSHIP_DATA] = $course->tools->getArrayCopy(); + } + + return array_merge($relationships, [self::REL_TOOLS => $relation]); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ private function getParticipatingInstitutes( array $relationships, \Course $course, diff --git a/lib/classes/SemClass.class.php b/lib/classes/SemClass.class.php index 36bfd70..0fd4868 100644 --- a/lib/classes/SemClass.class.php +++ b/lib/classes/SemClass.class.php @@ -71,10 +71,10 @@ class SemClass implements ArrayAccess $type = isset($INST_MODULES[$type]) ? $type : 'default'; $data = [ - 'name' => 'Generierte Standardinstitutsklasse', + 'name' => _('Generierte Standardinstitutsklasse'), 'visible' => 1, - 'overview' => 'CoreOverview', // always available - 'admin' => 'CoreAdmin' // always available + 'admin' => 'CoreAdmin', // always available + 'overview' => 'CoreOverview' // always available ]; $slots = [ 'forum' => 'CoreForum', @@ -86,8 +86,8 @@ class SemClass implements ArrayAccess 'personal' => 'CorePersonal' ]; $modules = [ + 'CoreAdmin' => ['activated' => 1, 'sticky' => 1], 'CoreOverview' => ['activated' => 1, 'sticky' => 1], - 'CoreAdmin' => ['activated' => 1, 'sticky' => 1] ]; foreach ($slots as $slot => $module) { diff --git a/lib/classes/forms/DatetimepickerInput.php b/lib/classes/forms/DatetimepickerInput.php index 9bee82b..060946f 100644 --- a/lib/classes/forms/DatetimepickerInput.php +++ b/lib/classes/forms/DatetimepickerInput.php @@ -22,4 +22,13 @@ class DatetimepickerInput extends Input $template->attributes = $attributes; return $template->render(); } + + /** + * Turns an empty string into null value. + * @return integer|null + */ + public function dataMapper($value) + { + return $value ?: null; + } } diff --git a/lib/classes/forms/Form.php b/lib/classes/forms/Form.php index 936f324..892e144 100644 --- a/lib/classes/forms/Form.php +++ b/lib/classes/forms/Form.php @@ -18,6 +18,7 @@ class Form extends Part protected $save_button_name = ''; protected $autoStore = false; + protected $debugmode = false; protected $success_message = ''; protected $collapsable = false; @@ -211,6 +212,17 @@ class Form extends Part return $this; } + public function setDebugMode(bool $debug = true): Form + { + $this->debugmode = $debug; + return $this; + } + + public function getDebugMode(): bool + { + return $this->debugmode; + } + public function getSuccessMessage() : string { return $this->success_message; @@ -301,11 +313,9 @@ class Form extends Part $all_values = []; foreach ($this->getAllInputs() as $input) { $value = $this->getStorableValueFromRequest($input); - if ($value !== null) { - $callback = $this->getStoringCallback($input); - if (is_callable($callback)) { - $stored += $callback($value, $input); - } + $callback = $this->getStoringCallback($input); + if (is_callable($callback)) { + $stored += $callback($value, $input); $all_values[$input->getName()] = $value; } } @@ -388,7 +398,11 @@ class Form extends Part return $input->store; } $context = $input->getParent()->getContextObject(); - if ($context && is_subclass_of($context, \SimpleORMap::class)) { + if ( + $context + && is_subclass_of($context, \SimpleORMap::class) + && $context->isField($input->getName()) + ) { return function ($value) use ($context, $input) { $context[$input->getName()] = $value; }; diff --git a/lib/classes/forms/InfoInput.php b/lib/classes/forms/InfoInput.php new file mode 100644 index 0000000..561feee --- /dev/null +++ b/lib/classes/forms/InfoInput.php @@ -0,0 +1,20 @@ +open('forms/info_input'); + $template->title = $this->title; + $template->value = $this->value; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } + + public function getAllInputNames() + { + return []; + } +} diff --git a/lib/classes/forms/Part.php b/lib/classes/forms/Part.php index 3609eb4..1d1c9d0 100644 --- a/lib/classes/forms/Part.php +++ b/lib/classes/forms/Part.php @@ -67,13 +67,13 @@ abstract class Part /** * Adds an Input to this Part. * @param Input $input - * @return Input + * @return $this */ public function addInput(Input $input) { $input->setParent($this); $this->parts[] = $input; - return $input; + return $this; } /** @@ -81,15 +81,15 @@ abstract class Part * * @param string $text The text to be added. * @param bool $text_is_html Whether the text is HTML (true) or plain text (false). Defaults to true. - * @return Text The added text form part. + * @return $this */ - public function addText(string $text, bool $text_is_html = true): Text + public function addText(string $text, bool $text_is_html = true) { $text_part = new Text(); $text_part->setText($text, $text_is_html); $text_part->setParent($this); $this->parts[] = $text_part; - return $text_part; + return $this; } /** @@ -100,16 +100,16 @@ abstract class Part * @param \Icon|null $icon The icon to be used for the link. * @param array $attributes Additional link attributes. * - * @return Link The Text form element containing the link as HTML. + * @return $this */ - public function addLink(string $title, string $url, ?\Icon $icon = null, array $attributes = []): Link + public function addLink(string $title, string $url, ?\Icon $icon = null, array $attributes = []) { $link = new Link($url, $title, $icon); $link->setAttributes($attributes); $this->addPart($link); - return $link; + return $this; } /** diff --git a/lib/classes/forms/WysiwygInput.php b/lib/classes/forms/WysiwygInput.php new file mode 100644 index 0000000..f270226 --- /dev/null +++ b/lib/classes/forms/WysiwygInput.php @@ -0,0 +1,30 @@ +open('forms/wysiwyg_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } + + public function getRequestValue() + { + $value = \Request::get($this->name); + if (trim($value)) { + return \Studip\Markup::markAsHtml( + \Studip\Markup::purifyHtml($value) + ); + } else { + return ''; + } + } +} diff --git a/lib/classes/sidebar/OptionsWidget.php b/lib/classes/sidebar/OptionsWidget.php index a7931ce..1e56ed1 100644 --- a/lib/classes/sidebar/OptionsWidget.php +++ b/lib/classes/sidebar/OptionsWidget.php @@ -55,8 +55,9 @@ class OptionsWidget extends ListWidget $url = html_entity_decode($url); $content = sprintf( - '%s', + '%s', htmlReady($url), + $checked ? 'true' : 'false', $checked ? 'checked' : 'unchecked', arrayToHtmlAttributes($attributes), htmlReady($label) diff --git a/lib/models/Plugin.php b/lib/models/Plugin.php new file mode 100644 index 0000000..2030e8a --- /dev/null +++ b/lib/models/Plugin.php @@ -0,0 +1,25 @@ + _('Schneller und einfacher Austausch von Informationen in Gesprächsform'), + 'displayname' => _('Blubber'), + 'summary' => _('Schneller Austausch von Informationen in Gesprächsform'), 'description' => _('Blubber ist eine Kommunikationsform mit Ähnlichkeiten zu einem Forum, in dem aber in Echtzeit miteinander kommuniziert werden kann und das durch den etwas informelleren Charakter eher einem Chat anmutet. Anders als im Forum ist es nicht notwendig, die Seiten neu zu laden, um die neuesten Einträge (z. B. Antworten auf eigene Postings) sehen zu können: Die Seite aktualisiert sich selbst bei neuen Einträgen. Dateien (z.B. Fotos, Audiodateien, Links) können per Drag and Drop in das Feld gezogen und somit verlinkt werden. Auch Textformatierungen sind möglich.'), 'descriptionlong' => _('Kommunikationsform mit Ähnlichkeiten zu einem Forum. Im Gegensatz zum Forum kann mit Blubber jedoch in Echtzeit miteinander kommuniziert werden. Das Tool ähnelt durch den etwas informelleren Charakter einem Messenger. Anders als im Forum ist es nicht notwendig, die Seiten neu zu laden, um die neuesten Einträge (z. B. Antworten auf eigene Postings) sehen zu können. Dateien (z. B. Fotos, Audiodateien, Links) können per drag and drop in das Feld gezogen und somit verlinkt werden. Auch Textformatierungen sind möglich.'), 'category' => _('Kommunikation und Zusammenarbeit'), 'keywords' => _('Einfach Text schreiben und mit abschicken; Direktes Kontaktieren anderer Stud.IP-NutzerInnen (@Vorname Nachname); Setzen von und Suche nach Stichworten über Hashtags (#Stichwort); Einbinden von Dateien per drag and drop'), 'icon' => Icon::create('blubber', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('blubber', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Blubber', 'pictures' => [ diff --git a/lib/modules/ConsultationModule.class.php b/lib/modules/ConsultationModule.class.php index 5671776..c68f8ca 100644 --- a/lib/modules/ConsultationModule.class.php +++ b/lib/modules/ConsultationModule.class.php @@ -143,6 +143,7 @@ class ConsultationModule extends CorePlugin implements StudipModule, SystemPlugi 'keywords' => _('Terminvergabe, Sprechstunden'), 'displayname' => _('Terminvergabe'), 'icon' => Icon::create('consultation', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('consultation', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Terminvergabe', 'pictures' => [ diff --git a/lib/modules/CoreAdmin.class.php b/lib/modules/CoreAdmin.class.php index ada2af0..948809c 100644 --- a/lib/modules/CoreAdmin.class.php +++ b/lib/modules/CoreAdmin.class.php @@ -24,15 +24,13 @@ class CoreAdmin extends CorePlugin implements StudipModule */ public function getTabNavigation($course_id) { - $sem_create_perm = in_array(Config::get()->SEM_CREATE_PERM, ['root','admin','dozent']) ? Config::get()->SEM_CREATE_PERM : 'dozent'; - if ($GLOBALS['perm']->have_studip_perm('tutor', $course_id)) { $navigation = new Navigation(_('Verwaltung')); $navigation->setImage(Icon::create('admin', Icon::ROLE_INFO_ALT)); $navigation->setActiveImage(Icon::create('admin', Icon::ROLE_INFO)); - $main = new Navigation(_('Verwaltung'), 'dispatch.php/course/management'); - $navigation->addSubNavigation('main', $main); + $main = new Navigation(_('Werkzeuge'), 'dispatch.php/course/contentmodules'); + $navigation->addSubNavigation('contentmodules', $main); if (!Context::isInstitute()) { $item = new Navigation(_('Grunddaten'), 'dispatch.php/course/basicdata/view/' . $course_id); diff --git a/lib/modules/CoreCalendar.class.php b/lib/modules/CoreCalendar.class.php index 78d4b87..c0df367 100644 --- a/lib/modules/CoreCalendar.class.php +++ b/lib/modules/CoreCalendar.class.php @@ -49,6 +49,7 @@ class CoreCalendar extends CorePlugin implements StudipModule 'summary' => _('Kalender'), 'category' => _('Lehr- und Lernorganisation'), 'icon' => Icon::create('schedule', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('schedule', Icon::ROLE_CLICKABLE), 'displayname' => _('Planer'), ]; } diff --git a/lib/modules/CoreDocuments.class.php b/lib/modules/CoreDocuments.class.php index 4543b87..2acfeef 100644 --- a/lib/modules/CoreDocuments.class.php +++ b/lib/modules/CoreDocuments.class.php @@ -154,7 +154,7 @@ class CoreDocuments extends CorePlugin implements StudipModule, OERModule public function getMetadata() { return [ - 'summary' => _('Austausch von Dateien'), + 'summary' => _('Austausch von Dateien, Hausaufgabenordner & Terminordner'), 'description' => _('Im Dateibereich können Dateien sowohl von ' . 'Lehrenden als auch von Studierenden hoch- bzw. ' . 'heruntergeladen werden. Es können Ordner angelegt und ' . @@ -183,6 +183,7 @@ class CoreDocuments extends CorePlugin implements StudipModule, OERModule 'können Im Dateibereich bestimmte Rechte (r, w, x, f) für Studierende, wie z.B. das ' . 'Leserecht (r), festgelegt werden.'), 'icon' => Icon::create('files', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('files', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Dateibereich_-_Dateiordnerberechtigung', 'pictures' => [ diff --git a/lib/modules/CoreElearningInterface.class.php b/lib/modules/CoreElearningInterface.class.php index 7e36a1b..a5f7119 100644 --- a/lib/modules/CoreElearningInterface.class.php +++ b/lib/modules/CoreElearningInterface.class.php @@ -120,6 +120,7 @@ class CoreElearningInterface extends CorePlugin implements StudipModule Zugang zu externen Lernplattformen; Aufgaben- und Test-Erstellung'), 'icon' => Icon::create('learnmodule', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('learnmodule', Icon::ROLE_CLICKABLE), 'descriptionshort' => _('Zugang zu extern erstellten Lernmodulen'), 'descriptionlong' => _('Über diese Schnittstelle ist es möglich, Selbstlerneinheiten, '. 'die in externen Programmen erstellt werden, in Stud.IP zur Verfügung '. diff --git a/lib/modules/CoreForum.class.php b/lib/modules/CoreForum.class.php index ba1ee64..3a43372 100644 --- a/lib/modules/CoreForum.class.php +++ b/lib/modules/CoreForum.class.php @@ -196,6 +196,7 @@ class CoreForum extends CorePlugin implements ForumModule 'category' => _('Kommunikation und Zusammenarbeit'), 'keywords' => _('Möglichkeit zum intensiven, nachhaltigen textbasierten Austausch; (nachträgliche) Strukturierung der Beiträge; Editierfunktion für Lehrende'), 'icon' => Icon::create('forum', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('forum', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Forum', 'pictures' => [ diff --git a/lib/modules/CoreOverview.class.php b/lib/modules/CoreOverview.class.php index 39145ae..af1b960 100644 --- a/lib/modules/CoreOverview.class.php +++ b/lib/modules/CoreOverview.class.php @@ -110,7 +110,10 @@ class CoreOverview extends CorePlugin implements StudipModule public function getMetadata() { return [ - 'displayname' => _('Übersicht') + 'displayname' => _('Übersicht'), + 'summary' => _('Ankündigungen, Termine, Fragebögen & Details'), + 'icon' => Icon::create('home', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('home', Icon::ROLE_CLICKABLE) ]; } diff --git a/lib/modules/CoreParticipants.class.php b/lib/modules/CoreParticipants.class.php index a9fac5c..14c885b 100644 --- a/lib/modules/CoreParticipants.class.php +++ b/lib/modules/CoreParticipants.class.php @@ -178,6 +178,7 @@ class CoreParticipants extends CorePlugin implements StudipModule 'bzw. einzelne Teilnehmende separat anzuschreiben.'), 'category' => _('Lehr- und Lernorganisation'), 'icon' => Icon::create('persons', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('persons', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/TeilnehmerInnen', 'pictures' => [ diff --git a/lib/modules/CorePersonal.class.php b/lib/modules/CorePersonal.class.php index 1f99d7f..71aaa6b 100644 --- a/lib/modules/CorePersonal.class.php +++ b/lib/modules/CorePersonal.class.php @@ -49,6 +49,7 @@ class CorePersonal extends CorePlugin implements StudipModule 'displayname' => _('MitarbeiterInnen'), 'category' => _('Sonstiges'), 'icon' => Icon::create('persons', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('persons', Icon::ROLE_CLICKABLE) ]; } diff --git a/lib/modules/CoreSchedule.class.php b/lib/modules/CoreSchedule.class.php index 14fab1d..601b618 100644 --- a/lib/modules/CoreSchedule.class.php +++ b/lib/modules/CoreSchedule.class.php @@ -106,6 +106,7 @@ class CoreSchedule extends CorePlugin implements StudipModule 'inhaltlichen Einstimmung der Studierenden können Lehrende den Terminen ' . 'Themen hinzufügen, die z. B. eine Kurzbeschreibung der Inhalte darstellen.'), 'icon' => Icon::create('schedule', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('schedule', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Ablaufplan', 'pictures' => [ diff --git a/lib/modules/CoreScm.class.php b/lib/modules/CoreScm.class.php index 86e10e1..d37023b 100644 --- a/lib/modules/CoreScm.class.php +++ b/lib/modules/CoreScm.class.php @@ -139,6 +139,7 @@ class CoreScm extends CorePlugin implements StudipModule 'Literatur. Sie kann aber auch für andere beliebige Zusatzinformationen (Links, Protokolle '. 'etc.) verwendet werden.'), 'icon' => Icon::create('infopage', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('infopage', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Freie_Informationsseite', 'pictures' => [ diff --git a/lib/modules/CoreStudygroupAdmin.class.php b/lib/modules/CoreStudygroupAdmin.class.php index 7c67a26..d311390 100644 --- a/lib/modules/CoreStudygroupAdmin.class.php +++ b/lib/modules/CoreStudygroupAdmin.class.php @@ -35,6 +35,7 @@ class CoreStudygroupAdmin extends CorePlugin implements StudipModule $navigation->setImage(Icon::create('admin', Icon::ROLE_INFO_ALT)); $navigation->setActiveImage(Icon::create('admin', Icon::ROLE_INFO)); + $navigation->addSubNavigation('contentmodules', new Navigation(_('Werkzeuge'), "dispatch.php/course/contentmodules?cid={$course_id}")); $navigation->addSubNavigation('main', new Navigation(_('Verwaltung'), "dispatch.php/course/studygroup/edit/?cid={$course_id}")); $navigation->addSubNavigation('avatar', new Navigation(_('Infobild'), "dispatch.php/avatar/update/course/{$course_id}?cid={$course_id}")); diff --git a/lib/modules/CoreWiki.class.php b/lib/modules/CoreWiki.class.php index 0034098..4700334 100644 --- a/lib/modules/CoreWiki.class.php +++ b/lib/modules/CoreWiki.class.php @@ -119,7 +119,7 @@ class CoreWiki extends CorePlugin implements StudipModule public function getMetadata() { return [ - 'summary' => _('Gemeinsames asynchrones Erstellen und Bearbeiten von Texten'), + 'summary' => _('Gemeinsames Erstellen und Bearbeiten von Texten'), 'description' => _('Im Wiki-Web oder kurz "Wiki" können '. 'verschiedene Autor/-innen gemeinsam Texte, Konzepte und andere '. 'schriftliche Arbeiten erstellen und gestalten, dies '. @@ -151,6 +151,7 @@ class CoreWiki extends CorePlugin implements StudipModule 'PDF-Datei ist integriert.'), 'category' => _('Kommunikation und Zusammenarbeit'), 'icon' => Icon::create('wiki', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('wiki', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Wiki-Web', 'pictures' => [ diff --git a/lib/modules/CoursewareModule.class.php b/lib/modules/CoursewareModule.class.php index d085de2..9de221f 100644 --- a/lib/modules/CoursewareModule.class.php +++ b/lib/modules/CoursewareModule.class.php @@ -81,11 +81,11 @@ class CoursewareModule extends CorePlugin implements SystemPlugin, StudipModule public function getIconNavigation($courseId, $last_visit, $user_id) { $statement = DBManager::get()->prepare(" - SELECT COUNT(DISTINCT elem.id) - FROM `cw_structural_elements` AS elem + SELECT COUNT(DISTINCT elem.id) + FROM `cw_structural_elements` AS elem INNER JOIN `cw_containers` as container ON (elem.id = container.structural_element_id) INNER JOIN `cw_blocks` as blocks ON (container.id = blocks.container_id) - WHERE elem.range_type = 'course' + WHERE elem.range_type = 'course' AND elem.range_id = :range_id AND blocks.payload != '' AND blocks.chdate > :last_visit @@ -141,6 +141,7 @@ class CoursewareModule extends CorePlugin implements SystemPlugin, StudipModule 'displayname' => _('Courseware'), 'category' => _('Lehr- und Lernorganisation'), 'icon' => Icon::create('courseware', 'info'), + 'icon_clickable' => Icon::create('courseware', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Courseware', 'pictures' => [ diff --git a/lib/modules/FeedbackModule.class.php b/lib/modules/FeedbackModule.class.php index c369ea7..8674f62 100644 --- a/lib/modules/FeedbackModule.class.php +++ b/lib/modules/FeedbackModule.class.php @@ -53,6 +53,7 @@ class FeedbackModule extends CorePlugin implements StudipModule, SystemPlugin 'category' => _('Kommunikation und Zusammenarbeit'), 'keywords' => _('Anlegen von Feedback-Elementen an verschiedenen Stellen; Auswahl verschiedener Feedback-Modi, wie Sternbewertung; Übersicht über alle Feedback-Elemente einer Veranstaltung'), 'icon' => Icon::create('star', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('star', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Feedback', 'pictures' => [ diff --git a/lib/modules/GradebookModule.class.php b/lib/modules/GradebookModule.class.php index ac8d69b..f459f10 100644 --- a/lib/modules/GradebookModule.class.php +++ b/lib/modules/GradebookModule.class.php @@ -147,6 +147,7 @@ class GradebookModule extends CorePlugin implements SystemPlugin, StudipModule 'category' => _('Lehr- und Lernorganisation'), 'keywords' => _('automatische und manuelle Erfassung von gewichteten Leistungen;Export von Leistungen;persönliche Fortschrittskontrolle'), 'icon' => Icon::create('assessment', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('assessment', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Gradebook', 'pictures' => [ diff --git a/lib/modules/IliasInterfaceModule.class.php b/lib/modules/IliasInterfaceModule.class.php index f5bc83f..a8cafbf 100644 --- a/lib/modules/IliasInterfaceModule.class.php +++ b/lib/modules/IliasInterfaceModule.class.php @@ -153,6 +153,7 @@ class IliasInterfaceModule extends CorePlugin implements StudipModule, SystemPlu Zugang zu ILIAS; Aufgaben- und Test-Erstellung'), 'icon' => Icon::create('learnmodule', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('learnmodule', Icon::ROLE_CLICKABLE), 'descriptionshort' => _('Zugang zu extern erstellten ILIAS-Lernobjekten'), 'descriptionlong' => _('Über diese Schnittstelle ist es möglich, Lernobjekte aus ' . 'einer ILIAS-Installation (> 5.3.8) in Stud.IP zur Verfügung ' . diff --git a/lib/modules/LtiToolModule.class.php b/lib/modules/LtiToolModule.class.php index 0383f11..eac2768 100644 --- a/lib/modules/LtiToolModule.class.php +++ b/lib/modules/LtiToolModule.class.php @@ -108,6 +108,7 @@ class LtiToolModule extends CorePlugin implements StudipModule, SystemPlugin, Pr 'category' => _('Kommunikation und Zusammenarbeit'), 'keywords' => _('Einbindung von LTI-Tools (Version 1.x)'), 'icon' => Icon::create('link-extern', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('link-extern', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Lti', 'pictures' => [ diff --git a/lib/navigation/CourseNavigation.php b/lib/navigation/CourseNavigation.php index 2ff62ad..6e37cf5 100644 --- a/lib/navigation/CourseNavigation.php +++ b/lib/navigation/CourseNavigation.php @@ -53,7 +53,26 @@ class CourseNavigation extends Navigation return; } - foreach ($context->tools as $tool) { + $admin_plugin_ids = []; + $core_admin = PluginManager::getInstance()->getPlugin('CoreAdmin'); + if ($core_admin) { + $admin_plugin_ids[] = $core_admin->getPluginId(); + } + $core_studygroup_admin = PluginManager::getInstance()->getPlugin('CoreStudygroupAdmin'); + if ($core_studygroup_admin) { + $admin_plugin_ids[] = $core_studygroup_admin->getPluginId(); + } + $tools = $context->tools->getArrayCopy(); + usort($tools, function ($a, $b) use ($admin_plugin_ids) { + if (in_array($a['plugin_id'], $admin_plugin_ids)) { + return -1; + } + if (in_array($b['plugin_id'], $admin_plugin_ids)) { + return 1; + } + return $a['position'] - $b['position']; + }); + foreach ($tools as $tool) { if (Context::isInstitute() || Seminar_Perm::get()->have_studip_perm($tool->getVisibilityPermission(), $context->id)) { $studip_module = $tool->getStudipModule(); if ($studip_module instanceof StudipModule) { diff --git a/lib/plugins/core/CorePlugin.php b/lib/plugins/core/CorePlugin.php index e16b77a..0bba289 100644 --- a/lib/plugins/core/CorePlugin.php +++ b/lib/plugins/core/CorePlugin.php @@ -60,6 +60,41 @@ abstract class CorePlugin return ''; } + public function getPluginDescription() + { + $metadata = $this->getMetadata(); + $language = getUserLanguage(User::findCurrent()->id); + if ($metadata['descriptionlong_' . $language]) { + return $metadata['descriptionlong_' . $language]; + } + if ($metadata['description_' . $language]) { + return $metadata['description_' . $language]; + } + $description = $metadata['descriptionlong'] ?? $metadata['description']; + + if ($this->plugin_info['description_mode'] === 'override_description') { + return $this->plugin_info['description']; + } else { + return '
' . $description . '
' . $this->plugin_info['description']; + } + } + + public function getDescriptionMode() + { + return $this->plugin_info['description_mode']; + } + + public function isHighlighted() + { + return $this->plugin_info['highlight_until'] > time(); + } + + public function getHighlightText() + { + return $this->plugin_info['highlight_text']; + } + + /** * Checks if the plugin is a core-plugin. Returns true if this is the case. * diff --git a/lib/plugins/core/StudIPPlugin.class.php b/lib/plugins/core/StudIPPlugin.class.php index fafd583..a3dacb6 100644 --- a/lib/plugins/core/StudIPPlugin.class.php +++ b/lib/plugins/core/StudIPPlugin.class.php @@ -81,6 +81,45 @@ abstract class StudIPPlugin } /** + * Returns the description of the plugin either from plugins-table or from the manifest's descriptionlong + * or description attribute. + * @return string|null + */ + public function getPluginDescription() + { + $metadata = $this->getMetadata(); + $language = getUserLanguage(User::findCurrent()->id); + if ($metadata['descriptionlong_' . $language]) { + return $metadata['descriptionlong_' . $language]; + } + if ($metadata['description_' . $language]) { + return $metadata['description_' . $language]; + } + $description = $metadata['descriptionlong'] ?? $metadata['description']; + + if ($this->plugin_info['description_mode'] === 'override_description') { + return $this->plugin_info['description']; + } else { + return $description . $this->plugin_info['description']; + } + } + + public function getDescriptionMode() + { + return $this->plugin_info['description_mode']; + } + + public function isHighlighted() + { + return $this->plugin_info['highlight_until'] > time(); + } + + public function getHighlightText() + { + return $this->plugin_info['highlight_text']; + } + + /** * Returns the version of this plugin as defined in manifest. * @return string */ diff --git a/lib/plugins/engine/PluginManager.class.php b/lib/plugins/engine/PluginManager.class.php index 86305a1..7fb69bc 100644 --- a/lib/plugins/engine/PluginManager.class.php +++ b/lib/plugins/engine/PluginManager.class.php @@ -85,7 +85,11 @@ class PluginManager 'depends' => (int) $plugin['dependentonid'], 'core' => $plugin['pluginpath'] === '', 'automatic_update_url' => $plugin['automatic_update_url'], - 'automatic_update_secret' => $plugin['automatic_update_secret'] + 'automatic_update_secret' => $plugin['automatic_update_secret'], + 'description' => $plugin['description'], + 'description_mode' => $plugin['description_mode'], + 'highlight_until' => $plugin['highlight_until'], + 'highlight_text' => $plugin['highlight_text'] ]; } } @@ -251,11 +255,16 @@ class PluginManager $activation->range_type = $range->getRangeType(); } $plugin = $this->getPluginById($id); + if ($active) { call_user_func([get_class($plugin), 'onActivation'], $id, $rangeId); + StudipLog::log('PLUGIN_ENABLE', $rangeId, $id, User::findCurrent()->id); + NotificationCenter::postNotification('PluginDidActivate', $rangeId, $id); return $activation->store(); } else { call_user_func([get_class($plugin), 'onDeactivation'], $id, $rangeId); + StudipLog::log('PLUGIN_DISABLE', $rangeId, $id, User::findCurrent()->id); + NotificationCenter::postNotification('PluginDidDeactivate', $rangeId, $id); return $activation->delete(); } } diff --git a/lib/seminar_open.php b/lib/seminar_open.php index a381d58..33436d4 100644 --- a/lib/seminar_open.php +++ b/lib/seminar_open.php @@ -143,19 +143,6 @@ if (Request::int('disable_plugins') !== null && ($user->id === 'nobody' || $perm // load the default set of plugins PluginEngine::loadPlugins(); -// add navigation item: add modules -if (Context::isCourse() && $perm->have_studip_perm('tutor', Context::getId())) { - $plus_nav = new Navigation(_('Mehr …'), 'dispatch.php/course/plus/index'); - $plus_nav->setDescription(_("Mehr Stud.IP-Funktionen für Ihre Veranstaltung")); - Navigation::addItem('/course/modules', $plus_nav); -} - -// add navigation item: add modules (institute) -if (Context::isInstitute() && $perm->have_studip_perm('admin', Context::getId())) { - $plus_nav = new Navigation(_('Mehr …'), 'dispatch.php/course/plus/index'); - $plus_nav->setDescription(_("Mehr Stud.IP-Funktionen für Ihre Einrichtung")); - Navigation::addItem('/course/modules', $plus_nav); -} // add navigation item for profile: add modules if (Navigation::hasItem('/profile/edit')) { $plus_nav = new Navigation(_('Mehr …'), 'dispatch.php/profilemodules/index'); diff --git a/resources/assets/javascripts/bootstrap/contentmodules.js b/resources/assets/javascripts/bootstrap/contentmodules.js new file mode 100644 index 0000000..3d3f886 --- /dev/null +++ b/resources/assets/javascripts/bootstrap/contentmodules.js @@ -0,0 +1,40 @@ +STUDIP.domReady(() => { + const node = document.querySelector('.content-modules-vue-app'); + if (!node) { + return; + } + + Promise.all([ + STUDIP.Vue.load(), + import('../../../vue/store/ContentModulesStore.js').then((config) => config.default), + import('../../../vue/components/ContentModules.vue').then((component) => component.default), + ]).then(([{ createApp, store }, storeConfig, ContentModules]) => { + store.registerModule('contentmodules', storeConfig); + + Object.entries(window.ContentModulesStoreData ?? {}).forEach(([key, value]) => { + store.commit(`contentmodules/${key}`, value); + }); + + const vm = createApp({ + components: { ContentModules } + }); + vm.$mount(node); + }); +}); + +STUDIP.dialogReady(() => { + const node = document.querySelector('.content-modules-controls-vue-app'); + if (!node) { + return; + } + + Promise.all([ + STUDIP.Vue.load(), + import('../../../vue/components/ContentModulesControl.vue').then((component) => component.default), + ]).then(([{ createApp }, ContentModulesControl]) => { + const vm = createApp({ + components: { ContentModulesControl } + }); + vm.$mount(node); + }); +}); diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js index 3d9be08..1a6bc53 100644 --- a/resources/assets/javascripts/bootstrap/forms.js +++ b/resources/assets/javascripts/bootstrap/forms.js @@ -251,6 +251,8 @@ STUDIP.ready(function () { params.STUDIPFORM_VALIDATIONNOTES = []; params.STUDIPFORM_AUTOSAVEURL = f.dataset.autosave; params.STUDIPFORM_REDIRECTURL = f.dataset.url; + params.STUDIPFORM_SELECTEDLANGUAGES = {}; + params.STUDIPFORM_DEBUGMODE = JSON.parse(f.dataset.debugmode); return params; }, methods: { @@ -278,8 +280,8 @@ STUDIP.ready(function () { data: params, type: 'post', success() { - if (v.STUDIPFORM_REDIRECTURL) { - window.location.href = v.STUDIPFORM_REDIRECTURL + if (v.STUDIPFORM_REDIRECTURL && !v.STUDIPFORM_DEBUGMODE) { + window.location.href = v.STUDIPFORM_REDIRECTURL; } } }); @@ -336,6 +338,13 @@ STUDIP.ready(function () { this[key] = value; } } + }, + selectLanguage(input_name, language_id) { + let languages = { + ...this.STUDIPFORM_SELECTEDLANGUAGES + }; + languages[input_name] = language_id; + this.STUDIPFORM_SELECTEDLANGUAGES = languages; } }, mounted () { diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index 915d960..9fcb57b 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -82,6 +82,7 @@ import "./bootstrap/admin-courses.js" import "./bootstrap/cache-admin.js" import "./bootstrap/oer.js" import "./bootstrap/courseware.js" +import "./bootstrap/contentmodules.js" import "./bootstrap/responsive-navigation.js" import "./bootstrap/treeview.js" import "./bootstrap/stock-images.js" diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js index 5527602..8ea0a27 100644 --- a/resources/assets/javascripts/init.js +++ b/resources/assets/javascripts/init.js @@ -54,7 +54,6 @@ import Overlay from './lib/overlay.js'; import PageLayout from './lib/page_layout.js'; import parseOptions from './lib/parse_options.js'; import PersonalNotifications from './lib/personal_notifications.js'; -import Plus from './lib/plus.js'; import QRCode from './lib/qr_code.js'; import Questionnaire from './lib/questionnaire.js'; import QuickSearch from './lib/quick_search.js'; @@ -145,7 +144,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, { PageLayout, parseOptions, PersonalNotifications, - Plus, QRCode, Questionnaire, QuickSearch, diff --git a/resources/assets/javascripts/lib/plus.js b/resources/assets/javascripts/lib/plus.js deleted file mode 100644 index 0d447fd..0000000 --- a/resources/assets/javascripts/lib/plus.js +++ /dev/null @@ -1,23 +0,0 @@ -const Plus = { - setModule: function () { - $.ajax({ - "url": STUDIP.URLHelper.getURL("dispatch.php/course/plus/trigger"), - "data": { - "moduleclass": $(this).data("moduleclass"), - "key": $(this).data("key"), - "active": $(this).is(":checked") ? 1 : 0 - }, - "dataType": "json", - "type": "post", - "success": function (output) { - if (output.tabs) { - $(".tabs_wrapper").replaceWith(output.tabs); - } - } - }); - } -}; - - - -export default Plus; diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss index d133615..fe376db 100644 --- a/resources/assets/stylesheets/scss/forms.scss +++ b/resources/assets/stylesheets/scss/forms.scss @@ -324,6 +324,7 @@ form.default { > * { box-sizing: border-box; flex: 1 0 auto; + max-width: 400px; &:not(:first-child) { margin-left: 3px; @@ -596,3 +597,5 @@ form.inline { } } } + + diff --git a/resources/assets/stylesheets/scss/plus.scss b/resources/assets/stylesheets/scss/plus.scss deleted file mode 100644 index ea0b7b3..0000000 --- a/resources/assets/stylesheets/scss/plus.scss +++ /dev/null @@ -1,79 +0,0 @@ -.plus { - .element_header { - display: inline-block; - width: 250px; - margin-left: 5px; - } - - .element_description { - display: inline-block; - margin-left: 20px; - } - - .plugin_icon { - width: 16px; - height: 16px; - } - - .shortdesc { - margin-left: 3px; - } - - .plus_expert { - margin-left: 20px; - width: 97%; - - display: flex; - flex-wrap: wrap; - } - - .screenshot_holder { - width: 250px; - flex: 0 250px; - margin-right: 5mm; - box-sizing: border-box; - } - - .big_thumb { - max-width: 250px; - max-height: 250px; - padding-top: 5mm; - } - - .small_thumb { - margin-left: 2px; - margin-top: 5px; - max-height: 25px; - } - - .thumb_holder { - width: 250px; - text-align: center; - background-color: $content-color-20; - border-top: 1px solid mix($brand-color-lighter, $white, 80%); - border-bottom: 1px solid mix($brand-color-lighter, $white, 80%); - } - - .descriptionbox { - flex: 1 305px; - max-width: 45em; - } - - .keywords { - padding: 5mm; - left: 5mm; - position: relative; - } - - .longdesc { - overflow: hidden; - } - - .helplink { - float: right; - } - - article.studip > section:not(:last-child) { - border-bottom: 1px solid $table-header-color; - } -} diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index be72f0a..edbc2e1 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -70,7 +70,6 @@ @import "scss/pagination"; @import "scss/personal-notifications"; @import "scss/plugins"; -@import "scss/plus"; @import "scss/progress_indicator.scss"; @import "scss/profile"; @import "scss/qrcode"; diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js index ccff7c5..b8cf935 100644 --- a/resources/vue/base-components.js +++ b/resources/vue/base-components.js @@ -13,6 +13,7 @@ import RangeInput from './components/RangeInput.vue'; import Datetimepicker from './components/Datetimepicker.vue'; import TextareaWithToolbar from './components/TextareaWithToolbar.vue'; import I18nTextarea from "./components/I18nTextarea.vue"; +import StudipWysiwyg from "./components/StudipWysiwyg.vue"; // import StudipLoadingIndicator from './StudipLoadingIndicator.vue'; import StudipMessageBox from './components/StudipMessageBox.vue'; import StudipProxyCheckbox from './components/StudipProxyCheckbox.vue'; @@ -36,6 +37,7 @@ const BaseComponents = { StudipFolderSize, StudipIcon, I18nTextarea, + StudipWysiwyg, // StudipLoadingIndicator, StudipMessageBox, StudipProxyCheckbox, diff --git a/resources/vue/components/ContentModules.vue b/resources/vue/components/ContentModules.vue new file mode 100644 index 0000000..0a9d54b --- /dev/null +++ b/resources/vue/components/ContentModules.vue @@ -0,0 +1,218 @@ + + + + diff --git a/resources/vue/components/ContentModulesControl.vue b/resources/vue/components/ContentModulesControl.vue new file mode 100644 index 0000000..0bfcc0d --- /dev/null +++ b/resources/vue/components/ContentModulesControl.vue @@ -0,0 +1,91 @@ + + + diff --git a/resources/vue/components/ContentModulesEditTiles.vue b/resources/vue/components/ContentModulesEditTiles.vue new file mode 100644 index 0000000..2edd439 --- /dev/null +++ b/resources/vue/components/ContentModulesEditTiles.vue @@ -0,0 +1,165 @@ + + + diff --git a/resources/vue/components/ContentmodulesEditTable.vue b/resources/vue/components/ContentmodulesEditTable.vue new file mode 100644 index 0000000..144f920 --- /dev/null +++ b/resources/vue/components/ContentmodulesEditTable.vue @@ -0,0 +1,100 @@ + + + + diff --git a/resources/vue/components/Datetimepicker.vue b/resources/vue/components/Datetimepicker.vue index 87f6dfd..6b12ad7 100644 --- a/resources/vue/components/Datetimepicker.vue +++ b/resources/vue/components/Datetimepicker.vue @@ -33,8 +33,12 @@ export default { setUnixTimestamp () { let formatted_date = this.$refs.visibleInput.value; let date = formatted_date.match(/(\d+)/g); - date = new Date(`${date[2]}-${date[1]}-${date[0]} ${date[3]}:${date[4]}`); - this.$emit('input', Math.floor(date / 1000)); + if (date) { + date = new Date(`${date[2]}-${date[1]}-${date[0]} ${date[3]}:${date[4]}`); + this.$emit('input', Math.floor(date / 1000)); + } else { + this.$emit('input', null); + } } }, mounted () { diff --git a/resources/vue/components/I18nTextarea.vue b/resources/vue/components/I18nTextarea.vue index 79a810d..6d31d7b 100644 --- a/resources/vue/components/I18nTextarea.vue +++ b/resources/vue/components/I18nTextarea.vue @@ -144,12 +144,14 @@ export default { values[this.selectedLanguage.id] = this.value; this.values = values; } + this.$emit('selectlanguage', this.selectedLanguage.id); }, methods: { selectLanguage (e) { for (let i in this.languages) { if (e.target.value === this.languages[i].id) { this.selectedLanguage = this.languages[i]; + this.$emit('selectlanguage', this.languages[i].id); this.$nextTick(() => { if (typeof this.$refs.inputfield.focus === "function") { this.$refs.inputfield.focus(); diff --git a/resources/vue/mixins/ContentModulesMixin.js b/resources/vue/mixins/ContentModulesMixin.js new file mode 100644 index 0000000..7df586d --- /dev/null +++ b/resources/vue/mixins/ContentModulesMixin.js @@ -0,0 +1,125 @@ +import draggable from 'vuedraggable'; +import { mapActions, mapState } from 'vuex'; + +export default { + components: { + draggable, + }, + data: () => ({ + order: [], + }), + computed: { + ...mapState('contentmodules', [ + 'categories', + 'filterCategory', + 'highlighted', + 'modules', + 'view', + ]), + activeModules() { + return this.sortedModules.filter(module => module.active); + }, + sortedModules: { + get() { + return Object.values(this.modules) + .filter(module => { + return this.filterCategory === null + || this.filterCategory === module.category; + }) + .sort(function (a, b) { + if (a.active && !b.active) { + return -1; + } else if (!a.active && b.active) { + return 1; + } else if (a.active) { + return a.position - b.position; + } else { + return a.displayname.localeCompare(b.displayname); + } + }); + }, + set(modules) { + let position = 0; + for (const key in modules) { + modules[key].position = position++; + } + this.exchangeModules(modules).then((output) => { + if (output.tabs) { + $('.tabs_wrapper').replaceWith(output.tabs); + } + }); + }, + }, + }, + methods: { + ...mapActions('contentmodules', [ + 'changeView', + 'exchangeModules', + 'setModuleActive', + 'setModuleVisible', + 'swapModules', + ]), + keyboardHandler(event, module) { + const activeIndex = this.activeModules.findIndex(m => m.id === module.id); + + let otherModule = null; + if (event.key === 'ArrowUp' && activeIndex > 0) { + otherModule = this.activeModules[activeIndex - 1]; + } else if (event.key === 'ArrowDown' && activeIndex !== this.activeModules.length - 1) { + otherModule = this.activeModules[activeIndex + 1]; + } + + if (otherModule === null) { + return; + } + + event.preventDefault(); + + this.swapModules({ + moduleA: module, + moduleB: otherModule, + }).then((output) => { + if (output.tabs) { + $('.tabs_wrapper').replaceWith(output.tabs); + } + }).then(() => { + this.$nextTick(() => { + this.$refs[`draghandle-${module.id}`][0].focus(); + }); + }); + }, + toggleModuleActivation(module) { + this.setModuleActive({ + moduleId: module.id, + active: !module.active, + }).then((output) => { + if (output.tabs) { + $('.tabs_wrapper').replaceWith(output.tabs); + } + }); + }, + toggleModuleVisibility(module) { + this.setModuleVisible({ + moduleId: module.id, + visible: module.visibility === 'tutor', + }).then((output) => { + if (output.tabs) { + $('.tabs_wrapper').replaceWith(output.tabs); + } + }); + }, + getRenameURL(module) { + return STUDIP.URLHelper.getURL(`dispatch.php/course/contentmodules/rename/${module.id}`); + }, + getDescriptionURL(module) { + return STUDIP.URLHelper.getURL(`dispatch.php/course/contentmodules/info/${module.id}`); + }, + getModuleCSSClasses(module, active= null) { + if (!(active ?? module.active)) { + return 'inactive'; + } + + return module.visibility === 'tutor' ? 'visibility-invisible' : 'visibility-visible'; + }, + }, +}; diff --git a/resources/vue/store/ContentModulesStore.js b/resources/vue/store/ContentModulesStore.js new file mode 100644 index 0000000..9dc4609 --- /dev/null +++ b/resources/vue/store/ContentModulesStore.js @@ -0,0 +1,124 @@ +export default { + namespaced: true, + + state: () => ({ + categories: [], + filterCategory: null, + highlighted: [], + modules: [], + userId: null, + view: 'tiles', + }), + getters: { + getModuleById: (state) => (moduleId) => { + return state.modules.find(module => module.id === moduleId); + }, + }, + mutations: { + setCategories(state, categories) { + state.categories = categories; + }, + setFilterCategory(state, category) { + state.filterCategory = category; + }, + setHighlighted(state, highlighted) { + state.highlighted = highlighted; + }, + setModule(state, module) { + let modules = state.modules.filter(m => m.id !== module.id); + modules.push(module); + + state.modules = modules; + }, + setModules(state, modules) { + state.modules = modules; + }, + setUserId(state, userId) { + state.userId = userId; + }, + setView(state, view) { + state.view = view; + }, + }, + actions: { + changeView({ commit, state }, view) { + commit('setView', view); + + const documentId = `${state.userId}_CONTENTMODULES_TILED_DISPLAY`; + + const data = { + id: documentId, + type: 'config-values', + attributes: { value: view === 'tiles' } + }; + + return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } }) ; + }, + exchangeModules({ commit, state }, modules) { + const order = modules.filter(module => module.active) + .sort((a, b) => a.position - b.position) + .map(module => module.id); + return $.post( + STUDIP.URLHelper.getURL('dispatch.php/course/contentmodules/reorder'), + { order } + ).then((output) => { + commit('setModules', modules); + + return output; + }); + }, + setModuleActive({ commit, state, getters }, { moduleId, active }) { + const module = getters.getModuleById(moduleId); + module.active = active; + + return $.post( + STUDIP.URLHelper.getURL('dispatch.php/course/contentmodules/trigger'), + { + moduleclass: module.moduleclass, + plugin_id: module.id, + active: module.active ? 1 : 0 + } + ).done((output) => { + module.position = output.position; + commit('setModule', module); + + return output; + }); + }, + setModuleVisible({ commit, state, getters }, { moduleId, visible }) { + const module = getters.getModuleById(moduleId); + + return $.post( + STUDIP.URLHelper.getURL('dispatch.php/course/contentmodules/change_visibility'), + { + moduleclass: module.moduleclass, + plugin_id: module.id, + visible: visible ? 1 : 0, + } + ).done((output) => { + module.visibility = output.visibility; + commit('setModule', module); + }); + }, + swapModules({ dispatch, state, getters }, { moduleA, moduleB }) { + let modules = state.modules.map(module => { + if (module.id === moduleA.id) { + return { + ...moduleA, + position: moduleB.position, + }; + } + + if (module.id === moduleB.id) { + return { + ...moduleB, + position: moduleA.position, + }; + } + + return module; + }); + return dispatch('exchangeModules', modules); + }, + } +} diff --git a/templates/forms/form.php b/templates/forms/form.php index 21eb9c8..094805e 100644 --- a/templates/forms/form.php +++ b/templates/forms/form.php @@ -24,6 +24,7 @@ $form_id = md5(uniqid()); novalidate id="" data-inputs="" + data-debugmode="getDebugMode())) ?>" data-required="" class="default studipformisCollapsable() ? ' collapsable' : '' ?>"> @@ -31,7 +32,7 @@ $form_id = md5(uniqid());
+ v-if="STUDIPFORM_REQUIRED.length > 0 || STUDIPFORM_VALIDATIONNOTES.length > 0">

asImg(17, ['class' => "text-bottom validation_notes_icon"]) ?> diff --git a/templates/forms/i18n_formatted_input.php b/templates/forms/i18n_formatted_input.php index 4e667f5..6466731 100644 --- a/templates/forms/i18n_formatted_input.php +++ b/templates/forms/i18n_formatted_input.php @@ -12,6 +12,7 @@ name="" value="" @allinputs="setInputs" + @selectlanguage="(language_id) => selectLanguage('name) ?>', language_id)" :wysiwyg_disabled="WYSIWYG ? 'false' : 'true' ?>" > diff --git a/templates/forms/i18n_textarea_input.php b/templates/forms/i18n_textarea_input.php index 3209b68..d9b2ff3 100644 --- a/templates/forms/i18n_textarea_input.php +++ b/templates/forms/i18n_textarea_input.php @@ -12,6 +12,7 @@ name="name) ?>" value="" + @selectlanguage="(language_id) => selectLanguage('name) ?>', language_id)" @allinputs="setInputs"> diff --git a/templates/forms/info_input.php b/templates/forms/info_input.php new file mode 100644 index 0000000..7461b7c --- /dev/null +++ b/templates/forms/info_input.php @@ -0,0 +1,8 @@ +
+
+
+

+
+ +
+
diff --git a/templates/forms/wysiwyg_input.php b/templates/forms/wysiwyg_input.php new file mode 100644 index 0000000..989bb5c --- /dev/null +++ b/templates/forms/wysiwyg_input.php @@ -0,0 +1,16 @@ +
+ required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + + > + +
-- cgit v1.0