aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Thienel <thienel@data-quest.de>2024-12-02 15:57:37 +0000
committerPeter Thienel <thienel@data-quest.de>2024-12-02 15:57:37 +0000
commit2e9f63211551b23ca064db6f1695e7a92ea2e889 (patch)
tree05fb1fa58d1413f2233a2f5da0f05baa15e74107
parent83b735caa7c01c83511c2981eecea4480dcb1705 (diff)
Resolve "JsonApi-Routen für MVV zur Ausgabe eines externen modularisierten Veranstaltungsverzeichnisses"
Closes #4255 Merge request studip/studip!3089
-rw-r--r--lib/classes/JsonApi/RouteMap.php25
-rw-r--r--lib/classes/JsonApi/Routes/Courses/CoursesByModuleComponentsIndex.php158
-rw-r--r--lib/classes/JsonApi/Routes/Courses/CoursesIndex.php55
-rw-r--r--lib/classes/JsonApi/Routes/Institutes/InstitutesIndex.php1
-rw-r--r--lib/classes/JsonApi/Routes/Institutes/InstitutesShow.php1
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/Authority.php68
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/ComponentVersionsDeep.php258
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/ComponentVersionsShow.php29
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/ComponentsByCoursesOfStudyIndex.php39
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/CourseOfStudyComponentsShow.php31
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyIndex.php121
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyShow.php31
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/DegreesByCoursesOfStudyShow.php26
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/DegreesIndex.php26
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/DegreesShow.php26
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/ModuleComponentsByModuleIndex.php35
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/ModuleComponentsShow.php29
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/ModulesIndex.php111
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/ModulesShow.php32
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/SubjectsByCourseOfStudyComponentsShow.php29
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/SubjectsIndex.php26
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/SubjectsShow.php29
-rw-r--r--lib/classes/JsonApi/Routes/Mvv/VersionsByCourseOfStudyComponentsIndex.php37
-rw-r--r--lib/classes/JsonApi/SchemaMap.php9
-rw-r--r--lib/classes/JsonApi/Schemas/ComponentSection.php52
-rw-r--r--lib/classes/JsonApi/Schemas/ComponentVersion.php94
-rw-r--r--lib/classes/JsonApi/Schemas/CourseOfStudy.php151
-rw-r--r--lib/classes/JsonApi/Schemas/CourseOfStudyComponent.php73
-rw-r--r--lib/classes/JsonApi/Schemas/Institute.php32
-rw-r--r--lib/classes/JsonApi/Schemas/Module.php171
-rw-r--r--lib/classes/JsonApi/Schemas/ModuleComponent.php70
-rw-r--r--lib/classes/JsonApi/Schemas/ModuleInstitute.php67
-rw-r--r--lib/classes/JsonApi/Schemas/Subject.php61
-rw-r--r--lib/models/Institute.php5
-rw-r--r--lib/models/Modulteil.php4
-rw-r--r--lib/models/StgteilVersion.php8
-rw-r--r--lib/models/Studiengang.php6
-rw-r--r--tests/jsonapi/_bootstrap.php1
38 files changed, 2020 insertions, 7 deletions
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index c7a6c89..a7048bd 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -127,6 +127,7 @@ class RouteMap
}
$this->addAuthenticatedAvatarRoutes($group);
+ $this->addAuthenticatedMvvRoutes($group);
$this->addAuthenticatedEventsRoutes($group);
$this->addAuthenticatedFeedbackRoutes($group);
$this->addAuthenticatedFilesRoutes($group);
@@ -393,6 +394,8 @@ class RouteMap
$group->get('/sem-classes/{id}/sem-types', Routes\Courses\SemTypesBySemClassIndex::class);
$group->get('/sem-types', Routes\Courses\SemTypesIndex::class);
$group->get('/sem-types/{id}', Routes\Courses\SemTypesShow::class);
+
+ $group->get('/module-components/{id}/courses', Routes\Courses\CoursesByModuleComponentsIndex::class);
}
private function addAuthenticatedCoursewareRoutes(RouteCollectorProxy $group): void
@@ -697,6 +700,28 @@ class RouteMap
$group->get('/user-filter-fields/{id}', Routes\UserFilters\UserFilterFieldsShow::class);
}
+ private function addAuthenticatedMvvRoutes(RouteCollectorProxy $group): void
+ {
+ $group->get('/courses-of-study', Routes\Mvv\CoursesOfStudyIndex::class);
+ $group->get('/courses-of-study/{id}', Routes\Mvv\CoursesOfStudyShow::class);
+ $group->get('/courses-of-study/{id}/components', Routes\Mvv\ComponentsByCoursesOfStudyIndex::class);
+ $group->get('/course-of-study-components/{id}/versions', Routes\Mvv\VersionsByCourseOfStudyComponentsIndex::class);
+ $group->get('/course-of-study-components/{id}/subject', Routes\Mvv\SubjectsByCourseOfStudyComponentsShow::class);
+ $group->get('/courses-of-study/{id}/degree', Routes\Mvv\DegreesByCoursesOfStudyShow::class);
+ $group->get('/degrees', Routes\Mvv\DegreesIndex::class);
+ $group->get('/degrees/{id}', Routes\Mvv\DegreesShow::class);
+ $group->get('/subjects',Routes\Mvv\SubjectsIndex::class);
+ $group->get('/subjects/{id}',Routes\Mvv\SubjectsShow::class);
+ $group->get('/component-versions/{id}', Routes\Mvv\ComponentVersionsShow::class);
+ $group->get('/modules', Routes\Mvv\ModulesIndex::class);
+ $group->get('/modules/{id}', Routes\Mvv\ModulesShow::class);
+ $group->get('/modules/{id}/module-components', Routes\Mvv\ModuleComponentsByModuleIndex::class);
+ $group->get('/module-components/{id}', Routes\Mvv\ModuleComponentsShow::class);
+ // not a JSON:API route
+ $group->get('/component-version-deep/{id}', Routes\Mvv\ComponentVersionsDeep::class);
+
+ }
+
private function addRelationship(RouteCollectorProxy $group, string $url, string $handler): void
{
$group->map(['GET', 'PATCH', 'POST', 'DELETE'], $url, $handler);
diff --git a/lib/classes/JsonApi/Routes/Courses/CoursesByModuleComponentsIndex.php b/lib/classes/JsonApi/Routes/Courses/CoursesByModuleComponentsIndex.php
new file mode 100644
index 0000000..e7eaee9
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courses/CoursesByModuleComponentsIndex.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace JsonApi\Routes\Courses;
+
+use Modulteil;
+use Course;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Semester;
+
+class CoursesByModuleComponentsIndex 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'];
+
+ protected $allowedFilteringParameters = ['semester', 'df'];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, ?array $args): Response
+ {
+ $component = Modulteil::find($args['id']);
+ if (!$component) {
+ throw new RecordNotFoundException();
+ }
+
+ $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+
+ $error = $this->validateFilters($filtering);
+ if ($error) {
+ throw new BadRequestException($error);
+ }
+
+ $courses = $this->findCoursesByComponent(
+ $component,
+ $filtering
+ );
+ [$offset, $limit] = $this->getOffsetAndLimit();
+
+ return $this->getPaginatedContentResponse(
+ array_slice($courses, $offset, $limit),
+ count($courses)
+ );
+ }
+
+ private function validateFilters(array $filtering): ?string
+ {
+ // semester
+ if (
+ isset($filtering['semester'])
+ && !Semester::exists($filtering['semester'])
+ ) {
+ return 'Invalid "semester".';
+ }
+
+ // data fields
+ if (isset($filtering['df']) && is_array($filtering['df'])) {
+ $accepted_dfs = $this->getAcceptedDataFields();
+ foreach (array_keys($filtering['df']) as $df) {
+ if (!in_array($df, $accepted_dfs)) {
+ return 'Invalid data field as filtering parameter.';
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get ids of accepted datafields for current user.
+ * Only simple types of bool, textline, selectbox and radio with global
+ * visibility for all users are accepted.
+ *
+ * @return array Accepted datafields
+ */
+ private function getAcceptedDataFields(): array
+ {
+ $data_fields = \DataField::findAndMapBySQL(
+ fn(\DataField $data_field) => $data_field->id,
+ "`object_type` = 'sem' AND `view_perms` = 'user'
+ AND `type` IN('bool', 'textline', 'selectbox', 'radio')"
+ );
+ return $data_fields;
+ }
+
+ private function getSemesterFilter(array $filtering): ?Semester
+ {
+ if (!isset($filtering['semester'])) {
+ return null;
+ }
+ return Semester::find($filtering['semester']);
+ }
+
+
+ /**
+ * Finds visible courses by given module component.
+ *
+ * @param Modulteil $component
+ * @param Semester|null $semester
+ *
+ * @return Course[] Visible courses assigned to module component
+ */
+ private function findCoursesByComponent(Modulteil $component, array $filtering): array
+ {
+ $course_ids = [];
+ foreach ($component->lvgruppen as $lvgruppe) {
+ $course_ids += $lvgruppe->courses->findBy('visible', '1')->pluck('id');
+ }
+ if (count($course_ids) === 0) {
+ return [];
+ }
+
+ if (isset($filtering['df']) && is_array($filtering['df'])) {
+ $df_course_ids = $course_ids;
+ foreach ($filtering['df'] as $id => $value) {
+ $df_course_ids = array_intersect($df_course_ids, \DatafieldEntryModel::findAndMapBySQL(
+ fn($df) => $df->range_id,
+ '`datafield_id` = ? AND `range_id` IN (?) AND `content` = ?',
+ [$id, $course_ids, $value]
+ ));
+ }
+
+ $course_ids = array_merge_recursive($df_course_ids);
+ }
+ $courses = Course::findMany(
+ $course_ids,
+ 'ORDER BY name'
+ );
+
+ $semester = $this->getSemesterFilter($filtering);
+ if ($semester) {
+ $courses = array_filter($courses, fn(\Course $course) => $course->isInSemester($semester));
+ }
+
+ return $courses;
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courses/CoursesIndex.php b/lib/classes/JsonApi/Routes/Courses/CoursesIndex.php
index d97cdc0..5c3e46e 100644
--- a/lib/classes/JsonApi/Routes/Courses/CoursesIndex.php
+++ b/lib/classes/JsonApi/Routes/Courses/CoursesIndex.php
@@ -10,7 +10,7 @@ use JsonApi\JsonApiController;
class CoursesIndex extends JsonApiController
{
- protected $allowedFilteringParameters = ['q', 'fields', 'semester', 'category', 'scope_choose', 'range_choose'];
+ protected $allowedFilteringParameters = ['q', 'fields', 'semester', 'category', 'scope_choose', 'range_choose', 'df'];
protected $allowedIncludePaths = [
'blubber-threads',
@@ -51,7 +51,7 @@ class CoursesIndex extends JsonApiController
list($offset, $limit) = $this->getOffsetAndLimit();
return $this->getPaginatedContentResponse(
- \Course::findMany(array_slice($courseIds, $offset, $limit)),
+ $this->getCourses(array_slice($courseIds, $offset, $limit)),
count($courseIds)
);
}
@@ -80,6 +80,16 @@ class CoursesIndex extends JsonApiController
return 'Invalid "semester".';
}
}
+
+ // data fields
+ if (isset($filtering['df']) && is_array($filtering['df'])) {
+ $accepted_dfs = $this->getAcceptedDataFields();
+ foreach (array_keys($filtering['df']) as $df) {
+ if (!in_array($df, $accepted_dfs)) {
+ return 'Invalid data field as filtering parameter.';
+ }
+ }
+ }
}
private function getContextFilters()
@@ -99,6 +109,47 @@ class CoursesIndex extends JsonApiController
return array_merge($defaults, $filtering);
}
+ private function getCourses(array $course_ids): array
+ {
+ $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+ if (isset($filtering['df']) && is_array($filtering['df'])) {
+ $df_where = [];
+ $params = [
+ $course_ids
+ ];
+ foreach ($filtering['df'] as $id => $value) {
+ $df_where[] = ' (`datafields_entries`.`datafield_id` = ? AND `datafields_entries`.`content` = ?) ';
+ $params[] = $id;
+ $params[] = $value;
+ }
+ return \Course::findBySQL("JOIN `datafields_entries`
+ ON `seminare`.`seminar_id` = `datafields_entries`.`range_id`
+ WHERE `seminare`.`seminar_id` IN (?)
+ AND " .
+ implode('AND', $df_where),
+ $params);
+ } else {
+ return \Course::findMany($course_ids);
+ }
+ }
+
+ /**
+ * Get ids of accepted datafields for current user.
+ * Only simple types of bool, textline, selectbox and radio with global
+ * visibility for all users are accepted.
+ *
+ * @return array
+ */
+ private function getAcceptedDataFields(): array
+ {
+ $data_fields = \DataField::findAndMapBySQL(
+ fn(\DataField $data_field) => $data_field->id,
+ "`object_type` = 'sem' AND `view_perms` = 'user'
+ AND `type` IN('bool', 'textline', 'selectbox', 'radio')"
+ );
+ return $data_fields;
+ }
+
/**
* @SuppressWarnings(PHPMD.Superglobals)
*/
diff --git a/lib/classes/JsonApi/Routes/Institutes/InstitutesIndex.php b/lib/classes/JsonApi/Routes/Institutes/InstitutesIndex.php
index 6eef1b6..d31cbe8 100644
--- a/lib/classes/JsonApi/Routes/Institutes/InstitutesIndex.php
+++ b/lib/classes/JsonApi/Routes/Institutes/InstitutesIndex.php
@@ -13,6 +13,7 @@ class InstitutesIndex extends JsonApiController
InstituteSchema::REL_FACULTY,
InstituteSchema::REL_STATUS_GROUPS,
InstituteSchema::REL_SUB_INSTITUTES,
+ InstituteSchema::REL_COURSES_OF_STUDY,
];
protected $allowedFilteringParameters = ['is-faculty'];
diff --git a/lib/classes/JsonApi/Routes/Institutes/InstitutesShow.php b/lib/classes/JsonApi/Routes/Institutes/InstitutesShow.php
index 5ef3b5a..6a3e0a9 100644
--- a/lib/classes/JsonApi/Routes/Institutes/InstitutesShow.php
+++ b/lib/classes/JsonApi/Routes/Institutes/InstitutesShow.php
@@ -18,6 +18,7 @@ class InstitutesShow extends JsonApiController
InstituteSchema::REL_FACULTY,
InstituteSchema::REL_STATUS_GROUPS,
InstituteSchema::REL_SUB_INSTITUTES,
+ InstituteSchema::REL_COURSES_OF_STUDY,
];
/**
diff --git a/lib/classes/JsonApi/Routes/Mvv/Authority.php b/lib/classes/JsonApi/Routes/Mvv/Authority.php
new file mode 100644
index 0000000..8c9dcf9
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/Authority.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Studiengang;
+use Modul;
+use User;
+
+class Authority
+{
+ public static function canIndexCoursesOfStudy(User $user): bool
+ {
+ return true;
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.Superglobals)
+ */
+ public static function canShowCourseOfStudy(User $user, Studiengang $resource): bool
+ {
+ return $GLOBALS['perm']->have_perm('user') && self::isReadableStudyCourse($user, $resource);
+ }
+
+ private static function isReadableStudyCourse(User $user, Studiengang $resource)
+ {
+ $public_status = \ModuleManagementModel::getPublicStatus(Studiengang::class);
+ return in_array($resource->stat, $public_status)
+ || \RolePersistence::isAssignedRole($user->id, 'MVVAdmin');
+
+ }
+
+ public static function canIndexModules(User $user): bool
+ {
+ return true;
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public static function canShowModule(User $user, Modul $resource): bool
+ {
+ return $GLOBALS['perm']->have_perm('user') && self::isReadableModule($user, $resource);
+ }
+
+ private static function isReadableModule(User $user, Modul $resource): bool
+ {
+ $public_status = \ModuleManagementModel::getPublicStatus(Modul::class);
+ return in_array($resource->stat, $public_status)
+ || \RolePersistence::isAssignedRole($user->id, 'MVVAdmin');
+
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public static function canShowComponentVersion(User $user, \StgteilVersion $resource): bool
+ {
+ return $GLOBALS['perm']->have_perm('user') && self::isReadableComponentVersion($user, $resource);
+ }
+
+ private static function isReadableComponentVersion(User $user, \StgteilVersion $resource): bool
+ {
+ $public_status = \ModuleManagementModel::getPublicStatus(\StgteilVersion::class);
+ return in_array($resource->stat, $public_status)
+ || \RolePersistence::isAssignedRole($user->id, 'MVVAdmin');
+
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsDeep.php b/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsDeep.php
new file mode 100644
index 0000000..7b81ccf
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsDeep.php
@@ -0,0 +1,258 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Errors\BadRequestException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\NonJsonApiController;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\StreamFactoryInterface;
+
+class ComponentVersionsDeep extends NonJsonApiController
+{
+ protected $allowedFilteringParameters = ['q', 'institute', 'semester', 'section'];
+
+ public function __construct(
+ ContainerInterface $container,
+ private StreamFactoryInterface $streamFactory
+ ) {
+ parent::__construct($container);
+ }
+
+ public function __invoke(Request $request, Response $response, array $args)
+ {
+ $component_version = \StgteilVersion::find($args['id']);
+ if (!$component_version) {
+ throw new RecordNotFoundException();
+ }
+
+ $parameters = $request->getQueryParams();
+
+ $this->validateParameters($parameters);
+
+ $data = $this->getVersionData($component_version, $parameters);
+
+ return $response
+ ->withHeader('Content-Type', 'application/json')
+ ->withBody($this->streamFactory->createStream(json_encode($data)));
+ }
+
+ private function validateParameters(array $parameters): void
+ {
+ if (!isset($parameters['semester'])) {
+ throw new BadRequestException('Parameter semester is missing');
+ }
+
+ if (!\Semester::exists($parameters['semester'])) {
+ throw new BadRequestException('Semester not found');
+ }
+ }
+
+ private function getVersionData(\StgteilVersion $version, array $parameters): array
+ {
+ $data = [
+ 'id' => $version->id,
+ 'display-name' => $version->getDisplayName(),
+ 'start-semester' => $version->start_semester ? $this->getSemesterData($version->start_semester) : '',
+ 'end-semester' => $version->end_semester ? $this->getSemesterData($version->end_semester) : '',
+ 'code' => $version->code,
+ 'description' => $version->beschreibung,
+ 'date-of-decision' => $version->beschlussdatum,
+ 'edition-number' => $version->fassung_nr,
+ 'edition-type' => \Config::get()->MVV_STGTEILVERSION['FASSUNG_TYP'][$version->fassung_typ] ?? '',
+ 'status' => \Config::get()->MVV_STGTEILVERSION['STATUS']['values'][$version->stat],
+ 'sections' => $this->getSectionsData($version, $parameters),
+ ];
+ return $data;
+ }
+
+ private function getSectionsData(\StgteilVersion $version, array $parameters): array
+ {
+ $data = [];
+ foreach ($version->abschnitte as $section) {
+ $data[] = [
+ 'id' => $section->id,
+ 'display-name' => $section->getDisplayName(),
+ 'comment' => $section->kommentar,
+ 'position' => $section->position,
+ 'cp' => $section->kp,
+ 'caption' => $section->ueberschrift,
+ 'modules' => $this->getModulesData($section, $parameters),
+ ];
+ }
+ return $data;
+ }
+
+ private function getModulesData(\StgteilAbschnitt $section, array $parameters): array
+ {
+ $status = \Config::get()->MVV_MODUL['STATUS']['values'];
+ $semester = \Semester::find($parameters['semester']);
+ $modules_filtered = $section->module->filter(
+ fn(\Modul $module) =>
+ ((empty($module->start_semester) || $module->start_semester->beginn <= $semester->ende)
+ && (empty($module->end_semester) || $module->end_semester->ende >= $semester->beginn)
+ && $status[$module->stat]['public'] === 1)
+ );
+ $data = [];
+ foreach ($modules_filtered as $module) {
+ $data[] = [
+ 'id' => $module->id,
+ 'display-name' => (string) $module->getDisplayName(),
+ 'code' => (string) $module->code,
+ 'date-of-decision' => $module->beschlussdatum ? date('c', $module->beschlussdatum) : '',
+ 'edition-number' => (string) $module->fassung_nr,
+ 'edition-type' => \Config::get()->MVV_MODUL['FASSUNG_TYP'][$module->fassung_typ] ?? '',
+ 'version-number' => (string) $module->version,
+ 'semester-duration' => (string) $module->dauer,
+ 'capacity' => (string) $module->kapazitaet,
+ 'cp' => $module->kp,
+ 'workload-self' => (string) $module->wl_selbst,
+ 'workload-exam' => (string) $module->wl_pruef,
+ 'examination-period' => \Config::get()->MVV_MODUL['PRUEF_EBENE']['values'][$module->pruef_ebene] ?? '',
+ 'grade-factor' => (string) $module->faktor_note,
+ 'foreign-key' => (string) $module->flexnow_modul,
+ 'name' => (string) $module->deskriptoren->bezeichnung,
+ 'responsible' => (string) $module->deskriptoren->verantwortlich,
+ 'prerequisite' => (string) $module->deskriptoren->voraussetzung,
+ 'objectives' => (string) $module->deskriptoren->kompetenzziele,
+ 'content' => (string) $module->deskriptoren->inhalte,
+ 'literature' => (string) $module->deskriptoren->literatur,
+ 'links' => (string) $module->deskriptoren->links,
+ 'comment' => (string) $module->deskriptoren->kommentar,
+ 'cycle' => (string) $module->deskriptoren->turnus,
+ 'comment-capacity' => (string) $module->deskriptoren->kommentar_kapazitaet,
+ 'comment_sws' => (string) $module->deskriptoren->kommentar_sws,
+ 'status' => \Config::get()->MVV_MODUL['STATUS']['values'][$module->stat],
+ 'module-languages' => $this->getModuleLanguagesData($module),
+ 'module-section-data' => $this->getModuleSectionData(
+ $module->abschnitte_modul->findOneBy('abschnitt_id', $section->id)),
+ 'module-components' => $this->getModuleComponentsData($module, $parameters),
+ 'start-semester' => $module->start_semester ? $this->getSemesterData($module->start_semester) : '',
+ 'end-semester' => $module->end_semester ? $this->getSemesterData($module->end_semester) : '',
+ ];
+ }
+ return $data;
+ }
+
+ private function getSemesterData(\Semester $semester): array
+ {
+ return [
+ 'id' => $semester->id,
+ 'name' => $semester->name,
+ 'short-name' => $semester->semester_token,
+ 'semester-start' => $semester->beginn,
+ 'semester-end' => $semester->ende,
+ 'foreign-key' => $semester->external_id,
+ 'teaching-start' => $semester->vorles_beginn,
+ 'teaching-end' => $semester->vorles_ende,
+ 'semester-switch-time' => $semester->sem_wechsel,
+ ];
+ }
+
+ private function getModuleComponentsData(\Modul $module, array $parameters): array
+ {
+ foreach ($module->modulteile as $component) {
+ $data[] = [
+ 'id' => $component->id,
+ 'name' => (string) $component->deskriptoren->bezeichnung,
+ 'position' => $component->position,
+ 'foreign-key' => $component->flexnow_modul,
+ 'number' => $component->nummer,
+ 'number-label' => \Config::get()->MVV_MODULTEIL['NUM_BEZEICHNUNG']['values'][$component->num_bezeichnung] ?? '',
+ 'teaching-method' => \Config::get()->MVV_MODULTEIL['LERNLEHRFORM']['values'][$component->lernlehrform] ?? '',
+ 'semester' => $component->semester,
+ 'number-of-participants' => $component->kapazitaet,
+ 'cp' => $component->kp,
+ 'sws' => $component->sws,
+ 'workload-compulsory' => $component->wl_praesenz,
+ 'workload-preparation' => $component->wl_bereitung,
+ 'workload-self' => $component->wl_selbst,
+ 'workload-exam' => $component->wl_pruef,
+ 'share-of-grade' => $component->anteil_note,
+ 'compensable' => $component->ausgleichbar,
+ 'compulsory-attendance' => $component->pflicht,
+ 'prerequisites' => $component->deskriptoren->voraussetzung,
+ 'comment' => $component->deskriptoren->kommentar,
+ 'comment-capacity' => $component->deskriptoren->kommentar_kapazitaet,
+ 'comment-wl-compulsory' => $component->deskriptoren->kommentar_wl_praesenz,
+ 'comment-wl-preparation' => $component->deskriptoren->kommentar_wl_bereitung,
+ 'comment-wl-self' => $component->deskriptoren->kommentar_wl_selbst,
+ 'comment-wl-exam' => $component->deskriptoren->kommentar_wl_pruef,
+ 'exam-prerequisites' => $component->deskriptoren->pruef_vorleistung,
+ 'exam-requirements' => $component->deskriptoren->pruef_leistung,
+ 'comment-compulsory-attendance' => $component->deskriptoren->kommentar_pflicht,
+ 'courses' => $this->getCoursesData($component, $parameters),
+ 'course-semesters' => $this->getModuleComponentSectionData($component),
+ ];
+ }
+ return $data;
+ }
+
+ private function getCoursesData(\Modulteil $component, array $parameters): array
+ {
+ $course_ids = [];
+ foreach ($component->lvgruppen as $lvgruppe) {
+ $course_ids += $lvgruppe->courses->pluck('id');
+ }
+
+ if (count($course_ids) === 0) {
+ return [];
+ }
+
+ $courses = \Course::findBySQL(
+ '`seminar_id` IN (?) AND `visible` = 1 ORDER BY start_time, name',
+ [$course_ids]
+ );
+ $semester = \Semester::find($parameters['semester']);
+ $courses = array_filter($courses, fn (\Course $course) => $course->isInSemester($semester));
+
+ $data = [];
+ foreach ($courses as $course) {
+ $data[] = [
+ 'id' => $course->id,
+ 'name' => $course->name,
+ 'number' => $course->veranstaltungsnummer
+ ];
+ }
+ return $data;
+ }
+
+ private function getModuleComponentSectionData(\Modulteil $component): array
+ {
+ $data = [];
+ foreach ($component->abschnitt_assignments as $assignment) {
+ $data = [
+ 'course-semester' => $assignment->fachsemester,
+ 'differentiation' => $assignment->differenzierung,
+ ];
+ }
+ return $data;
+ }
+
+ private function getModuleSectionData(\StgteilabschnittModul $module_section): array
+ {
+ return [
+ 'id' => $module_section->abschnitt_modul_id,
+ 'name' => $module_section->bezeichnung,
+ 'code' => $module_section->modulcode,
+ 'position' => $module_section->position,
+ 'foreign-key' => $module_section->flexnow_modul,
+ ];
+ }
+
+ private function getModuleLanguagesData(\Modul $module): array
+ {
+ $languages = \Config::get()->MVV_MODUL['SPRACHE']['values'];
+ $data = [];
+ foreach ($module->languages as $language) {
+ $data[] = [
+ 'language' => $language->lang,
+ 'name' => $languages[$language->lang]['name'],
+ 'position' => $language->position,
+ ];
+ }
+ return $data;
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsShow.php b/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsShow.php
new file mode 100644
index 0000000..248cc69
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsShow.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+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 ComponentVersionsShow extends JsonApiController
+{
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $version = \StgteilVersion::find($args['id']);
+ if (!$version) {
+ throw new RecordNotFoundException();
+ }
+
+ if (!Authority::canShowComponentVersion($this->getUser($request), $version)) {
+ throw new AuthorizationFailedException();
+ }
+
+ return $this->getContentResponse($version);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ComponentsByCoursesOfStudyIndex.php b/lib/classes/JsonApi/Routes/Mvv/ComponentsByCoursesOfStudyIndex.php
new file mode 100644
index 0000000..17fbdcf
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ComponentsByCoursesOfStudyIndex.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\CourseOfStudyComponent;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class ComponentsByCoursesOfStudyIndex extends JsonApiController
+{
+ protected $allowedPagingParameters = [
+ 'offset',
+ 'limit'
+ ];
+
+ protected $allowedIncludePaths = [
+ CourseOfStudyComponent::REL_SUBJECT,
+ CourseOfStudyComponent::REL_VERSIONS,
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $course_of_study = \Studiengang::find($args['id']);
+ if (!$course_of_study) {
+ throw new RecordNotFoundException();
+ }
+ [$offset, $limit] = $this->getOffsetAndLimit();
+
+ return $this->getPaginatedContentResponse(
+ $course_of_study->studiengangteile->limit($offset, $limit),
+ count($course_of_study->studiengangteile)
+ );
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/CourseOfStudyComponentsShow.php b/lib/classes/JsonApi/Routes/Mvv/CourseOfStudyComponentsShow.php
new file mode 100644
index 0000000..4969591
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/CourseOfStudyComponentsShow.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\CourseOfStudyComponent;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class CourseOfStudyComponentsShow extends JsonApiController
+{
+
+ protected $allowedIncludePaths = [
+ CourseOfStudyComponent::REL_SUBJECT,
+ CourseOfStudyComponent::REL_VERSIONS,
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $component = \StudiengangTeil::find($args['id']);
+ if (!$component) {
+ throw new RecordNotFoundException();
+ }
+
+ return $this->getContentResponse($component);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyIndex.php b/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyIndex.php
new file mode 100644
index 0000000..2040153
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyIndex.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\CourseOfStudy;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\JsonApiController;
+
+class CoursesOfStudyIndex extends JsonApiController
+{
+ protected $allowedFilteringParameters = ['q', 'institute', 'semester', 'degree', 'category', 'type'];
+
+ protected $allowedIncludePaths = [
+ CourseOfStudy::REL_SECTIONS,
+ CourseOfStudy::REL_INSTITUTE,
+ CourseOfStudy::REL_COMPONENTS,
+ CourseOfStudy::REL_DEGREE,
+ CourseOfStudy::REL_END_SEMESTER,
+ CourseOfStudy::REL_START_SEMESTER,
+ ];
+
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ if (!Authority::canIndexCoursesOfStudy($user = $this->getUser($request))) {
+ throw new AuthorizationFailedException();
+ }
+
+ $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+ $error = $this->validateFilters($filtering);
+ if ($error) {
+ throw new BadRequestException($error);
+ }
+
+ [$offset, $limit] = $this->getOffsetAndLimit();
+ $courses_of_study = $this->getCoursesOfStudy($filtering, $offset, $limit);
+
+ return $this->getPaginatedContentResponse(
+ $courses_of_study,
+ count($courses_of_study)
+ );
+ }
+
+ private function validateFilters($filtering)
+ {
+ // keyword aka q
+ if (isset($filtering['q']) && mb_strlen($filtering['q']) < 3) {
+ return 'Search term too short.';
+ }
+
+ // institute
+ if (isset($filtering['institute']) && !\Institute::exists($filtering['institute'])) {
+ return 'Filter `institute` must be a valid id.';
+ }
+
+ // degree
+ if (isset($filtering['degree']) && !\Abschluss::exists($filtering['degree'])) {
+ return 'Filter `degree` must be a valid id.';
+ }
+
+ // degree category
+ if (isset($filtering['category']) && !\AbschlussKategorie::find($filtering['category'])) {
+ return 'Filter `category` must be a valid id';
+ }
+
+ // semester
+ if (isset($filtering['semester']) && !\Semester::exists($filtering['semester'])) {
+ return 'Filter `semester` must be a valid id.';
+ }
+ }
+
+ private function getCoursesOfStudy($filtering, $offset, $limit): array
+ {
+ $join = '';
+ $where = ' 1 ';
+ $filtering['offset'] = $offset;
+ $filtering['limit'] = $limit;
+ if (isset($filtering['institute'])) {
+ $where .= ' AND `institut_id` = :institute ';
+ }
+ if (isset($filtering['type'])) {
+ $where .= ' AND `typ` = :type ';
+ }
+ if (isset($filtering['degree'])) {
+ $where .= ' AND `mvv_studiengang`.`abschluss_id` = :degree ';
+ }
+ if (isset($filtering['category'])) {
+ $join .= 'LEFT JOIN `mvv_abschluss_zuord` USING(`abschluss_id`) ';
+ $where .= ' AND `mvv_abschluss_zuord`.`kategorie_id` = :category';
+ }
+ if (isset($filtering['semester'])) {
+ $semester = \Semester::find($filtering['semester']);
+ unset($filtering['semester']);
+ $filtering['semester_start'] = $semester->beginn;
+ $filtering['semester_end'] = $semester->ende;
+ $join .= 'LEFT JOIN `semester_data` AS `start_sem`
+ ON (`mvv_studiengang`.`start` = `start_sem`.`semester_id`)
+ LEFT JOIN `semester_data` AS `end_sem`
+ ON (`mvv_studiengang`.`end` = `end_sem`.`semester_id`) ';
+ $where .= ' AND (`start_sem`.`beginn` <= :semester_end OR ISNULL(`start_sem`.`beginn`))
+ AND (`end_sem`.`ende` >= :semester_start OR ISNULL(`end_sem`.`ende`))';
+ }
+ if (isset($filtering['q'])) {
+ $where .= " AND (`mvv_studiengang`.`name` LIKE CONCAT('%', :q, '%') OR `mvv_studiengang`.`name_kurz` LIKE CONCAT('%', :q, '%')) ";
+ }
+ $where .= ' ORDER BY `mvv_studiengang`.`name` ASC
+ LIMIT :limit OFFSET :offset';
+ return \Studiengang::findBySQL(
+ ($join ? $join . ' WHERE ' : '') . $where,
+ $filtering
+ );
+ }
+
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyShow.php b/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyShow.php
new file mode 100644
index 0000000..8e8504a
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyShow.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+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 CoursesOfStudyShow extends JsonApiController
+{
+
+ protected $allowedIncludePaths = null;
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $course_of_study = \Studiengang::find($args['id']);
+ if (!$course_of_study) {
+ throw new RecordNotFoundException();
+ }
+ if (!Authority::canShowCourseOfStudy($user = $this->getUser($request), $course_of_study)) {
+ throw new AuthorizationFailedException();
+ }
+
+ return $this->getContentResponse($course_of_study);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/DegreesByCoursesOfStudyShow.php b/lib/classes/JsonApi/Routes/Mvv/DegreesByCoursesOfStudyShow.php
new file mode 100644
index 0000000..006b9e7
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/DegreesByCoursesOfStudyShow.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class DegreesByCoursesOfStudyShow extends JsonApiController
+{
+ protected $allowedIncludePaths = [];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $course_of_study = \Studiengang::find($args['id']);
+ if (empty($course_of_study->abschluss)) {
+ throw new RecordNotFoundException('Could not find degree.');
+ }
+
+ return $this->getContentResponse($course_of_study->abschluss);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/DegreesIndex.php b/lib/classes/JsonApi/Routes/Mvv/DegreesIndex.php
new file mode 100644
index 0000000..bff4c14
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/DegreesIndex.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Abschluss;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\JsonApiController;
+
+class DegreesIndex extends JsonApiController
+{
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ [$offset, $limit] = $this->getOffsetAndLimit();
+
+ return $this->getPaginatedContentResponse(
+ Abschluss::findBySQL("1 ORDER BY name LIMIT {$offset}, {$limit}"),
+ Abschluss::countBySql('1')
+ );
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/DegreesShow.php b/lib/classes/JsonApi/Routes/Mvv/DegreesShow.php
new file mode 100644
index 0000000..ce521e8
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/DegreesShow.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class DegreesShow extends JsonApiController
+{
+ protected $allowedIncludePaths = [];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $degree = \Abschluss::find($args['id']);
+ if (!$degree) {
+ throw new RecordNotFoundException('Could not find degree.');
+ }
+
+ return $this->getContentResponse($degree);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsByModuleIndex.php b/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsByModuleIndex.php
new file mode 100644
index 0000000..9fb2b0b
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsByModuleIndex.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\ModuleComponent;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class ModuleComponentsByModuleIndex extends JsonApiController
+{
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ protected $allowedIncludePaths = [
+ ModuleComponent::REL_COURSES,
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $module = \Modul::find($args['id']);
+ if (!$module) {
+ throw new RecordNotFoundException();
+ }
+ [$offset, $limit] = $this->getOffsetAndLimit();
+
+ return $this->getPaginatedContentResponse(
+ $module->modulteile->limit($offset, $limit),
+ count($module->modulteile)
+ );
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsShow.php b/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsShow.php
new file mode 100644
index 0000000..a3c4d44
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsShow.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\ModuleComponent;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class ModuleComponentsShow extends JsonApiController
+{
+ protected $allowedIncludePaths = [
+ ModuleComponent::REL_COURSES,
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $component = \Modulteil::find($args['id']);
+ if (!$component) {
+ throw new RecordNotFoundException('Could not find module component.');
+ }
+
+ return $this->getContentResponse($component);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ModulesIndex.php b/lib/classes/JsonApi/Routes/Mvv/ModulesIndex.php
new file mode 100644
index 0000000..3018fd4
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ModulesIndex.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\Schemas\Module;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\JsonApiController;
+
+class ModulesIndex extends JsonApiController
+{
+ protected $allowedFilteringParameters = ['q', 'institute', 'semester', 'section'];
+
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ protected $allowedIncludePaths = [
+ Module::REL_MODULE_COMPONENTS,
+ Module::REL_END_SEMESTER,
+ Module::REL_START_SEMESTER,
+ Module::REL_RESPONSIBLE_DEPARTMENT,
+ Module::REL_DEPARTMENTS,
+ Module::REL_SOURCE_MODULE,
+ Module::REL_VARIANT_MODULE,
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ if (!Authority::canIndexModules($user = $this->getUser($request))) {
+ throw new AuthorizationFailedException();
+ }
+
+ $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+ $error = $this->validateFilters($filtering);
+ if ($error) {
+ throw new BadRequestException($error);
+ }
+
+ [$offset, $limit] = $this->getOffsetAndLimit();
+ $modules = $this->getModules($filtering, $offset, $limit);
+
+ return $this->getPaginatedContentResponse(
+ $modules,
+ count($modules)
+ );
+ }
+
+ private function validateFilters($filtering)
+ {
+ // keyword aka q
+ if (isset($filtering['q']) && mb_strlen($filtering['q']) < 3) {
+ return 'Search term too short.';
+ }
+
+ // institute
+ if (isset($filtering['institute']) && !\Institute::exists($filtering['institute'])) {
+ return 'Filter `institute` must be a valid id.';
+ }
+
+ // section
+ if (isset($filtering['section']) && !\StgteilAbschnitt::exists($filtering['section'])) {
+ return 'Filter `section` must be a valid id';
+ }
+
+ // semester
+ if (isset($filtering['semester']) && !\Semester::exists($filtering['semester'])) {
+ return 'Filter `semester` must be a valid id.';
+ }
+ }
+
+ private function getModules($filtering, $offset, $limit): array
+ {
+ $join = '';
+ $where = ' 1 ';
+ $filtering['offset'] = $offset;
+ $filtering['limit'] = $limit;
+ if (isset($filtering['institute'])) {
+ $where .= ' AND `institut_id` = :institute ';
+ }
+ if (isset($filtering['section'])) {
+ $join .= 'LEFT JOIN `mvv_stgteilabschnitt_modul` USING(`modul_id`) ';
+ $where .= ' AND `mvv_stgteilabschnitt_modul`.`abschnitt_id` = :section';
+ }
+ if (isset($filtering['semester'])) {
+ $semester = \Semester::find($filtering['semester']);
+ unset($filtering['semester']);
+ $filtering['semester_start'] = $semester->beginn;
+ $filtering['semester_end'] = $semester->ende;
+ $join .= 'LEFT JOIN `semester_data` AS `start_sem`
+ ON (`mvv_modul`.`start` = `start_sem`.`semester_id`)
+ LEFT JOIN `semester_data` AS `end_sem`
+ ON (`mvv_modul`.`end` = `end_sem`.`semester_id`) ';
+ $where .= ' AND (`start_sem`.`beginn` <= :semester_end OR ISNULL(`start_sem`.`beginn`))
+ AND (`end_sem`.`ende` >= :semester_start OR ISNULL(`end_sem`.`ende`))';
+ }
+ $join .= 'LEFT JOIN `mvv_modul_deskriptor` USING(`modul_id`) ';
+ if (isset($filtering['q'])) {
+ $where .= " AND (`mvv_modul_deskriptor`.`bezeichnung` LIKE CONCAT('%', :q, '%') OR `mvv_modul`.`code` LIKE CONCAT('%', :q, '%')) ";
+ }
+ $where .= ' ORDER BY `mvv_modul`.`code` ASC, `mvv_modul_deskriptor`.`bezeichnung` ASC
+ LIMIT :limit OFFSET :offset';
+ return \Modul::findBySQL(
+ ($join ? $join . ' WHERE ' : '') . $where,
+ $filtering
+ );
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ModulesShow.php b/lib/classes/JsonApi/Routes/Mvv/ModulesShow.php
new file mode 100644
index 0000000..8db7773
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ModulesShow.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+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 ModulesShow extends JsonApiController
+{
+
+ protected $allowedIncludePaths = null;
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $module = \Modul::find($args['id']);
+ if (!$module) {
+ throw new RecordNotFoundException();
+ }
+
+ if (!Authority::canShowModule($this->getUser($request), $module)) {
+ throw new AuthorizationFailedException();
+ }
+
+ return $this->getContentResponse($module);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/SubjectsByCourseOfStudyComponentsShow.php b/lib/classes/JsonApi/Routes/Mvv/SubjectsByCourseOfStudyComponentsShow.php
new file mode 100644
index 0000000..cc19ee2
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/SubjectsByCourseOfStudyComponentsShow.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\Subject;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class SubjectsByCourseOfStudyComponentsShow extends JsonApiController
+{
+ protected $allowedIncludePaths = [
+ Subject::REL_DEPARTMENTS,
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $component = \StudiengangTeil::find($args['id']);
+ if (empty($component->fach)) {
+ throw new RecordNotFoundException('Could not find subject.');
+ }
+
+ return $this->getContentResponse($component->fach);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/SubjectsIndex.php b/lib/classes/JsonApi/Routes/Mvv/SubjectsIndex.php
new file mode 100644
index 0000000..338f71a
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/SubjectsIndex.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Fach;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\JsonApiController;
+
+class SubjectsIndex extends JsonApiController
+{
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ [$offset, $limit] = $this->getOffsetAndLimit();
+
+ return $this->getPaginatedContentResponse(
+ Fach::findBySQL("1 ORDER BY name LIMIT {$offset}, {$limit}"),
+ Fach::countBySql('1')
+ );
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/SubjectsShow.php b/lib/classes/JsonApi/Routes/Mvv/SubjectsShow.php
new file mode 100644
index 0000000..63428bc
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/SubjectsShow.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\Subject;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class SubjectsShow extends JsonApiController
+{
+ protected $allowedIncludePaths = [
+ Subject::REL_DEPARTMENTS,
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $subject = \Fach::find($args['id']);
+ if (!$subject) {
+ throw new RecordNotFoundException('Could not find subject.');
+ }
+
+ return $this->getContentResponse($subject);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/VersionsByCourseOfStudyComponentsIndex.php b/lib/classes/JsonApi/Routes/Mvv/VersionsByCourseOfStudyComponentsIndex.php
new file mode 100644
index 0000000..9d321b9
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/VersionsByCourseOfStudyComponentsIndex.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\ComponentVersion;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class VersionsByCourseOfStudyComponentsIndex extends JsonApiController
+{
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ protected $allowedIncludePaths = [
+ ComponentVersion::REL_SECTIONS,
+ ComponentVersion::REL_START_SEMESTER,
+ ComponentVersion::REL_END_SEMESTER,
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $component = \StudiengangTeil::find($args['id']);
+ if (!$component) {
+ throw new RecordNotFoundException();
+ }
+ [$offset, $limit] = $this->getOffsetAndLimit();
+
+ return $this->getPaginatedContentResponse(
+ $component->versionen->limit($offset, $limit),
+ count($component->versionen)
+ );
+ }
+}
diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php
index c651b87..801bf29 100644
--- a/lib/classes/JsonApi/SchemaMap.php
+++ b/lib/classes/JsonApi/SchemaMap.php
@@ -66,6 +66,15 @@ class SchemaMap
\FolderType::class => Schemas\Folder::class,
\UserFilter::class => Schemas\UserFilter::class,
\UserFilterField::class => Schemas\UserFilterField::class,
+ \Studiengang::class => Schemas\CourseOfStudy::class,
+ \StudiengangTeil::class => Schemas\CourseOfStudyComponent::class,
+ \StgteilVersion::class => Schemas\ComponentVersion::class,
+ \Fach::class => Schemas\Subject::class,
+ \Abschluss::class => Schemas\Degree::class,
+ \Modul::class => Schemas\Module::class,
+ \Modulteil::class => Schemas\ModuleComponent::class,
+ \StgteilAbschnitt::class => Schemas\ComponentSection::class,
+
\Courseware\Block::class => Schemas\Courseware\Block::class,
\Courseware\BlockComment::class => Schemas\Courseware\BlockComment::class,
\Courseware\BlockFeedback::class => Schemas\Courseware\BlockFeedback::class,
diff --git a/lib/classes/JsonApi/Schemas/ComponentSection.php b/lib/classes/JsonApi/Schemas/ComponentSection.php
new file mode 100644
index 0000000..459d6e0
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/ComponentSection.php
@@ -0,0 +1,52 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class ComponentSection extends SchemaProvider
+{
+ const REL_MODULES = 'modules';
+ const TYPE = 'component-sections';
+
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'display-name' => (string) $resource->getDisplayName(),
+ 'comment' => $resource->kommentar,
+ 'position' => $resource->position,
+ 'cp' => $resource->kp,
+ 'caption' => $resource->ueberschrift,
+ 'type' => get_class($resource)
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $relationships = $this->addModulesRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_MODULES));
+
+ return $relationships;
+ }
+
+ private function addModulesRelationship(array $relationships, $resource, $includeData)
+ {
+ $relationships[self::REL_MODULES] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_MODULES),
+ ],
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_MODULES][self::RELATIONSHIP_DATA] = $resource->module;
+ }
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/ComponentVersion.php b/lib/classes/JsonApi/Schemas/ComponentVersion.php
new file mode 100644
index 0000000..ea89716
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/ComponentVersion.php
@@ -0,0 +1,94 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class ComponentVersion extends SchemaProvider
+{
+ const REL_SECTIONS = 'component-sections';
+ const REL_START_SEMESTER = 'start-semester';
+ const REL_END_SEMESTER = 'end-semester';
+ const TYPE = 'component-versions';
+
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'display-name' => (string) $resource->getDisplayName(),
+ 'code' => (string) $resource->code,
+ 'date' => (string) $resource->beschlussdatum,
+ 'version-number' => (string) $resource->fassung_nr,
+ 'version-type' => (string) $resource->fassung_typ,
+ 'description' => (string) $resource->beschreibung,
+ 'status' => (string) $resource->stat,
+ 'status-name' => \Config::get()->MVV_STGTEILVERSION['STATUS']['values'][$resource->stat]['name'],
+ 'type' => get_class($resource)
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ if ($semester = $this->getStartSemester($resource)) {
+ $relationships[self::REL_START_SEMESTER] = $semester;
+ }
+ if ($semester = $this->getEndSemester($resource)) {
+ $relationships[self::REL_END_SEMESTER] = $semester;
+ }
+
+ $relationships = $this->addSectionsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SECTIONS));
+
+ return $relationships;
+ }
+
+ private function getStartSemester(\StgteilVersion $version)
+ {
+ $semester = \Semester::find($version->start_sem);
+ if (!$semester) {
+ return null;
+ }
+
+ return [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($semester),
+ ],
+ self::RELATIONSHIP_DATA => $semester,
+ ];
+ }
+
+ private function getEndSemester(\StgteilVersion $version)
+ {
+ $semester = \Semester::find($version->end_sem);
+ if (!$semester) {
+ return null;
+ }
+
+ return [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($semester),
+ ],
+ self::RELATIONSHIP_DATA => $semester,
+ ];
+ }
+
+ private function addSectionsRelationship(array $relationships, $resource, $includeData)
+ {
+ $relationships[self::REL_SECTIONS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_SECTIONS),
+ ],
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_SECTIONS][self::RELATIONSHIP_DATA] = $resource->abschnitte;
+ }
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/CourseOfStudy.php b/lib/classes/JsonApi/Schemas/CourseOfStudy.php
new file mode 100644
index 0000000..0e6a0a3
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/CourseOfStudy.php
@@ -0,0 +1,151 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class CourseOfStudy extends SchemaProvider
+{
+ const REL_SECTIONS = 'sections';
+ const REL_INSTITUTE = 'institute';
+ const REL_COMPONENTS = 'components';
+ const REL_DEGREE = 'degree';
+ const REL_END_SEMESTER = 'end-semester';
+ const REL_START_SEMESTER = 'start-semester';
+ const TYPE = 'courses-of-study';
+
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'display-name' => (string) $resource->getDisplayName(),
+ 'name' => (string) $resource->name,
+ 'short-name' => (string) $resource->name_kurz,
+ 'type' => (string) $resource->typ,
+ 'status' => (string) $resource->stat,
+ 'status-name' => \Config::get()->MVV_STUDIENGANG['STATUS']['values'][$resource->stat]['name'] ?? '',
+ 'classname' => get_class($resource)
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $institute = \Institute::find($resource->institut_id);
+ if ($institute) {
+ $relationships[self::REL_INSTITUTE] = $this->getInstitute($resource, $this->shouldInclude($context, self::REL_INSTITUTE));
+ }
+
+ if ($semester = $this->getStartSemester($resource)) {
+ $relationships[self::REL_START_SEMESTER] = $semester;
+ }
+ if ($semester = $this->getEndSemester($resource)) {
+ $relationships[self::REL_END_SEMESTER] = $semester;
+ }
+
+ $relationships = $this->addSectionsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SECTIONS));
+ $relationships = $this->addComponentsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_COMPONENTS));
+ $relationships = $this->addDegreeRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_DEGREE));
+
+ return $relationships;
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ private function getInstitute(\Studiengang $course_of_study, $shouldInclude)
+ {
+ $institute = \Institute::find($course_of_study->institut_id);
+ return $institute
+ ? [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($institute),
+ ],
+ self::RELATIONSHIP_DATA => $institute,
+ ]
+ : [
+ self::RELATIONSHIP_DATA => null,
+ ];
+ }
+
+ private function getStartSemester(\Studiengang $course_of_study)
+ {
+ $semester = \Semester::find($course_of_study->start);
+ if (!$semester) {
+ return null;
+ }
+
+ return [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($semester),
+ ],
+ self::RELATIONSHIP_DATA => $semester,
+ ];
+ }
+
+ private function getEndSemester(\Studiengang $course_of_study)
+ {
+ $semester = \Semester::find($course_of_study->end);
+ if (!$semester) {
+ return null;
+ }
+
+ return [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($semester),
+ ],
+ self::RELATIONSHIP_DATA => $semester,
+ ];
+ }
+
+ private function addSectionsRelationship(array $relationships, $resource, $includeData)
+ {
+ $relationships[self::REL_SECTIONS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_SECTIONS),
+ ],
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_SECTIONS][self::RELATIONSHIP_DATA] = $resource->stgteil_bezeichnungen;
+ }
+
+ return $relationships;
+ }
+
+ private function addComponentsRelationship(array $relationships, $resource, $includeData)
+ {
+ $relationships[self::REL_COMPONENTS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_COMPONENTS),
+ ],
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_COMPONENTS][self::RELATIONSHIP_DATA] = $resource->studiengangteile;
+ }
+
+ return $relationships;
+ }
+
+ private function addDegreeRelationship(array $relationships, $resource, $includeData)
+ {
+ $relationships[self::REL_DEGREE] = [
+ self::RELATIONSHIP_LINKS_SELF => $this->createLinkToResource($resource->abschluss),
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_DEGREE),
+ ],
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_DEGREE][self::RELATIONSHIP_DATA] = $resource->abschluss;
+ }
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/CourseOfStudyComponent.php b/lib/classes/JsonApi/Schemas/CourseOfStudyComponent.php
new file mode 100644
index 0000000..c351f12
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/CourseOfStudyComponent.php
@@ -0,0 +1,73 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class CourseOfStudyComponent extends SchemaProvider
+{
+ const REL_SUBJECT = 'subject';
+ const REL_VERSIONS = 'versions';
+ const TYPE = 'courses-of-study-components';
+
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'display-name' => (string) $resource->getDisplayName(),
+ 'title-supplement' => (string) $resource->zusatz,
+ 'cp' => (string) $resource->kp,
+ 'semesters' => (string) $resource->semester,
+ 'classname' => get_class($resource)
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ if ($resource->fach) {
+ $relationships[self::REL_SUBJECT] = $this->getSubject($resource, $this->shouldInclude($context, self::REL_SUBJECT));
+ }
+
+ $relationships = $this->addVersionsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_VERSIONS));
+
+ return $relationships;
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ private function getSubject(\StudiengangTeil $component, $shouldInclude)
+ {
+ return $component->fach
+ ? [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($component->fach),
+ ],
+ self::RELATIONSHIP_DATA => $component->fach,
+ ]
+ : [
+ self::RELATIONSHIP_DATA => null,
+ ];
+ }
+
+ private function addVersionsRelationship(array $relationships, $resource, $includeData)
+ {
+ $relationships[self::REL_VERSIONS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_VERSIONS),
+ ],
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_VERSIONS][self::RELATIONSHIP_DATA] = $resource->versionen;
+ }
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/Institute.php b/lib/classes/JsonApi/Schemas/Institute.php
index 0084ca6..61058f7 100644
--- a/lib/classes/JsonApi/Schemas/Institute.php
+++ b/lib/classes/JsonApi/Schemas/Institute.php
@@ -16,6 +16,7 @@ class Institute extends SchemaProvider
const REL_MEMBERSHIPS = 'memberships';
const REL_STATUS_GROUPS = 'status-groups';
const REL_SUB_INSTITUTES = 'sub-institutes';
+ const REL_COURSES_OF_STUDY = 'courses-of-study';
/**
* @param \Institute $institute
@@ -95,6 +96,12 @@ class Institute extends SchemaProvider
$this->shouldInclude($context, self::REL_SUB_INSTITUTES)
);
+ $relationships = $this->getCoursesOfStudyRelationship(
+ $relationships,
+ $resource,
+ $this->shouldInclude($context, self::REL_COURSES_OF_STUDY)
+ );
+
return $relationships;
}
@@ -156,6 +163,30 @@ class Institute extends SchemaProvider
return array_merge($relationships, [self::REL_STATUS_GROUPS => $relation]);
}
+ private function getCoursesOfStudyRelationship(
+ array $relationships,
+ $resource,
+ $includeData
+ ): array {
+ $relation = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_COURSES_OF_STUDY),
+ ],
+ ];
+
+ if ($includeData) {
+ $relation[self::RELATIONSHIP_DATA] = $resource->courses_of_study;
+ } else {
+ $relation[self::RELATIONSHIP_DATA] = $resource->courses_of_study->map(function (\Studiengang $cos): \Studiengang {
+ return \Studiengang::build(['id' => $cos->id]);
+ });
+ }
+
+ $relationships[self::REL_COURSES_OF_STUDY] = $relation;
+
+ return $relationships;
+ }
+
public function hasResourceMeta($resource): bool
{
return true;
@@ -168,6 +199,7 @@ class Institute extends SchemaProvider
{
return [
'sub-institutes-count' => count($resource->sub_institutes),
+ 'courses-of-study-count' => count($resource->courses_of_study),
];
}
}
diff --git a/lib/classes/JsonApi/Schemas/Module.php b/lib/classes/JsonApi/Schemas/Module.php
new file mode 100644
index 0000000..55fe085
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Module.php
@@ -0,0 +1,171 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class Module extends SchemaProvider
+{
+ const REL_DEPARTMENTS = 'departments';
+ const REL_RESPONSIBLE_DEPARTMENT = 'responsible-department';
+ const REL_SOURCE_MODULE = 'source-module';
+ const REL_VARIANT_MODULE = 'variant-module';
+ const REL_START_SEMESTER = 'start-semester';
+ const REL_END_SEMESTER = 'end-semester';
+ const REL_MODULE_COMPONENTS = 'module-components';
+ const REL_LANGUAGES = 'languages';
+
+ const TYPE = 'modules';
+
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'display-name' => (string) $resource->getDisplayName(),
+ 'code' => (string) $resource->code,
+ 'date-of-decision' => $resource->beschlussdatum ? date('c', $resource->beschlussdatum) : '',
+ 'edition-number' => (string) $resource->fassung_nr,
+ 'edition-type' => \Config::get()->MVV_STGTEILVERSION['FASSUNG_TYP'][$resource->fassung_typ] ?? '',
+ 'version-number' => (string) $resource->version,
+ 'semester-duration' => (string) $resource->dauer,
+ 'capacity' => (string) $resource->kapazitaet,
+ 'cp' => $resource->kp,
+ 'workload-self' => (string) $resource->wl_selbst,
+ 'workload-exam' => (string) $resource->wl_pruef,
+ 'examination-period' => \Config::get()->MVV_MODUL['PRUEF_EBENE']['values'][$resource->pruef_ebene] ?? '',
+ 'grade-factor' => (string) $resource->faktor_note,
+ // 'module-responsible' => (string) $resource->verantwortlich,
+ 'foreign-key' => (string) $resource->flexnow_modul,
+ 'name' => (string) $resource->deskriptoren->bezeichnung,
+ 'responsible' => (string) $resource->deskriptoren->verantwortlich,
+ 'prerequisite' => (string) $resource->deskriptoren->voraussetzung,
+ 'objectives' => (string) $resource->deskriptoren->kompetenzziele,
+ 'content' => (string) $resource->deskriptoren->inhalte,
+ 'literature' => (string) $resource->deskriptoren->literatur,
+ 'links' => (string) $resource->deskriptoren->links,
+ 'comment' => (string) $resource->deskriptoren->kommentar,
+ 'cycle' => (string) $resource->deskriptoren->turnus,
+ 'comment-capacity' => (string) $resource->deskriptoren->kommentar_kapazitaet,
+ 'comment_sws' => (string) $resource->deskriptoren->kommentar_sws,
+ 'type' => get_class($resource),
+ 'status' => \Config::get()->MVV_MODUL['STATUS']['values'][$resource->stat],
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ if ($semester = $this->getStartSemester($resource)) {
+ $relationships[self::REL_START_SEMESTER] = $semester;
+ }
+ if ($semester = $this->getEndSemester($resource)) {
+ $relationships[self::REL_END_SEMESTER] = $semester;
+ }
+ if ($responsible_department = $this->getResponsibleDepartment($resource)) {
+ $relationships[self::REL_RESPONSIBLE_DEPARTMENT] = $responsible_department;
+ }
+ /*
+ if (!empty($resource->responsible_institute)) {
+ $relationships[self::REL_RESPONSIBLE_DEPARTMENT] =
+ [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($resource->responsible_institute->institute),
+ ],
+ self::RELATIONSHIP_DATA => $resource->responsible_institute,
+ ];
+ }
+ */
+/*
+ $relationships = $this->addResponsibleDepartmentRelationship(
+ $relationships,
+ $resource,
+ $this->shouldInclude($context, self::REL_RESPONSIBLE_DEPARTMENT)
+ );
+*/
+ $relationships = $this->addDepartments(
+ $relationships,
+ $resource,
+ $this->shouldInclude($context, self::REL_DEPARTMENTS)
+ );
+ $relationships = $this->addModuleComponentsRelationship(
+ $relationships,
+ $resource,
+ $this->shouldInclude($context, self::REL_MODULE_COMPONENTS)
+ );
+
+ return $relationships;
+ }
+
+ private function getStartSemester(\Modul $modul)
+ {
+ if (!$semester = \Semester::find($modul->start)) {
+ return null;
+ }
+
+ return [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($semester),
+ ],
+ self::RELATIONSHIP_DATA => $semester,
+ ];
+ }
+
+ private function getEndSemester(\Modul $modul)
+ {
+ $semester = \Semester::find($modul->end);
+ if (!$semester) {
+ return null;
+ }
+
+ return [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($semester),
+ ],
+ self::RELATIONSHIP_DATA => $semester,
+ ];
+ }
+
+ private function getResponsibleDepartment(\Modul $modul)
+ {
+ $responsible_department = \Institute::build(['id' => $modul->responsible_institute->institut_id]);
+ if (!$responsible_department) {
+ return null;
+ }
+
+ return [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($responsible_department),
+ ],
+ self::RELATIONSHIP_DATA => $responsible_department,
+ ];
+ }
+
+ private function addDepartments(array $relationships, $resource, $includeData)
+ {
+ $departments = $resource->assigned_institutes->orderBy('position')->map(function (\ModulInst $module_inst) {
+ return \Institute::build(['id' => $module_inst->institut_id]);
+ });
+
+ $relationships[self::REL_DEPARTMENTS][self::RELATIONSHIP_DATA] = $departments;
+
+ return $relationships;
+ }
+
+ private function addModuleComponentsRelationship(array $relationships, $resource, $includeData)
+ {
+ $relationships[self::REL_MODULE_COMPONENTS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_MODULE_COMPONENTS),
+ ],
+ ];
+
+ $relationships[self::REL_MODULE_COMPONENTS][self::RELATIONSHIP_DATA] = $resource->modulteile;
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/ModuleComponent.php b/lib/classes/JsonApi/Schemas/ModuleComponent.php
new file mode 100644
index 0000000..7ff24e9
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/ModuleComponent.php
@@ -0,0 +1,70 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class ModuleComponent extends SchemaProvider
+{
+ const REL_COURSES = 'courses';
+ const TYPE = 'module-components';
+
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'name' => (string) $resource->deskriptoren->bezeichnung,
+ 'position' => $resource->position,
+ 'foreign_key' => $resource->flexnow_modul,
+ 'number' => $resource->nummer,
+ 'number_label' => \Config::get()->MVV_MODULTEIL['NUM_BEZEICHNUNG']['values'][$resource->num_bezeichnung] ?? '',
+ 'teaching_method' => \Config::get()->MVV_MODULTEIL['LERNLEHRFORM']['values'][$resource->lernlehrform] ?? '',
+ 'semester' => $resource->semester,
+ 'number_of_participants' => $resource->kapazitaet,
+ 'cp' => $resource->kp,
+ 'sws' => $resource->sws,
+ 'workload_compulsory' => $resource->wl_praesenz,
+ 'workload_preparation' => $resource->wl_bereitung,
+ 'workload_self' => $resource->wl_selbst,
+ 'workload_exam' => $resource->wl_pruef,
+ 'share_of_grade' => $resource->anteil_note,
+ 'compensable' => $resource->ausgleichbar,
+ 'compulsory_attendance' => $resource->pflicht,
+ 'prerequisites' => $resource->deskriptoren->voraussetzung,
+ 'comment' => $resource->deskriptoren->kommentar,
+ 'comment_capacity' => $resource->deskriptoren->kommentar_kapazitaet,
+ 'comment_wl_compulsory' => $resource->deskriptoren->kommentar_wl_praesenz,
+ 'comment_wl_preparation' => $resource->deskriptoren->kommentar_wl_bereitung,
+ 'comment_wl_self' => $resource->deskriptoren->kommentar_wl_selbst,
+ 'comment_wl_exam' => $resource->deskriptoren->kommentar_wl_pruef,
+ 'exam_prerequisites' => $resource->deskriptoren->pruef_vorleistung,
+ 'exam_requirements' => $resource->deskriptoren->pruef_leistung,
+ 'comment_compulsory_attendance' => $resource->deskriptoren->kommentar_pflicht,
+ 'type' => get_class($resource)
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $relationships = $this->addCoursesRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_COURSES));
+
+ return $relationships;
+ }
+
+ private function addCoursesRelationship(array $relationships, \Modulteil $resource, $includeData)
+ {
+ $relationships[self::REL_COURSES] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_COURSES),
+ ],
+ ];
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/ModuleInstitute.php b/lib/classes/JsonApi/Schemas/ModuleInstitute.php
new file mode 100644
index 0000000..d5b8e72
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/ModuleInstitute.php
@@ -0,0 +1,67 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class ModuleInstitute extends SchemaProvider
+{
+ const REL_MODULE = 'modules';
+ const REL_INSTITUTE = 'institutes';
+ const TYPE = 'module-institutes';
+
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'name' => (string) $resource->name,
+ 'short-name' => (string) $resource->name_kurz,
+ 'description' => (string) $resource->beschreibung,
+ 'type' => get_class($resource)
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $relationships = $this->addModuleRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_MODULE));
+ $relationships = $this->addInstituteRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_INSTITUTE));
+
+ return $relationships;
+ }
+
+ private function addModuleRelationship(array $relationships, $resource, $includeData)
+ {
+ $relationships[self::REL_MODULE] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_MODULE),
+ ],
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_MODULE][self::RELATIONSHIP_DATA] = $resource->module;
+ }
+
+ 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;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/Subject.php b/lib/classes/JsonApi/Schemas/Subject.php
new file mode 100644
index 0000000..d8fdd61
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Subject.php
@@ -0,0 +1,61 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Fach;
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class Subject extends SchemaProvider
+{
+ const REL_DEPARTMENTS = 'departments';
+ const TYPE = 'subjects';
+
+ /**
+ * @param Fach $resource
+ */
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ /**
+ * @param Fach $resource
+ */
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'name' => (string) $resource->name,
+ 'short-name' => (string) $resource->name_kurz,
+ 'description' => (string) $resource->beschreibung,
+ 'type' => get_class($resource)
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $relationships = $this->addDepartmentsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_DEPARTMENTS));
+
+ return $relationships;
+ }
+
+ private function addDepartmentsRelationship(array $relationships, $resource, $includeData)
+ {
+ $relationships[self::REL_DEPARTMENTS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_DEPARTMENTS),
+ ],
+ ];
+
+ if ($includeData) {
+ // use institute schema
+ if (!empty($resource->departments)) {
+ $institutes = \Institute::findMany($resource->departments->pluck('id'));
+ $relationships[self::REL_DEPARTMENTS][self::RELATIONSHIP_DATA] = $institutes;
+ }
+ }
+
+ return $relationships;
+ }
+}
diff --git a/lib/models/Institute.php b/lib/models/Institute.php
index 5ecdcef..e87a408 100644
--- a/lib/models/Institute.php
+++ b/lib/models/Institute.php
@@ -129,6 +129,11 @@ class Institute extends SimpleORMap implements Range
'order_by' => 'ORDER BY position',
'on_delete' => 'delete',
];
+ $config['has_many']['courses_of_study'] = [
+ 'class_name' => Studiengang::class,
+ 'assoc_foreign_key' => 'institut_id',
+ 'order_by' => 'ORDER BY name ASC',
+ ];
$config['additional_fields']['all_status_groups']['get'] = function ($institute) {
return Statusgruppen::findAllByRangeId($institute->id, true);
};
diff --git a/lib/models/Modulteil.php b/lib/models/Modulteil.php
index 397fc18..3b8f146 100644
--- a/lib/models/Modulteil.php
+++ b/lib/models/Modulteil.php
@@ -379,11 +379,11 @@ class Modulteil extends ModuleManagementModelTreeItem
/**
* Retrieves all courses this Modulteil is assigned by its LV-Gruppen.
* Filtered by a given semester considering the global visibility or the
- * the visibility for a given user.
+ * visibility for a given user.
*
* @param string $semester_id The id of a semester.
* @param mixed $only_visible Boolean true retrieves only visible courses, false
- * retrieves all courses. If $only_visible is an user id it depends on the users
+ * retrieves all courses. If $only_visible is a user id it depends on the users
* status which courses will be retrieved.
* @return array An array of course data.
*/
diff --git a/lib/models/StgteilVersion.php b/lib/models/StgteilVersion.php
index 5add60b..113875c 100644
--- a/lib/models/StgteilVersion.php
+++ b/lib/models/StgteilVersion.php
@@ -67,6 +67,14 @@ class StgteilVersion extends ModuleManagementModelTreeItem
'on_delete' => 'delete',
'on_store' => 'store'
];
+ $config['belongs_to']['start_semester'] = [
+ 'class_name' => Semester::class,
+ 'foreign_key' => 'start_sem',
+ ];
+ $config['belongs_to']['end_semester'] = [
+ 'class_name' => Semester::class,
+ 'foreign_key' => 'end_sem',
+ ];
$config['additional_fields']['count_abschnitte']['get'] =
function($version) { return $version->count_abschnitte; };
diff --git a/lib/models/Studiengang.php b/lib/models/Studiengang.php
index ee89abd..3c8a10f 100644
--- a/lib/models/Studiengang.php
+++ b/lib/models/Studiengang.php
@@ -171,7 +171,7 @@ class Studiengang extends ModuleManagementModelTreeItem
$config['i18n_fields']['name_kurz'] = true;
$config['i18n_fields']['beschreibung'] = true;
- $config['default_values']['enroll'] = $GLOBALS['MVV_STUDIENGANG']['ENROLL']['default'];
+ $config['default_values']['enroll'] = Config::get()->MVV_STUDIENGANG['ENROLL']['default'];
parent::configure($config);
}
@@ -648,7 +648,7 @@ class Studiengang extends ModuleManagementModelTreeItem
$result = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $status) {
$result[$status['stat']] = [
- 'name' => $GLOBALS['MVV_STUDIENGANG']['STATUS']['values'][$status['stat']]['name'] ?? _('Undefinierter Status'),
+ 'name' => Config::get()->MVV_STUDIENGANG['STATUS']['values'][$status['stat']]['name'] ?? _('Undefinierter Status'),
'count_objects' => $status['count_objects']
];
}
@@ -845,7 +845,7 @@ class Studiengang extends ModuleManagementModelTreeItem
{
$assigned_languages = array();
$languages_flipped = array_flip($languages);
- foreach ($GLOBALS['MVV_STUDIENGANG']['SPRACHE']['values'] as $key => $language) {
+ foreach (Config::get()->MVV_STUDIENGANG['SPRACHE']['values'] as $key => $language) {
if (isset($languages_flipped[$key])) {
$language = StudycourseLanguage::find([$this->id, $key]);
if (!$language) {
diff --git a/tests/jsonapi/_bootstrap.php b/tests/jsonapi/_bootstrap.php
index 01538aa..2b30aa9 100644
--- a/tests/jsonapi/_bootstrap.php
+++ b/tests/jsonapi/_bootstrap.php
@@ -28,6 +28,7 @@ $CACHING_ENABLE = false;
date_default_timezone_set('Europe/Berlin');
require 'config.inc.php';
+require 'mvv_config.php';
require_once __DIR__ . '/../../lib/bootstrap-autoload.php';