diff options
Diffstat (limited to 'lib/models')
| -rw-r--r-- | lib/models/Courseware/PeerReview.php | 100 | ||||
| -rw-r--r-- | lib/models/Courseware/PeerReviewProcess.php | 188 | ||||
| -rw-r--r-- | lib/models/Courseware/StructuralElement.php | 2 | ||||
| -rw-r--r-- | lib/models/Courseware/Task.php | 41 | ||||
| -rw-r--r-- | lib/models/Courseware/TaskGroup.php | 31 |
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); + } + } + } } |
