aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/JsonApi/Routes
diff options
context:
space:
mode:
Diffstat (limited to 'lib/classes/JsonApi/Routes')
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/Authority.php110
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesCreate.php125
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesDelete.php39
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesIndex.php108
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesShow.php49
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesUpdate.php122
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsByTaskIndex.php80
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsCreate.php184
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsDelete.php39
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsIndex.php80
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsOfProcessesIndex.php77
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsUpdate.php82
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php1
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/TasksIndex.php1
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/TasksShow.php4
15 files changed, 1100 insertions, 1 deletions
diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php
index 2acf83e..b5bee4a 100644
--- a/lib/classes/JsonApi/Routes/Courseware/Authority.php
+++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php
@@ -8,6 +8,8 @@ use Courseware\BlockFeedback;
use Courseware\Clipboard;
use Courseware\Container;
use Courseware\Instance;
+use Courseware\PeerReview;
+use Courseware\PeerReviewProcess;
use Courseware\StructuralElement;
use Courseware\StructuralElementComment;
use Courseware\StructuralElementFeedback;
@@ -324,7 +326,8 @@ class Authority
public static function canShowTask(User $user, Task $resource): bool
{
- return self::canUpdateTask($user, $resource);
+ return ($resource->isPeerReviewed() && $resource->isPeerReviewedBy($user)) ||
+ self::canUpdateTask($user, $resource);
}
public static function canIndexTasks(User $user): bool
@@ -584,4 +587,109 @@ class Authority
return $resource->user_id === $user->id;
}
+ public static function canIndexPeerReviewProcesses(User $user): bool
+ {
+ return (bool) $user;
+ }
+
+ public static function canShowPeerReviewProcess(User $user, PeerReviewProcess $process): bool
+ {
+ return $GLOBALS['perm']->have_studip_perm('user', $process->task_group['seminar_id'], $user->getId());
+ }
+
+ public static function canCreatePeerReviewProcesses(User $user, TaskGroup $taskGroup): bool
+ {
+ return $GLOBALS['perm']->have_studip_perm('tutor', $taskGroup['seminar_id'], $user->getId());
+ }
+
+ public static function canUpdatePeerReviewProcess(User $user, PeerReviewProcess $process): bool
+ {
+ return self::canCreatePeerReviewProcesses($user, $process->task_group);
+ }
+
+ public static function canDeletePeerReviewProcess(User $user, PeerReviewProcess $process): bool
+ {
+ return self::canCreatePeerReviewProcess($user, $process->task_group);
+ }
+
+ public static function canIndexPeerReviews(User $user)
+ {
+ // TODO: Reicht das? Werden die in der Route gefiltert? Brauchen das nur Lehrende?
+ return (bool) $user;
+ }
+
+ public static function canShowPeerReview(User $user, PeerReview $review): bool
+ {
+ $cid = $review->process->task_group['seminar_id'];
+ if ($GLOBALS['perm']->have_studip_perm('tutor', $cid, $user->getId())) {
+ return true;
+ }
+
+ return $review->isReviewer($user) ||
+ ($review->isSubmitter($user) && $review->process->getCurrentState() === PeerReviewProcess::STATE_AFTER);
+ }
+
+ public static function canShowPeerReviewReviewer(User $user, PeerReview $review): bool
+ {
+ $cid = $review->process->task_group['seminar_id'];
+ if ($GLOBALS['perm']->have_studip_perm('tutor', $cid, $user->getId())) {
+ return true;
+ }
+
+ if ($review->isReviewer($user)) {
+ return true;
+ }
+
+ return $review->isSubmitter($user) && !$review->isAnonymous();
+ }
+
+ public static function canShowPeerReviewSubmitter(User $user, PeerReview $review): bool
+ {
+ $cid = $review->process->task_group['seminar_id'];
+ if ($GLOBALS['perm']->have_studip_perm('tutor', $cid, $user->getId())) {
+ return true;
+ }
+
+ if ($review->isSubmitter($user)) {
+ return true;
+ }
+
+ return $review->isReviewer($user) && !$review->isAnonymous();
+ }
+
+ public static function canShowPeerReviewAssessment(User $user, PeerReview $review): bool
+ {
+ if ($review->isReviewer($user)) {
+ return true;
+ }
+
+ $isTutor = $GLOBALS['perm']->have_studip_perm(
+ 'tutor',
+ $review->process->task_group['seminar_id'],
+ $user->getId()
+ );
+
+ return ($isTutor || $review->isSubmitter($user)) &&
+ $review->process->getCurrentState() === PeerReviewProcess::STATE_AFTER;
+ }
+
+ public static function canIndexReviewsOfProcesses(User $user, PeerReviewProcess $process): bool
+ {
+ return self::canShowPeerReviewProcess($user, $process);
+ }
+
+ public static function canUpdatePeerReview(User $user, PeerReview $review): bool
+ {
+ return $review->process->getCurrentState() === PeerReviewProcess::STATE_ACTIVE && $review->isReviewer($user);
+ }
+
+ public static function canCreatePeerReviews(User $user, PeerReviewProcess $process): bool
+ {
+ return self::canCreatePeerReviewProcesses($user, $process->task_group);
+ }
+
+ public static function canDeletePeerReview(User $user, PeerReview $review): bool
+ {
+ return self::canCreatePeerReviews($user, $review->process);
+ }
}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesCreate.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesCreate.php
new file mode 100644
index 0000000..3de5832
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesCreate.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Courseware\PeerReviewProcess;
+use Courseware\TaskGroup;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Routes\TimestampTrait;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\PeerReviewProcess as PeerReviewProcessSchema;
+use JsonApi\Schemas\Courseware\TaskGroup as TaskGroupSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Create a PeerReviewProcess.
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class ProcessesCreate extends JsonApiController
+{
+ use TimestampTrait;
+ use ValidationTrait;
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ *
+ * @param array $args
+ *
+ * @return Response
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $json = $this->validate($request);
+ $taskGroup = $this->getTaskGroupFromJson($json);
+ $user = $this->getUser($request);
+
+ if (!Authority::canCreatePeerReviewProcesses($user, $taskGroup)) {
+ throw new AuthorizationFailedException();
+ }
+
+ $process = $this->create($user, $json);
+
+ return $this->getCreatedResponse($process);
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ *
+ * @param array $json
+ * @param mixed $data
+ *
+ * @return string|void
+ */
+ protected function validateResourceDocument($json, $data)
+ {
+ if (!self::arrayHas($json, 'data')) {
+ return 'Missing `data` member at document´s top level.';
+ }
+ if (PeerReviewProcessSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+ return 'Invalid `type` of document´s `data`.';
+ }
+ if (self::arrayHas($json, 'data.id')) {
+ return 'New document must not have an `id`.';
+ }
+
+ if (!self::arrayHas($json, 'data.attributes.configuration')) {
+ return 'Missing `configuration` attribute.';
+ }
+
+ if (!self::arrayHas($json, 'data.attributes.review-start')) {
+ return 'Missing `review-start` attribute.';
+ }
+ $startDate = self::arrayGet($json, 'data.attributes.review-start');
+ if (!self::isValidTimestamp($startDate)) {
+ return '`review-start` is not an ISO 8601 timestamp.';
+ }
+
+ if (!self::arrayHas($json, 'data.attributes.review-end')) {
+ return 'Missing `review-end` attribute.';
+ }
+ $endDate = self::arrayGet($json, 'data.attributes.review-end');
+ if (!self::isValidTimestamp($endDate)) {
+ return '`review-end` is not an ISO 8601 timestamp.';
+ }
+
+ if (!self::arrayHas($json, 'data.relationships.task-group')) {
+ return 'Missing `task-group` relationship.';
+ }
+ if (!$this->getTaskGroupFromJson($json)) {
+ return 'Invalid `task-group` relationship.';
+ }
+ }
+
+ private function getTaskGroupFromJson(array $json): ?TaskGroup
+ {
+ if (!$this->validateResourceObject($json, 'data.relationships.task-group', TaskGroupSchema::TYPE)) {
+ return null;
+ }
+ $resourceId = self::arrayGet($json, 'data.relationships.task-group.data.id');
+
+ return TaskGroup::find($resourceId);
+ }
+
+ private function create(\User $user, array $json): PeerReviewProcess
+ {
+ $taskGroup = $this->getTaskGroupFromJson($json);
+ $startDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.review-start'));
+ $endDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.review-end'));
+ $configuration = self::arrayGet($json, 'data.attributes.configuration');
+
+ /** @var PeerReviewProcess $process */
+ $process = PeerReviewProcess::create([
+ 'task_group_id' => $taskGroup->getId(),
+ 'owner_id' => $user->getId(),
+ 'configuration' => $configuration,
+ 'review_start' => $startDate->getTimestamp(),
+ 'review_end' => $endDate->getTimestamp(),
+ ]);
+
+ return $process;
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesDelete.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesDelete.php
new file mode 100644
index 0000000..b9ba42e
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesDelete.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Courseware\PeerPreviewProcess;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\Courseware\Authority;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Delete one PeerPreviewProcess.
+ */
+class ProcessesDelete extends JsonApiController
+{
+ /**
+ * @param array $args
+ * @return Response
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ /** @var ?PeerPreviewProcess $resource */
+ $resource = PeerPreviewProcess::find($args['id']);
+ if (!$resource) {
+ throw new RecordNotFoundException();
+ }
+ if (!Authority::canDeletePeerReviewProcess($this->getUser($request), $resource)) {
+ throw new AuthorizationFailedException();
+ }
+ $resource->delete();
+
+ return $this->getCodeResponse(204);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesIndex.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesIndex.php
new file mode 100644
index 0000000..42bee8a
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesIndex.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Course;
+use Courseware\PeerReviewProcess;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\Courses\Authority as CoursesAuthority;
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Schemas\Courseware\PeerReviewProcess as ProcessSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use User;
+
+/**
+ * Displays all visible PeerReviewProcesses.
+ *
+ * @SuppressWarnings(PHPMD.LongVariable)
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class ProcessesIndex extends JsonApiController
+{
+ protected $allowedFilteringParameters = ['cid'];
+
+ protected $allowedIncludePaths = [
+ ProcessSchema::REL_COURSE,
+ ProcessSchema::REL_OWNER,
+ ProcessSchema::REL_TASK_GROUP,
+ ];
+
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ *
+ * @param array $args
+ *
+ * @return Response
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $user = $this->getUser($request);
+ $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+
+ $this->validateFilters($filtering);
+ $this->authorize($user, $filtering);
+
+ $resources = empty($filtering) ? $this->findAllProcesses($user) : $this->filterProcesses($user, $filtering);
+
+ return $this->getPaginatedContentResponse(
+ array_slice($resources, ...$this->getOffsetAndLimit()),
+ count($resources)
+ );
+ }
+
+ /**
+ * @throws BadRequestException
+ */
+ private function validateFilters(array $filtering): void
+ {
+ if (isset($filtering['cid']) && !Course::exists($filtering['cid'])) {
+ throw new BadRequestException('Could not find a course matching this `filter[cid]`.');
+ }
+ }
+
+ /**
+ * @throws AuthorizationFailedException
+ */
+ private function authorize(User $user, array $filtering): void
+ {
+ if (!Authority::canIndexPeerReviewProcesses($user)) {
+ throw new AuthorizationFailedException();
+ }
+
+ if (isset($filtering['cid'])) {
+ if (
+ !CoursesAuthority::canShowCourse(
+ $user,
+ Course::find($filtering['cid']),
+ CoursesAuthority::SCOPE_EXTENDED
+ )
+ ) {
+ throw new AuthorizationFailedException();
+ }
+ }
+ }
+
+ private function findAllProcesses(User $user): iterable
+ {
+ return PeerReviewProcess::findByUser($user);
+ }
+
+ private function filterProcesses(User $user, array $filtering): iterable
+ {
+ if (isset($filtering['cid'])) {
+ /** @var ?\Course $course */
+ $course = \Course::find($filtering['cid']);
+
+ return array_filter(PeerReviewProcess::findByCourse($course), function ($process) use ($user) {
+ return Authority::canShowPeerReviewProcess($user, $process);
+ });
+ }
+
+ return [];
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesShow.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesShow.php
new file mode 100644
index 0000000..3d90421
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesShow.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Course;
+use Courseware\PeerReviewProcess;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Schemas\Courseware\PeerReviewProcess as ProcessSchema;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use User;
+
+/**
+ * Displays one PeerReviewProcess.
+ *
+ * @SuppressWarnings(PHPMD.LongVariable)
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class ProcessesShow extends JsonApiController
+{
+ protected $allowedIncludePaths = [
+ ProcessSchema::REL_COURSE,
+ ProcessSchema::REL_OWNER,
+ ProcessSchema::REL_TASK_GROUP,
+ ];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ * @param array $args
+ * @return Response
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ /** @var ?\Courseware\PeerReviewProcess $resource */
+ $resource = PeerReviewProcess::find($args['id']);
+ if (!$resource) {
+ throw new RecordNotFoundException();
+ }
+
+ if (!Authority::canShowPeerReviewProcess($this->getUser($request), $resource)) {
+ throw new AuthorizationFailedException();
+ }
+
+ return $this->getContentResponse($resource);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesUpdate.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesUpdate.php
new file mode 100644
index 0000000..5a0519e
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ProcessesUpdate.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Courseware\PeerReviewProcess;
+use Courseware\TaskGroup;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Schemas\Courseware\PeerReviewProcess as ProcessSchema;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\TimestampTrait;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\PeerReviewProcess as PeerReviewProcessSchema;
+use JsonApi\Schemas\Courseware\TaskGroup as TaskGroupSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use User;
+
+/**
+ * Updates one PeerReviewProcess.
+ *
+ * @SuppressWarnings(PHPMD.LongVariable)
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class ProcessesUpdate extends JsonApiController
+{
+ use TimestampTrait;
+ use ValidationTrait;
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ * @param array $args
+ * @return Response
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ /** @var ?\Courseware\PeerReviewProcess $resource */
+ $resource = PeerReviewProcess::find($args['id']);
+ if (!$resource) {
+ throw new RecordNotFoundException();
+ }
+ $json = $this->validate($request, $resource);
+ $user = $this->getUser($request);
+ if (!Authority::canUpdatePeerReviewProcess($user, $resource)) {
+ throw new AuthorizationFailedException();
+ }
+
+ $process = $this->update($user, $resource, $json);
+
+ return $this->getContentResponse($process);
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ *
+ * @param array $json
+ * @param mixed $data
+ *
+ * @return string|void
+ */
+ protected function validateResourceDocument($json, $data)
+ {
+ if (!self::arrayHas($json, 'data')) {
+ return 'Missing `data` member at document´s top level.';
+ }
+ if (PeerReviewProcessSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+ return 'Invalid `type` of document´s `data`.';
+ }
+
+ if (!self::arrayHas($json, 'data.attributes.configuration')) {
+ return 'Missing `configuration` attribute.';
+ }
+
+ if (!self::arrayHas($json, 'data.attributes.review-start')) {
+ return 'Missing `review-start` attribute.';
+ }
+ $startDate = self::arrayGet($json, 'data.attributes.review-start');
+ if (!self::isValidTimestamp($startDate)) {
+ return '`review-start` is not an ISO 8601 timestamp.';
+ }
+
+ if (!self::arrayHas($json, 'data.attributes.review-end')) {
+ return 'Missing `review-end` attribute.';
+ }
+ $endDate = self::arrayGet($json, 'data.attributes.review-end');
+ if (!self::isValidTimestamp($endDate)) {
+ return '`review-end` is not an ISO 8601 timestamp.';
+ }
+
+ if (self::arrayHas($json, 'data.relationships.task-group')) {
+ if (!$this->getTaskGroupFromJson($json)) {
+ return 'Invalid `task-group` relationship.';
+ }
+ }
+ }
+
+ private function getTaskGroupFromJson(array $json): ?TaskGroup
+ {
+ if (!$this->validateResourceObject($json, 'data.relationships.task-group', TaskGroupSchema::TYPE)) {
+ return null;
+ }
+ $resourceId = self::arrayGet($json, 'data.relationships.task-group.data.id');
+
+ return TaskGroup::find($resourceId);
+ }
+
+ private function update(User $user, PeerReviewProcess $process, array $json): PeerReviewProcess
+ {
+ $startDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.review-start'));
+ $endDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.review-end'));
+ $configuration = self::arrayGet($json, 'data.attributes.configuration');
+
+ $process->review_start = $startDate->getTimestamp();
+ $process->review_end = $endDate->getTimestamp();
+ $process->configuration = $configuration;
+
+ $process->store();
+
+ return $process;
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsByTaskIndex.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsByTaskIndex.php
new file mode 100644
index 0000000..fc3376f
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsByTaskIndex.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Courseware\PeerReview;
+use Courseware\PeerReviewProcess;
+use Courseware\Task;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\Courses\Authority as CoursesAuthority;
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Schemas\Courseware\PeerReview as PeerReviewSchema;
+use JsonApi\Schemas\Courseware\Task as TaskSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use User;
+
+/**
+ * Displays all PeerReviews of a course.
+ *
+ * @SuppressWarnings(PHPMD.LongVariable)
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class ReviewsByTaskIndex extends JsonApiController
+{
+ protected $allowedIncludePaths = [
+ PeerReviewSchema::REL_PROCESS,
+ PeerReviewSchema::REL_REVIEWER,
+ PeerReviewSchema::REL_SUBMITTER,
+ PeerReviewSchema::REL_TASK,
+ PeerReviewSchema::REL_TASK . '.' . TaskSchema::REL_STRUCTURAL_ELEMENT,
+ PeerReviewSchema::REL_TASK . '.' . TaskSchema::REL_TASK_GROUP,
+ ];
+
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ *
+ * @param array $args
+ *
+ * @return Response
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ /** @var ?Task $task */
+ $task = Task::find($args['id']);
+ if (!$task) {
+ throw new RecordNotFoundException();
+ }
+
+ $user = $this->getUser($request);
+ $this->authorize($user);
+
+ $resources = $this->findPeerReviews($task, $user);
+
+ return $this->getPaginatedContentResponse(
+ $resources->limit(...$this->getOffsetAndLimit()),
+ count($resources)
+ );
+ }
+
+ /**
+ * @throws AuthorizationFailedException
+ */
+ private function authorize(User $user): void
+ {
+ if (!Authority::canIndexPeerReviews($user)) {
+ throw new AuthorizationFailedException();
+ }
+ }
+
+ private function findPeerReviews(Task $task, User $user): iterable
+ {
+ return $task->peer_reviews->filter(function ($peerReview) use ($user) {
+ return Authority::canShowPeerReview($user, $peerReview);
+ });
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsCreate.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsCreate.php
new file mode 100644
index 0000000..26e566d
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsCreate.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Courseware\PeerReview;
+use Courseware\PeerReviewProcess;
+use Courseware\Task;
+use Courseware\TaskGroup;
+use InvalidArgumentException;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Routes\TimestampTrait;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\PeerReview as PeerReviewSchema;
+use JsonApi\Schemas\Courseware\PeerReviewProcess as PeerReviewProcessSchema;
+use JsonApi\Schemas\StatusGroup as StatusGroupSchema;
+use JsonApi\Schemas\User as UserSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Statusgruppen;
+use User;
+
+/**
+ * Create a PeerReview.
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class ReviewsCreate extends JsonApiController
+{
+ use ValidationTrait;
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ *
+ * @param array $args
+ *
+ * @return Response
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $json = $this->validate($request);
+ $process = $this->getProcessFromJson($json);
+ $user = $this->getUser($request);
+
+ if (!Authority::canCreatePeerReviews($user, $process)) {
+ throw new AuthorizationFailedException();
+ }
+
+ $resource = $this->create($json);
+
+ return $this->getCreatedResponse($resource);
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ *
+ * @param array $json
+ * @param mixed $data
+ *
+ * @return string|void
+ */
+ protected function validateResourceDocument($json, $data)
+ {
+ if (!self::arrayHas($json, 'data')) {
+ return 'Missing `data` member at document´s top level.';
+ }
+ if (PeerReviewSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+ return 'Invalid `type` of document´s `data`.';
+ }
+ if (self::arrayHas($json, 'data.id')) {
+ return 'New document must not have an `id`.';
+ }
+
+ // process
+ if (!self::arrayHas($json, 'data.relationships.process')) {
+ return 'Missing `process` relationship.';
+ }
+ if (!$this->getProcessFromJson($json)) {
+ return 'Invalid `process` relationship.';
+ }
+
+ // submitter
+ if (!self::arrayHas($json, 'data.relationships.submitter')) {
+ return 'Missing `submitter` relationship.';
+ }
+ if (!$this->getSubmitterFromJson($json)) {
+ return 'Invalid `submitter` relationship.';
+ }
+
+ // reviewer
+ if (!self::arrayHas($json, 'data.relationships.reviewer')) {
+ return 'Missing `reviewer` relationship.';
+ }
+ if (!$this->getReviewerFromJson($json)) {
+ return 'Invalid `reviewer` relationship.';
+ }
+ }
+
+ private function create(array $json): PeerReview
+ {
+ $process = $this->getProcessFromJson($json);
+ $reviewer = $this->getReviewerFromJson($json);
+ $submitter = $this->getSubmitterFromJson($json);
+
+ $task = $process['task_group']->findTaskBySolver($submitter);
+ $reviewerType = $this->getReviewerType($reviewer);
+
+ /** @var PeerReview $review */
+ $review = PeerReview::create([
+ 'process_id' => $process->id,
+ 'task_id' => $task->id,
+ 'submitter_id' => $submitter->id,
+ 'reviewer_id' => $reviewer->id,
+ 'reviewer_type' => $reviewerType,
+ ]);
+
+ return $review;
+ }
+
+ /**
+ * @return User|Statusgruppen|null
+ */
+ private function getActorFromJson(array $json, string $relation)
+ {
+ $relationship = 'data.relationships.' . $relation;
+ if (
+ !(
+ $this->validateResourceObject($json, $relationship, UserSchema::TYPE) ||
+ $this->validateResourceObject($json, $relationship, StatusGroupSchema::TYPE)
+ )
+ ) {
+ return null;
+ }
+ $resourceId = self::arrayGet($json, $relationship . '.data.id');
+
+ switch (self::arrayGet($json, $relationship . '.data.type')) {
+ case UserSchema::TYPE:
+ return User::find($resourceId);
+ case StatusGroupSchema::TYPE:
+ return Statusgruppen::find($resourceId);
+ }
+
+ throw new InvalidArgumentException();
+ }
+
+ private function getProcessFromJson(array $json): ?PeerReviewProcess
+ {
+ if (!$this->validateResourceObject($json, 'data.relationships.process', PeerReviewProcessSchema::TYPE)) {
+ return null;
+ }
+ $resourceId = self::arrayGet($json, 'data.relationships.process.data.id');
+
+ return PeerReviewProcess::find($resourceId);
+ }
+
+ /**
+ * @return User|Statusgruppen|null
+ */
+ private function getReviewerFromJson(array $json)
+ {
+ return $this->getActorFromJson($json, 'reviewer');
+ }
+
+ private function getReviewerType($reviewer): string
+ {
+ if ($reviewer instanceof User) {
+ return 'autor';
+ }
+ if ($reviewer instanceof Statusgruppen) {
+ return 'group';
+ }
+
+ throw new InvalidArgumentException();
+ }
+
+ /**
+ * @return User|Statusgruppen|null
+ */
+ private function getSubmitterFromJson(array $json)
+ {
+ return $this->getActorFromJson($json, 'submitter');
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsDelete.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsDelete.php
new file mode 100644
index 0000000..2b7edd6
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsDelete.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Courseware\PeerReview;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\Courseware\Authority;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Delete one PeerPreview.
+ */
+class ReviewsDelete extends JsonApiController
+{
+ /**
+ * @param array $args
+ * @return Response
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ /** @var ?PeerReview $resource */
+ $resource = PeerReview::find($args['id']);
+ if (!$resource) {
+ throw new RecordNotFoundException();
+ }
+ if (!Authority::canDeletePeerReview($this->getUser($request), $resource)) {
+ throw new AuthorizationFailedException();
+ }
+ $resource->delete();
+
+ return $this->getCodeResponse(204);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsIndex.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsIndex.php
new file mode 100644
index 0000000..91c7d58
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsIndex.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Course;
+use Courseware\PeerReview;
+use Courseware\PeerReviewProcess;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\Courses\Authority as CoursesAuthority;
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Schemas\Courseware\PeerReview as PeerReviewSchema;
+use JsonApi\Schemas\Courseware\Task as TaskSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use User;
+
+/**
+ * Displays all PeerReviews of a course.
+ *
+ * @SuppressWarnings(PHPMD.LongVariable)
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class ReviewsIndex extends JsonApiController
+{
+ protected $allowedIncludePaths = [
+ PeerReviewSchema::REL_PROCESS,
+ PeerReviewSchema::REL_REVIEWER,
+ PeerReviewSchema::REL_SUBMITTER,
+ PeerReviewSchema::REL_TASK,
+ PeerReviewSchema::REL_TASK . '.' . TaskSchema::REL_STRUCTURAL_ELEMENT,
+ PeerReviewSchema::REL_TASK . '.' . TaskSchema::REL_TASK_GROUP,
+ ];
+
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ *
+ * @param array $args
+ *
+ * @return Response
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ /** @var ?Course $course */
+ $course = Course::find($args['id']);
+ if (!$course) {
+ throw new RecordNotFoundException();
+ }
+
+ $user = $this->getUser($request);
+ $this->authorize($user);
+
+ $resources = $this->findPeerReviews($course, $user);
+
+ return $this->getPaginatedContentResponse(
+ array_slice($resources, ...$this->getOffsetAndLimit()),
+ count($resources)
+ );
+ }
+
+ /**
+ * @throws AuthorizationFailedException
+ */
+ private function authorize(User $user): void
+ {
+ if (!Authority::canIndexPeerReviews($user)) {
+ throw new AuthorizationFailedException();
+ }
+ }
+
+ private function findPeerReviews(Course $course, User $user): iterable
+ {
+ return array_filter(PeerReview::findByCourse($course), function ($peerReview) use ($user) {
+ return Authority::canShowPeerReview($user, $peerReview);
+ });
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsOfProcessesIndex.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsOfProcessesIndex.php
new file mode 100644
index 0000000..2a0b04d
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsOfProcessesIndex.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Course;
+use Courseware\PeerReview;
+use Courseware\PeerReviewProcess;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\Courses\Authority as CoursesAuthority;
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Schemas\Courseware\PeerReview as PeerReviewSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use User;
+
+/**
+ * Displays all visible PeerReviewProcesses.
+ *
+ * @SuppressWarnings(PHPMD.LongVariable)
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class ReviewsOfProcessesIndex extends JsonApiController
+{
+ protected $allowedIncludePaths = [
+ PeerReviewSchema::REL_PROCESS,
+ PeerReviewSchema::REL_REVIEWER,
+ PeerReviewSchema::REL_SUBMITTER,
+ PeerReviewSchema::REL_TASK,
+ ];
+
+ protected $allowedPagingParameters = ['offset', 'limit'];
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ *
+ * @param array $args
+ *
+ * @return Response
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ /** @var ?PeerReviewProcess $process */
+ $process = PeerReviewProcess::find($args['id']);
+ if (!$process) {
+ throw new RecordNotFoundException();
+ }
+
+ $user = $this->getUser($request);
+ $this->authorize($user, $process);
+
+ $resources = $this->findReviews($user, $process);
+
+ return $this->getPaginatedContentResponse(
+ $resources->limit(...$this->getOffsetAndLimit()),
+ count($resources)
+ );
+ }
+
+ /**
+ * @throws AuthorizationFailedException
+ */
+ private function authorize(User $user, PeerReviewProcess $process): void
+ {
+ if (!Authority::canIndexReviewsOfProcesses($user, $process)) {
+ throw new AuthorizationFailedException();
+ }
+ }
+
+ private function findReviews(User $user, PeerReviewProcess $process): iterable
+ {
+ return $process->peer_reviews->filter(function ($peerReview) use ($user) {
+ return Authority::canShowPeerReview($user, $peerReview);
+ });
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsUpdate.php b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsUpdate.php
new file mode 100644
index 0000000..cf3c600
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/PeerReview/ReviewsUpdate.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace JsonApi\Routes\Courseware\PeerReview;
+
+use Courseware\PeerReview;
+use Courseware\TaskGroup;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Routes\TimestampTrait;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Schemas\Courseware\PeerReview as PeerReviewSchema;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use User;
+
+/**
+ * Updates one PeerReview.
+ *
+ * @SuppressWarnings(PHPMD.LongVariable)
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class ReviewsUpdate extends JsonApiController
+{
+ use TimestampTrait;
+ use ValidationTrait;
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ * @param array $args
+ * @return Response
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ $resource = PeerReview::find($args['id']);
+ if (!$resource) {
+ throw new RecordNotFoundException();
+ }
+ $json = $this->validate($request, $resource);
+ $user = $this->getUser($request);
+ if (!Authority::canUpdatePeerReview($user, $resource)) {
+ throw new AuthorizationFailedException();
+ }
+
+ $review = $this->update($resource, $json);
+
+ return $this->getContentResponse($review);
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+ *
+ * @param array $json
+ * @param mixed $data
+ *
+ * @return string|void
+ */
+ protected function validateResourceDocument($json, $data)
+ {
+ if (!self::arrayHas($json, 'data')) {
+ return 'Missing `data` member at document´s top level.';
+ }
+ if (PeerReviewSchema::TYPE !== self::arrayGet($json, 'data.type')) {
+ return 'Invalid `type` of document´s `data`.';
+ }
+
+ if (!self::arrayHas($json, 'data.attributes.assessment')) {
+ return 'Missing `assessment` attribute.';
+ }
+
+ // TODO: validate assessment
+ }
+
+ private function update(PeerReview $review, array $json): PeerReview
+ {
+ $review->assessment = self::arrayGet($json, 'data.attributes.assessment');
+ $review->store();
+
+ return $review;
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php
index c8ebb86..ff3fba4 100644
--- a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php
+++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php
@@ -18,6 +18,7 @@ class TaskGroupsShow extends JsonApiController
protected $allowedIncludePaths = [
TaskGroupSchema::REL_COURSE,
TaskGroupSchema::REL_LECTURER,
+ TaskGroupSchema::REL_PEER_REVIEW_PROCESSES,
TaskGroupSchema::REL_SOLVERS,
TaskGroupSchema::REL_TARGET,
TaskGroupSchema::REL_TASK_TEMPLATE,
diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php b/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php
index 26a021c..9952437 100644
--- a/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php
+++ b/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php
@@ -25,6 +25,7 @@ class TasksIndex extends JsonApiController
TaskSchema::REL_STRUCTURAL_ELEMENT,
TaskSchema::REL_TASK_GROUP,
TaskSchema::REL_TASK_GROUP . '.' . TaskGroupSchema::REL_LECTURER,
+ TaskSchema::REL_TASK_GROUP . '.' . TaskGroupSchema::REL_PEER_REVIEW_PROCESSES,
];
/**
diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksShow.php b/lib/classes/JsonApi/Routes/Courseware/TasksShow.php
index 619e7ea..419f950 100644
--- a/lib/classes/JsonApi/Routes/Courseware/TasksShow.php
+++ b/lib/classes/JsonApi/Routes/Courseware/TasksShow.php
@@ -5,6 +5,7 @@ namespace JsonApi\Routes\Courseware;
use Courseware\Task;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\Schemas\Courseware\PeerReview as PeerReviewSchema;
use JsonApi\Schemas\Courseware\Task as TaskSchema;
use JsonApi\Schemas\Courseware\TaskGroup as TaskGroupSchema;
use JsonApi\JsonApiController;
@@ -18,10 +19,13 @@ class TasksShow extends JsonApiController
{
protected $allowedIncludePaths = [
TaskSchema::REL_FEEDBACK,
+ TaskSchema::REL_PEER_REVIEWS,
+ TaskSchema::REL_PEER_REVIEWS . '.' . PeerReviewSchema::REL_PROCESS,
TaskSchema::REL_SOLVER,
TaskSchema::REL_STRUCTURAL_ELEMENT,
TaskSchema::REL_TASK_GROUP,
TaskSchema::REL_TASK_GROUP . '.' . TaskGroupSchema::REL_LECTURER,
+ TaskSchema::REL_TASK_GROUP . '.' . TaskGroupSchema::REL_PEER_REVIEW_PROCESSES,
];
/**