Proyectos de Subversion Moodle

Rev

Rev 1254 | Ir a la última revisión | | 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
 * This file contains the definition for the class assignment
19
 *
20
 * This class provides all the functionality for the new assign module.
21
 *
22
 * @package   mod_assign
23
 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
24
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
// Assignment submission statuses.
30
define('ASSIGN_SUBMISSION_STATUS_NEW', 'new');
31
define('ASSIGN_SUBMISSION_STATUS_REOPENED', 'reopened');
32
define('ASSIGN_SUBMISSION_STATUS_DRAFT', 'draft');
33
define('ASSIGN_SUBMISSION_STATUS_SUBMITTED', 'submitted');
34
 
35
// Search filters for grading page.
36
define('ASSIGN_FILTER_NONE', 'none');
37
define('ASSIGN_FILTER_SUBMITTED', 'submitted');
38
define('ASSIGN_FILTER_NOT_SUBMITTED', 'notsubmitted');
39
define('ASSIGN_FILTER_SINGLE_USER', 'singleuser');
40
define('ASSIGN_FILTER_REQUIRE_GRADING', 'requiregrading');
41
define('ASSIGN_FILTER_GRANTED_EXTENSION', 'grantedextension');
42
define('ASSIGN_FILTER_DRAFT', 'draft');
43
 
44
// Marker filter for grading page.
45
define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
46
 
47
// Reopen attempt methods.
48
define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
49
define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
50
define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
51
 
52
// Special value means allow unlimited attempts.
53
define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
54
 
55
// Special value means no grade has been set.
56
define('ASSIGN_GRADE_NOT_SET', -1);
57
 
58
// Grading states.
59
define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
60
define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
61
 
62
// Marking workflow states.
63
define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
64
define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
65
define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
66
define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
67
define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
68
define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
69
 
70
/** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
71
define("ASSIGN_MAX_EVENT_LENGTH", "432000");
72
 
73
// Name of file area for intro attachments.
74
define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
75
 
76
// Name of file area for activity attachments.
77
define('ASSIGN_ACTIVITYATTACHMENT_FILEAREA', 'activityattachment');
78
 
79
// Event types.
80
define('ASSIGN_EVENT_TYPE_DUE', 'due');
81
define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
82
define('ASSIGN_EVENT_TYPE_OPEN', 'open');
83
define('ASSIGN_EVENT_TYPE_CLOSE', 'close');
84
 
85
require_once($CFG->libdir . '/accesslib.php');
86
require_once($CFG->libdir . '/formslib.php');
87
require_once($CFG->dirroot . '/repository/lib.php');
88
require_once($CFG->dirroot . '/mod/assign/mod_form.php');
89
require_once($CFG->libdir . '/gradelib.php');
90
require_once($CFG->dirroot . '/grade/grading/lib.php');
91
require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
92
require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
93
require_once($CFG->dirroot . '/mod/assign/renderable.php');
94
require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
95
require_once($CFG->libdir . '/portfolio/caller.php');
96
 
97
use mod_assign\event\submission_removed;
98
use mod_assign\event\submission_status_updated;
99
use \mod_assign\output\grading_app;
100
use \mod_assign\output\assign_header;
101
use \mod_assign\output\assign_submission_status;
102
use mod_assign\output\timelimit_panel;
103
use mod_assign\downloader;
104
 
105
/**
106
 * Standard base class for mod_assign (assignment types).
107
 *
108
 * @package   mod_assign
109
 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
110
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
111
 */
112
class assign {
113
 
114
    /** @var stdClass the assignment record that contains the global settings for this assign instance */
115
    private $instance;
116
 
117
    /** @var array $var array an array containing per-user assignment records, each having calculated properties (e.g. dates) */
118
    private $userinstances = [];
119
 
120
    /** @var grade_item the grade_item record for this assign instance's primary grade item. */
121
    private $gradeitem;
122
 
123
    /** @var context the context of the course module for this assign instance
124
     *               (or just the course if we are creating a new one)
125
     */
126
    private $context;
127
 
128
    /** @var stdClass the course this assign instance belongs to */
129
    private $course;
130
 
131
    /** @var stdClass the admin config for all assign instances  */
132
    private $adminconfig;
133
 
134
    /** @var assign_renderer the custom renderer for this module */
135
    private $output;
136
 
137
    /** @var cm_info the course module for this assign instance */
138
    private $coursemodule;
139
 
140
    /** @var array cache for things like the coursemodule name or the scale menu -
141
     *             only lives for a single request.
142
     */
143
    private $cache;
144
 
145
    /** @var array list of the installed submission plugins */
146
    private $submissionplugins;
147
 
148
    /** @var array list of the installed feedback plugins */
149
    private $feedbackplugins;
150
 
151
    /** @var string action to be used to return to this page
152
     *              (without repeating any form submissions etc).
153
     */
154
    private $returnaction = 'view';
155
 
156
    /** @var array params to be used to return to this page */
157
    private $returnparams = array();
158
 
159
    /** @var string modulename prevents excessive calls to get_string */
160
    private static $modulename = null;
161
 
162
    /** @var string modulenameplural prevents excessive calls to get_string */
163
    private static $modulenameplural = null;
164
 
165
    /** @var array of marking workflow states for the current user */
166
    private $markingworkflowstates = null;
167
 
168
    /** @var array of all marking workflow states */
169
    private $allmarkingworkflowstates = null;
170
 
171
    /** @var bool whether to exclude users with inactive enrolment */
172
    private $showonlyactiveenrol = null;
173
 
174
    /** @var string A key used to identify userlists created by this object. */
175
    private $useridlistid = null;
176
 
177
    /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
178
    private $participants = array();
179
 
180
    /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
181
    private $usersubmissiongroups = array();
182
 
183
    /** @var array cached list of user groups. The cache key will be the user. */
184
    private $usergroups = array();
185
 
186
    /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
187
    private $sharedgroupmembers = array();
188
 
189
    /**
190
     * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
191
     * to update the gradebook.
192
     */
193
    private $mostrecentteamsubmission = null;
194
 
195
    /** @var array Array of error messages encountered during the execution of assignment related operations. */
196
    private $errors = array();
197
 
198
    /** @var mixed This var can vary between false for no overrides to a stdClass of the overrides for a group */
199
    private $overridedata;
200
 
201
    /** @var float grade value. */
202
    public $grade;
203
 
204
    /**
205
     * Constructor for the base assign class.
206
     *
207
     * Note: For $coursemodule you can supply a stdclass if you like, but it
208
     * will be more efficient to supply a cm_info object.
209
     *
210
     * @param mixed $coursemodulecontext context|null the course module context
211
     *                                   (or the course context if the coursemodule has not been
212
     *                                   created yet).
213
     * @param mixed $coursemodule the current course module if it was already loaded,
214
     *                            otherwise this class will load one from the context as required.
215
     * @param mixed $course the current course  if it was already loaded,
216
     *                      otherwise this class will load one from the context as required.
217
     */
218
    public function __construct($coursemodulecontext, $coursemodule, $course) {
219
        $this->context = $coursemodulecontext;
220
        $this->course = $course;
221
 
222
        // Ensure that $this->coursemodule is a cm_info object (or null).
223
        $this->coursemodule = cm_info::create($coursemodule);
224
 
225
        // Temporary cache only lives for a single request - used to reduce db lookups.
226
        $this->cache = array();
227
 
228
        $this->submissionplugins = $this->load_plugins('assignsubmission');
229
        $this->feedbackplugins = $this->load_plugins('assignfeedback');
230
 
231
        // Extra entropy is required for uniqid() to work on cygwin.
232
        $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
233
    }
234
 
235
    /**
236
     * Set the action and parameters that can be used to return to the current page.
237
     *
238
     * @param string $action The action for the current page
239
     * @param array $params An array of name value pairs which form the parameters
240
     *                      to return to the current page.
241
     * @return void
242
     */
243
    public function register_return_link($action, $params) {
244
        global $PAGE;
245
        $params['action'] = $action;
246
        $cm = $this->get_course_module();
247
        if ($cm) {
248
            $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
249
        } else {
250
            $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
251
        }
252
 
253
        $currenturl->params($params);
254
        $PAGE->set_url($currenturl);
255
    }
256
 
257
    /**
258
     * Return an action that can be used to get back to the current page.
259
     *
260
     * @return string action
261
     */
262
    public function get_return_action() {
263
        global $PAGE;
264
 
265
        // Web services don't set a URL, we should avoid debugging when ussing the url object.
266
        if (!WS_SERVER) {
267
            $params = $PAGE->url->params();
268
        }
269
 
270
        if (!empty($params['action'])) {
271
            return $params['action'];
272
        }
273
        return '';
274
    }
275
 
276
    /**
277
     * Based on the current assignment settings should we display the intro.
278
     *
279
     * @return bool showintro
280
     */
281
    public function show_intro() {
282
        if ($this->get_instance()->alwaysshowdescription ||
283
                time() > $this->get_instance()->allowsubmissionsfromdate) {
284
            return true;
285
        }
286
        return false;
287
    }
288
 
289
    /**
290
     * Return a list of parameters that can be used to get back to the current page.
291
     *
292
     * @return array params
293
     */
294
    public function get_return_params() {
295
        global $PAGE;
296
 
297
        $params = array();
298
        if (!WS_SERVER) {
299
            $params = $PAGE->url->params();
300
        }
301
        unset($params['id']);
302
        unset($params['action']);
303
        return $params;
304
    }
305
 
306
    /**
307
     * Set the submitted form data.
308
     *
309
     * @param stdClass $data The form data (instance)
310
     */
311
    public function set_instance(stdClass $data) {
312
        $this->instance = $data;
313
    }
314
 
315
    /**
316
     * Set the context.
317
     *
318
     * @param context $context The new context
319
     */
320
    public function set_context(context $context) {
321
        $this->context = $context;
322
    }
323
 
324
    /**
325
     * Set the course data.
326
     *
327
     * @param stdClass $course The course data
328
     */
329
    public function set_course(stdClass $course) {
330
        $this->course = $course;
331
    }
332
 
333
    /**
334
     * Set error message.
335
     *
336
     * @param string $message The error message
337
     */
338
    protected function set_error_message(string $message) {
339
        $this->errors[] = $message;
340
    }
341
 
342
    /**
343
     * Get error messages.
344
     *
345
     * @return array The array of error messages
346
     */
347
    protected function get_error_messages(): array {
348
        return $this->errors;
349
    }
350
 
351
    /**
352
     * Get list of feedback plugins installed.
353
     *
354
     * @return array
355
     */
356
    public function get_feedback_plugins() {
357
        return $this->feedbackplugins;
358
    }
359
 
360
    /**
361
     * Get list of submission plugins installed.
362
     *
363
     * @return array
364
     */
365
    public function get_submission_plugins() {
366
        return $this->submissionplugins;
367
    }
368
 
369
    /**
370
     * Is blind marking enabled and reveal identities not set yet?
371
     *
372
     * @return bool
373
     */
374
    public function is_blind_marking() {
375
        return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
376
    }
377
 
378
    /**
379
     * Is hidden grading enabled?
380
     *
381
     * This just checks the assignment settings. Remember to check
382
     * the user has the 'showhiddengrader' capability too
383
     *
384
     * @return bool
385
     */
386
    public function is_hidden_grader() {
387
        return $this->get_instance()->hidegrader;
388
    }
389
 
390
    /**
391
     * Does an assignment have submission(s) or grade(s) already?
392
     *
393
     * @return bool
394
     */
395
    public function has_submissions_or_grades() {
396
        $allgrades = $this->count_grades();
397
        $allsubmissions = $this->count_submissions();
398
        if (($allgrades == 0) && ($allsubmissions == 0)) {
399
            return false;
400
        }
401
        return true;
402
    }
403
 
404
    /**
405
     * Get a specific submission plugin by its type.
406
     *
407
     * @param string $subtype assignsubmission | assignfeedback
408
     * @param string $type
409
     * @return mixed assign_plugin|null
410
     */
411
    public function get_plugin_by_type($subtype, $type) {
412
        $shortsubtype = substr($subtype, strlen('assign'));
413
        $name = $shortsubtype . 'plugins';
414
        if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
415
            return null;
416
        }
417
        $pluginlist = $this->$name;
418
        foreach ($pluginlist as $plugin) {
419
            if ($plugin->get_type() == $type) {
420
                return $plugin;
421
            }
422
        }
423
        return null;
424
    }
425
 
426
    /**
427
     * Get a feedback plugin by type.
428
     *
429
     * @param string $type - The type of plugin e.g comments
430
     * @return mixed assign_feedback_plugin|null
431
     */
432
    public function get_feedback_plugin_by_type($type) {
433
        return $this->get_plugin_by_type('assignfeedback', $type);
434
    }
435
 
436
    /**
437
     * Get a submission plugin by type.
438
     *
439
     * @param string $type - The type of plugin e.g comments
440
     * @return mixed assign_submission_plugin|null
441
     */
442
    public function get_submission_plugin_by_type($type) {
443
        return $this->get_plugin_by_type('assignsubmission', $type);
444
    }
445
 
446
    /**
447
     * Load the plugins from the sub folders under subtype.
448
     *
449
     * @param string $subtype - either submission or feedback
450
     * @return array - The sorted list of plugins
451
     */
452
    public function load_plugins($subtype) {
453
        global $CFG;
454
        $result = array();
455
 
456
        $names = core_component::get_plugin_list($subtype);
457
 
458
        foreach ($names as $name => $path) {
459
            if (file_exists($path . '/locallib.php')) {
460
                require_once($path . '/locallib.php');
461
 
462
                $shortsubtype = substr($subtype, strlen('assign'));
463
                $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
464
 
465
                $plugin = new $pluginclass($this, $name);
466
 
467
                if ($plugin instanceof assign_plugin) {
468
                    $idx = $plugin->get_sort_order();
469
                    while (array_key_exists($idx, $result)) {
470
                        $idx +=1;
471
                    }
472
                    $result[$idx] = $plugin;
473
                }
474
            }
475
        }
476
        ksort($result);
477
        return $result;
478
    }
479
 
480
    /**
481
     * Display the assignment, used by view.php
482
     *
483
     * The assignment is displayed differently depending on your role,
484
     * the settings for the assignment and the status of the assignment.
485
     *
486
     * @param string $action The current action if any.
487
     * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
488
     * @return string - The page output.
489
     */
490
    public function view($action='', $args = array()) {
491
        global $PAGE;
492
 
493
        $o = '';
494
        $mform = null;
495
        $notices = array();
496
        $nextpageparams = array();
497
 
498
        if (!empty($this->get_course_module()->id)) {
499
            $nextpageparams['id'] = $this->get_course_module()->id;
500
        }
501
 
502
        if (empty($action)) {
503
            $PAGE->add_body_class('limitedwidth');
504
        }
505
 
506
        // Handle form submissions first.
507
        if ($action == 'savesubmission') {
508
            $action = 'editsubmission';
509
            if ($this->process_save_submission($mform, $notices)) {
510
                $action = 'redirect';
511
                if ($this->can_grade()) {
512
                    $nextpageparams['action'] = 'grading';
513
                } else {
514
                    $nextpageparams['action'] = 'view';
515
                }
516
            }
517
        } else if ($action == 'editprevioussubmission') {
518
            $action = 'editsubmission';
519
            if ($this->process_copy_previous_attempt($notices)) {
520
                $action = 'redirect';
521
                $nextpageparams['action'] = 'editsubmission';
522
            }
523
        } else if ($action == 'lock') {
524
            $this->process_lock_submission();
525
            $action = 'redirect';
526
            $nextpageparams['action'] = 'grading';
527
        } else if ($action == 'removesubmission') {
528
            $this->process_remove_submission();
529
            $action = 'redirect';
530
            if ($this->can_grade()) {
531
                $nextpageparams['action'] = 'grading';
532
            } else {
533
                $nextpageparams['action'] = 'view';
534
            }
535
        } else if ($action == 'addattempt') {
536
            $this->process_add_attempt(required_param('userid', PARAM_INT));
537
            $action = 'redirect';
538
            $nextpageparams['action'] = 'grading';
539
        } else if ($action == 'reverttodraft') {
540
            $this->process_revert_to_draft();
541
            $action = 'redirect';
542
            $nextpageparams['action'] = 'grading';
543
        } else if ($action == 'unlock') {
544
            $this->process_unlock_submission();
545
            $action = 'redirect';
546
            $nextpageparams['action'] = 'grading';
547
        } else if ($action == 'setbatchmarkingworkflowstate') {
548
            $this->process_set_batch_marking_workflow_state();
549
            $action = 'redirect';
550
            $nextpageparams['action'] = 'grading';
551
        } else if ($action == 'setbatchmarkingallocation') {
552
            $this->process_set_batch_marking_allocation();
553
            $action = 'redirect';
554
            $nextpageparams['action'] = 'grading';
555
        } else if ($action == 'confirmsubmit') {
556
            $action = 'submit';
557
            if ($this->process_submit_for_grading($mform, $notices)) {
558
                $action = 'redirect';
559
                $nextpageparams['action'] = 'view';
560
            } else if ($notices) {
561
                $action = 'viewsubmitforgradingerror';
562
            }
563
        } else if ($action == 'submitotherforgrading') {
564
            if ($this->process_submit_other_for_grading($mform, $notices)) {
565
                $action = 'redirect';
566
                $nextpageparams['action'] = 'grading';
567
            } else {
568
                $action = 'viewsubmitforgradingerror';
569
            }
570
        } else if ($action == 'gradingbatchoperation') {
571
            $action = $this->process_grading_batch_operation($mform);
572
            if ($action == 'grading') {
573
                $action = 'redirect';
574
                $nextpageparams['action'] = 'grading';
575
            }
576
        } else if ($action == 'submitgrade') {
577
            if (optional_param('saveandshownext', null, PARAM_RAW)) {
578
                // Save and show next.
579
                $action = 'grade';
580
                if ($this->process_save_grade($mform)) {
581
                    $action = 'redirect';
582
                    $nextpageparams['action'] = 'grade';
583
                    $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
584
                    $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
585
                }
586
            } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
587
                $action = 'redirect';
588
                $nextpageparams['action'] = 'grade';
589
                $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
590
                $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
591
            } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
592
                $action = 'redirect';
593
                $nextpageparams['action'] = 'grade';
594
                $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
595
                $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
596
            } else if (optional_param('savegrade', null, PARAM_RAW)) {
597
                // Save changes button.
598
                $action = 'grade';
599
                if ($this->process_save_grade($mform)) {
600
                    $action = 'redirect';
601
                    $nextpageparams['action'] = 'savegradingresult';
602
                }
603
            } else {
604
                // Cancel button.
605
                $action = 'redirect';
606
                $nextpageparams['action'] = 'grading';
607
            }
608
        } else if ($action == 'quickgrade') {
609
            $message = $this->process_save_quick_grades();
610
            $action = 'quickgradingresult';
611
        } else if ($action == 'saveoptions') {
612
            $this->process_save_grading_options();
613
            $action = 'redirect';
614
            $nextpageparams['action'] = 'grading';
615
        } else if ($action == 'saveextension') {
616
            $action = 'grantextension';
617
            if ($this->process_save_extension($mform)) {
618
                $action = 'redirect';
619
                $nextpageparams['action'] = 'grading';
620
            }
621
        } else if ($action == 'revealidentitiesconfirm') {
622
            $this->process_reveal_identities();
623
            $action = 'redirect';
624
            $nextpageparams['action'] = 'grading';
625
        }
626
 
627
        $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
628
                              'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
629
        $this->register_return_link($action, $returnparams);
630
 
631
        // Include any page action as part of the body tag CSS id.
632
        if (!empty($action)) {
633
            $PAGE->set_pagetype('mod-assign-' . $action);
634
        }
635
        // Now show the right view page.
636
        if ($action == 'redirect') {
637
            $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
638
            $messages = '';
639
            $messagetype = \core\output\notification::NOTIFY_INFO;
640
            $errors = $this->get_error_messages();
641
            if (!empty($errors)) {
642
                $messages = html_writer::alist($errors, ['class' => 'mb-1 mt-1']);
643
                $messagetype = \core\output\notification::NOTIFY_ERROR;
644
            }
645
            redirect($nextpageurl, $messages, null, $messagetype);
646
            return;
647
        } else if ($action == 'savegradingresult') {
648
            $message = get_string('gradingchangessaved', 'assign');
649
            $o .= $this->view_savegrading_result($message);
650
        } else if ($action == 'quickgradingresult') {
651
            $mform = null;
652
            $o .= $this->view_quickgrading_result($message);
653
        } else if ($action == 'gradingpanel') {
654
            $o .= $this->view_single_grading_panel($args);
655
        } else if ($action == 'grade') {
656
            $o .= $this->view_single_grade_page($mform);
657
        } else if ($action == 'viewpluginassignfeedback') {
658
            $o .= $this->view_plugin_content('assignfeedback');
659
        } else if ($action == 'viewpluginassignsubmission') {
660
            $o .= $this->view_plugin_content('assignsubmission');
661
        } else if ($action == 'editsubmission') {
662
            $PAGE->add_body_class('limitedwidth');
663
            $o .= $this->view_edit_submission_page($mform, $notices);
664
        } else if ($action == 'grader') {
665
            $o .= $this->view_grader();
666
        } else if ($action == 'grading') {
667
            $o .= $this->view_grading_page();
668
        } else if ($action == 'downloadall') {
669
            $o .= $this->download_submissions();
670
        } else if ($action == 'submit') {
671
            $PAGE->add_body_class('limitedwidth');
672
            $o .= $this->check_submit_for_grading($mform);
673
        } else if ($action == 'grantextension') {
674
            $o .= $this->view_grant_extension($mform);
675
        } else if ($action == 'revealidentities') {
676
            $o .= $this->view_reveal_identities_confirm($mform);
677
        } else if ($action == 'removesubmissionconfirm') {
678
            $PAGE->add_body_class('limitedwidth');
679
            $o .= $this->view_remove_submission_confirm();
680
        } else if ($action == 'plugingradingbatchoperation') {
681
            $o .= $this->view_plugin_grading_batch_operation($mform);
682
        } else if ($action == 'viewpluginpage') {
683
             $o .= $this->view_plugin_page();
684
        } else if ($action == 'viewcourseindex') {
685
             $o .= $this->view_course_index();
686
        } else if ($action == 'viewbatchsetmarkingworkflowstate') {
687
             $o .= $this->view_batch_set_workflow_state($mform);
688
        } else if ($action == 'viewbatchmarkingallocation') {
689
            $o .= $this->view_batch_markingallocation($mform);
690
        } else if ($action == 'viewsubmitforgradingerror') {
691
            $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
692
        } else if ($action == 'fixrescalednullgrades') {
693
            $o .= $this->view_fix_rescaled_null_grades();
694
        } else {
695
            $PAGE->add_body_class('limitedwidth');
696
            $o .= $this->view_submission_page();
697
        }
698
 
699
        return $o;
700
    }
701
 
702
    /**
703
     * Add this instance to the database.
704
     *
705
     * @param stdClass $formdata The data submitted from the form
706
     * @param bool $callplugins This is used to skip the plugin code
707
     *             when upgrading an old assignment to a new one (the plugins get called manually)
708
     * @return mixed false if an error occurs or the int id of the new instance
709
     */
710
    public function add_instance(stdClass $formdata, $callplugins) {
711
        global $DB;
712
        $adminconfig = $this->get_admin_config();
713
 
714
        $err = '';
715
 
716
        // Add the database record.
717
        $update = new stdClass();
718
        $update->name = $formdata->name;
719
        $update->timemodified = time();
720
        $update->timecreated = time();
721
        $update->course = $formdata->course;
722
        $update->courseid = $formdata->course;
723
        $update->intro = $formdata->intro;
724
        $update->introformat = $formdata->introformat;
725
        $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
726
        if (isset($formdata->activityeditor)) {
727
            $update->activity = $this->save_editor_draft_files($formdata);
728
            $update->activityformat = $formdata->activityeditor['format'];
729
        }
730
        if (isset($formdata->submissionattachments)) {
731
            $update->submissionattachments = $formdata->submissionattachments;
732
        }
733
        $update->submissiondrafts = $formdata->submissiondrafts;
734
        $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
735
        $update->sendnotifications = $formdata->sendnotifications;
736
        $update->sendlatenotifications = $formdata->sendlatenotifications;
737
        $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
738
        if (isset($formdata->sendstudentnotifications)) {
739
            $update->sendstudentnotifications = $formdata->sendstudentnotifications;
740
        }
741
        $update->duedate = $formdata->duedate;
742
        $update->cutoffdate = $formdata->cutoffdate;
743
        $update->gradingduedate = $formdata->gradingduedate;
744
        if (isset($formdata->timelimit)) {
745
            $update->timelimit = $formdata->timelimit;
746
        }
747
        $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
748
        $update->grade = $formdata->grade;
749
        $update->completionsubmit = !empty($formdata->completionsubmit);
750
        $update->teamsubmission = $formdata->teamsubmission;
751
        $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
752
        if (isset($formdata->teamsubmissiongroupingid)) {
753
            $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
754
        }
755
        $update->blindmarking = $formdata->blindmarking;
756
        if (isset($formdata->hidegrader)) {
757
            $update->hidegrader = $formdata->hidegrader;
758
        }
759
        $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
760
        if (!empty($formdata->attemptreopenmethod)) {
761
            $update->attemptreopenmethod = $formdata->attemptreopenmethod;
762
        }
763
        if (!empty($formdata->maxattempts)) {
764
            $update->maxattempts = $formdata->maxattempts;
765
        }
766
        if (isset($formdata->preventsubmissionnotingroup)) {
767
            $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
768
        }
769
        $update->markingworkflow = $formdata->markingworkflow;
770
        $update->markingallocation = $formdata->markingallocation;
771
        if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
772
            $update->markingallocation = 0;
773
        }
774
        if (isset($formdata->markinganonymous)) {
775
            // If marking workflow is disabled, or anonymous submissions is disabled then make sure marking anonymous is disabled.
776
            if (empty($update->markingworkflow) || empty($update->blindmarking)) {
777
                $update->markinganonymous = 0;
778
            } else {
779
                $update->markinganonymous = $formdata->markinganonymous;
780
            }
781
        }
782
        $returnid = $DB->insert_record('assign', $update);
783
        $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
784
        // Cache the course record.
785
        $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
786
 
787
        $this->save_intro_draft_files($formdata);
788
        $this->save_editor_draft_files($formdata);
789
 
790
        if ($callplugins) {
791
            // Call save_settings hook for submission plugins.
792
            foreach ($this->submissionplugins as $plugin) {
793
                if (!$this->update_plugin_instance($plugin, $formdata)) {
794
                    throw new \moodle_exception($plugin->get_error());
795
                    return false;
796
                }
797
            }
798
            foreach ($this->feedbackplugins as $plugin) {
799
                if (!$this->update_plugin_instance($plugin, $formdata)) {
800
                    throw new \moodle_exception($plugin->get_error());
801
                    return false;
802
                }
803
            }
804
 
805
            // In the case of upgrades the coursemodule has not been set,
806
            // so we need to wait before calling these two.
807
            $this->update_calendar($formdata->coursemodule);
808
            if (!empty($formdata->completionexpected)) {
809
                \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance,
810
                        $formdata->completionexpected);
811
            }
812
            $this->update_gradebook(false, $formdata->coursemodule);
813
 
814
        }
815
 
816
        $update = new stdClass();
817
        $update->id = $this->get_instance()->id;
818
        $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
819
        $DB->update_record('assign', $update);
820
 
821
        return $returnid;
822
    }
823
 
824
    /**
825
     * Delete all grades from the gradebook for this assignment.
826
     *
827
     * @return bool
828
     */
829
    protected function delete_grades() {
830
        global $CFG;
831
 
832
        $result = grade_update('mod/assign',
833
                               $this->get_course()->id,
834
                               'mod',
835
                               'assign',
836
                               $this->get_instance()->id,
837
                               0,
838
                               null,
839
                               array('deleted'=>1));
840
        return $result == GRADE_UPDATE_OK;
841
    }
842
 
843
    /**
844
     * Delete this instance from the database.
845
     *
846
     * @return bool false if an error occurs
847
     */
848
    public function delete_instance() {
849
        global $DB;
850
        $result = true;
851
 
852
        foreach ($this->submissionplugins as $plugin) {
853
            if (!$plugin->delete_instance()) {
854
                throw new \moodle_exception($plugin->get_error());
855
                $result = false;
856
            }
857
        }
858
        foreach ($this->feedbackplugins as $plugin) {
859
            if (!$plugin->delete_instance()) {
860
                throw new \moodle_exception($plugin->get_error());
861
                $result = false;
862
            }
863
        }
864
 
865
        // Delete files associated with this assignment.
866
        $fs = get_file_storage();
867
        if (! $fs->delete_area_files($this->context->id) ) {
868
            $result = false;
869
        }
870
 
871
        $this->delete_all_overrides();
872
 
873
        // Delete_records will throw an exception if it fails - so no need for error checking here.
874
        $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
875
        $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
876
        $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
877
        $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
878
        $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
879
 
880
        // Delete items from the gradebook.
881
        if (! $this->delete_grades()) {
882
            $result = false;
883
        }
884
 
885
        // Delete the instance.
886
        // We must delete the module record after we delete the grade item.
887
        $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
888
 
889
        return $result;
890
    }
891
 
892
    /**
893
     * Deletes a assign override from the database and clears any corresponding calendar events
894
     *
895
     * @param int $overrideid The id of the override being deleted
896
     * @return bool true on success
897
     */
898
    public function delete_override($overrideid) {
899
        global $CFG, $DB;
900
 
901
        require_once($CFG->dirroot . '/calendar/lib.php');
902
 
903
        $cm = $this->get_course_module();
904
        if (empty($cm)) {
905
            $instance = $this->get_instance();
906
            $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course);
907
        }
908
 
909
        $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
910
 
911
        // Delete the events.
912
        $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
913
        if (isset($override->userid)) {
914
            $conds['userid'] = $override->userid;
915
            $cachekey = "{$cm->instance}_u_{$override->userid}";
916
        } else {
917
            $conds['groupid'] = $override->groupid;
918
            $cachekey = "{$cm->instance}_g_{$override->groupid}";
919
        }
920
        $events = $DB->get_records('event', $conds);
921
        foreach ($events as $event) {
922
            $eventold = calendar_event::load($event);
923
            $eventold->delete();
924
        }
925
 
926
        $DB->delete_records('assign_overrides', array('id' => $overrideid));
927
        cache::make('mod_assign', 'overrides')->delete($cachekey);
928
 
929
        // Set the common parameters for one of the events we will be triggering.
930
        $params = array(
931
            'objectid' => $override->id,
932
            'context' => context_module::instance($cm->id),
933
            'other' => array(
934
                'assignid' => $override->assignid
935
            )
936
        );
937
        // Determine which override deleted event to fire.
938
        if (!empty($override->userid)) {
939
            $params['relateduserid'] = $override->userid;
940
            $event = \mod_assign\event\user_override_deleted::create($params);
941
        } else {
942
            $params['other']['groupid'] = $override->groupid;
943
            $event = \mod_assign\event\group_override_deleted::create($params);
944
        }
945
 
946
        // Trigger the override deleted event.
947
        $event->add_record_snapshot('assign_overrides', $override);
948
        $event->trigger();
949
 
950
        return true;
951
    }
952
 
953
    /**
954
     * Deletes all assign overrides from the database and clears any corresponding calendar events
955
     */
956
    public function delete_all_overrides() {
957
        global $DB;
958
 
959
        $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id');
960
        foreach ($overrides as $override) {
961
            $this->delete_override($override->id);
962
        }
963
    }
964
 
965
    /**
966
     * Updates the assign properties with override information for a user.
967
     *
968
     * Algorithm:  For each assign setting, if there is a matching user-specific override,
969
     *   then use that otherwise, if there are group-specific overrides, return the most
970
     *   lenient combination of them.  If neither applies, leave the assign setting unchanged.
971
     *
972
     * @param int $userid The userid.
973
     */
974
    public function update_effective_access($userid) {
975
 
976
        $override = $this->override_exists($userid);
977
 
978
        // Merge with assign defaults.
979
        $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate', 'timelimit');
980
        foreach ($keys as $key) {
981
            if (isset($override->{$key})) {
982
                $this->get_instance($userid)->{$key} = $override->{$key};
983
            }
984
        }
985
 
986
    }
987
 
988
    /**
989
     * Returns whether an assign has any overrides.
990
     *
991
     * @return true if any, false if not
992
     */
993
    public function has_overrides() {
994
        global $DB;
995
 
996
        $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
997
 
998
        if ($override) {
999
            return true;
1000
        }
1001
 
1002
        return false;
1003
    }
1004
 
1005
    /**
1006
     * Returns user override
1007
     *
1008
     * Algorithm:  For each assign setting, if there is a matching user-specific override,
1009
     *   then use that otherwise, if there are group-specific overrides, use the one with the
1010
     *   lowest sort order. If neither applies, leave the assign setting unchanged.
1011
     *
1012
     * @param int $userid The userid.
1013
     * @return stdClass The override
1014
     */
1015
    public function override_exists($userid) {
1016
        global $DB;
1017
 
1018
        // Gets an assoc array containing the keys for defined user overrides only.
1019
        $getuseroverride = function($userid) use ($DB) {
1020
            $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
1021
            return $useroverride ? get_object_vars($useroverride) : [];
1022
        };
1023
 
1024
        // Gets an assoc array containing the keys for defined group overrides only.
1025
        $getgroupoverride = function($userid) use ($DB) {
1026
            $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
1027
 
1028
            if (empty($groupings[0])) {
1029
                return [];
1030
            }
1031
 
1032
            // Select all overrides that apply to the User's groups.
1033
            list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
1034
            $sql = "SELECT * FROM {assign_overrides}
1035
                    WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
1036
            $params[] = $this->get_instance()->id;
1037
            $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
1038
 
1039
            return $groupoverride ? get_object_vars($groupoverride) : [];
1040
        };
1041
 
1042
        // Later arguments clobber earlier ones with array_merge. The two helper functions
1043
        // return arrays containing keys for only the defined overrides. So we get the
1044
        // desired behaviour as per the algorithm.
1045
        return (object)array_merge(
1046
            ['timelimit' => null, 'duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
1047
            $getgroupoverride($userid),
1048
            $getuseroverride($userid)
1049
        );
1050
    }
1051
 
1052
    /**
1053
     * Check if the given calendar_event is either a user or group override
1054
     * event.
1055
     *
1056
     * @return bool
1057
     */
1058
    public function is_override_calendar_event(\calendar_event $event) {
1059
        global $DB;
1060
 
1061
        if (!isset($event->modulename)) {
1062
            return false;
1063
        }
1064
 
1065
        if ($event->modulename != 'assign') {
1066
            return false;
1067
        }
1068
 
1069
        if (!isset($event->instance)) {
1070
            return false;
1071
        }
1072
 
1073
        if (!isset($event->userid) && !isset($event->groupid)) {
1074
            return false;
1075
        }
1076
 
1077
        $overrideparams = [
1078
            'assignid' => $event->instance
1079
        ];
1080
 
1081
        if (isset($event->groupid)) {
1082
            $overrideparams['groupid'] = $event->groupid;
1083
        } else if (isset($event->userid)) {
1084
            $overrideparams['userid'] = $event->userid;
1085
        }
1086
 
1087
        if ($DB->get_record('assign_overrides', $overrideparams)) {
1088
            return true;
1089
        } else {
1090
            return false;
1091
        }
1092
    }
1093
 
1094
    /**
1095
     * This function calculates the minimum and maximum cutoff values for the timestart of
1096
     * the given event.
1097
     *
1098
     * It will return an array with two values, the first being the minimum cutoff value and
1099
     * the second being the maximum cutoff value. Either or both values can be null, which
1100
     * indicates there is no minimum or maximum, respectively.
1101
     *
1102
     * If a cutoff is required then the function must return an array containing the cutoff
1103
     * timestamp and error string to display to the user if the cutoff value is violated.
1104
     *
1105
     * A minimum and maximum cutoff return value will look like:
1106
     * [
1107
     *     [1505704373, 'The due date must be after the sbumission start date'],
1108
     *     [1506741172, 'The due date must be before the cutoff date']
1109
     * ]
1110
     *
1111
     * If the event does not have a valid timestart range then [false, false] will
1112
     * be returned.
1113
     *
1114
     * @param calendar_event $event The calendar event to get the time range for
1115
     * @return array
1116
     */
1117
    function get_valid_calendar_event_timestart_range(\calendar_event $event) {
1118
        $instance = $this->get_instance();
1119
        $submissionsfromdate = $instance->allowsubmissionsfromdate;
1120
        $cutoffdate = $instance->cutoffdate;
1121
        $duedate = $instance->duedate;
1122
        $gradingduedate = $instance->gradingduedate;
1123
        $mindate = null;
1124
        $maxdate = null;
1125
 
1126
        if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
1127
            // This check is in here because due date events are currently
1128
            // the only events that can be overridden, so we can save a DB
1129
            // query if we don't bother checking other events.
1130
            if ($this->is_override_calendar_event($event)) {
1131
                // This is an override event so there is no valid timestart
1132
                // range to set it to.
1133
                return [false, false];
1134
            }
1135
 
1136
            if ($submissionsfromdate) {
1137
                $mindate = [
1138
                    $submissionsfromdate,
1139
                    get_string('duedatevalidation', 'assign'),
1140
                ];
1141
            }
1142
 
1143
            if ($cutoffdate) {
1144
                $maxdate = [
1145
                    $cutoffdate,
1146
                    get_string('cutoffdatevalidation', 'assign'),
1147
                ];
1148
            }
1149
 
1150
            if ($gradingduedate) {
1151
                // If we don't have a cutoff date or we've got a grading due date
1152
                // that is earlier than the cutoff then we should use that as the
1153
                // upper limit for the due date.
1154
                if (!$cutoffdate || $gradingduedate < $cutoffdate) {
1155
                    $maxdate = [
1156
                        $gradingduedate,
1157
                        get_string('gradingdueduedatevalidation', 'assign'),
1158
                    ];
1159
                }
1160
            }
1161
        } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
1162
            if ($duedate) {
1163
                $mindate = [
1164
                    $duedate,
1165
                    get_string('gradingdueduedatevalidation', 'assign'),
1166
                ];
1167
            } else if ($submissionsfromdate) {
1168
                $mindate = [
1169
                    $submissionsfromdate,
1170
                    get_string('gradingduefromdatevalidation', 'assign'),
1171
                ];
1172
            }
1173
        }
1174
 
1175
        return [$mindate, $maxdate];
1176
    }
1177
 
1178
    /**
1179
     * Actual implementation of the reset course functionality, delete all the
1180
     * assignment submissions for course $data->courseid.
1181
     *
1182
     * @param stdClass $data the data submitted from the reset course.
1183
     * @return array status array
1184
     */
1185
    public function reset_userdata($data) {
1186
        global $CFG, $DB;
1187
 
1188
        $componentstr = get_string('modulenameplural', 'assign');
1189
        $status = array();
1190
 
1191
        $fs = get_file_storage();
1192
        if (!empty($data->reset_assign_submissions)) {
1193
            // Delete files associated with this assignment.
1194
            foreach ($this->submissionplugins as $plugin) {
1195
                $fileareas = array();
1196
                $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1197
                $fileareas = $plugin->get_file_areas();
1198
                foreach ($fileareas as $filearea => $notused) {
1199
                    $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1200
                }
1201
 
1202
                if (!$plugin->delete_instance()) {
1203
                    $status[] = array('component'=>$componentstr,
1204
                                      'item'=>get_string('deleteallsubmissions', 'assign'),
1205
                                      'error'=>$plugin->get_error());
1206
                }
1207
            }
1208
 
1209
            foreach ($this->feedbackplugins as $plugin) {
1210
                $fileareas = array();
1211
                $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1212
                $fileareas = $plugin->get_file_areas();
1213
                foreach ($fileareas as $filearea => $notused) {
1214
                    $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1215
                }
1216
 
1217
                if (!$plugin->delete_instance()) {
1218
                    $status[] = array('component'=>$componentstr,
1219
                                      'item'=>get_string('deleteallsubmissions', 'assign'),
1220
                                      'error'=>$plugin->get_error());
1221
                }
1222
            }
1223
 
1224
            $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
1225
            list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
1226
 
1227
            $DB->delete_records_select('assign_submission', "assignment $sql", $params);
1228
            $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
1229
 
1230
            $status[] = array('component'=>$componentstr,
1231
                              'item'=>get_string('deleteallsubmissions', 'assign'),
1232
                              'error'=>false);
1233
 
1234
            if (!empty($data->reset_gradebook_grades)) {
1235
                $DB->delete_records_select('assign_grades', "assignment $sql", $params);
1236
                // Remove all grades from gradebook.
1237
                require_once($CFG->dirroot.'/mod/assign/lib.php');
1238
                assign_reset_gradebook($data->courseid);
1239
            }
1240
 
1241
            // Reset revealidentities for assign if blindmarking is enabled.
1242
            if ($this->get_instance()->blindmarking) {
1243
                $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
1244
            }
1245
        }
1246
 
1247
        $purgeoverrides = false;
1248
 
1249
        // Remove user overrides.
1250
        if (!empty($data->reset_assign_user_overrides)) {
1251
            $DB->delete_records_select('assign_overrides',
1252
                'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
1253
            $status[] = array(
1254
                'component' => $componentstr,
1255
                'item' => get_string('useroverridesdeleted', 'assign'),
1256
                'error' => false);
1257
            $purgeoverrides = true;
1258
        }
1259
        // Remove group overrides.
1260
        if (!empty($data->reset_assign_group_overrides)) {
1261
            $DB->delete_records_select('assign_overrides',
1262
                'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
1263
            $status[] = array(
1264
                'component' => $componentstr,
1265
                'item' => get_string('groupoverridesdeleted', 'assign'),
1266
                'error' => false);
1267
            $purgeoverrides = true;
1268
        }
1269
 
1270
        // Updating dates - shift may be negative too.
1271
        if ($data->timeshift) {
1272
            $DB->execute("UPDATE {assign_overrides}
1273
                         SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
1274
                       WHERE assignid = ? AND allowsubmissionsfromdate <> 0",
1275
                array($data->timeshift, $this->get_instance()->id));
1276
            $DB->execute("UPDATE {assign_overrides}
1277
                         SET duedate = duedate + ?
1278
                       WHERE assignid = ? AND duedate <> 0",
1279
                array($data->timeshift, $this->get_instance()->id));
1280
            $DB->execute("UPDATE {assign_overrides}
1281
                         SET cutoffdate = cutoffdate + ?
1282
                       WHERE assignid =? AND cutoffdate <> 0",
1283
                array($data->timeshift, $this->get_instance()->id));
1284
 
1285
            $purgeoverrides = true;
1286
 
1287
            // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
1288
            // See MDL-9367.
1289
            shift_course_mod_dates('assign',
1290
                                    array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
1291
                                    $data->timeshift,
1292
                                    $data->courseid, $this->get_instance()->id);
1293
            $status[] = array('component'=>$componentstr,
1294
                              'item'=>get_string('datechanged'),
1295
                              'error'=>false);
1296
        }
1297
 
1298
        if ($purgeoverrides) {
1299
            cache::make('mod_assign', 'overrides')->purge();
1300
        }
1301
 
1302
        return $status;
1303
    }
1304
 
1305
    /**
1306
     * Update the settings for a single plugin.
1307
     *
1308
     * @param assign_plugin $plugin The plugin to update
1309
     * @param stdClass $formdata The form data
1310
     * @return bool false if an error occurs
1311
     */
1312
    protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
1313
        if ($plugin->is_visible()) {
1314
            $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1315
            if (!empty($formdata->$enabledname)) {
1316
                $plugin->enable();
1317
                if (!$plugin->save_settings($formdata)) {
1318
                    throw new \moodle_exception($plugin->get_error());
1319
                    return false;
1320
                }
1321
            } else {
1322
                $plugin->disable();
1323
            }
1324
        }
1325
        return true;
1326
    }
1327
 
1328
    /**
1329
     * Update the gradebook information for this assignment.
1330
     *
1331
     * @param bool $reset If true, will reset all grades in the gradbook for this assignment
1332
     * @param int $coursemoduleid This is required because it might not exist in the database yet
1333
     * @return bool
1334
     */
1335
    public function update_gradebook($reset, $coursemoduleid) {
1336
        global $CFG;
1337
 
1338
        require_once($CFG->dirroot.'/mod/assign/lib.php');
1339
        $assign = clone $this->get_instance();
1340
        $assign->cmidnumber = $coursemoduleid;
1341
 
1342
        // Set assign gradebook feedback plugin status (enabled and visible).
1343
        $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
1344
 
1345
        $param = null;
1346
        if ($reset) {
1347
            $param = 'reset';
1348
        }
1349
 
1350
        return assign_grade_item_update($assign, $param);
1351
    }
1352
 
1353
    /**
1354
     * Get the marking table page size
1355
     *
1356
     * @return integer
1357
     */
1358
    public function get_assign_perpage() {
1359
        $perpage = (int) get_user_preferences('assign_perpage', 10);
1360
        $adminconfig = $this->get_admin_config();
1361
        $maxperpage = -1;
1362
        if (isset($adminconfig->maxperpage)) {
1363
            $maxperpage = $adminconfig->maxperpage;
1364
        }
1365
        if (isset($maxperpage) &&
1366
            $maxperpage != -1 &&
1367
            ($perpage == -1 || $perpage > $maxperpage)) {
1368
            $perpage = $maxperpage;
1369
        }
1370
        return $perpage;
1371
    }
1372
 
1373
    /**
1374
     * Load and cache the admin config for this module.
1375
     *
1376
     * @return stdClass the plugin config
1377
     */
1378
    public function get_admin_config() {
1379
        if ($this->adminconfig) {
1380
            return $this->adminconfig;
1381
        }
1382
        $this->adminconfig = get_config('assign');
1383
        return $this->adminconfig;
1384
    }
1385
 
1386
    /**
1387
     * Update the calendar entries for this assignment.
1388
     *
1389
     * @param int $coursemoduleid - Required to pass this in because it might
1390
     *                              not exist in the database yet.
1391
     * @return bool
1392
     */
1393
    public function update_calendar($coursemoduleid) {
1394
        global $DB, $CFG;
1395
        require_once($CFG->dirroot.'/calendar/lib.php');
1396
 
1397
        // Special case for add_instance as the coursemodule has not been set yet.
1398
        $instance = $this->get_instance();
1399
 
1400
        // Start with creating the event.
1401
        $event = new stdClass();
1402
        $event->modulename  = 'assign';
1403
        $event->courseid = $instance->course;
1404
        $event->groupid = 0;
1405
        $event->userid  = 0;
1406
        $event->instance  = $instance->id;
1407
        $event->type = CALENDAR_EVENT_TYPE_ACTION;
1408
 
1409
        // Convert the links to pluginfile. It is a bit hacky but at this stage the files
1410
        // might not have been saved in the module area yet.
1411
        $intro = $instance->intro;
1412
        if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
1413
            $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
1414
        }
1415
 
1416
        // We need to remove the links to files as the calendar is not ready
1417
        // to support module events with file areas.
1418
        $intro = strip_pluginfile_content($intro);
1419
        if ($this->show_intro()) {
1420
            $event->description = array(
1421
                'text' => $intro,
1422
                'format' => $instance->introformat
1423
            );
1424
        } else {
1425
            $event->description = array(
1426
                'text' => '',
1427
                'format' => $instance->introformat
1428
            );
1429
        }
1430
 
1431
        $eventtype = ASSIGN_EVENT_TYPE_DUE;
1432
        if ($instance->duedate) {
1433
            $event->name = get_string('calendardue', 'assign', $instance->name);
1434
            $event->eventtype = $eventtype;
1435
            $event->timestart = $instance->duedate;
1436
            $event->timesort = $instance->duedate;
1437
            $select = "modulename = :modulename
1438
                       AND instance = :instance
1439
                       AND eventtype = :eventtype
1440
                       AND groupid = 0
1441
                       AND courseid <> 0";
1442
            $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
1443
            $event->id = $DB->get_field_select('event', 'id', $select, $params);
1444
 
1445
            // Now process the event.
1446
            if ($event->id) {
1447
                $calendarevent = calendar_event::load($event->id);
1448
                $calendarevent->update($event, false);
1449
            } else {
1450
                calendar_event::create($event, false);
1451
            }
1452
        } else {
1453
            $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1454
                'eventtype' => $eventtype));
1455
        }
1456
 
1457
        $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE;
1458
        if ($instance->gradingduedate) {
1459
            $event->name = get_string('calendargradingdue', 'assign', $instance->name);
1460
            $event->eventtype = $eventtype;
1461
            $event->timestart = $instance->gradingduedate;
1462
            $event->timesort = $instance->gradingduedate;
1463
            $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign',
1464
                'instance' => $instance->id, 'eventtype' => $event->eventtype));
1465
 
1466
            // Now process the event.
1467
            if ($event->id) {
1468
                $calendarevent = calendar_event::load($event->id);
1469
                $calendarevent->update($event, false);
1470
            } else {
1471
                calendar_event::create($event, false);
1472
            }
1473
        } else {
1474
            $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1475
                'eventtype' => $eventtype));
1476
        }
1477
 
1478
        return true;
1479
    }
1480
 
1481
    /**
1482
     * Update this instance in the database.
1483
     *
1484
     * @param stdClass $formdata - the data submitted from the form
1485
     * @return bool false if an error occurs
1486
     */
1487
    public function update_instance($formdata) {
1488
        global $DB;
1489
        $adminconfig = $this->get_admin_config();
1490
 
1491
        $update = new stdClass();
1492
        $update->id = $formdata->instance;
1493
        $update->name = $formdata->name;
1494
        $update->timemodified = time();
1495
        $update->course = $formdata->course;
1496
        $update->intro = $formdata->intro;
1497
        $update->introformat = $formdata->introformat;
1498
        $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
1499
        if (isset($formdata->activityeditor)) {
1500
            $update->activity = $this->save_editor_draft_files($formdata);
1501
            $update->activityformat = $formdata->activityeditor['format'];
1502
        }
1503
        if (isset($formdata->submissionattachments)) {
1504
            $update->submissionattachments = $formdata->submissionattachments;
1505
        }
1506
        $update->submissiondrafts = $formdata->submissiondrafts;
1507
        $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
1508
        $update->sendnotifications = $formdata->sendnotifications;
1509
        $update->sendlatenotifications = $formdata->sendlatenotifications;
1510
        $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
1511
        if (isset($formdata->sendstudentnotifications)) {
1512
            $update->sendstudentnotifications = $formdata->sendstudentnotifications;
1513
        }
1514
        $update->duedate = $formdata->duedate;
1515
        $update->cutoffdate = $formdata->cutoffdate;
1516
        if (isset($formdata->timelimit)) {
1517
            $update->timelimit = $formdata->timelimit;
1518
        }
1519
        $update->gradingduedate = $formdata->gradingduedate;
1520
        $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
1521
        $update->grade = $formdata->grade;
1522
        if (!empty($formdata->completionunlocked)) {
1523
            $update->completionsubmit = !empty($formdata->completionsubmit);
1524
        }
1525
        $update->teamsubmission = $formdata->teamsubmission;
1526
        $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
1527
        if (isset($formdata->teamsubmissiongroupingid)) {
1528
            $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
1529
        }
1530
        if (isset($formdata->hidegrader)) {
1531
            $update->hidegrader = $formdata->hidegrader;
1532
        }
1533
        $update->blindmarking = $formdata->blindmarking;
1534
        $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
1535
        if (!empty($formdata->attemptreopenmethod)) {
1536
            $update->attemptreopenmethod = $formdata->attemptreopenmethod;
1537
        }
1538
        if (!empty($formdata->maxattempts)) {
1539
            $update->maxattempts = $formdata->maxattempts;
1540
        }
1541
        if (isset($formdata->preventsubmissionnotingroup)) {
1542
            $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
1543
        }
1544
        $update->markingworkflow = $formdata->markingworkflow;
1545
        $update->markingallocation = $formdata->markingallocation;
1546
        if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
1547
            $update->markingallocation = 0;
1548
        }
1549
        $update->markinganonymous = $formdata->markinganonymous;
1550
        // If marking workflow is disabled, or blindmarking is disabled then make sure marking anonymous is disabled.
1551
        if (empty($update->markingworkflow) || empty($update->blindmarking)) {
1552
            $update->markinganonymous = 0;
1553
        }
1554
 
1555
        $result = $DB->update_record('assign', $update);
1556
        $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
1557
 
1558
        $this->save_intro_draft_files($formdata);
1559
 
1560
        // Load the assignment so the plugins have access to it.
1561
 
1562
        // Call save_settings hook for submission plugins.
1563
        foreach ($this->submissionplugins as $plugin) {
1564
            if (!$this->update_plugin_instance($plugin, $formdata)) {
1565
                throw new \moodle_exception($plugin->get_error());
1566
                return false;
1567
            }
1568
        }
1569
        foreach ($this->feedbackplugins as $plugin) {
1570
            if (!$this->update_plugin_instance($plugin, $formdata)) {
1571
                throw new \moodle_exception($plugin->get_error());
1572
                return false;
1573
            }
1574
        }
1575
 
1576
        $this->update_calendar($this->get_course_module()->id);
1577
        $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null;
1578
        \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance,
1579
                $completionexpected);
1580
        $this->update_gradebook(false, $this->get_course_module()->id);
1581
 
1582
        $update = new stdClass();
1583
        $update->id = $this->get_instance()->id;
1584
        $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1585
        $DB->update_record('assign', $update);
1586
 
1587
        return $result;
1588
    }
1589
 
1590
    /**
1591
     * Save the attachments in the intro description.
1592
     *
1593
     * @param stdClass $formdata
1594
     */
1595
    protected function save_intro_draft_files($formdata) {
1596
        if (isset($formdata->introattachments)) {
1597
            file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1598
                                       'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1599
        }
1600
    }
1601
 
1602
    /**
1603
     * Save the attachments in the editor description.
1604
     *
1605
     * @param stdClass $formdata
1606
     */
1607
    protected function save_editor_draft_files($formdata): string {
1608
        $text = '';
1609
        if (isset($formdata->activityeditor)) {
1610
            $text = $formdata->activityeditor['text'];
1611
            if (isset($formdata->activityeditor['itemid'])) {
1612
                $text = file_save_draft_area_files($formdata->activityeditor['itemid'], $this->get_context()->id,
1613
                    'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA,
1614
                    0, array('subdirs' => true), $formdata->activityeditor['text']);
1615
            }
1616
        }
1617
        return $text;
1618
    }
1619
 
1620
 
1621
    /**
1622
     * Add elements in grading plugin form.
1623
     *
1624
     * @param mixed $grade stdClass|null
1625
     * @param MoodleQuickForm $mform
1626
     * @param stdClass $data
1627
     * @param int $userid - The userid we are grading
1628
     * @return void
1629
     */
1630
    protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1631
        foreach ($this->feedbackplugins as $plugin) {
1632
            if ($plugin->is_enabled() && $plugin->is_visible()) {
1633
                $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1634
            }
1635
        }
1636
    }
1637
 
1638
 
1639
 
1640
    /**
1641
     * Add one plugins settings to edit plugin form.
1642
     *
1643
     * @param assign_plugin $plugin The plugin to add the settings from
1644
     * @param MoodleQuickForm $mform The form to add the configuration settings to.
1645
     *                               This form is modified directly (not returned).
1646
     * @param array $pluginsenabled A list of form elements to be added to a group.
1647
     *                              The new element is added to this array by this function.
1648
     * @return void
1649
     */
1650
    protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1651
        global $CFG;
1652
        if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1653
            $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1654
            $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1655
            $mform->setType($name, PARAM_BOOL);
1656
            $plugin->get_settings($mform);
1657
        } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1658
            $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1659
            $label = $plugin->get_name();
1660
            $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1661
            $helpicon = $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1662
            $pluginsenabled[] = $mform->createElement('static', '', '', $helpicon);
1663
 
1664
            $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1665
            if ($plugin->get_config('enabled') !== false) {
1666
                $default = $plugin->is_enabled();
1667
            }
1668
            $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1669
 
1670
            $plugin->get_settings($mform);
1671
 
1672
        }
1673
    }
1674
 
1675
    /**
1676
     * Add settings to edit plugin form.
1677
     *
1678
     * @param MoodleQuickForm $mform The form to add the configuration settings to.
1679
     *                               This form is modified directly (not returned).
1680
     * @return void
1681
     */
1682
    public function add_all_plugin_settings(MoodleQuickForm $mform) {
1683
        $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1684
 
1685
        $submissionpluginsenabled = array();
1686
        $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1687
        foreach ($this->submissionplugins as $plugin) {
1688
            $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1689
        }
1690
        $group->setElements($submissionpluginsenabled);
1691
 
1692
        $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1693
        $feedbackpluginsenabled = array();
1694
        $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1695
        foreach ($this->feedbackplugins as $plugin) {
1696
            $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1697
        }
1698
        $group->setElements($feedbackpluginsenabled);
1699
        $mform->setExpanded('submissiontypes');
1700
    }
1701
 
1702
    /**
1703
     * Allow each plugin an opportunity to update the defaultvalues
1704
     * passed in to the settings form (needed to set up draft areas for
1705
     * editor and filemanager elements)
1706
     *
1707
     * @param array $defaultvalues
1708
     */
1709
    public function plugin_data_preprocessing(&$defaultvalues) {
1710
        foreach ($this->submissionplugins as $plugin) {
1711
            if ($plugin->is_visible()) {
1712
                $plugin->data_preprocessing($defaultvalues);
1713
            }
1714
        }
1715
        foreach ($this->feedbackplugins as $plugin) {
1716
            if ($plugin->is_visible()) {
1717
                $plugin->data_preprocessing($defaultvalues);
1718
            }
1719
        }
1720
    }
1721
 
1722
    /**
1723
     * Get the name of the current module.
1724
     *
1725
     * @return string the module name (Assignment)
1726
     */
1727
    protected function get_module_name() {
1728
        if (isset(self::$modulename)) {
1729
            return self::$modulename;
1730
        }
1731
        self::$modulename = get_string('modulename', 'assign');
1732
        return self::$modulename;
1733
    }
1734
 
1735
    /**
1736
     * Get the plural name of the current module.
1737
     *
1738
     * @return string the module name plural (Assignments)
1739
     */
1740
    protected function get_module_name_plural() {
1741
        if (isset(self::$modulenameplural)) {
1742
            return self::$modulenameplural;
1743
        }
1744
        self::$modulenameplural = get_string('modulenameplural', 'assign');
1745
        return self::$modulenameplural;
1746
    }
1747
 
1748
    /**
1749
     * Has this assignment been constructed from an instance?
1750
     *
1751
     * @return bool
1752
     */
1753
    public function has_instance() {
1754
        return $this->instance || $this->get_course_module();
1755
    }
1756
 
1757
    /**
1758
     * Get the settings for the current instance of this assignment.
1759
     *
1760
     * @return stdClass The settings
1761
     * @throws dml_exception
1762
     */
1763
    public function get_default_instance() {
1764
        global $DB;
1765
        if (!$this->instance && $this->get_course_module()) {
1766
            $params = array('id' => $this->get_course_module()->instance);
1767
            $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1768
 
1769
            $this->userinstances = [];
1770
        }
1771
        return $this->instance;
1772
    }
1773
 
1774
    /**
1775
     * Get the settings for the current instance of this assignment
1776
     * @param int|null $userid the id of the user to load the assign instance for.
1777
     * @return stdClass The settings
1778
     */
1779
    public function get_instance(int $userid = null): stdClass {
1780
        global $USER;
1781
        $userid = $userid ?? $USER->id;
1782
 
1783
        $this->instance = $this->get_default_instance();
1784
 
1785
        // If we have the user instance already, just return it.
1786
        if (isset($this->userinstances[$userid])) {
1787
            return $this->userinstances[$userid];
1788
        }
1789
 
1790
        // Calculate properties which vary per user.
1791
        $this->userinstances[$userid] = $this->calculate_properties($this->instance, $userid);
1792
        return $this->userinstances[$userid];
1793
    }
1794
 
1795
    /**
1796
     * Calculates and updates various properties based on the specified user.
1797
     *
1798
     * @param stdClass $record the raw assign record.
1799
     * @param int $userid the id of the user to calculate the properties for.
1800
     * @return stdClass a new record having calculated properties.
1801
     */
1802
    private function calculate_properties(\stdClass $record, int $userid): \stdClass {
1803
        $record = clone ($record);
1804
 
1805
        // Relative dates.
1806
        if (!empty($record->duedate)) {
1807
            $course = $this->get_course();
1808
            $usercoursedates = course_get_course_dates_for_user_id($course, $userid);
1809
            if ($usercoursedates['start']) {
1810
                $userprops = ['duedate' => $record->duedate + $usercoursedates['startoffset']];
1811
                $record = (object) array_merge((array) $record, (array) $userprops);
1812
            }
1813
        }
1814
        return $record;
1815
    }
1816
 
1817
    /**
1818
     * Get the primary grade item for this assign instance.
1819
     *
1820
     * @return grade_item The grade_item record
1821
     */
1822
    public function get_grade_item() {
1823
        if ($this->gradeitem) {
1824
            return $this->gradeitem;
1825
        }
1826
        $instance = $this->get_instance();
1827
        $params = array('itemtype' => 'mod',
1828
                        'itemmodule' => 'assign',
1829
                        'iteminstance' => $instance->id,
1830
                        'courseid' => $instance->course,
1831
                        'itemnumber' => 0);
1832
        $this->gradeitem = grade_item::fetch($params);
1833
        if (!$this->gradeitem) {
1834
            throw new coding_exception('Improper use of the assignment class. ' .
1835
                                       'Cannot load the grade item.');
1836
        }
1837
        return $this->gradeitem;
1838
    }
1839
 
1840
    /**
1841
     * Get the context of the current course.
1842
     *
1843
     * @return mixed context|null The course context
1844
     */
1845
    public function get_course_context() {
1846
        if (!$this->context && !$this->course) {
1847
            throw new coding_exception('Improper use of the assignment class. ' .
1848
                                       'Cannot load the course context.');
1849
        }
1850
        if ($this->context) {
1851
            return $this->context->get_course_context();
1852
        } else {
1853
            return context_course::instance($this->course->id);
1854
        }
1855
    }
1856
 
1857
 
1858
    /**
1859
     * Get the current course module.
1860
     *
1861
     * @return cm_info|null The course module or null if not known
1862
     */
1863
    public function get_course_module() {
1864
        if ($this->coursemodule) {
1865
            return $this->coursemodule;
1866
        }
1867
        if (!$this->context) {
1868
            return null;
1869
        }
1870
 
1871
        if ($this->context->contextlevel == CONTEXT_MODULE) {
1872
            $modinfo = get_fast_modinfo($this->get_course());
1873
            $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1874
            return $this->coursemodule;
1875
        }
1876
        return null;
1877
    }
1878
 
1879
    /**
1880
     * Get context module.
1881
     *
1882
     * @return context
1883
     */
1884
    public function get_context() {
1885
        return $this->context;
1886
    }
1887
 
1888
    /**
1889
     * Get the current course.
1890
     *
1891
     * @return mixed stdClass|null The course
1892
     */
1893
    public function get_course() {
1894
        global $DB;
1895
 
1896
        if ($this->course && is_object($this->course)) {
1897
            return $this->course;
1898
        }
1899
 
1900
        if (!$this->context) {
1901
            return null;
1902
        }
1903
        $params = array('id' => $this->get_course_context()->instanceid);
1904
        $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1905
 
1906
        return $this->course;
1907
    }
1908
 
1909
    /**
1910
     * Count the number of intro attachments.
1911
     *
1912
     * @return int
1913
     */
1914
    protected function count_attachments() {
1915
 
1916
        $fs = get_file_storage();
1917
        $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1918
                        0, 'id', false);
1919
 
1920
        return count($files);
1921
    }
1922
 
1923
    /**
1924
     * Are there any intro attachments to display?
1925
     *
1926
     * @return boolean
1927
     */
1928
    protected function has_visible_attachments() {
1929
        return ($this->count_attachments() > 0);
1930
    }
1931
 
1932
    /**
1933
     * Check if the intro attachments should be provided to the user.
1934
     *
1935
     * @param int $userid User id.
1936
     * @return bool
1937
     */
1938
    public function should_provide_intro_attachments(int $userid): bool {
1939
        $instance = $this->get_instance($userid);
1940
 
1941
        // Check if user has permission to view attachments regardless of assignment settings.
1942
        if (has_capability('moodle/course:manageactivities', $this->get_context())) {
1943
            return true;
1944
        }
1945
 
1946
        // If assignment does not show intro, we never provide intro attachments.
1947
        if (!$this->show_intro()) {
1948
            return false;
1949
        }
1950
 
1951
        // If intro attachments should only be shown when submission is started, check if there is an open submission.
1952
        if (!empty($instance->submissionattachments) && !$this->submissions_open($userid, true)) {
1953
            return false;
1954
        }
1955
 
1956
        return true;
1957
    }
1958
 
1959
    /**
1960
     * Return a grade in user-friendly form, whether it's a scale or not.
1961
     *
1962
     * @param mixed $grade int|null
1963
     * @param boolean $editing Are we allowing changes to this grade?
1964
     * @param int $userid The user id the grade belongs to
1965
     * @param int $modified Timestamp from when the grade was last modified
1966
     * @return string User-friendly representation of grade
1967
     */
1968
    public function display_grade($grade, $editing, $userid=0, $modified=0) {
1969
        global $DB;
1970
 
1971
        static $scalegrades = array();
1972
 
1973
        $o = '';
1974
 
1975
        if ($this->get_instance()->grade >= 0) {
1976
            // Normal number.
1977
            if ($editing && $this->get_instance()->grade > 0) {
1978
                if ($grade < 0) {
1979
                    $displaygrade = '';
1980
                } else {
1981
                    $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals());
1982
                }
1983
                $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
1984
                       get_string('usergrade', 'assign') .
1985
                       '</label>';
1986
                $o .= '<input type="text"
1987
                              id="quickgrade_' . $userid . '"
1988
                              name="quickgrade_' . $userid . '"
1989
                              value="' .  $displaygrade . '"
1990
                              size="6"
1991
                              maxlength="10"
1992
                              class="quickgrade"/>';
1993
                $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
1994
                return $o;
1995
            } else {
1996
                if ($grade == -1 || $grade === null) {
1997
                    $o .= '-';
1998
                } else {
1999
                    $item = $this->get_grade_item();
2000
                    $o .= grade_format_gradevalue($grade, $item);
2001
                    if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
2002
                        // If displaying the raw grade, also display the total value.
2003
                        $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $item->get_decimals());
2004
                    }
2005
                }
2006
                return $o;
2007
            }
2008
 
2009
        } else {
2010
            // Scale.
2011
            if (empty($this->cache['scale'])) {
2012
                if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
2013
                    $this->cache['scale'] = make_menu_from_list($scale->scale);
2014
                } else {
2015
                    $o .= '-';
2016
                    return $o;
2017
                }
2018
            }
2019
            if ($editing) {
2020
                $o .= '<label class="accesshide"
2021
                              for="quickgrade_' . $userid . '">' .
2022
                      get_string('usergrade', 'assign') .
2023
                      '</label>';
2024
                $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
2025
                $o .= '<option value="-1">' . get_string('nograde') . '</option>';
2026
                foreach ($this->cache['scale'] as $optionid => $option) {
2027
                    $selected = '';
2028
                    if ($grade == $optionid) {
2029
                        $selected = 'selected="selected"';
2030
                    }
2031
                    $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
2032
                }
2033
                $o .= '</select>';
2034
                return $o;
2035
            } else {
2036
                $scaleid = (int)$grade;
2037
                if (isset($this->cache['scale'][$scaleid])) {
2038
                    $o .= $this->cache['scale'][$scaleid];
2039
                    return $o;
2040
                }
2041
                $o .= '-';
2042
                return $o;
2043
            }
2044
        }
2045
    }
2046
 
2047
    /**
2048
     * Get the submission status/grading status for all submissions in this assignment for the
2049
     * given paticipants.
2050
     *
2051
     * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
2052
     * If this is a group assignment, group info is also returned.
2053
     *
2054
     * @param array $participants an associative array where the key is the participant id and
2055
     *                            the value is the participant record.
2056
     * @return array an associative array where the key is the participant id and the value is
2057
     *               the participant record.
2058
     */
2059
    private function get_submission_info_for_participants($participants) {
2060
        global $DB;
2061
 
2062
        if (empty($participants)) {
2063
            return $participants;
2064
        }
2065
 
2066
        list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
2067
 
2068
        $assignid = $this->get_instance()->id;
2069
        $params['assignmentid1'] = $assignid;
2070
        $params['assignmentid2'] = $assignid;
2071
        $params['assignmentid3'] = $assignid;
2072
 
2073
        $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate';
2074
        $from = ' FROM {user} u
2075
                         LEFT JOIN {assign_submission} s
2076
                                ON u.id = s.userid
2077
                               AND s.assignment = :assignmentid1
2078
                               AND s.latest = 1
2079
                         LEFT JOIN {assign_grades} g
2080
                                ON u.id = g.userid
2081
                               AND g.assignment = :assignmentid2
2082
                               AND g.attemptnumber = s.attemptnumber
2083
                         LEFT JOIN {assign_user_flags} uf
2084
                                ON u.id = uf.userid
2085
                               AND uf.assignment = :assignmentid3
2086
            ';
2087
        $where = ' WHERE u.id ' . $insql;
2088
 
2089
        if (!empty($this->get_instance()->blindmarking)) {
2090
            $from .= 'LEFT JOIN {assign_user_mapping} um
2091
                             ON u.id = um.userid
2092
                            AND um.assignment = :assignmentid4 ';
2093
            $params['assignmentid4'] = $assignid;
2094
            $fields .= ', um.id as recordid ';
2095
        }
2096
 
2097
        $sql = "$fields $from $where";
2098
 
2099
        $records = $DB->get_records_sql($sql, $params);
2100
 
2101
        if ($this->get_instance()->teamsubmission) {
2102
            // Get all groups.
2103
            $allgroups = groups_get_all_groups($this->get_course()->id,
2104
                                               array_keys($participants),
2105
                                               $this->get_instance()->teamsubmissiongroupingid,
2106
                                               'DISTINCT g.id, g.name');
2107
 
2108
        }
2109
        foreach ($participants as $userid => $participant) {
2110
            $participants[$userid]->fullname = $this->fullname($participant);
2111
            $participants[$userid]->submitted = false;
2112
            $participants[$userid]->requiregrading = false;
2113
            $participants[$userid]->grantedextension = false;
2114
            $participants[$userid]->submissionstatus = '';
2115
        }
2116
 
2117
        foreach ($records as $userid => $submissioninfo) {
2118
            // These filters are 100% the same as the ones in the grading table SQL.
2119
            $submitted = false;
2120
            $requiregrading = false;
2121
            $grantedextension = false;
2122
            $submissionstatus = !empty($submissioninfo->status) ? $submissioninfo->status : '';
2123
 
2124
            if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2125
                $submitted = true;
2126
            }
2127
 
2128
            if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime ||
2129
                    empty($submissioninfo->gtime) ||
2130
                    $submissioninfo->grade === null)) {
2131
                $requiregrading = true;
2132
            }
2133
 
2134
            if (!empty($submissioninfo->extensionduedate)) {
2135
                $grantedextension = true;
2136
            }
2137
 
2138
            $participants[$userid]->submitted = $submitted;
2139
            $participants[$userid]->requiregrading = $requiregrading;
2140
            $participants[$userid]->grantedextension = $grantedextension;
2141
            $participants[$userid]->submissionstatus = $submissionstatus;
2142
            if ($this->get_instance()->teamsubmission) {
2143
                $group = $this->get_submission_group($userid);
2144
                if ($group) {
2145
                    $participants[$userid]->groupid = $group->id;
2146
                    $participants[$userid]->groupname = $group->name;
2147
                }
2148
            }
2149
        }
2150
        return $participants;
2151
    }
2152
 
2153
    /**
2154
     * Get the submission status/grading status for all submissions in this assignment.
2155
     * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
2156
     * If this is a group assignment, group info is also returned.
2157
     *
2158
     * @param int $currentgroup
2159
     * @param boolean $tablesort Apply current user table sorting preferences.
2160
     * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension',
2161
     *               'groupid', 'groupname'
2162
     */
2163
    public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) {
2164
        $participants = $this->list_participants($currentgroup, false, $tablesort);
2165
 
2166
        if (empty($participants)) {
2167
            return $participants;
2168
        } else {
2169
            return $this->get_submission_info_for_participants($participants);
2170
        }
2171
    }
2172
 
2173
    /**
2174
     * Return a valid order by segment for list_participants that matches
2175
     * the sorting of the current grading table. Not every field is supported,
2176
     * we are only concerned with a list of users so we can't search on anything
2177
     * that is not part of the user information (like grading statud or last modified stuff).
2178
     *
2179
     * @return string Order by clause for list_participants
2180
     */
2181
    private function get_grading_sort_sql() {
2182
        $usersort = flexible_table::get_sort_for_table('mod_assign_grading');
2183
        // TODO Does not support custom user profile fields (MDL-70456).
2184
        $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_userpic();
2185
        $userfields = $userfieldsapi->get_required_fields();
2186
        $orderfields = explode(',', $usersort);
2187
        $validlist = [];
2188
 
2189
        foreach ($orderfields as $orderfield) {
2190
            $orderfield = trim($orderfield);
2191
            foreach ($userfields as $field) {
2192
                $parts = explode(' ', $orderfield);
2193
                if ($parts[0] == $field) {
2194
                    // Prepend the user table prefix and count this as a valid order field.
2195
                    array_push($validlist, 'u.' . $orderfield);
2196
                }
2197
            }
2198
        }
2199
        // Produce a final list.
2200
        $result = implode(',', $validlist);
2201
        if (empty($result)) {
2202
            // Fall back ordering when none has been set.
2203
            $result = 'u.lastname, u.firstname, u.id';
2204
        }
2205
 
2206
        return $result;
2207
    }
2208
 
2209
    /**
2210
     * Returns array with sql code and parameters returning all ids of users who have submitted an assignment.
2211
     *
2212
     * @param int $group The group that the query is for.
2213
     * @return array list($sql, $params)
2214
     */
2215
    protected function get_submitted_sql($group = 0) {
2216
        // We need to guarentee unique table names.
2217
        static $i = 0;
2218
        $i++;
2219
        $prefix = 'sa' . $i . '_';
2220
        $params = [
2221
            "{$prefix}assignment" => (int) $this->get_instance()->id,
2222
            "{$prefix}status" => ASSIGN_SUBMISSION_STATUS_NEW,
2223
        ];
2224
        $capjoin = get_enrolled_with_capabilities_join($this->context, $prefix, '', $group, $this->show_only_active_users());
2225
        $params += $capjoin->params;
2226
        $sql = "SELECT {$prefix}s.userid
2227
                  FROM {assign_submission} {$prefix}s
2228
                  JOIN {user} {$prefix}u ON {$prefix}u.id = {$prefix}s.userid
2229
                  $capjoin->joins
2230
                 WHERE {$prefix}s.assignment = :{$prefix}assignment
2231
                   AND {$prefix}s.status <> :{$prefix}status
2232
                   AND $capjoin->wheres";
2233
        return array($sql, $params);
2234
    }
2235
 
2236
    /**
2237
     * Load a list of users enrolled in the current course with the specified permission and group.
2238
     * 0 for no group.
2239
     * Apply any current sort filters from the grading table.
2240
     *
2241
     * @param int $currentgroup
2242
     * @param bool $idsonly
2243
     * @param bool $tablesort
2244
     * @return array List of user records
2245
     */
2246
    public function list_participants($currentgroup, $idsonly, $tablesort = false) {
2247
        global $DB, $USER;
2248
 
2249
        // Get the last known sort order for the grading table.
2250
 
2251
        if (empty($currentgroup)) {
2252
            $currentgroup = 0;
2253
        }
2254
 
2255
        $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
2256
        if (!isset($this->participants[$key])) {
2257
            list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup,
2258
                    $this->show_only_active_users());
2259
            list($ssql, $sparams) = $this->get_submitted_sql($currentgroup);
2260
            $params += $sparams;
2261
 
2262
            $fields = 'u.*';
2263
            $orderby = 'u.lastname, u.firstname, u.id';
2264
 
2265
            $additionaljoins = '';
2266
            $additionalfilters = '';
2267
            $instance = $this->get_instance();
2268
            if (!empty($instance->blindmarking)) {
2269
                $additionaljoins .= " LEFT JOIN {assign_user_mapping} um
2270
                                  ON u.id = um.userid
2271
                                 AND um.assignment = :assignmentid1
2272
                           LEFT JOIN {assign_submission} s
2273
                                  ON u.id = s.userid
2274
                                 AND s.assignment = :assignmentid2
2275
                                 AND s.latest = 1
2276
                        ";
2277
                $params['assignmentid1'] = (int) $instance->id;
2278
                $params['assignmentid2'] = (int) $instance->id;
2279
                $fields .= ', um.id as recordid ';
2280
 
2281
                // Sort by submission time first, then by um.id to sort reliably by the blind marking id.
2282
                // Note, different DBs have different ordering of NULL values.
2283
                // Therefore we coalesce the current time into the timecreated field, and the max possible integer into
2284
                // the ID field.
2285
                if (empty($tablesort)) {
2286
                    $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC";
2287
                }
2288
            }
2289
 
2290
            if ($instance->markingworkflow &&
2291
                    $instance->markingallocation &&
2292
                    !has_capability('mod/assign:manageallocations', $this->get_context()) &&
2293
                    has_capability('mod/assign:grade', $this->get_context())) {
2294
 
2295
                $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf
2296
                                     ON u.id = uf.userid
2297
                                     AND uf.assignment = :assignmentid3';
2298
 
2299
                $params['assignmentid3'] = (int) $instance->id;
2300
 
2301
                $additionalfilters .= ' AND uf.allocatedmarker = :markerid';
2302
                $params['markerid'] = $USER->id;
2303
            }
2304
 
2305
            $sql = "SELECT $fields
2306
                      FROM {user} u
2307
                      JOIN ($esql UNION $ssql) je ON je.id = u.id
2308
                           $additionaljoins
2309
                     WHERE u.deleted = 0
2310
                           $additionalfilters
2311
                  ORDER BY $orderby";
2312
 
2313
            $users = $DB->get_records_sql($sql, $params);
2314
 
2315
            $cm = $this->get_course_module();
2316
            $info = new \core_availability\info_module($cm);
2317
            $users = $info->filter_user_list($users);
2318
 
2319
            $this->participants[$key] = $users;
2320
        }
2321
 
2322
        if ($tablesort) {
2323
            // Resort the user list according to the grading table sort and filter settings.
2324
            $sortedfiltereduserids = $this->get_grading_userid_list(true, '');
2325
            $sortedfilteredusers = [];
2326
            foreach ($sortedfiltereduserids as $nextid) {
2327
                $nextid = intval($nextid);
2328
                if (isset($this->participants[$key][$nextid])) {
2329
                    $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid];
2330
                }
2331
            }
2332
            $this->participants[$key] = $sortedfilteredusers;
2333
        }
2334
 
2335
        if ($idsonly) {
2336
            $idslist = array();
2337
            foreach ($this->participants[$key] as $id => $user) {
2338
                $idslist[$id] = new stdClass();
2339
                $idslist[$id]->id = $id;
2340
            }
2341
            return $idslist;
2342
        }
2343
        return $this->participants[$key];
2344
    }
2345
 
2346
    /**
2347
     * Load a user if they are enrolled in the current course. Populated with submission
2348
     * status for this assignment.
2349
     *
2350
     * @param int $userid
2351
     * @return null|stdClass user record
2352
     */
2353
    public function get_participant($userid) {
2354
        global $DB, $USER;
2355
 
2356
        if ($userid == $USER->id) {
2357
            $participant = clone ($USER);
2358
        } else {
2359
            $participant = $DB->get_record('user', array('id' => $userid));
2360
        }
2361
        if (!$participant) {
2362
            return null;
2363
        }
2364
 
2365
        if (!is_enrolled($this->context, $participant, '', $this->show_only_active_users())) {
2366
            return null;
2367
        }
2368
 
2369
        $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
2370
 
2371
        $submissioninfo = $result[$participant->id];
2372
        if (!$submissioninfo->submitted && !has_capability('mod/assign:submit', $this->context, $userid)) {
2373
            return null;
2374
        }
2375
 
2376
        return $submissioninfo;
2377
    }
2378
 
2379
    /**
2380
     * Load a count of valid teams for this assignment.
2381
     *
2382
     * @param int $activitygroup Activity active group
2383
     * @return int number of valid teams
2384
     */
2385
    public function count_teams($activitygroup = 0) {
2386
 
2387
        $count = 0;
2388
 
2389
        $participants = $this->list_participants($activitygroup, true);
2390
 
2391
        // If a team submission grouping id is provided all good as all returned groups
2392
        // are the submission teams, but if no team submission grouping was specified
2393
        // $groups will contain all participants groups.
2394
        if ($this->get_instance()->teamsubmissiongroupingid) {
2395
 
2396
            // We restrict the users to the selected group ones.
2397
            $groups = groups_get_all_groups($this->get_course()->id,
2398
                                            array_keys($participants),
2399
                                            $this->get_instance()->teamsubmissiongroupingid,
2400
                                            'DISTINCT g.id, g.name');
2401
 
2402
            $count = count($groups);
2403
 
2404
            // When a specific group is selected we don't count the default group users.
2405
            if ($activitygroup == 0) {
2406
                if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2407
                    // See if there are any users in the default group.
2408
                    $defaultusers = $this->get_submission_group_members(0, true);
2409
                    if (count($defaultusers) > 0) {
2410
                        $count += 1;
2411
                    }
2412
                }
2413
            } else if ($activitygroup != 0 && empty($groups)) {
2414
                // Set count to 1 if $groups returns empty.
2415
                // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid.
2416
                $count = 1;
2417
            }
2418
        } else {
2419
            // It is faster to loop around participants if no grouping was specified.
2420
            $groups = array();
2421
            foreach ($participants as $participant) {
2422
                if ($group = $this->get_submission_group($participant->id)) {
2423
                    $groups[$group->id] = true;
2424
                } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2425
                    $groups[0] = true;
2426
                }
2427
            }
2428
 
2429
            $count = count($groups);
2430
        }
2431
 
2432
        return $count;
2433
    }
2434
 
2435
    /**
2436
     * Load a count of active users enrolled in the current course with the specified permission and group.
2437
     * 0 for no group.
2438
     *
2439
     * @param int $currentgroup
2440
     * @return int number of matching users
2441
     */
2442
    public function count_participants($currentgroup) {
2443
        return count($this->list_participants($currentgroup, true));
2444
    }
2445
 
2446
    /**
2447
     * Load a count of active users submissions in the current module that require grading
2448
     * This means the submission modification time is more recent than the
2449
     * grading modification time and the status is SUBMITTED.
2450
     *
2451
     * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2452
     * @return int number of matching submissions
2453
     */
2454
    public function count_submissions_need_grading($currentgroup = null) {
2455
        global $DB;
2456
 
2457
        if ($this->get_instance()->teamsubmission) {
2458
            // This does not make sense for group assignment because the submission is shared.
2459
            return 0;
2460
        }
2461
 
2462
        if ($currentgroup === null) {
2463
            $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2464
        }
2465
        list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2466
 
2467
        $params['assignid'] = $this->get_instance()->id;
2468
        $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2469
        $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : '';
2470
 
2471
        $sql = 'SELECT COUNT(s.userid)
2472
                   FROM {assign_submission} s
2473
                   LEFT JOIN {assign_grades} g ON
2474
                        s.assignment = g.assignment AND
2475
                        s.userid = g.userid AND
2476
                        g.attemptnumber = s.attemptnumber
2477
                   JOIN(' . $esql . ') e ON e.id = s.userid
2478
                   WHERE
2479
                        s.latest = 1 AND
2480
                        s.assignment = :assignid AND
2481
                        s.timemodified IS NOT NULL AND
2482
                        s.status = :submitted AND
2483
                        (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL '
2484
                            . $sqlscalegrade . ')';
2485
 
2486
        return $DB->count_records_sql($sql, $params);
2487
    }
2488
 
2489
    /**
2490
     * Load a count of grades.
2491
     *
2492
     * @return int number of grades
2493
     */
2494
    public function count_grades() {
2495
        global $DB;
2496
 
2497
        if (!$this->has_instance()) {
2498
            return 0;
2499
        }
2500
 
2501
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2502
        list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2503
 
2504
        $params['assignid'] = $this->get_instance()->id;
2505
 
2506
        $sql = 'SELECT COUNT(g.userid)
2507
                   FROM {assign_grades} g
2508
                   JOIN(' . $esql . ') e ON e.id = g.userid
2509
                   WHERE g.assignment = :assignid';
2510
 
2511
        return $DB->count_records_sql($sql, $params);
2512
    }
2513
 
2514
    /**
2515
     * Load a count of submissions.
2516
     *
2517
     * @param bool $includenew When true, also counts the submissions with status 'new'.
2518
     * @return int number of submissions
2519
     */
2520
    public function count_submissions($includenew = false) {
2521
        global $DB;
2522
 
2523
        if (!$this->has_instance()) {
2524
            return 0;
2525
        }
2526
 
2527
        $params = array();
2528
        $sqlnew = '';
2529
 
2530
        if (!$includenew) {
2531
            $sqlnew = ' AND s.status <> :status ';
2532
            $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
2533
        }
2534
 
2535
        if ($this->get_instance()->teamsubmission) {
2536
            // We cannot join on the enrolment tables for group submissions (no userid).
2537
            $sql = 'SELECT COUNT(DISTINCT s.groupid)
2538
                        FROM {assign_submission} s
2539
                        WHERE
2540
                            s.assignment = :assignid AND
2541
                            s.timemodified IS NOT NULL AND
2542
                            s.userid = :groupuserid' .
2543
                            $sqlnew;
2544
 
2545
            $params['assignid'] = $this->get_instance()->id;
2546
            $params['groupuserid'] = 0;
2547
        } else {
2548
            $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2549
            list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2550
 
2551
            $params = array_merge($params, $enrolparams);
2552
            $params['assignid'] = $this->get_instance()->id;
2553
 
2554
            $sql = 'SELECT COUNT(DISTINCT s.userid)
2555
                       FROM {assign_submission} s
2556
                       JOIN(' . $esql . ') e ON e.id = s.userid
2557
                       WHERE
2558
                            s.assignment = :assignid AND
2559
                            s.timemodified IS NOT NULL ' .
2560
                            $sqlnew;
2561
 
2562
        }
2563
 
2564
        return $DB->count_records_sql($sql, $params);
2565
    }
2566
 
2567
    /**
2568
     * Load a count of submissions with a specified status.
2569
     *
2570
     * @param string $status The submission status - should match one of the constants
2571
     * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2572
     * @return int number of matching submissions
2573
     */
2574
    public function count_submissions_with_status($status, $currentgroup = null) {
2575
        global $DB;
2576
 
2577
        if ($currentgroup === null) {
2578
            $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2579
        }
2580
        list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2581
 
2582
        $params['assignid'] = $this->get_instance()->id;
2583
        $params['assignid2'] = $this->get_instance()->id;
2584
        $params['submissionstatus'] = $status;
2585
 
2586
        if ($this->get_instance()->teamsubmission) {
2587
 
2588
            $groupsstr = '';
2589
            if ($currentgroup != 0) {
2590
                // If there is an active group we should only display the current group users groups.
2591
                $participants = $this->list_participants($currentgroup, true);
2592
                $groups = groups_get_all_groups($this->get_course()->id,
2593
                                                array_keys($participants),
2594
                                                $this->get_instance()->teamsubmissiongroupingid,
2595
                                                'DISTINCT g.id, g.name');
2596
                if (empty($groups)) {
2597
                    // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid.
2598
                    // All submissions from students that do not belong to any of teamsubmissiongroupingid groups
2599
                    // count towards groupid = 0. Setting to true as only '0' key matters.
2600
                    $groups = [true];
2601
                }
2602
                list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
2603
                $groupsstr = 's.groupid ' . $groupssql . ' AND';
2604
                $params = $params + $groupsparams;
2605
            }
2606
            $sql = 'SELECT COUNT(s.groupid)
2607
                        FROM {assign_submission} s
2608
                        WHERE
2609
                            s.latest = 1 AND
2610
                            s.assignment = :assignid AND
2611
                            s.timemodified IS NOT NULL AND
2612
                            s.userid = :groupuserid AND '
2613
                            . $groupsstr . '
2614
                            s.status = :submissionstatus';
2615
            $params['groupuserid'] = 0;
2616
        } else {
2617
            $sql = 'SELECT COUNT(s.userid)
2618
                        FROM {assign_submission} s
2619
                        JOIN(' . $esql . ') e ON e.id = s.userid
2620
                        WHERE
2621
                            s.latest = 1 AND
2622
                            s.assignment = :assignid AND
2623
                            s.timemodified IS NOT NULL AND
2624
                            s.status = :submissionstatus';
2625
 
2626
        }
2627
 
2628
        return $DB->count_records_sql($sql, $params);
2629
    }
2630
 
2631
    /**
2632
     * Utility function to get the userid for every row in the grading table
2633
     * so the order can be frozen while we iterate it.
2634
     *
2635
     * @param boolean $cached If true, the cached list from the session could be returned.
2636
     * @param string $useridlistid String value used for caching the participant list.
2637
     * @return array An array of userids
2638
     */
2639
    protected function get_grading_userid_list($cached = false, $useridlistid = '') {
2640
        global $SESSION;
2641
 
2642
        if ($cached) {
2643
            if (empty($useridlistid)) {
2644
                $useridlistid = $this->get_useridlist_key_id();
2645
            }
2646
            $useridlistkey = $this->get_useridlist_key($useridlistid);
2647
            if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
2648
                $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, '');
2649
            }
2650
            return $SESSION->mod_assign_useridlist[$useridlistkey];
2651
        }
2652
        $filter = get_user_preferences('assign_filter', '');
2653
        $table = new assign_grading_table($this, 0, $filter, 0, false);
2654
 
2655
        $useridlist = $table->get_column_data('userid');
2656
 
2657
        return $useridlist;
2658
    }
2659
 
2660
    /**
2661
     * Is user id filtered by user filters and table preferences.
2662
     *
2663
     * @param int $userid User id that needs to be checked.
2664
     * @return bool
2665
     */
2666
    public function is_userid_filtered($userid) {
2667
        $users = $this->get_grading_userid_list();
2668
        return in_array($userid, $users);
2669
    }
2670
 
2671
    /**
2672
     * Finds all assignment notifications that have yet to be mailed out, and mails them.
2673
     *
2674
     * Cron function to be run periodically according to the moodle cron.
2675
     *
2676
     * @return bool
2677
     */
2678
    public static function cron() {
2679
        global $DB;
2680
 
2681
        // Only ever send a max of one days worth of updates.
2682
        $yesterday = time() - (24 * 3600);
2683
        $timenow   = time();
2684
        $task = \core\task\manager::get_scheduled_task(mod_assign\task\cron_task::class);
2685
        $lastruntime = $task->get_last_run_time();
2686
 
2687
        // Collect all submissions that require mailing.
2688
        // Submissions are included if all are true:
2689
        //   - The assignment is visible in the gradebook.
2690
        //   - No previous notification has been sent.
2691
        //   - The grader was a real user, not an automated process.
2692
        //   - The grade was updated in the past 24 hours.
2693
        //   - If marking workflow is enabled, the workflow state is at 'released'.
2694
        $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities, a.hidegrader,
2695
                       g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid
2696
                 FROM {assign} a
2697
                 JOIN {assign_grades} g ON g.assignment = a.id
2698
            LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
2699
                 JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
2700
                 JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
2701
                 JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
2702
            LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
2703
                 WHERE (a.markingworkflow = 0 OR (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
2704
                       g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0 AND
2705
                       g.timemodified >= :yesterday AND g.timemodified <= :today
2706
              ORDER BY a.course, cm.id";
2707
 
2708
        $params = array(
2709
            'yesterday' => $yesterday,
2710
            'today' => $timenow,
2711
            'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2712
        );
2713
        $submissions = $DB->get_records_sql($sql, $params);
2714
 
2715
        if (!empty($submissions)) {
2716
 
2717
            mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
2718
 
2719
            // Preload courses we are going to need those.
2720
            $courseids = array();
2721
            foreach ($submissions as $submission) {
2722
                $courseids[] = $submission->course;
2723
            }
2724
 
2725
            // Filter out duplicates.
2726
            $courseids = array_unique($courseids);
2727
            $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
2728
            list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
2729
            $sql = 'SELECT c.*, ' . $ctxselect .
2730
                      ' FROM {course} c
2731
                 LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
2732
                     WHERE c.id ' . $courseidsql;
2733
 
2734
            $params['contextlevel'] = CONTEXT_COURSE;
2735
            $courses = $DB->get_records_sql($sql, $params);
2736
 
2737
            // Clean up... this could go on for a while.
2738
            unset($courseids);
2739
            unset($ctxselect);
2740
            unset($courseidsql);
2741
            unset($params);
2742
 
2743
            // Message students about new feedback.
2744
            foreach ($submissions as $submission) {
2745
 
2746
                mtrace("Processing assignment submission $submission->id ...");
2747
 
2748
                // Do not cache user lookups - could be too many.
2749
                if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
2750
                    mtrace('Could not find user ' . $submission->userid);
2751
                    continue;
2752
                }
2753
 
2754
                // Use a cache to prevent the same DB queries happening over and over.
2755
                if (!array_key_exists($submission->course, $courses)) {
2756
                    mtrace('Could not find course ' . $submission->course);
2757
                    continue;
2758
                }
2759
                $course = $courses[$submission->course];
2760
                if (isset($course->ctxid)) {
2761
                    // Context has not yet been preloaded. Do so now.
2762
                    context_helper::preload_from_record($course);
2763
                }
2764
 
2765
                // Override the language and timezone of the "current" user, so that
2766
                // mail is customised for the receiver.
2767
                \core\cron::setup_user($user, $course);
2768
 
2769
                // Context lookups are already cached.
2770
                $coursecontext = context_course::instance($course->id);
2771
                if (!is_enrolled($coursecontext, $user->id)) {
2772
                    $courseshortname = format_string($course->shortname,
2773
                                                     true,
2774
                                                     array('context' => $coursecontext));
2775
                    mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
2776
                    continue;
2777
                }
2778
 
2779
                if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
2780
                    mtrace('Could not find grader ' . $submission->grader);
2781
                    continue;
2782
                }
2783
 
2784
                $modinfo = get_fast_modinfo($course, $user->id);
2785
                $cm = $modinfo->get_cm($submission->cmid);
2786
                // Context lookups are already cached.
2787
                $contextmodule = context_module::instance($cm->id);
2788
 
2789
                if (!$cm->uservisible) {
2790
                    // Hold mail notification for assignments the user cannot access until later.
2791
                    continue;
2792
                }
2793
 
2794
                // Notify the student. Default to the non-anon version.
2795
                $messagetype = 'feedbackavailable';
2796
                // Message type needs 'anon' if "hidden grading" is enabled and the student
2797
                // doesn't have permission to see the grader.
2798
                if ($submission->hidegrader && !has_capability('mod/assign:showhiddengrader', $contextmodule, $user)) {
2799
                    $messagetype = 'feedbackavailableanon';
2800
                    // There's no point in having an "anonymous grader" if the notification email
2801
                    // comes from them. Send the email from the noreply user instead.
2802
                    $grader = core_user::get_noreply_user();
2803
                }
2804
 
2805
                $eventtype = 'assign_notification';
2806
                $updatetime = $submission->lastmodified;
2807
                $modulename = get_string('modulename', 'assign');
2808
 
2809
                $uniqueid = 0;
2810
                if ($submission->blindmarking && !$submission->revealidentities) {
2811
                    if (empty($submission->recordid)) {
2812
                        $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id);
2813
                    } else {
2814
                        $uniqueid = $submission->recordid;
2815
                    }
2816
                }
2817
                $showusers = $submission->blindmarking && !$submission->revealidentities;
2818
                self::send_assignment_notification($grader,
2819
                                                   $user,
2820
                                                   $messagetype,
2821
                                                   $eventtype,
2822
                                                   $updatetime,
2823
                                                   $cm,
2824
                                                   $contextmodule,
2825
                                                   $course,
2826
                                                   $modulename,
2827
                                                   $submission->name,
2828
                                                   $showusers,
2829
                                                   $uniqueid);
2830
 
2831
                $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
2832
                if ($flags) {
2833
                    $flags->mailed = 1;
2834
                    $DB->update_record('assign_user_flags', $flags);
2835
                } else {
2836
                    $flags = new stdClass();
2837
                    $flags->userid = $user->id;
2838
                    $flags->assignment = $submission->assignment;
2839
                    $flags->mailed = 1;
2840
                    $DB->insert_record('assign_user_flags', $flags);
2841
                }
2842
 
2843
                mtrace('Done');
2844
            }
2845
            mtrace('Done processing ' . count($submissions) . ' assignment submissions');
2846
 
2847
            \core\cron::setup_user();
2848
 
2849
            // Free up memory just to be sure.
2850
            unset($courses);
2851
        }
2852
 
2853
        // Update calendar events to provide a description.
2854
        $sql = 'SELECT id
2855
                    FROM {assign}
2856
                    WHERE
2857
                        allowsubmissionsfromdate >= :lastruntime AND
2858
                        allowsubmissionsfromdate <= :timenow AND
2859
                        alwaysshowdescription = 0';
2860
        $params = array('lastruntime' => $lastruntime, 'timenow' => $timenow);
2861
        $newlyavailable = $DB->get_records_sql($sql, $params);
2862
        foreach ($newlyavailable as $record) {
2863
            $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
2864
            $context = context_module::instance($cm->id);
2865
 
2866
            $assignment = new assign($context, null, null);
2867
            $assignment->update_calendar($cm->id);
2868
        }
2869
 
2870
        return true;
2871
    }
2872
 
2873
    /**
2874
     * Mark in the database that this grade record should have an update notification sent by cron.
2875
     *
2876
     * @param stdClass $grade a grade record keyed on id
2877
     * @param bool $mailedoverride when true, flag notification to be sent again.
2878
     * @return bool true for success
2879
     */
2880
    public function notify_grade_modified($grade, $mailedoverride = false) {
2881
        global $DB;
2882
 
2883
        $flags = $this->get_user_flags($grade->userid, true);
2884
        if ($flags->mailed != 1 || $mailedoverride) {
2885
            $flags->mailed = 0;
2886
        }
2887
 
2888
        return $this->update_user_flags($flags);
2889
    }
2890
 
2891
    /**
2892
     * Update user flags for this user in this assignment.
2893
     *
2894
     * @param stdClass $flags a flags record keyed on id
2895
     * @return bool true for success
2896
     */
2897
    public function update_user_flags($flags) {
2898
        global $DB;
2899
        if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
2900
            return false;
2901
        }
2902
 
2903
        $result = $DB->update_record('assign_user_flags', $flags);
2904
        return $result;
2905
    }
2906
 
2907
    /**
2908
     * Update a grade in the grade table for the assignment and in the gradebook.
2909
     *
2910
     * @param stdClass $grade a grade record keyed on id
2911
     * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
2912
     * @return bool true for success
2913
     */
2914
    public function update_grade($grade, $reopenattempt = false) {
2915
        global $DB;
2916
 
2917
        $grade->timemodified = time();
2918
 
2919
        if (!empty($grade->workflowstate)) {
2920
            $validstates = $this->get_marking_workflow_states_for_current_user();
2921
            if (!array_key_exists($grade->workflowstate, $validstates)) {
2922
                return false;
2923
            }
2924
        }
2925
 
2926
        if ($grade->grade && $grade->grade != -1) {
2927
            if ($this->get_instance()->grade > 0) {
2928
                if (!is_numeric($grade->grade)) {
2929
                    return false;
2930
                } else if ($grade->grade > $this->get_instance()->grade) {
2931
                    return false;
2932
                } else if ($grade->grade < 0) {
2933
                    return false;
2934
                }
2935
            } else {
2936
                // This is a scale.
2937
                if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
2938
                    $scaleoptions = make_menu_from_list($scale->scale);
2939
                    if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
2940
                        return false;
2941
                    }
2942
                }
2943
            }
2944
        }
2945
 
2946
        if (empty($grade->attemptnumber)) {
2947
            // Set it to the default.
2948
            $grade->attemptnumber = 0;
2949
        }
2950
        $DB->update_record('assign_grades', $grade);
2951
 
2952
        $submission = null;
2953
        if ($this->get_instance()->teamsubmission) {
2954
            if (isset($this->mostrecentteamsubmission)) {
2955
                $submission = $this->mostrecentteamsubmission;
2956
            } else {
2957
                $submission = $this->get_group_submission($grade->userid, 0, false);
2958
            }
2959
        } else {
2960
            $submission = $this->get_user_submission($grade->userid, false);
2961
        }
2962
 
2963
        // Only push to gradebook if the update is for the most recent attempt.
2964
        if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
2965
            return true;
2966
        }
2967
 
2968
        if ($this->gradebook_item_update(null, $grade)) {
2969
            \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
2970
        }
2971
 
2972
        // If the conditions are met, allow another attempt.
2973
        if ($submission) {
2974
            $isreopened = $this->reopen_submission_if_required($grade->userid,
2975
                    $submission,
2976
                    $reopenattempt);
2977
            if ($isreopened) {
2978
                $completion = new completion_info($this->get_course());
2979
                if ($completion->is_enabled($this->get_course_module()) &&
2980
                    $this->get_instance()->completionsubmit) {
2981
                    $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $grade->userid);
2982
                }
2983
            }
2984
        }
2985
 
2986
        return true;
2987
    }
2988
 
2989
    /**
2990
     * View the grant extension date page.
2991
     *
2992
     * Uses url parameters 'userid'
2993
     * or from parameter 'selectedusers'
2994
     *
2995
     * @param moodleform $mform - Used for validation of the submitted data
2996
     * @return string
2997
     */
2998
    protected function view_grant_extension($mform) {
2999
        global $CFG;
3000
        require_once($CFG->dirroot . '/mod/assign/extensionform.php');
3001
 
3002
        $o = '';
3003
 
3004
        $data = new stdClass();
3005
        $data->id = $this->get_course_module()->id;
3006
 
3007
        $formparams = array(
3008
            'instance' => $this->get_instance(),
3009
            'assign' => $this
3010
        );
3011
 
3012
        $users = optional_param('userid', 0, PARAM_INT);
3013
        if (!$users) {
3014
            $users = required_param('selectedusers', PARAM_SEQUENCE);
3015
        }
3016
        $userlist = explode(',', $users);
3017
 
3018
        $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
3019
        $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
3020
        foreach ($userlist as $userid) {
3021
            // To validate extension date with users overrides.
3022
            $override = $this->override_exists($userid);
3023
            foreach ($keys as $key) {
3024
                if ($override->{$key}) {
3025
                    if ($maxoverride[$key] < $override->{$key}) {
3026
                        $maxoverride[$key] = $override->{$key};
3027
                    }
3028
                } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
3029
                    $maxoverride[$key] = $this->get_instance()->{$key};
3030
                }
3031
            }
3032
        }
3033
        foreach ($keys as $key) {
3034
            if ($maxoverride[$key]) {
3035
                $this->get_instance()->{$key} = $maxoverride[$key];
3036
            }
3037
        }
3038
 
3039
        $formparams['userlist'] = $userlist;
3040
 
3041
        $data->selectedusers = $users;
3042
        $data->userid = 0;
3043
 
3044
        if (empty($mform)) {
3045
            $mform = new mod_assign_extension_form(null, $formparams);
3046
        }
3047
        $mform->set_data($data);
3048
        $header = new assign_header($this->get_instance(),
3049
                                    $this->get_context(),
3050
                                    $this->show_intro(),
3051
                                    $this->get_course_module()->id,
3052
                                    get_string('grantextension', 'assign'));
3053
        $o .= $this->get_renderer()->render($header);
3054
        $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
3055
        $o .= $this->view_footer();
3056
        return $o;
3057
    }
3058
 
3059
    /**
3060
     * Get a list of the users in the same group as this user.
3061
     *
3062
     * @param int $groupid The id of the group whose members we want or 0 for the default group
3063
     * @param bool $onlyids Whether to retrieve only the user id's
3064
     * @param bool $excludesuspended Whether to exclude suspended users
3065
     * @return array The users (possibly id's only)
3066
     */
3067
    public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
3068
        $members = array();
3069
        if ($groupid != 0) {
3070
            $allusers = $this->list_participants($groupid, $onlyids);
3071
            foreach ($allusers as $user) {
3072
                if ($this->get_submission_group($user->id)) {
3073
                    $members[] = $user;
3074
                }
3075
            }
3076
        } else {
3077
            $allusers = $this->list_participants(null, $onlyids);
3078
            foreach ($allusers as $user) {
3079
                if ($this->get_submission_group($user->id) == null) {
3080
                    $members[] = $user;
3081
                }
3082
            }
3083
        }
3084
        // Exclude suspended users, if user can't see them.
3085
        if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
3086
            foreach ($members as $key => $member) {
3087
                if (!$this->is_active_user($member->id)) {
3088
                    unset($members[$key]);
3089
                }
3090
            }
3091
        }
3092
 
3093
        return $members;
3094
    }
3095
 
3096
    /**
3097
     * Get a list of the users in the same group as this user that have not submitted the assignment.
3098
     *
3099
     * @param int $groupid The id of the group whose members we want or 0 for the default group
3100
     * @param bool $onlyids Whether to retrieve only the user id's
3101
     * @return array The users (possibly id's only)
3102
     */
3103
    public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
3104
        $instance = $this->get_instance();
3105
        if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
3106
            return array();
3107
        }
3108
        $members = $this->get_submission_group_members($groupid, $onlyids);
3109
 
3110
        foreach ($members as $id => $member) {
3111
            $submission = $this->get_user_submission($member->id, false);
3112
            if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
3113
                unset($members[$id]);
3114
            } else {
3115
                if ($this->is_blind_marking()) {
3116
                    $members[$id]->alias = get_string('hiddenuser', 'assign') .
3117
                                           $this->get_uniqueid_for_user($id);
3118
                }
3119
            }
3120
        }
3121
        return $members;
3122
    }
3123
 
3124
    /**
3125
     * Load the group submission object for a particular user, optionally creating it if required.
3126
     *
3127
     * @param int $userid The id of the user whose submission we want
3128
     * @param int $groupid The id of the group for this user - may be 0 in which
3129
     *                     case it is determined from the userid.
3130
     * @param bool $create If set to true a new submission object will be created in the database
3131
     *                     with the status set to "new".
3132
     * @param int $attemptnumber - -1 means the latest attempt
3133
     * @return stdClass|false The submission
3134
     */
3135
    public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
3136
        global $DB;
3137
 
3138
        if ($groupid == 0) {
3139
            $group = $this->get_submission_group($userid);
3140
            if ($group) {
3141
                $groupid = $group->id;
3142
            }
3143
        }
3144
 
3145
        // Now get the group submission.
3146
        $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
3147
        if ($attemptnumber >= 0) {
3148
            $params['attemptnumber'] = $attemptnumber;
3149
        }
3150
 
3151
        // Only return the row with the highest attemptnumber.
3152
        $submission = null;
3153
        $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3154
        if ($submissions) {
3155
            $submission = reset($submissions);
3156
        }
3157
 
3158
        if ($submission) {
3159
            if ($create) {
3160
                $action = optional_param('action', '', PARAM_TEXT);
3161
                if ($action == 'editsubmission') {
3162
                    if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
3163
                        $submission->timestarted = time();
3164
                        $DB->update_record('assign_submission', $submission);
3165
                    }
3166
                }
3167
            }
3168
            return $submission;
3169
        }
3170
        if ($create) {
3171
            $submission = new stdClass();
3172
            $submission->assignment = $this->get_instance()->id;
3173
            $submission->userid = 0;
3174
            $submission->groupid = $groupid;
3175
            $submission->timecreated = time();
3176
            $submission->timemodified = $submission->timecreated;
3177
            if ($attemptnumber >= 0) {
3178
                $submission->attemptnumber = $attemptnumber;
3179
            } else {
3180
                $submission->attemptnumber = 0;
3181
            }
3182
            // Work out if this is the latest submission.
3183
            $submission->latest = 0;
3184
            $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
3185
            if ($attemptnumber == -1) {
3186
                // This is a new submission so it must be the latest.
3187
                $submission->latest = 1;
3188
            } else {
3189
                // We need to work this out.
3190
                $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3191
                if ($result) {
3192
                    $latestsubmission = reset($result);
3193
                }
3194
                if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
3195
                    $submission->latest = 1;
3196
                }
3197
            }
3198
            $transaction = $DB->start_delegated_transaction();
3199
            if ($submission->latest) {
3200
                // This is the case when we need to set latest to 0 for all the other attempts.
3201
                $DB->set_field('assign_submission', 'latest', 0, $params);
3202
            }
3203
            $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3204
            $sid = $DB->insert_record('assign_submission', $submission);
3205
            $transaction->allow_commit();
3206
            return $DB->get_record('assign_submission', array('id' => $sid));
3207
        }
3208
        return false;
3209
    }
3210
 
3211
    /**
3212
     * View a summary listing of all assignments in the current course.
3213
     *
3214
     * @return string
3215
     */
3216
    private function view_course_index() {
3217
        global $USER;
3218
 
3219
        $o = '';
3220
 
3221
        $course = $this->get_course();
3222
        $strplural = get_string('modulenameplural', 'assign');
3223
 
3224
        if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
3225
            $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
3226
            $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
3227
            return $o;
3228
        }
3229
 
3230
        $strsectionname = '';
3231
        $usesections = course_format_uses_sections($course->format);
3232
        $modinfo = get_fast_modinfo($course);
3233
 
3234
        if ($usesections) {
3235
            $strsectionname = get_string('sectionname', 'format_'.$course->format);
3236
            $sections = $modinfo->get_section_info_all();
3237
        }
3238
        $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
3239
 
3240
        $timenow = time();
3241
 
3242
        $currentsection = '';
3243
        foreach ($modinfo->instances['assign'] as $cm) {
3244
            if (!$cm->uservisible) {
3245
                continue;
3246
            }
3247
 
3248
            $timedue = $cms[$cm->id]->duedate;
3249
 
3250
            $sectionname = '';
3251
            if ($usesections && $cm->sectionnum) {
3252
                $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
3253
            }
3254
 
3255
            $submitted = '';
3256
            $context = context_module::instance($cm->id);
3257
 
3258
            $assignment = new assign($context, $cm, $course);
3259
 
3260
            // Apply overrides.
3261
            $assignment->update_effective_access($USER->id);
3262
            $timedue = $assignment->get_instance()->duedate;
3263
 
3264
            if (has_capability('mod/assign:submit', $context) &&
3265
                !has_capability('moodle/site:config', $context)) {
3266
                $cangrade = false;
3267
                if ($assignment->get_instance()->teamsubmission) {
3268
                    $usersubmission = $assignment->get_group_submission($USER->id, 0, false);
3269
                } else {
3270
                    $usersubmission = $assignment->get_user_submission($USER->id, false);
3271
                }
3272
 
3273
                if (!empty($usersubmission->status)) {
3274
                    $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
3275
                } else {
3276
                    $submitted = get_string('submissionstatus_', 'assign');
3277
                }
3278
 
3279
                $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
3280
                if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
3281
                        !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
3282
                    $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
3283
                } else {
3284
                    $grade = '-';
3285
                }
3286
            } else if (has_capability('mod/assign:grade', $context)) {
3287
                $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
3288
                $grade = $assignment->count_submissions_need_grading();
3289
                $cangrade = true;
3290
            }
3291
 
3292
            $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(),
3293
                $sectionname, $timedue, $submitted, $grade, $cangrade);
3294
        }
3295
 
3296
        $o .= $this->get_renderer()->render($courseindexsummary);
3297
        $o .= $this->view_footer();
3298
 
3299
        return $o;
3300
    }
3301
 
3302
    /**
3303
     * View a page rendered by a plugin.
3304
     *
3305
     * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
3306
     *
3307
     * @return string
3308
     */
3309
    protected function view_plugin_page() {
3310
        global $USER;
3311
 
3312
        $o = '';
3313
 
3314
        $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
3315
        $plugintype = required_param('plugin', PARAM_PLUGIN);
3316
        $pluginaction = required_param('pluginaction', PARAM_ALPHA);
3317
 
3318
        $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
3319
        if (!$plugin) {
3320
            throw new \moodle_exception('invalidformdata', '');
3321
            return;
3322
        }
3323
 
3324
        $o .= $plugin->view_page($pluginaction);
3325
 
3326
        return $o;
3327
    }
3328
 
3329
 
3330
    /**
3331
     * This is used for team assignments to get the group for the specified user.
3332
     * If the user is a member of multiple or no groups this will return false
3333
     *
3334
     * @param int $userid The id of the user whose submission we want
3335
     * @return mixed The group or false
3336
     */
3337
    public function get_submission_group($userid) {
3338
 
3339
        if (isset($this->usersubmissiongroups[$userid])) {
3340
            return $this->usersubmissiongroups[$userid];
3341
        }
3342
 
3343
        $groups = $this->get_all_groups($userid);
3344
        if (count($groups) != 1) {
3345
            $return = false;
3346
        } else {
3347
            $return = array_pop($groups);
3348
        }
3349
 
3350
        // Cache the user submission group.
3351
        $this->usersubmissiongroups[$userid] = $return;
3352
 
3353
        return $return;
3354
    }
3355
 
3356
    /**
3357
     * Gets all groups the user is a member of.
3358
     *
3359
     * @param int $userid Teh id of the user who's groups we are checking
3360
     * @return array The group objects
3361
     */
3362
    public function get_all_groups($userid) {
3363
        if (isset($this->usergroups[$userid])) {
3364
            return $this->usergroups[$userid];
3365
        }
3366
 
3367
        $grouping = $this->get_instance()->teamsubmissiongroupingid;
3368
        $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping, 'g.*', false, true);
3369
 
3370
        $this->usergroups[$userid] = $return;
3371
 
3372
        return $return;
3373
    }
3374
 
3375
 
3376
    /**
3377
     * Display the submission that is used by a plugin.
3378
     *
3379
     * Uses url parameters 'sid', 'gid' and 'plugin'.
3380
     *
3381
     * @param string $pluginsubtype
3382
     * @return string
3383
     */
3384
    protected function view_plugin_content($pluginsubtype) {
3385
        $o = '';
3386
 
3387
        $submissionid = optional_param('sid', 0, PARAM_INT);
3388
        $gradeid = optional_param('gid', 0, PARAM_INT);
3389
        $plugintype = required_param('plugin', PARAM_PLUGIN);
3390
        $item = null;
3391
        if ($pluginsubtype == 'assignsubmission') {
3392
            $plugin = $this->get_submission_plugin_by_type($plugintype);
3393
            if ($submissionid <= 0) {
3394
                throw new coding_exception('Submission id should not be 0');
3395
            }
3396
            $item = $this->get_submission($submissionid);
3397
 
3398
            // Check permissions.
3399
            if (empty($item->userid)) {
3400
                // Group submission.
3401
                $this->require_view_group_submission($item->groupid);
3402
            } else {
3403
                $this->require_view_submission($item->userid);
3404
            }
3405
            $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3406
                                                              $this->get_context(),
3407
                                                              $this->show_intro(),
3408
                                                              $this->get_course_module()->id,
3409
                                                              $plugin->get_name()));
3410
            $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
3411
                                                              $item,
3412
                                                              assign_submission_plugin_submission::FULL,
3413
                                                              $this->get_course_module()->id,
3414
                                                              $this->get_return_action(),
3415
                                                              $this->get_return_params()));
3416
 
3417
            // Trigger event for viewing a submission.
3418
            \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
3419
 
3420
        } else {
3421
            $plugin = $this->get_feedback_plugin_by_type($plugintype);
3422
            if ($gradeid <= 0) {
3423
                throw new coding_exception('Grade id should not be 0');
3424
            }
3425
            $item = $this->get_grade($gradeid);
3426
            // Check permissions.
3427
            $this->require_view_submission($item->userid);
3428
            $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3429
                                                              $this->get_context(),
3430
                                                              $this->show_intro(),
3431
                                                              $this->get_course_module()->id,
3432
                                                              $plugin->get_name()));
3433
            $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
3434
                                                              $item,
3435
                                                              assign_feedback_plugin_feedback::FULL,
3436
                                                              $this->get_course_module()->id,
3437
                                                              $this->get_return_action(),
3438
                                                              $this->get_return_params()));
3439
 
3440
            // Trigger event for viewing feedback.
3441
            \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
3442
        }
3443
 
3444
        $o .= $this->view_return_links();
3445
 
3446
        $o .= $this->view_footer();
3447
 
3448
        return $o;
3449
    }
3450
 
3451
    /**
3452
     * Rewrite plugin file urls so they resolve correctly in an exported zip.
3453
     *
3454
     * @param string $text - The replacement text
3455
     * @param stdClass $user - The user record
3456
     * @param assign_plugin $plugin - The assignment plugin
3457
     */
3458
    public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
3459
        // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance.
3460
        // Rather, it should be determined by checking the group submission settings of the instance,
3461
        // which is what download_submission() does when generating the file name prefixes.
3462
        $groupname = '';
3463
        if ($this->get_instance()->teamsubmission) {
3464
            $submissiongroup = $this->get_submission_group($user->id);
3465
            if ($submissiongroup) {
3466
                $groupname = $submissiongroup->name . '-';
3467
            } else {
3468
                $groupname = get_string('defaultteam', 'assign') . '-';
3469
            }
3470
        }
3471
 
3472
        if ($this->is_blind_marking()) {
3473
            $prefix = $groupname . get_string('participant', 'assign');
3474
            $prefix = str_replace('_', ' ', $prefix);
3475
            $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3476
        } else {
3477
            $prefix = $groupname . fullname($user);
3478
            $prefix = str_replace('_', ' ', $prefix);
3479
            $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3480
        }
3481
 
3482
        // Only prefix files if downloadasfolders user preference is NOT set.
3483
        if (!get_user_preferences('assign_downloadasfolders', 1)) {
3484
            $subtype = $plugin->get_subtype();
3485
            $type = $plugin->get_type();
3486
            $prefix = $prefix . $subtype . '_' . $type . '_';
3487
        } else {
3488
            $prefix = "";
3489
        }
3490
        $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
3491
 
3492
        return $result;
3493
    }
3494
 
3495
    /**
3496
     * Render the content in editor that is often used by plugin.
3497
     *
3498
     * @param string $filearea
3499
     * @param int $submissionid
3500
     * @param string $plugintype
3501
     * @param string $editor
3502
     * @param string $component
3503
     * @param bool $shortentext Whether to shorten the text content.
3504
     * @return string
3505
     */
3506
    public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) {
3507
        global $CFG;
3508
 
3509
        $result = '';
3510
 
3511
        $plugin = $this->get_submission_plugin_by_type($plugintype);
3512
 
3513
        $text = $plugin->get_editor_text($editor, $submissionid);
3514
        if ($shortentext) {
3515
            $text = shorten_text($text, 140);
3516
        }
3517
        $format = $plugin->get_editor_format($editor, $submissionid);
3518
 
3519
        $finaltext = file_rewrite_pluginfile_urls($text,
3520
                                                  'pluginfile.php',
3521
                                                  $this->get_context()->id,
3522
                                                  $component,
3523
                                                  $filearea,
3524
                                                  $submissionid);
3525
        $params = array('overflowdiv' => true, 'context' => $this->get_context());
3526
        $result .= format_text($finaltext, $format, $params);
3527
 
3528
        if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
3529
            require_once($CFG->libdir . '/portfoliolib.php');
3530
 
3531
            $button = new portfolio_add_button();
3532
            $portfolioparams = array('cmid' => $this->get_course_module()->id,
3533
                                     'sid' => $submissionid,
3534
                                     'plugin' => $plugintype,
3535
                                     'editor' => $editor,
3536
                                     'area'=>$filearea);
3537
            $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
3538
            $fs = get_file_storage();
3539
 
3540
            if ($files = $fs->get_area_files($this->context->id,
3541
                                             $component,
3542
                                             $filearea,
3543
                                             $submissionid,
3544
                                             'timemodified',
3545
                                             false)) {
3546
                $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3547
            } else {
3548
                $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3549
            }
3550
            $result .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3551
        }
3552
        return $result;
3553
    }
3554
 
3555
    /**
3556
     * Display a continue page after grading.
3557
     *
3558
     * @param string $message - The message to display.
3559
     * @return string
3560
     */
3561
    protected function view_savegrading_result($message) {
3562
        $o = '';
3563
        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3564
                                                      $this->get_context(),
3565
                                                      $this->show_intro(),
3566
                                                      $this->get_course_module()->id,
3567
                                                      get_string('savegradingresult', 'assign')));
3568
        $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
3569
                                                   $message,
3570
                                                   $this->get_course_module()->id);
3571
        $o .= $this->get_renderer()->render($gradingresult);
3572
        $o .= $this->view_footer();
3573
        return $o;
3574
    }
3575
    /**
3576
     * Display a continue page after quickgrading.
3577
     *
3578
     * @param string $message - The message to display.
3579
     * @return string
3580
     */
3581
    protected function view_quickgrading_result($message) {
3582
        $o = '';
3583
        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3584
                                                      $this->get_context(),
3585
                                                      $this->show_intro(),
3586
                                                      $this->get_course_module()->id,
3587
                                                      get_string('quickgradingresult', 'assign')));
3588
        $gradingerror = in_array($message, $this->get_error_messages());
3589
        $lastpage = optional_param('lastpage', null, PARAM_INT);
3590
        $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
3591
                                                   $message,
3592
                                                   $this->get_course_module()->id,
3593
                                                   $gradingerror,
3594
                                                   $lastpage);
3595
        $o .= $this->get_renderer()->render($gradingresult);
3596
        $o .= $this->view_footer();
3597
        return $o;
3598
    }
3599
 
3600
    /**
3601
     * Display the page footer.
3602
     *
3603
     * @return string
3604
     */
3605
    protected function view_footer() {
3606
        // When viewing the footer during PHPUNIT tests a set_state error is thrown.
3607
        if (!PHPUNIT_TEST) {
3608
            return $this->get_renderer()->render_footer();
3609
        }
3610
 
3611
        return '';
3612
    }
3613
 
3614
    /**
3615
     * Throw an error if the permissions to view this users' group submission are missing.
3616
     *
3617
     * @param int $groupid Group id.
3618
     * @throws required_capability_exception
3619
     */
3620
    public function require_view_group_submission($groupid) {
3621
        if (!$this->can_view_group_submission($groupid)) {
3622
            throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3623
        }
3624
    }
3625
 
3626
    /**
3627
     * Throw an error if the permissions to view this users submission are missing.
3628
     *
3629
     * @throws required_capability_exception
3630
     * @return none
3631
     */
3632
    public function require_view_submission($userid) {
3633
        if (!$this->can_view_submission($userid)) {
3634
            throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3635
        }
3636
    }
3637
 
3638
    /**
3639
     * Throw an error if the permissions to view grades in this assignment are missing.
3640
     *
3641
     * @throws required_capability_exception
3642
     * @return none
3643
     */
3644
    public function require_view_grades() {
3645
        if (!$this->can_view_grades()) {
3646
            throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3647
        }
3648
    }
3649
 
3650
    /**
3651
     * Does this user have view grade or grade permission for this assignment?
3652
     *
3653
     * @param mixed $groupid int|null when is set to a value, use this group instead calculating it
3654
     * @return bool
3655
     */
3656
    public function can_view_grades($groupid = null) {
3657
        // Permissions check.
3658
        if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
3659
            return false;
3660
        }
3661
        // Checks for the edge case when user belongs to no groups and groupmode is sep.
3662
        if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) {
3663
            if ($groupid === null) {
3664
                $groupid = groups_get_activity_allowed_groups($this->get_course_module());
3665
            }
3666
            $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context());
3667
            $groupflag = $groupflag || !empty($groupid);
3668
            return (bool)$groupflag;
3669
        }
3670
        return true;
3671
    }
3672
 
3673
    /**
3674
     * Does this user have grade permission for this assignment?
3675
     *
3676
     * @param int|stdClass $user The object or id of the user who will do the editing (default to current user).
3677
     * @return bool
3678
     */
3679
    public function can_grade($user = null) {
3680
        // Permissions check.
3681
        if (!has_capability('mod/assign:grade', $this->context, $user)) {
3682
            return false;
3683
        }
3684
 
3685
        return true;
3686
    }
3687
 
3688
    /**
3689
     * Download a zip file of all assignment submissions.
3690
     *
3691
     * @param array|null $userids Array of user ids to download assignment submissions in a zip file
3692
     * @return string - If an error occurs, this will contain the error page.
3693
     */
3694
    protected function download_submissions($userids = null) {
3695
        $downloader = new downloader($this, $userids ?: null);
3696
        if ($downloader->load_filelist()) {
3697
            $downloader->download_zip();
3698
        }
3699
        // Show some notification if we have nothing to download.
3700
        $cm = $this->get_course_module();
3701
        $renderer = $this->get_renderer();
3702
        $header = new assign_header(
3703
            $this->get_instance(),
3704
            $this->get_context(),
3705
            '',
3706
            $cm->id,
3707
            get_string('downloadall', 'mod_assign')
3708
        );
3709
        $result = $renderer->render($header);
3710
        $result .= $renderer->notification(get_string('nosubmission', 'mod_assign'));
3711
        $url = new moodle_url('/mod/assign/view.php', ['id' => $cm->id, 'action' => 'grading']);
3712
        $result .= $renderer->continue_button($url);
3713
        $result .= $this->view_footer();
3714
        return $result;
3715
    }
3716
 
3717
    /**
3718
     * @deprecated since 2.7 - Use new events system instead.
3719
     */
3720
    public function add_to_log() {
3721
        throw new coding_exception(__FUNCTION__ . ' has been deprecated, please do not use it any more');
3722
    }
3723
 
3724
    /**
3725
     * Lazy load the page renderer and expose the renderer to plugins.
3726
     *
3727
     * @return assign_renderer
3728
     */
3729
    public function get_renderer() {
3730
        global $PAGE;
3731
        if ($this->output) {
3732
            return $this->output;
3733
        }
3734
        $this->output = $PAGE->get_renderer('mod_assign', null, RENDERER_TARGET_GENERAL);
3735
        return $this->output;
3736
    }
3737
 
3738
    /**
3739
     * Load the submission object for a particular user, optionally creating it if required.
3740
     *
3741
     * For team assignments there are 2 submissions - the student submission and the team submission
3742
     * All files are associated with the team submission but the status of the students contribution is
3743
     * recorded separately.
3744
     *
3745
     * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
3746
     * @param bool $create If set to true a new submission object will be created in the database with the status set to "new".
3747
     * @param int $attemptnumber - -1 means the latest attempt
3748
     * @return stdClass|false The submission
3749
     */
3750
    public function get_user_submission($userid, $create, $attemptnumber=-1) {
3751
        global $DB, $USER;
3752
 
3753
        if (!$userid) {
3754
            $userid = $USER->id;
3755
        }
3756
        // If the userid is not null then use userid.
3757
        $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3758
        if ($attemptnumber >= 0) {
3759
            $params['attemptnumber'] = $attemptnumber;
3760
        }
3761
 
3762
        // Only return the row with the highest attemptnumber.
3763
        $submission = null;
3764
        $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3765
        if ($submissions) {
3766
            $submission = reset($submissions);
3767
        }
3768
 
3769
        if ($submission) {
3770
            if ($create) {
3771
                $action = optional_param('action', '', PARAM_TEXT);
3772
                if ($action == 'editsubmission') {
3773
                    if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
3774
                        $submission->timestarted = time();
3775
                        $DB->update_record('assign_submission', $submission);
3776
                    }
3777
                }
3778
            }
3779
            return $submission;
3780
        }
3781
        if ($create) {
3782
            $submission = new stdClass();
3783
            $submission->assignment   = $this->get_instance()->id;
3784
            $submission->userid       = $userid;
3785
            $submission->timecreated = time();
3786
            $submission->timemodified = $submission->timecreated;
3787
            $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3788
            if ($attemptnumber >= 0) {
3789
                $submission->attemptnumber = $attemptnumber;
3790
            } else {
3791
                $submission->attemptnumber = 0;
3792
            }
3793
            // Work out if this is the latest submission.
3794
            $submission->latest = 0;
3795
            $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3796
            if ($attemptnumber == -1) {
3797
                // This is a new submission so it must be the latest.
3798
                $submission->latest = 1;
3799
            } else {
3800
                // We need to work this out.
3801
                $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3802
                $latestsubmission = null;
3803
                if ($result) {
3804
                    $latestsubmission = reset($result);
3805
                }
3806
                if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) {
3807
                    $submission->latest = 1;
3808
                }
3809
            }
3810
            $transaction = $DB->start_delegated_transaction();
3811
            if ($submission->latest) {
3812
                // This is the case when we need to set latest to 0 for all the other attempts.
3813
                $DB->set_field('assign_submission', 'latest', 0, $params);
3814
            }
3815
            $sid = $DB->insert_record('assign_submission', $submission);
3816
            $transaction->allow_commit();
3817
            return $DB->get_record('assign_submission', array('id' => $sid));
3818
        }
3819
        return false;
3820
    }
3821
 
3822
    /**
3823
     * Load the submission object from it's id.
3824
     *
3825
     * @param int $submissionid The id of the submission we want
3826
     * @return stdClass The submission
3827
     */
3828
    protected function get_submission($submissionid) {
3829
        global $DB;
3830
 
3831
        $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid);
3832
        return $DB->get_record('assign_submission', $params, '*', MUST_EXIST);
3833
    }
3834
 
3835
    /**
3836
     * This will retrieve a user flags object from the db optionally creating it if required.
3837
     * The user flags was split from the user_grades table in 2.5.
3838
     *
3839
     * @param int $userid The user we are getting the flags for.
3840
     * @param bool $create If true the flags record will be created if it does not exist
3841
     * @return stdClass The flags record
3842
     */
3843
    public function get_user_flags($userid, $create) {
3844
        global $DB, $USER;
3845
 
3846
        // If the userid is not null then use userid.
3847
        if (!$userid) {
3848
            $userid = $USER->id;
3849
        }
3850
 
3851
        $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3852
 
3853
        $flags = $DB->get_record('assign_user_flags', $params);
3854
 
3855
        if ($flags) {
3856
            return $flags;
3857
        }
3858
        if ($create) {
3859
            $flags = new stdClass();
3860
            $flags->assignment = $this->get_instance()->id;
3861
            $flags->userid = $userid;
3862
            $flags->locked = 0;
3863
            $flags->extensionduedate = 0;
3864
            $flags->workflowstate = '';
3865
            $flags->allocatedmarker = 0;
3866
 
3867
            // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
3868
            // This is because students only want to be notified about certain types of update (grades and feedback).
3869
            $flags->mailed = 2;
3870
 
3871
            $fid = $DB->insert_record('assign_user_flags', $flags);
3872
            $flags->id = $fid;
3873
            return $flags;
3874
        }
3875
        return false;
3876
    }
3877
 
3878
    /**
3879
     * This will retrieve a grade object from the db, optionally creating it if required.
3880
     *
3881
     * @param int $userid The user we are grading
3882
     * @param bool $create If true the grade will be created if it does not exist
3883
     * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
3884
     * @return stdClass The grade record
3885
     */
3886
    public function get_user_grade($userid, $create, $attemptnumber=-1) {
3887
        global $DB, $USER;
3888
 
3889
        // If the userid is not null then use userid.
3890
        if (!$userid) {
3891
            $userid = $USER->id;
3892
        }
3893
        $submission = null;
3894
 
3895
        $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3896
        if ($attemptnumber < 0 || $create) {
3897
            // Make sure this grade matches the latest submission attempt.
3898
            if ($this->get_instance()->teamsubmission) {
3899
                $submission = $this->get_group_submission($userid, 0, true, $attemptnumber);
3900
            } else {
3901
                $submission = $this->get_user_submission($userid, true, $attemptnumber);
3902
            }
3903
            if ($submission) {
3904
                $attemptnumber = $submission->attemptnumber;
3905
            }
3906
        }
3907
 
3908
        if ($attemptnumber >= 0) {
3909
            $params['attemptnumber'] = $attemptnumber;
3910
        }
3911
 
3912
        $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
3913
 
3914
        if ($grades) {
3915
            return reset($grades);
3916
        }
3917
        if ($create) {
3918
            $grade = new stdClass();
3919
            $grade->assignment   = $this->get_instance()->id;
3920
            $grade->userid       = $userid;
3921
            $grade->timecreated = time();
3922
            // If we are "auto-creating" a grade - and there is a submission
3923
            // the new grade should not have a more recent timemodified value
3924
            // than the submission.
3925
            if ($submission) {
3926
                $grade->timemodified = $submission->timemodified;
3927
            } else {
3928
                $grade->timemodified = $grade->timecreated;
3929
            }
3930
            $grade->grade = -1;
3931
            // Do not set the grader id here as it would be the admin users which is incorrect.
3932
            $grade->grader = -1;
3933
            if ($attemptnumber >= 0) {
3934
                $grade->attemptnumber = $attemptnumber;
3935
            }
3936
 
3937
            $gid = $DB->insert_record('assign_grades', $grade);
3938
            $grade->id = $gid;
3939
            return $grade;
3940
        }
3941
        return false;
3942
    }
3943
 
3944
    /**
3945
     * This will retrieve a grade object from the db.
3946
     *
3947
     * @param int $gradeid The id of the grade
3948
     * @return stdClass The grade record
3949
     */
3950
    protected function get_grade($gradeid) {
3951
        global $DB;
3952
 
3953
        $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid);
3954
        return $DB->get_record('assign_grades', $params, '*', MUST_EXIST);
3955
    }
3956
 
3957
    /**
3958
     * Print the grading page for a single user submission.
3959
     *
3960
     * @param array $args Optional args array (better than pulling args from _GET and _POST)
3961
     * @return string
3962
     */
3963
    protected function view_single_grading_panel($args) {
3964
        global $DB, $CFG;
3965
 
3966
        $o = '';
3967
 
3968
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3969
 
3970
        // Need submit permission to submit an assignment.
3971
        require_capability('mod/assign:grade', $this->context);
3972
 
3973
        // If userid is passed - we are only grading a single student.
3974
        $userid = $args['userid'];
3975
        $attemptnumber = $args['attemptnumber'];
3976
        $instance = $this->get_instance($userid);
3977
 
3978
        // Apply overrides.
3979
        $this->update_effective_access($userid);
3980
 
3981
        $rownum = 0;
3982
        $useridlist = array($userid);
3983
 
3984
        $last = true;
3985
        // This variation on the url will link direct to this student, with no next/previous links.
3986
        // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
3987
        $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
3988
        $this->register_return_link('grade', $returnparams);
3989
 
3990
        $user = $DB->get_record('user', array('id' => $userid));
3991
        $submission = $this->get_user_submission($userid, false, $attemptnumber);
3992
        $submissiongroup = null;
3993
        $teamsubmission = null;
3994
        $notsubmitted = array();
3995
        if ($instance->teamsubmission) {
3996
            $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
3997
            $submissiongroup = $this->get_submission_group($userid);
3998
            $groupid = 0;
3999
            if ($submissiongroup) {
4000
                $groupid = $submissiongroup->id;
4001
            }
4002
            $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
4003
 
4004
        }
4005
 
4006
        // Get the requested grade.
4007
        $grade = $this->get_user_grade($userid, false, $attemptnumber);
4008
        $flags = $this->get_user_flags($userid, false);
4009
        if ($this->can_view_submission($userid)) {
4010
            $submissionlocked = ($flags && $flags->locked);
4011
            $extensionduedate = null;
4012
            if ($flags) {
4013
                $extensionduedate = $flags->extensionduedate;
4014
            }
4015
            $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
4016
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4017
            $usergroups = $this->get_all_groups($user->id);
4018
 
4019
            $submissionstatus = new assign_submission_status_compact($instance->allowsubmissionsfromdate,
4020
                                                                     $instance->alwaysshowdescription,
4021
                                                                     $submission,
4022
                                                                     $instance->teamsubmission,
4023
                                                                     $teamsubmission,
4024
                                                                     $submissiongroup,
4025
                                                                     $notsubmitted,
4026
                                                                     $this->is_any_submission_plugin_enabled(),
4027
                                                                     $submissionlocked,
4028
                                                                     $this->is_graded($userid),
4029
                                                                     $instance->duedate,
4030
                                                                     $instance->cutoffdate,
4031
                                                                     $this->get_submission_plugins(),
4032
                                                                     $this->get_return_action(),
4033
                                                                     $this->get_return_params(),
4034
                                                                     $this->get_course_module()->id,
4035
                                                                     $this->get_course()->id,
4036
                                                                     assign_submission_status::GRADER_VIEW,
4037
                                                                     $showedit,
4038
                                                                     false,
4039
                                                                     $viewfullnames,
4040
                                                                     $extensionduedate,
4041
                                                                     $this->get_context(),
4042
                                                                     $this->is_blind_marking(),
4043
                                                                     '',
4044
                                                                     $instance->attemptreopenmethod,
4045
                                                                     $instance->maxattempts,
4046
                                                                     $this->get_grading_status($userid),
4047
                                                                     $instance->preventsubmissionnotingroup,
4048
                                                                     $usergroups,
4049
                                                                     $instance->timelimit);
4050
            $o .= $this->get_renderer()->render($submissionstatus);
4051
        }
4052
 
4053
        if ($grade) {
4054
            $data = new stdClass();
4055
            if ($grade->grade !== null && $grade->grade >= 0) {
4056
                $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4057
            }
4058
        } else {
4059
            $data = new stdClass();
4060
            $data->grade = '';
4061
        }
4062
 
4063
        if (!empty($flags->workflowstate)) {
4064
            $data->workflowstate = $flags->workflowstate;
4065
        }
4066
        if (!empty($flags->allocatedmarker)) {
4067
            $data->allocatedmarker = $flags->allocatedmarker;
4068
        }
4069
 
4070
        // Warning if required.
4071
        $allsubmissions = $this->get_all_submissions($userid);
4072
 
4073
        if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
4074
            $params = array('attemptnumber' => $attemptnumber + 1,
4075
                            'totalattempts' => count($allsubmissions));
4076
            $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4077
            $o .= $this->get_renderer()->notification($message);
4078
        }
4079
 
4080
        $pagination = array('rownum' => $rownum,
4081
                            'useridlistid' => 0,
4082
                            'last' => $last,
4083
                            'userid' => $userid,
4084
                            'attemptnumber' => $attemptnumber,
4085
                            'gradingpanel' => true);
4086
 
4087
        if (!empty($args['formdata'])) {
4088
            $data = (array) $data;
4089
            $data = (object) array_merge($data, $args['formdata']);
4090
        }
4091
        $formparams = array($this, $data, $pagination);
4092
        $mform = new mod_assign_grade_form(null,
4093
                                           $formparams,
4094
                                           'post',
4095
                                           '',
4096
                                           array('class' => 'gradeform'));
4097
 
4098
        if (!empty($args['formdata'])) {
4099
            // If we were passed form data - we want the form to check the data
4100
            // and show errors.
4101
            $mform->is_validated();
4102
        }
4103
 
4104
        $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4105
 
4106
        if (count($allsubmissions) > 1) {
4107
            $allgrades = $this->get_all_grades($userid);
4108
            $history = new assign_attempt_history_chooser($allsubmissions,
4109
                                                          $allgrades,
4110
                                                          $this->get_course_module()->id,
4111
                                                          $userid);
4112
 
4113
            $o .= $this->get_renderer()->render($history);
4114
        }
4115
 
4116
        \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4117
 
4118
        return $o;
4119
    }
4120
 
4121
    /**
4122
     * Print the grading page for a single user submission.
4123
     *
4124
     * @param moodleform $mform
4125
     * @return string
4126
     */
4127
    protected function view_single_grade_page($mform) {
4128
        global $DB, $CFG, $SESSION;
4129
 
4130
        $o = '';
4131
        $instance = $this->get_instance();
4132
 
4133
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4134
 
4135
        // Need submit permission to submit an assignment.
4136
        require_capability('mod/assign:grade', $this->context);
4137
 
4138
        $header = new assign_header($instance,
4139
                                    $this->get_context(),
4140
                                    false,
4141
                                    $this->get_course_module()->id,
4142
                                    get_string('grading', 'assign'));
4143
        $o .= $this->get_renderer()->render($header);
4144
 
4145
        // If userid is passed - we are only grading a single student.
4146
        $rownum = optional_param('rownum', 0, PARAM_INT);
4147
        $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
4148
        $userid = optional_param('userid', 0, PARAM_INT);
4149
        $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
4150
 
4151
        if (!$userid) {
4152
            $useridlist = $this->get_grading_userid_list(true, $useridlistid);
4153
        } else {
4154
            $rownum = 0;
4155
            $useridlistid = 0;
4156
            $useridlist = array($userid);
4157
        }
4158
 
4159
        if ($rownum < 0 || $rownum > count($useridlist)) {
4160
            throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum);
4161
        }
4162
 
4163
        $last = false;
4164
        $userid = $useridlist[$rownum];
4165
        if ($rownum == count($useridlist) - 1) {
4166
            $last = true;
4167
        }
4168
        // This variation on the url will link direct to this student, with no next/previous links.
4169
        // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4170
        $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4171
        $this->register_return_link('grade', $returnparams);
4172
 
4173
        $user = $DB->get_record('user', array('id' => $userid));
4174
        if ($user) {
4175
            $this->update_effective_access($userid);
4176
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4177
            $usersummary = new assign_user_summary($user,
4178
                                                   $this->get_course()->id,
4179
                                                   $viewfullnames,
4180
                                                   $this->is_blind_marking(),
4181
                                                   $this->get_uniqueid_for_user($user->id),
4182
                                                   // TODO Does not support custom user profile fields (MDL-70456).
4183
                                                   \core_user\fields::get_identity_fields($this->get_context(), false),
4184
                                                   !$this->is_active_user($userid));
4185
            $o .= $this->get_renderer()->render($usersummary);
4186
        }
4187
        $submission = $this->get_user_submission($userid, false, $attemptnumber);
4188
        $submissiongroup = null;
4189
        $teamsubmission = null;
4190
        $notsubmitted = array();
4191
        if ($instance->teamsubmission) {
4192
            $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
4193
            $submissiongroup = $this->get_submission_group($userid);
4194
            $groupid = 0;
4195
            if ($submissiongroup) {
4196
                $groupid = $submissiongroup->id;
4197
            }
4198
            $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
4199
 
4200
        }
4201
 
4202
        // Get the requested grade.
4203
        $grade = $this->get_user_grade($userid, false, $attemptnumber);
4204
        $flags = $this->get_user_flags($userid, false);
4205
        if ($this->can_view_submission($userid)) {
4206
            $submissionlocked = ($flags && $flags->locked);
4207
            $extensionduedate = null;
4208
            if ($flags) {
4209
                $extensionduedate = $flags->extensionduedate;
4210
            }
4211
            $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
4212
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4213
            $usergroups = $this->get_all_groups($user->id);
4214
            $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
4215
                                                             $instance->alwaysshowdescription,
4216
                                                             $submission,
4217
                                                             $instance->teamsubmission,
4218
                                                             $teamsubmission,
4219
                                                             $submissiongroup,
4220
                                                             $notsubmitted,
4221
                                                             $this->is_any_submission_plugin_enabled(),
4222
                                                             $submissionlocked,
4223
                                                             $this->is_graded($userid),
4224
                                                             $instance->duedate,
4225
                                                             $instance->cutoffdate,
4226
                                                             $this->get_submission_plugins(),
4227
                                                             $this->get_return_action(),
4228
                                                             $this->get_return_params(),
4229
                                                             $this->get_course_module()->id,
4230
                                                             $this->get_course()->id,
4231
                                                             assign_submission_status::GRADER_VIEW,
4232
                                                             $showedit,
4233
                                                             false,
4234
                                                             $viewfullnames,
4235
                                                             $extensionduedate,
4236
                                                             $this->get_context(),
4237
                                                             $this->is_blind_marking(),
4238
                                                             '',
4239
                                                             $instance->attemptreopenmethod,
4240
                                                             $instance->maxattempts,
4241
                                                             $this->get_grading_status($userid),
4242
                                                             $instance->preventsubmissionnotingroup,
4243
                                                             $usergroups,
4244
                                                             $instance->timelimit);
4245
            $o .= $this->get_renderer()->render($submissionstatus);
4246
        }
4247
 
4248
        if ($grade) {
4249
            $data = new stdClass();
4250
            if ($grade->grade !== null && $grade->grade >= 0) {
4251
                $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4252
            }
4253
        } else {
4254
            $data = new stdClass();
4255
            $data->grade = '';
4256
        }
4257
 
4258
        if (!empty($flags->workflowstate)) {
4259
            $data->workflowstate = $flags->workflowstate;
4260
        }
4261
        if (!empty($flags->allocatedmarker)) {
4262
            $data->allocatedmarker = $flags->allocatedmarker;
4263
        }
4264
 
4265
        // Warning if required.
4266
        $allsubmissions = $this->get_all_submissions($userid);
4267
 
4268
        if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
4269
            $params = array('attemptnumber'=>$attemptnumber + 1,
4270
                            'totalattempts'=>count($allsubmissions));
4271
            $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4272
            $o .= $this->get_renderer()->notification($message);
4273
        }
4274
 
4275
        // Now show the grading form.
4276
        if (!$mform) {
4277
            $pagination = array('rownum' => $rownum,
4278
                                'useridlistid' => $useridlistid,
4279
                                'last' => $last,
4280
                                'userid' => $userid,
4281
                                'attemptnumber' => $attemptnumber);
4282
            $formparams = array($this, $data, $pagination);
4283
            $mform = new mod_assign_grade_form(null,
4284
                                               $formparams,
4285
                                               'post',
4286
                                               '',
4287
                                               array('class'=>'gradeform'));
4288
        }
4289
 
4290
        $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4291
 
4292
        if (count($allsubmissions) > 1 && $attemptnumber == -1) {
4293
            $allgrades = $this->get_all_grades($userid);
4294
            $history = new assign_attempt_history($allsubmissions,
4295
                                                  $allgrades,
4296
                                                  $this->get_submission_plugins(),
4297
                                                  $this->get_feedback_plugins(),
4298
                                                  $this->get_course_module()->id,
4299
                                                  $this->get_return_action(),
4300
                                                  $this->get_return_params(),
4301
                                                  true,
4302
                                                  $useridlistid,
4303
                                                  $rownum);
4304
 
4305
            $o .= $this->get_renderer()->render($history);
4306
        }
4307
 
4308
        \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4309
 
4310
        $o .= $this->view_footer();
4311
        return $o;
4312
    }
4313
 
4314
    /**
4315
     * Show a confirmation page to make sure they want to remove submission data.
4316
     *
4317
     * @return string
4318
     */
4319
    protected function view_remove_submission_confirm() {
4320
        global $USER;
4321
 
4322
        $userid = optional_param('userid', $USER->id, PARAM_INT);
4323
 
4324
        if (!$this->can_edit_submission($userid, $USER->id)) {
4325
            throw new \moodle_exception('nopermission');
4326
        }
4327
        $user = core_user::get_user($userid, '*', MUST_EXIST);
4328
 
4329
        $o = '';
4330
        $header = new assign_header($this->get_instance(),
4331
                                    $this->get_context(),
4332
                                    false,
4333
                                    $this->get_course_module()->id);
4334
        $o .= $this->get_renderer()->render($header);
4335
 
4336
        $urlparams = array('id' => $this->get_course_module()->id,
4337
                           'action' => 'removesubmission',
4338
                           'userid' => $userid,
4339
                           'sesskey' => sesskey());
4340
        $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4341
 
4342
        $urlparams = array('id' => $this->get_course_module()->id,
4343
                           'action' => 'view');
4344
        $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4345
 
4346
        if ($userid == $USER->id) {
4347
            if ($this->is_time_limit_enabled($userid)) {
4348
                $confirmstr = get_string('removesubmissionconfirmwithtimelimit', 'assign');
4349
            } else {
4350
                $confirmstr = get_string('removesubmissionconfirm', 'assign');
4351
            }
4352
        } else {
4353
            if ($this->is_time_limit_enabled($userid)) {
4354
                $confirmstr = get_string('removesubmissionconfirmforstudentwithtimelimit', 'assign', $this->fullname($user));
4355
            } else {
4356
                $confirmstr = get_string('removesubmissionconfirmforstudent', 'assign', $this->fullname($user));
4357
            }
4358
        }
4359
        $o .= $this->get_renderer()->confirm($confirmstr,
4360
                                             $confirmurl,
4361
                                             $cancelurl);
4362
        $o .= $this->view_footer();
4363
 
4364
        \mod_assign\event\remove_submission_form_viewed::create_from_user($this, $user)->trigger();
4365
 
4366
        return $o;
4367
    }
4368
 
4369
 
4370
    /**
4371
     * Show a confirmation page to make sure they want to release student identities.
4372
     *
4373
     * @return string
4374
     */
4375
    protected function view_reveal_identities_confirm() {
4376
        require_capability('mod/assign:revealidentities', $this->get_context());
4377
 
4378
        $o = '';
4379
        $header = new assign_header($this->get_instance(),
4380
                                    $this->get_context(),
4381
                                    false,
4382
                                    $this->get_course_module()->id);
4383
        $o .= $this->get_renderer()->render($header);
4384
 
4385
        $urlparams = array('id'=>$this->get_course_module()->id,
4386
                           'action'=>'revealidentitiesconfirm',
4387
                           'sesskey'=>sesskey());
4388
        $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4389
 
4390
        $urlparams = array('id'=>$this->get_course_module()->id,
4391
                           'action'=>'grading');
4392
        $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4393
 
4394
        $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'),
4395
                                             $confirmurl,
4396
                                             $cancelurl);
4397
        $o .= $this->view_footer();
4398
 
4399
        \mod_assign\event\reveal_identities_confirmation_page_viewed::create_from_assign($this)->trigger();
4400
 
4401
        return $o;
4402
    }
4403
 
4404
    /**
4405
     * View a link to go back to the previous page. Uses url parameters returnaction and returnparams.
4406
     *
4407
     * @return string
4408
     */
4409
    protected function view_return_links() {
4410
        $returnaction = optional_param('returnaction', '', PARAM_ALPHA);
4411
        $returnparams = optional_param('returnparams', '', PARAM_TEXT);
4412
 
4413
        $params = array();
4414
        $returnparams = str_replace('&amp;', '&', $returnparams);
4415
        parse_str($returnparams, $params);
4416
        $newparams = array('id' => $this->get_course_module()->id, 'action' => $returnaction);
4417
        $params = array_merge($newparams, $params);
4418
 
4419
        $url = new moodle_url('/mod/assign/view.php', $params);
4420
        return $this->get_renderer()->single_button($url, get_string('back'), 'get');
4421
    }
4422
 
4423
    /**
4424
     * View the grading table of all submissions for this assignment.
4425
     *
4426
     * @return string
4427
     */
4428
    protected function view_grading_table() {
4429
        global $USER, $CFG, $SESSION, $PAGE;
4430
 
4431
        // Include grading options form.
4432
        require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
4433
        require_once($CFG->dirroot . '/mod/assign/quickgradingform.php');
4434
        require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
4435
        $o = '';
4436
        $cmid = $this->get_course_module()->id;
4437
 
4438
        $links = array();
4439
        if (has_capability('gradereport/grader:view', $this->get_course_context()) &&
4440
                has_capability('moodle/grade:viewall', $this->get_course_context())) {
4441
            $gradebookurl = '/grade/report/grader/index.php?id=' . $this->get_course()->id;
4442
            $links[$gradebookurl] = get_string('viewgradebook', 'assign');
4443
        }
4444
        if ($this->is_blind_marking() &&
4445
                has_capability('mod/assign:revealidentities', $this->get_context())) {
4446
            $revealidentitiesurl = '/mod/assign/view.php?id=' . $cmid . '&action=revealidentities';
4447
            $links[$revealidentitiesurl] = get_string('revealidentities', 'assign');
4448
        }
4449
        foreach ($this->get_feedback_plugins() as $plugin) {
4450
            if ($plugin->is_enabled() && $plugin->is_visible()) {
4451
                foreach ($plugin->get_grading_actions() as $action => $description) {
4452
                    $url = '/mod/assign/view.php' .
4453
                           '?id=' .  $cmid .
4454
                           '&plugin=' . $plugin->get_type() .
4455
                           '&pluginsubtype=assignfeedback' .
4456
                           '&action=viewpluginpage&pluginaction=' . $action;
4457
                    $links[$url] = $description;
4458
                }
4459
            }
4460
        }
4461
 
4462
        // Sort links alphabetically based on the link description.
4463
        core_collator::asort($links);
4464
 
4465
        $gradingactions = new url_select($links);
4466
        $gradingactions->set_label(get_string('choosegradingaction', 'assign'));
4467
        $gradingactions->class .= ' mb-1';
4468
 
4469
        $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
4470
 
4471
        $perpage = $this->get_assign_perpage();
4472
        $filter = get_user_preferences('assign_filter', '');
4473
        $markerfilter = get_user_preferences('assign_markerfilter', '');
4474
        $workflowfilter = get_user_preferences('assign_workflowfilter', '');
4475
        $controller = $gradingmanager->get_active_controller();
4476
        $showquickgrading = empty($controller) && $this->can_grade();
4477
        $quickgrading = get_user_preferences('assign_quickgrading', false);
4478
        $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
4479
        $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
4480
 
4481
        $markingallocation = $this->get_instance()->markingworkflow &&
4482
            $this->get_instance()->markingallocation &&
4483
            has_capability('mod/assign:manageallocations', $this->context);
4484
        // Get markers to use in drop lists.
4485
        $markingallocationoptions = array();
4486
        if ($markingallocation) {
4487
            list($sort, $params) = users_order_by_sql('u');
4488
            // Only enrolled users could be assigned as potential markers.
4489
            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
4490
            $markingallocationoptions[''] = get_string('filternone', 'assign');
4491
            $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
4492
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
4493
            foreach ($markers as $marker) {
4494
                $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
4495
            }
4496
        }
4497
 
4498
        $markingworkflow = $this->get_instance()->markingworkflow;
4499
        // Get marking states to show in form.
4500
        $markingworkflowoptions = $this->get_marking_workflow_filters();
4501
 
4502
        // Print options for changing the filter and changing the number of results per page.
4503
        $gradingoptionsformparams = array('cm'=>$cmid,
4504
                                          'contextid'=>$this->context->id,
4505
                                          'userid'=>$USER->id,
4506
                                          'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
4507
                                          'showquickgrading'=>$showquickgrading,
4508
                                          'quickgrading'=>$quickgrading,
4509
                                          'markingworkflowopt'=>$markingworkflowoptions,
4510
                                          'markingallocationopt'=>$markingallocationoptions,
4511
                                          'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
4512
                                          'showonlyactiveenrol' => $this->show_only_active_users(),
4513
                                          'downloadasfolders' => $downloadasfolders);
4514
 
4515
        $classoptions = array('class'=>'gradingoptionsform');
4516
        $gradingoptionsform = new mod_assign_grading_options_form(null,
4517
                                                                  $gradingoptionsformparams,
4518
                                                                  'post',
4519
                                                                  '',
4520
                                                                  $classoptions);
4521
 
4522
        $batchformparams = array('cm'=>$cmid,
4523
                                 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
4524
                                 'duedate'=>$this->get_instance()->duedate,
4525
                                 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
4526
                                 'feedbackplugins'=>$this->get_feedback_plugins(),
4527
                                 'context'=>$this->get_context(),
4528
                                 'markingworkflow'=>$markingworkflow,
4529
                                 'markingallocation'=>$markingallocation);
4530
        $classoptions = [
4531
            'class' => 'gradingbatchoperationsform',
4532
            'data-double-submit-protection' => 'off',
4533
        ];
4534
 
4535
        $gradingbatchoperationsform = new mod_assign_grading_batch_operations_form(null,
4536
                                                                                   $batchformparams,
4537
                                                                                   'post',
4538
                                                                                   '',
4539
                                                                                   $classoptions);
4540
 
4541
        $gradingoptionsdata = new stdClass();
4542
        $gradingoptionsdata->perpage = $perpage;
4543
        $gradingoptionsdata->filter = $filter;
4544
        $gradingoptionsdata->markerfilter = $markerfilter;
4545
        $gradingoptionsdata->workflowfilter = $workflowfilter;
4546
        $gradingoptionsform->set_data($gradingoptionsdata);
4547
 
4548
        $buttons = new \mod_assign\output\grading_actionmenu($this->get_course_module()->id,
4549
             $this->is_any_submission_plugin_enabled(), $this->count_submissions());
4550
        $actionformtext = $this->get_renderer()->render($buttons);
4551
        $PAGE->activityheader->set_attrs(['hidecompletion' => true]);
4552
 
4553
        $currenturl = new moodle_url('/mod/assign/view.php', ['id' => $this->get_course_module()->id, 'action' => 'grading']);
4554
 
4555
        $header = new assign_header($this->get_instance(),
4556
                                    $this->get_context(),
4557
                                    false,
4558
                                    $this->get_course_module()->id,
4559
                                    get_string('grading', 'assign'),
4560
                                    '',
4561
                                    '',
4562
                                    $currenturl);
4563
        $o .= $this->get_renderer()->render($header);
4564
 
4565
        $o .= $actionformtext;
4566
 
4567
        $o .= $this->get_renderer()->heading(get_string('gradeitem:submissions', 'mod_assign'), 2);
4568
        $o .= $this->get_renderer()->render($gradingactions);
4569
 
4570
        $o .= groups_print_activity_menu($this->get_course_module(), $currenturl, true);
4571
 
4572
        // Plagiarism update status apearring in the grading book.
4573
        if (!empty($CFG->enableplagiarism)) {
4574
            require_once($CFG->libdir . '/plagiarismlib.php');
4575
            $o .= plagiarism_update_status($this->get_course(), $this->get_course_module());
4576
        }
4577
 
4578
        if ($this->is_blind_marking() && has_capability('mod/assign:viewblinddetails', $this->get_context())) {
4579
            $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage');
4580
        }
4581
 
4582
        // Load and print the table of submissions.
4583
        if ($showquickgrading && $quickgrading) {
4584
            $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, true);
4585
            $table = $this->get_renderer()->render($gradingtable);
4586
            $page = optional_param('page', null, PARAM_INT);
4587
            $quickformparams = array('cm'=>$this->get_course_module()->id,
4588
                                     'gradingtable'=>$table,
4589
                                     'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications,
4590
                                     'page' => $page);
4591
            $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams);
4592
 
4593
            $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
4594
        } else {
4595
            $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, false);
4596
            $o .= $this->get_renderer()->render($gradingtable);
4597
        }
4598
 
4599
        if ($this->can_grade()) {
4600
            // We need to store the order of uses in the table as the person may wish to grade them.
4601
            // This is done based on the row number of the user.
4602
            $useridlist = $gradingtable->get_column_data('userid');
4603
            $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist;
4604
        }
4605
 
4606
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4607
        $users = array_keys($this->list_participants($currentgroup, true));
4608
        if (count($users) != 0 && $this->can_grade()) {
4609
            // If no enrolled user in a course then don't display the batch operations feature.
4610
            $assignform = new assign_form('gradingbatchoperationsform', $gradingbatchoperationsform);
4611
            $o .= $this->get_renderer()->render($assignform);
4612
        }
4613
        $assignform = new assign_form('gradingoptionsform',
4614
                                      $gradingoptionsform,
4615
                                      'M.mod_assign.init_grading_options');
4616
        $o .= $this->get_renderer()->render($assignform);
4617
        return $o;
4618
    }
4619
 
4620
    /**
4621
     * View entire grader app.
4622
     *
4623
     * @return string
4624
     */
4625
    protected function view_grader() {
4626
        global $USER, $PAGE;
4627
 
4628
        $o = '';
4629
        // Need submit permission to submit an assignment.
4630
        $this->require_view_grades();
4631
 
4632
        $PAGE->set_pagelayout('embedded');
4633
 
4634
        $PAGE->activityheader->disable();
4635
 
4636
        $courseshortname = $this->get_context()->get_course_context()->get_context_name(false, true);
4637
        $args = [
4638
            'contextname' => $this->get_context()->get_context_name(false, true),
4639
            'subpage' => get_string('grading', 'assign')
4640
        ];
4641
        $title = get_string('subpagetitle', 'assign', $args);
4642
        $title = $courseshortname . ': ' . $title;
4643
        $PAGE->set_title($title);
4644
 
4645
        $o .= $this->get_renderer()->header();
4646
 
4647
        $userid = optional_param('userid', 0, PARAM_INT);
4648
        $blindid = optional_param('blindid', 0, PARAM_INT);
4649
 
4650
        if (!$userid && $blindid) {
4651
            $userid = $this->get_user_id_for_uniqueid($blindid);
4652
        }
4653
 
4654
        // Instantiate table object to apply table preferences.
4655
        $gradingtable = new assign_grading_table($this, 10, '', 0, false);
4656
        $gradingtable->setup();
4657
 
4658
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4659
        $framegrader = new grading_app($userid, $currentgroup, $this);
4660
 
4661
        $this->update_effective_access($userid);
4662
 
4663
        $o .= $this->get_renderer()->render($framegrader);
4664
 
4665
        $o .= $this->view_footer();
4666
 
4667
        \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4668
 
4669
        return $o;
4670
    }
4671
    /**
4672
     * View entire grading page.
4673
     *
4674
     * @return string
4675
     */
4676
    protected function view_grading_page() {
4677
        global $CFG;
4678
 
4679
        $o = '';
4680
        // Need submit permission to submit an assignment.
4681
        $this->require_view_grades();
4682
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4683
 
4684
        $this->add_grade_notices();
4685
 
4686
        // Only load this if it is.
4687
        $o .= $this->view_grading_table();
4688
 
4689
        $o .= $this->view_footer();
4690
 
4691
        \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4692
 
4693
        return $o;
4694
    }
4695
 
4696
    /**
4697
     * Capture the output of the plagiarism plugins disclosures and return it as a string.
4698
     *
4699
     * @return string
4700
     */
4701
    protected function plagiarism_print_disclosure() {
4702
        global $CFG;
4703
        $o = '';
4704
 
4705
        if (!empty($CFG->enableplagiarism)) {
4706
            require_once($CFG->libdir . '/plagiarismlib.php');
4707
 
4708
            $o .= plagiarism_print_disclosure($this->get_course_module()->id);
4709
        }
4710
 
4711
        return $o;
4712
    }
4713
 
4714
    /**
4715
     * Message for students when assignment submissions have been closed.
4716
     *
4717
     * @param string $title The page title
4718
     * @param array $notices The array of notices to show.
4719
     * @return string
4720
     */
4721
    protected function view_notices($title, $notices) {
4722
        global $CFG;
4723
 
4724
        $o = '';
4725
 
4726
        $header = new assign_header($this->get_instance(),
4727
                                    $this->get_context(),
4728
                                    $this->show_intro(),
4729
                                    $this->get_course_module()->id,
4730
                                    $title);
4731
        $o .= $this->get_renderer()->render($header);
4732
 
4733
        foreach ($notices as $notice) {
4734
            $o .= $this->get_renderer()->notification($notice);
4735
        }
4736
 
4737
        $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 'action'=>'view'));
4738
        $o .= $this->get_renderer()->continue_button($url);
4739
 
4740
        $o .= $this->view_footer();
4741
 
4742
        return $o;
4743
    }
4744
 
4745
    /**
4746
     * Get the name for a user - hiding their real name if blind marking is on.
4747
     *
4748
     * @param stdClass $user The user record as required by fullname()
4749
     * @return string The name.
4750
     */
4751
    public function fullname($user) {
4752
        if ($this->is_blind_marking()) {
4753
            $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
4754
            if (empty($user->recordid)) {
4755
                $uniqueid = $this->get_uniqueid_for_user($user->id);
4756
            } else {
4757
                $uniqueid = $user->recordid;
4758
            }
4759
            if ($hasviewblind) {
4760
                return get_string('participant', 'assign') . ' ' . $uniqueid . ' (' .
4761
                        fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())) . ')';
4762
            } else {
4763
                return get_string('participant', 'assign') . ' ' . $uniqueid;
4764
            }
4765
        } else {
4766
            return fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
4767
        }
4768
    }
4769
 
4770
    /**
4771
     * View edit submissions page.
4772
     *
4773
     * @param moodleform $mform
4774
     * @param array $notices A list of notices to display at the top of the
4775
     *                       edit submission form (e.g. from plugins).
4776
     * @return string The page output.
4777
     */
4778
    protected function view_edit_submission_page($mform, $notices) {
4779
        global $CFG, $USER, $DB, $PAGE;
4780
 
4781
        $o = '';
4782
        require_once($CFG->dirroot . '/mod/assign/submission_form.php');
4783
        // Need submit permission to submit an assignment.
4784
        $userid = optional_param('userid', $USER->id, PARAM_INT);
4785
        $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
4786
        $timelimitenabled = get_config('assign', 'enabletimelimit');
4787
 
4788
        // This variation on the url will link direct to this student.
4789
        // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4790
        $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4791
        $this->register_return_link('editsubmission', $returnparams);
4792
 
4793
        if ($userid == $USER->id) {
4794
            if (!$this->can_edit_submission($userid, $USER->id)) {
4795
                throw new \moodle_exception('nopermission');
4796
            }
4797
            // User is editing their own submission.
4798
            require_capability('mod/assign:submit', $this->context);
4799
            $title = get_string('editsubmission', 'assign');
4800
        } else {
4801
            // User is editing another user's submission.
4802
            if (!$this->can_edit_submission($userid, $USER->id)) {
4803
                throw new \moodle_exception('nopermission');
4804
            }
4805
 
4806
            $name = $this->fullname($user);
4807
            $title = get_string('editsubmissionother', 'assign', $name);
4808
        }
4809
 
4810
        if (!$this->submissions_open($userid)) {
4811
            $message = array(get_string('submissionsclosed', 'assign'));
4812
            return $this->view_notices($title, $message);
4813
        }
4814
 
4815
        $postfix = '';
4816
        if ($this->has_visible_attachments()) {
4817
            $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
4818
        }
4819
 
4820
        $data = new stdClass();
4821
        $data->userid = $userid;
4822
        if (!$mform) {
4823
            $mform = new mod_assign_submission_form(null, array($this, $data));
4824
        }
4825
 
4826
        if ($this->get_instance()->teamsubmission) {
4827
            $submission = $this->get_group_submission($userid, 0, false);
4828
        } else {
4829
            $submission = $this->get_user_submission($userid, false);
4830
        }
4831
 
4832
        if ($timelimitenabled && !empty($submission->timestarted) && $this->get_instance()->timelimit) {
4833
            $navbc = $this->get_timelimit_panel($submission);
4834
            $regions = $PAGE->blocks->get_regions();
4835
            $bc = new \block_contents();
4836
            $bc->attributes['id'] = 'mod_assign_timelimit_block';
4837
            $bc->attributes['role'] = 'navigation';
4838
            $bc->attributes['aria-labelledby'] = 'mod_assign_timelimit_block_title';
4839
            $bc->title = get_string('assigntimeleft', 'assign');
4840
            $bc->content = $navbc;
4841
            $PAGE->blocks->add_fake_block($bc, reset($regions));
4842
        }
4843
 
4844
        $o .= $this->get_renderer()->render(
4845
            new assign_header($this->get_instance(),
4846
                              $this->get_context(),
4847
                              $this->show_intro(),
4848
                              $this->get_course_module()->id,
4849
                              $title,
4850
                              '',
4851
                              $postfix,
4852
                              null,
4853
                              true
4854
            )
4855
        );
4856
 
4857
        // Show plagiarism disclosure for any user submitter.
4858
        $o .= $this->plagiarism_print_disclosure();
4859
 
4860
        foreach ($notices as $notice) {
4861
            $o .= $this->get_renderer()->notification($notice);
4862
        }
4863
 
4864
        $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
4865
        $o .= $this->view_footer();
4866
 
4867
        \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
4868
 
4869
        return $o;
4870
    }
4871
 
4872
    /**
4873
     * Get the time limit panel object for this submission attempt.
4874
     *
4875
     * @param stdClass $submission assign submission.
4876
     * @return string the panel output.
4877
     */
4878
    public function get_timelimit_panel(stdClass $submission): string {
4879
        global $USER;
4880
 
4881
        // Apply overrides.
4882
        $this->update_effective_access($USER->id);
4883
        $panel = new timelimit_panel($submission, $this->get_instance());
4884
        return $this->get_renderer()->render($panel);
4885
    }
4886
 
4887
    /**
4888
     * See if this assignment has a grade yet.
4889
     *
4890
     * @param int $userid
4891
     * @return bool
4892
     */
4893
    protected function is_graded($userid) {
4894
        $grade = $this->get_user_grade($userid, false);
4895
        if ($grade) {
4896
            return ($grade->grade !== null && $grade->grade >= 0);
4897
        }
4898
        return false;
4899
    }
4900
 
4901
    /**
4902
     * Perform an access check to see if the current $USER can edit this group submission.
4903
     *
4904
     * @param int $groupid
4905
     * @return bool
4906
     */
4907
    public function can_edit_group_submission($groupid) {
4908
        global $USER;
4909
 
4910
        $members = $this->get_submission_group_members($groupid, true);
4911
        foreach ($members as $member) {
4912
            // If we can edit any members submission, we can edit the submission for the group.
4913
            if ($this->can_edit_submission($member->id)) {
4914
                return true;
4915
            }
4916
        }
4917
        return false;
4918
    }
4919
 
4920
    /**
4921
     * Perform an access check to see if the current $USER can view this group submission.
4922
     *
4923
     * @param int $groupid
4924
     * @return bool
4925
     */
4926
    public function can_view_group_submission($groupid) {
4927
        global $USER;
4928
 
4929
        $members = $this->get_submission_group_members($groupid, true);
4930
        foreach ($members as $member) {
4931
            // If we can view any members submission, we can view the submission for the group.
4932
            if ($this->can_view_submission($member->id)) {
4933
                return true;
4934
            }
4935
        }
4936
        return false;
4937
    }
4938
 
4939
    /**
4940
     * Perform an access check to see if the current $USER can view this users submission.
4941
     *
4942
     * @param int $userid
4943
     * @return bool
4944
     */
4945
    public function can_view_submission($userid) {
4946
        global $USER;
4947
 
4948
        if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
4949
            return false;
4950
        }
4951
        if (!is_enrolled($this->get_course_context(), $userid)) {
4952
            return false;
4953
        }
4954
        if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
4955
            return true;
4956
        }
4957
        if ($userid == $USER->id) {
4958
            return true;
4959
        }
4960
        return false;
4961
    }
4962
 
4963
    /**
4964
     * Allows the plugin to show a batch grading operation page.
4965
     *
4966
     * @param moodleform $mform
4967
     * @return none
4968
     */
4969
    protected function view_plugin_grading_batch_operation($mform) {
4970
        require_capability('mod/assign:grade', $this->context);
4971
        $prefix = 'plugingradingbatchoperation_';
4972
 
4973
        if ($data = $mform->get_data()) {
4974
            $tail = substr($data->operation, strlen($prefix));
4975
            list($plugintype, $action) = explode('_', $tail, 2);
4976
 
4977
            $plugin = $this->get_feedback_plugin_by_type($plugintype);
4978
            if ($plugin) {
4979
                $users = $data->selectedusers;
4980
                $userlist = explode(',', $users);
4981
                echo $plugin->grading_batch_operation($action, $userlist);
4982
                return;
4983
            }
4984
        }
4985
        throw new \moodle_exception('invalidformdata', '');
4986
    }
4987
 
4988
    /**
4989
     * Ask the user to confirm they want to perform this batch operation
4990
     *
4991
     * @param moodleform $mform Set to a grading batch operations form
4992
     * @return string - the page to view after processing these actions
4993
     */
4994
    protected function process_grading_batch_operation(& $mform) {
4995
        global $CFG;
4996
        require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
4997
        require_sesskey();
4998
 
4999
        $markingallocation = $this->get_instance()->markingworkflow &&
5000
            $this->get_instance()->markingallocation &&
5001
            has_capability('mod/assign:manageallocations', $this->context);
5002
 
5003
        $batchformparams = array('cm'=>$this->get_course_module()->id,
5004
                                 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
5005
                                 'duedate'=>$this->get_instance()->duedate,
5006
                                 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
5007
                                 'feedbackplugins'=>$this->get_feedback_plugins(),
5008
                                 'context'=>$this->get_context(),
5009
                                 'markingworkflow'=>$this->get_instance()->markingworkflow,
5010
                                 'markingallocation'=>$markingallocation);
5011
        $formclasses = [
5012
            'class' => 'gradingbatchoperationsform',
5013
            'data-double-submit-protection' => 'off'
5014
        ];
5015
 
5016
        $mform = new mod_assign_grading_batch_operations_form(null,
5017
                                                              $batchformparams,
5018
                                                              'post',
5019
                                                              '',
5020
                                                              $formclasses);
5021
 
5022
        if ($data = $mform->get_data()) {
5023
            // Get the list of users.
5024
            $users = $data->selectedusers;
5025
            $userlist = explode(',', $users);
5026
 
5027
            $prefix = 'plugingradingbatchoperation_';
5028
 
5029
            if ($data->operation == 'grantextension') {
5030
                // Reset the form so the grant extension page will create the extension form.
5031
                $mform = null;
5032
                return 'grantextension';
5033
            } else if ($data->operation == 'setmarkingworkflowstate') {
5034
                return 'viewbatchsetmarkingworkflowstate';
5035
            } else if ($data->operation == 'setmarkingallocation') {
5036
                return 'viewbatchmarkingallocation';
5037
            } else if (strpos($data->operation, $prefix) === 0) {
5038
                $tail = substr($data->operation, strlen($prefix));
5039
                list($plugintype, $action) = explode('_', $tail, 2);
5040
 
5041
                $plugin = $this->get_feedback_plugin_by_type($plugintype);
5042
                if ($plugin) {
5043
                    return 'plugingradingbatchoperation';
5044
                }
5045
            }
5046
 
5047
            if ($data->operation == 'downloadselected') {
5048
                $this->download_submissions($userlist);
5049
            } else {
5050
                foreach ($userlist as $userid) {
5051
                    if ($data->operation == 'lock') {
5052
                        $this->process_lock_submission($userid);
5053
                    } else if ($data->operation == 'unlock') {
5054
                        $this->process_unlock_submission($userid);
5055
                    } else if ($data->operation == 'reverttodraft') {
5056
                        $this->process_revert_to_draft($userid);
5057
                    } else if ($data->operation == 'removesubmission') {
5058
                        $this->process_remove_submission($userid);
5059
                    } else if ($data->operation == 'addattempt') {
5060
                        if (!$this->get_instance()->teamsubmission) {
5061
                            $this->process_add_attempt($userid);
5062
                        }
5063
                    }
5064
                }
5065
            }
5066
            if ($this->get_instance()->teamsubmission && $data->operation == 'addattempt') {
5067
                // This needs to be handled separately so that each team submission is only re-opened one time.
5068
                $this->process_add_attempt_group($userlist);
5069
            }
5070
        }
5071
 
5072
        return 'grading';
5073
    }
5074
 
5075
    /**
5076
     * Shows a form that allows the workflow state for selected submissions to be changed.
5077
     *
5078
     * @param moodleform $mform Set to a grading batch operations form
5079
     * @return string - the page to view after processing these actions
5080
     */
5081
    protected function view_batch_set_workflow_state($mform) {
5082
        global $CFG, $DB;
5083
 
5084
        require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
5085
 
5086
        $o = '';
5087
 
5088
        $submitteddata = $mform->get_data();
5089
        $users = $submitteddata->selectedusers;
5090
        $userlist = explode(',', $users);
5091
 
5092
        $formdata = array('id' => $this->get_course_module()->id,
5093
                          'selectedusers' => $users);
5094
 
5095
        $usershtml = '';
5096
 
5097
        $usercount = 0;
5098
        // TODO Does not support custom user profile fields (MDL-70456).
5099
        $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5100
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5101
        foreach ($userlist as $userid) {
5102
            if ($usercount >= 5) {
5103
                $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5104
                break;
5105
            }
5106
            $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
5107
 
5108
            $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
5109
                                                                $this->get_course()->id,
5110
                                                                $viewfullnames,
5111
                                                                $this->is_blind_marking(),
5112
                                                                $this->get_uniqueid_for_user($user->id),
5113
                                                                $extrauserfields,
5114
                                                                !$this->is_active_user($userid)));
5115
            $usercount += 1;
5116
        }
5117
 
5118
        $formparams = array(
5119
            'userscount' => count($userlist),
5120
            'usershtml' => $usershtml,
5121
            'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
5122
        );
5123
 
5124
        $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
5125
        $mform->set_data($formdata);    // Initialises the hidden elements.
5126
        $header = new assign_header($this->get_instance(),
5127
            $this->get_context(),
5128
            $this->show_intro(),
5129
            $this->get_course_module()->id,
5130
            get_string('setmarkingworkflowstate', 'assign'));
5131
        $o .= $this->get_renderer()->render($header);
5132
        $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5133
        $o .= $this->view_footer();
5134
 
5135
        \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger();
5136
 
5137
        return $o;
5138
    }
5139
 
5140
    /**
5141
     * Shows a form that allows the allocated marker for selected submissions to be changed.
5142
     *
5143
     * @param moodleform $mform Set to a grading batch operations form
5144
     * @return string - the page to view after processing these actions
5145
     */
5146
    public function view_batch_markingallocation($mform) {
5147
        global $CFG, $DB;
5148
 
5149
        require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
5150
 
5151
        $o = '';
5152
 
5153
        $submitteddata = $mform->get_data();
5154
        $users = $submitteddata->selectedusers;
5155
        $userlist = explode(',', $users);
5156
 
5157
        $formdata = array('id' => $this->get_course_module()->id,
5158
                          'selectedusers' => $users);
5159
 
5160
        $usershtml = '';
5161
 
5162
        $usercount = 0;
5163
        // TODO Does not support custom user profile fields (MDL-70456).
5164
        $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5165
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5166
        foreach ($userlist as $userid) {
5167
            if ($usercount >= 5) {
5168
                $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5169
                break;
5170
            }
5171
            $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
5172
 
5173
            $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
5174
                $this->get_course()->id,
5175
                $viewfullnames,
5176
                $this->is_blind_marking(),
5177
                $this->get_uniqueid_for_user($user->id),
5178
                $extrauserfields,
5179
                !$this->is_active_user($userid)));
5180
            $usercount += 1;
5181
        }
5182
 
5183
        $formparams = array(
5184
            'userscount' => count($userlist),
5185
            'usershtml' => $usershtml,
5186
        );
5187
 
5188
        list($sort, $params) = users_order_by_sql('u');
5189
        // Only enrolled users could be assigned as potential markers.
5190
        $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
5191
        $markerlist = array();
5192
        foreach ($markers as $marker) {
5193
            $markerlist[$marker->id] = fullname($marker);
5194
        }
5195
 
5196
        $formparams['markers'] = $markerlist;
5197
 
5198
        $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
5199
        $mform->set_data($formdata);    // Initialises the hidden elements.
5200
        $header = new assign_header($this->get_instance(),
5201
            $this->get_context(),
5202
            $this->show_intro(),
5203
            $this->get_course_module()->id,
5204
            get_string('setmarkingallocation', 'assign'));
5205
        $o .= $this->get_renderer()->render($header);
5206
        $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5207
        $o .= $this->view_footer();
5208
 
5209
        \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger();
5210
 
5211
        return $o;
5212
    }
5213
 
5214
    /**
5215
     * Ask the user to confirm they want to submit their work for grading.
5216
     *
5217
     * @param moodleform $mform - null unless form validation has failed
5218
     * @return string
5219
     */
5220
    protected function check_submit_for_grading($mform) {
5221
        global $USER, $CFG;
5222
 
5223
        require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
5224
 
5225
        // Check that all of the submission plugins are ready for this submission.
5226
        // Also check whether there is something to be submitted as well against atleast one.
5227
        $notifications = array();
5228
        $submission = $this->get_user_submission($USER->id, false);
5229
        if ($this->get_instance()->teamsubmission) {
5230
            $submission = $this->get_group_submission($USER->id, 0, false);
5231
        }
5232
 
5233
        $plugins = $this->get_submission_plugins();
5234
        $hassubmission = false;
5235
        foreach ($plugins as $plugin) {
5236
            if ($plugin->is_enabled() && $plugin->is_visible()) {
5237
                $check = $plugin->precheck_submission($submission);
5238
                if ($check !== true) {
5239
                    $notifications[] = $check;
5240
                }
5241
 
5242
                if (is_object($submission) && !$plugin->is_empty($submission)) {
5243
                    $hassubmission = true;
5244
                }
5245
            }
5246
        }
5247
 
5248
        // If there are no submissions and no existing notifications to be displayed the stop.
5249
        if (!$hassubmission && !$notifications) {
5250
            $notifications[] = get_string('addsubmission_help', 'assign');
5251
        }
5252
 
5253
        $data = new stdClass();
5254
        $adminconfig = $this->get_admin_config();
5255
        $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
5256
        $submissionstatement = '';
5257
 
5258
        if ($requiresubmissionstatement) {
5259
            $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
5260
        }
5261
 
5262
        // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
5263
        // that the submission statement checkbox will be displayed.
5264
        if (empty($submissionstatement)) {
5265
            $requiresubmissionstatement = false;
5266
        }
5267
 
5268
        if ($mform == null) {
5269
            $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
5270
                                                                        $submissionstatement,
5271
                                                                        $this->get_course_module()->id,
5272
                                                                        $data));
5273
        }
5274
        $o = '';
5275
        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
5276
                                                              $this->get_context(),
5277
                                                              $this->show_intro(),
5278
                                                              $this->get_course_module()->id,
5279
                                                              get_string('confirmsubmissionheading', 'assign')));
5280
        $submitforgradingpage = new assign_submit_for_grading_page($notifications,
5281
                                                                   $this->get_course_module()->id,
5282
                                                                   $mform);
5283
        $o .= $this->get_renderer()->render($submitforgradingpage);
5284
        $o .= $this->view_footer();
5285
 
5286
        \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger();
5287
 
5288
        return $o;
5289
    }
5290
 
5291
    /**
5292
     * Creates an assign_submission_status renderable.
5293
     *
5294
     * @param stdClass $user the user to get the report for
5295
     * @param bool $showlinks return plain text or links to the profile
5296
     * @return assign_submission_status renderable object
5297
     */
5298
    public function get_assign_submission_status_renderable($user, $showlinks) {
5299
        global $PAGE;
5300
 
5301
        $instance = $this->get_instance();
5302
        $flags = $this->get_user_flags($user->id, false);
5303
        $submission = $this->get_user_submission($user->id, false);
5304
 
5305
        $teamsubmission = null;
5306
        $submissiongroup = null;
5307
        $notsubmitted = array();
5308
        if ($instance->teamsubmission) {
5309
            $teamsubmission = $this->get_group_submission($user->id, 0, false);
5310
            $submissiongroup = $this->get_submission_group($user->id);
5311
            $groupid = 0;
5312
            if ($submissiongroup) {
5313
                $groupid = $submissiongroup->id;
5314
            }
5315
            $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
5316
        }
5317
 
5318
        $showedit = $showlinks &&
5319
                    ($this->is_any_submission_plugin_enabled()) &&
5320
                    $this->can_edit_submission($user->id);
5321
 
5322
        $submissionlocked = ($flags && $flags->locked);
5323
 
5324
        // Grading criteria preview.
5325
        $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
5326
        $gradingcontrollerpreview = '';
5327
        if ($gradingmethod = $gradingmanager->get_active_method()) {
5328
            $controller = $gradingmanager->get_controller($gradingmethod);
5329
            if ($controller->is_form_defined()) {
5330
                $gradingcontrollerpreview = $controller->render_preview($PAGE);
5331
            }
5332
        }
5333
 
5334
        $showsubmit = ($showlinks && $this->submissions_open($user->id));
5335
        $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
5336
 
5337
        $extensionduedate = null;
5338
        if ($flags) {
5339
            $extensionduedate = $flags->extensionduedate;
5340
        }
5341
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5342
 
5343
        $gradingstatus = $this->get_grading_status($user->id);
5344
        $usergroups = $this->get_all_groups($user->id);
5345
        $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
5346
                                                          $instance->alwaysshowdescription,
5347
                                                          $submission,
5348
                                                          $instance->teamsubmission,
5349
                                                          $teamsubmission,
5350
                                                          $submissiongroup,
5351
                                                          $notsubmitted,
5352
                                                          $this->is_any_submission_plugin_enabled(),
5353
                                                          $submissionlocked,
5354
                                                          $this->is_graded($user->id),
5355
                                                          $instance->duedate,
5356
                                                          $instance->cutoffdate,
5357
                                                          $this->get_submission_plugins(),
5358
                                                          $this->get_return_action(),
5359
                                                          $this->get_return_params(),
5360
                                                          $this->get_course_module()->id,
5361
                                                          $this->get_course()->id,
5362
                                                          assign_submission_status::STUDENT_VIEW,
5363
                                                          $showedit,
5364
                                                          $showsubmit,
5365
                                                          $viewfullnames,
5366
                                                          $extensionduedate,
5367
                                                          $this->get_context(),
5368
                                                          $this->is_blind_marking(),
5369
                                                          $gradingcontrollerpreview,
5370
                                                          $instance->attemptreopenmethod,
5371
                                                          $instance->maxattempts,
5372
                                                          $gradingstatus,
5373
                                                          $instance->preventsubmissionnotingroup,
5374
                                                          $usergroups,
5375
                                                          $instance->timelimit);
5376
        return $submissionstatus;
5377
    }
5378
 
5379
 
5380
    /**
5381
     * Creates an assign_feedback_status renderable.
5382
     *
5383
     * @param stdClass $user the user to get the report for
5384
     * @return assign_feedback_status renderable object
5385
     */
5386
    public function get_assign_feedback_status_renderable($user) {
5387
        global $CFG, $DB, $PAGE;
5388
 
5389
        require_once($CFG->libdir.'/gradelib.php');
5390
        require_once($CFG->dirroot.'/grade/grading/lib.php');
5391
 
5392
        $instance = $this->get_instance();
5393
        $grade = $this->get_user_grade($user->id, false);
5394
        $gradingstatus = $this->get_grading_status($user->id);
5395
 
5396
        $gradinginfo = grade_get_grades($this->get_course()->id,
5397
                                    'mod',
5398
                                    'assign',
5399
                                    $instance->id,
5400
                                    $user->id);
5401
 
5402
        $gradingitem = null;
5403
        $gradebookgrade = null;
5404
        if (isset($gradinginfo->items[0])) {
5405
            $gradingitem = $gradinginfo->items[0];
5406
            $gradebookgrade = $gradingitem->grades[$user->id];
5407
        }
5408
 
5409
        // Check to see if all feedback plugins are empty.
5410
        $emptyplugins = true;
5411
        if ($grade) {
5412
            foreach ($this->get_feedback_plugins() as $plugin) {
5413
                if ($plugin->is_visible() && $plugin->is_enabled()) {
5414
                    if (!$plugin->is_empty($grade)) {
5415
                        $emptyplugins = false;
5416
                    }
5417
                }
5418
            }
5419
        }
5420
 
5421
        if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
5422
            $emptyplugins = true; // Don't show feedback plugins until released either.
5423
        }
5424
 
5425
        $cangrade = has_capability('mod/assign:grade', $this->get_context());
5426
        $hasgrade = $this->get_instance()->grade != GRADE_TYPE_NONE &&
5427
                        !is_null($gradebookgrade) && !is_null($gradebookgrade->grade);
5428
        $gradevisible = $cangrade || $this->get_instance()->grade == GRADE_TYPE_NONE ||
5429
                        (!is_null($gradebookgrade) && !$gradebookgrade->hidden);
5430
        // If there is a visible grade, show the summary.
5431
        if (($hasgrade || !$emptyplugins) && $gradevisible) {
5432
 
5433
            $gradefordisplay = null;
5434
            $gradeddate = null;
5435
            $grader = null;
5436
            $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5437
 
5438
            $gradingcontrollergrade = '';
5439
            if ($hasgrade) {
5440
                if ($controller = $gradingmanager->get_active_controller()) {
5441
                    $menu = make_grades_menu($this->get_instance()->grade);
5442
                    $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
5443
                    $gradingcontrollergrade = $controller->render_grade(
5444
                        $PAGE,
5445
                        $grade->id,
5446
                        $gradingitem,
5447
                        '',
5448
                        $cangrade
5449
                    );
5450
                    $gradefordisplay = $gradebookgrade->str_long_grade;
5451
                } else {
5452
                    $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
5453
                }
5454
                $gradeddate = $gradebookgrade->dategraded;
5455
 
5456
                // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5457
                if (has_capability('mod/assign:showhiddengrader', $this->context) || !$this->is_hidden_grader()) {
5458
                    // Only display the grader if it is in the right state.
5459
                    if (in_array($gradingstatus, [ASSIGN_GRADING_STATUS_GRADED, ASSIGN_MARKING_WORKFLOW_STATE_RELEASED])) {
5460
                        if (isset($grade->grader) && $grade->grader > 0) {
5461
                            $grader = $DB->get_record('user', array('id' => $grade->grader));
5462
                        } else if (isset($gradebookgrade->usermodified)
5463
                            && $gradebookgrade->usermodified > 0
5464
                            && has_capability('mod/assign:grade', $this->get_context(), $gradebookgrade->usermodified)) {
5465
                            // Grader not provided. Check that usermodified is a user who can grade.
5466
                            // Case 1: When an assignment is reopened an empty assign_grade is created so the feedback
5467
                            // plugin can know which attempt it's referring to. In this case, usermodifed is a student.
5468
                            // Case 2: When an assignment's grade is overrided via the gradebook, usermodified is a grader.
5469
                            $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
5470
                        }
5471
                    }
5472
                }
5473
            }
5474
 
5475
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5476
 
5477
            if ($grade) {
5478
                \mod_assign\event\feedback_viewed::create_from_grade($this, $grade)->trigger();
5479
            }
5480
            $feedbackstatus = new assign_feedback_status(
5481
                $gradefordisplay,
5482
                $gradeddate,
5483
                $grader,
5484
                $this->get_feedback_plugins(),
5485
                $grade,
5486
                $this->get_course_module()->id,
5487
                $this->get_return_action(),
5488
                $this->get_return_params(),
5489
                $viewfullnames,
5490
                $gradingcontrollergrade
5491
            );
5492
 
5493
            return $feedbackstatus;
5494
        }
5495
        return;
5496
    }
5497
 
5498
    /**
5499
     * Creates an assign_attempt_history renderable.
5500
     *
5501
     * @param stdClass $user the user to get the report for
5502
     * @return assign_attempt_history renderable object
5503
     */
5504
    public function get_assign_attempt_history_renderable($user) {
5505
 
5506
        $allsubmissions = $this->get_all_submissions($user->id);
5507
        $allgrades = $this->get_all_grades($user->id);
5508
 
5509
        $history = new assign_attempt_history($allsubmissions,
5510
                                              $allgrades,
5511
                                              $this->get_submission_plugins(),
5512
                                              $this->get_feedback_plugins(),
5513
                                              $this->get_course_module()->id,
5514
                                              $this->get_return_action(),
5515
                                              $this->get_return_params(),
5516
                                              false,
5517
                                              0,
5518
                                              0);
5519
        return $history;
5520
    }
5521
 
5522
    /**
5523
     * Print 2 tables of information with no action links -
5524
     * the submission summary and the grading summary.
5525
     *
5526
     * @param stdClass $user the user to print the report for
5527
     * @param bool $showlinks - Return plain text or links to the profile
5528
     * @return string - the html summary
5529
     */
5530
    public function view_student_summary($user, $showlinks) {
5531
 
5532
        $o = '';
5533
 
5534
        if ($this->can_view_submission($user->id)) {
5535
            if (has_capability('mod/assign:viewownsubmissionsummary', $this->get_context(), $user, false)) {
5536
                // The user can view the submission summary.
5537
                $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks);
5538
                $o .= $this->get_renderer()->render($submissionstatus);
5539
            }
5540
 
5541
            // If there is a visible grade, show the feedback.
5542
            $feedbackstatus = $this->get_assign_feedback_status_renderable($user);
5543
            if ($feedbackstatus) {
5544
                $o .= $this->get_renderer()->render($feedbackstatus);
5545
            }
5546
 
5547
            // If there is more than one submission, show the history.
5548
            $history = $this->get_assign_attempt_history_renderable($user);
5549
            if (count($history->submissions) > 1) {
5550
                $o .= $this->get_renderer()->render($history);
5551
            }
5552
        }
5553
        return $o;
5554
    }
5555
 
5556
    /**
5557
     * Returns true if the submit subsission button should be shown to the user.
5558
     *
5559
     * @param stdClass $submission The users own submission record.
5560
     * @param stdClass $teamsubmission The users team submission record if there is one
5561
     * @param int $userid The user
5562
     * @return bool
5563
     */
5564
    protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null) {
5565
        if (!has_capability('mod/assign:submit', $this->get_context(), $userid, false)) {
5566
            // The user does not have the capability to submit.
5567
            return false;
5568
        }
5569
        if ($teamsubmission) {
5570
            if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5571
                // The assignment submission has been completed.
5572
                return false;
5573
            } else if ($this->submission_empty($teamsubmission)) {
5574
                // There is nothing to submit yet.
5575
                return false;
5576
            } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5577
                // The user has already clicked the submit button on the team submission.
5578
                return false;
5579
            } else if (
5580
                !empty($this->get_instance()->preventsubmissionnotingroup)
5581
                && $this->get_submission_group($userid) == false
5582
            ) {
5583
                return false;
5584
            }
5585
        } else if ($submission) {
5586
            if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5587
                // The assignment submission has been completed.
5588
                return false;
5589
            } else if ($this->submission_empty($submission)) {
5590
                // There is nothing to submit.
5591
                return false;
5592
            }
5593
        } else {
5594
            // We've not got a valid submission or team submission.
5595
            return false;
5596
        }
5597
        // Last check is that this instance allows drafts.
5598
        return $this->get_instance()->submissiondrafts;
5599
    }
5600
 
5601
    /**
5602
     * Get the grades for all previous attempts.
5603
     * For each grade - the grader is a full user record,
5604
     * and gradefordisplay is added (rendered from grading manager).
5605
     *
5606
     * @param int $userid If not set, $USER->id will be used.
5607
     * @return array $grades All grade records for this user.
5608
     */
5609
    protected function get_all_grades($userid) {
5610
        global $DB, $USER, $PAGE;
5611
 
5612
        // If the userid is not null then use userid.
5613
        if (!$userid) {
5614
            $userid = $USER->id;
5615
        }
5616
 
5617
        $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5618
 
5619
        $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
5620
 
5621
        $gradercache = array();
5622
        $cangrade = has_capability('mod/assign:grade', $this->get_context());
5623
 
5624
        // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5625
        $showgradername = (
5626
            has_capability('mod/assign:showhiddengrader', $this->context, $userid) or
5627
            !$this->is_hidden_grader()
5628
        );
5629
 
5630
        // Need gradingitem and gradingmanager.
5631
        $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5632
        $controller = $gradingmanager->get_active_controller();
5633
 
5634
        $gradinginfo = grade_get_grades($this->get_course()->id,
5635
                                        'mod',
5636
                                        'assign',
5637
                                        $this->get_instance()->id,
5638
                                        $userid);
5639
 
5640
        $gradingitem = null;
5641
        if (isset($gradinginfo->items[0])) {
5642
            $gradingitem = $gradinginfo->items[0];
5643
        }
5644
 
5645
        foreach ($grades as $grade) {
5646
            // First lookup the grader info.
5647
            if (!$showgradername) {
5648
                $grade->grader = null;
5649
            } else if (isset($gradercache[$grade->grader])) {
5650
                $grade->grader = $gradercache[$grade->grader];
5651
            } else if ($grade->grader > 0) {
5652
                // Not in cache - need to load the grader record.
5653
                $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
5654
                if ($grade->grader) {
5655
                    $gradercache[$grade->grader->id] = $grade->grader;
5656
                }
5657
            }
5658
 
5659
            // Now get the gradefordisplay.
5660
            if ($controller) {
5661
                $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
5662
                $grade->gradefordisplay = $controller->render_grade($PAGE,
5663
                                                                     $grade->id,
5664
                                                                     $gradingitem,
5665
                                                                     $grade->grade,
5666
                                                                     $cangrade);
5667
            } else {
5668
                $grade->gradefordisplay = $this->display_grade($grade->grade, false);
5669
            }
5670
 
5671
        }
5672
 
5673
        return $grades;
5674
    }
5675
 
5676
    /**
5677
     * Get the submissions for all previous attempts.
5678
     *
5679
     * @param int $userid If not set, $USER->id will be used.
5680
     * @return array $submissions All submission records for this user (or group).
5681
     */
5682
    public function get_all_submissions($userid) {
5683
        global $DB, $USER;
5684
 
5685
        // If the userid is not null then use userid.
5686
        if (!$userid) {
5687
            $userid = $USER->id;
5688
        }
5689
 
5690
        $params = array();
5691
 
5692
        if ($this->get_instance()->teamsubmission) {
5693
            $groupid = 0;
5694
            $group = $this->get_submission_group($userid);
5695
            if ($group) {
5696
                $groupid = $group->id;
5697
            }
5698
 
5699
            // Params to get the group submissions.
5700
            $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
5701
        } else {
5702
            // Params to get the user submissions.
5703
            $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5704
        }
5705
 
5706
        // Return the submissions ordered by attempt.
5707
        $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
5708
 
5709
        return $submissions;
5710
    }
5711
 
5712
    /**
5713
     * Creates an assign_grading_summary renderable.
5714
     *
5715
     * @param mixed $activitygroup int|null the group for calculating the grading summary (if null the function will determine it)
5716
     * @return assign_grading_summary renderable object
5717
     */
5718
    public function get_assign_grading_summary_renderable($activitygroup = null) {
5719
 
5720
        $instance = $this->get_default_instance(); // Grading summary requires the raw dates, regardless of relativedates mode.
5721
        $cm = $this->get_course_module();
5722
        $course = $this->get_course();
5723
 
5724
        $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
5725
        $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5726
        $isvisible = $cm->visible;
5727
 
5728
        if ($activitygroup === null) {
5729
            $activitygroup = groups_get_activity_group($cm);
5730
        }
5731
 
5732
        if ($instance->teamsubmission) {
5733
            $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_NO;
5734
            $defaultteammembers = $this->get_submission_group_members(0, true);
5735
            if (count($defaultteammembers) > 0) {
5736
                if ($instance->preventsubmissionnotingroup) {
5737
                    $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_REQUIRED;
5738
                } else {
5739
                    $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_OPTIONAL;
5740
                }
5741
            }
5742
 
5743
            $summary = new assign_grading_summary(
5744
                $this->count_teams($activitygroup),
5745
                $instance->submissiondrafts,
5746
                $this->count_submissions_with_status($draft, $activitygroup),
5747
                $this->is_any_submission_plugin_enabled(),
5748
                $this->count_submissions_with_status($submitted, $activitygroup),
5749
                $this->get_cutoffdate($activitygroup),
5750
                $this->get_duedate($activitygroup),
5751
                $this->get_timelimit($activitygroup),
5752
                $this->get_course_module()->id,
5753
                $this->count_submissions_need_grading($activitygroup),
5754
                $instance->teamsubmission,
5755
                $warnofungroupedusers,
5756
                $course->relativedatesmode,
5757
                $course->startdate,
5758
                $this->can_grade(),
5759
                $isvisible,
5760
                $this->get_course_module()
5761
            );
5762
        } else {
5763
            // The active group has already been updated in groups_print_activity_menu().
5764
            $countparticipants = $this->count_participants($activitygroup);
5765
            $summary = new assign_grading_summary(
5766
                $countparticipants,
5767
                $instance->submissiondrafts,
5768
                $this->count_submissions_with_status($draft, $activitygroup),
5769
                $this->is_any_submission_plugin_enabled(),
5770
                $this->count_submissions_with_status($submitted, $activitygroup),
5771
                $this->get_cutoffdate($activitygroup),
5772
                $this->get_duedate($activitygroup),
5773
                $this->get_timelimit($activitygroup),
5774
                $this->get_course_module()->id,
5775
                $this->count_submissions_need_grading($activitygroup),
5776
                $instance->teamsubmission,
5777
                assign_grading_summary::WARN_GROUPS_NO,
5778
                $course->relativedatesmode,
5779
                $course->startdate,
5780
                $this->can_grade(),
5781
                $isvisible,
5782
                $this->get_course_module()
5783
            );
5784
        }
5785
 
5786
        return $summary;
5787
    }
5788
 
5789
    /**
5790
     * Helper function to allow up to fetch the group overrides via one query as opposed to many calls.
5791
     *
5792
     * @param int $activitygroup The group we want to check the overrides of
5793
     * @return mixed Can return either a fetched DB object, local object or false
5794
     */
5795
    private function get_override_data(int $activitygroup) {
5796
        global $DB;
5797
 
5798
        $instanceid = $this->get_instance()->id;
5799
        $cachekey = "$instanceid-$activitygroup";
5800
        if (isset($this->overridedata[$cachekey])) {
5801
            return $this->overridedata[$cachekey];
5802
        }
5803
 
5804
        $params = ['groupid' => $activitygroup, 'assignid' => $instanceid];
5805
        $this->overridedata[$cachekey] = $DB->get_record('assign_overrides', $params);
5806
        return $this->overridedata[$cachekey];
5807
    }
5808
 
5809
    /**
5810
     * Return group override duedate.
5811
     *
5812
     * @param int $activitygroup Activity active group
5813
     * @return int $duedate
5814
     */
5815
    private function get_duedate($activitygroup = null) {
5816
        if ($activitygroup === null) {
5817
            $activitygroup = groups_get_activity_group($this->get_course_module());
5818
        }
5819
        if ($this->can_view_grades() && !empty($activitygroup)) {
5820
            $groupoverride = $this->get_override_data($activitygroup);
5821
            if (!empty($groupoverride->duedate)) {
5822
                return $groupoverride->duedate;
5823
            }
5824
        }
5825
        return $this->get_instance()->duedate;
5826
    }
5827
 
5828
    /**
5829
     * Return group override timelimit.
5830
     *
5831
     * @param null|int $activitygroup Activity active group
5832
     * @return int $timelimit
5833
     */
5834
    private function get_timelimit(?int $activitygroup = null): int {
5835
        if ($activitygroup === null) {
5836
            $activitygroup = groups_get_activity_group($this->get_course_module());
5837
        }
5838
        if ($this->can_view_grades() && !empty($activitygroup)) {
5839
            $groupoverride = $this->get_override_data($activitygroup);
5840
            if (!empty($groupoverride->timelimit)) {
5841
                return $groupoverride->timelimit;
5842
            }
5843
        }
5844
        return $this->get_instance()->timelimit;
5845
    }
5846
 
5847
    /**
5848
     * Return group override cutoffdate.
5849
     *
5850
     * @param null|int $activitygroup Activity active group
5851
     * @return int $cutoffdate
5852
     */
5853
    private function get_cutoffdate(?int $activitygroup = null): int {
5854
        if ($activitygroup === null) {
5855
            $activitygroup = groups_get_activity_group($this->get_course_module());
5856
        }
5857
        if ($this->can_view_grades() && !empty($activitygroup)) {
5858
            $groupoverride = $this->get_override_data($activitygroup);
5859
            if (!empty($groupoverride->cutoffdate)) {
5860
                return $groupoverride->cutoffdate;
5861
            }
5862
        }
5863
        return $this->get_instance()->cutoffdate;
5864
    }
5865
 
5866
    /**
5867
     * View submissions page (contains details of current submission).
5868
     *
5869
     * @return string
5870
     */
5871
    protected function view_submission_page() {
5872
        global $CFG, $DB, $USER, $PAGE;
5873
 
5874
        $instance = $this->get_instance();
5875
 
5876
        $this->add_grade_notices();
5877
 
5878
        $o = '';
5879
 
5880
        $postfix = '';
5881
        if ($this->has_visible_attachments() && (!$this->get_instance($USER->id)->submissionattachments)) {
5882
            $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
5883
        }
5884
 
5885
        $o .= $this->get_renderer()->render(new assign_header($instance,
5886
                                                      $this->get_context(),
5887
                                                      $this->show_intro(),
5888
                                                      $this->get_course_module()->id,
5889
                                                      '', '', $postfix));
5890
 
5891
        // Display plugin specific headers.
5892
        $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins());
5893
        foreach ($plugins as $plugin) {
5894
            if ($plugin->is_enabled() && $plugin->is_visible()) {
5895
                $o .= $this->get_renderer()->render(new assign_plugin_header($plugin));
5896
            }
5897
        }
5898
 
5899
        if ($this->can_view_grades()) {
5900
            $actionbuttons = new \mod_assign\output\actionmenu($this->get_course_module()->id);
5901
            $o .= $this->get_renderer()->submission_actionmenu($actionbuttons);
5902
 
5903
            $summary = $this->get_assign_grading_summary_renderable();
5904
            $o .= $this->get_renderer()->render($summary);
5905
        }
5906
 
5907
        if ($this->can_view_submission($USER->id)) {
5908
            $o .= $this->view_submission_action_bar($instance, $USER);
5909
            $o .= $this->view_student_summary($USER, true);
5910
        }
5911
 
5912
        $o .= $this->view_footer();
5913
 
5914
        \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger();
5915
 
5916
        return $o;
5917
    }
5918
 
5919
    /**
5920
     * The action bar displayed in the submissions page.
5921
     *
5922
     * @param stdClass $instance The settings for the current instance of this assignment
5923
     * @param stdClass $user The user to print the action bar for
5924
     * @return string
5925
     */
5926
    public function view_submission_action_bar(stdClass $instance, stdClass $user): string {
5927
        $submission = $this->get_user_submission($user->id, false);
5928
        // Figure out if we are team or solitary submission.
5929
        $teamsubmission = null;
5930
        if ($instance->teamsubmission) {
5931
            $teamsubmission = $this->get_group_submission($user->id, 0, false);
5932
        }
5933
 
5934
        $showsubmit = ($this->submissions_open($user->id)
5935
            && $this->show_submit_button($submission, $teamsubmission, $user->id));
5936
        $showedit = ($this->is_any_submission_plugin_enabled()) && $this->can_edit_submission($user->id);
5937
 
5938
        // The method get_group_submission() says that it returns a stdClass, but it can return false >_>.
5939
        if ($teamsubmission === false) {
5940
            $teamsubmission = new stdClass();
5941
        }
5942
        // Same goes for get_user_submission().
5943
        if ($submission === false) {
5944
            $submission = new stdClass();
5945
        }
5946
        $actionbuttons = new \mod_assign\output\user_submission_actionmenu(
5947
            $this->get_course_module()->id,
5948
            $showsubmit,
5949
            $showedit,
5950
            $submission,
5951
            $teamsubmission,
5952
            $instance->timelimit
5953
        );
5954
 
5955
        return $this->get_renderer()->render($actionbuttons);
5956
    }
5957
 
5958
    /**
5959
     * Convert the final raw grade(s) in the grading table for the gradebook.
5960
     *
5961
     * @param stdClass $grade
5962
     * @return array
5963
     */
5964
    protected function convert_grade_for_gradebook(stdClass $grade) {
5965
        $gradebookgrade = array();
5966
        if ($grade->grade >= 0) {
5967
            $gradebookgrade['rawgrade'] = $grade->grade;
5968
        }
5969
        // Allow "no grade" to be chosen.
5970
        if ($grade->grade == -1) {
5971
            $gradebookgrade['rawgrade'] = NULL;
5972
        }
5973
        $gradebookgrade['userid'] = $grade->userid;
5974
        $gradebookgrade['usermodified'] = $grade->grader;
5975
        $gradebookgrade['datesubmitted'] = null;
5976
        $gradebookgrade['dategraded'] = $grade->timemodified;
5977
        if (isset($grade->feedbackformat)) {
5978
            $gradebookgrade['feedbackformat'] = $grade->feedbackformat;
5979
        }
5980
        if (isset($grade->feedbacktext)) {
5981
            $gradebookgrade['feedback'] = $grade->feedbacktext;
5982
        }
5983
        if (isset($grade->feedbackfiles)) {
5984
            $gradebookgrade['feedbackfiles'] = $grade->feedbackfiles;
5985
        }
5986
 
5987
        return $gradebookgrade;
5988
    }
5989
 
5990
    /**
5991
     * Convert submission details for the gradebook.
5992
     *
5993
     * @param stdClass $submission
5994
     * @return array
5995
     */
5996
    protected function convert_submission_for_gradebook(stdClass $submission) {
5997
        $gradebookgrade = array();
5998
 
5999
        $gradebookgrade['userid'] = $submission->userid;
6000
        $gradebookgrade['usermodified'] = $submission->userid;
6001
        $gradebookgrade['datesubmitted'] = $submission->timemodified;
6002
 
6003
        return $gradebookgrade;
6004
    }
6005
 
6006
    /**
6007
     * Update grades in the gradebook.
6008
     *
6009
     * @param mixed $submission stdClass|null
6010
     * @param mixed $grade stdClass|null
6011
     * @return bool
6012
     */
6013
    protected function gradebook_item_update($submission=null, $grade=null) {
6014
        global $CFG;
6015
 
6016
        require_once($CFG->dirroot.'/mod/assign/lib.php');
6017
        // Do not push grade to gradebook if blind marking is active as
6018
        // the gradebook would reveal the students.
6019
        if ($this->is_blind_marking()) {
6020
            return false;
6021
        }
6022
 
6023
        // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook.
6024
        if ($this->get_instance()->markingworkflow && !empty($grade) &&
6025
                $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
6026
            // Remove the grade (if it exists) from the gradebook as it is not 'final'.
6027
            $grade->grade = -1;
6028
            $grade->feedbacktext = '';
6029
            $grade->feebackfiles = [];
6030
        }
6031
 
6032
        if ($submission != null) {
6033
            if ($submission->userid == 0) {
6034
                // This is a group submission update.
6035
                $team = groups_get_members($submission->groupid, 'u.id');
6036
 
6037
                foreach ($team as $member) {
6038
                    $membersubmission = clone $submission;
6039
                    $membersubmission->groupid = 0;
6040
                    $membersubmission->userid = $member->id;
6041
                    $this->gradebook_item_update($membersubmission, null);
6042
                }
6043
                return;
6044
            }
6045
 
6046
            $gradebookgrade = $this->convert_submission_for_gradebook($submission);
6047
 
6048
        } else {
6049
            $gradebookgrade = $this->convert_grade_for_gradebook($grade);
6050
        }
6051
        // Grading is disabled, return.
6052
        if ($this->grading_disabled($gradebookgrade['userid'])) {
6053
            return false;
6054
        }
6055
        $assign = clone $this->get_instance();
6056
        $assign->cmidnumber = $this->get_course_module()->idnumber;
6057
        // Set assign gradebook feedback plugin status (enabled and visible).
6058
        $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
6059
        return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK;
6060
    }
6061
 
6062
    /**
6063
     * Update team submission.
6064
     *
6065
     * @param stdClass $submission
6066
     * @param int $userid
6067
     * @param bool $updatetime
6068
     * @return bool
6069
     */
6070
    protected function update_team_submission(stdClass $submission, $userid, $updatetime) {
6071
        global $DB;
6072
 
6073
        if ($updatetime) {
6074
            $submission->timemodified = time();
6075
        }
6076
 
6077
        // First update the submission for the current user.
6078
        $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
6079
        $mysubmission->status = $submission->status;
6080
 
6081
        $this->update_submission($mysubmission, 0, $updatetime, false);
6082
 
6083
        // Now check the team settings to see if this assignment qualifies as submitted or draft.
6084
        $team = $this->get_submission_group_members($submission->groupid, true);
6085
 
6086
        $allsubmitted = true;
6087
        $anysubmitted = false;
6088
        $result = true;
6089
        if (!in_array($submission->status, [ASSIGN_SUBMISSION_STATUS_NEW, ASSIGN_SUBMISSION_STATUS_REOPENED])) {
6090
            foreach ($team as $member) {
6091
                $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
6092
 
6093
                // If no submission found for team member and member is active then everyone has not submitted.
6094
                if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED
6095
                        && ($this->is_active_user($member->id))) {
6096
                    $allsubmitted = false;
6097
                    if ($anysubmitted) {
6098
                        break;
6099
                    }
6100
                } else {
6101
                    $anysubmitted = true;
6102
                }
6103
            }
6104
            if ($this->get_instance()->requireallteammemberssubmit) {
6105
                if ($allsubmitted) {
6106
                    $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6107
                } else {
6108
                    $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6109
                }
6110
                $result = $DB->update_record('assign_submission', $submission);
6111
            } else {
6112
                if ($anysubmitted) {
6113
                    $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6114
                } else {
6115
                    $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6116
                }
6117
                $result = $DB->update_record('assign_submission', $submission);
6118
            }
6119
        } else {
6120
            // Set the group submission to reopened.
6121
            foreach ($team as $member) {
6122
                $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
6123
                $membersubmission->status = $submission->status;
6124
                $result = $DB->update_record('assign_submission', $membersubmission) && $result;
6125
            }
6126
            $result = $DB->update_record('assign_submission', $submission) && $result;
6127
        }
6128
 
6129
        $this->gradebook_item_update($submission);
6130
        return $result;
6131
    }
6132
 
6133
    /**
6134
     * Update grades in the gradebook based on submission time.
6135
     *
6136
     * @param stdClass $submission
6137
     * @param int $userid
6138
     * @param bool $updatetime
6139
     * @param bool $teamsubmission
6140
     * @return bool
6141
     */
6142
    protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) {
6143
        global $DB;
6144
 
6145
        if ($teamsubmission) {
6146
            return $this->update_team_submission($submission, $userid, $updatetime);
6147
        }
6148
 
6149
        if ($updatetime) {
6150
            $submission->timemodified = time();
6151
        }
6152
        $result= $DB->update_record('assign_submission', $submission);
6153
        if ($result) {
6154
            $this->gradebook_item_update($submission);
6155
        }
6156
        return $result;
6157
    }
6158
 
6159
    /**
6160
     * Is this assignment open for submissions?
6161
     *
6162
     * Check the due date,
6163
     * prevent late submissions,
6164
     * has this person already submitted,
6165
     * is the assignment locked?
6166
     *
6167
     * @param int $userid - Optional userid so we can see if a different user can submit
6168
     * @param bool $skipenrolled - Skip enrollment checks (because they have been done already)
6169
     * @param stdClass $submission - Pre-fetched submission record (or false to fetch it)
6170
     * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it)
6171
     * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it)
6172
     * @return bool
6173
     */
6174
    public function submissions_open($userid = 0,
6175
                                     $skipenrolled = false,
6176
                                     $submission = false,
6177
                                     $flags = false,
6178
                                     $gradinginfo = false) {
6179
        global $USER;
6180
 
6181
        if (!$userid) {
6182
            $userid = $USER->id;
6183
        }
6184
 
6185
        $time = time();
6186
        $dateopen = true;
6187
        $finaldate = false;
6188
        if ($this->get_instance()->cutoffdate) {
6189
            $finaldate = $this->get_instance()->cutoffdate;
6190
        }
6191
 
6192
        if ($flags === false) {
6193
            $flags = $this->get_user_flags($userid, false);
6194
        }
6195
        if ($flags && $flags->locked) {
6196
            return false;
6197
        }
6198
 
6199
        // User extensions.
6200
        if ($finaldate) {
6201
            if ($flags && $flags->extensionduedate) {
6202
                // Extension can be before cut off date.
6203
                if ($flags->extensionduedate > $finaldate) {
6204
                    $finaldate = $flags->extensionduedate;
6205
                }
6206
            }
6207
        }
6208
 
6209
        if ($finaldate) {
6210
            $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate);
6211
        } else {
6212
            $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time);
6213
        }
6214
 
6215
        if (!$dateopen) {
6216
            return false;
6217
        }
6218
 
6219
        // Now check if this user has already submitted etc.
6220
        if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) {
6221
            return false;
6222
        }
6223
        // Note you can pass null for submission and it will not be fetched.
6224
        if ($submission === false) {
6225
            if ($this->get_instance()->teamsubmission) {
6226
                $submission = $this->get_group_submission($userid, 0, false);
6227
            } else {
6228
                $submission = $this->get_user_submission($userid, false);
6229
            }
6230
        }
6231
        if ($submission) {
6232
 
6233
            if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6234
                // Drafts are tracked and the student has submitted the assignment.
6235
                return false;
6236
            }
6237
        }
6238
 
6239
        // See if this user grade is locked in the gradebook.
6240
        if ($gradinginfo === false) {
6241
            $gradinginfo = grade_get_grades($this->get_course()->id,
6242
                                            'mod',
6243
                                            'assign',
6244
                                            $this->get_instance()->id,
6245
                                            array($userid));
6246
        }
6247
        if ($gradinginfo &&
6248
                isset($gradinginfo->items[0]->grades[$userid]) &&
6249
                $gradinginfo->items[0]->grades[$userid]->locked) {
6250
            return false;
6251
        }
6252
 
6253
        return true;
6254
    }
6255
 
6256
    /**
6257
     * Render the files in file area.
6258
     *
6259
     * @param string $component
6260
     * @param string $area
6261
     * @param int $submissionid
6262
     * @return string
6263
     */
6264
    public function render_area_files($component, $area, $submissionid) {
6265
        global $USER;
6266
 
6267
        return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component,
6268
                                                   $this->course, $this->coursemodule);
6269
 
6270
    }
6271
 
6272
    /**
6273
     * Capability check to make sure this grader can edit this submission.
6274
     *
6275
     * @param int $userid - The user whose submission is to be edited
6276
     * @param int $graderid (optional) - The user who will do the editing (default to $USER->id).
6277
     * @return bool
6278
     */
6279
    public function can_edit_submission($userid, $graderid = 0) {
6280
        global $USER;
6281
 
6282
        if (empty($graderid)) {
6283
            $graderid = $USER->id;
6284
        }
6285
 
6286
        $instance = $this->get_instance();
6287
        if ($userid == $graderid &&
6288
            $instance->teamsubmission &&
6289
            $instance->preventsubmissionnotingroup &&
6290
            $this->get_submission_group($userid) == false) {
6291
            return false;
6292
        }
6293
 
6294
        if ($userid == $graderid) {
6295
            if ($this->submissions_open($userid) &&
6296
                    has_capability('mod/assign:submit', $this->context, $graderid)) {
6297
                // User can edit their own submission.
6298
                return true;
6299
            } else {
6300
                // We need to return here because editothersubmission should never apply to a users own submission.
6301
                return false;
6302
            }
6303
        }
6304
 
6305
        if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) {
6306
            return false;
6307
        }
6308
 
6309
        $cm = $this->get_course_module();
6310
        if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
6311
            $sharedgroupmembers = $this->get_shared_group_members($cm, $graderid);
6312
            return in_array($userid, $sharedgroupmembers);
6313
        }
6314
        return true;
6315
    }
6316
 
6317
    /**
6318
     * Returns IDs of the users who share group membership with the specified user.
6319
     *
6320
     * @param stdClass|cm_info $cm Course-module
6321
     * @param int $userid User ID
6322
     * @return array An array of ID of users.
6323
     */
6324
    public function get_shared_group_members($cm, $userid) {
6325
        if (!isset($this->sharedgroupmembers[$userid])) {
6326
            $this->sharedgroupmembers[$userid] = array();
6327
            if ($members = groups_get_activity_shared_group_members($cm, $userid)) {
6328
                $this->sharedgroupmembers[$userid] = array_keys($members);
6329
            }
6330
        }
6331
 
6332
        return $this->sharedgroupmembers[$userid];
6333
    }
6334
 
6335
    /**
6336
     * Returns a list of teachers that should be grading given submission.
6337
     *
6338
     * @param int $userid The submission to grade
6339
     * @return array
6340
     */
6341
    protected function get_graders($userid) {
6342
        // Potential graders should be active users only.
6343
        $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true);
6344
 
6345
        $graders = array();
6346
        if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6347
            if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6348
                foreach ($groups as $group) {
6349
                    foreach ($potentialgraders as $grader) {
6350
                        if ($grader->id == $userid) {
6351
                            // Do not send self.
6352
                            continue;
6353
                        }
6354
                        if (groups_is_member($group->id, $grader->id)) {
6355
                            $graders[$grader->id] = $grader;
6356
                        }
6357
                    }
6358
                }
6359
            } else {
6360
                // User not in group, try to find graders without group.
6361
                foreach ($potentialgraders as $grader) {
6362
                    if ($grader->id == $userid) {
6363
                        // Do not send self.
6364
                        continue;
6365
                    }
6366
                    if (!groups_has_membership($this->get_course_module(), $grader->id)) {
6367
                        $graders[$grader->id] = $grader;
6368
                    }
6369
                }
6370
            }
6371
        } else {
6372
            foreach ($potentialgraders as $grader) {
6373
                if ($grader->id == $userid) {
6374
                    // Do not send self.
6375
                    continue;
6376
                }
6377
                // Must be enrolled.
6378
                if (is_enrolled($this->get_course_context(), $grader->id)) {
6379
                    $graders[$grader->id] = $grader;
6380
                }
6381
            }
6382
        }
6383
        return $graders;
6384
    }
6385
 
6386
    /**
6387
     * Returns a list of users that should receive notification about given submission.
6388
     *
6389
     * @param int $userid The submission to grade
6390
     * @return array
6391
     */
6392
    protected function get_notifiable_users($userid) {
6393
        // Potential users should be active users only.
6394
        $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications",
6395
                                             null, 'u.*', null, null, null, true);
6396
 
6397
        $notifiableusers = array();
6398
        if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6399
            if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6400
                foreach ($groups as $group) {
6401
                    foreach ($potentialusers as $potentialuser) {
6402
                        if ($potentialuser->id == $userid) {
6403
                            // Do not send self.
6404
                            continue;
6405
                        }
6406
                        if (groups_is_member($group->id, $potentialuser->id)) {
6407
                            $notifiableusers[$potentialuser->id] = $potentialuser;
6408
                        }
6409
                    }
6410
                }
6411
            } else {
6412
                // User not in group, try to find graders without group.
6413
                foreach ($potentialusers as $potentialuser) {
6414
                    if ($potentialuser->id == $userid) {
6415
                        // Do not send self.
6416
                        continue;
6417
                    }
6418
                    if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
6419
                        $notifiableusers[$potentialuser->id] = $potentialuser;
6420
                    }
6421
                }
6422
            }
6423
        } else {
6424
            foreach ($potentialusers as $potentialuser) {
6425
                if ($potentialuser->id == $userid) {
6426
                    // Do not send self.
6427
                    continue;
6428
                }
6429
                // Must be enrolled.
6430
                if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
6431
                    $notifiableusers[$potentialuser->id] = $potentialuser;
6432
                }
6433
            }
6434
        }
6435
        return $notifiableusers;
6436
    }
6437
 
6438
    /**
6439
     * Format a notification for plain text.
6440
     *
6441
     * @param string $messagetype
6442
     * @param stdClass $info
6443
     * @param stdClass $course
6444
     * @param stdClass $context
6445
     * @param string $modulename
6446
     * @param string $assignmentname
6447
     */
6448
    protected static function format_notification_message_text($messagetype,
6449
                                                             $info,
6450
                                                             $course,
6451
                                                             $context,
6452
                                                             $modulename,
6453
                                                             $assignmentname) {
6454
        $formatparams = array('context' => $context->get_course_context());
6455
        $posttext  = format_string($course->shortname, true, $formatparams) .
6456
                     ' -> ' .
6457
                     $modulename .
6458
                     ' -> ' .
6459
                     format_string($assignmentname, true, $formatparams) . "\n";
6460
        $posttext .= '---------------------------------------------------------------------' . "\n";
6461
        $posttext .= get_string($messagetype . 'text', 'assign', $info)."\n";
6462
        $posttext .= "\n---------------------------------------------------------------------\n";
6463
        return $posttext;
6464
    }
6465
 
6466
    /**
6467
     * Format a notification for HTML.
6468
     *
6469
     * @param string $messagetype
6470
     * @param stdClass $info
6471
     * @param stdClass $course
6472
     * @param stdClass $context
6473
     * @param string $modulename
6474
     * @param stdClass $coursemodule
6475
     * @param string $assignmentname
6476
     */
6477
    protected static function format_notification_message_html($messagetype,
6478
                                                             $info,
6479
                                                             $course,
6480
                                                             $context,
6481
                                                             $modulename,
6482
                                                             $coursemodule,
6483
                                                             $assignmentname) {
6484
        global $CFG;
6485
        $formatparams = array('context' => $context->get_course_context());
6486
        $posthtml  = '<p><font face="sans-serif">' .
6487
                     '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $course->id . '">' .
6488
                     format_string($course->shortname, true, $formatparams) .
6489
                     '</a> ->' .
6490
                     '<a href="' . $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id . '">' .
6491
                     $modulename .
6492
                     '</a> ->' .
6493
                     '<a href="' . $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id . '">' .
6494
                     format_string($assignmentname, true, $formatparams) .
6495
                     '</a></font></p>';
6496
        $posthtml .= '<hr /><font face="sans-serif">';
6497
        $posthtml .= '<p>' . get_string($messagetype . 'html', 'assign', $info) . '</p>';
6498
        $posthtml .= '</font><hr />';
6499
        return $posthtml;
6500
    }
6501
 
6502
    /**
6503
     * Message someone about something (static so it can be called from cron).
6504
     *
6505
     * @param stdClass $userfrom
6506
     * @param stdClass $userto
6507
     * @param string $messagetype
6508
     * @param string $eventtype
6509
     * @param int $updatetime
6510
     * @param stdClass $coursemodule
6511
     * @param stdClass $context
6512
     * @param stdClass $course
6513
     * @param string $modulename
6514
     * @param string $assignmentname
6515
     * @param bool $blindmarking
6516
     * @param int $uniqueidforuser
6517
     * @return void
6518
     */
6519
    public static function send_assignment_notification($userfrom,
6520
                                                        $userto,
6521
                                                        $messagetype,
6522
                                                        $eventtype,
6523
                                                        $updatetime,
6524
                                                        $coursemodule,
6525
                                                        $context,
6526
                                                        $course,
6527
                                                        $modulename,
6528
                                                        $assignmentname,
6529
                                                        $blindmarking,
6530
                                                        $uniqueidforuser) {
6531
        global $CFG, $PAGE;
6532
 
6533
        $info = new stdClass();
6534
        if ($blindmarking) {
6535
            $userfrom = clone($userfrom);
6536
            $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser;
6537
            $userfrom->firstname = get_string('participant', 'assign');
6538
            $userfrom->lastname = $uniqueidforuser;
6539
            $userfrom->email = $CFG->noreplyaddress;
6540
        } else {
6541
            $info->username = fullname($userfrom, true);
6542
        }
6543
        $info->assignment = format_string($assignmentname, true, array('context'=>$context));
6544
        $info->url = $CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id;
6545
        $info->timeupdated = userdate($updatetime, get_string('strftimerecentfull'));
6546
 
6547
        $postsubject = get_string($messagetype . 'small', 'assign', $info);
6548
        $posttext = self::format_notification_message_text($messagetype,
6549
                                                           $info,
6550
                                                           $course,
6551
                                                           $context,
6552
                                                           $modulename,
6553
                                                           $assignmentname);
6554
        $posthtml = '';
6555
        if ($userto->mailformat == 1) {
6556
            $posthtml = self::format_notification_message_html($messagetype,
6557
                                                               $info,
6558
                                                               $course,
6559
                                                               $context,
6560
                                                               $modulename,
6561
                                                               $coursemodule,
6562
                                                               $assignmentname);
6563
        }
6564
 
6565
        $eventdata = new \core\message\message();
6566
        $eventdata->courseid         = $course->id;
6567
        $eventdata->modulename       = 'assign';
6568
        $eventdata->userfrom         = $userfrom;
6569
        $eventdata->userto           = $userto;
6570
        $eventdata->subject          = $postsubject;
6571
        $eventdata->fullmessage      = $posttext;
6572
        $eventdata->fullmessageformat = FORMAT_PLAIN;
6573
        $eventdata->fullmessagehtml  = $posthtml;
6574
        $eventdata->smallmessage     = $postsubject;
6575
 
6576
        $eventdata->name            = $eventtype;
6577
        $eventdata->component       = 'mod_assign';
6578
        $eventdata->notification    = 1;
6579
        $eventdata->contexturl      = $info->url;
6580
        $eventdata->contexturlname  = $info->assignment;
6581
        $customdata = [
6582
            'cmid' => $coursemodule->id,
6583
            'instance' => $coursemodule->instance,
6584
            'messagetype' => $messagetype,
6585
            'blindmarking' => $blindmarking,
6586
            'uniqueidforuser' => $uniqueidforuser,
6587
        ];
6588
        // Check if the userfrom is real and visible.
6589
        if (!empty($userfrom->id) && core_user::is_real_user($userfrom->id)) {
6590
            $userpicture = new user_picture($userfrom);
6591
            $userpicture->size = 1; // Use f1 size.
6592
            $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
6593
            $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
6594
        }
6595
        $eventdata->customdata = $customdata;
6596
 
6597
        message_send($eventdata);
6598
    }
6599
 
6600
    /**
6601
     * Message someone about something.
6602
     *
6603
     * @param stdClass $userfrom
6604
     * @param stdClass $userto
6605
     * @param string $messagetype
6606
     * @param string $eventtype
6607
     * @param int $updatetime
6608
     * @return void
6609
     */
6610
    public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
6611
        global $USER;
6612
        $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
6613
        $uniqueid = $this->get_uniqueid_for_user($userid);
6614
        self::send_assignment_notification($userfrom,
6615
                                           $userto,
6616
                                           $messagetype,
6617
                                           $eventtype,
6618
                                           $updatetime,
6619
                                           $this->get_course_module(),
6620
                                           $this->get_context(),
6621
                                           $this->get_course(),
6622
                                           $this->get_module_name(),
6623
                                           $this->get_instance()->name,
6624
                                           $this->is_blind_marking(),
6625
                                           $uniqueid);
6626
    }
6627
 
6628
    /**
6629
     * Notify student upon successful submission copy.
6630
     *
6631
     * @param stdClass $submission
6632
     * @return void
6633
     */
6634
    protected function notify_student_submission_copied(stdClass $submission) {
6635
        global $DB, $USER;
6636
 
6637
        $adminconfig = $this->get_admin_config();
6638
        // Use the same setting for this - no need for another one.
6639
        if (empty($adminconfig->submissionreceipts)) {
6640
            // No need to do anything.
6641
            return;
6642
        }
6643
        if ($submission->userid) {
6644
            $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6645
        } else {
6646
            $user = $USER;
6647
        }
6648
        $this->send_notification($user,
6649
                                 $user,
6650
                                 'submissioncopied',
6651
                                 'assign_notification',
6652
                                 $submission->timemodified);
6653
    }
6654
    /**
6655
     * Notify student upon successful submission.
6656
     *
6657
     * @param stdClass $submission
6658
     * @return void
6659
     */
6660
    protected function notify_student_submission_receipt(stdClass $submission) {
6661
        global $DB, $USER;
6662
 
6663
        $adminconfig = $this->get_admin_config();
6664
        if (empty($adminconfig->submissionreceipts)) {
6665
            // No need to do anything.
6666
            return;
6667
        }
6668
        if ($submission->userid) {
6669
            $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6670
        } else {
6671
            $user = $USER;
6672
        }
6673
        if ($submission->userid == $USER->id) {
6674
            $this->send_notification(core_user::get_noreply_user(),
6675
                                     $user,
6676
                                     'submissionreceipt',
6677
                                     'assign_notification',
6678
                                     $submission->timemodified);
6679
        } else {
6680
            $this->send_notification($USER,
6681
                                     $user,
6682
                                     'submissionreceiptother',
6683
                                     'assign_notification',
6684
                                     $submission->timemodified);
6685
        }
6686
    }
6687
 
6688
    /**
6689
     * Send notifications to graders upon student submissions.
6690
     *
6691
     * @param stdClass $submission
6692
     * @return void
6693
     */
6694
    protected function notify_graders(stdClass $submission) {
6695
        global $DB, $USER;
6696
 
6697
        $instance = $this->get_instance();
6698
 
6699
        $late = $instance->duedate && ($instance->duedate < time());
6700
 
6701
        if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) {
6702
            // No need to do anything.
6703
            return;
6704
        }
6705
 
6706
        if ($submission->userid) {
6707
            $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6708
        } else {
6709
            $user = $USER;
6710
        }
6711
 
6712
        if ($notifyusers = $this->get_notifiable_users($user->id)) {
6713
            foreach ($notifyusers as $notifyuser) {
6714
                $this->send_notification($user,
6715
                                         $notifyuser,
6716
                                         'gradersubmissionupdated',
6717
                                         'assign_notification',
6718
                                         $submission->timemodified);
6719
            }
6720
        }
6721
    }
6722
 
6723
    /**
6724
     * Submit a submission for grading.
6725
     *
6726
     * @param stdClass $data - The form data
6727
     * @param array $notices - List of error messages to display on an error condition.
6728
     * @return bool Return false if the submission was not submitted.
6729
     */
6730
    public function submit_for_grading($data, $notices) {
6731
        global $USER;
6732
 
6733
        $userid = $USER->id;
6734
        if (!empty($data->userid)) {
6735
            $userid = $data->userid;
6736
        }
6737
        // Need submit permission to submit an assignment.
6738
        if ($userid == $USER->id) {
6739
            require_capability('mod/assign:submit', $this->context);
6740
        } else {
6741
            if (!$this->can_edit_submission($userid, $USER->id)) {
6742
                throw new \moodle_exception('nopermission');
6743
            }
6744
        }
6745
 
6746
        $instance = $this->get_instance();
6747
 
6748
        if ($instance->teamsubmission) {
6749
            $submission = $this->get_group_submission($userid, 0, true);
6750
        } else {
6751
            $submission = $this->get_user_submission($userid, true);
6752
        }
6753
 
6754
        if (!$this->submissions_open($userid)) {
6755
            $notices[] = get_string('submissionsclosed', 'assign');
6756
            return false;
6757
        }
6758
 
6759
        $adminconfig = $this->get_admin_config();
6760
 
6761
        $submissionstatement = '';
6762
        if ($instance->requiresubmissionstatement) {
6763
            $submissionstatement = $this->get_submissionstatement($adminconfig, $instance, $this->context);
6764
        }
6765
 
6766
        if (!empty($submissionstatement) && $instance->requiresubmissionstatement
6767
                && empty($data->submissionstatement) && $USER->id == $userid) {
6768
            return false;
6769
        }
6770
 
6771
        if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6772
            // Give each submission plugin a chance to process the submission.
6773
            $plugins = $this->get_submission_plugins();
6774
            foreach ($plugins as $plugin) {
6775
                if ($plugin->is_enabled() && $plugin->is_visible()) {
6776
                    $plugin->submit_for_grading($submission);
6777
                }
6778
            }
6779
 
6780
            $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6781
            $this->update_submission($submission, $userid, true, $instance->teamsubmission);
6782
            $completion = new completion_info($this->get_course());
6783
            if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
6784
                $this->update_activity_completion_records($instance->teamsubmission,
6785
                                                          $instance->requireallteammemberssubmit,
6786
                                                          $submission,
6787
                                                          $userid,
6788
                                                          COMPLETION_COMPLETE,
6789
                                                          $completion);
6790
            }
6791
 
6792
            if (!empty($data->submissionstatement) && $USER->id == $userid) {
6793
                \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
6794
            }
6795
            $this->notify_graders($submission);
6796
            $this->notify_student_submission_receipt($submission);
6797
 
6798
            \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger();
6799
 
6800
            return true;
6801
        }
6802
        $notices[] = get_string('submissionsclosed', 'assign');
6803
        return false;
6804
    }
6805
 
6806
    /**
6807
     * A students submission is submitted for grading by a teacher.
6808
     *
6809
     * @return bool
6810
     */
6811
    protected function process_submit_other_for_grading($mform, $notices) {
6812
        global $USER, $CFG;
6813
 
6814
        require_sesskey();
6815
 
6816
        $userid = optional_param('userid', $USER->id, PARAM_INT);
6817
 
6818
        if (!$this->submissions_open($userid)) {
6819
            $notices[] = get_string('submissionsclosed', 'assign');
6820
            return false;
6821
        }
6822
        $data = new stdClass();
6823
        $data->userid = $userid;
6824
        return $this->submit_for_grading($data, $notices);
6825
    }
6826
 
6827
    /**
6828
     * Assignment submission is processed before grading.
6829
     *
6830
     * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform.
6831
     *               It can be null.
6832
     * @return bool Return false if the validation fails. This affects which page is displayed next.
6833
     */
6834
    protected function process_submit_for_grading($mform, $notices) {
6835
        global $CFG;
6836
 
6837
        require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
6838
        require_sesskey();
6839
 
6840
        if (!$this->submissions_open()) {
6841
            $notices[] = get_string('submissionsclosed', 'assign');
6842
            return false;
6843
        }
6844
 
6845
        $data = new stdClass();
6846
        $adminconfig = $this->get_admin_config();
6847
        $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
6848
 
6849
        $submissionstatement = '';
6850
        if ($requiresubmissionstatement) {
6851
            $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
6852
        }
6853
 
6854
        // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
6855
        // that the submission statement checkbox will be displayed.
6856
        if (empty($submissionstatement)) {
6857
            $requiresubmissionstatement = false;
6858
        }
6859
 
6860
        if ($mform == null) {
6861
            $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
6862
                                                                    $submissionstatement,
6863
                                                                    $this->get_course_module()->id,
6864
                                                                    $data));
6865
        }
6866
 
6867
        $data = $mform->get_data();
6868
        if (!$mform->is_cancelled()) {
6869
            if ($mform->get_data() == false) {
6870
                return false;
6871
            }
6872
            return $this->submit_for_grading($data, $notices);
6873
        }
6874
        return true;
6875
    }
6876
 
6877
    /**
6878
     * Save the extension date for a single user.
6879
     *
6880
     * @param int $userid The user id
6881
     * @param mixed $extensionduedate Either an integer date or null
6882
     * @return boolean
6883
     */
6884
    public function save_user_extension($userid, $extensionduedate) {
6885
        global $DB;
6886
 
6887
        // Need submit permission to submit an assignment.
6888
        require_capability('mod/assign:grantextension', $this->context);
6889
 
6890
        if (!is_enrolled($this->get_course_context(), $userid)) {
6891
            return false;
6892
        }
6893
        if (!has_capability('mod/assign:submit', $this->context, $userid)) {
6894
            return false;
6895
        }
6896
 
6897
        if ($this->get_instance()->duedate && $extensionduedate) {
6898
            if ($this->get_instance()->duedate > $extensionduedate) {
6899
                return false;
6900
            }
6901
        }
6902
        if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) {
6903
            if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) {
6904
                return false;
6905
            }
6906
        }
6907
 
6908
        $flags = $this->get_user_flags($userid, true);
6909
        $flags->extensionduedate = $extensionduedate;
6910
 
6911
        $result = $this->update_user_flags($flags);
6912
 
6913
        if ($result) {
6914
            \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger();
6915
        }
6916
        return $result;
6917
    }
6918
 
6919
    /**
6920
     * Save extension date.
6921
     *
6922
     * @param moodleform $mform The submitted form
6923
     * @return boolean
6924
     */
6925
    protected function process_save_extension(& $mform) {
6926
        global $DB, $CFG;
6927
 
6928
        // Include extension form.
6929
        require_once($CFG->dirroot . '/mod/assign/extensionform.php');
6930
        require_sesskey();
6931
 
6932
        $users = optional_param('userid', 0, PARAM_INT);
6933
        if (!$users) {
6934
            $users = required_param('selectedusers', PARAM_SEQUENCE);
6935
        }
6936
        $userlist = explode(',', $users);
6937
 
6938
        $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
6939
        $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
6940
        foreach ($userlist as $userid) {
6941
            // To validate extension date with users overrides.
6942
            $override = $this->override_exists($userid);
6943
            foreach ($keys as $key) {
6944
                if ($override->{$key}) {
6945
                    if ($maxoverride[$key] < $override->{$key}) {
6946
                        $maxoverride[$key] = $override->{$key};
6947
                    }
6948
                } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
6949
                    $maxoverride[$key] = $this->get_instance()->{$key};
6950
                }
6951
            }
6952
        }
6953
        foreach ($keys as $key) {
6954
            if ($maxoverride[$key]) {
6955
                $this->get_instance()->{$key} = $maxoverride[$key];
6956
            }
6957
        }
6958
 
6959
        $formparams = array(
6960
            'instance' => $this->get_instance(),
6961
            'assign' => $this,
6962
            'userlist' => $userlist
6963
        );
6964
 
6965
        $mform = new mod_assign_extension_form(null, $formparams);
6966
 
6967
        if ($mform->is_cancelled()) {
6968
            return true;
6969
        }
6970
 
6971
        if ($formdata = $mform->get_data()) {
6972
            if (!empty($formdata->selectedusers)) {
6973
                $users = explode(',', $formdata->selectedusers);
6974
                $result = true;
6975
                foreach ($users as $userid) {
6976
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6977
                    $result = $this->save_user_extension($user->id, $formdata->extensionduedate) && $result;
6978
                }
6979
                return $result;
6980
            }
6981
            if (!empty($formdata->userid)) {
6982
                $user = $DB->get_record('user', array('id' => $formdata->userid), '*', MUST_EXIST);
6983
                return $this->save_user_extension($user->id, $formdata->extensionduedate);
6984
            }
6985
        }
6986
 
6987
        return false;
6988
    }
6989
 
6990
    /**
6991
     * Save quick grades.
6992
     *
6993
     * @return string The result of the save operation
6994
     */
6995
    protected function process_save_quick_grades() {
6996
        global $USER, $DB, $CFG;
6997
 
6998
        // Need grade permission.
6999
        require_capability('mod/assign:grade', $this->context);
7000
        require_sesskey();
7001
 
7002
        // Make sure advanced grading is disabled.
7003
        $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
7004
        $controller = $gradingmanager->get_active_controller();
7005
        if (!empty($controller)) {
7006
            $message = get_string('errorquickgradingvsadvancedgrading', 'assign');
7007
            $this->set_error_message($message);
7008
            return $message;
7009
        }
7010
 
7011
        $users = array();
7012
        // First check all the last modified values.
7013
        $currentgroup = groups_get_activity_group($this->get_course_module(), true);
7014
        $participants = $this->list_participants($currentgroup, true);
7015
 
7016
        // Gets a list of possible users and look for values based upon that.
7017
        foreach ($participants as $userid => $unused) {
7018
            $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
7019
            $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT);
7020
            // Gather the userid, updated grade and last modified value.
7021
            $record = new stdClass();
7022
            $record->userid = $userid;
7023
            if ($modified >= 0) {
7024
                $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
7025
                $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA);
7026
                $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
7027
            } else {
7028
                // This user was not in the grading table.
7029
                continue;
7030
            }
7031
            $record->attemptnumber = $attemptnumber;
7032
            $record->lastmodified = $modified;
7033
            $record->gradinginfo = grade_get_grades($this->get_course()->id,
7034
                                                    'mod',
7035
                                                    'assign',
7036
                                                    $this->get_instance()->id,
7037
                                                    array($userid));
7038
            $users[$userid] = $record;
7039
        }
7040
 
7041
        if (empty($users)) {
7042
            $message = get_string('nousersselected', 'assign');
7043
            $this->set_error_message($message);
7044
            return $message;
7045
        }
7046
 
7047
        list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
7048
        $params['assignid1'] = $this->get_instance()->id;
7049
        $params['assignid2'] = $this->get_instance()->id;
7050
 
7051
        // Check them all for currency.
7052
        $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt
7053
                              FROM {assign_submission} s
7054
                             WHERE s.assignment = :assignid1 AND s.latest = 1';
7055
 
7056
        $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified,
7057
                       uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber
7058
                  FROM {user} u
7059
             LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
7060
             LEFT JOIN {assign_grades} g ON
7061
                       u.id = g.userid AND
7062
                       g.assignment = :assignid2 AND
7063
                       g.attemptnumber = gmx.maxattempt
7064
             LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
7065
                 WHERE u.id ' . $userids;
7066
        $currentgrades = $DB->get_recordset_sql($sql, $params);
7067
 
7068
        $modifiedusers = array();
7069
        foreach ($currentgrades as $current) {
7070
            $modified = $users[(int)$current->userid];
7071
            $grade = $this->get_user_grade($modified->userid, false);
7072
            // Check to see if the grade column was even visible.
7073
            $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
7074
 
7075
            // Check to see if the outcomes were modified.
7076
            if ($CFG->enableoutcomes) {
7077
                foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7078
                    $oldoutcome = $outcome->grades[$modified->userid]->grade;
7079
                    $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7080
                    $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
7081
                    // Check to see if the outcome column was even visible.
7082
                    $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
7083
                    if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
7084
                        // Can't check modified time for outcomes because it is not reported.
7085
                        $modifiedusers[$modified->userid] = $modified;
7086
                        continue;
7087
                    }
7088
                }
7089
            }
7090
 
7091
            // Let plugins participate.
7092
            foreach ($this->feedbackplugins as $plugin) {
7093
                if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7094
                    // The plugins must handle is_quickgrading_modified correctly - ie
7095
                    // handle hidden columns.
7096
                    if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
7097
                        if ((int)$current->lastmodified > (int)$modified->lastmodified) {
7098
                            $message = get_string('errorrecordmodified', 'assign');
7099
                            $this->set_error_message($message);
7100
                            return $message;
7101
                        } else {
7102
                            $modifiedusers[$modified->userid] = $modified;
7103
                            continue;
7104
                        }
7105
                    }
7106
                }
7107
            }
7108
 
7109
            if (($current->grade < 0 || $current->grade === null) &&
7110
                ($modified->grade < 0 || $modified->grade === null)) {
7111
                // Different ways to indicate no grade.
7112
                $modified->grade = $current->grade; // Keep existing grade.
7113
            }
7114
            // Treat 0 and null as different values.
7115
            if ($current->grade !== null) {
7116
                $current->grade = floatval($current->grade);
7117
            }
7118
            $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
7119
            $markingallocationchanged = $this->get_instance()->markingworkflow &&
7120
                                        $this->get_instance()->markingallocation &&
7121
                                            ($modified->allocatedmarker !== false) &&
7122
                                            ($current->allocatedmarker != $modified->allocatedmarker);
7123
            $workflowstatechanged = $this->get_instance()->markingworkflow &&
7124
                                            ($modified->workflowstate !== false) &&
7125
                                            ($current->workflowstate != $modified->workflowstate);
7126
            if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
7127
                // Grade changed.
7128
                if ($this->grading_disabled($modified->userid)) {
7129
                    continue;
7130
                }
7131
                $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified;
7132
                $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber;
7133
                if ($badmodified || $badattempt) {
7134
                    // Error - record has been modified since viewing the page.
7135
                    $message = get_string('errorrecordmodified', 'assign');
7136
                    $this->set_error_message($message);
7137
                    return $message;
7138
                } else {
7139
                    $modifiedusers[$modified->userid] = $modified;
7140
                }
7141
            }
7142
 
7143
        }
7144
        $currentgrades->close();
7145
 
7146
        $adminconfig = $this->get_admin_config();
7147
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7148
 
7149
        // Ok - ready to process the updates.
7150
        foreach ($modifiedusers as $userid => $modified) {
7151
            $grade = $this->get_user_grade($userid, true);
7152
            $flags = $this->get_user_flags($userid, true);
7153
            $grade->grade= grade_floatval(unformat_float($modified->grade));
7154
            $grade->grader= $USER->id;
7155
            $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
7156
 
7157
            // Save plugins data.
7158
            foreach ($this->feedbackplugins as $plugin) {
7159
                if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7160
                    $plugin->save_quickgrading_changes($userid, $grade);
7161
                    if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
7162
                        // This is the feedback plugin chose to push comments to the gradebook.
7163
                        $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7164
                        $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7165
                        $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7166
                    }
7167
                }
7168
            }
7169
 
7170
            // These will be set to false if they are not present in the quickgrading
7171
            // form (e.g. column hidden).
7172
            $workflowstatemodified = ($modified->workflowstate !== false) &&
7173
                                        ($flags->workflowstate != $modified->workflowstate);
7174
 
7175
            $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
7176
                                        ($flags->allocatedmarker != $modified->allocatedmarker);
7177
 
7178
            if ($workflowstatemodified) {
7179
                $flags->workflowstate = $modified->workflowstate;
7180
            }
7181
            if ($allocatedmarkermodified) {
7182
                $flags->allocatedmarker = $modified->allocatedmarker;
7183
            }
7184
            if ($workflowstatemodified || $allocatedmarkermodified) {
7185
                if ($this->update_user_flags($flags) && $workflowstatemodified) {
7186
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7187
                    \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger();
7188
                }
7189
            }
7190
            $this->update_grade($grade);
7191
 
7192
            // Allow teachers to skip sending notifications.
7193
            if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) {
7194
                $this->notify_grade_modified($grade, true);
7195
            }
7196
 
7197
            // Save outcomes.
7198
            if ($CFG->enableoutcomes) {
7199
                $data = array();
7200
                foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7201
                    $oldoutcome = $outcome->grades[$modified->userid]->grade;
7202
                    $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7203
                    // This will be false if the input was not in the quickgrading
7204
                    // form (e.g. column hidden).
7205
                    $newoutcome = optional_param($paramname, false, PARAM_INT);
7206
                    if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
7207
                        $data[$outcomeid] = $newoutcome;
7208
                    }
7209
                }
7210
                if (count($data) > 0) {
7211
                    grade_update_outcomes('mod/assign',
7212
                                          $this->course->id,
7213
                                          'mod',
7214
                                          'assign',
7215
                                          $this->get_instance()->id,
7216
                                          $userid,
7217
                                          $data);
7218
                }
7219
            }
7220
        }
7221
 
7222
        return get_string('quickgradingchangessaved', 'assign');
7223
    }
7224
 
7225
    /**
7226
     * Reveal student identities to markers (and the gradebook).
7227
     *
7228
     * @return void
7229
     */
7230
    public function reveal_identities() {
7231
        global $DB;
7232
 
7233
        require_capability('mod/assign:revealidentities', $this->context);
7234
 
7235
        if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) {
7236
            return false;
7237
        }
7238
 
7239
        // Update the assignment record.
7240
        $update = new stdClass();
7241
        $update->id = $this->get_instance()->id;
7242
        $update->revealidentities = 1;
7243
        $DB->update_record('assign', $update);
7244
 
7245
        // Refresh the instance data.
7246
        $this->instance = null;
7247
 
7248
        // Release the grades to the gradebook.
7249
        // First create the column in the gradebook.
7250
        $this->update_gradebook(false, $this->get_course_module()->id);
7251
 
7252
        // Now release all grades.
7253
 
7254
        $adminconfig = $this->get_admin_config();
7255
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7256
        $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
7257
        $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id));
7258
 
7259
        $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
7260
 
7261
        foreach ($grades as $grade) {
7262
            // Fetch any comments for this student.
7263
            if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
7264
                $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7265
                $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7266
                $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7267
            }
7268
            $this->gradebook_item_update(null, $grade);
7269
        }
7270
 
7271
        \mod_assign\event\identities_revealed::create_from_assign($this)->trigger();
7272
    }
7273
 
7274
    /**
7275
     * Reveal student identities to markers (and the gradebook).
7276
     *
7277
     * @return void
7278
     */
7279
    protected function process_reveal_identities() {
7280
 
7281
        if (!confirm_sesskey()) {
7282
            return false;
7283
        }
7284
 
7285
        return $this->reveal_identities();
7286
    }
7287
 
7288
 
7289
    /**
7290
     * Save grading options.
7291
     *
7292
     * @return void
7293
     */
7294
    protected function process_save_grading_options() {
7295
        global $USER, $CFG;
7296
 
7297
        // Include grading options form.
7298
        require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
7299
 
7300
        // Need submit permission to submit an assignment.
7301
        $this->require_view_grades();
7302
        require_sesskey();
7303
 
7304
        // Is advanced grading enabled?
7305
        $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
7306
        $controller = $gradingmanager->get_active_controller();
7307
        $showquickgrading = empty($controller);
7308
        if (!is_null($this->context)) {
7309
            $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
7310
        } else {
7311
            $showonlyactiveenrolopt = false;
7312
        }
7313
 
7314
        $markingallocation = $this->get_instance()->markingworkflow &&
7315
            $this->get_instance()->markingallocation &&
7316
            has_capability('mod/assign:manageallocations', $this->context);
7317
        // Get markers to use in drop lists.
7318
        $markingallocationoptions = array();
7319
        if ($markingallocation) {
7320
            $markingallocationoptions[''] = get_string('filternone', 'assign');
7321
            $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
7322
            list($sort, $params) = users_order_by_sql('u');
7323
            // Only enrolled users could be assigned as potential markers.
7324
            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
7325
            foreach ($markers as $marker) {
7326
                $markingallocationoptions[$marker->id] = fullname($marker);
7327
            }
7328
        }
7329
 
7330
        // Get marking states to show in form.
7331
        $markingworkflowoptions = $this->get_marking_workflow_filters();
7332
 
7333
        $gradingoptionsparams = array('cm'=>$this->get_course_module()->id,
7334
                                      'contextid'=>$this->context->id,
7335
                                      'userid'=>$USER->id,
7336
                                      'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
7337
                                      'showquickgrading'=>$showquickgrading,
7338
                                      'quickgrading'=>false,
7339
                                      'markingworkflowopt' => $markingworkflowoptions,
7340
                                      'markingallocationopt' => $markingallocationoptions,
7341
                                      'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
7342
                                      'showonlyactiveenrol' => $this->show_only_active_users(),
7343
                                      'downloadasfolders' => get_user_preferences('assign_downloadasfolders', 1));
7344
        $mform = new mod_assign_grading_options_form(null, $gradingoptionsparams);
7345
        if ($formdata = $mform->get_data()) {
7346
            set_user_preference('assign_perpage', $formdata->perpage);
7347
            if (isset($formdata->filter)) {
7348
                set_user_preference('assign_filter', $formdata->filter);
7349
            }
7350
            if (isset($formdata->markerfilter)) {
7351
                set_user_preference('assign_markerfilter', $formdata->markerfilter);
7352
            }
7353
            if (isset($formdata->workflowfilter)) {
7354
                set_user_preference('assign_workflowfilter', $formdata->workflowfilter);
7355
            }
7356
            if ($showquickgrading) {
7357
                set_user_preference('assign_quickgrading', isset($formdata->quickgrading));
7358
            }
7359
            if (isset($formdata->downloadasfolders)) {
7360
                set_user_preference('assign_downloadasfolders', 1); // Enabled.
7361
            } else {
7362
                set_user_preference('assign_downloadasfolders', 0); // Disabled.
7363
            }
7364
            if (!empty($showonlyactiveenrolopt)) {
7365
                $showonlyactiveenrol = isset($formdata->showonlyactiveenrol);
7366
                set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrol);
7367
                $this->showonlyactiveenrol = $showonlyactiveenrol;
7368
            }
7369
        }
7370
    }
7371
 
7372
    /**
7373
     * @deprecated since 2.7
7374
     */
7375
    public function format_grade_for_log() {
7376
        throw new coding_exception(__FUNCTION__ . ' has been deprecated, please do not use it any more');
7377
    }
7378
 
7379
    /**
7380
     * @deprecated since 2.7
7381
     */
7382
    public function format_submission_for_log() {
7383
        throw new coding_exception(__FUNCTION__ . ' has been deprecated, please do not use it any more');
7384
    }
7385
 
7386
    /**
7387
     * Require a valid sess key and then call copy_previous_attempt.
7388
     *
7389
     * @param  array $notices Any error messages that should be shown
7390
     *                        to the user at the top of the edit submission form.
7391
     * @return bool
7392
     */
7393
    protected function process_copy_previous_attempt(&$notices) {
7394
        require_sesskey();
7395
 
7396
        return $this->copy_previous_attempt($notices);
7397
    }
7398
 
7399
    /**
7400
     * Copy the current assignment submission from the last submitted attempt.
7401
     *
7402
     * @param  array $notices Any error messages that should be shown
7403
     *                        to the user at the top of the edit submission form.
7404
     * @return bool
7405
     */
7406
    public function copy_previous_attempt(&$notices) {
7407
        global $USER, $CFG;
7408
 
7409
        require_capability('mod/assign:submit', $this->context);
7410
 
7411
        $instance = $this->get_instance();
7412
        if ($instance->teamsubmission) {
7413
            $submission = $this->get_group_submission($USER->id, 0, true);
7414
        } else {
7415
            $submission = $this->get_user_submission($USER->id, true);
7416
        }
7417
        if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
7418
            $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
7419
            return false;
7420
        }
7421
        $flags = $this->get_user_flags($USER->id, false);
7422
 
7423
        // Get the flags to check if it is locked.
7424
        if ($flags && $flags->locked) {
7425
            $notices[] = get_string('submissionslocked', 'assign');
7426
            return false;
7427
        }
7428
        if ($instance->submissiondrafts) {
7429
            $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7430
        } else {
7431
            $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7432
        }
7433
        $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
7434
 
7435
        // Find the previous submission.
7436
        if ($instance->teamsubmission) {
7437
            $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
7438
        } else {
7439
            $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
7440
        }
7441
 
7442
        if (!$previoussubmission) {
7443
            // There was no previous submission so there is nothing else to do.
7444
            return true;
7445
        }
7446
 
7447
        $pluginerror = false;
7448
        foreach ($this->get_submission_plugins() as $plugin) {
7449
            if ($plugin->is_visible() && $plugin->is_enabled()) {
7450
                if (!$plugin->copy_submission($previoussubmission, $submission)) {
7451
                    $notices[] = $plugin->get_error();
7452
                    $pluginerror = true;
7453
                }
7454
            }
7455
        }
7456
        if ($pluginerror) {
7457
            return false;
7458
        }
7459
 
7460
        \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger();
7461
 
7462
        $complete = COMPLETION_INCOMPLETE;
7463
        if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7464
            $complete = COMPLETION_COMPLETE;
7465
        }
7466
        $completion = new completion_info($this->get_course());
7467
        if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
7468
            $this->update_activity_completion_records($instance->teamsubmission,
7469
                                                      $instance->requireallteammemberssubmit,
7470
                                                      $submission,
7471
                                                      $USER->id,
7472
                                                      $complete,
7473
                                                      $completion);
7474
        }
7475
 
7476
        if (!$instance->submissiondrafts) {
7477
            // There is a case for not notifying the student about the submission copy,
7478
            // but it provides a record of the event and if they then cancel editing it
7479
            // is clear that the submission was copied.
7480
            $this->notify_student_submission_copied($submission);
7481
            $this->notify_graders($submission);
7482
 
7483
            // The same logic applies here - we could not notify teachers,
7484
            // but then they would wonder why there are submitted assignments
7485
            // and they haven't been notified.
7486
            \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7487
        }
7488
        return true;
7489
    }
7490
 
7491
    /**
7492
     * Determine if the current submission is empty or not.
7493
     *
7494
     * @param submission $submission the students submission record to check.
7495
     * @return bool
7496
     */
7497
    public function submission_empty($submission) {
7498
        $allempty = true;
7499
 
7500
        foreach ($this->submissionplugins as $plugin) {
7501
            if ($plugin->is_enabled() && $plugin->is_visible()) {
7502
                if (!$allempty || !$plugin->is_empty($submission)) {
7503
                    $allempty = false;
7504
                }
7505
            }
7506
        }
7507
        return $allempty;
7508
    }
7509
 
7510
    /**
7511
     * Determine if a new submission is empty or not
7512
     *
7513
     * @param stdClass $data Submission data
7514
     * @return bool
7515
     */
7516
    public function new_submission_empty($data) {
7517
        foreach ($this->submissionplugins as $plugin) {
7518
            if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() &&
7519
                    !$plugin->submission_is_empty($data)) {
7520
                return false;
7521
            }
7522
        }
7523
        return true;
7524
    }
7525
 
7526
    /**
7527
     * Save assignment submission for the current user.
7528
     *
7529
     * @param  stdClass $data
7530
     * @param  array $notices Any error messages that should be shown
7531
     *                        to the user.
7532
     * @return bool
7533
     */
7534
    public function save_submission(stdClass $data, & $notices) {
7535
        global $CFG, $USER, $DB;
7536
 
7537
        $userid = $USER->id;
7538
        if (!empty($data->userid)) {
7539
            $userid = $data->userid;
7540
        }
7541
 
7542
        $user = clone($USER);
7543
        if ($userid == $USER->id) {
7544
            require_capability('mod/assign:submit', $this->context);
7545
        } else {
7546
            $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
7547
            if (!$this->can_edit_submission($userid, $USER->id)) {
7548
                throw new \moodle_exception('nopermission');
7549
            }
7550
        }
7551
        $instance = $this->get_instance();
7552
 
7553
        if ($instance->teamsubmission) {
7554
            $submission = $this->get_group_submission($userid, 0, true);
7555
        } else {
7556
            $submission = $this->get_user_submission($userid, true);
7557
        }
7558
 
7559
        if ($this->new_submission_empty($data)) {
7560
            $notices[] = get_string('submissionempty', 'mod_assign');
7561
            return false;
7562
        }
7563
 
7564
        // Check that no one has modified the submission since we started looking at it.
7565
        if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) {
7566
            // Another user has submitted something. Notify the current user.
7567
            if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) {
7568
                $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign')
7569
                                                       : get_string('submissionmodified', 'mod_assign');
7570
                return false;
7571
            }
7572
        }
7573
 
7574
        if ($instance->submissiondrafts) {
7575
            $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7576
        } else {
7577
            $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7578
        }
7579
 
7580
        $flags = $this->get_user_flags($userid, false);
7581
 
7582
        // Get the flags to check if it is locked.
7583
        if ($flags && $flags->locked) {
7584
            throw new \moodle_exception('submissionslocked', 'assign');
7585
            return true;
7586
        }
7587
 
7588
        $pluginerror = false;
7589
        foreach ($this->submissionplugins as $plugin) {
7590
            if ($plugin->is_enabled() && $plugin->is_visible()) {
7591
                if (!$plugin->save($submission, $data)) {
7592
                    $notices[] = $plugin->get_error();
7593
                    $pluginerror = true;
7594
                }
7595
            }
7596
        }
7597
 
7598
        $allempty = $this->submission_empty($submission);
7599
        if ($pluginerror || $allempty) {
7600
            if ($allempty) {
7601
                $notices[] = get_string('submissionempty', 'mod_assign');
7602
            }
7603
            return false;
7604
        }
7605
 
7606
        $this->update_submission($submission, $userid, true, $instance->teamsubmission);
7607
        $users = [$userid];
7608
 
7609
        if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) {
7610
            $team = $this->get_submission_group_members($submission->groupid, true);
7611
 
7612
            foreach ($team as $member) {
7613
                if ($member->id != $userid) {
7614
                    $membersubmission = clone($submission);
7615
                    $this->update_submission($membersubmission, $member->id, true, $instance->teamsubmission);
7616
                    $users[] = $member->id;
7617
                }
7618
            }
7619
        }
7620
 
7621
        $complete = COMPLETION_INCOMPLETE;
7622
        if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7623
            $complete = COMPLETION_COMPLETE;
7624
        }
7625
 
7626
        $completion = new completion_info($this->get_course());
7627
        if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
7628
            foreach ($users as $id) {
7629
                $completion->update_state($this->get_course_module(), $complete, $id);
7630
            }
7631
        }
7632
 
7633
        // Logging.
7634
        if (isset($data->submissionstatement) && ($userid == $USER->id)) {
7635
            \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
7636
        }
7637
 
7638
        if (!$instance->submissiondrafts) {
7639
            $this->notify_student_submission_receipt($submission);
7640
            $this->notify_graders($submission);
7641
            \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7642
        }
7643
        return true;
7644
    }
7645
 
7646
    /**
7647
     * Save assignment submission.
7648
     *
7649
     * @param  moodleform $mform
7650
     * @param  array $notices Any error messages that should be shown
7651
     *                        to the user at the top of the edit submission form.
7652
     * @return bool
7653
     */
7654
    protected function process_save_submission(&$mform, &$notices) {
7655
        global $CFG, $USER;
7656
 
7657
        // Include submission form.
7658
        require_once($CFG->dirroot . '/mod/assign/submission_form.php');
7659
 
7660
        $userid = optional_param('userid', $USER->id, PARAM_INT);
7661
        // Need submit permission to submit an assignment.
7662
        require_sesskey();
7663
        if (!$this->submissions_open($userid)) {
7664
            $notices[] = get_string('duedatereached', 'assign');
7665
            return false;
7666
        }
7667
        $instance = $this->get_instance();
7668
 
7669
        $data = new stdClass();
7670
        $data->userid = $userid;
7671
        $mform = new mod_assign_submission_form(null, array($this, $data));
7672
        if ($mform->is_cancelled()) {
7673
            return true;
7674
        }
7675
        if ($data = $mform->get_data()) {
7676
            return $this->save_submission($data, $notices);
7677
        }
7678
        return false;
7679
    }
7680
 
7681
 
7682
    /**
7683
     * Determine if this users grade can be edited.
7684
     *
7685
     * @param int $userid - The student userid
7686
     * @param bool $checkworkflow - whether to include a check for the workflow state.
7687
     * @param stdClass $gradinginfo - optional, allow gradinginfo to be passed for performance.
7688
     * @return bool $gradingdisabled
7689
     */
7690
    public function grading_disabled($userid, $checkworkflow = true, $gradinginfo = null) {
7691
        if ($checkworkflow && $this->get_instance()->markingworkflow) {
7692
            $grade = $this->get_user_grade($userid, false);
7693
            $validstates = $this->get_marking_workflow_states_for_current_user();
7694
            if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) {
7695
                return true;
7696
            }
7697
        }
7698
 
7699
        if (is_null($gradinginfo)) {
7700
            $gradinginfo = grade_get_grades($this->get_course()->id,
7701
                'mod',
7702
                'assign',
7703
                $this->get_instance()->id,
7704
                array($userid));
7705
        }
7706
 
7707
        if (!$gradinginfo) {
7708
            return false;
7709
        }
7710
 
7711
        if (!isset($gradinginfo->items[0]->grades[$userid])) {
7712
            return false;
7713
        }
7714
        $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked ||
7715
                           $gradinginfo->items[0]->grades[$userid]->overridden;
7716
        return $gradingdisabled;
7717
    }
7718
 
7719
 
7720
    /**
7721
     * Get an instance of a grading form if advanced grading is enabled.
7722
     * This is specific to the assignment, marker and student.
7723
     *
7724
     * @param int $userid - The student userid
7725
     * @param stdClass|false $grade - The grade record
7726
     * @param bool $gradingdisabled
7727
     * @return mixed gradingform_instance|null $gradinginstance
7728
     */
7729
    protected function get_grading_instance($userid, $grade, $gradingdisabled) {
7730
        global $CFG, $USER;
7731
 
7732
        $grademenu = make_grades_menu($this->get_instance()->grade);
7733
        $allowgradedecimals = $this->get_instance()->grade > 0;
7734
 
7735
        $advancedgradingwarning = false;
7736
        $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
7737
        $gradinginstance = null;
7738
        if ($gradingmethod = $gradingmanager->get_active_method()) {
7739
            $controller = $gradingmanager->get_controller($gradingmethod);
7740
            if ($controller->is_form_available()) {
7741
                $itemid = null;
7742
                if ($grade) {
7743
                    $itemid = $grade->id;
7744
                }
7745
                if ($gradingdisabled && $itemid) {
7746
                    $gradinginstance = $controller->get_current_instance($USER->id, $itemid);
7747
                } else if (!$gradingdisabled) {
7748
                    $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
7749
                    $gradinginstance = $controller->get_or_create_instance($instanceid,
7750
                                                                           $USER->id,
7751
                                                                           $itemid);
7752
                }
7753
            } else {
7754
                $advancedgradingwarning = $controller->form_unavailable_notification();
7755
            }
7756
        }
7757
        if ($gradinginstance) {
7758
            $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals);
7759
        }
7760
        return $gradinginstance;
7761
    }
7762
 
7763
    /**
7764
     * Add elements to grade form.
7765
     *
7766
     * @param MoodleQuickForm $mform
7767
     * @param stdClass $data
7768
     * @param array $params
7769
     * @return void
7770
     */
7771
    public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) {
7772
        global $USER, $CFG, $SESSION;
7773
        $settings = $this->get_instance();
7774
 
7775
        $rownum = isset($params['rownum']) ? $params['rownum'] : 0;
7776
        $last = isset($params['last']) ? $params['last'] : true;
7777
        $useridlistid = isset($params['useridlistid']) ? $params['useridlistid'] : 0;
7778
        $userid = isset($params['userid']) ? $params['userid'] : 0;
7779
        $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0;
7780
        $gradingpanel = !empty($params['gradingpanel']);
7781
        $bothids = ($userid && $useridlistid);
7782
 
7783
        if (!$userid || $bothids) {
7784
            $useridlist = $this->get_grading_userid_list(true, $useridlistid);
7785
        } else {
7786
            $useridlist = array($userid);
7787
            $rownum = 0;
7788
            $useridlistid = '';
7789
        }
7790
 
7791
        $userid = $useridlist[$rownum];
7792
        // We need to create a grade record matching this attempt number
7793
        // or the feedback plugin will have no way to know what is the correct attempt.
7794
        $grade = $this->get_user_grade($userid, true, $attemptnumber);
7795
 
7796
        $submission = null;
7797
        if ($this->get_instance()->teamsubmission) {
7798
            $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
7799
        } else {
7800
            $submission = $this->get_user_submission($userid, false, $attemptnumber);
7801
        }
7802
 
7803
        // Add advanced grading.
7804
        $gradingdisabled = $this->grading_disabled($userid);
7805
        $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
7806
 
7807
        $mform->addElement('header', 'gradeheader', get_string('gradenoun'));
7808
        if ($gradinginstance) {
7809
            $gradingelement = $mform->addElement('grading',
7810
                                                 'advancedgrading',
7811
                                                 get_string('gradenoun') . ':',
7812
                                                 array('gradinginstance' => $gradinginstance));
7813
            if ($gradingdisabled) {
7814
                $gradingelement->freeze();
7815
            } else {
7816
                $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
7817
                $mform->setType('advancedgradinginstanceid', PARAM_INT);
7818
            }
7819
        } else {
7820
            // Use simple direct grading.
7821
            if ($this->get_instance()->grade > 0) {
7822
                $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade);
7823
                if (!$gradingdisabled) {
7824
                    $gradingelement = $mform->addElement('text', 'grade', $name);
7825
                    $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign');
7826
                    $mform->setType('grade', PARAM_RAW);
7827
                } else {
7828
                    $strgradelocked = get_string('gradelocked', 'assign');
7829
                    $mform->addElement('static', 'gradedisabled', $name, $strgradelocked);
7830
                    $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign');
7831
                }
7832
            } else {
7833
                $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade);
7834
                if (count($grademenu) > 1) {
7835
                    $gradingelement = $mform->addElement('select', 'grade', get_string('gradenoun') . ':', $grademenu);
7836
 
7837
                    // The grade is already formatted with format_float so it needs to be converted back to an integer.
7838
                    if (!empty($data->grade)) {
7839
                        $data->grade = (int)unformat_float($data->grade);
7840
                    }
7841
                    $mform->setType('grade', PARAM_INT);
7842
                    if ($gradingdisabled) {
7843
                        $gradingelement->freeze();
7844
                    }
7845
                }
7846
            }
7847
        }
7848
 
7849
        $gradinginfo = grade_get_grades($this->get_course()->id,
7850
                                        'mod',
7851
                                        'assign',
7852
                                        $this->get_instance()->id,
7853
                                        $userid);
7854
        if (!empty($CFG->enableoutcomes)) {
7855
            foreach ($gradinginfo->outcomes as $index => $outcome) {
7856
                $options = make_grades_menu(-$outcome->scaleid);
7857
                $options[0] = get_string('nooutcome', 'grades');
7858
                if ($outcome->grades[$userid]->locked) {
7859
                    $mform->addElement('static',
7860
                                       'outcome_' . $index . '[' . $userid . ']',
7861
                                       $outcome->name . ':',
7862
                                       $options[$outcome->grades[$userid]->grade]);
7863
                } else {
7864
                    $attributes = array('id' => 'menuoutcome_' . $index );
7865
                    $mform->addElement('select',
7866
                                       'outcome_' . $index . '[' . $userid . ']',
7867
                                       $outcome->name.':',
7868
                                       $options,
7869
                                       $attributes);
7870
                    $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT);
7871
                    $mform->setDefault('outcome_' . $index . '[' . $userid . ']',
7872
                                       $outcome->grades[$userid]->grade);
7873
                }
7874
            }
7875
        }
7876
 
7877
        $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall');
7878
        $usergrade = get_string('notgraded', 'assign');
7879
        if (has_all_capabilities($capabilitylist, $this->get_course_context())) {
7880
            $urlparams = array('id'=>$this->get_course()->id);
7881
            $url = new moodle_url('/grade/report/grader/index.php', $urlparams);
7882
            if (isset($gradinginfo->items[0]->grades[$userid]->grade)) {
7883
                $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7884
            }
7885
            $gradestring = $this->get_renderer()->action_link($url, $usergrade);
7886
        } else {
7887
            if (isset($gradinginfo->items[0]->grades[$userid]) &&
7888
                    !$gradinginfo->items[0]->grades[$userid]->hidden) {
7889
                $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7890
            }
7891
            $gradestring = $usergrade;
7892
        }
7893
 
7894
        if ($this->get_instance()->markingworkflow) {
7895
            $states = $this->get_marking_workflow_states_for_current_user();
7896
            $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
7897
            $select = $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
7898
            $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
7899
            if (!empty($data->workflowstate) && !array_key_exists($data->workflowstate, $states)) {
7900
                // In a workflow state that user should not be able to change, so freeze workflow selector.
7901
                // Have to add the state so it shows in the frozen selector.
7902
                $allworkflowstates = $this->get_all_marking_workflow_states();
7903
                $select->addOption($allworkflowstates[$data->workflowstate], $data->workflowstate);
7904
                $mform->freeze('workflowstate');
7905
            }
7906
            $gradingstatus = $this->get_grading_status($userid);
7907
            if ($gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7908
                if ($grade->grade && $grade->grade != -1) {
7909
                    if ($settings->grade > 0) {
7910
                        $assigngradestring = format_float($grade->grade, $this->get_grade_item()->get_decimals());
7911
                    } else {
7912
                        $assigngradestring = make_grades_menu($settings->grade)[grade_floatval($grade->grade)];
7913
                    }
7914
                    $assigngradestring = html_writer::span($assigngradestring, 'currentgrade');
7915
                    $label = get_string('currentassigngrade', 'assign');
7916
                    $mform->addElement('static', 'currentassigngrade', $label, $assigngradestring);
7917
                }
7918
            }
7919
        }
7920
 
7921
        if ($this->get_instance()->markingworkflow &&
7922
            $this->get_instance()->markingallocation &&
7923
            has_capability('mod/assign:manageallocations', $this->context)) {
7924
 
7925
            list($sort, $params) = users_order_by_sql('u');
7926
            // Only enrolled users could be assigned as potential markers.
7927
            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
7928
            $markerlist = array('' =>  get_string('choosemarker', 'assign'));
7929
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
7930
            foreach ($markers as $marker) {
7931
                $markerlist[$marker->id] = fullname($marker, $viewfullnames);
7932
            }
7933
            $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist);
7934
            $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign');
7935
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
7936
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW);
7937
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
7938
            $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
7939
        }
7940
 
7941
        $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
7942
        $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
7943
 
7944
        if (count($useridlist) > 1) {
7945
            $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist));
7946
            $name = get_string('outof', 'assign', $strparams);
7947
            $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
7948
        }
7949
 
7950
        // Let feedback plugins add elements to the grading form.
7951
        $this->add_plugin_grade_elements($grade, $mform, $data, $userid);
7952
 
7953
        // Hidden params.
7954
        $mform->addElement('hidden', 'id', $this->get_course_module()->id);
7955
        $mform->setType('id', PARAM_INT);
7956
        $mform->addElement('hidden', 'rownum', $rownum);
7957
        $mform->setType('rownum', PARAM_INT);
7958
        $mform->setConstant('rownum', $rownum);
7959
        $mform->addElement('hidden', 'useridlistid', $useridlistid);
7960
        $mform->setType('useridlistid', PARAM_ALPHANUM);
7961
        $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
7962
        $mform->setType('attemptnumber', PARAM_INT);
7963
        $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
7964
        $mform->setType('ajax', PARAM_INT);
7965
        $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT));
7966
        $mform->setType('userid', PARAM_INT);
7967
 
7968
        if ($this->get_instance()->teamsubmission) {
7969
            $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
7970
            $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
7971
            $mform->setDefault('applytoall', 1);
7972
        }
7973
 
7974
        // Do not show if we are editing a previous attempt.
7975
        if (($attemptnumber == -1 ||
7976
            ($attemptnumber + 1) == count($this->get_all_submissions($userid))) &&
7977
            $this->get_instance()->attemptreopenmethod != ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
7978
            $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
7979
            $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
7980
            $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
7981
 
7982
            $attemptnumber = 0;
7983
            if ($submission) {
7984
                $attemptnumber = $submission->attemptnumber;
7985
            }
7986
            $maxattempts = $this->get_instance()->maxattempts;
7987
            if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
7988
                $maxattempts = get_string('unlimitedattempts', 'assign');
7989
            }
7990
            $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
7991
            $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
7992
 
7993
            $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
7994
            $issubmission = !empty($submission);
7995
            $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
7996
            $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1));
7997
 
7998
            if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
7999
                $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
8000
                $mform->setDefault('addattempt', 0);
8001
            }
8002
        }
8003
        if (!$gradingpanel) {
8004
            $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
8005
        } else {
8006
            $mform->addElement('hidden', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
8007
            $mform->setType('sendstudentnotifications', PARAM_BOOL);
8008
        }
8009
        // Get assignment visibility information for student.
8010
        $modinfo = get_fast_modinfo($settings->course, $userid);
8011
        $cm = $modinfo->get_cm($this->get_course_module()->id);
8012
 
8013
        // Don't allow notification to be sent if the student can't access the assignment,
8014
        // or until in "Released" state if using marking workflow.
8015
        if (!$cm->uservisible) {
8016
            $mform->setDefault('sendstudentnotifications', 0);
8017
            $mform->freeze('sendstudentnotifications');
8018
        } else if ($this->get_instance()->markingworkflow) {
8019
            $mform->setDefault('sendstudentnotifications', 0);
8020
            if (!$gradingpanel) {
8021
                $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
8022
            }
8023
        } else {
8024
            $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications);
8025
        }
8026
 
8027
        $mform->addElement('hidden', 'action', 'submitgrade');
8028
        $mform->setType('action', PARAM_ALPHA);
8029
 
8030
        if (!$gradingpanel) {
8031
 
8032
            $buttonarray = array();
8033
            $name = get_string('savechanges', 'assign');
8034
            $buttonarray[] = $mform->createElement('submit', 'savegrade', $name);
8035
            if (!$last) {
8036
                $name = get_string('savenext', 'assign');
8037
                $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name);
8038
            }
8039
            $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel'));
8040
            $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
8041
            $mform->closeHeaderBefore('buttonar');
8042
            $buttonarray = array();
8043
 
8044
            if ($rownum > 0) {
8045
                $name = get_string('previous', 'assign');
8046
                $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name);
8047
            }
8048
 
8049
            if (!$last) {
8050
                $name = get_string('nosavebutnext', 'assign');
8051
                $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name);
8052
            }
8053
            if (!empty($buttonarray)) {
8054
                $mform->addGroup($buttonarray, 'navar', '', array(' '), false);
8055
            }
8056
        }
8057
        // The grading form does not work well with shortforms.
8058
        $mform->setDisableShortforms();
8059
    }
8060
 
8061
    /**
8062
     * Add elements in submission plugin form.
8063
     *
8064
     * @param mixed $submission stdClass|null
8065
     * @param MoodleQuickForm $mform
8066
     * @param stdClass $data
8067
     * @param int $userid The current userid (same as $USER->id)
8068
     * @return void
8069
     */
8070
    protected function add_plugin_submission_elements($submission,
8071
                                                    MoodleQuickForm $mform,
8072
                                                    stdClass $data,
8073
                                                    $userid) {
8074
        foreach ($this->submissionplugins as $plugin) {
8075
            if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
8076
                $plugin->get_form_elements_for_user($submission, $mform, $data, $userid);
8077
            }
8078
        }
8079
    }
8080
 
8081
    /**
8082
     * Check if feedback plugins installed are enabled.
8083
     *
8084
     * @return bool
8085
     */
8086
    public function is_any_feedback_plugin_enabled() {
8087
        if (!isset($this->cache['any_feedback_plugin_enabled'])) {
8088
            $this->cache['any_feedback_plugin_enabled'] = false;
8089
            foreach ($this->feedbackplugins as $plugin) {
8090
                if ($plugin->is_enabled() && $plugin->is_visible()) {
8091
                    $this->cache['any_feedback_plugin_enabled'] = true;
8092
                    break;
8093
                }
8094
            }
8095
        }
8096
 
8097
        return $this->cache['any_feedback_plugin_enabled'];
8098
 
8099
    }
8100
 
8101
    /**
8102
     * Check if submission plugins installed are enabled.
8103
     *
8104
     * @return bool
8105
     */
8106
    public function is_any_submission_plugin_enabled() {
8107
        if (!isset($this->cache['any_submission_plugin_enabled'])) {
8108
            $this->cache['any_submission_plugin_enabled'] = false;
8109
            foreach ($this->submissionplugins as $plugin) {
8110
                if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
8111
                    $this->cache['any_submission_plugin_enabled'] = true;
8112
                    break;
8113
                }
8114
            }
8115
        }
8116
 
8117
        return $this->cache['any_submission_plugin_enabled'];
8118
 
8119
    }
8120
 
8121
    /**
8122
     * Add elements to submission form.
8123
     * @param MoodleQuickForm $mform
8124
     * @param stdClass $data
8125
     * @return void
8126
     */
8127
    public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) {
8128
        global $USER;
8129
 
8130
        $userid = $data->userid;
8131
        // Team submissions.
8132
        if ($this->get_instance()->teamsubmission) {
8133
            $submission = $this->get_group_submission($userid, 0, false);
8134
        } else {
8135
            $submission = $this->get_user_submission($userid, false);
8136
        }
8137
 
8138
        // Submission statement.
8139
        $adminconfig = $this->get_admin_config();
8140
        $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
8141
 
8142
        $draftsenabled = $this->get_instance()->submissiondrafts;
8143
        $submissionstatement = '';
8144
 
8145
        if ($requiresubmissionstatement) {
8146
            $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
8147
        }
8148
 
8149
        // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
8150
        // that the submission statement checkbox will be displayed.
8151
        if (empty($submissionstatement)) {
8152
            $requiresubmissionstatement = false;
8153
        }
8154
 
8155
        $mform->addElement('header', 'submission header', get_string('addsubmission', 'mod_assign'));
8156
 
8157
        // Only show submission statement if we are editing our own submission.
8158
        if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
8159
            $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
8160
            $mform->addRule('submissionstatement', get_string('submissionstatementrequired', 'mod_assign'),
8161
                'required', null, 'client');
8162
        }
8163
 
8164
        $this->add_plugin_submission_elements($submission, $mform, $data, $userid);
8165
 
8166
        // Hidden params.
8167
        $mform->addElement('hidden', 'id', $this->get_course_module()->id);
8168
        $mform->setType('id', PARAM_INT);
8169
 
8170
        $mform->addElement('hidden', 'userid', $userid);
8171
        $mform->setType('userid', PARAM_INT);
8172
 
8173
        $mform->addElement('hidden', 'action', 'savesubmission');
8174
        $mform->setType('action', PARAM_ALPHA);
8175
    }
8176
 
8177
    /**
8178
     * Remove any data from the current submission.
8179
     *
8180
     * @param int $userid
8181
     * @return boolean
8182
     * @throws coding_exception
8183
     */
8184
    public function remove_submission($userid) {
8185
        global $USER;
8186
 
8187
        if (!$this->can_edit_submission($userid, $USER->id)) {
8188
            $user = core_user::get_user($userid);
8189
            $message = get_string('usersubmissioncannotberemoved', 'assign', fullname($user));
8190
            $this->set_error_message($message);
8191
            return false;
8192
        }
8193
 
8194
        if ($this->get_instance()->teamsubmission) {
8195
            $submission = $this->get_group_submission($userid, 0, false);
8196
        } else {
8197
            $submission = $this->get_user_submission($userid, false);
8198
        }
8199
 
8200
        if (!$submission) {
8201
            return false;
8202
        }
8203
        $submission->status = $submission->attemptnumber ? ASSIGN_SUBMISSION_STATUS_REOPENED : ASSIGN_SUBMISSION_STATUS_NEW;
8204
        $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8205
 
8206
        // Tell each submission plugin we were saved with no data.
8207
        $plugins = $this->get_submission_plugins();
8208
        foreach ($plugins as $plugin) {
8209
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8210
                $plugin->remove($submission);
8211
            }
8212
        }
8213
 
8214
        $completion = new completion_info($this->get_course());
8215
        if ($completion->is_enabled($this->get_course_module()) &&
8216
                $this->get_instance()->completionsubmit) {
8217
            $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8218
        }
8219
 
8220
        submission_removed::create_from_submission($this, $submission)->trigger();
8221
        submission_status_updated::create_from_submission($this, $submission)->trigger();
8222
        return true;
8223
    }
8224
 
8225
    /**
8226
     * Revert to draft.
8227
     *
8228
     * @param int $userid
8229
     * @return boolean
8230
     */
8231
    public function revert_to_draft($userid) {
8232
        global $DB, $USER;
8233
 
8234
        // Need grade permission.
8235
        require_capability('mod/assign:grade', $this->context);
8236
 
8237
        if ($this->get_instance()->teamsubmission) {
8238
            $submission = $this->get_group_submission($userid, 0, false);
8239
        } else {
8240
            $submission = $this->get_user_submission($userid, false);
8241
        }
8242
 
8243
        if (!$submission) {
8244
            return false;
8245
        }
8246
        $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
8247
        $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8248
 
8249
        // Give each submission plugin a chance to process the reverting to draft.
8250
        $plugins = $this->get_submission_plugins();
8251
        foreach ($plugins as $plugin) {
8252
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8253
                $plugin->revert_to_draft($submission);
8254
            }
8255
        }
8256
        // Update the modified time on the grade (grader modified).
8257
        $grade = $this->get_user_grade($userid, true);
8258
        $grade->grader = $USER->id;
8259
        $this->update_grade($grade);
8260
 
8261
        $completion = new completion_info($this->get_course());
8262
        if ($completion->is_enabled($this->get_course_module()) &&
8263
                $this->get_instance()->completionsubmit) {
8264
            $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8265
        }
8266
        \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
8267
        return true;
8268
    }
8269
 
8270
    /**
8271
     * Remove the current submission.
8272
     *
8273
     * @param int $userid
8274
     * @return boolean
8275
     */
8276
    protected function process_remove_submission($userid = 0) {
8277
        require_sesskey();
8278
 
8279
        if (!$userid) {
8280
            $userid = required_param('userid', PARAM_INT);
8281
        }
8282
 
8283
        return $this->remove_submission($userid);
8284
    }
8285
 
8286
    /**
8287
     * Revert to draft.
8288
     * Uses url parameter userid if userid not supplied as a parameter.
8289
     *
8290
     * @param int $userid
8291
     * @return boolean
8292
     */
8293
    protected function process_revert_to_draft($userid = 0) {
8294
        require_sesskey();
8295
 
8296
        if (!$userid) {
8297
            $userid = required_param('userid', PARAM_INT);
8298
        }
8299
 
8300
        return $this->revert_to_draft($userid);
8301
    }
8302
 
8303
    /**
8304
     * Prevent student updates to this submission
8305
     *
8306
     * @param int $userid
8307
     * @return bool
8308
     */
8309
    public function lock_submission($userid) {
8310
        global $USER, $DB;
8311
        // Need grade permission.
8312
        require_capability('mod/assign:grade', $this->context);
8313
 
8314
        // Give each submission plugin a chance to process the locking.
8315
        $plugins = $this->get_submission_plugins();
8316
        $submission = $this->get_user_submission($userid, false);
8317
 
8318
        $flags = $this->get_user_flags($userid, true);
8319
        $flags->locked = 1;
8320
        $this->update_user_flags($flags);
8321
 
8322
        foreach ($plugins as $plugin) {
8323
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8324
                $plugin->lock($submission, $flags);
8325
            }
8326
        }
8327
 
8328
        $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8329
        \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger();
8330
        return true;
8331
    }
8332
 
8333
 
8334
    /**
8335
     * Set the workflow state for multiple users
8336
     *
8337
     * @return void
8338
     */
8339
    protected function process_set_batch_marking_workflow_state() {
8340
        global $CFG, $DB;
8341
 
8342
        // Include batch marking workflow form.
8343
        require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
8344
 
8345
        $formparams = array(
8346
            'userscount' => 0,  // This form is never re-displayed, so we don't need to
8347
            'usershtml' => '',  // initialise these parameters with real information.
8348
            'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
8349
        );
8350
 
8351
        $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
8352
 
8353
        if ($mform->is_cancelled()) {
8354
            return true;
8355
        }
8356
 
8357
        if ($formdata = $mform->get_data()) {
8358
            $useridlist = explode(',', $formdata->selectedusers);
8359
            $state = $formdata->markingworkflowstate;
8360
 
8361
            foreach ($useridlist as $userid) {
8362
                $flags = $this->get_user_flags($userid, true);
8363
 
8364
                $flags->workflowstate = $state;
8365
 
8366
                // Clear the mailed flag if notification is requested, the student hasn't been
8367
                // notified previously, the student can access the assignment, and the state
8368
                // is "Released".
8369
                $modinfo = get_fast_modinfo($this->course, $userid);
8370
                $cm = $modinfo->get_cm($this->get_course_module()->id);
8371
                if ($formdata->sendstudentnotifications && $cm->uservisible &&
8372
                        $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8373
                    $flags->mailed = 0;
8374
                }
8375
 
8376
                $gradingdisabled = $this->grading_disabled($userid);
8377
 
8378
                // Will not apply update if user does not have permission to assign this workflow state.
8379
                if (!$gradingdisabled && $this->update_user_flags($flags)) {
8380
                    // Update Gradebook.
8381
                    $grade = $this->get_user_grade($userid, true);
8382
                    // Fetch any feedback for this student.
8383
                    $gradebookplugin = $this->get_admin_config()->feedback_plugin_for_gradebook;
8384
                    $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
8385
                    $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
8386
                    if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
8387
                        $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8388
                        $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8389
                        $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8390
                    }
8391
                    $this->update_grade($grade);
8392
                    $assign = clone $this->get_instance();
8393
                    $assign->cmidnumber = $this->get_course_module()->idnumber;
8394
                    // Set assign gradebook feedback plugin status.
8395
                    $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
8396
 
8397
                    // If markinganonymous is enabled then allow to release grades anonymously.
8398
                    if (isset($assign->markinganonymous) && $assign->markinganonymous == 1) {
8399
                        assign_update_grades($assign, $userid);
8400
                    }
8401
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8402
                    \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger();
8403
                }
8404
            }
8405
        }
8406
    }
8407
 
8408
    /**
8409
     * Set the marking allocation for multiple users
8410
     *
8411
     * @return void
8412
     */
8413
    protected function process_set_batch_marking_allocation() {
8414
        global $CFG, $DB;
8415
 
8416
        // Include batch marking allocation form.
8417
        require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
8418
 
8419
        $formparams = array(
8420
            'userscount' => 0,  // This form is never re-displayed, so we don't need to
8421
            'usershtml' => ''   // initialise these parameters with real information.
8422
        );
8423
 
8424
        list($sort, $params) = users_order_by_sql('u');
8425
        // Only enrolled users could be assigned as potential markers.
8426
        $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
8427
        $markerlist = array();
8428
        foreach ($markers as $marker) {
8429
            $markerlist[$marker->id] = fullname($marker);
8430
        }
8431
 
8432
        $formparams['markers'] = $markerlist;
8433
 
8434
        $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
8435
 
8436
        if ($mform->is_cancelled()) {
8437
            return true;
8438
        }
8439
 
8440
        if ($formdata = $mform->get_data()) {
8441
            $useridlist = explode(',', $formdata->selectedusers);
8442
            $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST);
8443
 
8444
            foreach ($useridlist as $userid) {
8445
                $flags = $this->get_user_flags($userid, true);
8446
                if ($flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW ||
8447
                    $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW ||
8448
                    $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE ||
8449
                    $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8450
 
8451
                    continue; // Allocated marker can only be changed in certain workflow states.
8452
                }
8453
 
8454
                $flags->allocatedmarker = $marker->id;
8455
 
8456
                if ($this->update_user_flags($flags)) {
8457
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8458
                    \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger();
8459
                }
8460
            }
8461
        }
8462
    }
8463
 
8464
 
8465
    /**
8466
     * Prevent student updates to this submission.
8467
     * Uses url parameter userid.
8468
     *
8469
     * @param int $userid
8470
     * @return void
8471
     */
8472
    protected function process_lock_submission($userid = 0) {
8473
 
8474
        require_sesskey();
8475
 
8476
        if (!$userid) {
8477
            $userid = required_param('userid', PARAM_INT);
8478
        }
8479
 
8480
        return $this->lock_submission($userid);
8481
    }
8482
 
8483
    /**
8484
     * Unlock the student submission.
8485
     *
8486
     * @param int $userid
8487
     * @return bool
8488
     */
8489
    public function unlock_submission($userid) {
8490
        global $USER, $DB;
8491
 
8492
        // Need grade permission.
8493
        require_capability('mod/assign:grade', $this->context);
8494
 
8495
        // Give each submission plugin a chance to process the unlocking.
8496
        $plugins = $this->get_submission_plugins();
8497
        $submission = $this->get_user_submission($userid, false);
8498
 
8499
        $flags = $this->get_user_flags($userid, true);
8500
        $flags->locked = 0;
8501
        $this->update_user_flags($flags);
8502
 
8503
        foreach ($plugins as $plugin) {
8504
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8505
                $plugin->unlock($submission, $flags);
8506
            }
8507
        }
8508
 
8509
        $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8510
        \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger();
8511
        return true;
8512
    }
8513
 
8514
    /**
8515
     * Unlock the student submission.
8516
     * Uses url parameter userid.
8517
     *
8518
     * @param int $userid
8519
     * @return bool
8520
     */
8521
    protected function process_unlock_submission($userid = 0) {
8522
 
8523
        require_sesskey();
8524
 
8525
        if (!$userid) {
8526
            $userid = required_param('userid', PARAM_INT);
8527
        }
8528
 
8529
        return $this->unlock_submission($userid);
8530
    }
8531
 
8532
    /**
8533
     * Apply a grade from a grading form to a user (may be called multiple times for a group submission).
8534
     *
8535
     * @param stdClass $formdata - the data from the form
8536
     * @param int $userid - the user to apply the grade to
8537
     * @param int $attemptnumber - The attempt number to apply the grade to.
8538
     * @return void
8539
     */
8540
    protected function apply_grade_to_user($formdata, $userid, $attemptnumber) {
8541
        global $USER, $CFG, $DB;
8542
 
8543
        $grade = $this->get_user_grade($userid, true, $attemptnumber);
8544
        $originalgrade = $grade->grade;
8545
        $gradingdisabled = $this->grading_disabled($userid);
8546
        $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
8547
        if (!$gradingdisabled) {
8548
            if ($gradinginstance) {
8549
                $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading,
8550
                                                                       $grade->id);
8551
            } else {
8552
                // Handle the case when grade is set to No Grade.
8553
                if (isset($formdata->grade)) {
8554
                    $grade->grade = grade_floatval(unformat_float($formdata->grade));
8555
                }
8556
            }
8557
            if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) {
8558
                $flags = $this->get_user_flags($userid, true);
8559
                $oldworkflowstate = $flags->workflowstate;
8560
                $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate;
8561
                $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker;
8562
                if ($this->update_user_flags($flags) &&
8563
                        isset($formdata->workflowstate) &&
8564
                        $formdata->workflowstate !== $oldworkflowstate) {
8565
                    $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8566
                    \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger();
8567
                }
8568
            }
8569
        }
8570
        $grade->grader= $USER->id;
8571
 
8572
        $adminconfig = $this->get_admin_config();
8573
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
8574
 
8575
        $feedbackmodified = false;
8576
 
8577
        // Call save in plugins.
8578
        foreach ($this->feedbackplugins as $plugin) {
8579
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8580
                $gradingmodified = $plugin->is_feedback_modified($grade, $formdata);
8581
                if ($gradingmodified) {
8582
                    if (!$plugin->save($grade, $formdata)) {
8583
                        $result = false;
8584
                        throw new \moodle_exception($plugin->get_error());
8585
                    }
8586
                    // If $feedbackmodified is true, keep it true.
8587
                    $feedbackmodified = $feedbackmodified || $gradingmodified;
8588
                }
8589
                if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
8590
                    // This is the feedback plugin chose to push comments to the gradebook.
8591
                    $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8592
                    $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8593
                    $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8594
                }
8595
            }
8596
        }
8597
 
8598
        // We do not want to update the timemodified if no grade was added.
8599
        if (!empty($formdata->addattempt) ||
8600
                ($originalgrade !== null && $originalgrade != -1) ||
8601
                ($grade->grade !== null && $grade->grade != -1) ||
8602
                $feedbackmodified) {
8603
            $this->update_grade($grade, !empty($formdata->addattempt));
8604
        }
8605
 
8606
        // We never send notifications if we have marking workflow and the grade is not released.
8607
        if ($this->get_instance()->markingworkflow &&
8608
                isset($formdata->workflowstate) &&
8609
                $formdata->workflowstate != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8610
            $formdata->sendstudentnotifications = false;
8611
        }
8612
 
8613
        // Note the default if not provided for this option is true (e.g. webservices).
8614
        // This is for backwards compatibility.
8615
        if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) {
8616
            $this->notify_grade_modified($grade, true);
8617
        }
8618
    }
8619
 
8620
 
8621
    /**
8622
     * Save outcomes submitted from grading form.
8623
     *
8624
     * @param int $userid
8625
     * @param stdClass $formdata
8626
     * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant
8627
     *                          for an outcome set to a user but applied to an entire group.
8628
     */
8629
    protected function process_outcomes($userid, $formdata, $sourceuserid = null) {
8630
        global $CFG, $USER;
8631
 
8632
        if (empty($CFG->enableoutcomes)) {
8633
            return;
8634
        }
8635
        if ($this->grading_disabled($userid)) {
8636
            return;
8637
        }
8638
 
8639
        require_once($CFG->libdir.'/gradelib.php');
8640
 
8641
        $data = array();
8642
        $gradinginfo = grade_get_grades($this->get_course()->id,
8643
                                        'mod',
8644
                                        'assign',
8645
                                        $this->get_instance()->id,
8646
                                        $userid);
8647
 
8648
        if (!empty($gradinginfo->outcomes)) {
8649
            foreach ($gradinginfo->outcomes as $index => $oldoutcome) {
8650
                $name = 'outcome_'.$index;
8651
                $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid;
8652
                if (isset($formdata->{$name}[$sourceuserid]) &&
8653
                        $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]) {
8654
                    $data[$index] = $formdata->{$name}[$sourceuserid];
8655
                }
8656
            }
8657
        }
8658
        if (count($data) > 0) {
8659
            grade_update_outcomes('mod/assign',
8660
                                  $this->course->id,
8661
                                  'mod',
8662
                                  'assign',
8663
                                  $this->get_instance()->id,
8664
                                  $userid,
8665
                                  $data);
8666
        }
8667
    }
8668
 
8669
    /**
8670
     * If the requirements are met - reopen the submission for another attempt.
8671
     * Only call this function when grading the latest attempt.
8672
     *
8673
     * @param int $userid The userid.
8674
     * @param stdClass $submission The submission (may be a group submission).
8675
     * @param bool $addattempt - True if the "allow another attempt" checkbox was checked.
8676
     * @return bool - true if another attempt was added.
8677
     */
8678
    protected function reopen_submission_if_required($userid, $submission, $addattempt) {
8679
        $instance = $this->get_instance();
8680
        $maxattemptsreached = !empty($submission) &&
8681
                              $submission->attemptnumber >= ($instance->maxattempts - 1) &&
8682
                              $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
8683
        $shouldreopen = false;
8684
        if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
8685
            // Check the gradetopass from the gradebook.
8686
            $gradeitem = $this->get_grade_item();
8687
            if ($gradeitem) {
8688
                $gradegrade = grade_grade::fetch(array('userid' => $userid, 'itemid' => $gradeitem->id));
8689
 
8690
                // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set.
8691
                if ($gradegrade && ($gradegrade->is_passed() === false)) {
8692
                    $shouldreopen = true;
8693
                }
8694
            }
8695
        }
8696
        if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL &&
8697
                !empty($addattempt)) {
8698
            $shouldreopen = true;
8699
        }
8700
        if ($shouldreopen && !$maxattemptsreached) {
8701
            $this->add_attempt($userid);
8702
            return true;
8703
        }
8704
        return false;
8705
    }
8706
 
8707
    /**
8708
     * Save grade update.
8709
     *
8710
     * @param int $userid
8711
     * @param  stdClass $data
8712
     * @return bool - was the grade saved
8713
     */
8714
    public function save_grade($userid, $data) {
8715
 
8716
        // Need grade permission.
8717
        require_capability('mod/assign:grade', $this->context);
8718
 
8719
        $instance = $this->get_instance();
8720
        $submission = null;
8721
        if ($instance->teamsubmission) {
8722
            // We need to know what the most recent group submission is.
8723
            // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8724
            // and when deciding if we need to update the gradebook with an edited grade.
8725
            $mostrecentsubmission = $this->get_group_submission($userid, 0, false, -1);
8726
            $this->set_most_recent_team_submission($mostrecentsubmission);
8727
            // Get the submission that we are saving grades for. The data attempt number determines which submission attempt.
8728
            $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
8729
        } else {
8730
            $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
8731
        }
8732
        if ($instance->teamsubmission && !empty($data->applytoall)) {
8733
            $groupid = 0;
8734
            if ($this->get_submission_group($userid)) {
8735
                $group = $this->get_submission_group($userid);
8736
                if ($group) {
8737
                    $groupid = $group->id;
8738
                }
8739
            }
8740
            $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
8741
            foreach ($members as $member) {
8742
                // We only want to update the grade for this group submission attempt. The data attempt number could be
8743
                // -1 which may end up in additional attempts being created for each group member instead of just one
8744
                // additional attempt for the group.
8745
                $this->apply_grade_to_user($data, $member->id, $submission->attemptnumber);
8746
                $this->process_outcomes($member->id, $data, $userid);
8747
            }
8748
        } else {
8749
            $this->apply_grade_to_user($data, $userid, $data->attemptnumber);
8750
 
8751
            $this->process_outcomes($userid, $data);
8752
        }
8753
 
8754
        return true;
8755
    }
8756
 
8757
    /**
8758
     * Save grade.
8759
     *
8760
     * @param  moodleform $mform
8761
     * @return bool - was the grade saved
8762
     */
8763
    protected function process_save_grade(&$mform) {
8764
        global $CFG, $SESSION;
8765
        // Include grade form.
8766
        require_once($CFG->dirroot . '/mod/assign/gradeform.php');
8767
 
8768
        require_sesskey();
8769
 
8770
        $instance = $this->get_instance();
8771
        $rownum = required_param('rownum', PARAM_INT);
8772
        $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
8773
        $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
8774
        $userid = optional_param('userid', 0, PARAM_INT);
8775
        if (!$userid) {
8776
            if (empty($SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)])) {
8777
                // If the userid list is not stored we must not save, as it is possible that the user in a
8778
                // given row position may not be the same now as when the grading page was generated.
8779
                $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
8780
                throw new moodle_exception('useridlistnotcached', 'mod_assign', $url);
8781
            }
8782
            $useridlist = $SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)];
8783
        } else {
8784
            $useridlist = array($userid);
8785
            $rownum = 0;
8786
        }
8787
 
8788
        $last = false;
8789
        $userid = $useridlist[$rownum];
8790
        if ($rownum == count($useridlist) - 1) {
8791
            $last = true;
8792
        }
8793
 
8794
        $data = new stdClass();
8795
 
8796
        $gradeformparams = array('rownum' => $rownum,
8797
                                 'useridlistid' => $useridlistid,
8798
                                 'last' => $last,
8799
                                 'attemptnumber' => $attemptnumber,
8800
                                 'userid' => $userid);
8801
        $mform = new mod_assign_grade_form(null,
8802
                                           array($this, $data, $gradeformparams),
8803
                                           'post',
8804
                                           '',
8805
                                           array('class'=>'gradeform'));
8806
 
8807
        if ($formdata = $mform->get_data()) {
8808
            return $this->save_grade($userid, $formdata);
8809
        } else {
8810
            return false;
8811
        }
8812
    }
8813
 
8814
    /**
8815
     * This function is a static wrapper around can_upgrade.
8816
     *
8817
     * @param string $type The plugin type
8818
     * @param int $version The plugin version
8819
     * @return bool
8820
     */
8821
    public static function can_upgrade_assignment($type, $version) {
8822
        $assignment = new assign(null, null, null);
8823
        return $assignment->can_upgrade($type, $version);
8824
    }
8825
 
8826
    /**
8827
     * This function returns true if it can upgrade an assignment from the 2.2 module.
8828
     *
8829
     * @param string $type The plugin type
8830
     * @param int $version The plugin version
8831
     * @return bool
8832
     */
8833
    public function can_upgrade($type, $version) {
8834
        if ($type == 'offline' && $version >= 2011112900) {
8835
            return true;
8836
        }
8837
        foreach ($this->submissionplugins as $plugin) {
8838
            if ($plugin->can_upgrade($type, $version)) {
8839
                return true;
8840
            }
8841
        }
8842
        foreach ($this->feedbackplugins as $plugin) {
8843
            if ($plugin->can_upgrade($type, $version)) {
8844
                return true;
8845
            }
8846
        }
8847
        return false;
8848
    }
8849
 
8850
    /**
8851
     * Copy all the files from the old assignment files area to the new one.
8852
     * This is used by the plugin upgrade code.
8853
     *
8854
     * @param int $oldcontextid The old assignment context id
8855
     * @param int $oldcomponent The old assignment component ('assignment')
8856
     * @param int $oldfilearea The old assignment filearea ('submissions')
8857
     * @param int $olditemid The old submissionid (can be null e.g. intro)
8858
     * @param int $newcontextid The new assignment context id
8859
     * @param int $newcomponent The new assignment component ('assignment')
8860
     * @param int $newfilearea The new assignment filearea ('submissions')
8861
     * @param int $newitemid The new submissionid (can be null e.g. intro)
8862
     * @return int The number of files copied
8863
     */
8864
    public function copy_area_files_for_upgrade($oldcontextid,
8865
                                                $oldcomponent,
8866
                                                $oldfilearea,
8867
                                                $olditemid,
8868
                                                $newcontextid,
8869
                                                $newcomponent,
8870
                                                $newfilearea,
8871
                                                $newitemid) {
8872
        // Note, this code is based on some code in filestorage - but that code
8873
        // deleted the old files (which we don't want).
8874
        $count = 0;
8875
 
8876
        $fs = get_file_storage();
8877
 
8878
        $oldfiles = $fs->get_area_files($oldcontextid,
8879
                                        $oldcomponent,
8880
                                        $oldfilearea,
8881
                                        $olditemid,
8882
                                        'id',
8883
                                        false);
8884
        foreach ($oldfiles as $oldfile) {
8885
            $filerecord = new stdClass();
8886
            $filerecord->contextid = $newcontextid;
8887
            $filerecord->component = $newcomponent;
8888
            $filerecord->filearea = $newfilearea;
8889
            $filerecord->itemid = $newitemid;
8890
            $fs->create_file_from_storedfile($filerecord, $oldfile);
8891
            $count += 1;
8892
        }
8893
 
8894
        return $count;
8895
    }
8896
 
8897
    /**
8898
     * Add a new attempt for each user in the list - but reopen each group assignment
8899
     * at most 1 time.
8900
     *
8901
     * @param array $useridlist Array of userids to reopen.
8902
     * @return bool
8903
     */
8904
    protected function process_add_attempt_group($useridlist) {
8905
        $groupsprocessed = array();
8906
        $result = true;
8907
 
8908
        foreach ($useridlist as $userid) {
8909
            $groupid = 0;
8910
            $group = $this->get_submission_group($userid);
8911
            if ($group) {
8912
                $groupid = $group->id;
8913
            }
8914
 
8915
            if (empty($groupsprocessed[$groupid])) {
8916
                // We need to know what the most recent group submission is.
8917
                // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8918
                // and when deciding if we need to update the gradebook with an edited grade.
8919
                $currentsubmission = $this->get_group_submission($userid, 0, false, -1);
8920
                $this->set_most_recent_team_submission($currentsubmission);
8921
                $result = $this->process_add_attempt($userid) && $result;
8922
                $groupsprocessed[$groupid] = true;
8923
            }
8924
        }
8925
        return $result;
8926
    }
8927
 
8928
    /**
8929
     * Check for a sess key and then call add_attempt.
8930
     *
8931
     * @param int $userid int The user to add the attempt for
8932
     * @return bool - true if successful.
8933
     */
8934
    protected function process_add_attempt($userid) {
8935
        require_sesskey();
8936
 
8937
        return $this->add_attempt($userid);
8938
    }
8939
 
8940
    /**
8941
     * Add a new attempt for a user.
8942
     *
8943
     * @param int $userid int The user to add the attempt for
8944
     * @return bool - true if successful.
8945
     */
8946
    protected function add_attempt($userid) {
8947
        require_capability('mod/assign:grade', $this->context);
8948
 
8949
        if ($this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
8950
            return false;
8951
        }
8952
 
8953
        if ($this->get_instance()->teamsubmission) {
8954
            $oldsubmission = $this->get_group_submission($userid, 0, false);
8955
        } else {
8956
            $oldsubmission = $this->get_user_submission($userid, false);
8957
        }
8958
 
8959
        if (!$oldsubmission) {
8960
            return false;
8961
        }
8962
 
8963
        // No more than max attempts allowed.
8964
        if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
8965
            $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) {
8966
            return false;
8967
        }
8968
 
8969
        // Create the new submission record for the group/user.
8970
        if ($this->get_instance()->teamsubmission) {
8971
            if (isset($this->mostrecentteamsubmission)) {
8972
                // Team submissions can end up in this function for each user (via save_grade). We don't want to create
8973
                // more than one attempt for the whole team.
8974
                if ($this->mostrecentteamsubmission->attemptnumber == $oldsubmission->attemptnumber) {
8975
                    $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
8976
                } else {
8977
                    $newsubmission = $this->get_group_submission($userid, 0, false, $oldsubmission->attemptnumber);
8978
                }
8979
            } else {
8980
                debugging('Please use set_most_recent_team_submission() before calling add_attempt', DEBUG_DEVELOPER);
8981
                $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
8982
            }
8983
        } else {
8984
            $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
8985
        }
8986
 
8987
        // Set the status of the new attempt to reopened.
8988
        $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
8989
 
8990
        // Give each submission plugin a chance to process the add_attempt.
8991
        $plugins = $this->get_submission_plugins();
8992
        foreach ($plugins as $plugin) {
8993
            if ($plugin->is_enabled() && $plugin->is_visible()) {
8994
                $plugin->add_attempt($oldsubmission, $newsubmission);
8995
            }
8996
        }
8997
 
8998
        $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission);
8999
        $flags = $this->get_user_flags($userid, false);
9000
        if (isset($flags->locked) && $flags->locked) { // May not exist.
9001
            $this->process_unlock_submission($userid);
9002
        }
9003
        return true;
9004
    }
9005
 
9006
    /**
9007
     * Get an upto date list of user grades and feedback for the gradebook.
9008
     *
9009
     * @param int $userid int or 0 for all users
9010
     * @return array of grade data formated for the gradebook api
9011
     *         The data required by the gradebook api is userid,
9012
     *                                                   rawgrade,
9013
     *                                                   feedback,
9014
     *                                                   feedbackformat,
9015
     *                                                   usermodified,
9016
     *                                                   dategraded,
9017
     *                                                   datesubmitted
9018
     */
9019
    public function get_user_grades_for_gradebook($userid) {
9020
        global $DB, $CFG;
9021
        $grades = array();
9022
        $assignmentid = $this->get_instance()->id;
9023
 
9024
        $adminconfig = $this->get_admin_config();
9025
        $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook;
9026
        $gradebookplugin = null;
9027
 
9028
        // Find the gradebook plugin.
9029
        foreach ($this->feedbackplugins as $plugin) {
9030
            if ($plugin->is_enabled() && $plugin->is_visible()) {
9031
                if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) {
9032
                    $gradebookplugin = $plugin;
9033
                }
9034
            }
9035
        }
9036
        if ($userid) {
9037
            $where = ' WHERE u.id = :userid ';
9038
        } else {
9039
            $where = ' WHERE u.id != :userid ';
9040
        }
9041
 
9042
        // When the gradebook asks us for grades - only return the last attempt for each user.
9043
        $params = array('assignid1'=>$assignmentid,
9044
                        'assignid2'=>$assignmentid,
9045
                        'userid'=>$userid);
9046
        $graderesults = $DB->get_recordset_sql('SELECT
9047
                                                    u.id as userid,
9048
                                                    s.timemodified as datesubmitted,
9049
                                                    g.grade as rawgrade,
9050
                                                    g.timemodified as dategraded,
9051
                                                    g.grader as usermodified
9052
                                                FROM {user} u
9053
                                                LEFT JOIN {assign_submission} s
9054
                                                    ON u.id = s.userid and s.assignment = :assignid1 AND
9055
                                                    s.latest = 1
9056
                                                JOIN {assign_grades} g
9057
                                                    ON u.id = g.userid and g.assignment = :assignid2 AND
9058
                                                    g.attemptnumber = s.attemptnumber' .
9059
                                                $where, $params);
9060
 
9061
        foreach ($graderesults as $result) {
9062
            $gradingstatus = $this->get_grading_status($result->userid);
9063
            if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
9064
                $gradebookgrade = clone $result;
9065
                // Now get the feedback.
9066
                if ($gradebookplugin) {
9067
                    $grade = $this->get_user_grade($result->userid, false);
9068
                    if ($grade) {
9069
                        $feedbacktext = $gradebookplugin->text_for_gradebook($grade);
9070
                        if (!empty($feedbacktext)) {
9071
                            $gradebookgrade->feedback = $feedbacktext;
9072
                        }
9073
                        $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade);
9074
                        $gradebookgrade->feedbackfiles = $gradebookplugin->files_for_gradebook($grade);
9075
                    }
9076
                }
9077
                $grades[$gradebookgrade->userid] = $gradebookgrade;
9078
            }
9079
        }
9080
 
9081
        $graderesults->close();
9082
        return $grades;
9083
    }
9084
 
9085
    /**
9086
     * Call the static version of this function
9087
     *
9088
     * @param int $userid The userid to lookup
9089
     * @return int The unique id
9090
     */
9091
    public function get_uniqueid_for_user($userid) {
9092
        return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid);
9093
    }
9094
 
9095
    /**
9096
     * Foreach participant in the course - assign them a random id.
9097
     *
9098
     * @param int $assignid The assignid to lookup
9099
     */
9100
    public static function allocate_unique_ids($assignid) {
9101
        global $DB;
9102
 
9103
        $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
9104
        $context = context_module::instance($cm->id);
9105
 
9106
        $currentgroup = groups_get_activity_group($cm, true);
9107
        $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id');
9108
 
9109
        // Shuffle the users.
9110
        shuffle($users);
9111
 
9112
        foreach ($users as $user) {
9113
            $record = $DB->get_record('assign_user_mapping',
9114
                                      array('assignment'=>$assignid, 'userid'=>$user->id),
9115
                                     'id');
9116
            if (!$record) {
9117
                $record = new stdClass();
9118
                $record->assignment = $assignid;
9119
                $record->userid = $user->id;
9120
                $DB->insert_record('assign_user_mapping', $record);
9121
            }
9122
        }
9123
    }
9124
 
9125
    /**
9126
     * Lookup this user id and return the unique id for this assignment.
9127
     *
9128
     * @param int $assignid The assignment id
9129
     * @param int $userid The userid to lookup
9130
     * @return int The unique id
9131
     */
9132
    public static function get_uniqueid_for_user_static($assignid, $userid) {
9133
        global $DB;
9134
 
9135
        // Search for a record.
9136
        $params = array('assignment'=>$assignid, 'userid'=>$userid);
9137
        if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9138
            return $record->id;
9139
        }
9140
 
9141
        // Be a little smart about this - there is no record for the current user.
9142
        // We should ensure any unallocated ids for the current participant
9143
        // list are distrubited randomly.
9144
        self::allocate_unique_ids($assignid);
9145
 
9146
        // Retry the search for a record.
9147
        if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9148
            return $record->id;
9149
        }
9150
 
9151
        // The requested user must not be a participant. Add a record anyway.
9152
        $record = new stdClass();
9153
        $record->assignment = $assignid;
9154
        $record->userid = $userid;
9155
 
9156
        return $DB->insert_record('assign_user_mapping', $record);
9157
    }
9158
 
9159
    /**
9160
     * Call the static version of this function.
9161
     *
9162
     * @param int $uniqueid The uniqueid to lookup
9163
     * @return int The user id or false if they don't exist
9164
     */
9165
    public function get_user_id_for_uniqueid($uniqueid) {
9166
        return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid);
9167
    }
9168
 
9169
    /**
9170
     * Lookup this unique id and return the user id for this assignment.
9171
     *
9172
     * @param int $assignid The id of the assignment this user mapping is in
9173
     * @param int $uniqueid The uniqueid to lookup
9174
     * @return int The user id or false if they don't exist
9175
     */
9176
    public static function get_user_id_for_uniqueid_static($assignid, $uniqueid) {
9177
        global $DB;
9178
 
9179
        // Search for a record.
9180
        if ($record = $DB->get_record('assign_user_mapping',
9181
                                      array('assignment'=>$assignid, 'id'=>$uniqueid),
9182
                                      'userid',
9183
                                      IGNORE_MISSING)) {
9184
            return $record->userid;
9185
        }
9186
 
9187
        return false;
9188
    }
9189
 
9190
    /**
9191
     * Get the list of marking_workflow states the current user has permission to transition a grade to.
9192
     *
9193
     * @return array of state => description
9194
     */
9195
    public function get_marking_workflow_states_for_current_user() {
9196
        if (!empty($this->markingworkflowstates)) {
9197
            return $this->markingworkflowstates;
9198
        }
9199
        $states = array();
9200
        if (has_capability('mod/assign:grade', $this->context)) {
9201
            $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign');
9202
            $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign');
9203
        }
9204
        if (has_any_capability(array('mod/assign:reviewgrades',
9205
                                     'mod/assign:managegrades'), $this->context)) {
9206
            $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign');
9207
            $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign');
9208
        }
9209
        if (has_any_capability(array('mod/assign:releasegrades',
9210
                                     'mod/assign:managegrades'), $this->context)) {
9211
            $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign');
9212
        }
9213
        $this->markingworkflowstates = $states;
9214
        return $this->markingworkflowstates;
9215
    }
9216
 
9217
    /**
9218
     * Get the list of marking_workflow states.
9219
     *
9220
     * @return array Array of multiple state => description.
9221
     */
9222
    public function get_all_marking_workflow_states(): array {
9223
        if (!empty($this->allmarkingworkflowstates)) {
9224
            return $this->allmarkingworkflowstates;
9225
        }
9226
 
9227
        $this->allmarkingworkflowstates = [
9228
            ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED => get_string('markingworkflowstatenotmarked', 'assign'),
9229
            ASSIGN_MARKING_WORKFLOW_STATE_INMARKING => get_string('markingworkflowstateinmarking', 'assign'),
9230
            ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW => get_string('markingworkflowstatereadyforreview', 'assign'),
9231
            ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW => get_string('markingworkflowstateinreview', 'assign'),
9232
            ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE => get_string('markingworkflowstatereadyforrelease', 'assign'),
9233
            ASSIGN_MARKING_WORKFLOW_STATE_RELEASED => get_string('markingworkflowstatereleased', 'assign'),
9234
        ];
9235
 
9236
        return $this->allmarkingworkflowstates;
9237
    }
9238
 
9239
    /**
9240
     * Check is only active users in course should be shown.
9241
     *
9242
     * @return bool true if only active users should be shown.
9243
     */
9244
    public function show_only_active_users() {
9245
        global $CFG;
9246
 
9247
        if (is_null($this->showonlyactiveenrol)) {
9248
            $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
9249
            $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
9250
 
9251
            if (!is_null($this->context)) {
9252
                $this->showonlyactiveenrol = $this->showonlyactiveenrol ||
9253
                            !has_capability('moodle/course:viewsuspendedusers', $this->context);
9254
            }
9255
        }
9256
        return $this->showonlyactiveenrol;
9257
    }
9258
 
9259
    /**
9260
     * Return true is user is active user in course else false
9261
     *
9262
     * @param int $userid
9263
     * @return bool true is user is active in course.
9264
     */
9265
    public function is_active_user($userid) {
9266
        return !in_array($userid, get_suspended_userids($this->context, true));
9267
    }
9268
 
9269
    /**
9270
     * Returns true if gradebook feedback plugin is enabled
9271
     *
9272
     * @return bool true if gradebook feedback plugin is enabled and visible else false.
9273
     */
9274
    public function is_gradebook_feedback_enabled() {
9275
        // Get default grade book feedback plugin.
9276
        $adminconfig = $this->get_admin_config();
9277
        $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
9278
        $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
9279
 
9280
        // Check if default gradebook feedback is visible and enabled.
9281
        $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin);
9282
 
9283
        if (empty($gradebookfeedbackplugin)) {
9284
            return false;
9285
        }
9286
 
9287
        if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) {
9288
            return true;
9289
        }
9290
 
9291
        // Gradebook feedback plugin is either not visible/enabled.
9292
        return false;
9293
    }
9294
 
9295
    /**
9296
     * Returns the grading status.
9297
     *
9298
     * @param int $userid the user id
9299
     * @return string returns the grading status
9300
     */
9301
    public function get_grading_status($userid) {
9302
        if ($this->get_instance()->markingworkflow) {
9303
            $flags = $this->get_user_flags($userid, false);
9304
            if (!empty($flags->workflowstate)) {
9305
                return $flags->workflowstate;
9306
            }
9307
            return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
9308
        } else {
9309
            $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
9310
            $grade = $this->get_user_grade($userid, false, $attemptnumber);
9311
 
9312
            if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) {
9313
                return ASSIGN_GRADING_STATUS_GRADED;
9314
            } else {
9315
                return ASSIGN_GRADING_STATUS_NOT_GRADED;
9316
            }
9317
        }
9318
    }
9319
 
9320
    /**
9321
     * The id used to uniquily identify the cache for this instance of the assign object.
9322
     *
9323
     * @return string
9324
     */
9325
    public function get_useridlist_key_id() {
9326
        return $this->useridlistid;
9327
    }
9328
 
9329
    /**
9330
     * Generates the key that should be used for an entry in the useridlist cache.
9331
     *
9332
     * @param string $id Generate a key for this instance (optional)
9333
     * @return string The key for the id, or new entry if no $id is passed.
9334
     */
9335
    public function get_useridlist_key($id = null) {
9336
        global $SESSION;
9337
 
9338
        // Ensure the user id list cache is initialised.
9339
        if (!isset($SESSION->mod_assign_useridlist)) {
9340
            $SESSION->mod_assign_useridlist = [];
9341
        }
9342
 
9343
        if ($id === null) {
9344
            $id = $this->get_useridlist_key_id();
9345
        }
9346
        return $this->get_course_module()->id . '_' . $id;
9347
    }
9348
 
9349
    /**
9350
     * Updates and creates the completion records in mdl_course_modules_completion.
9351
     *
9352
     * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
9353
     * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
9354
     * @param obj $submission the submission
9355
     * @param int $userid the user id
9356
     * @param int $complete
9357
     * @param obj $completion
9358
     *
9359
     * @return null
9360
     */
9361
    protected function update_activity_completion_records($teamsubmission,
9362
                                                          $requireallteammemberssubmit,
9363
                                                          $submission,
9364
                                                          $userid,
9365
                                                          $complete,
9366
                                                          $completion) {
9367
 
9368
        if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
9369
            ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
9370
             $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
9371
 
9372
            $members = groups_get_members($submission->groupid);
9373
 
9374
            foreach ($members as $member) {
9375
                $completion->update_state($this->get_course_module(), $complete, $member->id);
9376
            }
9377
        } else {
9378
            $completion->update_state($this->get_course_module(), $complete, $userid);
9379
        }
9380
 
9381
        return;
9382
    }
9383
 
9384
    /**
9385
     * Update the module completion status (set it viewed) and trigger module viewed event.
9386
     *
9387
     * @since Moodle 3.2
9388
     */
9389
    public function set_module_viewed() {
9390
        $completion = new completion_info($this->get_course());
9391
        $completion->set_module_viewed($this->get_course_module());
9392
 
9393
        // Trigger the course module viewed event.
9394
        $assigninstance = $this->get_instance();
9395
        $params = [
9396
            'objectid' => $assigninstance->id,
9397
            'context' => $this->get_context()
9398
        ];
9399
        if ($this->is_blind_marking()) {
9400
            $params['anonymous'] = 1;
9401
        }
9402
 
9403
        $event = \mod_assign\event\course_module_viewed::create($params);
9404
 
9405
        $event->add_record_snapshot('assign', $assigninstance);
9406
        $event->trigger();
9407
    }
9408
 
9409
    /**
9410
     * Checks for any grade notices, and adds notifications. Will display on assignment main page and grading table.
9411
     *
9412
     * @return void The notifications API will render the notifications at the appropriate part of the page.
9413
     */
9414
    protected function add_grade_notices() {
9415
        if (has_capability('mod/assign:grade', $this->get_context()) && get_config('assign', 'has_rescaled_null_grades_' . $this->get_instance()->id)) {
9416
            $link = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades'));
9417
            \core\notification::warning(get_string('fixrescalednullgrades', 'mod_assign', ['link' => $link->out()]));
9418
        }
9419
    }
9420
 
9421
    /**
9422
     * View fix rescaled null grades.
9423
     *
9424
     * @return bool True if null all grades are now fixed.
9425
     */
9426
    protected function fix_null_grades() {
9427
        global $DB;
9428
        $result = $DB->set_field_select(
9429
            'assign_grades',
9430
            'grade',
9431
            ASSIGN_GRADE_NOT_SET,
9432
            'grade <> ? AND grade < 0',
9433
            [ASSIGN_GRADE_NOT_SET]
9434
        );
9435
        $assign = clone $this->get_instance();
9436
        $assign->cmidnumber = $this->get_course_module()->idnumber;
9437
        assign_update_grades($assign);
9438
        return $result;
9439
    }
9440
 
9441
    /**
9442
     * View fix rescaled null grades.
9443
     *
9444
     * @return void The notifications API will render the notifications at the appropriate part of the page.
9445
     */
9446
    protected function view_fix_rescaled_null_grades() {
9447
        global $OUTPUT;
9448
 
9449
        $o = '';
9450
 
9451
        require_capability('mod/assign:grade', $this->get_context());
9452
 
9453
        $instance = $this->get_instance();
9454
 
9455
        $o .= $this->get_renderer()->render(
9456
            new assign_header(
9457
                $instance,
9458
                $this->get_context(),
9459
                $this->show_intro(),
9460
                $this->get_course_module()->id
9461
            )
9462
        );
9463
 
9464
        $confirm = optional_param('confirm', 0, PARAM_BOOL);
9465
 
9466
        if ($confirm) {
9467
            if (confirm_sesskey()) {
9468
                // Fix the grades.
9469
                $this->fix_null_grades();
9470
                unset_config('has_rescaled_null_grades_' . $instance->id, 'assign');
9471
                // Display the success notice.
9472
                $o .= $this->get_renderer()->notification(get_string('fixrescalednullgradesdone', 'assign'), 'notifysuccess');
9473
            } else {
9474
                // If the sesskey is not valid, then display the error notice.
9475
                $o .= $this->get_renderer()->notification(get_string('invalidsesskey', 'error'), 'notifyerror');
9476
            }
9477
            $url = new moodle_url(
9478
                url: '/mod/assign/view.php',
9479
                params: [
9480
                    'id' => $this->get_course_module()->id,
9481
                    'action' => 'grading',
9482
                ],
9483
            );
9484
            $o .= $this->get_renderer()->continue_button($url);
9485
        } else {
9486
            // Ask for confirmation.
9487
            $continue = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades', 'confirm' => true, 'sesskey' => sesskey()));
9488
            $cancel = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
9489
            $o .= $OUTPUT->confirm(get_string('fixrescalednullgradesconfirm', 'mod_assign'), $continue, $cancel);
9490
        }
9491
 
9492
        $o .= $this->view_footer();
9493
 
9494
        return $o;
9495
    }
9496
 
9497
    /**
9498
     * Set the most recent submission for the team.
9499
     * The most recent team submission is used to determine if another attempt should be created when allowing another
9500
     * attempt on a group assignment, and whether the gradebook should be updated.
9501
     *
9502
     * @since Moodle 3.4
9503
     * @param stdClass $submission The most recent submission of the group.
9504
     */
9505
    public function set_most_recent_team_submission($submission) {
9506
        $this->mostrecentteamsubmission = $submission;
9507
    }
9508
 
9509
    /**
9510
     * Return array of valid grading allocation filters for the grading interface.
9511
     *
9512
     * @param boolean $export Export the list of filters for a template.
9513
     * @return array
9514
     */
9515
    public function get_marking_allocation_filters($export = false) {
9516
        $markingallocation = $this->get_instance()->markingworkflow &&
9517
            $this->get_instance()->markingallocation &&
9518
            has_capability('mod/assign:manageallocations', $this->context);
9519
        // Get markers to use in drop lists.
9520
        $markingallocationoptions = array();
9521
        if ($markingallocation) {
9522
            list($sort, $params) = users_order_by_sql('u');
9523
            // Only enrolled users could be assigned as potential markers.
9524
            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
9525
            $markingallocationoptions[''] = get_string('filternone', 'assign');
9526
            $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
9527
            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
9528
            foreach ($markers as $marker) {
9529
                $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
9530
            }
9531
        }
9532
        if ($export) {
9533
            $allocationfilter = get_user_preferences('assign_markerfilter', '');
9534
            $result = [];
9535
            foreach ($markingallocationoptions as $option => $label) {
9536
                array_push($result, [
9537
                    'key' => $option,
9538
                    'name' => $label,
9539
                    'active' => ($allocationfilter == $option),
9540
                ]);
9541
            }
9542
            return $result;
9543
        }
9544
        return $markingworkflowoptions;
9545
    }
9546
 
9547
    /**
9548
     * Return array of valid grading workflow filters for the grading interface.
9549
     *
9550
     * @param boolean $export Export the list of filters for a template.
9551
     * @return array
9552
     */
9553
    public function get_marking_workflow_filters($export = false) {
9554
        $markingworkflow = $this->get_instance()->markingworkflow;
9555
        // Get marking states to show in form.
9556
        $markingworkflowoptions = array();
9557
        if ($markingworkflow) {
9558
            $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
9559
            $markingworkflowoptions[''] = get_string('filternone', 'assign');
9560
            $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
9561
            $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
9562
        }
9563
        if ($export) {
9564
            $workflowfilter = get_user_preferences('assign_workflowfilter', '');
9565
            $result = [];
9566
            foreach ($markingworkflowoptions as $option => $label) {
9567
                array_push($result, [
9568
                    'key' => $option,
9569
                    'name' => $label,
9570
                    'active' => ($workflowfilter == $option),
9571
                ]);
9572
            }
9573
            return $result;
9574
        }
9575
        return $markingworkflowoptions;
9576
    }
9577
 
9578
    /**
9579
     * Return array of valid search filters for the grading interface.
9580
     *
9581
     * @return array
9582
     */
9583
    public function get_filters() {
9584
        $filterkeys = [
9585
            ASSIGN_FILTER_NOT_SUBMITTED,
9586
            ASSIGN_FILTER_DRAFT,
9587
            ASSIGN_FILTER_SUBMITTED,
9588
            ASSIGN_FILTER_REQUIRE_GRADING,
9589
            ASSIGN_FILTER_GRANTED_EXTENSION
9590
        ];
9591
 
9592
        $current = get_user_preferences('assign_filter', '');
9593
 
9594
        $filters = [];
9595
        // First is always "no filter" option.
9596
        array_push($filters, [
9597
            'key' => 'none',
9598
            'name' => get_string('filternone', 'assign'),
9599
            'active' => ($current == '')
9600
        ]);
9601
 
9602
        foreach ($filterkeys as $key) {
9603
            array_push($filters, [
9604
                'key' => $key,
9605
                'name' => get_string('filter' . $key, 'assign'),
9606
                'active' => ($current == $key)
9607
            ]);
9608
        }
9609
        return $filters;
9610
    }
9611
 
9612
    /**
9613
     * Get the correct submission statement depending on single submisison, team submission or team submission
9614
     * where all team memebers must submit.
9615
     *
9616
     * @param stdClass $adminconfig
9617
     * @param stdClass $instance
9618
     * @param context $context
9619
     *
9620
     * @return string
9621
     */
9622
    protected function get_submissionstatement($adminconfig, $instance, $context) {
9623
        $submissionstatement = '';
9624
 
9625
        if (!($context instanceof context)) {
9626
            return $submissionstatement;
9627
        }
9628
 
9629
        // Single submission.
9630
        if (!$instance->teamsubmission) {
9631
            // Single submission statement is not empty.
9632
            if (!empty($adminconfig->submissionstatement)) {
9633
                // Format the submission statement before its sent. We turn off para because this is going within
9634
                // a form element.
9635
                $options = array(
9636
                    'context' => $context,
9637
                    'para'    => false
9638
                );
9639
                $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
9640
            }
9641
        } else { // Team submission.
9642
            // One user can submit for the whole team.
9643
            if (!empty($adminconfig->submissionstatementteamsubmission) && !$instance->requireallteammemberssubmit) {
9644
                // Format the submission statement before its sent. We turn off para because this is going within
9645
                // a form element.
9646
                $options = array(
9647
                    'context' => $context,
9648
                    'para'    => false
9649
                );
9650
                $submissionstatement = format_text($adminconfig->submissionstatementteamsubmission,
9651
                    FORMAT_MOODLE, $options);
9652
            } else if (!empty($adminconfig->submissionstatementteamsubmissionallsubmit) &&
9653
                $instance->requireallteammemberssubmit) {
9654
                // All team members must submit.
9655
                // Format the submission statement before its sent. We turn off para because this is going within
9656
                // a form element.
9657
                $options = array(
9658
                    'context' => $context,
9659
                    'para'    => false
9660
                );
9661
                $submissionstatement = format_text($adminconfig->submissionstatementteamsubmissionallsubmit,
9662
                    FORMAT_MOODLE, $options);
9663
            }
9664
        }
9665
 
9666
        return $submissionstatement;
9667
    }
9668
 
9669
    /**
9670
     * Check if time limit for assignment enabled and set up.
9671
     *
9672
     * @param int|null $userid User ID. If null, use global user.
9673
     * @return bool
9674
     */
9675
    public function is_time_limit_enabled(?int $userid = null): bool {
9676
        $instance = $this->get_instance($userid);
9677
        return get_config('assign', 'enabletimelimit') && !empty($instance->timelimit);
9678
    }
9679
 
9680
    /**
9681
     * Check if an assignment submission is already started and not yet submitted.
9682
     *
9683
     * @param int|null $userid User ID. If null, use global user.
9684
     * @param int $groupid Group ID. If 0, use user id to determine group.
9685
     * @param int $attemptnumber Attempt number. If -1, check latest submission.
9686
     * @return bool
9687
     */
9688
    public function is_attempt_in_progress(?int $userid = null, int $groupid = 0, int $attemptnumber = -1): bool {
9689
        if ($this->get_instance($userid)->teamsubmission) {
9690
            $submission = $this->get_group_submission($userid, $groupid, false, $attemptnumber);
9691
        } else {
9692
            $submission = $this->get_user_submission($userid, false, $attemptnumber);
9693
        }
9694
 
9695
        // If time limit is enabled, we only assume it is in progress if there is a start time for submission.
9696
        $timedattemptstarted = true;
9697
        if ($this->is_time_limit_enabled($userid)) {
9698
            $timedattemptstarted = !empty($submission) && !empty($submission->timestarted);
9699
        }
9700
 
9701
        return !empty($submission) && $submission->status !== ASSIGN_SUBMISSION_STATUS_SUBMITTED && $timedattemptstarted;
9702
    }
9703
}
9704
 
9705
/**
9706
 * Portfolio caller class for mod_assign.
9707
 *
9708
 * @package   mod_assign
9709
 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
9710
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9711
 */
9712
class assign_portfolio_caller extends portfolio_module_caller_base {
9713
 
9714
    /** @var int callback arg - the id of submission we export */
9715
    protected $sid;
9716
 
9717
    /** @var string component of the submission files we export*/
9718
    protected $component;
9719
 
9720
    /** @var string callback arg - the area of submission files we export */
9721
    protected $area;
9722
 
9723
    /** @var int callback arg - the id of file we export */
9724
    protected $fileid;
9725
 
9726
    /** @var int callback arg - the cmid of the assignment we export */
9727
    protected $cmid;
9728
 
9729
    /** @var string callback arg - the plugintype of the editor we export */
9730
    protected $plugin;
9731
 
9732
    /** @var string callback arg - the name of the editor field we export */
9733
    protected $editor;
9734
 
9735
    /**
9736
     * Callback arg for a single file export.
9737
     */
9738
    public static function expected_callbackargs() {
9739
        return array(
9740
            'cmid' => true,
9741
            'sid' => false,
9742
            'area' => false,
9743
            'component' => false,
9744
            'fileid' => false,
9745
            'plugin' => false,
9746
            'editor' => false,
9747
        );
9748
    }
9749
 
9750
    /**
9751
     * The constructor.
9752
     *
9753
     * @param array $callbackargs
9754
     */
9755
    public function __construct($callbackargs) {
9756
        parent::__construct($callbackargs);
9757
        $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST);
9758
    }
9759
 
9760
    /**
9761
     * Load data needed for the portfolio export.
9762
     *
9763
     * If the assignment type implements portfolio_load_data(), the processing is delegated
9764
     * to it. Otherwise, the caller must provide either fileid (to export single file) or
9765
     * submissionid and filearea (to export all data attached to the given submission file area)
9766
     * via callback arguments.
9767
     *
9768
     * @throws     portfolio_caller_exception
9769
     */
9770
    public function load_data() {
9771
        global $DB;
9772
 
9773
        $context = context_module::instance($this->cmid);
9774
 
9775
        if (empty($this->fileid)) {
9776
            if (empty($this->sid) || empty($this->area)) {
9777
                throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign');
9778
            }
9779
 
9780
            $submission = $DB->get_record('assign_submission', array('id' => $this->sid));
9781
        } else {
9782
            $submissionid = $DB->get_field('files', 'itemid', array('id' => $this->fileid, 'contextid' => $context->id));
9783
            if ($submissionid) {
9784
                $submission = $DB->get_record('assign_submission', array('id' => $submissionid));
9785
            }
9786
        }
9787
 
9788
        if (empty($submission)) {
9789
            throw new portfolio_caller_exception('filenotfound');
9790
        } else if ($submission->userid == 0) {
9791
            // This must be a group submission.
9792
            if (!groups_is_member($submission->groupid, $this->user->id)) {
9793
                throw new portfolio_caller_exception('filenotfound');
9794
            }
9795
        } else if ($this->user->id != $submission->userid) {
9796
            throw new portfolio_caller_exception('filenotfound');
9797
        }
9798
 
9799
        // Export either an area of files or a single file (see function for more detail).
9800
        // The first arg is an id or null. If it is an id, the rest of the args are ignored.
9801
        // If it is null, the rest of the args are used to load a list of files from get_areafiles.
9802
        $this->set_file_and_format_data($this->fileid,
9803
                                        $context->id,
9804
                                        $this->component,
9805
                                        $this->area,
9806
                                        $this->sid,
9807
                                        'timemodified',
9808
                                        false);
9809
 
9810
    }
9811
 
9812
    /**
9813
     * Prepares the package up before control is passed to the portfolio plugin.
9814
     *
9815
     * @throws portfolio_caller_exception
9816
     * @return mixed
9817
     */
9818
    public function prepare_package() {
9819
 
9820
        if ($this->plugin && $this->editor) {
9821
            $options = portfolio_format_text_options();
9822
            $context = context_module::instance($this->cmid);
9823
            $options->context = $context;
9824
 
9825
            $plugin = $this->get_submission_plugin();
9826
 
9827
            $text = $plugin->get_editor_text($this->editor, $this->sid);
9828
            $format = $plugin->get_editor_format($this->editor, $this->sid);
9829
 
9830
            $html = format_text($text, $format, $options);
9831
            $html = portfolio_rewrite_pluginfile_urls($html,
9832
                                                      $context->id,
9833
                                                      'mod_assign',
9834
                                                      $this->area,
9835
                                                      $this->sid,
9836
                                                      $this->exporter->get('format'));
9837
 
9838
            $exporterclass = $this->exporter->get('formatclass');
9839
            if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) {
9840
                if ($files = $this->exporter->get('caller')->get('multifiles')) {
9841
                    foreach ($files as $file) {
9842
                        $this->exporter->copy_existing_file($file);
9843
                    }
9844
                }
9845
                return $this->exporter->write_new_file($html, 'assignment.html', !empty($files));
9846
            } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
9847
                $leapwriter = $this->exporter->get('format')->leap2a_writer();
9848
                $entry = new portfolio_format_leap2a_entry($this->area . $this->cmid,
9849
                                                           $context->get_context_name(),
9850
                                                           'resource',
9851
                                                           $html);
9852
 
9853
                $entry->add_category('web', 'resource_type');
9854
                $entry->author = $this->user;
9855
                $leapwriter->add_entry($entry);
9856
                if ($files = $this->exporter->get('caller')->get('multifiles')) {
9857
                    $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file');
9858
                    foreach ($files as $file) {
9859
                        $this->exporter->copy_existing_file($file);
9860
                    }
9861
                }
9862
                return $this->exporter->write_new_file($leapwriter->to_xml(),
9863
                                                       $this->exporter->get('format')->manifest_name(),
9864
                                                       true);
9865
            } else {
9866
                debugging('invalid format class: ' . $this->exporter->get('formatclass'));
9867
            }
9868
 
9869
        }
9870
 
9871
        if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
9872
            $leapwriter = $this->exporter->get('format')->leap2a_writer();
9873
            $files = array();
9874
            if ($this->singlefile) {
9875
                $files[] = $this->singlefile;
9876
            } else if ($this->multifiles) {
9877
                $files = $this->multifiles;
9878
            } else {
9879
                throw new portfolio_caller_exception('invalidpreparepackagefile',
9880
                                                     'portfolio',
9881
                                                     $this->get_return_url());
9882
            }
9883
 
9884
            $entryids = array();
9885
            foreach ($files as $file) {
9886
                $entry = new portfolio_format_leap2a_file($file->get_filename(), $file);
9887
                $entry->author = $this->user;
9888
                $leapwriter->add_entry($entry);
9889
                $this->exporter->copy_existing_file($file);
9890
                $entryids[] = $entry->id;
9891
            }
9892
            if (count($files) > 1) {
9893
                $baseid = 'assign' . $this->cmid . $this->area;
9894
                $context = context_module::instance($this->cmid);
9895
 
9896
                // If we have multiple files, they should be grouped together into a folder.
9897
                $entry = new portfolio_format_leap2a_entry($baseid . 'group',
9898
                                                           $context->get_context_name(),
9899
                                                           'selection');
9900
                $leapwriter->add_entry($entry);
9901
                $leapwriter->make_selection($entry, $entryids, 'Folder');
9902
            }
9903
            return $this->exporter->write_new_file($leapwriter->to_xml(),
9904
                                                   $this->exporter->get('format')->manifest_name(),
9905
                                                   true);
9906
        }
9907
        return $this->prepare_package_file();
9908
    }
9909
 
9910
    /**
9911
     * Fetch the plugin by its type.
9912
     *
9913
     * @return assign_submission_plugin
9914
     */
9915
    protected function get_submission_plugin() {
9916
        global $CFG;
9917
        if (!$this->plugin || !$this->cmid) {
9918
            return null;
9919
        }
9920
 
9921
        require_once($CFG->dirroot . '/mod/assign/locallib.php');
9922
 
9923
        $context = context_module::instance($this->cmid);
9924
 
9925
        $assignment = new assign($context, null, null);
9926
        return $assignment->get_submission_plugin_by_type($this->plugin);
9927
    }
9928
 
9929
    /**
9930
     * Calculate a sha1 has of either a single file or a list
9931
     * of files based on the data set by load_data.
9932
     *
9933
     * @return string
9934
     */
9935
    public function get_sha1() {
9936
 
9937
        if ($this->plugin && $this->editor) {
9938
            $plugin = $this->get_submission_plugin();
9939
            $options = portfolio_format_text_options();
9940
            $options->context = context_module::instance($this->cmid);
9941
 
9942
            $text = format_text($plugin->get_editor_text($this->editor, $this->sid),
9943
                                $plugin->get_editor_format($this->editor, $this->sid),
9944
                                $options);
9945
            $textsha1 = sha1($text);
9946
            $filesha1 = '';
9947
            try {
9948
                $filesha1 = $this->get_sha1_file();
9949
            } catch (portfolio_caller_exception $e) {
9950
                // No files.
9951
            }
9952
            return sha1($textsha1 . $filesha1);
9953
        }
9954
        return $this->get_sha1_file();
9955
    }
9956
 
9957
    /**
9958
     * Calculate the time to transfer either a single file or a list
9959
     * of files based on the data set by load_data.
9960
     *
9961
     * @return int
9962
     */
9963
    public function expected_time() {
9964
        return $this->expected_time_file();
9965
    }
9966
 
9967
    /**
9968
     * Checking the permissions.
9969
     *
9970
     * @return bool
9971
     */
9972
    public function check_permissions() {
9973
        $context = context_module::instance($this->cmid);
9974
        return has_capability('mod/assign:exportownsubmission', $context);
9975
    }
9976
 
9977
    /**
9978
     * Display a module name.
9979
     *
9980
     * @return string
9981
     */
9982
    public static function display_name() {
9983
        return get_string('modulename', 'assign');
9984
    }
9985
 
9986
    /**
9987
     * Return array of formats supported by this portfolio call back.
9988
     *
9989
     * @return array
9990
     */
9991
    public static function base_supported_formats() {
9992
        return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);
9993
    }
9994
}
9995
 
9996
/**
9997
 * Logic to happen when a/some group(s) has/have been deleted in a course.
9998
 *
9999
 * @param int $courseid The course ID.
10000
 * @param int $groupid The group id if it is known
10001
 * @return void
10002
 */
10003
function assign_process_group_deleted_in_course($courseid, $groupid = null) {
10004
    global $DB;
10005
 
10006
    $params = array('courseid' => $courseid);
10007
    if ($groupid) {
10008
        $params['groupid'] = $groupid;
10009
        // We just update the group that was deleted.
10010
        $sql = "SELECT o.id, o.assignid, o.groupid
10011
                  FROM {assign_overrides} o
10012
                  JOIN {assign} assign ON assign.id = o.assignid
10013
                 WHERE assign.course = :courseid
10014
                   AND o.groupid = :groupid";
10015
    } else {
10016
        // No groupid, we update all orphaned group overrides for all assign in course.
10017
        $sql = "SELECT o.id, o.assignid, o.groupid
10018
                  FROM {assign_overrides} o
10019
                  JOIN {assign} assign ON assign.id = o.assignid
10020
             LEFT JOIN {groups} grp ON grp.id = o.groupid
10021
                 WHERE assign.course = :courseid
10022
                   AND o.groupid IS NOT NULL
10023
                   AND grp.id IS NULL";
10024
    }
10025
    $records = $DB->get_records_sql($sql, $params);
10026
    if (!$records) {
10027
        return; // Nothing to do.
10028
    }
10029
    $DB->delete_records_list('assign_overrides', 'id', array_keys($records));
10030
    $cache = cache::make('mod_assign', 'overrides');
10031
    foreach ($records as $record) {
10032
        $cache->delete("{$record->assignid}_g_{$record->groupid}");
10033
    }
10034
}
10035
 
10036
/**
10037
 * Change the sort order of an override
10038
 *
10039
 * @param int $id of the override
10040
 * @param string $move direction of move
10041
 * @param int $assignid of the assignment
10042
 * @return bool success of operation
10043
 */
10044
function move_group_override($id, $move, $assignid) {
10045
    global $DB;
10046
 
10047
    // Get the override object.
10048
    if (!$override = $DB->get_record('assign_overrides', ['id' => $id, 'assignid' => $assignid], 'id, sortorder, groupid')) {
10049
        return false;
10050
    }
10051
    // Count the number of group overrides.
10052
    $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assignid));
10053
 
10054
    // Calculate the new sortorder.
10055
    if ( ($move == 'up') and ($override->sortorder > 1)) {
10056
        $neworder = $override->sortorder - 1;
10057
    } else if (($move == 'down') and ($override->sortorder < $overridecountgroup)) {
10058
        $neworder = $override->sortorder + 1;
10059
    } else {
10060
        return false;
10061
    }
10062
 
10063
    // Retrieve the override object that is currently residing in the new position.
10064
    $params = ['sortorder' => $neworder, 'assignid' => $assignid];
10065
    if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder, groupid')) {
10066
 
10067
        // Swap the sortorders.
10068
        $swapoverride->sortorder = $override->sortorder;
10069
        $override->sortorder     = $neworder;
10070
 
10071
        // Update the override records.
10072
        $DB->update_record('assign_overrides', $override);
10073
        $DB->update_record('assign_overrides', $swapoverride);
10074
 
10075
        // Delete cache for the 2 records we updated above.
10076
        $cache = cache::make('mod_assign', 'overrides');
10077
        $cache->delete("{$assignid}_g_{$override->groupid}");
10078
        $cache->delete("{$assignid}_g_{$swapoverride->groupid}");
10079
    }
10080
 
10081
    reorder_group_overrides($assignid);
10082
    return true;
10083
}
10084
 
10085
/**
10086
 * Reorder the overrides starting at the override at the given startorder.
10087
 *
10088
 * @param int $assignid of the assigment
10089
 */
10090
function reorder_group_overrides($assignid) {
10091
    global $DB;
10092
 
10093
    $i = 1;
10094
    if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) {
10095
        $cache = cache::make('mod_assign', 'overrides');
10096
        foreach ($overrides as $override) {
10097
            $f = new stdClass();
10098
            $f->id = $override->id;
10099
            $f->sortorder = $i++;
10100
            $DB->update_record('assign_overrides', $f);
10101
            $cache->delete("{$assignid}_g_{$override->groupid}");
10102
 
10103
            // Update priorities of group overrides.
10104
            $params = [
10105
                'modulename' => 'assign',
10106
                'instance' => $override->assignid,
10107
                'groupid' => $override->groupid
10108
            ];
10109
            $DB->set_field('event', 'priority', $f->sortorder, $params);
10110
        }
10111
    }
10112
}
10113
 
10114
/**
10115
 * Get the information about the standard assign JavaScript module.
10116
 * @return array a standard jsmodule structure.
10117
 */
10118
function assign_get_js_module() {
10119
    return array(
10120
        'name' => 'mod_assign',
10121
        'fullpath' => '/mod/assign/module.js',
10122
    );
10123
}