aboutsummaryrefslogtreecommitdiff
path: root/lib/classes
diff options
context:
space:
mode:
authorThomas Hackl <hackl@data-quest.de>2023-06-28 13:27:46 +0000
committerThomas Hackl <hackl@data-quest.de>2023-06-28 13:27:46 +0000
commit559ab723fabd4d10f26e7df631808e4cb8d91c9b (patch)
tree91ef8cf94eba86973baf3efabca1cdbb8bf6826b /lib/classes
parentb7f0f8bcaad8fefd96fd3e6316377eda53929ad3 (diff)
Resolve "Neuentwicklung Verzeichnisstrukturen"
Closes #1664, #2693, and #2692 Merge request studip/studip!1081
Diffstat (limited to 'lib/classes')
-rw-r--r--lib/classes/JsonApi/RouteMap.php12
-rw-r--r--lib/classes/JsonApi/Routes/RangeTree/ChildrenOfRangeTreeNode.php39
-rw-r--r--lib/classes/JsonApi/Routes/RangeTree/CoursesOfRangeTreeNode.php51
-rw-r--r--lib/classes/JsonApi/Routes/RangeTree/InstituteOfRangeTreeNode.php30
-rw-r--r--lib/classes/JsonApi/Routes/RangeTree/ParentOfRangeTreeNode.php32
-rw-r--r--lib/classes/JsonApi/Routes/RangeTree/RangeTreeIndex.php53
-rw-r--r--lib/classes/JsonApi/Routes/RangeTree/RangeTreeShow.php32
-rw-r--r--lib/classes/JsonApi/Routes/StudyAreas/StudyAreasShow.php3
-rw-r--r--lib/classes/JsonApi/Routes/Tree/ChildrenOfTreeNode.php50
-rw-r--r--lib/classes/JsonApi/Routes/Tree/CourseInfoOfTreeNode.php83
-rw-r--r--lib/classes/JsonApi/Routes/Tree/CoursesOfTreeNode.php112
-rw-r--r--lib/classes/JsonApi/Routes/Tree/DetailsOfTreeNodeCourse.php79
-rw-r--r--lib/classes/JsonApi/Routes/Tree/PathinfoOfTreeNodeCourse.php34
-rw-r--r--lib/classes/JsonApi/Routes/Tree/TreeShow.php40
-rw-r--r--lib/classes/JsonApi/SchemaMap.php2
-rw-r--r--lib/classes/JsonApi/Schemas/StudyArea.php4
-rw-r--r--lib/classes/JsonApi/Schemas/TreeNode.php151
-rw-r--r--lib/classes/SemBrowse.class.php10
-rw-r--r--lib/classes/StudipTreeNode.php114
-rw-r--r--lib/classes/forms/QuicksearchInput.php2
-rw-r--r--lib/classes/searchtypes/TreeSearch.class.php96
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__);
+ }
+}