diff options
| author | Thomas Hackl <hackl@data-quest.de> | 2023-06-28 13:27:46 +0000 |
|---|---|---|
| committer | Thomas Hackl <hackl@data-quest.de> | 2023-06-28 13:27:46 +0000 |
| commit | 559ab723fabd4d10f26e7df631808e4cb8d91c9b (patch) | |
| tree | 91ef8cf94eba86973baf3efabca1cdbb8bf6826b /lib | |
| parent | b7f0f8bcaad8fefd96fd3e6316377eda53929ad3 (diff) | |
Resolve "Neuentwicklung Verzeichnisstrukturen"
Closes #1664, #2693, and #2692
Merge request studip/studip!1081
Diffstat (limited to 'lib')
24 files changed, 1471 insertions, 43 deletions
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index f60c4dd..52d4402 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -132,6 +132,7 @@ class RouteMap $this->addAuthenticatedMessagesRoutes($group); $this->addAuthenticatedNewsRoutes($group); $this->addAuthenticatedStudyAreasRoutes($group); + $this->addAuthenticatedTreeRoutes($group); $this->addAuthenticatedWikiRoutes($group); } @@ -281,6 +282,17 @@ 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 new file mode 100644 index 0000000..8773c0a --- /dev/null +++ b/lib/classes/JsonApi/Routes/RangeTree/ChildrenOfRangeTreeNode.php @@ -0,0 +1,39 @@ +<?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 new file mode 100644 index 0000000..980cac1 --- /dev/null +++ b/lib/classes/JsonApi/Routes/RangeTree/CoursesOfRangeTreeNode.php @@ -0,0 +1,51 @@ +<?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 new file mode 100644 index 0000000..6ecf52a --- /dev/null +++ b/lib/classes/JsonApi/Routes/RangeTree/InstituteOfRangeTreeNode.php @@ -0,0 +1,30 @@ +<?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 new file mode 100644 index 0000000..01a40d3 --- /dev/null +++ b/lib/classes/JsonApi/Routes/RangeTree/ParentOfRangeTreeNode.php @@ -0,0 +1,32 @@ +<?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 new file mode 100644 index 0000000..c706c48 --- /dev/null +++ b/lib/classes/JsonApi/Routes/RangeTree/RangeTreeIndex.php @@ -0,0 +1,53 @@ +<?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 new file mode 100644 index 0000000..c9f52ed --- /dev/null +++ b/lib/classes/JsonApi/Routes/RangeTree/RangeTreeShow.php @@ -0,0 +1,32 @@ +<?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 628abc8..9ce5728 100644 --- a/lib/classes/JsonApi/Routes/StudyAreas/StudyAreasShow.php +++ b/lib/classes/JsonApi/Routes/StudyAreas/StudyAreasShow.php @@ -22,7 +22,8 @@ class StudyAreasShow extends JsonApiController */ public function __invoke(Request $request, Response $response, $args) { - if (!$studyArea = \StudipStudyArea::find($args['id'])) { + $studyArea = \StudipStudyArea::find($args['id']); + if (!$studyArea && $args['id'] !== 'root') { throw new RecordNotFoundException(); } diff --git a/lib/classes/JsonApi/Routes/Tree/ChildrenOfTreeNode.php b/lib/classes/JsonApi/Routes/Tree/ChildrenOfTreeNode.php new file mode 100644 index 0000000..6419d03 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Tree/ChildrenOfTreeNode.php @@ -0,0 +1,50 @@ +<?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 new file mode 100644 index 0000000..2835931 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Tree/CourseInfoOfTreeNode.php @@ -0,0 +1,83 @@ +<?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 new file mode 100644 index 0000000..623e619 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Tree/CoursesOfTreeNode.php @@ -0,0 +1,112 @@ +<?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 new file mode 100644 index 0000000..cef1077 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Tree/DetailsOfTreeNodeCourse.php @@ -0,0 +1,79 @@ +<?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 new file mode 100644 index 0000000..282b7f9 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Tree/PathinfoOfTreeNodeCourse.php @@ -0,0 +1,34 @@ +<?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 new file mode 100644 index 0000000..1eaa797 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Tree/TreeShow.php @@ -0,0 +1,40 @@ +<?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 19d21f5..f4ac207 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -43,7 +43,7 @@ class SchemaMap \JsonApi\Models\StudipProperty::class => Schemas\StudipProperty::class, \StudipComment::class => Schemas\StudipComment::class, \StudipNews::class => Schemas\StudipNews::class, - \StudipStudyArea::class => Schemas\StudyArea::class, + \StudipTreeNode::class => Schemas\TreeNode::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 f077d83..e9779c7 100644 --- a/lib/classes/JsonApi/Schemas/StudyArea.php +++ b/lib/classes/JsonApi/Schemas/StudyArea.php @@ -1,5 +1,4 @@ <?php - namespace JsonApi\Schemas; use Neomerx\JsonApi\Contracts\Schema\ContextInterface; @@ -25,6 +24,9 @@ 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 new file mode 100644 index 0000000..90e6846 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/TreeNode.php @@ -0,0 +1,151 @@ +<?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 16f13f1..65d2856 100644 --- a/lib/classes/SemBrowse.class.php +++ b/lib/classes/SemBrowse.class.php @@ -1185,19 +1185,13 @@ class SemBrowse { return new Navigation(_('Vorlesungsverzeichnis'), URLHelper::getURL('dispatch.php/search/courses', [ - 'level' => 'vv', - 'cmd' => 'qs', - 'sset' => '0', - 'option' => '' + 'type' => 'semtree' ], true)); case 'rangetree': return new Navigation(_('Einrichtungsverzeichnis'), URLHelper::getURL('dispatch.php/search/courses', [ - 'level' => 'ev', - 'cmd' => 'qs', - 'sset' => '0', - 'option' => '' + 'type' => 'rangetree' ], true)); case 'module': return new MVVSearchNavigation(_('Modulverzeichnis'), diff --git a/lib/classes/StudipTreeNode.php b/lib/classes/StudipTreeNode.php new file mode 100644 index 0000000..a1d7258 --- /dev/null +++ b/lib/classes/StudipTreeNode.php @@ -0,0 +1,114 @@ +<?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 2531a2e..f4be547 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/checkbox_input'); + $template = $GLOBALS['template_factory']->open('forms/quicksearch_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 new file mode 100644 index 0000000..2fecf60 --- /dev/null +++ b/lib/classes/searchtypes/TreeSearch.class.php @@ -0,0 +1,96 @@ +<?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 new file mode 100644 index 0000000..d1c1823 --- /dev/null +++ b/lib/models/RangeTreeNode.php @@ -0,0 +1,263 @@ +<?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 database column + * @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 studip_object database column + * @property string studip_object_id database column + */ +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 6650526..49f008a 100644 --- a/lib/models/StudipStudyArea.class.php +++ b/lib/models/StudipStudyArea.class.php @@ -29,7 +29,7 @@ * @property SimpleORMapCollection courses has_and_belongs_to_many Course */ -class StudipStudyArea extends SimpleORMap +class StudipStudyArea extends SimpleORMap implements StudipTreeNode { /** * This constant represents the key of the root area. @@ -50,10 +50,6 @@ class StudipStudyArea extends SimpleORMap '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', @@ -124,11 +120,8 @@ class StudipStudyArea extends SimpleORMap /** * Get the display name of this study area. */ - public function getName() + public function getName(): string { - if ($this->studip_object_id) { - return $this->institute ? $this->institute->name : _('Unbekannte Einrichtung'); - } return $this->content['name']; } @@ -284,26 +277,6 @@ class StudipStudyArea extends SimpleORMap /** - * 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. */ public function getChildren() @@ -450,4 +423,191 @@ class StudipStudyArea extends SimpleORMap 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 1b28e24..6c9d7e3 100644 --- a/lib/navigation/AdminNavigation.php +++ b/lib/navigation/AdminNavigation.php @@ -91,11 +91,11 @@ class AdminNavigation extends Navigation $navigation = new Navigation(_('Standort')); 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')); + $navigation->addSubNavigation('range_tree', new Navigation(_('Einrichtungshierarchie'), 'dispatch.php/admin/tree/rangetree')); } 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')); + $navigation->addSubNavigation('sem_tree', new Navigation(_('Veranstaltungshierarchie'), 'dispatch.php/admin/tree/semtree')); } if ($perm->have_perm(Config::get()->LOCK_RULE_ADMIN_PERM ? Config::get()->LOCK_RULE_ADMIN_PERM : 'admin')) { |
