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