MDL-19870 Random allocation - work in progress
[moodle.git] / mod / workshop / locallib.php
CommitLineData
de811c0c 1<?php
53fad4b9
DM
2
3// This file is part of Moodle - http://moodle.org/
4//
de811c0c
DM
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
53fad4b9 14//
de811c0c
DM
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
53fad4b9 17
de811c0c 18/**
6e309973 19 * Library of internal classes and functions for module workshop
de811c0c 20 *
53fad4b9 21 * All the workshop specific functions, needed to implement the module
6e309973 22 * logic, should go to here. Instead of having bunch of function named
53fad4b9 23 * workshop_something() taking the workshop instance as the first
6e309973 24 * parameter, we use a class workshop_api that provides all methods.
53fad4b9 25 *
de811c0c
DM
26 * @package mod-workshop
27 * @copyright 2009 David Mudrak <david.mudrak@gmail.com>
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29 */
30
31defined('MOODLE_INTERNAL') || die();
32
6e309973 33require_once(dirname(__FILE__).'/lib.php'); // we extend this library here
0968b1a3 34
6e309973 35define('WORKSHOP_ALLOCATION_EXISTS', -1); // return status of {@link add_allocation}
6e309973
DM
36
37/**
38 * Full-featured workshop API
39 *
53fad4b9 40 * This extends the module base API and adds the internal methods that are called
6e309973
DM
41 * from the module itself. The class should be initialized right after you get
42 * $workshop and $cm records at the begining of the script.
43 */
44class workshop_api extends workshop {
45
46 /** grading strategy instance */
47 protected $strategy_api=null;
48
49 /**
50 * Initialize the object using the data from DB
51 *
52 * @param object $instance The instance data row from {workshop} table
53 * @param object $md Course module record
54 */
55 public function __construct($instance, $cm) {
66c9894d 56
6e309973
DM
57 parent::__construct($instance, $cm);
58 }
59
6e309973
DM
60 /**
61 * Fetches all users with the capability mod/workshop:submit in the current context
62 *
66c9894d 63 * Static variables used to cache the results. The returned objects contain id, lastname
6e309973 64 * and firstname properties and are ordered by lastname,firstname
53fad4b9
DM
65 *
66 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible authors otherwise.
67 * @return array array[userid] => stdClass{->id ->lastname ->firstname}
6e309973 68 */
53fad4b9 69 public function get_peer_authors($musthavesubmission=true) {
66c9894d
DM
70 global $DB;
71 static $users = null;
72 static $userswithsubmission = null;
6e309973
DM
73
74 if (is_null($users)) {
75 $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
76 $users = get_users_by_capability($context, 'mod/workshop:submit',
77 'u.id, u.lastname, u.firstname', 'u.lastname,u.firstname', '', '', '', '', false, false, true);
78 }
66c9894d
DM
79
80 if ($musthavesubmission && is_null($userswithsubmission)) {
81 $userswithsubmission = $DB->get_records_list('workshop_submissions', 'userid', array_keys($users),'', 'userid');
82 $userswithsubmission = array_intersect_key($users, $userswithsubmission);
83 }
84
85 if ($musthavesubmission) {
86 return $userswithsubmission;
87 } else {
88 return $users;
89 }
6e309973
DM
90 }
91
53fad4b9
DM
92 /**
93 * Returns all users with the capability mod/workshop:submit sorted by groups
94 *
95 * This takes the module grouping settings into account. If "Available for group members only"
96 * is set, returns only groups withing the course module grouping.
97 *
98 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible authors otherwise.
99 * @return array array[groupid][userid] => stdClass{->id ->lastname ->firstname}
100 */
101 public function get_peer_authors_by_group($musthavesubmission=true) {
102 global $DB;
103
104 $authors = $this->get_peer_authors($musthavesubmission);
105 $gauthors = array(); // grouped authors to be returned
106 if ($this->cm->groupmembersonly) {
107 // Available for group members only - the workshop is available only
108 // to users assigned to groups within the selected grouping, or to
109 // any group if no grouping is selected.
110 $groupingid = $this->cm->groupingid;
111 // All authors that are members of at least one group will be
112 // added into a virtual group id 0
113 $gauthors[0] = array();
114 } else {
115 $groupingid = 0;
116 // there is no need to be member of a group so $gauthors[0] will contain
117 // all authors with a submission
118 $gauthors[0] = $authors;
119 }
120 $gmemberships = groups_get_all_groups($this->cm->course, array_keys($authors), $groupingid,
121 'gm.id,gm.groupid,gm.userid');
122 foreach ($gmemberships as $gmembership) {
123 if (!isset($gauthors[$gmembership->groupid])) {
124 $gauthors[$gmembership->groupid] = array();
125 }
126 $gauthors[$gmembership->groupid][$gmembership->userid] = $authors[$gmembership->userid];
127 $gauthors[0][$gmembership->userid] = $authors[$gmembership->userid];
128 }
129 return $gauthors;
130 }
6e309973
DM
131
132 /**
133 * Fetches all users with the capability mod/workshop:peerassess in the current context
134 *
135 * Static variable used to cache the results. The returned objects contain id, lastname
136 * and firstname properties and are ordered by lastname,firstname
53fad4b9
DM
137 *
138 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible users otherwise.
139 * @return array array[userid] => stdClass{->id ->lastname ->firstname}
6e309973 140 */
53fad4b9 141 public function get_peer_reviewers($musthavesubmission=false) {
6e309973 142 global $DB;
53fad4b9
DM
143 static $users = null;
144 static $userswithsubmission = null;
6e309973
DM
145
146 if (is_null($users)) {
147 $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
148 $users = get_users_by_capability($context, 'mod/workshop:peerassess',
149 'u.id, u.lastname, u.firstname', 'u.lastname,u.firstname', '', '', '', '', false, false, true);
53fad4b9 150 if ($musthavesubmission && is_null($userswithsubmission)) {
66c9894d 151 // users without their own submission can not be reviewers
53fad4b9
DM
152 $userswithsubmission = $DB->get_records_list('workshop_submissions', 'userid', array_keys($users),'', 'userid');
153 $userswithsubmission = array_intersect_key($users, $userswithsubmission);
6e309973 154 }
0968b1a3 155 }
53fad4b9
DM
156 if ($musthavesubmission) {
157 return $userswithsubmission;
158 } else {
159 return $users;
160 }
0968b1a3
DM
161 }
162
53fad4b9
DM
163 /**
164 * Returns all users with the capability mod/workshop:peerassess sorted by groups
165 *
166 * This takes the module grouping settings into account. If "Available for group members only"
167 * is set, returns only groups withing the course module grouping.
168 *
169 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible users otherwise.
170 * @return array array[groupid][userid] => stdClass{->id ->lastname ->firstname}
171 */
172 public function get_peer_reviewers_by_group($musthavesubmission=false) {
173 global $DB;
174
175 $reviewers = $this->get_peer_reviewers($musthavesubmission);
176 $greviewers = array(); // grouped reviewers to be returned
177 if ($this->cm->groupmembersonly) {
178 // Available for group members only - the workshop is available only
179 // to users assigned to groups within the selected grouping, or to
180 // any group if no grouping is selected.
181 $groupingid = $this->cm->groupingid;
182 // All reviewers that are members of at least one group will be
183 // added into a virtual group id 0
184 $greviewers[0] = array();
185 } else {
186 $groupingid = 0;
187 // there is no need to be member of a group so $greviewers[0] will contain
188 // all reviewers
189 $greviewers[0] = $reviewers;
190 }
191 $gmemberships = groups_get_all_groups($this->cm->course, array_keys($reviewers), $groupingid,
192 'gm.id,gm.groupid,gm.userid');
193 foreach ($gmemberships as $gmembership) {
194 if (!isset($greviewers[$gmembership->groupid])) {
195 $greviewers[$gmembership->groupid] = array();
196 }
197 $greviewers[$gmembership->groupid][$gmembership->userid] = $reviewers[$gmembership->userid];
198 $greviewers[0][$gmembership->userid] = $reviewers[$gmembership->userid];
199 }
200 return $greviewers;
201 }
6e309973
DM
202
203 /**
204 * Returns submissions from this workshop
205 *
206 * Fetches data from {workshop_submissions} and adds some useful information from other
207 * tables.
53fad4b9
DM
208 *
209 * @param mixed $userid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only
210 * @param mixed $examples false|true|'all' Only regular submissions, only examples, all submissions
6e309973
DM
211 * @todo unittest
212 * @return object moodle_recordset
213 */
66c9894d 214 public function get_submissions_recordset($userid='all', $examples=false) {
6e309973
DM
215 global $DB;
216
217 $sql = 'SELECT s.*, u.lastname AS authorlastname, u.firstname AS authorfirstname
218 FROM {workshop_submissions} s
219 JOIN {user} u ON (s.userid = u.id)
220 WHERE s.workshopid = ?';
221 $params[0] = $this->id;
222
223 if ($examples === false) {
224 $sql .= ' AND example = 0';
225 }
226 if ($examples === true) {
227 $sql .= ' AND example = 1';
228 }
229 if (is_int($userid)) {
230 $sql .= ' AND userid = ?';
231 $params = array_merge($params, array($userid));
232 }
233 if (is_array($userid)) {
234 list($usql, $uparams) = $DB->get_in_or_equal($userid);
235 $sql .= ' AND userid ' . $usql;
236 $params = array_merge($params, $uparams);
237 }
238
239 return $DB->get_recordset_sql($sql, $params);
240 }
241
53fad4b9
DM
242 /**
243 * Returns a submission submitted by the given author or authors.
244 *
245 * This is intended for regular workshop participants, not for example submissions by teachers.
246 * If an array of authors is provided, returns array of stripped submission records so they do not
247 * include text fields (to prevent possible memory-lack issues).
248 *
249 * @param mixed $id integer|array author ID or IDs
250 * @return mixed false if not found, object if $id is int, array if $id is array
251 */
252 public function get_submission_by_author($id) {
253 if (empty($id)) {
254 return false;
255 }
256 $rs = $this->get_submissions_recordset($id, false);
257 if (is_int($id)) {
258 $submission = $rs->current();
259 $rs->close();
260 if (empty($submission->id)) {
261 return false;
262 } else {
263 return $submission;
264 }
265 } elseif (is_array($id)) {
266 $submissions = array();
267 foreach ($rs as $submission) {
268 $submissions[$submission->id] = new stdClass();
269 foreach ($submission as $property => $value) {
270 // we do not want text fields here to prevent possible memory issues
271 if (in_array($property, array('id', 'workshopid', 'example', 'userid', 'authorlastname', 'authorfirstname',
272 'timecreated', 'timemodified', 'grade', 'gradeover', 'gradeoverby', 'gradinggrade'))) {
273 $submissions[$submission->id]->{$property} = $value;
274 }
275 }
276 }
277 return $submissions;
278 } else {
279 throw new moodle_workshop_exception($this, 'wrongparameter');
280 }
281 }
6e309973
DM
282
283 /**
284 * Returns the list of assessments with some data added
285 *
286 * Fetches data from {workshop_assessments} and adds some useful information from other
287 * tables.
288 *
289 * @param mixed $reviewerid 'all'|int|array User ID of the reviewer
290 * @param mixed $id 'all'|int Assessment ID
291 * @return object moodle_recordset
292 */
66c9894d 293 public function get_assessments_recordset($reviewerid='all', $id='all') {
6e309973 294 global $DB;
53fad4b9 295
6e309973
DM
296 $sql = 'SELECT a.*,
297 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname as reviewerlastname,
298 s.title,
299 author.id AS authorid, author.firstname AS authorfirstname,author.lastname as authorlastname
300 FROM {workshop_assessments} a
53fad4b9
DM
301 INNER JOIN {user} reviewer ON (a.userid = reviewer.id)
302 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
303 INNER JOIN {user} author ON (s.userid = author.id)
6e309973
DM
304 WHERE s.workshopid = ?';
305 $params = array($this->id);
306 if (is_int($reviewerid)) {
307 $sql .= ' AND reviewerid = ?';
308 $params = array_merge($params, array($reviewerid));
309 }
310 if (is_array($reviewerid)) {
311 list($usql, $uparams) = $DB->get_in_or_equal($reviewerid);
312 $sql .= ' AND reviewerid ' . $usql;
313 $params = array_merge($params, $uparams);
314 }
315 if (is_int($id)) {
316 $sql .= ' AND a.id = ?';
317 $params = array_merge($params, array($id));
318 }
319
320 return $DB->get_recordset_sql($sql, $params);
321 }
322
53fad4b9
DM
323 /**
324 * Returns the list of assessments with some data added
325 *
326 * Fetches data from {workshop_assessments} and adds some useful information from other
327 * tables. The returned objects are lightweight version of those returned by get_assessments_recordset(),
328 * mainly they do not contain text fields.
329 *
330 * @param mixed $reviewerid 'all'|int|array User ID of the reviewer
da92436b 331 * @return array [assessmentid] => assessment object
53fad4b9
DM
332 * @see workshop_api::get_assessments_recordset() for the structure of returned objects
333 */
334 public function get_assessments($reviewerid='all') {
335 $rs = $this->get_assessments_recordset($reviewerid, 'all');
336 $assessments = array();
337 foreach ($rs as $assessment) {
338 // copy selected properties into the array to be returned. This is here mainly in order not
339 // to include text comments.
340 $assessments[$assessment->id] = new stdClass;
341 foreach ($assessment as $property => $value) {
342 if (in_array($property, array('id', 'submissionid', 'userid', 'timecreated', 'timemodified',
343 'timeagreed', 'grade', 'gradinggrade', 'gradinggradeover', 'gradinggradeoverby',
344 'reviewerid', 'reviewerfirstname', 'reviewerlastname', 'title', 'authorid',
345 'authorfirstname', 'authorlastname'))) {
346 $assessments[$assessment->id]->{$property} = $value;
347 }
348 }
349 }
350 $rs->close();
351 return $assessments;
352 }
353
354 /**
355 * Get the information about the given assessment
356 *
357 * @param int $id Assessment ID
358 * @see workshop_api::get_assessments_recordset() for the structure of data returned
359 * @return mixed false if not found, object otherwise
360 */
361 public function get_assessment_by_id($id) {
362 $rs = $this->get_assessments_recordset('all', $id);
363 $assessment = $rs->current();
364 $rs->close();
365 if (empty($assessment->id)) {
366 return false;
367 } else {
368 return $assessment;
369 }
370 }
371
372 /**
373 * Get the information about all assessments assigned to the given reviewer
374 *
375 * @param int $id Reviewer ID
376 * @see workshop_api::get_assessments_recordset() for the structure of data returned
377 * @return array array of objects
378 */
379 public function get_assessments_by_reviewer($id) {
380 $rs = $this->get_assessments_recordset($id);
381 $assessments = array();
382 foreach ($rs as $assessment) {
383 $assessments[$assessment->id] = $assessment;
384 }
385 $rs->close();
386 return $assessment;
387 }
6e309973
DM
388
389 /**
390 * Returns the list of allocations in the workshop
391 *
392 * This returns the list of all users who can submit their work or review submissions (or both
393 * which is the common case). So basically this is to return list of all students participating
394 * in the workshop. For every participant, it adds information about their submission and their
53fad4b9 395 * reviews.
6e309973
DM
396 *
397 * The returned structure is recordset of objects with following properties:
398 * [authorid] [authorfirstname] [authorlastname] [authorpicture] [authorimagealt]
399 * [submissionid] [submissiontitle] [submissiongrade] [assessmentid]
53fad4b9 400 * [timeallocated] [reviewerid] [reviewerfirstname] [reviewerlastname]
6e309973
DM
401 * [reviewerpicture] [reviewerimagealt]
402 *
403 * This should be refactored when capability handling proposed by Petr is implemented so that
404 * we can check capabilities directly in SQL joins.
53fad4b9
DM
405 * Note that the returned recordset includes participants without submission as well as those
406 * without any review allocated yet.
6e309973
DM
407 *
408 * @return object moodle_recordset
409 */
66c9894d 410 public function get_allocations_recordset() {
6e309973
DM
411 global $DB;
412 static $users=null;
413
414 if (is_null($users)) {
415 $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
416 $users = get_users_by_capability($context, array('mod/workshop:submit', 'mod/workshop:peerassess'),
417 'u.id', 'u.lastname,u.firstname', '', '', '', '', false, false, true);
418 }
419
420 list($usql, $params) = $DB->get_in_or_equal(array_keys($users));
421 $params[] = $this->id;
422
53fad4b9 423 $sql = 'SELECT author.id AS authorid, author.firstname AS authorfirstname, author.lastname AS authorlastname,
6e309973 424 author.picture AS authorpicture, author.imagealt AS authorimagealt,
53fad4b9
DM
425 s.id AS submissionid, s.title AS submissiontitle, s.grade AS submissiongrade,
426 a.id AS assessmentid, a.timecreated AS timeallocated, a.userid AS reviewerid,
6e309973
DM
427 reviewer.firstname AS reviewerfirstname, reviewer.lastname AS reviewerlastname,
428 reviewer.picture as reviewerpicture, reviewer.imagealt AS reviewerimagealt
429 FROM {user} author
430 LEFT JOIN {workshop_submissions} s ON (s.userid = author.id)
431 LEFT JOIN {workshop_assessments} a ON (s.id = a.submissionid)
432 LEFT JOIN {user} reviewer ON (a.userid = reviewer.id)
433 WHERE author.id ' . $usql . ' AND (s.workshopid = ? OR s.workshopid IS NULL)
434 ORDER BY author.lastname,author.firstname,reviewer.lastname,reviewer.firstname';
435 return $DB->get_recordset_sql($sql, $params);
436 }
437
6e309973
DM
438 /**
439 * Allocate a submission to a user for review
53fad4b9 440 *
6e309973
DM
441 * @param object $submission Submission record
442 * @param int $reviewerid User ID
53fad4b9 443 * @param bool $bulk repeated inserts into DB expected
6e309973
DM
444 * @return int ID of the new assessment or an error code
445 */
53fad4b9 446 public function add_allocation(stdClass $submission, $reviewerid, $bulk=false) {
6e309973
DM
447 global $DB;
448
449 if ($DB->record_exists('workshop_assessments', array('submissionid' => $submission->id, 'userid' => $reviewerid))) {
450 return WORKSHOP_ALLOCATION_EXISTS;
451 }
452
6e309973
DM
453 $now = time();
454 $assessment = new stdClass();
53fad4b9 455 $assessment->submissionid = $submission->id;
6e309973
DM
456 $assessment->userid = $reviewerid;
457 $assessment->timecreated = $now;
458 $assessment->timemodified = $now;
459
53fad4b9 460 return $DB->insert_record('workshop_assessments', $assessment, true, $bulk);
6e309973
DM
461 }
462
6e309973 463 /**
53fad4b9 464 * Delete assessment record or records
6e309973 465 *
53fad4b9
DM
466 * @param mixed $id int|array assessment id or array of assessments ids
467 * @return bool false if $id not a valid parameter, true otherwise
6e309973
DM
468 */
469 public function delete_assessment($id) {
470 global $DB;
471
472 // todo remove all given grades from workshop_grades;
6e309973 473
53fad4b9
DM
474 if (is_numeric($id)) {
475 return $DB->delete_records('workshop_assessments', array('id' => $id));
476 }
477 if (is_array($id)) {
478 return $DB->delete_records_list('workshop_assessments', 'id', $id);
479 }
480 return false;
481 }
6e309973
DM
482
483 /**
484 * Returns instance of grading strategy class
53fad4b9 485 *
6e309973
DM
486 * @param object $workshop Workshop record
487 * @return object Instance of a grading strategy
488 */
489 public function grading_strategy_instance() {
6e309973
DM
490 if (!($this->strategy === clean_param($workshop->strategy, PARAM_ALPHA))) {
491 throw new moodle_workshop_exception($this, 'invalidstrategyname');
492 }
493
494 if (is_null($this->strategy_api)) {
495 $strategylib = dirname(__FILE__) . '/grading/' . $workshop->strategy . '/strategy.php';
496 if (is_readable($strategylib)) {
497 require_once($strategylib);
498 } else {
53fad4b9 499 throw new moodle_workshop_exception($this, 'missingstrategy');
6e309973
DM
500 }
501 $classname = 'workshop_' . $workshop->strategy . '_strategy';
502 $this->strategy_api = new $classname($this);
503 if (!in_array('workshop_strategy', class_implements($this->strategy_api))) {
504 throw new moodle_workshop_exception($this, 'strategynotimplemented');
505 }
506 }
507
508 return $this->strategy_api;
509 }
510
66c9894d
DM
511 /**
512 * Return list of available allocation methods
513 *
514 * @return array Array ['string' => 'string'] of localized allocation method names
515 */
516 public function installed_allocators() {
66c9894d
DM
517 $installed = get_list_of_plugins('mod/workshop/allocation');
518 $forms = array();
519 foreach ($installed as $allocation) {
520 $forms[$allocation] = get_string('allocation' . $allocation, 'workshop');
521 }
522 // usability - make sure that manual allocation appears the first
523 if (isset($forms['manual'])) {
524 $m = array('manual' => $forms['manual']);
525 unset($forms['manual']);
526 $forms = array_merge($m, $forms);
527 }
528 return $forms;
529 }
0968b1a3 530
66c9894d
DM
531 /**
532 * Returns instance of submissions allocator
53fad4b9 533 *
66c9894d
DM
534 * @param object $method The name of the allocation method, must be PARAM_ALPHA
535 * @return object Instance of submissions allocator
536 */
537 public function allocator_instance($method) {
66c9894d
DM
538 $allocationlib = dirname(__FILE__) . '/allocation/' . $method . '/allocator.php';
539 if (is_readable($allocationlib)) {
540 require_once($allocationlib);
541 } else {
53fad4b9 542 throw new moodle_workshop_exception($this, 'missingallocator');
66c9894d
DM
543 }
544 $classname = 'workshop_' . $method . '_allocator';
545 return new $classname($this);
546 }
547
66c9894d 548}
6e309973 549
de811c0c 550/**
6e309973
DM
551 * Class for workshop exceptions. Just saves a couple of arguments of the
552 * constructor for a moodle_exception.
de811c0c 553 *
6e309973
DM
554 * @param object $workshop Should be workshop or its subclass
555 * @param string $errorcode
556 * @param mixed $a Object/variable to pass to get_string
557 * @param string $link URL to continue after the error notice
558 * @param $debuginfo
de811c0c 559 */
6e309973 560class moodle_workshop_exception extends moodle_exception {
de811c0c 561
6e309973
DM
562 function __construct($workshop, $errorcode, $a = NULL, $link = '', $debuginfo = null) {
563 global $CFG;
564
565 if (!$link) {
566 $link = $CFG->wwwroot . '/mod/workshop/view.php?a=' . $workshop->id;
567 }
568 if ('confirmsesskeybad' == $errorcode) {
569 $module = '';
570 } else {
571 $module = 'workshop';
572 }
573 parent::__construct($errorcode, $module, $link, $a, $debuginfo);
574 }
de811c0c
DM
575}
576