diff --git a/app/controllers/admin/courses.php b/app/controllers/admin/courses.php index 258c7e97ed06b9cc01e6629cf2c31e80b78048da..6eb99181090f63086f4f695c702dd15f26985af0 100644 --- a/app/controllers/admin/courses.php +++ b/app/controllers/admin/courses.php @@ -1417,14 +1417,6 @@ class Admin_CoursesController extends AuthenticatedController 'attributes' => ['data-dialog' => 'size=auto'], 'partial' => 'notice-action.php', ], - 21 => [ - 'name' => _('Mehrfachzuordnung von Studienbereichen'), - 'title' => _('Mehrfachzuordnung von Studienbereichen'), - 'url' => 'dispatch.php/admin/tree/batch_assign_semtree', - 'dialogform' => true, - 'multimode' => true, - 'partial' => 'batch_assign_semtree.php' - ], 22 => [ 'name' => _('Teilnehmendenexport'), 'title' => _('Teilnehmendenexport'), diff --git a/app/controllers/admin/tree.php b/app/controllers/admin/tree.php deleted file mode 100644 index 18ddb0652774917d3d91814070daabc755ba23ad..0000000000000000000000000000000000000000 --- a/app/controllers/admin/tree.php +++ /dev/null @@ -1,292 +0,0 @@ -<?php - -class Admin_TreeController extends AuthenticatedController -{ - public function rangetree_action() - { - $GLOBALS['perm']->check('root'); - Navigation::activateItem('/admin/locations/range_tree'); - PageLayout::setTitle(_('Einrichtungshierarchie bearbeiten')); - $this->startId = Request::get('node_id', 'RangeTreeNode_root'); - $this->semester = Request::option('semester', Semester::findCurrent()->id); - $this->classname = RangeTreeNode::class; - $this->setupSidebar(); - } - - public function semtree_action() - { - $GLOBALS['perm']->check('root'); - Navigation::activateItem('/admin/locations/sem_tree'); - PageLayout::setTitle(_('Veranstaltungshierarchie bearbeiten')); - $this->startId = Request::get('node_id', 'StudipStudyArea_root'); - $this->semester = Request::option('semester', Semester::findCurrent()->id); - $this->classname = StudipStudyArea::class; - $this->setupSidebar(); - } - - /** - * Edit the given node. - * - * @param string $class_id concatenated classname and node id - * @return void - */ - public function edit_action(string $class_id) - { - $GLOBALS['perm']->check('root'); - PageLayout::setTitle(_('Eintrag bearbeiten')); - - $data = $this->checkClassAndId($class_id); - $this->node = $data['classname']::getNode($data['id']); - $parent = $data['classname']::getNode($this->node->parent_id); - - $this->treesearch = QuickSearch::get( - 'parent_id', - new TreeSearch($data['classname'] === StudipStudyArea::class ? 'sem_tree_id' : 'range_tree_id') - )->withButton(); - $this->treesearch->defaultValue($parent->id, $parent->getName()); - - if ($data['classname'] === RangeTreeNode::class) { - $this->instsearch = QuickSearch::get( - 'studip_object_id', - new StandardSearch('Institut_id') - )->withButton(); - if ($this->node->studip_object_id) { - $this->instsearch->defaultValue($this->node->studip_object_id, $this->node->institute->name); - } - } - - $this->from = Request::get('from'); - } - - /** - * Create a new child node of the given parent. - * - * @param string $class_id concatenated classname and parent id - * @return void - */ - public function create_action(string $class_id) - { - $GLOBALS['perm']->check('root'); - PageLayout::setTitle(_('Neuen Eintrag anlegen')); - - $data = $this->checkClassAndId($class_id); - - $this->node = new $data['classname'](); - $this->node->parent_id = $data['id']; - $parent = $data['classname']::getNode($data['id']); - - $this->treesearch = QuickSearch::get( - 'parent_id', - new TreeSearch(get_class($this->node) === StudipStudyArea::class ? 'sem_tree_id' : 'range_tree_id') - )->withButton(); - $this->treesearch->defaultValue($parent->id, $parent->getName()); - - $this->instsearch = QuickSearch::get( - 'studip_object_id', - new StandardSearch('Institut_id') - )->withButton(); - - $this->from = Request::get('from'); - } - - /** - * Delete the given child node. - * - * @param string $class_id concatenated classname and node id - * @return void - */ - public function delete_action(string $class_id) - { - $GLOBALS['perm']->check('root'); - $data = $this->checkClassAndId($class_id); - - if (!Request::isPost()) { - throw new MethodNotAllowedException(); - } - $node = $data['classname']::getNode($data['id']); - - if ($node) { - $node->delete(); - } else { - $this->set_status(404); - } - - $this->render_nothing(); - } - - /** - * Store the given node. - * - * @param string $classname - * @param string $node_id - * @return void - */ - public function store_action(string $classname, string $node_id = '') - { - $GLOBALS['perm']->check('root'); - CSRFProtection::verifyUnsafeRequest(); - - $node = new $classname($node_id); - $node->parent_id = Request::option('parent_id'); - - $parent = $classname::getNode(Request::option('parent_id')); - $maxprio = max(array_map( - function ($c) { - return $c->priority; - }, - $parent->getChildNodes() - )); - $node->priority = $maxprio + 1; - - if (Request::option('studip_object_id')) { - $node->studip_object_id = Request::option('studip_object_id'); - $node->name = ''; - } else { - $node->name = Request::get('name'); - } - - if ($classname === StudipStudyArea::class) { - $node->info = Request::get('description'); - $node->type = Request::int('type'); - } - - if ($node->store() !== false) { - Pagelayout::postSuccess(_('Die Daten wurden gespeichert.')); - } else { - Pagelayout::postError(_('Die Daten konnten nicht gespeichert werden.')); - } - - $this->relocate(Request::get('from')); - } - - public function sort_action($parent_id) - { - $GLOBALS['perm']->check('root'); - $data = $this->checkClassAndId($parent_id); - - $parent = $data['classname']::getNode($data['id']); - $children = $parent->getChildNodes(); - - $data = json_decode(Request::get('sorting'), true); - - foreach ($children as $child) { - $child->priority = $data[$child->id]; - $child->store(); - } - - $this->render_nothing(); - } - - /** - * (De-)assign several courses at once to a sem_tree node - * @return void - * @throws Exception - */ - public function batch_assign_semtree_action() - { - $GLOBALS['perm']->check('admin'); - //set the page title with the area of Stud.IP: - PageLayout::setTitle(_('Veranstaltungszuordnungen bearbeiten')); - Navigation::activateItem('/browse/my_courses/list'); - - $GLOBALS['perm']->check('admin'); - - // check the assign_semtree array and extract the relevant course IDs: - $courseIds = Request::optionArray('assign_semtree'); - - $order = Config::get()->IMPORTANT_SEMNUMBER - ? "ORDER BY `start_time` DESC, `VeranstaltungsNummer`, `Name`" - : "ORDER BY `start_time` DESC, `Name`"; - $this->courses = Course::findMany($courseIds, $order); - - $this->return = Request::get('return'); - - // check if at least one course was selected (this can only happen from admin courses overview): - if (!$courseIds) { - PageLayout::postWarning('Es wurde keine Veranstaltung gewählt.'); - $this->relocate('admin/courses'); - } - } - - public function assign_courses_action($class_id) - { - $GLOBALS['perm']->check('root'); - $data = $this->checkClassAndId($class_id); - $GLOBALS['perm']->check('admin'); - - $this->search = QuickSearch::get('courses[]', new StandardSearch('Seminar_id'))->withButton(); - $this->node = $data['id']; - } - - /** - * Store (de-)assignments from courses to sem_tree nodes. - * @return void - */ - public function do_batch_assign_action() - { - $GLOBALS['perm']->check('admin'); - $astmt = DBManager::get()->prepare("INSERT IGNORE INTO `seminar_sem_tree` VALUES (:course, :node)"); - $dstmt = DBManager::get()->prepare( - "DELETE FROM `seminar_sem_tree` WHERE `seminar_id` IN (:courses) AND `sem_tree_id` = :node"); - - $success = true; - // Add course assignments to the specified nodes. - foreach (Request::optionArray('courses') as $course) { - foreach (Request::optionArray('add_assignments') as $a) { - $success = $astmt->execute(['course' => $course, 'node' => $a]); - } - } - - // Remove course assignments from the specified nodes. - foreach (Request::optionArray('delete_assignments') as $d) { - $success = $dstmt->execute(['courses' => Request::optionArray('courses'), 'node' => $d]); - } - - if ($success) { - PageLayout::postSuccess(_('Die Zuordnungen wurden gespeichert.')); - } else { - PageLayout::postError(_('Die Zuordnungen konnten nicht vollständig gespeichert werden.')); - } - - $this->relocate(Request::get('return', 'admin/courses')); - } - - private function setupSidebar() - { - $sidebar = Sidebar::Get(); - - $semWidget = new SemesterSelectorWidget($this->url_for(''), 'semester'); - $semWidget->includeAll(true); - $semWidget->setId('semester-selector'); - $semWidget->setSelection($this->semester); - $sidebar->addWidget($semWidget); - - if ($this->classname === StudipStudyArea::class) { - $sidebar->addWidget(new VueWidget('assign-widget')); - } - } - - /** - * CHeck a combination of class name and ID for validity: is this a StudipTreeNode subclass? - * If yes, return the corresponding object. - * - * @param string $class_id class name and ID, separated by '_' - * @return mixed - */ - private function checkClassAndId($class_id) - { - list($classname, $id) = explode('_', $class_id); - - if (is_a($classname, StudipTreeNode::class, true)) { - return [ - 'classname' => $classname, - 'id' => $id - ]; - } - - throw new InvalidArgumentException( - sprintf('The given class "%s" does not implement the StudipTreeNode interface!', $classname) - ); - - } -} diff --git a/app/controllers/search/courses.php b/app/controllers/search/courses.php index 71b7f7d689a231e484091b993d70e0f907d6a292..536c361ae412fdef1f33cb94ce0dc5955f632122 100644 --- a/app/controllers/search/courses.php +++ b/app/controllers/search/courses.php @@ -27,30 +27,108 @@ class Search_CoursesController extends AuthenticatedController PageLayout::setHelpKeyword('Basis.VeranstaltungenAbonnieren'); - $this->type = Request::option('type', 'semtree'); - $this->semester = Request::option('semester', Semester::findCurrent()->id); - $this->semClass = Request::int('semclass', 0); + // activate navigation item + $nav_options = Config::get()->COURSE_SEARCH_NAVIGATION_OPTIONS; + URLHelper::bindLinkParam('option', $this->nav_option); + if (!empty($nav_options[$this->nav_option]) + && Navigation::hasItem('/search/courses/' . $this->nav_option)) { + Navigation::activateItem('/search/courses/' . $this->nav_option); + } else { + URLHelper::removeLinkParam('option'); + $level = Request::get('level', $_SESSION['sem_browse_data']['level'] ?? ''); + $default_option = SemBrowse::getSearchOptionNavigation('sidebar'); + if (!$level) { + PageLayout::setTitle(_($default_option->getTitle())); + $this->relocate($default_option->getURL()); + } elseif ($level == 'f' && $nav_options['courses']['visible']) { + $course_option = SemBrowse::getSearchOptionNavigation('sidebar','courses'); + PageLayout::setTitle(_($course_option->getTitle())); + Navigation::activateItem('/search/courses/semtree'); + } elseif (($level == 'vv') && $nav_options['semtree']['visible']) { + $semtree_option = SemBrowse::getSearchOptionNavigation('sidebar','semtree'); + PageLayout::setTitle(_($semtree_option->getTitle())); + Navigation::activateItem('/search/courses/semtree'); + } elseif ($level == 'ev' && $nav_options['rangetree']['visible']) { + $rangetree_option = SemBrowse::getSearchOptionNavigation('sidebar','rangetree'); + PageLayout::setTitle(_($rangetree_option->getTitle())); + Navigation::activateItem('/search/courses/rangetree'); + } else { + throw new AccessDeniedException(); + } + } } public function index_action() { - $nodeClass = ''; - if (Request::option('type', 'semtree') === 'semtree') { - Navigation::activateItem('/search/courses/semtree'); - $nodeClass = StudipStudyArea::class; - $this->treeTitle = _('Studienbereiche'); - $this->breadcrumbIcon = 'literature'; - $this->editUrl = $this->url_for('studyarea/edit'); - } else if (Request::option('type', 'semtree') === 'rangetree') { - Navigation::activateItem('/search/courses/rangetree'); - $nodeClass = RangeTreeNode::class; - $this->treeTitle = _('Einrichtungen'); - $this->breadcrumbIcon = 'institute'; - $this->editUrl = $this->url_for('rangetree/edit'); + SemBrowse::transferSessionData(); + $this->sem_browse_obj = new SemBrowse(); + + if (!$GLOBALS['perm']->have_perm('root')) { + $this->sem_browse_obj->target_url = 'dispatch.php/course/details/'; + $this->sem_browse_obj->target_id = 'sem_id'; + } else { + $this->sem_browse_obj->target_url = 'seminar_main.php'; + $this->sem_browse_obj->target_id = 'auswahl'; + } + + $sidebar = Sidebar::get(); + + // add search options to sidebar + $level = Request::get('level', $_SESSION['sem_browse_data']['level'] ?? ''); + + $widget = new OptionsWidget(); + $widget->setTitle(_('Suche')); + //add a quicksearch input inside the widget + $search_content = $this->sem_browse_obj->getQuickSearchForm(); + $search_element = new WidgetElement($search_content); + $widget->addElement($search_element); + $widget->addCheckbox(_('Erweiterte Suche anzeigen'), + $_SESSION['sem_browse_data']['cmd'] == "xts", + URLHelper::getURL('?level='.$level.'&cmd=xts&sset=0&option='), + URLHelper::getURL('?level='.$level.'&cmd=qs&sset=0&option=')); + $sidebar->addWidget($widget); + + SemBrowse::setSemesterSelector($this->url_for('search/courses/index')); + SemBrowse::setClassesSelector($this->url_for('search/courses/index')); + + + if ($this->sem_browse_obj->show_result + && count($_SESSION['sem_browse_data']['search_result'])) { + $actions = new ActionsWidget(); + $actions->addLink(_('Download des Ergebnisses'), + URLHelper::getURL('dispatch.php/search/courses/export_results'), + Icon::create('file-office', 'clickable')); + $sidebar->addWidget($actions); + + $grouping = new OptionsWidget(); + $grouping->setTitle(_('Suchergebnis gruppieren:')); + foreach ($this->sem_browse_obj->group_by_fields as $i => $field) { + $grouping->addRadioButton( + $field['name'], + URLHelper::getURL('?', ['group_by' => $i, + 'keep_result_set' => 1]), + $_SESSION['sem_browse_data']['group_by'] == $i + ); + } + $sidebar->addWidget($grouping); + } + + // show information about course class if class was changed + $class = $GLOBALS['SEM_CLASS'][$_SESSION['sem_browse_data']['show_class']] ?? null; + if (is_object($class) && $class->countSeminars() > 0) { + if (trim($GLOBALS['SEM_CLASS'][$_SESSION['sem_browse_data']['show_class']]['description'])) { + PageLayout::postInfo(sprintf(_('Gewählte Veranstaltungsklasse <i>%1s</i>: %2s'), + $GLOBALS['SEM_CLASS'][$_SESSION['sem_browse_data']['show_class']]['name'], + $GLOBALS['SEM_CLASS'][$_SESSION['sem_browse_data']['show_class']]['description'])); + } else { + PageLayout::postInfo(sprintf(_('Gewählte Veranstaltungsklasse <i>%1s</i>.'), + $GLOBALS['SEM_CLASS'][$_SESSION['sem_browse_data']['show_class']]['name'])); + } + } elseif ($_SESSION['sem_browse_data']['show_class'] != 'all') { + PageLayout::postInfo(_('Im gewählten Semester ist in dieser Veranstaltungsklasse keine Veranstaltung verfügbar. Bitte wählen Sie eine andere Veranstaltungsklasse oder ein anderes Semester!')); } - $this->startId = Request::option('node_id', $nodeClass . '_root'); - $this->setupSidebar(); + $this->controller = $this; } public function export_results_action() @@ -65,33 +143,4 @@ class Search_CoursesController extends AuthenticatedController } } - private function setupSidebar() - { - $sidebar = Sidebar::Get(); - - $semWidget = new SemesterSelectorWidget($this->url_for(''), 'semester'); - $semWidget->includeAll(false); - $semWidget->setId('semester-selector'); - $semWidget->setSelection($this->semester); - $sidebar->addWidget($semWidget); - - $classWidget = $sidebar->addWidget(new SelectWidget( - _('Veranstaltungskategorie'), - URLHelper::getURL('', ['type' => $this->type, 'semester' => $this->semester]), - 'semclass' - )); - $classWidget->addElement(new SelectElement(0, _('Alle'))); - foreach (SemClass::getClasses() as $class) { - if (!$class['studygroup_mode']) { - $classWidget->addElement(new SelectElement( - $class['id'], - $class['name'], - $this->semClass == $class['id'] - )); - } - } - - $sidebar->addWidget(new VueWidget('search-widget')); - $sidebar->addWidget(new VueWidget('export-widget')); - } } diff --git a/app/controllers/studyarea.php b/app/controllers/studyarea.php deleted file mode 100644 index e43c9b0af15bfe919b648379ed47e6f8f2368ffc..0000000000000000000000000000000000000000 --- a/app/controllers/studyarea.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php - -/** - * treenode.php - Controller for editing tree nodes - * - * 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. - * - * @author Thomas Hackl <hackl@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 or later - * @category Stud.IP - * @since 5.4 - */ - -class StudyareaController extends AuthenticatedController -{ - public function edit_action($id = '') - { - if ($id !== '') { - $object = StudipStudyArea::find($id); - } else { - $object = new StudipStudyArea(); - } - - PageLayout::setTitle($object->isNew() ? _('Studienbereich anlegen') : _('Studienbereich bearbeiten')); - - $this->form = Studip\Forms\Form::fromSORM( - $object, - [ - 'legend' => $object->isNew() - ? _('Neuer Studienbereich') - : sprintf(_('Studienbereich %s'), $object->name), - 'text' => ['text' => ''], - 'fields' => [ - 'name' => [ - 'label' => _('Name'), - 'type' => 'text', - 'required' => true - ], - 'info' => [ - 'label' => _('Beschreibung'), - 'type' => 'textarea' - ] - ] - ] - )->setURL($this->url_for('studyarea/store', $object->id)); - } - -} diff --git a/app/controllers/tree.php b/app/controllers/tree.php deleted file mode 100644 index 7665b60c10a4c79c6c27f7e473a5c42eb206eb6b..0000000000000000000000000000000000000000 --- a/app/controllers/tree.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php - -class TreeController extends AuthenticatedController -{ - public function export_csv_action() - { - if (!Request::isPost()) { - throw new MethodNotAllowedException(); - } - - $ids = explode(',', Request::get('courses', '')); - $courses = Course::findMany($ids); - - $captions = [ - _('Veranstaltungsnummer'), - _('Name'), - _('Semester'), - _('Zeiten'), - _('Lehrende') - ]; - - $data = []; - foreach ($courses as $course) { - $sem = Seminar::getInstance($course->id); - $lecturers = SimpleCollection::createFromArray( - CourseMember::findByCourseAndStatus($course->id, 'dozent') - )->orderBy('position, nachname, vorname'); - - $lecturersSorted = array_map( - function ($l) { - return implode(', ', $l); - }, - $lecturers->toArray('nachname vorname title_front title_rear') - ); - - $data[] = [ - $course->veranstaltungsnummer, - $course->getFullname('type-number-name'), - $course->getTextualSemester(), - $sem->getDatesExport(), - implode(', ', $lecturersSorted) - ]; - } - - $tmpname = md5(uniqid('ErgebnisVeranstaltungssuche')); - if (array_to_csv($data, $GLOBALS['TMP_PATH'] . '/' . $tmpname, $captions)) { - $this->render_text(FileManager::getDownloadURLForTemporaryFile( - $tmpname, - 'veranstaltungssuche.csv' - )); - } else { - $this->set_status(400, 'The csv could not be created.'); - } - } -} diff --git a/app/views/admin/courses/batch_assign_semtree.php b/app/views/admin/courses/batch_assign_semtree.php deleted file mode 100644 index b28e96bb6117dce99f6337d5e4a8069ff8103262..0000000000000000000000000000000000000000 --- a/app/views/admin/courses/batch_assign_semtree.php +++ /dev/null @@ -1,8 +0,0 @@ -<?php -/** - * @var Course $course - */ -?> -<label> - <input name="assign_semtree[]" type="checkbox" value="<?= htmlReady($course->id) ?>"> -</label> diff --git a/app/views/admin/tree/assign_courses.php b/app/views/admin/tree/assign_courses.php deleted file mode 100644 index df57aef0acad5be1f8626b8deee012353555bb27..0000000000000000000000000000000000000000 --- a/app/views/admin/tree/assign_courses.php +++ /dev/null @@ -1,10 +0,0 @@ -<form action="<?= $controller->link_for('admin/tree/do_batch_assign') ?>" method="post"> - <section> - <?= $search->render() ?> - </section> - <input type="hidden" name="node" value="<?= htmlReady($node) ?>"> - <footer data-dialog-button> - <?= Studip\Button::createAccept(_('Zuordnen'), 'assign') ?> - <?= Studip\Button::createCancel(_('Abbrechen'), 'cancel', ['data-dialog' => 'close']) ?> - </footer> -</form> diff --git a/app/views/admin/tree/batch_assign_semtree.php b/app/views/admin/tree/batch_assign_semtree.php deleted file mode 100644 index c28660281182799305918f5e1a1c1854be204d68..0000000000000000000000000000000000000000 --- a/app/views/admin/tree/batch_assign_semtree.php +++ /dev/null @@ -1,43 +0,0 @@ -<form class="default" action="<?= $controller->link_for('admin/tree/do_batch_assign') ?>" method="post"> - <fieldset> - <legend><?= _('Studienbereichszuordnungen der ausgewählten Veranstaltungen bearbeiten') ?></legend> - <div data-studip-tree> - <studip-tree start-id="StudipStudyArea_root" :with-info="false" :open-levels="1" - :assignable="true"></studip-tree> - </div> - </fieldset> - <fieldset> - <legend><?= _('Diese Veranstaltungen werden zugewiesen') ?></legend> - <table class="default selected-courses"> - <colgroup> - <col> - </colgroup> - <thead> - <tr> - <th><?= _('Name') ?></th> - </tr> - </thead> - <tbody> - <? foreach ($courses as $course) : ?> - <tr> - <td> - <a href="<?= URLHelper::getLink('dispatch.php/course/overview', ['cid' => $course->id])?>" - title="<?= sprintf(_('Zur Veranstaltung %s'), htmlReady($course->getFullname())) ?>" - target="_blank"> - <?= htmlReady($course->getFullname('number-name-semester')) ?> - </a> - <input type="hidden" name="courses[]" value="<?= htmlReady($course->id) ?>"> - </td> - </tr> - <? endforeach ?> - </tbody> - </table> - </fieldset> - <? if ($return) : ?> - <input type="hidden" name="return" value="<?= htmlReady($return) ?>"> - <? endif ?> - <footer data-dialog-button> - <?= Studip\Button::createAccept(_('Speichern'), 'store') ?> - <?= Studip\Button::createCancel(_('Abbrechen'), 'cancel', ['data-dialog' => 'close']) ?> - </footer> -</form> diff --git a/app/views/admin/tree/create.php b/app/views/admin/tree/create.php deleted file mode 100644 index 6b7255fdc6a57e76216566eefd1d5a65a07c6ee0..0000000000000000000000000000000000000000 --- a/app/views/admin/tree/create.php +++ /dev/null @@ -1,49 +0,0 @@ -<form class="default" action="<?= $controller->link_for('admin/tree/store', get_class($node), $node->id ?: null) ?>" method="post"> - <section> - <label> - <?= _('Name') ?> - <input type="text" name="name" - placeholder="<?= _('Name des Eintrags (wird bei Zuweisung zu einer Stud.IP-Einrichtung überschrieben)') ?>"> - </label> - </section> - <? if (get_class($node) === StudipStudyArea::class): ?> - <section> - <label> - <?= _('Infotext') ?> - <textarea name="description" rows="3"></textarea> - </label> - </section> - <section> - <label> - <?= _('Typ') ?> - <select name="type"> - <? foreach ($GLOBALS['SEM_TREE_TYPES'] as $index => $type) : ?> - <option value="<?= htmlReady($index) ?>"> - <?= $type['name'] ?: _('Standard') ?> - <?= !$type['editable'] ? _('(nicht mehr nachträglich änderbar)') : '' ?> - <?= $type['hidden'] ? _('(dieser Knoten ist versteckt)') : '' ?> - </option> - <? endforeach ?> - </select> - </label> - </section> - <? endif ?> - <section> - <label> - <?= _('Elternelement') ?> - <?= $treesearch->render() ?> - </label> - </section> - <section> - <label> - <?= _('Zu einer Stud.IP-Einrichtung zuordnen') ?> - <?= $instsearch->render() ?> - </label> - </section> - <input type="hidden" name="from" value="<?= htmlReady($from) ?>"> - <?= CSRFProtection::tokenTag() ?> - <footer data-dialog-button> - <?= Studip\Button::createAccept(_('Speichern'), 'store') ?> - <?= Studip\Button::createCancel(_('Abbrechen'), 'cancel', ['data-dialog' => 'close']) ?> - </footer> -</form> diff --git a/app/views/admin/tree/edit.php b/app/views/admin/tree/edit.php deleted file mode 100644 index 0076b95c8ff0c73d0a7f51f73c0584c3b28b3a23..0000000000000000000000000000000000000000 --- a/app/views/admin/tree/edit.php +++ /dev/null @@ -1,54 +0,0 @@ -<form class="default" action="<?= $controller->link_for('admin/tree/store', get_class($node), $node->id) ?>" method="post"> - <section> - <label> - <?= (get_class($node) === RangeTreeNode::class && $node->studip_object_id) - ? _('Name (kann hier nicht bearbeitet werden, da es sich um ein Stud.IP-Objekt handelt)') - : _('Name') ?> - <input type="text" name="name" - value="<?= htmlReady($node->getName()) ?>" - <?= get_class($node) === RangeTreeNode::class && $node->studip_object_id ? ' disabled' : '' ?>> - </label> - </section> - <? if (get_class($node) === StudipStudyArea::class): ?> - <section> - <label> - <?= _('Infotext') ?> - <textarea name="description" rows="3"><?= htmlReady($node->info) ?></textarea> - </label> - </section> - <section> - <label> - <?= _('Typ') ?> - <select name="type"<?= empty($GLOBALS['SEM_TREE_TYPES'][$node->type]['editable']) ? ' disabled' : '' ?>> - <? foreach ($GLOBALS['SEM_TREE_TYPES'] as $index => $type) : ?> - <option value="<?= htmlReady($index) ?>"<?= $node->type == $index ? ' selected' : '' ?>> - <?= $type['name'] ?: _('Standard') ?> - <?= !$type['editable'] ? _('(nicht mehr nachträglich änderbar)') : '' ?> - <?= $type['hidden'] ? _('(dieser Knoten ist versteckt)') : '' ?> - </option> - <? endforeach ?> - </select> - </label> - </section> - <? endif ?> - <section> - <label> - <?= _('Elternelement') ?> - <?= $treesearch->render() ?> - </label> - </section> - <? if (get_class($node) === RangeTreeNode::class): ?> - <section> - <label> - <?= _('Zu einer Stud.IP-Einrichtung zuordnen') ?> - <?= $instsearch->render() ?> - </label> - </section> - <? endif ?> - <input type="hidden" name="from" value="<?= $from ?>"> - <?= CSRFProtection::tokenTag() ?> - <footer data-dialog-button> - <?= Studip\Button::createAccept(_('Speichern'), 'store') ?> - <?= Studip\Button::createCancel(_('Abbrechen'), 'cancel', ['data-dialog' => 'close']) ?> - </footer> -</form> diff --git a/app/views/admin/tree/rangetree.php b/app/views/admin/tree/rangetree.php deleted file mode 100644 index 1e3e9453f535997e9cf76fc69a504de4939f4a62..0000000000000000000000000000000000000000 --- a/app/views/admin/tree/rangetree.php +++ /dev/null @@ -1,9 +0,0 @@ -<div data-studip-tree> - <studip-tree start-id="<?= htmlReady($startId) ?>" view-type="table" breadcrumb-icon="institute" - :with-search="false" :visible-children-only="false" - :editable="true" edit-url="<?= $controller->url_for('admin/tree/edit') ?>" - create-url="<?= $controller->url_for('admin/tree/create') ?>" - delete-url="<?= $controller->url_for('admin/tree/delete') ?>" - :with-courses="true" semester="<?= htmlReady($semester) ?>" :show-structure-as-navigation="true" - title="<?= _('Einrichtungshierarchie bearbeiten') ?>"></studip-tree> -</div> diff --git a/app/views/admin/tree/semtree.php b/app/views/admin/tree/semtree.php deleted file mode 100644 index 0c48245a6ed599d147af257371e94fa3767fa57d..0000000000000000000000000000000000000000 --- a/app/views/admin/tree/semtree.php +++ /dev/null @@ -1,10 +0,0 @@ -<div data-studip-tree> - <studip-tree start-id="<?= htmlReady($startId) ?>" view-type="table" breadcrumb-icon="literature" - :with-search="false" :visible-children-only="false" - :editable="true" edit-url="<?= $controller->url_for('admin/tree/edit') ?>" - create-url="<?= $controller->url_for('admin/tree/create') ?>" - delete-url="<?= $controller->url_for('admin/tree/delete') ?>" - :show-structure-as-navigation="true" :with-course-assign="true" - :with-courses="true" semester="<?= htmlReady($semester) ?>" - title="<?= _('Veranstaltungshierarchie bearbeiten') ?>"></studip-tree> -</div> diff --git a/app/views/search/courses/index.php b/app/views/search/courses/index.php index c6f490506018972d0765c56676e34fb9b73d6835..63b9dc44aa26969231e6b1d6bf0e7bce2ad23315 100644 --- a/app/views/search/courses/index.php +++ b/app/views/search/courses/index.php @@ -1,12 +1,2 @@ -<?php -/** - * @var String $startId - * @var String $nodeClass - */ -?> -<div data-studip-tree> - <studip-tree start-id="<?= htmlReady($startId) ?>" view-type="list" :visible-children-only="true" - title="<?= htmlReady($treeTitle) ?>" breadcrumb-icon="<?= htmlReady($breadcrumbIcon) ?>" - :with-search="true" :with-export="true" :with-courses="true" semester="<?= htmlReady($semester) ?>" - :sem-class="<?= htmlReady($semClass) ?>" :with-export="true"></studip-tree> -</div> +<? +$sem_browse_obj->do_output(); diff --git a/app/views/studyarea/edit.php b/app/views/studyarea/edit.php deleted file mode 100644 index 45a48d133d2504635b8f3cf84647998abc64bdc5..0000000000000000000000000000000000000000 --- a/app/views/studyarea/edit.php +++ /dev/null @@ -1 +0,0 @@ -<?= $form->render() ?> diff --git a/db/migrations/5.4.6_tree_changes.php b/db/migrations/5.4.6_tree_changes.php deleted file mode 100644 index 94d015caed9a976c8b84b3ca0aa1551208bfa529..0000000000000000000000000000000000000000 --- a/db/migrations/5.4.6_tree_changes.php +++ /dev/null @@ -1,66 +0,0 @@ -<? - -final class TreeChanges extends Migration -{ - - const FIELDS = [ - 'RANGE_TREE_PERM', - 'SEM_TREE_PERM' - ]; - - public function description() - { - return 'Removes old sem_- and range_tree permission settings and institute assignments for sem_tree entries'; - } - - protected function up() - { - // Remove config fields for special permissions concerning sem_- and range_tree administration. - DBManager::get()->execute( - "DELETE FROM `config_values` WHERE `field` IN (:fields)", - ['fields' => self::FIELDS] - ); - DBManager::get()->execute( - "DELETE FROM `config` WHERE `field` IN (:fields)", - ['fields' => self::FIELDS] - ); - - // "Transfer" names from assigned institutes to sem_tree entries. - $stmt = DBManager::get()->prepare("UPDATE `sem_tree` SET `name` = :name WHERE `studip_object_id` = :inst"); - $query = "SELECT DISTINCT `Institut_id`, `Name` FROM `Institute` WHERE `Institut_id` IN ( - SELECT DISTINCT `studip_object_id` FROM `sem_tree` - )"; - foreach (DBManager::get()->fetchAll($query) as $institute) { - $stmt->execute(['name' => $institute['Name'], 'inst' => $institute['Institut_id']]); - } - // Remove institute assignments for sem_tree entries. - DBManager::get()->exec("ALTER TABLE `sem_tree` DROP `studip_object_id`"); - } - - protected function down() - { - // Restore config entries to their defaults. - DBManager::get()->exec("INSERT IGNORE INTO `config` - ( `config_id` , `parent_id` , `field` , `value` , - `is_default` , `type` , `range` , `section` , - `position` , `mkdate` , `chdate` , `description` , - `comment` , `message_template` ) - VALUES ( - MD5( 'RANGE_TREE_ADMIN_PERM' ) , '', 'RANGE_TREE_ADMIN_PERM', - 'admin', '1', 'string', 'global', '', '0', - UNIX_TIMESTAMP( ) , UNIX_TIMESTAMP( ) , - 'mit welchem Status darf die Einrichtungshierarchie bearbeitet werden (admin oder root)', '', '' - ), ( - MD5( 'SEM_TREE_ADMIN_PERM' ) , '', 'SEM_TREE_ADMIN_PERM', - 'admin', '1', 'string', 'global', '', '0', UNIX_TIMESTAMP( ) , - UNIX_TIMESTAMP( ) , 'mit welchem Status darf die Veranstaltungshierarchie bearbeitet werden (admin oder root)', '', '' - )"); - - // Add database column for sem_tree institute assignments. - DBManager::get()->exec("ALTER TABLE `sem_tree` ADD - `studip_object_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NULL DEFAULT NULL AFTER `name`"); - // Add index for studip_object_id. - DBManager::get()->exec("ALTER TABLE `sem_tree` ADD INDEX `studip_object_id` (`studip_object_id`)"); - } - -} diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index dadfa5c8733a2be74e9db832edcf5d940e310f83..d97e08d05acfdd6a16b6d3b9abc4bc563550b302 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -136,7 +136,6 @@ class RouteMap $this->addAuthenticatedNewsRoutes($group); $this->addAuthenticatedStockImagesRoutes($group); $this->addAuthenticatedStudyAreasRoutes($group); - $this->addAuthenticatedTreeRoutes($group); $this->addAuthenticatedWikiRoutes($group); } @@ -302,17 +301,6 @@ class RouteMap $group->get('/study-areas/{id}/parent', Routes\StudyAreas\ParentOfStudyAreas::class); } - private function addAuthenticatedTreeRoutes(RouteCollectorProxy $group): void - { - $group->get('/tree-node/{id}', Routes\Tree\TreeShow::class); - - $group->get('/tree-node/{id}/children', Routes\Tree\ChildrenOfTreeNode::class); - $group->get('/tree-node/{id}/courseinfo', Routes\Tree\CourseInfoOfTreeNode::class); - $group->get('/tree-node/{id}/courses', Routes\Tree\CoursesOfTreeNode::class); - $group->get('/tree-node/course/pathinfo/{classname}/{id}', Routes\Tree\PathinfoOfTreeNodeCourse::class); - $group->get('/tree-node/course/details/{id}', Routes\Tree\DetailsOfTreeNodeCourse::class); - } - private function addAuthenticatedWikiRoutes(RouteCollectorProxy $group): void { $this->addRelationship($group, '/wiki-pages/{id:.+}/relationships/parent', Routes\Wiki\Rel\ParentPage::class); diff --git a/lib/classes/JsonApi/Routes/RangeTree/ChildrenOfRangeTreeNode.php b/lib/classes/JsonApi/Routes/RangeTree/ChildrenOfRangeTreeNode.php deleted file mode 100644 index 8773c0a5b60940fbdff30041ce15ad60de8957e0..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/RangeTree/ChildrenOfRangeTreeNode.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php - -namespace JsonApi\Routes\RangeTree; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\JsonApiController; - -class ChildrenOfRangeTreeNode extends JsonApiController -{ - protected $allowedIncludePaths = [ - 'children', - 'courses', - 'institute', - 'parent', - ]; - protected $allowedPagingParameters = ['offset', 'limit']; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - if (!RangeTreeNode::getNode($args['id'])) { - throw new RecordNotFoundException(); - } - - list($offset, $limit) = $this->getOffsetAndLimit(); - $total = \RangeTreeNode::countByParent_id($args['id']); - $children = \RangeTreeNode::findByParent_id( - $args['id'], - "LIMIT {$offset}, {$limit}" - ); - - return $this->getPaginatedContentResponse($children, $total); - } -} diff --git a/lib/classes/JsonApi/Routes/RangeTree/CoursesOfRangeTreeNode.php b/lib/classes/JsonApi/Routes/RangeTree/CoursesOfRangeTreeNode.php deleted file mode 100644 index 980cac104afc8fab43771dc8d7a52bb78cea7b31..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/RangeTree/CoursesOfRangeTreeNode.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php - -namespace JsonApi\Routes\RangeTree; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\JsonApiController; - -class CoursesOfRangeTreeNode extends JsonApiController -{ - protected $allowedIncludePaths = [ - 'blubber-threads', - 'end-semester', - 'events', - 'feedback-elements', - 'file-refs', - 'folders', - 'forum-categories', - 'institute', - 'memberships', - 'news', - 'participating-institutes', - 'sem-class', - 'sem-type', - 'start-semester', - 'status-groups', - 'wiki-pages', - ]; - protected $allowedPagingParameters = ['offset', 'limit']; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - $node = \RangeTreeNode::find($args['id']); - if (!$node) { - throw new RecordNotFoundException(); - } - - list($offset, $limit) = $this->getOffsetAndLimit(); - $courses = $node->getCourses(); - - return $this->getPaginatedContentResponse( - $courses->limit($offset, $limit), - count($courses) - ); - } -} diff --git a/lib/classes/JsonApi/Routes/RangeTree/InstituteOfRangeTreeNode.php b/lib/classes/JsonApi/Routes/RangeTree/InstituteOfRangeTreeNode.php deleted file mode 100644 index 6ecf52a6033f6d5275872251b935fe3b45b300bc..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/RangeTree/InstituteOfRangeTreeNode.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php - -namespace JsonApi\Routes\RangeTree; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\JsonApiController; -use JsonApi\Schemas\Institute as InstituteSchema; - -class InstituteOfRangeTreeNode extends JsonApiController -{ - protected $allowedIncludePaths = [ - InstituteSchema::REL_STATUS_GROUPS, - ]; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - $node = \RangeTreeNode::find($args['id']); - if (!$node) { - throw new RecordNotFoundException(); - } - - return $this->getContentResponse($node->institute); - } -} diff --git a/lib/classes/JsonApi/Routes/RangeTree/ParentOfRangeTreeNode.php b/lib/classes/JsonApi/Routes/RangeTree/ParentOfRangeTreeNode.php deleted file mode 100644 index 01a40d3cd36d0d26ec0cfdd690c925a106f5477a..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/RangeTree/ParentOfRangeTreeNode.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -namespace JsonApi\Routes\RangeTree; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\JsonApiController; - -class ParentOfRangeTreeNode extends JsonApiController -{ - protected $allowedIncludePaths = [ - 'children', - 'courses', - 'institute', - 'parent', - ]; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - $node = \RangeTreeNode::find($args['id']); - if (!$node) { - throw new RecordNotFoundException(); - } - - return $this->getContentResponse($node->getParent()); - } -} diff --git a/lib/classes/JsonApi/Routes/RangeTree/RangeTreeIndex.php b/lib/classes/JsonApi/Routes/RangeTree/RangeTreeIndex.php deleted file mode 100644 index c706c482204dc2e8ec58e354b5c3602e7d008e7a..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/RangeTree/RangeTreeIndex.php +++ /dev/null @@ -1,53 +0,0 @@ -<?php - -namespace JsonApi\Routes\RangeTree; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\JsonApiController; - -/** - * Zeigt eine bestimmte Veranstaltung an. - */ -class RangeTreeIndex extends JsonApiController -{ - - protected $allowedIncludePaths = [ - 'children', - 'courses', - 'institute', - 'parent', - ]; - protected $allowedPagingParameters = ['offset', 'limit']; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function __invoke(Request $request, Response $response, $args) - { - $tree = \TreeAbstract::getInstance('StudipSemTree', ['visible_only' => 1]); - $studyAreas = self::mapTree('root', $tree); - list($offset, $limit) = $this->getOffsetAndLimit(); - - return $this->getPaginatedContentResponse( - array_slice($studyAreas, $offset, $limit), - count($studyAreas) - ); - } - - private function mapTree($parentId, &$tree) - { - $level = []; - $kids = $tree->getKids($parentId); - if (is_array($kids) && count($kids) > 0) { - foreach ($kids as $kid) { - $level[] = \StudipStudyArea::find($kid); - $level = array_merge($level, self::mapTree($kid, $tree)); - } - } - - return $level; - } -} diff --git a/lib/classes/JsonApi/Routes/RangeTree/RangeTreeShow.php b/lib/classes/JsonApi/Routes/RangeTree/RangeTreeShow.php deleted file mode 100644 index c9f52ed11787e3a90a524aedfeb353023980b17c..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/RangeTree/RangeTreeShow.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -namespace JsonApi\Routes\RangeTree; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\JsonApiController; - -class RangeTreeShow extends JsonApiController -{ - protected $allowedIncludePaths = [ - 'children', - 'courses', - 'institute', - 'parent', - ]; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - $node = \RangeTreeNode::find($args['id']); - if (!$node) { - throw new RecordNotFoundException(); - } - - return $this->getContentResponse($node); - } -} diff --git a/lib/classes/JsonApi/Routes/StudyAreas/StudyAreasShow.php b/lib/classes/JsonApi/Routes/StudyAreas/StudyAreasShow.php index 9ce57289658e1d62362a46032ccd2b5c08bfeaee..628abc828bd9966e2ba9b73031111dbfd62d1db3 100644 --- a/lib/classes/JsonApi/Routes/StudyAreas/StudyAreasShow.php +++ b/lib/classes/JsonApi/Routes/StudyAreas/StudyAreasShow.php @@ -22,8 +22,7 @@ class StudyAreasShow extends JsonApiController */ public function __invoke(Request $request, Response $response, $args) { - $studyArea = \StudipStudyArea::find($args['id']); - if (!$studyArea && $args['id'] !== 'root') { + if (!$studyArea = \StudipStudyArea::find($args['id'])) { throw new RecordNotFoundException(); } diff --git a/lib/classes/JsonApi/Routes/Tree/ChildrenOfTreeNode.php b/lib/classes/JsonApi/Routes/Tree/ChildrenOfTreeNode.php deleted file mode 100644 index 6419d03c6cef22ed815547a5878c548695bda047..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/Tree/ChildrenOfTreeNode.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php - -namespace JsonApi\Routes\Tree; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\JsonApiController; - -class ChildrenOfTreeNode extends JsonApiController -{ - protected $allowedFilteringParameters = ['visible']; - - protected $allowedIncludePaths = [ - 'children', - 'courses', - 'institute', - 'parent', - ]; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - list($classname, $id) = explode('_', $args['id']); - - $node = $classname::getNode($id); - if (!$node) { - throw new RecordNotFoundException(); - } - - $filters = $this->getContextFilters(); - - $data = $node->getChildNodes((bool) $filters['visible']); - - return $this->getContentResponse($data); - } - - private function getContextFilters() - { - $defaults = [ - 'visible' => false - ]; - - $filtering = $this->getQueryParameters()->getFilteringParameters() ?: []; - - return array_merge($defaults, $filtering); - } -} diff --git a/lib/classes/JsonApi/Routes/Tree/CourseInfoOfTreeNode.php b/lib/classes/JsonApi/Routes/Tree/CourseInfoOfTreeNode.php deleted file mode 100644 index 283593150cf5c0c4ec8d3fb5cc0ae2555b9ff2d4..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/Tree/CourseInfoOfTreeNode.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php - -namespace JsonApi\Routes\Tree; - -use JsonApi\Errors\BadRequestException; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\NonJsonApiController; - -class CourseinfoOfTreeNode extends NonJsonApiController -{ - protected $allowedFilteringParameters = ['q', 'semester', 'semclass', 'recursive']; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - list($classname, $id) = explode('_', $args['id']); - - $node = $classname::getNode($id); - if (!$node) { - throw new RecordNotFoundException(); - } - - $error = $this->validateFilters($request); - if ($error) { - throw new BadRequestException($error); - } - - $filters = $this->getContextFilters($request); - - $info = [ - 'courses' => (int) $node->countCourses($filters['semester'], $filters['semclass']), - 'allCourses' => (int) $node->countCourses($filters['semester'], $filters['semclass'], true) - ]; - - $response->getBody()->write(json_encode($info)); - - return $response->withHeader('Content-type', 'application/json'); - } - - private function validateFilters($request) - { - $filtering = $request->getQueryParams()['filter'] ?: []; - - // keyword aka q - if (isset($filtering['q']) && mb_strlen($filtering['q']) < 3) { - return 'Search term too short.'; - } - - // semester - if (isset($filtering['semester']) && $filtering['semester'] !== 'all') { - $semester = \Semester::find($filtering['semester']); - if (!$semester) { - return 'Invalid "semester".'; - } - } - - // course category - if (!empty($filtering['semclass'])) { - $semclass = \SeminarCategories::Get($filtering['semclass']); - if (!$semclass) { - return 'Invalid "course category".'; - } - } - } - - private function getContextFilters($request) - { - $defaults = [ - 'q' => '', - 'semester' => 'all', - 'semclass' => 0, - 'recursive' => false - ]; - - $filtering = $request->getQueryParams()['filter'] ?: []; - - return array_merge($defaults, $filtering); - } -} diff --git a/lib/classes/JsonApi/Routes/Tree/CoursesOfTreeNode.php b/lib/classes/JsonApi/Routes/Tree/CoursesOfTreeNode.php deleted file mode 100644 index 623e6198520aebc04e7201918be95b31a5e12f1a..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/Tree/CoursesOfTreeNode.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php - -namespace JsonApi\Routes\Tree; - -use JsonApi\Errors\BadRequestException; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\JsonApiController; - -class CoursesOfTreeNode extends JsonApiController -{ - protected $allowedFilteringParameters = ['q', 'semester', 'semclass', 'recursive', 'ids']; - - protected $allowedIncludePaths = [ - 'blubber-threads', - 'end-semester', - 'events', - 'feedback-elements', - 'file-refs', - 'folders', - 'forum-categories', - 'institute', - 'memberships', - 'news', - 'participating-institutes', - 'sem-class', - 'sem-type', - 'start-semester', - 'status-groups', - 'wiki-pages', - ]; - protected $allowedPagingParameters = ['offset', 'limit']; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - list($classname, $id) = explode('_', $args['id']); - - $node = $classname::getNode($id); - if (!$node) { - throw new RecordNotFoundException(); - } - - $error = $this->validateFilters(); - if ($error) { - throw new BadRequestException($error); - } - - $filters = $this->getContextFilters(); - - list($offset, $limit) = $this->getOffsetAndLimit(); - $courses = \SimpleCollection::createFromArray( - $node->getCourses( - $filters['semester'], - $filters['semclass'], - $filters['q'], - (bool) $filters['recursive'], - $filters['ids'] - ) - ); - - return $this->getPaginatedContentResponse( - $courses->limit($offset, $limit), - count($courses) - ); - } - - private function validateFilters() - { - $filtering = $this->getQueryParameters()->getFilteringParameters() ?: []; - - // keyword aka q - if (isset($filtering['q']) && mb_strlen($filtering['q']) < 3) { - return 'Search term too short.'; - } - - // semester - if (isset($filtering['semester']) && $filtering['semester'] !== 'all') { - $semester = \Semester::find($filtering['semester']); - if (!$semester) { - return 'Invalid "semester".'; - } - } - - // course category - if (!empty($filtering['semclass'])) { - $semclass = \SeminarCategories::Get($filtering['semclass']); - if (!$semclass) { - return 'Invalid "course category".'; - } - } - } - - private function getContextFilters() - { - $defaults = [ - 'q' => '', - 'semester' => 'all', - 'semclass' => 0, - 'recursive' => false, - 'ids' => [] - ]; - - $filtering = $this->getQueryParameters()->getFilteringParameters() ?: []; - - return array_merge($defaults, $filtering); - } -} diff --git a/lib/classes/JsonApi/Routes/Tree/DetailsOfTreeNodeCourse.php b/lib/classes/JsonApi/Routes/Tree/DetailsOfTreeNodeCourse.php deleted file mode 100644 index cef1077ecb7b1d8f15b515092865ab128c703c4f..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/Tree/DetailsOfTreeNodeCourse.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php - -namespace JsonApi\Routes\Tree; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\NonJsonApiController; - -class DetailsOfTreeNodeCourse extends NonJsonApiController -{ - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - $course = \Course::find($args['id']); - if (!$course) { - throw new RecordNotFoundException(); - } - - // Get course dates in textual form - $dates = \Seminar::GetInstance($args['id'])->getDatesHTML([ - 'semester_id' => null, - 'show_room' => true, - ]); - - $data = [ - 'semester' => $course->semester_text, - 'lecturers' => [], - 'admissionstate' => null, - 'dates' => $dates - ]; - - // Get lecturers - $lecturers = \SimpleCollection::createFromArray( - \CourseMember::findByCourseAndStatus($args['id'], 'dozent') - )->orderBy('position, nachname, vorname'); - foreach ($lecturers as $l) { - $data['lecturers'][] = [ - 'id' => $l->user_id, - 'username' => $l->username, - 'name' => $l->getUserFullname() - ]; - } - - // Get admission state indicator if necessary - if (\Config::get()->COURSE_SEARCH_SHOW_ADMISSION_STATE) { - switch (\GlobalSearchCourses::getStatusCourseAdmission($course->id, $course->admission_prelim)) { - case 1: - $data['admissionstate'] = [ - 'icon' => 'decline-circle', - 'role' => \Icon::ROLE_STATUS_YELLOW, - 'info' => _('Eingeschränkter Zugang') - ]; - break; - case 2: - $data['admissionstate'] = [ - 'icon' => 'decline-circle', - 'role' => \Icon::ROLE_STATUS_RED, - 'info' => _('Kein Zugang') - ]; - break; - default: - $data['admissionstate'] = [ - 'icon' => 'check-circle', - 'role' => \Icon::ROLE_STATUS_GREEN, - 'info' => _('Uneingeschränkter Zugang') - ]; - } - - } - - $response->getBody()->write(json_encode($data)); - - return $response->withHeader('Content-type', 'application/json'); - } -} diff --git a/lib/classes/JsonApi/Routes/Tree/PathinfoOfTreeNodeCourse.php b/lib/classes/JsonApi/Routes/Tree/PathinfoOfTreeNodeCourse.php deleted file mode 100644 index 282b7f9489031cc54149ebe7e49cc36547fa607c..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/Tree/PathinfoOfTreeNodeCourse.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -namespace JsonApi\Routes\Tree; - -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\NonJsonApiController; - -class PathinfoOfTreeNodeCourse extends NonJsonApiController -{ - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - $course = \Course::find($args['id']); - if (!$course) { - throw new RecordNotFoundException(); - } - - $classname = $args['classname']; - - $path = []; - foreach ($classname::getCourseNodes($args['id']) as $node) { - $path[] = $node->getAncestors(); - } - - $response->getBody()->write(json_encode($path)); - - return $response->withHeader('Content-type', 'application/json'); - } -} diff --git a/lib/classes/JsonApi/Routes/Tree/TreeShow.php b/lib/classes/JsonApi/Routes/Tree/TreeShow.php deleted file mode 100644 index 1eaa7972c6e4caf7996833a3330df142a90f0aa7..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Routes/Tree/TreeShow.php +++ /dev/null @@ -1,40 +0,0 @@ -<?php - -namespace JsonApi\Routes\Tree; - -use JsonApi\Errors\BadRequestException; -use Neomerx\JsonApi\Contracts\Http\ResponsesInterface; -use Neomerx\JsonApi\Contracts\Schema\ContextInterface; -use Neomerx\JsonApi\Schema\Link; -use Psr\Http\Message\ServerRequestInterface as Request; -use Psr\Http\Message\ResponseInterface as Response; -use JsonApi\Errors\AuthorizationFailedException; -use JsonApi\Errors\RecordNotFoundException; -use JsonApi\JsonApiController; - -class TreeShow extends JsonApiController -{ - protected $allowedIncludePaths = [ - 'children', - 'courseinfo', - 'courses', - 'institute', - 'parent' - ]; - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameters) - */ - public function __invoke(Request $request, Response $response, $args) - { - list($classname, $id) = explode('_', $args['id']); - - $node = $classname::getNode($id); - if (!$node) { - throw new RecordNotFoundException(); - } - - return $this->getContentResponse($node); - } - -} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index 97212bc65524bc7603199880c399811d81519b55..c1ce34d25bc8e41ce3d5f1952de5e5e0474c5ce5 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -46,7 +46,7 @@ class SchemaMap \JsonApi\Models\StudipProperty::class => Schemas\StudipProperty::class, \StudipComment::class => Schemas\StudipComment::class, \StudipNews::class => Schemas\StudipNews::class, - \StudipTreeNode::class => Schemas\TreeNode::class, + \StudipStudyArea::class => Schemas\StudyArea::class, \WikiPage::class => Schemas\WikiPage::class, \Studip\Activity\Activity::class => Schemas\Activity::class, \User::class => Schemas\User::class, diff --git a/lib/classes/JsonApi/Schemas/StudyArea.php b/lib/classes/JsonApi/Schemas/StudyArea.php index e9779c701090cb534361c248c005b8f22648a224..f077d83b70bf888c2a680bbfe232529c0995c3ef 100644 --- a/lib/classes/JsonApi/Schemas/StudyArea.php +++ b/lib/classes/JsonApi/Schemas/StudyArea.php @@ -1,4 +1,5 @@ <?php + namespace JsonApi\Schemas; use Neomerx\JsonApi\Contracts\Schema\ContextInterface; @@ -24,9 +25,6 @@ class StudyArea extends SchemaProvider 'info' => (string) $resource['info'], 'priority' => (int) $resource['priority'], 'type-name' => (string) $resource->getTypeName(), - 'has-children' => (bool) $resource->hasChildNodes(), - 'ancestors' => (array) $resource->getAncestors(), - 'classname' => get_class($resource) ]; } diff --git a/lib/classes/JsonApi/Schemas/TreeNode.php b/lib/classes/JsonApi/Schemas/TreeNode.php deleted file mode 100644 index 90e6846de9d577b78e64be6d0ee7f9bc48b2e734..0000000000000000000000000000000000000000 --- a/lib/classes/JsonApi/Schemas/TreeNode.php +++ /dev/null @@ -1,151 +0,0 @@ -<?php - -namespace JsonApi\Schemas; - -use Neomerx\JsonApi\Contracts\Factories\FactoryInterface; -use Neomerx\JsonApi\Contracts\Schema\ContextInterface; -use Neomerx\JsonApi\Contracts\Schema\SchemaContainerInterface; -use Neomerx\JsonApi\Schema\Link; - -class TreeNode extends SchemaProvider -{ - const REL_CHILDREN = 'children'; - - const REL_COURSEINFO = 'courseinfo'; - const REL_COURSES = 'courses'; - const REL_INSTITUTE = 'institute'; - const REL_PARENT = 'parent'; - - const TYPE = 'tree-node'; - - public function getId($resource): ?string - { - return get_class($resource) . '_' . $resource['id']; - } - - public function getAttributes($resource, ContextInterface $context): iterable - { - $schema = [ - 'id' => (string) $resource->getId(), - 'name' => (string) $resource->getName(), - 'description' => (string) $resource->getDescription(), - 'description-formatted' => (string) formatReady($resource->getDescription()), - 'has-children' => (bool) $resource->hasChildNodes(), - 'ancestors' => (array) $resource->getAncestors(), - 'classname' => get_class($resource), - 'visible' => true, - 'editable' => true, - 'assignable' => true - ]; - - // Some special options for sem_tree entries. - if (get_class($resource) === 'StudipStudyArea') { - if ($GLOBALS['SEM_TREE_TYPES'][$resource->type]['hidden'] ?? false) { - $schema['visible'] = false; - } - if ($GLOBALS['SEM_TREE_TYPES'][$resource->type]['editable'] ?? false) { - $schema['editable'] = false; - } - if (!\Config::get()->SEM_TREE_ALLOW_BRANCH_ASSIGN && $resource->hasChildNodes()) { - $schema['assignable'] = false; - } - } - - return $schema; - } - - public function getRelationships($resource, ContextInterface $context): iterable - { - $relationships = []; - - $relationships = $this->addChildrenRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_CHILDREN)); - - if (property_exists($resource, 'courses') || method_exists($resource, 'getCourses')) { - $relationships = $this->addCourseInfoRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_COURSEINFO)); - $relationships = $this->addCoursesRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_COURSES)); - } - $relationships = $this->addInstituteRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_INSTITUTE)); - $relationships = $this->addParentRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_PARENT)); - - return $relationships; - } - - private function addChildrenRelationship(array $relationships, $resource, $includeData) - { - $relationships[self::REL_CHILDREN] = [ - self::RELATIONSHIP_LINKS => [ - Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_CHILDREN), - ] - ]; - - if ($includeData) { - $children = $resource->getChildNodes(); - $relationships[self::REL_CHILDREN][self::RELATIONSHIP_DATA] = $children; - } - - return $relationships; - } - - - private function addCourseInfoRelationship(array $relationships, $resource, $includeData) - { - $relationships[self::REL_COURSEINFO] = [ - self::RELATIONSHIP_LINKS => [ - Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_COURSEINFO), - ], - ]; - - if ($includeData) { - $children = $resource->courses; - $relationships[self::REL_COURSES][self::RELATIONSHIP_DATA] = $children; - } - - return $relationships; - } - - private function addCoursesRelationship(array $relationships, $resource, $includeData) - { - $relationships[self::REL_COURSES] = [ - self::RELATIONSHIP_LINKS => [ - Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_COURSES) - ] - ]; - - if ($includeData) { - $courses = $resource->courses; - $relationships[self::REL_COURSES][self::RELATIONSHIP_DATA] = $courses; - } - - return $relationships; - } - - private function addInstituteRelationship(array $relationships, $resource, $includeData) - { - $relationships[self::REL_INSTITUTE] = [ - self::RELATIONSHIP_LINKS => [ - Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_INSTITUTE), - ], - ]; - - if ($includeData) { - $relationships[self::REL_INSTITUTE][self::RELATIONSHIP_DATA] = $resource->institute; - } - - return $relationships; - } - - private function addParentRelationship(array $relationships, $resource, $includeData) - { - $relationships[self::REL_PARENT] = [ - self::RELATIONSHIP_LINKS => [ - Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_PARENT) - ], - ]; - - if ($includeData) { - $relationships[self::REL_PARENT][self::RELATIONSHIP_DATA] = $resource->getParent(); - } - - return $relationships; - } -} diff --git a/lib/classes/SemBrowse.class.php b/lib/classes/SemBrowse.class.php index c1cae8d12f24a73b3cef19d3e9fdaf2fc3ae1766..14f0d0e51e13cf250ce4e6f9308aa2db522f2ad1 100644 --- a/lib/classes/SemBrowse.class.php +++ b/lib/classes/SemBrowse.class.php @@ -1191,13 +1191,19 @@ class SemBrowse { return new Navigation(_('Vorlesungsverzeichnis'), URLHelper::getURL('dispatch.php/search/courses', [ - 'type' => 'semtree' + 'level' => 'vv', + 'cmd' => 'qs', + 'sset' => '0', + 'option' => '' ], true)); case 'rangetree': return new Navigation(_('Einrichtungsverzeichnis'), URLHelper::getURL('dispatch.php/search/courses', [ - 'type' => 'rangetree' + 'level' => 'ev', + 'cmd' => 'qs', + 'sset' => '0', + 'option' => '' ], true)); case 'module': return new MVVSearchNavigation(_('Modulverzeichnis'), diff --git a/lib/classes/StudipTreeNode.php b/lib/classes/StudipTreeNode.php deleted file mode 100644 index a1d72586d71100ce73c091171b115c041c5f0323..0000000000000000000000000000000000000000 --- a/lib/classes/StudipTreeNode.php +++ /dev/null @@ -1,114 +0,0 @@ -<?php - -/** - * Interface StudipTreeNode - * An abstract representation of a tree node in Stud.IP - * - * @author Thomas Hackl <hackl@data-quest.de> - * @license GPL2 or any later version - * @since Stud.IP 5.3 - */ - -interface StudipTreeNode -{ - - /** - * Fetches a node by the given ID. The implementing class knows what to do. - * - * @param mixed $id - * @return StudipTreeNode - */ - public static function getNode($id): StudipTreeNode; - - /** - * Get all direct children of the given node. - * - * @param bool $onlyVisible fetch only visible nodes? - * @return StudipTreeNode[] - */ - public function getChildNodes(bool $onlyVisible = false): array; - - /** - * Fetches an array of all nodes the given course is assigned to. - * - * @param string $course_id - * @return array - */ - public static function getCourseNodes(string $course_id): array; - - /** - * This node's unique ID. - * - * @return mixed - */ - public function getId(); - - /** - * A name (=label) for this node. - * - * @return string - */ - public function getName(): string; - - /** - * Optional description for this node. - * - * @return string - */ - public function getDescription(): string; - - /** - * Gets an optional Image (Icon or Avatar) for this node. - * - * @return Icon|Avatar|null - */ - public function getImage(); - - /** - * Indicator if this node has children. - * - * @return bool - */ - public function hasChildNodes(): bool; - - /** - * How many courses are assigned to this node in the given semester? - * - * @param string $semester_id - * @param int $semclass - * @param bool $with_children - * @return int - */ - public function countCourses( - string $semester_id = '', - int $semclass = 0, - bool $with_children = false - ): int; - - /** - * Fetches courses assigned to this node in the given semester. - * - * @param string $semester_id - * @param int $semclass - * @param string $searchterm - * @param bool $with_children - * @param string[] $courses - * - * @return Course[] - */ - public function getCourses( - string $semester_id = 'all', - int $semclass = 0, - string $searchterm = '', - bool $with_children = false, - array $courses = [] - ): array; - - /** - * Returns an array containing all ancestor nodes with id and name. - * - * @return array - */ - public function getAncestors(): array; - -} diff --git a/lib/classes/forms/QuicksearchInput.php b/lib/classes/forms/QuicksearchInput.php index f4be547393429919b6196c6048bce7c64caf2ab2..2531a2e19c3ee13134ae6d54bf987fbf789aabd9 100644 --- a/lib/classes/forms/QuicksearchInput.php +++ b/lib/classes/forms/QuicksearchInput.php @@ -6,7 +6,7 @@ class QuicksearchInput extends Input { public function render() { - $template = $GLOBALS['template_factory']->open('forms/quicksearch_input'); + $template = $GLOBALS['template_factory']->open('forms/checkbox_input'); $template->title = $this->title; $template->name = $this->name; $template->value = $this->value; diff --git a/lib/classes/searchtypes/TreeSearch.class.php b/lib/classes/searchtypes/TreeSearch.class.php deleted file mode 100644 index 2fecf60e0cd9c81493b36424d47c12c8aed91fa0..0000000000000000000000000000000000000000 --- a/lib/classes/searchtypes/TreeSearch.class.php +++ /dev/null @@ -1,96 +0,0 @@ -<?php -/** - * TreeSearch.class.php - Class of type SearchType used for searches with QuickSearch - * - * 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. - * - * @author Thomas Hackl <hackl@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ - -class TreeSearch extends StandardSearch -{ - /** - * - * @param string $search The search type. - * - * @param Array $search_settings Settings for the selected seach type. - * Depending on the search type different settings are possible - * which can change the output or the display of the output - * of the search. The array must be an associative array - * with the setting as array key. - * The following settings are implemented: - * Search type 'room': - * - display_seats: If set to true, the seats will be displayed - * after the name of the room. - * - * @return void - */ - public function __construct($search, $search_settings = []) - { - if (is_array($search_settings)) { - $this->search_settings = $search_settings; - } - - $this->avatarLike = $this->search = $search; - $this->sql = $this->getSQL(); - } - - /** - * returns the title/description of the searchfield - * - * @return string title/description - */ - public function getTitle() - { - switch ($this->search) { - case 'sem_tree_id': - return _('Studienbereich suchen'); - case 'range_tree_id': - return _('Eintrag in der Einrichtungshierarchie suchen'); - default: - throw new UnexpectedValueException('Invalid search type {$this->search}'); - } - } - - /** - * returns a sql-string appropriate for the searchtype of the current class - * - * @return string - */ - private function getSQL() - { - switch ($this->search) { - case 'sem_tree_id': - return "SELECT `sem_tree_id`, `name` - FROM `sem_tree` - WHERE `name` LIKE :input - OR `info` LIKE :input - ORDER BY `name`"; - case 'range_tree_id': - return "SELECT t.`item_id`, IF(t.`studip_object_id` IS NULL, t.`name`, i.`name`) - FROM `range_tree` t - LEFT JOIN `Institute` i ON (i.`Institut_id` = t.`studip_object_id`) - WHERE t.`name` LIKE :input - OR i.`Name` LIKE :input - ORDER BY t.`name`, i.`Name`"; - default: - throw new UnexpectedValueException("Invalid search type {$this->search}"); - } - } - - /** - * A very simple overwrite of the same method from SearchType class. - * returns the absolute path to this class for autoincluding this class. - * - * @return: path to this class - */ - public function includePath() - { - return studip_relative_path(__FILE__); - } -} diff --git a/lib/models/RangeTreeNode.php b/lib/models/RangeTreeNode.php deleted file mode 100644 index 9592515abc929d80bc904973f69009cfd3ae855c..0000000000000000000000000000000000000000 --- a/lib/models/RangeTreeNode.php +++ /dev/null @@ -1,265 +0,0 @@ -<?php - -/** - * RangeTreeNode.php - * model class for table range_tree - * - * 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. - * - * @author Thomas Hackl <hackl@data-quest.de> - * @copyright 2022 Stud.IP Core-Group - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @since 5.3 - * - * @property string $id alias column for item_id - * @property string $item_id database column - * @property string $parent_id database column - * @property int $level database column - * @property int $priority database column - * @property string $name database column - * @property string|null $studip_object database column - * @property string|null $studip_object_id database column - * @property SimpleORMapCollection|RangeTreeNode[] $children has_many RangeTreeNode - * @property Institute|null $institute belongs_to Institute - * @property RangeTreeNode $parent belongs_to RangeTreeNode - */ -class RangeTreeNode extends SimpleORMap implements StudipTreeNode -{ - protected static function configure($config = []) - { - $config['db_table'] = 'range_tree'; - - $config['belongs_to']['institute'] = [ - 'class_name' => Institute::class, - 'foreign_key' => 'studip_object_id', - ]; - $config['belongs_to']['parent'] = [ - 'class_name' => RangeTreeNode::class, - 'foreign_key' => 'parent_id', - ]; - $config['has_many']['children'] = [ - 'class_name' => RangeTreeNode::class, - 'foreign_key' => 'item_id', - 'assoc_foreign_key' => 'parent_id', - 'order_by' => 'ORDER BY priority, name', - 'on_delete' => 'delete' - ]; - - parent::configure($config); - } - - public static function getNode($id): StudipTreeNode - { - if ($id === 'root') { - return static::build([ - 'id' => 'root', - 'name' => Config::get()->UNI_NAME_CLEAN, - ]); - } - - return static::find($id); - } - - public static function getCourseNodes(string $course_id): array - { - $nodes = []; - foreach (Course::find($course_id)->institutes as $institute) { - $range = self::findOneByStudip_object_id($institute->id); - if ($range) { - $nodes[] = $range; - } - } - return $nodes; - } - - public function getName(): string - { - if ($this->id === 'root') { - return Config::get()->UNI_NAME_CLEAN; - } - - if ($this->institute) { - return (string) $this->institute->name; - } - - return $this->content['name']; - } - - public function getDescription(): string - { - return ''; - } - - public function getImage() - { - return $this->institute ? - Avatar::getAvatar($this->studip_object_id) : - Icon::create('institute'); - } - - public function hasChildNodes(): bool - { - return count($this->children) > 0; - } - - /** - * @see StudipTreeNode::getChildNodes() - */ - public function getChildNodes(bool $onlyVisible = false): array - { - return self::findByParent_id($this->id, "ORDER BY `priority`, `name`"); - } - - /** - * @see StudipTreeNode::countCourses() - */ - public function countCourses($semester_id = '', $semclass = 0, $with_children = false): int - { - if ($semester_id) { - $query = "SELECT COUNT(DISTINCT i.`seminar_id`) - FROM `seminar_inst` i - JOIN `seminare` s ON (s.`Seminar_id` = i.`seminar_id`) - LEFT JOIN `semester_courses` sc ON (i.`seminar_id` = sc.`course_id`) - WHERE i.`institut_id` IN ( - SELECT DISTINCT `studip_object_id` - FROM `range_tree` - WHERE `item_id` IN (:ids) - ) AND ( - sc.`semester_id` = :semester - OR sc.`semester_id` IS NULL - )"; - $parameters = [ - 'ids' => $with_children ? $this->getDescendantIds() : [$this->id], - 'semester' => $semester_id - ]; - } else { - $query = "SELECT COUNT(DISTINCT `seminar_id`) - FROM `seminar_inst` i - JOIN `seminare` s ON (s.`Seminar_id` = i.`seminar_id`) - WHERE `institut_id` IN ( - SELECT DISTINCT `studip_object_id` - FROM `range_tree` - WHERE `item_id` IN (:ids) - )"; - $parameters = ['ids' => $with_children ? $this->getDescendantIds() : [$this->id]]; - } - - if ($semclass !== 0) { - $query .= " AND s.`status` IN (:types)"; - $parameters['types'] = array_map( - function ($type) { - return $type['id']; - }, - array_filter( - SemType::getTypes(), - function ($t) use ($semclass) { return $t['class'] === $semclass; } - ) - ); - } - - return !$this->institute && !$with_children ? 0 : DBManager::get()->fetchColumn($query, $parameters); - } - - public function getCourses( - $semester_id = 'all', - $semclass = 0, - $searchterm = '', - $with_children = false, - array $courses = [] - ): array - { - if ($semester_id !== 'all') { - $query = "SELECT DISTINCT s.* - FROM `seminare` s - JOIN `seminar_inst` i ON (i.`seminar_id` = s.`Seminar_id`) - LEFT JOIN `semester_courses` sem ON (sem.`course_id` = s.`Seminar_id`) - WHERE i.`institut_id` IN ( - SELECT DISTINCT `studip_object_id` - FROM `range_tree` - WHERE `item_id` IN (:ids) - ) AND ( - sem.`semester_id` = :semester - OR sem.`semester_id` IS NULL - )"; - - $parameters = [ - 'ids' => $with_children ? $this->getDescendantIds() : [$this->id], - 'semester' => $semester_id - ]; - } else { - $query = "SELECT DISTINCT s.* - FROM `seminare` s - JOIN `seminar_inst` i ON (i.`seminar_id` = s.`Seminar_id`) - WHERE i.`institut_id` IN ( - SELECT DISTINCT `studip_object_id` - FROM `range_tree` - WHERE `item_id` IN (:ids) - )"; - $parameters = ['ids' => $with_children ? $this->getDescendantIds() : [$this->id]]; - } - - if ($searchterm) { - $query .= " AND s.`Name` LIKE :searchterm"; - $parameters['searchterm'] = '%' . trim($searchterm) . '%'; - } - - if ($courses) { - $query .= " AND s.`Seminar_id` IN (:courses)"; - $parameters['courses'] = $courses; - } - - if ($semclass !== 0) { - $query .= " AND s.`status` IN (:types)"; - $parameters['types'] = array_map( - function ($type) { - return $type['id']; - }, - array_filter( - SemType::getTypes(), - function ($t) use ($semclass) { return $t['class'] === $semclass; } - ) - ); - } - - if (Config::get()->IMPORTANT_SEMNUMBER) { - $query .= " ORDER BY s.`start_time`, s.`VeranstaltungsNummer`, s.`Name`"; - } else { - $query .= " ORDER BY s.`start_time`, s.`Name`"; - } - - return DBManager::get()->fetchAll($query, $parameters, 'Course::buildExisting'); - } - - public function getDescendantIds() - { - $ids = []; - - foreach ($this->children as $child) { - $ids = array_merge($ids, [$child->id], $child->getDescendantIds()); - } - - return $ids; - } - - public function getAncestors(): array - { - $path = [ - [ - 'id' => $this->id, - 'name' => $this->getName(), - 'classname' => self::class - ] - ]; - - if ($this->parent_id) { - $path = array_merge($this->getNode($this->parent_id)->getAncestors(), $path); - } - - return $path; - } - -} diff --git a/lib/models/StudipStudyArea.class.php b/lib/models/StudipStudyArea.class.php index 8b79970d27bf7b8502e35942378c60a3b9a73b5b..06b023c98435673d1b27fa658a11b554c2cb51e2 100644 --- a/lib/models/StudipStudyArea.class.php +++ b/lib/models/StudipStudyArea.class.php @@ -30,7 +30,7 @@ * @property SimpleORMapCollection|Course[] $courses has_and_belongs_to_many Course */ -class StudipStudyArea extends SimpleORMap implements StudipTreeNode +class StudipStudyArea extends SimpleORMap { /** * This constant represents the key of the root area. @@ -51,6 +51,10 @@ class StudipStudyArea extends SimpleORMap implements StudipTreeNode 'class_name' => Course::class, 'thru_table' => 'seminar_sem_tree', ]; + $config['belongs_to']['institute'] = [ + 'class_name' => Institute::class, + 'foreign_key' => 'studip_object_id', + ]; $config['belongs_to']['_parent'] = [ 'class_name' => StudipStudyArea::class, 'foreign_key' => 'parent_id', @@ -121,8 +125,11 @@ class StudipStudyArea extends SimpleORMap implements StudipTreeNode /** * Get the display name of this study area. */ - public function getName(): string + public function getName() { + if ($this->studip_object_id) { + return $this->institute ? $this->institute->name : _('Unbekannte Einrichtung'); + } return $this->content['name']; } @@ -277,6 +284,26 @@ class StudipStudyArea extends SimpleORMap implements StudipTreeNode } + /** + * Get the studip_object_id of this study area. + */ + public function getStudipObjectId() + { + return $this->studip_object_id; + } + + + /** + * Set the studip_object_id of this study area. + */ + public function setStudipObjectId($id) + { + $this->studip_object_id = (string) $id; + $this->resetRelation('institute'); + return $this; + } + + /** * Returns the children of this study area. */ @@ -419,191 +446,4 @@ class StudipStudyArea extends SimpleORMap implements StudipTreeNode return $root; } - public static function getNode($id): StudipTreeNode - { - if ($id === 'root') { - return static::build([ - 'id' => 'root', - 'name' => Config::get()->UNI_NAME_CLEAN, - ]); - } - - return static::find($id); - } - - public static function getCourseNodes(string $course_id): array - { - return Course::find($course_id)->study_areas->getArrayCopy(); - } - - public function getDescription(): string - { - return $this->getInfo(); - } - - /** - * @see StudipTreeNode::getImage() - */ - public function getImage() - { - return null; - } - - public function hasChildNodes(): bool - { - return count($this->_children) > 0; - } - - /** - * @see StudipTreeNode::getChildNodes() - */ - public function getChildNodes(bool $onlyVisible = false): array - { - if ($onlyVisible) { - $visibleTypes = array_filter($GLOBALS['SEM_TREE_TYPES'], function ($t) { - return isset($t['hidden']) ? !$t['hidden'] : true; - }); - - return static::findBySQL( - "`parent_id` = :parent AND `type` IN (:types) ORDER BY `priority`, `name`", - ['parent' => $this->id, 'types' => $visibleTypes] - ); - } else { - return static::findByParent_id($this->id, "ORDER BY `priority`, `name`"); - } - } - - /** - * @see StudipTreeNode::countCourses() - */ - public function countCourses($semester_id = 'all', $semclass = 0, $with_children = false) :int - { - if ($semester_id !== 'all') { - $query = "SELECT COUNT(DISTINCT t.`seminar_id`) - FROM `seminar_sem_tree` t - JOIN `seminare` s ON (s.`Seminar_id` = t.`seminar_id`) - LEFT JOIN `semester_courses` sc ON (t.`seminar_id` = sc.`course_id`) - WHERE t.`sem_tree_id` IN (:ids) - AND ( - sc.`semester_id` = :semester - OR sc.`semester_id` IS NULL - )"; - $parameters = [ - 'ids' => $with_children ? $this->getDescendantIds() : [$this->id], - 'semester' => $semester_id - ]; - } else { - $query = "SELECT COUNT(DISTINCT t.`seminar_id`) - FROM `seminar_sem_tree` t - JOIN `seminare` s ON (s.`Seminar_id` = t.`seminar_id`) - WHERE `sem_tree_id` IN (:ids)"; - $parameters = ['ids' => $with_children ? $this->getDescendantIds() : [$this->id]]; - } - - if ($semclass !== 0) { - $query .= " AND s.`status` IN (:types)"; - $parameters['types'] = array_map( - function ($type) { - return $type['id']; - }, - array_filter( - SemType::getTypes(), - function ($t) use ($semclass) { return $t['class'] === $semclass; } - ) - ); - } - - return $this->id === 'root' && !$with_children ? 0 : DBManager::get()->fetchColumn($query, $parameters); - } - - public function getCourses( - $semester_id = 'all', - $semclass = 0, - $searchterm = '', - $with_children = false, - array $courses = [] - ): array - { - if ($semester_id !== 'all') { - $query = "SELECT DISTINCT s.* - FROM `seminare` s - JOIN `seminar_sem_tree` t ON (t.`seminar_id` = s.`Seminar_id`) - LEFT JOIN `semester_courses` sem ON (sem.`course_id` = s.`Seminar_id`) - WHERE t.`sem_tree_id` IN (:ids) - AND ( - sem.`semester_id` = :semester - OR sem.`semester_id` IS NULL - )"; - $parameters = [ - 'ids' => $with_children ? $this->getDescendantIds() : [$this->id], - 'semester' => $semester_id - ]; - } else { - $query = "SELECT DISTINCT s.* - FROM `seminare` s - JOIN `seminar_sem_tree` t ON (t.`seminar_id` = s.`Seminar_id`) - WHERE t.`sem_tree_id` IN (:ids)"; - $parameters = ['ids' => $with_children ? $this->getDescendantIds() : [$this->id]]; - } - - if ($semclass !== 0) { - $query .= " AND s.`status` IN (:types)"; - $parameters['types'] = array_map( - function ($type) { - return $type['id']; - }, - array_filter( - SemType::getTypes(), - function ($t) use ($semclass) { return $t['class'] === $semclass; } - ) - ); - } - - if ($searchterm) { - $query .= " AND s.`Name` LIKE :searchterm"; - $parameters['searchterm'] = '%' . trim($searchterm) . '%'; - } - - if ($courses) { - $query .= " AND t.`seminar_id` IN (:courses)"; - $parameters['courses'] = $courses; - } - - if (Config::get()->IMPORTANT_SEMNUMBER) { - $query .= " ORDER BY s.`start_time`, s.`VeranstaltungsNummer`, s.`Name`"; - } else { - $query .= " ORDER BY s.`start_time`, s.`Name`"; - } - - return DBManager::get()->fetchAll($query, $parameters, 'Course::buildExisting'); - } - - public function getAncestors(): array - { - $path = [ - [ - 'id' => $this->id, - 'name' => $this->getName(), - 'classname' => static::class - ] - ]; - - if ($this->parent_id) { - $path = array_merge($this->getNode($this->parent_id)->getAncestors(), $path); - } - - return $path; - } - - private function getDescendantIds() - { - $ids = []; - - foreach ($this->_children as $child) { - $ids = array_merge($ids, [$child->id], $child->getDescendantIds()); - } - - return $ids; - } - } diff --git a/lib/navigation/AdminNavigation.php b/lib/navigation/AdminNavigation.php index 43864ea1fec8afbec71cfac71eea98da4cc650c5..3b0afaf1eb48043d90c149aef144524c22f09772 100644 --- a/lib/navigation/AdminNavigation.php +++ b/lib/navigation/AdminNavigation.php @@ -90,9 +90,12 @@ class AdminNavigation extends Navigation $this->addSubNavigation('institute', $navigation); $navigation = new Navigation(_('Standort')); - if ($perm->have_perm('root')) { - $navigation->addSubNavigation('range_tree', new Navigation(_('Einrichtungshierarchie'), 'dispatch.php/admin/tree/rangetree')); - $navigation->addSubNavigation('sem_tree', new Navigation(_('Veranstaltungshierarchie'), 'dispatch.php/admin/tree/semtree')); + if ($perm->have_perm(Config::get()->RANGE_TREE_ADMIN_PERM ? Config::get()->RANGE_TREE_ADMIN_PERM : 'admin')) { + $navigation->addSubNavigation('range_tree', new Navigation(_('Einrichtungshierarchie'), 'admin_range_tree.php')); + } + + if ($perm->have_perm(Config::get()->SEM_TREE_ADMIN_PERM ? Config::get()->SEM_TREE_ADMIN_PERM : 'admin') && $perm->is_fak_admin()) { + $navigation->addSubNavigation('sem_tree', new Navigation(_('Veranstaltungshierarchie'), 'admin_sem_tree.php')); } if ($perm->have_perm(Config::get()->LOCK_RULE_ADMIN_PERM ? Config::get()->LOCK_RULE_ADMIN_PERM : 'admin')) { diff --git a/public/admin_range_tree.php b/public/admin_range_tree.php new file mode 100644 index 0000000000000000000000000000000000000000..364f4fc95ce57737ff34a2c5b4a2ddb0f1df8483 --- /dev/null +++ b/public/admin_range_tree.php @@ -0,0 +1,59 @@ +<?php +# Lifter002: TODO +# Lifter007: TODO +# Lifter003: TODO +# Lifter010: TODO +/** +* Frontend +* +* +* +* @author André Noack <andre.noack@data.quest.de> +* @access public +* @modulegroup admin_modules +* @module admin_range_tree +* @package Admin +*/ +// +---------------------------------------------------------------------------+ +// This file is part of Stud.IP +// admin_range_tree.php +// +// Copyright (c) 2002 André Noack <noack@data-quest.de> +// Suchi & Berg GmbH <info@data-quest.de> +// +---------------------------------------------------------------------------+ +// 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 any later version. +// +---------------------------------------------------------------------------+ +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +---------------------------------------------------------------------------+ + + +require '../lib/bootstrap.php'; + +page_open(["sess" => "Seminar_Session", "auth" => "Seminar_Auth", "perm" => "Seminar_Perm", "user" => "Seminar_User"]); +$perm->check(Config::get()->RANGE_TREE_ADMIN_PERM ?: 'admin'); + +include 'lib/seminar_open.php'; //hier werden die sessions initialisiert + +PageLayout::setTitle(_('Einrichtungshierarchie bearbeiten')); +Navigation::activateItem('/admin/locations/range_tree'); + +ob_start(); + +$the_tree = new StudipRangeTreeViewAdmin(); +$the_tree->open_ranges['root'] = true; +$the_tree->showTree(); + +$template = $GLOBALS['template_factory']->open('layouts/base.php'); +$template->content_for_layout = ob_get_clean(); +echo $template->render(); + +page_close(); diff --git a/public/admin_sem_tree.php b/public/admin_sem_tree.php new file mode 100644 index 0000000000000000000000000000000000000000..e981be7230d0d51971a9698059cbbcdf16e9adb5 --- /dev/null +++ b/public/admin_sem_tree.php @@ -0,0 +1,310 @@ +<?php +# Lifter001: TEST +# Lifter002: TODO +# Lifter007: TODO +# Lifter003: TODO +# Lifter010: TODO +// +---------------------------------------------------------------------------+ +// This file is part of Stud.IP +// admin_sem_tree.php +// +// +// Copyright (c) 2003 André Noack <noack@data-quest.de> +// Suchi & Berg GmbH <info@data-quest.de> +// +---------------------------------------------------------------------------+ +// 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 any later version. +// +---------------------------------------------------------------------------+ +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +---------------------------------------------------------------------------+ + +use Studip\Button, Studip\LinkButton; + +require '../lib/bootstrap.php'; + +page_open(["sess" => "Seminar_Session", "auth" => "Seminar_Auth", "perm" => "Seminar_Perm", "user" => "Seminar_User"]); +$perm->check(Config::get()->SEM_TREE_ADMIN_PERM ?: 'admin'); +if (!$perm->is_fak_admin()){ + $perm->perm_invalid(0,0); + page_close(); + die; +} + +include 'lib/seminar_open.php'; // initialise Stud.IP-Session + +PageLayout::setTitle(_('Veranstaltungshierachie bearbeiten')); +Navigation::activateItem('/admin/locations/sem_tree'); + +// Start of Output +ob_start(); + +$view = DbView::getView('sem_tree'); +$the_tree = new StudipSemTreeViewAdmin(Request::option('start_item_id')); +$search_obj = new StudipSemSearch(); + +$_open_items =& $the_tree->open_items; +$_open_ranges =& $the_tree->open_ranges; +$_possible_open_items = []; + +if (!Config::GetInstance()->getValue('SEM_TREE_ALLOW_BRANCH_ASSIGN')){ + if(is_array($_open_items)){ + foreach($_open_items as $item_id => $value){ + if(!$the_tree->tree->getNumKids($item_id)) $_possible_open_items[$item_id] = $value; + } + } +} else { + $_possible_open_items = $_open_items; +} + +// allow add only for items where user has admin permission and which are not hidden +if (is_array($_possible_open_items)) { + foreach ($_possible_open_items as $item_id => $value) { + if (!$the_tree->isItemAdmin($item_id) || $the_tree->tree->isHiddenItem($item_id)) { + unset($_possible_open_items[$item_id]); + } + } +} + +if ($search_obj->search_done){ + if ($search_obj->search_result->numRows > 50){ + PageLayout::postError(_("Es wurden mehr als 50 Veranstaltungen gefunden! Bitte schränken Sie Ihre Suche weiter ein.")); + } elseif ($search_obj->search_result->numRows > 0){ + PageLayout::postSuccess(sprintf( + _("Es wurden %s Veranstaltungen gefunden, und in Ihre Merkliste eingefügt"), + $search_obj->search_result->numRows + )); + if (is_array($_SESSION['_marked_sem']) && count($_SESSION['_marked_sem'])){ + $_SESSION['_marked_sem'] = array_merge( + (array)$_SESSION['_marked_sem'], + (array)$search_obj->search_result->getDistinctRows("seminar_id") + ); + } else { + $_SESSION['_marked_sem'] = $search_obj->search_result->getDistinctRows("seminar_id"); + } + } else { + PageLayout::postInfo(_("Es wurden keine Veranstaltungen gefunden, auf die Ihre Suchkriterien zutreffen.")); + } +} + +if (Request::option('cmd') === "MarkList"){ + $sem_mark_list = Request::quotedArray('sem_mark_list'); + if ($sem_mark_list){ + if (Request::quoted('mark_list_aktion') == "del"){ + $count_del = 0; + for ($i = 0; $i < count($sem_mark_list); ++$i){ + if (isset($_SESSION['_marked_sem'][$sem_mark_list[$i]])){ + ++$count_del; + unset($_SESSION['_marked_sem'][$sem_mark_list[$i]]); + } + } + PageLayout::postSuccess(sprintf( + _("%s Veranstaltung(en) wurde(n) aus Ihrer Merkliste entfernt."), + $count_del + )); + } else { + $tmp = explode("_",Request::quoted('mark_list_aktion')); + $item_ids[0] = $tmp[1]; + if ($item_ids[0] === "all"){ + $item_ids = []; + foreach ($_possible_open_items as $key => $value){ + if($key !== 'root') + $item_ids[] = $key; + } + } + for ($i = 0; $i < count($item_ids); ++$i){ + $count_ins = 0; + for ($j = 0; $j < count($sem_mark_list); ++$j){ + if ($sem_mark_list[$j]){ + $count_ins += StudipSemTree::InsertSemEntry($item_ids[$i], $sem_mark_list[$j]); + } + } + $_msg .= sprintf( + _("%s Veranstaltung(en) in <b>" .htmlReady($the_tree->tree->tree_data[$item_ids[$i]]['name']) . "</b> eingetragen.<br>"), + $count_ins + ); + } + if ($_msg) { + PageLayout::postSuccess($_msg); + } + $the_tree->tree->init(); + } + } +} +if ($the_tree->mode === "MoveItem" || $the_tree->mode === "CopyItem"){ + if ($_msg){ + $_msg .= "§"; + } + if ($the_tree->mode === "MoveItem"){ + $text = _("Der Verschiebemodus ist aktiviert. Bitte wählen Sie ein Einfügesymbol %s aus, um das Element <b>%s</b> an diese Stelle zu verschieben.%s"); + } else { + $text = _("Der Kopiermodus ist aktiviert. Bitte wählen Sie ein Einfügesymbol %s aus, um das Element <b>%s</b> an diese Stelle zu kopieren.%s"); + } + PageLayout::postInfo(sprintf( + $text , + Icon::create('arr_2right', 'sort', ['title' => _('Einfügesymbol')])->asImg(), + htmlReady($the_tree->tree->tree_data[$the_tree->move_item_id]['name']), + "<div align=\"right\">" + . LinkButton::createCancel( + _('Abbrechen'), + $the_tree->getSelf("cmd=Cancel&item_id=$the_tree->move_item_id"), + ['title' => _("Verschieben / Kopieren abbrechen")] + ) + ."</div>" + )); +} + +?> + <? + $search_obj->attributes_default = ['style' => '']; + // $search_obj->search_fields['type']['size'] = 30 ; + echo $search_obj->getFormStart(URLHelper::getLink($the_tree->getSelf()), ['class' => 'default narrow']); + ?> + <fieldset> + <legend><?= _("Veranstaltungssuche") ?></legend> + + <label class="col-3"> + <?=_("Titel")?> + <?=$search_obj->getSearchField("title")?> + </label> + + <label class="col-3"> + <?=_("Untertitel")?> + <?=$search_obj->getSearchField("sub_title")?> + </label> + + <label class="col-3"> + <?=_("Nummer")?> + <?=$search_obj->getSearchField("number")?> + </label> + + <label class="col-3"> + <?=_("Kommentar")?> + <?=$search_obj->getSearchField("comment")?> + </label> + + <label class="col-3"> + <?=_("Lehrende")?> + <?=$search_obj->getSearchField("lecturer")?> + </label> + + <label class="col-3"> + <?=_("Bereich")?> + <?=$search_obj->getSearchField("scope")?> + </label> + + <label> + <?=_("Kombination")?> + <?=$search_obj->getSearchField('combination')?> + </label> + + <label class="col-3"> + <?=_("Typ")?> + <?=$search_obj->getSearchField("type", ['class' => 'size-s'])?> + </label> + + <label class="col-3"> + <?=_("Semester")?> + <?=$search_obj->getSearchField("sem", ['class' => 'size-s'])?> + </label> + </fieldset> + + <footer> + <?=$search_obj->getSearchButton();?> + <?=$search_obj->getNewSearchButton();?> + </footer> + + <?=$search_obj->getFormEnd();?> +<br> +<table width="100%" border="0" cellpadding="0" cellspacing="0"> + <tr> + <td class="blank" width="75%" align="left" valign="top" colspan="2"> + <? $the_tree->showSemTree(); ?> + </td> + </tr> +</table> + +<? +// Create Clipboard (use a second output buffer) +ob_start(); +?> + <form action="<?=URLHelper::getLink($the_tree->getSelf("cmd=MarkList"))?>" method="post" class="default"> + <?= CSRFProtection::tokenTag() ?> + <select multiple size="10" name="sem_mark_list[]" style="font-size:8pt;width:100%" class="nested-select"> + <? + $cols = 50; + if (is_array($_SESSION['_marked_sem']) && count($_SESSION['_marked_sem'])){ + $view->params[0] = array_keys($_SESSION['_marked_sem']); + $entries = new DbSnapshot($view->get_query("view:SEMINAR_GET_SEMDATA")); + $sem_data = $entries->getGroupedResult("seminar_id"); + $sem_number = -1; + foreach ($sem_data as $seminar_id => $data) { + if ((int)key($data['sem_number']) !== $sem_number){ + if ($sem_number !== -1) { + echo '</optgroup>'; + } + $sem_number = key($data['sem_number']); + echo "\n<optgroup label=\"" . $the_tree->tree->sem_dates[$sem_number]['name'] . "\">"; + } + $sem_name = key($data["Name"]); + $sem_number_end = (int)key($data["sem_number_end"]); + if ($sem_number !== $sem_number_end){ + $sem_name .= " (" . $the_tree->tree->sem_dates[$sem_number]['name'] . " - "; + $sem_name .= (($sem_number_end === -1) ? _("unbegrenzt") : $the_tree->tree->sem_dates[$sem_number_end]['name']) . ")"; + } + $line = htmlReady(my_substr($sem_name,0,$cols)); + $tooltip = $sem_name . " (" . join(",",array_keys($data["doz_name"])) . ")"; + echo "\n<option value=\"$seminar_id\" " . tooltip($tooltip,false) . ">$line</option>"; + } + echo '</optgroup>'; + } + ?> + </select> + <select name="mark_list_aktion" style="font-size:8pt;width:100%;margin-top:5px;"> + <? + if (is_array($_possible_open_items) && count($_possible_open_items) && !(count($_possible_open_items) === 1 && $_possible_open_items['root'])){ + echo "\n<option value=\"insert_all\">" . _("Markierte in alle geöffneten Bereiche eintragen") . "</option>"; + foreach ($_possible_open_items as $item_id => $value){ + echo "\n<option value=\"insert_{$item_id}\">" + . sprintf( + _('Markierte in "%s" eintragen'), + htmlReady(my_substr($the_tree->tree->tree_data[$item_id]['name'],0,floor($cols * .8)) + )) + . "</option>"; + } + } + ?> + <option value="del"><?=_("Markierte aus der Merkliste löschen")?></option> + </select> + <div align="center"> + <?= Button::create( + _('OK'), + [ + 'title' => _("Gewählte Aktion starten"), + 'style' => 'vertical-align:middle;margin:3px;', + 'class' => 'accept button' + ] + ); ?> + </div> + </form> +<? + +// Add Clipboard to Sidebar (get the inner/second output buffer) +$content = ob_get_clean(); +$widget = new SidebarWidget(); +$widget->setTitle(_('Merkliste')); +$widget->addElement(new WidgetElement($content)); +Sidebar::get()->addWidget($widget); + +$template = $GLOBALS['template_factory']->open('layouts/base.php'); +$template->content_for_layout = ob_get_clean(); +echo $template->render(); + +page_close(); diff --git a/resources/assets/javascripts/bootstrap/treeview.js b/resources/assets/javascripts/bootstrap/treeview.js deleted file mode 100644 index 998a70e72ff8bbb2838a8e03d95a89307b956569..0000000000000000000000000000000000000000 --- a/resources/assets/javascripts/bootstrap/treeview.js +++ /dev/null @@ -1,12 +0,0 @@ -import StudipTree from '../../../vue/components/tree/StudipTree.vue' - -STUDIP.ready(() => { - document.querySelectorAll('[data-studip-tree]').forEach(element => { - STUDIP.Vue.load().then(({ createApp }) => { - createApp({ - el: element, - components: { StudipTree } - }) - }) - }); -}); diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index 470e8699b31066e5a966384183e9785e3969dcdf..10686df05d7e0d669da1e00118cb31a2fb6dab5a 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -83,7 +83,6 @@ 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" import "./mvv_course_wizard.js" diff --git a/resources/assets/stylesheets/scss/tree.scss b/resources/assets/stylesheets/scss/tree.scss deleted file mode 100644 index dbad68b1bc507c14dacc893b93662e8631eb6e84..0000000000000000000000000000000000000000 --- a/resources/assets/stylesheets/scss/tree.scss +++ /dev/null @@ -1,384 +0,0 @@ -$tree-outline: 1px solid var(--light-gray-color-40); - -.studip-tree { - &.studip-tree-navigatable { - > header { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - - h1 { - display: inline-block; - width: calc(100% - 28px); - } - } - - .contentbar { - display: relative; - - .contentbar-wrapper-right { - display: inherit; - - .action-menu { - button { - top: -2px; - } - } - } - } - - .studip-tree-navigation-wrapper { - margin-right: 15px; - text-indent: 0; - - .studip-tree-navigation { - background-color: var(--white); - border: 1px solid var(--content-color-40); - box-shadow: 2px 2px mix($base-gray, $white, 20%); - right: -20px; - padding: 10px; - position: absolute; - top: -15px; - width: 400px; - z-index: 3; - - > header { - border-bottom: 1px solid var(--content-color-40); - display: flex; - height: 60px; - margin-bottom: 15px; - margin-top: -15px; - padding: 2px 0; - - h1 { - line-height:60px; - margin-bottom: 0; - width: calc(100% - 40px); - } - - button { - flex: 0; - padding-top: 10px; - } - } - - .studip-tree-node { - width: 100%; - } - } - } - } - - section { - margin-left: 0; - margin-right: 0; - } - - button { - background: transparent; - border: 0; - color: var(--base-color); - cursor: pointer; - padding: 0; - - &:hover { - .studip-tree-child-title { - text-decoration: underline; - } - } - } - - .studip-tree-course { - .course-dates { - color: var(--dark-gray-color-80); - font-size: $font-size-small; - padding-left: 35px; - } - - .course-details { - color: var(--dark-gray-color-80); - font-size: $font-size-small; - text-align: right; - - .admission-state { - height: 18px; - } - - .course-lecturers { - list-style: none; - padding-left: 0; - } - } - } - - /* Display as foldable tree */ - .studip-tree-node { - - width: 100%; - - a { - cursor: pointer; - display: flex; - - img { - vertical-align: bottom; - } - } - - .studip-tree-node-content { - - display: flex; - - &.studip-tree-node-active { - background-color: var(--light-gray-color-20); - margin: -5px; - padding: 5px; - } - - .studip-tree-node-toggle { - margin-left: -2px; - margin-right: 5px; - } - - .tooltip { - line-height: 24px; - margin-left: 5px; - } - - .studip-tree-node-assignment-state { - margin-right: 10px; - - img, svg { - vertical-align: text-bottom; - } - } - - a.studip-tree-node-edit-link { - opacity: 0; - visibility: hidden; - - } - - &:hover { - background-color: var(--light-gray-color-20); - - a.studip-tree-node-edit-link { - opacity: 1; - visibility: visible; - } - } - } - - .studip-tree-children { - list-style: none; - padding-left: 38px; - - li { - border-left: $tree-outline; - display: flex; - margin-left: -31px; - padding: 5px 0 5px 5px; - - &:before { - border-bottom: $tree-outline; - content: ""; - display: inline-block; - height: 1em; - left: -5px; - position: relative; - top: -5px; - vertical-align: top; - width: 10px; - } - - &:last-child { - border-left: none; - - &:before { - border-left: $tree-outline; - } - } - } - } - } - - > .studip-tree-node { - width: calc(100% - 25px); - } - - /* Top breadcrumb */ - .studip-tree-breadcrumb { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - max-width: 100%; - padding: 1em; - top: 2px; - - .contentbar-wrapper-left { - max-width: calc(100% - 25px); - - &.with-navigation { - max-width: calc(100% - 50px); - } - - &.editable { - max-width: calc(100% - 50px); - } - - &.with-navigation-and-editable { - max-width: calc(100% - 75px); - } - - img { - vertical-align: text-bottom; - } - - .studip-tree-breadcrumb-list { - display: inline-block; - flex: 1; - line-height: 24px; - margin-left: 15px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - - .action-menu { - position: relative; - top: 5px; - width: 30px; - } - - } - - /* Display as tiled list */ - .studip-tree-list { - section, nav:not(.contentbar-nav) { - padding: 15px; - } - - .studip-tree-children { - display: grid; - grid-gap: 15px; - grid-template-columns: repeat(auto-fit, $sidebar-width); - list-style: none; - overflow-wrap: break-word; - padding-left: 0; - - .studip-tree-child { - background: var(--dark-gray-color-5); - border: solid thin var(--light-gray-color-40); - display: flex; - height: 130px; - padding: 10px; - - /* Handle for drag&drop */ - .drag-handle { - background-position-y: 8px; - } - - a { - display: flex; - flex-direction: column; - padding: 10px; - text-align: left; - - .studip-tree-child-title { - font-size: 1.1em; - font-weight: bold; - } - } - - &:hover { - background: var(--white); - - button { - .studip-tree-child-title { - color: var(--red); - } - } - } - } - } - - table { - tr { - td { - line-height: 24px; - padding: 10px; - vertical-align: top; - - a { - img { - margin-right: 5px; - vertical-align: bottom; - } - } - } - } - } - } - - /* Display as table */ - .studip-tree-table { - table { - .studip-tree-node-info { - font-size: 0.9em; - padding: 15px; - } - - tbody { - tr { - - &.studip-tree-course { - .course-dates { - padding-left: 0; - } - } - - td { - line-height: 28px; - padding: 5px; - vertical-align: top; - - /* Handle for drag&drop */ - .drag-handle { - background-position-y: -5px; - padding-right: 10px; - } - - button { - background: transparent; - border: 0; - color: var(--base-color); - cursor: pointer; - - &:hover { - text-decoration: underline; - } - } - } - } - } - } - } - - .studip-tree-course-path { - font-size: 0.9em; - list-style: none; - padding: 5px; - - button { - padding: 0; - } - } -} - -form.default { - .studip-tree-node { - padding-top: unset !important; - } -} - diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index 467c50243ee266c2df38c91eb3c0e993688da2a2..8cd12b32b34192bce51d3886e9fb6939bd4da704 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -98,7 +98,6 @@ @import "scss/tooltip"; @import "scss/tfa"; @import "scss/tour"; -@import "scss/tree"; @import "scss/typography"; @import "scss/user-administration"; @import "scss/wiki"; diff --git a/resources/vue/components/SearchWidget.vue b/resources/vue/components/SearchWidget.vue deleted file mode 100644 index d590b0092447257009da9989a626772c22620d6e..0000000000000000000000000000000000000000 --- a/resources/vue/components/SearchWidget.vue +++ /dev/null @@ -1,56 +0,0 @@ -<template> - <sidebar-widget id="search-widget" class="sidebar-search" :title="$gettext('Suche')"> - <template #content> - <form class="sidebar-search"> - <ul class="needles"> - <li> - <div class="input-group files-search"> - <input type="text" id="searchterm" name="searchterm" v-model="searchterm" - :placeholder="$gettext('Veranstaltung suchen')" - :aria-label="$gettext('Veranstaltung suchen')"> - <a v-if="isActive" @click.prevent="cancelSearch" class="reset-search"> - <studip-icon shape="decline" size="20"></studip-icon> - </a> - <button type="submit" class="submit-search" :title="$gettext('Suchen')" - @click.prevent="doSearch"> - <studip-icon shape="search" :size="20"></studip-icon> - </button> - </div> - </li> - </ul> - </form> - </template> - </sidebar-widget> -</template> - -<script> -import SidebarWidget from './SidebarWidget.vue'; -import StudipIcon from './StudipIcon.vue'; - -export default { - name: 'search-widget', - components: { - StudipIcon, - SidebarWidget - }, - data() { - return { - searchterm: '', - isActive: false - }; - }, - methods: { - doSearch() { - if (this.searchterm !== '') { - this.isActive = true; - STUDIP.eventBus.emit('do-search', this.searchterm); - } - }, - cancelSearch() { - this.isActive = false; - this.searchterm = ''; - STUDIP.eventBus.emit('cancel-search'); - } - } -} -</script> diff --git a/resources/vue/components/StudipProgressIndicator.vue b/resources/vue/components/StudipProgressIndicator.vue index 62a33a8840fa4c476571da1d25bc903d126b150c..b6f7ee23537f6553d2cc30751dd64f3800c44838 100644 --- a/resources/vue/components/StudipProgressIndicator.vue +++ b/resources/vue/components/StudipProgressIndicator.vue @@ -31,7 +31,7 @@ export default { }; }, hasDescription () { - return this.description?.trim().length > 0; + return this.description.trim().length > 0; } } } diff --git a/resources/vue/components/tree/AssignLinkWidget.vue b/resources/vue/components/tree/AssignLinkWidget.vue deleted file mode 100644 index fec478f43b994f4c5572980bd82a6b013acb46c2..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/AssignLinkWidget.vue +++ /dev/null @@ -1,46 +0,0 @@ -<template> - <sidebar-widget v-if="node" id="assignwidget" class="sidebar-assign" :title="$gettext('Zuordnung')"> - <template #content> - <a :href="assignUrl" :title="$gettext('Angezeigte Veranstaltungen zuordnen')" - @click.prevent="assignCurrentCourses"> - <studip-icon shape="arr_2right"></studip-icon> - {{ $gettext('Angezeigte Veranstaltungen zuordnen') }} - </a> - </template> - </sidebar-widget> -</template> - -<script> -import SidebarWidget from '../SidebarWidget.vue'; -import StudipIcon from '../StudipIcon.vue'; -import { TreeMixin } from '../../mixins/TreeMixin'; - -export default { - name: 'AssignLinkWidget', - components: { SidebarWidget, StudipIcon }, - mixins: [ TreeMixin ], - props: { - node: { - type: String, - required: true - }, - courses: { - type: Array, - default: () => [] - } - }, - computed: { - assignUrl() { - return STUDIP.URLHelper.getURL('dispatch.php/admin/tree/batch_assign_semtree'); - } - }, - methods: { - assignCurrentCourses() { - STUDIP.Dialog.fromURL(this.assignUrl, { data: { - assign_semtree: this.courses.map(course => course.id), - return: window.location.href - }}); - } - } -} -</script> diff --git a/resources/vue/components/tree/StudipTree.vue b/resources/vue/components/tree/StudipTree.vue deleted file mode 100644 index ff493739f9823a121fc00fd54550fab639070c5c..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/StudipTree.vue +++ /dev/null @@ -1,214 +0,0 @@ -<template> - <div> - <div v-if="!isSearching" - class="studip-tree" :class="{'studip-tree-navigatable': showStructureAsNavigation}"> - <studip-progress-indicator v-if="isLoading" :size="48"></studip-progress-indicator> - <studip-tree-list v-if="viewType === 'list' && startNode" :with-children="withChildren" - :visible-children-only="visibleChildrenOnly" - :with-courses="withCourses" :semester="semester" :sem-class="semClass" :node="startNode" - :breadcrumb-icon="breadcrumbIcon" :editable="editable" :edit-url="editUrl" - :create-url="createUrl" :delete-url="deleteUrl" :with-export="withExport" - :show-structure-as-navigation="showStructureAsNavigation" :assignable="assignable" - :with-course-assign="withCourseAssign" - @change-current-node="changeCurrentNode"></studip-tree-list> - <studip-tree-table v-else-if="viewType === 'table' && startNode" :with-children="withChildren" - :visible-children-only="visibleChildrenOnly" - :with-courses="withCourses" :semester="semester" :sem-class="semClass" :node="startNode" - :breadcrumb-icon="breadcrumbIcon" :editable="editable" :edit-url="editUrl" - :create-url="createUrl" :delete-url="deleteUrl" :with-export="withExport" - :show-structure-as-navigation="showStructureAsNavigation" :assignable="assignable" - :with-course-assign="withCourseAssign" - @change-current-node="changeCurrentNode"></studip-tree-table> - <studip-tree-node v-else-if="viewType === 'tree' && startNode" :with-info="withInfo" - :visible-children-only="visibleChildrenOnly" :node="startNode" - :open-levels="openLevels" :openNodes="openNodes" :breadcrumb-icon="breadcrumbIcon" - :editable="editable" :edit-url="editUrl" :create-url="createUrl" :delete-url="deleteUrl" - :assignable="assignable" :assign-leaves-only="assignLeavesOnly" - :not-assignable-nodes="notAssignableNodes"></studip-tree-node> - - </div> - <div v-else class="studip-tree"> - <tree-search-result :search-config="searchConfig"></tree-search-result> - </div> - <MountingPortal v-if="withSearch" mountTo="#search-widget" name="sidebar-search"> - <search-widget></search-widget> - </MountingPortal> - </div> -</template> - -<script> -import axios from 'axios'; -import { TreeMixin } from '../../mixins/TreeMixin'; -import StudipProgressIndicator from '../StudipProgressIndicator.vue'; -import SearchWidget from '../SearchWidget.vue'; -import StudipTreeList from './StudipTreeList.vue'; -import StudipTreeTable from './StudipTreeTable.vue'; -import StudipTreeNode from './StudipTreeNode.vue'; -import TreeSearchResult from './TreeSearchResult.vue'; - -export default { - name: 'StudipTree', - components: { - TreeSearchResult, SearchWidget, StudipProgressIndicator, StudipTreeList, StudipTreeTable, StudipTreeNode - }, - mixins: [ TreeMixin ], - props: { - viewType: { - type: String, - default: 'tree' - }, - treeId: { - type: String, - default: '' - }, - startId: { - type: String, - required: true - }, - title: { - type: String, - default: '' - }, - openNodes: { - type: Array, - default: () => [] - }, - openLevels: { - type: Number, - default: 0 - }, - withChildren: { - type: Boolean, - default: true - }, - withInfo: { - type: Boolean, - default: true - }, - visibleChildrenOnly: { - type: Boolean, - default: true - }, - withCourses: { - type: Boolean, - default: false - }, - semester: { - type: String, - default: '' - }, - semClass: { - type: Number, - default: 0 - }, - breadcrumbIcon: { - type: String, - default: 'literature' - }, - itemIcon: { - type: String, - default: 'literature' - }, - withSearch: { - type: Boolean, - default: false - }, - withExport: { - type: Boolean, - default: false - }, - withCourseAssign: { - type: Boolean, - default: false - }, - editable: { - type: Boolean, - default: false - }, - editUrl: { - type: String, - default: '' - }, - createUrl: { - type: String, - default: '' - }, - deleteUrl: { - type: String, - default: '' - }, - showStructureAsNavigation: { - type: Boolean, - default: false - }, - assignable: { - type: Boolean, - default: false - }, - assignLeavesOnly: { - type: Boolean, - default: false - }, - notAssignableNodes: { - type: Array, - default: () => [] - } - }, - data() { - return { - nodeId: this.startId, - startNode: null, - currentNode: this.startNode, - loaded: false, - isLoading: false, - showStructuralNavigation: false, - searchConfig: {}, - isSearching: false - } - }, - methods: { - changeCurrentNode(node) { - this.currentNode = node; - this.$nextTick(() => { - document.getElementById('tree-breadcrumb-' + node.attributes.id)?.focus(); - }); - }, - exportUrl() { - return STUDIP.URLHelper.getURL('dispatch.php/tree/export_csv'); - } - }, - mounted() { - window.focus(); - - const loadingIndicator = axios.interceptors.request.use(config => { - setTimeout(() => { - if (!this.loaded) { - this.isLoading = true; - } - }, this.showProgressIndicatorTimeout); - return config; - }); - - this.getNode(this.startId).then(response => { - this.startNode = response.data.data; - this.currentNode = this.startNode; - this.loaded = true; - this.isLoading = false; - }); - - axios.interceptors.request.eject(loadingIndicator); - - this.globalOn('do-search', searchterm => { - this.searchConfig.searchterm = searchterm; - this.searchConfig.semester = this.semester; - this.searchConfig.classname = this.startNode.attributes.classname; - this.isSearching = true; - }); - - this.globalOn('cancel-search', () => { - this.searchConfig = {}; - this.isSearching = false; - }); - } -} -</script> diff --git a/resources/vue/components/tree/StudipTreeList.vue b/resources/vue/components/tree/StudipTreeList.vue deleted file mode 100644 index d773f433f891041b28e161f3786e578c44c763ca..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/StudipTreeList.vue +++ /dev/null @@ -1,359 +0,0 @@ -<template> - <article class="studip-tree-list"> - <header> - <tree-breadcrumb v-if="currentNode.id !== 'root'" :node="currentNode" - :edit-url="editUrl" :icon="breadcrumbIcon" :assignable="assignable" - :num-children="children.length" :num-courses="courses.length" - :show-navigation="showStructureAsNavigation" - :visible-children-only="visibleChildrenOnly"></tree-breadcrumb> - </header> - <studip-progress-indicator v-if="isLoading"></studip-progress-indicator> - <section v-else> - <h1> - {{ currentNode.attributes.name }} - <a v-if="isEditable && currentNode.attributes.id !== 'root'" - :href="editUrl + '/' + currentNode.attributes.id" - @click.prevent="editNode(editUrl, currentNode.id)" data-dialog="size=medium" - :title="$gettextInterpolate($gettext('%{name} bearbeiten'), {name: currentNode.attributes.name})"> - <studip-icon shape="edit" :size="20"></studip-icon> - </a> - </h1> - <p v-if="currentNode.attributes.description?.trim() !== ''" class="studip-tree-node-info" - v-html="currentNode.attributes['description-formatted']"> - </p> - </section> - - <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span> - - <nav v-if="withChildren && currentNode.attributes['has-children']" > - <h1> - {{ $gettext('Unterebenen') }} - </h1> - <draggable v-model="children" handle=".drag-handle" :animation="300" tag="ul" - class="studip-tree-children" @end="dropChild"> - <li v-for="(child, index) in children" :key="index" class="studip-tree-child"> - <a v-if="editable && children.length > 1" class="drag-link" - tabindex="0" - :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name})" - @keydown="keyHandler($event, index)" - :ref="'draghandle-' + index"> - <span class="drag-handle"></span> - </a> - <tree-node-tile :node="child" :semester="withCourses ? semester : 'all'" :sem-class="semClass" - :url="nodeUrl(child.id, semester !== 'all' ? semester : null)"></tree-node-tile> - </li> - </draggable> - </nav> - <section v-else-if="withChildren && !currentNode.attributes['has-children']" class="studip-tree-node-no-children"> - {{ $gettext('Auf dieser Ebene existieren keine weiteren Unterebenen.') }} - </section> - <section v-if="withCourses && thisLevelCourses === 0" class="studip-tree-node-no-courses"> - {{ $gettext('Auf dieser Ebene sind keine Veranstaltungen zugeordnet.')}} - </section> - - <section v-if="thisLevelCourses + subLevelsCourses > 0"> - <span v-if="withCourses && showingAllCourses"> - <button type="button" @click="showAllCourses(false)" - :title="$gettext('Veranstaltungen auf dieser Ebene anzeigen')"> - Veranstaltungen auf dieser Ebene anzeigen - </button> - </span> - <template v-if="thisLevelCourses > 0 && subLevelsCourses > 0"> - | - </template> - <span v-if="withCourses && subLevelsCourses > 0 && !showingAllCourses"> - <button type="button" @click="showAllCourses(true)" - :title="$gettext('Veranstaltungen auf allen Unterebenen anzeigen')"> - Veranstaltungen auf allen Unterebenen anzeigen - </button> - </span> - </section> - <table v-if="courses.length > 0" class="default"> - <caption>{{ $gettext('Veranstaltungen') }}</caption> - <colgroup> - <col> - <col> - </colgroup> - <thead> - <tr> - <th>{{ $gettext('Name') }}</th> - <th>{{ $gettext('Information') }}</th> - </tr> - </thead> - <tbody> - <tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course"> - <td> - <a :href="courseUrl(course.id)" - :title="$gettextInterpolate($gettext('Zur Veranstaltung %{ course }'), - { course: course.attributes.title })"> - <studip-icon shape="seminar" :size="26"></studip-icon> - <template v-if="course.attributes['course-number']"> - {{ course.attributes['course-number'] }} - </template> - {{ course.attributes.title }} - </a> - <div :id="'course-dates-' + course.id" class="course-dates"></div> - </td> - <td> - <tree-course-details :course="course.id"></tree-course-details> - </td> - </tr> - </tbody> - </table> - <MountingPortal v-if="withExport" mountTo="#export-widget" name="sidebar-export"> - <tree-export-widget v-if="courses.length > 0" - :title="$gettext('Veranstaltungen exportieren')" :url="exportUrl()" - :export-data="courses"></tree-export-widget> - </MountingPortal> - <MountingPortal v-if="withCourseAssign" mountTo="#assign-widget" name="sidebar-assign-courses"> - <assign-link-widget v-if="courses.length > 0" :node="currentNode" :courses="courses"></assign-link-widget> - </MountingPortal> - </article> -</template> - -<script> -import draggable from 'vuedraggable'; -import { TreeMixin } from '../../mixins/TreeMixin'; -import TreeExportWidget from './TreeExportWidget.vue'; -import TreeBreadcrumb from './TreeBreadcrumb.vue'; -import TreeNodeTile from './TreeNodeTile.vue'; -import StudipProgressIndicator from '../StudipProgressIndicator.vue'; -import TreeCourseDetails from './TreeCourseDetails.vue'; -import AssignLinkWidget from "./AssignLinkWidget.vue"; - -export default { - name: 'StudipTreeList', - components: { - draggable, StudipProgressIndicator, TreeExportWidget, TreeBreadcrumb, TreeNodeTile, TreeCourseDetails, - AssignLinkWidget - }, - mixins: [ TreeMixin ], - props: { - node: { - type: Object, - required: true - }, - breadcrumbIcon: { - type: String, - default: 'literature' - }, - editable: { - type: Boolean, - default: false - }, - editUrl: { - type: String, - default: '' - }, - createUrl: { - type: String, - default: '' - }, - deleteUrl: { - type: String, - default: '' - }, - withCourses: { - type: Boolean, - default: false - }, - withExport: { - type: Boolean, - default: false - }, - withChildren: { - type: Boolean, - default: true - }, - visibleChildrenOnly: { - type: Boolean, - default: true - }, - assignable: { - type: Boolean, - default: false - }, - withCourseAssign: { - type: Boolean, - default: false - }, - semester: { - type: String, - default: '' - }, - semClass: { - type: Number, - default: 0 - }, - showStructureAsNavigation: { - type: Boolean, - default: false - } - }, - data() { - return { - currentNode: this.node, - isLoading: false, - isLoaded: false, - children: [], - courses: [], - assistiveLive: '', - subLevelsCourses: 0, - thisLevelCourses: 0, - showingAllCourses: false - } - }, - methods: { - openNode(node, pushState = true) { - this.currentNode = node; - this.$emit('change-current-node', node); - - if (this.withChildren) { - this.getNodeChildren(node, this.visibleChildrenOnly).then(response => { - this.children = response.data.data; - }); - } - - this.getNodeCourseInfo(node, this.semester, this.semClass) - .then(response => { - this.thisLevelCourses = response?.data.courses; - this.subLevelsCourses = response?.data.allCourses; - }); - - if (this.withCourses) { - this.getNodeCourses(node, this.semester, this.semClass, '', false) - .then(courses => { - this.courses = courses.data.data; - }); - } - - // Update browser history. - if (pushState) { - const nodeId = node.id; - const url = STUDIP.URLHelper.getURL('', {node_id: nodeId}); - window.history.pushState({nodeId}, '', url); - } - - // Update node_id for semester selector. - const semesterSelector = document.querySelector('#semester-selector-node-id'); - semesterSelector.value = node.id; - }, - dropChild() { - this.updateSorting(this.currentNode.id, this.children); - }, - keyHandler(e, index) { - switch (e.keyCode) { - case 38: // up - e.preventDefault(); - this.decreasePosition(index); - this.$nextTick(() => { - this.$refs['draghandle-' + (index - 1)][0].focus(); - this.assistiveLive = this.$gettextInterpolate( - this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'), - { pos: index, listLength: this.children.length } - ); - }); - break; - case 40: // down - e.preventDefault(); - this.increasePosition(index); - this.$nextTick(function () { - this.$refs['draghandle-' + (index + 1)][0].focus(); - this.assistiveLive = this.$gettextInterpolate( - this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'), - { pos: index + 2, listLength: this.children.length } - ); - }); - break; - } - }, - decreasePosition(index) { - if (index > 0) { - const temp = this.children[index - 1]; - this.children[index - 1] = this.children[index]; - this.children[index] = temp; - this.updateSorting(this.currentNode.id, this.children); - } - }, - increasePosition(index) { - if (index < this.children.length) { - const temp = this.children[index + 1]; - this.children[index + 1] = this.children[index]; - this.children[index] = temp; - this.updateSorting(this.currentNode.id, this.children); - } - }, - showAllCourses(state) { - this.getNodeCourses(this.currentNode, this.semester, this.semClass, '', state) - .then(courses => { - this.courses = courses.data.data; - this.showingAllCourses = state; - }); - } - }, - mounted() { - if (this.withChildren) { - this.getNodeChildren(this.currentNode, this.visibleChildrenOnly).then(response => { - this.children = response.data.data; - }); - } - - this.getNodeCourseInfo(this.currentNode, this.semester, this.semClass) - .then(response => { - this.thisLevelCourses = response?.data.courses; - this.subLevelsCourses = response?.data.allCourses; - }); - - if (this.withCourses) { - this.getNodeCourses(this.currentNode, this.semester, this.semClass) - .then(courses => { - this.courses = courses.data.data; - }); - } - - this.globalOn('open-tree-node', node => { - this.openNode(node); - }); - - this.globalOn('load-tree-node', id => { - this.getNode(id).then(response => { - this.openNode(response.data.data); - }); - }); - - this.globalOn('sort-tree-children', data => { - if (this.currentNode.id === data.parent) { - this.children = data.children; - } - }); - - window.addEventListener('popstate', (event) => { - if (event.state) { - if ('nodeId' in event.state) { - this.getNode(event.state.nodeId).then(response => { - this.openNode(response.data.data, false); - }); - } - } else { - this.openNode(this.node, false); - } - }); - - // Add current node to semester selector widget. - this.$nextTick(() => { - const semesterForm = document.querySelector('#semester-selector .sidebar-widget-content form'); - const nodeField = document.createElement('input'); - nodeField.id = 'semester-selector-node-id'; - nodeField.type = 'hidden'; - nodeField.name = 'node_id'; - nodeField.value = this.node.id; - semesterForm.appendChild(nodeField); - }); - }, - beforeDestroy() { - STUDIP.eventBus.off('open-tree-node'); - STUDIP.eventBus.off('load-tree-node'); - STUDIP.eventBus.off('sort-tree-children'); - } -} -</script> diff --git a/resources/vue/components/tree/StudipTreeNode.vue b/resources/vue/components/tree/StudipTreeNode.vue deleted file mode 100644 index 39f696ccc14e12480701eb2501a71b34cffacf4a..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/StudipTreeNode.vue +++ /dev/null @@ -1,277 +0,0 @@ -<template> - <section class="studip-tree-node"> - <span :class="{ 'studip-tree-node-content': true, 'studip-tree-node-active': node?.id === activeNode?.id }"> - <a @click.prevent="toggleNode(true)"> - <div v-if="node.attributes['has-children']" class="studip-tree-node-toggle"> - <studip-icon :shape="openState ? 'arr_1down': 'arr_1right'" :size="20"/> - </div> - </a> - <button v-if="isAssignable && node.attributes.id !== 'root'" class="studip-tree-node-assignment-state" - @click.prevent="changeAssignmentState()" :title="$gettext('Zuordnung ändern')"> - <studip-icon :shape="assignmentState === 0 - ? 'checkbox-unchecked' - : (assignmentState === 1 ? 'checkbox-checked' : 'checkbox-indeterminate')"></studip-icon> - </button> - <a @click.prevent="toggleNode(true)"> - <div class="studip-tree-node-name"> - {{ node.attributes.name }} - </div> - </a> - <studip-tooltip-icon v-if="withInfo && !isLoading && node.attributes.description?.trim() !== ''" - :text="node.attributes['description-formatted'].trim()"></studip-tooltip-icon> - <input v-if="isAssignable && node.attributes.id !== 'root'" type="hidden" :name="assignmentAction" - :value="node.attributes.id"> - <a v-if="editable && node.attributes.id !== 'root'" :href="editUrl + '/' + node.attributes.id" - @click.prevent="editNode(editUrl, node.id)" data-dialog="size=medium" - class="studip-tree-node-edit-link"> - <studip-icon shape="edit"></studip-icon> - </a> - </span> - <div v-if="isLoading" class="studip-spinner"> - <studip-asset-img file="ajax-indicator-black.svg" width="20"/> - {{ $gettext('Daten werden geladen...' )}} - </div> - <ul v-if="node.attributes['has-children'] && openState" class="studip-tree-children"> - <li v-for="(child) in children" :key="child.id" > - <studip-tree-node :node="child" :editable="editable" :edit-url="editUrl" :create-url="createUrl" - :delete-url="deleteUrl" :assignable="assignable" :ancestors="theAncestors" - :not-assignable-nodes="notAssignableNodes" :open-nodes="openNodes" - :open-levels="openLevels > 0 ? (openLevels - 1) : 0" - :visible-children-only="visibleChildrenOnly" - :active-node="activeNode" :with-info="withInfo"></studip-tree-node> - </li> - </ul> - </section> -</template> - -<script> -import { TreeMixin } from '../../mixins/TreeMixin'; -import StudipIcon from '../StudipIcon.vue'; -import StudipAssetImg from '../StudipAssetImg.vue'; -import axios from 'axios'; -import StudipTooltipIcon from '../StudipTooltipIcon.vue'; - -export default { - name: 'StudipTreeNode', - components: { StudipTooltipIcon, StudipAssetImg, StudipIcon }, - mixins: [ TreeMixin ], - props: { - node: { - type: Object, - required: true - }, - activeNode: { - type: Object, - default: null - }, - isOpen: { - type: Boolean, - default: false - }, - breadcrumbIcon: { - type: String, - default: 'literature' - }, - withInfo: { - type: Boolean, - default: true - }, - editable: { - type: Boolean, - default: false - }, - editUrl: { - type: String, - default: '' - }, - createUrl: { - type: String, - default: '' - }, - deleteUrl: { - type: String, - default: '' - }, - visibleChildrenOnly: { - type: Boolean, - default: true - }, - withCourses: { - type: Boolean, - default: true - }, - assignable: { - type: Boolean, - default: false - }, - assignLeavesOnly: { - type: Boolean, - default: false - }, - notAssignableNodes: { - type: Array, - default: () => [] - }, - openLevels: { - type: Number, - default: 0 - }, - openNodes: { - type: Array, - default: () => [] - }, - ancestors: { - type: Array, - default: () => [] - } - }, - data() { - return { - isLoading: false, - childrenLoaded: false, - children: [], - semester: 'all', - openState: this.isOpen, - theAncestors: this.ancestors, - assignedCourses: 0, - assignmentState: 0, - assignmentAction: '' - } - }, - methods: { - toggleNode(emitEvent = false) { - this.courses = []; - this.openState = !this.openState; - if (emitEvent) { - STUDIP.eventBus.emit('load-tree-node', this.node.id); - } - if (!this.childrenLoaded) { - this.children = []; - const loadingIndicator = axios.interceptors.request.use(config => { - setTimeout(() => { - if (!this.childrenLoaded) { - this.isLoading = true; - } - }, 500); - return config; - }); - this.getNodeChildren(this.node, this.visibleChildrenOnly) - .then(response => { - this.isLoading = false; - this.children = response.data.data; - this.childrenLoaded = true; - }); - axios.interceptors.request.eject(loadingIndicator); - } - }, - /** - * Check whether currently selected course are assigned to this node. - */ - checkAssignments() { - const courses = document.querySelectorAll('table.selected-courses input[name="courses[]"]') ?? []; - let ids = []; - for (const course of courses) { - ids.push(course.value); - } - - if (ids.length > 0) { - this.getNodeCourses(this.node, 'all', 0, '', false, ids) - .then(response => { - // None of the given courses are assigned here. - if (response.data.data.length === 0) { - this.assignedCourses = this.assignmentState = 0; - // All of the given courses are assigned here. - } else if (response.data.data.length === ids.length) { - this.assignedCourses = this.assignmentState = 1; - // Some of the given courses are assigned here. - } else { - this.assignedCourses = this.assignmentState = -1; - } - }); - } - }, - /** - * Change what shall be done on submitting the form. - */ - changeAssignmentState() { - // Current state is 0 -> remove all assignments. - if (this.assignmentState === 0) { - // Not all courses are assigned here -> next state is indeterminate. - if (this.assignedCourses === -1) { - this.assignmentState = -1; - // Next state is 1 -> add assignments here. - } else { - this.assignmentState = 1; - } - // Current state is 1 -> next state is 0 -> remove assignments here. - } else if (this.assignmentState === 1) { - this.assignmentState = 0; - // Current state is indeterminate -> next state is 1 -> add assignments here. - } else { - this.assignmentState = 1; - } - - // Current state returned to original, nothing needs to be done. - if (this.assignmentState === this.assignedCourses) { - this.assignmentAction = ''; - // Current state is different from original state -> add or remove. - } else { - switch (this.assignmentState) { - case 0: - this.assignmentAction = 'delete_assignments[]'; - break; - case 1: - this.assignmentAction = 'add_assignments[]'; - break; - } - } - - } - }, - computed: { - isAssignable() { - return this.assignable && !this.notAssignableNodes?.includes(this.node.id); - } - }, - mounted() { - if (this.openLevels > 0) { - this.toggleNode(); - } - - if (this.ancestors.length === 0) { - for (const open of this.openNodes) { - this.getNode(open).then((response) => { - const haystack = response.data.data.attributes.ancestors?.map(element => { - return element.classname + '_' + element.id; - }); - if (haystack) { - this.theAncestors = haystack; - if (this.theAncestors.includes(this.node.id)) { - this.toggleNode(); - } - } - }); - - } - } - - this.globalOn('sort-tree-children', data => { - if (this.node.id === data.parent) { - this.children = data.children; - } - }); - - this.$nextTick(() => { - if (this.theAncestors?.includes(this.node.id) && !this.openState) { - this.toggleNode(); - } - if (this.isAssignable && this.node.attributes.id !== 'root') { - this.checkAssignments(); - } - }); - }, - beforeDestroy() { - STUDIP.eventBus.off('sort-tree-children'); - } -} -</script> diff --git a/resources/vue/components/tree/StudipTreeTable.vue b/resources/vue/components/tree/StudipTreeTable.vue deleted file mode 100644 index 0bfc244fb7554d8b48fdec1d034795a1d32a6226..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/StudipTreeTable.vue +++ /dev/null @@ -1,377 +0,0 @@ -<template> - <div v-if="isLoading"> - <studip-progress-indicator></studip-progress-indicator> - </div> - <article v-else class="studip-tree-table"> - <header> - <tree-breadcrumb v-if="currentNode.id !== 'root'" :node="currentNode" - :icon="breadcrumbIcon" :editable="editable" :edit-url="editUrl" :create-url="createUrl" - :delete-url="deleteUrl" :show-navigation="showStructureAsNavigation" - :num-children="children.length" :num-courses="courses.length" - :assignable="assignable" :visible-children-only="visibleChildrenOnly"></tree-breadcrumb> - </header> - <section v-if="withChildren && !currentNode.attributes['has-children']" class="studip-tree-node-no-children"> - {{ $gettext('Auf dieser Ebene existieren keine weiteren Unterebenen.')}} - </section> - - <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span> - - <div v-if="currentNode.attributes.description?.trim() !== ''" - v-html="currentNode.attributes['description-formatted']"></div> - - <section v-if="thisLevelCourses === 0" class="studip-tree-node-no-courses"> - {{ $gettext('Auf dieser Ebene sind keine Veranstaltungen zugeordnet.')}} - </section> - - <section v-if="thisLevelCourses + subLevelsCourses > 0"> - <span v-if="withCourses && showingAllCourses"> - <button type="button" @click="showAllCourses(false)" - :title="$gettext('Veranstaltungen auf dieser Ebene anzeigen')"> - {{ $gettext('Veranstaltungen auf dieser Ebene anzeigen') }} - </button> - </span> - <template v-if="thisLevelCourses > 0 && subLevelsCourses > 0"> - | - </template> - <span v-if="withCourses && subLevelsCourses > 0 && !showingAllCourses"> - <button type="button" @click="showAllCourses(true)" - :title="$gettext('Veranstaltungen auf allen Unterebenen anzeigen')"> - {{ $gettext('Veranstaltungen auf allen Unterebenen anzeigen') }} - </button> - </span> - </section> - - <table v-if="currentNode.attributes['has-children'] || courses.length > 0" class="default"> - <caption class="studip-tree-node-info"> - <span v-if="withChildren && children.length > 0"> - {{ $gettextInterpolate($gettext('%{ count } Unterebenen'), { count: children.length }) }} - </span> - <span v-if="withChildren && children.length > 0 && withCourses && courses.length > 0"> - , - </span> - </caption> - <colgroup> - <col style="width: 20px"> - <col style="width: 30px"> - <col> - <col style="width: 40%"> - </colgroup> - <thead> - <tr> - <th></th> - <th>{{ $gettext('Typ') }}</th> - <th>{{ $gettext('Name') }}</th> - <th>{{ $gettext('Information') }}</th> - </tr> - </thead> - <draggable v-model="children" handle=".drag-handle" :animation="300" - @end="dropChild" tag="tbody" role="listbox"> - <tr v-for="(child, index) in children" :key="index" class="studip-tree-child"> - <td> - <a v-if="editable && children.length > 1" class="drag-link" role="option" - tabindex="0" - :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name})" - @keydown="keyHandler($event, index)" - :ref="'draghandle-' + index"> - <span class="drag-handle"></span> - </a> - </td> - <td> - <studip-icon :shape="child.attributes['has-children'] ? 'folder-full' : 'folder-empty'" - :size="26"></studip-icon> - </td> - <td> - <a :href="nodeUrl(child.id, semester !== 'all' ? semester : null)" tabindex="0" - @click.prevent="openNode(child)" - :title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'), - { node: node.attributes.name })"> - {{ child.attributes.name }} - </a> - </td> - <td> - <tree-node-course-info :node="child" :semester="semester" - :sem-class="semClass"></tree-node-course-info> - </td> - </tr> - <tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course"> - <td></td> - <td> - <studip-icon shape="seminar" :size="26"></studip-icon> - </td> - <td> - <a :href="courseUrl(course.id)" tabindex="0" - :title="$gettextInterpolate($gettext('Zur Veranstaltung %{ course }'), - { course: course.attributes.title })"> - <template v-if="course.attributes['course-number']"> - {{ course.attributes['course-number'] }} - </template> - {{ course.attributes.title }} - </a> - <div :id="'course-dates-' + course.id" class="course-dates"></div> - </td> - <td :colspan="editable ? 2 : null"> - <tree-course-details :course="course.id"></tree-course-details> - </td> - </tr> - </draggable> - </table> - <MountingPortal v-if="withExport" mountTo="#export-widget" name="sidebar-export"> - <tree-export-widget v-if="courses.length > 0" :title="$gettext('Download des Ergebnisses')" :url="exportUrl()" - :export-data="courses"></tree-export-widget> - </MountingPortal> - <MountingPortal v-if="withCourseAssign" mountTo="#assign-widget" name="sidebar-assign-courses"> - <assign-link-widget v-if="courses.length > 0" :node="currentNode" :courses="courses"></assign-link-widget> - </MountingPortal> - </article> -</template> - -<script> -import draggable from 'vuedraggable'; -import { TreeMixin } from '../../mixins/TreeMixin'; -import TreeExportWidget from './TreeExportWidget.vue'; -import TreeBreadcrumb from './TreeBreadcrumb.vue'; -import StudipProgressIndicator from '../StudipProgressIndicator.vue'; -import StudipIcon from '../StudipIcon.vue'; -import TreeNodeCourseInfo from './TreeNodeCourseInfo.vue'; -import TreeCourseDetails from "./TreeCourseDetails.vue"; -import AssignLinkWidget from "./AssignLinkWidget.vue"; - -export default { - name: 'StudipTreeTable', - components: { - draggable, TreeExportWidget, TreeCourseDetails, StudipIcon, StudipProgressIndicator, TreeBreadcrumb, - TreeNodeCourseInfo, AssignLinkWidget - }, - mixins: [ TreeMixin ], - props: { - node: { - type: Object, - required: true - }, - breadcrumbIcon: { - type: String, - default: 'literature' - }, - editable: { - type: Boolean, - default: false - }, - editUrl: { - type: String, - default: '' - }, - createUrl: { - type: String, - default: '' - }, - deleteUrl: { - type: String, - default: '' - }, - withCourses: { - type: Boolean, - default: false - }, - withExport: { - type: Boolean, - default: false - }, - withChildren: { - type: Boolean, - default: true - }, - visibleChildrenOnly: { - type: Boolean, - default: true - }, - assignable: { - type: Boolean, - default: false - }, - withCourseAssign: { - type: Boolean, - default: false - }, - semester: { - type: String, - default: '' - }, - semClass: { - type: Number, - default: 0 - }, - showStructureAsNavigation: { - type: Boolean, - default: false - } - }, - data() { - return { - currentNode: this.node, - isLoading: false, - isLoaded: false, - children: [], - courses: [], - assistiveLive: '', - subLevelsCourses: 0, - thisLevelCourses: 0, - showingAllCourses: false - } - }, - methods: { - openNode(node, pushState = true) { - this.currentNode = node; - this.$emit('change-current-node', node); - - if (this.withChildren) { - this.getNodeChildren(node, this.visibleChildrenOnly).then(response => { - this.children = response.data.data; - }); - } - - this.getNodeCourseInfo(node, this.semester, this.semClass) - .then(response => { - this.thisLevelCourses = response?.data.courses; - this.subLevelsCourses = response?.data.allCourses; - }); - - if (this.withCourses) { - - this.getNodeCourses(node, this.semester, this.semClass, '', false) - .then(response => { - this.courses = response.data.data; - }); - } - - // Update browser history. - if (pushState) { - const nodeId = node.id; - const url = STUDIP.URLHelper.getURL('', {node_id: nodeId}); - window.history.pushState({nodeId}, '', url); - } - - // Update node_id for semester selector. - const semesterSelector = document.querySelector('#semester-selector-node-id'); - semesterSelector.value = node.id; - }, - dropChild() { - this.updateSorting(this.currentNode.id, this.children); - }, - keyHandler(e, index) { - switch (e.keyCode) { - case 38: // up - e.preventDefault(); - this.decreasePosition(index); - this.$nextTick(() => { - this.$refs['draghandle-' + (index - 1)][0].focus(); - this.assistiveLive = this.$gettextInterpolate( - this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'), - { pos: index, listLength: this.children.length } - ); - }); - break; - case 40: // down - e.preventDefault(); - this.increasePosition(index); - this.$nextTick(function () { - this.$refs['draghandle-' + (index + 1)][0].focus(); - this.assistiveLive = this.$gettextInterpolate( - this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'), - { pos: index + 2, listLength: this.children.length } - ); - }); - break; - } - }, - decreasePosition(index) { - if (index > 0) { - const temp = this.children[index - 1]; - this.children[index - 1] = this.children[index]; - this.children[index] = temp; - this.updateSorting(this.currentNode.id, this.children); - } - }, - increasePosition(index) { - if (index < this.children.length) { - const temp = this.children[index + 1]; - this.children[index + 1] = this.children[index]; - this.children[index] = temp; - this.updateSorting(this.currentNode.id, this.children); - } - }, - showAllCourses(state) { - this.getNodeCourses(this.currentNode, this.semester, this.semClass, '', state) - .then(courses => { - this.courses = courses.data.data; - this.showingAllCourses = state; - }); - } - }, - mounted() { - if (this.withChildren) { - this.getNodeChildren(this.node, this.visibleChildrenOnly).then(response => { - this.children = response.data.data; - }); - } - - this.getNodeCourseInfo(this.currentNode, this.semester, this.semClass) - .then(response => { - this.thisLevelCourses = response?.data.courses; - this.subLevelsCourses = response?.data.allCourses; - }); - - if (this.withCourses) { - this.getNodeCourses(this.currentNode, this.semester, this.semClass) - .then(courses => { - this.courses = courses.data.data; - }); - } - - this.globalOn('open-tree-node', node => { - STUDIP.eventBus.emit('cancel-search'); - this.openNode(node); - }); - - this.globalOn('load-tree-node', id => { - STUDIP.eventBus.emit('cancel-search'); - this.getNode(id).then(response => { - this.openNode(response.data.data); - }); - }); - - this.globalOn('sort-tree-children', data => { - if (this.currentNode.id === data.parent) { - this.children = data.children; - } - }); - - window.addEventListener('popstate', (event) => { - if (event.state) { - if ('nodeId' in event.state) { - this.getNode(event.state.nodeId).then(response => { - this.openNode(response.data.data, false); - }); - } - } else { - this.openNode(this.node, false); - } - }); - - // Add current node to semester selector widget. - this.$nextTick(() => { - const semesterForm = document.querySelector('#semester-selector .sidebar-widget-content form'); - const nodeField = document.createElement('input'); - nodeField.id = 'semester-selector-node-id'; - nodeField.type = 'hidden'; - nodeField.name = 'node_id'; - nodeField.value = this.node.id; - semesterForm.appendChild(nodeField); - }); - }, - beforeDestroy() { - STUDIP.eventBus.off('open-tree-node'); - STUDIP.eventBus.off('load-tree-node'); - STUDIP.eventBus.off('sort-tree-children'); - } -} -</script> diff --git a/resources/vue/components/tree/TreeBreadcrumb.vue b/resources/vue/components/tree/TreeBreadcrumb.vue deleted file mode 100644 index 33b04b3c0a21ed310863957f07b177ef0dd3c06f..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/TreeBreadcrumb.vue +++ /dev/null @@ -1,194 +0,0 @@ -<template> - <div class="studip-tree-breadcrumb contentbar"> - <nav class="contentbar-nav"></nav> - <div :class="{'contentbar-wrapper-left': true, 'with-navigation': showNavigation, 'editable': editable, - 'with-navigation-and-editable': showNavigation && editable}"> - <studip-icon :shape="icon" :size="24"></studip-icon> - <nav v-if="node.attributes.ancestors" class="studip-tree-breadcrumb-list contentbar-nav"> - <span v-for="(ancestor, index) in node.attributes.ancestors" - :key="ancestor.id"> - <a :href="nodeUrl(ancestor.classname + '_' + ancestor.id)" :ref="ancestor.id" - @click.prevent="openNode(ancestor.id, ancestor.classname)" tabindex="0" - :id="'tree-breadcrumb-' + ancestor.id" - :title="$gettextInterpolate($gettext('%{ node } öffnen'), { node: ancestor.name})"> - {{ ancestor.name }} - </a> - <template v-if="index !== node.attributes.ancestors.length - 1"> - / - </template> - </span> - </nav> - </div> - <div class="contentbar-wrapper-right"> - <div v-if="showNavigation" class="studip-tree-navigation-wrapper"> - <button type="button" tabindex="0" - :title="navigationOpen ? $gettext('Navigation schließen') : $gettext('Navigation öffnen')" - @click.prevent="toggleNavigation" :aria-expanded="navigationOpen"> - <studip-icon shape="table-of-contents" :size="24"></studip-icon> - </button> - <article class="studip-tree-navigation" v-if="navigationOpen"> - <header> - <h1>{{ $gettext('Inhalt') }}</h1> - <button type="button" tabindex="0" - @click.prevent="toggleNavigation"> - <studip-icon shape="decline" :size="24"></studip-icon> - </button> - </header> - <studip-tree-node :with-info="false" :node="rootNode" :active-node="node" :open-nodes="[ node.id ]" - :visible-children-only="visibleChildrenOnly"></studip-tree-node> - </article> - </div> - <button v-if="assignable" type="submit" class="assign-button" - :title="$gettext('Diesen Eintrag zuweisen')"> - <studip-icon shape="arr_2right" :size="20"></studip-icon> - </button> - <studip-action-menu v-if="editable" :items="actionMenuItems()" - @add-tree-node="addNode" @edit-tree-node="editNode" @delete-tree-node="deleteNode"/> - </div> - </div> -</template> - -<script> -import { TreeMixin } from '../../mixins/TreeMixin'; -import StudipIcon from '../StudipIcon.vue'; -import StudipTreeNode from './StudipTreeNode.vue'; -import axios from 'axios'; - -export default { - name: 'TreeBreadcrumb', - components: { StudipIcon, StudipTreeNode }, - mixins: [ TreeMixin ], - props: { - node: { - type: Object, - required: true - }, - icon: { - type: String, - required: true - }, - editable: { - type: Boolean, - default: false - }, - editUrl: { - type: String, - default: '' - }, - createUrl: { - type: String, - default: '' - }, - deleteUrl: { - type: String, - default: '' - }, - showNavigation: { - type: Boolean, - default: false - }, - assignable: { - type: Boolean, - default: false - }, - numChildren: { - type: Number, - default: 0 - }, - numCourses: { - type: Number, - default: 0 - }, - visibleChildrenOnly: { - type: Boolean, - default: true - } - }, - data() { - return { - navigationOpen: false, - rootNode: null - } - }, - methods: { - openNode(id, classname) { - STUDIP.eventBus.emit('load-tree-node', classname + '_' + id); - this.$refs[id][0].focus(); - }, - actionMenuItems() { - let entries = []; - - if (this.editable && this.createUrl !== '') { - entries.push({ - id: 'create', - label: this.$gettext('Neues Unterelement anlegen'), - icon: 'add', - emit: 'add-tree-node', - emitArguments: this.node - }); - } - - if (this.editable && this.node.attributes.id !== 'root') { - entries.push({ - id: 'edit', - label: this.$gettext('Dieses Element bearbeiten'), - icon: 'edit', - emit: 'edit-tree-node', - emitArguments: this.node - }); - entries.push({ - id: 'delete', - label: this.$gettext('Dieses Element löschen'), - icon: 'trash', - emit: 'delete-tree-node', - emitArguments: this.node - }); - } - - return entries; - }, - toggleNavigation() { - this.navigationOpen = !this.navigationOpen; - }, - addNode(parent) { - STUDIP.Dialog.fromURL(this.createUrl + '/' + parent.id, { data: { from: this.nodeUrl(parent.id) }}); - }, - editNode(node) { - STUDIP.Dialog.fromURL(this.editUrl + '/' + node.id, { data: { from: this.nodeUrl(node.id) }}); - }, - deleteNode(node) { - let text = this.$gettext('Sind sie sicher, dass der Eintrag "%{ node }" gelöscht werden soll?'); - let context = { - node: node.attributes.name - }; - - if (this.numChildren > 0 && this.numCourses === 0) { - text = this.$gettext('Sind sie sicher, dass der Eintrag "%{ node }" gelöscht werden soll? Er hat %{ children } Unterelemente.'); - context.children = this.numChildren; - } else if (this.numChildren === 0 && this.numCourses > 0) { - text = this.$gettext('Sind sie sicher, dass der Eintrag "%{ node }" gelöscht werden soll? Er hat %{ courses } Veranstaltungszuordnungen.'); - context.courses = this.numCourses; - } else if (this.numChildren > 0 && this.numCourses > 0) { - text = this.$gettext('Sind sie sicher, dass der Eintrag "%{ node }" gelöscht werden soll? Er hat %{ children } Unterelemente und %{ courses } Veranstaltungszuordnungen.'); - context.children = this.numChildren; - context.courses = this.numCourses; - } - - STUDIP.Dialog.confirm( - this.$gettextInterpolate(text, context) - ).done(() => { - axios.post(this.deleteUrl + '/' + node.id).then(() => { - const parent = node.attributes.ancestors[node.attributes.ancestors.length - 2]; - window.location = this.nodeUrl(parent.classname + '_' + parent.id); - }); - }); - } - }, - mounted() { - const root = this.node.attributes.ancestors[0]; - this.getNode(root.classname + '_' + root.id).then(response => { - this.rootNode = response.data.data; - }); - } -} -</script> diff --git a/resources/vue/components/tree/TreeCourseDetails.vue b/resources/vue/components/tree/TreeCourseDetails.vue deleted file mode 100644 index 30f101b2413912d3020f27fb8419a2c52d81b342..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/TreeCourseDetails.vue +++ /dev/null @@ -1,51 +0,0 @@ -<template> - <div v-if="details" class="course-details"> - <div class="semester"> - ({{ details.semester }}) - </div> - <div class="admission-state" v-if="details.admissionstate"> - <studip-icon :shape="details.admissionstate.icon" :role="details.admissionstate.role" - :title="details.admissionstate.info"></studip-icon> - </div> - <ul class="course-lecturers"> - <li v-for="(lecturer, index) in details.lecturers" :key="index"> - <a :href="profileUrl(lecturer.username)" - :title="$gettextInterpolate($gettext('Zum Profil von %{ user }'), - { user: lecturer.name })"> - {{ lecturer.name }} - </a> - </li> - </ul> - <MountingPortal :mountTo="'#course-dates-' + course" :append="true"> - <span v-html="details.dates"></span> - </MountingPortal> - </div> -</template> - -<script> -import axios from 'axios'; -import { TreeMixin } from '../../mixins/TreeMixin'; - -export default { - name: 'TreeCourseDetails', - mixins: [ TreeMixin ], - props: { - course: { - type: String, - required: true - } - }, - data() { - return { - details: null - } - }, - mounted() { - axios.get( - STUDIP.URLHelper.getURL('jsonapi.php/v1/tree-node/course/details/' + this.course) - ).then(response => { - this.details = response.data; - }); - } -} -</script> diff --git a/resources/vue/components/tree/TreeExportWidget.vue b/resources/vue/components/tree/TreeExportWidget.vue deleted file mode 100644 index 62b73b0378cde62eb48dcb80d5f4c2ee9db336b0..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/TreeExportWidget.vue +++ /dev/null @@ -1,50 +0,0 @@ -<template> - <sidebar-widget v-if="exportData.length > 0" id="export-widget" class="sidebar-export" :title="$gettext('Export')"> - <template #content> - <form class="sidebar-export"> - <studip-icon shape="export" :size="16"></studip-icon> - <a :href="url" :title="title" @click.prevent="createExport()">{{ title }}</a> - </form> - </template> - </sidebar-widget> -</template> - -<script> -import axios from 'axios'; -import SidebarWidget from '../SidebarWidget.vue'; -import StudipIcon from '../StudipIcon.vue'; - -export default { - name: 'TreeExportWidget', - components: { - SidebarWidget, StudipIcon - }, - props: { - url: { - type: String, - required: true - }, - title: { - type: String, - required: true - }, - exportData: { - type: Array, - default: () => [] - } - }, - methods: { - createExport() { - const fd = new FormData(); - fd.append('courses', this.exportData.map(entry => entry.id)); - axios.post( - this.url, - fd, - { headers: { 'Content-Type': 'multipart/form-data' }} - ).then(response => { - window.open(response.data); - }); - } - } -} -</script> diff --git a/resources/vue/components/tree/TreeNodeCourseInfo.vue b/resources/vue/components/tree/TreeNodeCourseInfo.vue deleted file mode 100644 index db31d148c3f144ec4b221f91fd77667f5cf6f250..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/TreeNodeCourseInfo.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> - <div class="studip-tree-child-description"> - <template v-if="showingAllCourses"> - <div v-translate="{ count: courseCount }" :translate-n="courseCount" - translate-plural="<strong>%{count}</strong> Veranstaltungen auf dieser Ebene."> - <strong>Eine</strong> Veranstaltung auf dieser Ebene. - </div> - </template> - <div v-else v-translate="{ count: courseCount }" :translate-n="courseCount" - translate-plural="<strong>%{count}</strong> Veranstaltungen auf dieser Ebene."> - <strong>Eine</strong> Veranstaltung auf dieser Ebene. - </div> - <template v-if="!showingAllCourses"> - <div v-translate="{ count: allCourseCount }" :translate-n="allCourseCount" - translate-plural="<strong>%{count}</strong> Veranstaltungen auf allen Unterebenen."> - <strong>Eine</strong> Veranstaltung auf allen Unterebenen. - </div> - </template> - <div v-else v-translate="{ count: allCourseCount }" :translate-n="allCourseCount" - translate-plural="<strong>%{count}</strong> Veranstaltungen auf allen Unterebenen."> - <strong>Eine</strong> Veranstaltung auf allen Unterebenen. - </div> - </div> -</template> - -<script> -import { TreeMixin } from '../../mixins/TreeMixin'; - -export default { - name: 'TreeNodeCourseInfo', - mixins: [ TreeMixin ], - props: { - node: { - type: Object, - required: true - }, - semester: { - type: String, - default: 'all' - }, - semClass: { - type: Number, - default: 0 - } - }, - data() { - return { - courseCount: 0, - allCourseCount: 0, - showingAllCourses: false - } - }, - methods: { - showAllCourses(state) { - this.showingAllCourses = state; - this.$emit('showAllCourses', state); - } - }, - mounted() { - this.getNodeCourseInfo(this.node, this.semester, this.semClass) - .then(info => { - this.courseCount = info?.data.courses; - this.allCourseCount = info?.data.allCourses; - }); - }, - watch: { - node(newNode) { - this.getNodeCourseInfo(newNode, this.semester, this.semClass) - .then(info => { - this.courseCount = info?.data.courses; - this.allCourseCount = info?.data.allCourses; - }); - } - } -} -</script> diff --git a/resources/vue/components/tree/TreeNodeCoursePath.vue b/resources/vue/components/tree/TreeNodeCoursePath.vue deleted file mode 100644 index 26ab88eb4d42e30cc0d6f1d1faa6b19e53ab9a7b..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/TreeNodeCoursePath.vue +++ /dev/null @@ -1,54 +0,0 @@ -<template> - <div> - <studip-icon shape="info-circle" @click="togglePathInfo"></studip-icon> - <ul v-if="showPaths" class="studip-tree-course-path"> - <li v-for="(path, pindex) in paths" :key="pindex"> - <button @click.prevent="openNode(path[path.length - 1].id)"> - <template v-for="(segment) in path"> - / {{ segment.name }} - </template> - </button> - </li> - </ul> - </div> -</template> -<script> -import axios from 'axios'; -import StudipIcon from '../StudipIcon.vue'; - -export default { - name: 'TreeNodeCoursePath', - components: { StudipIcon }, - props: { - courseId: { - type: String, - required: true - }, - nodeClass: { - type: String, - required: true - } - }, - data() { - return { - paths: [], - showPaths: false - } - }, - methods: { - openNode(id) { - STUDIP.eventBus.emit('load-tree-node', this.nodeClass + '_' + id); - }, - togglePathInfo() { - this.showPaths = !this.showPaths; - } - }, - mounted() { - axios.get( - STUDIP.URLHelper.getURL('jsonapi.php/v1/tree-node/course/pathinfo/' + this.nodeClass + '/' + this.courseId) - ).then(response => { - this.paths = response.data; - }); - } -} -</script> diff --git a/resources/vue/components/tree/TreeNodeTile.vue b/resources/vue/components/tree/TreeNodeTile.vue deleted file mode 100644 index cbb0679eaeffeedb844b823abf4a2a5c0293e955..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/TreeNodeTile.vue +++ /dev/null @@ -1,46 +0,0 @@ -<template> - <a :href="url" @click.prevent="openNode" :title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'), - { node: node.attributes.name })"> - <p class="studip-tree-child-title"> - {{ node.attributes.name }} - </p> - - <tree-node-course-info :node="node" :semester="semester" - :sem-class="semClass"></tree-node-course-info> - </a> -</template> - -<script> -import TreeNodeCourseInfo from './TreeNodeCourseInfo.vue'; -export default { - name: 'TreeNodeTile', - components: { TreeNodeCourseInfo }, - props: { - node: { - type: Object, - required: true - }, - url: { - type: String, - required: true - }, - withChildren: { - type: Boolean, - default: true - }, - semester: { - type: String, - default: 'all' - }, - semClass: { - type: Number, - default: 0 - } - }, - methods: { - openNode() { - STUDIP.eventBus.emit('open-tree-node', this.node); - } - } -} -</script> diff --git a/resources/vue/components/tree/TreeSearchResult.vue b/resources/vue/components/tree/TreeSearchResult.vue deleted file mode 100644 index 19bc6b927a4c56e57d76f6b6a0c59add4642bbab..0000000000000000000000000000000000000000 --- a/resources/vue/components/tree/TreeSearchResult.vue +++ /dev/null @@ -1,85 +0,0 @@ -<template> - <div v-if="isLoading"> - <studip-progress-indicator></studip-progress-indicator> - </div> - <article v-else class="studip-tree-table"> - <table v-if="courses.length > 0" class="default studip-tree-table"> - <caption> - <studip-icon shape="search" :size="20"></studip-icon> - {{ $gettextInterpolate($ngettext('Ein Eintrag für den Begriff "%{searchterm}" gefunden', - '%{count} Einträge für den Begriff "%{searchterm}" gefunden', courses.length), - { count: courses.length, searchterm: searchConfig.searchterm}) }} - </caption> - <colgroup> - <col style="width: 30px"> - <col> - <col> - </colgroup> - <thead> - <tr> - <th></th> - <th>{{ $gettext('Name') }}</th> - <th>{{ $gettext('Information') }}</th> - </tr> - </thead> - <tbody> - <tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course"> - <td> - <studip-icon shape="seminar" :size="26"></studip-icon> - </td> - <td> - <a :href="courseUrl(course.id)" - :title="$gettextInterpolate($gettext('Zur Veranstaltung %{name}'), {name: + course.attributes.title})"> - <template v-if="course.attributes['course-number']"> - {{ course.attributes['course-number'] }} - </template> - {{ course.attributes.title }} - <div :id="'course-dates-' + course.id" class="course-dates"></div> - </a> - <tree-node-course-path :node-class="searchConfig.classname" - :course-id="course.id"></tree-node-course-path> - </td> - <td> - <tree-course-details :course="course.id"></tree-course-details> - </td> - </tr> - </tbody> - </table> - </article> -</template> - -<script> -import { TreeMixin } from '../../mixins/TreeMixin'; -import StudipProgressIndicator from '../StudipProgressIndicator.vue'; -import StudipIcon from '../StudipIcon.vue'; -import TreeNodeCoursePath from './TreeNodeCoursePath.vue'; -import TreeCourseDetails from './TreeCourseDetails.vue'; - -export default { - name: 'TreeSearchResult', - components: { StudipIcon, StudipProgressIndicator, TreeNodeCoursePath, TreeCourseDetails }, - mixins: [ TreeMixin ], - props: { - searchConfig: { - type: Object, - required: true - } - }, - data() { - return { - node: null, - isLoading: false, - isLoaded: false, - courses: [] - } - }, - mounted() { - this.getNode(this.searchConfig.classname + '_root').then(response => { - this.getNodeCourses(response.data.data, this.searchConfig.semester,0, this.searchConfig.searchterm, true) - .then(courses => { - this.courses = courses.data.data; - }); - }); - } -} -</script> diff --git a/resources/vue/mixins/TreeMixin.js b/resources/vue/mixins/TreeMixin.js deleted file mode 100644 index 9a0292ecf337bbe592bbea793b121336228db64f..0000000000000000000000000000000000000000 --- a/resources/vue/mixins/TreeMixin.js +++ /dev/null @@ -1,108 +0,0 @@ -import axios from 'axios'; - -export const TreeMixin = { - data() { - return { - showProgressIndicatorTimeout: 500 - }; - }, - methods: { - async getNode(id) { - return axios.get(STUDIP.URLHelper.getURL('jsonapi.php/v1/tree-node/' + id)); - }, - async getNodeChildren(node, visibleOnly = true) { - let parameters = {}; - - if (visibleOnly) { - parameters['filter[visible]'] = true; - } - - return axios.get( - STUDIP.URLHelper.getURL('jsonapi.php/v1/tree-node/' + node.id + '/children'), - { params: parameters } - ); - }, - async getNodeCourses(node, semesterId = 'all', semClass = 0, searchterm = '', recursive = false, ids = []) { - let parameters = {}; - - if (semesterId !== 'all' && semesterId !== '0') { - parameters['filter[semester]'] = semesterId; - } - - if (searchterm !== '') { - parameters['filter[q]'] = searchterm; - } - - if (semClass !== 0) { - parameters['filter[semclass]'] = semClass; - } - - if (recursive) { - parameters['filter[recursive]'] = true; - } - - if (ids.length > 0) { - parameters['filter[ids]'] = ids; - } - - return axios.get( - STUDIP.URLHelper.getURL('jsonapi.php/v1/tree-node/' + node.id + '/courses'), - {params: parameters} - ); - }, - async getNodeCourseInfo(node, semesterId, semClass = 0) { - let parameters = {}; - - if (semesterId !== 'all' && semesterId !== '0') { - parameters['filter[semester]'] = semesterId; - } - - if (semClass !== 0) { - parameters['filter[semclass]'] = semClass; - } - - return axios.get( - STUDIP.URLHelper.getURL('jsonapi.php/v1/tree-node/' + node.id + '/courseinfo'), - { params: parameters } - ); - }, - nodeUrl(node_id, semester = null ) { - return STUDIP.URLHelper.getURL('', { node_id, semester }) - }, - courseUrl(courseId) { - return STUDIP.URLHelper.getURL('dispatch.php/course/details', { cid: courseId }) - }, - profileUrl(username) { - return STUDIP.URLHelper.getURL('dispatch.php/profile', { username }) - }, - exportUrl() { - return STUDIP.URLHelper.getURL('dispatch.php/tree/export_csv'); - }, - editNode(editUrl, id) { - STUDIP.Dialog.fromURL( - editUrl + '/' + id, - { - size: 'medium' - } - ); - }, - updateSorting(parentId, children) { - let data = {}; - - let position = 0; - for (const child of children) { - data[child.attributes.id] = position; - position++; - } - - const fd = new FormData(); - fd.append('sorting', JSON.stringify(data)); - axios.post( - STUDIP.URLHelper.getURL('dispatch.php/admin/tree/sort/' + parentId), - fd, - { headers: { 'Content-Type': 'multipart/form-data' }} - ); - STUDIP.Vue.emit('sort-tree-children', { parent: parentId, children: children }); - } - } -}