Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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