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/classes | |
| parent | b7f0f8bcaad8fefd96fd3e6316377eda53929ad3 (diff) | |
Resolve "Neuentwicklung Verzeichnisstrukturen"
Closes #1664, #2693, and #2692
Merge request studip/studip!1081
Diffstat (limited to 'lib/classes')
21 files changed, 1017 insertions, 12 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__); + } +} |
