[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/mod/workshop/allocation/random/ -> lib.php (source)

   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  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1