Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

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