Commit bb871bc4 authored by Petr Skoda's avatar Petr Skoda
Browse files

Merge branch 'workshop_random_allocator' of git://github.com/mudrd8mz/moodle

parents a477c0da 241e58e4
......@@ -32,7 +32,10 @@ $string['allocationsettings'] = 'Allocation settings';
$string['assessmentdeleteddetail'] = 'Assessment deallocated: <strong>{$a->reviewername}</strong> is no longer reviewer of <strong>{$a->authorname}</strong>';
$string['assesswosubmission'] = 'Participants can assess without having submitted anything';
$string['confignumofreviews'] = 'Default number of submissions to be randomly allocated';
$string['excludesamegroup'] = 'Prevent reviews by peers from the same group';
$string['noallocationtoadd'] = 'No allocations to add';
$string['nogroupusers'] = '<p>Warning: If the workshop is in \'visible groups\' mode or \'separate groups\' mode, then users MUST be part of at least one group to have peer-assessments allocated to them by this tool. Non-grouped users can still be given new self-assessments or have existing assessments removed.</p>
<p>These users are currently not in a group: {$a}</p>';
$string['numofdeallocatedassessment'] = 'Deallocating {$a} assessment(s)';
$string['numofrandomlyallocatedsubmissions'] = 'Randomly allocating {$a} submissions';
$string['numofreviews'] = 'Number of reviews';
......
......@@ -76,6 +76,7 @@ class workshop_random_allocator implements workshop_allocator {
$o = array(); // list of output messages
$numofreviews = required_param('numofreviews', PARAM_INT);
$numper = required_param('numper', PARAM_INT);
$excludesamegroup = optional_param('excludesamegroup', false, PARAM_BOOL);
$removecurrent = optional_param('removecurrent', false, PARAM_BOOL);
$assesswosubmission = optional_param('assesswosubmission', false, PARAM_BOOL);
$addselfassessment = optional_param('addselfassessment', false, PARAM_BOOL);
......@@ -96,7 +97,11 @@ class workshop_random_allocator implements workshop_allocator {
} else {
$curassessments = $assessments;
}
$randomallocations = $this->random_allocation($authors, $reviewers, $curassessments, $numofreviews, $numper, $o);
$options = array();
$options['numofreviews'] = $numofreviews;
$options['numper'] = $numper;
$options['excludesamegroup'] = $excludesamegroup;
$randomallocations = $this->random_allocation($authors, $reviewers, $curassessments, $o, $options);
$newallocations = array_merge($newallocations, $randomallocations);
$o[] = 'ok::' . get_string('numofrandomlyallocatedsubmissions', 'workshopallocation_random', count($randomallocations));
unset($randomallocations);
......@@ -178,10 +183,37 @@ class workshop_random_allocator implements workshop_allocator {
$this->mform->display();
$out .= ob_get_contents();
ob_end_clean();
$out .= $output->container_end();
// if there are some not-grouped participant in a group mode, warn the user
$gmode = groups_get_activity_groupmode($this->workshop->cm, $this->workshop->course);
if (VISIBLEGROUPS == $gmode or SEPARATEGROUPS == $gmode) {
$users = $this->workshop->get_potential_authors() + $this->workshop->get_potential_reviewers();
$users = $this->workshop->get_grouped($users);
if (isset($users[0])) {
$nogroupusers = $users[0];
foreach ($users as $groupid => $groupusers) {
if ($groupid == 0) {
continue;
}
foreach ($groupusers as $groupuserid => $groupuser) {
unset($nogroupusers[$groupuserid]);
}
}
if (!empty($nogroupusers)) {
$list = array();
foreach ($nogroupusers as $nogroupuser) {
$list[] = fullname($nogroupuser);
}
$a = implode(', ', $list);
$out .= $output->box(get_string('nogroupusers', 'workshopallocation_random', $a), 'generalbox warning nogroupusers');
}
}
}
// TODO $out .= $output->heading(get_string('stats', 'workshopallocation_random'));
$out .= $output->container_end();
return $out;
}
......@@ -371,19 +403,27 @@ class workshop_random_allocator implements workshop_allocator {
* 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 resource $reviewers structure of grouped reviewers
* @param array $assessments currently assigned assessments to be kept
* @param mixed $numofreviews number of reviews to be allocated to each circle
* @param mixed $numper what user type the circles represent
* @param array $o reference to an array of log messages
* @param array $options allocation options
* @return array array of (reviewerid => authorid) pairs
*/
protected function random_allocation($authors, $reviewers, $assessments, $numofreviews, $numper, &$o) {
protected function random_allocation($authors, $reviewers, $assessments, &$o, array $options) {
if (empty($authors) || empty($reviewers)) {
// nothing to be done
return array();
}
$numofreviews = $options['numofreviews'];
$numper = $options['numper'];
if (self::USERTYPE_AUTHOR == $numper) {
// circles are authors, squares are reviewers
$o[] = 'info::Trying to allocate ' . $numofreviews . ' review(s) per author'; // todo translate
......@@ -401,6 +441,22 @@ class workshop_random_allocator implements workshop_allocator {
} else {
throw new moodle_exception('unknownusertypepassed', 'workshop');
}
// 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]);
}
}
// $o[] = 'debug::circle links = ' . json_encode($circlelinks);
// $o[] = 'debug::square links = ' . json_encode($squarelinks);
$squareworkload = array(); // individual workload indexed by squareid
......@@ -428,76 +484,96 @@ class workshop_random_allocator implements workshop_allocator {
// all users will be processed at once
$circlegroups = array(0);
}
$this->shuffle_assoc($circlegroups);
// $o[] = 'debug::circle groups = ' . json_encode($circlegroups);
foreach ($circlegroups as $circlegroupid) {
$o[] = 'debug::processing circle group id ' . $circlegroupid;
$circles = $allcircles[$circlegroupid];
$this->shuffle_assoc($circles);
foreach ($circles as $circleid => $circle) {
$o[] = 'debug::processing circle id ' . $circleid;
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]) < $numofreviews)) {
// firstly, choose a group to pick the square from
if (NOGROUPS == $gmode) {
if (in_array(0, $failedgroups)) {
$keeptrying = false;
$o[] = 'error::indent::No more peers available'; // todo translate
break;
// 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);
$o[] = 'debug::iteration ' . $requiredreviews;
foreach ($circles as $circleid => $circle) {
if (VISIBLEGROUPS == $gmode and isset($nogroupcircles[$circleid])) {
$o[] = 'debug::skipping circle id ' . $circleid;
continue;
}
$o[] = 'debug::processing circle id ' . $circleid;
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;
$o[] = 'error::indent::No more peers available'; // todo translate
break;
}
$targetgroup = 0;
} elseif (SEPARATEGROUPS == $gmode) {
if (in_array($circlegroupid, $failedgroups)) {
$keeptrying = false;
$o[] = 'error::indent::No more peers available in this separate group'; // todo translate
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);
}
$targetgroup = 0;
} elseif (SEPARATEGROUPS == $gmode) {
if (in_array($circlegroupid, $failedgroups)) {
if ($targetgroup === false) {
$keeptrying = false;
$o[] = 'error::indent::No more peers available in this separate group'; // todo translate
$o[] = 'error::indent::Not enough peers available'; // todo translate
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
$targetgroup = $this->get_element_with_lowest_workload($trygroups);
}
if ($targetgroup === false) {
$keeptrying = false;
$o[] = 'error::indent::Not enough peers available'; // todo translate
break;
}
$o[] = 'debug::indent::next square should be from group id ' . $targetgroup;
// now, choose a square from the target group
$trysquares = array_intersect_key($squareworkload, $allsquares[$targetgroup]);
// $o[] = 'debug::indent::individual workloads in this group are ' . json_encode($trysquares);
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) {
$o[] = 'debug::indent::unable to find an available square. trying another group';
$failedgroups[] = $targetgroup;
continue;
}
$o[] = 'debug::indent::target square = ' . $targetsquare;
// ok - we have found the square
$circlelinks[$circleid][] = $targetsquare;
$squarelinks[$targetsquare][] = $circleid;
$squareworkload[$targetsquare]++;
$o[] = 'debug::indent::increasing square workload to ' . $squareworkload[$targetsquare];
if ($targetgroup) {
// recalculate the group workload
$squaregroupsworkload[$targetgroup] = 0;
foreach ($allsquares[$targetgroup] as $squareid => $square) {
$squaregroupsworkload[$targetgroup] += $squareworkload[$squareid];
$o[] = 'debug::indent::next square should be from group id ' . $targetgroup;
// now, choose a square from the target group
$trysquares = array_intersect_key($squareworkload, $allsquares[$targetgroup]);
// $o[] = 'debug::indent::individual workloads in this group are ' . json_encode($trysquares);
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) {
$o[] = 'debug::indent::unable to find an available square. trying another group';
$failedgroups[] = $targetgroup;
continue;
}
$squaregroupsworkload[$targetgroup] /= count($allsquares[$targetgroup]);
$o[] = 'debug::indent::increasing group workload to ' . $squaregroupsworkload[$targetgroup];
}
} // end of processing this circle
} // end of processing circles in the group
$o[] = 'debug::indent::target square = ' . $targetsquare;
// ok - we have found the square
$circlelinks[$circleid][] = $targetsquare;
$squarelinks[$targetsquare][] = $circleid;
$squareworkload[$targetsquare]++;
$o[] = 'debug::indent::increasing square workload to ' . $squareworkload[$targetsquare];
if ($targetgroup) {
// recalculate the group workload
$squaregroupsworkload[$targetgroup] = 0;
foreach ($allsquares[$targetgroup] as $squareid => $square) {
$squaregroupsworkload[$targetgroup] += $squareworkload[$squareid];
}
$squaregroupsworkload[$targetgroup] /= count($allsquares[$targetgroup]);
$o[] = 'debug::indent::increasing group workload to ' . $squaregroupsworkload[$targetgroup];
}
} // 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 (self::USERTYPE_AUTHOR == $numper) {
......@@ -569,15 +645,14 @@ class workshop_random_allocator implements workshop_allocator {
/**
* Shuffle the order of array elements preserving the key=>values
*
* @author rich at home dot nl
* @link http://php.net/manual/en/function.shuffle.php#80586
* @param array $array to be shuffled
* @return true
*/
protected function shuffle_assoc(&$array) {
if (count($array) > 1) {
// $keys needs to be an array, no need to shuffle 1 item or empty arrays, anyway
$keys = array_rand($array, count($array));
$keys = array_keys($array);
shuffle($keys);
foreach($keys as $key) {
$new[$key] = $array[$key];
}
......
......@@ -74,6 +74,14 @@ class workshop_random_allocator_form extends moodleform {
$mform->setDefault('numper', workshop_random_allocator::USERTYPE_AUTHOR);
$mform->addGroup($grpnumofreviews, 'grpnumofreviews', get_string('numofreviews', 'workshopallocation_random'),
array(' '), false);
if (VISIBLEGROUPS == $gmode) {
$mform->addElement('checkbox', 'excludesamegroup', get_string('excludesamegroup', 'workshopallocation_random'));
$mform->setDefault('excludesamegroup', 0);
} else {
$mform->addElement('hidden', 'excludesamegroup', 0);
}
$mform->addElement('checkbox', 'removecurrent', get_string('removecurrentallocations', 'workshopallocation_random'));
$mform->setDefault('removecurrent', 0);
......
.path-mod-workshop .random-allocator .warning {
width: 80%;
margin: 0px auto 15px auto;
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment