lib.php 27.7 KB
Newer Older
1
2
<?php

3
4
// This file is part of Moodle - http://moodle.org/
//
5
6
7
8
9
10
11
12
13
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
14
//
15
16
17
18
19
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Allocates the submissions randomly
20
 *
21
22
23
24
 * @package    workshopallocation
 * @subpackage random
 * @copyright  2009 David Mudrak <david.mudrak@gmail.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
26
27
28
 */

defined('MOODLE_INTERNAL') || die();

29
30
31
32
33
34
35
36
37
global $CFG;    // access to global variables during unit test

require_once(dirname(dirname(__FILE__)) . '/lib.php');                  // interface definition
require_once(dirname(dirname(dirname(__FILE__))) . '/locallib.php');    // workshop internal API
require_once(dirname(__FILE__) . '/settings_form.php');                 // settings form

/**
 * Allocates the submissions randomly
 */
38
39
class workshop_random_allocator implements workshop_allocator {

40
41
42
43
44
45
46
    /** constants used to pass status messages between init() and ui() */
    const MSG_SUCCESS       = 1;

    /** constants used in allocation settings form */
    const USERTYPE_AUTHOR   = 1;
    const USERTYPE_REVIEWER = 2;

47
48
49
    /** workshop instance */
    protected $workshop;

50
51
    /** mform with settings */
    protected $mform;
52

53
    /**
54
     * @param workshop $workshop Workshop API object
55
56
     */
    public function __construct(workshop $workshop) {
57
        $this->workshop = $workshop;
58
    }
59

60
61
62
63
64
    /**
     * Allocate submissions as requested by user
     */
    public function init() {
        global $PAGE;
65

66
        $customdata = array();
67
        $customdata['workshop'] = $this->workshop;
68
69
        $this->mform = new workshop_random_allocator_form($PAGE->url, $customdata);
        if ($this->mform->is_cancelled()) {
70
            redirect($PAGE->url->out(false));
71
72
73
        } else if ($settings = $this->mform->get_data()) {
            // process validated data
            if (!confirm_sesskey()) {
David Mudrak's avatar
David Mudrak committed
74
                throw new moodle_exception('confirmsesskeybad');
75
76
77
78
            }
            $o                  = array();      // list of output messages
            $numofreviews       = required_param('numofreviews', PARAM_INT);
            $numper             = required_param('numper', PARAM_INT);
79
80
81
            $removecurrent      = optional_param('removecurrent', false, PARAM_BOOL);
            $assesswosubmission = optional_param('assesswosubmission', false, PARAM_BOOL);
            $addselfassessment  = optional_param('addselfassessment', false, PARAM_BOOL);
82
            $musthavesubmission = empty($assesswosubmission);
83

David Mudrak's avatar
David Mudrak committed
84
            $authors            = $this->workshop->get_potential_authors();
85
            $authors            = $this->workshop->get_grouped($authors);
David Mudrak's avatar
David Mudrak committed
86
            $reviewers          = $this->workshop->get_potential_reviewers($musthavesubmission);
87
            $reviewers          = $this->workshop->get_grouped($reviewers);
David Mudrak's avatar
David Mudrak committed
88
            $assessments        = $this->workshop->get_all_assessments();
89

90
            $newallocations     = array();      // array of array(reviewer => reviewee)
91
92

            if ($numofreviews) {
93
94
95
96
97
98
99
                if ($removecurrent) {
                    // behave as if there were no current assessments
                    $curassessments = array();
                } else {
                    $curassessments = $assessments;
                }
                $randomallocations  = $this->random_allocation($authors, $reviewers, $curassessments, $numofreviews, $numper, $o);
100
                $newallocations     = array_merge($newallocations, $randomallocations);
101
                $o[] = 'ok::' . get_string('numofrandomlyallocatedsubmissions', 'workshopallocation_random', count($randomallocations));
102
103
104
105
106
                unset($randomallocations);
            }
            if ($addselfassessment) {
                $selfallocations    = $this->self_allocation($authors, $reviewers, $assessments);
                $newallocations     = array_merge($newallocations, $selfallocations);
107
                $o[] = 'ok::' . get_string('numofselfallocatedsubmissions', 'workshopallocation_random', count($selfallocations));
108
109
110
                unset($selfallocations);
            }
            if (empty($newallocations)) {
111
                $o[] = 'info::' . get_string('noallocationtoadd', 'workshopallocation_random');
112
            } else {
113
114
115
                $newnonexistingallocations = $newallocations;
                $this->filter_current_assessments($newnonexistingallocations, $assessments);
                $this->add_new_allocations($newnonexistingallocations, $authors, $reviewers);
116
117
                foreach ($newallocations as $newallocation) {
                    list($reviewerid, $authorid) = each($newallocation);
118
                    $a                  = new stdclass();
119
120
                    $a->reviewername    = fullname($reviewers[0][$reviewerid]);
                    $a->authorname      = fullname($authors[0][$authorid]);
121
122
123
124
125
                    if (in_array($newallocation, $newnonexistingallocations)) {
                        $o[] = 'ok::indent::' . get_string('allocationaddeddetail', 'workshopallocation_random', $a);
                    } else {
                        $o[] = 'ok::indent::' . get_string('allocationreuseddetail', 'workshopallocation_random', $a);
                    }
126
127
128
129
130
131
                }
            }
            if ($removecurrent) {
                $delassessments = $this->get_unkept_assessments($assessments, $newallocations, $addselfassessment);
                // random allocator should not be able to delete assessments that have already been graded
                // by reviewer
132
                $o[] = 'info::' . get_string('numofdeallocatedassessment', 'workshopallocation_random', count($delassessments));
133
                foreach ($delassessments as $delassessmentkey => $delassessmentid) {
134
                    $a = new stdclass();
135
136
137
138
139
140
141
                    $a->authorname      = fullname((object)array(
                            'lastname'  => $assessments[$delassessmentid]->authorlastname,
                            'firstname' => $assessments[$delassessmentid]->authorfirstname));
                    $a->reviewername    = fullname((object)array(
                            'lastname'  => $assessments[$delassessmentid]->reviewerlastname,
                            'firstname' => $assessments[$delassessmentid]->reviewerfirstname));
                    if (!is_null($assessments[$delassessmentid]->grade)) {
142
                        $o[] = 'error::indent::' . get_string('allocationdeallocategraded', 'workshopallocation_random', $a);
143
                        unset($delassessments[$delassessmentkey]);
144
                    } else {
145
                        $o[] = 'info::indent::' . get_string('assessmentdeleteddetail', 'workshopallocation_random', $a);
146
147
148
149
150
151
152
153
154
                    }
                }
                $this->workshop->delete_assessment($delassessments);
            }
            return $o;
        } 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.
155
        }
156
    }
157

158
    /**
159
     * Returns the HTML code to print the user interface
160
     */
161
    public function ui() {
162
163
164
        global $PAGE;

        $output = $PAGE->get_renderer('mod_workshop');
165
166

        $m = optional_param('m', null, PARAM_INT);  // status message code
167
        $message = new workshop_message();
168
        if ($m == self::MSG_SUCCESS) {
169
170
            $message->set_text(get_string('randomallocationdone', 'workshopallocation_random'));
            $message->set_type(workshop_message::TYPE_OK);
171
172
        }

173
174
        $out  = $output->container_start('random-allocator');
        $out .= $output->render($message);
175
176
177
        // 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();
178
        $this->mform->display();
179
180
        $out .= ob_get_contents();
        ob_end_clean();
181
182
183
        $out .= $output->container_end();

        // TODO $out .= $output->heading(get_string('stats', 'workshopallocation_random'));
184
185

        return $out;
186
187
    }

188
189
190
191
192
193
194
195
196
197
198
199
200
    /**
     * 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;
    }

David Mudrak's avatar
David Mudrak committed
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
    /**
     * 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 = array();
        $options[30] = 30;
        $options[20] = 20;
        $options[15] = 15;
        for ($i = 10; $i >= 0; $i--) {
            $options[$i] = $i;
        }
        return $options;
    }

219
220
221
222
223
224
    /**
     * 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.
     *
David Mudrak's avatar
David Mudrak committed
225
226
227
     * @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()}
228
229
230
231
232
233
     * @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();
234
        }
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
        $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;
253
254
    }

255
256
257
258
259
260
261
262
    /**
     * 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
     */
David Mudrak's avatar
David Mudrak committed
263
    protected function add_new_allocations(array $newallocations, array $dataauthors, array $datareviewers) {
264
        global $DB;
265

266
267
        $newallocations = $this->get_unique_allocations($newallocations);
        $authorids      = $this->get_author_ids($newallocations);
268
        $submissions    = $this->workshop->get_submissions($authorids);
269
270
271
272
        $submissions    = $this->index_submissions_by_authors($submissions);
        foreach ($newallocations as $newallocation) {
            list($reviewerid, $authorid) = each($newallocation);
            if (!isset($submissions[$authorid])) {
David Mudrak's avatar
David Mudrak committed
273
                throw new moodle_exception('unabletoallocateauthorwithoutsubmission', 'workshop');
274
275
            }
            $submission = $submissions[$authorid];
276
            $status = $this->workshop->add_allocation($submission, $reviewerid, 1, true);   // todo configurable weight?
277
            if (workshop::ALLOCATION_EXISTS == $status) {
278
279
280
                debugging('newallocations array contains existing allocation, this should not happen');
            }
        }
281
282
    }

283
    /**
David Mudrak's avatar
David Mudrak committed
284
     * Flips the structure of submission so it is indexed by authorid attribute
285
286
287
288
289
290
291
292
293
294
295
     *
     * 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) {
David Mudrak's avatar
David Mudrak committed
296
                if (isset($byauthor[$submission->authorid])) {
David Mudrak's avatar
David Mudrak committed
297
                    throw new moodle_exception('moresubmissionsbyauthor', 'workshop');
298
                }
David Mudrak's avatar
David Mudrak committed
299
                $byauthor[$submission->authorid] = $submission;
300
301
302
303
            }
        }
        return $byauthor;
    }
304

305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
    /**
     * 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;
320
321
    }

322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
    /**
     * 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) {
                list($nrid, $naid) = each($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));
    }

    /**
367
368
369
370
371
372
     * 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).
373
     *
374
375
376
     * @param array    $authors      structure of grouped authors
     * @param resource $reviewers    structure of grouped reviewers
     * @param array    $assessments  currently assigned assessments to be kept
377
     * @param mixed    $numofreviews number of reviews to be allocated to each circle
378
379
380
     * @param mixed    $numper       what user type the circles represent
     * @param array    $o            reference to an array of log messages
     * @return array                 array of (reviewerid => authorid) pairs
381
     */
382
    protected function random_allocation($authors, $reviewers, $assessments, $numofreviews, $numper, &$o) {
383
384
385
386
        if (empty($authors) || empty($reviewers)) {
            // nothing to be done
            return array();
        }
387
        if (self::USERTYPE_AUTHOR == $numper) {
388
389
390
391
392
393
            // circles are authors, squares are reviewers
            $o[] = 'info::Trying to allocate ' . $numofreviews . ' review(s) per author'; // todo translate
            $allcircles = $authors;
            $allsquares = $reviewers;
            // get current workload
            list($circlelinks, $squarelinks) = $this->convert_assessments_to_links($assessments);
394
        } elseif (self::USERTYPE_REVIEWER == $numper) {
395
396
397
398
399
400
401
            // circles are reviewers, squares are authors
            $o[] = 'info::trying to allocate ' . $numofreviews . ' review(s) per reviewer'; // todo translate
            $allcircles = $reviewers;
            $allsquares = $authors;
            // get current workload
            list($squarelinks, $circlelinks) = $this->convert_assessments_to_links($assessments);
        } else {
David Mudrak's avatar
David Mudrak committed
402
            throw new moodle_exception('unknownusertypepassed', 'workshop');
403
        }
David Mudrak's avatar
David Mudrak committed
404
405
        // $o[] = 'debug::circle links = ' . json_encode($circlelinks);
        // $o[] = 'debug::square links = ' . json_encode($squarelinks);
406
407
408
409
410
411
412
413
414
415
416
417
418
419
        $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
David Mudrak's avatar
David Mudrak committed
420
421
        // $o[] = 'debug::square workload = ' . json_encode($squareworkload);
        // $o[] = 'debug::square group workload = ' . json_encode($squaregroupsworkload);
David Mudrak's avatar
David Mudrak committed
422
        $gmode = groups_get_activity_groupmode($this->workshop->cm, $this->workshop->course);
423
424
425
426
427
428
429
430
431
        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);
        }
        $this->shuffle_assoc($circlegroups);
David Mudrak's avatar
David Mudrak committed
432
        // $o[] = 'debug::circle groups = ' . json_encode($circlegroups);
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
        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;
                        }
                        $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]
464
                        $trygroups = array_diff_key($trygroups, array_flip($failedgroups));     // without previous failures
465
466
467
468
469
470
471
472
473
474
                        $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]);
David Mudrak's avatar
David Mudrak committed
475
                    // $o[] = 'debug::indent::individual workloads in this group are ' . json_encode($trysquares);
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
                    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];
                        }
                        $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
        } // end of processing circle groups
        $returned = array();
503
        if (self::USERTYPE_AUTHOR == $numper) {
504
505
506
507
508
509
510
            // circles are authors, squares are reviewers
            foreach ($circlelinks as $circleid => $squares) {
                foreach ($squares as $squareid) {
                    $returned[] = array($squareid => $circleid);
                }
            }
        }
511
        if (self::USERTYPE_REVIEWER == $numper) {
512
513
514
515
516
517
518
519
            // circles are reviewers, squares are authors
            foreach ($circlelinks as $circleid => $squares) {
                foreach ($squares as $squareid) {
                    $returned[] = array($circleid => $squareid);
                }
            }
        }
        return $returned;
520
    }
521

522
523
524
    /**
     * Extracts the information about reviews from the authors' and reviewers' perspectives
     *
David Mudrak's avatar
David Mudrak committed
525
     * @param array $assessments array of assessments as returned by {@link workshop::get_all_assessments()}
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
     * @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) {
554
555
        $precision = 10;

556
557
558
        if (empty($workload)) {
            return false;
        }
559
560
561
562
563
564
565
        $minload = round(min($workload), $precision);
        $minkeys = array();
        foreach ($workload as $key => $val) {
            if (round($val, $precision) == $minload) {
                $minkeys[$key] = $val;
            }
        }
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
        return array_rand($minkeys);
    }

    /**
     * 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));
            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        = array_keys($newallocations, $allocation);
            $newallocations = array_diff_key($newallocations, array_flip($foundat));
        }
    }
603
}