Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Library of internal classes and functions for module workshop
19
 *
20
 * All the workshop specific functions, needed to implement the module
21
 * logic, should go to here. Instead of having bunch of function named
22
 * workshop_something() taking the workshop instance as the first
23
 * parameter, we use a class workshop that provides all methods.
24
 *
25
 * @package    mod_workshop
26
 * @copyright  2009 David Mudrak <david.mudrak@gmail.com>
27
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28
 */
29
 
30
defined('MOODLE_INTERNAL') || die();
31
 
32
require_once(__DIR__.'/lib.php');     // we extend this library here
33
require_once($CFG->libdir . '/gradelib.php');   // we use some rounding and comparing routines here
34
require_once($CFG->libdir . '/filelib.php');
35
 
36
/**
37
 * Full-featured workshop API
38
 *
39
 * This wraps the workshop database record with a set of methods that are called
40
 * from the module itself. The class should be initialized right after you get
41
 * $workshop, $cm and $course records at the begining of the script.
42
 */
43
class workshop {
44
 
45
    /** error status of the {@link self::add_allocation()} */
46
    const ALLOCATION_EXISTS             = -9999;
47
 
48
    /** the internal code of the workshop phases as are stored in the database */
49
    const PHASE_SETUP                   = 10;
50
    const PHASE_SUBMISSION              = 20;
51
    const PHASE_ASSESSMENT              = 30;
52
    const PHASE_EVALUATION              = 40;
53
    const PHASE_CLOSED                  = 50;
54
 
55
    /** the internal code of the examples modes as are stored in the database */
56
    const EXAMPLES_VOLUNTARY            = 0;
57
    const EXAMPLES_BEFORE_SUBMISSION    = 1;
58
    const EXAMPLES_BEFORE_ASSESSMENT    = 2;
59
 
60
    /** @var stdclass workshop record from database */
61
    public $dbrecord;
62
 
63
    /** @var cm_info course module record */
64
    public $cm;
65
 
66
    /** @var stdclass course record */
67
    public $course;
68
 
69
    /** @var stdclass context object */
70
    public $context;
71
 
72
    /** @var int workshop instance identifier */
73
    public $id;
74
 
75
    /** @var string workshop activity name */
76
    public $name;
77
 
78
    /** @var string introduction or description of the activity */
79
    public $intro;
80
 
81
    /** @var int format of the {@link $intro} */
82
    public $introformat;
83
 
84
    /** @var string instructions for the submission phase */
85
    public $instructauthors;
86
 
87
    /** @var int format of the {@link $instructauthors} */
88
    public $instructauthorsformat;
89
 
90
    /** @var string instructions for the assessment phase */
91
    public $instructreviewers;
92
 
93
    /** @var int format of the {@link $instructreviewers} */
94
    public $instructreviewersformat;
95
 
96
    /** @var int timestamp of when the module was modified */
97
    public $timemodified;
98
 
99
    /** @var int current phase of workshop, for example {@link workshop::PHASE_SETUP} */
100
    public $phase;
101
 
102
    /** @var bool optional feature: students practise evaluating on example submissions from teacher */
103
    public $useexamples;
104
 
105
    /** @var bool optional feature: students perform peer assessment of others' work (deprecated, consider always enabled) */
106
    public $usepeerassessment;
107
 
108
    /** @var bool optional feature: students perform self assessment of their own work */
109
    public $useselfassessment;
110
 
111
    /** @var float number (10, 5) unsigned, the maximum grade for submission */
112
    public $grade;
113
 
114
    /** @var float number (10, 5) unsigned, the maximum grade for assessment */
115
    public $gradinggrade;
116
 
117
    /** @var string type of the current grading strategy used in this workshop, for example 'accumulative' */
118
    public $strategy;
119
 
120
    /** @var string the name of the evaluation plugin to use for grading grades calculation */
121
    public $evaluation;
122
 
123
    /** @var int number of digits that should be shown after the decimal point when displaying grades */
124
    public $gradedecimals;
125
 
126
    /** @var int number of allowed submission attachments and the files embedded into submission */
127
    public $nattachments;
128
 
129
     /** @var string list of allowed file types that are allowed to be embedded into submission */
130
    public $submissionfiletypes = null;
131
 
132
    /** @var bool allow submitting the work after the deadline */
133
    public $latesubmissions;
134
 
135
    /** @var int maximum size of the one attached file in bytes */
136
    public $maxbytes;
137
 
138
    /** @var int mode of example submissions support, for example {@link workshop::EXAMPLES_VOLUNTARY} */
139
    public $examplesmode;
140
 
141
    /** @var int if greater than 0 then the submission is not allowed before this timestamp */
142
    public $submissionstart;
143
 
144
    /** @var int if greater than 0 then the submission is not allowed after this timestamp */
145
    public $submissionend;
146
 
147
    /** @var int if greater than 0 then the peer assessment is not allowed before this timestamp */
148
    public $assessmentstart;
149
 
150
    /** @var int if greater than 0 then the peer assessment is not allowed after this timestamp */
151
    public $assessmentend;
152
 
153
    /** @var bool automatically switch to the assessment phase after the submissions deadline */
154
    public $phaseswitchassessment;
155
 
156
    /** @var string conclusion text to be displayed at the end of the activity */
157
    public $conclusion;
158
 
159
    /** @var int format of the conclusion text */
160
    public $conclusionformat;
161
 
162
    /** @var int the mode of the overall feedback */
163
    public $overallfeedbackmode;
164
 
165
    /** @var int maximum number of overall feedback attachments */
166
    public $overallfeedbackfiles;
167
 
168
    /** @var string list of allowed file types that can be attached to the overall feedback */
169
    public $overallfeedbackfiletypes = null;
170
 
171
    /** @var int maximum size of one file attached to the overall feedback */
172
    public $overallfeedbackmaxbytes;
173
 
174
    /** @var int Should the submission form show the text field? */
175
    public $submissiontypetext;
176
 
177
    /** @var int Should the submission form show the file attachment field? */
178
    public $submissiontypefile;
179
 
180
    /**
181
     * @var workshop_strategy grading strategy instance
182
     * Do not use directly, get the instance using {@link workshop::grading_strategy_instance()}
183
     */
184
    protected $strategyinstance = null;
185
 
186
    /**
187
     * @var workshop_evaluation grading evaluation instance
188
     * Do not use directly, get the instance using {@link workshop::grading_evaluation_instance()}
189
     */
190
    protected $evaluationinstance = null;
191
 
192
    /**
193
     * @var array It gets initialised in init_initial_bar, and may have keys 'i_first' and 'i_last' depending on what is selected.
194
     */
195
    protected $initialbarprefs = [];
196
 
197
    /**
198
     * Initializes the workshop API instance using the data from DB
199
     *
200
     * Makes deep copy of all passed records properties.
201
     *
202
     * For unit testing only, $cm and $course may be set to null. This is so that
203
     * you can test without having any real database objects if you like. Not all
204
     * functions will work in this situation.
205
     *
206
     * @param stdClass $dbrecord Workshop instance data from {workshop} table
207
     * @param stdClass|cm_info $cm Course module record
208
     * @param stdClass $course Course record from {course} table
209
     * @param stdClass $context The context of the workshop instance
210
     */
211
    public function __construct(stdclass $dbrecord, $cm, $course, stdclass $context=null) {
212
        $this->dbrecord = $dbrecord;
213
        foreach ($this->dbrecord as $field => $value) {
214
            if (property_exists('workshop', $field)) {
215
                $this->{$field} = $value;
216
            }
217
        }
218
 
219
        $this->strategy = clean_param($this->strategy, PARAM_PLUGIN);
220
        $this->evaluation = clean_param($this->evaluation, PARAM_PLUGIN);
221
 
222
        if (is_null($cm) || is_null($course)) {
223
            throw new coding_exception('Must specify $cm and $course');
224
        }
225
        $this->course = $course;
226
        if ($cm instanceof cm_info) {
227
            $this->cm = $cm;
228
        } else {
229
            $modinfo = get_fast_modinfo($course);
230
            $this->cm = $modinfo->get_cm($cm->id);
231
        }
232
        if (is_null($context)) {
233
            $this->context = context_module::instance($this->cm->id);
234
        } else {
235
            $this->context = $context;
236
        }
237
    }
238
 
239
    ////////////////////////////////////////////////////////////////////////////////
240
    // Static methods                                                             //
241
    ////////////////////////////////////////////////////////////////////////////////
242
 
243
    /**
244
     * Return list of available allocation methods
245
     *
246
     * @return array Array ['string' => 'string'] of localized allocation method names
247
     */
248
    public static function installed_allocators() {
249
        $installed = core_component::get_plugin_list('workshopallocation');
250
        $forms = array();
251
        foreach ($installed as $allocation => $allocationpath) {
252
            if (file_exists($allocationpath . '/lib.php')) {
253
                $forms[$allocation] = get_string('pluginname', 'workshopallocation_' . $allocation);
254
            }
255
        }
256
        // usability - make sure that manual allocation appears the first
257
        if (isset($forms['manual'])) {
258
            $m = array('manual' => $forms['manual']);
259
            unset($forms['manual']);
260
            $forms = array_merge($m, $forms);
261
        }
262
        return $forms;
263
    }
264
 
265
    /**
266
     * Returns an array of options for the editors that are used for submitting and assessing instructions
267
     *
268
     * @param stdClass $context
269
     * @uses EDITOR_UNLIMITED_FILES hard-coded value for the 'maxfiles' option
270
     * @return array
271
     */
272
    public static function instruction_editors_options(stdclass $context) {
273
        return array('subdirs' => 1, 'maxbytes' => 0, 'maxfiles' => -1,
274
                     'changeformat' => 1, 'context' => $context, 'noclean' => 1, 'trusttext' => 0);
275
    }
276
 
277
    /**
278
     * Given the percent and the total, returns the number
279
     *
280
     * @param float $percent from 0 to 100
281
     * @param float $total   the 100% value
282
     * @return float
283
     */
284
    public static function percent_to_value($percent, $total) {
285
        if ($percent < 0 or $percent > 100) {
286
            throw new coding_exception('The percent can not be less than 0 or higher than 100');
287
        }
288
 
289
        return $total * $percent / 100;
290
    }
291
 
292
    /**
293
     * Returns an array of numeric values that can be used as maximum grades
294
     *
295
     * @return array Array of integers
296
     */
297
    public static function available_maxgrades_list() {
298
        $grades = array();
299
        for ($i=100; $i>=0; $i--) {
300
            $grades[$i] = $i;
301
        }
302
        return $grades;
303
    }
304
 
305
    /**
306
     * Returns the localized list of supported examples modes
307
     *
308
     * @return array
309
     */
310
    public static function available_example_modes_list() {
311
        $options = array();
312
        $options[self::EXAMPLES_VOLUNTARY]         = get_string('examplesvoluntary', 'workshop');
313
        $options[self::EXAMPLES_BEFORE_SUBMISSION] = get_string('examplesbeforesubmission', 'workshop');
314
        $options[self::EXAMPLES_BEFORE_ASSESSMENT] = get_string('examplesbeforeassessment', 'workshop');
315
        return $options;
316
    }
317
 
318
    /**
319
     * Returns the list of available grading strategy methods
320
     *
321
     * @return array ['string' => 'string']
322
     */
323
    public static function available_strategies_list() {
324
        $installed = core_component::get_plugin_list('workshopform');
325
        $forms = array();
326
        foreach ($installed as $strategy => $strategypath) {
327
            if (file_exists($strategypath . '/lib.php')) {
328
                $forms[$strategy] = get_string('pluginname', 'workshopform_' . $strategy);
329
            }
330
        }
331
        return $forms;
332
    }
333
 
334
    /**
335
     * Returns the list of available grading evaluation methods
336
     *
337
     * @return array of (string)name => (string)localized title
338
     */
339
    public static function available_evaluators_list() {
340
        $evals = array();
341
        foreach (core_component::get_plugin_list_with_file('workshopeval', 'lib.php', false) as $eval => $evalpath) {
342
            $evals[$eval] = get_string('pluginname', 'workshopeval_' . $eval);
343
        }
344
        return $evals;
345
    }
346
 
347
    /**
348
     * Return an array of possible values of assessment dimension weight
349
     *
350
     * @return array of integers 0, 1, 2, ..., 16
351
     */
352
    public static function available_dimension_weights_list() {
353
        $weights = array();
354
        for ($i=16; $i>=0; $i--) {
355
            $weights[$i] = $i;
356
        }
357
        return $weights;
358
    }
359
 
360
    /**
361
     * Return an array of possible values of assessment weight
362
     *
363
     * Note there is no real reason why the maximum value here is 16. It used to be 10 in
364
     * workshop 1.x and I just decided to use the same number as in the maximum weight of
365
     * a single assessment dimension.
366
     * The value looks reasonable, though. Teachers who would want to assign themselves
367
     * higher weight probably do not want peer assessment really...
368
     *
369
     * @return array of integers 0, 1, 2, ..., 16
370
     */
371
    public static function available_assessment_weights_list() {
372
        $weights = array();
373
        for ($i=16; $i>=0; $i--) {
374
            $weights[$i] = $i;
375
        }
376
        return $weights;
377
    }
378
 
379
    /**
380
     * Helper function returning the greatest common divisor
381
     *
382
     * @param int $a
383
     * @param int $b
384
     * @return int
385
     */
386
    public static function gcd($a, $b) {
387
        return ($b == 0) ? ($a):(self::gcd($b, $a % $b));
388
    }
389
 
390
    /**
391
     * Helper function returning the least common multiple
392
     *
393
     * @param int $a
394
     * @param int $b
395
     * @return int
396
     */
397
    public static function lcm($a, $b) {
398
        return ($a / self::gcd($a,$b)) * $b;
399
    }
400
 
401
    /**
402
     * Returns an object suitable for strings containing dates/times
403
     *
404
     * The returned object contains properties date, datefullshort, datetime, ... containing the given
405
     * timestamp formatted using strftimedate, strftimedatefullshort, strftimedatetime, ... from the
406
     * current lang's langconfig.php
407
     * This allows translators and administrators customize the date/time format.
408
     *
409
     * @param int $timestamp the timestamp in UTC
410
     * @return stdclass
411
     */
412
    public static function timestamp_formats($timestamp) {
413
        $formats = array('date', 'datefullshort', 'dateshort', 'datetime',
414
                'datetimeshort', 'daydate', 'daydatetime', 'dayshort', 'daytime',
415
                'monthyear', 'recent', 'recentfull', 'time');
416
        $a = new stdclass();
417
        foreach ($formats as $format) {
418
            $a->{$format} = userdate($timestamp, get_string('strftime'.$format, 'langconfig'));
419
        }
420
        $day = userdate($timestamp, '%Y%m%d', 99, false);
421
        $today = userdate(time(), '%Y%m%d', 99, false);
422
        $tomorrow = userdate(time() + DAYSECS, '%Y%m%d', 99, false);
423
        $yesterday = userdate(time() - DAYSECS, '%Y%m%d', 99, false);
424
        $distance = (int)round(abs(time() - $timestamp) / DAYSECS);
425
        if ($day == $today) {
426
            $a->distanceday = get_string('daystoday', 'workshop');
427
        } elseif ($day == $yesterday) {
428
            $a->distanceday = get_string('daysyesterday', 'workshop');
429
        } elseif ($day < $today) {
430
            $a->distanceday = get_string('daysago', 'workshop', $distance);
431
        } elseif ($day == $tomorrow) {
432
            $a->distanceday = get_string('daystomorrow', 'workshop');
433
        } elseif ($day > $today) {
434
            $a->distanceday = get_string('daysleft', 'workshop', $distance);
435
        }
436
        return $a;
437
    }
438
 
439
    /**
440
     * Converts the argument into an array (list) of file extensions.
441
     *
442
     * The list can be separated by whitespace, end of lines, commas colons and semicolons.
443
     * Empty values are not returned. Values are converted to lowercase.
444
     * Duplicates are removed. Glob evaluation is not supported.
445
     *
446
     * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util}
447
     * @param string|array $extensions list of file extensions
448
     * @return array of strings
449
     */
450
    public static function normalize_file_extensions($extensions) {
451
 
452
        debugging('The method workshop::normalize_file_extensions() is deprecated.
453
            Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER);
454
 
455
        if ($extensions === '') {
456
            return array();
457
        }
458
 
459
        if (!is_array($extensions)) {
460
            $extensions = preg_split('/[\s,;:"\']+/', $extensions, -1, PREG_SPLIT_NO_EMPTY);
461
        }
462
 
463
        foreach ($extensions as $i => $extension) {
464
            $extension = str_replace('*.', '', $extension);
465
            $extension = strtolower($extension);
466
            $extension = ltrim($extension, '.');
467
            $extension = trim($extension);
468
            $extensions[$i] = $extension;
469
        }
470
 
471
        foreach ($extensions as $i => $extension) {
472
            if (strpos($extension, '*') !== false or strpos($extension, '?') !== false) {
473
                unset($extensions[$i]);
474
            }
475
        }
476
 
477
        $extensions = array_filter($extensions, 'strlen');
478
        $extensions = array_keys(array_flip($extensions));
479
 
480
        foreach ($extensions as $i => $extension) {
481
            $extensions[$i] = '.'.$extension;
482
        }
483
 
484
        return $extensions;
485
    }
486
 
487
    /**
488
     * Cleans the user provided list of file extensions.
489
     *
490
     * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util}
491
     * @param string $extensions
492
     * @return string
493
     */
494
    public static function clean_file_extensions($extensions) {
495
 
496
        debugging('The method workshop::clean_file_extensions() is deprecated.
497
            Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER);
498
 
499
        $extensions = self::normalize_file_extensions($extensions);
500
 
501
        foreach ($extensions as $i => $extension) {
502
            $extensions[$i] = ltrim($extension, '.');
503
        }
504
 
505
        return implode(', ', $extensions);
506
    }
507
 
508
    /**
509
     * Check given file types and return invalid/unknown ones.
510
     *
511
     * Empty allowlist is interpretted as "any extension is valid".
512
     *
513
     * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util}
514
     * @param string|array $extensions list of file extensions
515
     * @param string|array $allowlist list of valid extensions
516
     * @return array list of invalid extensions not found in the allowlist
517
     */
518
    public static function invalid_file_extensions($extensions, $allowlist) {
519
 
520
        debugging('The method workshop::invalid_file_extensions() is deprecated.
521
            Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER);
522
 
523
        $extensions = self::normalize_file_extensions($extensions);
524
        $allowlist = self::normalize_file_extensions($allowlist);
525
 
526
        if (empty($extensions) or empty($allowlist)) {
527
            return array();
528
        }
529
 
530
        // Return those items from $extensions that are not present in $allowlist.
531
        return array_keys(array_diff_key(array_flip($extensions), array_flip($allowlist)));
532
    }
533
 
534
    /**
535
     * Is the file have allowed to be uploaded to the workshop?
536
     *
537
     * Empty allowlist is interpretted as "any file type is allowed" rather
538
     * than "no file can be uploaded".
539
     *
540
     * @deprecated since Moodle 3.4 MDL-56486 - please use the {@link core_form\filetypes_util}
541
     * @param string $filename the file name
542
     * @param string|array $allowlist list of allowed file extensions
543
     * @return false
544
     */
545
    public static function is_allowed_file_type($filename, $allowlist) {
546
 
547
        debugging('The method workshop::is_allowed_file_type() is deprecated.
548
            Please use the methods provided by the \core_form\filetypes_util class.', DEBUG_DEVELOPER);
549
 
550
        $allowlist = self::normalize_file_extensions($allowlist);
551
 
552
        if (empty($allowlist)) {
553
            return true;
554
        }
555
 
556
        $haystack = strrev(trim(strtolower($filename)));
557
 
558
        foreach ($allowlist as $extension) {
559
            if (strpos($haystack, strrev($extension)) === 0) {
560
                // The file name ends with the extension.
561
                return true;
562
            }
563
        }
564
 
565
        return false;
566
    }
567
 
568
    ////////////////////////////////////////////////////////////////////////////////
569
    // Workshop API                                                               //
570
    ////////////////////////////////////////////////////////////////////////////////
571
 
572
    /**
573
     * Fetches all enrolled users with the capability mod/workshop:submit in the current workshop
574
     *
575
     * The returned objects contain properties required by user_picture and are ordered by lastname, firstname.
576
     * Only users with the active enrolment are returned.
577
     *
578
     * @param bool $musthavesubmission if true, return only users who have already submitted
579
     * @param int $groupid 0 means ignore groups, any other value limits the result by group id
580
     * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set)
581
     * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set)
582
     * @return array array[userid] => stdClass
583
     */
584
    public function get_potential_authors($musthavesubmission=true, $groupid=0, $limitfrom=0, $limitnum=0) {
585
        global $DB;
586
 
587
        list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:submit', $musthavesubmission, $groupid);
588
 
589
        if (empty($sql)) {
590
            return array();
591
        }
592
 
593
        list($sort, $sortparams) = users_order_by_sql('tmp');
594
        $sql = "SELECT *
595
                  FROM ($sql) tmp
596
              ORDER BY $sort";
597
 
598
        return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum);
599
    }
600
 
601
    /**
602
     * Returns the total number of users that would be fetched by {@link self::get_potential_authors()}
603
     *
604
     * @param bool $musthavesubmission if true, count only users who have already submitted
605
     * @param int $groupid 0 means ignore groups, any other value limits the result by group id
606
     * @return int
607
     */
608
    public function count_potential_authors($musthavesubmission=true, $groupid=0) {
609
        global $DB;
610
 
611
        list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:submit', $musthavesubmission, $groupid);
612
 
613
        if (empty($sql)) {
614
            return 0;
615
        }
616
 
617
        $sql = "SELECT COUNT(*)
618
                  FROM ($sql) tmp";
619
 
620
        return $DB->count_records_sql($sql, $params);
621
    }
622
 
623
    /**
624
     * Fetches all enrolled users with the capability mod/workshop:peerassess in the current workshop
625
     *
626
     * The returned objects contain properties required by user_picture and are ordered by lastname, firstname.
627
     * Only users with the active enrolment are returned.
628
     *
629
     * @param bool $musthavesubmission if true, return only users who have already submitted
630
     * @param int $groupid 0 means ignore groups, any other value limits the result by group id
631
     * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set)
632
     * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set)
633
     * @return array array[userid] => stdClass
634
     */
635
    public function get_potential_reviewers($musthavesubmission=false, $groupid=0, $limitfrom=0, $limitnum=0) {
636
        global $DB;
637
 
638
        list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:peerassess', $musthavesubmission, $groupid);
639
 
640
        if (empty($sql)) {
641
            return array();
642
        }
643
 
644
        list($sort, $sortparams) = users_order_by_sql('tmp');
645
        $sql = "SELECT *
646
                  FROM ($sql) tmp
647
              ORDER BY $sort";
648
 
649
        return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum);
650
    }
651
 
652
    /**
653
     * Returns the total number of users that would be fetched by {@link self::get_potential_reviewers()}
654
     *
655
     * @param bool $musthavesubmission if true, count only users who have already submitted
656
     * @param int $groupid 0 means ignore groups, any other value limits the result by group id
657
     * @return int
658
     */
659
    public function count_potential_reviewers($musthavesubmission=false, $groupid=0) {
660
        global $DB;
661
 
662
        list($sql, $params) = $this->get_users_with_capability_sql('mod/workshop:peerassess', $musthavesubmission, $groupid);
663
 
664
        if (empty($sql)) {
665
            return 0;
666
        }
667
 
668
        $sql = "SELECT COUNT(*)
669
                  FROM ($sql) tmp";
670
 
671
        return $DB->count_records_sql($sql, $params);
672
    }
673
 
674
    /**
675
     * Fetches all enrolled users that are authors or reviewers (or both) in the current workshop
676
     *
677
     * The returned objects contain properties required by user_picture and are ordered by lastname, firstname.
678
     * Only users with the active enrolment are returned.
679
     *
680
     * @see self::get_potential_authors()
681
     * @see self::get_potential_reviewers()
682
     * @param bool $musthavesubmission if true, return only users who have already submitted
683
     * @param int $groupid 0 means ignore groups, any other value limits the result by group id
684
     * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set)
685
     * @param int $limitnum return a subset containing this number of records (optional, required if $limitfrom is set)
686
     * @return array array[userid] => stdClass
687
     */
688
    public function get_participants($musthavesubmission=false, $groupid=0, $limitfrom=0, $limitnum=0) {
689
        global $DB;
690
 
691
        list($sql, $params) = $this->get_participants_sql($musthavesubmission, $groupid);
692
        list($filteringsql, $filteringparams) = $this->get_users_with_initial_filtering_sql_where();
693
        $wheresql = "";
694
 
695
        if ($filteringsql) {
696
            $wheresql .= $filteringsql;
697
            $params = array_merge($params, $filteringparams);
698
        }
699
        if (empty($sql)) {
700
            return array();
701
        }
702
 
703
        list($sort, $sortparams) = users_order_by_sql('tmp');
704
        $sql = "SELECT * FROM ($sql) tmp";
705
 
706
        if ($wheresql) {
707
            $sql .= " WHERE $wheresql";
708
        }
709
        $sql .= " ORDER BY $sort";
710
 
711
        return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum);
712
    }
713
 
714
    /**
715
     * Returns the total number of records that would be returned by {@link self::get_participants()}
716
     *
717
     * @param bool $musthavesubmission if true, return only users who have already submitted
718
     * @param int $groupid 0 means ignore groups, any other value limits the result by group id
719
     * @return int
720
     */
721
    public function count_participants($musthavesubmission=false, $groupid=0) {
722
        global $DB;
723
 
724
        list($sql, $params) = $this->get_participants_sql($musthavesubmission, $groupid);
725
 
726
        if (empty($sql)) {
727
            return 0;
728
        }
729
 
730
        $sql = "SELECT COUNT(*)
731
                  FROM ($sql) tmp";
732
 
733
        return $DB->count_records_sql($sql, $params);
734
    }
735
 
736
    /**
737
     * Checks if the given user is an actively enrolled participant in the workshop
738
     *
739
     * @param int $userid, defaults to the current $USER
740
     * @return boolean
741
     */
742
    public function is_participant($userid=null) {
743
        global $USER, $DB;
744
 
745
        if (is_null($userid)) {
746
            $userid = $USER->id;
747
        }
748
 
749
        list($sql, $params) = $this->get_participants_sql();
750
 
751
        if (empty($sql)) {
752
            return false;
753
        }
754
 
755
        $sql = "SELECT COUNT(*)
756
                  FROM {user} uxx
757
                  JOIN ({$sql}) pxx ON uxx.id = pxx.id
758
                 WHERE uxx.id = :uxxid";
759
        $params['uxxid'] = $userid;
760
 
761
        if ($DB->count_records_sql($sql, $params)) {
762
            return true;
763
        }
764
 
765
        return false;
766
    }
767
 
768
    /**
769
     * Groups the given users by the group membership
770
     *
771
     * This takes the module grouping settings into account. If a grouping is
772
     * set, returns only groups withing the course module grouping. Always
773
     * returns group [0] with all the given users.
774
     *
775
     * @param array $users array[userid] => stdclass{->id ->lastname ->firstname}
776
     * @return array array[groupid][userid] => stdclass{->id ->lastname ->firstname}
777
     */
778
    public function get_grouped($users) {
779
        global $DB;
780
        global $CFG;
781
 
782
        $grouped = array();  // grouped users to be returned
783
        if (empty($users)) {
784
            return $grouped;
785
        }
786
        if ($this->cm->groupingid) {
787
            // Group workshop set to specified grouping - only consider groups
788
            // within this grouping, and leave out users who aren't members of
789
            // this grouping.
790
            $groupingid = $this->cm->groupingid;
791
            // All users that are members of at least one group will be
792
            // added into a virtual group id 0
793
            $grouped[0] = array();
794
        } else {
795
            $groupingid = 0;
796
            // there is no need to be member of a group so $grouped[0] will contain
797
            // all users
798
            $grouped[0] = $users;
799
        }
800
        $gmemberships = groups_get_all_groups($this->cm->course, array_keys($users), $groupingid,
801
                            'gm.id,gm.groupid,gm.userid');
802
        foreach ($gmemberships as $gmembership) {
803
            if (!isset($grouped[$gmembership->groupid])) {
804
                $grouped[$gmembership->groupid] = array();
805
            }
806
            $grouped[$gmembership->groupid][$gmembership->userid] = $users[$gmembership->userid];
807
            $grouped[0][$gmembership->userid] = $users[$gmembership->userid];
808
        }
809
        return $grouped;
810
    }
811
 
812
    /**
813
     * Returns the list of all allocations (i.e. assigned assessments) in the workshop
814
     *
815
     * Assessments of example submissions are ignored
816
     *
817
     * @return array
818
     */
819
    public function get_allocations() {
820
        global $DB;
821
 
822
        $sql = 'SELECT a.id, a.submissionid, a.reviewerid, s.authorid
823
                  FROM {workshop_assessments} a
824
            INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
825
                 WHERE s.example = 0 AND s.workshopid = :workshopid';
826
        $params = array('workshopid' => $this->id);
827
 
828
        return $DB->get_records_sql($sql, $params);
829
    }
830
 
831
    /**
832
     * Returns the total number of records that would be returned by {@link self::get_submissions()}
833
     *
834
     * @param mixed $authorid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only
835
     * @param int $groupid If non-zero, return only submissions by authors in the specified group
836
     * @return int number of records
837
     */
838
    public function count_submissions($authorid='all', $groupid=0) {
839
        global $DB;
840
 
841
        $params = array('workshopid' => $this->id);
842
        $sql = "SELECT COUNT(s.id)
843
                  FROM {workshop_submissions} s
844
                  JOIN {user} u ON (s.authorid = u.id)";
845
        if ($groupid) {
846
            $sql .= " JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)";
847
            $params['groupid'] = $groupid;
848
        }
849
        $sql .= " WHERE s.example = 0 AND s.workshopid = :workshopid";
850
 
851
        if ('all' === $authorid) {
852
            // no additional conditions
853
        } elseif (!empty($authorid)) {
854
            list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED);
855
            $sql .= " AND authorid $usql";
856
            $params = array_merge($params, $uparams);
857
        } else {
858
            // $authorid is empty
859
            return 0;
860
        }
861
 
862
        return $DB->count_records_sql($sql, $params);
863
    }
864
 
865
 
866
    /**
867
     * Returns submissions from this workshop
868
     *
869
     * Fetches data from {workshop_submissions} and adds some useful information from other
870
     * tables. Does not return textual fields to prevent possible memory lack issues.
871
     *
872
     * @see self::count_submissions()
873
     * @param mixed $authorid int|array|'all' If set to [array of] integer, return submission[s] of the given user[s] only
874
     * @param int $groupid If non-zero, return only submissions by authors in the specified group
875
     * @param int $limitfrom Return a subset of records, starting at this point (optional)
876
     * @param int $limitnum Return a subset containing this many records in total (optional, required if $limitfrom is set)
877
     * @return array of records or an empty array
878
     */
879
    public function get_submissions($authorid='all', $groupid=0, $limitfrom=0, $limitnum=0) {
880
        global $DB;
881
 
882
        $userfieldsapi = \core_user\fields::for_userpic();
883
        $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects;
884
        $gradeoverbyfields = $userfieldsapi->get_sql('t', false, 'over', 'gradeoverbyx', false)->selects;
885
        $params            = array('workshopid' => $this->id);
886
        $sql = "SELECT s.id, s.workshopid, s.example, s.authorid, s.timecreated, s.timemodified,
887
                       s.title, s.grade, s.gradeover, s.gradeoverby, s.published,
888
                       $authorfields, $gradeoverbyfields
889
                  FROM {workshop_submissions} s
890
                  JOIN {user} u ON (s.authorid = u.id)";
891
        if ($groupid) {
892
            $sql .= " JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)";
893
            $params['groupid'] = $groupid;
894
        }
895
        $sql .= " LEFT JOIN {user} t ON (s.gradeoverby = t.id)
896
                 WHERE s.example = 0 AND s.workshopid = :workshopid";
897
 
898
        if ('all' === $authorid) {
899
            // no additional conditions
900
        } elseif (!empty($authorid)) {
901
            list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED);
902
            $sql .= " AND authorid $usql";
903
            $params = array_merge($params, $uparams);
904
        } else {
905
            // $authorid is empty
906
            return array();
907
        }
908
        list($sort, $sortparams) = users_order_by_sql('u');
909
        $sql .= " ORDER BY $sort";
910
 
911
        return $DB->get_records_sql($sql, array_merge($params, $sortparams), $limitfrom, $limitnum);
912
    }
913
 
914
    /**
915
     * Returns submissions from this workshop that are viewable by the current user (except example submissions).
916
     *
917
     * @param mixed $authorid int|array If set to [array of] integer, return submission[s] of the given user[s] only
918
     * @param int $groupid If non-zero, return only submissions by authors in the specified group. 0 for all groups.
919
     * @param int $limitfrom Return a subset of records, starting at this point (optional)
920
     * @param int $limitnum Return a subset containing this many records in total (optional, required if $limitfrom is set)
921
     * @return array of records and the total submissions count
922
     * @since  Moodle 3.4
923
     */
924
    public function get_visible_submissions($authorid = 0, $groupid = 0, $limitfrom = 0, $limitnum = 0) {
925
        global $DB, $USER;
926
 
927
        $submissions = array();
928
        $select = "SELECT s.*";
929
        $selectcount = "SELECT COUNT(s.id)";
930
        $from = " FROM {workshop_submissions} s";
931
        $params = array('workshopid' => $this->id);
932
 
933
        // Check if the passed group (or all groups when groupid is 0) is visible by the current user.
934
        if (!groups_group_visible($groupid, $this->course, $this->cm)) {
935
            return array($submissions, 0);
936
        }
937
 
938
        if ($groupid) {
939
            $from .= " JOIN {groups_members} gm ON (gm.userid = s.authorid AND gm.groupid = :groupid)";
940
            $params['groupid'] = $groupid;
941
        }
942
        $where = " WHERE s.workshopid = :workshopid AND s.example = 0";
943
 
944
        if (!has_capability('mod/workshop:viewallsubmissions', $this->context)) {
945
            // Check published submissions.
946
            $workshopclosed = $this->phase == self::PHASE_CLOSED;
947
            $canviewpublished = has_capability('mod/workshop:viewpublishedsubmissions', $this->context);
948
            if ($workshopclosed && $canviewpublished) {
949
                $published = " OR s.published = 1";
950
            } else {
951
                $published = '';
952
            }
953
 
954
            // Always get submissions I did or I provided feedback to.
955
            $where .= " AND (s.authorid = :authorid OR s.gradeoverby = :graderid $published)";
956
            $params['authorid'] = $USER->id;
957
            $params['graderid'] = $USER->id;
958
        }
959
 
960
        // Now, user filtering.
961
        if (!empty($authorid)) {
962
            list($usql, $uparams) = $DB->get_in_or_equal($authorid, SQL_PARAMS_NAMED);
963
            $where .= " AND s.authorid $usql";
964
            $params = array_merge($params, $uparams);
965
        }
966
 
967
        $order = " ORDER BY s.timecreated";
968
 
969
        $totalcount = $DB->count_records_sql($selectcount.$from.$where, $params);
970
        if ($totalcount) {
971
            $submissions = $DB->get_records_sql($select.$from.$where.$order, $params, $limitfrom, $limitnum);
972
        }
973
        return array($submissions, $totalcount);
974
    }
975
 
976
 
977
    /**
978
     * Returns a submission record with the author's data
979
     *
980
     * @param int $id submission id
981
     * @return stdclass
982
     */
983
    public function get_submission_by_id($id) {
984
        global $DB;
985
 
986
        // we intentionally check the workshopid here, too, so the workshop can't touch submissions
987
        // from other instances
988
        $userfieldsapi = \core_user\fields::for_userpic();
989
        $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects;
990
        $gradeoverbyfields = $userfieldsapi->get_sql('g', false, 'gradeoverby', 'gradeoverbyx', false)->selects;
991
        $sql = "SELECT s.*, $authorfields, $gradeoverbyfields
992
                  FROM {workshop_submissions} s
993
            INNER JOIN {user} u ON (s.authorid = u.id)
994
             LEFT JOIN {user} g ON (s.gradeoverby = g.id)
995
                 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.id = :id";
996
        $params = array('workshopid' => $this->id, 'id' => $id);
997
        return $DB->get_record_sql($sql, $params, MUST_EXIST);
998
    }
999
 
1000
    /**
1001
     * Returns a submission submitted by the given author
1002
     *
1003
     * @param int $id author id
1004
     * @return stdclass|false
1005
     */
1006
    public function get_submission_by_author($authorid) {
1007
        global $DB;
1008
 
1009
        if (empty($authorid)) {
1010
            return false;
1011
        }
1012
        $userfieldsapi = \core_user\fields::for_userpic();
1013
        $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects;
1014
        $gradeoverbyfields = $userfieldsapi->get_sql('g', false, 'gradeoverby', 'gradeoverbyx', false)->selects;
1015
        $sql = "SELECT s.*, $authorfields, $gradeoverbyfields
1016
                  FROM {workshop_submissions} s
1017
            INNER JOIN {user} u ON (s.authorid = u.id)
1018
             LEFT JOIN {user} g ON (s.gradeoverby = g.id)
1019
                 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.authorid = :authorid";
1020
        $params = array('workshopid' => $this->id, 'authorid' => $authorid);
1021
        return $DB->get_record_sql($sql, $params);
1022
    }
1023
 
1024
    /**
1025
     * Returns published submissions with their authors data
1026
     *
1027
     * @return array of stdclass
1028
     */
1029
    public function get_published_submissions($orderby='finalgrade DESC') {
1030
        global $DB;
1031
 
1032
        $userfieldsapi = \core_user\fields::for_userpic();
1033
        $authorfields = $userfieldsapi->get_sql('u', false, 'author', 'authoridx', false)->selects;
1034
        $sql = "SELECT s.id, s.authorid, s.timecreated, s.timemodified,
1035
                       s.title, s.grade, s.gradeover, COALESCE(s.gradeover,s.grade) AS finalgrade,
1036
                       $authorfields
1037
                  FROM {workshop_submissions} s
1038
            INNER JOIN {user} u ON (s.authorid = u.id)
1039
                 WHERE s.example = 0 AND s.workshopid = :workshopid AND s.published = 1
1040
              ORDER BY $orderby";
1041
        $params = array('workshopid' => $this->id);
1042
        return $DB->get_records_sql($sql, $params);
1043
    }
1044
 
1045
    /**
1046
     * Returns full record of the given example submission
1047
     *
1048
     * @param int $id example submission od
1049
     * @return object
1050
     */
1051
    public function get_example_by_id($id) {
1052
        global $DB;
1053
        return $DB->get_record('workshop_submissions',
1054
                array('id' => $id, 'workshopid' => $this->id, 'example' => 1), '*', MUST_EXIST);
1055
    }
1056
 
1057
    /**
1058
     * Returns the list of example submissions in this workshop with reference assessments attached
1059
     *
1060
     * @return array of objects or an empty array
1061
     * @see workshop::prepare_example_summary()
1062
     */
1063
    public function get_examples_for_manager() {
1064
        global $DB;
1065
 
1066
        $sql = 'SELECT s.id, s.title,
1067
                       a.id AS assessmentid, a.grade, a.gradinggrade
1068
                  FROM {workshop_submissions} s
1069
             LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id AND a.weight = 1)
1070
                 WHERE s.example = 1 AND s.workshopid = :workshopid
1071
              ORDER BY s.title';
1072
        return $DB->get_records_sql($sql, array('workshopid' => $this->id));
1073
    }
1074
 
1075
    /**
1076
     * Returns the list of all example submissions in this workshop with the information of assessments done by the given user
1077
     *
1078
     * @param int $reviewerid user id
1079
     * @return array of objects, indexed by example submission id
1080
     * @see workshop::prepare_example_summary()
1081
     */
1082
    public function get_examples_for_reviewer($reviewerid) {
1083
        global $DB;
1084
 
1085
        if (empty($reviewerid)) {
1086
            return false;
1087
        }
1088
        $sql = 'SELECT s.id, s.title,
1089
                       a.id AS assessmentid, a.grade, a.gradinggrade
1090
                  FROM {workshop_submissions} s
1091
             LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id AND a.reviewerid = :reviewerid AND a.weight = 0)
1092
                 WHERE s.example = 1 AND s.workshopid = :workshopid
1093
              ORDER BY s.title';
1094
        return $DB->get_records_sql($sql, array('workshopid' => $this->id, 'reviewerid' => $reviewerid));
1095
    }
1096
 
1097
    /**
1098
     * Prepares renderable submission component
1099
     *
1100
     * @param stdClass $record required by {@see workshop_submission}
1101
     * @param bool $showauthor show the author-related information
1102
     * @return workshop_submission
1103
     */
1104
    public function prepare_submission(stdClass $record, $showauthor = false) {
1105
 
1106
        $submission         = new workshop_submission($this, $record, $showauthor);
1107
        $submission->url    = $this->submission_url($record->id);
1108
 
1109
        return $submission;
1110
    }
1111
 
1112
    /**
1113
     * Prepares renderable submission summary component
1114
     *
1115
     * @param stdClass $record required by {@see workshop_submission_summary}
1116
     * @param bool $showauthor show the author-related information
1117
     * @return workshop_submission_summary
1118
     */
1119
    public function prepare_submission_summary(stdClass $record, $showauthor = false) {
1120
 
1121
        $summary        = new workshop_submission_summary($this, $record, $showauthor);
1122
        $summary->url   = $this->submission_url($record->id);
1123
 
1124
        return $summary;
1125
    }
1126
 
1127
    /**
1128
     * Prepares renderable example submission component
1129
     *
1130
     * @param stdClass $record required by {@see workshop_example_submission}
1131
     * @return workshop_example_submission
1132
     */
1133
    public function prepare_example_submission(stdClass $record) {
1134
 
1135
        $example = new workshop_example_submission($this, $record);
1136
 
1137
        return $example;
1138
    }
1139
 
1140
    /**
1141
     * Prepares renderable example submission summary component
1142
     *
1143
     * If the example is editable, the caller must set the 'editable' flag explicitly.
1144
     *
1145
     * @param stdClass $example as returned by {@link workshop::get_examples_for_manager()} or {@link workshop::get_examples_for_reviewer()}
1146
     * @return workshop_example_submission_summary to be rendered
1147
     */
1148
    public function prepare_example_summary(stdClass $example) {
1149
 
1150
        $summary = new workshop_example_submission_summary($this, $example);
1151
 
1152
        if (is_null($example->grade)) {
1153
            $summary->status = 'notgraded';
1154
            $summary->assesslabel = get_string('assess', 'workshop');
1155
        } else {
1156
            $summary->status = 'graded';
1157
            $summary->assesslabel = get_string('reassess', 'workshop');
1158
        }
1159
 
1160
        $summary->gradeinfo           = new stdclass();
1161
        $summary->gradeinfo->received = $this->real_grade($example->grade);
1162
        $summary->gradeinfo->max      = $this->real_grade(100);
1163
 
1164
        $summary->url       = new moodle_url($this->exsubmission_url($example->id));
1165
        $summary->editurl   = new moodle_url($this->exsubmission_url($example->id), array('edit' => 'on'));
1166
        $summary->assessurl = new moodle_url($this->exsubmission_url($example->id), array('assess' => 'on', 'sesskey' => sesskey()));
1167
 
1168
        return $summary;
1169
    }
1170
 
1171
    /**
1172
     * Prepares renderable assessment component
1173
     *
1174
     * The $options array supports the following keys:
1175
     * showauthor - should the author user info be available for the renderer
1176
     * showreviewer - should the reviewer user info be available for the renderer
1177
     * showform - show the assessment form if it is available
1178
     * showweight - should the assessment weight be available for the renderer
1179
     *
1180
     * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()}
1181
     * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()}
1182
     * @param array $options
1183
     * @return workshop_assessment
1184
     */
1185
    public function prepare_assessment(stdClass $record, $form, array $options = array()) {
1186
 
1187
        $assessment             = new workshop_assessment($this, $record, $options);
1188
        $assessment->url        = $this->assess_url($record->id);
1189
        $assessment->maxgrade   = $this->real_grade(100);
1190
 
1191
        if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) {
1192
            debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER);
1193
        }
1194
 
1195
        if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) {
1196
            $assessment->form = $form;
1197
        }
1198
 
1199
        if (empty($options['showweight'])) {
1200
            $assessment->weight = null;
1201
        }
1202
 
1203
        if (!is_null($record->grade)) {
1204
            $assessment->realgrade = $this->real_grade($record->grade);
1205
        }
1206
 
1207
        return $assessment;
1208
    }
1209
 
1210
    /**
1211
     * Prepares renderable example submission's assessment component
1212
     *
1213
     * The $options array supports the following keys:
1214
     * showauthor - should the author user info be available for the renderer
1215
     * showreviewer - should the reviewer user info be available for the renderer
1216
     * showform - show the assessment form if it is available
1217
     *
1218
     * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()}
1219
     * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()}
1220
     * @param array $options
1221
     * @return workshop_example_assessment
1222
     */
1223
    public function prepare_example_assessment(stdClass $record, $form = null, array $options = array()) {
1224
 
1225
        $assessment             = new workshop_example_assessment($this, $record, $options);
1226
        $assessment->url        = $this->exassess_url($record->id);
1227
        $assessment->maxgrade   = $this->real_grade(100);
1228
 
1229
        if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) {
1230
            debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER);
1231
        }
1232
 
1233
        if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) {
1234
            $assessment->form = $form;
1235
        }
1236
 
1237
        if (!is_null($record->grade)) {
1238
            $assessment->realgrade = $this->real_grade($record->grade);
1239
        }
1240
 
1241
        $assessment->weight = null;
1242
 
1243
        return $assessment;
1244
    }
1245
 
1246
    /**
1247
     * Prepares renderable example submission's reference assessment component
1248
     *
1249
     * The $options array supports the following keys:
1250
     * showauthor - should the author user info be available for the renderer
1251
     * showreviewer - should the reviewer user info be available for the renderer
1252
     * showform - show the assessment form if it is available
1253
     *
1254
     * @param stdClass $record as returned by eg {@link self::get_assessment_by_id()}
1255
     * @param workshop_assessment_form|null $form as returned by {@link workshop_strategy::get_assessment_form()}
1256
     * @param array $options
1257
     * @return workshop_example_reference_assessment
1258
     */
1259
    public function prepare_example_reference_assessment(stdClass $record, $form = null, array $options = array()) {
1260
 
1261
        $assessment             = new workshop_example_reference_assessment($this, $record, $options);
1262
        $assessment->maxgrade   = $this->real_grade(100);
1263
 
1264
        if (!empty($options['showform']) and !($form instanceof workshop_assessment_form)) {
1265
            debugging('Not a valid instance of workshop_assessment_form supplied', DEBUG_DEVELOPER);
1266
        }
1267
 
1268
        if (!empty($options['showform']) and ($form instanceof workshop_assessment_form)) {
1269
            $assessment->form = $form;
1270
        }
1271
 
1272
        if (!is_null($record->grade)) {
1273
            $assessment->realgrade = $this->real_grade($record->grade);
1274
        }
1275
 
1276
        $assessment->weight = null;
1277
 
1278
        return $assessment;
1279
    }
1280
 
1281
    /**
1282
     * Removes the submission and all relevant data
1283
     *
1284
     * @param stdClass $submission record to delete
1285
     * @return void
1286
     */
1287
    public function delete_submission(stdclass $submission) {
1288
        global $DB;
1289
 
1290
        $assessments = $DB->get_records('workshop_assessments', array('submissionid' => $submission->id), '', 'id');
1291
        $this->delete_assessment(array_keys($assessments));
1292
 
1293
        $fs = get_file_storage();
1294
        $fs->delete_area_files($this->context->id, 'mod_workshop', 'submission_content', $submission->id);
1295
        $fs->delete_area_files($this->context->id, 'mod_workshop', 'submission_attachment', $submission->id);
1296
 
1297
        $DB->delete_records('workshop_submissions', array('id' => $submission->id));
1298
 
1299
        // Event information.
1300
        $params = array(
1301
            'context' => $this->context,
1302
            'courseid' => $this->course->id,
1303
            'relateduserid' => $submission->authorid,
1304
            'other' => array(
1305
                'submissiontitle' => $submission->title
1306
            )
1307
        );
1308
        $params['objectid'] = $submission->id;
1309
        $event = \mod_workshop\event\submission_deleted::create($params);
1310
        $event->add_record_snapshot('workshop', $this->dbrecord);
1311
        $event->trigger();
1312
    }
1313
 
1314
    /**
1315
     * Returns the list of all assessments in the workshop with some data added
1316
     *
1317
     * Fetches data from {workshop_assessments} and adds some useful information from other
1318
     * tables. The returned object does not contain textual fields (i.e. comments) to prevent memory
1319
     * lack issues.
1320
     *
1321
     * @return array [assessmentid] => assessment stdclass
1322
     */
1323
    public function get_all_assessments() {
1324
        global $DB;
1325
 
1326
        $userfieldsapi = \core_user\fields::for_userpic();
1327
        $reviewerfields = $userfieldsapi->get_sql('reviewer', false, '', 'revieweridx', false)->selects;
1328
        $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects;
1329
        $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects;
1330
        list($sort, $params) = users_order_by_sql('reviewer');
1331
        $sql = "SELECT a.id, a.submissionid, a.reviewerid, a.timecreated, a.timemodified,
1332
                       a.grade, a.gradinggrade, a.gradinggradeover, a.gradinggradeoverby,
1333
                       $reviewerfields, $authorfields, $overbyfields,
1334
                       s.title
1335
                  FROM {workshop_assessments} a
1336
            INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
1337
            INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1338
            INNER JOIN {user} author ON (s.authorid = author.id)
1339
             LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id)
1340
                 WHERE s.workshopid = :workshopid AND s.example = 0
1341
              ORDER BY $sort";
1342
        $params['workshopid'] = $this->id;
1343
 
1344
        return $DB->get_records_sql($sql, $params);
1345
    }
1346
 
1347
    /**
1348
     * Get the complete information about the given assessment
1349
     *
1350
     * @param int $id Assessment ID
1351
     * @return stdclass
1352
     */
1353
    public function get_assessment_by_id($id) {
1354
        global $DB;
1355
 
1356
        $userfieldsapi = \core_user\fields::for_userpic();
1357
        $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects;
1358
        $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects;
1359
        $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects;
1360
        $sql = "SELECT a.*, s.title, $reviewerfields, $authorfields, $overbyfields
1361
                  FROM {workshop_assessments} a
1362
            INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
1363
            INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1364
            INNER JOIN {user} author ON (s.authorid = author.id)
1365
             LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id)
1366
                 WHERE a.id = :id AND s.workshopid = :workshopid";
1367
        $params = array('id' => $id, 'workshopid' => $this->id);
1368
 
1369
        return $DB->get_record_sql($sql, $params, MUST_EXIST);
1370
    }
1371
 
1372
    /**
1373
     * Get the complete information about the user's assessment of the given submission
1374
     *
1375
     * @param int $sid submission ID
1376
     * @param int $uid user ID of the reviewer
1377
     * @return false|stdclass false if not found, stdclass otherwise
1378
     */
1379
    public function get_assessment_of_submission_by_user($submissionid, $reviewerid) {
1380
        global $DB;
1381
 
1382
        $userfieldsapi = \core_user\fields::for_userpic();
1383
        $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects;
1384
        $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects;
1385
        $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects;
1386
        $sql = "SELECT a.*, s.title, $reviewerfields, $authorfields, $overbyfields
1387
                  FROM {workshop_assessments} a
1388
            INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
1389
            INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0)
1390
            INNER JOIN {user} author ON (s.authorid = author.id)
1391
             LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id)
1392
                 WHERE s.id = :sid AND reviewer.id = :rid AND s.workshopid = :workshopid";
1393
        $params = array('sid' => $submissionid, 'rid' => $reviewerid, 'workshopid' => $this->id);
1394
 
1395
        return $DB->get_record_sql($sql, $params, IGNORE_MISSING);
1396
    }
1397
 
1398
    /**
1399
     * Get the complete information about all assessments of the given submission
1400
     *
1401
     * @param int $submissionid
1402
     * @return array
1403
     */
1404
    public function get_assessments_of_submission($submissionid) {
1405
        global $DB;
1406
 
1407
        $userfieldsapi = \core_user\fields::for_userpic();
1408
        $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects;
1409
        $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects;
1410
        list($sort, $params) = users_order_by_sql('reviewer');
1411
        $sql = "SELECT a.*, s.title, $reviewerfields, $overbyfields
1412
                  FROM {workshop_assessments} a
1413
            INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
1414
            INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1415
             LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id)
1416
                 WHERE s.example = 0 AND s.id = :submissionid AND s.workshopid = :workshopid
1417
              ORDER BY $sort";
1418
        $params['submissionid'] = $submissionid;
1419
        $params['workshopid']   = $this->id;
1420
 
1421
        return $DB->get_records_sql($sql, $params);
1422
    }
1423
 
1424
    /**
1425
     * Get the complete information about all assessments allocated to the given reviewer
1426
     *
1427
     * @param int $reviewerid
1428
     * @return array
1429
     */
1430
    public function get_assessments_by_reviewer($reviewerid) {
1431
        global $DB;
1432
 
1433
        $userfieldsapi = \core_user\fields::for_userpic();
1434
        $reviewerfields = $userfieldsapi->get_sql('reviewer', false, 'reviewer', 'revieweridx', false)->selects;
1435
        $authorfields = $userfieldsapi->get_sql('author', false, 'author', 'authorid', false)->selects;
1436
        $overbyfields = $userfieldsapi->get_sql('overby', false, 'overby', 'gradinggradeoverbyx', false)->selects;
1437
        $sql = "SELECT a.*, $reviewerfields, $authorfields, $overbyfields,
1438
                       s.id AS submissionid, s.title AS submissiontitle, s.timecreated AS submissioncreated,
1439
                       s.timemodified AS submissionmodified
1440
                  FROM {workshop_assessments} a
1441
            INNER JOIN {user} reviewer ON (a.reviewerid = reviewer.id)
1442
            INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
1443
            INNER JOIN {user} author ON (s.authorid = author.id)
1444
             LEFT JOIN {user} overby ON (a.gradinggradeoverby = overby.id)
1445
                 WHERE s.example = 0 AND reviewer.id = :reviewerid AND s.workshopid = :workshopid";
1446
        $params = array('reviewerid' => $reviewerid, 'workshopid' => $this->id);
1447
 
1448
        return $DB->get_records_sql($sql, $params);
1449
    }
1450
 
1451
    /**
1452
     * Get allocated assessments not graded yet by the given reviewer
1453
     *
1454
     * @see self::get_assessments_by_reviewer()
1455
     * @param int $reviewerid the reviewer id
1456
     * @param null|int|array $exclude optional assessment id (or list of them) to be excluded
1457
     * @return array
1458
     */
1459
    public function get_pending_assessments_by_reviewer($reviewerid, $exclude = null) {
1460
 
1461
        $assessments = $this->get_assessments_by_reviewer($reviewerid);
1462
 
1463
        foreach ($assessments as $id => $assessment) {
1464
            if (!is_null($assessment->grade)) {
1465
                unset($assessments[$id]);
1466
                continue;
1467
            }
1468
            if (!empty($exclude)) {
1469
                if (is_array($exclude) and in_array($id, $exclude)) {
1470
                    unset($assessments[$id]);
1471
                    continue;
1472
                } else if ($id == $exclude) {
1473
                    unset($assessments[$id]);
1474
                    continue;
1475
                }
1476
            }
1477
        }
1478
 
1479
        return $assessments;
1480
    }
1481
 
1482
    /**
1483
     * Allocate a submission to a user for review
1484
     *
1485
     * @param stdClass $submission Submission object with at least id property
1486
     * @param int $reviewerid User ID
1487
     * @param int $weight of the new assessment, from 0 to 16
1488
     * @param bool $bulk repeated inserts into DB expected
1489
     * @return int ID of the new assessment or an error code {@link self::ALLOCATION_EXISTS} if the allocation already exists
1490
     */
1491
    public function add_allocation(stdclass $submission, $reviewerid, $weight=1, $bulk=false) {
1492
        global $DB;
1493
 
1494
        if ($DB->record_exists('workshop_assessments', array('submissionid' => $submission->id, 'reviewerid' => $reviewerid))) {
1495
            return self::ALLOCATION_EXISTS;
1496
        }
1497
 
1498
        $weight = (int)$weight;
1499
        if ($weight < 0) {
1500
            $weight = 0;
1501
        }
1502
        if ($weight > 16) {
1503
            $weight = 16;
1504
        }
1505
 
1506
        $now = time();
1507
        $assessment = new stdclass();
1508
        $assessment->submissionid           = $submission->id;
1509
        $assessment->reviewerid             = $reviewerid;
1510
        $assessment->timecreated            = $now;         // do not set timemodified here
1511
        $assessment->weight                 = $weight;
1512
        $assessment->feedbackauthorformat   = editors_get_preferred_format();
1513
        $assessment->feedbackreviewerformat = editors_get_preferred_format();
1514
 
1515
        return $DB->insert_record('workshop_assessments', $assessment, true, $bulk);
1516
    }
1517
 
1518
    /**
1519
     * Delete assessment record or records.
1520
     *
1521
     * Removes associated records from the workshop_grades table, too.
1522
     *
1523
     * @param int|array $id assessment id or array of assessments ids
1524
     * @todo Give grading strategy plugins a chance to clean up their data, too.
1525
     * @return bool true
1526
     */
1527
    public function delete_assessment($id) {
1528
        global $DB;
1529
 
1530
        if (empty($id)) {
1531
            return true;
1532
        }
1533
 
1534
        $fs = get_file_storage();
1535
 
1536
        if (is_array($id)) {
1537
            $DB->delete_records_list('workshop_grades', 'assessmentid', $id);
1538
            foreach ($id as $itemid) {
1539
                $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_content', $itemid);
1540
                $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_attachment', $itemid);
1541
            }
1542
            $DB->delete_records_list('workshop_assessments', 'id', $id);
1543
 
1544
        } else {
1545
            $DB->delete_records('workshop_grades', array('assessmentid' => $id));
1546
            $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_content', $id);
1547
            $fs->delete_area_files($this->context->id, 'mod_workshop', 'overallfeedback_attachment', $id);
1548
            $DB->delete_records('workshop_assessments', array('id' => $id));
1549
        }
1550
 
1551
        return true;
1552
    }
1553
 
1554
    /**
1555
     * Returns instance of grading strategy class
1556
     *
1557
     * @return stdclass Instance of a grading strategy
1558
     */
1559
    public function grading_strategy_instance() {
1560
        global $CFG;    // because we require other libs here
1561
 
1562
        if (is_null($this->strategyinstance)) {
1563
            if (empty($this->strategy)) {
1564
                throw new coding_exception('Unknown grading strategy');
1565
            }
1566
            $strategylib = __DIR__ . '/form/' . $this->strategy . '/lib.php';
1567
            if (is_readable($strategylib)) {
1568
                require_once($strategylib);
1569
            } else {
1570
                throw new coding_exception('the grading forms subplugin must contain library ' . $strategylib);
1571
            }
1572
            $classname = 'workshop_' . $this->strategy . '_strategy';
1573
            $this->strategyinstance = new $classname($this);
1574
            if (!in_array('workshop_strategy', class_implements($this->strategyinstance))) {
1575
                throw new coding_exception($classname . ' does not implement workshop_strategy interface');
1576
            }
1577
        }
1578
        return $this->strategyinstance;
1579
    }
1580
 
1581
    /**
1582
     * Sets the current evaluation method to the given plugin.
1583
     *
1584
     * @param string $method the name of the workshopeval subplugin
1585
     * @return bool true if successfully set
1586
     * @throws coding_exception if attempting to set a non-installed evaluation method
1587
     */
1588
    public function set_grading_evaluation_method($method) {
1589
        global $DB;
1590
 
1591
        $method = clean_param($method, PARAM_PLUGIN);
1592
        $evaluationlib = __DIR__ . '/eval/' . $method . '/lib.php';
1593
 
1594
        if (is_readable($evaluationlib)) {
1595
            $this->evaluationinstance = null;
1596
            $this->evaluation = $method;
1597
            $DB->set_field('workshop', 'evaluation', $method, array('id' => $this->id));
1598
            return true;
1599
        }
1600
 
1601
        throw new coding_exception('Attempt to set a non-existing evaluation method.');
1602
    }
1603
 
1604
    /**
1605
     * Returns instance of grading evaluation class
1606
     *
1607
     * @return stdclass Instance of a grading evaluation
1608
     */
1609
    public function grading_evaluation_instance() {
1610
        global $CFG;    // because we require other libs here
1611
 
1612
        if (is_null($this->evaluationinstance)) {
1613
            if (empty($this->evaluation)) {
1614
                $this->evaluation = 'best';
1615
            }
1616
            $evaluationlib = __DIR__ . '/eval/' . $this->evaluation . '/lib.php';
1617
            if (is_readable($evaluationlib)) {
1618
                require_once($evaluationlib);
1619
            } else {
1620
                // Fall back in case the subplugin is not available.
1621
                $this->evaluation = 'best';
1622
                $evaluationlib = __DIR__ . '/eval/' . $this->evaluation . '/lib.php';
1623
                if (is_readable($evaluationlib)) {
1624
                    require_once($evaluationlib);
1625
                } else {
1626
                    // Fall back in case the subplugin is not available any more.
1627
                    throw new coding_exception('Missing default grading evaluation library ' . $evaluationlib);
1628
                }
1629
            }
1630
            $classname = 'workshop_' . $this->evaluation . '_evaluation';
1631
            $this->evaluationinstance = new $classname($this);
1632
            if (!in_array('workshop_evaluation', class_parents($this->evaluationinstance))) {
1633
                throw new coding_exception($classname . ' does not extend workshop_evaluation class');
1634
            }
1635
        }
1636
        return $this->evaluationinstance;
1637
    }
1638
 
1639
    /**
1640
     * Returns instance of submissions allocator
1641
     *
1642
     * @param string $method The name of the allocation method, must be PARAM_ALPHA
1643
     * @return stdclass Instance of submissions allocator
1644
     */
1645
    public function allocator_instance($method) {
1646
        global $CFG;    // because we require other libs here
1647
 
1648
        $allocationlib = __DIR__ . '/allocation/' . $method . '/lib.php';
1649
        if (is_readable($allocationlib)) {
1650
            require_once($allocationlib);
1651
        } else {
1652
            throw new coding_exception('Unable to find the allocation library ' . $allocationlib);
1653
        }
1654
        $classname = 'workshop_' . $method . '_allocator';
1655
        return new $classname($this);
1656
    }
1657
 
1658
    /**
1659
     * @return moodle_url of this workshop's view page
1660
     */
1661
    public function view_url() {
1662
        global $CFG;
1663
        return new moodle_url('/mod/workshop/view.php', array('id' => $this->cm->id));
1664
    }
1665
 
1666
    /**
1667
     * @return moodle_url of the page for editing this workshop's grading form
1668
     */
1669
    public function editform_url() {
1670
        global $CFG;
1671
        return new moodle_url('/mod/workshop/editform.php', array('cmid' => $this->cm->id));
1672
    }
1673
 
1674
    /**
1675
     * @return moodle_url of the page for previewing this workshop's grading form
1676
     */
1677
    public function previewform_url() {
1678
        global $CFG;
1679
        return new moodle_url('/mod/workshop/editformpreview.php', array('cmid' => $this->cm->id));
1680
    }
1681
 
1682
    /**
1683
     * @param int $assessmentid The ID of assessment record
1684
     * @return moodle_url of the assessment page
1685
     */
1686
    public function assess_url($assessmentid) {
1687
        global $CFG;
1688
        $assessmentid = clean_param($assessmentid, PARAM_INT);
1689
        return new moodle_url('/mod/workshop/assessment.php', array('asid' => $assessmentid));
1690
    }
1691
 
1692
    /**
1693
     * @param int $assessmentid The ID of assessment record
1694
     * @return moodle_url of the example assessment page
1695
     */
1696
    public function exassess_url($assessmentid) {
1697
        global $CFG;
1698
        $assessmentid = clean_param($assessmentid, PARAM_INT);
1699
        return new moodle_url('/mod/workshop/exassessment.php', array('asid' => $assessmentid));
1700
    }
1701
 
1702
    /**
1703
     * @return moodle_url of the page to view a submission, defaults to the own one
1704
     */
1705
    public function submission_url($id=null) {
1706
        global $CFG;
1707
        return new moodle_url('/mod/workshop/submission.php', array('cmid' => $this->cm->id, 'id' => $id));
1708
    }
1709
 
1710
    /**
1711
     * @param int $id example submission id
1712
     * @return moodle_url of the page to view an example submission
1713
     */
1714
    public function exsubmission_url($id) {
1715
        global $CFG;
1716
        return new moodle_url('/mod/workshop/exsubmission.php', array('cmid' => $this->cm->id, 'id' => $id));
1717
    }
1718
 
1719
    /**
1720
     * @param int $sid submission id
1721
     * @param array $aid of int assessment ids
1722
     * @return moodle_url of the page to compare assessments of the given submission
1723
     */
1724
    public function compare_url($sid, array $aids) {
1725
        global $CFG;
1726
 
1727
        $url = new moodle_url('/mod/workshop/compare.php', array('cmid' => $this->cm->id, 'sid' => $sid));
1728
        $i = 0;
1729
        foreach ($aids as $aid) {
1730
            $url->param("aid{$i}", $aid);
1731
            $i++;
1732
        }
1733
        return $url;
1734
    }
1735
 
1736
    /**
1737
     * @param int $sid submission id
1738
     * @param int $aid assessment id
1739
     * @return moodle_url of the page to compare the reference assessments of the given example submission
1740
     */
1741
    public function excompare_url($sid, $aid) {
1742
        global $CFG;
1743
        return new moodle_url('/mod/workshop/excompare.php', array('cmid' => $this->cm->id, 'sid' => $sid, 'aid' => $aid));
1744
    }
1745
 
1746
    /**
1747
     * @return moodle_url of the mod_edit form
1748
     */
1749
    public function updatemod_url() {
1750
        global $CFG;
1751
        return new moodle_url('/course/modedit.php', array('update' => $this->cm->id, 'return' => 1));
1752
    }
1753
 
1754
    /**
1755
     * @param string $method allocation method
1756
     * @return moodle_url to the allocation page
1757
     */
1758
    public function allocation_url($method=null) {
1759
        global $CFG;
1760
        $params = array('cmid' => $this->cm->id);
1761
        if (!empty($method)) {
1762
            $params['method'] = $method;
1763
        }
1764
        return new moodle_url('/mod/workshop/allocation.php', $params);
1765
    }
1766
 
1767
    /**
1768
     * @param int $phasecode The internal phase code
1769
     * @return moodle_url of the script to change the current phase to $phasecode
1770
     */
1771
    public function switchphase_url($phasecode) {
1772
        global $CFG;
1773
        $phasecode = clean_param($phasecode, PARAM_INT);
1774
        return new moodle_url('/mod/workshop/switchphase.php', array('cmid' => $this->cm->id, 'phase' => $phasecode));
1775
    }
1776
 
1777
    /**
1778
     * @return moodle_url to the aggregation page
1779
     */
1780
    public function aggregate_url() {
1781
        global $CFG;
1782
        return new moodle_url('/mod/workshop/aggregate.php', array('cmid' => $this->cm->id));
1783
    }
1784
 
1785
    /**
1786
     * @return moodle_url of this workshop's toolbox page
1787
     */
1788
    public function toolbox_url($tool) {
1789
        global $CFG;
1790
        return new moodle_url('/mod/workshop/toolbox.php', array('id' => $this->cm->id, 'tool' => $tool));
1791
    }
1792
 
1793
    /**
1794
     * Workshop wrapper around {@see add_to_log()}
1795
     * @deprecated since 2.7 Please use the provided event classes for logging actions.
1796
     *
1797
     * @param string $action to be logged
1798
     * @param moodle_url $url absolute url as returned by {@see workshop::submission_url()} and friends
1799
     * @param mixed $info additional info, usually id in a table
1800
     * @param bool $return true to return the arguments for add_to_log.
1801
     * @return void|array array of arguments for add_to_log if $return is true
1802
     */
1803
    public function log($action, moodle_url $url = null, $info = null, $return = false) {
1804
        debugging('The log method is now deprecated, please use event classes instead', DEBUG_DEVELOPER);
1805
 
1806
        if (is_null($url)) {
1807
            $url = $this->view_url();
1808
        }
1809
 
1810
        if (is_null($info)) {
1811
            $info = $this->id;
1812
        }
1813
 
1814
        $logurl = $this->log_convert_url($url);
1815
        $args = array($this->course->id, 'workshop', $action, $logurl, $info, $this->cm->id);
1816
        if ($return) {
1817
            return $args;
1818
        }
1819
        call_user_func_array('add_to_log', $args);
1820
    }
1821
 
1822
    /**
1823
     * Is the given user allowed to create their submission?
1824
     *
1825
     * @param int $userid
1826
     * @return bool
1827
     */
1828
    public function creating_submission_allowed($userid) {
1829
 
1830
        $now = time();
1831
        $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid);
1832
 
1833
        if ($this->latesubmissions) {
1834
            if ($this->phase != self::PHASE_SUBMISSION and $this->phase != self::PHASE_ASSESSMENT) {
1835
                // late submissions are allowed in the submission and assessment phase only
1836
                return false;
1837
            }
1838
            if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) {
1839
                // late submissions are not allowed before the submission start
1840
                return false;
1841
            }
1842
            return true;
1843
 
1844
        } else {
1845
            if ($this->phase != self::PHASE_SUBMISSION) {
1846
                // submissions are allowed during the submission phase only
1847
                return false;
1848
            }
1849
            if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) {
1850
                // if enabled, submitting is not allowed before the date/time defined in the mod_form
1851
                return false;
1852
            }
1853
            if (!$ignoredeadlines and !empty($this->submissionend) and $now > $this->submissionend ) {
1854
                // if enabled, submitting is not allowed after the date/time defined in the mod_form unless late submission is allowed
1855
                return false;
1856
            }
1857
            return true;
1858
        }
1859
    }
1860
 
1861
    /**
1862
     * Is the given user allowed to modify their existing submission?
1863
     *
1864
     * @param int $userid
1865
     * @return bool
1866
     */
1867
    public function modifying_submission_allowed($userid) {
1868
 
1869
        $now = time();
1870
        $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid);
1871
 
1872
        if ($this->phase != self::PHASE_SUBMISSION) {
1873
            // submissions can be edited during the submission phase only
1874
            return false;
1875
        }
1876
        if (!$ignoredeadlines and !empty($this->submissionstart) and $this->submissionstart > $now) {
1877
            // if enabled, re-submitting is not allowed before the date/time defined in the mod_form
1878
            return false;
1879
        }
1880
        if (!$ignoredeadlines and !empty($this->submissionend) and $now > $this->submissionend) {
1881
            // if enabled, re-submitting is not allowed after the date/time defined in the mod_form even if late submission is allowed
1882
            return false;
1883
        }
1884
        return true;
1885
    }
1886
 
1887
    /**
1888
     * Is the given reviewer allowed to create/edit their assessments?
1889
     *
1890
     * @param int $userid
1891
     * @return bool
1892
     */
1893
    public function assessing_allowed($userid) {
1894
 
1895
        if ($this->phase != self::PHASE_ASSESSMENT) {
1896
            // assessing is allowed in the assessment phase only, unless the user is a teacher
1897
            // providing additional assessment during the evaluation phase
1898
            if ($this->phase != self::PHASE_EVALUATION or !has_capability('mod/workshop:overridegrades', $this->context, $userid)) {
1899
                return false;
1900
            }
1901
        }
1902
 
1903
        $now = time();
1904
        $ignoredeadlines = has_capability('mod/workshop:ignoredeadlines', $this->context, $userid);
1905
 
1906
        if (!$ignoredeadlines and !empty($this->assessmentstart) and $this->assessmentstart > $now) {
1907
            // if enabled, assessing is not allowed before the date/time defined in the mod_form
1908
            return false;
1909
        }
1910
        if (!$ignoredeadlines and !empty($this->assessmentend) and $now > $this->assessmentend) {
1911
            // if enabled, assessing is not allowed after the date/time defined in the mod_form
1912
            return false;
1913
        }
1914
        // here we go, assessing is allowed
1915
        return true;
1916
    }
1917
 
1918
    /**
1919
     * Are reviewers allowed to create/edit their assessments of the example submissions?
1920
     *
1921
     * Returns null if example submissions are not enabled in this workshop. Otherwise returns
1922
     * true or false. Note this does not check other conditions like the number of already
1923
     * assessed examples, examples mode etc.
1924
     *
1925
     * @return null|bool
1926
     */
1927
    public function assessing_examples_allowed() {
1928
        if (empty($this->useexamples)) {
1929
            return null;
1930
        }
1931
        if (self::EXAMPLES_VOLUNTARY == $this->examplesmode) {
1932
            return true;
1933
        }
1934
        if (self::EXAMPLES_BEFORE_SUBMISSION == $this->examplesmode and self::PHASE_SUBMISSION == $this->phase) {
1935
            return true;
1936
        }
1937
        if (self::EXAMPLES_BEFORE_ASSESSMENT == $this->examplesmode and self::PHASE_ASSESSMENT == $this->phase) {
1938
            return true;
1939
        }
1940
        return false;
1941
    }
1942
 
1943
    /**
1944
     * Are the peer-reviews available to the authors?
1945
     *
1946
     * @return bool
1947
     */
1948
    public function assessments_available() {
1949
        return $this->phase == self::PHASE_CLOSED;
1950
    }
1951
 
1952
    /**
1953
     * Switch to a new workshop phase
1954
     *
1955
     * Modifies the underlying database record. You should terminate the script shortly after calling this.
1956
     *
1957
     * @param int $newphase new phase code
1958
     * @return bool true if success, false otherwise
1959
     */
1960
    public function switch_phase($newphase) {
1961
        global $DB;
1962
 
1963
        $known = $this->available_phases_list();
1964
        if (!isset($known[$newphase])) {
1965
            return false;
1966
        }
1967
 
1968
        if (self::PHASE_CLOSED == $newphase) {
1969
            // push the grades into the gradebook
1970
            $workshop = new stdclass();
1971
            foreach ($this as $property => $value) {
1972
                $workshop->{$property} = $value;
1973
            }
1974
            $workshop->course     = $this->course->id;
1975
            $workshop->cmidnumber = $this->cm->id;
1976
            $workshop->modname    = 'workshop';
1977
            workshop_update_grades($workshop);
1978
        }
1979
 
1980
        $DB->set_field('workshop', 'phase', $newphase, array('id' => $this->id));
1981
        $this->phase = $newphase;
1982
        $eventdata = array(
1983
            'objectid' => $this->id,
1984
            'context' => $this->context,
1985
            'other' => array(
1986
                'workshopphase' => $this->phase
1987
            )
1988
        );
1989
        $event = \mod_workshop\event\phase_switched::create($eventdata);
1990
        $event->trigger();
1991
        return true;
1992
    }
1993
 
1994
    /**
1995
     * Saves a raw grade for submission as calculated from the assessment form fields
1996
     *
1997
     * @param array $assessmentid assessment record id, must exists
1998
     * @param mixed $grade        raw percentual grade from 0.00000 to 100.00000
1999
     * @return false|float        the saved grade
2000
     */
2001
    public function set_peer_grade($assessmentid, $grade) {
2002
        global $DB;
2003
 
2004
        if (is_null($grade)) {
2005
            return false;
2006
        }
2007
        $data = new stdclass();
2008
        $data->id = $assessmentid;
2009
        $data->grade = $grade;
2010
        $data->timemodified = time();
2011
        $DB->update_record('workshop_assessments', $data);
2012
        return $grade;
2013
    }
2014
 
2015
    /**
2016
     * Prepares data object with all workshop grades to be rendered
2017
     *
2018
     * @param int $userid the user we are preparing the report for
2019
     * @param int $groupid if non-zero, prepare the report for the given group only
2020
     * @param int $page the current page (for the pagination)
2021
     * @param int $perpage participants per page (for the pagination)
2022
     * @param string $sortby lastname|firstname|submissiontitle|submissiongrade|gradinggrade
2023
     * @param string $sorthow ASC|DESC
2024
     * @return stdclass data for the renderer
2025
     */
2026
    public function prepare_grading_report_data($userid, $groupid, $page, $perpage, $sortby, $sorthow) {
2027
        global $DB;
2028
 
2029
        $canviewall     = has_capability('mod/workshop:viewallassessments', $this->context, $userid);
2030
        $isparticipant  = $this->is_participant($userid);
2031
 
2032
        if (!$canviewall and !$isparticipant) {
2033
            // who the hell is this?
2034
            return array();
2035
        }
2036
 
2037
        if (!in_array($sortby, array('lastname', 'firstname', 'submissiontitle', 'submissionmodified',
2038
                'submissiongrade', 'gradinggrade'))) {
2039
            $sortby = 'lastname';
2040
        }
2041
 
2042
        if (!($sorthow === 'ASC' or $sorthow === 'DESC')) {
2043
            $sorthow = 'ASC';
2044
        }
2045
 
2046
        // get the list of user ids to be displayed
2047
        if ($canviewall) {
2048
            $participants = $this->get_participants(false, $groupid);
2049
        } else {
2050
            // this is an ordinary workshop participant (aka student) - display the report just for him/her
2051
            $participants = array($userid => (object)array('id' => $userid));
2052
        }
2053
 
2054
        // we will need to know the number of all records later for the pagination purposes
2055
        $numofparticipants = count($participants);
2056
 
2057
        if ($numofparticipants > 0) {
2058
            // load all fields which can be used for sorting and paginate the records
2059
            list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
2060
            $params['workshopid1'] = $this->id;
2061
            $params['workshopid2'] = $this->id;
2062
            $sqlsort = array();
2063
            $sqlsortfields = array($sortby => $sorthow) + array('lastname' => 'ASC', 'firstname' => 'ASC', 'u.id' => 'ASC');
2064
            foreach ($sqlsortfields as $sqlsortfieldname => $sqlsortfieldhow) {
2065
                $sqlsort[] = $sqlsortfieldname . ' ' . $sqlsortfieldhow;
2066
            }
2067
            $sqlsort = implode(',', $sqlsort);
2068
            $userfieldsapi = \core_user\fields::for_userpic();
2069
            $picturefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;
2070
            $sql = "SELECT $picturefields, s.title AS submissiontitle, s.timemodified AS submissionmodified,
2071
                           s.grade AS submissiongrade, ag.gradinggrade
2072
                      FROM {user} u
2073
                 LEFT JOIN {workshop_submissions} s ON (s.authorid = u.id AND s.workshopid = :workshopid1 AND s.example = 0)
2074
                 LEFT JOIN {workshop_aggregations} ag ON (ag.userid = u.id AND ag.workshopid = :workshopid2)
2075
                     WHERE u.id $participantids
2076
                  ORDER BY $sqlsort";
2077
            $participants = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
2078
        } else {
2079
            $participants = array();
2080
        }
2081
 
2082
        // this will hold the information needed to display user names and pictures
2083
        $userinfo = array();
2084
 
2085
        // get the user details for all participants to display
2086
        $additionalnames = \core_user\fields::get_name_fields();
2087
        foreach ($participants as $participant) {
2088
            if (!isset($userinfo[$participant->userid])) {
2089
                $userinfo[$participant->userid]            = new stdclass();
2090
                $userinfo[$participant->userid]->id        = $participant->userid;
2091
                $userinfo[$participant->userid]->picture   = $participant->picture;
2092
                $userinfo[$participant->userid]->imagealt  = $participant->imagealt;
2093
                $userinfo[$participant->userid]->email     = $participant->email;
2094
                foreach ($additionalnames as $addname) {
2095
                    $userinfo[$participant->userid]->$addname = $participant->$addname;
2096
                }
2097
            }
2098
        }
2099
 
2100
        // load the submissions details
2101
        $submissions = $this->get_submissions(array_keys($participants));
2102
 
2103
        // get the user details for all moderators (teachers) that have overridden a submission grade
2104
        foreach ($submissions as $submission) {
2105
            if (!isset($userinfo[$submission->gradeoverby])) {
2106
                $userinfo[$submission->gradeoverby]            = new stdclass();
2107
                $userinfo[$submission->gradeoverby]->id        = $submission->gradeoverby;
2108
                $userinfo[$submission->gradeoverby]->picture   = $submission->overpicture;
2109
                $userinfo[$submission->gradeoverby]->imagealt  = $submission->overimagealt;
2110
                $userinfo[$submission->gradeoverby]->email     = $submission->overemail;
2111
                foreach ($additionalnames as $addname) {
2112
                    $temp = 'over' . $addname;
2113
                    $userinfo[$submission->gradeoverby]->$addname = $submission->$temp;
2114
                }
2115
            }
2116
        }
2117
 
2118
        // get the user details for all reviewers of the displayed participants
2119
        $reviewers = array();
2120
 
2121
        if ($submissions) {
2122
            list($submissionids, $params) = $DB->get_in_or_equal(array_keys($submissions), SQL_PARAMS_NAMED);
2123
            list($sort, $sortparams) = users_order_by_sql('r');
2124
            $userfieldsapi = \core_user\fields::for_userpic();
2125
            $picturefields = $userfieldsapi->get_sql('r', false, '', 'reviewerid', false)->selects;
2126
            $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover, a.weight,
2127
                           $picturefields, s.id AS submissionid, s.authorid
2128
                      FROM {workshop_assessments} a
2129
                      JOIN {user} r ON (a.reviewerid = r.id)
2130
                      JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0)
2131
                     WHERE a.submissionid $submissionids
2132
                  ORDER BY a.weight DESC, $sort";
2133
            $reviewers = $DB->get_records_sql($sql, array_merge($params, $sortparams));
2134
            foreach ($reviewers as $reviewer) {
2135
                if (!isset($userinfo[$reviewer->reviewerid])) {
2136
                    $userinfo[$reviewer->reviewerid]            = new stdclass();
2137
                    $userinfo[$reviewer->reviewerid]->id        = $reviewer->reviewerid;
2138
                    $userinfo[$reviewer->reviewerid]->picture   = $reviewer->picture;
2139
                    $userinfo[$reviewer->reviewerid]->imagealt  = $reviewer->imagealt;
2140
                    $userinfo[$reviewer->reviewerid]->email     = $reviewer->email;
2141
                    foreach ($additionalnames as $addname) {
2142
                        $userinfo[$reviewer->reviewerid]->$addname = $reviewer->$addname;
2143
                    }
2144
                }
2145
            }
2146
        }
2147
 
2148
        // get the user details for all reviewees of the displayed participants
2149
        $reviewees = array();
2150
        if ($participants) {
2151
            list($participantids, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
2152
            list($sort, $sortparams) = users_order_by_sql('e');
2153
            $params['workshopid'] = $this->id;
2154
            $userfieldsapi = \core_user\fields::for_userpic();
2155
            $picturefields = $userfieldsapi->get_sql('e', false, '', 'authorid', false)->selects;
2156
            $sql = "SELECT a.id AS assessmentid, a.submissionid, a.grade, a.gradinggrade, a.gradinggradeover, a.reviewerid, a.weight,
2157
                           s.id AS submissionid, $picturefields
2158
                      FROM {user} u
2159
                      JOIN {workshop_assessments} a ON (a.reviewerid = u.id)
2160
                      JOIN {workshop_submissions} s ON (a.submissionid = s.id AND s.example = 0)
2161
                      JOIN {user} e ON (s.authorid = e.id)
2162
                     WHERE u.id $participantids AND s.workshopid = :workshopid
2163
                  ORDER BY a.weight DESC, $sort";
2164
            $reviewees = $DB->get_records_sql($sql, array_merge($params, $sortparams));
2165
            foreach ($reviewees as $reviewee) {
2166
                if (!isset($userinfo[$reviewee->authorid])) {
2167
                    $userinfo[$reviewee->authorid]            = new stdclass();
2168
                    $userinfo[$reviewee->authorid]->id        = $reviewee->authorid;
2169
                    $userinfo[$reviewee->authorid]->picture   = $reviewee->picture;
2170
                    $userinfo[$reviewee->authorid]->imagealt  = $reviewee->imagealt;
2171
                    $userinfo[$reviewee->authorid]->email     = $reviewee->email;
2172
                    foreach ($additionalnames as $addname) {
2173
                        $userinfo[$reviewee->authorid]->$addname = $reviewee->$addname;
2174
                    }
2175
                }
2176
            }
2177
        }
2178
 
2179
        // finally populate the object to be rendered
2180
        $grades = $participants;
2181
 
2182
        foreach ($participants as $participant) {
2183
            // set up default (null) values
2184
            $grades[$participant->userid]->submissionid = null;
2185
            $grades[$participant->userid]->submissiontitle = null;
2186
            $grades[$participant->userid]->submissiongrade = null;
2187
            $grades[$participant->userid]->submissiongradeover = null;
2188
            $grades[$participant->userid]->submissiongradeoverby = null;
2189
            $grades[$participant->userid]->submissionpublished = null;
2190
            $grades[$participant->userid]->reviewedby = array();
2191
            $grades[$participant->userid]->reviewerof = array();
2192
        }
2193
        unset($participants);
2194
        unset($participant);
2195
 
2196
        foreach ($submissions as $submission) {
2197
            $grades[$submission->authorid]->submissionid = $submission->id;
2198
            $grades[$submission->authorid]->submissiontitle = $submission->title;
2199
            $grades[$submission->authorid]->submissiongrade = $this->real_grade($submission->grade);
2200
            $grades[$submission->authorid]->submissiongradeover = $this->real_grade($submission->gradeover);
2201
            $grades[$submission->authorid]->submissiongradeoverby = $submission->gradeoverby;
2202
            $grades[$submission->authorid]->submissionpublished = $submission->published;
2203
        }
2204
        unset($submissions);
2205
        unset($submission);
2206
 
2207
        foreach($reviewers as $reviewer) {
2208
            $info = new stdclass();
2209
            $info->userid = $reviewer->reviewerid;
2210
            $info->assessmentid = $reviewer->assessmentid;
2211
            $info->submissionid = $reviewer->submissionid;
2212
            $info->grade = $this->real_grade($reviewer->grade);
2213
            $info->gradinggrade = $this->real_grading_grade($reviewer->gradinggrade);
2214
            $info->gradinggradeover = $this->real_grading_grade($reviewer->gradinggradeover);
2215
            $info->weight = $reviewer->weight;
2216
            $grades[$reviewer->authorid]->reviewedby[$reviewer->reviewerid] = $info;
2217
        }
2218
        unset($reviewers);
2219
        unset($reviewer);
2220
 
2221
        foreach($reviewees as $reviewee) {
2222
            $info = new stdclass();
2223
            $info->userid = $reviewee->authorid;
2224
            $info->assessmentid = $reviewee->assessmentid;
2225
            $info->submissionid = $reviewee->submissionid;
2226
            $info->grade = $this->real_grade($reviewee->grade);
2227
            $info->gradinggrade = $this->real_grading_grade($reviewee->gradinggrade);
2228
            $info->gradinggradeover = $this->real_grading_grade($reviewee->gradinggradeover);
2229
            $info->weight = $reviewee->weight;
2230
            $grades[$reviewee->reviewerid]->reviewerof[$reviewee->authorid] = $info;
2231
        }
2232
        unset($reviewees);
2233
        unset($reviewee);
2234
 
2235
        foreach ($grades as $grade) {
2236
            $grade->gradinggrade = $this->real_grading_grade($grade->gradinggrade);
2237
        }
2238
 
2239
        $data = new stdclass();
2240
        $data->grades = $grades;
2241
        $data->userinfo = $userinfo;
2242
        $data->totalcount = $numofparticipants;
2243
        $data->maxgrade = $this->real_grade(100);
2244
        $data->maxgradinggrade = $this->real_grading_grade(100);
2245
        return $data;
2246
    }
2247
 
2248
    /**
2249
     * Calculates the real value of a grade
2250
     *
2251
     * @param float $value percentual value from 0 to 100
2252
     * @param float $max   the maximal grade
2253
     * @return string
2254
     */
2255
    public function real_grade_value($value, $max) {
2256
        $localized = true;
2257
        if (is_null($value) or $value === '') {
2258
            return null;
2259
        } elseif ($max == 0) {
2260
            return 0;
2261
        } else {
2262
            return format_float($max * $value / 100, $this->gradedecimals, $localized);
2263
        }
2264
    }
2265
 
2266
    /**
2267
     * Calculates the raw (percentual) value from a real grade
2268
     *
2269
     * This is used in cases when a user wants to give a grade such as 12 of 20 and we need to save
2270
     * this value in a raw percentual form into DB
2271
     * @param float $value given grade
2272
     * @param float $max   the maximal grade
2273
     * @return float       suitable to be stored as numeric(10,5)
2274
     */
2275
    public function raw_grade_value($value, $max) {
2276
        if (is_null($value) or $value === '') {
2277
            return null;
2278
        }
2279
        if ($max == 0 or $value < 0) {
2280
            return 0;
2281
        }
2282
        $p = $value / $max * 100;
2283
        if ($p > 100) {
2284
            return $max;
2285
        }
2286
        return grade_floatval($p);
2287
    }
2288
 
2289
    /**
2290
     * Calculates the real value of grade for submission
2291
     *
2292
     * @param float $value percentual value from 0 to 100
2293
     * @return string
2294
     */
2295
    public function real_grade($value) {
2296
        return $this->real_grade_value($value, $this->grade);
2297
    }
2298
 
2299
    /**
2300
     * Calculates the real value of grade for assessment
2301
     *
2302
     * @param float $value percentual value from 0 to 100
2303
     * @return string
2304
     */
2305
    public function real_grading_grade($value) {
2306
        return $this->real_grade_value($value, $this->gradinggrade);
2307
    }
2308
 
2309
    /**
2310
     * Sets the given grades and received grading grades to null
2311
     *
2312
     * This does not clear the information about how the peers filled the assessment forms, but
2313
     * clears the calculated grades in workshop_assessments. Therefore reviewers have to re-assess
2314
     * the allocated submissions.
2315
     *
2316
     * @return void
2317
     */
2318
    public function clear_assessments() {
2319
        global $DB;
2320
 
2321
        $submissions = $this->get_submissions();
2322
        if (empty($submissions)) {
2323
            // no money, no love
2324
            return;
2325
        }
2326
        $submissions = array_keys($submissions);
2327
        list($sql, $params) = $DB->get_in_or_equal($submissions, SQL_PARAMS_NAMED);
2328
        $sql = "submissionid $sql";
2329
        $DB->set_field_select('workshop_assessments', 'grade', null, $sql, $params);
2330
        $DB->set_field_select('workshop_assessments', 'gradinggrade', null, $sql, $params);
2331
    }
2332
 
2333
    /**
2334
     * Sets the grades for submission to null
2335
     *
2336
     * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s)
2337
     * @return void
2338
     */
2339
    public function clear_submission_grades($restrict=null) {
2340
        global $DB;
2341
 
2342
        $sql = "workshopid = :workshopid AND example = 0";
2343
        $params = array('workshopid' => $this->id);
2344
 
2345
        if (is_null($restrict)) {
2346
            // update all users - no more conditions
2347
        } elseif (!empty($restrict)) {
2348
            list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
2349
            $sql .= " AND authorid $usql";
2350
            $params = array_merge($params, $uparams);
2351
        } else {
2352
            throw new coding_exception('Empty value is not a valid parameter here');
2353
        }
2354
 
2355
        $DB->set_field_select('workshop_submissions', 'grade', null, $sql, $params);
2356
    }
2357
 
2358
    /**
2359
     * Calculates grades for submission for the given participant(s) and updates it in the database
2360
     *
2361
     * @param null|int|array $restrict If null, update all authors, otherwise update just grades for the given author(s)
2362
     * @return void
2363
     */
2364
    public function aggregate_submission_grades($restrict=null) {
2365
        global $DB;
2366
 
2367
        // fetch a recordset with all assessments to process
2368
        $sql = 'SELECT s.id AS submissionid, s.grade AS submissiongrade,
2369
                       a.weight, a.grade
2370
                  FROM {workshop_submissions} s
2371
             LEFT JOIN {workshop_assessments} a ON (a.submissionid = s.id)
2372
                 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
2373
        $params = array('workshopid' => $this->id);
2374
 
2375
        if (is_null($restrict)) {
2376
            // update all users - no more conditions
2377
        } elseif (!empty($restrict)) {
2378
            list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
2379
            $sql .= " AND s.authorid $usql";
2380
            $params = array_merge($params, $uparams);
2381
        } else {
2382
            throw new coding_exception('Empty value is not a valid parameter here');
2383
        }
2384
 
2385
        $sql .= ' ORDER BY s.id'; // this is important for bulk processing
2386
 
2387
        $rs         = $DB->get_recordset_sql($sql, $params);
2388
        $batch      = array();    // will contain a set of all assessments of a single submission
2389
        $previous   = null;       // a previous record in the recordset
2390
 
2391
        foreach ($rs as $current) {
2392
            if (is_null($previous)) {
2393
                // we are processing the very first record in the recordset
2394
                $previous   = $current;
2395
            }
2396
            if ($current->submissionid == $previous->submissionid) {
2397
                // we are still processing the current submission
2398
                $batch[] = $current;
2399
            } else {
2400
                // process all the assessments of a sigle submission
2401
                $this->aggregate_submission_grades_process($batch);
2402
                // and then start to process another submission
2403
                $batch      = array($current);
2404
                $previous   = $current;
2405
            }
2406
        }
2407
        // do not forget to process the last batch!
2408
        $this->aggregate_submission_grades_process($batch);
2409
        $rs->close();
2410
    }
2411
 
2412
    /**
2413
     * Sets the aggregated grades for assessment to null
2414
     *
2415
     * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
2416
     * @return void
2417
     */
2418
    public function clear_grading_grades($restrict=null) {
2419
        global $DB;
2420
 
2421
        $sql = "workshopid = :workshopid";
2422
        $params = array('workshopid' => $this->id);
2423
 
2424
        if (is_null($restrict)) {
2425
            // update all users - no more conditions
2426
        } elseif (!empty($restrict)) {
2427
            list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
2428
            $sql .= " AND userid $usql";
2429
            $params = array_merge($params, $uparams);
2430
        } else {
2431
            throw new coding_exception('Empty value is not a valid parameter here');
2432
        }
2433
 
2434
        $DB->set_field_select('workshop_aggregations', 'gradinggrade', null, $sql, $params);
2435
    }
2436
 
2437
    /**
2438
     * Calculates grades for assessment for the given participant(s)
2439
     *
2440
     * Grade for assessment is calculated as a simple mean of all grading grades calculated by the grading evaluator.
2441
     * The assessment weight is not taken into account here.
2442
     *
2443
     * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewer(s)
2444
     * @return void
2445
     */
2446
    public function aggregate_grading_grades($restrict=null) {
2447
        global $DB;
2448
 
2449
        // fetch a recordset with all assessments to process
2450
        $sql = 'SELECT a.reviewerid, a.gradinggrade, a.gradinggradeover,
2451
                       ag.id AS aggregationid, ag.gradinggrade AS aggregatedgrade
2452
                  FROM {workshop_assessments} a
2453
            INNER JOIN {workshop_submissions} s ON (a.submissionid = s.id)
2454
             LEFT JOIN {workshop_aggregations} ag ON (ag.userid = a.reviewerid AND ag.workshopid = s.workshopid)
2455
                 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
2456
        $params = array('workshopid' => $this->id);
2457
 
2458
        if (is_null($restrict)) {
2459
            // update all users - no more conditions
2460
        } elseif (!empty($restrict)) {
2461
            list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
2462
            $sql .= " AND a.reviewerid $usql";
2463
            $params = array_merge($params, $uparams);
2464
        } else {
2465
            throw new coding_exception('Empty value is not a valid parameter here');
2466
        }
2467
 
2468
        $sql .= ' ORDER BY a.reviewerid'; // this is important for bulk processing
2469
 
2470
        $rs         = $DB->get_recordset_sql($sql, $params);
2471
        $batch      = array();    // will contain a set of all assessments of a single submission
2472
        $previous   = null;       // a previous record in the recordset
2473
 
2474
        foreach ($rs as $current) {
2475
            if (is_null($previous)) {
2476
                // we are processing the very first record in the recordset
2477
                $previous   = $current;
2478
            }
2479
            if ($current->reviewerid == $previous->reviewerid) {
2480
                // we are still processing the current reviewer
2481
                $batch[] = $current;
2482
            } else {
2483
                // process all the assessments of a sigle submission
2484
                $this->aggregate_grading_grades_process($batch);
2485
                // and then start to process another reviewer
2486
                $batch      = array($current);
2487
                $previous   = $current;
2488
            }
2489
        }
2490
        // do not forget to process the last batch!
2491
        $this->aggregate_grading_grades_process($batch);
2492
        $rs->close();
2493
    }
2494
 
2495
    /**
2496
     * Returns the mform the teachers use to put a feedback for the reviewer
2497
     *
2498
     * @param mixed moodle_url|null $actionurl
2499
     * @param stdClass $assessment
2500
     * @param array $options editable, editableweight, overridablegradinggrade
2501
     * @return workshop_feedbackreviewer_form
2502
     */
2503
    public function get_feedbackreviewer_form($actionurl, stdclass $assessment, $options=array()) {
2504
        global $CFG;
2505
        require_once(__DIR__ . '/feedbackreviewer_form.php');
2506
 
2507
        $current = new stdclass();
2508
        $current->asid                      = $assessment->id;
2509
        $current->weight                    = $assessment->weight;
2510
        $current->gradinggrade              = $this->real_grading_grade($assessment->gradinggrade);
2511
        $current->gradinggradeover          = $this->real_grading_grade($assessment->gradinggradeover);
2512
        $current->feedbackreviewer          = $assessment->feedbackreviewer;
2513
        $current->feedbackreviewerformat    = $assessment->feedbackreviewerformat;
2514
        if (is_null($current->gradinggrade)) {
2515
            $current->gradinggrade = get_string('nullgrade', 'workshop');
2516
        }
2517
        if (!isset($options['editable'])) {
2518
            $editable = true;   // by default
2519
        } else {
2520
            $editable = (bool)$options['editable'];
2521
        }
2522
 
2523
        // prepare wysiwyg editor
2524
        $current = file_prepare_standard_editor($current, 'feedbackreviewer', array());
2525
 
2526
        return new workshop_feedbackreviewer_form($actionurl,
2527
                array('workshop' => $this, 'current' => $current, 'editoropts' => array(), 'options' => $options),
2528
                'post', '', null, $editable);
2529
    }
2530
 
2531
    /**
2532
     * Returns the mform the teachers use to put a feedback for the author on their submission
2533
     *
2534
     * @mixed moodle_url|null $actionurl
2535
     * @param stdClass $submission
2536
     * @param array $options editable
2537
     * @return workshop_feedbackauthor_form
2538
     */
2539
    public function get_feedbackauthor_form($actionurl, stdclass $submission, $options=array()) {
2540
        global $CFG;
2541
        require_once(__DIR__ . '/feedbackauthor_form.php');
2542
 
2543
        $current = new stdclass();
2544
        $current->submissionid          = $submission->id;
2545
        $current->published             = $submission->published;
2546
        $current->grade                 = $this->real_grade($submission->grade);
2547
        $current->gradeover             = $this->real_grade($submission->gradeover);
2548
        $current->feedbackauthor        = $submission->feedbackauthor;
2549
        $current->feedbackauthorformat  = $submission->feedbackauthorformat;
2550
        if (is_null($current->grade)) {
2551
            $current->grade = get_string('nullgrade', 'workshop');
2552
        }
2553
        if (!isset($options['editable'])) {
2554
            $editable = true;   // by default
2555
        } else {
2556
            $editable = (bool)$options['editable'];
2557
        }
2558
 
2559
        // prepare wysiwyg editor
2560
        $current = file_prepare_standard_editor($current, 'feedbackauthor', array());
2561
 
2562
        return new workshop_feedbackauthor_form($actionurl,
2563
                array('workshop' => $this, 'current' => $current, 'editoropts' => array(), 'options' => $options),
2564
                'post', '', null, $editable);
2565
    }
2566
 
2567
    /**
2568
     * Returns the information about the user's grades as they are stored in the gradebook
2569
     *
2570
     * The submission grade is returned for users with the capability mod/workshop:submit and the
2571
     * assessment grade is returned for users with the capability mod/workshop:peerassess. Unless the
2572
     * user has the capability to view hidden grades, grades must be visible to be returned. Null
2573
     * grades are not returned. If none grade is to be returned, this method returns false.
2574
     *
2575
     * @param int $userid the user's id
2576
     * @return workshop_final_grades|false
2577
     */
2578
    public function get_gradebook_grades($userid) {
2579
        global $CFG;
2580
        require_once($CFG->libdir.'/gradelib.php');
2581
 
2582
        if (empty($userid)) {
2583
            throw new coding_exception('User id expected, empty value given.');
2584
        }
2585
 
2586
        // Read data via the Gradebook API
2587
        $gradebook = grade_get_grades($this->course->id, 'mod', 'workshop', $this->id, $userid);
2588
 
2589
        $grades = new workshop_final_grades();
2590
 
2591
        if (has_capability('mod/workshop:submit', $this->context, $userid)) {
2592
            if (!empty($gradebook->items[0]->grades)) {
2593
                $submissiongrade = reset($gradebook->items[0]->grades);
2594
                if (!is_null($submissiongrade->grade)) {
2595
                    if (!$submissiongrade->hidden or has_capability('moodle/grade:viewhidden', $this->context, $userid)) {
2596
                        $grades->submissiongrade = $submissiongrade;
2597
                    }
2598
                }
2599
            }
2600
        }
2601
 
2602
        if (has_capability('mod/workshop:peerassess', $this->context, $userid)) {
2603
            if (!empty($gradebook->items[1]->grades)) {
2604
                $assessmentgrade = reset($gradebook->items[1]->grades);
2605
                if (!is_null($assessmentgrade->grade)) {
2606
                    if (!$assessmentgrade->hidden or has_capability('moodle/grade:viewhidden', $this->context, $userid)) {
2607
                        $grades->assessmentgrade = $assessmentgrade;
2608
                    }
2609
                }
2610
            }
2611
        }
2612
 
2613
        if (!is_null($grades->submissiongrade) or !is_null($grades->assessmentgrade)) {
2614
            return $grades;
2615
        }
2616
 
2617
        return false;
2618
    }
2619
 
2620
    /**
2621
     * Return the editor options for the submission content field.
2622
     *
2623
     * @return array
2624
     */
2625
    public function submission_content_options() {
2626
        global $CFG;
2627
        require_once($CFG->dirroot.'/repository/lib.php');
2628
 
2629
        return array(
2630
            'trusttext' => true,
2631
            'subdirs' => false,
2632
            'maxfiles' => $this->nattachments,
2633
            'maxbytes' => $this->maxbytes,
2634
            'context' => $this->context,
2635
            'return_types' => FILE_INTERNAL | FILE_EXTERNAL,
2636
          );
2637
    }
2638
 
2639
    /**
2640
     * Return the filemanager options for the submission attachments field.
2641
     *
2642
     * @return array
2643
     */
2644
    public function submission_attachment_options() {
2645
        global $CFG;
2646
        require_once($CFG->dirroot.'/repository/lib.php');
2647
 
2648
        $options = array(
2649
            'subdirs' => true,
2650
            'maxfiles' => $this->nattachments,
2651
            'maxbytes' => $this->maxbytes,
2652
            'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK,
2653
        );
2654
 
2655
        $filetypesutil = new \core_form\filetypes_util();
2656
        $options['accepted_types'] = $filetypesutil->normalize_file_types($this->submissionfiletypes);
2657
 
2658
        return $options;
2659
    }
2660
 
2661
    /**
2662
     * Return the editor options for the overall feedback for the author.
2663
     *
2664
     * @return array
2665
     */
2666
    public function overall_feedback_content_options() {
2667
        global $CFG;
2668
        require_once($CFG->dirroot.'/repository/lib.php');
2669
 
2670
        return array(
2671
            'subdirs' => 0,
2672
            'maxbytes' => $this->overallfeedbackmaxbytes,
2673
            'maxfiles' => $this->overallfeedbackfiles,
2674
            'changeformat' => 1,
2675
            'context' => $this->context,
2676
            'return_types' => FILE_INTERNAL,
2677
        );
2678
    }
2679
 
2680
    /**
2681
     * Return the filemanager options for the overall feedback for the author.
2682
     *
2683
     * @return array
2684
     */
2685
    public function overall_feedback_attachment_options() {
2686
        global $CFG;
2687
        require_once($CFG->dirroot.'/repository/lib.php');
2688
 
2689
        $options = array(
2690
            'subdirs' => 1,
2691
            'maxbytes' => $this->overallfeedbackmaxbytes,
2692
            'maxfiles' => $this->overallfeedbackfiles,
2693
            'return_types' => FILE_INTERNAL | FILE_CONTROLLED_LINK,
2694
        );
2695
 
2696
        $filetypesutil = new \core_form\filetypes_util();
2697
        $options['accepted_types'] = $filetypesutil->normalize_file_types($this->overallfeedbackfiletypes);
2698
 
2699
        return $options;
2700
    }
2701
 
2702
    /**
2703
     * Performs the reset of this workshop instance.
2704
     *
2705
     * @param stdClass $data The actual course reset settings.
2706
     * @return array List of results, each being array[(string)component, (string)item, (string)error]
2707
     */
2708
    public function reset_userdata(stdClass $data) {
2709
 
2710
        $componentstr = get_string('pluginname', 'workshop').': '.format_string($this->name);
2711
        $status = array();
2712
 
2713
        if (!empty($data->reset_workshop_assessments) or !empty($data->reset_workshop_submissions)) {
2714
            // Reset all data related to assessments, including assessments of
2715
            // example submissions.
2716
            $result = $this->reset_userdata_assessments($data);
2717
            if ($result === true) {
2718
                $status[] = array(
2719
                    'component' => $componentstr,
2720
                    'item' => get_string('resetassessments', 'mod_workshop'),
2721
                    'error' => false,
2722
                );
2723
            } else {
2724
                $status[] = array(
2725
                    'component' => $componentstr,
2726
                    'item' => get_string('resetassessments', 'mod_workshop'),
2727
                    'error' => $result,
2728
                );
2729
            }
2730
        }
2731
 
2732
        if (!empty($data->reset_workshop_submissions)) {
2733
            // Reset all remaining data related to submissions.
2734
            $result = $this->reset_userdata_submissions($data);
2735
            if ($result === true) {
2736
                $status[] = array(
2737
                    'component' => $componentstr,
2738
                    'item' => get_string('resetsubmissions', 'mod_workshop'),
2739
                    'error' => false,
2740
                );
2741
            } else {
2742
                $status[] = array(
2743
                    'component' => $componentstr,
2744
                    'item' => get_string('resetsubmissions', 'mod_workshop'),
2745
                    'error' => $result,
2746
                );
2747
            }
2748
        }
2749
 
2750
        if (!empty($data->reset_workshop_phase)) {
2751
            // Do not use the {@link workshop::switch_phase()} here, we do not
2752
            // want to trigger events.
2753
            $this->reset_phase();
2754
            $status[] = array(
2755
                'component' => $componentstr,
2756
                'item' => get_string('resetsubmissions', 'mod_workshop'),
2757
                'error' => false,
2758
            );
2759
        }
2760
 
2761
        return $status;
2762
    }
2763
 
2764
    /**
2765
     * Check if the current user can access the other user's group.
2766
     *
2767
     * This is typically used for teacher roles that have permissions like
2768
     * 'view all submissions'. Even with such a permission granted, we have to
2769
     * check the workshop activity group mode.
2770
     *
2771
     * If the workshop is not in a group mode, or if it is in the visible group
2772
     * mode, this method returns true. This is consistent with how the
2773
     * {@link groups_get_activity_allowed_groups()} behaves.
2774
     *
2775
     * If the workshop is in a separate group mode, the current user has to
2776
     * have the 'access all groups' permission, or share at least one
2777
     * accessible group with the other user.
2778
     *
2779
     * @param int $otheruserid The ID of the other user, e.g. the author of a submission.
2780
     * @return bool False if the current user cannot access the other user's group.
2781
     */
2782
    public function check_group_membership($otheruserid) {
2783
        global $USER;
2784
 
2785
        if (groups_get_activity_groupmode($this->cm) != SEPARATEGROUPS) {
2786
            // The workshop is not in a group mode, or it is in a visible group mode.
2787
            return true;
2788
 
2789
        } else if (has_capability('moodle/site:accessallgroups', $this->context)) {
2790
            // The current user can access all groups.
2791
            return true;
2792
 
2793
        } else {
2794
            $thisusersgroups = groups_get_all_groups($this->course->id, $USER->id, $this->cm->groupingid, 'g.id');
2795
            $otherusersgroups = groups_get_all_groups($this->course->id, $otheruserid, $this->cm->groupingid, 'g.id');
2796
            $commongroups = array_intersect_key($thisusersgroups, $otherusersgroups);
2797
 
2798
            if (empty($commongroups)) {
2799
                // The current user has no group common with the other user.
2800
                return false;
2801
 
2802
            } else {
2803
                // The current user has a group common with the other user.
2804
                return true;
2805
            }
2806
        }
2807
    }
2808
 
2809
    /**
2810
     * Check whether the given user has assessed all his required examples before submission.
2811
     *
2812
     * @param  int $userid the user to check
2813
     * @return bool        false if there are examples missing assessment, true otherwise.
2814
     * @since  Moodle 3.4
2815
     */
2816
    public function check_examples_assessed_before_submission($userid) {
2817
 
2818
        if ($this->useexamples and $this->examplesmode == self::EXAMPLES_BEFORE_SUBMISSION
2819
            and !has_capability('mod/workshop:manageexamples', $this->context)) {
2820
 
2821
            // Check that all required examples have been assessed by the user.
2822
            $examples = $this->get_examples_for_reviewer($userid);
2823
            foreach ($examples as $exampleid => $example) {
2824
                if (is_null($example->assessmentid)) {
2825
                    $examples[$exampleid]->assessmentid = $this->add_allocation($example, $userid, 0);
2826
                }
2827
                if (is_null($example->grade)) {
2828
                    return false;
2829
                }
2830
            }
2831
        }
2832
        return true;
2833
    }
2834
 
2835
    /**
2836
     * Check that all required examples have been assessed by the given user.
2837
     *
2838
     * @param  stdClass $userid     the user (reviewer) to check
2839
     * @return mixed bool|state     false and notice code if there are examples missing assessment, true otherwise.
2840
     * @since  Moodle 3.4
2841
     */
2842
    public function check_examples_assessed_before_assessment($userid) {
2843
 
2844
        if ($this->useexamples and $this->examplesmode == self::EXAMPLES_BEFORE_ASSESSMENT
2845
                and !has_capability('mod/workshop:manageexamples', $this->context)) {
2846
 
2847
            // The reviewer must have submitted their own submission.
2848
            $reviewersubmission = $this->get_submission_by_author($userid);
2849
            if (!$reviewersubmission) {
2850
                // No money, no love.
2851
                return array(false, 'exampleneedsubmission');
2852
            } else {
2853
                $examples = $this->get_examples_for_reviewer($userid);
2854
                foreach ($examples as $exampleid => $example) {
2855
                    if (is_null($example->grade)) {
2856
                        return array(false, 'exampleneedassessed');
2857
                    }
2858
                }
2859
            }
2860
        }
2861
        return array(true, null);
2862
    }
2863
 
2864
    /**
2865
     * Trigger module viewed event and set the module viewed for completion.
2866
     *
2867
     * @since  Moodle 3.4
2868
     */
2869
    public function set_module_viewed() {
2870
        global $CFG;
2871
        require_once($CFG->libdir . '/completionlib.php');
2872
 
2873
        // Mark viewed.
2874
        $completion = new completion_info($this->course);
2875
        $completion->set_module_viewed($this->cm);
2876
 
2877
        $eventdata = array();
2878
        $eventdata['objectid'] = $this->id;
2879
        $eventdata['context'] = $this->context;
2880
 
2881
        // Trigger module viewed event.
2882
        $event = \mod_workshop\event\course_module_viewed::create($eventdata);
2883
        $event->add_record_snapshot('course', $this->course);
2884
        $event->add_record_snapshot('workshop', $this->dbrecord);
2885
        $event->add_record_snapshot('course_modules', $this->cm);
2886
        $event->trigger();
2887
    }
2888
 
2889
    /**
2890
     * Validates the submission form or WS data.
2891
     *
2892
     * @param  array $data the data to be validated
2893
     * @return array       the validation errors (if any)
2894
     * @since  Moodle 3.4
2895
     */
2896
    public function validate_submission_data($data) {
2897
        global $DB, $USER;
2898
 
2899
        $errors = array();
2900
        if (empty($data['id']) and empty($data['example'])) {
2901
            // Make sure there is no submission saved meanwhile from another browser window.
2902
            $sql = "SELECT COUNT(s.id)
2903
                      FROM {workshop_submissions} s
2904
                      JOIN {workshop} w ON (s.workshopid = w.id)
2905
                      JOIN {course_modules} cm ON (w.id = cm.instance)
2906
                      JOIN {modules} m ON (m.name = 'workshop' AND m.id = cm.module)
2907
                     WHERE cm.id = ? AND s.authorid = ? AND s.example = 0";
2908
 
2909
            if ($DB->count_records_sql($sql, array($data['cmid'], $USER->id))) {
2910
                $errors['title'] = get_string('err_multiplesubmissions', 'mod_workshop');
2911
            }
2912
        }
2913
        // Get the workshop record by id or cmid, depending on whether we're creating or editing a submission.
2914
        if (empty($data['workshopid'])) {
2915
            $workshop = $DB->get_record_select('workshop', 'id = (SELECT instance FROM {course_modules} WHERE id = ?)',
2916
                    [$data['cmid']]);
2917
        } else {
2918
            $workshop = $DB->get_record('workshop', ['id' => $data['workshopid']]);
2919
        }
2920
 
2921
        if (isset($data['attachment_filemanager'])) {
2922
            $getfiles = file_get_drafarea_files($data['attachment_filemanager']);
2923
            $attachments = $getfiles->list;
2924
        } else {
2925
            $attachments = array();
2926
        }
2927
 
2928
        if ($workshop->submissiontypefile == WORKSHOP_SUBMISSION_TYPE_REQUIRED) {
2929
            if (empty($attachments)) {
2930
                $errors['attachment_filemanager'] = get_string('err_required', 'form');
2931
            }
2932
        } else if ($workshop->submissiontypefile == WORKSHOP_SUBMISSION_TYPE_DISABLED && !empty($data['attachment_filemanager'])) {
2933
            $errors['attachment_filemanager'] = get_string('submissiontypedisabled', 'mod_workshop');
2934
        }
2935
 
2936
        if ($workshop->submissiontypetext == WORKSHOP_SUBMISSION_TYPE_REQUIRED && html_is_blank($data['content_editor']['text'])) {
2937
            $errors['content_editor'] = get_string('err_required', 'form');
2938
        } else if ($workshop->submissiontypetext == WORKSHOP_SUBMISSION_TYPE_DISABLED && !empty($data['content_editor']['text'])) {
2939
            $errors['content_editor'] = get_string('submissiontypedisabled', 'mod_workshop');
2940
        }
2941
 
2942
        // If neither type is explicitly required, one or the other must be submitted.
2943
        if ($workshop->submissiontypetext != WORKSHOP_SUBMISSION_TYPE_REQUIRED
2944
                && $workshop->submissiontypefile != WORKSHOP_SUBMISSION_TYPE_REQUIRED
2945
                && empty($attachments) && html_is_blank($data['content_editor']['text'])) {
2946
            $errors['content_editor'] = get_string('submissionrequiredcontent', 'mod_workshop');
2947
            $errors['attachment_filemanager'] = get_string('submissionrequiredfile', 'mod_workshop');
2948
        }
2949
 
2950
        return $errors;
2951
    }
2952
 
2953
    /**
2954
     * Adds or updates a submission.
2955
     *
2956
     * @param stdClass $submission The submissin data (via form or via WS).
2957
     * @return the new or updated submission id.
2958
     * @since  Moodle 3.4
2959
     */
2960
    public function edit_submission($submission) {
2961
        global $USER, $DB;
2962
 
2963
        if ($submission->example == 0) {
2964
            // This was used just for validation, it must be set to zero when dealing with normal submissions.
2965
            unset($submission->example);
2966
        } else {
2967
            throw new coding_exception('Invalid submission form data value: example');
2968
        }
2969
        $timenow = time();
2970
        if (is_null($submission->id)) {
2971
            $submission->workshopid     = $this->id;
2972
            $submission->example        = 0;
2973
            $submission->authorid       = $USER->id;
2974
            $submission->timecreated    = $timenow;
2975
            $submission->feedbackauthorformat = editors_get_preferred_format();
2976
        }
2977
        $submission->timemodified       = $timenow;
2978
        $submission->title              = trim($submission->title);
2979
        $submission->content            = '';          // Updated later.
2980
        $submission->contentformat      = FORMAT_HTML; // Updated later.
2981
        $submission->contenttrust       = 0;           // Updated later.
2982
        $submission->late               = 0x0;         // Bit mask.
2983
        if (!empty($this->submissionend) and ($this->submissionend < time())) {
2984
            $submission->late = $submission->late | 0x1;
2985
        }
2986
        if ($this->phase == self::PHASE_ASSESSMENT) {
2987
            $submission->late = $submission->late | 0x2;
2988
        }
2989
 
2990
        // Event information.
2991
        $params = array(
2992
            'context' => $this->context,
2993
            'courseid' => $this->course->id,
2994
            'other' => array(
2995
                'submissiontitle' => $submission->title
2996
            )
2997
        );
2998
        $logdata = null;
2999
        if (is_null($submission->id)) {
3000
            $submission->id = $DB->insert_record('workshop_submissions', $submission);
3001
            $params['objectid'] = $submission->id;
3002
            $event = \mod_workshop\event\submission_created::create($params);
3003
            $event->trigger();
3004
        } else {
3005
            if (empty($submission->id) or empty($submission->id) or ($submission->id != $submission->id)) {
3006
                throw new moodle_exception('err_submissionid', 'workshop');
3007
            }
3008
        }
3009
        $params['objectid'] = $submission->id;
3010
 
3011
        // Save and relink embedded images and save attachments.
3012
        if ($this->submissiontypetext != WORKSHOP_SUBMISSION_TYPE_DISABLED) {
3013
            $submission = file_postupdate_standard_editor($submission, 'content', $this->submission_content_options(),
3014
                    $this->context, 'mod_workshop', 'submission_content', $submission->id);
3015
        }
3016
 
3017
        $submission = file_postupdate_standard_filemanager($submission, 'attachment', $this->submission_attachment_options(),
3018
            $this->context, 'mod_workshop', 'submission_attachment', $submission->id);
3019
 
3020
        if (empty($submission->attachment)) {
3021
            // Explicit cast to zero integer.
3022
            $submission->attachment = 0;
3023
        }
3024
        // Store the updated values or re-save the new submission (re-saving needed because URLs are now rewritten).
3025
        $DB->update_record('workshop_submissions', $submission);
3026
        $event = \mod_workshop\event\submission_updated::create($params);
3027
        $event->add_record_snapshot('workshop', $this->dbrecord);
3028
        $event->trigger();
3029
 
3030
        // Send submitted content for plagiarism detection.
3031
        $fs = get_file_storage();
3032
        $files = $fs->get_area_files($this->context->id, 'mod_workshop', 'submission_attachment', $submission->id);
3033
 
3034
        $params['other']['content'] = $submission->content;
3035
        $params['other']['pathnamehashes'] = array_keys($files);
3036
 
3037
        $event = \mod_workshop\event\assessable_uploaded::create($params);
3038
        $event->trigger();
3039
 
3040
        return $submission->id;
3041
    }
3042
 
3043
    /**
3044
     * Helper method for validating if the current user can view the given assessment.
3045
     *
3046
     * @param  stdClass   $assessment assessment object
3047
     * @param  stdClass   $submission submission object
3048
     * @return void
3049
     * @throws moodle_exception
3050
     * @since  Moodle 3.4
3051
     */
3052
    public function check_view_assessment($assessment, $submission) {
3053
        global $USER;
3054
 
3055
        $isauthor = $submission->authorid == $USER->id;
3056
        $isreviewer = $assessment->reviewerid == $USER->id;
3057
        $canviewallassessments  = has_capability('mod/workshop:viewallassessments', $this->context);
3058
        $canviewallsubmissions  = has_capability('mod/workshop:viewallsubmissions', $this->context);
3059
 
3060
        $canviewallsubmissions = $canviewallsubmissions && $this->check_group_membership($submission->authorid);
3061
 
3062
        if (!$isreviewer and !$isauthor and !($canviewallassessments and $canviewallsubmissions)) {
3063
            throw new \moodle_exception('nopermissions', 'error', $this->view_url(), 'view this assessment');
3064
        }
3065
 
3066
        if ($isauthor and !$isreviewer and !$canviewallassessments and $this->phase != self::PHASE_CLOSED) {
3067
            // Authors can see assessments of their work at the end of workshop only.
3068
            throw new \moodle_exception('nopermissions', 'error', $this->view_url(),
3069
                'view assessment of own work before workshop is closed');
3070
        }
3071
    }
3072
 
3073
    /**
3074
     * Helper method for validating if the current user can edit the given assessment.
3075
     *
3076
     * @param  stdClass   $assessment assessment object
3077
     * @param  stdClass   $submission submission object
3078
     * @return void
3079
     * @throws moodle_exception
3080
     * @since  Moodle 3.4
3081
     */
3082
    public function check_edit_assessment($assessment, $submission) {
3083
        global $USER;
3084
 
3085
        $this->check_view_assessment($assessment, $submission);
3086
        // Further checks.
3087
        $isreviewer = ($USER->id == $assessment->reviewerid);
3088
 
3089
        $assessmenteditable = $isreviewer && $this->assessing_allowed($USER->id);
3090
        if (!$assessmenteditable) {
3091
            throw new moodle_exception('nopermissions', 'error', '', 'edit assessments');
3092
        }
3093
 
3094
        list($assessed, $notice) = $this->check_examples_assessed_before_assessment($assessment->reviewerid);
3095
        if (!$assessed) {
3096
            throw new moodle_exception($notice, 'mod_workshop');
3097
        }
3098
    }
3099
 
3100
    /**
3101
     * Adds information to an allocated assessment (function used the first time a review is done or when updating an existing one).
3102
     *
3103
     * @param  stdClass $assessment the assessment
3104
     * @param  stdClass $submission the submission
3105
     * @param  stdClass $data       the assessment data to be added or Updated
3106
     * @param  stdClass $strategy   the strategy instance
3107
     * @return float|null           Raw percentual grade (0.00000 to 100.00000) for submission
3108
     * @since  Moodle 3.4
3109
     */
3110
    public function edit_assessment($assessment, $submission, $data, $strategy) {
3111
        global $DB;
3112
 
3113
        $cansetassessmentweight = has_capability('mod/workshop:allocate', $this->context);
3114
 
3115
        // Let the grading strategy subplugin save its data.
3116
        $rawgrade = $strategy->save_assessment($assessment, $data);
3117
 
3118
        // Store the data managed by the workshop core.
3119
        $coredata = (object)array('id' => $assessment->id);
3120
        if (isset($data->feedbackauthor_editor)) {
3121
            $coredata->feedbackauthor_editor = $data->feedbackauthor_editor;
3122
            $coredata = file_postupdate_standard_editor($coredata, 'feedbackauthor', $this->overall_feedback_content_options(),
3123
                $this->context, 'mod_workshop', 'overallfeedback_content', $assessment->id);
3124
            unset($coredata->feedbackauthor_editor);
3125
        }
3126
        if (isset($data->feedbackauthorattachment_filemanager)) {
3127
            $coredata->feedbackauthorattachment_filemanager = $data->feedbackauthorattachment_filemanager;
3128
            $coredata = file_postupdate_standard_filemanager($coredata, 'feedbackauthorattachment',
3129
                $this->overall_feedback_attachment_options(), $this->context, 'mod_workshop', 'overallfeedback_attachment',
3130
                $assessment->id);
3131
            unset($coredata->feedbackauthorattachment_filemanager);
3132
            if (empty($coredata->feedbackauthorattachment)) {
3133
                $coredata->feedbackauthorattachment = 0;
3134
            }
3135
        }
3136
        if (isset($data->weight) and $cansetassessmentweight) {
3137
            $coredata->weight = $data->weight;
3138
        }
3139
        // Update the assessment data if there is something other than just the 'id'.
3140
        if (count((array)$coredata) > 1 ) {
3141
            $DB->update_record('workshop_assessments', $coredata);
3142
            $params = array(
3143
                'relateduserid' => $submission->authorid,
3144
                'objectid' => $assessment->id,
3145
                'context' => $this->context,
3146
                'other' => array(
3147
                    'workshopid' => $this->id,
3148
                    'submissionid' => $assessment->submissionid
3149
                )
3150
            );
3151
 
3152
            if (is_null($assessment->grade)) {
3153
                // All workshop_assessments are created when allocations are made. The create event is of more use located here.
3154
                $event = \mod_workshop\event\submission_assessed::create($params);
3155
                $event->trigger();
3156
            } else {
3157
                $params['other']['grade'] = $assessment->grade;
3158
                $event = \mod_workshop\event\submission_reassessed::create($params);
3159
                $event->trigger();
3160
            }
3161
        }
3162
        return $rawgrade;
3163
    }
3164
 
3165
    /**
3166
     * Evaluates an assessment.
3167
     *
3168
     * @param  stdClass $assessment the assessment
3169
     * @param  stdClass $data       the assessment data to be updated
3170
     * @param  bool $cansetassessmentweight   whether the user can change the assessment weight
3171
     * @param  bool $canoverridegrades   whether the user can override the assessment grades
3172
     * @return void
3173
     * @since  Moodle 3.4
3174
     */
3175
    public function evaluate_assessment($assessment, $data, $cansetassessmentweight, $canoverridegrades) {
3176
        global $DB, $USER;
3177
 
3178
        $data = file_postupdate_standard_editor($data, 'feedbackreviewer', array(), $this->context);
3179
        $record = new stdclass();
3180
        $record->id = $assessment->id;
3181
        if ($cansetassessmentweight) {
3182
            $record->weight = $data->weight;
3183
        }
3184
        if ($canoverridegrades) {
3185
            $record->gradinggradeover = $this->raw_grade_value($data->gradinggradeover, $this->gradinggrade);
3186
            $record->gradinggradeoverby = $USER->id;
3187
            $record->feedbackreviewer = $data->feedbackreviewer;
3188
            $record->feedbackreviewerformat = $data->feedbackreviewerformat;
3189
        }
3190
        $DB->update_record('workshop_assessments', $record);
3191
    }
3192
 
3193
    /**
3194
     * Trigger submission viewed event.
3195
     *
3196
     * @param stdClass $submission submission object
3197
     * @since  Moodle 3.4
3198
     */
3199
    public function set_submission_viewed($submission) {
3200
        $params = array(
3201
            'objectid' => $submission->id,
3202
            'context' => $this->context,
3203
            'courseid' => $this->course->id,
3204
            'relateduserid' => $submission->authorid,
3205
            'other' => array(
3206
                'workshopid' => $this->id
3207
            )
3208
        );
3209
 
3210
        $event = \mod_workshop\event\submission_viewed::create($params);
3211
        $event->trigger();
3212
    }
3213
 
3214
    /**
3215
     * Evaluates a submission.
3216
     *
3217
     * @param  stdClass $submission the submission
3218
     * @param  stdClass $data       the submission data to be updated
3219
     * @param  bool $canpublish     whether the user can publish the submission
3220
     * @param  bool $canoverride    whether the user can override the submission grade
3221
     * @return void
3222
     * @since  Moodle 3.4
3223
     */
3224
    public function evaluate_submission($submission, $data, $canpublish, $canoverride) {
3225
        global $DB, $USER;
3226
 
3227
        $data = file_postupdate_standard_editor($data, 'feedbackauthor', array(), $this->context);
3228
        $record = new stdclass();
3229
        $record->id = $submission->id;
3230
        if ($canoverride) {
3231
            $record->gradeover = $this->raw_grade_value($data->gradeover, $this->grade);
3232
            $record->gradeoverby = $USER->id;
3233
            $record->feedbackauthor = $data->feedbackauthor;
3234
            $record->feedbackauthorformat = $data->feedbackauthorformat;
3235
        }
3236
        if ($canpublish) {
3237
            $record->published = !empty($data->published);
3238
        }
3239
        $DB->update_record('workshop_submissions', $record);
3240
    }
3241
 
3242
    /**
3243
     * Get the initial first name.
3244
     *
3245
     * @return string|null initial of first name we are currently filtering by.
3246
     */
3247
    public function get_initial_first(): ?string {
3248
        if (empty($this->initialbarprefs['i_first'])) {
3249
            return null;
3250
        }
3251
 
3252
        return $this->initialbarprefs['i_first'];
3253
    }
3254
 
3255
    /**
3256
     * Get the initial last name.
3257
     *
3258
     * @return string|null initial of last name we are currently filtering by.
3259
     */
3260
    public function get_initial_last(): ?string {
3261
        if (empty($this->initialbarprefs['i_last'])) {
3262
            return null;
3263
        }
3264
 
3265
        return $this->initialbarprefs['i_last'];
3266
    }
3267
 
3268
    /**
3269
     * Init method for initial bars.
3270
     * @return void
3271
     */
3272
    public function init_initial_bar(): void {
3273
        global $SESSION;
3274
        if ($this->phase === self::PHASE_SETUP) {
3275
            return;
3276
        }
3277
 
3278
        $ifirst = optional_param('ifirst', null, PARAM_NOTAGS);
3279
        $ilast = optional_param('ilast', null, PARAM_NOTAGS);
3280
 
3281
        if (empty($SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id])) {
3282
            $SESSION->mod_workshop = new stdClass();
3283
            $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id] = [];
3284
        }
3285
        if (!empty($SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_first'])) {
3286
            $this->initialbarprefs['i_first'] = $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_first'];
3287
        }
3288
        if (!empty($SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_last'])) {
3289
            $this->initialbarprefs['i_last'] = $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_last'];
3290
        }
3291
        if (!is_null($ifirst)) {
3292
            $this->initialbarprefs['i_first'] = $ifirst;
3293
            $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_first'] = $ifirst;
3294
        }
3295
 
3296
        if (!is_null($ilast)) {
3297
            $this->initialbarprefs['i_last'] = $ilast;
3298
            $SESSION->mod_workshop->initialbarprefs['id-'.$this->context->id]['i_last'] = $ilast;
3299
        }
3300
    }
3301
 
3302
    ////////////////////////////////////////////////////////////////////////////////
3303
    // Internal methods (implementation details)                                  //
3304
    ////////////////////////////////////////////////////////////////////////////////
3305
 
3306
    /**
3307
     * Given an array of all assessments of a single submission, calculates the final grade for this submission
3308
     *
3309
     * This calculates the weighted mean of the passed assessment grades. If, however, the submission grade
3310
     * was overridden by a teacher, the gradeover value is returned and the rest of grades are ignored.
3311
     *
3312
     * @param array $assessments of stdclass(->submissionid ->submissiongrade ->gradeover ->weight ->grade)
3313
     * @return void
3314
     */
3315
    protected function aggregate_submission_grades_process(array $assessments) {
3316
        global $DB;
3317
 
3318
        $submissionid   = null; // the id of the submission being processed
3319
        $current        = null; // the grade currently saved in database
3320
        $finalgrade     = null; // the new grade to be calculated
3321
        $sumgrades      = 0;
3322
        $sumweights     = 0;
3323
 
3324
        foreach ($assessments as $assessment) {
3325
            if (is_null($submissionid)) {
3326
                // the id is the same in all records, fetch it during the first loop cycle
3327
                $submissionid = $assessment->submissionid;
3328
            }
3329
            if (is_null($current)) {
3330
                // the currently saved grade is the same in all records, fetch it during the first loop cycle
3331
                $current = $assessment->submissiongrade;
3332
            }
3333
            if (is_null($assessment->grade)) {
3334
                // this was not assessed yet
3335
                continue;
3336
            }
3337
            if ($assessment->weight == 0) {
3338
                // this does not influence the calculation
3339
                continue;
3340
            }
3341
            $sumgrades  += $assessment->grade * $assessment->weight;
3342
            $sumweights += $assessment->weight;
3343
        }
3344
        if ($sumweights > 0 and is_null($finalgrade)) {
3345
            $finalgrade = grade_floatval($sumgrades / $sumweights);
3346
        }
3347
        // check if the new final grade differs from the one stored in the database
3348
        if (grade_floats_different($finalgrade, $current)) {
3349
            // we need to save new calculation into the database
3350
            $record = new stdclass();
3351
            $record->id = $submissionid;
3352
            $record->grade = $finalgrade;
3353
            $record->timegraded = time();
3354
            $DB->update_record('workshop_submissions', $record);
3355
        }
3356
    }
3357
 
3358
    /**
3359
     * Given an array of all assessments done by a single reviewer, calculates the final grading grade
3360
     *
3361
     * This calculates the simple mean of the passed grading grades. If, however, the grading grade
3362
     * was overridden by a teacher, the gradinggradeover value is returned and the rest of grades are ignored.
3363
     *
3364
     * @param array $assessments of stdclass(->reviewerid ->gradinggrade ->gradinggradeover ->aggregationid ->aggregatedgrade)
3365
     * @param null|int $timegraded explicit timestamp of the aggregation, defaults to the current time
3366
     * @return void
3367
     */
3368
    protected function aggregate_grading_grades_process(array $assessments, $timegraded = null) {
3369
        global $DB;
3370
 
3371
        $reviewerid = null; // the id of the reviewer being processed
3372
        $current    = null; // the gradinggrade currently saved in database
3373
        $finalgrade = null; // the new grade to be calculated
3374
        $agid       = null; // aggregation id
3375
        $sumgrades  = 0;
3376
        $count      = 0;
3377
 
3378
        if (is_null($timegraded)) {
3379
            $timegraded = time();
3380
        }
3381
 
3382
        foreach ($assessments as $assessment) {
3383
            if (is_null($reviewerid)) {
3384
                // the id is the same in all records, fetch it during the first loop cycle
3385
                $reviewerid = $assessment->reviewerid;
3386
            }
3387
            if (is_null($agid)) {
3388
                // the id is the same in all records, fetch it during the first loop cycle
3389
                $agid = $assessment->aggregationid;
3390
            }
3391
            if (is_null($current)) {
3392
                // the currently saved grade is the same in all records, fetch it during the first loop cycle
3393
                $current = $assessment->aggregatedgrade;
3394
            }
3395
            if (!is_null($assessment->gradinggradeover)) {
3396
                // the grading grade for this assessment is overridden by a teacher
3397
                $sumgrades += $assessment->gradinggradeover;
3398
                $count++;
3399
            } else {
3400
                if (!is_null($assessment->gradinggrade)) {
3401
                    $sumgrades += $assessment->gradinggrade;
3402
                    $count++;
3403
                }
3404
            }
3405
        }
3406
        if ($count > 0) {
3407
            $finalgrade = grade_floatval($sumgrades / $count);
3408
        }
3409
 
3410
        // Event information.
3411
        $params = array(
3412
            'context' => $this->context,
3413
            'courseid' => $this->course->id,
3414
            'relateduserid' => $reviewerid
3415
        );
3416
 
3417
        // check if the new final grade differs from the one stored in the database
3418
        if (grade_floats_different($finalgrade, $current)) {
3419
            $params['other'] = array(
3420
                'currentgrade' => $current,
3421
                'finalgrade' => $finalgrade
3422
            );
3423
 
3424
            // we need to save new calculation into the database
3425
            if (is_null($agid)) {
3426
                // no aggregation record yet
3427
                $record = new stdclass();
3428
                $record->workshopid = $this->id;
3429
                $record->userid = $reviewerid;
3430
                $record->gradinggrade = $finalgrade;
3431
                $record->timegraded = $timegraded;
3432
                $record->id = $DB->insert_record('workshop_aggregations', $record);
3433
                $params['objectid'] = $record->id;
3434
                $event = \mod_workshop\event\assessment_evaluated::create($params);
3435
                $event->trigger();
3436
            } else {
3437
                $record = new stdclass();
3438
                $record->id = $agid;
3439
                $record->gradinggrade = $finalgrade;
3440
                $record->timegraded = $timegraded;
3441
                $DB->update_record('workshop_aggregations', $record);
3442
                $params['objectid'] = $agid;
3443
                $event = \mod_workshop\event\assessment_reevaluated::create($params);
3444
                $event->trigger();
3445
            }
3446
        }
3447
    }
3448
 
3449
    /**
3450
     * Returns SQL to fetch all enrolled users with the given capability in the current workshop
3451
     *
3452
     * The returned array consists of string $sql and the $params array. Note that the $sql can be
3453
     * empty if a grouping is selected and it has no groups.
3454
     *
3455
     * The list is automatically restricted according to any availability restrictions
3456
     * that apply to user lists (e.g. group, grouping restrictions).
3457
     *
3458
     * @param string $capability the name of the capability
3459
     * @param bool $musthavesubmission ff true, return only users who have already submitted
3460
     * @param int $groupid 0 means ignore groups, any other value limits the result by group id
3461
     * @return array of (string)sql, (array)params
3462
     */
3463
    protected function get_users_with_capability_sql($capability, $musthavesubmission, $groupid) {
3464
        global $CFG;
3465
        /** @var int static counter used to generate unique parameter holders */
3466
        static $inc = 0;
3467
        $inc++;
3468
 
3469
        // If the caller requests all groups and we are using a selected grouping,
3470
        // recursively call this function for each group in the grouping (this is
3471
        // needed because get_enrolled_sql only supports a single group).
3472
        if (empty($groupid) and $this->cm->groupingid) {
3473
            $groupingid = $this->cm->groupingid;
3474
            $groupinggroupids = array_keys(groups_get_all_groups($this->cm->course, 0, $this->cm->groupingid, 'g.id'));
3475
            $sql = array();
3476
            $params = array();
3477
            foreach ($groupinggroupids as $groupinggroupid) {
3478
                if ($groupinggroupid > 0) { // just in case in order not to fall into the endless loop
3479
                    list($gsql, $gparams) = $this->get_users_with_capability_sql($capability, $musthavesubmission, $groupinggroupid);
3480
                    $sql[] = $gsql;
3481
                    $params = array_merge($params, $gparams);
3482
                }
3483
            }
3484
            $sql = implode(PHP_EOL." UNION ".PHP_EOL, $sql);
3485
            return array($sql, $params);
3486
        }
3487
 
3488
        list($esql, $params) = get_enrolled_sql($this->context, $capability, $groupid, true);
3489
 
3490
        $userfieldsapi = \core_user\fields::for_userpic();
3491
        $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
3492
 
3493
        $sql = "SELECT $userfields
3494
                  FROM {user} u
3495
                  JOIN ($esql) je ON (je.id = u.id AND u.deleted = 0) ";
3496
 
3497
        if ($musthavesubmission) {
3498
            $sql .= " JOIN {workshop_submissions} ws ON (ws.authorid = u.id AND ws.example = 0 AND ws.workshopid = :workshopid{$inc}) ";
3499
            $params['workshopid'.$inc] = $this->id;
3500
        }
3501
 
3502
        // If the activity is restricted so that only certain users should appear
3503
        // in user lists, integrate this into the same SQL.
3504
        $info = new \core_availability\info_module($this->cm);
3505
        list ($listsql, $listparams) = $info->get_user_list_sql(false);
3506
        if ($listsql) {
3507
            $sql .= " JOIN ($listsql) restricted ON restricted.id = u.id ";
3508
            $params = array_merge($params, $listparams);
3509
        }
3510
 
3511
        return array($sql, $params);
3512
    }
3513
 
3514
    /**
3515
     * Returns SQL to fetch all enrolled users with the first name or last name.
3516
     *
3517
     * @return array
3518
     */
3519
    protected function get_users_with_initial_filtering_sql_where(): array {
3520
        global $DB;
3521
        $conditions = [];
3522
        $params = [];
3523
        $ifirst = $this->get_initial_first();
3524
        $ilast = $this->get_initial_last();
3525
        if ($ifirst) {
3526
            $conditions[] = $DB->sql_like('LOWER(tmp.firstname)', ':i_first' , false, false);
3527
            $params['i_first'] = $DB->sql_like_escape($ifirst) . '%';
3528
        }
3529
        if ($ilast) {
3530
            $conditions[] = $DB->sql_like('LOWER(tmp.lastname)', ':i_last' , false, false);
3531
            $params['i_last'] = $DB->sql_like_escape($ilast) . '%';
3532
        }
3533
        return [implode(" AND ", $conditions), $params];
3534
    }
3535
 
3536
    /**
3537
     * Returns SQL statement that can be used to fetch all actively enrolled participants in the workshop
3538
     *
3539
     * @param bool $musthavesubmission if true, return only users who have already submitted
3540
     * @param int $groupid 0 means ignore groups, any other value limits the result by group id
3541
     * @return array of (string)sql, (array)params
3542
     */
3543
    protected function get_participants_sql($musthavesubmission=false, $groupid=0) {
3544
 
3545
        list($sql1, $params1) = $this->get_users_with_capability_sql('mod/workshop:submit', $musthavesubmission, $groupid);
3546
        list($sql2, $params2) = $this->get_users_with_capability_sql('mod/workshop:peerassess', $musthavesubmission, $groupid);
3547
 
3548
        if (empty($sql1) or empty($sql2)) {
3549
            if (empty($sql1) and empty($sql2)) {
3550
                return array('', array());
3551
            } else if (empty($sql1)) {
3552
                $sql = $sql2;
3553
                $params = $params2;
3554
            } else {
3555
                $sql = $sql1;
3556
                $params = $params1;
3557
            }
3558
        } else {
3559
            $sql = $sql1.PHP_EOL." UNION ".PHP_EOL.$sql2;
3560
            $params = array_merge($params1, $params2);
3561
        }
3562
 
3563
        return array($sql, $params);
3564
    }
3565
 
3566
    /**
3567
     * @return array of available workshop phases
3568
     */
3569
    protected function available_phases_list() {
3570
        return array(
3571
            self::PHASE_SETUP       => true,
3572
            self::PHASE_SUBMISSION  => true,
3573
            self::PHASE_ASSESSMENT  => true,
3574
            self::PHASE_EVALUATION  => true,
3575
            self::PHASE_CLOSED      => true,
3576
        );
3577
    }
3578
 
3579
    /**
3580
     * Converts absolute URL to relative URL needed by {@see add_to_log()}
3581
     *
3582
     * @param moodle_url $url absolute URL
3583
     * @return string
3584
     */
3585
    protected function log_convert_url(moodle_url $fullurl) {
3586
        static $baseurl;
3587
 
3588
        if (!isset($baseurl)) {
3589
            $baseurl = new moodle_url('/mod/workshop/');
3590
            $baseurl = $baseurl->out();
3591
        }
3592
 
3593
        return substr($fullurl->out(), strlen($baseurl));
3594
    }
3595
 
3596
    /**
3597
     * Removes all user data related to assessments (including allocations).
3598
     *
3599
     * This includes assessments of example submissions as long as they are not
3600
     * referential assessments.
3601
     *
3602
     * @param stdClass $data The actual course reset settings.
3603
     * @return bool|string True on success, error message otherwise.
3604
     */
3605
    protected function reset_userdata_assessments(stdClass $data) {
3606
        global $DB;
3607
 
3608
        $sql = "SELECT a.id
3609
                  FROM {workshop_assessments} a
3610
                  JOIN {workshop_submissions} s ON (a.submissionid = s.id)
3611
                 WHERE s.workshopid = :workshopid
3612
                       AND (s.example = 0 OR (s.example = 1 AND a.weight = 0))";
3613
 
3614
        $assessments = $DB->get_records_sql($sql, array('workshopid' => $this->id));
3615
        $this->delete_assessment(array_keys($assessments));
3616
 
3617
        $DB->delete_records('workshop_aggregations', array('workshopid' => $this->id));
3618
 
3619
        return true;
3620
    }
3621
 
3622
    /**
3623
     * Removes all user data related to participants' submissions.
3624
     *
3625
     * @param stdClass $data The actual course reset settings.
3626
     * @return bool|string True on success, error message otherwise.
3627
     */
3628
    protected function reset_userdata_submissions(stdClass $data) {
3629
        global $DB;
3630
 
3631
        $submissions = $this->get_submissions();
3632
        foreach ($submissions as $submission) {
3633
            $this->delete_submission($submission);
3634
        }
3635
 
3636
        return true;
3637
    }
3638
 
3639
    /**
3640
     * Hard set the workshop phase to the setup one.
3641
     */
3642
    protected function reset_phase() {
3643
        global $DB;
3644
 
3645
        $DB->set_field('workshop', 'phase', self::PHASE_SETUP, array('id' => $this->id));
3646
        $this->phase = self::PHASE_SETUP;
3647
    }
3648
}
3649
 
3650
////////////////////////////////////////////////////////////////////////////////
3651
// Renderable components
3652
////////////////////////////////////////////////////////////////////////////////
3653
 
3654
/**
3655
 * Represents the user planner tool
3656
 *
3657
 * Planner contains list of phases. Each phase contains list of tasks. Task is a simple object with
3658
 * title, link and completed (true/false/null logic).
3659
 */
3660
class workshop_user_plan implements renderable {
3661
 
3662
    /** @var int id of the user this plan is for */
3663
    public $userid;
3664
    /** @var workshop */
3665
    public $workshop;
3666
    /** @var array of (stdclass)tasks */
3667
    public $phases = array();
3668
    /** @var null|array of example submissions to be assessed by the planner owner */
3669
    protected $examples = null;
3670
 
3671
    /**
3672
     * Prepare an individual workshop plan for the given user.
3673
     *
3674
     * @param workshop $workshop instance
3675
     * @param int $userid whom the plan is prepared for
3676
     */
3677
    public function __construct(workshop $workshop, $userid) {
3678
        global $DB;
3679
 
3680
        $this->workshop = $workshop;
3681
        $this->userid   = $userid;
3682
 
3683
        //---------------------------------------------------------
3684
        // * SETUP | submission | assessment | evaluation | closed
3685
        //---------------------------------------------------------
3686
        $phase = new stdclass();
3687
        $phase->title = get_string('phasesetup', 'workshop');
3688
        $phase->tasks = array();
3689
        if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
3690
            $task = new stdclass();
3691
            $task->title = get_string('taskintro', 'workshop');
3692
            $task->link = $workshop->updatemod_url();
3693
            $task->completed = !(trim($workshop->intro) == '');
3694
            $phase->tasks['intro'] = $task;
3695
        }
3696
        if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
3697
            $task = new stdclass();
3698
            $task->title = get_string('taskinstructauthors', 'workshop');
3699
            $task->link = $workshop->updatemod_url();
3700
            $task->completed = !(trim($workshop->instructauthors) == '');
3701
            $phase->tasks['instructauthors'] = $task;
3702
        }
3703
        if (has_capability('mod/workshop:editdimensions', $workshop->context, $userid)) {
3704
            $task = new stdclass();
3705
            $task->title = get_string('editassessmentform', 'workshop');
3706
            $task->link = $workshop->editform_url();
3707
            if ($workshop->grading_strategy_instance()->form_ready()) {
3708
                $task->completed = true;
3709
            } elseif ($workshop->phase > workshop::PHASE_SETUP) {
3710
                $task->completed = false;
3711
            }
3712
            $phase->tasks['editform'] = $task;
3713
        }
3714
        if ($workshop->useexamples and has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) {
3715
            $task = new stdclass();
3716
            $task->title = get_string('prepareexamples', 'workshop');
3717
            if ($DB->count_records('workshop_submissions', array('example' => 1, 'workshopid' => $workshop->id)) > 0) {
3718
                $task->completed = true;
3719
            } elseif ($workshop->phase > workshop::PHASE_SETUP) {
3720
                $task->completed = false;
3721
            }
3722
            $phase->tasks['prepareexamples'] = $task;
3723
        }
3724
        if (empty($phase->tasks) and $workshop->phase == workshop::PHASE_SETUP) {
3725
            // if we are in the setup phase and there is no task (typical for students), let us
3726
            // display some explanation what is going on
3727
            $task = new stdclass();
3728
            $task->title = get_string('undersetup', 'workshop');
3729
            $task->completed = 'info';
3730
            $phase->tasks['setupinfo'] = $task;
3731
        }
3732
        $this->phases[workshop::PHASE_SETUP] = $phase;
3733
 
3734
        //---------------------------------------------------------
3735
        // setup | * SUBMISSION | assessment | evaluation | closed
3736
        //---------------------------------------------------------
3737
        $phase = new stdclass();
3738
        $phase->title = get_string('phasesubmission', 'workshop');
3739
        $phase->tasks = array();
3740
        if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
3741
            $task = new stdclass();
3742
            $task->title = get_string('taskinstructreviewers', 'workshop');
3743
            $task->link = $workshop->updatemod_url();
3744
            if (trim($workshop->instructreviewers)) {
3745
                $task->completed = true;
3746
            } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) {
3747
                $task->completed = false;
3748
            }
3749
            $phase->tasks['instructreviewers'] = $task;
3750
        }
3751
        if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_SUBMISSION
3752
                and has_capability('mod/workshop:submit', $workshop->context, $userid, false)
3753
                    and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) {
3754
            $task = new stdclass();
3755
            $task->title = get_string('exampleassesstask', 'workshop');
3756
            $examples = $this->get_examples();
3757
            $a = new stdclass();
3758
            $a->expected = count($examples);
3759
            $a->assessed = 0;
3760
            foreach ($examples as $exampleid => $example) {
3761
                if (!is_null($example->grade)) {
3762
                    $a->assessed++;
3763
                }
3764
            }
3765
            $task->details = get_string('exampleassesstaskdetails', 'workshop', $a);
3766
            if ($a->assessed == $a->expected) {
3767
                $task->completed = true;
3768
            } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) {
3769
                $task->completed = false;
3770
            }
3771
            $phase->tasks['examples'] = $task;
3772
        }
3773
        if (has_capability('mod/workshop:submit', $workshop->context, $userid, false)) {
3774
            $task = new stdclass();
3775
            $task->title = get_string('tasksubmit', 'workshop');
3776
            $task->link = $workshop->submission_url();
3777
            if ($DB->record_exists('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0, 'authorid'=>$userid))) {
3778
                $task->completed = true;
3779
            } elseif ($workshop->phase >= workshop::PHASE_ASSESSMENT) {
3780
                $task->completed = false;
3781
            } else {
3782
                $task->completed = null;    // still has a chance to submit
3783
            }
3784
            $phase->tasks['submit'] = $task;
3785
        }
3786
        if (has_capability('mod/workshop:allocate', $workshop->context, $userid)) {
3787
            if ($workshop->phaseswitchassessment) {
3788
                $task = new stdClass();
3789
                $allocator = $DB->get_record('workshopallocation_scheduled', array('workshopid' => $workshop->id));
3790
                if (empty($allocator)) {
3791
                    $task->completed = false;
3792
                } else if ($allocator->enabled and is_null($allocator->resultstatus)) {
3793
                    $task->completed = true;
3794
                } else if ($workshop->submissionend > time()) {
3795
                    $task->completed = null;
3796
                } else {
3797
                    $task->completed = false;
3798
                }
3799
                $task->title = get_string('setup', 'workshopallocation_scheduled');
3800
                $task->link = $workshop->allocation_url('scheduled');
3801
                $phase->tasks['allocatescheduled'] = $task;
3802
            }
3803
            $task = new stdclass();
3804
            $task->title = get_string('allocate', 'workshop');
3805
            $task->link = $workshop->allocation_url();
3806
            $numofauthors = $workshop->count_potential_authors(false);
3807
            $numofsubmissions = $DB->count_records('workshop_submissions', array('workshopid'=>$workshop->id, 'example'=>0));
3808
            $sql = 'SELECT COUNT(s.id) AS nonallocated
3809
                      FROM {workshop_submissions} s
3810
                 LEFT JOIN {workshop_assessments} a ON (a.submissionid=s.id)
3811
                     WHERE s.workshopid = :workshopid AND s.example=0 AND a.submissionid IS NULL';
3812
            $params['workshopid'] = $workshop->id;
3813
            $numnonallocated = $DB->count_records_sql($sql, $params);
3814
            if ($numofsubmissions == 0) {
3815
                $task->completed = null;
3816
            } elseif ($numnonallocated == 0) {
3817
                $task->completed = true;
3818
            } elseif ($workshop->phase > workshop::PHASE_SUBMISSION) {
3819
                $task->completed = false;
3820
            } else {
3821
                $task->completed = null;    // still has a chance to allocate
3822
            }
3823
            $a = new stdclass();
3824
            $a->expected    = $numofauthors;
3825
            $a->submitted   = $numofsubmissions;
3826
            $a->allocate    = $numnonallocated;
3827
            $task->details  = get_string('allocatedetails', 'workshop', $a);
3828
            unset($a);
3829
            $phase->tasks['allocate'] = $task;
3830
 
3831
            if ($numofsubmissions < $numofauthors and $workshop->phase >= workshop::PHASE_SUBMISSION) {
3832
                $task = new stdclass();
3833
                $task->title = get_string('someuserswosubmission', 'workshop');
3834
                $task->completed = 'info';
3835
                $phase->tasks['allocateinfo'] = $task;
3836
            }
3837
 
3838
        }
3839
        if ($workshop->submissionstart) {
3840
            $task = new stdclass();
3841
            $task->title = get_string('submissionstartdatetime', 'workshop', workshop::timestamp_formats($workshop->submissionstart));
3842
            $task->completed = 'info';
3843
            $phase->tasks['submissionstartdatetime'] = $task;
3844
        }
3845
        if ($workshop->submissionend) {
3846
            $task = new stdclass();
3847
            $task->title = get_string('submissionenddatetime', 'workshop', workshop::timestamp_formats($workshop->submissionend));
3848
            $task->completed = 'info';
3849
            $phase->tasks['submissionenddatetime'] = $task;
3850
        }
3851
        if (($workshop->submissionstart < time()) and $workshop->latesubmissions) {
3852
            // If submission deadline has passed and late submissions are allowed, only display 'latesubmissionsallowed' text to
3853
            // users (students) who have not submitted and users(teachers, admins)  who can switch pahase..
3854
            if (has_capability('mod/workshop:switchphase', $workshop->context, $userid) ||
3855
                    (!$workshop->get_submission_by_author($userid) && $workshop->submissionend < time())) {
3856
                $task = new stdclass();
3857
                $task->title = get_string('latesubmissionsallowed', 'workshop');
3858
                $task->completed = 'info';
3859
                $phase->tasks['latesubmissionsallowed'] = $task;
3860
            }
3861
        }
3862
        if (isset($phase->tasks['submissionstartdatetime']) or isset($phase->tasks['submissionenddatetime'])) {
3863
            if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) {
3864
                $task = new stdclass();
3865
                $task->title = get_string('deadlinesignored', 'workshop');
3866
                $task->completed = 'info';
3867
                $phase->tasks['deadlinesignored'] = $task;
3868
            }
3869
        }
3870
        $this->phases[workshop::PHASE_SUBMISSION] = $phase;
3871
 
3872
        //---------------------------------------------------------
3873
        // setup | submission | * ASSESSMENT | evaluation | closed
3874
        //---------------------------------------------------------
3875
        $phase = new stdclass();
3876
        $phase->title = get_string('phaseassessment', 'workshop');
3877
        $phase->tasks = array();
3878
        $phase->isreviewer = has_capability('mod/workshop:peerassess', $workshop->context, $userid);
3879
        if ($workshop->phase == workshop::PHASE_SUBMISSION and $workshop->phaseswitchassessment
3880
                and has_capability('mod/workshop:switchphase', $workshop->context, $userid)) {
3881
            $task = new stdClass();
3882
            $task->title = get_string('switchphase30auto', 'mod_workshop', workshop::timestamp_formats($workshop->submissionend));
3883
            $task->completed = 'info';
3884
            $phase->tasks['autoswitchinfo'] = $task;
3885
        }
3886
        if ($workshop->useexamples and $workshop->examplesmode == workshop::EXAMPLES_BEFORE_ASSESSMENT
3887
                and $phase->isreviewer and !has_capability('mod/workshop:manageexamples', $workshop->context, $userid)) {
3888
            $task = new stdclass();
3889
            $task->title = get_string('exampleassesstask', 'workshop');
3890
            $examples = $workshop->get_examples_for_reviewer($userid);
3891
            $a = new stdclass();
3892
            $a->expected = count($examples);
3893
            $a->assessed = 0;
3894
            foreach ($examples as $exampleid => $example) {
3895
                if (!is_null($example->grade)) {
3896
                    $a->assessed++;
3897
                }
3898
            }
3899
            $task->details = get_string('exampleassesstaskdetails', 'workshop', $a);
3900
            if ($a->assessed == $a->expected) {
3901
                $task->completed = true;
3902
            } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) {
3903
                $task->completed = false;
3904
            }
3905
            $phase->tasks['examples'] = $task;
3906
        }
3907
        if (empty($phase->tasks['examples']) or !empty($phase->tasks['examples']->completed)) {
3908
            $phase->assessments = $workshop->get_assessments_by_reviewer($userid);
3909
            $numofpeers     = 0;    // number of allocated peer-assessments
3910
            $numofpeerstodo = 0;    // number of peer-assessments to do
3911
            $numofself      = 0;    // number of allocated self-assessments - should be 0 or 1
3912
            $numofselftodo  = 0;    // number of self-assessments to do - should be 0 or 1
3913
            foreach ($phase->assessments as $a) {
3914
                if ($a->authorid == $userid) {
3915
                    $numofself++;
3916
                    if (is_null($a->grade)) {
3917
                        $numofselftodo++;
3918
                    }
3919
                } else {
3920
                    $numofpeers++;
3921
                    if (is_null($a->grade)) {
3922
                        $numofpeerstodo++;
3923
                    }
3924
                }
3925
            }
3926
            unset($a);
3927
            if ($numofpeers) {
3928
                $task = new stdclass();
3929
                if ($numofpeerstodo == 0) {
3930
                    $task->completed = true;
3931
                } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) {
3932
                    $task->completed = false;
3933
                }
3934
                $a = new stdclass();
3935
                $a->total = $numofpeers;
3936
                $a->todo  = $numofpeerstodo;
3937
                $task->title = get_string('taskassesspeers', 'workshop');
3938
                $task->details = get_string('taskassesspeersdetails', 'workshop', $a);
3939
                unset($a);
3940
                $phase->tasks['assesspeers'] = $task;
3941
            }
3942
            if ($workshop->useselfassessment and $numofself) {
3943
                $task = new stdclass();
3944
                if ($numofselftodo == 0) {
3945
                    $task->completed = true;
3946
                } elseif ($workshop->phase > workshop::PHASE_ASSESSMENT) {
3947
                    $task->completed = false;
3948
                }
3949
                $task->title = get_string('taskassessself', 'workshop');
3950
                $phase->tasks['assessself'] = $task;
3951
            }
3952
        }
3953
        if ($workshop->assessmentstart) {
3954
            $task = new stdclass();
3955
            $task->title = get_string('assessmentstartdatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentstart));
3956
            $task->completed = 'info';
3957
            $phase->tasks['assessmentstartdatetime'] = $task;
3958
        }
3959
        if ($workshop->assessmentend) {
3960
            $task = new stdclass();
3961
            $task->title = get_string('assessmentenddatetime', 'workshop', workshop::timestamp_formats($workshop->assessmentend));
3962
            $task->completed = 'info';
3963
            $phase->tasks['assessmentenddatetime'] = $task;
3964
        }
3965
        if (isset($phase->tasks['assessmentstartdatetime']) or isset($phase->tasks['assessmentenddatetime'])) {
3966
            if (has_capability('mod/workshop:ignoredeadlines', $workshop->context, $userid)) {
3967
                $task = new stdclass();
3968
                $task->title = get_string('deadlinesignored', 'workshop');
3969
                $task->completed = 'info';
3970
                $phase->tasks['deadlinesignored'] = $task;
3971
            }
3972
        }
3973
        $this->phases[workshop::PHASE_ASSESSMENT] = $phase;
3974
 
3975
        //---------------------------------------------------------
3976
        // setup | submission | assessment | * EVALUATION | closed
3977
        //---------------------------------------------------------
3978
        $phase = new stdclass();
3979
        $phase->title = get_string('phaseevaluation', 'workshop');
3980
        $phase->tasks = array();
3981
        if (has_capability('mod/workshop:overridegrades', $workshop->context)) {
3982
            $expected = $workshop->count_potential_authors(false);
3983
            $calculated = $DB->count_records_select('workshop_submissions',
3984
                    'workshopid = ? AND (grade IS NOT NULL OR gradeover IS NOT NULL)', array($workshop->id));
3985
            $task = new stdclass();
3986
            $task->title = get_string('calculatesubmissiongrades', 'workshop');
3987
            $a = new stdclass();
3988
            $a->expected    = $expected;
3989
            $a->calculated  = $calculated;
3990
            $task->details  = get_string('calculatesubmissiongradesdetails', 'workshop', $a);
3991
            if ($calculated >= $expected) {
3992
                $task->completed = true;
3993
            } elseif ($workshop->phase > workshop::PHASE_EVALUATION) {
3994
                $task->completed = false;
3995
            }
3996
            $phase->tasks['calculatesubmissiongrade'] = $task;
3997
 
3998
            $expected = $workshop->count_potential_reviewers(false);
3999
            $calculated = $DB->count_records_select('workshop_aggregations',
4000
                    'workshopid = ? AND gradinggrade IS NOT NULL', array($workshop->id));
4001
            $task = new stdclass();
4002
            $task->title = get_string('calculategradinggrades', 'workshop');
4003
            $a = new stdclass();
4004
            $a->expected    = $expected;
4005
            $a->calculated  = $calculated;
4006
            $task->details  = get_string('calculategradinggradesdetails', 'workshop', $a);
4007
            if ($calculated >= $expected) {
4008
                $task->completed = true;
4009
            } elseif ($workshop->phase > workshop::PHASE_EVALUATION) {
4010
                $task->completed = false;
4011
            }
4012
            $phase->tasks['calculategradinggrade'] = $task;
4013
 
4014
        } elseif ($workshop->phase == workshop::PHASE_EVALUATION) {
4015
            $task = new stdclass();
4016
            $task->title = get_string('evaluategradeswait', 'workshop');
4017
            $task->completed = 'info';
4018
            $phase->tasks['evaluateinfo'] = $task;
4019
        }
4020
 
4021
        if (has_capability('moodle/course:manageactivities', $workshop->context, $userid)) {
4022
            $task = new stdclass();
4023
            $task->title = get_string('taskconclusion', 'workshop');
4024
            $task->link = $workshop->updatemod_url();
4025
            if (trim($workshop->conclusion)) {
4026
                $task->completed = true;
4027
            } elseif ($workshop->phase >= workshop::PHASE_EVALUATION) {
4028
                $task->completed = false;
4029
            }
4030
            $phase->tasks['conclusion'] = $task;
4031
        }
4032
 
4033
        $this->phases[workshop::PHASE_EVALUATION] = $phase;
4034
 
4035
        //---------------------------------------------------------
4036
        // setup | submission | assessment | evaluation | * CLOSED
4037
        //---------------------------------------------------------
4038
        $phase = new stdclass();
4039
        $phase->title = get_string('phaseclosed', 'workshop');
4040
        $phase->tasks = array();
4041
        $this->phases[workshop::PHASE_CLOSED] = $phase;
4042
 
4043
        // Polish data, set default values if not done explicitly
4044
        foreach ($this->phases as $phasecode => $phase) {
4045
            $phase->title       = isset($phase->title)      ? $phase->title     : '';
4046
            $phase->tasks       = isset($phase->tasks)      ? $phase->tasks     : array();
4047
            if ($phasecode == $workshop->phase) {
4048
                $phase->active = true;
4049
            } else {
4050
                $phase->active = false;
4051
            }
4052
            if (!isset($phase->actions)) {
4053
                $phase->actions = array();
4054
            }
4055
 
4056
            foreach ($phase->tasks as $taskcode => $task) {
4057
                $task->title        = isset($task->title)       ? $task->title      : '';
4058
                $task->link         = isset($task->link)        ? $task->link       : null;
4059
                $task->details      = isset($task->details)     ? $task->details    : '';
4060
                $task->completed    = isset($task->completed)   ? $task->completed  : null;
4061
            }
4062
        }
4063
 
4064
        // Add phase switching actions.
4065
        if (has_capability('mod/workshop:switchphase', $workshop->context, $userid)) {
4066
            $nextphases = array(
4067
                workshop::PHASE_SETUP => workshop::PHASE_SUBMISSION,
4068
                workshop::PHASE_SUBMISSION => workshop::PHASE_ASSESSMENT,
4069
                workshop::PHASE_ASSESSMENT => workshop::PHASE_EVALUATION,
4070
                workshop::PHASE_EVALUATION => workshop::PHASE_CLOSED,
4071
            );
4072
            foreach ($this->phases as $phasecode => $phase) {
4073
                if ($phase->active) {
4074
                    if (isset($nextphases[$workshop->phase])) {
4075
                        $task = new stdClass();
4076
                        $task->title = get_string('switchphasenext', 'mod_workshop');
4077
                        $task->link = $workshop->switchphase_url($nextphases[$workshop->phase]);
4078
                        $task->details = '';
4079
                        $task->completed = null;
4080
                        $phase->tasks['switchtonextphase'] = $task;
4081
                    }
4082
 
4083
                } else {
4084
                    $action = new stdclass();
4085
                    $action->type = 'switchphase';
4086
                    $action->url  = $workshop->switchphase_url($phasecode);
4087
                    $phase->actions[] = $action;
4088
                }
4089
            }
4090
        }
4091
    }
4092
 
4093
    /**
4094
     * Returns example submissions to be assessed by the owner of the planner
4095
     *
4096
     * This is here to cache the DB query because the same list is needed later in view.php
4097
     *
4098
     * @see workshop::get_examples_for_reviewer() for the format of returned value
4099
     * @return array
4100
     */
4101
    public function get_examples() {
4102
        if (is_null($this->examples)) {
4103
            $this->examples = $this->workshop->get_examples_for_reviewer($this->userid);
4104
        }
4105
        return $this->examples;
4106
    }
4107
}
4108
 
4109
/**
4110
 * Common base class for submissions and example submissions rendering
4111
 *
4112
 * Subclasses of this class convert raw submission record from
4113
 * workshop_submissions table (as returned by {@see workshop::get_submission_by_id()}
4114
 * for example) into renderable objects.
4115
 */
4116
abstract class workshop_submission_base {
4117
 
4118
    /** @var bool is the submission anonymous (i.e. contains author information) */
4119
    protected $anonymous;
4120
 
4121
    /* @var array of columns from workshop_submissions that are assigned as properties */
4122
    protected $fields = array();
4123
 
4124
    /** @var workshop */
4125
    protected $workshop;
4126
 
4127
    /**
4128
     * Copies the properties of the given database record into properties of $this instance
4129
     *
4130
     * @param workshop $workshop
4131
     * @param stdClass $submission full record
4132
     * @param bool $showauthor show the author-related information
4133
     * @param array $options additional properties
4134
     */
4135
    public function __construct(workshop $workshop, stdClass $submission, $showauthor = false) {
4136
 
4137
        $this->workshop = $workshop;
4138
 
4139
        foreach ($this->fields as $field) {
4140
            if (!property_exists($submission, $field)) {
4141
                throw new coding_exception('Submission record must provide public property ' . $field);
4142
            }
4143
            if (!property_exists($this, $field)) {
4144
                throw new coding_exception('Renderable component must accept public property ' . $field);
4145
            }
4146
            $this->{$field} = $submission->{$field};
4147
        }
4148
 
4149
        if ($showauthor) {
4150
            $this->anonymous = false;
4151
        } else {
4152
            $this->anonymize();
4153
        }
4154
    }
4155
 
4156
    /**
4157
     * Unsets all author-related properties so that the renderer does not have access to them
4158
     *
4159
     * Usually this is called by the contructor but can be called explicitely, too.
4160
     */
4161
    public function anonymize() {
4162
        $authorfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
4163
        foreach ($authorfields as $field) {
4164
            $prefixedusernamefield = 'author' . $field;
4165
            unset($this->{$prefixedusernamefield});
4166
        }
4167
        $this->anonymous = true;
4168
    }
4169
 
4170
    /**
4171
     * Does the submission object contain author-related information?
4172
     *
4173
     * @return null|boolean
4174
     */
4175
    public function is_anonymous() {
4176
        return $this->anonymous;
4177
    }
4178
}
4179
 
4180
/**
4181
 * Renderable object containing a basic set of information needed to display the submission summary
4182
 *
4183
 * @see workshop_renderer::render_workshop_submission_summary
4184
 */
4185
class workshop_submission_summary extends workshop_submission_base implements renderable {
4186
 
4187
    /** @var int */
4188
    public $id;
4189
    /** @var string */
4190
    public $title;
4191
    /** @var string graded|notgraded */
4192
    public $status;
4193
    /** @var int */
4194
    public $timecreated;
4195
    /** @var int */
4196
    public $timemodified;
4197
    /** @var int */
4198
    public $authorid;
4199
    /** @var string */
4200
    public $authorfirstname;
4201
    /** @var string */
4202
    public $authorlastname;
4203
    /** @var string */
4204
    public $authorfirstnamephonetic;
4205
    /** @var string */
4206
    public $authorlastnamephonetic;
4207
    /** @var string */
4208
    public $authormiddlename;
4209
    /** @var string */
4210
    public $authoralternatename;
4211
    /** @var int */
4212
    public $authorpicture;
4213
    /** @var string */
4214
    public $authorimagealt;
4215
    /** @var string */
4216
    public $authoremail;
4217
    /** @var moodle_url to display submission */
4218
    public $url;
4219
 
4220
    /**
4221
     * @var array of columns from workshop_submissions that are assigned as properties
4222
     * of instances of this class
4223
     */
4224
    protected $fields = array(
4225
        'id', 'title', 'timecreated', 'timemodified',
4226
        'authorid', 'authorfirstname', 'authorlastname', 'authorfirstnamephonetic', 'authorlastnamephonetic',
4227
        'authormiddlename', 'authoralternatename', 'authorpicture',
4228
        'authorimagealt', 'authoremail');
4229
}
4230
 
4231
/**
4232
 * Renderable object containing all the information needed to display the submission
4233
 *
4234
 * @see workshop_renderer::render_workshop_submission()
4235
 */
4236
class workshop_submission extends workshop_submission_summary implements renderable {
4237
 
4238
    /** @var string */
4239
    public $content;
4240
    /** @var int */
4241
    public $contentformat;
4242
    /** @var bool */
4243
    public $contenttrust;
4244
    /** @var array */
4245
    public $attachment;
4246
 
4247
    /**
4248
     * @var array of columns from workshop_submissions that are assigned as properties
4249
     * of instances of this class
4250
     */
4251
    protected $fields = array(
4252
        'id', 'title', 'timecreated', 'timemodified', 'content', 'contentformat', 'contenttrust',
4253
        'attachment', 'authorid', 'authorfirstname', 'authorlastname', 'authorfirstnamephonetic', 'authorlastnamephonetic',
4254
        'authormiddlename', 'authoralternatename', 'authorpicture', 'authorimagealt', 'authoremail');
4255
}
4256
 
4257
/**
4258
 * Renderable object containing a basic set of information needed to display the example submission summary
4259
 *
4260
 * @see workshop::prepare_example_summary()
4261
 * @see workshop_renderer::render_workshop_example_submission_summary()
4262
 */
4263
class workshop_example_submission_summary extends workshop_submission_base implements renderable {
4264
 
4265
    /** @var int */
4266
    public $id;
4267
    /** @var string */
4268
    public $title;
4269
    /** @var string graded|notgraded */
4270
    public $status;
4271
    /** @var stdClass */
4272
    public $gradeinfo;
4273
    /** @var moodle_url */
4274
    public $url;
4275
    /** @var moodle_url */
4276
    public $editurl;
4277
    /** @var string */
4278
    public $assesslabel;
4279
    /** @var moodle_url */
4280
    public $assessurl;
4281
    /** @var bool must be set explicitly by the caller */
4282
    public $editable = false;
4283
 
4284
    /**
4285
     * @var array of columns from workshop_submissions that are assigned as properties
4286
     * of instances of this class
4287
     */
4288
    protected $fields = array('id', 'title');
4289
 
4290
    /**
4291
     * Example submissions are always anonymous
4292
     *
4293
     * @return true
4294
     */
4295
    public function is_anonymous() {
4296
        return true;
4297
    }
4298
}
4299
 
4300
/**
4301
 * Renderable object containing all the information needed to display the example submission
4302
 *
4303
 * @see workshop_renderer::render_workshop_example_submission()
4304
 */
4305
class workshop_example_submission extends workshop_example_submission_summary implements renderable {
4306
 
4307
    /** @var string */
4308
    public $content;
4309
    /** @var int */
4310
    public $contentformat;
4311
    /** @var bool */
4312
    public $contenttrust;
4313
    /** @var array */
4314
    public $attachment;
4315
 
4316
    /**
4317
     * @var array of columns from workshop_submissions that are assigned as properties
4318
     * of instances of this class
4319
     */
4320
    protected $fields = array('id', 'title', 'content', 'contentformat', 'contenttrust', 'attachment');
4321
}
4322
 
4323
 
4324
/**
4325
 * Common base class for assessments rendering
4326
 *
4327
 * Subclasses of this class convert raw assessment record from
4328
 * workshop_assessments table (as returned by {@see workshop::get_assessment_by_id()}
4329
 * for example) into renderable objects.
4330
 */
4331
abstract class workshop_assessment_base {
4332
 
4333
    /** @var string the optional title of the assessment */
4334
    public $title = '';
4335
 
4336
    /** @var workshop_assessment_form $form as returned by {@link workshop_strategy::get_assessment_form()} */
4337
    public $form;
4338
 
4339
    /** @var moodle_url */
4340
    public $url;
4341
 
4342
    /** @var float|null the real received grade */
4343
    public $realgrade = null;
4344
 
4345
    /** @var float the real maximum grade */
4346
    public $maxgrade;
4347
 
4348
    /** @var stdClass|null reviewer user info */
4349
    public $reviewer = null;
4350
 
4351
    /** @var stdClass|null assessed submission's author user info */
4352
    public $author = null;
4353
 
4354
    /** @var array of actions */
4355
    public $actions = array();
4356
 
4357
    /* @var array of columns that are assigned as properties */
4358
    protected $fields = array();
4359
 
4360
    /** @var workshop */
4361
    public $workshop;
4362
 
4363
    /**
4364
     * Copies the properties of the given database record into properties of $this instance
4365
     *
4366
     * The $options keys are: showreviewer, showauthor
4367
     * @param workshop $workshop
4368
     * @param stdClass $assessment full record
4369
     * @param array $options additional properties
4370
     */
4371
    public function __construct(workshop $workshop, stdClass $record, array $options = array()) {
4372
 
4373
        $this->workshop = $workshop;
4374
        $this->validate_raw_record($record);
4375
 
4376
        foreach ($this->fields as $field) {
4377
            if (!property_exists($record, $field)) {
4378
                throw new coding_exception('Assessment record must provide public property ' . $field);
4379
            }
4380
            if (!property_exists($this, $field)) {
4381
                throw new coding_exception('Renderable component must accept public property ' . $field);
4382
            }
4383
            $this->{$field} = $record->{$field};
4384
        }
4385
 
4386
        if (!empty($options['showreviewer'])) {
4387
            $this->reviewer = user_picture::unalias($record, null, 'revieweridx', 'reviewer');
4388
        }
4389
 
4390
        if (!empty($options['showauthor'])) {
4391
            $this->author = user_picture::unalias($record, null, 'authorid', 'author');
4392
        }
4393
    }
4394
 
4395
    /**
4396
     * Adds a new action
4397
     *
4398
     * @param moodle_url $url action URL
4399
     * @param string $label action label
4400
     * @param string $method get|post
4401
     */
4402
    public function add_action(moodle_url $url, $label, $method = 'get') {
4403
 
4404
        $action = new stdClass();
4405
        $action->url = $url;
4406
        $action->label = $label;
4407
        $action->method = $method;
4408
 
4409
        $this->actions[] = $action;
4410
    }
4411
 
4412
    /**
4413
     * Makes sure that we can cook the renderable component from the passed raw database record
4414
     *
4415
     * @param stdClass $assessment full assessment record
4416
     * @throws coding_exception if the caller passed unexpected data
4417
     */
4418
    protected function validate_raw_record(stdClass $record) {
4419
        // nothing to do here
4420
    }
4421
}
4422
 
4423
 
4424
/**
4425
 * Represents a rendarable full assessment
4426
 */
4427
class workshop_assessment extends workshop_assessment_base implements renderable {
4428
 
4429
    /** @var int */
4430
    public $id;
4431
 
4432
    /** @var int */
4433
    public $submissionid;
4434
 
4435
    /** @var int */
4436
    public $weight;
4437
 
4438
    /** @var int */
4439
    public $timecreated;
4440
 
4441
    /** @var int */
4442
    public $timemodified;
4443
 
4444
    /** @var float */
4445
    public $grade;
4446
 
4447
    /** @var float */
4448
    public $gradinggrade;
4449
 
4450
    /** @var float */
4451
    public $gradinggradeover;
4452
 
4453
    /** @var string */
4454
    public $feedbackauthor;
4455
 
4456
    /** @var int */
4457
    public $feedbackauthorformat;
4458
 
4459
    /** @var int */
4460
    public $feedbackauthorattachment;
4461
 
4462
    /** @var array */
4463
    protected $fields = array('id', 'submissionid', 'weight', 'timecreated',
4464
        'timemodified', 'grade', 'gradinggrade', 'gradinggradeover', 'feedbackauthor',
4465
        'feedbackauthorformat', 'feedbackauthorattachment');
4466
 
4467
    /**
4468
     * Format the overall feedback text content
4469
     *
4470
     * False is returned if the overall feedback feature is disabled. Null is returned
4471
     * if the overall feedback content has not been found. Otherwise, string with
4472
     * formatted feedback text is returned.
4473
     *
4474
     * @return string|bool|null
4475
     */
4476
    public function get_overall_feedback_content() {
4477
 
4478
        if ($this->workshop->overallfeedbackmode == 0) {
4479
            return false;
4480
        }
4481
 
4482
        if (trim($this->feedbackauthor) === '') {
4483
            return null;
4484
        }
4485
 
4486
        $content = file_rewrite_pluginfile_urls($this->feedbackauthor, 'pluginfile.php', $this->workshop->context->id,
4487
            'mod_workshop', 'overallfeedback_content', $this->id);
4488
        $content = format_text($content, $this->feedbackauthorformat,
4489
            array('overflowdiv' => true, 'context' => $this->workshop->context));
4490
 
4491
        return $content;
4492
    }
4493
 
4494
    /**
4495
     * Prepares the list of overall feedback attachments
4496
     *
4497
     * Returns false if overall feedback attachments are not allowed. Otherwise returns
4498
     * list of attachments (may be empty).
4499
     *
4500
     * @return bool|array of stdClass
4501
     */
4502
    public function get_overall_feedback_attachments() {
4503
 
4504
        if ($this->workshop->overallfeedbackmode == 0) {
4505
            return false;
4506
        }
4507
 
4508
        if ($this->workshop->overallfeedbackfiles == 0) {
4509
            return false;
4510
        }
4511
 
4512
        if (empty($this->feedbackauthorattachment)) {
4513
            return array();
4514
        }
4515
 
4516
        $attachments = array();
4517
        $fs = get_file_storage();
4518
        $files = $fs->get_area_files($this->workshop->context->id, 'mod_workshop', 'overallfeedback_attachment', $this->id);
4519
        foreach ($files as $file) {
4520
            if ($file->is_directory()) {
4521
                continue;
4522
            }
4523
            $filepath = $file->get_filepath();
4524
            $filename = $file->get_filename();
4525
            $fileurl = moodle_url::make_pluginfile_url($this->workshop->context->id, 'mod_workshop',
4526
                'overallfeedback_attachment', $this->id, $filepath, $filename, true);
4527
            $previewurl = new moodle_url(moodle_url::make_pluginfile_url($this->workshop->context->id, 'mod_workshop',
4528
                'overallfeedback_attachment', $this->id, $filepath, $filename, false), array('preview' => 'bigthumb'));
4529
            $attachments[] = (object)array(
4530
                'filepath' => $filepath,
4531
                'filename' => $filename,
4532
                'fileurl' => $fileurl,
4533
                'previewurl' => $previewurl,
4534
                'mimetype' => $file->get_mimetype(),
4535
 
4536
            );
4537
        }
4538
 
4539
        return $attachments;
4540
    }
4541
}
4542
 
4543
 
4544
/**
4545
 * Represents a renderable training assessment of an example submission
4546
 */
4547
class workshop_example_assessment extends workshop_assessment implements renderable {
4548
 
4549
    /**
4550
     * @see parent::validate_raw_record()
4551
     */
4552
    protected function validate_raw_record(stdClass $record) {
4553
        if ($record->weight != 0) {
4554
            throw new coding_exception('Invalid weight of example submission assessment');
4555
        }
4556
        parent::validate_raw_record($record);
4557
    }
4558
}
4559
 
4560
 
4561
/**
4562
 * Represents a renderable reference assessment of an example submission
4563
 */
4564
class workshop_example_reference_assessment extends workshop_assessment implements renderable {
4565
 
4566
    /**
4567
     * @see parent::validate_raw_record()
4568
     */
4569
    protected function validate_raw_record(stdClass $record) {
4570
        if ($record->weight != 1) {
4571
            throw new coding_exception('Invalid weight of the reference example submission assessment');
4572
        }
4573
        parent::validate_raw_record($record);
4574
    }
4575
}
4576
 
4577
 
4578
/**
4579
 * Renderable message to be displayed to the user
4580
 *
4581
 * Message can contain an optional action link with a label that is supposed to be rendered
4582
 * as a button or a link.
4583
 *
4584
 * @see workshop::renderer::render_workshop_message()
4585
 */
4586
class workshop_message implements renderable {
4587
 
4588
    const TYPE_INFO     = 10;
4589
    const TYPE_OK       = 20;
4590
    const TYPE_ERROR    = 30;
4591
 
4592
    /** @var string */
4593
    protected $text = '';
4594
    /** @var int */
4595
    protected $type = self::TYPE_INFO;
4596
    /** @var moodle_url */
4597
    protected $actionurl = null;
4598
    /** @var string */
4599
    protected $actionlabel = '';
4600
 
4601
    /**
4602
     * @param string $text short text to be displayed
4603
     * @param string $type optional message type info|ok|error
4604
     */
4605
    public function __construct($text = null, $type = self::TYPE_INFO) {
4606
        $this->set_text($text);
4607
        $this->set_type($type);
4608
    }
4609
 
4610
    /**
4611
     * Sets the message text
4612
     *
4613
     * @param string $text short text to be displayed
4614
     */
4615
    public function set_text($text) {
4616
        $this->text = $text;
4617
    }
4618
 
4619
    /**
4620
     * Sets the message type
4621
     *
4622
     * @param int $type
4623
     */
4624
    public function set_type($type = self::TYPE_INFO) {
4625
        if (in_array($type, array(self::TYPE_OK, self::TYPE_ERROR, self::TYPE_INFO))) {
4626
            $this->type = $type;
4627
        } else {
4628
            throw new coding_exception('Unknown message type.');
4629
        }
4630
    }
4631
 
4632
    /**
4633
     * Sets the optional message action
4634
     *
4635
     * @param moodle_url $url to follow on action
4636
     * @param string $label action label
4637
     */
4638
    public function set_action(moodle_url $url, $label) {
4639
        $this->actionurl    = $url;
4640
        $this->actionlabel  = $label;
4641
    }
4642
 
4643
    /**
4644
     * Returns message text with HTML tags quoted
4645
     *
4646
     * @return string
4647
     */
4648
    public function get_message() {
4649
        return s($this->text);
4650
    }
4651
 
4652
    /**
4653
     * Returns message type
4654
     *
4655
     * @return int
4656
     */
4657
    public function get_type() {
4658
        return $this->type;
4659
    }
4660
 
4661
    /**
4662
     * Returns action URL
4663
     *
4664
     * @return moodle_url|null
4665
     */
4666
    public function get_action_url() {
4667
        return $this->actionurl;
4668
    }
4669
 
4670
    /**
4671
     * Returns action label
4672
     *
4673
     * @return string
4674
     */
4675
    public function get_action_label() {
4676
        return $this->actionlabel;
4677
    }
4678
}
4679
 
4680
 
4681
/**
4682
 * Renderable component containing all the data needed to display the grading report
4683
 */
4684
class workshop_grading_report implements renderable {
4685
 
4686
    /** @var stdClass returned by {@see workshop::prepare_grading_report_data()} */
4687
    protected $data;
4688
    /** @var stdClass rendering options */
4689
    protected $options;
4690
 
4691
    /**
4692
     * Grades in $data must be already rounded to the set number of decimals or must be null
4693
     * (in which later case, the [mod_workshop,nullgrade] string shall be displayed)
4694
     *
4695
     * @param stdClass $data prepared by {@link workshop::prepare_grading_report_data()}
4696
     * @param stdClass $options display options (showauthornames, showreviewernames, sortby, sorthow, showsubmissiongrade, showgradinggrade)
4697
     */
4698
    public function __construct(stdClass $data, stdClass $options) {
4699
        $this->data     = $data;
4700
        $this->options  = $options;
4701
    }
4702
 
4703
    /**
4704
     * @return stdClass grading report data
4705
     */
4706
    public function get_data() {
4707
        return $this->data;
4708
    }
4709
 
4710
    /**
4711
     * @return stdClass rendering options
4712
     */
4713
    public function get_options() {
4714
        return $this->options;
4715
    }
4716
 
4717
    /**
4718
     * Prepare the data to be exported to a external system via Web Services.
4719
     *
4720
     * This function applies extra capabilities checks.
4721
     * @return stdClass the data ready for external systems
4722
     */
4723
    public function export_data_for_external() {
4724
        $data = $this->get_data();
4725
        $options = $this->get_options();
4726
 
4727
        foreach ($data->grades as $reportdata) {
4728
            // If we are in submission phase ignore the following data.
4729
            if ($options->workshopphase == workshop::PHASE_SUBMISSION) {
4730
                unset($reportdata->submissiongrade);
4731
                unset($reportdata->gradinggrade);
4732
                unset($reportdata->submissiongradeover);
4733
                unset($reportdata->submissiongradeoverby);
4734
                unset($reportdata->submissionpublished);
4735
                unset($reportdata->reviewedby);
4736
                unset($reportdata->reviewerof);
4737
                continue;
4738
            }
4739
 
4740
            if (!$options->showsubmissiongrade) {
4741
                unset($reportdata->submissiongrade);
4742
                unset($reportdata->submissiongradeover);
4743
            }
4744
 
4745
            if (!$options->showgradinggrade and $tr == 0) {
4746
                unset($reportdata->gradinggrade);
4747
            }
4748
 
4749
            if (!$options->showreviewernames) {
4750
                foreach ($reportdata->reviewedby as $reviewedby) {
4751
                    $reviewedby->userid = 0;
4752
                }
4753
            }
4754
 
4755
            if (!$options->showauthornames) {
4756
                foreach ($reportdata->reviewerof as $reviewerof) {
4757
                    $reviewerof->userid = 0;
4758
                }
4759
            }
4760
        }
4761
 
4762
        return $data;
4763
    }
4764
}
4765
 
4766
 
4767
/**
4768
 * Base class for renderable feedback for author and feedback for reviewer
4769
 */
4770
abstract class workshop_feedback {
4771
 
4772
    /** @var stdClass the user info */
4773
    protected $provider = null;
4774
 
4775
    /** @var string the feedback text */
4776
    protected $content = null;
4777
 
4778
    /** @var int format of the feedback text */
4779
    protected $format = null;
4780
 
4781
    /**
4782
     * @return stdClass the user info
4783
     */
4784
    public function get_provider() {
4785
 
4786
        if (is_null($this->provider)) {
4787
            throw new coding_exception('Feedback provider not set');
4788
        }
4789
 
4790
        return $this->provider;
4791
    }
4792
 
4793
    /**
4794
     * @return string the feedback text
4795
     */
4796
    public function get_content() {
4797
 
4798
        if (is_null($this->content)) {
4799
            throw new coding_exception('Feedback content not set');
4800
        }
4801
 
4802
        return $this->content;
4803
    }
4804
 
4805
    /**
4806
     * @return int format of the feedback text
4807
     */
4808
    public function get_format() {
4809
 
4810
        if (is_null($this->format)) {
4811
            throw new coding_exception('Feedback text format not set');
4812
        }
4813
 
4814
        return $this->format;
4815
    }
4816
}
4817
 
4818
 
4819
/**
4820
 * Renderable feedback for the author of submission
4821
 */
4822
class workshop_feedback_author extends workshop_feedback implements renderable {
4823
 
4824
    /**
4825
     * Extracts feedback from the given submission record
4826
     *
4827
     * @param stdClass $submission record as returned by {@see self::get_submission_by_id()}
4828
     */
4829
    public function __construct(stdClass $submission) {
4830
 
4831
        $this->provider = user_picture::unalias($submission, null, 'gradeoverbyx', 'gradeoverby');
4832
        $this->content  = $submission->feedbackauthor;
4833
        $this->format   = $submission->feedbackauthorformat;
4834
    }
4835
}
4836
 
4837
 
4838
/**
4839
 * Renderable feedback for the reviewer
4840
 */
4841
class workshop_feedback_reviewer extends workshop_feedback implements renderable {
4842
 
4843
    /**
4844
     * Extracts feedback from the given assessment record
4845
     *
4846
     * @param stdClass $assessment record as returned by eg {@see self::get_assessment_by_id()}
4847
     */
4848
    public function __construct(stdClass $assessment) {
4849
 
4850
        $this->provider = user_picture::unalias($assessment, null, 'gradinggradeoverbyx', 'overby');
4851
        $this->content  = $assessment->feedbackreviewer;
4852
        $this->format   = $assessment->feedbackreviewerformat;
4853
    }
4854
}
4855
 
4856
 
4857
/**
4858
 * Holds the final grades for the activity as are stored in the gradebook
4859
 */
4860
class workshop_final_grades implements renderable {
4861
 
4862
    /** @var object the info from the gradebook about the grade for submission */
4863
    public $submissiongrade = null;
4864
 
4865
    /** @var object the infor from the gradebook about the grade for assessment */
4866
    public $assessmentgrade = null;
4867
}