aboutsummaryrefslogtreecommitdiff
path: root/lib/classes
diff options
context:
space:
mode:
authorMarcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>2023-11-14 11:57:16 +0100
committerMarcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>2024-07-09 09:19:01 +0200
commit62cc5d1f509b245159ffcbd0dbd08ab389e51615 (patch)
tree84070ab147fdfa4ecb26767f42de7d1374a304c1 /lib/classes
parent2aa22a3decc515ef19681e3fbb303e395bfef6d4 (diff)
Add Peer Review on top of feature/better-tasks.feature/peerreview-6
Diffstat (limited to 'lib/classes')
-rw-r--r--lib/classes/JsonApi/RouteMap.php16
-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
-rw-r--r--lib/classes/JsonApi/SchemaMap.php2
-rw-r--r--lib/classes/JsonApi/Schemas/Courseware/PeerReview.php99
-rw-r--r--lib/classes/JsonApi/Schemas/Courseware/PeerReviewProcess.php77
-rw-r--r--lib/classes/JsonApi/Schemas/Courseware/Task.php43
-rw-r--r--lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php18
21 files changed, 1351 insertions, 5 deletions
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index e9a0a01..e43e6a7 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -563,6 +563,22 @@ class RouteMap
$group->delete('/courseware-clipboards/{id}', Routes\Courseware\ClipboardsDelete::class);
$group->post('/courseware-clipboards/{id}/insert', Routes\Courseware\ClipboardsInsert::class);
+
+ $group->get('/courseware-peer-review-processes', Routes\Courseware\PeerReview\ProcessesIndex::class);
+ $group->get('/courseware-peer-review-processes/{id}', Routes\Courseware\PeerReview\ProcessesShow::class);
+ $group->get('/courseware-peer-review-processes/{id}/peer-reviews', Routes\Courseware\PeerReview\ReviewsOfProcessesIndex::class);
+
+ $group->patch('/courseware-peer-review-processes/{id}', Routes\Courseware\PeerReview\ProcessesUpdate::class);
+ $group->delete('/courseware-peer-review-processes/{id}', Routes\Courseware\PeerReview\ProcessesDelete::class);
+
+ $group->post('/courseware-peer-review-processes', Routes\Courseware\PeerReview\ProcessesCreate::class);
+
+ $group->get('/courses/{id}/courseware-peer-reviews', Routes\Courseware\PeerReview\ReviewsIndex::class);
+ $group->get('/courseware-tasks/{id}/peer-reviews', Routes\Courseware\PeerReview\ReviewsByTaskIndex::class);
+
+ $group->post('/courseware-peer-reviews', Routes\Courseware\PeerReview\ReviewsCreate::class);
+ $group->patch('/courseware-peer-reviews/{id}', Routes\Courseware\PeerReview\ReviewsUpdate::class);
+ $group->delete('/courseware-peer-reviews/{id}', Routes\Courseware\PeerReview\ReviewsDelete::class);
}
private function addAuthenticatedFilesRoutes(RouteCollectorProxy $group): void
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,
];
/**
diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php
index ff5040d..411757d 100644
--- a/lib/classes/JsonApi/SchemaMap.php
+++ b/lib/classes/JsonApi/SchemaMap.php
@@ -66,6 +66,8 @@ class SchemaMap
\Courseware\Clipboard::class => Schemas\Courseware\Clipboard::class,
\Courseware\Container::class => Schemas\Courseware\Container::class,
\Courseware\Instance::class => Schemas\Courseware\Instance::class,
+ \Courseware\PeerReview::class => Schemas\Courseware\PeerReview::class,
+ \Courseware\PeerReviewProcess::class => Schemas\Courseware\PeerReviewProcess::class,
\Courseware\PublicLink::class => Schemas\Courseware\PublicLink::class,
\Courseware\StructuralElement::class => Schemas\Courseware\StructuralElement::class,
\Courseware\StructuralElementComment::class => Schemas\Courseware\StructuralElementComment::class,
diff --git a/lib/classes/JsonApi/Schemas/Courseware/PeerReview.php b/lib/classes/JsonApi/Schemas/Courseware/PeerReview.php
new file mode 100644
index 0000000..0d2503c
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Courseware/PeerReview.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace JsonApi\Schemas\Courseware;
+
+use JsonApi\Routes\Courseware\Authority;
+use JsonApi\Schemas\SchemaProvider;
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class PeerReview extends SchemaProvider
+{
+ public const TYPE = 'courseware-peer-reviews';
+
+ public const REL_PROCESS = 'process';
+ public const REL_REVIEWER = 'reviewer';
+ public const REL_SUBMITTER = 'submitter';
+ public const REL_TASK = 'task';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ $user = $this->currentUser;
+ $assessment = null;
+ if ($resource->assessment && Authority::canShowPeerReviewAssessment($user, $resource)) {
+ $assessment = $resource->assessment->getIterator();
+ }
+ return [
+ 'assessment' => $assessment,
+ 'mkdate' => date('c', $resource['mkdate']),
+ 'chdate' => date('c', $resource['chdate']),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $relationships[self::REL_PROCESS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($resource->process),
+ ],
+ self::RELATIONSHIP_DATA => $resource->process,
+ ];
+
+ $user = $this->currentUser;
+
+ if (Authority::canShowPeerReviewReviewer($user, $resource)) {
+ $reviewer = $resource->getReviewer();
+ $relationships[self::REL_REVIEWER] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($reviewer),
+ ],
+ self::RELATIONSHIP_DATA => $reviewer,
+ ];
+ } else {
+ $relationships[self::REL_REVIEWER] = [
+ self::RELATIONSHIP_DATA => null,
+ ];
+ }
+
+ if (Authority::canShowPeerReviewSubmitter($user, $resource)) {
+ $submitter = $resource->getSubmitter();
+ $relationships[self::REL_SUBMITTER] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($submitter),
+ ],
+ self::RELATIONSHIP_DATA => $submitter,
+ ];
+ } else {
+ $relationships[self::REL_SUBMITTER] = [
+ self::RELATIONSHIP_DATA => null,
+ ];
+ }
+
+ $relationships[self::REL_TASK] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($resource->task),
+ ],
+ self::RELATIONSHIP_DATA => $resource->task,
+ ];
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/Courseware/PeerReviewProcess.php b/lib/classes/JsonApi/Schemas/Courseware/PeerReviewProcess.php
new file mode 100644
index 0000000..0eca67c
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Courseware/PeerReviewProcess.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace JsonApi\Schemas\Courseware;
+
+use JsonApi\Schemas\SchemaProvider;
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class PeerReviewProcess extends SchemaProvider
+{
+ const TYPE = 'courseware-peer-review-processes';
+
+ const REL_COURSE = 'course';
+ const REL_OWNER = 'owner';
+ const REL_PEER_REVIEWS = 'reviews';
+ const REL_TASK_GROUP = 'task-group';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'configuration' => $resource['configuration']->getIterator(),
+ 'review-start' => date('c', $resource['review_start']),
+ 'review-end' => date('c', $resource['review_end']),
+ 'mkdate' => date('c', $resource['mkdate']),
+ 'chdate' => date('c', $resource['chdate']),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $course = $resource->getCourse();
+ $relationships[self::REL_COURSE] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($course),
+ ],
+ self::RELATIONSHIP_DATA => $course,
+ ];
+
+ $relationships[self::REL_OWNER] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($resource->owner),
+ ],
+ self::RELATIONSHIP_DATA => $resource->owner,
+ ];
+
+ $relationships[self::REL_PEER_REVIEWS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_PEER_REVIEWS),
+ ],
+ ];
+
+ $relationships[self::REL_TASK_GROUP] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($resource->task_group),
+ ],
+ self::RELATIONSHIP_DATA => $resource->task_group,
+ ];
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/Courseware/Task.php b/lib/classes/JsonApi/Schemas/Courseware/Task.php
index 81c7a0d..cf41e62 100644
--- a/lib/classes/JsonApi/Schemas/Courseware/Task.php
+++ b/lib/classes/JsonApi/Schemas/Courseware/Task.php
@@ -13,6 +13,7 @@ class Task extends SchemaProvider
const TYPE = 'courseware-tasks';
const REL_FEEDBACK = 'task-feedback';
+ const REL_PEER_REVIEWS = 'peer-reviews';
const REL_SOLVER = 'solver';
const REL_STRUCTURAL_ELEMENT = 'structural-element';
const REL_TASK_GROUP = 'task-group';
@@ -30,12 +31,15 @@ class Task extends SchemaProvider
*/
public function getAttributes($resource, ContextInterface $context): iterable
{
+ $user = $this->currentUser;
+
return [
'progress' => (float) $resource->getTaskProgress(),
'submission-date' => date('c', $resource['submission_date']),
'submitted' => (bool) $resource['submitted'],
'renewal' => empty($resource['renewal']) ? null : (string) $resource['renewal'],
'renewal-date' => date('c', $resource['renewal_date']),
+ 'can-peer-review' => $resource->userIsAPeerReviewer($user),
'mkdate' => date('c', $resource['mkdate']),
'chdate' => date('c', $resource['chdate']),
];
@@ -58,13 +62,18 @@ class Task extends SchemaProvider
]
: [self::RELATIONSHIP_DATA => null];
- $solver = $resource->getSolver();
- $relationships[self::REL_SOLVER] = $solver
+ $relationships = $this->addPeerReviews(
+ $relationships,
+ $resource,
+ $this->shouldInclude($context, self::REL_PEER_REVIEWS)
+ );
+
+ $relationships[self::REL_SOLVER] = $resource['solver_id']
? [
self::RELATIONSHIP_LINKS => [
- Link::RELATED => $this->createLinkToResource($solver),
+ Link::RELATED => $this->createLinkToResource($resource->solver),
],
- self::RELATIONSHIP_DATA => $solver,
+ self::RELATIONSHIP_DATA => $resource->solver,
]
: [self::RELATIONSHIP_DATA => null];
@@ -86,4 +95,30 @@ class Task extends SchemaProvider
return $relationships;
}
+
+ /**
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+ private function addPeerReviews(array $relationships, TaskModel $resource, bool $includeData): array
+ {
+ $relationships[self::REL_PEER_REVIEWS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_PEER_REVIEWS),
+ ],
+ ];
+
+ if ($includeData) {
+ $data = [];
+ $user = $this->currentUser;
+ if ($resource->isPeerReviewedBy($this->currentUser)) {
+ $data = $resource->peer_reviews->filter(function ($review) use ($user) {
+ return CoursewareAuthority::canShowPeerReview($user, $review);
+ });
+ }
+
+ $relationships[self::REL_PEER_REVIEWS][self::RELATIONSHIP_DATA] = $data;
+ }
+
+ return $relationships;
+ }
}
diff --git a/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php b/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php
index c950671..6870664 100644
--- a/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php
+++ b/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php
@@ -15,6 +15,7 @@ class TaskGroup extends SchemaProvider
const REL_COURSE = 'course';
const REL_LECTURER = 'lecturer';
+ const REL_PEER_REVIEW_PROCESSES = 'peer-review-processes';
const REL_SOLVERS = 'solvers';
const REL_TARGET = 'target';
const REL_TASK_TEMPLATE = 'task-template';
@@ -68,6 +69,8 @@ class TaskGroup extends SchemaProvider
]
: [self::RELATIONSHIP_DATA => null];
+ $relationships = $this->addPeerReviewProcessesRelationship($relationships, $resource, $context);
+
$relationships[self::REL_SOLVERS] = [
self::RELATIONSHIP_DATA => $resource->getSolvers(),
];
@@ -104,4 +107,19 @@ class TaskGroup extends SchemaProvider
return $relationships;
}
+
+ private function addPeerReviewProcessesRelationship(iterable $relationships, TaskGroupModel $resource, ContextInterface $context): iterable
+ {
+ $relationships[self::REL_PEER_REVIEW_PROCESSES] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_PEER_REVIEW_PROCESSES),
+ ],
+ ];
+
+ if ($this->shouldInclude($context, self::REL_PEER_REVIEW_PROCESSES)) {
+ $relationships[self::REL_PEER_REVIEW_PROCESSES][self::RELATIONSHIP_DATA] = $resource->peer_review_processes;
+ }
+
+ return $relationships;
+ }
}