PageRenderTime 116ms CodeModel.GetById 58ms app.highlight 25ms RepoModel.GetById 1ms app.codeStats 3ms

/mod/assign/locallib.php

https://bitbucket.org/moodle/moodle
PHP | 9806 lines | 6471 code | 1222 blank | 2113 comment | 1410 complexity | 142664dd9affcbf3e2dbedfeac72cd8b MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   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');
  42
  43// Marker filter for grading page.
  44define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
  45
  46// Reopen attempt methods.
  47define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
  48define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
  49define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
  50
  51// Special value means allow unlimited attempts.
  52define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
  53
  54// Special value means no grade has been set.
  55define('ASSIGN_GRADE_NOT_SET', -1);
  56
  57// Grading states.
  58define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
  59define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
  60
  61// Marking workflow states.
  62define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
  63define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
  64define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
  65define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
  66define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
  67define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
  68
  69/** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
  70define("ASSIGN_MAX_EVENT_LENGTH", "432000");
  71
  72// Name of file area for intro attachments.
  73define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
  74
  75// Event types.
  76define('ASSIGN_EVENT_TYPE_DUE', 'due');
  77define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
  78define('ASSIGN_EVENT_TYPE_OPEN', 'open');
  79define('ASSIGN_EVENT_TYPE_CLOSE', 'close');
  80
  81require_once($CFG->libdir . '/accesslib.php');
  82require_once($CFG->libdir . '/formslib.php');
  83require_once($CFG->dirroot . '/repository/lib.php');
  84require_once($CFG->dirroot . '/mod/assign/mod_form.php');
  85require_once($CFG->libdir . '/gradelib.php');
  86require_once($CFG->dirroot . '/grade/grading/lib.php');
  87require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
  88require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
  89require_once($CFG->dirroot . '/mod/assign/renderable.php');
  90require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
  91require_once($CFG->libdir . '/portfolio/caller.php');
  92
  93use \mod_assign\output\grading_app;
  94
  95/**
  96 * Standard base class for mod_assign (assignment types).
  97 *
  98 * @package   mod_assign
  99 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
 100 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 101 */
 102class assign {
 103
 104    /** @var stdClass the assignment record that contains the global settings for this assign instance */
 105    private $instance;
 106
 107    /** @var array $var array an array containing per-user assignment records, each having calculated properties (e.g. dates) */
 108    private $userinstances = [];
 109
 110    /** @var grade_item the grade_item record for this assign instance's primary grade item. */
 111    private $gradeitem;
 112
 113    /** @var context the context of the course module for this assign instance
 114     *               (or just the course if we are creating a new one)
 115     */
 116    private $context;
 117
 118    /** @var stdClass the course this assign instance belongs to */
 119    private $course;
 120
 121    /** @var stdClass the admin config for all assign instances  */
 122    private $adminconfig;
 123
 124    /** @var assign_renderer the custom renderer for this module */
 125    private $output;
 126
 127    /** @var cm_info the course module for this assign instance */
 128    private $coursemodule;
 129
 130    /** @var array cache for things like the coursemodule name or the scale menu -
 131     *             only lives for a single request.
 132     */
 133    private $cache;
 134
 135    /** @var array list of the installed submission plugins */
 136    private $submissionplugins;
 137
 138    /** @var array list of the installed feedback plugins */
 139    private $feedbackplugins;
 140
 141    /** @var string action to be used to return to this page
 142     *              (without repeating any form submissions etc).
 143     */
 144    private $returnaction = 'view';
 145
 146    /** @var array params to be used to return to this page */
 147    private $returnparams = array();
 148
 149    /** @var string modulename prevents excessive calls to get_string */
 150    private static $modulename = null;
 151
 152    /** @var string modulenameplural prevents excessive calls to get_string */
 153    private static $modulenameplural = null;
 154
 155    /** @var array of marking workflow states for the current user */
 156    private $markingworkflowstates = null;
 157
 158    /** @var bool whether to exclude users with inactive enrolment */
 159    private $showonlyactiveenrol = null;
 160
 161    /** @var string A key used to identify userlists created by this object. */
 162    private $useridlistid = null;
 163
 164    /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
 165    private $participants = array();
 166
 167    /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
 168    private $usersubmissiongroups = array();
 169
 170    /** @var array cached list of user groups. The cache key will be the user. */
 171    private $usergroups = array();
 172
 173    /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
 174    private $sharedgroupmembers = array();
 175
 176    /**
 177     * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
 178     * to update the gradebook.
 179     */
 180    private $mostrecentteamsubmission = null;
 181
 182    /** @var array Array of error messages encountered during the execution of assignment related operations. */
 183    private $errors = array();
 184
 185    /**
 186     * Constructor for the base assign class.
 187     *
 188     * Note: For $coursemodule you can supply a stdclass if you like, but it
 189     * will be more efficient to supply a cm_info object.
 190     *
 191     * @param mixed $coursemodulecontext context|null the course module context
 192     *                                   (or the course context if the coursemodule has not been
 193     *                                   created yet).
 194     * @param mixed $coursemodule the current course module if it was already loaded,
 195     *                            otherwise this class will load one from the context as required.
 196     * @param mixed $course the current course  if it was already loaded,
 197     *                      otherwise this class will load one from the context as required.
 198     */
 199    public function __construct($coursemodulecontext, $coursemodule, $course) {
 200        global $SESSION;
 201
 202        $this->context = $coursemodulecontext;
 203        $this->course = $course;
 204
 205        // Ensure that $this->coursemodule is a cm_info object (or null).
 206        $this->coursemodule = cm_info::create($coursemodule);
 207
 208        // Temporary cache only lives for a single request - used to reduce db lookups.
 209        $this->cache = array();
 210
 211        $this->submissionplugins = $this->load_plugins('assignsubmission');
 212        $this->feedbackplugins = $this->load_plugins('assignfeedback');
 213
 214        // Extra entropy is required for uniqid() to work on cygwin.
 215        $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
 216
 217        if (!isset($SESSION->mod_assign_useridlist)) {
 218            $SESSION->mod_assign_useridlist = [];
 219        }
 220    }
 221
 222    /**
 223     * Set the action and parameters that can be used to return to the current page.
 224     *
 225     * @param string $action The action for the current page
 226     * @param array $params An array of name value pairs which form the parameters
 227     *                      to return to the current page.
 228     * @return void
 229     */
 230    public function register_return_link($action, $params) {
 231        global $PAGE;
 232        $params['action'] = $action;
 233        $cm = $this->get_course_module();
 234        if ($cm) {
 235            $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
 236        } else {
 237            $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
 238        }
 239
 240        $currenturl->params($params);
 241        $PAGE->set_url($currenturl);
 242    }
 243
 244    /**
 245     * Return an action that can be used to get back to the current page.
 246     *
 247     * @return string action
 248     */
 249    public function get_return_action() {
 250        global $PAGE;
 251
 252        // Web services don't set a URL, we should avoid debugging when ussing the url object.
 253        if (!WS_SERVER) {
 254            $params = $PAGE->url->params();
 255        }
 256
 257        if (!empty($params['action'])) {
 258            return $params['action'];
 259        }
 260        return '';
 261    }
 262
 263    /**
 264     * Based on the current assignment settings should we display the intro.
 265     *
 266     * @return bool showintro
 267     */
 268    public function show_intro() {
 269        if ($this->get_instance()->alwaysshowdescription ||
 270                time() > $this->get_instance()->allowsubmissionsfromdate) {
 271            return true;
 272        }
 273        return false;
 274    }
 275
 276    /**
 277     * Return a list of parameters that can be used to get back to the current page.
 278     *
 279     * @return array params
 280     */
 281    public function get_return_params() {
 282        global $PAGE;
 283
 284        $params = array();
 285        if (!WS_SERVER) {
 286            $params = $PAGE->url->params();
 287        }
 288        unset($params['id']);
 289        unset($params['action']);
 290        return $params;
 291    }
 292
 293    /**
 294     * Set the submitted form data.
 295     *
 296     * @param stdClass $data The form data (instance)
 297     */
 298    public function set_instance(stdClass $data) {
 299        $this->instance = $data;
 300    }
 301
 302    /**
 303     * Set the context.
 304     *
 305     * @param context $context The new context
 306     */
 307    public function set_context(context $context) {
 308        $this->context = $context;
 309    }
 310
 311    /**
 312     * Set the course data.
 313     *
 314     * @param stdClass $course The course data
 315     */
 316    public function set_course(stdClass $course) {
 317        $this->course = $course;
 318    }
 319
 320    /**
 321     * Set error message.
 322     *
 323     * @param string $message The error message
 324     */
 325    protected function set_error_message(string $message) {
 326        $this->errors[] = $message;
 327    }
 328
 329    /**
 330     * Get error messages.
 331     *
 332     * @return array The array of error messages
 333     */
 334    protected function get_error_messages(): array {
 335        return $this->errors;
 336    }
 337
 338    /**
 339     * Get list of feedback plugins installed.
 340     *
 341     * @return array
 342     */
 343    public function get_feedback_plugins() {
 344        return $this->feedbackplugins;
 345    }
 346
 347    /**
 348     * Get list of submission plugins installed.
 349     *
 350     * @return array
 351     */
 352    public function get_submission_plugins() {
 353        return $this->submissionplugins;
 354    }
 355
 356    /**
 357     * Is blind marking enabled and reveal identities not set yet?
 358     *
 359     * @return bool
 360     */
 361    public function is_blind_marking() {
 362        return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
 363    }
 364
 365    /**
 366     * Is hidden grading enabled?
 367     *
 368     * This just checks the assignment settings. Remember to check
 369     * the user has the 'showhiddengrader' capability too
 370     *
 371     * @return bool
 372     */
 373    public function is_hidden_grader() {
 374        return $this->get_instance()->hidegrader;
 375    }
 376
 377    /**
 378     * Does an assignment have submission(s) or grade(s) already?
 379     *
 380     * @return bool
 381     */
 382    public function has_submissions_or_grades() {
 383        $allgrades = $this->count_grades();
 384        $allsubmissions = $this->count_submissions();
 385        if (($allgrades == 0) && ($allsubmissions == 0)) {
 386            return false;
 387        }
 388        return true;
 389    }
 390
 391    /**
 392     * Get a specific submission plugin by its type.
 393     *
 394     * @param string $subtype assignsubmission | assignfeedback
 395     * @param string $type
 396     * @return mixed assign_plugin|null
 397     */
 398    public function get_plugin_by_type($subtype, $type) {
 399        $shortsubtype = substr($subtype, strlen('assign'));
 400        $name = $shortsubtype . 'plugins';
 401        if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
 402            return null;
 403        }
 404        $pluginlist = $this->$name;
 405        foreach ($pluginlist as $plugin) {
 406            if ($plugin->get_type() == $type) {
 407                return $plugin;
 408            }
 409        }
 410        return null;
 411    }
 412
 413    /**
 414     * Get a feedback plugin by type.
 415     *
 416     * @param string $type - The type of plugin e.g comments
 417     * @return mixed assign_feedback_plugin|null
 418     */
 419    public function get_feedback_plugin_by_type($type) {
 420        return $this->get_plugin_by_type('assignfeedback', $type);
 421    }
 422
 423    /**
 424     * Get a submission plugin by type.
 425     *
 426     * @param string $type - The type of plugin e.g comments
 427     * @return mixed assign_submission_plugin|null
 428     */
 429    public function get_submission_plugin_by_type($type) {
 430        return $this->get_plugin_by_type('assignsubmission', $type);
 431    }
 432
 433    /**
 434     * Load the plugins from the sub folders under subtype.
 435     *
 436     * @param string $subtype - either submission or feedback
 437     * @return array - The sorted list of plugins
 438     */
 439    public function load_plugins($subtype) {
 440        global $CFG;
 441        $result = array();
 442
 443        $names = core_component::get_plugin_list($subtype);
 444
 445        foreach ($names as $name => $path) {
 446            if (file_exists($path . '/locallib.php')) {
 447                require_once($path . '/locallib.php');
 448
 449                $shortsubtype = substr($subtype, strlen('assign'));
 450                $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
 451
 452                $plugin = new $pluginclass($this, $name);
 453
 454                if ($plugin instanceof assign_plugin) {
 455                    $idx = $plugin->get_sort_order();
 456                    while (array_key_exists($idx, $result)) {
 457                        $idx +=1;
 458                    }
 459                    $result[$idx] = $plugin;
 460                }
 461            }
 462        }
 463        ksort($result);
 464        return $result;
 465    }
 466
 467    /**
 468     * Display the assignment, used by view.php
 469     *
 470     * The assignment is displayed differently depending on your role,
 471     * the settings for the assignment and the status of the assignment.
 472     *
 473     * @param string $action The current action if any.
 474     * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
 475     * @return string - The page output.
 476     */
 477    public function view($action='', $args = array()) {
 478        global $PAGE;
 479
 480        $o = '';
 481        $mform = null;
 482        $notices = array();
 483        $nextpageparams = array();
 484
 485        if (!empty($this->get_course_module()->id)) {
 486            $nextpageparams['id'] = $this->get_course_module()->id;
 487        }
 488
 489        // Handle form submissions first.
 490        if ($action == 'savesubmission') {
 491            $action = 'editsubmission';
 492            if ($this->process_save_submission($mform, $notices)) {
 493                $action = 'redirect';
 494                if ($this->can_grade()) {
 495                    $nextpageparams['action'] = 'grading';
 496                } else {
 497                    $nextpageparams['action'] = 'view';
 498                }
 499            }
 500        } else if ($action == 'editprevioussubmission') {
 501            $action = 'editsubmission';
 502            if ($this->process_copy_previous_attempt($notices)) {
 503                $action = 'redirect';
 504                $nextpageparams['action'] = 'editsubmission';
 505            }
 506        } else if ($action == 'lock') {
 507            $this->process_lock_submission();
 508            $action = 'redirect';
 509            $nextpageparams['action'] = 'grading';
 510        } else if ($action == 'removesubmission') {
 511            $this->process_remove_submission();
 512            $action = 'redirect';
 513            if ($this->can_grade()) {
 514                $nextpageparams['action'] = 'grading';
 515            } else {
 516                $nextpageparams['action'] = 'view';
 517            }
 518        } else if ($action == 'addattempt') {
 519            $this->process_add_attempt(required_param('userid', PARAM_INT));
 520            $action = 'redirect';
 521            $nextpageparams['action'] = 'grading';
 522        } else if ($action == 'reverttodraft') {
 523            $this->process_revert_to_draft();
 524            $action = 'redirect';
 525            $nextpageparams['action'] = 'grading';
 526        } else if ($action == 'unlock') {
 527            $this->process_unlock_submission();
 528            $action = 'redirect';
 529            $nextpageparams['action'] = 'grading';
 530        } else if ($action == 'setbatchmarkingworkflowstate') {
 531            $this->process_set_batch_marking_workflow_state();
 532            $action = 'redirect';
 533            $nextpageparams['action'] = 'grading';
 534        } else if ($action == 'setbatchmarkingallocation') {
 535            $this->process_set_batch_marking_allocation();
 536            $action = 'redirect';
 537            $nextpageparams['action'] = 'grading';
 538        } else if ($action == 'confirmsubmit') {
 539            $action = 'submit';
 540            if ($this->process_submit_for_grading($mform, $notices)) {
 541                $action = 'redirect';
 542                $nextpageparams['action'] = 'view';
 543            } else if ($notices) {
 544                $action = 'viewsubmitforgradingerror';
 545            }
 546        } else if ($action == 'submitotherforgrading') {
 547            if ($this->process_submit_other_for_grading($mform, $notices)) {
 548                $action = 'redirect';
 549                $nextpageparams['action'] = 'grading';
 550            } else {
 551                $action = 'viewsubmitforgradingerror';
 552            }
 553        } else if ($action == 'gradingbatchoperation') {
 554            $action = $this->process_grading_batch_operation($mform);
 555            if ($action == 'grading') {
 556                $action = 'redirect';
 557                $nextpageparams['action'] = 'grading';
 558            }
 559        } else if ($action == 'submitgrade') {
 560            if (optional_param('saveandshownext', null, PARAM_RAW)) {
 561                // Save and show next.
 562                $action = 'grade';
 563                if ($this->process_save_grade($mform)) {
 564                    $action = 'redirect';
 565                    $nextpageparams['action'] = 'grade';
 566                    $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
 567                    $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
 568                }
 569            } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
 570                $action = 'redirect';
 571                $nextpageparams['action'] = 'grade';
 572                $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
 573                $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
 574            } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
 575                $action = 'redirect';
 576                $nextpageparams['action'] = 'grade';
 577                $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
 578                $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
 579            } else if (optional_param('savegrade', null, PARAM_RAW)) {
 580                // Save changes button.
 581                $action = 'grade';
 582                if ($this->process_save_grade($mform)) {
 583                    $action = 'redirect';
 584                    $nextpageparams['action'] = 'savegradingresult';
 585                }
 586            } else {
 587                // Cancel button.
 588                $action = 'redirect';
 589                $nextpageparams['action'] = 'grading';
 590            }
 591        } else if ($action == 'quickgrade') {
 592            $message = $this->process_save_quick_grades();
 593            $action = 'quickgradingresult';
 594        } else if ($action == 'saveoptions') {
 595            $this->process_save_grading_options();
 596            $action = 'redirect';
 597            $nextpageparams['action'] = 'grading';
 598        } else if ($action == 'saveextension') {
 599            $action = 'grantextension';
 600            if ($this->process_save_extension($mform)) {
 601                $action = 'redirect';
 602                $nextpageparams['action'] = 'grading';
 603            }
 604        } else if ($action == 'revealidentitiesconfirm') {
 605            $this->process_reveal_identities();
 606            $action = 'redirect';
 607            $nextpageparams['action'] = 'grading';
 608        }
 609
 610        $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
 611                              'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
 612        $this->register_return_link($action, $returnparams);
 613
 614        // Include any page action as part of the body tag CSS id.
 615        if (!empty($action)) {
 616            $PAGE->set_pagetype('mod-assign-' . $action);
 617        }
 618        // Now show the right view page.
 619        if ($action == 'redirect') {
 620            $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
 621            $messages = '';
 622            $messagetype = \core\output\notification::NOTIFY_INFO;
 623            $errors = $this->get_error_messages();
 624            if (!empty($errors)) {
 625                $messages = html_writer::alist($errors, ['class' => 'mb-1 mt-1']);
 626                $messagetype = \core\output\notification::NOTIFY_ERROR;
 627            }
 628            redirect($nextpageurl, $messages, null, $messagetype);
 629            return;
 630        } else if ($action == 'savegradingresult') {
 631            $message = get_string('gradingchangessaved', 'assign');
 632            $o .= $this->view_savegrading_result($message);
 633        } else if ($action == 'quickgradingresult') {
 634            $mform = null;
 635            $o .= $this->view_quickgrading_result($message);
 636        } else if ($action == 'gradingpanel') {
 637            $o .= $this->view_single_grading_panel($args);
 638        } else if ($action == 'grade') {
 639            $o .= $this->view_single_grade_page($mform);
 640        } else if ($action == 'viewpluginassignfeedback') {
 641            $o .= $this->view_plugin_content('assignfeedback');
 642        } else if ($action == 'viewpluginassignsubmission') {
 643            $o .= $this->view_plugin_content('assignsubmission');
 644        } else if ($action == 'editsubmission') {
 645            $o .= $this->view_edit_submission_page($mform, $notices);
 646        } else if ($action == 'grader') {
 647            $o .= $this->view_grader();
 648        } else if ($action == 'grading') {
 649            $o .= $this->view_grading_page();
 650        } else if ($action == 'downloadall') {
 651            $o .= $this->download_submissions();
 652        } else if ($action == 'submit') {
 653            $o .= $this->check_submit_for_grading($mform);
 654        } else if ($action == 'grantextension') {
 655            $o .= $this->view_grant_extension($mform);
 656        } else if ($action == 'revealidentities') {
 657            $o .= $this->view_reveal_identities_confirm($mform);
 658        } else if ($action == 'removesubmissionconfirm') {
 659            $o .= $this->view_remove_submission_confirm();
 660        } else if ($action == 'plugingradingbatchoperation') {
 661            $o .= $this->view_plugin_grading_batch_operation($mform);
 662        } else if ($action == 'viewpluginpage') {
 663             $o .= $this->view_plugin_page();
 664        } else if ($action == 'viewcourseindex') {
 665             $o .= $this->view_course_index();
 666        } else if ($action == 'viewbatchsetmarkingworkflowstate') {
 667             $o .= $this->view_batch_set_workflow_state($mform);
 668        } else if ($action == 'viewbatchmarkingallocation') {
 669            $o .= $this->view_batch_markingallocation($mform);
 670        } else if ($action == 'viewsubmitforgradingerror') {
 671            $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
 672        } else if ($action == 'fixrescalednullgrades') {
 673            $o .= $this->view_fix_rescaled_null_grades();
 674        } else {
 675            $o .= $this->view_submission_page();
 676        }
 677
 678        return $o;
 679    }
 680
 681    /**
 682     * Add this instance to the database.
 683     *
 684     * @param stdClass $formdata The data submitted from the form
 685     * @param bool $callplugins This is used to skip the plugin code
 686     *             when upgrading an old assignment to a new one (the plugins get called manually)
 687     * @return mixed false if an error occurs or the int id of the new instance
 688     */
 689    public function add_instance(stdClass $formdata, $callplugins) {
 690        global $DB;
 691        $adminconfig = $this->get_admin_config();
 692
 693        $err = '';
 694
 695        // Add the database record.
 696        $update = new stdClass();
 697        $update->name = $formdata->name;
 698        $update->timemodified = time();
 699        $update->timecreated = time();
 700        $update->course = $formdata->course;
 701        $update->courseid = $formdata->course;
 702        $update->intro = $formdata->intro;
 703        $update->introformat = $formdata->introformat;
 704        $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
 705        $update->submissiondrafts = $formdata->submissiondrafts;
 706        $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
 707        $update->sendnotifications = $formdata->sendnotifications;
 708        $update->sendlatenotifications = $formdata->sendlatenotifications;
 709        $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
 710        if (isset($formdata->sendstudentnotifications)) {
 711            $update->sendstudentnotifications = $formdata->sendstudentnotifications;
 712        }
 713        $update->duedate = $formdata->duedate;
 714        $update->cutoffdate = $formdata->cutoffdate;
 715        $update->gradingduedate = $formdata->gradingduedate;
 716        $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
 717        $update->grade = $formdata->grade;
 718        $update->completionsubmit = !empty($formdata->completionsubmit);
 719        $update->teamsubmission = $formdata->teamsubmission;
 720        $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
 721        if (isset($formdata->teamsubmissiongroupingid)) {
 722            $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
 723        }
 724        $update->blindmarking = $formdata->blindmarking;
 725        if (isset($formdata->hidegrader)) {
 726            $update->hidegrader = $formdata->hidegrader;
 727        }
 728        $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
 729        if (!empty($formdata->attemptreopenmethod)) {
 730            $update->attemptreopenmethod = $formdata->attemptreopenmethod;
 731        }
 732        if (!empty($formdata->maxattempts)) {
 733            $update->maxattempts = $formdata->maxattempts;
 734        }
 735        if (isset($formdata->preventsubmissionnotingroup)) {
 736            $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
 737        }
 738        $update->markingworkflow = $formdata->markingworkflow;
 739        $update->markingallocation = $formdata->markingallocation;
 740        if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
 741            $update->markingallocation = 0;
 742        }
 743
 744        $returnid = $DB->insert_record('assign', $update);
 745        $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
 746        // Cache the course record.
 747        $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
 748
 749        $this->save_intro_draft_files($formdata);
 750
 751        if ($callplugins) {
 752            // Call save_settings hook for submission plugins.
 753            foreach ($this->submissionplugins as $plugin) {
 754                if (!$this->update_plugin_instance($plugin, $formdata)) {
 755                    print_error($plugin->get_error());
 756                    return false;
 757                }
 758            }
 759            foreach ($this->feedbackplugins as $plugin) {
 760                if (!$this->update_plugin_instance($plugin, $formdata)) {
 761                    print_error($plugin->get_error());
 762                    return false;
 763                }
 764            }
 765
 766            // In the case of upgrades the coursemodule has not been set,
 767            // so we need to wait before calling these two.
 768            $this->update_calendar($formdata->coursemodule);
 769            if (!empty($formdata->completionexpected)) {
 770                \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance,
 771                        $formdata->completionexpected);
 772            }
 773            $this->update_gradebook(false, $formdata->coursemodule);
 774
 775        }
 776
 777        $update = new stdClass();
 778        $update->id = $this->get_instance()->id;
 779        $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
 780        $DB->update_record('assign', $update);
 781
 782        return $returnid;
 783    }
 784
 785    /**
 786     * Delete all grades from the gradebook for this assignment.
 787     *
 788     * @return bool
 789     */
 790    protected function delete_grades() {
 791        global $CFG;
 792
 793        $result = grade_update('mod/assign',
 794                               $this->get_course()->id,
 795                               'mod',
 796                               'assign',
 797                               $this->get_instance()->id,
 798                               0,
 799                               null,
 800                               array('deleted'=>1));
 801        return $result == GRADE_UPDATE_OK;
 802    }
 803
 804    /**
 805     * Delete this instance from the database.
 806     *
 807     * @return bool false if an error occurs
 808     */
 809    public function delete_instance() {
 810        global $DB;
 811        $result = true;
 812
 813        foreach ($this->submissionplugins as $plugin) {
 814            if (!$plugin->delete_instance()) {
 815                print_error($plugin->get_error());
 816                $result = false;
 817            }
 818        }
 819        foreach ($this->feedbackplugins as $plugin) {
 820            if (!$plugin->delete_instance()) {
 821                print_error($plugin->get_error());
 822                $result = false;
 823            }
 824        }
 825
 826        // Delete files associated with this assignment.
 827        $fs = get_file_storage();
 828        if (! $fs->delete_area_files($this->context->id) ) {
 829            $result = false;
 830        }
 831
 832        $this->delete_all_overrides();
 833
 834        // Delete_records will throw an exception if it fails - so no need for error checking here.
 835        $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
 836        $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
 837        $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
 838        $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
 839        $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
 840
 841        // Delete items from the gradebook.
 842        if (! $this->delete_grades()) {
 843            $result = false;
 844        }
 845
 846        // Delete the instance.
 847        // We must delete the module record after we delete the grade item.
 848        $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
 849
 850        return $result;
 851    }
 852
 853    /**
 854     * Deletes a assign override from the database and clears any corresponding calendar events
 855     *
 856     * @param int $overrideid The id of the override being deleted
 857     * @return bool true on success
 858     */
 859    public function delete_override($overrideid) {
 860        global $CFG, $DB;
 861
 862        require_once($CFG->dirroot . '/calendar/lib.php');
 863
 864        $cm = $this->get_course_module();
 865        if (empty($cm)) {
 866            $instance = $this->get_instance();
 867            $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course);
 868        }
 869
 870        $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
 871
 872        // Delete the events.
 873        $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
 874        if (isset($override->userid)) {
 875            $conds['userid'] = $override->userid;
 876        } else {
 877            $conds['groupid'] = $override->groupid;
 878        }
 879        $events = $DB->get_records('event', $conds);
 880        foreach ($events as $event) {
 881            $eventold = calendar_event::load($event);
 882            $eventold->delete();
 883        }
 884
 885        $DB->delete_records('assign_overrides', array('id' => $overrideid));
 886
 887        // Set the common parameters for one of the events we will be triggering.
 888        $params = array(
 889            'objectid' => $override->id,
 890            'context' => context_module::instance($cm->id),
 891            'other' => array(
 892                'assignid' => $override->assignid
 893            )
 894        );
 895        // Determine which override deleted event to fire.
 896        if (!empty($override->userid)) {
 897            $params['relateduserid'] = $override->userid;
 898            $event = \mod_assign\event\user_override_deleted::create($params);
 899        } else {
 900            $params['other']['groupid'] = $override->groupid;
 901            $event = \mod_assign\event\group_override_deleted::create($params);
 902        }
 903
 904        // Trigger the override deleted event.
 905        $event->add_record_snapshot('assign_overrides', $override);
 906        $event->trigger();
 907
 908        return true;
 909    }
 910
 911    /**
 912     * Deletes all assign overrides from the database and clears any corresponding calendar events
 913     */
 914    public function delete_all_overrides() {
 915        global $DB;
 916
 917        $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id');
 918        foreach ($overrides as $override) {
 919            $this->delete_override($override->id);
 920        }
 921    }
 922
 923    /**
 924     * Updates the assign properties with override information for a user.
 925     *
 926     * Algorithm:  For each assign setting, if there is a matching user-specific override,
 927     *   then use that otherwise, if there are group-specific overrides, return the most
 928     *   lenient combination of them.  If neither applies, leave the assign setting unchanged.
 929     *
 930     * @param int $userid The userid.
 931     */
 932    public function update_effective_access($userid) {
 933
 934        $override = $this->override_exists($userid);
 935
 936        // Merge with assign defaults.
 937        $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
 938        foreach ($keys as $key) {
 939            if (isset($override->{$key})) {
 940                $this->get_instance($userid)->{$key} = $override->{$key};
 941            }
 942        }
 943
 944    }
 945
 946    /**
 947     * Returns whether an assign has any overrides.
 948     *
 949     * @return true if any, false if not
 950     */
 951    public function has_overrides() {
 952        global $DB;
 953
 954        $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
 955
 956        if ($override) {
 957            return true;
 958        }
 959
 960        return false;
 961    }
 962
 963    /**
 964     * Returns user override
 965     *
 966     * Algorithm:  For each assign setting, if there is a matching user-specific override,
 967     *   then use that otherwise, if there are group-specific overrides, use the one with the
 968     *   lowest sort order. If neither applies, leave the assign setting unchanged.
 969     *
 970     * @param int $userid The userid.
 971     * @return stdClass The override
 972     */
 973    public function override_exists($userid) {
 974        global $DB;
 975
 976        // Gets an assoc array containing the keys for defined user overrides only.
 977        $getuseroverride = function($userid) use ($DB) {
 978            $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
 979            return $useroverride ? get_object_vars($useroverride) : [];
 980        };
 981
 982        // Gets an assoc array containing the keys for defined group overrides only.
 983        $getgroupoverride = function($userid) use ($DB) {
 984            $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
 985
 986            if (empty($groupings[0])) {
 987                return [];
 988            }
 989
 990            // Select all overrides that apply to the User's groups.
 991            list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
 992            $sql = "SELECT * FROM {assign_overrides}
 993                    WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
 994            $params[] = $this->get_instance()->id;
 995            $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
 996
 997            return $groupoverride ? get_object_vars($groupoverride) : [];
 998        };
 999
1000        // Later arguments clobber earlier ones with array_merge. The two helper functions
1001        // return arrays containing keys for only the defined overrides. So we get the
1002        // desired behaviour as per the algorithm.
1003        return (object)array_merge(
1004            ['duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
1005            $getgroupoverride($userid),
1006            $getuseroverride($userid)
1007        );
1008    }
1009
1010    /**
1011     * Check if the given calendar_event is either a user or group override
1012     * event.
1013     *
1014     * @return bool
1015     */
1016    public function is_override_calendar_event(\calendar_event $event) {
1017        global $DB;
1018
1019        if (!isset($event->modulename)) {
1020            return false;
1021        }
1022
1023        if ($event->modulename != 'assign') {
1024            return false;
1025        }
1026
1027        if (!isset($event->instance)) {
1028            return false;
1029        }
1030
1031        if (!isset($event->userid) && !isset($event->groupid)) {
1032            return false;
1033        }
1034
1035        $overrideparams = [
1036            'assignid' => $event->instance
1037        ];
1038
1039        if (isset($event->groupid)) {
1040            $overrideparams['groupid'] = $event->groupid;
1041        } else if (isset($event->userid)) {
1042            $overrideparams['userid'] = $event->userid;
1043        }
1044
1045        if ($DB->get_record('assign_overrides', $overrideparams)) {
1046            return true;
1047        } else {
1048            return false;
1049        }
1050    }
1051
1052    /**
1053     * This function calculates the minimum and maximum cutoff values for the timestart of
1054     * the given event.
1055     *
1056     * It will return an array with two values, the first being the minimum cutoff value and
1057     * the second being the maximum cutoff value. Either or both values can be null, which
1058     * indicates there is no minimum or maximum, respectively.
1059     *
1060     * If a cutoff is required then the function must return an array containing the cutoff
1061     * timestamp and error string to display to the user if the cutoff value is violated.
1062     *
1063     * A minimum and maximum cutoff return value will look like:
1064     * [
1065     *     [1505704373, 'The due date must be after the sbumission start date'],
1066     *     [1506741172, 'The due date must be before the cutoff date']
1067     * ]
1068     *
1069     * If the event does not have a valid timestart range then [false, false] will
1070     * be returned.
1071     *
1072     * @param calendar_event $event The calendar event to get the time range for
1073     * @return array
1074     */
1075    function get_valid_calendar_event_timestart_range(\calendar_event $event) {
1076        $instance = $this->get_instance();
1077        $submissionsfromdate = $instance->allowsubmissionsfromdate;
1078        $cutoffdate = $instance->cutoffdate;
1079        $duedate = $instance->duedate;
1080        $gradingduedate = $instance->gradingduedate;
1081        $mindate = null;
1082        $maxdate = null;
1083
1084        if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
1085            // This check is in here because due date events are currently
1086            // the only events that can be overridden, so we can save a DB
1087            // query if we don't bother checking other events.
1088            if ($this->is_override_calendar_event($event)) {
1089                // This is an override event so there is no valid timestart
1090                // range to set it to.
1091                return [false, false];
1092            }
1093
1094            if ($submissionsfromdate) {
1095                $mindate = [
1096                    $submissionsfromdate,
1097                    get_string('duedatevalidation', 'assign'),
1098                ];
1099            }
1100
1101            if ($cutoffdate) {
1102                $maxdate = [
1103                    $cutoffdate,
1104                    get_string('cutoffdatevalidation', 'assign'),
1105                ];
1106            }
1107
1108            if ($gradingduedate) {
1109                // If we don't have a cutoff date or we've got a grading due date
1110                // that is earlier than the cutoff then we should use that as the
1111                // upper limit for the due date.
1112                if (!$cutoffdate || $gradingduedate < $cutoffdate) {
1113                    $maxdate = [
1114                        $gradingduedate,
1115                        get_string('gradingdueduedatevalidation', 'assign'),
1116                    ];
1117                }
1118            }
1119        } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
1120            if ($duedate) {
1121                $mindate = [
1122                    $duedate,
1123                    get_string('gradingdueduedatevalidation', 'assign'),
1124                ];
1125            } else if ($submissionsfromdate) {
1126                $mindate = [
1127                    $submissionsfromdate,
1128                    get_string('gradingduefromdatevalidation', 'assign'),
1129                ];
1130            }
1131        }
1132
1133        return [$mindate, $maxdate];
1134    }
1135
1136    /**
1137     * Actual implementation of the reset course functionality, delete all the
1138     * assignment submissions for course $data->courseid.
1139     *
1140     * @param stdClass $data the data submitted from the reset course.
1141     * @return array status array
1142     */
1143    public function reset_userdata($data) {
1144        global $CFG, $DB;
1145
1146        $componentstr = get_string('modulenameplural', 'assign');
1147        $status = array();
1148
1149        $fs = get_file_storage();
1150        if (!empty($data->reset_assign_submissions)) {
1151            // Delete files associated with this assignment.
1152            foreach ($this->submissionplugins as $plugin) {
1153                $fileareas = array();
1154                $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1155                $fileareas = $plugin->get_file_areas();
1156                foreach ($fileareas as $filearea => $notused) {
1157                    $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1158                }
1159
1160                if (!$plugin->delete_instance()) {
1161                    $status[] = array('component'=>$componentstr,
1162                                      'item'=>get_string('deleteallsubmissions', 'assign'),
1163                                      'error'=>$plugin->get_error());
1164                }
1165            }
1166
1167            foreach ($this->feedbackplugins as $plugin) {
1168                $fileareas = array();
1169                $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1170                $fileareas = $plugin->get_file_areas();
1171                foreach ($fileareas as $filearea => $notused) {
1172                    $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1173                }
1174
1175                if (!$plugin->delete_instance()) {
1176                    $status[] = array('component'=>$componentstr,
1177                                      'item'=>get_string('deleteallsubmissions', 'assign'),
1178                                      'error'=>$plugin->get_error());
1179                }
1180            }
1181
1182            $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
1183            list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
1184
1185            $DB->delete_records_select('assign_submission', "assignment $sql", $params);
1186            $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
1187
1188            $status[] = array('component'=>$componentstr,
1189                              'item'=>get_string('deleteallsubmissions', 'assign'),
1190                              'error'=>false);
1191
1192            if (!empty($data->reset_gradebook_grades)) {
1193                $DB->delete_records_select('assign_grades', "assignment $sql", $params);
1194                // Remove all grades from gradebook.
1195                require_once($CFG->dirroot.'/mod/assign/lib.php');
1196                assign_reset_gradebook($data->courseid);
1197            }
1198
1199            // Reset revealidentities for assign if blindmarking is enabled.
1200            if ($this->get_instance()->blindmarking) {
1201                $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
1202            }
1203        }
1204
1205        // Remove user overrides.
1206        if (!empty($data->reset_assign_user_overrides)) {
1207            $DB->delete_records_select('assign_overrides',
1208                'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
1209            $status[] = array(
1210                'component' => $componentstr,
1211                'item' => get_string('useroverridesdeleted', 'assign'),
1212                'error' => false);
1213        }
1214        // Remove group overrides.
1215        if (!empty($data->reset_assign_group_overrides)) {
1216            $DB->delete_records_select('assign_overrides',
1217                'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
1218            $status[] = array(
1219                'component' => $componentstr,
1220                'item' => get_string('groupoverridesdeleted', 'assign'),
1221                'error' => false);
1222        }
1223
1224        // Updating dates - shift may be negative too.
1225        if ($data->timeshift) {
1226            $DB->execute("UPDATE {assign_overrides}
1227                         SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
1228                       WHERE assignid = ? AND allowsubmissionsfromdate <> 0",
1229                array($data->timeshift, $this->get_instance()->id));
1230            $DB->execute("UPDATE {assign_overrides}
1231                         SET duedate = duedate + ?
1232                       WHERE assignid = ? AND duedate <> 0",
1233                array($data->timeshift, $this->get_instance()->id));
1234            $DB->execute("UPDATE {assign_overrides}
1235                         SET cutoffdate = cutoffdate + ?
1236                       WHERE assignid =? AND cutoffdate <> 0",
1237                array($data->timeshift, $this->get_instance()->id));
1238
1239            // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
1240            // See MDL-9367.
1241            shift_course_mod_dates('assign',
1242                                    array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
1243                                    $data->timeshift,
1244                                    $data->courseid, $this->get_instance()->id);
1245            $status[] = array('component'=>$componentstr,
1246                              'item'=>get_string('datechanged'),
1247                              'error'=>false);
1248        }
1249
1250        return $status;
1251    }
1252
1253    /**
1254     * Update the settings for a single plugin.
1255     *
1256     * @param assign_plugin $plugin The plugin to update
1257     * @param stdClass $formdata The form data
1258     * @return bool false if an error occurs
1259     */
1260    protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
1261        if ($plugin->is_visible()) {
1262            $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1263            if (!empty($formdata->$enabledname)) {
1264                $plugin->enable();
1265                if (!$plugin->save_settings($formdata)) {
1266                    print_error($plugin->get_error());
1267                    return false;
1268                }
1269            } else {
1270                $plugin->disable();
1271            }
1272        }
1273        return true;
1274    }
1275
1276    /**
1277     * Update the gradebook information for this assignment.
1278     *
1279     * @param bool $reset If true, will reset all grades in the gradbook for this assignment
1280     * @param int $coursemoduleid This is required because it might not exist in the database yet
1281     * @return bool
1282     */
1283    public function update_gradebook($reset, $coursemoduleid) {
1284        global $CFG;
1285
1286        require_once($CFG->dirroot.'/mod/assign/lib.php');
1287        $assign = clone $this->get_instance();
1288        $assign->cmidnumber = $coursemodule…

Large files files are truncated, but you can click here to view the full file