Differences Between: [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 267 $options = []; 268 269 for ($i = 100; $i > 20; $i = $i - 10) { 270 $options[$i] = $i; 271 } 272 273 for ($i = 20; $i >= 0; $i--) { 274 $options[$i] = $i; 275 } 276 277 return $options; 278 } 279 280 /** 281 * Allocates submissions to their authors for review 282 * 283 * If the submission has already been allocated, it is skipped. If the author is not found among 284 * reviewers, the submission is not assigned. 285 * 286 * @param array $authors grouped of {@see workshop::get_potential_authors()} 287 * @param array $reviewers grouped by {@see workshop::get_potential_reviewers()} 288 * @param array $assessments as returned by {@see workshop::get_all_assessments()} 289 * @return array of new allocations to be created, array of array(reviewerid => authorid) 290 */ 291 protected function self_allocation($authors=array(), $reviewers=array(), $assessments=array()) { 292 if (!isset($authors[0]) || !isset($reviewers[0])) { 293 // no authors or no reviewers 294 return array(); 295 } 296 $alreadyallocated = array(); 297 foreach ($assessments as $assessment) { 298 if ($assessment->authorid == $assessment->reviewerid) { 299 $alreadyallocated[$assessment->authorid] = 1; 300 } 301 } 302 $add = array(); // list of new allocations to be created 303 foreach ($authors[0] as $authorid => $author) { 304 // for all authors in all groups 305 if (isset($reviewers[0][$authorid])) { 306 // if the author can be reviewer 307 if (!isset($alreadyallocated[$authorid])) { 308 // and the allocation does not exist yet, then 309 $add[] = array($authorid => $authorid); 310 } 311 } 312 } 313 return $add; 314 } 315 316 /** 317 * Creates new assessment records 318 * 319 * @param array $newallocations pairs 'reviewerid' => 'authorid' 320 * @param array $dataauthors authors by group, group [0] contains all authors 321 * @param array $datareviewers reviewers by group, group [0] contains all reviewers 322 * @return bool 323 */ 324 protected function add_new_allocations(array $newallocations, array $dataauthors, array $datareviewers) { 325 global $DB; 326 327 $newallocations = $this->get_unique_allocations($newallocations); 328 $authorids = $this->get_author_ids($newallocations); 329 $submissions = $this->workshop->get_submissions($authorids); 330 $submissions = $this->index_submissions_by_authors($submissions); 331 foreach ($newallocations as $newallocation) { 332 $reviewerid = key($newallocation); 333 $authorid = current($newallocation); 334 if (!isset($submissions[$authorid])) { 335 throw new moodle_exception('unabletoallocateauthorwithoutsubmission', 'workshop'); 336 } 337 $submission = $submissions[$authorid]; 338 $status = $this->workshop->add_allocation($submission, $reviewerid, 1, true); // todo configurable weight? 339 if (workshop::ALLOCATION_EXISTS == $status) { 340 debugging('newallocations array contains existing allocation, this should not happen'); 341 } 342 } 343 } 344 345 /** 346 * Flips the structure of submission so it is indexed by authorid attribute 347 * 348 * It is the caller's responsibility to make sure the submissions are not teacher 349 * examples so no user is the author of more submissions. 350 * 351 * @param string $submissions array indexed by submission id 352 * @return array indexed by author id 353 */ 354 protected function index_submissions_by_authors($submissions) { 355 $byauthor = array(); 356 if (is_array($submissions)) { 357 foreach ($submissions as $submissionid => $submission) { 358 if (isset($byauthor[$submission->authorid])) { 359 throw new moodle_exception('moresubmissionsbyauthor', 'workshop'); 360 } 361 $byauthor[$submission->authorid] = $submission; 362 } 363 } 364 return $byauthor; 365 } 366 367 /** 368 * Extracts unique list of authors' IDs from the structure of new allocations 369 * 370 * @param array $newallocations of pairs 'reviewerid' => 'authorid' 371 * @return array of authorids 372 */ 373 protected function get_author_ids($newallocations) { 374 $authors = array(); 375 foreach ($newallocations as $newallocation) { 376 $authorid = reset($newallocation); 377 if (!in_array($authorid, $authors)) { 378 $authors[] = $authorid; 379 } 380 } 381 return $authors; 382 } 383 384 /** 385 * Removes duplicate allocations 386 * 387 * @param mixed $newallocations array of 'reviewerid' => 'authorid' pairs 388 * @return array 389 */ 390 protected function get_unique_allocations($newallocations) { 391 return array_merge(array_map('unserialize', array_unique(array_map('serialize', $newallocations)))); 392 } 393 394 /** 395 * Returns the list of assessments to remove 396 * 397 * If user selects "removecurrentallocations", we should remove all current assessment records 398 * and insert new ones. But this would needlessly waste table ids. Instead, let us find only those 399 * assessments that have not been re-allocated in this run of allocation. So, the once-allocated 400 * submissions are kept with their original id. 401 * 402 * @param array $assessments list of current assessments 403 * @param mixed $newallocations array of 'reviewerid' => 'authorid' pairs 404 * @param bool $keepselfassessments do not remove already allocated self assessments 405 * @return array of assessments ids to be removed 406 */ 407 protected function get_unkept_assessments($assessments, $newallocations, $keepselfassessments) { 408 $keepids = array(); // keep these assessments 409 foreach ($assessments as $assessmentid => $assessment) { 410 $aaid = $assessment->authorid; 411 $arid = $assessment->reviewerid; 412 if (($keepselfassessments) && ($aaid == $arid)) { 413 $keepids[$assessmentid] = null; 414 continue; 415 } 416 foreach ($newallocations as $newallocation) { 417 $nrid = key($newallocation); 418 $naid = current($newallocation); 419 if (array($arid, $aaid) == array($nrid, $naid)) { 420 // re-allocation found - let us continue with the next assessment 421 $keepids[$assessmentid] = null; 422 continue 2; 423 } 424 } 425 } 426 return array_keys(array_diff_key($assessments, $keepids)); 427 } 428 429 /** 430 * Allocates submission reviews randomly 431 * 432 * The algorithm of this function has been described at http://moodle.org/mod/forum/discuss.php?d=128473 433 * Please see the PDF attached to the post before you study the implementation. The goal of the function 434 * is to connect each "circle" (circles are representing either authors or reviewers) with a required 435 * number of "squares" (the other type than circles are). 436 * 437 * The passed $options array must provide keys: 438 * (int)numofreviews - number of reviews to be allocated to each circle 439 * (int)numper - what user type the circles represent. 440 * (bool)excludesamegroup - whether to prevent peer submissions from the same group in visible group mode 441 * 442 * @param array $authors structure of grouped authors 443 * @param array $reviewers structure of grouped reviewers 444 * @param array $assessments currently assigned assessments to be kept 445 * @param workshop_allocation_result $result allocation result logger 446 * @param array $options allocation options 447 * @return array array of (reviewerid => authorid) pairs 448 */ 449 protected function random_allocation($authors, $reviewers, $assessments, $result, array $options) { 450 if (empty($authors) || empty($reviewers)) { 451 // nothing to be done 452 return array(); 453 } 454 455 $numofreviews = $options['numofreviews']; 456 $numper = $options['numper']; 457 458 if (workshop_random_allocator_setting::NUMPER_SUBMISSION == $numper) { 459 // circles are authors, squares are reviewers 460 $result->log(get_string('resultnumperauthor', 'workshopallocation_random', $numofreviews), 'info'); 461 $allcircles = $authors; 462 $allsquares = $reviewers; 463 // get current workload 464 list($circlelinks, $squarelinks) = $this->convert_assessments_to_links($assessments); 465 } elseif (workshop_random_allocator_setting::NUMPER_REVIEWER == $numper) { 466 // circles are reviewers, squares are authors 467 $result->log(get_string('resultnumperreviewer', 'workshopallocation_random', $numofreviews), 'info'); 468 $allcircles = $reviewers; 469 $allsquares = $authors; 470 // get current workload 471 list($squarelinks, $circlelinks) = $this->convert_assessments_to_links($assessments); 472 } else { 473 throw new moodle_exception('unknownusertypepassed', 'workshop'); 474 } 475 // get the users that are not in any group. in visible groups mode, these users are exluded 476 // from allocation by this method 477 // $nogroupcircles is array (int)$userid => undefined 478 if (isset($allcircles[0])) { 479 $nogroupcircles = array_flip(array_keys($allcircles[0])); 480 } else { 481 $nogroupcircles = array(); 482 } 483 foreach ($allcircles as $circlegroupid => $circles) { 484 if ($circlegroupid == 0) { 485 continue; 486 } 487 foreach ($circles as $circleid => $circle) { 488 unset($nogroupcircles[$circleid]); 489 } 490 } 491 // $result->log('circle links = ' . json_encode($circlelinks), 'debug'); 492 // $result->log('square links = ' . json_encode($squarelinks), 'debug'); 493 $squareworkload = array(); // individual workload indexed by squareid 494 $squaregroupsworkload = array(); // group workload indexed by squaregroupid 495 foreach ($allsquares as $squaregroupid => $squares) { 496 $squaregroupsworkload[$squaregroupid] = 0; 497 foreach ($squares as $squareid => $square) { 498 if (!isset($squarelinks[$squareid])) { 499 $squarelinks[$squareid] = array(); 500 } 501 $squareworkload[$squareid] = count($squarelinks[$squareid]); 502 $squaregroupsworkload[$squaregroupid] += $squareworkload[$squareid]; 503 } 504 $squaregroupsworkload[$squaregroupid] /= count($squares); 505 } 506 unset($squaregroupsworkload[0]); // [0] is not real group, it contains all users 507 // $result->log('square workload = ' . json_encode($squareworkload), 'debug'); 508 // $result->log('square group workload = ' . json_encode($squaregroupsworkload), 'debug'); 509 $gmode = groups_get_activity_groupmode($this->workshop->cm, $this->workshop->course); 510 if (SEPARATEGROUPS == $gmode) { 511 // shuffle all groups but [0] which means "all users" 512 $circlegroups = array_keys(array_diff_key($allcircles, array(0 => null))); 513 shuffle($circlegroups); 514 } else { 515 // all users will be processed at once 516 $circlegroups = array(0); 517 } 518 // $result->log('circle groups = ' . json_encode($circlegroups), 'debug'); 519 foreach ($circlegroups as $circlegroupid) { 520 $result->log('processing circle group id ' . $circlegroupid, 'debug'); 521 $circles = $allcircles[$circlegroupid]; 522 // iterate over all circles in the group until the requested number of links per circle exists 523 // or it is not possible to fulfill that requirment 524 // during the first iteration, we try to make sure that at least one circlelink exists. during the 525 // second iteration, we try to allocate two, etc. 526 for ($requiredreviews = 1; $requiredreviews <= $numofreviews; $requiredreviews++) { 527 $this->shuffle_assoc($circles); 528 $result->log('iteration ' . $requiredreviews, 'debug'); 529 foreach ($circles as $circleid => $circle) { 530 if (VISIBLEGROUPS == $gmode and isset($nogroupcircles[$circleid])) { 531 $result->log('skipping circle id ' . $circleid, 'debug'); 532 continue; 533 } 534 $result->log('processing circle id ' . $circleid, 'debug'); 535 if (!isset($circlelinks[$circleid])) { 536 $circlelinks[$circleid] = array(); 537 } 538 $keeptrying = true; // is there a chance to find a square for this circle? 539 $failedgroups = array(); // array of groupids where the square should be chosen from (because 540 // of their group workload) but it was not possible (for example there 541 // was the only square and it had been already connected 542 while ($keeptrying && (count($circlelinks[$circleid]) < $requiredreviews)) { 543 // firstly, choose a group to pick the square from 544 if (NOGROUPS == $gmode) { 545 if (in_array(0, $failedgroups)) { 546 $keeptrying = false; 547 $result->log(get_string('resultnomorepeers', 'workshopallocation_random'), 'error', 1); 548 break; 549 } 550 $targetgroup = 0; 551 } elseif (SEPARATEGROUPS == $gmode) { 552 if (in_array($circlegroupid, $failedgroups)) { 553 $keeptrying = false; 554 $result->log(get_string('resultnomorepeersingroup', 'workshopallocation_random'), 'error', 1); 555 break; 556 } 557 $targetgroup = $circlegroupid; 558 } elseif (VISIBLEGROUPS == $gmode) { 559 $trygroups = array_diff_key($squaregroupsworkload, array(0 => null)); // all but [0] 560 $trygroups = array_diff_key($trygroups, array_flip($failedgroups)); // without previous failures 561 if ($options['excludesamegroup']) { 562 // exclude groups the circle is member of 563 $excludegroups = array(); 564 foreach (array_diff_key($allcircles, array(0 => null)) as $exgroupid => $exgroupmembers) { 565 if (array_key_exists($circleid, $exgroupmembers)) { 566 $excludegroups[$exgroupid] = null; 567 } 568 } 569 $trygroups = array_diff_key($trygroups, $excludegroups); 570 } 571 $targetgroup = $this->get_element_with_lowest_workload($trygroups); 572 } 573 if ($targetgroup === false) { 574 $keeptrying = false; 575 $result->log(get_string('resultnotenoughpeers', 'workshopallocation_random'), 'error', 1); 576 break; 577 } 578 $result->log('next square should be from group id ' . $targetgroup, 'debug', 1); 579 // now, choose a square from the target group 580 $trysquares = array_intersect_key($squareworkload, $allsquares[$targetgroup]); 581 // $result->log('individual workloads in this group are ' . json_encode($trysquares), 'debug', 1); 582 unset($trysquares[$circleid]); // can't allocate to self 583 $trysquares = array_diff_key($trysquares, array_flip($circlelinks[$circleid])); // can't re-allocate the same 584 $targetsquare = $this->get_element_with_lowest_workload($trysquares); 585 if (false === $targetsquare) { 586 $result->log('unable to find an available square. trying another group', 'debug', 1); 587 $failedgroups[] = $targetgroup; 588 continue; 589 } 590 $result->log('target square = ' . $targetsquare, 'debug', 1); 591 // ok - we have found the square 592 $circlelinks[$circleid][] = $targetsquare; 593 $squarelinks[$targetsquare][] = $circleid; 594 $squareworkload[$targetsquare]++; 595 $result->log('increasing square workload to ' . $squareworkload[$targetsquare], 'debug', 1); 596 if ($targetgroup) { 597 // recalculate the group workload 598 $squaregroupsworkload[$targetgroup] = 0; 599 foreach ($allsquares[$targetgroup] as $squareid => $square) { 600 $squaregroupsworkload[$targetgroup] += $squareworkload[$squareid]; 601 } 602 $squaregroupsworkload[$targetgroup] /= count($allsquares[$targetgroup]); 603 $result->log('increasing group workload to ' . $squaregroupsworkload[$targetgroup], 'debug', 1); 604 } 605 } // end of processing this circle 606 } // end of one iteration of processing circles in the group 607 } // end of all iterations over circles in the group 608 } // end of processing circle groups 609 $returned = array(); 610 if (workshop_random_allocator_setting::NUMPER_SUBMISSION == $numper) { 611 // circles are authors, squares are reviewers 612 foreach ($circlelinks as $circleid => $squares) { 613 foreach ($squares as $squareid) { 614 $returned[] = array($squareid => $circleid); 615 } 616 } 617 } 618 if (workshop_random_allocator_setting::NUMPER_REVIEWER == $numper) { 619 // circles are reviewers, squares are authors 620 foreach ($circlelinks as $circleid => $squares) { 621 foreach ($squares as $squareid) { 622 $returned[] = array($circleid => $squareid); 623 } 624 } 625 } 626 return $returned; 627 } 628 629 /** 630 * Extracts the information about reviews from the authors' and reviewers' perspectives 631 * 632 * @param array $assessments array of assessments as returned by {@link workshop::get_all_assessments()} 633 * @return array of two arrays 634 */ 635 protected function convert_assessments_to_links($assessments) { 636 $authorlinks = array(); // [authorid] => array(reviewerid, reviewerid, ...) 637 $reviewerlinks = array(); // [reviewerid] => array(authorid, authorid, ...) 638 foreach ($assessments as $assessment) { 639 if (!isset($authorlinks[$assessment->authorid])) { 640 $authorlinks[$assessment->authorid] = array(); 641 } 642 if (!isset($reviewerlinks[$assessment->reviewerid])) { 643 $reviewerlinks[$assessment->reviewerid] = array(); 644 } 645 $authorlinks[$assessment->authorid][] = $assessment->reviewerid; 646 $reviewerlinks[$assessment->reviewerid][] = $assessment->authorid; 647 } 648 return array($authorlinks, $reviewerlinks); 649 } 650 651 /** 652 * Selects an element with the lowest workload 653 * 654 * If there are more elements with the same workload, choose one of them randomly. This may be 655 * used to select a group or user. 656 * 657 * @param array $workload [groupid] => (int)workload 658 * @return mixed int|bool id of the selected element or false if it is impossible to choose 659 */ 660 protected function get_element_with_lowest_workload($workload) { 661 $precision = 10; 662 663 if (empty($workload)) { 664 return false; 665 } 666 $minload = round(min($workload), $precision); 667 $minkeys = array(); 668 foreach ($workload as $key => $val) { 669 if (round($val, $precision) == $minload) { 670 $minkeys[$key] = $val; 671 } 672 } 673 return array_rand($minkeys); 674 } 675 676 /** 677 * Shuffle the order of array elements preserving the key=>values 678 * 679 * @param array $array to be shuffled 680 * @return true 681 */ 682 protected function shuffle_assoc(&$array) { 683 if (count($array) > 1) { 684 // $keys needs to be an array, no need to shuffle 1 item or empty arrays, anyway 685 $keys = array_keys($array); 686 shuffle($keys); 687 foreach($keys as $key) { 688 $new[$key] = $array[$key]; 689 } 690 $array = $new; 691 } 692 return true; // because this behaves like in-built shuffle(), which returns true 693 } 694 695 /** 696 * Filter new allocations so that they do not contain an already existing assessment 697 * 698 * @param mixed $newallocations array of ('reviewerid' => 'authorid') tuples 699 * @param array $assessments array of assessment records 700 * @return void 701 */ 702 protected function filter_current_assessments(&$newallocations, $assessments) { 703 foreach ($assessments as $assessment) { 704 $allocation = array($assessment->reviewerid => $assessment->authorid); 705 $foundat = array_keys($newallocations, $allocation); 706 $newallocations = array_diff_key($newallocations, array_flip($foundat)); 707 } 708 } 709 } 710 711 712 /** 713 * Data object defining the settings structure for the random allocator 714 */ 715 class workshop_random_allocator_setting { 716 717 /** aim to a number of reviews per one submission {@see self::$numper} */ 718 const NUMPER_SUBMISSION = 1; 719 /** aim to a number of reviews per one reviewer {@see self::$numper} */ 720 const NUMPER_REVIEWER = 2; 721 722 /** @var int number of reviews */ 723 public $numofreviews; 724 /** @var int either {@link self::NUMPER_SUBMISSION} or {@link self::NUMPER_REVIEWER} */ 725 public $numper; 726 /** @var bool prevent reviews by peers from the same group */ 727 public $excludesamegroup; 728 /** @var bool remove current allocations */ 729 public $removecurrent; 730 /** @var bool participants can assess without having submitted anything */ 731 public $assesswosubmission; 732 /** @var bool add self-assessments */ 733 public $addselfassessment; 734 735 /** 736 * Use the factory method {@link self::instance_from_object()} 737 */ 738 protected function __construct() { 739 } 740 741 /** 742 * Factory method making the instance from data in the passed object 743 * 744 * @param stdClass $data an object holding the values for our public properties 745 * @return workshop_random_allocator_setting 746 */ 747 public static function instance_from_object(stdClass $data) { 748 $i = new self(); 749 750 if (!isset($data->numofreviews)) { 751 throw new coding_exception('Missing value of the numofreviews property'); 752 } else { 753 $i->numofreviews = (int)$data->numofreviews; 754 } 755 756 if (!isset($data->numper)) { 757 throw new coding_exception('Missing value of the numper property'); 758 } else { 759 $i->numper = (int)$data->numper; 760 if ($i->numper !== self::NUMPER_SUBMISSION and $i->numper !== self::NUMPER_REVIEWER) { 761 throw new coding_exception('Invalid value of the numper property'); 762 } 763 } 764 765 foreach (array('excludesamegroup', 'removecurrent', 'assesswosubmission', 'addselfassessment') as $k) { 766 if (isset($data->$k)) { 767 $i->$k = (bool)$data->$k; 768 } else { 769 $i->$k = false; 770 } 771 } 772 773 return $i; 774 } 775 776 /** 777 * Factory method making the instance from data in the passed text 778 * 779 * @param string $text as returned by {@link self::export_text()} 780 * @return workshop_random_allocator_setting 781 */ 782 public static function instance_from_text($text) { 783 return self::instance_from_object(json_decode($text)); 784 } 785 786 /** 787 * Exports the instance data as a text for persistant storage 788 * 789 * The returned data can be later used by {@self::instance_from_text()} factory method 790 * to restore the instance data. The current implementation uses JSON export format. 791 * 792 * @return string JSON representation of our public properties 793 */ 794 public function export_text() { 795 $getvars = function($obj) { return get_object_vars($obj); }; 796 return json_encode($getvars($this)); 797 } 798 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body