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