MDL-19930 workshop: comments grading strategy
[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();
e554671d
DM
432 $assessment->submissionid = $submission->id;
433 $assessment->reviewerid = $reviewerid;
434 $assessment->timecreated = $now;
435 $assessment->timemodified = $now;
436 $assessment->weight = 1;
437 $assessment->generalcommentformat = FORMAT_HTML; // todo better default handling
438 $assessment->feedbackreviewerformat = FORMAT_HTML; // todo better default handling
6e309973 439
235b31c8 440 return $DB->insert_record('workshop_assessments', $assessment, true, $bulk);
6e309973
DM
441 }
442
6e309973 443 /**
53fad4b9 444 * Delete assessment record or records
6e309973 445 *
53fad4b9
DM
446 * @param mixed $id int|array assessment id or array of assessments ids
447 * @return bool false if $id not a valid parameter, true otherwise
6e309973
DM
448 */
449 public function delete_assessment($id) {
450 global $DB;
451
452 // todo remove all given grades from workshop_grades;
6e309973 453
53fad4b9 454 if (is_array($id)) {
235b31c8 455 return $DB->delete_records_list('workshop_assessments', 'id', $id);
3d2924e9 456 } else {
235b31c8 457 return $DB->delete_records('workshop_assessments', array('id' => $id));
53fad4b9 458 }
53fad4b9 459 }
6e309973
DM
460
461 /**
462 * Returns instance of grading strategy class
53fad4b9 463 *
65ba104c 464 * @return stdClass Instance of a grading strategy
6e309973
DM
465 */
466 public function grading_strategy_instance() {
3d2924e9
DM
467 global $CFG; // because we require other libs here
468
3fd2b0e1 469 if (is_null($this->strategyinstance)) {
f05c168d 470 $strategylib = dirname(__FILE__) . '/form/' . $this->strategy . '/lib.php';
6e309973
DM
471 if (is_readable($strategylib)) {
472 require_once($strategylib);
473 } else {
f05c168d 474 throw new coding_exception('the grading forms subplugin must contain library ' . $strategylib);
6e309973 475 }
0dc47fb9 476 $classname = 'workshop_' . $this->strategy . '_strategy';
3fd2b0e1
DM
477 $this->strategyinstance = new $classname($this);
478 if (!in_array('workshop_strategy', class_implements($this->strategyinstance))) {
b761e6d9 479 throw new coding_exception($classname . ' does not implement workshop_strategy interface');
6e309973
DM
480 }
481 }
3fd2b0e1 482 return $this->strategyinstance;
6e309973
DM
483 }
484
45d24d39
DM
485 /**
486 * Returns instance of grading evaluation class
487 *
488 * @return stdClass Instance of a grading evaluation
489 */
490 public function grading_evaluation_instance() {
491 global $CFG; // because we require other libs here
492
493 if (is_null($this->evaluationinstance)) {
494 $evaluationlib = dirname(__FILE__) . '/eval/' . $this->evaluation . '/lib.php';
495 if (is_readable($evaluationlib)) {
496 require_once($evaluationlib);
497 } else {
498 throw new coding_exception('the grading evaluation subplugin must contain library ' . $evaluationlib);
499 }
500 $classname = 'workshop_' . $this->evaluation . '_evaluation';
501 $this->evaluationinstance = new $classname($this);
502 if (!in_array('workshop_evaluation', class_implements($this->evaluationinstance))) {
503 throw new coding_exception($classname . ' does not implement workshop_evaluation interface');
504 }
505 }
506 return $this->evaluationinstance;
507 }
508
66c9894d
DM
509 /**
510 * Returns instance of submissions allocator
53fad4b9 511 *
130ae619 512 * @param string $method The name of the allocation method, must be PARAM_ALPHA
65ba104c 513 * @return stdClass Instance of submissions allocator
66c9894d
DM
514 */
515 public function allocator_instance($method) {
3d2924e9
DM
516 global $CFG; // because we require other libs here
517
f05c168d 518 $allocationlib = dirname(__FILE__) . '/allocation/' . $method . '/lib.php';
66c9894d
DM
519 if (is_readable($allocationlib)) {
520 require_once($allocationlib);
521 } else {
f05c168d 522 throw new coding_exception('Unable to find the allocation library ' . $allocationlib);
66c9894d
DM
523 }
524 $classname = 'workshop_' . $method . '_allocator';
525 return new $classname($this);
526 }
527
b8ead2e6 528 /**
454e8dd9 529 * @return moodle_url of this workshop's view page
b8ead2e6
DM
530 */
531 public function view_url() {
532 global $CFG;
533 return new moodle_url($CFG->wwwroot . '/mod/workshop/view.php', array('id' => $this->cm->id));
534 }
535
536 /**
454e8dd9 537 * @return moodle_url of the page for editing this workshop's grading form
b8ead2e6
DM
538 */
539 public function editform_url() {
540 global $CFG;
541 return new moodle_url($CFG->wwwroot . '/mod/workshop/editform.php', array('cmid' => $this->cm->id));
542 }
543
544 /**
454e8dd9 545 * @return moodle_url of the page for previewing this workshop's grading form
b8ead2e6
DM
546 */
547 public function previewform_url() {
548 global $CFG;
43b34576 549 return new moodle_url($CFG->wwwroot . '/mod/workshop/editformpreview.php', array('cmid' => $this->cm->id));
b8ead2e6
DM
550 }
551
552 /**
553 * @param int $assessmentid The ID of assessment record
454e8dd9 554 * @return moodle_url of the assessment page
b8ead2e6 555 */
a39d7d87 556 public function assess_url($assessmentid) {
b8ead2e6 557 global $CFG;
454e8dd9 558 $assessmentid = clean_param($assessmentid, PARAM_INT);
a39d7d87 559 return new moodle_url($CFG->wwwroot . '/mod/workshop/assessment.php', array('asid' => $assessmentid));
b8ead2e6
DM
560 }
561
39861053 562 /**
454e8dd9 563 * @return moodle_url of the page to view own submission
39861053
DM
564 */
565 public function submission_url() {
566 global $CFG;
567 return new moodle_url($CFG->wwwroot . '/mod/workshop/submission.php', array('cmid' => $this->cm->id));
568 }
569
da0b1f70 570 /**
454e8dd9 571 * @return moodle_url of the mod_edit form
da0b1f70
DM
572 */
573 public function updatemod_url() {
574 global $CFG;
575 return new moodle_url($CFG->wwwroot . '/course/modedit.php', array('update' => $this->cm->id, 'return' => 1));
576 }
577
454e8dd9
DM
578 /**
579 * @return moodle_url to the allocation page
580 */
da0b1f70
DM
581 public function allocation_url() {
582 global $CFG;
583 return new moodle_url($CFG->wwwroot . '/mod/workshop/allocation.php', array('cmid' => $this->cm->id));
584 }
585
454e8dd9
DM
586 /**
587 * @param int $phasecode The internal phase code
588 * @return moodle_url of the script to change the current phase to $phasecode
589 */
590 public function switchphase_url($phasecode) {
591 global $CFG;
592 $phasecode = clean_param($phasecode, PARAM_INT);
593 return new moodle_url($CFG->wwwroot . '/mod/workshop/switchphase.php', array('cmid' => $this->cm->id, 'phase' => $phasecode));
594 }
595
89c1aa97
DM
596 /**
597 * @return moodle_url to the aggregation page
598 */
599 public function aggregate_url() {
600 global $CFG;
601 return new moodle_url($CFG->wwwroot . '/mod/workshop/aggregate.php', array('cmid' => $this->cm->id));
602 }
603
b8ead2e6 604 /**
407b1e91
DM
605 * Are users allowed to create/edit their submissions?
606 *
607 * TODO: this depends on the workshop phase, phase deadlines, submitting after deadlines possibility
b8ead2e6 608 *
407b1e91 609 * @return bool
b8ead2e6 610 */
407b1e91
DM
611 public function submitting_allowed() {
612 return true;
b8ead2e6
DM
613 }
614
c1e883bb 615 /**
407b1e91 616 * Are reviewers allowed to create/edit their assessments?
c1e883bb 617 *
407b1e91 618 * TODO: this depends on the workshop phase, phase deadlines
c1e883bb
DM
619 *
620 * @return bool
621 */
407b1e91 622 public function assessing_allowed() {
c1e883bb
DM
623 return true;
624 }
625
407b1e91 626
3dc78e5b
DM
627 /**
628 * Are the peer-reviews available to the authors?
629 *
630 * TODO: this depends on the workshop phase
631 *
632 * @return bool
633 */
634 public function assessments_available() {
635 return true;
636 }
637
638 /**
639 * Can the given grades be displayed to the authors?
640 *
641 * Grades are not displayed if {@link self::assessments_available()} return false. The returned
642 * value may be true (if yes, display grades), false (no, hide grades yet) or null (only
643 * display grades if the assessment has been agreed by the author).
644 *
645 * @return bool|null
646 */
647 public function grades_available() {
648 return true;
649 }
650
b13142da
DM
651 /**
652 * Returns the localized name of the grading strategy method to be displayed to the users
653 *
654 * @return string
655 */
656 public function strategy_name() {
f05c168d 657 return get_string('pluginname', 'workshopform_' . $this->strategy);
b13142da 658 }
b761e6d9
DM
659
660 /**
661 * Prepare an individual workshop plan for the given user.
662 *
f05c168d
DM
663 * @param int $userid whom the plan is prepared for
664 * @param stdClass context of the planned workshop
665 * @return stdClass data object to be passed to the renderer
b761e6d9 666 */
d895c6aa 667 public function prepare_user_plan($userid) {
b761e6d9
DM
668 global $DB;
669
670 $phases = array();
671
672 // Prepare tasks for the setup phase
673 $phase = new stdClass();
674 $phase->title = get_string('phasesetup', 'workshop');
675 $phase->tasks = array();
d895c6aa 676 if (has_capability('moodle/course:manageactivities', $this->context, $userid)) {
da0b1f70
DM
677 $task = new stdClass();
678 $task->title = get_string('taskintro', 'workshop');
679 $task->link = $this->updatemod_url();
680 $task->completed = !(trim(strip_tags($this->intro)) == '');
681 $phase->tasks['intro'] = $task;
682 }
d895c6aa 683 if (has_capability('moodle/course:manageactivities', $this->context, $userid)) {
454e8dd9
DM
684 $task = new stdClass();
685 $task->title = get_string('taskinstructauthors', 'workshop');
686 $task->link = $this->updatemod_url();
687 $task->completed = !(trim(strip_tags($this->instructauthors)) == '');
688 $phase->tasks['instructauthors'] = $task;
689 }
d895c6aa 690 if (has_capability('mod/workshop:editdimensions', $this->context, $userid)) {
b761e6d9 691 $task = new stdClass();
da0b1f70
DM
692 $task->title = get_string('editassessmentform', 'workshop');
693 $task->link = $this->editform_url();
694 if ($this->assessment_form_ready()) {
695 $task->completed = true;
696 } elseif ($this->phase > self::PHASE_SETUP) {
697 $task->completed = false;
698 }
b761e6d9
DM
699 $phase->tasks['editform'] = $task;
700 }
da0b1f70
DM
701 if (empty($phase->tasks) and $this->phase == self::PHASE_SETUP) {
702 // if we are in the setup phase and there is no task (typical for students), let us
703 // display some explanation what is going on
704 $task = new stdClass();
705 $task->title = get_string('undersetup', 'workshop');
706 $task->completed = 'info';
707 $phase->tasks['setupinfo'] = $task;
708 }
b761e6d9
DM
709 $phases[self::PHASE_SETUP] = $phase;
710
711 // Prepare tasks for the submission phase
712 $phase = new stdClass();
713 $phase->title = get_string('phasesubmission', 'workshop');
714 $phase->tasks = array();
d895c6aa 715 if (has_capability('moodle/course:manageactivities', $this->context, $userid)) {
b761e6d9 716 $task = new stdClass();
00aca3c1
DM
717 $task->title = get_string('taskinstructreviewers', 'workshop');
718 $task->link = $this->updatemod_url();
719 if (trim(strip_tags($this->instructreviewers))) {
da0b1f70
DM
720 $task->completed = true;
721 } elseif ($this->phase >= self::PHASE_ASSESSMENT) {
722 $task->completed = false;
da0b1f70 723 }
00aca3c1 724 $phase->tasks['instructreviewers'] = $task;
b761e6d9 725 }
d895c6aa 726 if (has_capability('mod/workshop:submit', $this->context, $userid, false)) {
da0b1f70 727 $task = new stdClass();
00aca3c1
DM
728 $task->title = get_string('tasksubmit', 'workshop');
729 $task->link = $this->submission_url();
730 if ($DB->record_exists('workshop_submissions', array('workshopid'=>$this->id, 'example'=>0, 'authorid'=>$userid))) {
da0b1f70
DM
731 $task->completed = true;
732 } elseif ($this->phase >= self::PHASE_ASSESSMENT) {
733 $task->completed = false;
00aca3c1
DM
734 } else {
735 $task->completed = null; // still has a chance to submit
da0b1f70 736 }
00aca3c1 737 $phase->tasks['submit'] = $task;
da0b1f70 738 }
b761e6d9 739 $phases[self::PHASE_SUBMISSION] = $phase;
d895c6aa 740 if (has_capability('mod/workshop:allocate', $this->context, $userid)) {
da0b1f70
DM
741 $task = new stdClass();
742 $task->title = get_string('allocate', 'workshop');
743 $task->link = $this->allocation_url();
d895c6aa 744 $numofauthors = count(get_users_by_capability($this->context, 'mod/workshop:submit', 'u.id', '', '', '',
a3610b08
DM
745 '', '', false, true));
746 $numofsubmissions = $DB->count_records('workshop_submissions', array('workshopid'=>$this->id, 'example'=>0));
747 $sql = 'SELECT COUNT(s.id) AS nonallocated
748 FROM {workshop_submissions} s
749 LEFT JOIN {workshop_assessments} a ON (a.submissionid=s.id)
750 WHERE s.workshopid = :workshopid AND s.example=0 AND a.submissionid IS NULL';
751 $params['workshopid'] = $this->id;
752 $numnonallocated = $DB->count_records_sql($sql, $params);
da0b1f70
DM
753 if ($numofsubmissions == 0) {
754 $task->completed = null;
a3610b08 755 } elseif ($numnonallocated == 0) {
da0b1f70
DM
756 $task->completed = true;
757 } elseif ($this->phase > self::PHASE_SUBMISSION) {
758 $task->completed = false;
759 } else {
760 $task->completed = null; // still has a chance to allocate
761 }
762 $a = new stdClass();
3189fb2d
DM
763 $a->expected = $numofauthors;
764 $a->submitted = $numofsubmissions;
a3610b08 765 $a->allocate = $numnonallocated;
3189fb2d 766 $task->details = get_string('allocatedetails', 'workshop', $a);
da0b1f70 767 unset($a);
3189fb2d 768 $phase->tasks['allocate'] = $task;
3dc78e5b
DM
769
770 if ($numofsubmissions < $numofauthors and $this->phase >= self::PHASE_SUBMISSION) {
771 $task = new stdClass();
772 $task->title = get_string('someuserswosubmission', 'workshop');
773 $task->completed = 'info';
774 $phase->tasks['allocateinfo'] = $task;
775 }
da0b1f70 776 }
b761e6d9
DM
777
778 // Prepare tasks for the peer-assessment phase (includes eventual self-assessments)
779 $phase = new stdClass();
780 $phase->title = get_string('phaseassessment', 'workshop');
781 $phase->tasks = array();
d895c6aa 782 $phase->isreviewer = has_capability('mod/workshop:peerassess', $this->context, $userid);
3dc78e5b 783 $phase->assessments = $this->get_assessments_by_reviewer($userid);
b761e6d9
DM
784 $numofpeers = 0; // number of allocated peer-assessments
785 $numofpeerstodo = 0; // number of peer-assessments to do
786 $numofself = 0; // number of allocated self-assessments - should be 0 or 1
787 $numofselftodo = 0; // number of self-assessments to do - should be 0 or 1
788 foreach ($phase->assessments as $a) {
789 if ($a->authorid == $userid) {
790 $numofself++;
791 if (is_null($a->grade)) {
792 $numofselftodo++;
793 }
794 } else {
795 $numofpeers++;
796 if (is_null($a->grade)) {
797 $numofpeerstodo++;
798 }
799 }
800 }
801 unset($a);
802 if ($numofpeers) {
803 $task = new stdClass();
3dc78e5b
DM
804 if ($numofpeerstodo == 0) {
805 $task->completed = true;
806 } elseif ($this->phase > self::PHASE_ASSESSMENT) {
807 $task->completed = false;
808 }
b761e6d9
DM
809 $a = new stdClass();
810 $a->total = $numofpeers;
811 $a->todo = $numofpeerstodo;
812 $task->title = get_string('taskassesspeers', 'workshop');
da0b1f70 813 $task->details = get_string('taskassesspeersdetails', 'workshop', $a);
b761e6d9
DM
814 unset($a);
815 $phase->tasks['assesspeers'] = $task;
816 }
817 if ($numofself) {
818 $task = new stdClass();
3dc78e5b
DM
819 if ($numofselftodo == 0) {
820 $task->completed = true;
821 } elseif ($this->phase > self::PHASE_ASSESSMENT) {
822 $task->completed = false;
823 }
b761e6d9
DM
824 $task->title = get_string('taskassessself', 'workshop');
825 $phase->tasks['assessself'] = $task;
826 }
827 $phases[self::PHASE_ASSESSMENT] = $phase;
828
1fed6ce3 829 // Prepare tasks for the grading evaluation phase
b761e6d9
DM
830 $phase = new stdClass();
831 $phase->title = get_string('phaseevaluation', 'workshop');
832 $phase->tasks = array();
1fed6ce3
DM
833 if (has_capability('mod/workshop:overridegrades', $this->context)) {
834 $authors = $this->get_potential_authors(false);
835 $reviewers = $this->get_potential_reviewers(false);
836 $expected = count($authors + $reviewers);
837 unset($authors);
838 unset($reviewers);
839 $known = $DB->count_records_select('workshop_aggregations', 'workshopid = ? AND totalgrade IS NOT NULL',
840 array($this->id));
841 $task = new stdClass();
842 $task->title = get_string('calculatetotalgrades', 'workshop');
843 $a = new stdClass();
844 $a->expected = $expected;
845 $a->known = $known;
846 $task->details = get_string('calculatetotalgradesdetails', 'workshop', $a);
847 if ($known >= $expected) {
848 $task->completed = true;
849 } elseif ($this->phase > self::PHASE_EVALUATION) {
850 $task->completed = false;
851 }
f55650e6
DM
852 $phase->tasks['calculatetotalgrade'] = $task;
853 if ($known > 0 and $known < $expected) {
854 $task = new stdClass();
855 $task->title = get_string('totalgradesmissing', 'workshop');
856 $task->completed = 'info';
857 $phase->tasks['totalgradesmissinginfo'] = $task;
858 }
1fed6ce3
DM
859 } else {
860 $task = new stdClass();
861 $task->title = get_string('evaluategradeswait', 'workshop');
862 $task->completed = 'info';
863 $phase->tasks['evaluateinfo'] = $task;
864 }
b761e6d9
DM
865 $phases[self::PHASE_EVALUATION] = $phase;
866
867 // Prepare tasks for the "workshop closed" phase - todo
868 $phase = new stdClass();
869 $phase->title = get_string('phaseclosed', 'workshop');
870 $phase->tasks = array();
871 $phases[self::PHASE_CLOSED] = $phase;
872
873 // Polish data, set default values if not done explicitly
874 foreach ($phases as $phasecode => $phase) {
875 $phase->title = isset($phase->title) ? $phase->title : '';
876 $phase->tasks = isset($phase->tasks) ? $phase->tasks : array();
877 if ($phasecode == $this->phase) {
878 $phase->active = true;
879 } else {
880 $phase->active = false;
881 }
454e8dd9
DM
882 if (!isset($phase->actions)) {
883 $phase->actions = array();
884 }
b761e6d9
DM
885
886 foreach ($phase->tasks as $taskcode => $task) {
887 $task->title = isset($task->title) ? $task->title : '';
da0b1f70
DM
888 $task->link = isset($task->link) ? $task->link : null;
889 $task->details = isset($task->details) ? $task->details : '';
b761e6d9
DM
890 $task->completed = isset($task->completed) ? $task->completed : null;
891 }
892 }
454e8dd9
DM
893
894 // Add phase swithing actions
d895c6aa 895 if (has_capability('mod/workshop:switchphase', $this->context, $userid)) {
454e8dd9
DM
896 foreach ($phases as $phasecode => $phase) {
897 if (! $phase->active) {
898 $action = new stdClass();
899 $action->type = 'switchphase';
900 $action->url = $this->switchphase_url($phasecode);
901 $phase->actions[] = $action;
902 }
903 }
904 }
905
b761e6d9
DM
906 return $phases;
907 }
908
909 /**
910 * Has the assessment form been defined?
911 *
912 * @return bool
913 */
914 public function assessment_form_ready() {
915 return $this->grading_strategy_instance()->form_ready();
916 }
917
454e8dd9
DM
918 /**
919 * Switch to a new workshop phase
920 *
921 * Modifies the underlying database record. You should terminate the script shortly after calling this.
922 *
923 * @param int $newphase new phase code
924 * @return bool true if success, false otherwise
925 */
926 public function switch_phase($newphase) {
927 global $DB;
928
929 $known = $this->available_phases();
930 if (!isset($known[$newphase])) {
931 return false;
932 }
933 $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id));
934 return true;
935 }
ddb59c77
DM
936
937 /**
938 * Saves a raw grade for submission as calculated from the assessment form fields
939 *
940 * @param array $assessmentid assessment record id, must exists
00aca3c1 941 * @param mixed $grade raw percentual grade from 0.00000 to 100.00000
ddb59c77
DM
942 * @return false|float the saved grade
943 */
944 public function set_peer_grade($assessmentid, $grade) {
945 global $DB;
946
947 if (is_null($grade)) {
948 return false;
949 }
950 $data = new stdClass();
951 $data->id = $assessmentid;
952 $data->grade = $grade;
953 $DB->update_record('workshop_assessments', $data);
954 return $grade;
955 }
6516b9e9 956
29dc43e7
DM
957 /**
958 * Prepares data object with all workshop grades to be rendered
959 *
5e71cefb
DM
960 * @param int $userid the user we are preparing the report for
961 * @param mixed $groups single group or array of groups - only show users who are in one of these group(s). Defaults to all
29dc43e7 962 * @param int $page the current page (for the pagination)
5e71cefb
DM
963 * @param int $perpage participants per page (for the pagination)
964 * @param string $sortby lastname|firstname|submissiontitle|submissiongrade|gradinggrade|totalgrade
965 * @param string $sorthow ASC|DESC
29dc43e7
DM
966 * @return stdClass data for the renderer
967 */
d895c6aa 968 public function prepare_grading_report($userid, $groups, $page, $perpage, $sortby, $sorthow) {
29dc43e7
DM
969 global $DB;
970
d895c6aa
DM
971 $canviewall = has_capability('mod/workshop:viewallassessments', $this->context, $userid);
972 $isparticipant = has_any_capability(array('mod/workshop:submit', 'mod/workshop:peerassess'), $this->context, $userid);
29dc43e7
DM
973
974 if (!$canviewall and !$isparticipant) {
975 // who the hell is this?
976 return array();
977 }
978
5e71cefb
DM
979 if (!in_array($sortby, array('lastname','firstname','submissiontitle','submissiongrade','gradinggrade','totalgrade'))) {
980 $sortby = 'lastname';
981 }
982
983 if (!($sorthow === 'ASC' or $sorthow === 'DESC')) {
984 $sorthow = 'ASC';
985 }
986
987 // get the list of user ids to be displayed
29dc43e7
DM
988 if ($canviewall) {
989 // fetch the list of ids of all workshop participants - this may get really long so fetch just id
d895c6aa 990 $participants = get_users_by_capability($this->context, array('mod/workshop:submit', 'mod/workshop:peerassess'),
5e71cefb 991 'u.id', '', '', '', $groups, '', false, false, true);
29dc43e7
DM
992 } else {
993 // this is an ordinary workshop participant (aka student) - display the report just for him/her
994 $participants = array($userid => (object)array('id' => $userid));
995 }
996
5e71cefb 997 // we will need to know the number of all records later for the pagination purposes
29dc43e7
DM
998 $numofparticipants = count($participants);
999
5e71cefb
DM
1000 // load all fields which can be used sorting and paginate the records
1001 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1002 $params['workshopid'] = $this->id;
1003 $sqlsort = $sortby . ' ' . $sorthow . ',u.lastname,u.firstname,u.id';
1004 $sql = "SELECT u.id AS userid,u.firstname,u.lastname,u.picture,u.imagealt,
89c1aa97 1005 s.title AS submissiontitle, s.grade AS submissiongrade, ag.gradinggrade, ag.totalgrade
5e71cefb
DM
1006 FROM {user} u
1007 LEFT JOIN {workshop_submissions} s ON (s.authorid = u.id)
89c1aa97 1008 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = u.id AND ag.workshopid = s.workshopid)
5e71cefb
DM
1009 WHERE s.workshopid = :workshopid AND s.example = 0 AND u.id $participantids
1010 ORDER BY $sqlsort";
1011 $participants = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
29dc43e7
DM
1012
1013 // this will hold the information needed to display user names and pictures
5e71cefb
DM
1014 $userinfo = array();
1015
1016 // get the user details for all participants to display
1017 foreach ($participants as $participant) {
1018 if (!isset($userinfo[$participant->userid])) {
1019 $userinfo[$participant->userid] = new stdClass();
1020 $userinfo[$participant->userid]->id = $participant->userid;
1021 $userinfo[$participant->userid]->firstname = $participant->firstname;
1022 $userinfo[$participant->userid]->lastname = $participant->lastname;
1023 $userinfo[$participant->userid]->picture = $participant->picture;
1024 $userinfo[$participant->userid]->imagealt = $participant->imagealt;
1025 }
1026 }
29dc43e7 1027
5e71cefb 1028 // load the submissions details
29dc43e7 1029 $submissions = $this->get_submissions(array_keys($participants));
5e71cefb
DM
1030
1031 // get the user details for all moderators (teachers) that have overridden a submission grade
29dc43e7 1032 foreach ($submissions as $submission) {
29dc43e7
DM
1033 if (!isset($userinfo[$submission->gradeoverby])) {
1034 $userinfo[$submission->gradeoverby] = new stdClass();
1035 $userinfo[$submission->gradeoverby]->id = $submission->gradeoverby;
1036 $userinfo[$submission->gradeoverby]->firstname = $submission->overfirstname;
1037 $userinfo[$submission->gradeoverby]->lastname = $submission->overlastname;
1038 $userinfo[$submission->gradeoverby]->picture = $submission->overpicture;
1039 $userinfo[$submission->gradeoverby]->imagealt = $submission->overimagealt;
1040 }
1041 }
1042
5e71cefb 1043 // get the user details for all reviewers of the displayed participants
29dc43e7
DM
1044 $reviewers = array();
1045 if ($submissions) {
1046 list($submissionids, $params) = $DB->get_in_or_equal(array_keys($submissions), SQL_PARAMS_NAMED);
1047 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover,
1048 r.id AS reviewerid, r.lastname, r.firstname, r.picture, r.imagealt,
1049 s.id AS submissionid, s.authorid
1050 FROM {workshop_assessments} a
1051 JOIN {user} r ON (a.reviewerid = r.id)
1052 JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1053 WHERE a.submissionid $submissionids";
1054 $reviewers = $DB->get_records_sql($sql, $params);
1055 foreach ($reviewers as $reviewer) {
1056 if (!isset($userinfo[$reviewer->reviewerid])) {
1057 $userinfo[$reviewer->reviewerid] = new stdClass();
1058 $userinfo[$reviewer->reviewerid]->id = $reviewer->reviewerid;
1059 $userinfo[$reviewer->reviewerid]->firstname = $reviewer->firstname;
1060 $userinfo[$reviewer->reviewerid]->lastname = $reviewer->lastname;
1061 $userinfo[$reviewer->reviewerid]->picture = $reviewer->picture;
1062 $userinfo[$reviewer->reviewerid]->imagealt = $reviewer->imagealt;
1063 }
1064 }
1065 }
1066
5e71cefb 1067 // get the user details for all reviewees of the displayed participants
934329e5
DM
1068 $reviewees = array();
1069 if ($participants) {
1070 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1071 $params['workshopid'] = $this->id;
1072 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover,
1073 u.id AS reviewerid,
1074 s.id AS submissionid,
1075 e.id AS authorid, e.lastname, e.firstname, e.picture, e.imagealt
1076 FROM {user} u
1077 JOIN {workshop_assessments} a ON (a.reviewerid = u.id)
1078 JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1079 JOIN {user} e ON (s.authorid = e.id)
1080 WHERE u.id $participantids AND s.workshopid = :workshopid";
1081 $reviewees = $DB->get_records_sql($sql, $params);
1082 foreach ($reviewees as $reviewee) {
1083 if (!isset($userinfo[$reviewee->authorid])) {
1084 $userinfo[$reviewee->authorid] = new stdClass();
1085 $userinfo[$reviewee->authorid]->id = $reviewee->authorid;
1086 $userinfo[$reviewee->authorid]->firstname = $reviewee->firstname;
1087 $userinfo[$reviewee->authorid]->lastname = $reviewee->lastname;
1088 $userinfo[$reviewee->authorid]->picture = $reviewee->picture;
1089 $userinfo[$reviewee->authorid]->imagealt = $reviewee->imagealt;
1090 }
29dc43e7
DM
1091 }
1092 }
1093
5e71cefb
DM
1094 // finally populate the object to be rendered
1095 $grades = $participants;
29dc43e7
DM
1096
1097 foreach ($participants as $participant) {
1098 // set up default (null) values
5e71cefb
DM
1099 $grades[$participant->userid]->reviewedby = array();
1100 $grades[$participant->userid]->reviewerof = array();
29dc43e7
DM
1101 }
1102 unset($participants);
1103 unset($participant);
1104
1105 foreach ($submissions as $submission) {
1106 $grades[$submission->authorid]->submissionid = $submission->id;
1107 $grades[$submission->authorid]->submissiontitle = $submission->title;
b4857acb
DM
1108 $grades[$submission->authorid]->submissiongrade = $this->real_grade($submission->grade);
1109 $grades[$submission->authorid]->submissiongradeover = $this->real_grade($submission->gradeover);
29dc43e7
DM
1110 $grades[$submission->authorid]->submissiongradeoverby = $submission->gradeoverby;
1111 }
1112 unset($submissions);
1113 unset($submission);
1114
1115 foreach($reviewers as $reviewer) {
1116 $info = new stdClass();
1117 $info->userid = $reviewer->reviewerid;
1118 $info->assessmentid = $reviewer->assessmentid;
1119 $info->submissionid = $reviewer->submissionid;
b4857acb
DM
1120 $info->grade = $this->real_grade($reviewer->grade);
1121 $info->gradinggrade = $this->real_grading_grade($reviewer->gradinggrade);
1122 $info->gradinggradeover = $this->real_grading_grade($reviewer->gradinggradeover);
29dc43e7
DM
1123 $grades[$reviewer->authorid]->reviewedby[$reviewer->reviewerid] = $info;
1124 }
1125 unset($reviewers);
1126 unset($reviewer);
1127
1128 foreach($reviewees as $reviewee) {
1129 $info = new stdClass();
1130 $info->userid = $reviewee->authorid;
1131 $info->assessmentid = $reviewee->assessmentid;
1132 $info->submissionid = $reviewee->submissionid;
b4857acb
DM
1133 $info->grade = $this->real_grade($reviewee->grade);
1134 $info->gradinggrade = $this->real_grading_grade($reviewee->gradinggrade);
1135 $info->gradinggradeover = $this->real_grading_grade($reviewee->gradinggradeover);
29dc43e7
DM
1136 $grades[$reviewee->reviewerid]->reviewerof[$reviewee->authorid] = $info;
1137 }
1138 unset($reviewees);
1139 unset($reviewee);
1140
b4857acb
DM
1141 foreach ($grades as $grade) {
1142 $grade->gradinggrade = $this->real_grading_grade($grade->gradinggrade);
1143 $grade->totalgrade = $this->format_total_grade($grade->totalgrade);
1144 }
1145
29dc43e7
DM
1146 $data = new stdClass();
1147 $data->grades = $grades;
1148 $data->userinfo = $userinfo;
1149 $data->totalcount = $numofparticipants;
b4857acb
DM
1150 $data->maxgrade = $this->real_grade(100);
1151 $data->maxgradinggrade = $this->real_grading_grade(100);
1fed6ce3 1152 $data->maxtotalgrade = $this->format_total_grade($data->maxgrade + $data->maxgradinggrade);
29dc43e7
DM
1153 return $data;
1154 }
1155
29dc43e7 1156 /**
b4857acb 1157 * Calculates the real value of a grade
29dc43e7 1158 *
b4857acb
DM
1159 * @param float $value percentual value from 0 to 100
1160 * @param float $max the maximal grade
1161 * @return string
1162 */
1163 public function real_grade_value($value, $max) {
1164 $localized = true;
1165 if (is_null($value)) {
1166 return null;
1167 } elseif ($max == 0) {
1168 return 0;
1169 } else {
1170 return format_float($max * $value / 100, $this->gradedecimals, $localized);
1171 }
1172 }
1173
e554671d
DM
1174 /**
1175 * Calculates the raw (percentual) value from a real grade
1176 *
1177 * This is used in cases when a user wants to give a grade such as 12 of 20 and we need to save
1178 * this value in a raw percentual form into DB
1179 * @param float $value given grade
1180 * @param float $max the maximal grade
1181 * @return float suitable to be stored as numeric(10,5)
1182 */
1183 public function raw_grade_value($value, $max) {
1184 if (empty($value)) {
1185 return null;
1186 }
1187 if ($max == 0 or $value < 0) {
1188 return 0;
1189 }
1190 $p = $value / $max * 100;
1191 if ($p > 100) {
1192 return $max;
1193 }
1194 return grade_floatval($p);
1195 }
1196
b4857acb
DM
1197 /**
1198 * Rounds the value from DB to be displayed
29dc43e7 1199 *
b4857acb
DM
1200 * @param float $raw value from {workshop_aggregations}
1201 * @return string
29dc43e7 1202 */
b4857acb
DM
1203 public function format_total_grade($raw) {
1204 if (is_null($raw)) {
29dc43e7
DM
1205 return null;
1206 }
1fed6ce3 1207 return format_float($raw, $this->gradedecimals, true);
b4857acb
DM
1208 }
1209
1210 /**
1211 * Calculates the real value of grade for submission
1212 *
1213 * @param float $value percentual value from 0 to 100
1214 * @return string
1215 */
1216 public function real_grade($value) {
1217 return $this->real_grade_value($value, $this->grade);
1218 }
1219
1220 /**
1221 * Calculates the real value of grade for assessment
1222 *
1223 * @param float $value percentual value from 0 to 100
1224 * @return string
1225 */
1226 public function real_grading_grade($value) {
1227 return $this->real_grade_value($value, $this->gradinggrade);
29dc43e7
DM
1228 }
1229
89c1aa97 1230 /**
e9a90e69 1231 * Calculates grades for submission for the given participant(s) and updates it in the database
89c1aa97
DM
1232 *
1233 * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s)
1234 * @return void
1235 */
8a1ba8ac 1236 public function aggregate_submission_grades($restrict=null) {
89c1aa97
DM
1237 global $DB;
1238
1239 // fetch a recordset with all assessments to process
1696f36c 1240 $sql = 'SELECT s.id AS submissionid, s.grade AS submissiongrade,
89c1aa97
DM
1241 a.weight, a.grade
1242 FROM {workshop_submissions} s
1243 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id)
1244 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
1245 $params = array('workshopid' => $this->id);
1246
1247 if (is_null($restrict)) {
1248 // update all users - no more conditions
1249 } elseif (!empty($restrict)) {
1250 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1251 $sql .= " AND s.authorid $usql";
1252 $params = array_merge($params, $uparams);
1253 } else {
1254 throw new coding_exception('Empty value is not a valid parameter here');
1255 }
1256
1257 $sql .= ' ORDER BY s.id'; // this is important for bulk processing
89c1aa97 1258
e9a90e69
DM
1259 $rs = $DB->get_recordset_sql($sql, $params);
1260 $batch = array(); // will contain a set of all assessments of a single submission
1261 $previous = null; // a previous record in the recordset
1262
89c1aa97
DM
1263 foreach ($rs as $current) {
1264 if (is_null($previous)) {
1265 // we are processing the very first record in the recordset
1266 $previous = $current;
89c1aa97 1267 }
e9a90e69 1268 if ($current->submissionid == $previous->submissionid) {
89c1aa97 1269 // we are still processing the current submission
e9a90e69
DM
1270 $batch[] = $current;
1271 } else {
1272 // process all the assessments of a sigle submission
1273 $this->aggregate_submission_grades_process($batch);
1274 // and then start to process another submission
1275 $batch = array($current);
1276 $previous = $current;
89c1aa97
DM
1277 }
1278 }
e9a90e69
DM
1279 // do not forget to process the last batch!
1280 $this->aggregate_submission_grades_process($batch);
89c1aa97
DM
1281 $rs->close();
1282 }
1283
1284 /**
1285 * Calculates grades for assessment for the given participant(s)
1286 *
39411930
DM
1287 * Grade for assessment is calculated as a simple mean of all grading grades calculated by the grading evaluator.
1288 * The assessment weight is not taken into account here.
89c1aa97
DM
1289 *
1290 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
1291 * @return void
1292 */
8a1ba8ac 1293 public function aggregate_grading_grades($restrict=null) {
89c1aa97
DM
1294 global $DB;
1295
39411930
DM
1296 // fetch a recordset with all assessments to process
1297 $sql = 'SELECT a.reviewerid, a.gradinggrade, a.gradinggradeover,
1298 ag.id AS aggregationid, ag.gradinggrade AS aggregatedgrade
1299 FROM {workshop_assessments} a
1300 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1301 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = a.reviewerid AND ag.workshopid = s.workshopid)
1302 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
1303 $params = array('workshopid' => $this->id);
1304
1305 if (is_null($restrict)) {
1306 // update all users - no more conditions
1307 } elseif (!empty($restrict)) {
1308 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1309 $sql .= " AND a.reviewerid $usql";
1310 $params = array_merge($params, $uparams);
1311 } else {
1312 throw new coding_exception('Empty value is not a valid parameter here');
1313 }
1314
1315 $sql .= ' ORDER BY a.reviewerid'; // this is important for bulk processing
1316
1317 $rs = $DB->get_recordset_sql($sql, $params);
1318 $batch = array(); // will contain a set of all assessments of a single submission
1319 $previous = null; // a previous record in the recordset
1320
1321 foreach ($rs as $current) {
1322 if (is_null($previous)) {
1323 // we are processing the very first record in the recordset
1324 $previous = $current;
1325 }
1326 if ($current->reviewerid == $previous->reviewerid) {
1327 // we are still processing the current reviewer
1328 $batch[] = $current;
1329 } else {
1330 // process all the assessments of a sigle submission
1331 $this->aggregate_grading_grades_process($batch);
1332 // and then start to process another reviewer
1333 $batch = array($current);
1334 $previous = $current;
1335 }
1336 }
1337 // do not forget to process the last batch!
1338 $this->aggregate_grading_grades_process($batch);
1339 $rs->close();
89c1aa97
DM
1340 }
1341
ad6a8f69
DM
1342 /**
1343 * Calculates the workshop total grades for the given participant(s)
1344 *
1345 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
1346 * @return void
1347 */
1348 public function aggregate_total_grades($restrict=null) {
1349 global $DB;
1350
1fed6ce3
DM
1351 // fetch a recordset with all assessments to process
1352 $sql = 'SELECT s.grade, s.gradeover, s.authorid AS userid,
1353 ag.id AS agid, ag.gradinggrade, ag.totalgrade
1354 FROM {workshop_submissions} s
1355 INNER JOIN {workshop_aggregations} ag ON (ag.userid = s.authorid)
1356 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
1357 $params = array('workshopid' => $this->id);
1358
1359 if (is_null($restrict)) {
1360 // update all users - no more conditions
1361 } elseif (!empty($restrict)) {
1362 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1363 $sql .= " AND ag.userid $usql";
1364 $params = array_merge($params, $uparams);
1365 } else {
1366 throw new coding_exception('Empty value is not a valid parameter here');
1367 }
1368
1369 $sql .= ' ORDER BY ag.userid'; // this is important for bulk processing
1370
1371 $rs = $DB->get_recordset_sql($sql, $params);
1372
1373 foreach ($rs as $current) {
1374 $this->aggregate_total_grades_process($current);
1375 }
1376 $rs->close();
ad6a8f69
DM
1377 }
1378
77f43e7d
DM
1379 /**
1380 * TODO: short description.
1381 *
1382 * @param array $actionurl
1383 * @return TODO
1384 */
e554671d 1385 public function get_feedbackreviewer_form(moodle_url $actionurl, stdClass $assessment, $editable=true) {
77f43e7d
DM
1386 global $CFG;
1387 require_once(dirname(__FILE__) . '/feedbackreviewer_form.php');
1388
e554671d
DM
1389 $current = new stdClass();
1390 $current->asid = $assessment->id;
1391 $current->gradinggrade = $this->real_grading_grade($assessment->gradinggrade);
1392 $current->gradinggradeover = $this->real_grading_grade($assessment->gradinggradeover);
1393 $current->feedbackreviewer = $assessment->feedbackreviewer;
1394 $current->feedbackreviewerformat = $assessment->feedbackreviewerformat;
1395 if (is_null($current->gradinggrade)) {
1396 $current->gradinggrade = get_string('nullgrade', 'workshop');
1397 }
1398
1399 // prepare wysiwyg editor
1400 $current = file_prepare_standard_editor($current, 'feedbackreviewer', array());
1401
77f43e7d 1402 return new workshop_feedbackreviewer_form($actionurl,
e554671d 1403 array('workshop' => $this, 'current' => $current, 'feedbackopts' => array()),
77f43e7d
DM
1404 'post', '', null, $editable);
1405 }
1406
d5506aac
DM
1407 ////////////////////////////////////////////////////////////////////////////
1408 // Helper methods //
1409 ////////////////////////////////////////////////////////////////////////////
1410
1411 /**
1412 * Helper function returning the greatest common divisor
1413 *
1414 * @param int $a
1415 * @param int $b
1416 * @return int
1417 */
1418 public static function gcd($a, $b) {
1419 return ($b == 0) ? ($a):(self::gcd($b, $a % $b));
1420 }
1421
1422 /**
1423 * Helper function returning the least common multiple
1424 *
1425 * @param int $a
1426 * @param int $b
1427 * @return int
1428 */
1429 public static function lcm($a, $b) {
1430 return ($a / self::gcd($a,$b)) * $b;
1431 }
ad6a8f69 1432
aa40adbf
DM
1433 ////////////////////////////////////////////////////////////////////////////////
1434 // Internal methods (implementation details) //
1435 ////////////////////////////////////////////////////////////////////////////////
6516b9e9 1436
e9a90e69
DM
1437 /**
1438 * Given an array of all assessments of a single submission, calculates the final grade for this submission
1439 *
1440 * This calculates the weighted mean of the passed assessment grades. If, however, the submission grade
1441 * was overridden by a teacher, the gradeover value is returned and the rest of grades are ignored.
1442 *
1443 * @param array $assessments of stdClass(->submissionid ->submissiongrade ->gradeover ->weight ->grade)
1fed6ce3 1444 * @return void
e9a90e69
DM
1445 */
1446 protected function aggregate_submission_grades_process(array $assessments) {
1447 global $DB;
1448
1449 $submissionid = null; // the id of the submission being processed
1450 $current = null; // the grade currently saved in database
1451 $finalgrade = null; // the new grade to be calculated
1452 $sumgrades = 0;
1453 $sumweights = 0;
1454
1455 foreach ($assessments as $assessment) {
1456 if (is_null($submissionid)) {
1457 // the id is the same in all records, fetch it during the first loop cycle
1458 $submissionid = $assessment->submissionid;
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->submissiongrade;
1463 }
e9a90e69
DM
1464 if (is_null($assessment->grade)) {
1465 // this was not assessed yet
1466 continue;
1467 }
1468 if ($assessment->weight == 0) {
1469 // this does not influence the calculation
1470 continue;
1471 }
1472 $sumgrades += $assessment->grade * $assessment->weight;
1473 $sumweights += $assessment->weight;
1474 }
1475 if ($sumweights > 0 and is_null($finalgrade)) {
1476 $finalgrade = grade_floatval($sumgrades / $sumweights);
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 $DB->set_field('workshop_submissions', 'grade', $finalgrade, array('id' => $submissionid));
1482 }
1483 }
1484
39411930
DM
1485 /**
1486 * Given an array of all assessments done by a single reviewer, calculates the final grading grade
1487 *
1488 * This calculates the simple mean of the passed grading grades. If, however, the grading grade
1489 * was overridden by a teacher, the gradinggradeover value is returned and the rest of grades are ignored.
1490 *
1491 * @param array $assessments of stdClass(->reviewerid ->gradinggrade ->gradinggradeover ->aggregationid ->aggregatedgrade)
1fed6ce3 1492 * @return void
39411930
DM
1493 */
1494 protected function aggregate_grading_grades_process(array $assessments) {
1495 global $DB;
1496
1497 $reviewerid = null; // the id of the reviewer being processed
1498 $current = null; // the gradinggrade currently saved in database
1499 $finalgrade = null; // the new grade to be calculated
1500 $agid = null; // aggregation id
1501 $sumgrades = 0;
1502 $count = 0;
1503
1504 foreach ($assessments as $assessment) {
1505 if (is_null($reviewerid)) {
1506 // the id is the same in all records, fetch it during the first loop cycle
1507 $reviewerid = $assessment->reviewerid;
1508 }
1509 if (is_null($agid)) {
1510 // the id is the same in all records, fetch it during the first loop cycle
1511 $agid = $assessment->aggregationid;
1512 }
1513 if (is_null($current)) {
1514 // the currently saved grade is the same in all records, fetch it during the first loop cycle
1515 $current = $assessment->aggregatedgrade;
1516 }
1517 if (!is_null($assessment->gradinggradeover)) {
1518 // the grading grade for this assessment is overriden by a teacher
1519 $sumgrades += $assessment->gradinggradeover;
1520 $count++;
1521 } else {
1522 if (!is_null($assessment->gradinggrade)) {
1523 $sumgrades += $assessment->gradinggrade;
1524 $count++;
1525 }
1526 }
1527 }
1528 if ($count > 0) {
1529 $finalgrade = grade_floatval($sumgrades / $count);
1530 }
1531 // check if the new final grade differs from the one stored in the database
1532 if (grade_floats_different($finalgrade, $current)) {
1533 // we need to save new calculation into the database
1534 if (is_null($agid)) {
1535 // no aggregation record yet
1536 $record = new stdClass();
1537 $record->workshopid = $this->id;
1538 $record->userid = $reviewerid;
1539 $record->gradinggrade = $finalgrade;
1540 $DB->insert_record('workshop_aggregations', $record);
1541 } else {
1542 $DB->set_field('workshop_aggregations', 'gradinggrade', $finalgrade, array('id' => $agid));
1543 }
1544 }
1545 }
1546
1fed6ce3
DM
1547 /**
1548 * Given an object with final grade for submission and final grade for assessment, updates the total grade in DB
1549 *
1550 * @param stdClass $data
1551 * @return void
1552 */
1553 protected function aggregate_total_grades_process(stdClass $data) {
1554 global $DB;
1555
1556 if (!is_null($data->gradeover)) {
1557 $submissiongrade = $data->gradeover;
1558 } else {
1559 $submissiongrade = $data->grade;
1560 }
1561
1562 // If we do not have enough information to update totalgrade, do not do
1563 // anything. Please note there may be a lot of reasons why the workshop
1564 // participant does not have one of these grades - maybe she was ill or
1565 // just did not reach the deadlines. Teacher has to fix grades in
1566 // gradebook manually.
1567
1568 if (is_null($submissiongrade) or (!empty($this->gradinggrade) and is_null($this->gradinggrade))) {
1569 return;
1570 }
1571
1572 $totalgrade = $this->grade * $submissiongrade / 100 + $this->gradinggrade * $data->gradinggrade / 100;
1573
1574 // check if the new total grade differs from the one stored in the database
1575 if (grade_floats_different($totalgrade, $data->totalgrade)) {
1576 // we need to save new calculation into the database
1577 if (is_null($data->agid)) {
1578 // no aggregation record yet
1579 $record = new stdClass();
1580 $record->workshopid = $this->id;
1581 $record->userid = $data->userid;
1582 $record->totalgrade = $totalgrade;
1583 $DB->insert_record('workshop_aggregations', $record);
1584 } else {
1585 $DB->set_field('workshop_aggregations', 'totalgrade', $totalgrade, array('id' => $data->agid));
1586 }
1587 }
1588 }
1589
6516b9e9 1590 /**
aa40adbf 1591 * Given a list of user ids, returns the filtered one containing just ids of users with own submission
6516b9e9 1592 *
aa40adbf
DM
1593 * Example submissions are ignored.
1594 *
1595 * @param array $userids
6516b9e9
DM
1596 * @return array
1597 */
aa40adbf
DM
1598 protected function users_with_submission(array $userids) {
1599 global $DB;
1600
1601 if (empty($userids)) {
1602 return array();
1603 }
1604 $userswithsubmission = array();
1605 list($usql, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
00aca3c1 1606 $sql = "SELECT id,authorid
aa40adbf 1607 FROM {workshop_submissions}
00aca3c1 1608 WHERE example = 0 AND workshopid = :workshopid AND authorid $usql";
aa40adbf
DM
1609 $params = array('workshopid' => $this->id);
1610 $params = array_merge($params, $uparams);
1611 $submissions = $DB->get_records_sql($sql, $params);
1612 foreach ($submissions as $submission) {
00aca3c1 1613 $userswithsubmission[$submission->authorid] = true;
aa40adbf
DM
1614 }
1615
1616 return $userswithsubmission;
6516b9e9
DM
1617 }
1618
aa40adbf
DM
1619 /**
1620 * @return array of available workshop phases
1621 */
1622 protected function available_phases() {
1623 return array(
1624 self::PHASE_SETUP => true,
1625 self::PHASE_SUBMISSION => true,
1626 self::PHASE_ASSESSMENT => true,
1627 self::PHASE_EVALUATION => true,
1628 self::PHASE_CLOSED => true,
1629 );
1630 }
1631
66c9894d 1632}