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