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