Added workshop_evaluations to store aggregated grades for assessments
[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
b13142da 33require_once(dirname(__FILE__).'/lib.php'); // we extend this library here
0968b1a3 34
6e309973
DM
35/**
36 * Full-featured workshop API
37 *
a39d7d87 38 * This wraps the workshop database record with a set of methods that are called
6e309973 39 * from the module itself. The class should be initialized right after you get
a39d7d87 40 * $workshop, $cm and $course records at the begining of the script.
6e309973 41 */
a39d7d87
DM
42class workshop {
43
b761e6d9
DM
44 /** return statuses of {@link add_allocation} to be passed to a workshop renderer method */
45 const ALLOCATION_EXISTS = -1;
46 const ALLOCATION_ERROR = -2;
47
48 /** the internal code of the workshop phases as are stored in the database */
49 const PHASE_SETUP = 10;
50 const PHASE_SUBMISSION = 20;
51 const PHASE_ASSESSMENT = 30;
52 const PHASE_EVALUATION = 40;
53 const PHASE_CLOSED = 50;
54
65ba104c 55 /** @var stdClass course module record */
a39d7d87
DM
56 public $cm = null;
57
65ba104c 58 /** @var stdClass course record */
a39d7d87 59 public $course = null;
6e309973 60
b761e6d9
DM
61 /**
62 * @var workshop_strategy grading strategy instance
63 * Do not use directly, get the instance using {@link workshop::grading_strategy_instance()}
64 */
b13142da
DM
65 protected $strategyinstance = null;
66
45d24d39
DM
67 /**
68 * @var workshop_evaluation grading evaluation instance
69 * Do not use directly, get the instance using {@link workshop::grading_evaluation_instance()}
70 */
71 protected $evaluationinstance = null;
72
6e309973 73 /**
65ba104c 74 * Initializes the workshop API instance using the data from DB
a39d7d87
DM
75 *
76 * Makes deep copy of all passed records properties. Replaces integer $course attribute
77 * with a full database record (course should not be stored in instances table anyway).
6e309973 78 *
b13142da 79 * @param stdClass $dbrecord Workshop instance data from {workshop} table
06d73dd5
DM
80 * @param stdClass $cm Course module record as returned by {@link get_coursemodule_from_id()}
81 * @param stdClass $course Course record from {course} table
0dc47fb9 82 */
b13142da 83 public function __construct(stdClass $dbrecord, stdClass $cm, stdClass $course) {
f05c168d
DM
84 foreach ($dbrecord as $field => $value) {
85 $this->{$field} = $value;
a39d7d87 86 }
45d24d39
DM
87 $this->evaluation = 'best'; // todo make this configurable
88 $this->cm = $cm;
89 $this->course = $course; // beware - this replaces the standard course field in the instance table
90 // this is intentional - IMO there should be no such field as it violates
91 // 3rd normal form with no real performance gain
6e309973
DM
92 }
93
aa40adbf
DM
94 ////////////////////////////////////////////////////////////////////////////////
95 // Static methods //
96 ////////////////////////////////////////////////////////////////////////////////
97
da0b1f70 98 /**
aa40adbf 99 * Return list of available allocation methods
da0b1f70 100 *
aa40adbf 101 * @return array Array ['string' => 'string'] of localized allocation method names
da0b1f70 102 */
aa40adbf
DM
103 public static function installed_allocators() {
104 $installed = get_plugin_list('workshopallocation');
105 $forms = array();
106 foreach ($installed as $allocation => $allocationpath) {
107 if (file_exists($allocationpath . '/lib.php')) {
108 $forms[$allocation] = get_string('pluginname', 'workshopallocation_' . $allocation);
109 }
f05c168d 110 }
aa40adbf
DM
111 // usability - make sure that manual allocation appears the first
112 if (isset($forms['manual'])) {
113 $m = array('manual' => $forms['manual']);
114 unset($forms['manual']);
115 $forms = array_merge($m, $forms);
da0b1f70 116 }
aa40adbf
DM
117 return $forms;
118 }
da0b1f70 119
aa40adbf
DM
120 /**
121 * Returns an array of options for the editors that are used for submitting and assessing instructions
122 *
123 * @param stdClass $context
124 * @return array
125 */
126 public static function instruction_editors_options(stdClass $context) {
127 return array('subdirs' => 1, 'maxbytes' => 0, 'maxfiles' => EDITOR_UNLIMITED_FILES,
128 'changeformat' => 1, 'context' => $context, 'noclean' => 1, 'trusttext' => 0);
da0b1f70
DM
129 }
130
aa40adbf
DM
131 ////////////////////////////////////////////////////////////////////////////////
132 // Workshop API //
133 ////////////////////////////////////////////////////////////////////////////////
134
6e309973
DM
135 /**
136 * Fetches all users with the capability mod/workshop:submit in the current context
137 *
3d2924e9 138 * The returned objects contain id, lastname and firstname properties and are ordered by lastname,firstname
53fad4b9 139 *
aa40adbf 140 * @todo handle with limits and groups
f05c168d 141 * @param stdClass $context
53fad4b9 142 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible authors otherwise.
65ba104c 143 * @return array array[userid] => stdClass{->id ->lastname ->firstname}
6e309973 144 */
f05c168d
DM
145 public function get_potential_authors(stdClass $context, $musthavesubmission=true) {
146 $users = get_users_by_capability($context, 'mod/workshop:submit',
aa40adbf 147 'u.id,u.lastname,u.firstname', 'u.lastname,u.firstname,u.id', 0, 1000, '', '', false, false, true);
3d2924e9 148 if ($musthavesubmission) {
da0b1f70 149 $users = array_intersect_key($users, $this->users_with_submission(array_keys($users)));
66c9894d 150 }
da0b1f70 151 return $users;
6e309973
DM
152 }
153
6e309973
DM
154 /**
155 * Fetches all users with the capability mod/workshop:peerassess in the current context
156 *
b13142da 157 * The returned objects contain id, lastname and firstname properties and are ordered by lastname,firstname
53fad4b9 158 *
aa40adbf 159 * @todo handle with limits and groups
f05c168d 160 * @param stdClass $context
53fad4b9 161 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible users otherwise.
65ba104c 162 * @return array array[userid] => stdClass{->id ->lastname ->firstname}
6e309973 163 */
f05c168d
DM
164 public function get_potential_reviewers(stdClass $context, $musthavesubmission=false) {
165 $users = get_users_by_capability($context, 'mod/workshop:peerassess',
aa40adbf 166 'u.id, u.lastname, u.firstname', 'u.lastname,u.firstname,u.id', 0, 1000, '', '', false, false, true);
3d2924e9
DM
167 if ($musthavesubmission) {
168 // users without their own submission can not be reviewers
da0b1f70 169 $users = array_intersect_key($users, $this->users_with_submission(array_keys($users)));
0968b1a3 170 }
da0b1f70 171 return $users;
0968b1a3
DM
172 }
173
b8ead2e6
DM
174 /**
175 * Groups the given users by the group membership
176 *
177 * This takes the module grouping settings into account. If "Available for group members only"
178 * is set, returns only groups withing the course module grouping. Always returns group [0] with
179 * all the given users.
180 *
65ba104c
DM
181 * @param array $users array[userid] => stdClass{->id ->lastname ->firstname}
182 * @return array array[groupid][userid] => stdClass{->id ->lastname ->firstname}
53fad4b9 183 */
3d2924e9 184 public function get_grouped($users) {
53fad4b9 185 global $DB;
3d2924e9 186 global $CFG;
53fad4b9 187
b8ead2e6
DM
188 $grouped = array(); // grouped users to be returned
189 if (empty($users)) {
190 return $grouped;
a7c5b918 191 }
3d2924e9 192 if (!empty($CFG->enablegroupings) and $this->cm->groupmembersonly) {
53fad4b9
DM
193 // Available for group members only - the workshop is available only
194 // to users assigned to groups within the selected grouping, or to
195 // any group if no grouping is selected.
196 $groupingid = $this->cm->groupingid;
b8ead2e6 197 // All users that are members of at least one group will be
53fad4b9 198 // added into a virtual group id 0
b8ead2e6 199 $grouped[0] = array();
53fad4b9
DM
200 } else {
201 $groupingid = 0;
b8ead2e6
DM
202 // there is no need to be member of a group so $grouped[0] will contain
203 // all users
204 $grouped[0] = $users;
53fad4b9 205 }
b8ead2e6 206 $gmemberships = groups_get_all_groups($this->cm->course, array_keys($users), $groupingid,
53fad4b9
DM
207 'gm.id,gm.groupid,gm.userid');
208 foreach ($gmemberships as $gmembership) {
b8ead2e6
DM
209 if (!isset($grouped[$gmembership->groupid])) {
210 $grouped[$gmembership->groupid] = array();
53fad4b9 211 }
b8ead2e6
DM
212 $grouped[$gmembership->groupid][$gmembership->userid] = $users[$gmembership->userid];
213 $grouped[0][$gmembership->userid] = $users[$gmembership->userid];
53fad4b9 214 }
b8ead2e6 215 return $grouped;
53fad4b9 216 }
6e309973 217
aa40adbf
DM
218 /**
219 * Returns the list of all allocations (it est assigned assessments) in the workshop
220 *
221 * Assessments of example submissions are ignored
222 *
223 * @return array
224 */
225 public function get_allocations() {
226 global $DB;
227
228 $sql = 'SELECT a.id, a.submissionid, a.userid AS reviewerid,
229 s.userid AS authorid
230 FROM {workshop_assessments} a
231 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
232 WHERE s.example = 0 AND s.workshopid = :workshopid';
233 $params = array('workshopid' => $this->id);
234
235 return $DB->get_records_sql($sql, $params);
236 }
237
6e309973
DM
238 /**
239 * Returns submissions from this workshop
240 *
3dc78e5b
DM
241 * Fetches data from {workshop_submissions} and adds some useful information from other
242 * tables. Does not return textual fields to prevent possible memory lack issues.
53fad4b9
DM
243 *
244 * @param mixed $userid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only
245 * @param mixed $examples false|true|'all' Only regular submissions, only examples, all submissions
f05c168d 246 * @return array
6e309973 247 */
3dc78e5b 248 public function get_submissions($userid='all', $examples=false) {
6e309973
DM
249 global $DB;
250
e9b0f0ab
DM
251 $sql = 'SELECT s.id, s.workshopid, s.example, s.userid, s.timecreated, s.timemodified,
252 s.title, s.grade, s.gradeover, s.gradeoverby, s.gradinggrade,
253 u.lastname AS authorlastname, u.firstname AS authorfirstname, u.id AS authorid,
254 u.picture AS authorpicture, u.imagealt AS authorimagealt
3d2924e9
DM
255 FROM {workshop_submissions} s
256 INNER JOIN {user} u ON (s.userid = u.id)
235b31c8 257 WHERE s.workshopid = :workshopid';
3d2924e9 258 $params = array('workshopid' => $this->id);
6e309973 259
b13142da
DM
260 if ('all' === $examples) {
261 // no additional conditions
262 } elseif ($examples === true) {
235b31c8 263 $sql .= ' AND example = 1';
b13142da 264 } elseif ($examples === false) {
235b31c8 265 $sql .= ' AND example = 0';
b13142da
DM
266 } else {
267 throw new coding_exception('Illegal parameter value: $examples may be false|true|"all"');
6e309973 268 }
3d2924e9
DM
269
270 if ('all' === $userid) {
271 // no additional conditions
272 } elseif (is_array($userid)) {
273 list($usql, $uparams) = $DB->get_in_or_equal($userid, SQL_PARAMS_NAMED);
274 $sql .= " AND userid $usql";
6e309973 275 $params = array_merge($params, $uparams);
3d2924e9 276 } else {
235b31c8 277 $sql .= ' AND userid = :userid';
3d2924e9 278 $params['userid'] = $userid;
6e309973 279 }
3dc78e5b 280 $sql .= ' ORDER BY u.lastname, u.firstname';
6e309973 281
3dc78e5b 282 return $DB->get_records_sql($sql, $params);
6e309973
DM
283 }
284
51508f25
DM
285 /**
286 * Returns a submission record with the author's data
287 *
288 * @param int $id submission id
289 * @return stdClass
290 */
291 public function get_submission_by_id($id) {
292 global $DB;
293
294 $sql = 'SELECT s.*,
295 u.lastname AS authorlastname, u.firstname AS authorfirstname, u.id AS authorid,
296 u.picture AS authorpicture, u.imagealt AS authorimagealt
297 FROM {workshop_submissions} s
298 INNER JOIN {user} u ON (s.userid = u.id)
299 WHERE s.workshopid = :workshopid AND s.id = :id';
300 $params = array('workshopid' => $this->id, 'id' => $id);
301 return $DB->get_record_sql($sql, $params, MUST_EXIST);
302 }
303
53fad4b9 304 /**
3dc78e5b 305 * Returns a submission submitted by the given author
53fad4b9 306 *
3dc78e5b
DM
307 * @param int $id author id
308 * @return stdClass|false
53fad4b9
DM
309 */
310 public function get_submission_by_author($id) {
e9b0f0ab
DM
311 global $DB;
312
53fad4b9
DM
313 if (empty($id)) {
314 return false;
315 }
3dc78e5b
DM
316 $sql = 'SELECT s.*,
317 u.lastname AS authorlastname, u.firstname AS authorfirstname, u.id AS authorid,
318 u.picture AS authorpicture, u.imagealt AS authorimagealt
319 FROM {workshop_submissions} s
320 INNER JOIN {user} u ON (s.userid = u.id)
321 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.userid = :userid';
322 $params = array('workshopid' => $this->id, 'userid' => $id);
323 return $DB->get_record_sql($sql, $params);
53fad4b9 324 }
6e309973
DM
325
326 /**
3dc78e5b 327 * Returns the list of all assessments in the workshop with some data added
6e309973
DM
328 *
329 * Fetches data from {workshop_assessments} and adds some useful information from other
3dc78e5b
DM
330 * tables. The returned object does not contain textual fields (ie comments) to prevent memory
331 * lack issues.
332 *
333 * @return array [assessmentid] => assessment stdClass
6e309973 334 */
3dc78e5b 335 public function get_all_assessments() {
6e309973 336 global $DB;
53fad4b9 337
3dc78e5b
DM
338 $sql = 'SELECT a.id, a.submissionid, a.userid, a.timecreated, a.timemodified, a.timeagreed,
339 a.grade, a.gradinggrade, a.gradinggradeover, a.gradinggradeoverby,
3d2924e9
DM
340 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname as reviewerlastname,
341 s.title,
ddb59c77 342 author.id AS authorid, author.firstname AS authorfirstname,author.lastname AS authorlastname
3d2924e9
DM
343 FROM {workshop_assessments} a
344 INNER JOIN {user} reviewer ON (a.userid = reviewer.id)
345 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
346 INNER JOIN {user} author ON (s.userid = author.id)
3dc78e5b
DM
347 WHERE s.workshopid = :workshopid AND s.example = 0
348 ORDER BY reviewer.lastname, reviewer.firstname';
3d2924e9
DM
349 $params = array('workshopid' => $this->id);
350
3dc78e5b 351 return $DB->get_records_sql($sql, $params);
53fad4b9
DM
352 }
353
354 /**
3dc78e5b 355 * Get the complete information about the given assessment
53fad4b9
DM
356 *
357 * @param int $id Assessment ID
65ba104c 358 * @return mixed false if not found, stdClass otherwise
53fad4b9
DM
359 */
360 public function get_assessment_by_id($id) {
3dc78e5b
DM
361 global $DB;
362
363 $sql = 'SELECT a.*,
364 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname as reviewerlastname,
365 s.title,
366 author.id AS authorid, author.firstname AS authorfirstname,author.lastname as authorlastname
367 FROM {workshop_assessments} a
368 INNER JOIN {user} reviewer ON (a.userid = reviewer.id)
369 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
370 INNER JOIN {user} author ON (s.userid = author.id)
371 WHERE a.id = :id AND s.workshopid = :workshopid';
372 $params = array('id' => $id, 'workshopid' => $this->id);
373
374 return $DB->get_record_sql($sql, $params, MUST_EXIST);
53fad4b9
DM
375 }
376
377 /**
3dc78e5b 378 * Get the complete information about all assessments allocated to the given reviewer
53fad4b9 379 *
3dc78e5b
DM
380 * @param int $userid reviewer id
381 * @return array
53fad4b9 382 */
3dc78e5b
DM
383 public function get_assessments_by_reviewer($userid) {
384 global $DB;
385
386 $sql = 'SELECT a.*,
ddb59c77
DM
387 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname AS reviewerlastname,
388 s.id AS submissionid, s.title AS submissiontitle, s.timecreated AS submissioncreated,
389 s.timemodified AS submissionmodified,
390 author.id AS authorid, author.firstname AS authorfirstname,author.lastname AS authorlastname,
391 author.picture AS authorpicture, author.imagealt AS authorimagealt
3dc78e5b
DM
392 FROM {workshop_assessments} a
393 INNER JOIN {user} reviewer ON (a.userid = reviewer.id)
394 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
395 INNER JOIN {user} author ON (s.userid = author.id)
396 WHERE s.example = 0 AND reviewer.id = :userid AND s.workshopid = :workshopid';
397 $params = array('userid' => $userid, 'workshopid' => $this->id);
398
399 return $DB->get_records_sql($sql, $params);
53fad4b9 400 }
6e309973 401
6e309973
DM
402 /**
403 * Allocate a submission to a user for review
53fad4b9 404 *
65ba104c 405 * @param stdClass $submission Submission record
6e309973 406 * @param int $reviewerid User ID
53fad4b9 407 * @param bool $bulk repeated inserts into DB expected
6e309973
DM
408 * @return int ID of the new assessment or an error code
409 */
65ba104c 410 public function add_allocation(stdClass $submission, $reviewerid, $bulk=false) {
6e309973
DM
411 global $DB;
412
413 if ($DB->record_exists('workshop_assessments', array('submissionid' => $submission->id, 'userid' => $reviewerid))) {
b761e6d9 414 return self::ALLOCATION_EXISTS;
6e309973
DM
415 }
416
6e309973 417 $now = time();
65ba104c 418 $assessment = new stdClass();
53fad4b9 419 $assessment->submissionid = $submission->id;
6e309973
DM
420 $assessment->userid = $reviewerid;
421 $assessment->timecreated = $now;
422 $assessment->timemodified = $now;
423
235b31c8 424 return $DB->insert_record('workshop_assessments', $assessment, true, $bulk);
6e309973
DM
425 }
426
6e309973 427 /**
53fad4b9 428 * Delete assessment record or records
6e309973 429 *
53fad4b9
DM
430 * @param mixed $id int|array assessment id or array of assessments ids
431 * @return bool false if $id not a valid parameter, true otherwise
6e309973
DM
432 */
433 public function delete_assessment($id) {
434 global $DB;
435
436 // todo remove all given grades from workshop_grades;
6e309973 437
53fad4b9 438 if (is_array($id)) {
235b31c8 439 return $DB->delete_records_list('workshop_assessments', 'id', $id);
3d2924e9 440 } else {
235b31c8 441 return $DB->delete_records('workshop_assessments', array('id' => $id));
53fad4b9 442 }
53fad4b9 443 }
6e309973
DM
444
445 /**
446 * Returns instance of grading strategy class
53fad4b9 447 *
65ba104c 448 * @return stdClass Instance of a grading strategy
6e309973
DM
449 */
450 public function grading_strategy_instance() {
3d2924e9
DM
451 global $CFG; // because we require other libs here
452
3fd2b0e1 453 if (is_null($this->strategyinstance)) {
f05c168d 454 $strategylib = dirname(__FILE__) . '/form/' . $this->strategy . '/lib.php';
6e309973
DM
455 if (is_readable($strategylib)) {
456 require_once($strategylib);
457 } else {
f05c168d 458 throw new coding_exception('the grading forms subplugin must contain library ' . $strategylib);
6e309973 459 }
0dc47fb9 460 $classname = 'workshop_' . $this->strategy . '_strategy';
3fd2b0e1
DM
461 $this->strategyinstance = new $classname($this);
462 if (!in_array('workshop_strategy', class_implements($this->strategyinstance))) {
b761e6d9 463 throw new coding_exception($classname . ' does not implement workshop_strategy interface');
6e309973
DM
464 }
465 }
3fd2b0e1 466 return $this->strategyinstance;
6e309973
DM
467 }
468
45d24d39
DM
469 /**
470 * Returns instance of grading evaluation class
471 *
472 * @return stdClass Instance of a grading evaluation
473 */
474 public function grading_evaluation_instance() {
475 global $CFG; // because we require other libs here
476
477 if (is_null($this->evaluationinstance)) {
478 $evaluationlib = dirname(__FILE__) . '/eval/' . $this->evaluation . '/lib.php';
479 if (is_readable($evaluationlib)) {
480 require_once($evaluationlib);
481 } else {
482 throw new coding_exception('the grading evaluation subplugin must contain library ' . $evaluationlib);
483 }
484 $classname = 'workshop_' . $this->evaluation . '_evaluation';
485 $this->evaluationinstance = new $classname($this);
486 if (!in_array('workshop_evaluation', class_implements($this->evaluationinstance))) {
487 throw new coding_exception($classname . ' does not implement workshop_evaluation interface');
488 }
489 }
490 return $this->evaluationinstance;
491 }
492
66c9894d
DM
493 /**
494 * Returns instance of submissions allocator
53fad4b9 495 *
65ba104c
DM
496 * @param stdClass $method The name of the allocation method, must be PARAM_ALPHA
497 * @return stdClass Instance of submissions allocator
66c9894d
DM
498 */
499 public function allocator_instance($method) {
3d2924e9
DM
500 global $CFG; // because we require other libs here
501
f05c168d 502 $allocationlib = dirname(__FILE__) . '/allocation/' . $method . '/lib.php';
66c9894d
DM
503 if (is_readable($allocationlib)) {
504 require_once($allocationlib);
505 } else {
f05c168d 506 throw new coding_exception('Unable to find the allocation library ' . $allocationlib);
66c9894d
DM
507 }
508 $classname = 'workshop_' . $method . '_allocator';
509 return new $classname($this);
510 }
511
b8ead2e6 512 /**
454e8dd9 513 * @return moodle_url of this workshop's view page
b8ead2e6
DM
514 */
515 public function view_url() {
516 global $CFG;
517 return new moodle_url($CFG->wwwroot . '/mod/workshop/view.php', array('id' => $this->cm->id));
518 }
519
520 /**
454e8dd9 521 * @return moodle_url of the page for editing this workshop's grading form
b8ead2e6
DM
522 */
523 public function editform_url() {
524 global $CFG;
525 return new moodle_url($CFG->wwwroot . '/mod/workshop/editform.php', array('cmid' => $this->cm->id));
526 }
527
528 /**
454e8dd9 529 * @return moodle_url of the page for previewing this workshop's grading form
b8ead2e6
DM
530 */
531 public function previewform_url() {
532 global $CFG;
533 return new moodle_url($CFG->wwwroot . '/mod/workshop/assessment.php', array('preview' => $this->cm->id));
534 }
535
536 /**
537 * @param int $assessmentid The ID of assessment record
454e8dd9 538 * @return moodle_url of the assessment page
b8ead2e6 539 */
a39d7d87 540 public function assess_url($assessmentid) {
b8ead2e6 541 global $CFG;
454e8dd9 542 $assessmentid = clean_param($assessmentid, PARAM_INT);
a39d7d87 543 return new moodle_url($CFG->wwwroot . '/mod/workshop/assessment.php', array('asid' => $assessmentid));
b8ead2e6
DM
544 }
545
39861053 546 /**
454e8dd9 547 * @return moodle_url of the page to view own submission
39861053
DM
548 */
549 public function submission_url() {
550 global $CFG;
551 return new moodle_url($CFG->wwwroot . '/mod/workshop/submission.php', array('cmid' => $this->cm->id));
552 }
553
da0b1f70 554 /**
454e8dd9 555 * @return moodle_url of the mod_edit form
da0b1f70
DM
556 */
557 public function updatemod_url() {
558 global $CFG;
559 return new moodle_url($CFG->wwwroot . '/course/modedit.php', array('update' => $this->cm->id, 'return' => 1));
560 }
561
454e8dd9
DM
562 /**
563 * @return moodle_url to the allocation page
564 */
da0b1f70
DM
565 public function allocation_url() {
566 global $CFG;
567 return new moodle_url($CFG->wwwroot . '/mod/workshop/allocation.php', array('cmid' => $this->cm->id));
568 }
569
454e8dd9
DM
570 /**
571 * @param int $phasecode The internal phase code
572 * @return moodle_url of the script to change the current phase to $phasecode
573 */
574 public function switchphase_url($phasecode) {
575 global $CFG;
576 $phasecode = clean_param($phasecode, PARAM_INT);
577 return new moodle_url($CFG->wwwroot . '/mod/workshop/switchphase.php', array('cmid' => $this->cm->id, 'phase' => $phasecode));
578 }
579
b8ead2e6
DM
580 /**
581 * Returns an object containing all data to display the user's full name and picture
582 *
583 * @param int $id optional user id, defaults to the current user
65ba104c 584 * @return stdClass containing properties lastname, firstname, picture and imagealt
b8ead2e6
DM
585 */
586 public function user_info($id=null) {
587 global $USER, $DB;
588
589 if (is_null($id) || ($id == $USER->id)) {
590 return $USER;
591 } else {
592 return $DB->get_record('user', array('id' => $id), 'id,lastname,firstname,picture,imagealt', MUST_EXIST);
593 }
594 }
595
c1e883bb 596 /**
b13142da 597 * Are users allowed to create/edit their submissions?
c1e883bb
DM
598 *
599 * TODO: this depends on the workshop phase, phase deadlines, submitting after deadlines possibility
600 *
601 * @return bool
602 */
603 public function submitting_allowed() {
604 return true;
605 }
606
3dc78e5b
DM
607 /**
608 * Are the peer-reviews available to the authors?
609 *
610 * TODO: this depends on the workshop phase
611 *
612 * @return bool
613 */
614 public function assessments_available() {
615 return true;
616 }
617
618 /**
619 * Can the given grades be displayed to the authors?
620 *
621 * Grades are not displayed if {@link self::assessments_available()} return false. The returned
622 * value may be true (if yes, display grades), false (no, hide grades yet) or null (only
623 * display grades if the assessment has been agreed by the author).
624 *
625 * @return bool|null
626 */
627 public function grades_available() {
628 return true;
629 }
630
b13142da
DM
631 /**
632 * Returns the localized name of the grading strategy method to be displayed to the users
633 *
634 * @return string
635 */
636 public function strategy_name() {
f05c168d 637 return get_string('pluginname', 'workshopform_' . $this->strategy);
b13142da 638 }
b761e6d9
DM
639
640 /**
641 * Prepare an individual workshop plan for the given user.
642 *
f05c168d
DM
643 * @param int $userid whom the plan is prepared for
644 * @param stdClass context of the planned workshop
645 * @return stdClass data object to be passed to the renderer
b761e6d9 646 */
f05c168d 647 public function prepare_user_plan($userid, stdClass $context) {
b761e6d9
DM
648 global $DB;
649
650 $phases = array();
651
652 // Prepare tasks for the setup phase
653 $phase = new stdClass();
654 $phase->title = get_string('phasesetup', 'workshop');
655 $phase->tasks = array();
f05c168d 656 if (has_capability('moodle/course:manageactivities', $context, $userid)) {
da0b1f70
DM
657 $task = new stdClass();
658 $task->title = get_string('taskintro', 'workshop');
659 $task->link = $this->updatemod_url();
660 $task->completed = !(trim(strip_tags($this->intro)) == '');
661 $phase->tasks['intro'] = $task;
662 }
f05c168d 663 if (has_capability('moodle/course:manageactivities', $context, $userid)) {
454e8dd9
DM
664 $task = new stdClass();
665 $task->title = get_string('taskinstructauthors', 'workshop');
666 $task->link = $this->updatemod_url();
667 $task->completed = !(trim(strip_tags($this->instructauthors)) == '');
668 $phase->tasks['instructauthors'] = $task;
669 }
f05c168d 670 if (has_capability('mod/workshop:editdimensions', $context, $userid)) {
b761e6d9 671 $task = new stdClass();
da0b1f70
DM
672 $task->title = get_string('editassessmentform', 'workshop');
673 $task->link = $this->editform_url();
674 if ($this->assessment_form_ready()) {
675 $task->completed = true;
676 } elseif ($this->phase > self::PHASE_SETUP) {
677 $task->completed = false;
678 }
b761e6d9
DM
679 $phase->tasks['editform'] = $task;
680 }
da0b1f70
DM
681 if (empty($phase->tasks) and $this->phase == self::PHASE_SETUP) {
682 // if we are in the setup phase and there is no task (typical for students), let us
683 // display some explanation what is going on
684 $task = new stdClass();
685 $task->title = get_string('undersetup', 'workshop');
686 $task->completed = 'info';
687 $phase->tasks['setupinfo'] = $task;
688 }
b761e6d9
DM
689 $phases[self::PHASE_SETUP] = $phase;
690
691 // Prepare tasks for the submission phase
692 $phase = new stdClass();
693 $phase->title = get_string('phasesubmission', 'workshop');
694 $phase->tasks = array();
f05c168d 695 if (has_capability('mod/workshop:submit', $context, $userid)) {
b761e6d9
DM
696 $task = new stdClass();
697 $task->title = get_string('tasksubmit', 'workshop');
da0b1f70
DM
698 $task->link = $this->submission_url();
699 if ($DB->record_exists('workshop_submissions', array('workshopid'=>$this->id, 'example'=>0, 'userid'=>$userid))) {
700 $task->completed = true;
701 } elseif ($this->phase >= self::PHASE_ASSESSMENT) {
702 $task->completed = false;
703 } else {
704 $task->completed = null; // still has a chance to submit
705 }
b761e6d9
DM
706 $phase->tasks['submit'] = $task;
707 }
f05c168d 708 if (has_capability('moodle/course:manageactivities', $context, $userid)) {
da0b1f70
DM
709 $task = new stdClass();
710 $task->title = get_string('taskinstructreviewers', 'workshop');
711 $task->link = $this->updatemod_url();
712 if (trim(strip_tags($this->instructreviewers))) {
713 $task->completed = true;
714 } elseif ($this->phase >= self::PHASE_ASSESSMENT) {
715 $task->completed = false;
716 }
717 $phase->tasks['instructreviewers'] = $task;
718 }
b761e6d9 719 $phases[self::PHASE_SUBMISSION] = $phase;
f05c168d 720 if (has_capability('mod/workshop:allocate', $context, $userid)) {
da0b1f70
DM
721 $task = new stdClass();
722 $task->title = get_string('allocate', 'workshop');
723 $task->link = $this->allocation_url();
3189fb2d 724 $authors = array();
da0b1f70 725 $allocations = array(); // 'submissionid' => isallocated
aa40adbf
DM
726 $records = $this->get_allocations();
727 foreach ($records as $allocation) {
728 if (!isset($authors[$allocation->authorid])) {
729 $authors[$allocation->authorid] = true;
730 }
731 if (isset($allocation->submissionid)) {
732 if (!isset($allocations[$allocation->submissionid])) {
733 $allocations[$allocation->submissionid] = false;
3189fb2d 734 }
aa40adbf
DM
735 if (!empty($allocation->reviewerid)) {
736 $allocations[$allocation->submissionid] = true;
3189fb2d 737 }
da0b1f70
DM
738 }
739 }
3189fb2d 740 $numofauthors = count($authors);
da0b1f70
DM
741 $numofsubmissions = count($allocations);
742 $numofallocated = count(array_filter($allocations));
da0b1f70
DM
743 if ($numofsubmissions == 0) {
744 $task->completed = null;
3dc78e5b 745 } elseif ($numofsubmissions == $numofallocated) {
da0b1f70
DM
746 $task->completed = true;
747 } elseif ($this->phase > self::PHASE_SUBMISSION) {
748 $task->completed = false;
749 } else {
750 $task->completed = null; // still has a chance to allocate
751 }
752 $a = new stdClass();
3189fb2d
DM
753 $a->expected = $numofauthors;
754 $a->submitted = $numofsubmissions;
755 $a->allocated = $numofallocated;
756 $task->details = get_string('allocatedetails', 'workshop', $a);
da0b1f70 757 unset($a);
3189fb2d 758 $phase->tasks['allocate'] = $task;
3dc78e5b
DM
759
760 if ($numofsubmissions < $numofauthors and $this->phase >= self::PHASE_SUBMISSION) {
761 $task = new stdClass();
762 $task->title = get_string('someuserswosubmission', 'workshop');
763 $task->completed = 'info';
764 $phase->tasks['allocateinfo'] = $task;
765 }
da0b1f70 766 }
b761e6d9
DM
767
768 // Prepare tasks for the peer-assessment phase (includes eventual self-assessments)
769 $phase = new stdClass();
770 $phase->title = get_string('phaseassessment', 'workshop');
771 $phase->tasks = array();
f05c168d 772 $phase->isreviewer = has_capability('mod/workshop:peerassess', $context, $userid);
3dc78e5b 773 $phase->assessments = $this->get_assessments_by_reviewer($userid);
b761e6d9
DM
774 $numofpeers = 0; // number of allocated peer-assessments
775 $numofpeerstodo = 0; // number of peer-assessments to do
776 $numofself = 0; // number of allocated self-assessments - should be 0 or 1
777 $numofselftodo = 0; // number of self-assessments to do - should be 0 or 1
778 foreach ($phase->assessments as $a) {
779 if ($a->authorid == $userid) {
780 $numofself++;
781 if (is_null($a->grade)) {
782 $numofselftodo++;
783 }
784 } else {
785 $numofpeers++;
786 if (is_null($a->grade)) {
787 $numofpeerstodo++;
788 }
789 }
790 }
791 unset($a);
792 if ($numofpeers) {
793 $task = new stdClass();
3dc78e5b
DM
794 if ($numofpeerstodo == 0) {
795 $task->completed = true;
796 } elseif ($this->phase > self::PHASE_ASSESSMENT) {
797 $task->completed = false;
798 }
b761e6d9
DM
799 $a = new stdClass();
800 $a->total = $numofpeers;
801 $a->todo = $numofpeerstodo;
802 $task->title = get_string('taskassesspeers', 'workshop');
da0b1f70 803 $task->details = get_string('taskassesspeersdetails', 'workshop', $a);
b761e6d9
DM
804 unset($a);
805 $phase->tasks['assesspeers'] = $task;
806 }
807 if ($numofself) {
808 $task = new stdClass();
3dc78e5b
DM
809 if ($numofselftodo == 0) {
810 $task->completed = true;
811 } elseif ($this->phase > self::PHASE_ASSESSMENT) {
812 $task->completed = false;
813 }
b761e6d9
DM
814 $task->title = get_string('taskassessself', 'workshop');
815 $phase->tasks['assessself'] = $task;
816 }
817 $phases[self::PHASE_ASSESSMENT] = $phase;
818
819 // Prepare tasks for the grading evaluation phase - todo
820 $phase = new stdClass();
821 $phase->title = get_string('phaseevaluation', 'workshop');
822 $phase->tasks = array();
823 $phases[self::PHASE_EVALUATION] = $phase;
824
825 // Prepare tasks for the "workshop closed" phase - todo
826 $phase = new stdClass();
827 $phase->title = get_string('phaseclosed', 'workshop');
828 $phase->tasks = array();
829 $phases[self::PHASE_CLOSED] = $phase;
830
831 // Polish data, set default values if not done explicitly
832 foreach ($phases as $phasecode => $phase) {
833 $phase->title = isset($phase->title) ? $phase->title : '';
834 $phase->tasks = isset($phase->tasks) ? $phase->tasks : array();
835 if ($phasecode == $this->phase) {
836 $phase->active = true;
837 } else {
838 $phase->active = false;
839 }
454e8dd9
DM
840 if (!isset($phase->actions)) {
841 $phase->actions = array();
842 }
b761e6d9
DM
843
844 foreach ($phase->tasks as $taskcode => $task) {
845 $task->title = isset($task->title) ? $task->title : '';
da0b1f70
DM
846 $task->link = isset($task->link) ? $task->link : null;
847 $task->details = isset($task->details) ? $task->details : '';
b761e6d9
DM
848 $task->completed = isset($task->completed) ? $task->completed : null;
849 }
850 }
454e8dd9
DM
851
852 // Add phase swithing actions
f05c168d 853 if (has_capability('mod/workshop:switchphase', $context, $userid)) {
454e8dd9
DM
854 foreach ($phases as $phasecode => $phase) {
855 if (! $phase->active) {
856 $action = new stdClass();
857 $action->type = 'switchphase';
858 $action->url = $this->switchphase_url($phasecode);
859 $phase->actions[] = $action;
860 }
861 }
862 }
863
b761e6d9
DM
864 return $phases;
865 }
866
867 /**
868 * Has the assessment form been defined?
869 *
870 * @return bool
871 */
872 public function assessment_form_ready() {
873 return $this->grading_strategy_instance()->form_ready();
874 }
875
454e8dd9
DM
876 /**
877 * Switch to a new workshop phase
878 *
879 * Modifies the underlying database record. You should terminate the script shortly after calling this.
880 *
881 * @param int $newphase new phase code
882 * @return bool true if success, false otherwise
883 */
884 public function switch_phase($newphase) {
885 global $DB;
886
887 $known = $this->available_phases();
888 if (!isset($known[$newphase])) {
889 return false;
890 }
891 $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id));
892 return true;
893 }
ddb59c77
DM
894
895 /**
896 * Saves a raw grade for submission as calculated from the assessment form fields
897 *
898 * @param array $assessmentid assessment record id, must exists
899 * @param mixed $grade raw percentual grade from 0 to 1
900 * @return false|float the saved grade
901 */
902 public function set_peer_grade($assessmentid, $grade) {
903 global $DB;
904
905 if (is_null($grade)) {
906 return false;
907 }
908 $data = new stdClass();
909 $data->id = $assessmentid;
910 $data->grade = $grade;
911 $DB->update_record('workshop_assessments', $data);
912 return $grade;
913 }
6516b9e9 914
aa40adbf
DM
915 ////////////////////////////////////////////////////////////////////////////////
916 // Internal methods (implementation details) //
917 ////////////////////////////////////////////////////////////////////////////////
6516b9e9
DM
918
919 /**
aa40adbf 920 * Given a list of user ids, returns the filtered one containing just ids of users with own submission
6516b9e9 921 *
aa40adbf
DM
922 * Example submissions are ignored.
923 *
924 * @param array $userids
6516b9e9
DM
925 * @return array
926 */
aa40adbf
DM
927 protected function users_with_submission(array $userids) {
928 global $DB;
929
930 if (empty($userids)) {
931 return array();
932 }
933 $userswithsubmission = array();
934 list($usql, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
935 $sql = "SELECT id,userid
936 FROM {workshop_submissions}
937 WHERE example = 0 AND workshopid = :workshopid AND userid $usql";
938 $params = array('workshopid' => $this->id);
939 $params = array_merge($params, $uparams);
940 $submissions = $DB->get_records_sql($sql, $params);
941 foreach ($submissions as $submission) {
942 $userswithsubmission[$submission->userid] = null;
943 }
944
945 return $userswithsubmission;
6516b9e9
DM
946 }
947
aa40adbf
DM
948 /**
949 * @return array of available workshop phases
950 */
951 protected function available_phases() {
952 return array(
953 self::PHASE_SETUP => true,
954 self::PHASE_SUBMISSION => true,
955 self::PHASE_ASSESSMENT => true,
956 self::PHASE_EVALUATION => true,
957 self::PHASE_CLOSED => true,
958 );
959 }
960
961
66c9894d 962}