ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK\a,?aa styles.cssnu[.path-mod-workshop .random-allocator .warning { width: 100%; margin: 0 auto 15px auto; } PK\rLEE%lang/en/workshopallocation_random.phpnu[. /** * Strings for component 'workshopallocation_random', language 'en', branch 'MOODLE_20_STABLE' * * @package workshopallocation * @subpackage random * @copyright 2009 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['addselfassessment'] = 'Add self-assessments'; $string['allocationaddeddetail'] = 'New assessment to be done: {$a->reviewername} is reviewer of {$a->authorname}'; $string['allocationdeallocategraded'] = 'Unable to deallocate already graded assessment: reviewer {$a->reviewername}, submission author {$a->authorname}'; $string['allocationreuseddetail'] = 'Reused assessment: {$a->reviewername} kept as reviewer of {$a->authorname}'; $string['allocationsettings'] = 'Allocation settings'; $string['assessmentdeleteddetail'] = 'Assessment deallocated: {$a->reviewername} is no longer reviewer of {$a->authorname}'; $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'] = '

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.

These users are currently not in a group: {$a}

'; $string['numofdeallocatedassessment'] = 'Deallocating {$a} assessment(s)'; $string['numofrandomlyallocatedsubmissions'] = 'Randomly assigning {$a} allocations'; $string['numofreviews'] = 'Number of reviews'; $string['numofselfallocatedsubmissions'] = 'Self-allocating {$a} submission(s)'; $string['numperauthor'] = 'per submission'; $string['numperreviewer'] = 'per reviewer'; $string['pluginname'] = 'Random allocation'; $string['privacy:metadata'] = 'The Random allocation plugin does not store any personal data. Actual personal data about who is going to assess whom are stored by the Workshop module itself and they form basis for exporting the assessments details.'; $string['randomallocationdone'] = 'Random allocation done'; $string['resultnomorepeers'] = 'No more peers available'; $string['resultnomorepeersingroup'] = 'No more peers available in this separate group'; $string['resultnotenoughpeers'] = 'Not enough peers available'; $string['resultnumperauthor'] = 'Trying to allocate {$a} review(s) per author'; $string['resultnumperreviewer'] = 'Trying to allocate {$a} review(s) per reviewer'; $string['removecurrentallocations'] = 'Remove current allocations'; $string['stats'] = 'Current allocation statistics'; PK\ ,lib.phpnu[. /** * Allocates the submissions randomly * * @package workshopallocation_random * @subpackage mod_workshop * @copyright 2009 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; // access to global variables during unit test require_once(__DIR__ . '/../lib.php'); // interface definition require_once(__DIR__ . '/../../locallib.php'); // workshop internal API require_once(__DIR__ . '/settings_form.php'); // settings form /** * Allocates the submissions randomly */ class workshop_random_allocator implements workshop_allocator { /** constants used to pass status messages between init() and ui() */ const MSG_SUCCESS = 1; /** workshop instance */ protected $workshop; /** mform with settings */ protected $mform; /** * @param workshop $workshop Workshop API object */ public function __construct(workshop $workshop) { $this->workshop = $workshop; } /** * Allocate submissions as requested by user * * @return workshop_allocation_result */ public function init() { global $PAGE; $result = new workshop_allocation_result($this); $customdata = array(); $customdata['workshop'] = $this->workshop; $this->mform = new workshop_random_allocator_form($PAGE->url, $customdata); if ($this->mform->is_cancelled()) { redirect($this->workshop->view_url()); } else if ($settings = $this->mform->get_data()) { $settings = workshop_random_allocator_setting::instance_from_object($settings); $this->execute($settings, $result); return $result; } else { // this branch is executed if the form is submitted but the data // doesn't validate and the form should be redisplayed // or on the first display of the form. $result->set_status(workshop_allocation_result::STATUS_VOID); return $result; } } /** * Executes the allocation based on the given settings * * @param workshop_random_allocator_setting $setting * @param workshop_allocation_result allocation result logger */ public function execute(workshop_random_allocator_setting $settings, workshop_allocation_result $result) { $authors = $this->workshop->get_potential_authors(); $authors = $this->workshop->get_grouped($authors); $reviewers = $this->workshop->get_potential_reviewers(!$settings->assesswosubmission); $reviewers = $this->workshop->get_grouped($reviewers); $assessments = $this->workshop->get_all_assessments(); $newallocations = array(); // array of array(reviewer => reviewee) if ($settings->numofreviews) { if ($settings->removecurrent) { // behave as if there were no current assessments $curassessments = array(); } else { $curassessments = $assessments; } $options = array(); $options['numofreviews'] = $settings->numofreviews; $options['numper'] = $settings->numper; $options['excludesamegroup'] = $settings->excludesamegroup; $randomallocations = $this->random_allocation($authors, $reviewers, $curassessments, $result, $options); $newallocations = array_merge($newallocations, $randomallocations); $result->log(get_string('numofrandomlyallocatedsubmissions', 'workshopallocation_random', count($randomallocations))); unset($randomallocations); } if ($settings->addselfassessment) { $selfallocations = $this->self_allocation($authors, $reviewers, $assessments); $newallocations = array_merge($newallocations, $selfallocations); $result->log(get_string('numofselfallocatedsubmissions', 'workshopallocation_random', count($selfallocations))); unset($selfallocations); } if (empty($newallocations)) { $result->log(get_string('noallocationtoadd', 'workshopallocation_random'), 'info'); } else { $newnonexistingallocations = $newallocations; $this->filter_current_assessments($newnonexistingallocations, $assessments); $this->add_new_allocations($newnonexistingallocations, $authors, $reviewers); $allreviewers = $reviewers[0]; $allreviewersreloaded = false; foreach ($newallocations as $newallocation) { $reviewerid = key($newallocation); $authorid = current($newallocation); $a = new stdClass(); if (isset($allreviewers[$reviewerid])) { $a->reviewername = fullname($allreviewers[$reviewerid]); } else { // this may happen if $settings->assesswosubmission is false but the reviewer // of the re-used assessment has not submitted anything. let us reload // the list of reviewers name including those without their submission if (!$allreviewersreloaded) { $allreviewers = $this->workshop->get_potential_reviewers(false); $allreviewersreloaded = true; } if (isset($allreviewers[$reviewerid])) { $a->reviewername = fullname($allreviewers[$reviewerid]); } else { // this should not happen usually unless the list of participants was changed // in between two cycles of allocations $a->reviewername = '#'.$reviewerid; } } if (isset($authors[0][$authorid])) { $a->authorname = fullname($authors[0][$authorid]); } else { $a->authorname = '#'.$authorid; } if (in_array($newallocation, $newnonexistingallocations)) { $result->log(get_string('allocationaddeddetail', 'workshopallocation_random', $a), 'ok', 1); } else { $result->log(get_string('allocationreuseddetail', 'workshopallocation_random', $a), 'ok', 1); } } } if ($settings->removecurrent) { $delassessments = $this->get_unkept_assessments($assessments, $newallocations, $settings->addselfassessment); // random allocator should not be able to delete assessments that have already been graded // by reviewer $result->log(get_string('numofdeallocatedassessment', 'workshopallocation_random', count($delassessments)), 'info'); foreach ($delassessments as $delassessmentkey => $delassessmentid) { $author = (object) []; $reviewer = (object) []; username_load_fields_from_object($author, $assessments[$delassessmentid], 'author'); username_load_fields_from_object($reviewer, $assessments[$delassessmentid], 'reviewer'); $a = [ 'authorname' => fullname($author), 'reviewername' => fullname($reviewer), ]; if (!is_null($assessments[$delassessmentid]->grade)) { $result->log(get_string('allocationdeallocategraded', 'workshopallocation_random', $a), 'error', 1); unset($delassessments[$delassessmentkey]); } else { $result->log(get_string('assessmentdeleteddetail', 'workshopallocation_random', $a), 'info', 1); } } $this->workshop->delete_assessment($delassessments); } $result->set_status(workshop_allocation_result::STATUS_EXECUTED); } /** * Returns the HTML code to print the user interface */ public function ui() { global $PAGE; $output = $PAGE->get_renderer('mod_workshop'); $m = optional_param('m', null, PARAM_INT); // status message code $message = new workshop_message(); if ($m == self::MSG_SUCCESS) { $message->set_text(get_string('randomallocationdone', 'workshopallocation_random')); $message->set_type(workshop_message::TYPE_OK); } $out = $output->container_start('random-allocator'); $out .= $output->render($message); // the nasty hack follows to bypass the sad fact that moodle quickforms do not allow to actually // return the HTML content, just to display it ob_start(); $this->mform->display(); $out .= ob_get_contents(); ob_end_clean(); // 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; } /** * Delete all data related to a given workshop module instance * * This plugin does not store any data. * * @see workshop_delete_instance() * @param int $workshopid id of the workshop module instance being deleted * @return void */ public static function delete_instance($workshopid) { return; } /** * Return an array of possible numbers of reviews to be done * * Should contain numbers 1, 2, 3, ... 10 and possibly others up to a reasonable value * * @return array of integers */ public static function available_numofreviews_list() { $options = []; for ($i = 100; $i > 20; $i = $i - 10) { $options[$i] = $i; } for ($i = 20; $i >= 0; $i--) { $options[$i] = $i; } return $options; } /** * Allocates submissions to their authors for review * * If the submission has already been allocated, it is skipped. If the author is not found among * reviewers, the submission is not assigned. * * @param array $authors grouped of {@see workshop::get_potential_authors()} * @param array $reviewers grouped by {@see workshop::get_potential_reviewers()} * @param array $assessments as returned by {@see workshop::get_all_assessments()} * @return array of new allocations to be created, array of array(reviewerid => authorid) */ protected function self_allocation($authors=array(), $reviewers=array(), $assessments=array()) { if (!isset($authors[0]) || !isset($reviewers[0])) { // no authors or no reviewers return array(); } $alreadyallocated = array(); foreach ($assessments as $assessment) { if ($assessment->authorid == $assessment->reviewerid) { $alreadyallocated[$assessment->authorid] = 1; } } $add = array(); // list of new allocations to be created foreach ($authors[0] as $authorid => $author) { // for all authors in all groups if (isset($reviewers[0][$authorid])) { // if the author can be reviewer if (!isset($alreadyallocated[$authorid])) { // and the allocation does not exist yet, then $add[] = array($authorid => $authorid); } } } return $add; } /** * Creates new assessment records * * @param array $newallocations pairs 'reviewerid' => 'authorid' * @param array $dataauthors authors by group, group [0] contains all authors * @param array $datareviewers reviewers by group, group [0] contains all reviewers * @return bool */ protected function add_new_allocations(array $newallocations, array $dataauthors, array $datareviewers) { global $DB; $newallocations = $this->get_unique_allocations($newallocations); $authorids = $this->get_author_ids($newallocations); $submissions = $this->workshop->get_submissions($authorids); $submissions = $this->index_submissions_by_authors($submissions); foreach ($newallocations as $newallocation) { $reviewerid = key($newallocation); $authorid = current($newallocation); if (!isset($submissions[$authorid])) { throw new moodle_exception('unabletoallocateauthorwithoutsubmission', 'workshop'); } $submission = $submissions[$authorid]; $status = $this->workshop->add_allocation($submission, $reviewerid, 1, true); // todo configurable weight? if (workshop::ALLOCATION_EXISTS == $status) { debugging('newallocations array contains existing allocation, this should not happen'); } } } /** * Flips the structure of submission so it is indexed by authorid attribute * * It is the caller's responsibility to make sure the submissions are not teacher * examples so no user is the author of more submissions. * * @param string $submissions array indexed by submission id * @return array indexed by author id */ protected function index_submissions_by_authors($submissions) { $byauthor = array(); if (is_array($submissions)) { foreach ($submissions as $submissionid => $submission) { if (isset($byauthor[$submission->authorid])) { throw new moodle_exception('moresubmissionsbyauthor', 'workshop'); } $byauthor[$submission->authorid] = $submission; } } return $byauthor; } /** * Extracts unique list of authors' IDs from the structure of new allocations * * @param array $newallocations of pairs 'reviewerid' => 'authorid' * @return array of authorids */ protected function get_author_ids($newallocations) { $authors = array(); foreach ($newallocations as $newallocation) { $authorid = reset($newallocation); if (!in_array($authorid, $authors)) { $authors[] = $authorid; } } return $authors; } /** * Removes duplicate allocations * * @param mixed $newallocations array of 'reviewerid' => 'authorid' pairs * @return array */ protected function get_unique_allocations($newallocations) { return array_merge(array_map('unserialize', array_unique(array_map('serialize', $newallocations)))); } /** * Returns the list of assessments to remove * * If user selects "removecurrentallocations", we should remove all current assessment records * and insert new ones. But this would needlessly waste table ids. Instead, let us find only those * assessments that have not been re-allocated in this run of allocation. So, the once-allocated * submissions are kept with their original id. * * @param array $assessments list of current assessments * @param mixed $newallocations array of 'reviewerid' => 'authorid' pairs * @param bool $keepselfassessments do not remove already allocated self assessments * @return array of assessments ids to be removed */ protected function get_unkept_assessments($assessments, $newallocations, $keepselfassessments) { $keepids = array(); // keep these assessments foreach ($assessments as $assessmentid => $assessment) { $aaid = $assessment->authorid; $arid = $assessment->reviewerid; if (($keepselfassessments) && ($aaid == $arid)) { $keepids[$assessmentid] = null; continue; } foreach ($newallocations as $newallocation) { $nrid = key($newallocation); $naid = current($newallocation); if (array($arid, $aaid) == array($nrid, $naid)) { // re-allocation found - let us continue with the next assessment $keepids[$assessmentid] = null; continue 2; } } } return array_keys(array_diff_key($assessments, $keepids)); } /** * 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 workshop_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 (workshop_random_allocator_setting::NUMPER_SUBMISSION == $numper) { // circles are authors, squares are reviewers $result->log(get_string('resultnumperauthor', 'workshopallocation_random', $numofreviews), 'info'); $allcircles = $authors; $allsquares = $reviewers; // get current workload list($circlelinks, $squarelinks) = $this->convert_assessments_to_links($assessments); } elseif (workshop_random_allocator_setting::NUMPER_REVIEWER == $numper) { // circles are reviewers, squares are authors $result->log(get_string('resultnumperreviewer', 'workshopallocation_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', '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]); } } // $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->workshop->cm, $this->workshop->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', 'workshopallocation_random'), 'error', 1); break; } $targetgroup = 0; } elseif (SEPARATEGROUPS == $gmode) { if (in_array($circlegroupid, $failedgroups)) { $keeptrying = false; $result->log(get_string('resultnomorepeersingroup', 'workshopallocation_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', 'workshopallocation_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 (workshop_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 (workshop_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; } /** * Extracts the information about reviews from the authors' and reviewers' perspectives * * @param array $assessments array of assessments as returned by {@link workshop::get_all_assessments()} * @return array of two arrays */ protected function convert_assessments_to_links($assessments) { $authorlinks = array(); // [authorid] => array(reviewerid, reviewerid, ...) $reviewerlinks = array(); // [reviewerid] => array(authorid, authorid, ...) foreach ($assessments as $assessment) { if (!isset($authorlinks[$assessment->authorid])) { $authorlinks[$assessment->authorid] = array(); } if (!isset($reviewerlinks[$assessment->reviewerid])) { $reviewerlinks[$assessment->reviewerid] = array(); } $authorlinks[$assessment->authorid][] = $assessment->reviewerid; $reviewerlinks[$assessment->reviewerid][] = $assessment->authorid; } return array($authorlinks, $reviewerlinks); } /** * Selects an element with the lowest workload * * If there are more elements with the same workload, choose one of them randomly. This may be * used to select a group or user. * * @param array $workload [groupid] => (int)workload * @return mixed int|bool id of the selected element or false if it is impossible to choose */ protected function get_element_with_lowest_workload($workload) { $precision = 10; if (empty($workload)) { return false; } $minload = round(min($workload), $precision); $minkeys = array(); foreach ($workload as $key => $val) { if (round($val, $precision) == $minload) { $minkeys[$key] = $val; } } return array_rand($minkeys); } /** * Shuffle the order of array elements preserving the key=>values * * @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_keys($array); shuffle($keys); foreach($keys as $key) { $new[$key] = $array[$key]; } $array = $new; } return true; // because this behaves like in-built shuffle(), which returns true } /** * Filter new allocations so that they do not contain an already existing assessment * * @param mixed $newallocations array of ('reviewerid' => 'authorid') tuples * @param array $assessments array of assessment records * @return void */ protected function filter_current_assessments(&$newallocations, $assessments) { foreach ($assessments as $assessment) { $allocation = array($assessment->reviewerid => $assessment->authorid); $foundat = moodle_array_keys_filter($newallocations, $allocation); $newallocations = array_diff_key($newallocations, array_flip($foundat)); } } } /** * Data object defining the settings structure for the random allocator */ class workshop_random_allocator_setting { /** aim to a number of reviews per one submission {@see self::$numper} */ const NUMPER_SUBMISSION = 1; /** aim to a number of reviews per one reviewer {@see self::$numper} */ const NUMPER_REVIEWER = 2; /** @var int number of reviews */ public $numofreviews; /** @var int either {@link self::NUMPER_SUBMISSION} or {@link self::NUMPER_REVIEWER} */ public $numper; /** @var bool prevent reviews by peers from the same group */ public $excludesamegroup; /** @var bool remove current allocations */ public $removecurrent; /** @var bool participants can assess without having submitted anything */ public $assesswosubmission; /** @var bool add self-assessments */ public $addselfassessment; /** @var bool scheduled allocation status */ public $enablescheduled; /** * Use the factory method {@link self::instance_from_object()} */ protected function __construct() { } /** * Factory method making the instance from data in the passed object * * @param stdClass $data an object holding the values for our public properties * @return workshop_random_allocator_setting */ public static function instance_from_object(stdClass $data) { $i = new self(); if (!isset($data->numofreviews)) { throw new coding_exception('Missing value of the numofreviews property'); } else { $i->numofreviews = (int)$data->numofreviews; } if (!isset($data->numper)) { throw new coding_exception('Missing value of the numper property'); } else { $i->numper = (int)$data->numper; if ($i->numper !== self::NUMPER_SUBMISSION and $i->numper !== self::NUMPER_REVIEWER) { throw new coding_exception('Invalid value of the numper property'); } } foreach (array('excludesamegroup', 'removecurrent', 'assesswosubmission', 'addselfassessment') as $k) { if (isset($data->$k)) { $i->$k = (bool)$data->$k; } else { $i->$k = false; } } return $i; } /** * Factory method making the instance from data in the passed text * * @param string $text as returned by {@link self::export_text()} * @return workshop_random_allocator_setting */ public static function instance_from_text($text) { return self::instance_from_object(json_decode($text)); } /** * Exports the instance data as a text for persistant storage * * The returned data can be later used by {@self::instance_from_text()} factory method * to restore the instance data. The current implementation uses JSON export format. * * @return string JSON representation of our public properties */ public function export_text() { $getvars = function($obj) { return get_object_vars($obj); }; return json_encode($getvars($this)); } } PK\R#pp version.phpnu[. /** * Defines the version of the subplugin * * @package workshopallocation_random * @subpackage mod_workshop * @copyright 2009 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'workshopallocation_random'; $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->maturity = MATURITY_STABLE;PK\%classes/privacy/provider.phpnu[. /** * Provides the class {@link workshopallocation_random\privacy\provider} * * @package workshopallocation_random * @category privacy * @copyright 2018 David Mudrák * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace workshopallocation_random\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy API implementation for the Random allocation method. * * @copyright 2018 David Mudrák * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Explain that this plugin stores no personal data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK\|settings_form.phpnu[. /** * Random allocator settings form * * @package workshopallocation * @subpackage random * @copyright 2009 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/lib/formslib.php'); /** * Allocator settings form * * This is used by {@see workshop_random_allocator::ui()} to set up allocation parameters. * * @copyright 2009 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class workshop_random_allocator_form extends moodleform { /** * Definition of the setting form elements */ public function definition() { $mform = $this->_form; $workshop = $this->_customdata['workshop']; $plugindefaults = get_config('workshopallocation_random'); $mform->addElement('header', 'randomallocationsettings', get_string('allocationsettings', 'workshopallocation_random')); $gmode = groups_get_activity_groupmode($workshop->cm, $workshop->course); switch ($gmode) { case NOGROUPS: $grouplabel = get_string('groupsnone', 'group'); break; case VISIBLEGROUPS: $grouplabel = get_string('groupsvisible', 'group'); break; case SEPARATEGROUPS: $grouplabel = get_string('groupsseparate', 'group'); break; } $mform->addElement('static', 'groupmode', get_string('groupmode', 'group'), $grouplabel); $options_numper = array( workshop_random_allocator_setting::NUMPER_SUBMISSION => get_string('numperauthor', 'workshopallocation_random'), workshop_random_allocator_setting::NUMPER_REVIEWER => get_string('numperreviewer', 'workshopallocation_random') ); $grpnumofreviews = array(); $grpnumofreviews[] = $mform->createElement('text', 'numofreviews', '', array('size' => 5, 'maxlength' => 20)); $mform->setType('numofreviews', PARAM_INT); $mform->setDefault('numofreviews', $plugindefaults->numofreviews); $grpnumofreviews[] = $mform->createElement('select', 'numper', '', $options_numper); $mform->setDefault('numper', workshop_random_allocator_setting::NUMPER_SUBMISSION); $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->setType('excludesamegroup', PARAM_BOOL); } $mform->addElement('checkbox', 'removecurrent', get_string('removecurrentallocations', 'workshopallocation_random')); $mform->setDefault('removecurrent', 0); $mform->addElement('checkbox', 'assesswosubmission', get_string('assesswosubmission', 'workshopallocation_random')); $mform->setDefault('assesswosubmission', 0); if (empty($workshop->useselfassessment)) { $mform->addElement('static', 'addselfassessment', get_string('addselfassessment', 'workshopallocation_random'), get_string('selfassessmentdisabled', 'workshop')); } else { $mform->addElement('checkbox', 'addselfassessment', get_string('addselfassessment', 'workshopallocation_random')); } $this->add_action_buttons(false); } /** * Validate the allocation settings. * * @param array $data * @param array $files * @return array */ public function validation($data, $files): array { $errors = parent::validation($data, $files); if ($data['numofreviews'] < 0) { $errors['grpnumofreviews'] = get_string('invalidnum', 'core_error'); } return $errors; } } PK\+55tests/allocator_test.phpnu[. /** * Unit tests for Random allocation * * @package workshopallocation_random * @category test * @copyright 2009 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace workshopallocation_random; use workshop; use workshop_random_allocator; defined('MOODLE_INTERNAL') || die(); // Include the code to test global $CFG; require_once($CFG->dirroot . '/mod/workshop/locallib.php'); require_once($CFG->dirroot . '/mod/workshop/allocation/random/lib.php'); /** * Unit tests for Random allocation * * @package workshopallocation_random * @category test * @copyright 2009 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class allocator_test extends \advanced_testcase { /** workshop instance emulation */ protected $workshop; /** allocator instance */ protected $allocator; protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $workshop = $this->getDataGenerator()->create_module('workshop', array('course' => $course)); $cm = get_fast_modinfo($course)->instances['workshop'][$workshop->id]; $this->workshop = new workshop($workshop, $cm, $course); $this->allocator = new testable_workshop_random_allocator($this->workshop); } protected function tearDown(): void { $this->allocator = null; $this->workshop = null; parent::tearDown(); } public function test_self_allocation_empty_values(): void { // fixture setup & exercise SUT & verify $this->assertEquals(array(), $this->allocator->self_allocation()); } public function test_self_allocation_equal_user_groups(): void { // fixture setup $authors = array(0 => array_fill_keys(array(4, 6, 10), new \stdClass())); $reviewers = array(0 => array_fill_keys(array(4, 6, 10), new \stdClass())); // exercise SUT $newallocations = $this->allocator->self_allocation($authors, $reviewers); // verify $this->assertEquals(array(array(4 => 4), array(6 => 6), array(10 => 10)), $newallocations); } public function test_self_allocation_different_user_groups(): void { // fixture setup $authors = array(0 => array_fill_keys(array(1, 4, 5, 10, 13), new \stdClass())); $reviewers = array(0 => array_fill_keys(array(4, 7, 10), new \stdClass())); // exercise SUT $newallocations = $this->allocator->self_allocation($authors, $reviewers); // verify $this->assertEquals(array(array(4 => 4), array(10 => 10)), $newallocations); } public function test_self_allocation_skip_existing(): void { // fixture setup $authors = array(0 => array_fill_keys(array(3, 4, 10), new \stdClass())); $reviewers = array(0 => array_fill_keys(array(3, 4, 10), new \stdClass())); $assessments = array(23 => (object)array('authorid' => 3, 'reviewerid' => 3)); // exercise SUT $newallocations = $this->allocator->self_allocation($authors, $reviewers, $assessments); // verify $this->assertEquals(array(array(4 => 4), array(10 => 10)), $newallocations); } public function test_get_author_ids(): void { // fixture setup $newallocations = array(array(1 => 3), array(2 => 1), array(3 => 1)); // exercise SUT & verify $this->assertEquals(array(3, 1), $this->allocator->get_author_ids($newallocations)); } public function test_index_submissions_by_authors(): void { // fixture setup $submissions = array( 676 => (object)array('id' => 676, 'authorid' => 23), 121 => (object)array('id' => 121, 'authorid' => 56), ); // exercise SUT $submissions = $this->allocator->index_submissions_by_authors($submissions); // verify $this->assertEquals(array( 23 => (object)array('id' => 676, 'authorid' => 23), 56 => (object)array('id' => 121, 'authorid' => 56), ), $submissions); } public function test_index_submissions_by_authors_duplicate_author(): void { // fixture setup $submissions = array( 14 => (object)array('id' => 676, 'authorid' => 3), 87 => (object)array('id' => 121, 'authorid' => 3), ); // exercise SUT $this->expectException(\moodle_exception::class); $submissions = $this->allocator->index_submissions_by_authors($submissions); } public function test_get_unique_allocations(): void { // fixture setup $newallocations = array(array(4 => 5), array(6 => 6), array(1 => 16), array(4 => 5), array(16 => 1)); // exercise SUT $newallocations = $this->allocator->get_unique_allocations($newallocations); // verify $this->assertEquals(array(array(4 => 5), array(6 => 6), array(1 => 16), array(16 => 1)), $newallocations); } public function test_get_unkept_assessments_no_keep_selfassessments(): void { // fixture setup $assessments = array( 23 => (object)array('authorid' => 3, 'reviewerid' => 3), 45 => (object)array('authorid' => 5, 'reviewerid' => 11), 12 => (object)array('authorid' => 6, 'reviewerid' => 3), ); $newallocations = array(array(4 => 5), array(11 => 5), array(1 => 16), array(4 => 5), array(16 => 1)); // exercise SUT $delassessments = $this->allocator->get_unkept_assessments($assessments, $newallocations, false); // verify // we want to keep $assessments[45] because it has been re-allocated $this->assertEquals(array(23, 12), $delassessments); } public function test_get_unkept_assessments_keep_selfassessments(): void { // fixture setup $assessments = array( 23 => (object)array('authorid' => 3, 'reviewerid' => 3), 45 => (object)array('authorid' => 5, 'reviewerid' => 11), 12 => (object)array('authorid' => 6, 'reviewerid' => 3), ); $newallocations = array(array(4 => 5), array(11 => 5), array(1 => 16), array(4 => 5), array(16 => 1)); // exercise SUT $delassessments = $this->allocator->get_unkept_assessments($assessments, $newallocations, true); // verify // we want to keep $assessments[45] because it has been re-allocated // we want to keep $assessments[23] because if is self assessment $this->assertEquals(array(12), $delassessments); } /** * Aggregates assessment info per author and per reviewer */ public function test_convert_assessments_to_links(): void { // fixture setup $assessments = array( 23 => (object)array('authorid' => 3, 'reviewerid' => 3), 45 => (object)array('authorid' => 5, 'reviewerid' => 11), 12 => (object)array('authorid' => 5, 'reviewerid' => 3), ); // exercise SUT list($authorlinks, $reviewerlinks) = $this->allocator->convert_assessments_to_links($assessments); // verify $this->assertEquals(array(3 => array(3), 5 => array(11, 3)), $authorlinks); $this->assertEquals(array(3 => array(3, 5), 11 => array(5)), $reviewerlinks); } /** * Trivial case */ public function test_convert_assessments_to_links_empty(): void { // fixture setup $assessments = array(); // exercise SUT list($authorlinks, $reviewerlinks) = $this->allocator->convert_assessments_to_links($assessments); // verify $this->assertEquals(array(), $authorlinks); $this->assertEquals(array(), $reviewerlinks); } /** * If there is a single element with the lowest workload, it should be chosen */ public function test_get_element_with_lowest_workload_deterministic(): void { // fixture setup $workload = array(4 => 6, 9 => 1, 10 => 2); // exercise SUT $chosen = $this->allocator->get_element_with_lowest_workload($workload); // verify $this->assertEquals(9, $chosen); } /** * If there are no elements available, must return false */ public function test_get_element_with_lowest_workload_impossible(): void { // fixture setup $workload = array(); // exercise SUT $chosen = $this->allocator->get_element_with_lowest_workload($workload); // verify $this->assertTrue($chosen === false); } /** * If there are several elements with the lowest workload, one of them should be chosen randomly */ public function test_get_element_with_lowest_workload_random(): void { // fixture setup $workload = array(4 => 6, 9 => 2, 10 => 2); // exercise SUT $elements = $this->allocator->get_element_with_lowest_workload($workload); // verify // in theory, this test can fail even if the function works well. However, the probability of getting // a row of a hundred same ids in this use case is 1/pow(2, 100) // also, this just tests that each of the two elements has been chosen at least once. this is not to // measure the quality or randomness of the algorithm $counts = array(4 => 0, 9 => 0, 10 => 0); for ($i = 0; $i < 100; $i++) { $chosen = $this->allocator->get_element_with_lowest_workload($workload); if (!in_array($chosen, array(4, 9, 10))) { $this->fail('Invalid element ' . var_export($chosen, true) . ' chosen'); break; } else { $counts[$this->allocator->get_element_with_lowest_workload($workload)]++; } } $this->assertTrue(($counts[9] > 0) && ($counts[10] > 0)); } /** * Floats should be rounded before they are compared * * This should test */ public function test_get_element_with_lowest_workload_random_floats(): void { // fixture setup $workload = array(1 => 1/13, 2 => 0.0769230769231); // should be considered as the same value // exercise SUT $elements = $this->allocator->get_element_with_lowest_workload($workload); // verify $counts = array(1 => 0, 2 => 0); for ($i = 0; $i < 100; $i++) { $chosen = $this->allocator->get_element_with_lowest_workload($workload); if (!in_array($chosen, array(1, 2))) { $this->fail('Invalid element ' . var_export($chosen, true) . ' chosen'); break; } else { $counts[$this->allocator->get_element_with_lowest_workload($workload)]++; } } $this->assertTrue(($counts[1] > 0) && ($counts[2] > 0)); } /** * Filter new assessments so they do not contain existing */ public function test_filter_current_assessments(): void { // fixture setup $newallocations = array(array(3 => 5), array(11 => 5), array(2 => 9), array(3 => 5)); $assessments = array( 23 => (object)array('authorid' => 3, 'reviewerid' => 3), 45 => (object)array('authorid' => 5, 'reviewerid' => 11), 12 => (object)array('authorid' => 5, 'reviewerid' => 3), ); // exercise SUT $this->allocator->filter_current_assessments($newallocations, $assessments); // verify $this->assertEquals(array_values($newallocations), array(array(2 => 9))); } } /** * Make protected methods we want to test public */ class testable_workshop_random_allocator extends workshop_random_allocator { public function self_allocation($authors=array(), $reviewers=array(), $assessments=array()) { return parent::self_allocation($authors, $reviewers, $assessments); } public function get_author_ids($newallocations) { return parent::get_author_ids($newallocations); } public function index_submissions_by_authors($submissions) { return parent::index_submissions_by_authors($submissions); } public function get_unique_allocations($newallocations) { return parent::get_unique_allocations($newallocations); } public function get_unkept_assessments($assessments, $newallocations, $keepselfassessments) { return parent::get_unkept_assessments($assessments, $newallocations, $keepselfassessments); } public function convert_assessments_to_links($assessments) { return parent::convert_assessments_to_links($assessments); } public function get_element_with_lowest_workload($workload) { return parent::get_element_with_lowest_workload($workload); } public function filter_current_assessments(&$newallocations, $assessments) { return parent::filter_current_assessments($newallocations, $assessments); } } PK\ޤV44 settings.phpnu[. /** * The configuration variables for "Random allocation" subplugin * * @package workshopallocation * @subpackage random * @copyright 2009 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/lib.php'); $settings->add(new admin_setting_configselect('workshopallocation_random/numofreviews', get_string('numofreviews', 'workshopallocation_random'), get_string('confignumofreviews', 'workshopallocation_random'), 5, workshop_random_allocator::available_numofreviews_list())); PK\a,?aa styles.cssnu[PK\rLEE%lang/en/workshopallocation_random.phpnu[PK\ ,5lib.phpnu[PK\R#pp Tversion.phpnu[PK\%classes/privacy/provider.phpnu[PK\|8settings_form.phpnu[PK\+55Ntests/allocator_test.phpnu[PK\ޤV44 _settings.phpnu[PK