3 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
19 * Library of internal classes and functions for module workshop
21 * All the workshop specific functions, needed to implement the module
22 * logic, should go to here. Instead of having bunch of function named
23 * workshop_something() taking the workshop instance as the first
24 * parameter, we use a class workshop that provides all methods.
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
32 defined('MOODLE_INTERNAL') || die();
34 require_once(dirname(__FILE__).'/lib.php'); // we extend this library here
35 require_once($CFG->libdir . '/gradelib.php'); // we use some rounding and comparing routines here
36 require_once($CFG->libdir . '/filelib.php');
39 * Full-featured workshop API
41 * This wraps the workshop database record with a set of methods that are called
42 * from the module itself. The class should be initialized right after you get
43 * $workshop, $cm and $course records at the begining of the script.
47 /** error status of the {@link self::add_allocation()} */
48 const ALLOCATION_EXISTS = -9999;
50 /** the internal code of the workshop phases as are stored in the database */
51 const PHASE_SETUP = 10;
52 const PHASE_SUBMISSION = 20;
53 const PHASE_ASSESSMENT = 30;
54 const PHASE_EVALUATION = 40;
55 const PHASE_CLOSED = 50;
57 /** the internal code of the examples modes as are stored in the database */
58 const EXAMPLES_VOLUNTARY = 0;
59 const EXAMPLES_BEFORE_SUBMISSION = 1;
60 const EXAMPLES_BEFORE_ASSESSMENT = 2;
62 /** @var stdclass course module record */
65 /** @var stdclass course record */
68 /** @var stdclass context object */
71 /** @var int workshop instance identifier */
74 /** @var string workshop activity name */
77 /** @var string introduction or description of the activity */
80 /** @var int format of the {@link $intro} */
83 /** @var string instructions for the submission phase */
84 public $instructauthors;
86 /** @var int format of the {@link $instructauthors} */
87 public $instructauthorsformat;
89 /** @var string instructions for the assessment phase */
90 public $instructreviewers;
92 /** @var int format of the {@link $instructreviewers} */
93 public $instructreviewersformat;
95 /** @var int timestamp of when the module was modified */
98 /** @var int current phase of workshop, for example {@link workshop::PHASE_SETUP} */
101 /** @var bool optional feature: students practise evaluating on example submissions from teacher */
104 /** @var bool optional feature: students perform peer assessment of others' work (deprecated, consider always enabled) */
105 public $usepeerassessment;
107 /** @var bool optional feature: students perform self assessment of their own work */
108 public $useselfassessment;
110 /** @var float number (10, 5) unsigned, the maximum grade for submission */
113 /** @var float number (10, 5) unsigned, the maximum grade for assessment */
114 public $gradinggrade;
116 /** @var string type of the current grading strategy used in this workshop, for example 'accumulative' */
119 /** @var string the name of the evaluation plugin to use for grading grades calculation */
122 /** @var int number of digits that should be shown after the decimal point when displaying grades */
123 public $gradedecimals;
125 /** @var int number of allowed submission attachments and the files embedded into submission */
126 public $nattachments;
128 /** @var bool allow submitting the work after the deadline */
129 public $latesubmissions;
131 /** @var int maximum size of the one attached file in bytes */
134 /** @var int mode of example submissions support, for example {@link workshop::EXAMPLES_VOLUNTARY} */
135 public $examplesmode;
137 /** @var int if greater than 0 then the submission is not allowed before this timestamp */
138 public $submissionstart;
140 /** @var int if greater than 0 then the submission is not allowed after this timestamp */
141 public $submissionend;
143 /** @var int if greater than 0 then the peer assessment is not allowed before this timestamp */
144 public $assessmentstart;
146 /** @var int if greater than 0 then the peer assessment is not allowed after this timestamp */
147 public $assessmentend;
149 /** @var bool automatically switch to the assessment phase after the submissions deadline */
150 public $phaseswitchassessment;
152 /** @var string conclusion text to be displayed at the end of the activity */
155 /** @var int format of the conclusion text */
156 public $conclusionformat;
158 /** @var int the mode of the overall feedback */
159 public $overallfeedbackmode;
161 /** @var int maximum number of overall feedback attachments */
162 public $overallfeedbackfiles;
164 /** @var int maximum size of one file attached to the overall feedback */
165 public $overallfeedbackmaxbytes;
168 * @var workshop_strategy grading strategy instance
169 * Do not use directly, get the instance using {@link workshop::grading_strategy_instance()}
171 protected $strategyinstance = null;
174 * @var workshop_evaluation grading evaluation instance
175 * Do not use directly, get the instance using {@link workshop::grading_evaluation_instance()}
177 protected $evaluationinstance = null;
180 * Initializes the workshop API instance using the data from DB
182 * Makes deep copy of all passed records properties. Replaces integer $course attribute
183 * with a full database record (course should not be stored in instances table anyway).
185 * @param stdClass $dbrecord Workshop instance data from {workshop} table
186 * @param stdClass $cm Course module record as returned by {@link get_coursemodule_from_id()}
187 * @param stdClass $course Course record from {course} table
188 * @param stdClass $context The context of the workshop instance
190 public function __construct(stdclass $dbrecord, stdclass $cm, stdclass $course, stdclass $context=null) {
191 foreach ($dbrecord as $field => $value) {
192 if (property_exists('workshop', $field)) {
193 $this->{$field} = $value;
197 $this->course = $course;
198 if (is_null($context)) {
199 $this->context = context_module::instance($this->cm->id);
201 $this->context = $context;
205 ////////////////////////////////////////////////////////////////////////////////
207 ////////////////////////////////////////////////////////////////////////////////
210 * Return list of available allocation methods
212 * @return array Array ['string' => 'string'] of localized allocation method names
214 public static function installed_allocators() {
215 $installed = core_component::get_plugin_list('workshopallocation');
217 foreach ($installed as $allocation => $allocationpath) {
218 if (file_exists($allocationpath . '/lib.php')) {
219 $forms[$allocation] = get_string('pluginname', 'workshopallocation_' . $allocation);
222 // usability - make sure that manual allocation appears the first
223 if (isset($forms['manual'])) {
224 $m = array('manual' => $forms['manual']);
225 unset($forms['manual']);
226 $forms = array_merge($m, $forms);
232 * Returns an array of options for the editors that are used for submitting and assessing instructions
234 * @param stdClass $context
235 * @uses EDITOR_UNLIMITED_FILES hard-coded value for the 'maxfiles' option
238 public static function instruction_editors_options(stdclass $context) {
239 return array('subdirs' => 1, 'maxbytes' => 0, 'maxfiles' => -1,
240 'changeformat' => 1, 'context' => $context, 'noclean' => 1, 'trusttext' => 0);
244 * Given the percent and the total, returns the number
246 * @param float $percent from 0 to 100
247 * @param float $total the 100% value
250 public static function percent_to_value($percent, $total) {
251 if ($percent < 0 or $percent > 100) {
252 throw new coding_exception('The percent can not be less than 0 or higher than 100');
255 return $total * $percent / 100;
259 * Returns an array of numeric values that can be used as maximum grades
261 * @return array Array of integers
263 public static function available_maxgrades_list() {
265 for ($i=100; $i>=0; $i--) {
272 * Returns the localized list of supported examples modes
276 public static function available_example_modes_list() {
278 $options[self::EXAMPLES_VOLUNTARY] = get_string('examplesvoluntary', 'workshop');
279 $options[self::EXAMPLES_BEFORE_SUBMISSION] = get_string('examplesbeforesubmission', 'workshop');
280 $options[self::EXAMPLES_BEFORE_ASSESSMENT] = get_string('examplesbeforeassessment', 'workshop');
285 * Returns the list of available grading strategy methods
287 * @return array ['string' => 'string']
289 public static function available_strategies_list() {
290 $installed = core_component::get_plugin_list('workshopform');
292 foreach ($installed as $strategy => $strategypath) {
293 if (file_exists($strategypath . '/lib.php')) {
294 $forms[$strategy] = get_string('pluginname', 'workshopform_' . $strategy);
301 * Returns the list of available grading evaluation methods
303 * @return array of (string)name => (string)localized title
305 public static function available_evaluators_list() {
307 foreach (core_component::get_plugin_list_with_file('workshopeval', 'lib.php', false) as $eval => $evalpath) {
308 $evals[$eval] = get_string('pluginname', 'workshopeval_' . $eval);
314 * Return an array of possible values of assessment dimension weight
316 * @return array of integers 0, 1, 2, ..., 16
318 public static function available_dimension_weights_list() {
320 for ($i=16; $i>=0; $i--) {
327 * Return an array of possible values of assessment weight
329 * Note there is no real reason why the maximum value here is 16. It used to be 10 in
330 * workshop 1.x and I just decided to use the same number as in the maximum weight of
331 * a single assessment dimension.
332 * The value looks reasonable, though. Teachers who would want to assign themselves
333 * higher weight probably do not want peer assessment really...
335 * @return array of integers 0, 1, 2, ..., 16
337 public static function available_assessment_weights_list() {
339 for ($i=16; $i>=0; $i--) {
346 * Helper function returning the greatest common divisor
352 public static function gcd($a, $b) {
353 return ($b == 0) ? ($a):(self::gcd($b, $a % $b));
357 * Helper function returning the least common multiple
363 public static function lcm($a, $b) {
364 return ($a / self::gcd($a,$b)) * $b;
368 * Returns an object suitable for strings containing dates/times
370 * The returned object contains properties date, datefullshort, datetime, ... containing the given
371 * timestamp formatted using strftimedate, strftimedatefullshort, strftimedatetime, ... from the
372 * current lang's langconfig.php
373 * This allows translators and administrators customize the date/time format.
375 * @param int $timestamp the timestamp in UTC
378 public static function timestamp_formats($timestamp) {
379 $formats = array('date', 'datefullshort', 'dateshort', 'datetime',
380 'datetimeshort', 'daydate', 'daydatetime', 'dayshort', 'daytime',
381 'monthyear', 'recent', 'recentfull', 'time');
383 foreach ($formats as $format) {
384 $a->{$format} = userdate($timestamp, get_string('strftime'.$format, 'langconfig'));
386 $day = userdate($timestamp, '%Y%m%d', 99, false);
387 $today = userdate(time(), '%Y%m%d', 99, false);
388 $tomorrow = userdate(time() + DAYSECS, '%Y%m%d', 99, false);
389 $yesterday = userdate(time() - DAYSECS, '%Y%m%d', 99, false);
390 $distance = (int)round(abs(time() - $timestamp) / DAYSECS);
391 if ($day == $today) {
392 $a->distanceday = get_string('daystoday', 'workshop');
393 } elseif ($day == $yesterday) {
394 $a->distanceday = get_string('daysyesterday', 'workshop');
395 } elseif ($day < $today) {
396 $a->distanceday = get_string('daysago', 'workshop', $distance);
397 } elseif ($day == $tomorrow) {
398 $a->distanceday = get_string('daystomorrow', 'workshop');
399 } elseif ($day > $today) {
400 $a->distanceday = get_string('daysleft', 'workshop', $distance);
405 ////////////////////////////////////////////////////////////////////////////////
407 ////////////////////////////////////////////////////////////////////////////////
410 * Fetches all enrolled users with the capability mod/workshop:submit in the current workshop
412 * The returned objects contain properties required by user_picture and are ordered by lastname, firstname.
413 * Only users with the active enrolment are returned.
415 * @param bool $musthavesubmission if true, return only users who have already submitted
416 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
417 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set)
418 * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set)
419 * @return array array[userid] => stdClass
421 public function get_potential_authors($musthavesubmission=true, $groupid=0, $limitfrom=0, $limitnum=0) {
424 list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:submit', $musthavesubmission, $groupid);
430 list($sort, $sortparams) = users_order_by_sql('tmp');
435 return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum);
439 * Returns the total number of users that would be fetched by {@link self::get_potential_authors()}
441 * @param bool $musthavesubmission if true, count only users who have already submitted
442 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
445 public function count_potential_authors($musthavesubmission=true, $groupid=0) {
448 list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:submit', $musthavesubmission, $groupid);
454 $sql = "SELECT COUNT(*)
457 return $DB->count_records_sql($sql, $params);
461 * Fetches all enrolled users with the capability mod/workshop:peerassess in the current workshop
463 * The returned objects contain properties required by user_picture and are ordered by lastname, firstname.
464 * Only users with the active enrolment are returned.
466 * @param bool $musthavesubmission if true, return only users who have already submitted
467 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
468 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set)
469 * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set)
470 * @return array array[userid] => stdClass
472 public function get_potential_reviewers($musthavesubmission=false, $groupid=0, $limitfrom=0, $limitnum=0) {
475 list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:peerassess', $musthavesubmission, $groupid);
481 list($sort, $sortparams) = users_order_by_sql('tmp');
486 return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum);
490 * Returns the total number of users that would be fetched by {@link self::get_potential_reviewers()}
492 * @param bool $musthavesubmission if true, count only users who have already submitted
493 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
496 public function count_potential_reviewers($musthavesubmission=false, $groupid=0) {
499 list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:peerassess', $musthavesubmission, $groupid);
505 $sql = "SELECT COUNT(*)
508 return $DB->count_records_sql($sql, $params);
512 * Fetches all enrolled users that are authors or reviewers (or both) in the current workshop
514 * The returned objects contain properties required by user_picture and are ordered by lastname, firstname.
515 * Only users with the active enrolment are returned.
517 * @see self::get_potential_authors()
518 * @see self::get_potential_reviewers()
519 * @param bool $musthavesubmission if true, return only users who have already submitted
520 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
521 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set)
522 * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set)
523 * @return array array[userid] => stdClass
525 public function get_participants($musthavesubmission=false, $groupid=0, $limitfrom=0, $limitnum=0) {
528 list($sql, $params) = $this->get_participants_sql($musthavesubmission, $groupid);
534 list($sort, $sortparams) = users_order_by_sql('tmp');
539 return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum);
543 * Returns the total number of records that would be returned by {@link self::get_participants()}
545 * @param bool $musthavesubmission if true, return only users who have already submitted
546 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
549 public function count_participants($musthavesubmission=false, $groupid=0) {
552 list($sql, $params) = $this->get_participants_sql($musthavesubmission, $groupid);
558 $sql = "SELECT COUNT(*)
561 return $DB->count_records_sql($sql, $params);
565 * Checks if the given user is an actively enrolled participant in the workshop
567 * @param int $userid, defaults to the current $USER
570 public function is_participant($userid=null) {
573 if (is_null($userid)) {
577 list($sql, $params) = $this->get_participants_sql();
583 $sql = "SELECT COUNT(*)
585 JOIN ({$sql}) pxx ON uxx.id = pxx.id
586 WHERE uxx.id = :uxxid";
587 $params['uxxid'] = $userid;
589 if ($DB->count_records_sql($sql, $params)) {
597 * Groups the given users by the group membership
599 * This takes the module grouping settings into account. If "Available for group members only"
600 * is set, returns only groups withing the course module grouping. Always returns group [0] with
601 * all the given users.
603 * @param array $users array[userid] => stdclass{->id ->lastname ->firstname}
604 * @return array array[groupid][userid] => stdclass{->id ->lastname ->firstname}
606 public function get_grouped($users) {
610 $grouped = array(); // grouped users to be returned
614 if (!empty($CFG->enablegroupmembersonly) and $this->cm->groupmembersonly) {
615 // Available for group members only - the workshop is available only
616 // to users assigned to groups within the selected grouping, or to
617 // any group if no grouping is selected.
618 $groupingid = $this->cm->groupingid;
619 // All users that are members of at least one group will be
620 // added into a virtual group id 0
621 $grouped[0] = array();
624 // there is no need to be member of a group so $grouped[0] will contain
626 $grouped[0] = $users;
628 $gmemberships = groups_get_all_groups($this->cm->course, array_keys($users), $groupingid,
629 'gm.id,gm.groupid,gm.userid');
630 foreach ($gmemberships as $gmembership) {
631 if (!isset($grouped[$gmembership->groupid])) {
632 $grouped[$gmembership->groupid] = array();
634 $grouped[$gmembership->groupid][$gmembership->userid] = $users[$gmembership->userid];
635 $grouped[0][$gmembership->userid] = $users[$gmembership->userid];
641 * Returns the list of all allocations (i.e. assigned assessments) in the workshop
643 * Assessments of example submissions are ignored
647 public function get_allocations() {
650 $sql = 'SELECT a.id, a.submissionid, a.reviewerid, s.authorid
651 FROM {workshop_assessments} a
652 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
653 WHERE s.example = 0 AND s.workshopid = :workshopid';
654 $params = array('workshopid' => $this->id);
656 return $DB->get_records_sql($sql, $params);
660 * Returns the total number of records that would be returned by {@link self::get_submissions()}
662 * @param mixed $authorid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only
663 * @param int $groupid If non-zero, return only submissions by authors in the specified group
664 * @return int number of records
666 public function count_submissions($authorid='all', $groupid=0) {
669 $params = array('workshopid' => $this->id);
670 $sql = "SELECT COUNT(s.id)
671 FROM {workshop_submissions} s
672 JOIN {user} u ON (s.authorid = u.id)";
674 $sql .= " JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)";
675 $params['groupid'] = $groupid;
677 $sql .= " WHERE s.example = 0 AND s.workshopid = :workshopid";
679 if ('all' === $authorid) {
680 // no additional conditions
681 } elseif (!empty($authorid)) {
682 list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED);
683 $sql .= " AND authorid $usql";
684 $params = array_merge($params, $uparams);
686 // $authorid is empty
690 return $DB->count_records_sql($sql, $params);
695 * Returns submissions from this workshop
697 * Fetches data from {workshop_submissions} and adds some useful information from other
698 * tables. Does not return textual fields to prevent possible memory lack issues.
700 * @see self::count_submissions()
701 * @param mixed $authorid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only
702 * @param int $groupid If non-zero, return only submissions by authors in the specified group
703 * @param int $limitfrom Return a subset of records, starting at this point (optional)
704 * @param int $limitnum Return a subset containing this many records in total (optional, required if $limitfrom is set)
705 * @return array of records or an empty array
707 public function get_submissions($authorid='all', $groupid=0, $limitfrom=0, $limitnum=0) {
710 $authorfields = user_picture::fields('u', null, 'authoridx', 'author');
711 $gradeoverbyfields = user_picture::fields('t', null, 'gradeoverbyx', 'over');
712 $params = array('workshopid' => $this->id);
713 $sql = "SELECT s.id, s.workshopid, s.example, s.authorid, s.timecreated, s.timemodified,
714 s.title, s.grade, s.gradeover, s.gradeoverby, s.published,
715 $authorfields, $gradeoverbyfields
716 FROM {workshop_submissions} s
717 JOIN {user} u ON (s.authorid = u.id)";
719 $sql .= " JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)";
720 $params['groupid'] = $groupid;
722 $sql .= " LEFT JOIN {user} t ON (s.gradeoverby = t.id)
723 WHERE s.example = 0 AND s.workshopid = :workshopid";
725 if ('all' === $authorid) {
726 // no additional conditions
727 } elseif (!empty($authorid)) {
728 list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED);
729 $sql .= " AND authorid $usql";
730 $params = array_merge($params, $uparams);
732 // $authorid is empty
735 list($sort, $sortparams) = users_order_by_sql('u');
736 $sql .= " ORDER BY $sort";
738 return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum);
742 * Returns a submission record with the author's data
744 * @param int $id submission id
747 public function get_submission_by_id($id) {
750 // we intentionally check the workshopid here, too, so the workshop can't touch submissions
751 // from other instances
752 $authorfields = user_picture::fields('u', null, 'authoridx', 'author');
753 $gradeoverbyfields = user_picture::fields('g', null, 'gradeoverbyx', 'gradeoverby');
754 $sql = "SELECT s.*, $authorfields, $gradeoverbyfields
755 FROM {workshop_submissions} s
756 INNER JOIN {user} u ON (s.authorid = u.id)
757 LEFT JOIN {user} g ON (s.gradeoverby = g.id)
758 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.id = :id";
759 $params = array('workshopid' => $this->id, 'id' => $id);
760 return $DB->get_record_sql($sql, $params, MUST_EXIST);
764 * Returns a submission submitted by the given author
766 * @param int $id author id
767 * @return stdclass|false
769 public function get_submission_by_author($authorid) {
772 if (empty($authorid)) {
775 $authorfields = user_picture::fields('u', null, 'authoridx', 'author');
776 $gradeoverbyfields = user_picture::fields('g', null, 'gradeoverbyx', 'gradeoverby');
777 $sql = "SELECT s.*, $authorfields, $gradeoverbyfields
778 FROM {workshop_submissions} s
779 INNER JOIN {user} u ON (s.authorid = u.id)
780 LEFT JOIN {user} g ON (s.gradeoverby = g.id)
781 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.authorid = :authorid";
782 $params = array('workshopid' => $this->id, 'authorid' => $authorid);
783 return $DB->get_record_sql($sql, $params);
787 * Returns published submissions with their authors data
789 * @return array of stdclass
791 public function get_published_submissions($orderby='finalgrade DESC') {
794 $authorfields = user_picture::fields('u', null, 'authoridx', 'author');
795 $sql = "SELECT s.id, s.authorid, s.timecreated, s.timemodified,
796 s.title, s.grade, s.gradeover, COALESCE(s.gradeover,s.grade) AS finalgrade,
798 FROM {workshop_submissions} s
799 INNER JOIN {user} u ON (s.authorid = u.id)
800 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.published = 1
802 $params = array('workshopid' => $this->id);
803 return $DB->get_records_sql($sql, $params);
807 * Returns full record of the given example submission
809 * @param int $id example submission od
812 public function get_example_by_id($id) {
814 return $DB->get_record('workshop_submissions',
815 array('id' => $id, 'workshopid' => $this->id, 'example' => 1), '*', MUST_EXIST);
819 * Returns the list of example submissions in this workshop with reference assessments attached
821 * @return array of objects or an empty array
822 * @see workshop::prepare_example_summary()
824 public function get_examples_for_manager() {
827 $sql = 'SELECT s.id, s.title,
828 a.id AS assessmentid, a.grade, a.gradinggrade
829 FROM {workshop_submissions} s
830 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id AND a.weight = 1)
831 WHERE s.example = 1 AND s.workshopid = :workshopid
833 return $DB->get_records_sql($sql, array('workshopid' => $this->id));
837 * Returns the list of all example submissions in this workshop with the information of assessments done by the given user
839 * @param int $reviewerid user id
840 * @return array of objects, indexed by example submission id
841 * @see workshop::prepare_example_summary()
843 public function get_examples_for_reviewer($reviewerid) {
846 if (empty($reviewerid)) {
849 $sql = 'SELECT s.id, s.title,
850 a.id AS assessmentid, a.grade, a.gradinggrade
851 FROM {workshop_submissions} s
852 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id AND a.reviewerid = :reviewerid AND a.weight = 0)
853 WHERE s.example = 1 AND s.workshopid = :workshopid
855 return $DB->get_records_sql($sql, array('workshopid' => $this->id, 'reviewerid' => $reviewerid));
859 * Prepares renderable submission component
861 * @param stdClass $record required by {@see workshop_submission}
862 * @param bool $showauthor show the author-related information
863 * @return workshop_submission
865 public function prepare_submission(stdClass $record, $showauthor = false) {
867 $submission = new workshop_submission($this, $record, $showauthor);
868 $submission->url = $this->submission_url($record->id);
874 * Prepares renderable submission summary component
876 * @param stdClass $record required by {@see workshop_submission_summary}
877 * @param bool $showauthor show the author-related information
878 * @return workshop_submission_summary
880 public function prepare_submission_summary(stdClass $record, $showauthor = false) {
882 $summary = new workshop_submission_summary($this, $record, $showauthor);
883 $summary->url = $this->submission_url($record->id);
889 * Prepares renderable example submission component
891 * @param stdClass $record required by {@see workshop_example_submission}
892 * @return workshop_example_submission
894 public function prepare_example_submission(stdClass $record) {
896 $example = new workshop_example_submission($this, $record);
902 * Prepares renderable example submission summary component
904 * If the example is editable, the caller must set the 'editable' flag explicitly.
906 * @param stdClass $example as returned by {@link workshop::get_examples_for_manager()} or {@link workshop::get_examples_for_reviewer()}
907 * @return workshop_example_submission_summary to be rendered
909 public function prepare_example_summary(stdClass $example) {
911 $summary = new workshop_example_submission_summary($this, $example);
913 if (is_null($example->grade)) {
914 $summary->status = 'notgraded';
915 $summary->assesslabel = get_string('assess', 'workshop');
917 $summary->status = 'graded';
918 $summary->assesslabel = get_string('reassess', 'workshop');
921 $summary->gradeinfo = new stdclass();
922 $summary->gradeinfo->received = $this->real_grade($example->grade);
923 $summary->gradeinfo->max = $this->real_grade(100);
925 $summary->url = new moodle_url($this->exsubmission_url($example->id));
926 $summary->editurl = new moodle_url($this->exsubmission_url($example->id), array('edit' => 'on'));
927 $summary->assessurl = new moodle_url($this->exsubmission_url($example->id), array('assess' => 'on', 'sesskey' => sesskey()));
933 * Prepares renderable assessment component
935 * The $options array supports the following keys:
936 * showauthor - should the author user info be available for the renderer
937 * showreviewer - should the reviewer user info be available for the renderer
938 * showform - show the assessment form if it is available
939 * showweight - should the assessment weight be available for the renderer
941 * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()}
942 * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()}
943 * @param array $options
944 * @return workshop_assessment
946 public function prepare_assessment(stdClass $record, $form, array $options = array()) {
948 $assessment = new workshop_assessment($this, $record, $options);
949 $assessment->url = $this->assess_url($record->id);
950 $assessment->maxgrade = $this->real_grade(100);
952 if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) {
953 debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER);
956 if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) {
957 $assessment->form = $form;
960 if (empty($options['showweight'])) {
961 $assessment->weight = null;
964 if (!is_null($record->grade)) {
965 $assessment->realgrade = $this->real_grade($record->grade);
972 * Prepares renderable example submission's assessment component
974 * The $options array supports the following keys:
975 * showauthor - should the author user info be available for the renderer
976 * showreviewer - should the reviewer user info be available for the renderer
977 * showform - show the assessment form if it is available
979 * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()}
980 * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()}
981 * @param array $options
982 * @return workshop_example_assessment
984 public function prepare_example_assessment(stdClass $record, $form = null, array $options = array()) {
986 $assessment = new workshop_example_assessment($this, $record, $options);
987 $assessment->url = $this->exassess_url($record->id);
988 $assessment->maxgrade = $this->real_grade(100);
990 if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) {
991 debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER);
994 if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) {
995 $assessment->form = $form;
998 if (!is_null($record->grade)) {
999 $assessment->realgrade = $this->real_grade($record->grade);
1002 $assessment->weight = null;
1008 * Prepares renderable example submission's reference assessment component
1010 * The $options array supports the following keys:
1011 * showauthor - should the author user info be available for the renderer
1012 * showreviewer - should the reviewer user info be available for the renderer
1013 * showform - show the assessment form if it is available
1015 * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()}
1016 * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()}
1017 * @param array $options
1018 * @return workshop_example_reference_assessment
1020 public function prepare_example_reference_assessment(stdClass $record, $form = null, array $options = array()) {
1022 $assessment = new workshop_example_reference_assessment($this, $record, $options);
1023 $assessment->maxgrade = $this->real_grade(100);
1025 if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) {
1026 debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER);
1029 if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) {
1030 $assessment->form = $form;
1033 if (!is_null($record->grade)) {
1034 $assessment->realgrade = $this->real_grade($record->grade);
1037 $assessment->weight = null;
1043 * Removes the submission and all relevant data
1045 * @param stdClass $submission record to delete
1048 public function delete_submission(stdclass $submission) {
1050 $assessments = $DB->get_records('workshop_assessments', array('submissionid' => $submission->id), '', 'id');
1051 $this->delete_assessment(array_keys($assessments));
1052 $DB->delete_records('workshop_submissions', array('id' => $submission->id));
1056 * Returns the list of all assessments in the workshop with some data added
1058 * Fetches data from {workshop_assessments} and adds some useful information from other
1059 * tables. The returned object does not contain textual fields (i.e. comments) to prevent memory
1062 * @return array [assessmentid] => assessment stdclass
1064 public function get_all_assessments() {
1067 $reviewerfields = user_picture::fields('reviewer', null, 'revieweridx', 'reviewer');
1068 $authorfields = user_picture::fields('author', null, 'authorid', 'author');
1069 $overbyfields = user_picture::fields('overby', null, 'gradinggradeoverbyx', 'overby');
1070 list($sort, $params) = users_order_by_sql('reviewer');
1071 $sql = "SELECT a.id, a.submissionid, a.reviewerid, a.timecreated, a.timemodified,
1072 a.grade, a.gradinggrade, a.gradinggradeover, a.gradinggradeoverby,
1073 $reviewerfields, $authorfields, $overbyfields,
1075 FROM {workshop_assessments} a
1076 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
1077 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1078 INNER JOIN {user} author ON (s.authorid = author.id)
1079 LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id)
1080 WHERE s.workshopid = :workshopid AND s.example = 0
1082 $params['workshopid'] = $this->id;
1084 return $DB->get_records_sql($sql, $params);
1088 * Get the complete information about the given assessment
1090 * @param int $id Assessment ID
1093 public function get_assessment_by_id($id) {
1096 $reviewerfields = user_picture::fields('reviewer', null, 'revieweridx', 'reviewer');
1097 $authorfields = user_picture::fields('author', null, 'authorid', 'author');
1098 $overbyfields = user_picture::fields('overby', null, 'gradinggradeoverbyx', 'overby');
1099 $sql = "SELECT a.*, s.title, $reviewerfields, $authorfields, $overbyfields
1100 FROM {workshop_assessments} a
1101 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
1102 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1103 INNER JOIN {user} author ON (s.authorid = author.id)
1104 LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id)
1105 WHERE a.id = :id AND s.workshopid = :workshopid";
1106 $params = array('id' => $id, 'workshopid' => $this->id);
1108 return $DB->get_record_sql($sql, $params, MUST_EXIST);
1112 * Get the complete information about the user's assessment of the given submission
1114 * @param int $sid submission ID
1115 * @param int $uid user ID of the reviewer
1116 * @return false|stdclass false if not found, stdclass otherwise
1118 public function get_assessment_of_submission_by_user($submissionid, $reviewerid) {
1121 $reviewerfields = user_picture::fields('reviewer', null, 'revieweridx', 'reviewer');
1122 $authorfields = user_picture::fields('author', null, 'authorid', 'author');
1123 $overbyfields = user_picture::fields('overby', null, 'gradinggradeoverbyx', 'overby');
1124 $sql = "SELECT a.*, s.title, $reviewerfields, $authorfields, $overbyfields
1125 FROM {workshop_assessments} a
1126 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
1127 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0)
1128 INNER JOIN {user} author ON (s.authorid = author.id)
1129 LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id)
1130 WHERE s.id = :sid AND reviewer.id = :rid AND s.workshopid = :workshopid";
1131 $params = array('sid' => $submissionid, 'rid' => $reviewerid, 'workshopid' => $this->id);
1133 return $DB->get_record_sql($sql, $params, IGNORE_MISSING);
1137 * Get the complete information about all assessments of the given submission
1139 * @param int $submissionid
1142 public function get_assessments_of_submission($submissionid) {
1145 $reviewerfields = user_picture::fields('reviewer', null, 'revieweridx', 'reviewer');
1146 $overbyfields = user_picture::fields('overby', null, 'gradinggradeoverbyx', 'overby');
1147 list($sort, $params) = users_order_by_sql('reviewer');
1148 $sql = "SELECT a.*, s.title, $reviewerfields, $overbyfields
1149 FROM {workshop_assessments} a
1150 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
1151 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1152 LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id)
1153 WHERE s.example = 0 AND s.id = :submissionid AND s.workshopid = :workshopid
1155 $params['submissionid'] = $submissionid;
1156 $params['workshopid'] = $this->id;
1158 return $DB->get_records_sql($sql, $params);
1162 * Get the complete information about all assessments allocated to the given reviewer
1164 * @param int $reviewerid
1167 public function get_assessments_by_reviewer($reviewerid) {
1170 $reviewerfields = user_picture::fields('reviewer', null, 'revieweridx', 'reviewer');
1171 $authorfields = user_picture::fields('author', null, 'authorid', 'author');
1172 $overbyfields = user_picture::fields('overby', null, 'gradinggradeoverbyx', 'overby');
1173 $sql = "SELECT a.*, $reviewerfields, $authorfields, $overbyfields,
1174 s.id AS submissionid, s.title AS submissiontitle, s.timecreated AS submissioncreated,
1175 s.timemodified AS submissionmodified
1176 FROM {workshop_assessments} a
1177 INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
1178 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1179 INNER JOIN {user} author ON (s.authorid = author.id)
1180 LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id)
1181 WHERE s.example = 0 AND reviewer.id = :reviewerid AND s.workshopid = :workshopid";
1182 $params = array('reviewerid' => $reviewerid, 'workshopid' => $this->id);
1184 return $DB->get_records_sql($sql, $params);
1188 * Get allocated assessments not graded yet by the given reviewer
1190 * @see self::get_assessments_by_reviewer()
1191 * @param int $reviewerid the reviewer id
1192 * @param null|int|array $exclude optional assessment id (or list of them) to be excluded
1195 public function get_pending_assessments_by_reviewer($reviewerid, $exclude = null) {
1197 $assessments = $this->get_assessments_by_reviewer($reviewerid);
1199 foreach ($assessments as $id => $assessment) {
1200 if (!is_null($assessment->grade)) {
1201 unset($assessments[$id]);
1204 if (!empty($exclude)) {
1205 if (is_array($exclude) and in_array($id, $exclude)) {
1206 unset($assessments[$id]);
1208 } else if ($id == $exclude) {
1209 unset($assessments[$id]);
1215 return $assessments;
1219 * Allocate a submission to a user for review
1221 * @param stdClass $submission Submission object with at least id property
1222 * @param int $reviewerid User ID
1223 * @param int $weight of the new assessment, from 0 to 16
1224 * @param bool $bulk repeated inserts into DB expected
1225 * @return int ID of the new assessment or an error code {@link self::ALLOCATION_EXISTS} if the allocation already exists
1227 public function add_allocation(stdclass $submission, $reviewerid, $weight=1, $bulk=false) {
1230 if ($DB->record_exists('workshop_assessments', array('submissionid' => $submission->id, 'reviewerid' => $reviewerid))) {
1231 return self::ALLOCATION_EXISTS;
1234 $weight = (int)$weight;
1243 $assessment = new stdclass();
1244 $assessment->submissionid = $submission->id;
1245 $assessment->reviewerid = $reviewerid;
1246 $assessment->timecreated = $now; // do not set timemodified here
1247 $assessment->weight = $weight;
1248 $assessment->feedbackauthorformat = editors_get_preferred_format();
1249 $assessment->feedbackreviewerformat = editors_get_preferred_format();
1251 return $DB->insert_record('workshop_assessments', $assessment, true, $bulk);
1255 * Delete assessment record or records
1257 * @param mixed $id int|array assessment id or array of assessments ids
1258 * @return bool false if $id not a valid parameter, true otherwise
1260 public function delete_assessment($id) {
1263 // todo remove all given grades from workshop_grades;
1265 if (is_array($id)) {
1266 return $DB->delete_records_list('workshop_assessments', 'id', $id);
1268 return $DB->delete_records('workshop_assessments', array('id' => $id));
1273 * Returns instance of grading strategy class
1275 * @return stdclass Instance of a grading strategy
1277 public function grading_strategy_instance() {
1278 global $CFG; // because we require other libs here
1280 if (is_null($this->strategyinstance)) {
1281 $strategylib = dirname(__FILE__) . '/form/' . $this->strategy . '/lib.php';
1282 if (is_readable($strategylib)) {
1283 require_once($strategylib);
1285 throw new coding_exception('the grading forms subplugin must contain library ' . $strategylib);
1287 $classname = 'workshop_' . $this->strategy . '_strategy';
1288 $this->strategyinstance = new $classname($this);
1289 if (!in_array('workshop_strategy', class_implements($this->strategyinstance))) {
1290 throw new coding_exception($classname . ' does not implement workshop_strategy interface');
1293 return $this->strategyinstance;
1297 * Sets the current evaluation method to the given plugin.
1299 * @param string $method the name of the workshopeval subplugin
1300 * @return bool true if successfully set
1301 * @throws coding_exception if attempting to set a non-installed evaluation method
1303 public function set_grading_evaluation_method($method) {
1306 $evaluationlib = dirname(__FILE__) . '/eval/' . $method . '/lib.php';
1308 if (is_readable($evaluationlib)) {
1309 $this->evaluationinstance = null;
1310 $this->evaluation = $method;
1311 $DB->set_field('workshop', 'evaluation', $method, array('id' => $this->id));
1315 throw new coding_exception('Attempt to set a non-existing evaluation method.');
1319 * Returns instance of grading evaluation class
1321 * @return stdclass Instance of a grading evaluation
1323 public function grading_evaluation_instance() {
1324 global $CFG; // because we require other libs here
1326 if (is_null($this->evaluationinstance)) {
1327 if (empty($this->evaluation)) {
1328 $this->evaluation = 'best';
1330 $evaluationlib = dirname(__FILE__) . '/eval/' . $this->evaluation . '/lib.php';
1331 if (is_readable($evaluationlib)) {
1332 require_once($evaluationlib);
1334 // Fall back in case the subplugin is not available.
1335 $this->evaluation = 'best';
1336 $evaluationlib = dirname(__FILE__) . '/eval/' . $this->evaluation . '/lib.php';
1337 if (is_readable($evaluationlib)) {
1338 require_once($evaluationlib);
1340 // Fall back in case the subplugin is not available any more.
1341 throw new coding_exception('Missing default grading evaluation library ' . $evaluationlib);
1344 $classname = 'workshop_' . $this->evaluation . '_evaluation';
1345 $this->evaluationinstance = new $classname($this);
1346 if (!in_array('workshop_evaluation', class_parents($this->evaluationinstance))) {
1347 throw new coding_exception($classname . ' does not extend workshop_evaluation class');
1350 return $this->evaluationinstance;
1354 * Returns instance of submissions allocator
1356 * @param string $method The name of the allocation method, must be PARAM_ALPHA
1357 * @return stdclass Instance of submissions allocator
1359 public function allocator_instance($method) {
1360 global $CFG; // because we require other libs here
1362 $allocationlib = dirname(__FILE__) . '/allocation/' . $method . '/lib.php';
1363 if (is_readable($allocationlib)) {
1364 require_once($allocationlib);
1366 throw new coding_exception('Unable to find the allocation library ' . $allocationlib);
1368 $classname = 'workshop_' . $method . '_allocator';
1369 return new $classname($this);
1373 * @return moodle_url of this workshop's view page
1375 public function view_url() {
1377 return new moodle_url('/mod/workshop/view.php', array('id' => $this->cm->id));
1381 * @return moodle_url of the page for editing this workshop's grading form
1383 public function editform_url() {
1385 return new moodle_url('/mod/workshop/editform.php', array('cmid' => $this->cm->id));
1389 * @return moodle_url of the page for previewing this workshop's grading form
1391 public function previewform_url() {
1393 return new moodle_url('/mod/workshop/editformpreview.php', array('cmid' => $this->cm->id));
1397 * @param int $assessmentid The ID of assessment record
1398 * @return moodle_url of the assessment page
1400 public function assess_url($assessmentid) {
1402 $assessmentid = clean_param($assessmentid, PARAM_INT);
1403 return new moodle_url('/mod/workshop/assessment.php', array('asid' => $assessmentid));
1407 * @param int $assessmentid The ID of assessment record
1408 * @return moodle_url of the example assessment page
1410 public function exassess_url($assessmentid) {
1412 $assessmentid = clean_param($assessmentid, PARAM_INT);
1413 return new moodle_url('/mod/workshop/exassessment.php', array('asid' => $assessmentid));
1417 * @return moodle_url of the page to view a submission, defaults to the own one
1419 public function submission_url($id=null) {
1421 return new moodle_url('/mod/workshop/submission.php', array('cmid' => $this->cm->id, 'id' => $id));
1425 * @param int $id example submission id
1426 * @return moodle_url of the page to view an example submission
1428 public function exsubmission_url($id) {
1430 return new moodle_url('/mod/workshop/exsubmission.php', array('cmid' => $this->cm->id, 'id' => $id));
1434 * @param int $sid submission id
1435 * @param array $aid of int assessment ids
1436 * @return moodle_url of the page to compare assessments of the given submission
1438 public function compare_url($sid, array $aids) {
1441 $url = new moodle_url('/mod/workshop/compare.php', array('cmid' => $this->cm->id, 'sid' => $sid));
1443 foreach ($aids as $aid) {
1444 $url->param("aid{$i}", $aid);
1451 * @param int $sid submission id
1452 * @param int $aid assessment id
1453 * @return moodle_url of the page to compare the reference assessments of the given example submission
1455 public function excompare_url($sid, $aid) {
1457 return new moodle_url('/mod/workshop/excompare.php', array('cmid' => $this->cm->id, 'sid' => $sid, 'aid' => $aid));
1461 * @return moodle_url of the mod_edit form
1463 public function updatemod_url() {
1465 return new moodle_url('/course/modedit.php', array('update' => $this->cm->id, 'return' => 1));
1469 * @param string $method allocation method
1470 * @return moodle_url to the allocation page
1472 public function allocation_url($method=null) {
1474 $params = array('cmid' => $this->cm->id);
1475 if (!empty($method)) {
1476 $params['method'] = $method;
1478 return new moodle_url('/mod/workshop/allocation.php', $params);
1482 * @param int $phasecode The internal phase code
1483 * @return moodle_url of the script to change the current phase to $phasecode
1485 public function switchphase_url($phasecode) {
1487 $phasecode = clean_param($phasecode, PARAM_INT);
1488 return new moodle_url('/mod/workshop/switchphase.php', array('cmid' => $this->cm->id, 'phase' => $phasecode));
1492 * @return moodle_url to the aggregation page
1494 public function aggregate_url() {
1496 return new moodle_url('/mod/workshop/aggregate.php', array('cmid' => $this->cm->id));
1500 * @return moodle_url of this workshop's toolbox page
1502 public function toolbox_url($tool) {
1504 return new moodle_url('/mod/workshop/toolbox.php', array('id' => $this->cm->id, 'tool' => $tool));
1508 * Workshop wrapper around {@see add_to_log()}
1510 * @param string $action to be logged
1511 * @param moodle_url $url absolute url as returned by {@see workshop::submission_url()} and friends
1512 * @param mixed $info additional info, usually id in a table
1514 public function log($action, moodle_url $url = null, $info = null) {
1516 if (is_null($url)) {
1517 $url = $this->view_url();
1520 if (is_null($info)) {
1524 $logurl = $this->log_convert_url($url);
1525 add_to_log($this->course->id, 'workshop', $action, $logurl, $info, $this->cm->id);
1529 * Is the given user allowed to create their submission?
1531 * @param int $userid
1534 public function creating_submission_allowed($userid) {
1537 $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid);
1539 if ($this->latesubmissions) {
1540 if ($this->phase != self::PHASE_SUBMISSION and $this->phase != self::PHASE_ASSESSMENT) {
1541 // late submissions are allowed in the submission and assessment phase only
1544 if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) {
1545 // late submissions are not allowed before the submission start
1551 if ($this->phase != self::PHASE_SUBMISSION) {
1552 // submissions are allowed during the submission phase only
1555 if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) {
1556 // if enabled, submitting is not allowed before the date/time defined in the mod_form
1559 if (!$ignoredeadlines and !empty($this->submissionend) and $now > $this->submissionend ) {
1560 // if enabled, submitting is not allowed after the date/time defined in the mod_form unless late submission is allowed
1568 * Is the given user allowed to modify their existing submission?
1570 * @param int $userid
1573 public function modifying_submission_allowed($userid) {
1576 $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid);
1578 if ($this->phase != self::PHASE_SUBMISSION) {
1579 // submissions can be edited during the submission phase only
1582 if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) {
1583 // if enabled, re-submitting is not allowed before the date/time defined in the mod_form
1586 if (!$ignoredeadlines and !empty($this->submissionend) and $now > $this->submissionend) {
1587 // if enabled, re-submitting is not allowed after the date/time defined in the mod_form even if late submission is allowed
1594 * Is the given reviewer allowed to create/edit their assessments?
1596 * @param int $userid
1599 public function assessing_allowed($userid) {
1601 if ($this->phase != self::PHASE_ASSESSMENT) {
1602 // assessing is allowed in the assessment phase only, unless the user is a teacher
1603 // providing additional assessment during the evaluation phase
1604 if ($this->phase != self::PHASE_EVALUATION or !has_capability('mod/workshop:overridegrades', $this->context, $userid)) {
1610 $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid);
1612 if (!$ignoredeadlines and !empty($this->assessmentstart) and $this->assessmentstart > $now) {
1613 // if enabled, assessing is not allowed before the date/time defined in the mod_form
1616 if (!$ignoredeadlines and !empty($this->assessmentend) and $now > $this->assessmentend) {
1617 // if enabled, assessing is not allowed after the date/time defined in the mod_form
1620 // here we go, assessing is allowed
1625 * Are reviewers allowed to create/edit their assessments of the example submissions?
1627 * Returns null if example submissions are not enabled in this workshop. Otherwise returns
1628 * true or false. Note this does not check other conditions like the number of already
1629 * assessed examples, examples mode etc.
1633 public function assessing_examples_allowed() {
1634 if (empty($this->useexamples)) {
1637 if (self::EXAMPLES_VOLUNTARY == $this->examplesmode) {
1640 if (self::EXAMPLES_BEFORE_SUBMISSION == $this->examplesmode and self::PHASE_SUBMISSION == $this->phase) {
1643 if (self::EXAMPLES_BEFORE_ASSESSMENT == $this->examplesmode and self::PHASE_ASSESSMENT == $this->phase) {
1650 * Are the peer-reviews available to the authors?
1654 public function assessments_available() {
1655 return $this->phase == self::PHASE_CLOSED;
1659 * Switch to a new workshop phase
1661 * Modifies the underlying database record. You should terminate the script shortly after calling this.
1663 * @param int $newphase new phase code
1664 * @return bool true if success, false otherwise
1666 public function switch_phase($newphase) {
1669 $known = $this->available_phases_list();
1670 if (!isset($known[$newphase])) {
1674 if (self::PHASE_CLOSED == $newphase) {
1675 // push the grades into the gradebook
1676 $workshop = new stdclass();
1677 foreach ($this as $property => $value) {
1678 $workshop->{$property} = $value;
1680 $workshop->course = $this->course->id;
1681 $workshop->cmidnumber = $this->cm->id;
1682 $workshop->modname = 'workshop';
1683 workshop_update_grades($workshop);
1686 $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id));
1687 $this->phase = $newphase;
1692 * Saves a raw grade for submission as calculated from the assessment form fields
1694 * @param array $assessmentid assessment record id, must exists
1695 * @param mixed $grade raw percentual grade from 0.00000 to 100.00000
1696 * @return false|float the saved grade
1698 public function set_peer_grade($assessmentid, $grade) {
1701 if (is_null($grade)) {
1704 $data = new stdclass();
1705 $data->id = $assessmentid;
1706 $data->grade = $grade;
1707 $data->timemodified = time();
1708 $DB->update_record('workshop_assessments', $data);
1713 * Prepares data object with all workshop grades to be rendered
1715 * @param int $userid the user we are preparing the report for
1716 * @param int $groupid if non-zero, prepare the report for the given group only
1717 * @param int $page the current page (for the pagination)
1718 * @param int $perpage participants per page (for the pagination)
1719 * @param string $sortby lastname|firstname|submissiontitle|submissiongrade|gradinggrade
1720 * @param string $sorthow ASC|DESC
1721 * @return stdclass data for the renderer
1723 public function prepare_grading_report_data($userid, $groupid, $page, $perpage, $sortby, $sorthow) {
1726 $canviewall = has_capability('mod/workshop:viewallassessments', $this->context, $userid);
1727 $isparticipant = $this->is_participant($userid);
1729 if (!$canviewall and !$isparticipant) {
1730 // who the hell is this?
1734 if (!in_array($sortby, array('lastname','firstname','submissiontitle','submissiongrade','gradinggrade'))) {
1735 $sortby = 'lastname';
1738 if (!($sorthow === 'ASC' or $sorthow === 'DESC')) {
1742 // get the list of user ids to be displayed
1744 $participants = $this->get_participants(false, $groupid);
1746 // this is an ordinary workshop participant (aka student) - display the report just for him/her
1747 $participants = array($userid => (object)array('id' => $userid));
1750 // we will need to know the number of all records later for the pagination purposes
1751 $numofparticipants = count($participants);
1753 if ($numofparticipants > 0) {
1754 // load all fields which can be used for sorting and paginate the records
1755 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1756 $params['workshopid1'] = $this->id;
1757 $params['workshopid2'] = $this->id;
1759 $sqlsortfields = array($sortby => $sorthow) + array('lastname' => 'ASC', 'firstname' => 'ASC', 'u.id' => 'ASC');
1760 foreach ($sqlsortfields as $sqlsortfieldname => $sqlsortfieldhow) {
1761 $sqlsort[] = $sqlsortfieldname . ' ' . $sqlsortfieldhow;
1763 $sqlsort = implode(',', $sqlsort);
1764 $picturefields = user_picture::fields('u', array(), 'userid');
1765 $sql = "SELECT $picturefields, s.title AS submissiontitle, s.grade AS submissiongrade, ag.gradinggrade
1767 LEFT JOIN {workshop_submissions} s ON (s.authorid = u.id AND s.workshopid = :workshopid1 AND s.example = 0)
1768 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = u.id AND ag.workshopid = :workshopid2)
1769 WHERE u.id $participantids
1771 $participants = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
1773 $participants = array();
1776 // this will hold the information needed to display user names and pictures
1777 $userinfo = array();
1779 // get the user details for all participants to display
1780 $additionalnames = get_all_user_name_fields();
1781 foreach ($participants as $participant) {
1782 if (!isset($userinfo[$participant->userid])) {
1783 $userinfo[$participant->userid] = new stdclass();
1784 $userinfo[$participant->userid]->id = $participant->userid;
1785 $userinfo[$participant->userid]->picture = $participant->picture;
1786 $userinfo[$participant->userid]->imagealt = $participant->imagealt;
1787 $userinfo[$participant->userid]->email = $participant->email;
1788 foreach ($additionalnames as $addname) {
1789 $userinfo[$participant->userid]->$addname = $participant->$addname;
1794 // load the submissions details
1795 $submissions = $this->get_submissions(array_keys($participants));
1797 // get the user details for all moderators (teachers) that have overridden a submission grade
1798 foreach ($submissions as $submission) {
1799 if (!isset($userinfo[$submission->gradeoverby])) {
1800 $userinfo[$submission->gradeoverby] = new stdclass();
1801 $userinfo[$submission->gradeoverby]->id = $submission->gradeoverby;
1802 $userinfo[$submission->gradeoverby]->picture = $submission->overpicture;
1803 $userinfo[$submission->gradeoverby]->imagealt = $submission->overimagealt;
1804 $userinfo[$submission->gradeoverby]->email = $submission->overemail;
1805 foreach ($additionalnames as $addname) {
1806 $temp = 'over' . $addname;
1807 $userinfo[$submission->gradeoverby]->$addname = $submission->$temp;
1812 // get the user details for all reviewers of the displayed participants
1813 $reviewers = array();
1816 list($submissionids, $params) = $DB->get_in_or_equal(array_keys($submissions), SQL_PARAMS_NAMED);
1817 list($sort, $sortparams) = users_order_by_sql('r');
1818 $picturefields = user_picture::fields('r', array(), 'reviewerid');
1819 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover, a.weight,
1820 $picturefields, s.id AS submissionid, s.authorid
1821 FROM {workshop_assessments} a
1822 JOIN {user} r ON (a.reviewerid = r.id)
1823 JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0)
1824 WHERE a.submissionid $submissionids
1825 ORDER BY a.weight DESC, $sort";
1826 $reviewers = $DB->get_records_sql($sql, array_merge($params, $sortparams));
1827 foreach ($reviewers as $reviewer) {
1828 if (!isset($userinfo[$reviewer->reviewerid])) {
1829 $userinfo[$reviewer->reviewerid] = new stdclass();
1830 $userinfo[$reviewer->reviewerid]->id = $reviewer->reviewerid;
1831 $userinfo[$reviewer->reviewerid]->picture = $reviewer->picture;
1832 $userinfo[$reviewer->reviewerid]->imagealt = $reviewer->imagealt;
1833 $userinfo[$reviewer->reviewerid]->email = $reviewer->email;
1834 foreach ($additionalnames as $addname) {
1835 $userinfo[$reviewer->reviewerid]->$addname = $reviewer->$addname;
1841 // get the user details for all reviewees of the displayed participants
1842 $reviewees = array();
1843 if ($participants) {
1844 list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1845 list($sort, $sortparams) = users_order_by_sql('e');
1846 $params['workshopid'] = $this->id;
1847 $picturefields = user_picture::fields('e', array(), 'authorid');
1848 $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover, a.reviewerid, a.weight,
1849 s.id AS submissionid, $picturefields
1851 JOIN {workshop_assessments} a ON (a.reviewerid = u.id)
1852 JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0)
1853 JOIN {user} e ON (s.authorid = e.id)
1854 WHERE u.id $participantids AND s.workshopid = :workshopid
1855 ORDER BY a.weight DESC, $sort";
1856 $reviewees = $DB->get_records_sql($sql, array_merge($params, $sortparams));
1857 foreach ($reviewees as $reviewee) {
1858 if (!isset($userinfo[$reviewee->authorid])) {
1859 $userinfo[$reviewee->authorid] = new stdclass();
1860 $userinfo[$reviewee->authorid]->id = $reviewee->authorid;
1861 $userinfo[$reviewee->authorid]->picture = $reviewee->picture;
1862 $userinfo[$reviewee->authorid]->imagealt = $reviewee->imagealt;
1863 $userinfo[$reviewee->authorid]->email = $reviewee->email;
1864 foreach ($additionalnames as $addname) {
1865 $userinfo[$reviewee->authorid]->$addname = $reviewee->$addname;
1871 // finally populate the object to be rendered
1872 $grades = $participants;
1874 foreach ($participants as $participant) {
1875 // set up default (null) values
1876 $grades[$participant->userid]->submissionid = null;
1877 $grades[$participant->userid]->submissiontitle = null;
1878 $grades[$participant->userid]->submissiongrade = null;
1879 $grades[$participant->userid]->submissiongradeover = null;
1880 $grades[$participant->userid]->submissiongradeoverby = null;
1881 $grades[$participant->userid]->submissionpublished = null;
1882 $grades[$participant->userid]->reviewedby = array();
1883 $grades[$participant->userid]->reviewerof = array();
1885 unset($participants);
1886 unset($participant);
1888 foreach ($submissions as $submission) {
1889 $grades[$submission->authorid]->submissionid = $submission->id;
1890 $grades[$submission->authorid]->submissiontitle = $submission->title;
1891 $grades[$submission->authorid]->submissiongrade = $this->real_grade($submission->grade);
1892 $grades[$submission->authorid]->submissiongradeover = $this->real_grade($submission->gradeover);
1893 $grades[$submission->authorid]->submissiongradeoverby = $submission->gradeoverby;
1894 $grades[$submission->authorid]->submissionpublished = $submission->published;
1896 unset($submissions);
1899 foreach($reviewers as $reviewer) {
1900 $info = new stdclass();
1901 $info->userid = $reviewer->reviewerid;
1902 $info->assessmentid = $reviewer->assessmentid;
1903 $info->submissionid = $reviewer->submissionid;
1904 $info->grade = $this->real_grade($reviewer->grade);
1905 $info->gradinggrade = $this->real_grading_grade($reviewer->gradinggrade);
1906 $info->gradinggradeover = $this->real_grading_grade($reviewer->gradinggradeover);
1907 $info->weight = $reviewer->weight;
1908 $grades[$reviewer->authorid]->reviewedby[$reviewer->reviewerid] = $info;
1913 foreach($reviewees as $reviewee) {
1914 $info = new stdclass();
1915 $info->userid = $reviewee->authorid;
1916 $info->assessmentid = $reviewee->assessmentid;
1917 $info->submissionid = $reviewee->submissionid;
1918 $info->grade = $this->real_grade($reviewee->grade);
1919 $info->gradinggrade = $this->real_grading_grade($reviewee->gradinggrade);
1920 $info->gradinggradeover = $this->real_grading_grade($reviewee->gradinggradeover);
1921 $info->weight = $reviewee->weight;
1922 $grades[$reviewee->reviewerid]->reviewerof[$reviewee->authorid] = $info;
1927 foreach ($grades as $grade) {
1928 $grade->gradinggrade = $this->real_grading_grade($grade->gradinggrade);
1931 $data = new stdclass();
1932 $data->grades = $grades;
1933 $data->userinfo = $userinfo;
1934 $data->totalcount = $numofparticipants;
1935 $data->maxgrade = $this->real_grade(100);
1936 $data->maxgradinggrade = $this->real_grading_grade(100);
1941 * Calculates the real value of a grade
1943 * @param float $value percentual value from 0 to 100
1944 * @param float $max the maximal grade
1947 public function real_grade_value($value, $max) {
1949 if (is_null($value) or $value === '') {
1951 } elseif ($max == 0) {
1954 return format_float($max * $value / 100, $this->gradedecimals, $localized);
1959 * Calculates the raw (percentual) value from a real grade
1961 * This is used in cases when a user wants to give a grade such as 12 of 20 and we need to save
1962 * this value in a raw percentual form into DB
1963 * @param float $value given grade
1964 * @param float $max the maximal grade
1965 * @return float suitable to be stored as numeric(10,5)
1967 public function raw_grade_value($value, $max) {
1968 if (is_null($value) or $value === '') {
1971 if ($max == 0 or $value < 0) {
1974 $p = $value / $max * 100;
1978 return grade_floatval($p);
1982 * Calculates the real value of grade for submission
1984 * @param float $value percentual value from 0 to 100
1987 public function real_grade($value) {
1988 return $this->real_grade_value($value, $this->grade);
1992 * Calculates the real value of grade for assessment
1994 * @param float $value percentual value from 0 to 100
1997 public function real_grading_grade($value) {
1998 return $this->real_grade_value($value, $this->gradinggrade);
2002 * Sets the given grades and received grading grades to null
2004 * This does not clear the information about how the peers filled the assessment forms, but
2005 * clears the calculated grades in workshop_assessments. Therefore reviewers have to re-assess
2006 * the allocated submissions.
2010 public function clear_assessments() {
2013 $submissions = $this->get_submissions();
2014 if (empty($submissions)) {
2015 // no money, no love
2018 $submissions = array_keys($submissions);
2019 list($sql, $params) = $DB->get_in_or_equal($submissions, SQL_PARAMS_NAMED);
2020 $sql = "submissionid $sql";
2021 $DB->set_field_select('workshop_assessments', 'grade', null, $sql, $params);
2022 $DB->set_field_select('workshop_assessments', 'gradinggrade', null, $sql, $params);
2026 * Sets the grades for submission to null
2028 * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s)
2031 public function clear_submission_grades($restrict=null) {
2034 $sql = "workshopid = :workshopid AND example = 0";
2035 $params = array('workshopid' => $this->id);
2037 if (is_null($restrict)) {
2038 // update all users - no more conditions
2039 } elseif (!empty($restrict)) {
2040 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
2041 $sql .= " AND authorid $usql";
2042 $params = array_merge($params, $uparams);
2044 throw new coding_exception('Empty value is not a valid parameter here');
2047 $DB->set_field_select('workshop_submissions', 'grade', null, $sql, $params);
2051 * Calculates grades for submission for the given participant(s) and updates it in the database
2053 * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s)
2056 public function aggregate_submission_grades($restrict=null) {
2059 // fetch a recordset with all assessments to process
2060 $sql = 'SELECT s.id AS submissionid, s.grade AS submissiongrade,
2062 FROM {workshop_submissions} s
2063 LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id)
2064 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
2065 $params = array('workshopid' => $this->id);
2067 if (is_null($restrict)) {
2068 // update all users - no more conditions
2069 } elseif (!empty($restrict)) {
2070 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
2071 $sql .= " AND s.authorid $usql";
2072 $params = array_merge($params, $uparams);
2074 throw new coding_exception('Empty value is not a valid parameter here');
2077 $sql .= ' ORDER BY s.id'; // this is important for bulk processing
2079 $rs = $DB->get_recordset_sql($sql, $params);
2080 $batch = array(); // will contain a set of all assessments of a single submission
2081 $previous = null; // a previous record in the recordset
2083 foreach ($rs as $current) {
2084 if (is_null($previous)) {
2085 // we are processing the very first record in the recordset
2086 $previous = $current;
2088 if ($current->submissionid == $previous->submissionid) {
2089 // we are still processing the current submission
2090 $batch[] = $current;
2092 // process all the assessments of a sigle submission
2093 $this->aggregate_submission_grades_process($batch);
2094 // and then start to process another submission
2095 $batch = array($current);
2096 $previous = $current;
2099 // do not forget to process the last batch!
2100 $this->aggregate_submission_grades_process($batch);
2105 * Sets the aggregated grades for assessment to null
2107 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
2110 public function clear_grading_grades($restrict=null) {
2113 $sql = "workshopid = :workshopid";
2114 $params = array('workshopid' => $this->id);
2116 if (is_null($restrict)) {
2117 // update all users - no more conditions
2118 } elseif (!empty($restrict)) {
2119 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
2120 $sql .= " AND userid $usql";
2121 $params = array_merge($params, $uparams);
2123 throw new coding_exception('Empty value is not a valid parameter here');
2126 $DB->set_field_select('workshop_aggregations', 'gradinggrade', null, $sql, $params);
2130 * Calculates grades for assessment for the given participant(s)
2132 * Grade for assessment is calculated as a simple mean of all grading grades calculated by the grading evaluator.
2133 * The assessment weight is not taken into account here.
2135 * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
2138 public function aggregate_grading_grades($restrict=null) {
2141 // fetch a recordset with all assessments to process
2142 $sql = 'SELECT a.reviewerid, a.gradinggrade, a.gradinggradeover,
2143 ag.id AS aggregationid, ag.gradinggrade AS aggregatedgrade
2144 FROM {workshop_assessments} a
2145 INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
2146 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = a.reviewerid AND ag.workshopid = s.workshopid)
2147 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
2148 $params = array('workshopid' => $this->id);
2150 if (is_null($restrict)) {
2151 // update all users - no more conditions
2152 } elseif (!empty($restrict)) {
2153 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
2154 $sql .= " AND a.reviewerid $usql";
2155 $params = array_merge($params, $uparams);
2157 throw new coding_exception('Empty value is not a valid parameter here');
2160 $sql .= ' ORDER BY a.reviewerid'; // this is important for bulk processing
2162 $rs = $DB->get_recordset_sql($sql, $params);
2163 $batch = array(); // will contain a set of all assessments of a single submission
2164 $previous = null; // a previous record in the recordset
2166 foreach ($rs as $current) {
2167 if (is_null($previous)) {
2168 // we are processing the very first record in the recordset
2169 $previous = $current;
2171 if ($current->reviewerid == $previous->reviewerid) {
2172 // we are still processing the current reviewer
2173 $batch[] = $current;
2175 // process all the assessments of a sigle submission
2176 $this->aggregate_grading_grades_process($batch);
2177 // and then start to process another reviewer
2178 $batch = array($current);
2179 $previous = $current;
2182 // do not forget to process the last batch!
2183 $this->aggregate_grading_grades_process($batch);
2188 * Returns the mform the teachers use to put a feedback for the reviewer
2190 * @param moodle_url $actionurl
2191 * @param stdClass $assessment
2192 * @param array $options editable, editableweight, overridablegradinggrade
2193 * @return workshop_feedbackreviewer_form
2195 public function get_feedbackreviewer_form(moodle_url $actionurl, stdclass $assessment, $options=array()) {
2197 require_once(dirname(__FILE__) . '/feedbackreviewer_form.php');
2199 $current = new stdclass();
2200 $current->asid = $assessment->id;
2201 $current->weight = $assessment->weight;
2202 $current->gradinggrade = $this->real_grading_grade($assessment->gradinggrade);
2203 $current->gradinggradeover = $this->real_grading_grade($assessment->gradinggradeover);
2204 $current->feedbackreviewer = $assessment->feedbackreviewer;
2205 $current->feedbackreviewerformat = $assessment->feedbackreviewerformat;
2206 if (is_null($current->gradinggrade)) {
2207 $current->gradinggrade = get_string('nullgrade', 'workshop');
2209 if (!isset($options['editable'])) {
2210 $editable = true; // by default
2212 $editable = (bool)$options['editable'];
2215 // prepare wysiwyg editor
2216 $current = file_prepare_standard_editor($current, 'feedbackreviewer', array());
2218 return new workshop_feedbackreviewer_form($actionurl,
2219 array('workshop' => $this, 'current' => $current, 'editoropts' => array(), 'options' => $options),
2220 'post', '', null, $editable);
2224 * Returns the mform the teachers use to put a feedback for the author on their submission
2226 * @param moodle_url $actionurl
2227 * @param stdClass $submission
2228 * @param array $options editable
2229 * @return workshop_feedbackauthor_form
2231 public function get_feedbackauthor_form(moodle_url $actionurl, stdclass $submission, $options=array()) {
2233 require_once(dirname(__FILE__) . '/feedbackauthor_form.php');
2235 $current = new stdclass();
2236 $current->submissionid = $submission->id;
2237 $current->published = $submission->published;
2238 $current->grade = $this->real_grade($submission->grade);
2239 $current->gradeover = $this->real_grade($submission->gradeover);
2240 $current->feedbackauthor = $submission->feedbackauthor;
2241 $current->feedbackauthorformat = $submission->feedbackauthorformat;
2242 if (is_null($current->grade)) {
2243 $current->grade = get_string('nullgrade', 'workshop');
2245 if (!isset($options['editable'])) {
2246 $editable = true; // by default
2248 $editable = (bool)$options['editable'];
2251 // prepare wysiwyg editor
2252 $current = file_prepare_standard_editor($current, 'feedbackauthor', array());
2254 return new workshop_feedbackauthor_form($actionurl,
2255 array('workshop' => $this, 'current' => $current, 'editoropts' => array(), 'options' => $options),
2256 'post', '', null, $editable);
2260 * Returns the information about the user's grades as they are stored in the gradebook
2262 * The submission grade is returned for users with the capability mod/workshop:submit and the
2263 * assessment grade is returned for users with the capability mod/workshop:peerassess. Unless the
2264 * user has the capability to view hidden grades, grades must be visible to be returned. Null
2265 * grades are not returned. If none grade is to be returned, this method returns false.
2267 * @param int $userid the user's id
2268 * @return workshop_final_grades|false
2270 public function get_gradebook_grades($userid) {
2272 require_once($CFG->libdir.'/gradelib.php');
2274 if (empty($userid)) {
2275 throw new coding_exception('User id expected, empty value given.');
2278 // Read data via the Gradebook API
2279 $gradebook = grade_get_grades($this->course->id, 'mod', 'workshop', $this->id, $userid);
2281 $grades = new workshop_final_grades();
2283 if (has_capability('mod/workshop:submit', $this->context, $userid)) {
2284 if (!empty($gradebook->items[0]->grades)) {
2285 $submissiongrade = reset($gradebook->items[0]->grades);
2286 if (!is_null($submissiongrade->grade)) {
2287 if (!$submissiongrade->hidden or has_capability('moodle/grade:viewhidden', $this->context, $userid)) {
2288 $grades->submissiongrade = $submissiongrade;
2294 if (has_capability('mod/workshop:peerassess', $this->context, $userid)) {
2295 if (!empty($gradebook->items[1]->grades)) {
2296 $assessmentgrade = reset($gradebook->items[1]->grades);
2297 if (!is_null($assessmentgrade->grade)) {
2298 if (!$assessmentgrade->hidden or has_capability('moodle/grade:viewhidden', $this->context, $userid)) {
2299 $grades->assessmentgrade = $assessmentgrade;
2305 if (!is_null($grades->submissiongrade) or !is_null($grades->assessmentgrade)) {
2313 * Return the editor options for the overall feedback for the author.
2317 public function overall_feedback_content_options() {
2320 'maxbytes' => $this->overallfeedbackmaxbytes,
2321 'maxfiles' => $this->overallfeedbackfiles,
2322 'changeformat' => 1,
2323 'context' => $this->context,
2328 * Return the filemanager options for the overall feedback for the author.
2332 public function overall_feedback_attachment_options() {
2335 'maxbytes' => $this->overallfeedbackmaxbytes,
2336 'maxfiles' => $this->overallfeedbackfiles,
2337 'return_types' => FILE_INTERNAL,
2341 ////////////////////////////////////////////////////////////////////////////////
2342 // Internal methods (implementation details) //
2343 ////////////////////////////////////////////////////////////////////////////////
2346 * Given an array of all assessments of a single submission, calculates the final grade for this submission
2348 * This calculates the weighted mean of the passed assessment grades. If, however, the submission grade
2349 * was overridden by a teacher, the gradeover value is returned and the rest of grades are ignored.
2351 * @param array $assessments of stdclass(->submissionid ->submissiongrade ->gradeover ->weight ->grade)
2354 protected function aggregate_submission_grades_process(array $assessments) {
2357 $submissionid = null; // the id of the submission being processed
2358 $current = null; // the grade currently saved in database
2359 $finalgrade = null; // the new grade to be calculated
2363 foreach ($assessments as $assessment) {
2364 if (is_null($submissionid)) {
2365 // the id is the same in all records, fetch it during the first loop cycle
2366 $submissionid = $assessment->submissionid;
2368 if (is_null($current)) {
2369 // the currently saved grade is the same in all records, fetch it during the first loop cycle
2370 $current = $assessment->submissiongrade;
2372 if (is_null($assessment->grade)) {
2373 // this was not assessed yet
2376 if ($assessment->weight == 0) {
2377 // this does not influence the calculation
2380 $sumgrades += $assessment->grade * $assessment->weight;
2381 $sumweights += $assessment->weight;
2383 if ($sumweights > 0 and is_null($finalgrade)) {
2384 $finalgrade = grade_floatval($sumgrades / $sumweights);
2386 // check if the new final grade differs from the one stored in the database
2387 if (grade_floats_different($finalgrade, $current)) {
2388 // we need to save new calculation into the database
2389 $record = new stdclass();
2390 $record->id = $submissionid;
2391 $record->grade = $finalgrade;
2392 $record->timegraded = time();
2393 $DB->update_record('workshop_submissions', $record);
2398 * Given an array of all assessments done by a single reviewer, calculates the final grading grade
2400 * This calculates the simple mean of the passed grading grades. If, however, the grading grade
2401 * was overridden by a teacher, the gradinggradeover value is returned and the rest of grades are ignored.
2403 * @param array $assessments of stdclass(->reviewerid ->gradinggrade ->gradinggradeover ->aggregationid ->aggregatedgrade)
2404 * @param null|int $timegraded explicit timestamp of the aggregation, defaults to the current time
2407 protected function aggregate_grading_grades_process(array $assessments, $timegraded = null) {
2410 $reviewerid = null; // the id of the reviewer being processed
2411 $current = null; // the gradinggrade currently saved in database
2412 $finalgrade = null; // the new grade to be calculated
2413 $agid = null; // aggregation id
2417 if (is_null($timegraded)) {
2418 $timegraded = time();
2421 foreach ($assessments as $assessment) {
2422 if (is_null($reviewerid)) {
2423 // the id is the same in all records, fetch it during the first loop cycle
2424 $reviewerid = $assessment->reviewerid;
2426 if (is_null($agid)) {
2427 // the id is the same in all records, fetch it during the first loop cycle
2428 $agid = $assessment->aggregationid;
2430 if (is_null($current)) {
2431 // the currently saved grade is the same in all records, fetch it during the first loop cycle
2432 $current = $assessment->aggregatedgrade;
2434 if (!is_null($assessment->gradinggradeover)) {
2435 // the grading grade for this assessment is overridden by a teacher
2436 $sumgrades += $assessment->gradinggradeover;
2439 if (!is_null($assessment->gradinggrade)) {
2440 $sumgrades += $assessment->gradinggrade;
2446 $finalgrade = grade_floatval($sumgrades / $count);
2448 // check if the new final grade differs from the one stored in the database
2449 if (grade_floats_different($finalgrade, $current)) {
2450 // we need to save new calculation into the database
2451 if (is_null($agid)) {
2452 // no aggregation record yet
2453 $record = new stdclass();
2454 $record->workshopid = $this->id;
2455 $record->userid = $reviewerid;
2456 $record->gradinggrade = $finalgrade;
2457 $record->timegraded = $timegraded;
2458 $DB->insert_record('workshop_aggregations', $record);
2460 $record = new stdclass();
2461 $record->id = $agid;
2462 $record->gradinggrade = $finalgrade;
2463 $record->timegraded = $timegraded;
2464 $DB->update_record('workshop_aggregations', $record);
2470 * Returns SQL to fetch all enrolled users with the given capability in the current workshop
2472 * The returned array consists of string $sql and the $params array. Note that the $sql can be
2473 * empty if groupmembersonly is enabled and the associated grouping is empty.
2475 * @param string $capability the name of the capability
2476 * @param bool $musthavesubmission ff true, return only users who have already submitted
2477 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
2478 * @return array of (string)sql, (array)params
2480 protected function get_users_with_capability_sql($capability, $musthavesubmission, $groupid) {
2482 /** @var int static counter used to generate unique parameter holders */
2486 // if the caller requests all groups and we are in groupmembersonly mode, use the
2487 // recursive call of itself to get users from all groups in the grouping
2488 if (empty($groupid) and !empty($CFG->enablegroupmembersonly) and $this->cm->groupmembersonly) {
2489 $groupingid = $this->cm->groupingid;
2490 $groupinggroupids = array_keys(groups_get_all_groups($this->cm->course, 0, $this->cm->groupingid, 'g.id'));
2493 foreach ($groupinggroupids as $groupinggroupid) {
2494 if ($groupinggroupid > 0) { // just in case in order not to fall into the endless loop
2495 list($gsql, $gparams) = $this->get_users_with_capability_sql($capability, $musthavesubmission, $groupinggroupid);
2497 $params = array_merge($params, $gparams);
2500 $sql = implode(PHP_EOL." UNION ".PHP_EOL, $sql);
2501 return array($sql, $params);
2504 list($esql, $params) = get_enrolled_sql($this->context, $capability, $groupid, true);
2506 $userfields = user_picture::fields('u');
2508 $sql = "SELECT $userfields
2510 JOIN ($esql) je ON (je.id = u.id AND u.deleted = 0) ";
2512 if ($musthavesubmission) {
2513 $sql .= " JOIN {workshop_submissions} ws ON (ws.authorid = u.id AND ws.example = 0 AND ws.workshopid = :workshopid{$inc}) ";
2514 $params['workshopid'.$inc] = $this->id;
2517 return array($sql, $params);
2521 * Returns SQL statement that can be used to fetch all actively enrolled participants in the workshop
2523 * @param bool $musthavesubmission if true, return only users who have already submitted
2524 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
2525 * @return array of (string)sql, (array)params
2527 protected function get_participants_sql($musthavesubmission=false, $groupid=0) {
2529 list($sql1, $params1) = $this->get_users_with_capability_sql('mod/workshop:submit', $musthavesubmission, $groupid);
2530 list($sql2, $params2) = $this->get_users_with_capability_sql('mod/workshop:peerassess', $musthavesubmission, $groupid);
2532 if (empty($sql1) or empty($sql2)) {
2533 if (empty($sql1) and empty($sql2)) {
2534 return array('', array());
2535 } else if (empty($sql1)) {
2543 $sql = $sql1.PHP_EOL." UNION ".PHP_EOL.$sql2;
2544 $params = array_merge($params1, $params2);
2547 return array($sql, $params);
2551 * @return array of available workshop phases
2553 protected function available_phases_list() {
2555 self::PHASE_SETUP => true,
2556 self::PHASE_SUBMISSION => true,
2557 self::PHASE_ASSESSMENT => true,
2558 self::PHASE_EVALUATION => true,
2559 self::PHASE_CLOSED => true,
2564 * Converts absolute URL to relative URL needed by {@see add_to_log()}
2566 * @param moodle_url $url absolute URL
2569 protected function log_convert_url(moodle_url $fullurl) {
2572 if (!isset($baseurl)) {
2573 $baseurl = new moodle_url('/mod/workshop/');
2574 $baseurl = $baseurl->out();
2577 return substr($fullurl->out(), strlen($baseurl));
2581 ////////////////////////////////////////////////////////////////////////////////
2582 // Renderable components
2583 ////////////////////////////////////////////////////////////////////////////////
2586 * Represents the user planner tool
2588 * Planner contains list of phases. Each phase contains list of tasks. Task is a simple object with
2589 * title, link and completed (true/false/null logic).
2591 class workshop_user_plan implements renderable {
2593 /** @var int id of the user this plan is for */
2595 /** @var workshop */
2597 /** @var array of (stdclass)tasks */
2598 public $phases = array();
2599 /** @var null|array of example submissions to be assessed by the planner owner */
2600 protected $examples = null;
2603 * Prepare an individual workshop plan for the given user.
2605 * @param workshop $workshop instance
2606 * @param int $userid whom the plan is prepared for
2608 public function __construct(workshop $workshop, $userid) {
2611 $this->workshop = $workshop;
2612 $this->userid = $userid;
2614 //---------------------------------------------------------
2615 // * SETUP | submission | assessment | evaluation | closed
2616 //---------------------------------------------------------
2617 $phase = new stdclass();
2618 $phase->title = get_string('phasesetup', 'workshop');
2619 $phase->tasks = array();
2620 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
2621 $task = new stdclass();
2622 $task->title = get_string('taskintro', 'workshop');
2623 $task->link = $workshop->updatemod_url();
2624 $task->completed = !(trim($workshop->intro) == '');
2625 $phase->tasks['intro'] = $task;
2627 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
2628 $task = new stdclass();
2629 $task->title = get_string('taskinstructauthors', 'workshop');
2630 $task->link = $workshop->updatemod_url();
2631 $task->completed = !(trim($workshop->instructauthors) == '');
2632 $phase->tasks['instructauthors'] = $task;
2634 if (has_capability('mod/workshop:editdimensions', $workshop->context, $userid)) {
2635 $task = new stdclass();
2636 $task->title = get_string('editassessmentform', 'workshop');
2637 $task->link = $workshop->editform_url();
2638 if ($workshop->grading_strategy_instance()->form_ready()) {
2639 $task->completed = true;
2640 } elseif ($workshop->phase > workshop::PHASE_SETUP) {
2641 $task->completed = false;
2643 $phase->tasks['editform'] = $task;
2645 if ($workshop->useexamples and has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) {
2646 $task = new stdclass();
2647 $task->title = get_string('prepareexamples', 'workshop');
2648 if ($DB->count_records('workshop_submissions', array('example' => 1, 'workshopid' => $workshop->id)) > 0) {
2649 $task->completed = true;
2650 } elseif ($workshop->phase > workshop::PHASE_SETUP) {
2651 $task->completed = false;
2653 $phase->tasks['prepareexamples'] = $task;
2655 if (empty($phase->tasks) and $workshop->phase == workshop::PHASE_SETUP) {
2656 // if we are in the setup phase and there is no task (typical for students), let us
2657 // display some explanation what is going on
2658 $task = new stdclass();
2659 $task->title = get_string('undersetup', 'workshop');
2660 $task->completed = 'info';
2661 $phase->tasks['setupinfo'] = $task;
2663 $this->phases[workshop::PHASE_SETUP] = $phase;
2665 //---------------------------------------------------------
2666 // setup | * SUBMISSION | assessment | evaluation | closed
2667 //---------------------------------------------------------
2668 $phase = new stdclass();
2669 $phase->title = get_string('phasesubmission', 'workshop');
2670 $phase->tasks = array();
2671 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
2672 $task = new stdclass();
2673 $task->title = get_string('taskinstructreviewers', 'workshop');
2674 $task->link = $workshop->updatemod_url();
2675 if (trim($workshop->instructreviewers)) {
2676 $task->completed = true;
2677 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) {
2678 $task->completed = false;
2680 $phase->tasks['instructreviewers'] = $task;
2682 if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_SUBMISSION
2683 and has_capability('mod/workshop:submit', $workshop->context, $userid, false)
2684 and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) {
2685 $task = new stdclass();
2686 $task->title = get_string('exampleassesstask', 'workshop');
2687 $examples = $this->get_examples();
2688 $a = new stdclass();
2689 $a->expected = count($examples);
2691 foreach ($examples as $exampleid => $example) {
2692 if (!is_null($example->grade)) {
2696 $task->details = get_string('exampleassesstaskdetails', 'workshop', $a);
2697 if ($a->assessed == $a->expected) {
2698 $task->completed = true;
2699 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) {
2700 $task->completed = false;
2702 $phase->tasks['examples'] = $task;
2704 if (has_capability('mod/workshop:submit', $workshop->context, $userid, false)) {
2705 $task = new stdclass();
2706 $task->title = get_string('tasksubmit', 'workshop');
2707 $task->link = $workshop->submission_url();
2708 if ($DB->record_exists('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0, 'authorid'=>$userid))) {
2709 $task->completed = true;
2710 } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) {
2711 $task->completed = false;
2713 $task->completed = null; // still has a chance to submit
2715 $phase->tasks['submit'] = $task;
2717 if (has_capability('mod/workshop:allocate', $workshop->context, $userid)) {
2718 if ($workshop->phaseswitchassessment) {
2719 $task = new stdClass();
2720 $allocator = $DB->get_record('workshopallocation_scheduled', array('workshopid' => $workshop->id));
2721 if (empty($allocator)) {
2722 $task->completed = false;
2723 } else if ($allocator->enabled and is_null($allocator->resultstatus)) {
2724 $task->completed = true;
2725 } else if ($workshop->submissionend > time()) {
2726 $task->completed = null;
2728 $task->completed = false;
2730 $task->title = get_string('setup', 'workshopallocation_scheduled');
2731 $task->link = $workshop->allocation_url('scheduled');
2732 $phase->tasks['allocatescheduled'] = $task;
2734 $task = new stdclass();
2735 $task->title = get_string('allocate', 'workshop');
2736 $task->link = $workshop->allocation_url();
2737 $numofauthors = $workshop->count_potential_authors(false);
2738 $numofsubmissions = $DB->count_records('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0));
2739 $sql = 'SELECT COUNT(s.id) AS nonallocated
2740 FROM {workshop_submissions} s
2741 LEFT JOIN {workshop_assessments} a ON (a.submissionid=s.id)
2742 WHERE s.workshopid = :workshopid AND s.example=0 AND a.submissionid IS NULL';
2743 $params['workshopid'] = $workshop->id;
2744 $numnonallocated = $DB->count_records_sql($sql, $params);
2745 if ($numofsubmissions == 0) {
2746 $task->completed = null;
2747 } elseif ($numnonallocated == 0) {
2748 $task->completed = true;
2749 } elseif ($workshop->phase > workshop::PHASE_SUBMISSION) {
2750 $task->completed = false;
2752 $task->completed = null; // still has a chance to allocate
2754 $a = new stdclass();
2755 $a->expected = $numofauthors;
2756 $a->submitted = $numofsubmissions;
2757 $a->allocate = $numnonallocated;
2758 $task->details = get_string('allocatedetails', 'workshop', $a);
2760 $phase->tasks['allocate'] = $task;
2762 if ($numofsubmissions < $numofauthors and $workshop->phase >= workshop::PHASE_SUBMISSION) {
2763 $task = new stdclass();
2764 $task->title = get_string('someuserswosubmission', 'workshop');
2765 $task->completed = 'info';
2766 $phase->tasks['allocateinfo'] = $task;
2770 if ($workshop->submissionstart) {
2771 $task = new stdclass();
2772 $task->title = get_string('submissionstartdatetime', 'workshop', workshop::timestamp_formats($workshop->submissionstart));
2773 $task->completed = 'info';
2774 $phase->tasks['submissionstartdatetime'] = $task;
2776 if ($workshop->submissionend) {
2777 $task = new stdclass();
2778 $task->title = get_string('submissionenddatetime', 'workshop', workshop::timestamp_formats($workshop->submissionend));
2779 $task->completed = 'info';
2780 $phase->tasks['submissionenddatetime'] = $task;
2782 if (($workshop->submissionstart < time()) and $workshop->latesubmissions) {
2783 $task = new stdclass();
2784 $task->title = get_string('latesubmissionsallowed', 'workshop');
2785 $task->completed = 'info';
2786 $phase->tasks['latesubmissionsallowed'] = $task;
2788 if (isset($phase->tasks['submissionstartdatetime']) or isset($phase->tasks['submissionenddatetime'])) {
2789 if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) {
2790 $task = new stdclass();
2791 $task->title = get_string('deadlinesignored', 'workshop');
2792 $task->completed = 'info';
2793 $phase->tasks['deadlinesignored'] = $task;
2796 $this->phases[workshop::PHASE_SUBMISSION] = $phase;
2798 //---------------------------------------------------------
2799 // setup | submission | * ASSESSMENT | evaluation | closed
2800 //---------------------------------------------------------
2801 $phase = new stdclass();
2802 $phase->title = get_string('phaseassessment', 'workshop');
2803 $phase->tasks = array();
2804 $phase->isreviewer = has_capability('mod/workshop:peerassess', $workshop->context, $userid);
2805 if ($workshop->phase == workshop::PHASE_SUBMISSION and $workshop->phaseswitchassessment
2806 and has_capability('mod/workshop:switchphase', $workshop->context, $userid)) {
2807 $task = new stdClass();
2808 $task->title = get_string('switchphase30auto', 'mod_workshop', workshop::timestamp_formats($workshop->submissionend));
2809 $task->completed = 'info';
2810 $phase->tasks['autoswitchinfo'] = $task;
2812 if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_ASSESSMENT
2813 and $phase->isreviewer and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) {
2814 $task = new stdclass();
2815 $task->title = get_string('exampleassesstask', 'workshop');
2816 $examples = $workshop->get_examples_for_reviewer($userid);
2817 $a = new stdclass();
2818 $a->expected = count($examples);
2820 foreach ($examples as $exampleid => $example) {
2821 if (!is_null($example->grade)) {
2825 $task->details = get_string('exampleassesstaskdetails', 'workshop', $a);
2826 if ($a->assessed == $a->expected) {
2827 $task->completed = true;
2828 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) {
2829 $task->completed = false;
2831 $phase->tasks['examples'] = $task;
2833 if (empty($phase->tasks['examples']) or !empty($phase->tasks['examples']->completed)) {
2834 $phase->assessments = $workshop->get_assessments_by_reviewer($userid);
2835 $numofpeers = 0; // number of allocated peer-assessments
2836 $numofpeerstodo = 0; // number of peer-assessments to do
2837 $numofself = 0; // number of allocated self-assessments - should be 0 or 1
2838 $numofselftodo = 0; // number of self-assessments to do - should be 0 or 1
2839 foreach ($phase->assessments as $a) {
2840 if ($a->authorid == $userid) {
2842 if (is_null($a->grade)) {
2847 if (is_null($a->grade)) {
2854 $task = new stdclass();
2855 if ($numofpeerstodo == 0) {
2856 $task->completed = true;
2857 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) {
2858 $task->completed = false;
2860 $a = new stdclass();
2861 $a->total = $numofpeers;
2862 $a->todo = $numofpeerstodo;
2863 $task->title = get_string('taskassesspeers', 'workshop');
2864 $task->details = get_string('taskassesspeersdetails', 'workshop', $a);
2866 $phase->tasks['assesspeers'] = $task;
2868 if ($workshop->useselfassessment and $numofself) {
2869 $task = new stdclass();
2870 if ($numofselftodo == 0) {
2871 $task->completed = true;
2872 } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) {
2873 $task->completed = false;
2875 $task->title = get_string('taskassessself', 'workshop');
2876 $phase->tasks['assessself'] = $task;
2879 if ($workshop->assessmentstart) {
2880 $task = new stdclass();
2881 $task->title = get_string('assessmentstartdatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentstart));
2882 $task->completed = 'info';
2883 $phase->tasks['assessmentstartdatetime'] = $task;
2885 if ($workshop->assessmentend) {
2886 $task = new stdclass();
2887 $task->title = get_string('assessmentenddatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentend));
2888 $task->completed = 'info';
2889 $phase->tasks['assessmentenddatetime'] = $task;
2891 if (isset($phase->tasks['assessmentstartdatetime']) or isset($phase->tasks['assessmentenddatetime'])) {
2892 if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) {
2893 $task = new stdclass();
2894 $task->title = get_string('deadlinesignored', 'workshop');
2895 $task->completed = 'info';
2896 $phase->tasks['deadlinesignored'] = $task;
2899 $this->phases[workshop::PHASE_ASSESSMENT] = $phase;
2901 //---------------------------------------------------------
2902 // setup | submission | assessment | * EVALUATION | closed
2903 //---------------------------------------------------------
2904 $phase = new stdclass();
2905 $phase->title = get_string('phaseevaluation', 'workshop');
2906 $phase->tasks = array();
2907 if (has_capability('mod/workshop:overridegrades', $workshop->context)) {
2908 $expected = $workshop->count_potential_authors(false);
2909 $calculated = $DB->count_records_select('workshop_submissions',
2910 'workshopid = ? AND (grade IS NOT NULL OR gradeover IS NOT NULL)', array($workshop->id));
2911 $task = new stdclass();
2912 $task->title = get_string('calculatesubmissiongrades', 'workshop');
2913 $a = new stdclass();
2914 $a->expected = $expected;
2915 $a->calculated = $calculated;
2916 $task->details = get_string('calculatesubmissiongradesdetails', 'workshop', $a);
2917 if ($calculated >= $expected) {
2918 $task->completed = true;
2919 } elseif ($workshop->phase > workshop::PHASE_EVALUATION) {
2920 $task->completed = false;
2922 $phase->tasks['calculatesubmissiongrade'] = $task;
2924 $expected = $workshop->count_potential_reviewers(false);
2925 $calculated = $DB->count_records_select('workshop_aggregations',
2926 'workshopid = ? AND gradinggrade IS NOT NULL', array($workshop->id));
2927 $task = new stdclass();
2928 $task->title = get_string('calculategradinggrades', 'workshop');
2929 $a = new stdclass();
2930 $a->expected = $expected;
2931 $a->calculated = $calculated;
2932 $task->details = get_string('calculategradinggradesdetails', 'workshop', $a);
2933 if ($calculated >= $expected) {
2934 $task->completed = true;
2935 } elseif ($workshop->phase > workshop::PHASE_EVALUATION) {
2936 $task->completed = false;
2938 $phase->tasks['calculategradinggrade'] = $task;
2940 } elseif ($workshop->phase == workshop::PHASE_EVALUATION) {
2941 $task = new stdclass();
2942 $task->title = get_string('evaluategradeswait', 'workshop');
2943 $task->completed = 'info';
2944 $phase->tasks['evaluateinfo'] = $task;
2947 if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
2948 $task = new stdclass();
2949 $task->title = get_string('taskconclusion', 'workshop');
2950 $task->link = $workshop->updatemod_url();
2951 if (trim($workshop->conclusion)) {
2952 $task->completed = true;
2953 } elseif ($workshop->phase >= workshop::PHASE_EVALUATION) {
2954 $task->completed = false;
2956 $phase->tasks['conclusion'] = $task;
2959 $this->phases[workshop::PHASE_EVALUATION] = $phase;
2961 //---------------------------------------------------------
2962 // setup | submission | assessment | evaluation | * CLOSED
2963 //---------------------------------------------------------
2964 $phase = new stdclass();
2965 $phase->title = get_string('phaseclosed', 'workshop');
2966 $phase->tasks = array();
2967 $this->phases[workshop::PHASE_CLOSED] = $phase;
2969 // Polish data, set default values if not done explicitly
2970 foreach ($this->phases as $phasecode => $phase) {
2971 $phase->title = isset($phase->title) ? $phase->title : '';
2972 $phase->tasks = isset($phase->tasks) ? $phase->tasks : array();
2973 if ($phasecode == $workshop->phase) {
2974 $phase->active = true;
2976 $phase->active = false;
2978 if (!isset($phase->actions)) {
2979 $phase->actions = array();
2982 foreach ($phase->tasks as $taskcode => $task) {
2983 $task->title = isset($task->title) ? $task->title : '';
2984 $task->link = isset($task->link) ? $task->link : null;
2985 $task->details = isset($task->details) ? $task->details : '';
2986 $task->completed = isset($task->completed) ? $task->completed : null;
2990 // Add phase switching actions
2991 if (has_capability('mod/workshop:switchphase', $workshop->context, $userid)) {
2992 foreach ($this->phases as $phasecode => $phase) {
2993 if (! $phase->active) {
2994 $action = new stdclass();
2995 $action->type = 'switchphase';
2996 $action->url = $workshop->switchphase_url($phasecode);
2997 $phase->actions[] = $action;
3004 * Returns example submissions to be assessed by the owner of the planner
3006 * This is here to cache the DB query because the same list is needed later in view.php
3008 * @see workshop::get_examples_for_reviewer() for the format of returned value
3011 public function get_examples() {
3012 if (is_null($this->examples)) {
3013 $this->examples = $this->workshop->get_examples_for_reviewer($this->userid);
3015 return $this->examples;
3020 * Common base class for submissions and example submissions rendering
3022 * Subclasses of this class convert raw submission record from
3023 * workshop_submissions table (as returned by {@see workshop::get_submission_by_id()}
3024 * for example) into renderable objects.
3026 abstract class workshop_submission_base {
3028 /** @var bool is the submission anonymous (i.e. contains author information) */
3029 protected $anonymous;
3031 /* @var array of columns from workshop_submissions that are assigned as properties */
3032 protected $fields = array();
3034 /** @var workshop */
3035 protected $workshop;
3038 * Copies the properties of the given database record into properties of $this instance
3040 * @param workshop $workshop
3041 * @param stdClass $submission full record
3042 * @param bool $showauthor show the author-related information
3043 * @param array $options additional properties
3045 public function __construct(workshop $workshop, stdClass $submission, $showauthor = false) {
3047 $this->workshop = $workshop;
3049 foreach ($this->fields as $field) {
3050 if (!property_exists($submission, $field)) {
3051 throw new coding_exception('Submission record must provide public property ' . $field);
3053 if (!property_exists($this, $field)) {
3054 throw new coding_exception('Renderable component must accept public property ' . $field);
3056 $this->{$field} = $submission->{$field};
3060 $this->anonymous = false;
3067 * Unsets all author-related properties so that the renderer does not have access to them
3069 * Usually this is called by the contructor but can be called explicitely, too.
3071 public function anonymize() {
3072 foreach (array('authorid', 'authorfirstname', 'authorlastname',
3073 'authorpicture', 'authorimagealt', 'authoremail') as $field) {
3074 unset($this->{$field});
3076 $this->anonymous = true;
3080 * Does the submission object contain author-related information?
3082 * @return null|boolean
3084 public function is_anonymous() {
3085 return $this->anonymous;
3090 * Renderable object containing a basic set of information needed to display the submission summary
3092 * @see workshop_renderer::render_workshop_submission_summary
3094 class workshop_submission_summary extends workshop_submission_base implements renderable {
3100 /** @var string graded|notgraded */
3103 public $timecreated;
3105 public $timemodified;
3109 public $authorfirstname;
3111 public $authorlastname;
3113 public $authorpicture;
3115 public $authorimagealt;
3117 public $authoremail;
3118 /** @var moodle_url to display submission */
3122 * @var array of columns from workshop_submissions that are assigned as properties
3123 * of instances of this class
3125 protected $fields = array(
3126 'id', 'title', 'timecreated', 'timemodified',
3127 'authorid', 'authorfirstname', 'authorlastname', 'authorpicture',
3128 'authorimagealt', 'authoremail');
3132 * Renderable object containing all the information needed to display the submission
3134 * @see workshop_renderer::render_workshop_submission()
3136 class workshop_submission extends workshop_submission_summary implements renderable {
3141 public $contentformat;
3143 public $contenttrust;
3148 * @var array of columns from workshop_submissions that are assigned as properties
3149 * of instances of this class
3151 protected $fields = array(
3152 'id', 'title', 'timecreated', 'timemodified', 'content', 'contentformat', 'contenttrust',
3153 'attachment', 'authorid', 'authorfirstname', 'authorlastname', 'authorpicture',
3154 'authorimagealt', 'authoremail');
3158 * Renderable object containing a basic set of information needed to display the example submission summary
3160 * @see workshop::prepare_example_summary()
3161 * @see workshop_renderer::render_workshop_example_submission_summary()
3163 class workshop_example_submission_summary extends workshop_submission_base implements renderable {
3169 /** @var string graded|notgraded */
3171 /** @var stdClass */
3173 /** @var moodle_url */
3175 /** @var moodle_url */
3178 public $assesslabel;
3179 /** @var moodle_url */
3181 /** @var bool must be set explicitly by the caller */
3182 public $editable = false;
3185 * @var array of columns from workshop_submissions that are assigned as properties
3186 * of instances of this class
3188 protected $fields = array('id', 'title');
3191 * Example submissions are always anonymous
3195 public function is_anonymous() {
3201 * Renderable object containing all the information needed to display the example submission
3203 * @see workshop_renderer::render_workshop_example_submission()
3205 class workshop_example_submission extends workshop_example_submission_summary implements renderable {
3210 public $contentformat;
3212 public $contenttrust;
3217 * @var array of columns from workshop_submissions that are assigned as properties
3218 * of instances of this class
3220 protected $fields = array('id', 'title', 'content', 'contentformat', 'contenttrust', 'attachment');
3225 * Common base class for assessments rendering
3227 * Subclasses of this class convert raw assessment record from
3228 * workshop_assessments table (as returned by {@see workshop::get_assessment_by_id()}
3229 * for example) into renderable objects.
3231 abstract class workshop_assessment_base {
3233 /** @var string the optional title of the assessment */
3236 /** @var workshop_assessment_form $form as returned by {@link workshop_strategy::get_assessment_form()} */
3239 /** @var moodle_url */
3242 /** @var float|null the real received grade */
3243 public $realgrade = null;
3245 /** @var float the real maximum grade */
3248 /** @var stdClass|null reviewer user info */
3249 public $reviewer = null;
3251 /** @var stdClass|null assessed submission's author user info */
3252 public $author = null;
3254 /** @var array of actions */
3255 public $actions = array();
3257 /* @var array of columns that are assigned as properties */
3258 protected $fields = array();
3260 /** @var workshop */
3261 protected $workshop;
3264 * Copies the properties of the given database record into properties of $this instance
3266 * The $options keys are: showreviewer, showauthor
3267 * @param workshop $workshop
3268 * @param stdClass $assessment full record
3269 * @param array $options additional properties
3271 public function __construct(workshop $workshop, stdClass $record, array $options = array()) {
3273 $this->workshop = $workshop;
3274 $this->validate_raw_record($record);
3276 foreach ($this->fields as $field) {
3277 if (!property_exists($record, $field)) {
3278 throw new coding_exception('Assessment record must provide public property ' . $field);
3280 if (!property_exists($this, $field)) {
3281 throw new coding_exception('Renderable component must accept public property ' . $field);
3283 $this->{$field} = $record->{$field};
3286 if (!empty($options['showreviewer'])) {
3287 $this->reviewer = user_picture::unalias($record, null, 'revieweridx', 'reviewer');
3290 if (!empty($options['showauthor'])) {
3291 $this->author = user_picture::unalias($record, null, 'authorid', 'author');
3298 * @param moodle_url $url action URL
3299 * @param string $label action label
3300 * @param string $method get|post
3302 public function add_action(moodle_url $url, $label, $method = 'get') {
3304 $action = new stdClass();
3305 $action->url = $url;
3306 $action->label = $label;
3307 $action->method = $method;
3309 $this->actions[] = $action;
3313 * Makes sure that we can cook the renderable component from the passed raw database record
3315 * @param stdClass $assessment full assessment record
3316 * @throws coding_exception if the caller passed unexpected data
3318 protected function validate_raw_record(stdClass $record) {
3319 // nothing to do here
3325 * Represents a rendarable full assessment
3327 class workshop_assessment extends workshop_assessment_base implements renderable {
3333 public $submissionid;
3339 public $timecreated;
3342 public $timemodified;
3348 public $gradinggrade;
3351 public $gradinggradeover;
3354 public $feedbackauthor;
3357 public $feedbackauthorformat;
3360 public $feedbackauthorattachment;
3363 protected $fields = array('id', 'submissionid', 'weight', 'timecreated',
3364 'timemodified', 'grade', 'gradinggrade', 'gradinggradeover', 'feedbackauthor',
3365 'feedbackauthorformat', 'feedbackauthorattachment');
3368 * Format the overall feedback text content
3370 * False is returned if the&n