See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
1 <?php 2 3 // This file is part of Moodle - http://moodle.org/ 4 // 5 // Moodle is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // Moodle is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU General Public License for more details. 14 // 15 // You should have received a copy of the GNU General Public License 16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 17 18 /** 19 * Allocates the submissions randomly 20 * 21 * @package workshopallocation_random 22 * @subpackage mod_workshop 23 * @copyright 2009 David Mudrak <david.mudrak@gmail.com> 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 */ 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 global $CFG; // access to global variables during unit test 30 31 require_once (__DIR__ . '/../lib.php'); // interface definition 32 require_once (__DIR__ . '/../../locallib.php'); // workshop internal API 33 require_once (__DIR__ . '/settings_form.php'); // settings form 34 35 /** 36 * Allocates the submissions randomly 37 */ 38 class workshop_random_allocator implements workshop_allocator { 39 40 /** constants used to pass status messages between init() and ui() */ 41 const MSG_SUCCESS = 1; 42 43 /** workshop instance */ 44 protected $workshop; 45 46 /** mform with settings */ 47 protected $mform; 48 49 /** 50 * @param workshop $workshop Workshop API object 51 */ 52 public function __construct(workshop $workshop) { 53 $this->workshop = $workshop; 54 } 55 56 /** 57 * Allocate submissions as requested by user 58 * 59 * @return workshop_allocation_result 60 */ 61 public function init() { 62 global $PAGE; 63 64 $result = new workshop_allocation_result($this); 65 $customdata = array(); 66 $customdata['workshop'] = $this->workshop; 67 $this->mform = new workshop_random_allocator_form($PAGE->url, $customdata); 68 if ($this->mform->is_cancelled()) { 69 redirect($this->workshop->view_url()); 70 } else if ($settings = $this->mform->get_data()) { 71 $settings = workshop_random_allocator_setting::instance_from_object($settings); 72 $this->execute($settings, $result); 73 return $result; 74 } else { 75 // this branch is executed if the form is submitted but the data 76 // doesn't validate and the form should be redisplayed 77 // or on the first display of the form. 78 $result->set_status(workshop_allocation_result::STATUS_VOID); 79 return $result; 80 } 81 } 82 83 /** 84 * Executes the allocation based on the given settings 85 * 86 * @param workshop_random_allocator_setting $setting 87 * @param workshop_allocation_result allocation result logger 88 */ 89 public function execute(workshop_random_allocator_setting $settings, workshop_allocation_result $result) { 90 91 $authors = $this->workshop->get_potential_authors(); 92 $authors = $this->workshop->get_grouped($authors); 93 $reviewers = $this->workshop->get_potential_reviewers(!$settings->assesswosubmission); 94 $reviewers = $this->workshop->get_grouped($reviewers); 95 $assessments = $this->workshop->get_all_assessments(); 96 $newallocations = array(); // array of array(reviewer => reviewee) 97 98 if ($settings->numofreviews) { 99 if ($settings->removecurrent) { 100 // behave as if there were no current assessments 101 $curassessments = array(); 102 } else { 103 $curassessments = $assessments; 104 } 105 $options = array(); 106 $options['numofreviews'] = $settings->numofreviews; 107 $options['numper'] = $settings->numper; 108 $options['excludesamegroup'] = $settings->excludesamegroup; 109 $randomallocations = $this->random_allocation($authors, $reviewers, $curassessments, $result, $options); 110 $newallocations = array_merge($newallocations, $randomallocations); 111 $result->log(get_string('numofrandomlyallocatedsubmissions', 'workshopallocation_random', count($randomallocations))); 112 unset($randomallocations); 113 } 114 if ($settings->addselfassessment) { 115 $selfallocations = $this->self_allocation($authors, $reviewers, $assessments); 116 $newallocations = array_merge($newallocations, $selfallocations); 117 $result->log(get_string('numofselfallocatedsubmissions', 'workshopallocation_random', count($selfallocations))); 118 unset($selfallocations); 119 } 120 if (empty($newallocations)) { 121 $result->log(get_string('noallocationtoadd', 'workshopallocation_random'), 'info'); 122 } else { 123 $newnonexistingallocations = $newallocations; 124 $this->filter_current_assessments($newnonexistingallocations, $assessments); 125 $this->add_new_allocations($newnonexistingallocations, $authors, $reviewers); 126 $allreviewers = $reviewers[0]; 127 $allreviewersreloaded = false; 128 foreach ($newallocations as $newallocation) { 129 $reviewerid = key($newallocation); 130 $authorid = current($newallocation); 131 $a = new stdClass(); 132 if (isset($allreviewers[$reviewerid])) { 133 $a->reviewername = fullname($allreviewers[$reviewerid]); 134 } else { 135 // this may happen if $settings->assesswosubmission is false but the reviewer 136 // of the re-used assessment has not submitted anything. let us reload 137 // the list of reviewers name including those without their submission 138 if (!$allreviewersreloaded) { 139 $allreviewers = $this->workshop->get_potential_reviewers(false); 140 $allreviewersreloaded = true; 141 } 142 if (isset($allreviewers[$reviewerid])) { 143 $a->reviewername = fullname($allreviewers[$reviewerid]); 144 } else { 145 // this should not happen usually unless the list of participants was changed 146 // in between two cycles of allocations 147 $a->reviewername = '#'.$reviewerid; 148 } 149 } 150 if (isset($authors[0][$authorid])) { 151 $a->authorname = fullname($authors[0][$authorid]); 152 } else { 153 $a->authorname = '#'.$authorid; 154 } 155 if (in_array($newallocation, $newnonexistingallocations)) { 156 $result->log(get_string('allocationaddeddetail', 'workshopallocation_random', $a), 'ok', 1); 157 } else { 158 $result->log(get_string('allocationreuseddetail', 'workshopallocation_random', $a), 'ok', 1); 159 } 160 } 161 } 162 if ($settings->removecurrent) { 163 $delassessments = $this->get_unkept_assessments($assessments, $newallocations, $settings->addselfassessment); 164 // random allocator should not be able to delete assessments that have already been graded 165 // by reviewer 166 $result->log(get_string('numofdeallocatedassessment', 'workshopallocation_random', count($delassessments)), 'info'); 167 foreach ($delassessments as $delassessmentkey => $delassessmentid) { 168 $author = (object) []; 169 $reviewer = (object) []; 170 username_load_fields_from_object($author, $assessments[$delassessmentid], 'author'); 171 username_load_fields_from_object($reviewer, $assessments[$delassessmentid], 'reviewer'); 172 $a = [ 173 'authorname' => fullname($author), 174 'reviewername' => fullname($reviewer), 175 ]; 176 if (!is_null($assessments[$delassessmentid]->grade)) { 177 $result->log(get_string('allocationdeallocategraded', 'workshopallocation_random', $a), 'error', 1); 178 unset($delassessments[$delassessmentkey]); 179 } else { 180 $result->log(get_string('assessmentdeleteddetail', 'workshopallocation_random', $a), 'info', 1); 181 } 182 } 183 $this->workshop->delete_assessment($delassessments); 184 } 185 $result->set_status(workshop_allocation_result::STATUS_EXECUTED); 186 } 187 188 /** 189 * Returns the HTML code to print the user interface 190 */ 191 public function ui() { 192 global $PAGE; 193 194 $output = $PAGE->get_renderer('mod_workshop'); 195 196 $m = optional_param('m', null, PARAM_INT); // status message code 197 $message = new workshop_message(); 198 if ($m == self::MSG_SUCCESS) { 199 $message->set_text(get_string('randomallocationdone', 'workshopallocation_random')); 200 $message->set_type(workshop_message::TYPE_OK); 201 } 202 203 $out = $output->container_start('random-allocator'); 204 $out .= $output->render($message); 205 // the nasty hack follows to bypass the sad fact that moodle quickforms do not allow to actually 206 // return the HTML content, just to display it 207 ob_start(); 208 $this->mform->display(); 209 $out .= ob_get_contents(); 210 ob_end_clean(); 211 212 // if there are some not-grouped participant in a group mode, warn the user 213 $gmode = groups_get_activity_groupmode($this->workshop->cm, $this->workshop->course); 214 if (VISIBLEGROUPS == $gmode or SEPARATEGROUPS == $gmode) { 215 $users = $this->workshop->get_potential_authors() + $this->workshop->get_potential_reviewers(); 216 $users = $this->workshop->get_grouped($users); 217 if (isset($users[0])) { 218 $nogroupusers = $users[0]; 219 foreach ($users as $groupid => $groupusers) { 220 if ($groupid == 0) { 221 continue; 222 } 223 foreach ($groupusers as $groupuserid => $groupuser) { 224 unset($nogroupusers[$groupuserid]); 225 } 226 } 227 if (!empty($nogroupusers)) { 228 $list = array(); 229 foreach ($nogroupusers as $nogroupuser) { 230 $list[] = fullname($nogroupuser); 231 } 232 $a = implode(', ', $list); 233 $out .= $output->box(get_string('nogroupusers', 'workshopallocation_random', $a), 'generalbox warning nogroupusers'); 234 } 235 } 236 } 237 238 // TODO $out .= $output->heading(get_string('stats', 'workshopallocation_random')); 239 240 $out .= $output->container_end(); 241 242 return $out; 243 } 244 245 /** 246 * Delete all data related to a given workshop module instance 247 * 248 * This plugin does not store any data. 249 * 250 * @see workshop_delete_instance() 251 * @param int $workshopid id of the workshop module instance being deleted 252 * @return void 253 */ 254 public static function delete_instance($workshopid) { 255 return; 256 } 257 258 /** 259 * Return an array of possible numbers of reviews to be done 260 * 261 * Should contain numbers 1, 2, 3, ... 10 and possibly others up to a reasonable value 262 * 263 * @return array of integers 264 */ 265 public static function available_numofreviews_list() { 266 $options = array(); 267 $options[30] = 30; 268 $options[20] = 20; 269 $options[15] = 15; 270 for ($i = 10; $i >= 0; $i--) { 271 $options[$i] = $i; 272 } 273 return $options; 274 } 275 276 /** 277 * Allocates submissions to their authors for review 278 * 279 * If the submission has already been allocated, it is skipped. If the author is not found among 280 * reviewers, the submission is not assigned. 281 * 282 * @param array $authors grouped of {@see workshop::get_potential_authors()} 283 * @param array $reviewers grouped by {@see workshop::get_potential_reviewers()} 284 * @param array $assessments as returned by {@see workshop::get_all_assessments()} 285 * @return array of new allocations to be created, array of array(reviewerid => authorid) 286 */ 287 protected function self_allocation($authors=array(), $reviewers=array(), $assessments=array()) { 288 if (!isset($authors[0]) || !isset($reviewers[0])) { 289 // no authors or no reviewers 290 return array(); 291 } 292 $alreadyallocated = array(); 293 foreach ($assessments as $assessment) { 294 if ($assessment->authorid == $assessment->reviewerid) { 295 $alreadyallocated[$assessment->authorid] = 1; 296 } 297 } 298 $add = array(); // list of new allocations to be created 299 foreach ($authors[0] as $authorid => $author) { 300 // for all authors in all groups 301 if (isset($reviewers[0][$authorid])) { 302 // if the author can be reviewer 303 if (!isset($alreadyallocated[$authorid])) { 304 // and the allocation does not exist yet, then 305 $add[] = array($authorid => $authorid); 306 } 307 } 308 } 309 return $add; 310 } 311 312 /** 313 * Creates new assessment records 314 * 315 * @param array $newallocations pairs 'reviewerid' => 'authorid' 316 * @param array $dataauthors authors by group, group [0] contains all authors 317 * @param array $datareviewers reviewers by group, group [0] contains all reviewers 318 * @return bool 319 */ 320 protected function add_new_allocations(array $newallocations, array $dataauthors, array $datareviewers) { 321 global $DB; 322 323 $newallocations = $this->get_unique_allocations($newallocations); 324 $authorids = $this->get_author_ids($newallocations); 325 $submissions = $this->workshop->get_submissions($authorids); 326 $submissions = $this->index_submissions_by_authors($submissions); 327 foreach ($newallocations as $newallocation) { 328 $reviewerid = key($newallocation); 329 $authorid = current($newallocation); 330 if (!isset($submissions[$authorid])) { 331 throw new moodle_exception('unabletoallocateauthorwithoutsubmission', 'workshop'); 332 } 333 $submission = $submissions[$authorid]; 334 $status = $this->workshop->add_allocation($submission, $reviewerid, 1, true); // todo configurable weight? 335 if (workshop::ALLOCATION_EXISTS == $status) { 336 debugging('newallocations array contains existing allocation, this should not happen'); 337 } 338 } 339 } 340 341 /** 342 * Flips the structure of submission so it is indexed by authorid attribute 343 * 344 * It is the caller's responsibility to make sure the submissions are not teacher 345 * examples so no user is the author of more submissions. 346 * 347 * @param string $submissions array indexed by submission id 348 * @return array indexed by author id 349 */ 350 protected function index_submissions_by_authors($submissions) { 351 $byauthor = array(); 352 if (is_array($submissions)) { 353 foreach ($submissions as $submissionid => $submission) { 354 if (isset($byauthor[$submission->authorid])) { 355 throw new moodle_exception('moresubmissionsbyauthor', 'workshop'); 356 } 357 $byauthor[$submission->authorid] = $submission; 358 } 359 } 360 return $byauthor; 361 } 362 363 /** 364 * Extracts unique list of authors' IDs from the structure of new allocations 365 * 366 * @param array $newallocations of pairs 'reviewerid' => 'authorid' 367 * @return array of authorids 368 */ 369 protected function get_author_ids($newallocations) { 370 $authors = array(); 371 foreach ($newallocations as $newallocation) { 372 $authorid = reset($newallocation); 373 if (!in_array($authorid, $authors)) { 374 $authors[] = $authorid; 375 } 376 } 377 return $authors; 378 } 379 380 /** 381 * Removes duplicate allocations 382 * 383 * @param mixed $newallocations array of 'reviewerid' => 'authorid' pairs 384 * @return array 385 */ 386 protected function get_unique_allocations($newallocations) { 387 return array_merge(array_map('unserialize', array_unique(array_map('serialize', $newallocations)))); 388 } 389 390 /** 391 * Returns the list of assessments to remove 392 * 393 * If user selects "removecurrentallocations", we should remove all current assessment records 394 * and insert new ones. But this would needlessly waste table ids. Instead, let us find only those 395 * assessments that have not been re-allocated in this run of allocation. So, the once-allocated 396 * submissions are kept with their original id. 397 * 398 * @param array $assessments list of current assessments 399 * @param mixed $newallocations array of 'reviewerid' => 'authorid' pairs 400 * @param bool $keepselfassessments do not remove already allocated self assessments 401 * @return array of assessments ids to be removed 402 */ 403 protected function get_unkept_assessments($assessments, $newallocations, $keepselfassessments) { 404 $keepids = array(); // keep these assessments 405 foreach ($assessments as $assessmentid => $assessment) { 406 $aaid = $assessment->authorid; 407 $arid = $assessment->reviewerid; 408 if (($keepselfassessments) && ($aaid == $arid)) { 409 $keepids[$assessmentid] = null; 410 continue; 411 } 412 foreach ($newallocations as $newallocation) { 413 $nrid = key($newallocation); 414 $naid = current($newallocation); 415 if (array($arid, $aaid) == array($nrid, $naid)) { 416 // re-allocation found - let us continue with the next assessment 417 $keepids[$assessmentid] = null; 418 continue 2; 419 } 420 } 421 } 422 return array_keys(array_diff_key($assessments, $keepids)); 423 } 424 425 /** 426 * Allocates submission reviews randomly 427 * 428 * The algorithm of this function has been described at http://moodle.org/mod/forum/discuss.php?d=128473 429 * Please see the PDF attached to the post before you study the implementation. The goal of the function 430 * is to connect each "circle" (circles are representing either authors or reviewers) with a required 431 * number of "squares" (the other type than circles are). 432 * 433 * The passed $options array must provide keys: 434 * (int)numofreviews - number of reviews to be allocated to each circle 435 * (int)numper - what user type the circles represent. 436 * (bool)excludesamegroup - whether to prevent peer submissions from the same group in visible group mode 437 * 438 * @param array $authors structure of grouped authors 439 * @param array $reviewers structure of grouped reviewers 440 * @param array $assessments currently assigned assessments to be kept 441 * @param workshop_allocation_result $result allocation result logger 442 * @param array $options allocation options 443 * @return array array of (reviewerid => authorid) pairs 444 */ 445 protected function random_allocation($authors, $reviewers, $assessments, $result, array $options) { 446 if (empty($authors) || empty($reviewers)) { 447 // nothing to be done 448 return array(); 449 } 450 451 $numofreviews = $options['numofreviews']; 452 $numper = $options['numper']; 453 454 if (workshop_random_allocator_setting::NUMPER_SUBMISSION == $numper) { 455 // circles are authors, squares are reviewers 456 $result->log(get_string('resultnumperauthor', 'workshopallocation_random', $numofreviews), 'info'); 457 $allcircles = $authors; 458 $allsquares = $reviewers; 459 // get current workload 460 list($circlelinks, $squarelinks) = $this->convert_assessments_to_links($assessments); 461 } elseif (workshop_random_allocator_setting::NUMPER_REVIEWER == $numper) { 462 // circles are reviewers, squares are authors 463 $result->log(get_string('resultnumperreviewer', 'workshopallocation_random', $numofreviews), 'info'); 464 $allcircles = $reviewers; 465 $allsquares = $authors; 466 // get current workload 467 list($squarelinks, $circlelinks) = $this->convert_assessments_to_links($assessments); 468 } else { 469 throw new moodle_exception('unknownusertypepassed', 'workshop'); 470 } 471 // get the users that are not in any group. in visible groups mode, these users are exluded 472 // from allocation by this method 473 // $nogroupcircles is array (int)$userid => undefined 474 if (isset($allcircles[0])) { 475 $nogroupcircles = array_flip(array_keys($allcircles[0])); 476 } else { 477 $nogroupcircles = array(); 478 } 479 foreach ($allcircles as $circlegroupid => $circles) { 480 if ($circlegroupid == 0) { 481 continue; 482 } 483 foreach ($circles as $circleid => $circle) { 484 unset($nogroupcircles[$circleid]); 485 } 486 } 487 // $result->log('circle links = ' . json_encode($circlelinks), 'debug'); 488 // $result->log('square links = ' . json_encode($squarelinks), 'debug'); 489 $squareworkload = array(); // individual workload indexed by squareid 490 $squaregroupsworkload = array(); // group workload indexed by squaregroupid 491 foreach ($allsquares as $squaregroupid => $squares) { 492 $squaregroupsworkload[$squaregroupid] = 0; 493 foreach ($squares as $squareid => $square) { 494 if (!isset($squarelinks[$squareid])) { 495 $squarelinks[$squareid] = array(); 496 } 497 $squareworkload[$squareid] = count($squarelinks[$squareid]); 498 $squaregroupsworkload[$squaregroupid] += $squareworkload[$squareid]; 499 } 500 $squaregroupsworkload[$squaregroupid] /= count($squares); 501 } 502 unset($squaregroupsworkload[0]); // [0] is not real group, it contains all users 503 // $result->log('square workload = ' . json_encode($squareworkload), 'debug'); 504 // $result->log('square group workload = ' . json_encode($squaregroupsworkload), 'debug'); 505 $gmode = groups_get_activity_groupmode($this->workshop->cm, $this->workshop->course); 506 if (SEPARATEGROUPS == $gmode) { 507 // shuffle all groups but [0] which means "all users" 508 $circlegroups = array_keys(array_diff_key($allcircles, array(0 => null))); 509 shuffle($circlegroups); 510 } else { 511 // all users will be processed at once 512 $circlegroups = array(0); 513 } 514 // $result->log('circle groups = ' . json_encode($circlegroups), 'debug'); 515 foreach ($circlegroups as $circlegroupid) { 516 $result->log('processing circle group id ' . $circlegroupid, 'debug'); 517 $circles = $allcircles[$circlegroupid]; 518 // iterate over all circles in the group until the requested number of links per circle exists 519 // or it is not possible to fulfill that requirment 520 // during the first iteration, we try to make sure that at least one circlelink exists. during the 521 // second iteration, we try to allocate two, etc. 522 for ($requiredreviews = 1; $requiredreviews <= $numofreviews; $requiredreviews++) { 523 $this->shuffle_assoc($circles); 524 $result->log('iteration ' . $requiredreviews, 'debug'); 525 foreach ($circles as $circleid => $circle) { 526 if (VISIBLEGROUPS == $gmode and isset($nogroupcircles[$circleid])) { 527 $result->log('skipping circle id ' . $circleid, 'debug'); 528 continue; 529 } 530 $result->log('processing circle id ' . $circleid, 'debug'); 531 if (!isset($circlelinks[$circleid])) { 532 $circlelinks[$circleid] = array(); 533 } 534 $keeptrying = true; // is there a chance to find a square for this circle? 535 $failedgroups = array(); // array of groupids where the square should be chosen from (because 536 // of their group workload) but it was not possible (for example there 537 // was the only square and it had been already connected 538 while ($keeptrying && (count($circlelinks[$circleid]) < $requiredreviews)) { 539 // firstly, choose a group to pick the square from 540 if (NOGROUPS == $gmode) { 541 if (in_array(0, $failedgroups)) { 542 $keeptrying = false; 543 $result->log(get_string('resultnomorepeers', 'workshopallocation_random'), 'error', 1); 544 break; 545 } 546 $targetgroup = 0; 547 } elseif (SEPARATEGROUPS == $gmode) { 548 if (in_array($circlegroupid, $failedgroups)) { 549 $keeptrying = false; 550 $result->log(get_string('resultnomorepeersingroup', 'workshopallocation_random'), 'error', 1); 551 break; 552 } 553 $targetgroup = $circlegroupid; 554 } elseif (VISIBLEGROUPS == $gmode) { 555 $trygroups = array_diff_key($squaregroupsworkload, array(0 => null)); // all but [0] 556 $trygroups = array_diff_key($trygroups, array_flip($failedgroups)); // without previous failures 557 if ($options['excludesamegroup']) { 558 // exclude groups the circle is member of 559 $excludegroups = array(); 560 foreach (array_diff_key($allcircles, array(0 => null)) as $exgroupid => $exgroupmembers) { 561 if (array_key_exists($circleid, $exgroupmembers)) { 562 $excludegroups[$exgroupid] = null; 563 } 564 } 565 $trygroups = array_diff_key($trygroups, $excludegroups); 566 } 567 $targetgroup = $this->get_element_with_lowest_workload($trygroups); 568 } 569 if ($targetgroup === false) { 570 $keeptrying = false; 571 $result->log(get_string('resultnotenoughpeers', 'workshopallocation_random'), 'error', 1); 572 break; 573 } 574 $result->log('next square should be from group id ' . $targetgroup, 'debug', 1); 575 // now, choose a square from the target group 576 $trysquares = array_intersect_key($squareworkload, $allsquares[$targetgroup]); 577 // $result->log('individual workloads in this group are ' . json_encode($trysquares), 'debug', 1); 578 unset($trysquares[$circleid]); // can't allocate to self 579 $trysquares = array_diff_key($trysquares, array_flip($circlelinks[$circleid])); // can't re-allocate the same 580 $targetsquare = $this->get_element_with_lowest_workload($trysquares); 581 if (false === $targetsquare) { 582 $result->log('unable to find an available square. trying another group', 'debug', 1); 583 $failedgroups[] = $targetgroup; 584 continue; 585 } 586 $result->log('target square = ' . $targetsquare, 'debug', 1); 587 // ok - we have found the square 588 $circlelinks[$circleid][] = $targetsquare; 589 $squarelinks[$targetsquare][] = $circleid; 590 $squareworkload[$targetsquare]++; 591 $result->log('increasing square workload to ' . $squareworkload[$targetsquare], 'debug', 1); 592 if ($targetgroup) { 593 // recalculate the group workload 594 $squaregroupsworkload[$targetgroup] = 0; 595 foreach ($allsquares[$targetgroup] as $squareid => $square) { 596 $squaregroupsworkload[$targetgroup] += $squareworkload[$squareid]; 597 } 598 $squaregroupsworkload[$targetgroup] /= count($allsquares[$targetgroup]); 599 $result->log('increasing group workload to ' . $squaregroupsworkload[$targetgroup], 'debug', 1); 600 } 601 } // end of processing this circle 602 } // end of one iteration of processing circles in the group 603 } // end of all iterations over circles in the group 604 } // end of processing circle groups 605 $returned = array(); 606 if (workshop_random_allocator_setting::NUMPER_SUBMISSION == $numper) { 607 // circles are authors, squares are reviewers 608 foreach ($circlelinks as $circleid => $squares) { 609 foreach ($squares as $squareid) { 610 $returned[] = array($squareid => $circleid); 611 } 612 } 613 } 614 if (workshop_random_allocator_setting::NUMPER_REVIEWER == $numper) { 615 // circles are reviewers, squares are authors 616 foreach ($circlelinks as $circleid => $squares) { 617 foreach ($squares as $squareid) { 618 $returned[] = array($circleid => $squareid); 619 } 620 } 621 } 622 return $returned; 623 } 624 625 /** 626 * Extracts the information about reviews from the authors' and reviewers' perspectives 627 * 628 * @param array $assessments array of assessments as returned by {@link workshop::get_all_assessments()} 629 * @return array of two arrays 630 */ 631 protected function convert_assessments_to_links($assessments) { 632 $authorlinks = array(); // [authorid] => array(reviewerid, reviewerid, ...) 633 $reviewerlinks = array(); // [reviewerid] => array(authorid, authorid, ...) 634 foreach ($assessments as $assessment) { 635 if (!isset($authorlinks[$assessment->authorid])) { 636 $authorlinks[$assessment->authorid] = array(); 637 } 638 if (!isset($reviewerlinks[$assessment->reviewerid])) { 639 $reviewerlinks[$assessment->reviewerid] = array(); 640 } 641 $authorlinks[$assessment->authorid][] = $assessment->reviewerid; 642 $reviewerlinks[$assessment->reviewerid][] = $assessment->authorid; 643 } 644 return array($authorlinks, $reviewerlinks); 645 } 646 647 /** 648 * Selects an element with the lowest workload 649 * 650 * If there are more elements with the same workload, choose one of them randomly. This may be 651 * used to select a group or user. 652 * 653 * @param array $workload [groupid] => (int)workload 654 * @return mixed int|bool id of the selected element or false if it is impossible to choose 655 */ 656 protected function get_element_with_lowest_workload($workload) { 657 $precision = 10; 658 659 if (empty($workload)) { 660 return false; 661 } 662 $minload = round(min($workload), $precision); 663 $minkeys = array(); 664 foreach ($workload as $key => $val) { 665 if (round($val, $precision) == $minload) { 666 $minkeys[$key] = $val; 667 } 668 } 669 return array_rand($minkeys); 670 } 671 672 /** 673 * Shuffle the order of array elements preserving the key=>values 674 * 675 * @param array $array to be shuffled 676 * @return true 677 */ 678 protected function shuffle_assoc(&$array) { 679 if (count($array) > 1) { 680 // $keys needs to be an array, no need to shuffle 1 item or empty arrays, anyway 681 $keys = array_keys($array); 682 shuffle($keys); 683 foreach($keys as $key) { 684 $new[$key] = $array[$key]; 685 } 686 $array = $new; 687 } 688 return true; // because this behaves like in-built shuffle(), which returns true 689 } 690 691 /** 692 * Filter new allocations so that they do not contain an already existing assessment 693 * 694 * @param mixed $newallocations array of ('reviewerid' => 'authorid') tuples 695 * @param array $assessments array of assessment records 696 * @return void 697 */ 698 protected function filter_current_assessments(&$newallocations, $assessments) { 699 foreach ($assessments as $assessment) { 700 $allocation = array($assessment->reviewerid => $assessment->authorid); 701 $foundat = array_keys($newallocations, $allocation); 702 $newallocations = array_diff_key($newallocations, array_flip($foundat)); 703 } 704 } 705 } 706 707 708 /** 709 * Data object defining the settings structure for the random allocator 710 */ 711 class workshop_random_allocator_setting { 712 713 /** aim to a number of reviews per one submission {@see self::$numper} */ 714 const NUMPER_SUBMISSION = 1; 715 /** aim to a number of reviews per one reviewer {@see self::$numper} */ 716 const NUMPER_REVIEWER = 2; 717 718 /** @var int number of reviews */ 719 public $numofreviews; 720 /** @var int either {@link self::NUMPER_SUBMISSION} or {@link self::NUMPER_REVIEWER} */ 721 public $numper; 722 /** @var bool prevent reviews by peers from the same group */ 723 public $excludesamegroup; 724 /** @var bool remove current allocations */ 725 public $removecurrent; 726 /** @var bool participants can assess without having submitted anything */ 727 public $assesswosubmission; 728 /** @var bool add self-assessments */ 729 public $addselfassessment; 730 731 /** 732 * Use the factory method {@link self::instance_from_object()} 733 */ 734 protected function __construct() { 735 } 736 737 /** 738 * Factory method making the instance from data in the passed object 739 * 740 * @param stdClass $data an object holding the values for our public properties 741 * @return workshop_random_allocator_setting 742 */ 743 public static function instance_from_object(stdClass $data) { 744 $i = new self(); 745 746 if (!isset($data->numofreviews)) { 747 throw new coding_exception('Missing value of the numofreviews property'); 748 } else { 749 $i->numofreviews = (int)$data->numofreviews; 750 } 751 752 if (!isset($data->numper)) { 753 throw new coding_exception('Missing value of the numper property'); 754 } else { 755 $i->numper = (int)$data->numper; 756 if ($i->numper !== self::NUMPER_SUBMISSION and $i->numper !== self::NUMPER_REVIEWER) { 757 throw new coding_exception('Invalid value of the numper property'); 758 } 759 } 760 761 foreach (array('excludesamegroup', 'removecurrent', 'assesswosubmission', 'addselfassessment') as $k) { 762 if (isset($data->$k)) { 763 $i->$k = (bool)$data->$k; 764 } else { 765 $i->$k = false; 766 } 767 } 768 769 return $i; 770 } 771 772 /** 773 * Factory method making the instance from data in the passed text 774 * 775 * @param string $text as returned by {@link self::export_text()} 776 * @return workshop_random_allocator_setting 777 */ 778 public static function instance_from_text($text) { 779 return self::instance_from_object(json_decode($text)); 780 } 781 782 /** 783 * Exports the instance data as a text for persistant storage 784 * 785 * The returned data can be later used by {@self::instance_from_text()} factory method 786 * to restore the instance data. The current implementation uses JSON export format. 787 * 788 * @return string JSON representation of our public properties 789 */ 790 public function export_text() { 791 $getvars = function($obj) { return get_object_vars($obj); }; 792 return json_encode($getvars($this)); 793 } 794 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body