Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
96.83% |
214 / 221 |
|
80.95% |
17 / 21 |
CRAP | |
0.00% |
0 / 1 |
| double_marking | |
96.83% |
214 / 221 |
|
80.95% |
17 / 21 |
101 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| for_cmid | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| assign | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| is_enabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| tolerance | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| rule | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| max_grade | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| unavailable_reason | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
| configure | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
6.13 | |||
| disable | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| delete_for_cmid | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| markers | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| students | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| allocate | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
6.13 | |||
| share_out | |
96.15% |
25 / 26 |
|
0.00% |
0 / 1 |
11 | |||
| record_mark | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
11 | |||
| agree | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
5.12 | |||
| save_agreed | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| release | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
10 | |||
| rows | |
100.00% |
50 / 50 |
|
100.00% |
1 / 1 |
23 | |||
| for_marker | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
10 | |||
| 1 | <?php |
| 2 | // This file is part of Moodle - https://moodle.org/ |
| 3 | // |
| 4 | // Moodle is free software: you can redistribute it and/or modify |
| 5 | // it under the terms of the GNU General Public License as published by |
| 6 | // the Free Software Foundation, either version 3 of the License, or |
| 7 | // (at your option) any later version. |
| 8 | // |
| 9 | // Moodle is distributed in the hope that it will be useful, |
| 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | // GNU General Public License for more details. |
| 13 | // |
| 14 | // You should have received a copy of the GNU General Public License |
| 15 | // along with Moodle. If not, see <https://www.gnu.org/licenses/>. |
| 16 | |
| 17 | namespace local_doublemark\local; |
| 18 | |
| 19 | use moodle_exception; |
| 20 | |
| 21 | /** |
| 22 | * Double marking on one assignment: settings, the two markers of each student, their marks, |
| 23 | * the agreed mark and its release to the assignment. |
| 24 | * |
| 25 | * Marks live in this plugin's tables until they are released, then go through |
| 26 | * assign::save_grade() like a grade typed by the lead, so the gradebook, marking |
| 27 | * workflow and feedback plugins behave as usual. |
| 28 | * |
| 29 | * @package local_doublemark |
| 30 | * @copyright 2026 DevLMS <contact@devlms.com> |
| 31 | * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later |
| 32 | */ |
| 33 | class double_marking { |
| 34 | /** A student without two markers yet. */ |
| 35 | public const UNALLOCATED = 'unallocated'; |
| 36 | /** The lead set the agreed mark. */ |
| 37 | public const RESOLVED = 'resolved'; |
| 38 | /** The agreed mark is in the assignment. */ |
| 39 | public const RELEASED = 'released'; |
| 40 | |
| 41 | /** @var int Course module id. */ |
| 42 | public readonly int $cmid; |
| 43 | |
| 44 | /** @var \stdClass|null Settings row, null when double marking is off. */ |
| 45 | private ?\stdClass $config; |
| 46 | |
| 47 | /** |
| 48 | * Use for_cmid(). |
| 49 | * |
| 50 | * @param \assign $assign |
| 51 | */ |
| 52 | private function __construct( |
| 53 | /** @var \assign The assignment. */ |
| 54 | private readonly \assign $assign, |
| 55 | ) { |
| 56 | global $DB; |
| 57 | $this->cmid = (int) $assign->get_course_module()->id; |
| 58 | $this->config = $DB->get_record('local_doublemark_config', ['cmid' => $this->cmid]) ?: null; |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Double marking for an assignment. |
| 63 | * |
| 64 | * @param int $cmid |
| 65 | * @return self |
| 66 | */ |
| 67 | public static function for_cmid(int $cmid): self { |
| 68 | global $CFG; |
| 69 | require_once($CFG->dirroot . '/mod/assign/locallib.php'); |
| 70 | [$course, $cm] = get_course_and_cm_from_cmid($cmid, 'assign'); |
| 71 | return new self(new \assign(\context_module::instance($cm->id), $cm, $course)); |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * The assignment. |
| 76 | * |
| 77 | * @return \assign |
| 78 | */ |
| 79 | public function assign(): \assign { |
| 80 | return $this->assign; |
| 81 | } |
| 82 | |
| 83 | /** |
| 84 | * Whether double marking is on for this assignment. |
| 85 | * |
| 86 | * @return bool |
| 87 | */ |
| 88 | public function is_enabled(): bool { |
| 89 | return $this->config !== null; |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Largest gap that still counts as agreement. |
| 94 | * |
| 95 | * @return float |
| 96 | */ |
| 97 | public function tolerance(): float { |
| 98 | return (float) ($this->config->tolerance ?? 0); |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * How agreeing marks become the agreed mark. |
| 103 | * |
| 104 | * @return string One of reconciler::RULES. |
| 105 | */ |
| 106 | public function rule(): string { |
| 107 | return $this->config->agreerule ?? reconciler::AVERAGE; |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Highest grade of the assignment. |
| 112 | * |
| 113 | * @return float |
| 114 | */ |
| 115 | public function max_grade(): float { |
| 116 | return (float) $this->assign->get_instance()->grade; |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * Why this assignment cannot use double marking, as a string key, or null when it can. |
| 121 | * |
| 122 | * @return string|null |
| 123 | */ |
| 124 | public function unavailable_reason(): ?string { |
| 125 | $instance = $this->assign->get_instance(); |
| 126 | if ((float) $instance->grade <= 0) { |
| 127 | return 'unavailable:scale'; |
| 128 | } |
| 129 | if (!empty($instance->teamsubmission)) { |
| 130 | return 'unavailable:team'; |
| 131 | } |
| 132 | if (get_grading_manager($this->assign->get_context(), 'mod_assign', 'submissions')->get_active_method()) { |
| 133 | return 'unavailable:advancedgrading'; |
| 134 | } |
| 135 | return null; |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Turn double marking on, or change its settings. |
| 140 | * |
| 141 | * @param float $tolerance |
| 142 | * @param string $rule |
| 143 | */ |
| 144 | public function configure(float $tolerance, string $rule): void { |
| 145 | global $DB, $USER; |
| 146 | if ($reason = $this->unavailable_reason()) { |
| 147 | throw new moodle_exception($reason, 'local_doublemark'); |
| 148 | } |
| 149 | if ($tolerance < 0 || $tolerance > $this->max_grade()) { |
| 150 | throw new moodle_exception('error:badtolerance', 'local_doublemark'); |
| 151 | } |
| 152 | if (!in_array($rule, reconciler::RULES, true)) { |
| 153 | throw new \coding_exception("Unknown agreement rule '$rule'"); |
| 154 | } |
| 155 | $row = (object) ['cmid' => $this->cmid, 'tolerance' => $tolerance, 'agreerule' => $rule, |
| 156 | 'usermodified' => (int) $USER->id, 'timemodified' => time()]; |
| 157 | if ($this->config) { |
| 158 | $row->id = $this->config->id; |
| 159 | $DB->update_record('local_doublemark_config', $row); |
| 160 | } else { |
| 161 | $row->id = $DB->insert_record('local_doublemark_config', $row); |
| 162 | } |
| 163 | $this->config = $row; |
| 164 | } |
| 165 | |
| 166 | /** |
| 167 | * Turn double marking off. Allocations and marks stay, so turning it back on loses nothing. |
| 168 | */ |
| 169 | public function disable(): void { |
| 170 | global $DB; |
| 171 | $DB->delete_records('local_doublemark_config', ['cmid' => $this->cmid]); |
| 172 | $this->config = null; |
| 173 | } |
| 174 | |
| 175 | /** |
| 176 | * Remove everything this plugin stored for a course module. |
| 177 | * |
| 178 | * @param int $cmid |
| 179 | */ |
| 180 | public static function delete_for_cmid(int $cmid): void { |
| 181 | global $DB; |
| 182 | foreach (['config', 'alloc', 'mark', 'agreed'] as $table) { |
| 183 | $DB->delete_records("local_doublemark_$table", ['cmid' => $cmid]); |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | /** |
| 188 | * People who can be markers: everyone with local/doublemark:mark on the assignment. |
| 189 | * |
| 190 | * @return \stdClass[] User records keyed by id, sorted by name. |
| 191 | */ |
| 192 | public function markers(): array { |
| 193 | $fields = 'u.id' . \core_user\fields::for_name()->get_sql('u')->selects; |
| 194 | return get_users_by_capability($this->assign->get_context(), 'local/doublemark:mark', $fields, 'u.lastname, u.firstname'); |
| 195 | } |
| 196 | |
| 197 | /** |
| 198 | * Students of the assignment, as the assignment itself lists them. |
| 199 | * |
| 200 | * @return \stdClass[] Keyed by user id. |
| 201 | */ |
| 202 | private function students(): array { |
| 203 | return $this->assign->list_participants(0, false); |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Set the two markers of a student. |
| 208 | * |
| 209 | * @param int $userid Student. |
| 210 | * @param int $marker1 |
| 211 | * @param int $marker2 |
| 212 | */ |
| 213 | public function allocate(int $userid, int $marker1, int $marker2): void { |
| 214 | global $DB; |
| 215 | if (!isset($this->students()[$userid])) { |
| 216 | throw new moodle_exception('error:notstudent', 'local_doublemark'); |
| 217 | } |
| 218 | $markers = $this->markers(); |
| 219 | if (!isset($markers[$marker1]) || !isset($markers[$marker2])) { |
| 220 | throw new moodle_exception('error:notmarker', 'local_doublemark'); |
| 221 | } |
| 222 | if ($marker1 === $marker2) { |
| 223 | throw new moodle_exception('error:samemarker', 'local_doublemark'); |
| 224 | } |
| 225 | $row = (object) ['cmid' => $this->cmid, 'userid' => $userid, 'marker1' => $marker1, 'marker2' => $marker2, |
| 226 | 'timemodified' => time()]; |
| 227 | if ($id = $DB->get_field('local_doublemark_alloc', 'id', ['cmid' => $this->cmid, 'userid' => $userid])) { |
| 228 | $row->id = $id; |
| 229 | $DB->update_record('local_doublemark_alloc', $row); |
| 230 | } else { |
| 231 | $DB->insert_record('local_doublemark_alloc', $row); |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | /** |
| 236 | * Give every student without two markers a pair, choosing the least loaded markers first. |
| 237 | * |
| 238 | * @param int[] $markerids At least two people who can mark. |
| 239 | * @return int How many students got markers. |
| 240 | */ |
| 241 | public function share_out(array $markerids): int { |
| 242 | global $DB; |
| 243 | $markerids = array_values(array_unique(array_map('intval', $markerids))); |
| 244 | $markers = $this->markers(); |
| 245 | if (count($markerids) < 2) { |
| 246 | throw new moodle_exception('error:fewmarkers', 'local_doublemark'); |
| 247 | } |
| 248 | foreach ($markerids as $id) { |
| 249 | if (!isset($markers[$id])) { |
| 250 | throw new moodle_exception('error:notmarker', 'local_doublemark'); |
| 251 | } |
| 252 | } |
| 253 | $allocs = $DB->get_records('local_doublemark_alloc', ['cmid' => $this->cmid], '', 'userid, marker1, marker2'); |
| 254 | $load = array_fill_keys($markerids, 0); |
| 255 | foreach ($allocs as $a) { |
| 256 | foreach ([(int) $a->marker1, (int) $a->marker2] as $m) { |
| 257 | if (isset($load[$m])) { |
| 258 | $load[$m]++; |
| 259 | } |
| 260 | } |
| 261 | } |
| 262 | $count = 0; |
| 263 | foreach (array_keys($this->students()) as $userid) { |
| 264 | $a = $allocs[$userid] ?? null; |
| 265 | if ($a && $a->marker1 && $a->marker2) { |
| 266 | continue; |
| 267 | } |
| 268 | // Least loaded first; ties keep the order the lead gave. |
| 269 | $order = $markerids; |
| 270 | usort($order, fn($x, $y) => [$load[$x], array_search($x, $markerids)] <=> [$load[$y], array_search($y, $markerids)]); |
| 271 | [$first, $second] = $order; |
| 272 | $this->allocate((int) $userid, $first, $second); |
| 273 | $load[$first]++; |
| 274 | $load[$second]++; |
| 275 | $count++; |
| 276 | } |
| 277 | return $count; |
| 278 | } |
| 279 | |
| 280 | /** |
| 281 | * Record the current user's mark for a student they are allocated to. |
| 282 | * |
| 283 | * @param int $userid Student. |
| 284 | * @param float $grade |
| 285 | * @param string $comment Private, for the other marker and the lead. |
| 286 | */ |
| 287 | public function record_mark(int $userid, float $grade, string $comment): void { |
| 288 | global $DB, $USER; |
| 289 | require_capability('local/doublemark:mark', $this->assign->get_context()); |
| 290 | $alloc = $DB->get_record('local_doublemark_alloc', ['cmid' => $this->cmid, 'userid' => $userid]); |
| 291 | $position = match (true) { |
| 292 | $alloc && (int) $alloc->marker1 === (int) $USER->id => 1, |
| 293 | $alloc && (int) $alloc->marker2 === (int) $USER->id => 2, |
| 294 | default => 0, |
| 295 | }; |
| 296 | if (!$position) { |
| 297 | throw new moodle_exception('error:notallocated', 'local_doublemark'); |
| 298 | } |
| 299 | if ( |
| 300 | $DB->record_exists_select( |
| 301 | 'local_doublemark_agreed', |
| 302 | 'cmid = ? AND userid = ? AND timereleased > 0', |
| 303 | [$this->cmid, $userid] |
| 304 | ) |
| 305 | ) { |
| 306 | throw new moodle_exception('error:released', 'local_doublemark'); |
| 307 | } |
| 308 | if ($grade < 0 || $grade > $this->max_grade()) { |
| 309 | throw new moodle_exception('error:range', 'local_doublemark', '', format_float($this->max_grade(), -1)); |
| 310 | } |
| 311 | $now = time(); |
| 312 | $existing = $DB->get_record('local_doublemark_mark', ['cmid' => $this->cmid, 'userid' => $userid, 'position' => $position]); |
| 313 | $row = (object) ['cmid' => $this->cmid, 'userid' => $userid, 'position' => $position, 'markerid' => (int) $USER->id, |
| 314 | 'grade' => $grade, 'comment' => $comment, 'timemodified' => $now]; |
| 315 | if ($existing) { |
| 316 | $row->id = $existing->id; |
| 317 | $DB->update_record('local_doublemark_mark', $row); |
| 318 | } else { |
| 319 | $row->timecreated = $now; |
| 320 | $DB->insert_record('local_doublemark_mark', $row); |
| 321 | } |
| 322 | \local_doublemark\event\mark_recorded::create([ |
| 323 | 'context' => $this->assign->get_context(), |
| 324 | 'relateduserid' => $userid, |
| 325 | 'other' => ['position' => $position], |
| 326 | ])->trigger(); |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * The lead sets the agreed mark of a student whose two marks are in. |
| 331 | * |
| 332 | * @param int $userid Student. |
| 333 | * @param float $grade |
| 334 | * @param string $note Why, for staff. |
| 335 | * @param string $feedback For the student, sent with the grade. |
| 336 | */ |
| 337 | public function agree(int $userid, float $grade, string $note, string $feedback): void { |
| 338 | global $USER; |
| 339 | require_capability('local/doublemark:manage', $this->assign->get_context()); |
| 340 | $row = $this->rows()[$userid] ?? null; |
| 341 | if (!$row || !in_array($row->status, [reconciler::AGREED, reconciler::RECONCILE, self::RESOLVED], true)) { |
| 342 | throw new moodle_exception('error:notmarked', 'local_doublemark'); |
| 343 | } |
| 344 | if ($grade < 0 || $grade > $this->max_grade()) { |
| 345 | throw new moodle_exception('error:range', 'local_doublemark', '', format_float($this->max_grade(), -1)); |
| 346 | } |
| 347 | $this->save_agreed($userid, ['grade' => $grade, 'note' => $note, 'feedback' => $feedback, 'source' => 'lead', |
| 348 | 'agreedby' => (int) $USER->id, 'timeagreed' => time()]); |
| 349 | \local_doublemark\event\mark_agreed::create([ |
| 350 | 'context' => $this->assign->get_context(), |
| 351 | 'relateduserid' => $userid, |
| 352 | ])->trigger(); |
| 353 | } |
| 354 | |
| 355 | /** |
| 356 | * Insert or update the agreed row of a student. |
| 357 | * |
| 358 | * @param int $userid |
| 359 | * @param array $fields |
| 360 | */ |
| 361 | private function save_agreed(int $userid, array $fields): void { |
| 362 | global $DB; |
| 363 | $existing = $DB->get_record('local_doublemark_agreed', ['cmid' => $this->cmid, 'userid' => $userid]); |
| 364 | $row = (object) (['cmid' => $this->cmid, 'userid' => $userid] + $fields); |
| 365 | if ($existing) { |
| 366 | $row->id = $existing->id; |
| 367 | $DB->update_record('local_doublemark_agreed', $row); |
| 368 | } else { |
| 369 | $DB->insert_record('local_doublemark_agreed', $row); |
| 370 | } |
| 371 | } |
| 372 | |
| 373 | /** |
| 374 | * Write every agreed mark that is not in the assignment yet to the assignment. |
| 375 | * |
| 376 | * @return int How many marks were released. |
| 377 | */ |
| 378 | public function release(): int { |
| 379 | global $USER; |
| 380 | require_capability('local/doublemark:manage', $this->assign->get_context()); |
| 381 | $instance = $this->assign->get_instance(); |
| 382 | $comments = $this->assign->get_feedback_plugin_by_type('comments'); |
| 383 | $count = 0; |
| 384 | foreach ($this->rows() as $userid => $row) { |
| 385 | if (!in_array($row->status, [reconciler::AGREED, self::RESOLVED], true)) { |
| 386 | continue; |
| 387 | } |
| 388 | $data = (object) ['grade' => $row->agreed, 'attemptnumber' => -1, |
| 389 | 'sendstudentnotifications' => (bool) $instance->sendstudentnotifications]; |
| 390 | if (!empty($instance->markingworkflow)) { |
| 391 | $data->workflowstate = ASSIGN_MARKING_WORKFLOW_STATE_RELEASED; |
| 392 | } |
| 393 | if ($comments && $comments->is_enabled()) { |
| 394 | // The comments plugin reads its editor on every save; keep what is there when there is no new feedback. |
| 395 | $grade = $this->assign->get_user_grade($userid, false); |
| 396 | $text = $row->feedback !== '' ? format_text($row->feedback, FORMAT_PLAIN) |
| 397 | : ($grade ? (string) $comments->get_editor_text('comments', $grade->id) : ''); |
| 398 | $data->assignfeedbackcomments_editor = ['text' => $text, 'format' => FORMAT_HTML]; |
| 399 | } |
| 400 | $this->assign->save_grade($userid, $data); |
| 401 | $fields = ['timereleased' => time(), 'releasedby' => (int) $USER->id]; |
| 402 | if ($row->status === reconciler::AGREED) { |
| 403 | $fields += ['grade' => $row->agreed, 'source' => 'rule', 'agreedby' => 0, 'timeagreed' => time()]; |
| 404 | } |
| 405 | $this->save_agreed($userid, $fields); |
| 406 | $count++; |
| 407 | } |
| 408 | if ($count) { |
| 409 | \local_doublemark\event\marks_released::create([ |
| 410 | 'context' => $this->assign->get_context(), |
| 411 | 'other' => ['count' => $count], |
| 412 | ])->trigger(); |
| 413 | } |
| 414 | return $count; |
| 415 | } |
| 416 | |
| 417 | /** |
| 418 | * Every student with markers, marks, status and agreed mark. |
| 419 | * |
| 420 | * @return \stdClass[] Keyed by student id, in the assignment's participant order. |
| 421 | */ |
| 422 | public function rows(): array { |
| 423 | global $DB; |
| 424 | $params = ['cmid' => $this->cmid]; |
| 425 | $allocs = $DB->get_records('local_doublemark_alloc', $params, '', 'userid, marker1, marker2'); |
| 426 | $agreed = $DB->get_records('local_doublemark_agreed', $params, '', 'userid, grade, feedback, note, source, timereleased'); |
| 427 | $marks = []; |
| 428 | foreach ($DB->get_records('local_doublemark_mark', $params) as $m) { |
| 429 | $marks[$m->userid][(int) $m->position] = (object) ['grade' => (float) $m->grade, 'comment' => (string) $m->comment, |
| 430 | 'markerid' => (int) $m->markerid, 'timemodified' => (int) $m->timemodified]; |
| 431 | } |
| 432 | $graded = $DB->get_fieldset_select( |
| 433 | 'assign_grades', |
| 434 | 'DISTINCT userid', |
| 435 | 'assignment = ? AND grade >= 0', |
| 436 | [$this->assign->get_instance()->id] |
| 437 | ); |
| 438 | $graded = array_flip(array_map('intval', $graded)); |
| 439 | $anonymous = $this->assign->is_blind_marking(); |
| 440 | |
| 441 | $rows = []; |
| 442 | foreach ($this->students() as $userid => $user) { |
| 443 | $userid = (int) $userid; |
| 444 | $a = $allocs[$userid] ?? null; |
| 445 | $ag = $agreed[$userid] ?? null; |
| 446 | $mark1 = $marks[$userid][1] ?? null; |
| 447 | $mark2 = $marks[$userid][2] ?? null; |
| 448 | $released = $ag && (int) $ag->timereleased > 0; |
| 449 | [$status, $ruleagreed] = reconciler::resolve($mark1?->grade, $mark2?->grade, $this->tolerance(), $this->rule()); |
| 450 | if ($released) { |
| 451 | $status = self::RELEASED; |
| 452 | } else if (!$a || !$a->marker1 || !$a->marker2) { |
| 453 | $status = self::UNALLOCATED; |
| 454 | } else if ($ag && $ag->source === 'lead' && $status !== reconciler::MARKING) { |
| 455 | $status = self::RESOLVED; |
| 456 | } |
| 457 | $rows[$userid] = (object) [ |
| 458 | 'userid' => $userid, |
| 459 | 'user' => $user, |
| 460 | 'anonymous' => $anonymous, |
| 461 | 'name' => $anonymous |
| 462 | ? get_string('participant', 'assign') . ' ' . $this->assign->get_uniqueid_for_user($userid) |
| 463 | : fullname($user), |
| 464 | 'marker1' => $a ? (int) $a->marker1 : 0, |
| 465 | 'marker2' => $a ? (int) $a->marker2 : 0, |
| 466 | 'mark1' => $mark1, |
| 467 | 'mark2' => $mark2, |
| 468 | 'gap' => $mark1 && $mark2 ? round(abs($mark1->grade - $mark2->grade), 2) : null, |
| 469 | 'status' => $status, |
| 470 | 'agreed' => $ag && ($released || $status === self::RESOLVED) ? (float) $ag->grade : $ruleagreed, |
| 471 | 'source' => $ag ? $ag->source : null, |
| 472 | 'feedback' => $ag ? (string) $ag->feedback : '', |
| 473 | 'note' => $ag ? (string) $ag->note : '', |
| 474 | // A grade typed in the assignment itself, outside double marking. |
| 475 | 'gradedoutside' => isset($graded[$userid]) && !$released, |
| 476 | ]; |
| 477 | } |
| 478 | return $rows; |
| 479 | } |
| 480 | |
| 481 | /** |
| 482 | * The students one marker marks, with their own mark and, once both are in, the other one. |
| 483 | * |
| 484 | * @param int $markerid |
| 485 | * @return \stdClass[] Keyed by student id. |
| 486 | */ |
| 487 | public function for_marker(int $markerid): array { |
| 488 | $mine = []; |
| 489 | foreach ($this->rows() as $userid => $row) { |
| 490 | $position = match ($markerid) { |
| 491 | $row->marker1 => 1, |
| 492 | $row->marker2 => 2, |
| 493 | default => 0, |
| 494 | }; |
| 495 | if (!$position) { |
| 496 | continue; |
| 497 | } |
| 498 | $both = $row->mark1 && $row->mark2; |
| 499 | $mine[$userid] = (object) [ |
| 500 | 'row' => $row, |
| 501 | 'position' => $position, |
| 502 | 'own' => $position === 1 ? $row->mark1 : $row->mark2, |
| 503 | 'other' => $both ? ($position === 1 ? $row->mark2 : $row->mark1) : null, |
| 504 | ]; |
| 505 | } |
| 506 | return $mine; |
| 507 | } |
| 508 | } |