aboutsummaryrefslogtreecommitdiff
path: root/lib/models
diff options
context:
space:
mode:
Diffstat (limited to 'lib/models')
-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
5 files changed, 361 insertions, 1 deletions
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);
+ }
+ }
+ }
}