Faking assessments reworked
[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',
aa40adbf 168 'u.id,u.lastname,u.firstname', 'u.lastname,u.firstname,u.id', 0, 1000, '', '', 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',
aa40adbf 186 'u.id, u.lastname, u.firstname', 'u.lastname,u.firstname,u.id', 0, 1000, '', '', 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;
547 return new moodle_url($CFG->wwwroot . '/mod/workshop/assessment.php', array('preview' => $this->cm->id));
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
DM
602 /**
603 * Returns an object containing all data to display the user's full name and picture
604 *
605 * @param int $id optional user id, defaults to the current user
65ba104c 606 * @return stdClass containing properties lastname, firstname, picture and imagealt
b8ead2e6
DM
607 */
608 public function user_info($id=null) {
609 global $USER, $DB;
610
611 if (is_null($id) || ($id == $USER->id)) {
612 return $USER;
613 } else {
614 return $DB->get_record('user', array('id' => $id), 'id,lastname,firstname,picture,imagealt', MUST_EXIST);
615 }
616 }
617
c1e883bb 618 /**
b13142da 619 * Are users allowed to create/edit their submissions?
c1e883bb
DM
620 *
621 * TODO: this depends on the workshop phase, phase deadlines, submitting after deadlines possibility
622 *
623 * @return bool
624 */
625 public function submitting_allowed() {
626 return true;
627 }
628
3dc78e5b
DM
629 /**
630 * Are the peer-reviews available to the authors?
631 *
632 * TODO: this depends on the workshop phase
633 *
634 * @return bool
635 */
636 public function assessments_available() {
637 return true;
638 }
639
640 /**
641 * Can the given grades be displayed to the authors?
642 *
643 * Grades are not displayed if {@link self::assessments_available()} return false. The returned
644 * value may be true (if yes, display grades), false (no, hide grades yet) or null (only
645 * display grades if the assessment has been agreed by the author).
646 *
647 * @return bool|null
648 */
649 public function grades_available() {
650 return true;
651 }
652
b13142da
DM
653 /**
654 * Returns the localized name of the grading strategy method to be displayed to the users
655 *
656 * @return string
657 */
658 public function strategy_name() {
f05c168d 659 return get_string('pluginname', 'workshopform_' . $this->strategy);
b13142da 660 }
b761e6d9
DM
661
662 /**
663 * Prepare an individual workshop plan for the given user.
664 *
f05c168d
DM
665 * @param int $userid whom the plan is prepared for
666 * @param stdClass context of the planned workshop
667 * @return stdClass data object to be passed to the renderer
b761e6d9 668 */
d895c6aa 669 public function prepare_user_plan($userid) {
b761e6d9
DM
670 global $DB;
671
672 $phases = array();
673
674 // Prepare tasks for the setup phase
675 $phase = new stdClass();
676 $phase->title = get_string('phasesetup', 'workshop');
677 $phase->tasks = array();
d895c6aa 678 if (has_capability('moodle/course:manageactivities', $this->context, $userid)) {
da0b1f70
DM
679 $task = new stdClass();
680 $task->title = get_string('taskintro', 'workshop');
681 $task->link = $this->updatemod_url();
682 $task->completed = !(trim(strip_tags($this->intro)) == '');
683 $phase->tasks['intro'] = $task;
684 }
d895c6aa 685 if (has_capability('moodle/course:manageactivities', $this->context, $userid)) {
454e8dd9
DM
686 $task = new stdClass();
687 $task->title = get_string('taskinstructauthors', 'workshop');
688 $task->link = $this->updatemod_url();
689 $task->completed = !(trim(strip_tags($this->instructauthors)) == '');
690 $phase->tasks['instructauthors'] = $task;
691 }
d895c6aa 692 if (has_capability('mod/workshop:editdimensions', $this->context, $userid)) {
b761e6d9 693 $task = new stdClass();
da0b1f70
DM
694 $task->title = get_string('editassessmentform', 'workshop');
695 $task->link = $this->editform_url();
696 if ($this->assessment_form_ready()) {
697 $task->completed = true;
698 } elseif ($this->phase > self::PHASE_SETUP) {
699 $task->completed = false;
700 }
b761e6d9
DM
701 $phase->tasks['editform'] = $task;
702 }
da0b1f70
DM
703 if (empty($phase->tasks) and $this->phase == self::PHASE_SETUP) {
704 // if we are in the setup phase and there is no task (typical for students), let us
705 // display some explanation what is going on
706 $task = new stdClass();
707 $task->title = get_string('undersetup', 'workshop');
708 $task->completed = 'info';
709 $phase->tasks['setupinfo'] = $task;
710 }
b761e6d9
DM
711 $phases[self::PHASE_SETUP] = $phase;
712
713 // Prepare tasks for the submission phase
714 $phase = new stdClass();
715 $phase->title = get_string('phasesubmission', 'workshop');
716 $phase->tasks = array();
d895c6aa 717 if (has_capability('moodle/course:manageactivities', $this->context, $userid)) {
b761e6d9 718 $task = new stdClass();
00aca3c1
DM
719 $task->title = get_string('taskinstructreviewers', 'workshop');
720 $task->link = $this->updatemod_url();
721 if (trim(strip_tags($this->instructreviewers))) {
da0b1f70
DM
722 $task->completed = true;
723 } elseif ($this->phase >= self::PHASE_ASSESSMENT) {
724 $task->completed = false;
da0b1f70 725 }
00aca3c1 726 $phase->tasks['instructreviewers'] = $task;
b761e6d9 727 }
d895c6aa 728 if (has_capability('mod/workshop:submit', $this->context, $userid, false)) {
da0b1f70 729 $task = new stdClass();
00aca3c1
DM
730 $task->title = get_string('tasksubmit', 'workshop');
731 $task->link = $this->submission_url();
732 if ($DB->record_exists('workshop_submissions', array('workshopid'=>$this->id, 'example'=>0, 'authorid'=>$userid))) {
da0b1f70
DM
733 $task->completed = true;
734 } elseif ($this->phase >= self::PHASE_ASSESSMENT) {
735 $task->completed = false;
00aca3c1
DM
736 } else {
737 $task->completed = null; // still has a chance to submit
da0b1f70 738 }
00aca3c1 739 $phase->tasks['submit'] = $task;
da0b1f70 740 }
b761e6d9 741 $phases[self::PHASE_SUBMISSION] = $phase;
d895c6aa 742 if (has_capability('mod/workshop:allocate', $this->context, $userid)) {
da0b1f70
DM
743 $task = new stdClass();
744 $task->title = get_string('allocate', 'workshop');
745 $task->link = $this->allocation_url();
d895c6aa 746 $numofauthors = count(get_users_by_capability($this->context, 'mod/workshop:submit', 'u.id', '', '', '',
a3610b08
DM
747 '', '', false, true));
748 $numofsubmissions = $DB->count_records('workshop_submissions', array('workshopid'=>$this->id, 'example'=>0));
749 $sql = 'SELECT COUNT(s.id) AS nonallocated
750 FROM {workshop_submissions} s
751 LEFT JOIN {workshop_assessments} a ON (a.submissionid=s.id)
752 WHERE s.workshopid = :workshopid AND s.example=0 AND a.submissionid IS NULL';
753 $params['workshopid'] = $this->id;
754 $numnonallocated = $DB->count_records_sql($sql, $params);
da0b1f70
DM
755 if ($numofsubmissions == 0) {
756 $task->completed = null;
a3610b08 757 } elseif ($numnonallocated == 0) {
da0b1f70
DM
758 $task->completed = true;
759 } elseif ($this->phase > self::PHASE_SUBMISSION) {
760 $task->completed = false;
761 } else {
762 $task->completed = null; // still has a chance to allocate
763 }
764 $a = new stdClass();
3189fb2d
DM
765 $a->expected = $numofauthors;
766 $a->submitted = $numofsubmissions;
a3610b08 767 $a->allocate = $numnonallocated;
3189fb2d 768 $task->details = get_string('allocatedetails', 'workshop', $a);
da0b1f70 769 unset($a);
3189fb2d 770 $phase->tasks['allocate'] = $task;
3dc78e5b
DM
771
772 if ($numofsubmissions < $numofauthors and $this->phase >= self::PHASE_SUBMISSION) {
773 $task = new stdClass();
774 $task->title = get_string('someuserswosubmission', 'workshop');
775 $task->completed = 'info';
776 $phase->tasks['allocateinfo'] = $task;
777 }
da0b1f70 778 }
b761e6d9
DM
779
780 // Prepare tasks for the peer-assessment phase (includes eventual self-assessments)
781 $phase = new stdClass();
782 $phase->title = get_string('phaseassessment', 'workshop');
783 $phase->tasks = array();
d895c6aa 784 $phase->isreviewer = has_capability('mod/workshop:peerassess', $this->context, $userid);
3dc78e5b 785 $phase->assessments = $this->get_assessments_by_reviewer($userid);
b761e6d9
DM
786 $numofpeers = 0; // number of allocated peer-assessments
787 $numofpeerstodo = 0; // number of peer-assessments to do
788 $numofself = 0; // number of allocated self-assessments - should be 0 or 1
789 $numofselftodo = 0; // number of self-assessments to do - should be 0 or 1
790 foreach ($phase->assessments as $a) {
791 if ($a->authorid == $userid) {
792 $numofself++;
793 if (is_null($a->grade)) {
794 $numofselftodo++;
795 }
796 } else {
797 $numofpeers++;
798 if (is_null($a->grade)) {
799 $numofpeerstodo++;
800 }
801 }
802 }
803 unset($a);
804 if ($numofpeers) {
805 $task = new stdClass();
3dc78e5b
DM
806 if ($numofpeerstodo == 0) {
807 $task->completed = true;
808 } elseif ($this->phase > self::PHASE_ASSESSMENT) {
809 $task->completed = false;
810 }
b761e6d9
DM
811 $a = new stdClass();
812 $a->total = $numofpeers;
813 $a->todo = $numofpeerstodo;
814 $task->title = get_string('taskassesspeers', 'workshop');
da0b1f70 815 $task->details = get_string('taskassesspeersdetails', 'workshop', $a);
b761e6d9
DM
816 unset($a);
817 $phase->tasks['assesspeers'] = $task;
818 }
819 if ($numofself) {
820 $task = new stdClass();
3dc78e5b
DM
821 if ($numofselftodo == 0) {
822 $task->completed = true;
823 } elseif ($this->phase > self::PHASE_ASSESSMENT) {
824 $task->completed = false;
825 }
b761e6d9
DM
826 $task->title = get_string('taskassessself', 'workshop');
827 $phase->tasks['assessself'] = $task;
828 }
829 $phases[self::PHASE_ASSESSMENT] = $phase;
830
831 // Prepare tasks for the grading evaluation phase - todo
832 $phase = new stdClass();
833 $phase->title = get_string('phaseevaluation', 'workshop');
834 $phase->tasks = array();
835 $phases[self::PHASE_EVALUATION] = $phase;
836
837 // Prepare tasks for the "workshop closed" phase - todo
838 $phase = new stdClass();
839 $phase->title = get_string('phaseclosed', 'workshop');
840 $phase->tasks = array();
841 $phases[self::PHASE_CLOSED] = $phase;
842
843 // Polish data, set default values if not done explicitly
844 foreach ($phases as $phasecode => $phase) {
845 $phase->title = isset($phase->title) ? $phase->title : '';
846 $phase->tasks = isset($phase->tasks) ? $phase->tasks : array();
847 if ($phasecode == $this->phase) {
848 $phase->active = true;
849 } else {
850 $phase->active = false;
851 }
454e8dd9
DM
852 if (!isset($phase->actions)) {
853 $phase->actions = array();
854 }
b761e6d9
DM
855
856 foreach ($phase->tasks as $taskcode => $task) {
857 $task->title = isset($task->title) ? $task->title : '';
da0b1f70
DM
858 $task->link = isset($task->link) ? $task->link : null;
859 $task->details = isset($task->details) ? $task->details : '';
b761e6d9
DM
860 $task->completed = isset($task->completed) ? $task->completed : null;
861 }
862 }
454e8dd9
DM
863
864 // Add phase swithing actions
d895c6aa 865 if (has_capability('mod/workshop:switchphase', $this->context, $userid)) {
454e8dd9
DM
866 foreach ($phases as $phasecode => $phase) {
867 if (! $phase->active) {
868 $action = new stdClass();
869 $action->type = 'switchphase';
870 $action->url = $this->switchphase_url($phasecode);
871 $phase->actions[] = $action;
872 }
873 }
874 }
875
b761e6d9
DM
876 return $phases;
877 }
878
879 /**
880 * Has the assessment form been defined?
881 *
882 * @return bool
883 */
884 public function assessment_form_ready() {
885 return $this->grading_strategy_instance()->form_ready();
886 }
887
454e8dd9
DM
888 /**
889 * Switch to a new workshop phase
890 *
891 * Modifies the underlying database record. You should terminate the script shortly after calling this.
892 *
893 * @param int $newphase new phase code
894 * @return bool true if success, false otherwise
895 */
896 public function switch_phase($newphase) {
897 global $DB;
898
899 $known = $this->available_phases();
900 if (!isset($known[$newphase])) {
901 return false;
902 }
903 $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id));
904 return true;
905 }
ddb59c77
DM
906
907 /**
908 * Saves a raw grade for submission as calculated from the assessment form fields
909 *
910 * @param array $assessmentid assessment record id, must exists
00aca3c1 911 * @param mixed $grade raw percentual grade from 0.00000 to 100.00000
ddb59c77
DM
912 * @return false|float the saved grade
913 */
914 public function set_peer_grade($assessmentid, $grade) {
915 global $DB;
916
917 if (is_null($grade)) {
918 return false;
919 }
920 $data = new stdClass();
921 $data->id = $assessmentid;
922 $data->grade = $grade;
923 $DB->update_record('workshop_assessments', $data);
924 return $grade;
925 }
6516b9e9 926
29dc43e7
DM
927 /**
928 * Prepares data object with all workshop grades to be rendered
929 *
5e71cefb
DM
930 * @param int $userid the user we are preparing the report for
931 * @param mixed $groups single group or array of groups - only show users who are in one of these group(s). Defaults to all
29dc43e7 932 * @param int $page the current page (for the pagination)
5e71cefb
DM
933 * @param int $perpage participants per page (for the pagination)
934 * @param string $sortby lastname|firstname|submissiontitle|submissiongrade|gradinggrade|totalgrade
935 * @param string $sorthow ASC|DESC
29dc43e7
DM
936 * @return stdClass data for the renderer
937 */
d895c6aa 938 public function prepare_grading_report($userid, $groups, $page, $perpage, $sortby, $sorthow) {
29dc43e7
DM
939 global $DB;
940
d895c6aa
DM
941 $canviewall = has_capability('mod/workshop:viewallassessments', $this->context, $userid);
942 $isparticipant = has_any_capability(array('mod/workshop:submit', 'mod/workshop:peerassess'), $this->context, $userid);
29dc43e7
DM
943
944 if (!$canviewall and !$isparticipant) {
945 // who the hell is this?
946 return array();
947 }
948
5e71cefb
DM
949 if (!in_array($sortby, array('lastname','firstname','submissiontitle','submissiongrade','gradinggrade','totalgrade'))) {
950 $sortby = 'lastname';
951 }
952
953 if (!($sorthow === 'ASC' or $sorthow === 'DESC')) {
954 $sorthow = 'ASC';
955 }
956
957 // get the list of user ids to be displayed
29dc43e7
DM
958 if ($canviewall) {
959 // fetch the list of ids of all workshop participants - this may get really long so fetch just id
d895c6aa 960 $participants = get_users_by_capability($this->context, array('mod/workshop:submit', 'mod/workshop:peerassess'),
5e71cefb 961 'u.id', '', '', '', $groups, '', false, false, true);
29dc43e7
DM
962 } else {
963 // this is an ordinary workshop participant (aka student) - display the report just for him/her
964 $participants = array($userid => (object)array('id' => $userid));
965 }
966
5e71cefb 967 // we will need to know the number of all records later for the pagination purposes
29dc43e7
DM
968 $numofparticipants = count($participants);
969
5e71cefb
DM
970 // load all fields which can be used sorting and paginate the records
971 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
972 $params['workshopid'] = $this->id;
973 $sqlsort = $sortby . ' ' . $sorthow . ',u.lastname,u.firstname,u.id';
974 $sql = "SELECT u.id AS userid,u.firstname,u.lastname,u.picture,u.imagealt,
89c1aa97 975 s.title AS submissiontitle, s.grade AS submissiongrade, ag.gradinggrade, ag.totalgrade
5e71cefb
DM
976 FROM {user} u
977 LEFT JOIN {workshop_submissions} s ON (s.authorid = u.id)
89c1aa97 978 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = u.id AND ag.workshopid = s.workshopid)
5e71cefb
DM
979 WHERE s.workshopid = :workshopid AND s.example = 0 AND u.id $participantids
980 ORDER BY $sqlsort";
981 $participants = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
29dc43e7
DM
982
983 // this will hold the information needed to display user names and pictures
5e71cefb
DM
984 $userinfo = array();
985
986 // get the user details for all participants to display
987 foreach ($participants as $participant) {
988 if (!isset($userinfo[$participant->userid])) {
989 $userinfo[$participant->userid] = new stdClass();
990 $userinfo[$participant->userid]->id = $participant->userid;
991 $userinfo[$participant->userid]->firstname = $participant->firstname;
992 $userinfo[$participant->userid]->lastname = $participant->lastname;
993 $userinfo[$participant->userid]->picture = $participant->picture;
994 $userinfo[$participant->userid]->imagealt = $participant->imagealt;
995 }
996 }
29dc43e7 997
5e71cefb 998 // load the submissions details
29dc43e7 999 $submissions = $this->get_submissions(array_keys($participants));
5e71cefb
DM
1000
1001 // get the user details for all moderators (teachers) that have overridden a submission grade
29dc43e7 1002 foreach ($submissions as $submission) {
29dc43e7
DM
1003 if (!isset($userinfo[$submission->gradeoverby])) {
1004 $userinfo[$submission->gradeoverby] = new stdClass();
1005 $userinfo[$submission->gradeoverby]->id = $submission->gradeoverby;
1006 $userinfo[$submission->gradeoverby]->firstname = $submission->overfirstname;
1007 $userinfo[$submission->gradeoverby]->lastname = $submission->overlastname;
1008 $userinfo[$submission->gradeoverby]->picture = $submission->overpicture;
1009 $userinfo[$submission->gradeoverby]->imagealt = $submission->overimagealt;
1010 }
1011 }
1012
5e71cefb 1013 // get the user details for all reviewers of the displayed participants
29dc43e7
DM
1014 $reviewers = array();
1015 if ($submissions) {
1016 list($submissionids, $params) = $DB->get_in_or_equal(array_keys($submissions), SQL_PARAMS_NAMED);
1017 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover,
1018 r.id AS reviewerid, r.lastname, r.firstname, r.picture, r.imagealt,
1019 s.id AS submissionid, s.authorid
1020 FROM {workshop_assessments} a
1021 JOIN {user} r ON (a.reviewerid = r.id)
1022 JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1023 WHERE a.submissionid $submissionids";
1024 $reviewers = $DB->get_records_sql($sql, $params);
1025 foreach ($reviewers as $reviewer) {
1026 if (!isset($userinfo[$reviewer->reviewerid])) {
1027 $userinfo[$reviewer->reviewerid] = new stdClass();
1028 $userinfo[$reviewer->reviewerid]->id = $reviewer->reviewerid;
1029 $userinfo[$reviewer->reviewerid]->firstname = $reviewer->firstname;
1030 $userinfo[$reviewer->reviewerid]->lastname = $reviewer->lastname;
1031 $userinfo[$reviewer->reviewerid]->picture = $reviewer->picture;
1032 $userinfo[$reviewer->reviewerid]->imagealt = $reviewer->imagealt;
1033 }
1034 }
1035 }
1036
5e71cefb 1037 // get the user details for all reviewees of the displayed participants
934329e5
DM
1038 $reviewees = array();
1039 if ($participants) {
1040 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1041 $params['workshopid'] = $this->id;
1042 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover,
1043 u.id AS reviewerid,
1044 s.id AS submissionid,
1045 e.id AS authorid, e.lastname, e.firstname, e.picture, e.imagealt
1046 FROM {user} u
1047 JOIN {workshop_assessments} a ON (a.reviewerid = u.id)
1048 JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1049 JOIN {user} e ON (s.authorid = e.id)
1050 WHERE u.id $participantids AND s.workshopid = :workshopid";
1051 $reviewees = $DB->get_records_sql($sql, $params);
1052 foreach ($reviewees as $reviewee) {
1053 if (!isset($userinfo[$reviewee->authorid])) {
1054 $userinfo[$reviewee->authorid] = new stdClass();
1055 $userinfo[$reviewee->authorid]->id = $reviewee->authorid;
1056 $userinfo[$reviewee->authorid]->firstname = $reviewee->firstname;
1057 $userinfo[$reviewee->authorid]->lastname = $reviewee->lastname;
1058 $userinfo[$reviewee->authorid]->picture = $reviewee->picture;
1059 $userinfo[$reviewee->authorid]->imagealt = $reviewee->imagealt;
1060 }
29dc43e7
DM
1061 }
1062 }
1063
5e71cefb
DM
1064 // finally populate the object to be rendered
1065 $grades = $participants;
29dc43e7
DM
1066
1067 foreach ($participants as $participant) {
1068 // set up default (null) values
5e71cefb
DM
1069 $grades[$participant->userid]->reviewedby = array();
1070 $grades[$participant->userid]->reviewerof = array();
29dc43e7
DM
1071 }
1072 unset($participants);
1073 unset($participant);
1074
1075 foreach ($submissions as $submission) {
1076 $grades[$submission->authorid]->submissionid = $submission->id;
1077 $grades[$submission->authorid]->submissiontitle = $submission->title;
1078 $grades[$submission->authorid]->submissiongrade = $submission->grade;
1079 $grades[$submission->authorid]->submissiongradeover = $submission->gradeover;
1080 $grades[$submission->authorid]->submissiongradeoverby = $submission->gradeoverby;
1081 }
1082 unset($submissions);
1083 unset($submission);
1084
1085 foreach($reviewers as $reviewer) {
1086 $info = new stdClass();
1087 $info->userid = $reviewer->reviewerid;
1088 $info->assessmentid = $reviewer->assessmentid;
1089 $info->submissionid = $reviewer->submissionid;
1090 $info->grade = $reviewer->grade;
1091 $info->gradinggrade = $reviewer->gradinggrade;
1092 $info->gradinggradeover = $reviewer->gradinggradeover;
1093 $grades[$reviewer->authorid]->reviewedby[$reviewer->reviewerid] = $info;
1094 }
1095 unset($reviewers);
1096 unset($reviewer);
1097
1098 foreach($reviewees as $reviewee) {
1099 $info = new stdClass();
1100 $info->userid = $reviewee->authorid;
1101 $info->assessmentid = $reviewee->assessmentid;
1102 $info->submissionid = $reviewee->submissionid;
1103 $info->grade = $reviewee->grade;
1104 $info->gradinggrade = $reviewee->gradinggrade;
1105 $info->gradinggradeover = $reviewee->gradinggradeover;
1106 $grades[$reviewee->reviewerid]->reviewerof[$reviewee->authorid] = $info;
1107 }
1108 unset($reviewees);
1109 unset($reviewee);
1110
29dc43e7
DM
1111 $data = new stdClass();
1112 $data->grades = $grades;
1113 $data->userinfo = $userinfo;
1114 $data->totalcount = $numofparticipants;
1115 return $data;
1116 }
1117
29dc43e7
DM
1118 /**
1119 * Calculate the participant's total grade given the aggregated grades for submission and assessments
1120 *
1121 * todo there will be a setting how to deal with null values (for example no grade for submission) - if
1122 * they are considered as 0 or excluded
1123 *
1124 * @param float|null $grade for submission
1125 * @param float|null $gradinggrade for assessment
1126 * @return float|null
1127 */
1128 public function total_grade($grade=null, $gradinggrade=null) {
1129 if (is_null($grade) and is_null($gradinggrade)) {
1130 return null;
1131 }
1132 return grade_floatval((float)$grade + (float)$gradinggrade);
1133 }
1134
89c1aa97 1135 /**
e9a90e69 1136 * Calculates grades for submission for the given participant(s) and updates it in the database
89c1aa97
DM
1137 *
1138 * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s)
1139 * @return void
1140 */
8a1ba8ac 1141 public function aggregate_submission_grades($restrict=null) {
89c1aa97
DM
1142 global $DB;
1143
1144 // fetch a recordset with all assessments to process
e9a90e69 1145 $sql = 'SELECT s.id AS submissionid, s.grade AS submissiongrade, s.gradeover,
89c1aa97
DM
1146 a.weight, a.grade
1147 FROM {workshop_submissions} s
1148 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id)
1149 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
1150 $params = array('workshopid' => $this->id);
1151
1152 if (is_null($restrict)) {
1153 // update all users - no more conditions
1154 } elseif (!empty($restrict)) {
1155 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1156 $sql .= " AND s.authorid $usql";
1157 $params = array_merge($params, $uparams);
1158 } else {
1159 throw new coding_exception('Empty value is not a valid parameter here');
1160 }
1161
1162 $sql .= ' ORDER BY s.id'; // this is important for bulk processing
89c1aa97 1163
e9a90e69
DM
1164 $rs = $DB->get_recordset_sql($sql, $params);
1165 $batch = array(); // will contain a set of all assessments of a single submission
1166 $previous = null; // a previous record in the recordset
1167
89c1aa97
DM
1168 foreach ($rs as $current) {
1169 if (is_null($previous)) {
1170 // we are processing the very first record in the recordset
1171 $previous = $current;
89c1aa97 1172 }
e9a90e69 1173 if ($current->submissionid == $previous->submissionid) {
89c1aa97 1174 // we are still processing the current submission
e9a90e69
DM
1175 $batch[] = $current;
1176 } else {
1177 // process all the assessments of a sigle submission
1178 $this->aggregate_submission_grades_process($batch);
1179 // and then start to process another submission
1180 $batch = array($current);
1181 $previous = $current;
89c1aa97
DM
1182 }
1183 }
e9a90e69
DM
1184 // do not forget to process the last batch!
1185 $this->aggregate_submission_grades_process($batch);
89c1aa97
DM
1186 $rs->close();
1187 }
1188
1189 /**
1190 * Calculates grades for assessment for the given participant(s)
1191 *
1192 * Grade for submission is calculated as a weighted mean of all given grades
1193 *
1194 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
1195 * @return void
1196 */
8a1ba8ac 1197 public function aggregate_grading_grades($restrict=null) {
89c1aa97
DM
1198 global $DB;
1199
1200 // todo
1201 }
1202
ad6a8f69
DM
1203 /**
1204 * Calculates the workshop total grades for the given participant(s)
1205 *
1206 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
1207 * @return void
1208 */
1209 public function aggregate_total_grades($restrict=null) {
1210 global $DB;
1211
1212 // todo
1213 }
1214
d5506aac
DM
1215 ////////////////////////////////////////////////////////////////////////////
1216 // Helper methods //
1217 ////////////////////////////////////////////////////////////////////////////
1218
1219 /**
1220 * Helper function returning the greatest common divisor
1221 *
1222 * @param int $a
1223 * @param int $b
1224 * @return int
1225 */
1226 public static function gcd($a, $b) {
1227 return ($b == 0) ? ($a):(self::gcd($b, $a % $b));
1228 }
1229
1230 /**
1231 * Helper function returning the least common multiple
1232 *
1233 * @param int $a
1234 * @param int $b
1235 * @return int
1236 */
1237 public static function lcm($a, $b) {
1238 return ($a / self::gcd($a,$b)) * $b;
1239 }
ad6a8f69 1240
aa40adbf
DM
1241 ////////////////////////////////////////////////////////////////////////////////
1242 // Internal methods (implementation details) //
1243 ////////////////////////////////////////////////////////////////////////////////
6516b9e9 1244
e9a90e69
DM
1245 /**
1246 * Given an array of all assessments of a single submission, calculates the final grade for this submission
1247 *
1248 * This calculates the weighted mean of the passed assessment grades. If, however, the submission grade
1249 * was overridden by a teacher, the gradeover value is returned and the rest of grades are ignored.
1250 *
1251 * @param array $assessments of stdClass(->submissionid ->submissiongrade ->gradeover ->weight ->grade)
1252 * @return null|float the aggregated grade rounded to numeric(10,5)
1253 */
1254 protected function aggregate_submission_grades_process(array $assessments) {
1255 global $DB;
1256
1257 $submissionid = null; // the id of the submission being processed
1258 $current = null; // the grade currently saved in database
1259 $finalgrade = null; // the new grade to be calculated
1260 $sumgrades = 0;
1261 $sumweights = 0;
1262
1263 foreach ($assessments as $assessment) {
1264 if (is_null($submissionid)) {
1265 // the id is the same in all records, fetch it during the first loop cycle
1266 $submissionid = $assessment->submissionid;
1267 }
1268 if (is_null($current)) {
1269 // the currently saved grade is the same in all records, fetch it during the first loop cycle
1270 $current = $assessment->submissiongrade;
1271 }
1272 if (!is_null($assessment->gradeover)) {
1273 // the grade for this assessment is overriden by a teacher, no need to calculate anything
1274 $finalgrade = grade_floatval($assessment->gradeover);
1275 break;
1276 }
1277 if (is_null($assessment->grade)) {
1278 // this was not assessed yet
1279 continue;
1280 }
1281 if ($assessment->weight == 0) {
1282 // this does not influence the calculation
1283 continue;
1284 }
1285 $sumgrades += $assessment->grade * $assessment->weight;
1286 $sumweights += $assessment->weight;
1287 }
1288 if ($sumweights > 0 and is_null($finalgrade)) {
1289 $finalgrade = grade_floatval($sumgrades / $sumweights);
1290 }
1291 // check if the new final grade differs from the one stored in the database
1292 if (grade_floats_different($finalgrade, $current)) {
1293 // we need to save new calculation into the database
1294 $DB->set_field('workshop_submissions', 'grade', $finalgrade, array('id' => $submissionid));
1295 }
1296 }
1297
6516b9e9 1298 /**
aa40adbf 1299 * Given a list of user ids, returns the filtered one containing just ids of users with own submission
6516b9e9 1300 *
aa40adbf
DM
1301 * Example submissions are ignored.
1302 *
1303 * @param array $userids
6516b9e9
DM
1304 * @return array
1305 */
aa40adbf
DM
1306 protected function users_with_submission(array $userids) {
1307 global $DB;
1308
1309 if (empty($userids)) {
1310 return array();
1311 }
1312 $userswithsubmission = array();
1313 list($usql, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
00aca3c1 1314 $sql = "SELECT id,authorid
aa40adbf 1315 FROM {workshop_submissions}
00aca3c1 1316 WHERE example = 0 AND workshopid = :workshopid AND authorid $usql";
aa40adbf
DM
1317 $params = array('workshopid' => $this->id);
1318 $params = array_merge($params, $uparams);
1319 $submissions = $DB->get_records_sql($sql, $params);
1320 foreach ($submissions as $submission) {
00aca3c1 1321 $userswithsubmission[$submission->authorid] = true;
aa40adbf
DM
1322 }
1323
1324 return $userswithsubmission;
6516b9e9
DM
1325 }
1326
aa40adbf
DM
1327 /**
1328 * @return array of available workshop phases
1329 */
1330 protected function available_phases() {
1331 return array(
1332 self::PHASE_SETUP => true,
1333 self::PHASE_SUBMISSION => true,
1334 self::PHASE_ASSESSMENT => true,
1335 self::PHASE_EVALUATION => true,
1336 self::PHASE_CLOSED => true,
1337 );
1338 }
1339
66c9894d 1340}