Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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