aboutsummaryrefslogtreecommitdiff
path: root/lib
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
parentb7f0f8bcaad8fefd96fd3e6316377eda53929ad3 (diff)
Resolve "Neuentwicklung Verzeichnisstrukturen"
Closes #1664, #2693, and #2692 Merge request studip/studip!1081
Diffstat (limited to 'lib')
-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
-rw-r--r--lib/models/RangeTreeNode.php263
-rw-r--r--lib/models/StudipStudyArea.class.php218
-rw-r--r--lib/navigation/AdminNavigation.php4
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')) {