MDL-28298 workshop: do not use a constant without explicit inclusion of the library
[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 *
65601f04
DM
26 * @package mod
27 * @subpackage workshop
28 * @copyright 2009 David Mudrak <david.mudrak@gmail.com>
29 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
de811c0c
DM
30 */
31
32defined('MOODLE_INTERNAL') || die();
33
89c1aa97
DM
34require_once(dirname(__FILE__).'/lib.php'); // we extend this library here
35require_once($CFG->libdir . '/gradelib.php'); // we use some rounding and comparing routines here
99d19c13 36require_once($CFG->libdir . '/filelib.php');
0968b1a3 37
6e309973
DM
38/**
39 * Full-featured workshop API
40 *
a39d7d87 41 * This wraps the workshop database record with a set of methods that are called
6e309973 42 * from the module itself. The class should be initialized right after you get
a39d7d87 43 * $workshop, $cm and $course records at the begining of the script.
6e309973 44 */
a39d7d87
DM
45class workshop {
46
b761e6d9 47 /** return statuses of {@link add_allocation} to be passed to a workshop renderer method */
cbf87967
DM
48 const ALLOCATION_EXISTS = -9999;
49 const ALLOCATION_ERROR = -9998;
b761e6d9
DM
50
51 /** the internal code of the workshop phases as are stored in the database */
f6e8b318
DM
52 const PHASE_SETUP = 10;
53 const PHASE_SUBMISSION = 20;
54 const PHASE_ASSESSMENT = 30;
55 const PHASE_EVALUATION = 40;
56 const PHASE_CLOSED = 50;
57
58 /** the internal code of the examples modes as are stored in the database */
59 const EXAMPLES_VOLUNTARY = 0;
60 const EXAMPLES_BEFORE_SUBMISSION = 1;
61 const EXAMPLES_BEFORE_ASSESSMENT = 2;
b761e6d9 62
7a789aa8 63 /** @var stdclass course module record */
e9ab520f 64 public $cm;
a39d7d87 65
7a789aa8 66 /** @var stdclass course record */
e9ab520f 67 public $course;
6e309973 68
7a789aa8 69 /** @var stdclass context object */
e9ab520f
DM
70 public $context;
71
72 /** @var int workshop instance identifier */
73 public $id;
74
75 /** @var string workshop activity name */
76 public $name;
77
78 /** @var string introduction or description of the activity */
79 public $intro;
80
81 /** @var int format of the {@link $intro} */
82 public $introformat;
83
84 /** @var string instructions for the submission phase */
85 public $instructauthors;
86
87 /** @var int format of the {@link $instructauthors} */
88 public $instructauthorsformat;
89
90 /** @var string instructions for the assessment phase */
91 public $instructreviewers;
92
93 /** @var int format of the {@link $instructreviewers} */
94 public $instructreviewersformat;
95
96 /** @var int timestamp of when the module was modified */
97 public $timemodified;
98
99 /** @var int current phase of workshop, for example {@link workshop::PHASE_SETUP} */
100 public $phase;
101
102 /** @var bool optional feature: students practise evaluating on example submissions from teacher */
103 public $useexamples;
104
105 /** @var bool optional feature: students perform peer assessment of others' work */
106 public $usepeerassessment;
107
108 /** @var bool optional feature: students perform self assessment of their own work */
109 public $useselfassessment;
110
111 /** @var float number (10, 5) unsigned, the maximum grade for submission */
112 public $grade;
113
114 /** @var float number (10, 5) unsigned, the maximum grade for assessment */
115 public $gradinggrade;
116
117 /** @var string type of the current grading strategy used in this workshop, for example 'accumulative' */
118 public $strategy;
119
c2d2eb6e
DM
120 /** @var string the name of the evaluation plugin to use for grading grades calculation */
121 public $evaluation;
122
e9ab520f
DM
123 /** @var int number of digits that should be shown after the decimal point when displaying grades */
124 public $gradedecimals;
125
126 /** @var int number of allowed submission attachments and the files embedded into submission */
127 public $nattachments;
128
129 /** @var bool allow submitting the work after the deadline */
130 public $latesubmissions;
131
132 /** @var int maximum size of the one attached file in bytes */
133 public $maxbytes;
134
135 /** @var int mode of example submissions support, for example {@link workshop::EXAMPLES_VOLUNTARY} */
136 public $examplesmode;
137
138 /** @var int if greater than 0 then the submission is not allowed before this timestamp */
139 public $submissionstart;
140
141 /** @var int if greater than 0 then the submission is not allowed after this timestamp */
142 public $submissionend;
143
144 /** @var int if greater than 0 then the peer assessment is not allowed before this timestamp */
145 public $assessmentstart;
146
147 /** @var int if greater than 0 then the peer assessment is not allowed after this timestamp */
148 public $assessmentend;
149
b761e6d9
DM
150 /**
151 * @var workshop_strategy grading strategy instance
152 * Do not use directly, get the instance using {@link workshop::grading_strategy_instance()}
153 */
b13142da
DM
154 protected $strategyinstance = null;
155
45d24d39
DM
156 /**
157 * @var workshop_evaluation grading evaluation instance
158 * Do not use directly, get the instance using {@link workshop::grading_evaluation_instance()}
159 */
160 protected $evaluationinstance = null;
161
6e309973 162 /**
65ba104c 163 * Initializes the workshop API instance using the data from DB
a39d7d87
DM
164 *
165 * Makes deep copy of all passed records properties. Replaces integer $course attribute
166 * with a full database record (course should not be stored in instances table anyway).
6e309973 167 *
5924db72
PS
168 * @param stdClass $dbrecord Workshop instance data from {workshop} table
169 * @param stdClass $cm Course module record as returned by {@link get_coursemodule_from_id()}
170 * @param stdClass $course Course record from {course} table
171 * @param stdClass $context The context of the workshop instance
0dc47fb9 172 */
7a789aa8 173 public function __construct(stdclass $dbrecord, stdclass $cm, stdclass $course, stdclass $context=null) {
f05c168d 174 foreach ($dbrecord as $field => $value) {
eef46586 175 if (property_exists('workshop', $field)) {
ac239eba
DM
176 $this->{$field} = $value;
177 }
a39d7d87 178 }
45d24d39 179 $this->cm = $cm;
ac239eba 180 $this->course = $course;
4efd7b5d
DM
181 if (is_null($context)) {
182 $this->context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
183 } else {
184 $this->context = $context;
185 }
186 $this->evaluation = 'best'; // todo make this configurable although we have no alternatives yet
6e309973
DM
187 }
188
aa40adbf
DM
189 ////////////////////////////////////////////////////////////////////////////////
190 // Static methods //
191 ////////////////////////////////////////////////////////////////////////////////
192
da0b1f70 193 /**
aa40adbf 194 * Return list of available allocation methods
da0b1f70 195 *
aa40adbf 196 * @return array Array ['string' => 'string'] of localized allocation method names
da0b1f70 197 */
aa40adbf
DM
198 public static function installed_allocators() {
199 $installed = get_plugin_list('workshopallocation');
200 $forms = array();
201 foreach ($installed as $allocation => $allocationpath) {
202 if (file_exists($allocationpath . '/lib.php')) {
203 $forms[$allocation] = get_string('pluginname', 'workshopallocation_' . $allocation);
204 }
f05c168d 205 }
aa40adbf
DM
206 // usability - make sure that manual allocation appears the first
207 if (isset($forms['manual'])) {
208 $m = array('manual' => $forms['manual']);
209 unset($forms['manual']);
210 $forms = array_merge($m, $forms);
da0b1f70 211 }
aa40adbf
DM
212 return $forms;
213 }
da0b1f70 214
aa40adbf
DM
215 /**
216 * Returns an array of options for the editors that are used for submitting and assessing instructions
217 *
5924db72 218 * @param stdClass $context
c8ea2c45 219 * @uses EDITOR_UNLIMITED_FILES hard-coded value for the 'maxfiles' option
aa40adbf
DM
220 * @return array
221 */
7a789aa8 222 public static function instruction_editors_options(stdclass $context) {
c8ea2c45 223 return array('subdirs' => 1, 'maxbytes' => 0, 'maxfiles' => -1,
aa40adbf 224 'changeformat' => 1, 'context' => $context, 'noclean' => 1, 'trusttext' => 0);
da0b1f70
DM
225 }
226
61b737a5
DM
227 /**
228 * Given the percent and the total, returns the number
229 *
230 * @param float $percent from 0 to 100
231 * @param float $total the 100% value
232 * @return float
233 */
234 public static function percent_to_value($percent, $total) {
235 if ($percent < 0 or $percent > 100) {
236 throw new coding_exception('The percent can not be less than 0 or higher than 100');
237 }
238
239 return $total * $percent / 100;
240 }
241
f6e8b318
DM
242 /**
243 * Returns an array of numeric values that can be used as maximum grades
244 *
245 * @return array Array of integers
246 */
247 public static function available_maxgrades_list() {
248 $grades = array();
249 for ($i=100; $i>=0; $i--) {
250 $grades[$i] = $i;
251 }
252 return $grades;
253 }
254
255 /**
256 * Returns the localized list of supported examples modes
257 *
258 * @return array
259 */
260 public static function available_example_modes_list() {
261 $options = array();
262 $options[self::EXAMPLES_VOLUNTARY] = get_string('examplesvoluntary', 'workshop');
263 $options[self::EXAMPLES_BEFORE_SUBMISSION] = get_string('examplesbeforesubmission', 'workshop');
264 $options[self::EXAMPLES_BEFORE_ASSESSMENT] = get_string('examplesbeforeassessment', 'workshop');
265 return $options;
266 }
267
268 /**
269 * Returns the list of available grading strategy methods
270 *
271 * @return array ['string' => 'string']
272 */
273 public static function available_strategies_list() {
274 $installed = get_plugin_list('workshopform');
275 $forms = array();
276 foreach ($installed as $strategy => $strategypath) {
277 if (file_exists($strategypath . '/lib.php')) {
278 $forms[$strategy] = get_string('pluginname', 'workshopform_' . $strategy);
279 }
280 }
281 return $forms;
282 }
283
284 /**
285 * Return an array of possible values of assessment dimension weight
286 *
287 * @return array of integers 0, 1, 2, ..., 16
288 */
289 public static function available_dimension_weights_list() {
290 $weights = array();
291 for ($i=16; $i>=0; $i--) {
292 $weights[$i] = $i;
293 }
294 return $weights;
295 }
296
c6b784f0
DM
297 /**
298 * Return an array of possible values of assessment weight
299 *
300 * Note there is no real reason why the maximum value here is 16. It used to be 10 in
301 * workshop 1.x and I just decided to use the same number as in the maximum weight of
302 * a single assessment dimension.
303 * The value looks reasonable, though. Teachers who would want to assign themselves
304 * higher weight probably do not want peer assessment really...
305 *
306 * @return array of integers 0, 1, 2, ..., 16
307 */
308 public static function available_assessment_weights_list() {
309 $weights = array();
310 for ($i=16; $i>=0; $i--) {
311 $weights[$i] = $i;
312 }
313 return $weights;
314 }
315
f6e8b318
DM
316 /**
317 * Helper function returning the greatest common divisor
318 *
319 * @param int $a
320 * @param int $b
321 * @return int
322 */
323 public static function gcd($a, $b) {
324 return ($b == 0) ? ($a):(self::gcd($b, $a % $b));
325 }
326
327 /**
328 * Helper function returning the least common multiple
329 *
330 * @param int $a
331 * @param int $b
332 * @return int
333 */
334 public static function lcm($a, $b) {
335 return ($a / self::gcd($a,$b)) * $b;
336 }
337
5bab64a3
DM
338 /**
339 * Returns an object suitable for strings containing dates/times
340 *
341 * The returned object contains properties date, datefullshort, datetime, ... containing the given
342 * timestamp formatted using strftimedate, strftimedatefullshort, strftimedatetime, ... from the
343 * current lang's langconfig.php
344 * This allows translators and administrators customize the date/time format.
345 *
346 * @param int $timestamp the timestamp in UTC
347 * @return stdclass
348 */
349 public static function timestamp_formats($timestamp) {
350 $formats = array('date', 'datefullshort', 'dateshort', 'datetime',
351 'datetimeshort', 'daydate', 'daydatetime', 'dayshort', 'daytime',
352 'monthyear', 'recent', 'recentfull', 'time');
353 $a = new stdclass();
354 foreach ($formats as $format) {
355 $a->{$format} = userdate($timestamp, get_string('strftime'.$format, 'langconfig'));
356 }
357 $day = userdate($timestamp, '%Y%m%d', 99, false);
358 $today = userdate(time(), '%Y%m%d', 99, false);
359 $tomorrow = userdate(time() + DAYSECS, '%Y%m%d', 99, false);
360 $yesterday = userdate(time() - DAYSECS, '%Y%m%d', 99, false);
361 $distance = (int)round(abs(time() - $timestamp) / DAYSECS);
362 if ($day == $today) {
363 $a->distanceday = get_string('daystoday', 'workshop');
364 } elseif ($day == $yesterday) {
365 $a->distanceday = get_string('daysyesterday', 'workshop');
366 } elseif ($day < $today) {
367 $a->distanceday = get_string('daysago', 'workshop', $distance);
368 } elseif ($day == $tomorrow) {
369 $a->distanceday = get_string('daystomorrow', 'workshop');
370 } elseif ($day > $today) {
371 $a->distanceday = get_string('daysleft', 'workshop', $distance);
372 }
373 return $a;
374 }
375
aa40adbf
DM
376 ////////////////////////////////////////////////////////////////////////////////
377 // Workshop API //
378 ////////////////////////////////////////////////////////////////////////////////
379
6e309973
DM
380 /**
381 * Fetches all users with the capability mod/workshop:submit in the current context
382 *
3d2924e9 383 * The returned objects contain id, lastname and firstname properties and are ordered by lastname,firstname
53fad4b9 384 *
aa40adbf 385 * @todo handle with limits and groups
53fad4b9 386 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible authors otherwise.
7a789aa8 387 * @return array array[userid] => stdclass{->id ->lastname ->firstname}
6e309973 388 */
d895c6aa
DM
389 public function get_potential_authors($musthavesubmission=true) {
390 $users = get_users_by_capability($this->context, 'mod/workshop:submit',
1fed6ce3 391 'u.id,u.lastname,u.firstname', 'u.lastname,u.firstname,u.id', '', '', '', '', false, false, true);
3d2924e9 392 if ($musthavesubmission) {
da0b1f70 393 $users = array_intersect_key($users, $this->users_with_submission(array_keys($users)));
66c9894d 394 }
da0b1f70 395 return $users;
6e309973
DM
396 }
397
6e309973
DM
398 /**
399 * Fetches all users with the capability mod/workshop:peerassess in the current context
400 *
b13142da 401 * The returned objects contain id, lastname and firstname properties and are ordered by lastname,firstname
53fad4b9 402 *
aa40adbf 403 * @todo handle with limits and groups
53fad4b9 404 * @param bool $musthavesubmission If true, return only users who have already submitted. All possible users otherwise.
7a789aa8 405 * @return array array[userid] => stdclass{->id ->lastname ->firstname}
6e309973 406 */
d895c6aa
DM
407 public function get_potential_reviewers($musthavesubmission=false) {
408 $users = get_users_by_capability($this->context, 'mod/workshop:peerassess',
1fed6ce3 409 'u.id, u.lastname, u.firstname', 'u.lastname,u.firstname,u.id', '', '', '', '', false, false, true);
3d2924e9
DM
410 if ($musthavesubmission) {
411 // users without their own submission can not be reviewers
da0b1f70 412 $users = array_intersect_key($users, $this->users_with_submission(array_keys($users)));
0968b1a3 413 }
da0b1f70 414 return $users;
0968b1a3
DM
415 }
416
b8ead2e6
DM
417 /**
418 * Groups the given users by the group membership
419 *
420 * This takes the module grouping settings into account. If "Available for group members only"
421 * is set, returns only groups withing the course module grouping. Always returns group [0] with
422 * all the given users.
423 *
7a789aa8
DM
424 * @param array $users array[userid] => stdclass{->id ->lastname ->firstname}
425 * @return array array[groupid][userid] => stdclass{->id ->lastname ->firstname}
53fad4b9 426 */
3d2924e9 427 public function get_grouped($users) {
53fad4b9 428 global $DB;
3d2924e9 429 global $CFG;
53fad4b9 430
b8ead2e6
DM
431 $grouped = array(); // grouped users to be returned
432 if (empty($users)) {
433 return $grouped;
a7c5b918 434 }
98da6021 435 if (!empty($CFG->enablegroupmembersonly) and $this->cm->groupmembersonly) {
53fad4b9
DM
436 // Available for group members only - the workshop is available only
437 // to users assigned to groups within the selected grouping, or to
438 // any group if no grouping is selected.
439 $groupingid = $this->cm->groupingid;
b8ead2e6 440 // All users that are members of at least one group will be
53fad4b9 441 // added into a virtual group id 0
b8ead2e6 442 $grouped[0] = array();
53fad4b9
DM
443 } else {
444 $groupingid = 0;
b8ead2e6
DM
445 // there is no need to be member of a group so $grouped[0] will contain
446 // all users
447 $grouped[0] = $users;
53fad4b9 448 }
b8ead2e6 449 $gmemberships = groups_get_all_groups($this->cm->course, array_keys($users), $groupingid,
53fad4b9
DM
450 'gm.id,gm.groupid,gm.userid');
451 foreach ($gmemberships as $gmembership) {
b8ead2e6
DM
452 if (!isset($grouped[$gmembership->groupid])) {
453 $grouped[$gmembership->groupid] = array();
53fad4b9 454 }
b8ead2e6
DM
455 $grouped[$gmembership->groupid][$gmembership->userid] = $users[$gmembership->userid];
456 $grouped[0][$gmembership->userid] = $users[$gmembership->userid];
53fad4b9 457 }
b8ead2e6 458 return $grouped;
53fad4b9 459 }
6e309973 460
aa40adbf 461 /**
de6aaa72 462 * Returns the list of all allocations (i.e. assigned assessments) in the workshop
aa40adbf
DM
463 *
464 * Assessments of example submissions are ignored
465 *
466 * @return array
467 */
468 public function get_allocations() {
469 global $DB;
470
00aca3c1 471 $sql = 'SELECT a.id, a.submissionid, a.reviewerid, s.authorid
aa40adbf
DM
472 FROM {workshop_assessments} a
473 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
474 WHERE s.example = 0 AND s.workshopid = :workshopid';
475 $params = array('workshopid' => $this->id);
476
477 return $DB->get_records_sql($sql, $params);
478 }
479
6e309973
DM
480 /**
481 * Returns submissions from this workshop
482 *
3dc78e5b
DM
483 * Fetches data from {workshop_submissions} and adds some useful information from other
484 * tables. Does not return textual fields to prevent possible memory lack issues.
53fad4b9 485 *
00aca3c1 486 * @param mixed $authorid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only
934329e5 487 * @return array of records or an empty array
6e309973 488 */
29dc43e7 489 public function get_submissions($authorid='all') {
6e309973
DM
490 global $DB;
491
00aca3c1 492 $sql = 'SELECT s.id, s.workshopid, s.example, s.authorid, s.timecreated, s.timemodified,
232175e4 493 s.title, s.grade, s.gradeover, s.gradeoverby, s.published,
00aca3c1 494 u.lastname AS authorlastname, u.firstname AS authorfirstname,
3a11c09f 495 u.picture AS authorpicture, u.imagealt AS authorimagealt, u.email AS authoremail,
00aca3c1 496 t.lastname AS overlastname, t.firstname AS overfirstname,
3a11c09f 497 t.picture AS overpicture, t.imagealt AS overimagealt, t.email AS overemail
3d2924e9 498 FROM {workshop_submissions} s
00aca3c1 499 INNER JOIN {user} u ON (s.authorid = u.id)
29dc43e7
DM
500 LEFT JOIN {user} t ON (s.gradeoverby = t.id)
501 WHERE s.example = 0 AND s.workshopid = :workshopid';
3d2924e9 502 $params = array('workshopid' => $this->id);
6e309973 503
00aca3c1 504 if ('all' === $authorid) {
3d2924e9 505 // no additional conditions
934329e5 506 } elseif (!empty($authorid)) {
00aca3c1
DM
507 list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED);
508 $sql .= " AND authorid $usql";
6e309973 509 $params = array_merge($params, $uparams);
3d2924e9 510 } else {
934329e5
DM
511 // $authorid is empty
512 return array();
6e309973 513 }
3dc78e5b 514 $sql .= ' ORDER BY u.lastname, u.firstname';
6e309973 515
3dc78e5b 516 return $DB->get_records_sql($sql, $params);
6e309973
DM
517 }
518
51508f25
DM
519 /**
520 * Returns a submission record with the author's data
521 *
522 * @param int $id submission id
7a789aa8 523 * @return stdclass
51508f25
DM
524 */
525 public function get_submission_by_id($id) {
526 global $DB;
527
29dc43e7
DM
528 // we intentionally check the workshopid here, too, so the workshop can't touch submissions
529 // from other instances
51508f25
DM
530 $sql = 'SELECT s.*,
531 u.lastname AS authorlastname, u.firstname AS authorfirstname, u.id AS authorid,
3a11c09f 532 u.picture AS authorpicture, u.imagealt AS authorimagealt, u.email AS authoremail
51508f25 533 FROM {workshop_submissions} s
00aca3c1 534 INNER JOIN {user} u ON (s.authorid = u.id)
81eccf0a 535 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.id = :id';
51508f25
DM
536 $params = array('workshopid' => $this->id, 'id' => $id);
537 return $DB->get_record_sql($sql, $params, MUST_EXIST);
538 }
539
53fad4b9 540 /**
3dc78e5b 541 * Returns a submission submitted by the given author
53fad4b9 542 *
3dc78e5b 543 * @param int $id author id
7a789aa8 544 * @return stdclass|false
53fad4b9 545 */
00aca3c1 546 public function get_submission_by_author($authorid) {
e9b0f0ab
DM
547 global $DB;
548
00aca3c1 549 if (empty($authorid)) {
53fad4b9
DM
550 return false;
551 }
3dc78e5b
DM
552 $sql = 'SELECT s.*,
553 u.lastname AS authorlastname, u.firstname AS authorfirstname, u.id AS authorid,
3a11c09f 554 u.picture AS authorpicture, u.imagealt AS authorimagealt, u.email AS authoremail
3dc78e5b 555 FROM {workshop_submissions} s
00aca3c1
DM
556 INNER JOIN {user} u ON (s.authorid = u.id)
557 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.authorid = :authorid';
558 $params = array('workshopid' => $this->id, 'authorid' => $authorid);
3dc78e5b 559 return $DB->get_record_sql($sql, $params);
53fad4b9 560 }
6e309973 561
00bc77ee
DM
562 /**
563 * Returns published submissions with their authors data
564 *
565 * @return array of stdclass
566 */
567 public function get_published_submissions($orderby='finalgrade DESC') {
568 global $DB;
569
570 $sql = "SELECT s.id, s.authorid, s.timecreated, s.timemodified,
571 s.title, s.grade, s.gradeover, COALESCE(s.gradeover,s.grade) AS finalgrade,
572 u.lastname AS authorlastname, u.firstname AS authorfirstname, u.id AS authorid,
3a11c09f 573 u.picture AS authorpicture, u.imagealt AS authorimagealt, u.email AS authoremail
00bc77ee
DM
574 FROM {workshop_submissions} s
575 INNER JOIN {user} u ON (s.authorid = u.id)
576 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.published = 1
577 ORDER BY $orderby";
578 $params = array('workshopid' => $this->id);
579 return $DB->get_records_sql($sql, $params);
580 }
581
81eccf0a
DM
582 /**
583 * Returns full record of the given example submission
584 *
585 * @param int $id example submission od
586 * @return object
587 */
588 public function get_example_by_id($id) {
589 global $DB;
590 return $DB->get_record('workshop_submissions',
591 array('id' => $id, 'workshopid' => $this->id, 'example' => 1), '*', MUST_EXIST);
592 }
593
cbf87967
DM
594 /**
595 * Returns the list of example submissions in this workshop with reference assessments attached
596 *
597 * @return array of objects or an empty array
598 * @see workshop::prepare_example_summary()
599 */
600 public function get_examples_for_manager() {
601 global $DB;
602
603 $sql = 'SELECT s.id, s.title,
81b22887 604 a.id AS assessmentid, a.grade, a.gradinggrade
cbf87967
DM
605 FROM {workshop_submissions} s
606 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id AND a.weight = 1)
607 WHERE s.example = 1 AND s.workshopid = :workshopid
608 ORDER BY s.title';
609 return $DB->get_records_sql($sql, array('workshopid' => $this->id));
610 }
611
612 /**
613 * Returns the list of all example submissions in this workshop with the information of assessments done by the given user
614 *
615 * @param int $reviewerid user id
616 * @return array of objects, indexed by example submission id
617 * @see workshop::prepare_example_summary()
618 */
619 public function get_examples_for_reviewer($reviewerid) {
620 global $DB;
621
622 if (empty($reviewerid)) {
623 return false;
624 }
625 $sql = 'SELECT s.id, s.title,
81b22887 626 a.id AS assessmentid, a.grade, a.gradinggrade
cbf87967
DM
627 FROM {workshop_submissions} s
628 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id AND a.reviewerid = :reviewerid AND a.weight = 0)
629 WHERE s.example = 1 AND s.workshopid = :workshopid
630 ORDER BY s.title';
631 return $DB->get_records_sql($sql, array('workshopid' => $this->id, 'reviewerid' => $reviewerid));
632 }
633
634 /**
81b22887
DM
635 * Prepares renderable submission component
636 *
637 * @param stdClass $record required by {@see workshop_submission}
638 * @param bool $showauthor show the author-related information
639 * @return workshop_submission
640 */
641 public function prepare_submission(stdClass $record, $showauthor = false) {
642
643 $submission = new workshop_submission($record, $showauthor);
644 $submission->url = $this->submission_url($record->id);
645
646 return $submission;
647 }
648
649 /**
650 * Prepares renderable submission summary component
651 *
652 * @param stdClass $record required by {@see workshop_submission_summary}
653 * @param bool $showauthor show the author-related information
654 * @return workshop_submission_summary
655 */
656 public function prepare_submission_summary(stdClass $record, $showauthor = false) {
657
658 $summary = new workshop_submission_summary($record, $showauthor);
659 $summary->url = $this->submission_url($record->id);
660
661 return $summary;
662 }
663
664 /**
665 * Prepares renderable example submission component
666 *
667 * @param stdClass $record required by {@see workshop_example_submission}
668 * @return workshop_example_submission
669 */
670 public function prepare_example_submission(stdClass $record) {
671
672 $example = new workshop_example_submission($record);
673
674 return $example;
675 }
676
677 /**
678 * Prepares renderable example submission summary component
679 *
680 * If the example is editable, the caller must set the 'editable' flag explicitly.
cbf87967 681 *
5924db72 682 * @param stdClass $example as returned by {@link workshop::get_examples_for_manager()} or {@link workshop::get_examples_for_reviewer()}
81b22887 683 * @return workshop_example_submission_summary to be rendered
cbf87967 684 */
81b22887
DM
685 public function prepare_example_summary(stdClass $example) {
686
687 $summary = new workshop_example_submission_summary($example);
cbf87967 688
cbf87967 689 if (is_null($example->grade)) {
81b22887
DM
690 $summary->status = 'notgraded';
691 $summary->assesslabel = get_string('assess', 'workshop');
cbf87967 692 } else {
81b22887
DM
693 $summary->status = 'graded';
694 $summary->assesslabel = get_string('reassess', 'workshop');
cbf87967
DM
695 }
696
7a789aa8 697 $summary->gradeinfo = new stdclass();
cbf87967
DM
698 $summary->gradeinfo->received = $this->real_grade($example->grade);
699 $summary->gradeinfo->max = $this->real_grade(100);
700
81b22887
DM
701 $summary->url = new moodle_url($this->exsubmission_url($example->id));
702 $summary->editurl = new moodle_url($this->exsubmission_url($example->id), array('edit' => 'on'));
703 $summary->assessurl = new moodle_url($this->exsubmission_url($example->id), array('assess' => 'on', 'sesskey' => sesskey()));
cbf87967
DM
704
705 return $summary;
706 }
707
81eccf0a
DM
708 /**
709 * Removes the submission and all relevant data
710 *
5924db72 711 * @param stdClass $submission record to delete
81eccf0a
DM
712 * @return void
713 */
7a789aa8 714 public function delete_submission(stdclass $submission) {
81eccf0a
DM
715 global $DB;
716 $assessments = $DB->get_records('workshop_assessments', array('submissionid' => $submission->id), '', 'id');
717 $this->delete_assessment(array_keys($assessments));
718 $DB->delete_records('workshop_submissions', array('id' => $submission->id));
719 }
720
6e309973 721 /**
3dc78e5b 722 * Returns the list of all assessments in the workshop with some data added
6e309973
DM
723 *
724 * Fetches data from {workshop_assessments} and adds some useful information from other
de6aaa72 725 * tables. The returned object does not contain textual fields (i.e. comments) to prevent memory
3dc78e5b
DM
726 * lack issues.
727 *
7a789aa8 728 * @return array [assessmentid] => assessment stdclass
6e309973 729 */
3dc78e5b 730 public function get_all_assessments() {
6e309973 731 global $DB;
53fad4b9 732
f6e8b318 733 $sql = 'SELECT a.id, a.submissionid, a.reviewerid, a.timecreated, a.timemodified,
3dc78e5b 734 a.grade, a.gradinggrade, a.gradinggradeover, a.gradinggradeoverby,
3d2924e9
DM
735 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname as reviewerlastname,
736 s.title,
ddb59c77 737 author.id AS authorid, author.firstname AS authorfirstname,author.lastname AS authorlastname
3d2924e9 738 FROM {workshop_assessments} a
00aca3c1 739 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
3d2924e9 740 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
00aca3c1 741 INNER JOIN {user} author ON (s.authorid = author.id)
3dc78e5b
DM
742 WHERE s.workshopid = :workshopid AND s.example = 0
743 ORDER BY reviewer.lastname, reviewer.firstname';
3d2924e9
DM
744 $params = array('workshopid' => $this->id);
745
3dc78e5b 746 return $DB->get_records_sql($sql, $params);
53fad4b9
DM
747 }
748
749 /**
3dc78e5b 750 * Get the complete information about the given assessment
53fad4b9
DM
751 *
752 * @param int $id Assessment ID
5a372494 753 * @return stdclass
53fad4b9
DM
754 */
755 public function get_assessment_by_id($id) {
3dc78e5b
DM
756 global $DB;
757
758 $sql = 'SELECT a.*,
759 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname as reviewerlastname,
760 s.title,
761 author.id AS authorid, author.firstname AS authorfirstname,author.lastname as authorlastname
762 FROM {workshop_assessments} a
00aca3c1 763 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
3dc78e5b 764 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
00aca3c1 765 INNER JOIN {user} author ON (s.authorid = author.id)
3dc78e5b
DM
766 WHERE a.id = :id AND s.workshopid = :workshopid';
767 $params = array('id' => $id, 'workshopid' => $this->id);
768
769 return $DB->get_record_sql($sql, $params, MUST_EXIST);
53fad4b9
DM
770 }
771
5a372494
DM
772 /**
773 * Get the complete information about the user's assessment of the given submission
774 *
775 * @param int $sid submission ID
776 * @param int $uid user ID of the reviewer
777 * @return false|stdclass false if not found, stdclass otherwise
778 */
779 public function get_assessment_of_submission_by_user($submissionid, $reviewerid) {
780 global $DB;
781
782 $sql = 'SELECT a.*,
783 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname as reviewerlastname,
784 s.title,
785 author.id AS authorid, author.firstname AS authorfirstname,author.lastname as authorlastname
786 FROM {workshop_assessments} a
787 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
788 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0)
789 INNER JOIN {user} author ON (s.authorid = author.id)
790 WHERE s.id = :sid AND reviewer.id = :rid AND s.workshopid = :workshopid';
791 $params = array('sid' => $submissionid, 'rid' => $reviewerid, 'workshopid' => $this->id);
792
793 return $DB->get_record_sql($sql, $params, IGNORE_MISSING);
794 }
795
796 /**
797 * Get the complete information about all assessments of the given submission
798 *
799 * @param int $submissionid
800 * @return array
801 */
802 public function get_assessments_of_submission($submissionid) {
803 global $DB;
804
805 $sql = 'SELECT a.*,
806 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname AS reviewerlastname
807 FROM {workshop_assessments} a
808 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
809 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
810 WHERE s.example = 0 AND s.id = :submissionid AND s.workshopid = :workshopid';
811 $params = array('submissionid' => $submissionid, 'workshopid' => $this->id);
812
813 return $DB->get_records_sql($sql, $params);
814 }
815
53fad4b9 816 /**
3dc78e5b 817 * Get the complete information about all assessments allocated to the given reviewer
53fad4b9 818 *
00aca3c1 819 * @param int $reviewerid
3dc78e5b 820 * @return array
53fad4b9 821 */
00aca3c1 822 public function get_assessments_by_reviewer($reviewerid) {
3dc78e5b
DM
823 global $DB;
824
825 $sql = 'SELECT a.*,
ddb59c77
DM
826 reviewer.id AS reviewerid,reviewer.firstname AS reviewerfirstname,reviewer.lastname AS reviewerlastname,
827 s.id AS submissionid, s.title AS submissiontitle, s.timecreated AS submissioncreated,
828 s.timemodified AS submissionmodified,
829 author.id AS authorid, author.firstname AS authorfirstname,author.lastname AS authorlastname,
3a11c09f 830 author.picture AS authorpicture, author.imagealt AS authorimagealt, author.email AS authoremail
3dc78e5b 831 FROM {workshop_assessments} a
00aca3c1 832 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
3dc78e5b 833 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
00aca3c1
DM
834 INNER JOIN {user} author ON (s.authorid = author.id)
835 WHERE s.example = 0 AND reviewer.id = :reviewerid AND s.workshopid = :workshopid';
836 $params = array('reviewerid' => $reviewerid, 'workshopid' => $this->id);
3dc78e5b
DM
837
838 return $DB->get_records_sql($sql, $params);
53fad4b9 839 }
6e309973 840
6e309973
DM
841 /**
842 * Allocate a submission to a user for review
53fad4b9 843 *
5924db72 844 * @param stdClass $submission Submission object with at least id property
6e309973 845 * @param int $reviewerid User ID
becec954 846 * @param int $weight of the new assessment, from 0 to 16
67ae13d9 847 * @param bool $bulk repeated inserts into DB expected
6e309973
DM
848 * @return int ID of the new assessment or an error code
849 */
67ae13d9 850 public function add_allocation(stdclass $submission, $reviewerid, $weight=1, $bulk=false) {
6e309973
DM
851 global $DB;
852
00aca3c1 853 if ($DB->record_exists('workshop_assessments', array('submissionid' => $submission->id, 'reviewerid' => $reviewerid))) {
b761e6d9 854 return self::ALLOCATION_EXISTS;
6e309973
DM
855 }
856
67ae13d9
DM
857 $weight = (int)$weight;
858 if ($weight < 0) {
859 $weight = 0;
860 }
861 if ($weight > 16) {
862 $weight = 16;
863 }
864
6e309973 865 $now = time();
7a789aa8 866 $assessment = new stdclass();
e554671d
DM
867 $assessment->submissionid = $submission->id;
868 $assessment->reviewerid = $reviewerid;
7a5f4be0 869 $assessment->timecreated = $now; // do not set timemodified here
becec954 870 $assessment->weight = $weight;
884482fb
DM
871 $assessment->generalcommentformat = editors_get_preferred_format();
872 $assessment->feedbackreviewerformat = editors_get_preferred_format();
6e309973 873
235b31c8 874 return $DB->insert_record('workshop_assessments', $assessment, true, $bulk);
6e309973
DM
875 }
876
6e309973 877 /**
53fad4b9 878 * Delete assessment record or records
6e309973 879 *
53fad4b9
DM
880 * @param mixed $id int|array assessment id or array of assessments ids
881 * @return bool false if $id not a valid parameter, true otherwise
6e309973
DM
882 */
883 public function delete_assessment($id) {
884 global $DB;
885
886 // todo remove all given grades from workshop_grades;
6e309973 887
53fad4b9 888 if (is_array($id)) {
235b31c8 889 return $DB->delete_records_list('workshop_assessments', 'id', $id);
3d2924e9 890 } else {
235b31c8 891 return $DB->delete_records('workshop_assessments', array('id' => $id));
53fad4b9 892 }
53fad4b9 893 }
6e309973
DM
894
895 /**
896 * Returns instance of grading strategy class
53fad4b9 897 *
7a789aa8 898 * @return stdclass Instance of a grading strategy
6e309973
DM
899 */
900 public function grading_strategy_instance() {
3d2924e9
DM
901 global $CFG; // because we require other libs here
902
3fd2b0e1 903 if (is_null($this->strategyinstance)) {
f05c168d 904 $strategylib = dirname(__FILE__) . '/form/' . $this->strategy . '/lib.php';
6e309973
DM
905 if (is_readable($strategylib)) {
906 require_once($strategylib);
907 } else {
f05c168d 908 throw new coding_exception('the grading forms subplugin must contain library ' . $strategylib);
6e309973 909 }
0dc47fb9 910 $classname = 'workshop_' . $this->strategy . '_strategy';
3fd2b0e1
DM
911 $this->strategyinstance = new $classname($this);
912 if (!in_array('workshop_strategy', class_implements($this->strategyinstance))) {
b761e6d9 913 throw new coding_exception($classname . ' does not implement workshop_strategy interface');
6e309973
DM
914 }
915 }
3fd2b0e1 916 return $this->strategyinstance;
6e309973
DM
917 }
918
45d24d39
DM
919 /**
920 * Returns instance of grading evaluation class
921 *
7a789aa8 922 * @return stdclass Instance of a grading evaluation
45d24d39
DM
923 */
924 public function grading_evaluation_instance() {
925 global $CFG; // because we require other libs here
926
927 if (is_null($this->evaluationinstance)) {
928 $evaluationlib = dirname(__FILE__) . '/eval/' . $this->evaluation . '/lib.php';
929 if (is_readable($evaluationlib)) {
930 require_once($evaluationlib);
931 } else {
932 throw new coding_exception('the grading evaluation subplugin must contain library ' . $evaluationlib);
933 }
934 $classname = 'workshop_' . $this->evaluation . '_evaluation';
935 $this->evaluationinstance = new $classname($this);
936 if (!in_array('workshop_evaluation', class_implements($this->evaluationinstance))) {
937 throw new coding_exception($classname . ' does not implement workshop_evaluation interface');
938 }
939 }
940 return $this->evaluationinstance;
941 }
942
66c9894d
DM
943 /**
944 * Returns instance of submissions allocator
53fad4b9 945 *
130ae619 946 * @param string $method The name of the allocation method, must be PARAM_ALPHA
7a789aa8 947 * @return stdclass Instance of submissions allocator
66c9894d
DM
948 */
949 public function allocator_instance($method) {
3d2924e9
DM
950 global $CFG; // because we require other libs here
951
f05c168d 952 $allocationlib = dirname(__FILE__) . '/allocation/' . $method . '/lib.php';
66c9894d
DM
953 if (is_readable($allocationlib)) {
954 require_once($allocationlib);
955 } else {
f05c168d 956 throw new coding_exception('Unable to find the allocation library ' . $allocationlib);
66c9894d
DM
957 }
958 $classname = 'workshop_' . $method . '_allocator';
959 return new $classname($this);
960 }
961
b8ead2e6 962 /**
454e8dd9 963 * @return moodle_url of this workshop's view page
b8ead2e6
DM
964 */
965 public function view_url() {
966 global $CFG;
a6855934 967 return new moodle_url('/mod/workshop/view.php', array('id' => $this->cm->id));
b8ead2e6
DM
968 }
969
970 /**
454e8dd9 971 * @return moodle_url of the page for editing this workshop's grading form
b8ead2e6
DM
972 */
973 public function editform_url() {
974 global $CFG;
a6855934 975 return new moodle_url('/mod/workshop/editform.php', array('cmid' => $this->cm->id));
b8ead2e6
DM
976 }
977
978 /**
454e8dd9 979 * @return moodle_url of the page for previewing this workshop's grading form
b8ead2e6
DM
980 */
981 public function previewform_url() {
982 global $CFG;
a6855934 983 return new moodle_url('/mod/workshop/editformpreview.php', array('cmid' => $this->cm->id));
b8ead2e6
DM
984 }
985
986 /**
987 * @param int $assessmentid The ID of assessment record
454e8dd9 988 * @return moodle_url of the assessment page
b8ead2e6 989 */
a39d7d87 990 public function assess_url($assessmentid) {
b8ead2e6 991 global $CFG;
454e8dd9 992 $assessmentid = clean_param($assessmentid, PARAM_INT);
a6855934 993 return new moodle_url('/mod/workshop/assessment.php', array('asid' => $assessmentid));
b8ead2e6
DM
994 }
995
becec954
DM
996 /**
997 * @param int $assessmentid The ID of assessment record
998 * @return moodle_url of the example assessment page
999 */
1000 public function exassess_url($assessmentid) {
1001 global $CFG;
1002 $assessmentid = clean_param($assessmentid, PARAM_INT);
a6855934 1003 return new moodle_url('/mod/workshop/exassessment.php', array('asid' => $assessmentid));
becec954
DM
1004 }
1005
39861053 1006 /**
67cd00ba 1007 * @return moodle_url of the page to view a submission, defaults to the own one
39861053 1008 */
67cd00ba 1009 public function submission_url($id=null) {
39861053 1010 global $CFG;
a6855934 1011 return new moodle_url('/mod/workshop/submission.php', array('cmid' => $this->cm->id, 'id' => $id));
39861053
DM
1012 }
1013
81eccf0a
DM
1014 /**
1015 * @param int $id example submission id
1016 * @return moodle_url of the page to view an example submission
1017 */
becec954 1018 public function exsubmission_url($id) {
81eccf0a 1019 global $CFG;
a6855934 1020 return new moodle_url('/mod/workshop/exsubmission.php', array('cmid' => $this->cm->id, 'id' => $id));
81eccf0a
DM
1021 }
1022
cbf87967
DM
1023 /**
1024 * @param int $sid submission id
1025 * @param array $aid of int assessment ids
1026 * @return moodle_url of the page to compare assessments of the given submission
1027 */
1028 public function compare_url($sid, array $aids) {
1029 global $CFG;
1030
a6855934 1031 $url = new moodle_url('/mod/workshop/compare.php', array('cmid' => $this->cm->id, 'sid' => $sid));
cbf87967
DM
1032 $i = 0;
1033 foreach ($aids as $aid) {
1034 $url->param("aid{$i}", $aid);
1035 $i++;
1036 }
1037 return $url;
1038 }
1039
1040 /**
1041 * @param int $sid submission id
1042 * @param int $aid assessment id
1043 * @return moodle_url of the page to compare the reference assessments of the given example submission
1044 */
1045 public function excompare_url($sid, $aid) {
1046 global $CFG;
a6855934 1047 return new moodle_url('/mod/workshop/excompare.php', array('cmid' => $this->cm->id, 'sid' => $sid, 'aid' => $aid));
cbf87967
DM
1048 }
1049
da0b1f70 1050 /**
454e8dd9 1051 * @return moodle_url of the mod_edit form
da0b1f70
DM
1052 */
1053 public function updatemod_url() {
1054 global $CFG;
a6855934 1055 return new moodle_url('/course/modedit.php', array('update' => $this->cm->id, 'return' => 1));
da0b1f70
DM
1056 }
1057
454e8dd9 1058 /**
08af32af 1059 * @param string $method allocation method
454e8dd9
DM
1060 * @return moodle_url to the allocation page
1061 */
08af32af 1062 public function allocation_url($method=null) {
da0b1f70 1063 global $CFG;
08af32af
DM
1064 $params = array('cmid' => $this->cm->id);
1065 if (!empty($method)) {
1066 $params['method'] = $method;
1067 }
1068 return new moodle_url('/mod/workshop/allocation.php', $params);
da0b1f70
DM
1069 }
1070
454e8dd9
DM
1071 /**
1072 * @param int $phasecode The internal phase code
1073 * @return moodle_url of the script to change the current phase to $phasecode
1074 */
1075 public function switchphase_url($phasecode) {
1076 global $CFG;
1077 $phasecode = clean_param($phasecode, PARAM_INT);
a6855934 1078 return new moodle_url('/mod/workshop/switchphase.php', array('cmid' => $this->cm->id, 'phase' => $phasecode));
454e8dd9
DM
1079 }
1080
89c1aa97
DM
1081 /**
1082 * @return moodle_url to the aggregation page
1083 */
1084 public function aggregate_url() {
1085 global $CFG;
a6855934 1086 return new moodle_url('/mod/workshop/aggregate.php', array('cmid' => $this->cm->id));
89c1aa97
DM
1087 }
1088
32c78bc3
DM
1089 /**
1090 * @return moodle_url of this workshop's toolbox page
1091 */
1092 public function toolbox_url($tool) {
1093 global $CFG;
1094 return new moodle_url('/mod/workshop/toolbox.php', array('id' => $this->cm->id, 'tool' => $tool));
1095 }
1096
5450f7b6
DM
1097 /**
1098 * Workshop wrapper around {@see add_to_log()}
1099 *
1100 * @param string $action to be logged
1101 * @param moodle_url $url absolute url as returned by {@see workshop::submission_url()} and friends
1102 * @param mixed $info additional info, usually id in a table
1103 */
1104 public function log($action, moodle_url $url = null, $info = null) {
1105
1106 if (is_null($url)) {
1107 $url = $this->view_url();
1108 }
1109
1110 if (is_null($info)) {
1111 $info = $this->id;
1112 }
1113
1114 $logurl = $this->log_convert_url($url);
1115 add_to_log($this->course->id, 'workshop', $action, $logurl, $info, $this->cm->id);
1116 }
1117
b8ead2e6 1118 /**
9ddff589 1119 * Is the given user allowed to create their submission?
407b1e91 1120 *
9ddff589 1121 * @param int $userid
407b1e91 1122 * @return bool
b8ead2e6 1123 */
9ddff589
DM
1124 public function creating_submission_allowed($userid) {
1125
2f289d36 1126 $now = time();
9ddff589 1127 $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid);
2f289d36
DM
1128
1129 if ($this->latesubmissions) {
1130 if ($this->phase != self::PHASE_SUBMISSION and $this->phase != self::PHASE_ASSESSMENT) {
1131 // late submissions are allowed in the submission and assessment phase only
1132 return false;
1133 }
9ddff589 1134 if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) {
2f289d36
DM
1135 // late submissions are not allowed before the submission start
1136 return false;
1137 }
1138 return true;
1139
1140 } else {
1141 if ($this->phase != self::PHASE_SUBMISSION) {
1142 // submissions are allowed during the submission phase only
1143 return false;
1144 }
9ddff589 1145 if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) {
2f289d36
DM
1146 // if enabled, submitting is not allowed before the date/time defined in the mod_form
1147 return false;
1148 }
9ddff589 1149 if (!$ignoredeadlines and !empty($this->submissionend) and $now > $this->submissionend ) {
2f289d36
DM
1150 // if enabled, submitting is not allowed after the date/time defined in the mod_form unless late submission is allowed
1151 return false;
1152 }
1153 return true;
1154 }
1155 }
1156
1157 /**
9ddff589 1158 * Is the given user allowed to modify their existing submission?
2f289d36 1159 *
9ddff589 1160 * @param int $userid
2f289d36
DM
1161 * @return bool
1162 */
9ddff589
DM
1163 public function modifying_submission_allowed($userid) {
1164
2f289d36 1165 $now = time();
9ddff589 1166 $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid);
2f289d36 1167
74bf8a94 1168 if ($this->phase != self::PHASE_SUBMISSION) {
2f289d36 1169 // submissions can be edited during the submission phase only
74bf8a94
DM
1170 return false;
1171 }
9ddff589 1172 if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) {
c1ab2b10 1173 // if enabled, re-submitting is not allowed before the date/time defined in the mod_form
74bf8a94
DM
1174 return false;
1175 }
c1ab2b10
DM
1176 if (!$ignoredeadlines and !empty($this->submissionend) and $now > $this->submissionend) {
1177 // if enabled, re-submitting is not allowed after the date/time defined in the mod_form even if late submission is allowed
74bf8a94
DM
1178 return false;
1179 }
407b1e91 1180 return true;
b8ead2e6
DM
1181 }
1182
c1e883bb 1183 /**
9ddff589 1184 * Is the given reviewer allowed to create/edit their assessments?
c1e883bb 1185 *
9ddff589 1186 * @param int $userid
c1e883bb
DM
1187 * @return bool
1188 */
9ddff589
DM
1189 public function assessing_allowed($userid) {
1190
74bf8a94
DM
1191 if ($this->phase != self::PHASE_ASSESSMENT) {
1192 // assessing is not allowed but in the assessment phase
1193 return false;
1194 }
9ddff589 1195
74bf8a94 1196 $now = time();
9ddff589
DM
1197 $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid);
1198
1199 if (!$ignoredeadlines and !empty($this->assessmentstart) and $this->assessmentstart > $now) {
74bf8a94
DM
1200 // if enabled, assessing is not allowed before the date/time defined in the mod_form
1201 return false;
1202 }
9ddff589 1203 if (!$ignoredeadlines and !empty($this->assessmentend) and $now > $this->assessmentend) {
74bf8a94
DM
1204 // if enabled, assessing is not allowed after the date/time defined in the mod_form
1205 return false;
1206 }
1207 // here we go, assessing is allowed
c1e883bb
DM
1208 return true;
1209 }
1210
becec954
DM
1211 /**
1212 * Are reviewers allowed to create/edit their assessments of the example submissions?
1213 *
514d8c22
DM
1214 * Returns null if example submissions are not enabled in this workshop. Otherwise returns
1215 * true or false. Note this does not check other conditions like the number of already
1216 * assessed examples, examples mode etc.
becec954 1217 *
74bf8a94 1218 * @return null|bool
becec954
DM
1219 */
1220 public function assessing_examples_allowed() {
74bf8a94
DM
1221 if (empty($this->useexamples)) {
1222 return null;
1223 }
1224 if (self::EXAMPLES_VOLUNTARY == $this->examplesmode) {
1225 return true;
1226 }
1227 if (self::EXAMPLES_BEFORE_SUBMISSION == $this->examplesmode and self::PHASE_SUBMISSION == $this->phase) {
1228 return true;
1229 }
1230 if (self::EXAMPLES_BEFORE_ASSESSMENT == $this->examplesmode and self::PHASE_ASSESSMENT == $this->phase) {
1231 return true;
1232 }
1233 return false;
becec954 1234 }
407b1e91 1235
3dc78e5b
DM
1236 /**
1237 * Are the peer-reviews available to the authors?
1238 *
3dc78e5b
DM
1239 * @return bool
1240 */
1241 public function assessments_available() {
5a372494 1242 return $this->phase == self::PHASE_CLOSED;
3dc78e5b
DM
1243 }
1244
454e8dd9
DM
1245 /**
1246 * Switch to a new workshop phase
1247 *
1248 * Modifies the underlying database record. You should terminate the script shortly after calling this.
1249 *
1250 * @param int $newphase new phase code
1251 * @return bool true if success, false otherwise
1252 */
1253 public function switch_phase($newphase) {
1254 global $DB;
1255
365c2cc2 1256 $known = $this->available_phases_list();
454e8dd9
DM
1257 if (!isset($known[$newphase])) {
1258 return false;
1259 }
f6e8b318
DM
1260
1261 if (self::PHASE_CLOSED == $newphase) {
f27b70fb 1262 // push the grades into the gradebook
7a789aa8 1263 $workshop = new stdclass();
10bc4bce
DM
1264 foreach ($this as $property => $value) {
1265 $workshop->{$property} = $value;
1266 }
1267 $workshop->course = $this->course->id;
1268 $workshop->cmidnumber = $this->cm->id;
1269 $workshop->modname = 'workshop';
1270 workshop_update_grades($workshop);
f6e8b318
DM
1271 }
1272
454e8dd9
DM
1273 $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id));
1274 return true;
1275 }
ddb59c77
DM
1276
1277 /**
1278 * Saves a raw grade for submission as calculated from the assessment form fields
1279 *
1280 * @param array $assessmentid assessment record id, must exists
00aca3c1 1281 * @param mixed $grade raw percentual grade from 0.00000 to 100.00000
ddb59c77
DM
1282 * @return false|float the saved grade
1283 */
1284 public function set_peer_grade($assessmentid, $grade) {
1285 global $DB;
1286
1287 if (is_null($grade)) {
1288 return false;
1289 }
7a789aa8 1290 $data = new stdclass();
ddb59c77
DM
1291 $data->id = $assessmentid;
1292 $data->grade = $grade;
7a5f4be0 1293 $data->timemodified = time();
ddb59c77
DM
1294 $DB->update_record('workshop_assessments', $data);
1295 return $grade;
1296 }
6516b9e9 1297
29dc43e7
DM
1298 /**
1299 * Prepares data object with all workshop grades to be rendered
1300 *
5e71cefb
DM
1301 * @param int $userid the user we are preparing the report for
1302 * @param mixed $groups single group or array of groups - only show users who are in one of these group(s). Defaults to all
29dc43e7 1303 * @param int $page the current page (for the pagination)
5e71cefb 1304 * @param int $perpage participants per page (for the pagination)
f27b70fb 1305 * @param string $sortby lastname|firstname|submissiontitle|submissiongrade|gradinggrade
5e71cefb 1306 * @param string $sorthow ASC|DESC
7a789aa8 1307 * @return stdclass data for the renderer
29dc43e7 1308 */
c2a35266 1309 public function prepare_grading_report_data($userid, $groups, $page, $perpage, $sortby, $sorthow) {
29dc43e7
DM
1310 global $DB;
1311
d895c6aa
DM
1312 $canviewall = has_capability('mod/workshop:viewallassessments', $this->context, $userid);
1313 $isparticipant = has_any_capability(array('mod/workshop:submit', 'mod/workshop:peerassess'), $this->context, $userid);
29dc43e7
DM
1314
1315 if (!$canviewall and !$isparticipant) {
1316 // who the hell is this?
1317 return array();
1318 }
1319
f27b70fb 1320 if (!in_array($sortby, array('lastname','firstname','submissiontitle','submissiongrade','gradinggrade'))) {
5e71cefb
DM
1321 $sortby = 'lastname';
1322 }
1323
1324 if (!($sorthow === 'ASC' or $sorthow === 'DESC')) {
1325 $sorthow = 'ASC';
1326 }
1327
1328 // get the list of user ids to be displayed
29dc43e7
DM
1329 if ($canviewall) {
1330 // fetch the list of ids of all workshop participants - this may get really long so fetch just id
d895c6aa 1331 $participants = get_users_by_capability($this->context, array('mod/workshop:submit', 'mod/workshop:peerassess'),
5e71cefb 1332 'u.id', '', '', '', $groups, '', false, false, true);
29dc43e7
DM
1333 } else {
1334 // this is an ordinary workshop participant (aka student) - display the report just for him/her
1335 $participants = array($userid => (object)array('id' => $userid));
1336 }
1337
5e71cefb 1338 // we will need to know the number of all records later for the pagination purposes
29dc43e7
DM
1339 $numofparticipants = count($participants);
1340
deea6e7a
DM
1341 if ($numofparticipants > 0) {
1342 // load all fields which can be used for sorting and paginate the records
1343 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1344 $params['workshopid1'] = $this->id;
1345 $params['workshopid2'] = $this->id;
20e7fd83
DM
1346 $sqlsort = array();
1347 $sqlsortfields = array($sortby => $sorthow) + array('lastname' => 'ASC', 'firstname' => 'ASC', 'u.id' => 'ASC');
1348 foreach ($sqlsortfields as $sqlsortfieldname => $sqlsortfieldhow) {
1349 $sqlsort[] = $sqlsortfieldname . ' ' . $sqlsortfieldhow;
1350 }
1351 $sqlsort = implode(',', $sqlsort);
3a11c09f 1352 $sql = "SELECT u.id AS userid,u.firstname,u.lastname,u.picture,u.imagealt,u.email,
deea6e7a
DM
1353 s.title AS submissiontitle, s.grade AS submissiongrade, ag.gradinggrade
1354 FROM {user} u
1355 LEFT JOIN {workshop_submissions} s ON (s.authorid = u.id AND s.workshopid = :workshopid1 AND s.example = 0)
1356 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = u.id AND ag.workshopid = :workshopid2)
1357 WHERE u.id $participantids
1358 ORDER BY $sqlsort";
1359 $participants = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
1360 } else {
1361 $participants = array();
1362 }
29dc43e7
DM
1363
1364 // this will hold the information needed to display user names and pictures
5e71cefb
DM
1365 $userinfo = array();
1366
1367 // get the user details for all participants to display
1368 foreach ($participants as $participant) {
1369 if (!isset($userinfo[$participant->userid])) {
7a789aa8 1370 $userinfo[$participant->userid] = new stdclass();
5e71cefb
DM
1371 $userinfo[$participant->userid]->id = $participant->userid;
1372 $userinfo[$participant->userid]->firstname = $participant->firstname;
1373 $userinfo[$participant->userid]->lastname = $participant->lastname;
1374 $userinfo[$participant->userid]->picture = $participant->picture;
1375 $userinfo[$participant->userid]->imagealt = $participant->imagealt;
3a11c09f 1376 $userinfo[$participant->userid]->email = $participant->email;
5e71cefb
DM
1377 }
1378 }
29dc43e7 1379
5e71cefb 1380 // load the submissions details
29dc43e7 1381 $submissions = $this->get_submissions(array_keys($participants));
5e71cefb
DM
1382
1383 // get the user details for all moderators (teachers) that have overridden a submission grade
29dc43e7 1384 foreach ($submissions as $submission) {
29dc43e7 1385 if (!isset($userinfo[$submission->gradeoverby])) {
7a789aa8 1386 $userinfo[$submission->gradeoverby] = new stdclass();
29dc43e7
DM
1387 $userinfo[$submission->gradeoverby]->id = $submission->gradeoverby;
1388 $userinfo[$submission->gradeoverby]->firstname = $submission->overfirstname;
1389 $userinfo[$submission->gradeoverby]->lastname = $submission->overlastname;
1390 $userinfo[$submission->gradeoverby]->picture = $submission->overpicture;
1391 $userinfo[$submission->gradeoverby]->imagealt = $submission->overimagealt;
3a11c09f 1392 $userinfo[$submission->gradeoverby]->email = $submission->overemail;
29dc43e7
DM
1393 }
1394 }
1395
5e71cefb 1396 // get the user details for all reviewers of the displayed participants
29dc43e7
DM
1397 $reviewers = array();
1398 if ($submissions) {
1399 list($submissionids, $params) = $DB->get_in_or_equal(array_keys($submissions), SQL_PARAMS_NAMED);
581878b8 1400 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover, a.weight,
3a11c09f 1401 r.id AS reviewerid, r.lastname, r.firstname, r.picture, r.imagealt, r.email,
29dc43e7
DM
1402 s.id AS submissionid, s.authorid
1403 FROM {workshop_assessments} a
1404 JOIN {user} r ON (a.reviewerid = r.id)
0324b6f1 1405 JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0)
c6b784f0
DM
1406 WHERE a.submissionid $submissionids
1407 ORDER BY a.weight DESC, r.lastname, r.firstname";
29dc43e7
DM
1408 $reviewers = $DB->get_records_sql($sql, $params);
1409 foreach ($reviewers as $reviewer) {
1410 if (!isset($userinfo[$reviewer->reviewerid])) {
7a789aa8 1411 $userinfo[$reviewer->reviewerid] = new stdclass();
29dc43e7
DM
1412 $userinfo[$reviewer->reviewerid]->id = $reviewer->reviewerid;
1413 $userinfo[$reviewer->reviewerid]->firstname = $reviewer->firstname;
1414 $userinfo[$reviewer->reviewerid]->lastname = $reviewer->lastname;
1415 $userinfo[$reviewer->reviewerid]->picture = $reviewer->picture;
1416 $userinfo[$reviewer->reviewerid]->imagealt = $reviewer->imagealt;
3a11c09f 1417 $userinfo[$reviewer->reviewerid]->email = $reviewer->email;
29dc43e7
DM
1418 }
1419 }
1420 }
1421
5e71cefb 1422 // get the user details for all reviewees of the displayed participants
934329e5
DM
1423 $reviewees = array();
1424 if ($participants) {
1425 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1426 $params['workshopid'] = $this->id;
581878b8 1427 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover, a.reviewerid, a.weight,
934329e5 1428 s.id AS submissionid,
3a11c09f 1429 e.id AS authorid, e.lastname, e.firstname, e.picture, e.imagealt, e.email
934329e5
DM
1430 FROM {user} u
1431 JOIN {workshop_assessments} a ON (a.reviewerid = u.id)
0324b6f1 1432 JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0)
934329e5 1433 JOIN {user} e ON (s.authorid = e.id)
c6b784f0
DM
1434 WHERE u.id $participantids AND s.workshopid = :workshopid
1435 ORDER BY a.weight DESC, e.lastname, e.firstname";
934329e5
DM
1436 $reviewees = $DB->get_records_sql($sql, $params);
1437 foreach ($reviewees as $reviewee) {
1438 if (!isset($userinfo[$reviewee->authorid])) {
7a789aa8 1439 $userinfo[$reviewee->authorid] = new stdclass();
934329e5
DM
1440 $userinfo[$reviewee->authorid]->id = $reviewee->authorid;
1441 $userinfo[$reviewee->authorid]->firstname = $reviewee->firstname;
1442 $userinfo[$reviewee->authorid]->lastname = $reviewee->lastname;
1443 $userinfo[$reviewee->authorid]->picture = $reviewee->picture;
1444 $userinfo[$reviewee->authorid]->imagealt = $reviewee->imagealt;
3a11c09f 1445 $userinfo[$reviewee->authorid]->email = $reviewee->email;
934329e5 1446 }
29dc43e7
DM
1447 }
1448 }
1449
5e71cefb
DM
1450 // finally populate the object to be rendered
1451 $grades = $participants;
29dc43e7
DM
1452
1453 foreach ($participants as $participant) {
1454 // set up default (null) values
d183140d
DM
1455 $grades[$participant->userid]->submissionid = null;
1456 $grades[$participant->userid]->submissiontitle = null;
1457 $grades[$participant->userid]->submissiongrade = null;
1458 $grades[$participant->userid]->submissiongradeover = null;
1459 $grades[$participant->userid]->submissiongradeoverby = null;
232175e4 1460 $grades[$participant->userid]->submissionpublished = null;
5e71cefb
DM
1461 $grades[$participant->userid]->reviewedby = array();
1462 $grades[$participant->userid]->reviewerof = array();
29dc43e7
DM
1463 }
1464 unset($participants);
1465 unset($participant);
1466
1467 foreach ($submissions as $submission) {
1468 $grades[$submission->authorid]->submissionid = $submission->id;
1469 $grades[$submission->authorid]->submissiontitle = $submission->title;
b4857acb
DM
1470 $grades[$submission->authorid]->submissiongrade = $this->real_grade($submission->grade);
1471 $grades[$submission->authorid]->submissiongradeover = $this->real_grade($submission->gradeover);
29dc43e7 1472 $grades[$submission->authorid]->submissiongradeoverby = $submission->gradeoverby;
232175e4 1473 $grades[$submission->authorid]->submissionpublished = $submission->published;
29dc43e7
DM
1474 }
1475 unset($submissions);
1476 unset($submission);
1477
1478 foreach($reviewers as $reviewer) {
7a789aa8 1479 $info = new stdclass();
29dc43e7
DM
1480 $info->userid = $reviewer->reviewerid;
1481 $info->assessmentid = $reviewer->assessmentid;
1482 $info->submissionid = $reviewer->submissionid;
b4857acb
DM
1483 $info->grade = $this->real_grade($reviewer->grade);
1484 $info->gradinggrade = $this->real_grading_grade($reviewer->gradinggrade);
1485 $info->gradinggradeover = $this->real_grading_grade($reviewer->gradinggradeover);
581878b8 1486 $info->weight = $reviewer->weight;
29dc43e7
DM
1487 $grades[$reviewer->authorid]->reviewedby[$reviewer->reviewerid] = $info;
1488 }
1489 unset($reviewers);
1490 unset($reviewer);
1491
1492 foreach($reviewees as $reviewee) {
7a789aa8 1493 $info = new stdclass();
29dc43e7
DM
1494 $info->userid = $reviewee->authorid;
1495 $info->assessmentid = $reviewee->assessmentid;
1496 $info->submissionid = $reviewee->submissionid;
b4857acb
DM
1497 $info->grade = $this->real_grade($reviewee->grade);
1498 $info->gradinggrade = $this->real_grading_grade($reviewee->gradinggrade);
1499 $info->gradinggradeover = $this->real_grading_grade($reviewee->gradinggradeover);
581878b8 1500 $info->weight = $reviewee->weight;
29dc43e7
DM
1501 $grades[$reviewee->reviewerid]->reviewerof[$reviewee->authorid] = $info;
1502 }
1503 unset($reviewees);
1504 unset($reviewee);
1505
b4857acb
DM
1506 foreach ($grades as $grade) {
1507 $grade->gradinggrade = $this->real_grading_grade($grade->gradinggrade);
b4857acb
DM
1508 }
1509
7a789aa8 1510 $data = new stdclass();
29dc43e7
DM
1511 $data->grades = $grades;
1512 $data->userinfo = $userinfo;
1513 $data->totalcount = $numofparticipants;
b4857acb
DM
1514 $data->maxgrade = $this->real_grade(100);
1515 $data->maxgradinggrade = $this->real_grading_grade(100);
29dc43e7
DM
1516 return $data;
1517 }
1518
29dc43e7 1519 /**
b4857acb 1520 * Calculates the real value of a grade
29dc43e7 1521 *
b4857acb
DM
1522 * @param float $value percentual value from 0 to 100
1523 * @param float $max the maximal grade
1524 * @return string
1525 */
1526 public function real_grade_value($value, $max) {
1527 $localized = true;
557a1100 1528 if (is_null($value) or $value === '') {
b4857acb
DM
1529 return null;
1530 } elseif ($max == 0) {
1531 return 0;
1532 } else {
1533 return format_float($max * $value / 100, $this->gradedecimals, $localized);
1534 }
1535 }
1536
e554671d
DM
1537 /**
1538 * Calculates the raw (percentual) value from a real grade
1539 *
1540 * This is used in cases when a user wants to give a grade such as 12 of 20 and we need to save
1541 * this value in a raw percentual form into DB
1542 * @param float $value given grade
1543 * @param float $max the maximal grade
1544 * @return float suitable to be stored as numeric(10,5)
1545 */
1546 public function raw_grade_value($value, $max) {
557a1100 1547 if (is_null($value) or $value === '') {
e554671d
DM
1548 return null;
1549 }
1550 if ($max == 0 or $value < 0) {
1551 return 0;
1552 }
1553 $p = $value / $max * 100;
1554 if ($p > 100) {
1555 return $max;
1556 }
1557 return grade_floatval($p);
1558 }
1559
b4857acb
DM
1560 /**
1561 * Calculates the real value of grade for submission
1562 *
1563 * @param float $value percentual value from 0 to 100
1564 * @return string
1565 */
1566 public function real_grade($value) {
1567 return $this->real_grade_value($value, $this->grade);
1568 }
1569
1570 /**
1571 * Calculates the real value of grade for assessment
1572 *
1573 * @param float $value percentual value from 0 to 100
1574 * @return string
1575 */
1576 public function real_grading_grade($value) {
1577 return $this->real_grade_value($value, $this->gradinggrade);
29dc43e7
DM
1578 }
1579
e706b9c3
DM
1580 /**
1581 * Sets the given grades and received grading grades to null
1582 *
1583 * This does not clear the information about how the peers filled the assessment forms, but
1584 * clears the calculated grades in workshop_assessments. Therefore reviewers have to re-assess
1585 * the allocated submissions.
1586 *
1587 * @return void
1588 */
1589 public function clear_assessments() {
1590 global $DB;
1591
1592 $submissions = $this->get_submissions();
1593 if (empty($submissions)) {
1594 // no money, no love
1595 return;
1596 }
1597 $submissions = array_keys($submissions);
1598 list($sql, $params) = $DB->get_in_or_equal($submissions, SQL_PARAMS_NAMED);
1599 $sql = "submissionid $sql";
1600 $DB->set_field_select('workshop_assessments', 'grade', null, $sql, $params);
1601 $DB->set_field_select('workshop_assessments', 'gradinggrade', null, $sql, $params);
1602 }
1603
32c78bc3
DM
1604 /**
1605 * Sets the grades for submission to null
1606 *
1607 * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s)
1608 * @return void
1609 */
1610 public function clear_submission_grades($restrict=null) {
1611 global $DB;
1612
1613 $sql = "workshopid = :workshopid AND example = 0";
1614 $params = array('workshopid' => $this->id);
1615
1616 if (is_null($restrict)) {
1617 // update all users - no more conditions
1618 } elseif (!empty($restrict)) {
1619 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1620 $sql .= " AND authorid $usql";
1621 $params = array_merge($params, $uparams);
1622 } else {
1623 throw new coding_exception('Empty value is not a valid parameter here');
1624 }
1625
1626 $DB->set_field_select('workshop_submissions', 'grade', null, $sql, $params);
1627 }
1628
89c1aa97 1629 /**
e9a90e69 1630 * Calculates grades for submission for the given participant(s) and updates it in the database
89c1aa97
DM
1631 *
1632 * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s)
1633 * @return void
1634 */
8a1ba8ac 1635 public function aggregate_submission_grades($restrict=null) {
89c1aa97
DM
1636 global $DB;
1637
1638 // fetch a recordset with all assessments to process
1696f36c 1639 $sql = 'SELECT s.id AS submissionid, s.grade AS submissiongrade,
89c1aa97
DM
1640 a.weight, a.grade
1641 FROM {workshop_submissions} s
1642 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id)
1643 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
1644 $params = array('workshopid' => $this->id);
1645
1646 if (is_null($restrict)) {
1647 // update all users - no more conditions
1648 } elseif (!empty($restrict)) {
1649 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1650 $sql .= " AND s.authorid $usql";
1651 $params = array_merge($params, $uparams);
1652 } else {
1653 throw new coding_exception('Empty value is not a valid parameter here');
1654 }
1655
1656 $sql .= ' ORDER BY s.id'; // this is important for bulk processing
89c1aa97 1657
e9a90e69
DM
1658 $rs = $DB->get_recordset_sql($sql, $params);
1659 $batch = array(); // will contain a set of all assessments of a single submission
1660 $previous = null; // a previous record in the recordset
1661
89c1aa97
DM
1662 foreach ($rs as $current) {
1663 if (is_null($previous)) {
1664 // we are processing the very first record in the recordset
1665 $previous = $current;
89c1aa97 1666 }
e9a90e69 1667 if ($current->submissionid == $previous->submissionid) {
89c1aa97 1668 // we are still processing the current submission
e9a90e69
DM
1669 $batch[] = $current;
1670 } else {
1671 // process all the assessments of a sigle submission
1672 $this->aggregate_submission_grades_process($batch);
1673 // and then start to process another submission
1674 $batch = array($current);
1675 $previous = $current;
89c1aa97
DM
1676 }
1677 }
e9a90e69
DM
1678 // do not forget to process the last batch!
1679 $this->aggregate_submission_grades_process($batch);
89c1aa97
DM
1680 $rs->close();
1681 }
1682
32c78bc3
DM
1683 /**
1684 * Sets the aggregated grades for assessment to null
1685 *
1686 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
1687 * @return void
1688 */
1689 public function clear_grading_grades($restrict=null) {
1690 global $DB;
1691
1692 $sql = "workshopid = :workshopid";
1693 $params = array('workshopid' => $this->id);
1694
1695 if (is_null($restrict)) {
1696 // update all users - no more conditions
1697 } elseif (!empty($restrict)) {
1698 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1699 $sql .= " AND userid $usql";
1700 $params = array_merge($params, $uparams);
1701 } else {
1702 throw new coding_exception('Empty value is not a valid parameter here');
1703 }
1704
1705 $DB->set_field_select('workshop_aggregations', 'gradinggrade', null, $sql, $params);
1706 }
1707
89c1aa97
DM
1708 /**
1709 * Calculates grades for assessment for the given participant(s)
1710 *
39411930
DM
1711 * Grade for assessment is calculated as a simple mean of all grading grades calculated by the grading evaluator.
1712 * The assessment weight is not taken into account here.
89c1aa97
DM
1713 *
1714 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
1715 * @return void
1716 */
8a1ba8ac 1717 public function aggregate_grading_grades($restrict=null) {
89c1aa97
DM
1718 global $DB;
1719
39411930
DM
1720 // fetch a recordset with all assessments to process
1721 $sql = 'SELECT a.reviewerid, a.gradinggrade, a.gradinggradeover,
1722 ag.id AS aggregationid, ag.gradinggrade AS aggregatedgrade
1723 FROM {workshop_assessments} a
1724 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1725 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = a.reviewerid AND ag.workshopid = s.workshopid)
1726 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
1727 $params = array('workshopid' => $this->id);
1728
1729 if (is_null($restrict)) {
1730 // update all users - no more conditions
1731 } elseif (!empty($restrict)) {
1732 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
1733 $sql .= " AND a.reviewerid $usql";
1734 $params = array_merge($params, $uparams);
1735 } else {
1736 throw new coding_exception('Empty value is not a valid parameter here');
1737 }
1738
1739 $sql .= ' ORDER BY a.reviewerid'; // this is important for bulk processing
1740
1741 $rs = $DB->get_recordset_sql($sql, $params);
1742 $batch = array(); // will contain a set of all assessments of a single submission
1743 $previous = null; // a previous record in the recordset
1744
1745 foreach ($rs as $current) {
1746 if (is_null($previous)) {
1747 // we are processing the very first record in the recordset
1748 $previous = $current;
1749 }
1750 if ($current->reviewerid == $previous->reviewerid) {
1751 // we are still processing the current reviewer
1752 $batch[] = $current;
1753 } else {
1754 // process all the assessments of a sigle submission
1755 $this->aggregate_grading_grades_process($batch);
1756 // and then start to process another reviewer
1757 $batch = array($current);
1758 $previous = $current;
1759 }
1760 }
1761 // do not forget to process the last batch!
1762 $this->aggregate_grading_grades_process($batch);
1763 $rs->close();
89c1aa97
DM
1764 }
1765
77f43e7d 1766 /**
f6e8b318 1767 * Returns the mform the teachers use to put a feedback for the reviewer
77f43e7d 1768 *
c6b784f0 1769 * @param moodle_url $actionurl
5924db72 1770 * @param stdClass $assessment
c6b784f0 1771 * @param array $options editable, editableweight, overridablegradinggrade
f6e8b318 1772 * @return workshop_feedbackreviewer_form
77f43e7d 1773 */
c6b784f0 1774 public function get_feedbackreviewer_form(moodle_url $actionurl, stdclass $assessment, $options=array()) {
77f43e7d
DM
1775 global $CFG;
1776 require_once(dirname(__FILE__) . '/feedbackreviewer_form.php');
1777
7a789aa8 1778 $current = new stdclass();
e554671d 1779 $current->asid = $assessment->id;
c6b784f0 1780 $current->weight = $assessment->weight;
e554671d
DM
1781 $current->gradinggrade = $this->real_grading_grade($assessment->gradinggrade);
1782 $current->gradinggradeover = $this->real_grading_grade($assessment->gradinggradeover);
1783 $current->feedbackreviewer = $assessment->feedbackreviewer;
1784 $current->feedbackreviewerformat = $assessment->feedbackreviewerformat;
1785 if (is_null($current->gradinggrade)) {
1786 $current->gradinggrade = get_string('nullgrade', 'workshop');
1787 }
c6b784f0
DM
1788 if (!isset($options['editable'])) {
1789 $editable = true; // by default
1790 } else {
1791 $editable = (bool)$options['editable'];
1792 }
e554671d
DM
1793
1794 // prepare wysiwyg editor
1795 $current = file_prepare_standard_editor($current, 'feedbackreviewer', array());
1796
77f43e7d 1797 return new workshop_feedbackreviewer_form($actionurl,
c6b784f0 1798 array('workshop' => $this, 'current' => $current, 'editoropts' => array(), 'options' => $options),
77f43e7d
DM
1799 'post', '', null, $editable);
1800 }
1801
67cd00ba
DM
1802 /**
1803 * Returns the mform the teachers use to put a feedback for the author on their submission
1804 *
c6b784f0 1805 * @param moodle_url $actionurl
5924db72 1806 * @param stdClass $submission
c6b784f0 1807 * @param array $options editable
67cd00ba
DM
1808 * @return workshop_feedbackauthor_form
1809 */
c6b784f0 1810 public function get_feedbackauthor_form(moodle_url $actionurl, stdclass $submission, $options=array()) {
67cd00ba
DM
1811 global $CFG;
1812 require_once(dirname(__FILE__) . '/feedbackauthor_form.php');
1813
7a789aa8 1814 $current = new stdclass();
67cd00ba 1815 $current->submissionid = $submission->id;
232175e4 1816 $current->published = $submission->published;
557a1100
DM
1817 $current->grade = $this->real_grade($submission->grade);
1818 $current->gradeover = $this->real_grade($submission->gradeover);
1819 $current->feedbackauthor = $submission->feedbackauthor;
1820 $current->feedbackauthorformat = $submission->feedbackauthorformat;
67cd00ba
DM
1821 if (is_null($current->grade)) {
1822 $current->grade = get_string('nullgrade', 'workshop');
1823 }
c6b784f0
DM
1824 if (!isset($options['editable'])) {
1825 $editable = true; // by default
1826 } else {
1827 $editable = (bool)$options['editable'];
1828 }
67cd00ba
DM
1829
1830 // prepare wysiwyg editor
1831 $current = file_prepare_standard_editor($current, 'feedbackauthor', array());
1832
1833 return new workshop_feedbackauthor_form($actionurl,
232175e4 1834 array('workshop' => $this, 'current' => $current, 'editoropts' => array(), 'options' => $options),
67cd00ba
DM
1835 'post', '', null, $editable);
1836 }
1837
aa40adbf
DM
1838 ////////////////////////////////////////////////////////////////////////////////
1839 // Internal methods (implementation details) //
1840 ////////////////////////////////////////////////////////////////////////////////
6516b9e9 1841
e9a90e69
DM
1842 /**
1843 * Given an array of all assessments of a single submission, calculates the final grade for this submission
1844 *
1845 * This calculates the weighted mean of the passed assessment grades. If, however, the submission grade
1846 * was overridden by a teacher, the gradeover value is returned and the rest of grades are ignored.
1847 *
7a789aa8 1848 * @param array $assessments of stdclass(->submissionid ->submissiongrade ->gradeover ->weight ->grade)
1fed6ce3 1849 * @return void
e9a90e69
DM
1850 */
1851 protected function aggregate_submission_grades_process(array $assessments) {
1852 global $DB;
1853
1854 $submissionid = null; // the id of the submission being processed
1855 $current = null; // the grade currently saved in database
1856 $finalgrade = null; // the new grade to be calculated
1857 $sumgrades = 0;
1858 $sumweights = 0;
1859
1860 foreach ($assessments as $assessment) {
1861 if (is_null($submissionid)) {
1862 // the id is the same in all records, fetch it during the first loop cycle
1863 $submissionid = $assessment->submissionid;
1864 }
1865 if (is_null($current)) {
1866 // the currently saved grade is the same in all records, fetch it during the first loop cycle
1867 $current = $assessment->submissiongrade;
1868 }
e9a90e69
DM
1869 if (is_null($assessment->grade)) {
1870 // this was not assessed yet
1871 continue;
1872 }
1873 if ($assessment->weight == 0) {
1874 // this does not influence the calculation
1875 continue;
1876 }
1877 $sumgrades += $assessment->grade * $assessment->weight;
1878 $sumweights += $assessment->weight;
1879 }
1880 if ($sumweights > 0 and is_null($finalgrade)) {
1881 $finalgrade = grade_floatval($sumgrades / $sumweights);
1882 }
1883 // check if the new final grade differs from the one stored in the database
1884 if (grade_floats_different($finalgrade, $current)) {
1885 // we need to save new calculation into the database
7a789aa8 1886 $record = new stdclass();
10bc4bce
DM
1887 $record->id = $submissionid;
1888 $record->grade = $finalgrade;
1889 $record->timegraded = time();
1890 $DB->update_record('workshop_submissions', $record);
e9a90e69
DM
1891 }
1892 }
1893
39411930
DM
1894 /**
1895 * Given an array of all assessments done by a single reviewer, calculates the final grading grade
1896 *
1897 * This calculates the simple mean of the passed grading grades. If, however, the grading grade
1898 * was overridden by a teacher, the gradinggradeover value is returned and the rest of grades are ignored.
1899 *
7a789aa8 1900 * @param array $assessments of stdclass(->reviewerid ->gradinggrade ->gradinggradeover ->aggregationid ->aggregatedgrade)
1fed6ce3 1901 * @return void
39411930
DM
1902 */
1903 protected function aggregate_grading_grades_process(array $assessments) {
1904 global $DB;
1905
1906 $reviewerid = null; // the id of the reviewer being processed
1907 $current = null; // the gradinggrade currently saved in database
1908 $finalgrade = null; // the new grade to be calculated
1909 $agid = null; // aggregation id
1910 $sumgrades = 0;
1911 $count = 0;
1912
1913 foreach ($assessments as $assessment) {
1914 if (is_null($reviewerid)) {
1915 // the id is the same in all records, fetch it during the first loop cycle
1916 $reviewerid = $assessment->reviewerid;
1917 }
1918 if (is_null($agid)) {
1919 // the id is the same in all records, fetch it during the first loop cycle
1920 $agid = $assessment->aggregationid;
1921 }
1922 if (is_null($current)) {
1923 // the currently saved grade is the same in all records, fetch it during the first loop cycle
1924 $current = $assessment->aggregatedgrade;
1925 }
1926 if (!is_null($assessment->gradinggradeover)) {
5924db72 1927 // the grading grade for this assessment is overridden by a teacher
39411930
DM
1928 $sumgrades += $assessment->gradinggradeover;
1929 $count++;
1930 } else {
1931 if (!is_null($assessment->gradinggrade)) {
1932 $sumgrades += $assessment->gradinggrade;
1933 $count++;
1934 }
1935 }
1936 }
1937 if ($count > 0) {
1938 $finalgrade = grade_floatval($sumgrades / $count);
1939 }
1940 // check if the new final grade differs from the one stored in the database
1941 if (grade_floats_different($finalgrade, $current)) {
1942 // we need to save new calculation into the database
1943 if (is_null($agid)) {
1944 // no aggregation record yet
7a789aa8 1945 $record = new stdclass();
39411930
DM
1946 $record->workshopid = $this->id;
1947 $record->userid = $reviewerid;
1948 $record->gradinggrade = $finalgrade;
10bc4bce 1949 $record->timegraded = time();
39411930
DM
1950 $DB->insert_record('workshop_aggregations', $record);
1951 } else {
7a789aa8 1952 $record = new stdclass();
10bc4bce
DM
1953 $record->id = $agid;
1954 $record->gradinggrade = $finalgrade;
1955 $record->timegraded = time();
1956 $DB->update_record('workshop_aggregations', $record);
39411930
DM
1957 }
1958 }
1959 }
1960
6516b9e9 1961 /**
aa40adbf 1962 * Given a list of user ids, returns the filtered one containing just ids of users with own submission
6516b9e9 1963 *
aa40adbf
DM
1964 * Example submissions are ignored.
1965 *
1966 * @param array $userids
6516b9e9
DM
1967 * @return array
1968 */
aa40adbf
DM
1969 protected function users_with_submission(array $userids) {
1970 global $DB;
1971
1972 if (empty($userids)) {
1973 return array();
1974 }
1975 $userswithsubmission = array();
1976 list($usql, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
00aca3c1 1977 $sql = "SELECT id,authorid
aa40adbf 1978 FROM {workshop_submissions}
00aca3c1 1979 WHERE example = 0 AND workshopid = :workshopid AND authorid $usql";
aa40adbf
DM
1980 $params = array('workshopid' => $this->id);
1981 $params = array_merge($params, $uparams);
1982 $submissions = $DB->get_records_sql($sql, $params);
1983 foreach ($submissions as $submission) {
00aca3c1 1984 $userswithsubmission[$submission->authorid] = true;
aa40adbf
DM
1985 }
1986
1987 return $userswithsubmission;
6516b9e9
DM
1988 }
1989
aa40adbf
DM
1990 /**
1991 * @return array of available workshop phases
1992 */
365c2cc2 1993 protected function available_phases_list() {
aa40adbf
DM
1994 return array(
1995 self::PHASE_SETUP => true,
1996 self::PHASE_SUBMISSION => true,
1997 self::PHASE_ASSESSMENT => true,
1998 self::PHASE_EVALUATION => true,
1999 self::PHASE_CLOSED => true,
2000 );
2001 }
2002
5450f7b6
DM
2003 /**
2004 * Converts absolute URL to relative URL needed by {@see add_to_log()}
2005 *
2006 * @param moodle_url $url absolute URL
2007 * @return string
2008 */
2009 protected function log_convert_url(moodle_url $fullurl) {
2010 static $baseurl;
2011
2012 if (!isset($baseurl)) {
2013 $baseurl = new moodle_url('/mod/workshop/');
2014 $baseurl = $baseurl->out();
2015 }
2016
2017 return substr($fullurl->out(), strlen($baseurl));
2018 }
66c9894d 2019}
55fc1e59 2020
81b22887
DM
2021////////////////////////////////////////////////////////////////////////////////
2022// Renderable components
2023////////////////////////////////////////////////////////////////////////////////
2024
55fc1e59
DM
2025/**
2026 * Represents the user planner tool
2027 *
2028 * Planner contains list of phases. Each phase contains list of tasks. Task is a simple object with
2029 * title, link and completed (true/false/null logic).
2030 */
2031class workshop_user_plan implements renderable {
2032
cff28ef0
DM
2033 /** @var int id of the user this plan is for */
2034 public $userid;
55fc1e59
DM
2035 /** @var workshop */
2036 public $workshop;
2037 /** @var array of (stdclass)tasks */
2038 public $phases = array();
cff28ef0
DM
2039 /** @var null|array of example submissions to be assessed by the planner owner */
2040 protected $examples = null;
55fc1e59
DM
2041
2042 /**
2043 * Prepare an individual workshop plan for the given user.
2044 *
2045 * @param workshop $workshop instance
2046 * @param int $userid whom the plan is prepared for
2047 */
2048 public function __construct(workshop $workshop, $userid) {
2049 global $DB;
2050
2051 $this->workshop = $workshop;
cff28ef0 2052 $this->userid = $userid;
55fc1e59 2053
5bab64a3
DM
2054 //---------------------------------------------------------
2055 // * SETUP | submission | assessment | evaluation | closed
2056 //---------------------------------------------------------
55fc1e59
DM
2057 $phase = new stdclass();
2058 $phase->title = get_string('phasesetup', 'workshop');
2059 $phase->tasks = array();
cff28ef0 2060 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
55fc1e59
DM
2061 $task = new stdclass();
2062 $task->title = get_string('taskintro', 'workshop');
cff28ef0 2063 $task->link = $workshop->updatemod_url();
bfbca63d 2064 $task->completed = !(trim($workshop->intro) == '');
55fc1e59
DM
2065 $phase->tasks['intro'] = $task;
2066 }
cff28ef0 2067 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
55fc1e59
DM
2068 $task = new stdclass();
2069 $task->title = get_string('taskinstructauthors', 'workshop');
cff28ef0 2070 $task->link = $workshop->updatemod_url();
bfbca63d 2071 $task->completed = !(trim($workshop->instructauthors) == '');
55fc1e59
DM
2072 $phase->tasks['instructauthors'] = $task;
2073 }
cff28ef0 2074 if (has_capability('mod/workshop:editdimensions', $workshop->context, $userid)) {
55fc1e59
DM
2075 $task = new stdclass();
2076 $task->title = get_string('editassessmentform', 'workshop');
cff28ef0
DM
2077 $task->link = $workshop->editform_url();
2078 if ($workshop->grading_strategy_instance()->form_ready()) {
55fc1e59 2079 $task->completed = true;
cff28ef0 2080 } elseif ($workshop->phase > workshop::PHASE_SETUP) {
55fc1e59
DM
2081 $task->completed = false;
2082 }
2083 $phase->tasks['editform'] = $task;
2084 }
cff28ef0 2085 if ($workshop->useexamples and has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) {
55fc1e59
DM
2086 $task = new stdclass();
2087 $task->title = get_string('prepareexamples', 'workshop');
cff28ef0 2088 if ($DB->count_records('workshop_submissions', array('example' => 1, 'workshopid' => $workshop->id)) > 0) {
55fc1e59 2089 $task->completed = true;
cff28ef0 2090 } elseif ($workshop->phase > workshop::PHASE_SETUP) {
55fc1e59
DM
2091 $task->completed = false;
2092 }
2093 $phase->tasks['prepareexamples'] = $task;
2094 }
cff28ef0 2095 if (empty($phase->tasks) and $workshop->phase == workshop::PHASE_SETUP) {
55fc1e59
DM
2096 // if we are in the setup phase and there is no task (typical for students), let us
2097 // display some explanation what is going on
2098 $task = new stdclass();
2099 $task->title = get_string('undersetup', 'workshop');
2100 $task->completed = 'info';
2101 $phase->tasks['setupinfo'] = $task;
2102 }
2103 $this->phases[workshop::PHASE_SETUP] = $phase;
2104
5bab64a3
DM
2105 //---------------------------------------------------------
2106 // setup | * SUBMISSION | assessment | evaluation | closed
2107 //---------------------------------------------------------
55fc1e59
DM
2108 $phase = new stdclass();
2109 $phase->title = get_string('phasesubmission', 'workshop');
2110 $phase->tasks = array();
cff28ef0
DM
2111 if (($workshop->usepeerassessment or $workshop->useselfassessment)
2112 and has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
55fc1e59
DM
2113 $task = new stdclass();
2114 $task->title = get_string('taskinstructreviewers', 'workshop');
cff28ef0 2115 $task->link = $workshop->updatemod_url();
bfbca63d 2116 if (trim($workshop->instructreviewers)) {
55fc1e59 2117 $task->completed = true;
cff28ef0 2118 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) {
55fc1e59
DM
2119 $task->completed = false;
2120 }
2121 $phase->tasks['instructreviewers'] = $task;
2122 }
cff28ef0 2123 if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_SUBMISSION
514d8c22 2124 and has_capability('mod/workshop:submit', $workshop->context, $userid, false)
cff28ef0 2125 and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) {
514d8c22
DM
2126 $task = new stdclass();
2127 $task->title = get_string('exampleassesstask', 'workshop');
cff28ef0 2128 $examples = $this->get_examples();
514d8c22
DM
2129 $a = new stdclass();
2130 $a->expected = count($examples);
2131 $a->assessed = 0;
2132 foreach ($examples as $exampleid => $example) {
2133 if (!is_null($example->grade)) {
2134 $a->assessed++;
2135 }
2136 }
2137 $task->details = get_string('exampleassesstaskdetails', 'workshop', $a);
2138 if ($a->assessed == $a->expected) {
2139 $task->completed = true;
cff28ef0 2140 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) {
514d8c22
DM
2141 $task->completed = false;
2142 }
2143 $phase->tasks['examples'] = $task;
2144 }
cff28ef0 2145 if (has_capability('mod/workshop:submit', $workshop->context, $userid, false)) {
55fc1e59
DM
2146 $task = new stdclass();
2147 $task->title = get_string('tasksubmit', 'workshop');
cff28ef0
DM
2148 $task->link = $workshop->submission_url();
2149 if ($DB->record_exists('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0, 'authorid'=>$userid))) {
55fc1e59 2150 $task->completed = true;
cff28ef0 2151 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) {
55fc1e59
DM
2152 $task->completed = false;
2153 } else {
2154 $task->completed = null; // still has a chance to submit
2155 }
2156 $phase->tasks['submit'] = $task;
2157 }
cff28ef0 2158 if (has_capability('mod/workshop:allocate', $workshop->context, $userid)) {
55fc1e59
DM
2159 $task = new stdclass();
2160 $task->title = get_string('allocate', 'workshop');
cff28ef0
DM
2161 $task->link = $workshop->allocation_url();
2162 $numofauthors = count(get_users_by_capability($workshop->context, 'mod/workshop:submit', 'u.id', '', '', '',
55fc1e59 2163 '', '', false, true));
cff28ef0 2164 $numofsubmissions = $DB->count_records('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0));
55fc1e59
DM
2165 $sql = 'SELECT COUNT(s.id) AS nonallocated
2166 FROM {workshop_submissions} s
2167 LEFT JOIN {workshop_assessments} a ON (a.submissionid=s.id)
2168 WHERE s.workshopid = :workshopid AND s.example=0 AND a.submissionid IS NULL';
cff28ef0 2169 $params['workshopid'] = $workshop->id;
55fc1e59
DM
2170 $numnonallocated = $DB->count_records_sql($sql, $params);
2171 if ($numofsubmissions == 0) {
2172 $task->completed = null;
2173 } elseif ($numnonallocated == 0) {
2174 $task->completed = true;
cff28ef0 2175 } elseif ($workshop->phase > workshop::PHASE_SUBMISSION) {
55fc1e59
DM
2176 $task->completed = false;
2177 } else {
2178 $task->completed = null; // still has a chance to allocate
2179 }
2180 $a = new stdclass();
2181 $a->expected = $numofauthors;
2182 $a->submitted = $numofsubmissions;
2183 $a->allocate = $numnonallocated;
2184 $task->details = get_string('allocatedetails', 'workshop', $a);
2185 unset($a);
2186 $phase->tasks['allocate'] = $task;
2187
cff28ef0 2188 if ($numofsubmissions < $numofauthors and $workshop->phase >= workshop::PHASE_SUBMISSION) {
55fc1e59
DM
2189 $task = new stdclass();
2190 $task->title = get_string('someuserswosubmission', 'workshop');
2191 $task->completed = 'info';
2192 $phase->tasks['allocateinfo'] = $task;
2193 }
2194 }
cff28ef0 2195 if ($workshop->submissionstart) {
5bab64a3 2196 $task = new stdclass();
cff28ef0 2197 $task->title = get_string('submissionstartdatetime', 'workshop', workshop::timestamp_formats($workshop->submissionstart));
5bab64a3
DM
2198 $task->completed = 'info';
2199 $phase->tasks['submissionstartdatetime'] = $task;
2200 }
cff28ef0 2201 if ($workshop->submissionend) {
5bab64a3 2202 $task = new stdclass();
cff28ef0 2203 $task->title = get_string('submissionenddatetime', 'workshop', workshop::timestamp_formats($workshop->submissionend));
5bab64a3
DM
2204 $task->completed = 'info';
2205 $phase->tasks['submissionenddatetime'] = $task;
2206 }
2f289d36
DM
2207 if (($workshop->submissionstart < time()) and $workshop->latesubmissions) {
2208 $task = new stdclass();
2209 $task->title = get_string('latesubmissionsallowed', 'workshop');
2210 $task->completed = 'info';
2211 $phase->tasks['latesubmissionsallowed'] = $task;
2212 }
9ddff589
DM
2213 if (isset($phase->tasks['submissionstartdatetime']) or isset($phase->tasks['submissionenddatetime'])) {
2214 if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) {
2215 $task = new stdclass();
2216 $task->title = get_string('deadlinesignored', 'workshop');
2217 $task->completed = 'info';
2218 $phase->tasks['deadlinesignored'] = $task;
2219 }
2220 }
55fc1e59
DM
2221 $this->phases[workshop::PHASE_SUBMISSION] = $phase;
2222
5bab64a3
DM
2223 //---------------------------------------------------------
2224 // setup | submission | * ASSESSMENT | evaluation | closed
2225 //---------------------------------------------------------
55fc1e59
DM
2226 $phase = new stdclass();
2227 $phase->title = get_string('phaseassessment', 'workshop');
2228 $phase->tasks = array();
cff28ef0
DM
2229 $phase->isreviewer = has_capability('mod/workshop:peerassess', $workshop->context, $userid);
2230 if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_ASSESSMENT
2231 and $phase->isreviewer and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) {
2232 $task = new stdclass();
2233 $task->title = get_string('exampleassesstask', 'workshop');
2234 $examples = $workshop->get_examples_for_reviewer($userid);
2235 $a = new stdclass();
2236 $a->expected = count($examples);
2237 $a->assessed = 0;
2238 foreach ($examples as $exampleid => $example) {
2239 if (!is_null($example->grade)) {
2240 $a->assessed++;
55fc1e59
DM
2241 }
2242 }
cff28ef0
DM
2243 $task->details = get_string('exampleassesstaskdetails', 'workshop', $a);
2244 if ($a->assessed == $a->expected) {
55fc1e59 2245 $task->completed = true;
cff28ef0 2246 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) {
55fc1e59
DM
2247 $task->completed = false;
2248 }
cff28ef0 2249 $phase->tasks['examples'] = $task;
55fc1e59 2250 }
cff28ef0
DM
2251 if (empty($phase->tasks['examples']) or !empty($phase->tasks['examples']->completed)) {
2252 $phase->assessments = $workshop->get_assessments_by_reviewer($userid);
2253 $numofpeers = 0; // number of allocated peer-assessments
2254 $numofpeerstodo = 0; // number of peer-assessments to do
2255 $numofself = 0; // number of allocated self-assessments - should be 0 or 1
2256 $numofselftodo = 0; // number of self-assessments to do - should be 0 or 1
2257 foreach ($phase->assessments as $a) {
2258 if ($a->authorid == $userid) {
2259 $numofself++;
2260 if (is_null($a->grade)) {
2261 $numofselftodo++;
2262 }
2263 } else {
2264 $numofpeers++;
2265 if (is_null($a->grade)) {
2266 $numofpeerstodo++;
2267 }
2268 }
2269 }
2270 unset($a);
2271 if ($workshop->usepeerassessment and $numofpeers) {
2272 $task = new stdclass();
2273 if ($numofpeerstodo == 0) {
2274 $task->completed = true;
2275 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) {
2276 $task->completed = false;
2277 }
2278 $a = new stdclass();
2279 $a->total = $numofpeers;
2280 $a->todo = $numofpeerstodo;
2281 $task->title = get_string('taskassesspeers', 'workshop');
2282 $task->details = get_string('taskassesspeersdetails', 'workshop', $a);
2283 unset($a);
2284 $phase->tasks['assesspeers'] = $task;
2285 }
2286 if ($workshop->useselfassessment and $numofself) {
2287 $task = new stdclass();
2288 if ($numofselftodo == 0) {
2289 $task->completed = true;
2290 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) {
2291 $task->completed = false;
2292 }
2293 $task->title = get_string('taskassessself', 'workshop');
2294 $phase->tasks['assessself'] = $task;
55fc1e59 2295 }
55fc1e59 2296 }
cff28ef0 2297 if ($workshop->assessmentstart) {
5bab64a3 2298 $task = new stdclass();
cff28ef0 2299 $task->title = get_string('assessmentstartdatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentstart));
5bab64a3
DM
2300 $task->completed = 'info';
2301 $phase->tasks['assessmentstartdatetime'] = $task;
2302 }
cff28ef0 2303 if ($workshop->assessmentend) {
5bab64a3 2304 $task = new stdclass();
cff28ef0 2305 $task->title = get_string('assessmentenddatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentend));
5bab64a3
DM
2306 $task->completed = 'info';
2307 $phase->tasks['assessmentenddatetime'] = $task;
2308 }
9ddff589
DM
2309 if (isset($phase->tasks['assessmentstartdatetime']) or isset($phase->tasks['assessmentenddatetime'])) {
2310 if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) {
2311 $task = new stdclass();
2312 $task->title = get_string('deadlinesignored', 'workshop');
2313 $task->completed = 'info';
2314 $phase->tasks['deadlinesignored'] = $task;
2315 }
2316 }
55fc1e59
DM
2317 $this->phases[workshop::PHASE_ASSESSMENT] = $phase;
2318
5bab64a3
DM
2319 //---------------------------------------------------------
2320 // setup | submission | assessment | * EVALUATION | closed
2321 //---------------------------------------------------------
55fc1e59
DM
2322 $phase = new stdclass();
2323 $phase->title = get_string('phaseevaluation', 'workshop');
2324 $phase->tasks = array();
cff28ef0
DM
2325 if (has_capability('mod/workshop:overridegrades', $workshop->context)) {
2326 $expected = count($workshop->get_potential_authors(false));
55fc1e59 2327 $calculated = $DB->count_records_select('workshop_submissions',
cff28ef0 2328 'workshopid = ? AND (grade IS NOT NULL OR gradeover IS NOT NULL)', array($workshop->id));
55fc1e59
DM
2329 $task = new stdclass();
2330 $task->title = get_string('calculatesubmissiongrades', 'workshop');
2331 $a = new stdclass();
2332 $a->expected = $expected;
2333 $a->calculated = $calculated;
2334 $task->details = get_string('calculatesubmissiongradesdetails', 'workshop', $a);
2335 if ($calculated >= $expected) {
2336 $task->completed = true;
cff28ef0 2337 } elseif ($workshop->phase > workshop::PHASE_EVALUATION) {
55fc1e59
DM
2338 $task->completed = false;
2339 }
2340 $phase->tasks['calculatesubmissiongrade'] = $task;
2341
cff28ef0 2342 $expected = count($workshop->get_potential_reviewers(false));
55fc1e59 2343 $calculated = $DB->count_records_select('workshop_aggregations',
cff28ef0 2344 'workshopid = ? AND gradinggrade IS NOT NULL', array($workshop->id));
55fc1e59
DM
2345 $task = new stdclass();
2346 $task->title = get_string('calculategradinggrades', 'workshop');
2347 $a = new stdclass();
2348 $a->expected = $expected;
2349 $a->calculated = $calculated;
2350 $task->details = get_string('calculategradinggradesdetails', 'workshop', $a);
2351 if ($calculated >= $expected) {
2352 $task->completed = true;
cff28ef0 2353 } elseif ($workshop->phase > workshop::PHASE_EVALUATION) {
55fc1e59
DM
2354 $task->completed = false;
2355 }
2356 $phase->tasks['calculategradinggrade'] = $task;
2357
cff28ef0 2358 } elseif ($workshop->phase == workshop::PHASE_EVALUATION) {
55fc1e59
DM
2359 $task = new stdclass();
2360 $task->title = get_string('evaluategradeswait', 'workshop');
2361 $task->completed = 'info';
2362 $phase->tasks['evaluateinfo'] = $task;
2363 }
2364 $this->phases[workshop::PHASE_EVALUATION] = $phase;
2365
5bab64a3
DM
2366 //---------------------------------------------------------
2367 // setup | submission | assessment | evaluation | * CLOSED
2368 //---------------------------------------------------------
55fc1e59
DM
2369 $phase = new stdclass();
2370 $phase->title = get_string('phaseclosed', 'workshop');
2371 $phase->tasks = array();
2372 $this->phases[workshop::PHASE_CLOSED] = $phase;
2373
2374 // Polish data, set default values if not done explicitly
2375 foreach ($this->phases as $phasecode => $phase) {
2376 $phase->title = isset($phase->title) ? $phase->title : '';
2377 $phase->tasks = isset($phase->tasks) ? $phase->tasks : array();
cff28ef0 2378 if ($phasecode == $workshop->phase) {
55fc1e59
DM
2379 $phase->active = true;
2380 } else {
2381 $phase->active = false;
2382 }
2383 if (!isset($phase->actions)) {
2384 $phase->actions = array();
2385 }
2386
2387 foreach ($phase->tasks as $taskcode => $task) {
2388 $task->title = isset($task->title) ? $task->title : '';
2389 $task->link = isset($task->link) ? $task->link : null;
2390 $task->details = isset($task->details) ? $task->details : '';
2391 $task->completed = isset($task->completed) ? $task->completed : null;
2392 }
2393 }
2394
5924db72 2395 // Add phase switching actions
cff28ef0 2396 if (has_capability('mod/workshop:switchphase', $workshop->context, $userid)) {
55fc1e59
DM
2397 foreach ($this->phases as $phasecode => $phase) {
2398 if (! $phase->active) {
2399 $action = new stdclass();
2400 $action->type = 'switchphase';
cff28ef0 2401 $action->url = $workshop->switchphase_url($phasecode);
55fc1e59
DM
2402 $phase->actions[] = $action;
2403 }
2404 }
2405 }
2406 }
cff28ef0
DM
2407
2408 /**
2409 * Returns example submissions to be assessed by the owner of the planner
2410 *
2411 * This is here to cache the DB query because the same list is needed later in view.php
2412 *
2413 * @see workshop::get_examples_for_reviewer() for the format of returned value
2414 * @return array
2415 */
2416 public function get_examples() {
2417 if (is_null($this->examples)) {
2418 $this->examples = $this->workshop->get_examples_for_reviewer($this->userid);
2419 }
2420 return $this->examples;
2421 }
55fc1e59 2422}
81b22887
DM
2423
2424/**
2425 * Common base class for submissions and example submissions rendering
2426 *
2427 * Subclasses of this class convert raw submission record from
2428 * workshop_submissions table (as returned by {@see workshop::get_submission_by_id()}
2429 * for example) into renderable objects.
2430 */
2431abstract class workshop_submission_base {
2432
de6aaa72 2433 /** @var bool is the submission anonymous (i.e. contains author information) */
81b22887
DM
2434 protected $anonymous;
2435
2436 /* @var array of columns from workshop_submissions that are assigned as properties */
2437 protected $fields = array();
2438
2439 /**
2440 * Copies the properties of the given database record into properties of $this instance
2441 *
2442 * @param stdClass $submission full record
2443 * @param bool $showauthor show the author-related information
2444 * @param array $options additional properties
2445 */
2446 public function __construct(stdClass $submission, $showauthor = false) {
2447
2448 foreach ($this->fields as $field) {
2449 if (!property_exists($submission, $field)) {
2450 throw new coding_exception('Submission record must provide public property ' . $field);
2451 }
2452 if (!property_exists($this, $field)) {
2453 throw new coding_exception('Renderable component must accept public property ' . $field);
2454 }
2455 $this->{$field} = $submission->{$field};
2456 }
2457
2458 if ($showauthor) {
2459 $this->anonymous = false;
2460 } else {
2461 $this->anonymize();
2462 }
2463 }
2464
2465 /**
2466 * Unsets all author-related properties so that the renderer does not have access to them
2467 *
2468 * Usually this is called by the contructor but can be called explicitely, too.
2469 */
2470 public function anonymize() {
2471 foreach (array('authorid', 'authorfirstname', 'authorlastname',
2472 'authorpicture', 'authorimagealt', 'authoremail') as $field) {
2473 unset($this->{$field});
2474 }
2475 $this->anonymous = true;
2476 }
2477
2478 /**
2479 * Does the submission object contain author-related information?
2480 *
2481 * @return null|boolean
2482 */
2483 public function is_anonymous() {
2484 return $this->anonymous;
2485 }
2486}
2487
2488/**
2489 * Renderable object containing a basic set of information needed to display the submission summary
2490 *
2491 * @see workshop_renderer::render_workshop_submission_summary
2492 */
2493class workshop_submission_summary extends workshop_submission_base implements renderable {
2494
2495 /** @var int */
2496 public $id;
2497 /** @var string */
2498 public $title;
2499 /** @var string graded|notgraded */
2500 public $status;
2501 /** @var int */
2502 public $timecreated;
2503 /** @var int */
2504 public $timemodified;
2505 /** @var int */
2506 public $authorid;
2507 /** @var string */
2508 public $authorfirstname;
2509 /** @var string */
2510 public $authorlastname;
2511 /** @var int */
2512 public $authorpicture;
2513 /** @var string */
2514 public $authorimagealt;
2515 /** @var string */
2516 public $authoremail;
2517 /** @var moodle_url to display submission */
2518 public $url;
2519
2520 /**
2521 * @var array of columns from workshop_submissions that are assigned as properties
2522 * of instances of this class
2523 */
2524 protected $fields = array(
2525 'id', 'title', 'timecreated', 'timemodified',
2526 'authorid', 'authorfirstname', 'authorlastname', 'authorpicture',
2527 'authorimagealt', 'authoremail');
2528}
2529
2530/**
2531 * Renderable object containing all the information needed to display the submission
2532 *
2533 * @see workshop_renderer::render_workshop_submission()
2534 */
2535class workshop_submission extends workshop_submission_summary implements renderable {
2536
2537 /** @var string */
2538 public $content;
2539 /** @var int */
2540 public $contentformat;
2541 /** @var bool */
2542 public $contenttrust;
2543 /** @var array */
2544 public $attachment;
2545
2546 /**
2547 * @var array of columns from workshop_submissions that are assigned as properties
2548 * of instances of this class
2549 */
2550 protected $fields = array(
2551 'id', 'title', 'timecreated', 'timemodified', 'content', 'contentformat', 'contenttrust',
2552 'attachment', 'authorid', 'authorfirstname', 'authorlastname', 'authorpicture',
2553 'authorimagealt', 'authoremail');
2554}
2555
2556/**
2557 * Renderable object containing a basic set of information needed to display the example submission summary
2558 *
2559 * @see workshop::prepare_example_summary()
2560 * @see workshop_renderer::render_workshop_example_submission_summary()
2561 */
2562class workshop_example_submission_summary extends workshop_submission_base implements renderable {
2563
2564 /** @var int */
2565 public $id;
2566 /** @var string */
2567 public $title;
2568 /** @var string graded|notgraded */
2569 public $status;
2570 /** @var stdClass */
2571 public $gradeinfo;
2572 /** @var moodle_url */
2573 public $url;
2574 /** @var moodle_url */
2575 public $editurl;
2576 /** @var string */
2577 public $assesslabel;
2578 /** @var moodle_url */
2579 public $assessurl;
2580 /** @var bool must be set explicitly by the caller */
2581 public $editable = false;
2582
2583 /**
2584 * @var array of columns from workshop_submissions that are assigned as properties
2585 * of instances of this class
2586 */
2587 protected $fields = array('id', 'title');
2588
2589 /**
2590 * Example submissions are always anonymous
2591 *
2592 * @return true
2593 */
2594 public function is_anonymous() {
2595 return true;
2596 }
2597}
2598
2599/**
2600 * Renderable object containing all the information needed to display the example submission
2601 *
2602 * @see workshop_renderer::render_workshop_example_submission()
2603 */
2604class workshop_example_submission extends workshop_example_submission_summary implements renderable {
2605
2606 /** @var string */
2607 public $content;
2608 /** @var int */
2609 public $contentformat;
2610 /** @var bool */
2611 public $contenttrust;
2612 /** @var array */
2613 public $attachment;
2614
2615 /**
2616 * @var array of columns from workshop_submissions that are assigned as properties
2617 * of instances of this class
2618 */
2619 protected $fields = array('id', 'title', 'content', 'contentformat', 'contenttrust', 'attachment');
2620}
a8b309a3
DM
2621
2622/**
2623 * Renderable message to be displayed to the user
2624 *
2625 * Message can contain an optional action link with a label that is supposed to be rendered
2626 * as a button or a link.
2627 *
2628 * @see workshop::renderer::render_workshop_message()
2629 */
2630class workshop_message implements renderable {
2631
2632 const TYPE_INFO = 10;
2633 const TYPE_OK = 20;
2634 const TYPE_ERROR = 30;
2635
2636 /** @var string */
2637 protected $text = '';
2638 /** @var int */
2639 protected $type = self::TYPE_INFO;
2640 /** @var moodle_url */
2641 protected $actionurl = null;
2642 /** @var string */
2643 protected $actionlabel = '';
2644
2645 /**
2646 * @param string $text short text to be displayed
2647 * @param string $type optional message type info|ok|error
2648 */
2649 public function __construct($text = null, $type = self::TYPE_INFO) {
2650 $this->set_text($text);
2651 $this->set_type($type);
2652 }
2653
2654 /**
2655 * Sets the message text
2656 *
2657 * @param string $text short text to be displayed
2658 */
2659 public function set_text($text) {
2660 $this->text = $text;
2661 }
2662
2663 /**
2664 * Sets the message type
2665 *
2666 * @param int $type
2667 */
2668 public function set_type($type = self::TYPE_INFO) {
2669 if (in_array($type, array(self::TYPE_OK, self::TYPE_ERROR, self::TYPE_INFO))) {
2670 $this->type = $type;
2671 } else {
2672 throw new coding_exception('Unknown message type.');
2673 }
2674 }
2675
2676 /**
2677 * Sets the optional message action
2678 *
2679 * @param moodle_url $url to follow on action
2680 * @param string $label action label
2681 */
2682 public function set_action(moodle_url $url, $label) {
2683 $this->actionurl = $url;
2684 $this->actionlabel = $label;
2685 }
2686
2687 /**
2688 * Returns message text with HTML tags quoted
2689 *
2690 * @return string
2691 */
2692 public function get_message() {
2693 return s($this->text);
2694 }
2695
2696 /**
2697 * Returns message type
2698 *
2699 * @return int
2700 */
2701 public function get_type() {
2702 return $this->type;
2703 }
2704
2705 /**
2706 * Returns action URL
2707 *
2708 * @return moodle_url|null
2709 */
2710 public function get_action_url() {
2711 return $this->actionurl;
2712 }
2713
2714 /**
2715 * Returns action label
2716 *
2717 * @return string
2718 */
2719 public function get_action_label() {
2720 return $this->actionlabel;
2721 }
2722}
2723
2724/**
2725 * Renderable output of submissions allocation process
2726 */
2727class workshop_allocation_init_result implements renderable {
2728
2729 /** @var workshop_message */
2730 protected $message;
2731 /** @var array of steps */
2732 protected $info = array();
2733 /** @var moodle_url */
2734 protected $continue;
2735
2736 /**
2737 * Supplied argument can be either integer status code or an array of string messages. Messages
2738 * in a array can have optional prefix or prefixes, using '::' as delimiter. Prefixes determine
2739 * the type of the message and may influence its visualisation.
2740 *
2741 * @param mixed $result int|array returned by {@see workshop_allocator::init()}
2742 * @param moodle_url to continue
2743 */
2744 public function __construct($result, moodle_url $continue) {
2745
2746 if ($result === workshop::ALLOCATION_ERROR) {
2747 $this->message = new workshop_message(get_string('allocationerror', 'workshop'), workshop_message::TYPE_ERROR);
2748 } else {
2749 $this->message = new workshop_message(get_string('allocationdone', 'workshop'), workshop_message::TYPE_OK);
2750 if (is_array($result)) {
2751 $this->info = $result;
2752 }
2753 }
2754
2755 $this->continue = $continue;
2756 }
2757
2758 /**
2759 * @return workshop_message instance to render
2760 */
2761 public function get_message() {
2762 return $this->message;
2763 }
2764
2765 /**
2766 * @return array of strings with allocation process details
2767 */
2768 public function get_info() {
2769 return $this->info;
2770 }
2771
2772 /**
2773 * @return moodle_url where the user shoudl continue
2774 */
2775 public function get_continue_url() {
2776 return $this->continue;
2777 }
2778}
c2a35266
DM
2779
2780/**
2781 * Renderable component containing all the data needed to display the grading report
2782 */
2783class workshop_grading_report implements renderable {
2784
2785 /** @var stdClass returned by {@see workshop::prepare_grading_report_data()} */
2786 protected $data;
2787 /** @var stdClass rendering options */
2788 protected $options;
2789
2790 /**
2791 * Grades in $data must be already rounded to the set number of decimals or must be null
2792 * (in which later case, the [mod_workshop,nullgrade] string shall be displayed)
2793 *
2794 * @param stdClass $data prepared by {@link workshop::prepare_grading_report_data()}
2795 * @param stdClass $options display options (showauthornames, showreviewernames, sortby, sorthow, showsubmissiongrade, showgradinggrade)
2796 */
2797 public function __construct(stdClass $data, stdClass $options) {
2798 $this->data = $data;
2799 $this->options = $options;
2800 }
2801
2802 /**
2803 * @return stdClass grading report data
2804 */
2805 public function get_data() {
2806 return $this->data;
2807 }
2808
2809 /**
2810 * @return stdClass rendering options
2811 */
2812 public function get_options() {
2813 return $this->options;
2814 }
2815}