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