Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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