MDL-19941 Submission attachments and embedded files draft support
[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
6e309973 24 * parameter, we use a class workshop_api 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
6e309973 33require_once(dirname(__FILE__).'/lib.php'); // we extend this library here
0968b1a3 34
6e309973 35define('WORKSHOP_ALLOCATION_EXISTS', -1); // return status of {@link add_allocation}
6e309973
DM
36
37/**
38 * Full-featured workshop API
39 *
53fad4b9 40 * This extends the module base API and adds the internal methods that are called
6e309973
DM
41 * from the module itself. The class should be initialized right after you get
42 * $workshop and $cm records at the begining of the script.
43 */
44class workshop_api extends workshop {
45
46 /** grading strategy instance */
47 protected $strategy_api=null;
48
49 /**
50 * Initialize the object using the data from DB
51 *
a7c5b918
DM
52 * @param object $instance The instance data row from {workshop} table
53 * @param object $cm Course module record
54 * @param object $courserecord Course record
0dc47fb9 55 */
a7c5b918
DM
56 public function __construct($instance, $cm, $courserecord) {
57 parent::__construct($instance, $cm, $courserecord);
6e309973
DM
58 }
59
6e309973
DM
60 /**
61 * Fetches all users with the capability mod/workshop:submit in the current context
62 *
66c9894d 63 * Static variables used to cache the results. The returned objects contain id, lastname
6e309973 64 * and firstname properties and are ordered by lastname,firstname
53fad4b9
DM
65 *
66 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible authors otherwise.
67 * @return array array[userid] => stdClass{->id ->lastname ->firstname}
6e309973 68 */
53fad4b9 69 public function get_peer_authors($musthavesubmission=true) {
66c9894d
DM
70 global $DB;
71 static $users = null;
72 static $userswithsubmission = null;
6e309973
DM
73
74 if (is_null($users)) {
75 $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
76 $users = get_users_by_capability($context, 'mod/workshop:submit',
77 'u.id, u.lastname, u.firstname', 'u.lastname,u.firstname', '', '', '', '', false, false, true);
78 }
66c9894d
DM
79
80 if ($musthavesubmission && is_null($userswithsubmission)) {
81 $userswithsubmission = $DB->get_records_list('workshop_submissions', 'userid', array_keys($users),'', 'userid');
82 $userswithsubmission = array_intersect_key($users, $userswithsubmission);
83 }
84
85 if ($musthavesubmission) {
86 return $userswithsubmission;
87 } else {
88 return $users;
89 }
6e309973
DM
90 }
91
53fad4b9
DM
92 /**
93 * Returns all users with the capability mod/workshop:submit sorted by groups
94 *
95 * This takes the module grouping settings into account. If "Available for group members only"
96 * is set, returns only groups withing the course module grouping.
97 *
98 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible authors otherwise.
99 * @return array array[groupid][userid] => stdClass{->id ->lastname ->firstname}
100 */
101 public function get_peer_authors_by_group($musthavesubmission=true) {
102 global $DB;
103
104 $authors = $this->get_peer_authors($musthavesubmission);
105 $gauthors = array(); // grouped authors to be returned
a7c5b918
DM
106 if (empty($authors)) {
107 return $gauthors;
108 }
53fad4b9
DM
109 if ($this->cm->groupmembersonly) {
110 // Available for group members only - the workshop is available only
111 // to users assigned to groups within the selected grouping, or to
112 // any group if no grouping is selected.
113 $groupingid = $this->cm->groupingid;
114 // All authors that are members of at least one group will be
115 // added into a virtual group id 0
116 $gauthors[0] = array();
117 } else {
118 $groupingid = 0;
119 // there is no need to be member of a group so $gauthors[0] will contain
120 // all authors with a submission
121 $gauthors[0] = $authors;
122 }
123 $gmemberships = groups_get_all_groups($this->cm->course, array_keys($authors), $groupingid,
124 'gm.id,gm.groupid,gm.userid');
125 foreach ($gmemberships as $gmembership) {
126 if (!isset($gauthors[$gmembership->groupid])) {
127 $gauthors[$gmembership->groupid] = array();
128 }
129 $gauthors[$gmembership->groupid][$gmembership->userid] = $authors[$gmembership->userid];
130 $gauthors[0][$gmembership->userid] = $authors[$gmembership->userid];
131 }
132 return $gauthors;
133 }
6e309973
DM
134
135 /**
136 * Fetches all users with the capability mod/workshop:peerassess in the current context
137 *
138 * Static variable used to cache the results. The returned objects contain id, lastname
139 * and firstname properties and are ordered by lastname,firstname
53fad4b9
DM
140 *
141 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible users otherwise.
142 * @return array array[userid] => stdClass{->id ->lastname ->firstname}
6e309973 143 */
53fad4b9 144 public function get_peer_reviewers($musthavesubmission=false) {
6e309973 145 global $DB;
53fad4b9
DM
146 static $users = null;
147 static $userswithsubmission = null;
6e309973
DM
148
149 if (is_null($users)) {
150 $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
151 $users = get_users_by_capability($context, 'mod/workshop:peerassess',
152 'u.id, u.lastname, u.firstname', 'u.lastname,u.firstname', '', '', '', '', false, false, true);
53fad4b9 153 if ($musthavesubmission && is_null($userswithsubmission)) {
66c9894d 154 // users without their own submission can not be reviewers
53fad4b9
DM
155 $userswithsubmission = $DB->get_records_list('workshop_submissions', 'userid', array_keys($users),'', 'userid');
156 $userswithsubmission = array_intersect_key($users, $userswithsubmission);
6e309973 157 }
0968b1a3 158 }
53fad4b9
DM
159 if ($musthavesubmission) {
160 return $userswithsubmission;
161 } else {
162 return $users;
163 }
0968b1a3
DM
164 }
165
53fad4b9
DM
166 /**
167 * Returns all users with the capability mod/workshop:peerassess sorted by groups
168 *
169 * This takes the module grouping settings into account. If "Available for group members only"
170 * is set, returns only groups withing the course module grouping.
171 *
172 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible users otherwise.
173 * @return array array[groupid][userid] => stdClass{->id ->lastname ->firstname}
174 */
175 public function get_peer_reviewers_by_group($musthavesubmission=false) {
176 global $DB;
177
178 $reviewers = $this->get_peer_reviewers($musthavesubmission);
179 $greviewers = array(); // grouped reviewers to be returned
a7c5b918
DM
180 if (empty($reviewers)) {
181 return $greviewers;
182 }
53fad4b9
DM
183 if ($this->cm->groupmembersonly) {
184 // Available for group members only - the workshop is available only
185 // to users assigned to groups within the selected grouping, or to
186 // any group if no grouping is selected.
187 $groupingid = $this->cm->groupingid;
188 // All reviewers that are members of at least one group will be
189 // added into a virtual group id 0
190 $greviewers[0] = array();
191 } else {
192 $groupingid = 0;
193 // there is no need to be member of a group so $greviewers[0] will contain
194 // all reviewers
195 $greviewers[0] = $reviewers;
196 }
197 $gmemberships = groups_get_all_groups($this->cm->course, array_keys($reviewers), $groupingid,
198 'gm.id,gm.groupid,gm.userid');
199 foreach ($gmemberships as $gmembership) {
200 if (!isset($greviewers[$gmembership->groupid])) {
201 $greviewers[$gmembership->groupid] = array();
202 }
203 $greviewers[$gmembership->groupid][$gmembership->userid] = $reviewers[$gmembership->userid];
204 $greviewers[0][$gmembership->userid] = $reviewers[$gmembership->userid];
205 }
206 return $greviewers;
207 }
6e309973
DM
208
209 /**
210 * Returns submissions from this workshop
211 *
212 * Fetches data from {workshop_submissions} and adds some useful information from other
213 * tables.
53fad4b9
DM
214 *
215 * @param mixed $userid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only
216 * @param mixed $examples false|true|'all' Only regular submissions, only examples, all submissions
6e309973
DM
217 * @todo unittest
218 * @return object moodle_recordset
219 */
66c9894d 220 public function get_submissions_recordset($userid='all', $examples=false) {
6e309973
DM
221 global $DB;
222
223 $sql = 'SELECT s.*, u.lastname AS authorlastname, u.firstname AS authorfirstname
224 FROM {workshop_submissions} s
225 JOIN {user} u ON (s.userid = u.id)
226 WHERE s.workshopid = ?';
227 $params[0] = $this->id;
228
229 if ($examples === false) {
230 $sql .= ' AND example = 0';
231 }
232 if ($examples === true) {
233 $sql .= ' AND example = 1';
234 }
0dc47fb9 235 if (is_numeric($userid)) {
6e309973
DM
236 $sql .= ' AND userid = ?';
237 $params = array_merge($params, array($userid));
238 }
239 if (is_array($userid)) {
240 list($usql, $uparams) = $DB->get_in_or_equal($userid);
241 $sql .= ' AND userid ' . $usql;
242 $params = array_merge($params, $uparams);
243 }
244
245 return $DB->get_recordset_sql($sql, $params);
246 }
247
53fad4b9
DM
248 /**
249 * Returns a submission submitted by the given author or authors.
250 *
251 * This is intended for regular workshop participants, not for example submissions by teachers.
252 * If an array of authors is provided, returns array of stripped submission records so they do not
253 * include text fields (to prevent possible memory-lack issues).
254 *
255 * @param mixed $id integer|array author ID or IDs
256 * @return mixed false if not found, object if $id is int, array if $id is array
257 */
258 public function get_submission_by_author($id) {
259 if (empty($id)) {
260 return false;
261 }
262 $rs = $this->get_submissions_recordset($id, false);
0dc47fb9 263 if (is_numeric($id)) {
53fad4b9
DM
264 $submission = $rs->current();
265 $rs->close();
266 if (empty($submission->id)) {
267 return false;
268 } else {
269 return $submission;
270 }
271 } elseif (is_array($id)) {
272 $submissions = array();
273 foreach ($rs as $submission) {
274 $submissions[$submission->id] = new stdClass();
275 foreach ($submission as $property => $value) {
276 // we do not want text fields here to prevent possible memory issues
277 if (in_array($property, array('id', 'workshopid', 'example', 'userid', 'authorlastname', 'authorfirstname',
278 'timecreated', 'timemodified', 'grade', 'gradeover', 'gradeoverby', 'gradinggrade'))) {
279 $submissions[$submission->id]->{$property} = $value;
280 }
281 }
282 }
283 return $submissions;
284 } else {
285 throw new moodle_workshop_exception($this, 'wrongparameter');
286 }
287 }
6e309973
DM
288
289 /**
290 * Returns the list of assessments with some data added
291 *
292 * Fetches data from {workshop_assessments} and adds some useful information from other
293 * tables.
294 *
295 * @param mixed $reviewerid 'all'|int|array User ID of the reviewer
296 * @param mixed $id 'all'|int Assessment ID
297 * @return object moodle_recordset
298 */
66c9894d 299 public function get_assessments_recordset($reviewerid='all', $id='all') {
6e309973 300 global $DB;
53fad4b9 301
6e309973
DM
302 $sql = 'SELECT a.*,
303 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname as reviewerlastname,
304 s.title,
305 author.id AS authorid, author.firstname AS authorfirstname,author.lastname as authorlastname
306 FROM {workshop_assessments} a
53fad4b9
DM
307 INNER JOIN {user} reviewer ON (a.userid = reviewer.id)
308 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
309 INNER JOIN {user} author ON (s.userid = author.id)
6e309973
DM
310 WHERE s.workshopid = ?';
311 $params = array($this->id);
0dc47fb9
DM
312 if (is_numeric($reviewerid)) {
313 $sql .= ' AND reviewer.id = ?';
6e309973
DM
314 $params = array_merge($params, array($reviewerid));
315 }
316 if (is_array($reviewerid)) {
317 list($usql, $uparams) = $DB->get_in_or_equal($reviewerid);
0dc47fb9 318 $sql .= ' AND reviewer.id ' . $usql;
6e309973
DM
319 $params = array_merge($params, $uparams);
320 }
0dc47fb9 321 if (is_numeric($id)) {
6e309973
DM
322 $sql .= ' AND a.id = ?';
323 $params = array_merge($params, array($id));
324 }
325
326 return $DB->get_recordset_sql($sql, $params);
327 }
328
53fad4b9
DM
329 /**
330 * Returns the list of assessments with some data added
331 *
332 * Fetches data from {workshop_assessments} and adds some useful information from other
333 * tables. The returned objects are lightweight version of those returned by get_assessments_recordset(),
334 * mainly they do not contain text fields.
335 *
336 * @param mixed $reviewerid 'all'|int|array User ID of the reviewer
da92436b 337 * @return array [assessmentid] => assessment object
53fad4b9
DM
338 * @see workshop_api::get_assessments_recordset() for the structure of returned objects
339 */
340 public function get_assessments($reviewerid='all') {
341 $rs = $this->get_assessments_recordset($reviewerid, 'all');
342 $assessments = array();
343 foreach ($rs as $assessment) {
344 // copy selected properties into the array to be returned. This is here mainly in order not
345 // to include text comments.
346 $assessments[$assessment->id] = new stdClass;
347 foreach ($assessment as $property => $value) {
348 if (in_array($property, array('id', 'submissionid', 'userid', 'timecreated', 'timemodified',
349 'timeagreed', 'grade', 'gradinggrade', 'gradinggradeover', 'gradinggradeoverby',
350 'reviewerid', 'reviewerfirstname', 'reviewerlastname', 'title', 'authorid',
351 'authorfirstname', 'authorlastname'))) {
352 $assessments[$assessment->id]->{$property} = $value;
353 }
354 }
355 }
356 $rs->close();
357 return $assessments;
358 }
359
360 /**
361 * Get the information about the given assessment
362 *
363 * @param int $id Assessment ID
364 * @see workshop_api::get_assessments_recordset() for the structure of data returned
365 * @return mixed false if not found, object otherwise
366 */
367 public function get_assessment_by_id($id) {
368 $rs = $this->get_assessments_recordset('all', $id);
369 $assessment = $rs->current();
370 $rs->close();
371 if (empty($assessment->id)) {
372 return false;
373 } else {
374 return $assessment;
375 }
376 }
377
378 /**
379 * Get the information about all assessments assigned to the given reviewer
380 *
381 * @param int $id Reviewer ID
382 * @see workshop_api::get_assessments_recordset() for the structure of data returned
383 * @return array array of objects
384 */
385 public function get_assessments_by_reviewer($id) {
386 $rs = $this->get_assessments_recordset($id);
387 $assessments = array();
388 foreach ($rs as $assessment) {
389 $assessments[$assessment->id] = $assessment;
390 }
391 $rs->close();
392 return $assessment;
393 }
6e309973
DM
394
395 /**
396 * Returns the list of allocations in the workshop
397 *
398 * This returns the list of all users who can submit their work or review submissions (or both
399 * which is the common case). So basically this is to return list of all students participating
400 * in the workshop. For every participant, it adds information about their submission and their
53fad4b9 401 * reviews.
6e309973
DM
402 *
403 * The returned structure is recordset of objects with following properties:
404 * [authorid] [authorfirstname] [authorlastname] [authorpicture] [authorimagealt]
405 * [submissionid] [submissiontitle] [submissiongrade] [assessmentid]
53fad4b9 406 * [timeallocated] [reviewerid] [reviewerfirstname] [reviewerlastname]
6e309973
DM
407 * [reviewerpicture] [reviewerimagealt]
408 *
409 * This should be refactored when capability handling proposed by Petr is implemented so that
410 * we can check capabilities directly in SQL joins.
53fad4b9
DM
411 * Note that the returned recordset includes participants without submission as well as those
412 * without any review allocated yet.
6e309973
DM
413 *
414 * @return object moodle_recordset
415 */
66c9894d 416 public function get_allocations_recordset() {
6e309973
DM
417 global $DB;
418 static $users=null;
419
420 if (is_null($users)) {
421 $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
422 $users = get_users_by_capability($context, array('mod/workshop:submit', 'mod/workshop:peerassess'),
423 'u.id', 'u.lastname,u.firstname', '', '', '', '', false, false, true);
424 }
425
426 list($usql, $params) = $DB->get_in_or_equal(array_keys($users));
427 $params[] = $this->id;
428
53fad4b9 429 $sql = 'SELECT author.id AS authorid, author.firstname AS authorfirstname, author.lastname AS authorlastname,
6e309973 430 author.picture AS authorpicture, author.imagealt AS authorimagealt,
53fad4b9
DM
431 s.id AS submissionid, s.title AS submissiontitle, s.grade AS submissiongrade,
432 a.id AS assessmentid, a.timecreated AS timeallocated, a.userid AS reviewerid,
6e309973
DM
433 reviewer.firstname AS reviewerfirstname, reviewer.lastname AS reviewerlastname,
434 reviewer.picture as reviewerpicture, reviewer.imagealt AS reviewerimagealt
435 FROM {user} author
436 LEFT JOIN {workshop_submissions} s ON (s.userid = author.id)
437 LEFT JOIN {workshop_assessments} a ON (s.id = a.submissionid)
438 LEFT JOIN {user} reviewer ON (a.userid = reviewer.id)
439 WHERE author.id ' . $usql . ' AND (s.workshopid = ? OR s.workshopid IS NULL)
440 ORDER BY author.lastname,author.firstname,reviewer.lastname,reviewer.firstname';
441 return $DB->get_recordset_sql($sql, $params);
442 }
443
6e309973
DM
444 /**
445 * Allocate a submission to a user for review
53fad4b9 446 *
6e309973
DM
447 * @param object $submission Submission record
448 * @param int $reviewerid User ID
53fad4b9 449 * @param bool $bulk repeated inserts into DB expected
6e309973
DM
450 * @return int ID of the new assessment or an error code
451 */
53fad4b9 452 public function add_allocation(stdClass $submission, $reviewerid, $bulk=false) {
6e309973
DM
453 global $DB;
454
455 if ($DB->record_exists('workshop_assessments', array('submissionid' => $submission->id, 'userid' => $reviewerid))) {
456 return WORKSHOP_ALLOCATION_EXISTS;
457 }
458
6e309973
DM
459 $now = time();
460 $assessment = new stdClass();
53fad4b9 461 $assessment->submissionid = $submission->id;
6e309973
DM
462 $assessment->userid = $reviewerid;
463 $assessment->timecreated = $now;
464 $assessment->timemodified = $now;
465
53fad4b9 466 return $DB->insert_record('workshop_assessments', $assessment, true, $bulk);
6e309973
DM
467 }
468
6e309973 469 /**
53fad4b9 470 * Delete assessment record or records
6e309973 471 *
53fad4b9
DM
472 * @param mixed $id int|array assessment id or array of assessments ids
473 * @return bool false if $id not a valid parameter, true otherwise
6e309973
DM
474 */
475 public function delete_assessment($id) {
476 global $DB;
477
478 // todo remove all given grades from workshop_grades;
6e309973 479
53fad4b9
DM
480 if (is_numeric($id)) {
481 return $DB->delete_records('workshop_assessments', array('id' => $id));
482 }
483 if (is_array($id)) {
484 return $DB->delete_records_list('workshop_assessments', 'id', $id);
485 }
486 return false;
487 }
6e309973
DM
488
489 /**
490 * Returns instance of grading strategy class
53fad4b9 491 *
6e309973
DM
492 * @return object Instance of a grading strategy
493 */
494 public function grading_strategy_instance() {
6e309973 495 if (is_null($this->strategy_api)) {
0dc47fb9 496 $strategylib = dirname(__FILE__) . '/grading/' . $this->strategy . '/strategy.php';
6e309973
DM
497 if (is_readable($strategylib)) {
498 require_once($strategylib);
499 } else {
53fad4b9 500 throw new moodle_workshop_exception($this, 'missingstrategy');
6e309973 501 }
0dc47fb9 502 $classname = 'workshop_' . $this->strategy . '_strategy';
6e309973
DM
503 $this->strategy_api = new $classname($this);
504 if (!in_array('workshop_strategy', class_implements($this->strategy_api))) {
505 throw new moodle_workshop_exception($this, 'strategynotimplemented');
506 }
507 }
6e309973
DM
508 return $this->strategy_api;
509 }
510
66c9894d
DM
511 /**
512 * Return list of available allocation methods
513 *
514 * @return array Array ['string' => 'string'] of localized allocation method names
515 */
516 public function installed_allocators() {
66c9894d
DM
517 $installed = get_list_of_plugins('mod/workshop/allocation');
518 $forms = array();
519 foreach ($installed as $allocation) {
520 $forms[$allocation] = get_string('allocation' . $allocation, 'workshop');
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 *
66c9894d
DM
534 * @param object $method The name of the allocation method, must be PARAM_ALPHA
535 * @return object Instance of submissions allocator
536 */
537 public function allocator_instance($method) {
66c9894d
DM
538 $allocationlib = dirname(__FILE__) . '/allocation/' . $method . '/allocator.php';
539 if (is_readable($allocationlib)) {
540 require_once($allocationlib);
541 } else {
53fad4b9 542 throw new moodle_workshop_exception($this, 'missingallocator');
66c9894d
DM
543 }
544 $classname = 'workshop_' . $method . '_allocator';
545 return new $classname($this);
546 }
547
66c9894d 548}
6e309973 549
de811c0c 550/**
6e309973
DM
551 * Class for workshop exceptions. Just saves a couple of arguments of the
552 * constructor for a moodle_exception.
de811c0c 553 *
6e309973
DM
554 * @param object $workshop Should be workshop or its subclass
555 * @param string $errorcode
556 * @param mixed $a Object/variable to pass to get_string
557 * @param string $link URL to continue after the error notice
558 * @param $debuginfo
de811c0c 559 */
6e309973 560class moodle_workshop_exception extends moodle_exception {
de811c0c 561
6e309973
DM
562 function __construct($workshop, $errorcode, $a = NULL, $link = '', $debuginfo = null) {
563 global $CFG;
564
565 if (!$link) {
566 $link = $CFG->wwwroot . '/mod/workshop/view.php?a=' . $workshop->id;
567 }
568 if ('confirmsesskeybad' == $errorcode) {
569 $module = '';
570 } else {
571 $module = 'workshop';
572 }
573 parent::__construct($errorcode, $module, $link, $a, $debuginfo);
574 }
de811c0c 575}