aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-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
-rw-r--r--lib/models/Courseware/PeerReview.php100
-rw-r--r--lib/models/Courseware/PeerReviewProcess.php188
-rw-r--r--lib/models/Courseware/StructuralElement.php2
-rw-r--r--lib/models/Courseware/Task.php41
-rw-r--r--lib/models/Courseware/TaskGroup.php31
-rw-r--r--lib/modules/CoursewareModule.php6
27 files changed, 1718 insertions, 6 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;
+ }
}
diff --git a/lib/models/Courseware/PeerReview.php b/lib/models/Courseware/PeerReview.php
new file mode 100644
index 0000000..08ae2be
--- /dev/null
+++ b/lib/models/Courseware/PeerReview.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Courseware;
+
+use Course;
+use DBManager;
+use Statusgruppen;
+use User;
+
+/**
+ * Courseware's peer review instances.
+ *
+ * @since Stud.IP 5.5
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+class PeerReview extends \SimpleORMap
+{
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'cw_peer_reviews';
+
+ $config['serialized_fields']['assessment'] = 'JSONArrayObject';
+
+ $config['belongs_to']['process'] = [
+ 'class_name' => PeerReviewProcess::class,
+ 'foreign_key' => 'process_id',
+ ];
+ $config['belongs_to']['task'] = [
+ 'class_name' => Task::class,
+ 'foreign_key' => 'task_id',
+ ];
+ $config['belongs_to']['submitter'] = [
+ 'class_name' => User::class,
+ 'foreign_key' => 'submitter_id',
+ ];
+ $config['belongs_to']['reviewer'] = [
+ 'class_name' => User::class,
+ 'foreign_key' => 'reviewer_id',
+ ];
+
+ parent::configure($config);
+ }
+
+ public static function findByCourse(Course $course): iterable
+ {
+ $collections = [];
+ foreach (PeerReviewProcess::findByCourse($course) as $process) {
+ $collections[] = $process->getPeerReviews()->getArrayCopy();
+ }
+
+ return array_flatten($collections);
+ }
+
+ public function getCourse(): Course
+ {
+ return $this->process->getCourse();
+ }
+
+ public function isAnonymous(): bool
+ {
+ return $this->process->isAnonymous();
+ }
+
+ public function isReviewer(User $user): bool
+ {
+ switch ($this->reviewer_type) {
+ case 'autor':
+ return $this->reviewer_id === $user->getId();
+ case 'group':
+ return \Statusgruppen::isMemberOf($this->reviewer_id, $user->getId());
+ }
+ }
+
+ public function getReviewer(): User|Statusgruppen
+ {
+ switch ($this->reviewer_type) {
+ case 'autor':
+ return User::find($this->reviewer_id);
+ case 'group':
+ return Statusgruppen::find($this->reviewer_id);
+ }
+ }
+
+ public function isSubmitter(User $user): bool
+ {
+ return $this->submitter_id === $user->id;
+ }
+
+ public function getSubmitter(): User|Statusgruppen
+ {
+ $user = User::find($this->submitter_id);
+ if ($user) {
+ return $user;
+ }
+
+ $statusGroup = Statusgruppen::find($this->submitter_id);
+ return $statusGroup;
+ }
+}
diff --git a/lib/models/Courseware/PeerReviewProcess.php b/lib/models/Courseware/PeerReviewProcess.php
new file mode 100644
index 0000000..51c3c84
--- /dev/null
+++ b/lib/models/Courseware/PeerReviewProcess.php
@@ -0,0 +1,188 @@
+<?php
+
+namespace Courseware;
+
+use Course;
+use DBManager;
+use SimpleORMapCollection;
+use User;
+
+/**
+ * A PeerReviewProcess groups a set of PeerReviews.
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ *
+ * @since Stud.IP 5.5
+ */
+class PeerReviewProcess extends \SimpleORMap
+{
+ public const DEFAULT_DURATION = 7;
+
+ public const STATE_BEFORE = 'before';
+ public const STATE_ACTIVE = 'active';
+ public const STATE_AFTER = 'after';
+
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'cw_peer_review_processes';
+
+ $config['serialized_fields']['configuration'] = 'JSONArrayObject';
+
+ $config['belongs_to']['task_group'] = [
+ 'class_name' => TaskGroup::class,
+ 'foreign_key' => 'task_group_id',
+ ];
+ $config['belongs_to']['owner'] = [
+ 'class_name' => User::class,
+ 'foreign_key' => 'owner_id',
+ ];
+
+ $config['additional_fields']['peer_reviews'] = [
+ 'get' => 'getPeerReviews',
+ 'set' => false,
+ ];
+
+ $config['has_many']['_peer_reviews'] = [
+ 'class_name' => PeerReview::class,
+ 'assoc_foreign_key' => 'process_id',
+ 'on_delete' => 'delete',
+ 'on_store' => 'store',
+ 'order_by' => 'ORDER BY mkdate',
+ ];
+
+ parent::configure($config);
+ }
+
+ public static function findByCourse(Course $course): iterable
+ {
+ return self::findBySQL('task_group_id IN (?) ORDER BY mkdate', [
+ DBManager::get()->fetchFirst('SELECT id FROM `cw_task_groups` WHERE seminar_id = ?', [$course->getId()]),
+ ]);
+ }
+
+ public static function findByUser(User $user): iterable
+ {
+ return self::findMany(
+ DBManager::get()->fetchFirst(
+ 'SELECT id FROM cw_peer_review_processes
+ WHERE task_group_id IN (
+ SELECT id FROM cw_task_groups
+ WHERE cw_task_groups.seminar_id IN (
+ SELECT seminar_id FROM seminar_user WHERE user_id = ?))',
+ [$user->getId()]
+ )
+ );
+ }
+
+ public function getCourse(): Course
+ {
+ return $this->task_group->course;
+ }
+
+ public function getPeerReviews(): SimpleORMapCollection
+ {
+ $this->checkAutomaticPairing();
+
+ return SimpleORMapCollection::createFromArray(
+ PeerReview::findBySql('process_id = ? ORDER BY mkdate', [$this->getId()])
+ );
+ }
+
+ public function getDuration(): int
+ {
+ if (!isset($this->configuration['duration'])) {
+ return self::DEFAULT_DURATION;
+ }
+
+ return (int) $this->configuration['duration'];
+ }
+
+ public function isAnonymous(): bool
+ {
+ if (!isset($this->configuration['anonymous'])) {
+ return true;
+ }
+
+ return (bool) $this->configuration['automaticPairing'];
+ }
+
+ public function isAutomaticPairing(): bool
+ {
+ if (!isset($this->configuration['automaticPairing'])) {
+ return true;
+ }
+
+ return (bool) $this->configuration['automaticPairing'];
+ }
+
+ public function getCurrentState(int $date = null): string
+ {
+ if (is_null($date)) {
+ $date = time();
+ }
+
+ if ($this->review_end < $date) {
+ return self::STATE_AFTER;
+ }
+
+ if ($date < $this->review_start) {
+ return self::STATE_BEFORE;
+ }
+
+ return self::STATE_ACTIVE;
+ }
+
+ public function checkAutomaticPairing(): void
+ {
+ if ($this->isAutomaticPairing() && !$this->paired_at) {
+ $now = time();
+ if ($now > $this->review_start) {
+ $this->createAutomaticPairings();
+ $this->content['paired_at'] = $now;
+ $this->content_db['paired_at'] = $now;
+ $stmt = \DBManager::get()->prepare(
+ 'UPDATE `' . $this->db_table() . '` SET `paired_at` = ? WHERE id = ?'
+ );
+ $stmt->execute([$now, $this->getId()]);
+ }
+ }
+ }
+
+ public function createAutomaticPairings(): iterable
+ {
+ $taskGroup = $this->task_group;
+ $submitters = $taskGroup->getSubmitters();
+
+ if (count($submitters) < 2) {
+ return [];
+ }
+
+ shuffle($submitters);
+ $copy = $submitters;
+ array_push($copy, array_shift($copy));
+ $pairings = array_map(null, $submitters, $copy);
+
+ return array_map(function ($pairing) use ($taskGroup) {
+ list($submitter, $reviewer) = $pairing;
+ $task = $taskGroup->findTaskBySolver($submitter);
+
+ return PeerReview::create([
+ 'process_id' => $this->getId(),
+ 'task_id' => $task->getId(),
+ 'submitter_id' => $submitter->getId(),
+ 'reviewer_id' => $reviewer->getId(),
+ 'reviewer_type' => $reviewer instanceof User ? 'autor' : 'group',
+ ]);
+ }, $pairings);
+ }
+
+ public function rescheduleTo(int $newStartDate): void
+ {
+ $newEndDate = $newStartDate + $this->getDuration() * (24 * 60 * 60);
+ $this->setData([
+ "review_start" => $newStartDate,
+ "review_end" => $newEndDate,
+ ]);
+ $this->store();
+ }
+}
diff --git a/lib/models/Courseware/StructuralElement.php b/lib/models/Courseware/StructuralElement.php
index 9b68ea2..f15a706 100644
--- a/lib/models/Courseware/StructuralElement.php
+++ b/lib/models/Courseware/StructuralElement.php
@@ -391,7 +391,7 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac
return true;
}
- return $task->userIsASolver($user);
+ return $task->userIsASolver($user) || $task->userIsAPeerReviewer($user);
}
if ($this->canEdit($user)) {
diff --git a/lib/models/Courseware/Task.php b/lib/models/Courseware/Task.php
index d409676..3d82b64 100644
--- a/lib/models/Courseware/Task.php
+++ b/lib/models/Courseware/Task.php
@@ -78,6 +78,14 @@ class Task extends \SimpleORMap
'foreign_key' => 'feedback_id',
];
+ $config['has_many']['peer_reviews'] = [
+ 'class_name' => PeerReview::class,
+ 'assoc_foreign_key' => 'task_id',
+ 'on_delete' => 'delete',
+ 'on_store' => 'store',
+ 'order_by' => 'ORDER BY mkdate',
+ ];
+
$config['additional_fields']['solver'] = [
'get' => 'getSolver',
'set' => false,
@@ -162,6 +170,14 @@ class Task extends \SimpleORMap
}
/**
+ * @param \User|\Seminar_User $user
+ */
+ public function userIsAPeerReviewer($user): bool
+ {
+ return $this->isPeerReviewed() && $this->isPeerReviewedBy($user);
+ }
+
+ /**
* @return \User|\Statusgruppen|null the solver
*/
public function getSolver()
@@ -235,6 +251,31 @@ class Task extends \SimpleORMap
$this->store();
}
+ public function isPeerReviewed(): bool
+ {
+ return PeerReview::countBySql('task_id = ?', [$this->getId()]) !== 0;
+ }
+
+ /**
+ * @param \User|\Seminar_User $user
+ */
+ public function isPeerReviewedBy($user): bool
+ {
+ $sql = 'task_id = ? AND reviewer_id = ? AND reviewer_type = "autor"';
+ if (PeerReview::countBySql($sql, [$this->getId(), $user->id]) !== 0) {
+ return true;
+ }
+
+ $sql = 'SELECT reviewer_id FROM cw_peer_reviews WHERE task_id = ? AND reviewer_type = "group"';
+ foreach (\DBManager::get()->fetchFirst($sql, [$this->getId()]) as $reviewerId) {
+ if (\Statusgruppen::isMemberOf($reviewerId, $user->id)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private function getStructuralElementProgress(StructuralElement $structural_element): float
{
$containers = Container::findBySQL('structural_element_id = ?', [intval($structural_element->id)]);
diff --git a/lib/models/Courseware/TaskGroup.php b/lib/models/Courseware/TaskGroup.php
index 6902cb3..626e7cc 100644
--- a/lib/models/Courseware/TaskGroup.php
+++ b/lib/models/Courseware/TaskGroup.php
@@ -30,6 +30,7 @@ use User;
* @property \Course $course belongs_to \Course
* @property \Courseware\StructuralElement $target belongs_to Courseware\StructuralElement
* @property \SimpleORMapCollection $tasks has_many Courseware\Task
+ * @property \SimpleORMapCollection $peer_review_processes has_many Courseware\PeerReviewProcess
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
@@ -62,6 +63,16 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject
'order_by' => 'ORDER BY mkdate',
];
+ $config['has_many']['peer_review_processes'] = [
+ 'class_name' => PeerReviewProcess::class,
+ 'assoc_foreign_key' => 'task_group_id',
+ 'on_delete' => 'delete',
+ 'on_store' => 'store',
+ 'order_by' => 'ORDER BY mkdate',
+ ];
+
+ $config['registered_callbacks']['after_store'][] = 'cbAfterStore';
+
parent::configure($config);
}
@@ -109,6 +120,11 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject
);
}
+ public function hasPeerReviewProcesses(): bool
+ {
+ return PeerReviewProcess::countBySql('task_group_id = ?', [$this->getId()]) > 0;
+ }
+
/**
* Returns the task of this TaskGroup given to $solver.
*
@@ -130,4 +146,19 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject
return empty($row) ? null : Task::find($row['id']);
}
+ public function cbAfterStore(): void
+ {
+ if ($this->isFieldDirty('end_date')) {
+ $this->reschedulePeerReviewProcesses();
+ }
+ }
+
+ private function reschedulePeerReviewProcesses(): void
+ {
+ if ($this->hasPeerReviewProcesses()) {
+ foreach ($this->peer_review_processes as $process) {
+ $process->rescheduleTo($this->end_date);
+ }
+ }
+ }
}
diff --git a/lib/modules/CoursewareModule.php b/lib/modules/CoursewareModule.php
index 8deeda7..de85ddb 100644
--- a/lib/modules/CoursewareModule.php
+++ b/lib/modules/CoursewareModule.php
@@ -71,6 +71,12 @@ class CoursewareModule extends CorePlugin implements SystemPlugin, StudipModule
'tasks',
new Navigation(_('Aufgaben'), 'dispatch.php/course/courseware/tasks?cid=' . $courseId)
);
+ if (!$GLOBALS['perm']->have_studip_perm('tutor', $courseId)) {
+ $navigation->addSubNavigation(
+ 'peer-review',
+ new Navigation(_('Peer-Reviews'), 'dispatch.php/course/courseware/tasks/peer-review-processes?cid=' . $courseId)
+ );
+ }
$navigation->addSubNavigation(
'comments',
new Navigation(_('Kommentare und Anmerkungen'), 'dispatch.php/course/courseware/comments_overview?cid=' . $courseId)