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