VIP Assessment review
[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
a39d7d87 24 * parameter, we use a class workshop 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
89c1aa97
DM
33require_once(dirname(__FILE__).'/lib.php'); // we extend this library here
34require_once($CFG->libdir . '/gradelib.php'); // we use some rounding and comparing routines here
0968b1a3 35
6e309973
DM
36/**
37 * Full-featured workshop API
38 *
a39d7d87 39 * This wraps the workshop database record with a set of methods that are called
6e309973 40 * from the module itself. The class should be initialized right after you get
a39d7d87 41 * $workshop, $cm and $course records at the begining of the script.
6e309973 42 */
a39d7d87
DM
43class workshop {
44
b761e6d9
DM
45 /** return statuses of {@link add_allocation} to be passed to a workshop renderer method */
46 const ALLOCATION_EXISTS = -1;
47 const ALLOCATION_ERROR = -2;
48
49 /** the internal code of the workshop phases as are stored in the database */
50 const PHASE_SETUP = 10;
51 const PHASE_SUBMISSION = 20;
52 const PHASE_ASSESSMENT = 30;
53 const PHASE_EVALUATION = 40;
54 const PHASE_CLOSED = 50;
55
65ba104c 56 /** @var stdClass course module record */
a39d7d87
DM
57 public $cm = null;
58
65ba104c 59 /** @var stdClass course record */
a39d7d87 60 public $course = null;
6e309973 61
b761e6d9
DM
62 /**
63 * @var workshop_strategy grading strategy instance
64 * Do not use directly, get the instance using {@link workshop::grading_strategy_instance()}
65 */
b13142da
DM
66 protected $strategyinstance = null;
67
45d24d39
DM
68 /**
69 * @var workshop_evaluation grading evaluation instance
70 * Do not use directly, get the instance using {@link workshop::grading_evaluation_instance()}
71 */
72 protected $evaluationinstance = null;
73
6e309973 74 /**
65ba104c 75 * Initializes the workshop API instance using the data from DB
a39d7d87
DM
76 *
77 * Makes deep copy of all passed records properties. Replaces integer $course attribute
78 * with a full database record (course should not be stored in instances table anyway).
6e309973 79 *
b13142da 80 * @param stdClass $dbrecord Workshop instance data from {workshop} table
06d73dd5
DM
81 * @param stdClass $cm Course module record as returned by {@link get_coursemodule_from_id()}
82 * @param stdClass $course Course record from {course} table
4efd7b5d 83 * @param stdClass $context The context of the workshop instance
0dc47fb9 84 */
4efd7b5d 85 public function __construct(stdClass $dbrecord, stdClass $cm, stdClass $course, stdClass $context=null) {
f05c168d
DM
86 foreach ($dbrecord as $field => $value) {
87 $this->{$field} = $value;
a39d7d87 88 }
45d24d39
DM
89 $this->cm = $cm;
90 $this->course = $course; // beware - this replaces the standard course field in the instance table
91 // this is intentional - IMO there should be no such field as it violates
4efd7b5d
DM
92 // 3rd normal form with no real performance gain
93 if (is_null($context)) {
94 $this->context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
95 } else {
96 $this->context = $context;
97 }
98 $this->evaluation = 'best'; // todo make this configurable although we have no alternatives yet
6e309973
DM
99 }
100
aa40adbf
DM
101 ////////////////////////////////////////////////////////////////////////////////
102 // Static methods //
103 ////////////////////////////////////////////////////////////////////////////////
104
da0b1f70 105 /**
aa40adbf 106 * Return list of available allocation methods
da0b1f70 107 *
aa40adbf 108 * @return array Array ['string' => 'string'] of localized allocation method names
da0b1f70 109 */
aa40adbf
DM
110 public static function installed_allocators() {
111 $installed = get_plugin_list('workshopallocation');
112 $forms = array();
113 foreach ($installed as $allocation => $allocationpath) {
114 if (file_exists($allocationpath . '/lib.php')) {
115 $forms[$allocation] = get_string('pluginname', 'workshopallocation_' . $allocation);
116 }
f05c168d 117 }
aa40adbf
DM
118 // usability - make sure that manual allocation appears the first
119 if (isset($forms['manual'])) {
120 $m = array('manual' => $forms['manual']);
121 unset($forms['manual']);
122 $forms = array_merge($m, $forms);
da0b1f70 123 }
aa40adbf
DM
124 return $forms;
125 }
da0b1f70 126
aa40adbf
DM
127 /**
128 * Returns an array of options for the editors that are used for submitting and assessing instructions
129 *
130 * @param stdClass $context
131 * @return array
132 */
133 public static function instruction_editors_options(stdClass $context) {
134 return array('subdirs' => 1, 'maxbytes' => 0, 'maxfiles' => EDITOR_UNLIMITED_FILES,
135 'changeformat' => 1, 'context' => $context, 'noclean' => 1, 'trusttext' => 0);
da0b1f70
DM
136 }
137
61b737a5
DM
138 /**
139 * Given the percent and the total, returns the number
140 *
141 * @param float $percent from 0 to 100
142 * @param float $total the 100% value
143 * @return float
144 */
145 public static function percent_to_value($percent, $total) {
146 if ($percent < 0 or $percent > 100) {
147 throw new coding_exception('The percent can not be less than 0 or higher than 100');
148 }
149
150 return $total * $percent / 100;
151 }
152
aa40adbf
DM
153 ////////////////////////////////////////////////////////////////////////////////
154 // Workshop API //
155 ////////////////////////////////////////////////////////////////////////////////
156
6e309973
DM
157 /**
158 * Fetches all users with the capability mod/workshop:submit in the current context
159 *
3d2924e9 160 * The returned objects contain id, lastname and firstname properties and are ordered by lastname,firstname
53fad4b9 161 *
aa40adbf 162 * @todo handle with limits and groups
53fad4b9 163 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible authors otherwise.
65ba104c 164 * @return array array[userid] => stdClass{->id ->lastname ->firstname}
6e309973 165 */
d895c6aa
DM
166 public function get_potential_authors($musthavesubmission=true) {
167 $users = get_users_by_capability($this->context, 'mod/workshop:submit',
1fed6ce3 168 'u.id,u.lastname,u.firstname', 'u.lastname,u.firstname,u.id', '', '', '', '', false, false, true);
3d2924e9 169 if ($musthavesubmission) {
da0b1f70 170 $users = array_intersect_key($users, $this->users_with_submission(array_keys($users)));
66c9894d 171 }
da0b1f70 172 return $users;
6e309973
DM
173 }
174
6e309973
DM
175 /**
176 * Fetches all users with the capability mod/workshop:peerassess in the current context
177 *
b13142da 178 * The returned objects contain id, lastname and firstname properties and are ordered by lastname,firstname
53fad4b9 179 *
aa40adbf 180 * @todo handle with limits and groups
53fad4b9 181 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible users otherwise.
65ba104c 182 * @return array array[userid] => stdClass{->id ->lastname ->firstname}
6e309973 183 */
d895c6aa
DM
184 public function get_potential_reviewers($musthavesubmission=false) {
185 $users = get_users_by_capability($this->context, 'mod/workshop:peerassess',
1fed6ce3 186 'u.id, u.lastname, u.firstname', 'u.lastname,u.firstname,u.id', '', '', '', '', false, false, true);
3d2924e9
DM
187 if ($musthavesubmission) {
188 // users without their own submission can not be reviewers
da0b1f70 189 $users = array_intersect_key($users, $this->users_with_submission(array_keys($users)));
0968b1a3 190 }
da0b1f70 191 return $users;
0968b1a3
DM
192 }
193
b8ead2e6
DM
194 /**
195 * Groups the given users by the group membership
196 *
197 * This takes the module grouping settings into account. If "Available for group members only"
198 * is set, returns only groups withing the course module grouping. Always returns group [0] with
199 * all the given users.
200 *
65ba104c
DM
201 * @param array $users array[userid] => stdClass{->id ->lastname ->firstname}
202 * @return array array[groupid][userid] => stdClass{->id ->lastname ->firstname}
53fad4b9 203 */
3d2924e9 204 public function get_grouped($users) {
53fad4b9 205 global $DB;
3d2924e9 206 global $CFG;
53fad4b9 207
b8ead2e6
DM
208 $grouped = array(); // grouped users to be returned
209 if (empty($users)) {
210 return $grouped;
a7c5b918 211 }
3d2924e9 212 if (!empty($CFG->enablegroupings) and $this->cm->groupmembersonly) {
53fad4b9
DM
213 // Available for group members only - the workshop is available only
214 // to users assigned to groups within the selected grouping, or to
215 // any group if no grouping is selected.
216 $groupingid = $this->cm->groupingid;
b8ead2e6 217 // All users that are members of at least one group will be
53fad4b9 218 // added into a virtual group id 0
b8ead2e6 219 $grouped[0] = array();
53fad4b9
DM
220 } else {
221 $groupingid = 0;
b8ead2e6
DM
222 // there is no need to be member of a group so $grouped[0] will contain
223 // all users
224 $grouped[0] = $users;
53fad4b9 225 }
b8ead2e6 226 $gmemberships = groups_get_all_groups($this->cm->course, array_keys($users), $groupingid,
53fad4b9
DM
227 'gm.id,gm.groupid,gm.userid');
228 foreach ($gmemberships as $gmembership) {
b8ead2e6
DM
229 if (!isset($grouped[$gmembership->groupid])) {
230 $grouped[$gmembership->groupid] = array();
53fad4b9 231 }
b8ead2e6
DM
232 $grouped[$gmembership->groupid][$gmembership->userid] = $users[$gmembership->userid];
233 $grouped[0][$gmembership->userid] = $users[$gmembership->userid];
53fad4b9 234 }
b8ead2e6 235 return $grouped;
53fad4b9 236 }
6e309973 237
aa40adbf
DM
238 /**
239 * Returns the list of all allocations (it est assigned assessments) in the workshop
240 *
241 * Assessments of example submissions are ignored
242 *
243 * @return array
244 */
245 public function get_allocations() {
246 global $DB;
247
00aca3c1 248 $sql = 'SELECT a.id, a.submissionid, a.reviewerid, s.authorid
aa40adbf
DM
249 FROM {workshop_assessments} a
250 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
251 WHERE s.example = 0 AND s.workshopid = :workshopid';
252 $params = array('workshopid' => $this->id);
253
254 return $DB->get_records_sql($sql, $params);
255 }
256
6e309973
DM
257 /**
258 * Returns submissions from this workshop
259 *
3dc78e5b
DM
260 * Fetches data from {workshop_submissions} and adds some useful information from other
261 * tables. Does not return textual fields to prevent possible memory lack issues.
53fad4b9 262 *
00aca3c1 263 * @param mixed $authorid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only
934329e5 264 * @return array of records or an empty array
6e309973 265 */
29dc43e7 266 public function get_submissions($authorid='all') {
6e309973
DM
267 global $DB;
268
00aca3c1
DM
269 $sql = 'SELECT s.id, s.workshopid, s.example, s.authorid, s.timecreated, s.timemodified,
270 s.title, s.grade, s.gradeover, s.gradeoverby,
271 u.lastname AS authorlastname, u.firstname AS authorfirstname,
272 u.picture AS authorpicture, u.imagealt AS authorimagealt,
273 t.lastname AS overlastname, t.firstname AS overfirstname,
274 t.picture AS overpicture, t.imagealt AS overimagealt
3d2924e9 275 FROM {workshop_submissions} s
00aca3c1 276 INNER JOIN {user} u ON (s.authorid = u.id)
29dc43e7
DM
277 LEFT JOIN {user} t ON (s.gradeoverby = t.id)
278 WHERE s.example = 0 AND s.workshopid = :workshopid';
3d2924e9 279 $params = array('workshopid' => $this->id);
6e309973 280
00aca3c1 281 if ('all' === $authorid) {
3d2924e9 282 // no additional conditions
934329e5 283 } elseif (!empty($authorid)) {
00aca3c1
DM
284 list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED);
285 $sql .= " AND authorid $usql";
6e309973 286 $params = array_merge($params, $uparams);
3d2924e9 287 } else {
934329e5
DM
288 // $authorid is empty
289 return array();
6e309973 290 }
3dc78e5b 291 $sql .= ' ORDER BY u.lastname, u.firstname';
6e309973 292
3dc78e5b 293 return $DB->get_records_sql($sql, $params);
6e309973
DM
294 }
295
51508f25
DM
296 /**
297 * Returns a submission record with the author's data
298 *
299 * @param int $id submission id
300 * @return stdClass
301 */
302 public function get_submission_by_id($id) {
303 global $DB;
304
29dc43e7
DM
305 // we intentionally check the workshopid here, too, so the workshop can't touch submissions
306 // from other instances
51508f25
DM
307 $sql = 'SELECT s.*,
308 u.lastname AS authorlastname, u.firstname AS authorfirstname, u.id AS authorid,
309 u.picture AS authorpicture, u.imagealt AS authorimagealt
310 FROM {workshop_submissions} s
00aca3c1 311 INNER JOIN {user} u ON (s.authorid = u.id)
51508f25
DM
312 WHERE s.workshopid = :workshopid AND s.id = :id';
313 $params = array('workshopid' => $this->id, 'id' => $id);
314 return $DB->get_record_sql($sql, $params, MUST_EXIST);
315 }
316
53fad4b9 317 /**
3dc78e5b 318 * Returns a submission submitted by the given author
53fad4b9 319 *
3dc78e5b
DM
320 * @param int $id author id
321 * @return stdClass|false
53fad4b9 322 */
00aca3c1 323 public function get_submission_by_author($authorid) {
e9b0f0ab
DM
324 global $DB;
325
00aca3c1 326 if (empty($authorid)) {
53fad4b9
DM
327 return false;
328 }
3dc78e5b
DM
329 $sql = 'SELECT s.*,
330 u.lastname AS authorlastname, u.firstname AS authorfirstname, u.id AS authorid,
331 u.picture AS authorpicture, u.imagealt AS authorimagealt
332 FROM {workshop_submissions} s
00aca3c1
DM
333 INNER JOIN {user} u ON (s.authorid = u.id)
334 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.authorid = :authorid';
335 $params = array('workshopid' => $this->id, 'authorid' => $authorid);
3dc78e5b 336 return $DB->get_record_sql($sql, $params);
53fad4b9 337 }
6e309973
DM
338
339 /**
3dc78e5b 340 * Returns the list of all assessments in the workshop with some data added
6e309973
DM
341 *
342 * Fetches data from {workshop_assessments} and adds some useful information from other
3dc78e5b
DM
343 * tables. The returned object does not contain textual fields (ie comments) to prevent memory
344 * lack issues.
345 *
346 * @return array [assessmentid] => assessment stdClass
6e309973 347 */
3dc78e5b 348 public function get_all_assessments() {
6e309973 349 global $DB;
53fad4b9 350
00aca3c1 351 $sql = 'SELECT a.id, a.submissionid, a.reviewerid, a.timecreated, a.timemodified, a.timeagreed,
3dc78e5b 352 a.grade, a.gradinggrade, a.gradinggradeover, a.gradinggradeoverby,
3d2924e9
DM
353 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname as reviewerlastname,
354 s.title,
ddb59c77 355 author.id AS authorid, author.firstname AS authorfirstname,author.lastname AS authorlastname
3d2924e9 356 FROM {workshop_assessments} a
00aca3c1 357 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
3d2924e9 358 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
00aca3c1 359 INNER JOIN {user} author ON (s.authorid = author.id)
3dc78e5b
DM
360 WHERE s.workshopid = :workshopid AND s.example = 0
361 ORDER BY reviewer.lastname, reviewer.firstname';
3d2924e9
DM
362 $params = array('workshopid' => $this->id);
363
3dc78e5b 364 return $DB->get_records_sql($sql, $params);
53fad4b9
DM
365 }
366
367 /**
3dc78e5b 368 * Get the complete information about the given assessment
53fad4b9
DM
369 *
370 * @param int $id Assessment ID
65ba104c 371 * @return mixed false if not found, stdClass otherwise
53fad4b9
DM
372 */
373 public function get_assessment_by_id($id) {
3dc78e5b
DM
374 global $DB;
375
376 $sql = 'SELECT a.*,
377 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname as reviewerlastname,
378 s.title,
379 author.id AS authorid, author.firstname AS authorfirstname,author.lastname as authorlastname
380 FROM {workshop_assessments} a
00aca3c1 381 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
3dc78e5b 382 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
00aca3c1 383 INNER JOIN {user} author ON (s.authorid = author.id)
3dc78e5b
DM
384 WHERE a.id = :id AND s.workshopid = :workshopid';
385 $params = array('id' => $id, 'workshopid' => $this->id);
386
387 return $DB->get_record_sql($sql, $params, MUST_EXIST);
53fad4b9
DM
388 }
389
390 /**
3dc78e5b 391 * Get the complete information about all assessments allocated to the given reviewer
53fad4b9 392 *
00aca3c1 393 * @param int $reviewerid
3dc78e5b 394 * @return array
53fad4b9 395 */
00aca3c1 396 public function get_assessments_by_reviewer($reviewerid) {
3dc78e5b
DM
397 global $DB;
398
399 $sql = 'SELECT a.*,
ddb59c77
DM
400 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname AS reviewerlastname,
401 s.id AS submissionid, s.title AS submissiontitle, s.timecreated AS submissioncreated,
402 s.timemodified AS submissionmodified,
403 author.id AS authorid, author.firstname AS authorfirstname,author.lastname AS authorlastname,
404 author.picture AS authorpicture, author.imagealt AS authorimagealt
3dc78e5b 405 FROM {workshop_assessments} a
00aca3c1 406 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
3dc78e5b 407 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
00aca3c1
DM
408 INNER JOIN {user} author ON (s.authorid = author.id)
409 WHERE s.example = 0 AND reviewer.id = :reviewerid AND s.workshopid = :workshopid';
410 $params = array('reviewerid' => $reviewerid, 'workshopid' => $this->id);
3dc78e5b
DM
411
412 return $DB->get_records_sql($sql, $params);
53fad4b9 413 }
6e309973 414
6e309973
DM
415 /**
416 * Allocate a submission to a user for review
53fad4b9 417 *
65ba104c 418 * @param stdClass $submission Submission record
6e309973 419 * @param int $reviewerid User ID
53fad4b9 420 * @param bool $bulk repeated inserts into DB expected
6e309973
DM
421 * @return int ID of the new assessment or an error code
422 */
65ba104c 423 public function add_allocation(stdClass $submission, $reviewerid, $bulk=false) {
6e309973
DM
424 global $DB;
425
00aca3c1 426 if ($DB->record_exists('workshop_assessments', array('submissionid' => $submission->id, 'reviewerid' => $reviewerid))) {
b761e6d9 427 return self::ALLOCATION_EXISTS;
6e309973
DM
428 }
429
6e309973 430 $now = time();
65ba104c 431 $assessment = new stdClass();
53fad4b9 432 $assessment->submissionid = $submission->id;
00aca3c1 433 $assessment->reviewerid = $reviewerid;
6e309973
DM
434 $assessment->timecreated = $now;
435 $assessment->timemodified = $now;
a3610b08 436 $assessment->weight = 1; // todo better handling of the weight value/default
6e309973 437
235b31c8 438 return $DB->insert_record('workshop_assessments', $assessment, true, $bulk);
6e309973
DM
439 }
440
6e309973 441 /**
53fad4b9 442 * Delete assessment record or records
6e309973 443 *
53fad4b9
DM
444 * @param mixed $id int|array assessment id or array of assessments ids
445 * @return bool false if $id not a valid parameter, true otherwise
6e309973
DM
446 */
447 public function delete_assessment($id) {
448 global $DB;
449
450 // todo remove all given grades from workshop_grades;
6e309973 451
53fad4b9 452 if (is_array($id)) {
235b31c8 453 return $DB->delete_records_list('workshop_assessments', 'id', $id);
3d2924e9 454 } else {
235b31c8 455 return $DB->delete_records('workshop_assessments', array('id' => $id));
53fad4b9 456 }
53fad4b9 457 }
6e309973
DM
458
459 /**
460 * Returns instance of grading strategy class
53fad4b9 461 *
65ba104c 462 * @return stdClass Instance of a grading strategy
6e309973
DM
463 */
464 public function grading_strategy_instance() {
3d2924e9
DM
465 global $CFG; // because we require other libs here
466
3fd2b0e1 467 if (is_null($this->strategyinstance)) {
f05c168d 468 $strategylib = dirname(__FILE__) . '/form/' . $this->strategy . '/lib.php';
6e309973
DM
469 if (is_readable($strategylib)) {
470 require_once($strategylib);
471 } else {
f05c168d 472 throw new coding_exception('the grading forms subplugin must contain library ' . $strategylib);
6e309973 473 }
0dc47fb9 474 $classname = 'workshop_' . $this->strategy . '_strategy';
3fd2b0e1
DM
475 $this->strategyinstance = new $classname($this);
476 if (!in_array('workshop_strategy', class_implements($this->strategyinstance))) {
b761e6d9 477 throw new coding_exception($classname . ' does not implement workshop_strategy interface');
6e309973
DM
478 }
479 }
3fd2b0e1 480 return $this->strategyinstance;
6e309973
DM
481 }
482
45d24d39
DM
483 /**
484 * Returns instance of grading evaluation class
485 *
486 * @return stdClass Instance of a grading evaluation
487 */
488 public function grading_evaluation_instance() {
489 global $CFG; // because we require other libs here
490
491 if (is_null($this->evaluationinstance)) {
492 $evaluationlib = dirname(__FILE__) . '/eval/' . $this->evaluation . '/lib.php';
493 if (is_readable($evaluationlib)) {
494 require_once($evaluationlib);
495 } else {
496 throw new coding_exception('the grading evaluation subplugin must contain library ' . $evaluationlib);
497 }
498 $classname = 'workshop_' . $this->evaluation . '_evaluation';
499 $this->evaluationinstance = new $classname($this);
500 if (!in_array('workshop_evaluation', class_implements($this->evaluationinstance))) {
501 throw new coding_exception($classname . ' does not implement workshop_evaluation interface');
502 }
503 }
504 return $this->evaluationinstance;
505 }
506
66c9894d
DM
507 /**
508 * Returns instance of submissions allocator
53fad4b9 509 *
130ae619 510 * @param string $method The name of the allocation method, must be PARAM_ALPHA
65ba104c 511 * @return stdClass Instance of submissions allocator
66c9894d
DM
512 */
513 public function allocator_instance($method) {
3d2924e9
DM
514 global $CFG; // because we require other libs here
515
f05c168d 516 $allocationlib = dirname(__FILE__) . '/allocation/' . $method . '/lib.php';
66c9894d
DM
517 if (is_readable($allocationlib)) {
518 require_once($allocationlib);
519 } else {
f05c168d 520 throw new coding_exception('Unable to find the allocation library ' . $allocationlib);
66c9894d
DM
521 }
522 $classname = 'workshop_' . $method . '_allocator';
523 return new $classname($this);
524 }
525
b8ead2e6 526 /**
454e8dd9 527 * @return moodle_url of this workshop's view page
b8ead2e6
DM
528 */
529 public function view_url() {
530 global $CFG;
531 return new moodle_url($CFG->wwwroot . '/mod/workshop/view.php', array('id' => $this->cm->id));
532 }
533
534 /**
454e8dd9 535 * @return moodle_url of the page for editing this workshop's grading form
b8ead2e6
DM
536 */
537 public function editform_url() {
538 global $CFG;
539 return new moodle_url($CFG->wwwroot . '/mod/workshop/editform.php', array('cmid' => $this->cm->id));
540 }
541
542 /**
454e8dd9 543 * @return moodle_url of the page for previewing this workshop's grading form
b8ead2e6
DM
544 */
545 public function previewform_url() {
546 global $CFG;
43b34576 547 return new moodle_url($CFG->wwwroot . '/mod/workshop/editformpreview.php', array('cmid' => $this->cm->id));
b8ead2e6
DM
548 }
549
550 /**
551 * @param int $assessmentid The ID of assessment record
454e8dd9 552 * @return moodle_url of the assessment page
b8ead2e6 553 */
a39d7d87 554 public function assess_url($assessmentid) {
b8ead2e6 555 global $CFG;
454e8dd9 556 $assessmentid = clean_param($assessmentid, PARAM_INT);
a39d7d87 557 return new moodle_url($CFG->wwwroot . '/mod/workshop/assessment.php', array('asid' => $assessmentid));
b8ead2e6
DM
558 }
559
39861053 560 /**
454e8dd9 561 * @return moodle_url of the page to view own submission
39861053
DM
562 */
563 public function submission_url() {
564 global $CFG;
565 return new moodle_url($CFG->wwwroot . '/mod/workshop/submission.php', array('cmid' => $this->cm->id));
566 }
567
da0b1f70 568 /**
454e8dd9 569 * @return moodle_url of the mod_edit form
da0b1f70
DM
570 */
571 public function updatemod_url() {
572 global $CFG;
573 return new moodle_url($CFG->wwwroot . '/course/modedit.php', array('update' => $this->cm->id, 'return' => 1));
574 }
575
454e8dd9
DM
576 /**
577 * @return moodle_url to the allocation page
578 */
da0b1f70
DM
579 public function allocation_url() {
580 global $CFG;
581 return new moodle_url($CFG->wwwroot . '/mod/workshop/allocation.php', array('cmid' => $this->cm->id));
582 }
583
454e8dd9
DM
584 /**
585 * @param int $phasecode The internal phase code
586 * @return moodle_url of the script to change the current phase to $phasecode
587 */
588 public function switchphase_url($phasecode) {
589 global $CFG;
590 $phasecode = clean_param($phasecode, PARAM_INT);
591 return new moodle_url($CFG->wwwroot . '/mod/workshop/switchphase.php', array('cmid' => $this->cm->id, 'phase' => $phasecode));
592 }
593
89c1aa97
DM
594 /**
595 * @return moodle_url to the aggregation page
596 */
597 public function aggregate_url() {
598 global $CFG;
599 return new moodle_url($CFG->wwwroot . '/mod/workshop/aggregate.php', array('cmid' => $this->cm->id));
600 }
601
b8ead2e6 602 /**
407b1e91
DM
603 * Are users allowed to create/edit their submissions?
604 *
605 * TODO: this depends on the workshop phase, phase deadlines, submitting after deadlines possibility
b8ead2e6 606 *
407b1e91 607 * @return bool
b8ead2e6 608 */
407b1e91
DM
609 public function submitting_allowed() {
610 return true;
b8ead2e6
DM
611 }
612
c1e883bb 613 /**
407b1e91 614 * Are reviewers allowed to create/edit their assessments?
c1e883bb 615 *
407b1e91 616 * TODO: this depends on the workshop phase, phase deadlines
c1e883bb
DM
617 *
618 * @return bool
619 */
407b1e91 620 public function assessing_allowed() {
c1e883bb
DM
621 return true;
622 }
623
407b1e91 624
3dc78e5b
DM
625 /**
626 * Are the peer-reviews available to the authors?
627 *
628 * TODO: this depends on the workshop phase
629 *
630 * @return bool
631 */
632 public function assessments_available() {
633 return true;
634 }
635
636 /**
637 * Can the given grades be displayed to the authors?
638 *
639 * Grades are not displayed if {@link self::assessments_available()} return false. The returned
640 * value may be true (if yes, display grades), false (no, hide grades yet) or null (only
641 * display grades if the assessment has been agreed by the author).
642 *
643 * @return bool|null
644 */
645 public function grades_available() {
646 return true;
647 }
648
b13142da
DM
649 /**
650 * Returns the localized name of the grading strategy method to be displayed to the users
651 *
652 * @return string
653 */
654 public function strategy_name() {
f05c168d 655 return get_string('pluginname', 'workshopform_' . $this->strategy);
b13142da 656 }
b761e6d9
DM
657
658 /**
659 * Prepare an individual workshop plan for the given user.
660 *
f05c168d
DM
661 * @param int $userid whom the plan is prepared for
662 * @param stdClass context of the planned workshop
663 * @return stdClass data object to be passed to the renderer
b761e6d9 664 */
d895c6aa 665 public function prepare_user_plan($userid) {
b761e6d9
DM
666 global $DB;
667
668 $phases = array();
669
670 // Prepare tasks for the setup phase
671 $phase = new stdClass();
672 $phase->title = get_string('phasesetup', 'workshop');
673 $phase->tasks = array();
d895c6aa 674 if (has_capability('moodle/course:manageactivities', $this->context, $userid)) {
da0b1f70
DM
675 $task = new stdClass();
676 $task->title = get_string('taskintro', 'workshop');
677 $task->link = $this->updatemod_url();
678 $task->completed = !(trim(strip_tags($this->intro)) == '');
679 $phase->tasks['intro'] = $task;
680 }
d895c6aa 681 if (has_capability('moodle/course:manageactivities', $this->context, $userid)) {
454e8dd9
DM
682 $task = new stdClass();
683 $task->title = get_string('taskinstructauthors', 'workshop');
684 $task->link = $this->updatemod_url();
685 $task->completed = !(trim(strip_tags($this->instructauthors)) == '');
686 $phase->tasks['instructauthors'] = $task;
687 }
d895c6aa 688 if (has_capability('mod/workshop:editdimensions', $this->context, $userid)) {
b761e6d9 689 $task = new stdClass();
da0b1f70
DM
690 $task->title = get_string('editassessmentform', 'workshop');
691 $task->link = $this->editform_url();
692 if ($this->assessment_form_ready()) {
693 $task->completed = true;
694 } elseif ($this->phase > self::PHASE_SETUP) {
695 $task->completed = false;
696 }
b761e6d9
DM
697 $phase->tasks['editform'] = $task;
698 }
da0b1f70
DM
699 if (empty($phase->tasks) and $this->phase == self::PHASE_SETUP) {
700 // if we are in the setup phase and there is no task (typical for students), let us
701 // display some explanation what is going on
702 $task = new stdClass();
703 $task->title = get_string('undersetup', 'workshop');
704 $task->completed = 'info';
705 $phase->tasks['setupinfo'] = $task;
706 }
b761e6d9
DM
707 $phases[self::PHASE_SETUP] = $phase;
708
709 // Prepare tasks for the submission phase
710 $phase = new stdClass();
711 $phase->title = get_string('phasesubmission', 'workshop');
712 $phase->tasks = array();
d895c6aa 713 if (has_capability('moodle/course:manageactivities', $this->context, $userid)) {
b761e6d9 714 $task = new stdClass();
00aca3c1
DM
715 $task->title = get_string('taskinstructreviewers', 'workshop');
716 $task->link = $this->updatemod_url();
717 if (trim(strip_tags($this->instructreviewers))) {
da0b1f70
DM
718 $task->completed = true;
719 } elseif ($this->phase >= self::PHASE_ASSESSMENT) {
720 $task->completed = false;
da0b1f70 721 }
00aca3c1 722 $phase->tasks['instructreviewers'] = $task;
b761e6d9 723 }
d895c6aa 724 if (has_capability('mod/workshop:submit', $this->context, $userid, false)) {
da0b1f70 725 $task = new stdClass();
00aca3c1
DM
726 $task->title = get_string('tasksubmit', 'workshop');
727 $task->link = $this->submission_url();
728 if ($DB->record_exists('workshop_submissions', array('workshopid'=>$this->id, 'example'=>0, 'authorid'=>$userid))) {
da0b1f70
DM
729 $task->completed = true;
730 } elseif ($this->phase >= self::PHASE_ASSESSMENT) {
731 $task->completed = false;
00aca3c1
DM
732 } else {
733 $task->completed = null; // still has a chance to submit
da0b1f70 734 }
00aca3c1 735 $phase->tasks['submit'] = $task;
da0b1f70 736 }
b761e6d9 737 $phases[self::PHASE_SUBMISSION] = $phase;
d895c6aa 738 if (has_capability('mod/workshop:allocate', $this->context, $userid)) {
da0b1f70
DM
739 $task = new stdClass();
740 $task->title = get_string('allocate', 'workshop');
741 $task->link = $this->allocation_url();
d895c6aa 742 $numofauthors = count(get_users_by_capability($this->context, 'mod/workshop:submit', 'u.id', '', '', '',
a3610b08
DM
743 '', '', false, true));
744 $numofsubmissions = $DB->count_records('workshop_submissions', array('workshopid'=>$this->id, 'example'=>0));
745 $sql = 'SELECT COUNT(s.id) AS nonallocated
746 FROM {workshop_submissions} s
747 LEFT JOIN {workshop_assessments} a ON (a.submissionid=s.id)
748 WHERE s.workshopid = :workshopid AND s.example=0 AND a.submissionid IS NULL';
749 $params['workshopid'] = $this->id;
750 $numnonallocated = $DB->count_records_sql($sql, $params);
da0b1f70
DM
751 if ($numofsubmissions == 0) {
752 $task->completed = null;
a3610b08 753 } elseif ($numnonallocated == 0) {
da0b1f70
DM
754 $task->completed = true;
755 } elseif ($this->phase > self::PHASE_SUBMISSION) {
756 $task->completed = false;
757 } else {
758 $task->completed = null; // still has a chance to allocate
759 }
760 $a = new stdClass();
3189fb2d
DM
761 $a->expected = $numofauthors;
762 $a->submitted = $numofsubmissions;
a3610b08 763 $a->allocate = $numnonallocated;
3189fb2d 764 $task->details = get_string('allocatedetails', 'workshop', $a);
da0b1f70 765 unset($a);
3189fb2d 766 $phase->tasks['allocate'] = $task;
3dc78e5b
DM
767
768 if ($numofsubmissions < $numofauthors and $this->phase >= self::PHASE_SUBMISSION) {
769 $task = new stdClass();
770 $task->title = get_string('someuserswosubmission', 'workshop');
771 $task->completed = 'info';
772 $phase->tasks['allocateinfo'] = $task;
773 }
da0b1f70 774 }
b761e6d9
DM
775
776 // Prepare tasks for the peer-assessment phase (includes eventual self-assessments)
777 $phase = new stdClass();
778 $phase->title = get_string('phaseassessment', 'workshop');
779 $phase->tasks = array();
d895c6aa 780 $phase->isreviewer = has_capability('mod/workshop:peerassess', $this->context, $userid);
3dc78e5b 781 $phase->assessments = $this->get_assessments_by_reviewer($userid);
b761e6d9
DM
782 $numofpeers = 0; // number of allocated peer-assessments
783 $numofpeerstodo = 0; // number of peer-assessments to do
784 $numofself = 0; // number of allocated self-assessments - should be 0 or 1
785 $numofselftodo = 0; // number of self-assessments to do - should be 0 or 1
786 foreach ($phase->assessments as $a) {
787 if ($a->authorid == $userid) {
788 $numofself++;
789 if (is_null($a->grade)) {
790 $numofselftodo++;
791 }
792 } else {
793 $numofpeers++;
794 if (is_null($a->grade)) {
795 $numofpeerstodo++;
796 }
797 }
798 }
799 unset($a);
800 if ($numofpeers) {
801 $task = new stdClass();
3dc78e5b
DM
802 if ($numofpeerstodo == 0) {
803 $task->completed = true;
804 } elseif ($this->phase > self::PHASE_ASSESSMENT) {
805 $task->completed = false;
806 }
b761e6d9
DM
807 $a = new stdClass();
808 $a->total = $numofpeers;
809 $a->todo = $numofpeerstodo;
810 $task->title = get_string('taskassesspeers', 'workshop');
da0b1f70 811 $task->details = get_string('taskassesspeersdetails', 'workshop', $a);
b761e6d9
DM
812 unset($a);
813 $phase->tasks['assesspeers'] = $task;
814 }
815 if ($numofself) {
816 $task = new stdClass();
3dc78e5b
DM
817 if ($numofselftodo == 0) {
818 $task->completed = true;
819 } elseif ($this->phase > self::PHASE_ASSESSMENT) {
820 $task->completed = false;
821 }
b761e6d9
DM
822 $task->title = get_string('taskassessself', 'workshop');
823 $phase->tasks['assessself'] = $task;
824 }
825 $phases[self::PHASE_ASSESSMENT] = $phase;
826
1fed6ce3 827 // Prepare tasks for the grading evaluation phase
b761e6d9
DM
828 $phase = new stdClass();
829 $phase->title = get_string('phaseevaluation', 'workshop');
830 $phase->tasks = array();
1fed6ce3
DM
831 if (has_capability('mod/workshop:overridegrades', $this->context)) {
832 $authors = $this->get_potential_authors(false);
833 $reviewers = $this->get_potential_reviewers(false);
834 $expected = count($authors + $reviewers);
835 unset($authors);
836 unset($reviewers);
837 $known = $DB->count_records_select('workshop_aggregations', 'workshopid = ? AND totalgrade IS NOT NULL',
838 array($this->id));
839 $task = new stdClass();
840 $task->title = get_string('calculatetotalgrades', 'workshop');
841 $a = new stdClass();
842 $a->expected = $expected;
843 $a->known = $known;
844 $task->details = get_string('calculatetotalgradesdetails', 'workshop', $a);
845 if ($known >= $expected) {
846 $task->completed = true;
847 } elseif ($this->phase > self::PHASE_EVALUATION) {
848 $task->completed = false;
849 }
f55650e6
DM
850 $phase->tasks['calculatetotalgrade'] = $task;
851 if ($known > 0 and $known < $expected) {
852 $task = new stdClass();
853 $task->title = get_string('totalgradesmissing', 'workshop');
854 $task->completed = 'info';
855 $phase->tasks['totalgradesmissinginfo'] = $task;
856 }
1fed6ce3
DM
857 } else {
858 $task = new stdClass();
859 $task->title = get_string('evaluategradeswait', 'workshop');
860 $task->completed = 'info';
861 $phase->tasks['evaluateinfo'] = $task;
862 }
b761e6d9
DM
863 $phases[self::PHASE_EVALUATION] = $phase;
864
865 // Prepare tasks for the "workshop closed" phase - todo
866 $phase = new stdClass();
867 $phase->title = get_string('phaseclosed', 'workshop');
868 $phase->tasks = array();
869 $phases[self::PHASE_CLOSED] = $phase;
870
871 // Polish data, set default values if not done explicitly
872 foreach ($phases as $phasecode => $phase) {
873 $phase->title = isset($phase->title) ? $phase->title : '';
874 $phase->tasks = isset($phase->tasks) ? $phase->tasks : array();
875 if ($phasecode == $this->phase) {
876 $phase->active = true;
877 } else {
878 $phase->active = false;
879 }
454e8dd9
DM
880 if (!isset($phase->actions)) {
881 $phase->actions = array();
882 }
b761e6d9
DM
883
884 foreach ($phase->tasks as $taskcode => $task) {
885 $task->title = isset($task->title) ? $task->title : '';
da0b1f70
DM
886 $task->link = isset($task->link) ? $task->link : null;
887 $task->details = isset($task->details) ? $task->details : '';
b761e6d9
DM
888 $task->completed = isset($task->completed) ? $task->completed : null;
889 }
890 }
454e8dd9
DM
891
892 // Add phase swithing actions
d895c6aa 893 if (has_capability('mod/workshop:switchphase', $this->context, $userid)) {
454e8dd9
DM
894 foreach ($phases as $phasecode => $phase) {
895 if (! $phase->active) {
896 $action = new stdClass();
897 $action->type = 'switchphase';
898 $action->url = $this->switchphase_url($phasecode);
899 $phase->actions[] = $action;
900 }
901 }
902 }
903
b761e6d9
DM
904 return $phases;
905 }
906
907 /**
908 * Has the assessment form been defined?
909 *
910 * @return bool
911 */
912 public function assessment_form_ready() {
913 return $this->grading_strategy_instance()->form_ready();
914 }
915
454e8dd9
DM
916 /**
917 * Switch to a new workshop phase
918 *
919 * Modifies the underlying database record. You should terminate the script shortly after calling this.
920 *
921 * @param int $newphase new phase code
922 * @return bool true if success, false otherwise
923 */
924 public function switch_phase($newphase) {
925 global $DB;
926
927 $known = $this->available_phases();
928 if (!isset($known[$newphase])) {
929 return false;
930 }
931 $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id));
932 return true;
933 }
ddb59c77
DM
934
935 /**
936 * Saves a raw grade for submission as calculated from the assessment form fields
937 *
938 * @param array $assessmentid assessment record id, must exists
00aca3c1 939 * @param mixed $grade raw percentual grade from 0.00000 to 100.00000
ddb59c77
DM
940 * @return false|float the saved grade
941 */
942 public function set_peer_grade($assessmentid, $grade) {
943 global $DB;
944
945 if (is_null($grade)) {
946 return false;
947 }
948 $data = new stdClass();
949 $data->id = $assessmentid;
950 $data->grade = $grade;
951 $DB->update_record('workshop_assessments', $data);
952 return $grade;
953 }
6516b9e9 954
29dc43e7
DM
955 /**
956 * Prepares data object with all workshop grades to be rendered
957 *
5e71cefb
DM
958 * @param int $userid the user we are preparing the report for
959 * @param mixed $groups single group or array of groups - only show users who are in one of these group(s). Defaults to all
29dc43e7 960 * @param int $page the current page (for the pagination)
5e71cefb
DM
961 * @param int $perpage participants per page (for the pagination)
962 * @param string $sortby lastname|firstname|submissiontitle|submissiongrade|gradinggrade|totalgrade
963 * @param string $sorthow ASC|DESC
29dc43e7
DM
964 * @return stdClass data for the renderer
965 */
d895c6aa 966 public function prepare_grading_report($userid, $groups, $page, $perpage, $sortby, $sorthow) {
29dc43e7
DM
967 global $DB;
968
d895c6aa
DM
969 $canviewall = has_capability('mod/workshop:viewallassessments', $this->context, $userid);
970 $isparticipant = has_any_capability(array('mod/workshop:submit', 'mod/workshop:peerassess'), $this->context, $userid);
29dc43e7
DM
971
972 if (!$canviewall and !$isparticipant) {
973 // who the hell is this?
974 return array();
975 }
976
5e71cefb
DM
977 if (!in_array($sortby, array('lastname','firstname','submissiontitle','submissiongrade','gradinggrade','totalgrade'))) {
978 $sortby = 'lastname';
979 }
980
981 if (!($sorthow === 'ASC' or $sorthow === 'DESC')) {
982 $sorthow = 'ASC';
983 }
984
985 // get the list of user ids to be displayed
29dc43e7
DM
986 if ($canviewall) {
987 // fetch the list of ids of all workshop participants - this may get really long so fetch just id
d895c6aa 988 $participants = get_users_by_capability($this->context, array('mod/workshop:submit', 'mod/workshop:peerassess'),
5e71cefb 989 'u.id', '', '', '', $groups, '', false, false, true);
29dc43e7
DM
990 } else {
991 // this is an ordinary workshop participant (aka student) - display the report just for him/her
992 $participants = array($userid => (object)array('id' => $userid));
993 }
994
5e71cefb 995 // we will need to know the number of all records later for the pagination purposes
29dc43e7
DM
996 $numofparticipants = count($participants);
997
5e71cefb
DM
998 // load all fields which can be used sorting and paginate the records
999 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1000 $params['workshopid'] = $this->id;
1001 $sqlsort = $sortby . ' ' . $sorthow . ',u.lastname,u.firstname,u.id';
1002 $sql = "SELECT u.id AS userid,u.firstname,u.lastname,u.picture,u.imagealt,
89c1aa97 1003 s.title AS submissiontitle, s.grade AS submissiongrade, ag.gradinggrade, ag.totalgrade
5e71cefb
DM
1004 FROM {user} u
1005 LEFT JOIN {workshop_submissions} s ON (s.authorid = u.id)
89c1aa97 1006 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = u.id AND ag.workshopid = s.workshopid)
5e71cefb
DM
1007 WHERE s.workshopid = :workshopid AND s.example = 0 AND u.id $participantids
1008 ORDER BY $sqlsort";
1009 $participants = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
29dc43e7
DM
1010
1011 // this will hold the information needed to display user names and pictures
5e71cefb
DM
1012 $userinfo = array();
1013
1014 // get the user details for all participants to display
1015 foreach ($participants as $participant) {
1016 if (!isset($userinfo[$participant->userid])) {
1017 $userinfo[$participant->userid] = new stdClass();
1018 $userinfo[$participant->userid]->id = $participant->userid;
1019 $userinfo[$participant->userid]->firstname = $participant->firstname;
1020 $userinfo[$participant->userid]->lastname = $participant->lastname;
1021 $userinfo[$participant->userid]->picture = $participant->picture;
1022 $userinfo[$participant->userid]->imagealt = $participant->imagealt;
1023 }
1024 }
29dc43e7 1025
5e71cefb 1026 // load the submissions details
29dc43e7 1027 $submissions = $this->get_submissions(array_keys($participants));
5e71cefb
DM
1028
1029 // get the user details for all moderators (teachers) that have overridden a submission grade
29dc43e7 1030 foreach ($submissions as $submission) {
29dc43e7
DM
1031 if (!isset($userinfo[$submission->gradeoverby])) {
1032 $userinfo[$submission->gradeoverby] = new stdClass();
1033 $userinfo[$submission->gradeoverby]->id = $submission->gradeoverby;
1034 $userinfo[$submission->gradeoverby]->firstname = $submission->overfirstname;
1035 $userinfo[$submission->gradeoverby]->lastname = $submission->overlastname;
1036 $userinfo[$submission->gradeoverby]->picture = $submission->overpicture;
1037 $userinfo[$submission->gradeoverby]->imagealt = $submission->overimagealt;
1038 }
1039 }
1040
5e71cefb 1041 // get the user details for all reviewers of the displayed participants
29dc43e7
DM
1042 $reviewers = array();
1043 if ($submissions) {
1044 list($submissionids, $params) = $DB->get_in_or_equal(array_keys($submissions), SQL_PARAMS_NAMED);
1045 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover,
1046 r.id AS reviewerid, r.lastname, r.firstname, r.picture, r.imagealt,
1047 s.id AS submissionid, s.authorid
1048 FROM {workshop_assessments} a
1049 JOIN {user} r ON (a.reviewerid = r.id)
1050 JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1051 WHERE a.submissionid $submissionids";
1052 $reviewers = $DB->get_records_sql($sql, $params);
1053 foreach ($reviewers as $reviewer) {
1054 if (!isset($userinfo[$reviewer->reviewerid])) {
1055 $userinfo[$reviewer->reviewerid] = new stdClass();
1056 $userinfo[$reviewer->reviewerid]->id = $reviewer->reviewerid;
1057 $userinfo[$reviewer->reviewerid]->firstname = $reviewer->firstname;
1058 $userinfo[$reviewer->reviewerid]->lastname = $reviewer->lastname;
1059 $userinfo[$reviewer->reviewerid]->picture = $reviewer->picture;
1060 $userinfo[$reviewer->reviewerid]->imagealt = $reviewer->imagealt;
1061 }
1062 }
1063 }
1064
5e71cefb 1065 // get the user details for all reviewees of the displayed participants
934329e5
DM
1066 $reviewees = array();
1067 if ($participants) {
1068 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1069 $params['workshopid'] = $this->id;
1070 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover,
1071 u.id AS reviewerid,
1072 s.id AS submissionid,
1073 e.id AS authorid, e.lastname, e.firstname, e.picture, e.imagealt
1074 FROM {user} u
1075 JOIN {workshop_assessments} a ON (a.reviewerid = u.id)
1076 JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1077 JOIN {user} e ON (s.authorid = e.id)
1078 WHERE u.id $participantids AND s.workshopid = :workshopid";
1079 $reviewees = $DB->get_records_sql($sql, $params);
1080 foreach ($reviewees as $reviewee) {
1081 if (!isset($userinfo[$reviewee->authorid])) {
1082 $userinfo[$reviewee->authorid] = new stdClass();
1083 $userinfo[$reviewee->authorid]->id = $reviewee->authorid;
1084 $userinfo[$reviewee->authorid]->firstname = $reviewee->firstname;
1085 $userinfo[$reviewee->authorid]->lastname = $reviewee->lastname;
1086 $userinfo[$reviewee->authorid]->picture = $reviewee->picture;
1087 $userinfo[$reviewee->authorid]->imagealt = $reviewee->imagealt;
1088 }
29dc43e7
DM
1089 }
1090 }
1091
5e71cefb
DM
1092 // finally populate the object to be rendered
1093 $grades = $participants;
29dc43e7
DM
1094
1095 foreach ($participants as $participant) {
1096 // set up default (null) values
5e71cefb
DM
1097 $grades[$participant->userid]->reviewedby = array();
1098 $grades[$participant->userid]->reviewerof = array();
29dc43e7
DM
1099 }
1100 unset($participants);
1101 unset($participant);
1102
1103 foreach ($submissions as $submission) {
1104 $grades[$submission->authorid]->submissionid = $submission->id;
1105 $grades[$submission->authorid]->submissiontitle = $submission->title;
b4857acb
DM
1106 $grades[$submission->authorid]->submissiongrade = $this->real_grade($submission->grade);
1107 $grades[$submission->authorid]->submissiongradeover = $this->real_grade($submission->gradeover);
29dc43e7
DM
1108 $grades[$submission->authorid]->submissiongradeoverby = $submission->gradeoverby;
1109 }
1110 unset($submissions);
1111 unset($submission);
1112
1113 foreach($reviewers as $reviewer) {
1114 $info = new stdClass();
1115 $info->userid = $reviewer->reviewerid;
1116 $info->assessmentid = $reviewer->assessmentid;
1117 $info->submissionid = $reviewer->submissionid;
b4857acb
DM
1118 $info->grade = $this->real_grade($reviewer->grade);
1119 $info->gradinggrade = $this->real_grading_grade($reviewer->gradinggrade);
1120 $info->gradinggradeover = $this->real_grading_grade($reviewer->gradinggradeover);
29dc43e7
DM
1121 $grades[$reviewer->authorid]->reviewedby[$reviewer->reviewerid] = $info;
1122 }
1123 unset($reviewers);
1124 unset($reviewer);
1125
1126 foreach($reviewees as $reviewee) {
1127 $info = new stdClass();
1128 $info->userid = $reviewee->authorid;
1129 $info->assessmentid = $reviewee->assessmentid;
1130 $info->submissionid = $reviewee->submissionid;
b4857acb
DM
1131 $info->grade = $this->real_grade($reviewee->grade);
1132 $info->gradinggrade = $this->real_grading_grade($reviewee->gradinggrade);
1133 $info->gradinggradeover = $this->real_grading_grade($reviewee->gradinggradeover);
29dc43e7
DM
1134 $grades[$reviewee->reviewerid]->reviewerof[$reviewee->authorid] = $info;
1135 }
1136 unset($reviewees);
1137 unset($reviewee);
1138
b4857acb
DM
1139 foreach ($grades as $grade) {
1140 $grade->gradinggrade = $this->real_grading_grade($grade->gradinggrade);
1141 $grade->totalgrade = $this->format_total_grade($grade->totalgrade);
1142 }
1143
29dc43e7
DM
1144 $data = new stdClass();
1145 $data->grades = $grades;
1146 $data->userinfo = $userinfo;
1147 $data->totalcount = $numofparticipants;
b4857acb
DM
1148 $data->maxgrade = $this->real_grade(100);
1149 $data->maxgradinggrade = $this->real_grading_grade(100);
1fed6ce3 1150 $data->maxtotalgrade = $this->format_total_grade($data->maxgrade + $data->maxgradinggrade);
29dc43e7
DM
1151 return $data;
1152 }
1153
29dc43e7 1154 /**
b4857acb 1155 * Calculates the real value of a grade
29dc43e7 1156 *
b4857acb
DM
1157 * @param float $value percentual value from 0 to 100
1158 * @param float $max the maximal grade
1159 * @return string
1160 */
1161 public function real_grade_value($value, $max) {
1162 $localized = true;
1163 if (is_null($value)) {
1164 return null;
1165 } elseif ($max == 0) {
1166 return 0;
1167 } else {
1168 return format_float($max * $value / 100, $this->gradedecimals, $localized);
1169 }
1170 }
1171
1172 /**
1173 * Rounds the value from DB to be displayed
29dc43e7 1174 *
b4857acb
DM
1175 * @param float $raw value from {workshop_aggregations}
1176 * @return string
29dc43e7 1177 */
b4857acb
DM
1178 public function format_total_grade($raw) {
1179 if (is_null($raw)) {
29dc43e7
DM
1180 return null;
1181 }
1fed6ce3 1182 return format_float($raw, $this->gradedecimals, true);
b4857acb
DM
1183 }
1184
1185 /**
1186 * Calculates the real value of grade for submission
1187 *
1188 * @param float $value percentual value from 0 to 100
1189 * @return string
1190 */
1191 public function real_grade($value) {
1192 return $this->real_grade_value($value, $this->grade);
1193 }
1194
1195 /**
1196 * Calculates the real value of grade for assessment
1197 *
1198 * @param float $value percentual value from 0 to 100
1199 * @return string
1200 */
1201 public function real_grading_grade($value) {
1202 return $this->real_grade_value($value, $this->gradinggrade);
29dc43e7
DM
1203 }
1204
89c1aa97 1205 /**
e9a90e69 1206 * Calculates grades for submission for the given participant(s) and updates it in the database
89c1aa97
DM
1207 *
1208 * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s)
1209 * @return void
1210 */
8a1ba8ac 1211 public function aggregate_submission_grades($restrict=null) {
89c1aa97
DM
1212 global $DB;
1213
1214 // fetch a recordset with all assessments to process
1696f36c 1215 $sql = 'SELECT s.id AS submissionid, s.grade AS submissiongrade,
89c1aa97
DM
1216 a.weight, a.grade
1217 FROM {workshop_submissions} s
1218 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id)
1219 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
1220 $params = array('workshopid' => $this->id);
1221
1222 if (is_null($restrict)) {
1223 // update all users - no more conditions
1224 } elseif (!empty($restrict)) {
1225 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1226 $sql .= " AND s.authorid $usql";
1227 $params = array_merge($params, $uparams);
1228 } else {
1229 throw new coding_exception('Empty value is not a valid parameter here');
1230 }
1231
1232 $sql .= ' ORDER BY s.id'; // this is important for bulk processing
89c1aa97 1233
e9a90e69
DM
1234 $rs = $DB->get_recordset_sql($sql, $params);
1235 $batch = array(); // will contain a set of all assessments of a single submission
1236 $previous = null; // a previous record in the recordset
1237
89c1aa97
DM
1238 foreach ($rs as $current) {
1239 if (is_null($previous)) {
1240 // we are processing the very first record in the recordset
1241 $previous = $current;
89c1aa97 1242 }
e9a90e69 1243 if ($current->submissionid == $previous->submissionid) {
89c1aa97 1244 // we are still processing the current submission
e9a90e69
DM
1245 $batch[] = $current;
1246 } else {
1247 // process all the assessments of a sigle submission
1248 $this->aggregate_submission_grades_process($batch);
1249 // and then start to process another submission
1250 $batch = array($current);
1251 $previous = $current;
89c1aa97
DM
1252 }
1253 }
e9a90e69
DM
1254 // do not forget to process the last batch!
1255 $this->aggregate_submission_grades_process($batch);
89c1aa97
DM
1256 $rs->close();
1257 }
1258
1259 /**
1260 * Calculates grades for assessment for the given participant(s)
1261 *
39411930
DM
1262 * Grade for assessment is calculated as a simple mean of all grading grades calculated by the grading evaluator.
1263 * The assessment weight is not taken into account here.
89c1aa97
DM
1264 *
1265 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
1266 * @return void
1267 */
8a1ba8ac 1268 public function aggregate_grading_grades($restrict=null) {
89c1aa97
DM
1269 global $DB;
1270
39411930
DM
1271 // fetch a recordset with all assessments to process
1272 $sql = 'SELECT a.reviewerid, a.gradinggrade, a.gradinggradeover,
1273 ag.id AS aggregationid, ag.gradinggrade AS aggregatedgrade
1274 FROM {workshop_assessments} a
1275 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1276 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = a.reviewerid AND ag.workshopid = s.workshopid)
1277 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
1278 $params = array('workshopid' => $this->id);
1279
1280 if (is_null($restrict)) {
1281 // update all users - no more conditions
1282 } elseif (!empty($restrict)) {
1283 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1284 $sql .= " AND a.reviewerid $usql";
1285 $params = array_merge($params, $uparams);
1286 } else {
1287 throw new coding_exception('Empty value is not a valid parameter here');
1288 }
1289
1290 $sql .= ' ORDER BY a.reviewerid'; // this is important for bulk processing
1291
1292 $rs = $DB->get_recordset_sql($sql, $params);
1293 $batch = array(); // will contain a set of all assessments of a single submission
1294 $previous = null; // a previous record in the recordset
1295
1296 foreach ($rs as $current) {
1297 if (is_null($previous)) {
1298 // we are processing the very first record in the recordset
1299 $previous = $current;
1300 }
1301 if ($current->reviewerid == $previous->reviewerid) {
1302 // we are still processing the current reviewer
1303 $batch[] = $current;
1304 } else {
1305 // process all the assessments of a sigle submission
1306 $this->aggregate_grading_grades_process($batch);
1307 // and then start to process another reviewer
1308 $batch = array($current);
1309 $previous = $current;
1310 }
1311 }
1312 // do not forget to process the last batch!
1313 $this->aggregate_grading_grades_process($batch);
1314 $rs->close();
89c1aa97
DM
1315 }
1316
ad6a8f69
DM
1317 /**
1318 * Calculates the workshop total grades for the given participant(s)
1319 *
1320 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
1321 * @return void
1322 */
1323 public function aggregate_total_grades($restrict=null) {
1324 global $DB;
1325
1fed6ce3
DM
1326 // fetch a recordset with all assessments to process
1327 $sql = 'SELECT s.grade, s.gradeover, s.authorid AS userid,
1328 ag.id AS agid, ag.gradinggrade, ag.totalgrade
1329 FROM {workshop_submissions} s
1330 INNER JOIN {workshop_aggregations} ag ON (ag.userid = s.authorid)
1331 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
1332 $params = array('workshopid' => $this->id);
1333
1334 if (is_null($restrict)) {
1335 // update all users - no more conditions
1336 } elseif (!empty($restrict)) {
1337 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1338 $sql .= " AND ag.userid $usql";
1339 $params = array_merge($params, $uparams);
1340 } else {
1341 throw new coding_exception('Empty value is not a valid parameter here');
1342 }
1343
1344 $sql .= ' ORDER BY ag.userid'; // this is important for bulk processing
1345
1346 $rs = $DB->get_recordset_sql($sql, $params);
1347
1348 foreach ($rs as $current) {
1349 $this->aggregate_total_grades_process($current);
1350 }
1351 $rs->close();
ad6a8f69
DM
1352 }
1353
d5506aac
DM
1354 ////////////////////////////////////////////////////////////////////////////
1355 // Helper methods //
1356 ////////////////////////////////////////////////////////////////////////////
1357
1358 /**
1359 * Helper function returning the greatest common divisor
1360 *
1361 * @param int $a
1362 * @param int $b
1363 * @return int
1364 */
1365 public static function gcd($a, $b) {
1366 return ($b == 0) ? ($a):(self::gcd($b, $a % $b));
1367 }
1368
1369 /**
1370 * Helper function returning the least common multiple
1371 *
1372 * @param int $a
1373 * @param int $b
1374 * @return int
1375 */
1376 public static function lcm($a, $b) {
1377 return ($a / self::gcd($a,$b)) * $b;
1378 }
ad6a8f69 1379
aa40adbf
DM
1380 ////////////////////////////////////////////////////////////////////////////////
1381 // Internal methods (implementation details) //
1382 ////////////////////////////////////////////////////////////////////////////////
6516b9e9 1383
e9a90e69
DM
1384 /**
1385 * Given an array of all assessments of a single submission, calculates the final grade for this submission
1386 *
1387 * This calculates the weighted mean of the passed assessment grades. If, however, the submission grade
1388 * was overridden by a teacher, the gradeover value is returned and the rest of grades are ignored.
1389 *
1390 * @param array $assessments of stdClass(->submissionid ->submissiongrade ->gradeover ->weight ->grade)
1fed6ce3 1391 * @return void
e9a90e69
DM
1392 */
1393 protected function aggregate_submission_grades_process(array $assessments) {
1394 global $DB;
1395
1396 $submissionid = null; // the id of the submission being processed
1397 $current = null; // the grade currently saved in database
1398 $finalgrade = null; // the new grade to be calculated
1399 $sumgrades = 0;
1400 $sumweights = 0;
1401
1402 foreach ($assessments as $assessment) {
1403 if (is_null($submissionid)) {
1404 // the id is the same in all records, fetch it during the first loop cycle
1405 $submissionid = $assessment->submissionid;
1406 }
1407 if (is_null($current)) {
1408 // the currently saved grade is the same in all records, fetch it during the first loop cycle
1409 $current = $assessment->submissiongrade;
1410 }
e9a90e69
DM
1411 if (is_null($assessment->grade)) {
1412 // this was not assessed yet
1413 continue;
1414 }
1415 if ($assessment->weight == 0) {
1416 // this does not influence the calculation
1417 continue;
1418 }
1419 $sumgrades += $assessment->grade * $assessment->weight;
1420 $sumweights += $assessment->weight;
1421 }
1422 if ($sumweights > 0 and is_null($finalgrade)) {
1423 $finalgrade = grade_floatval($sumgrades / $sumweights);
1424 }
1425 // check if the new final grade differs from the one stored in the database
1426 if (grade_floats_different($finalgrade, $current)) {
1427 // we need to save new calculation into the database
1428 $DB->set_field('workshop_submissions', 'grade', $finalgrade, array('id' => $submissionid));
1429 }
1430 }
1431
39411930
DM
1432 /**
1433 * Given an array of all assessments done by a single reviewer, calculates the final grading grade
1434 *
1435 * This calculates the simple mean of the passed grading grades. If, however, the grading grade
1436 * was overridden by a teacher, the gradinggradeover value is returned and the rest of grades are ignored.
1437 *
1438 * @param array $assessments of stdClass(->reviewerid ->gradinggrade ->gradinggradeover ->aggregationid ->aggregatedgrade)
1fed6ce3 1439 * @return void
39411930
DM
1440 */
1441 protected function aggregate_grading_grades_process(array $assessments) {
1442 global $DB;
1443
1444 $reviewerid = null; // the id of the reviewer being processed
1445 $current = null; // the gradinggrade currently saved in database
1446 $finalgrade = null; // the new grade to be calculated
1447 $agid = null; // aggregation id
1448 $sumgrades = 0;
1449 $count = 0;
1450
1451 foreach ($assessments as $assessment) {
1452 if (is_null($reviewerid)) {
1453 // the id is the same in all records, fetch it during the first loop cycle
1454 $reviewerid = $assessment->reviewerid;
1455 }
1456 if (is_null($agid)) {
1457 // the id is the same in all records, fetch it during the first loop cycle
1458 $agid = $assessment->aggregationid;
1459 }
1460 if (is_null($current)) {
1461 // the currently saved grade is the same in all records, fetch it during the first loop cycle
1462 $current = $assessment->aggregatedgrade;
1463 }
1464 if (!is_null($assessment->gradinggradeover)) {
1465 // the grading grade for this assessment is overriden by a teacher
1466 $sumgrades += $assessment->gradinggradeover;
1467 $count++;
1468 } else {
1469 if (!is_null($assessment->gradinggrade)) {
1470 $sumgrades += $assessment->gradinggrade;
1471 $count++;
1472 }
1473 }
1474 }
1475 if ($count > 0) {
1476 $finalgrade = grade_floatval($sumgrades / $count);
1477 }
1478 // check if the new final grade differs from the one stored in the database
1479 if (grade_floats_different($finalgrade, $current)) {
1480 // we need to save new calculation into the database
1481 if (is_null($agid)) {
1482 // no aggregation record yet
1483 $record = new stdClass();
1484 $record->workshopid = $this->id;
1485 $record->userid = $reviewerid;
1486 $record->gradinggrade = $finalgrade;
1487 $DB->insert_record('workshop_aggregations', $record);
1488 } else {
1489 $DB->set_field('workshop_aggregations', 'gradinggrade', $finalgrade, array('id' => $agid));
1490 }
1491 }
1492 }
1493
1fed6ce3
DM
1494 /**
1495 * Given an object with final grade for submission and final grade for assessment, updates the total grade in DB
1496 *
1497 * @param stdClass $data
1498 * @return void
1499 */
1500 protected function aggregate_total_grades_process(stdClass $data) {
1501 global $DB;
1502
1503 if (!is_null($data->gradeover)) {
1504 $submissiongrade = $data->gradeover;
1505 } else {
1506 $submissiongrade = $data->grade;
1507 }
1508
1509 // If we do not have enough information to update totalgrade, do not do
1510 // anything. Please note there may be a lot of reasons why the workshop
1511 // participant does not have one of these grades - maybe she was ill or
1512 // just did not reach the deadlines. Teacher has to fix grades in
1513 // gradebook manually.
1514
1515 if (is_null($submissiongrade) or (!empty($this->gradinggrade) and is_null($this->gradinggrade))) {
1516 return;
1517 }
1518
1519 $totalgrade = $this->grade * $submissiongrade / 100 + $this->gradinggrade * $data->gradinggrade / 100;
1520
1521 // check if the new total grade differs from the one stored in the database
1522 if (grade_floats_different($totalgrade, $data->totalgrade)) {
1523 // we need to save new calculation into the database
1524 if (is_null($data->agid)) {
1525 // no aggregation record yet
1526 $record = new stdClass();
1527 $record->workshopid = $this->id;
1528 $record->userid = $data->userid;
1529 $record->totalgrade = $totalgrade;
1530 $DB->insert_record('workshop_aggregations', $record);
1531 } else {
1532 $DB->set_field('workshop_aggregations', 'totalgrade', $totalgrade, array('id' => $data->agid));
1533 }
1534 }
1535 }
1536
6516b9e9 1537 /**
aa40adbf 1538 * Given a list of user ids, returns the filtered one containing just ids of users with own submission
6516b9e9 1539 *
aa40adbf
DM
1540 * Example submissions are ignored.
1541 *
1542 * @param array $userids
6516b9e9
DM
1543 * @return array
1544 */
aa40adbf
DM
1545 protected function users_with_submission(array $userids) {
1546 global $DB;
1547
1548 if (empty($userids)) {
1549 return array();
1550 }
1551 $userswithsubmission = array();
1552 list($usql, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
00aca3c1 1553 $sql = "SELECT id,authorid
aa40adbf 1554 FROM {workshop_submissions}
00aca3c1 1555 WHERE example = 0 AND workshopid = :workshopid AND authorid $usql";
aa40adbf
DM
1556 $params = array('workshopid' => $this->id);
1557 $params = array_merge($params, $uparams);
1558 $submissions = $DB->get_records_sql($sql, $params);
1559 foreach ($submissions as $submission) {
00aca3c1 1560 $userswithsubmission[$submission->authorid] = true;
aa40adbf
DM
1561 }
1562
1563 return $userswithsubmission;
6516b9e9
DM
1564 }
1565
aa40adbf
DM
1566 /**
1567 * @return array of available workshop phases
1568 */
1569 protected function available_phases() {
1570 return array(
1571 self::PHASE_SETUP => true,
1572 self::PHASE_SUBMISSION => true,
1573 self::PHASE_ASSESSMENT => true,
1574 self::PHASE_EVALUATION => true,
1575 self::PHASE_CLOSED => true,
1576 );
1577 }
1578
66c9894d 1579}