/** * Allocate submissions as requested by user * * @return workshopplus_allocation_result */ public function init() { global $PAGE; $mode = optional_param('mode', 'display', PARAM_ALPHA); $perpage = optional_param('perpage', null, PARAM_INT); if ($perpage and $perpage > 0 and $perpage <= 1000) { require_sesskey(); set_user_preference('workshopplusallocation_manual_perpage', $perpage); redirect($PAGE->url); } $result = new workshopplus_allocation_result($this); switch ($mode) { case 'new': if (!confirm_sesskey()) { throw new moodle_exception('confirmsesskeybad'); } $reviewerid = required_param('by', PARAM_INT); $authorid = required_param('of', PARAM_INT); $m = array(); // message object to be passed to the next page $submission = $this->workshopplus->get_submission_by_author($authorid); if (!$submission) { // nothing submitted by the given user $m[] = self::MSG_NOSUBMISSION; $m[] = $authorid; } else { // ok, we have the submission $res = $this->workshopplus->add_allocation($submission, $reviewerid); if ($res == workshopplus::ALLOCATION_EXISTS) { $m[] = self::MSG_EXISTS; $m[] = $submission->authorid; $m[] = $reviewerid; } else { $m[] = self::MSG_ADDED; $m[] = $submission->authorid; $m[] = $reviewerid; } } $m = implode('-', $m); // serialize message object to be passed via URL redirect($PAGE->url->out(false, array('m' => $m))); break; case 'del': if (!confirm_sesskey()) { throw new moodle_exception('confirmsesskeybad'); } $assessmentid = required_param('what', PARAM_INT); $confirmed = optional_param('confirm', 0, PARAM_INT); $assessment = $this->workshopplus->get_assessment_by_id($assessmentid); if ($assessment) { if (!$confirmed) { $m[] = self::MSG_CONFIRM_DEL; $m[] = $assessment->id; $m[] = $assessment->authorid; $m[] = $assessment->reviewerid; if (is_null($assessment->grade)) { $m[] = 0; } else { $m[] = 1; } } else { if ($this->workshopplus->delete_assessment($assessment->id)) { $m[] = self::MSG_DELETED; $m[] = $assessment->authorid; $m[] = $assessment->reviewerid; } else { $m[] = self::MSG_DELETE_ERROR; $m[] = $assessment->authorid; $m[] = $assessment->reviewerid; } } $m = implode('-', $m); // serialize message object to be passed via URL redirect($PAGE->url->out(false, array('m' => $m))); } break; } $result->set_status(workshopplus_allocation_result::STATUS_VOID); return $result; }
/** * Renders the result of the submissions allocation process * * @param workshopplus_allocation_result $result as returned by the allocator's init() method * @return string HTML to be echoed */ protected function render_workshopplus_allocation_result(workshopplus_allocation_result $result) { global $CFG; $status = $result->get_status(); if (is_null($status) or $status == workshopplus_allocation_result::STATUS_VOID) { debugging('Attempt to render workshopplus_allocation_result with empty status', DEBUG_DEVELOPER); return ''; } switch ($status) { case workshopplus_allocation_result::STATUS_FAILED: if ($message = $result->get_message()) { $message = new workshopplus_message($message, workshopplus_message::TYPE_ERROR); } else { $message = new workshopplus_message(get_string('allocationerror', 'workshopplus'), workshopplus_message::TYPE_ERROR); } break; case workshopplus_allocation_result::STATUS_CONFIGURED: if ($message = $result->get_message()) { $message = new workshopplus_message($message, workshopplus_message::TYPE_INFO); } else { $message = new workshopplus_message(get_string('allocationconfigured', 'workshopplus'), workshopplus_message::TYPE_INFO); } break; case workshopplus_allocation_result::STATUS_EXECUTED: if ($message = $result->get_message()) { $message = new workshopplus_message($message, workshopplus_message::TYPE_OK); } else { $message = new workshopplus_message(get_string('allocationdone', 'workshopplus'), workshopplus_message::TYPE_OK); } break; default: throw new coding_exception('Unknown allocation result status', $status); } // start with the message $o = $this->render($message); // display the details about the process if available $logs = $result->get_logs(); if (is_array($logs) and !empty($logs)) { $o .= html_writer::start_tag('ul', array('class' => 'allocation-init-results')); foreach ($logs as $log) { if ($log->type == 'debug' and !$CFG->debugdeveloper) { // display allocation debugging messages for developers only continue; } $class = $log->type; if ($log->indent) { $class .= ' indent'; } $o .= html_writer::tag('li', $log->message, array('class' => $class)) . PHP_EOL; } $o .= html_writer::end_tag('ul'); } return $o; }
/** * Allocates submission reviews randomly * * The algorithm of this function has been described at http://moodle.org/mod/forum/discuss.php?d=128473 * Please see the PDF attached to the post before you study the implementation. The goal of the function * is to connect each "circle" (circles are representing either authors or reviewers) with a required * number of "squares" (the other type than circles are). * * The passed $options array must provide keys: * (int)numofreviews - number of reviews to be allocated to each circle * (int)numper - what user type the circles represent. * (bool)excludesamegroup - whether to prevent peer submissions from the same group in visible group mode * * @param array $authors structure of grouped authors * @param array $reviewers structure of grouped reviewers * @param array $assessments currently assigned assessments to be kept * @param workshopplus_allocation_result $result allocation result logger * @param array $options allocation options * @return array array of (reviewerid => authorid) pairs */ protected function random_allocation($authors, $reviewers, $assessments, $result, array $options) { if (empty($authors) || empty($reviewers)) { // nothing to be done return array(); } $numofreviews = $options['numofreviews']; $numper = $options['numper']; if (workshopplus_random_allocator_setting::NUMPER_SUBMISSION == $numper) { // circles are authors, squares are reviewers $result->log(get_string('resultnumperauthor', 'workshopplusallocation_random', $numofreviews), 'info'); $allcircles = $authors; $allsquares = $reviewers; // get current workload list($circlelinks, $squarelinks) = $this->convert_assessments_to_links($assessments); } elseif (workshopplus_random_allocator_setting::NUMPER_REVIEWER == $numper) { // circles are reviewers, squares are authors $result->log(get_string('resultnumperreviewer', 'workshopplusallocation_random', $numofreviews), 'info'); $allcircles = $reviewers; $allsquares = $authors; // get current workload list($squarelinks, $circlelinks) = $this->convert_assessments_to_links($assessments); } else { throw new moodle_exception('unknownusertypepassed', 'workshopplus'); } // get the users that are not in any group. in visible groups mode, these users are exluded // from allocation by this method // $nogroupcircles is array (int)$userid => undefined if (isset($allcircles[0])) { $nogroupcircles = array_flip(array_keys($allcircles[0])); } else { $nogroupcircles = array(); } foreach ($allcircles as $circlegroupid => $circles) { if ($circlegroupid == 0) { continue; } foreach ($circles as $circleid => $circle) { unset($nogroupcircles[$circleid]); } } // $result->log('circle links = ' . json_encode($circlelinks), 'debug'); // $result->log('square links = ' . json_encode($squarelinks), 'debug'); $squareworkload = array(); // individual workload indexed by squareid $squaregroupsworkload = array(); // group workload indexed by squaregroupid foreach ($allsquares as $squaregroupid => $squares) { $squaregroupsworkload[$squaregroupid] = 0; foreach ($squares as $squareid => $square) { if (!isset($squarelinks[$squareid])) { $squarelinks[$squareid] = array(); } $squareworkload[$squareid] = count($squarelinks[$squareid]); $squaregroupsworkload[$squaregroupid] += $squareworkload[$squareid]; } $squaregroupsworkload[$squaregroupid] /= count($squares); } unset($squaregroupsworkload[0]); // [0] is not real group, it contains all users // $result->log('square workload = ' . json_encode($squareworkload), 'debug'); // $result->log('square group workload = ' . json_encode($squaregroupsworkload), 'debug'); $gmode = groups_get_activity_groupmode($this->workshopplus->cm, $this->workshopplus->course); if (SEPARATEGROUPS == $gmode) { // shuffle all groups but [0] which means "all users" $circlegroups = array_keys(array_diff_key($allcircles, array(0 => null))); shuffle($circlegroups); } else { // all users will be processed at once $circlegroups = array(0); } // $result->log('circle groups = ' . json_encode($circlegroups), 'debug'); foreach ($circlegroups as $circlegroupid) { $result->log('processing circle group id ' . $circlegroupid, 'debug'); $circles = $allcircles[$circlegroupid]; // iterate over all circles in the group until the requested number of links per circle exists // or it is not possible to fulfill that requirment // during the first iteration, we try to make sure that at least one circlelink exists. during the // second iteration, we try to allocate two, etc. for ($requiredreviews = 1; $requiredreviews <= $numofreviews; $requiredreviews++) { $this->shuffle_assoc($circles); $result->log('iteration ' . $requiredreviews, 'debug'); foreach ($circles as $circleid => $circle) { if (VISIBLEGROUPS == $gmode and isset($nogroupcircles[$circleid])) { $result->log('skipping circle id ' . $circleid, 'debug'); continue; } $result->log('processing circle id ' . $circleid, 'debug'); if (!isset($circlelinks[$circleid])) { $circlelinks[$circleid] = array(); } $keeptrying = true; // is there a chance to find a square for this circle? $failedgroups = array(); // array of groupids where the square should be chosen from (because // of their group workload) but it was not possible (for example there // was the only square and it had been already connected while ($keeptrying && count($circlelinks[$circleid]) < $requiredreviews) { // firstly, choose a group to pick the square from if (NOGROUPS == $gmode) { if (in_array(0, $failedgroups)) { $keeptrying = false; $result->log(get_string('resultnomorepeers', 'workshopplusallocation_random'), 'error', 1); break; } $targetgroup = 0; } elseif (SEPARATEGROUPS == $gmode) { if (in_array($circlegroupid, $failedgroups)) { $keeptrying = false; $result->log(get_string('resultnomorepeersingroup', 'workshopplusallocation_random'), 'error', 1); break; } $targetgroup = $circlegroupid; } elseif (VISIBLEGROUPS == $gmode) { $trygroups = array_diff_key($squaregroupsworkload, array(0 => null)); // all but [0] $trygroups = array_diff_key($trygroups, array_flip($failedgroups)); // without previous failures if ($options['excludesamegroup']) { // exclude groups the circle is member of $excludegroups = array(); foreach (array_diff_key($allcircles, array(0 => null)) as $exgroupid => $exgroupmembers) { if (array_key_exists($circleid, $exgroupmembers)) { $excludegroups[$exgroupid] = null; } } $trygroups = array_diff_key($trygroups, $excludegroups); } $targetgroup = $this->get_element_with_lowest_workload($trygroups); } if ($targetgroup === false) { $keeptrying = false; $result->log(get_string('resultnotenoughpeers', 'workshopplusallocation_random'), 'error', 1); break; } $result->log('next square should be from group id ' . $targetgroup, 'debug', 1); // now, choose a square from the target group $trysquares = array_intersect_key($squareworkload, $allsquares[$targetgroup]); // $result->log('individual workloads in this group are ' . json_encode($trysquares), 'debug', 1); unset($trysquares[$circleid]); // can't allocate to self $trysquares = array_diff_key($trysquares, array_flip($circlelinks[$circleid])); // can't re-allocate the same $targetsquare = $this->get_element_with_lowest_workload($trysquares); if (false === $targetsquare) { $result->log('unable to find an available square. trying another group', 'debug', 1); $failedgroups[] = $targetgroup; continue; } $result->log('target square = ' . $targetsquare, 'debug', 1); // ok - we have found the square $circlelinks[$circleid][] = $targetsquare; $squarelinks[$targetsquare][] = $circleid; $squareworkload[$targetsquare]++; $result->log('increasing square workload to ' . $squareworkload[$targetsquare], 'debug', 1); if ($targetgroup) { // recalculate the group workload $squaregroupsworkload[$targetgroup] = 0; foreach ($allsquares[$targetgroup] as $squareid => $square) { $squaregroupsworkload[$targetgroup] += $squareworkload[$squareid]; } $squaregroupsworkload[$targetgroup] /= count($allsquares[$targetgroup]); $result->log('increasing group workload to ' . $squaregroupsworkload[$targetgroup], 'debug', 1); } } // end of processing this circle } // end of one iteration of processing circles in the group } // end of all iterations over circles in the group } // end of processing circle groups $returned = array(); if (workshopplus_random_allocator_setting::NUMPER_SUBMISSION == $numper) { // circles are authors, squares are reviewers foreach ($circlelinks as $circleid => $squares) { foreach ($squares as $squareid) { $returned[] = array($squareid => $circleid); } } } if (workshopplus_random_allocator_setting::NUMPER_REVIEWER == $numper) { // circles are reviewers, squares are authors foreach ($circlelinks as $circleid => $squares) { foreach ($squares as $squareid) { $returned[] = array($circleid => $squareid); } } } return $returned; }
/** * Stores the pre-defined random allocation settings for later usage * * @param bool $enabled is the scheduled allocation enabled * @param bool $reset reset the recent execution info * @param workshopplus_random_allocator_setting $settings settings form data * @param workshopplus_allocation_result $result logger */ protected function store_settings($enabled, $reset, workshopplus_random_allocator_setting $settings, workshopplus_allocation_result $result) { global $DB; $data = new stdClass(); $data->workshopplusid = $this->workshopplus->id; $data->enabled = $enabled; $data->submissionend = $this->workshopplus->submissionend; $data->settings = $settings->export_text(); if ($reset) { $data->timeallocated = null; $data->resultstatus = null; $data->resultmessage = null; $data->resultlog = null; } $result->log($data->settings, 'debug'); $current = $DB->get_record('workshopplusallocation_scheduled', array('workshopplusid' => $data->workshopplusid), '*', IGNORE_MISSING); if ($current === false) { $DB->insert_record('workshopplusallocation_scheduled', $data); } else { $data->id = $current->id; $DB->update_record('workshopplusallocation_scheduled', $data); } }