Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * This file contains the definition for the class assignment
  19   *
  20   * This class provides all the functionality for the new assign module.
  21   *
  22   * @package   mod_assign
  23   * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
  24   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  // Assignment submission statuses.
  30  define('ASSIGN_SUBMISSION_STATUS_NEW', 'new');
  31  define('ASSIGN_SUBMISSION_STATUS_REOPENED', 'reopened');
  32  define('ASSIGN_SUBMISSION_STATUS_DRAFT', 'draft');
  33  define('ASSIGN_SUBMISSION_STATUS_SUBMITTED', 'submitted');
  34  
  35  // Search filters for grading page.
  36  define('ASSIGN_FILTER_NONE', 'none');
  37  define('ASSIGN_FILTER_SUBMITTED', 'submitted');
  38  define('ASSIGN_FILTER_NOT_SUBMITTED', 'notsubmitted');
  39  define('ASSIGN_FILTER_SINGLE_USER', 'singleuser');
  40  define('ASSIGN_FILTER_REQUIRE_GRADING', 'requiregrading');
  41  define('ASSIGN_FILTER_GRANTED_EXTENSION', 'grantedextension');
  42  define('ASSIGN_FILTER_DRAFT', 'draft');
  43  
  44  // Marker filter for grading page.
  45  define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
  46  
  47  // Reopen attempt methods.
  48  define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
  49  define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
  50  define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
  51  
  52  // Special value means allow unlimited attempts.
  53  define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
  54  
  55  // Special value means no grade has been set.
  56  define('ASSIGN_GRADE_NOT_SET', -1);
  57  
  58  // Grading states.
  59  define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
  60  define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
  61  
  62  // Marking workflow states.
  63  define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
  64  define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
  65  define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
  66  define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
  67  define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
  68  define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
  69  
  70  /** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
  71  define("ASSIGN_MAX_EVENT_LENGTH", "432000");
  72  
  73  // Name of file area for intro attachments.
  74  define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
  75  
  76  // Event types.
  77  define('ASSIGN_EVENT_TYPE_DUE', 'due');
  78  define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
  79  define('ASSIGN_EVENT_TYPE_OPEN', 'open');
  80  define('ASSIGN_EVENT_TYPE_CLOSE', 'close');
  81  
  82  require_once($CFG->libdir . '/accesslib.php');
  83  require_once($CFG->libdir . '/formslib.php');
  84  require_once($CFG->dirroot . '/repository/lib.php');
  85  require_once($CFG->dirroot . '/mod/assign/mod_form.php');
  86  require_once($CFG->libdir . '/gradelib.php');
  87  require_once($CFG->dirroot . '/grade/grading/lib.php');
  88  require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
  89  require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
  90  require_once($CFG->dirroot . '/mod/assign/renderable.php');
  91  require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
  92  require_once($CFG->libdir . '/portfolio/caller.php');
  93  
  94  use \mod_assign\output\grading_app;
  95  
  96  /**
  97   * Standard base class for mod_assign (assignment types).
  98   *
  99   * @package   mod_assign
 100   * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
 101   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 102   */
 103  class assign {
 104  
 105      /** @var stdClass the assignment record that contains the global settings for this assign instance */
 106      private $instance;
 107  
 108      /** @var array $var array an array containing per-user assignment records, each having calculated properties (e.g. dates) */
 109      private $userinstances = [];
 110  
 111      /** @var grade_item the grade_item record for this assign instance's primary grade item. */
 112      private $gradeitem;
 113  
 114      /** @var context the context of the course module for this assign instance
 115       *               (or just the course if we are creating a new one)
 116       */
 117      private $context;
 118  
 119      /** @var stdClass the course this assign instance belongs to */
 120      private $course;
 121  
 122      /** @var stdClass the admin config for all assign instances  */
 123      private $adminconfig;
 124  
 125      /** @var assign_renderer the custom renderer for this module */
 126      private $output;
 127  
 128      /** @var cm_info the course module for this assign instance */
 129      private $coursemodule;
 130  
 131      /** @var array cache for things like the coursemodule name or the scale menu -
 132       *             only lives for a single request.
 133       */
 134      private $cache;
 135  
 136      /** @var array list of the installed submission plugins */
 137      private $submissionplugins;
 138  
 139      /** @var array list of the installed feedback plugins */
 140      private $feedbackplugins;
 141  
 142      /** @var string action to be used to return to this page
 143       *              (without repeating any form submissions etc).
 144       */
 145      private $returnaction = 'view';
 146  
 147      /** @var array params to be used to return to this page */
 148      private $returnparams = array();
 149  
 150      /** @var string modulename prevents excessive calls to get_string */
 151      private static $modulename = null;
 152  
 153      /** @var string modulenameplural prevents excessive calls to get_string */
 154      private static $modulenameplural = null;
 155  
 156      /** @var array of marking workflow states for the current user */
 157      private $markingworkflowstates = null;
 158  
 159      /** @var bool whether to exclude users with inactive enrolment */
 160      private $showonlyactiveenrol = null;
 161  
 162      /** @var string A key used to identify userlists created by this object. */
 163      private $useridlistid = null;
 164  
 165      /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
 166      private $participants = array();
 167  
 168      /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
 169      private $usersubmissiongroups = array();
 170  
 171      /** @var array cached list of user groups. The cache key will be the user. */
 172      private $usergroups = array();
 173  
 174      /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
 175      private $sharedgroupmembers = array();
 176  
 177      /**
 178       * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
 179       * to update the gradebook.
 180       */
 181      private $mostrecentteamsubmission = null;
 182  
 183      /** @var array Array of error messages encountered during the execution of assignment related operations. */
 184      private $errors = array();
 185  
 186      /**
 187       * Constructor for the base assign class.
 188       *
 189       * Note: For $coursemodule you can supply a stdclass if you like, but it
 190       * will be more efficient to supply a cm_info object.
 191       *
 192       * @param mixed $coursemodulecontext context|null the course module context
 193       *                                   (or the course context if the coursemodule has not been
 194       *                                   created yet).
 195       * @param mixed $coursemodule the current course module if it was already loaded,
 196       *                            otherwise this class will load one from the context as required.
 197       * @param mixed $course the current course  if it was already loaded,
 198       *                      otherwise this class will load one from the context as required.
 199       */
 200      public function __construct($coursemodulecontext, $coursemodule, $course) {
 201          global $SESSION;
 202  
 203          $this->context = $coursemodulecontext;
 204          $this->course = $course;
 205  
 206          // Ensure that $this->coursemodule is a cm_info object (or null).
 207          $this->coursemodule = cm_info::create($coursemodule);
 208  
 209          // Temporary cache only lives for a single request - used to reduce db lookups.
 210          $this->cache = array();
 211  
 212          $this->submissionplugins = $this->load_plugins('assignsubmission');
 213          $this->feedbackplugins = $this->load_plugins('assignfeedback');
 214  
 215          // Extra entropy is required for uniqid() to work on cygwin.
 216          $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
 217  
 218          if (!isset($SESSION->mod_assign_useridlist)) {
 219              $SESSION->mod_assign_useridlist = [];
 220          }
 221      }
 222  
 223      /**
 224       * Set the action and parameters that can be used to return to the current page.
 225       *
 226       * @param string $action The action for the current page
 227       * @param array $params An array of name value pairs which form the parameters
 228       *                      to return to the current page.
 229       * @return void
 230       */
 231      public function register_return_link($action, $params) {
 232          global $PAGE;
 233          $params['action'] = $action;
 234          $cm = $this->get_course_module();
 235          if ($cm) {
 236              $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
 237          } else {
 238              $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
 239          }
 240  
 241          $currenturl->params($params);
 242          $PAGE->set_url($currenturl);
 243      }
 244  
 245      /**
 246       * Return an action that can be used to get back to the current page.
 247       *
 248       * @return string action
 249       */
 250      public function get_return_action() {
 251          global $PAGE;
 252  
 253          // Web services don't set a URL, we should avoid debugging when ussing the url object.
 254          if (!WS_SERVER) {
 255              $params = $PAGE->url->params();
 256          }
 257  
 258          if (!empty($params['action'])) {
 259              return $params['action'];
 260          }
 261          return '';
 262      }
 263  
 264      /**
 265       * Based on the current assignment settings should we display the intro.
 266       *
 267       * @return bool showintro
 268       */
 269      public function show_intro() {
 270          if ($this->get_instance()->alwaysshowdescription ||
 271                  time() > $this->get_instance()->allowsubmissionsfromdate) {
 272              return true;
 273          }
 274          return false;
 275      }
 276  
 277      /**
 278       * Return a list of parameters that can be used to get back to the current page.
 279       *
 280       * @return array params
 281       */
 282      public function get_return_params() {
 283          global $PAGE;
 284  
 285          $params = array();
 286          if (!WS_SERVER) {
 287              $params = $PAGE->url->params();
 288          }
 289          unset($params['id']);
 290          unset($params['action']);
 291          return $params;
 292      }
 293  
 294      /**
 295       * Set the submitted form data.
 296       *
 297       * @param stdClass $data The form data (instance)
 298       */
 299      public function set_instance(stdClass $data) {
 300          $this->instance = $data;
 301      }
 302  
 303      /**
 304       * Set the context.
 305       *
 306       * @param context $context The new context
 307       */
 308      public function set_context(context $context) {
 309          $this->context = $context;
 310      }
 311  
 312      /**
 313       * Set the course data.
 314       *
 315       * @param stdClass $course The course data
 316       */
 317      public function set_course(stdClass $course) {
 318          $this->course = $course;
 319      }
 320  
 321      /**
 322       * Set error message.
 323       *
 324       * @param string $message The error message
 325       */
 326      protected function set_error_message(string $message) {
 327          $this->errors[] = $message;
 328      }
 329  
 330      /**
 331       * Get error messages.
 332       *
 333       * @return array The array of error messages
 334       */
 335      protected function get_error_messages(): array {
 336          return $this->errors;
 337      }
 338  
 339      /**
 340       * Get list of feedback plugins installed.
 341       *
 342       * @return array
 343       */
 344      public function get_feedback_plugins() {
 345          return $this->feedbackplugins;
 346      }
 347  
 348      /**
 349       * Get list of submission plugins installed.
 350       *
 351       * @return array
 352       */
 353      public function get_submission_plugins() {
 354          return $this->submissionplugins;
 355      }
 356  
 357      /**
 358       * Is blind marking enabled and reveal identities not set yet?
 359       *
 360       * @return bool
 361       */
 362      public function is_blind_marking() {
 363          return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
 364      }
 365  
 366      /**
 367       * Is hidden grading enabled?
 368       *
 369       * This just checks the assignment settings. Remember to check
 370       * the user has the 'showhiddengrader' capability too
 371       *
 372       * @return bool
 373       */
 374      public function is_hidden_grader() {
 375          return $this->get_instance()->hidegrader;
 376      }
 377  
 378      /**
 379       * Does an assignment have submission(s) or grade(s) already?
 380       *
 381       * @return bool
 382       */
 383      public function has_submissions_or_grades() {
 384          $allgrades = $this->count_grades();
 385          $allsubmissions = $this->count_submissions();
 386          if (($allgrades == 0) && ($allsubmissions == 0)) {
 387              return false;
 388          }
 389          return true;
 390      }
 391  
 392      /**
 393       * Get a specific submission plugin by its type.
 394       *
 395       * @param string $subtype assignsubmission | assignfeedback
 396       * @param string $type
 397       * @return mixed assign_plugin|null
 398       */
 399      public function get_plugin_by_type($subtype, $type) {
 400          $shortsubtype = substr($subtype, strlen('assign'));
 401          $name = $shortsubtype . 'plugins';
 402          if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
 403              return null;
 404          }
 405          $pluginlist = $this->$name;
 406          foreach ($pluginlist as $plugin) {
 407              if ($plugin->get_type() == $type) {
 408                  return $plugin;
 409              }
 410          }
 411          return null;
 412      }
 413  
 414      /**
 415       * Get a feedback plugin by type.
 416       *
 417       * @param string $type - The type of plugin e.g comments
 418       * @return mixed assign_feedback_plugin|null
 419       */
 420      public function get_feedback_plugin_by_type($type) {
 421          return $this->get_plugin_by_type('assignfeedback', $type);
 422      }
 423  
 424      /**
 425       * Get a submission plugin by type.
 426       *
 427       * @param string $type - The type of plugin e.g comments
 428       * @return mixed assign_submission_plugin|null
 429       */
 430      public function get_submission_plugin_by_type($type) {
 431          return $this->get_plugin_by_type('assignsubmission', $type);
 432      }
 433  
 434      /**
 435       * Load the plugins from the sub folders under subtype.
 436       *
 437       * @param string $subtype - either submission or feedback
 438       * @return array - The sorted list of plugins
 439       */
 440      public function load_plugins($subtype) {
 441          global $CFG;
 442          $result = array();
 443  
 444          $names = core_component::get_plugin_list($subtype);
 445  
 446          foreach ($names as $name => $path) {
 447              if (file_exists($path . '/locallib.php')) {
 448                  require_once ($path . '/locallib.php');
 449  
 450                  $shortsubtype = substr($subtype, strlen('assign'));
 451                  $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
 452  
 453                  $plugin = new $pluginclass($this, $name);
 454  
 455                  if ($plugin instanceof assign_plugin) {
 456                      $idx = $plugin->get_sort_order();
 457                      while (array_key_exists($idx, $result)) {
 458                          $idx +=1;
 459                      }
 460                      $result[$idx] = $plugin;
 461                  }
 462              }
 463          }
 464          ksort($result);
 465          return $result;
 466      }
 467  
 468      /**
 469       * Display the assignment, used by view.php
 470       *
 471       * The assignment is displayed differently depending on your role,
 472       * the settings for the assignment and the status of the assignment.
 473       *
 474       * @param string $action The current action if any.
 475       * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
 476       * @return string - The page output.
 477       */
 478      public function view($action='', $args = array()) {
 479          global $PAGE;
 480  
 481          $o = '';
 482          $mform = null;
 483          $notices = array();
 484          $nextpageparams = array();
 485  
 486          if (!empty($this->get_course_module()->id)) {
 487              $nextpageparams['id'] = $this->get_course_module()->id;
 488          }
 489  
 490          // Handle form submissions first.
 491          if ($action == 'savesubmission') {
 492              $action = 'editsubmission';
 493              if ($this->process_save_submission($mform, $notices)) {
 494                  $action = 'redirect';
 495                  if ($this->can_grade()) {
 496                      $nextpageparams['action'] = 'grading';
 497                  } else {
 498                      $nextpageparams['action'] = 'view';
 499                  }
 500              }
 501          } else if ($action == 'editprevioussubmission') {
 502              $action = 'editsubmission';
 503              if ($this->process_copy_previous_attempt($notices)) {
 504                  $action = 'redirect';
 505                  $nextpageparams['action'] = 'editsubmission';
 506              }
 507          } else if ($action == 'lock') {
 508              $this->process_lock_submission();
 509              $action = 'redirect';
 510              $nextpageparams['action'] = 'grading';
 511          } else if ($action == 'removesubmission') {
 512              $this->process_remove_submission();
 513              $action = 'redirect';
 514              if ($this->can_grade()) {
 515                  $nextpageparams['action'] = 'grading';
 516              } else {
 517                  $nextpageparams['action'] = 'view';
 518              }
 519          } else if ($action == 'addattempt') {
 520              $this->process_add_attempt(required_param('userid', PARAM_INT));
 521              $action = 'redirect';
 522              $nextpageparams['action'] = 'grading';
 523          } else if ($action == 'reverttodraft') {
 524              $this->process_revert_to_draft();
 525              $action = 'redirect';
 526              $nextpageparams['action'] = 'grading';
 527          } else if ($action == 'unlock') {
 528              $this->process_unlock_submission();
 529              $action = 'redirect';
 530              $nextpageparams['action'] = 'grading';
 531          } else if ($action == 'setbatchmarkingworkflowstate') {
 532              $this->process_set_batch_marking_workflow_state();
 533              $action = 'redirect';
 534              $nextpageparams['action'] = 'grading';
 535          } else if ($action == 'setbatchmarkingallocation') {
 536              $this->process_set_batch_marking_allocation();
 537              $action = 'redirect';
 538              $nextpageparams['action'] = 'grading';
 539          } else if ($action == 'confirmsubmit') {
 540              $action = 'submit';
 541              if ($this->process_submit_for_grading($mform, $notices)) {
 542                  $action = 'redirect';
 543                  $nextpageparams['action'] = 'view';
 544              } else if ($notices) {
 545                  $action = 'viewsubmitforgradingerror';
 546              }
 547          } else if ($action == 'submitotherforgrading') {
 548              if ($this->process_submit_other_for_grading($mform, $notices)) {
 549                  $action = 'redirect';
 550                  $nextpageparams['action'] = 'grading';
 551              } else {
 552                  $action = 'viewsubmitforgradingerror';
 553              }
 554          } else if ($action == 'gradingbatchoperation') {
 555              $action = $this->process_grading_batch_operation($mform);
 556              if ($action == 'grading') {
 557                  $action = 'redirect';
 558                  $nextpageparams['action'] = 'grading';
 559              }
 560          } else if ($action == 'submitgrade') {
 561              if (optional_param('saveandshownext', null, PARAM_RAW)) {
 562                  // Save and show next.
 563                  $action = 'grade';
 564                  if ($this->process_save_grade($mform)) {
 565                      $action = 'redirect';
 566                      $nextpageparams['action'] = 'grade';
 567                      $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
 568                      $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
 569                  }
 570              } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
 571                  $action = 'redirect';
 572                  $nextpageparams['action'] = 'grade';
 573                  $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
 574                  $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
 575              } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
 576                  $action = 'redirect';
 577                  $nextpageparams['action'] = 'grade';
 578                  $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
 579                  $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
 580              } else if (optional_param('savegrade', null, PARAM_RAW)) {
 581                  // Save changes button.
 582                  $action = 'grade';
 583                  if ($this->process_save_grade($mform)) {
 584                      $action = 'redirect';
 585                      $nextpageparams['action'] = 'savegradingresult';
 586                  }
 587              } else {
 588                  // Cancel button.
 589                  $action = 'redirect';
 590                  $nextpageparams['action'] = 'grading';
 591              }
 592          } else if ($action == 'quickgrade') {
 593              $message = $this->process_save_quick_grades();
 594              $action = 'quickgradingresult';
 595          } else if ($action == 'saveoptions') {
 596              $this->process_save_grading_options();
 597              $action = 'redirect';
 598              $nextpageparams['action'] = 'grading';
 599          } else if ($action == 'saveextension') {
 600              $action = 'grantextension';
 601              if ($this->process_save_extension($mform)) {
 602                  $action = 'redirect';
 603                  $nextpageparams['action'] = 'grading';
 604              }
 605          } else if ($action == 'revealidentitiesconfirm') {
 606              $this->process_reveal_identities();
 607              $action = 'redirect';
 608              $nextpageparams['action'] = 'grading';
 609          }
 610  
 611          $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
 612                                'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
 613          $this->register_return_link($action, $returnparams);
 614  
 615          // Include any page action as part of the body tag CSS id.
 616          if (!empty($action)) {
 617              $PAGE->set_pagetype('mod-assign-' . $action);
 618          }
 619          // Now show the right view page.
 620          if ($action == 'redirect') {
 621              $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
 622              $messages = '';
 623              $messagetype = \core\output\notification::NOTIFY_INFO;
 624              $errors = $this->get_error_messages();
 625              if (!empty($errors)) {
 626                  $messages = html_writer::alist($errors, ['class' => 'mb-1 mt-1']);
 627                  $messagetype = \core\output\notification::NOTIFY_ERROR;
 628              }
 629              redirect($nextpageurl, $messages, null, $messagetype);
 630              return;
 631          } else if ($action == 'savegradingresult') {
 632              $message = get_string('gradingchangessaved', 'assign');
 633              $o .= $this->view_savegrading_result($message);
 634          } else if ($action == 'quickgradingresult') {
 635              $mform = null;
 636              $o .= $this->view_quickgrading_result($message);
 637          } else if ($action == 'gradingpanel') {
 638              $o .= $this->view_single_grading_panel($args);
 639          } else if ($action == 'grade') {
 640              $o .= $this->view_single_grade_page($mform);
 641          } else if ($action == 'viewpluginassignfeedback') {
 642              $o .= $this->view_plugin_content('assignfeedback');
 643          } else if ($action == 'viewpluginassignsubmission') {
 644              $o .= $this->view_plugin_content('assignsubmission');
 645          } else if ($action == 'editsubmission') {
 646              $o .= $this->view_edit_submission_page($mform, $notices);
 647          } else if ($action == 'grader') {
 648              $o .= $this->view_grader();
 649          } else if ($action == 'grading') {
 650              $o .= $this->view_grading_page();
 651          } else if ($action == 'downloadall') {
 652              $o .= $this->download_submissions();
 653          } else if ($action == 'submit') {
 654              $o .= $this->check_submit_for_grading($mform);
 655          } else if ($action == 'grantextension') {
 656              $o .= $this->view_grant_extension($mform);
 657          } else if ($action == 'revealidentities') {
 658              $o .= $this->view_reveal_identities_confirm($mform);
 659          } else if ($action == 'removesubmissionconfirm') {
 660              $o .= $this->view_remove_submission_confirm();
 661          } else if ($action == 'plugingradingbatchoperation') {
 662              $o .= $this->view_plugin_grading_batch_operation($mform);
 663          } else if ($action == 'viewpluginpage') {
 664               $o .= $this->view_plugin_page();
 665          } else if ($action == 'viewcourseindex') {
 666               $o .= $this->view_course_index();
 667          } else if ($action == 'viewbatchsetmarkingworkflowstate') {
 668               $o .= $this->view_batch_set_workflow_state($mform);
 669          } else if ($action == 'viewbatchmarkingallocation') {
 670              $o .= $this->view_batch_markingallocation($mform);
 671          } else if ($action == 'viewsubmitforgradingerror') {
 672              $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
 673          } else if ($action == 'fixrescalednullgrades') {
 674              $o .= $this->view_fix_rescaled_null_grades();
 675          } else {
 676              $o .= $this->view_submission_page();
 677          }
 678  
 679          return $o;
 680      }
 681  
 682      /**
 683       * Add this instance to the database.
 684       *
 685       * @param stdClass $formdata The data submitted from the form
 686       * @param bool $callplugins This is used to skip the plugin code
 687       *             when upgrading an old assignment to a new one (the plugins get called manually)
 688       * @return mixed false if an error occurs or the int id of the new instance
 689       */
 690      public function add_instance(stdClass $formdata, $callplugins) {
 691          global $DB;
 692          $adminconfig = $this->get_admin_config();
 693  
 694          $err = '';
 695  
 696          // Add the database record.
 697          $update = new stdClass();
 698          $update->name = $formdata->name;
 699          $update->timemodified = time();
 700          $update->timecreated = time();
 701          $update->course = $formdata->course;
 702          $update->courseid = $formdata->course;
 703          $update->intro = $formdata->intro;
 704          $update->introformat = $formdata->introformat;
 705          $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
 706          $update->submissiondrafts = $formdata->submissiondrafts;
 707          $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
 708          $update->sendnotifications = $formdata->sendnotifications;
 709          $update->sendlatenotifications = $formdata->sendlatenotifications;
 710          $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
 711          if (isset($formdata->sendstudentnotifications)) {
 712              $update->sendstudentnotifications = $formdata->sendstudentnotifications;
 713          }
 714          $update->duedate = $formdata->duedate;
 715          $update->cutoffdate = $formdata->cutoffdate;
 716          $update->gradingduedate = $formdata->gradingduedate;
 717          $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
 718          $update->grade = $formdata->grade;
 719          $update->completionsubmit = !empty($formdata->completionsubmit);
 720          $update->teamsubmission = $formdata->teamsubmission;
 721          $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
 722          if (isset($formdata->teamsubmissiongroupingid)) {
 723              $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
 724          }
 725          $update->blindmarking = $formdata->blindmarking;
 726          if (isset($formdata->hidegrader)) {
 727              $update->hidegrader = $formdata->hidegrader;
 728          }
 729          $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
 730          if (!empty($formdata->attemptreopenmethod)) {
 731              $update->attemptreopenmethod = $formdata->attemptreopenmethod;
 732          }
 733          if (!empty($formdata->maxattempts)) {
 734              $update->maxattempts = $formdata->maxattempts;
 735          }
 736          if (isset($formdata->preventsubmissionnotingroup)) {
 737              $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
 738          }
 739          $update->markingworkflow = $formdata->markingworkflow;
 740          $update->markingallocation = $formdata->markingallocation;
 741          if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
 742              $update->markingallocation = 0;
 743          }
 744  
 745          $returnid = $DB->insert_record('assign', $update);
 746          $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
 747          // Cache the course record.
 748          $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
 749  
 750          $this->save_intro_draft_files($formdata);
 751  
 752          if ($callplugins) {
 753              // Call save_settings hook for submission plugins.
 754              foreach ($this->submissionplugins as $plugin) {
 755                  if (!$this->update_plugin_instance($plugin, $formdata)) {
 756                      print_error($plugin->get_error());
 757                      return false;
 758                  }
 759              }
 760              foreach ($this->feedbackplugins as $plugin) {
 761                  if (!$this->update_plugin_instance($plugin, $formdata)) {
 762                      print_error($plugin->get_error());
 763                      return false;
 764                  }
 765              }
 766  
 767              // In the case of upgrades the coursemodule has not been set,
 768              // so we need to wait before calling these two.
 769              $this->update_calendar($formdata->coursemodule);
 770              if (!empty($formdata->completionexpected)) {
 771                  \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance,
 772                          $formdata->completionexpected);
 773              }
 774              $this->update_gradebook(false, $formdata->coursemodule);
 775  
 776          }
 777  
 778          $update = new stdClass();
 779          $update->id = $this->get_instance()->id;
 780          $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
 781          $DB->update_record('assign', $update);
 782  
 783          return $returnid;
 784      }
 785  
 786      /**
 787       * Delete all grades from the gradebook for this assignment.
 788       *
 789       * @return bool
 790       */
 791      protected function delete_grades() {
 792          global $CFG;
 793  
 794          $result = grade_update('mod/assign',
 795                                 $this->get_course()->id,
 796                                 'mod',
 797                                 'assign',
 798                                 $this->get_instance()->id,
 799                                 0,
 800                                 null,
 801                                 array('deleted'=>1));
 802          return $result == GRADE_UPDATE_OK;
 803      }
 804  
 805      /**
 806       * Delete this instance from the database.
 807       *
 808       * @return bool false if an error occurs
 809       */
 810      public function delete_instance() {
 811          global $DB;
 812          $result = true;
 813  
 814          foreach ($this->submissionplugins as $plugin) {
 815              if (!$plugin->delete_instance()) {
 816                  print_error($plugin->get_error());
 817                  $result = false;
 818              }
 819          }
 820          foreach ($this->feedbackplugins as $plugin) {
 821              if (!$plugin->delete_instance()) {
 822                  print_error($plugin->get_error());
 823                  $result = false;
 824              }
 825          }
 826  
 827          // Delete files associated with this assignment.
 828          $fs = get_file_storage();
 829          if (! $fs->delete_area_files($this->context->id) ) {
 830              $result = false;
 831          }
 832  
 833          $this->delete_all_overrides();
 834  
 835          // Delete_records will throw an exception if it fails - so no need for error checking here.
 836          $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
 837          $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
 838          $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
 839          $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
 840          $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
 841  
 842          // Delete items from the gradebook.
 843          if (! $this->delete_grades()) {
 844              $result = false;
 845          }
 846  
 847          // Delete the instance.
 848          // We must delete the module record after we delete the grade item.
 849          $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
 850  
 851          return $result;
 852      }
 853  
 854      /**
 855       * Deletes a assign override from the database and clears any corresponding calendar events
 856       *
 857       * @param int $overrideid The id of the override being deleted
 858       * @return bool true on success
 859       */
 860      public function delete_override($overrideid) {
 861          global $CFG, $DB;
 862  
 863          require_once($CFG->dirroot . '/calendar/lib.php');
 864  
 865          $cm = $this->get_course_module();
 866          if (empty($cm)) {
 867              $instance = $this->get_instance();
 868              $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course);
 869          }
 870  
 871          $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
 872  
 873          // Delete the events.
 874          $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
 875          if (isset($override->userid)) {
 876              $conds['userid'] = $override->userid;
 877              $cachekey = "{$cm->instance}_u_{$override->userid}";
 878          } else {
 879              $conds['groupid'] = $override->groupid;
 880              $cachekey = "{$cm->instance}_g_{$override->groupid}";
 881          }
 882          $events = $DB->get_records('event', $conds);
 883          foreach ($events as $event) {
 884              $eventold = calendar_event::load($event);
 885              $eventold->delete();
 886          }
 887  
 888          $DB->delete_records('assign_overrides', array('id' => $overrideid));
 889          cache::make('mod_assign', 'overrides')->delete($cachekey);
 890  
 891          // Set the common parameters for one of the events we will be triggering.
 892          $params = array(
 893              'objectid' => $override->id,
 894              'context' => context_module::instance($cm->id),
 895              'other' => array(
 896                  'assignid' => $override->assignid
 897              )
 898          );
 899          // Determine which override deleted event to fire.
 900          if (!empty($override->userid)) {
 901              $params['relateduserid'] = $override->userid;
 902              $event = \mod_assign\event\user_override_deleted::create($params);
 903          } else {
 904              $params['other']['groupid'] = $override->groupid;
 905              $event = \mod_assign\event\group_override_deleted::create($params);
 906          }
 907  
 908          // Trigger the override deleted event.
 909          $event->add_record_snapshot('assign_overrides', $override);
 910          $event->trigger();
 911  
 912          return true;
 913      }
 914  
 915      /**
 916       * Deletes all assign overrides from the database and clears any corresponding calendar events
 917       */
 918      public function delete_all_overrides() {
 919          global $DB;
 920  
 921          $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id');
 922          foreach ($overrides as $override) {
 923              $this->delete_override($override->id);
 924          }
 925      }
 926  
 927      /**
 928       * Updates the assign properties with override information for a user.
 929       *
 930       * Algorithm:  For each assign setting, if there is a matching user-specific override,
 931       *   then use that otherwise, if there are group-specific overrides, return the most
 932       *   lenient combination of them.  If neither applies, leave the assign setting unchanged.
 933       *
 934       * @param int $userid The userid.
 935       */
 936      public function update_effective_access($userid) {
 937  
 938          $override = $this->override_exists($userid);
 939  
 940          // Merge with assign defaults.
 941          $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
 942          foreach ($keys as $key) {
 943              if (isset($override->{$key})) {
 944                  $this->get_instance($userid)->{$key} = $override->{$key};
 945              }
 946          }
 947  
 948      }
 949  
 950      /**
 951       * Returns whether an assign has any overrides.
 952       *
 953       * @return true if any, false if not
 954       */
 955      public function has_overrides() {
 956          global $DB;
 957  
 958          $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
 959  
 960          if ($override) {
 961              return true;
 962          }
 963  
 964          return false;
 965      }
 966  
 967      /**
 968       * Returns user override
 969       *
 970       * Algorithm:  For each assign setting, if there is a matching user-specific override,
 971       *   then use that otherwise, if there are group-specific overrides, use the one with the
 972       *   lowest sort order. If neither applies, leave the assign setting unchanged.
 973       *
 974       * @param int $userid The userid.
 975       * @return stdClass The override
 976       */
 977      public function override_exists($userid) {
 978          global $DB;
 979  
 980          // Gets an assoc array containing the keys for defined user overrides only.
 981          $getuseroverride = function($userid) use ($DB) {
 982              $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
 983              return $useroverride ? get_object_vars($useroverride) : [];
 984          };
 985  
 986          // Gets an assoc array containing the keys for defined group overrides only.
 987          $getgroupoverride = function($userid) use ($DB) {
 988              $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
 989  
 990              if (empty($groupings[0])) {
 991                  return [];
 992              }
 993  
 994              // Select all overrides that apply to the User's groups.
 995              list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
 996              $sql = "SELECT * FROM {assign_overrides}
 997                      WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
 998              $params[] = $this->get_instance()->id;
 999              $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
1000  
1001              return $groupoverride ? get_object_vars($groupoverride) : [];
1002          };
1003  
1004          // Later arguments clobber earlier ones with array_merge. The two helper functions
1005          // return arrays containing keys for only the defined overrides. So we get the
1006          // desired behaviour as per the algorithm.
1007          return (object)array_merge(
1008              ['duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
1009              $getgroupoverride($userid),
1010              $getuseroverride($userid)
1011          );
1012      }
1013  
1014      /**
1015       * Check if the given calendar_event is either a user or group override
1016       * event.
1017       *
1018       * @return bool
1019       */
1020      public function is_override_calendar_event(\calendar_event $event) {
1021          global $DB;
1022  
1023          if (!isset($event->modulename)) {
1024              return false;
1025          }
1026  
1027          if ($event->modulename != 'assign') {
1028              return false;
1029          }
1030  
1031          if (!isset($event->instance)) {
1032              return false;
1033          }
1034  
1035          if (!isset($event->userid) && !isset($event->groupid)) {
1036              return false;
1037          }
1038  
1039          $overrideparams = [
1040              'assignid' => $event->instance
1041          ];
1042  
1043          if (isset($event->groupid)) {
1044              $overrideparams['groupid'] = $event->groupid;
1045          } else if (isset($event->userid)) {
1046              $overrideparams['userid'] = $event->userid;
1047          }
1048  
1049          if ($DB->get_record('assign_overrides', $overrideparams)) {
1050              return true;
1051          } else {
1052              return false;
1053          }
1054      }
1055  
1056      /**
1057       * This function calculates the minimum and maximum cutoff values for the timestart of
1058       * the given event.
1059       *
1060       * It will return an array with two values, the first being the minimum cutoff value and
1061       * the second being the maximum cutoff value. Either or both values can be null, which
1062       * indicates there is no minimum or maximum, respectively.
1063       *
1064       * If a cutoff is required then the function must return an array containing the cutoff
1065       * timestamp and error string to display to the user if the cutoff value is violated.
1066       *
1067       * A minimum and maximum cutoff return value will look like:
1068       * [
1069       *     [1505704373, 'The due date must be after the sbumission start date'],
1070       *     [1506741172, 'The due date must be before the cutoff date']
1071       * ]
1072       *
1073       * If the event does not have a valid timestart range then [false, false] will
1074       * be returned.
1075       *
1076       * @param calendar_event $event The calendar event to get the time range for
1077       * @return array
1078       */
1079      function get_valid_calendar_event_timestart_range(\calendar_event $event) {
1080          $instance = $this->get_instance();
1081          $submissionsfromdate = $instance->allowsubmissionsfromdate;
1082          $cutoffdate = $instance->cutoffdate;
1083          $duedate = $instance->duedate;
1084          $gradingduedate = $instance->gradingduedate;
1085          $mindate = null;
1086          $maxdate = null;
1087  
1088          if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
1089              // This check is in here because due date events are currently
1090              // the only events that can be overridden, so we can save a DB
1091              // query if we don't bother checking other events.
1092              if ($this->is_override_calendar_event($event)) {
1093                  // This is an override event so there is no valid timestart
1094                  // range to set it to.
1095                  return [false, false];
1096              }
1097  
1098              if ($submissionsfromdate) {
1099                  $mindate = [
1100                      $submissionsfromdate,
1101                      get_string('duedatevalidation', 'assign'),
1102                  ];
1103              }
1104  
1105              if ($cutoffdate) {
1106                  $maxdate = [
1107                      $cutoffdate,
1108                      get_string('cutoffdatevalidation', 'assign'),
1109                  ];
1110              }
1111  
1112              if ($gradingduedate) {
1113                  // If we don't have a cutoff date or we've got a grading due date
1114                  // that is earlier than the cutoff then we should use that as the
1115                  // upper limit for the due date.
1116                  if (!$cutoffdate || $gradingduedate < $cutoffdate) {
1117                      $maxdate = [
1118                          $gradingduedate,
1119                          get_string('gradingdueduedatevalidation', 'assign'),
1120                      ];
1121                  }
1122              }
1123          } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
1124              if ($duedate) {
1125                  $mindate = [
1126                      $duedate,
1127                      get_string('gradingdueduedatevalidation', 'assign'),
1128                  ];
1129              } else if ($submissionsfromdate) {
1130                  $mindate = [
1131                      $submissionsfromdate,
1132                      get_string('gradingduefromdatevalidation', 'assign'),
1133                  ];
1134              }
1135          }
1136  
1137          return [$mindate, $maxdate];
1138      }
1139  
1140      /**
1141       * Actual implementation of the reset course functionality, delete all the
1142       * assignment submissions for course $data->courseid.
1143       *
1144       * @param stdClass $data the data submitted from the reset course.
1145       * @return array status array
1146       */
1147      public function reset_userdata($data) {
1148          global $CFG, $DB;
1149  
1150          $componentstr = get_string('modulenameplural', 'assign');
1151          $status = array();
1152  
1153          $fs = get_file_storage();
1154          if (!empty($data->reset_assign_submissions)) {
1155              // Delete files associated with this assignment.
1156              foreach ($this->submissionplugins as $plugin) {
1157                  $fileareas = array();
1158                  $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1159                  $fileareas = $plugin->get_file_areas();
1160                  foreach ($fileareas as $filearea => $notused) {
1161                      $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1162                  }
1163  
1164                  if (!$plugin->delete_instance()) {
1165                      $status[] = array('component'=>$componentstr,
1166                                        'item'=>get_string('deleteallsubmissions', 'assign'),
1167                                        'error'=>$plugin->get_error());
1168                  }
1169              }
1170  
1171              foreach ($this->feedbackplugins as $plugin) {
1172                  $fileareas = array();
1173                  $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1174                  $fileareas = $plugin->get_file_areas();
1175                  foreach ($fileareas as $filearea => $notused) {
1176                      $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1177                  }
1178  
1179                  if (!$plugin->delete_instance()) {
1180                      $status[] = array('component'=>$componentstr,
1181                                        'item'=>get_string('deleteallsubmissions', 'assign'),
1182                                        'error'=>$plugin->get_error());
1183                  }
1184              }
1185  
1186              $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
1187              list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
1188  
1189              $DB->delete_records_select('assign_submission', "assignment $sql", $params);
1190              $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
1191  
1192              $status[] = array('component'=>$componentstr,
1193                                'item'=>get_string('deleteallsubmissions', 'assign'),
1194                                'error'=>false);
1195  
1196              if (!empty($data->reset_gradebook_grades)) {
1197                  $DB->delete_records_select('assign_grades', "assignment $sql", $params);
1198                  // Remove all grades from gradebook.
1199                  require_once($CFG->dirroot.'/mod/assign/lib.php');
1200                  assign_reset_gradebook($data->courseid);
1201              }
1202  
1203              // Reset revealidentities for assign if blindmarking is enabled.
1204              if ($this->get_instance()->blindmarking) {
1205                  $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
1206              }
1207          }
1208  
1209          $purgeoverrides = false;
1210  
1211          // Remove user overrides.
1212          if (!empty($data->reset_assign_user_overrides)) {
1213              $DB->delete_records_select('assign_overrides',
1214                  'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
1215              $status[] = array(
1216                  'component' => $componentstr,
1217                  'item' => get_string('useroverridesdeleted', 'assign'),
1218                  'error' => false);
1219              $purgeoverrides = true;
1220          }
1221          // Remove group overrides.
1222          if (!empty($data->reset_assign_group_overrides)) {
1223              $DB->delete_records_select('assign_overrides',
1224                  'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
1225              $status[] = array(
1226                  'component' => $componentstr,
1227                  'item' => get_string('groupoverridesdeleted', 'assign'),
1228                  'error' => false);
1229              $purgeoverrides = true;
1230          }
1231  
1232          // Updating dates - shift may be negative too.
1233          if ($data->timeshift) {
1234              $DB->execute("UPDATE {assign_overrides}
1235                           SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
1236                         WHERE assignid = ? AND allowsubmissionsfromdate <> 0",
1237                  array($data->timeshift, $this->get_instance()->id));
1238              $DB->execute("UPDATE {assign_overrides}
1239                           SET duedate = duedate + ?
1240                         WHERE assignid = ? AND duedate <> 0",
1241                  array($data->timeshift, $this->get_instance()->id));
1242              $DB->execute("UPDATE {assign_overrides}
1243                           SET cutoffdate = cutoffdate + ?
1244                         WHERE assignid =? AND cutoffdate <> 0",
1245                  array($data->timeshift, $this->get_instance()->id));
1246  
1247              $purgeoverrides = true;
1248  
1249              // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
1250              // See MDL-9367.
1251              shift_course_mod_dates('assign',
1252                                      array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
1253                                      $data->timeshift,
1254                                      $data->courseid, $this->get_instance()->id);
1255              $status[] = array('component'=>$componentstr,
1256                                'item'=>get_string('datechanged'),
1257                                'error'=>false);
1258          }
1259  
1260          if ($purgeoverrides) {
1261              cache::make('mod_assign', 'overrides')->purge();
1262          }
1263  
1264          return $status;
1265      }
1266  
1267      /**
1268       * Update the settings for a single plugin.
1269       *
1270       * @param assign_plugin $plugin The plugin to update
1271       * @param stdClass $formdata The form data
1272       * @return bool false if an error occurs
1273       */
1274      protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
1275          if ($plugin->is_visible()) {
1276              $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1277              if (!empty($formdata->$enabledname)) {
1278                  $plugin->enable();
1279                  if (!$plugin->save_settings($formdata)) {
1280                      print_error($plugin->get_error());
1281                      return false;
1282                  }
1283              } else {
1284                  $plugin->disable();
1285              }
1286          }
1287          return true;
1288      }
1289  
1290      /**
1291       * Update the gradebook information for this assignment.
1292       *
1293       * @param bool $reset If true, will reset all grades in the gradbook for this assignment
1294       * @param int $coursemoduleid This is required because it might not exist in the database yet
1295       * @return bool
1296       */
1297      public function update_gradebook($reset, $coursemoduleid) {
1298          global $CFG;
1299  
1300          require_once($CFG->dirroot.'/mod/assign/lib.php');
1301          $assign = clone $this->get_instance();
1302          $assign->cmidnumber = $coursemoduleid;
1303  
1304          // Set assign gradebook feedback plugin status (enabled and visible).
1305          $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
1306  
1307          $param = null;
1308          if ($reset) {
1309              $param = 'reset';
1310          }
1311  
1312          return assign_grade_item_update($assign, $param);
1313      }
1314  
1315      /**
1316       * Get the marking table page size
1317       *
1318       * @return integer
1319       */
1320      public function get_assign_perpage() {
1321          $perpage = (int) get_user_preferences('assign_perpage', 10);
1322          $adminconfig = $this->get_admin_config();
1323          $maxperpage = -1;
1324          if (isset($adminconfig->maxperpage)) {
1325              $maxperpage = $adminconfig->maxperpage;
1326          }
1327          if (isset($maxperpage) &&
1328              $maxperpage != -1 &&
1329              ($perpage == -1 || $perpage > $maxperpage)) {
1330              $perpage = $maxperpage;
1331          }
1332          return $perpage;
1333      }
1334  
1335      /**
1336       * Load and cache the admin config for this module.
1337       *
1338       * @return stdClass the plugin config
1339       */
1340      public function get_admin_config() {
1341          if ($this->adminconfig) {
1342              return $this->adminconfig;
1343          }
1344          $this->adminconfig = get_config('assign');
1345          return $this->adminconfig;
1346      }
1347  
1348      /**
1349       * Update the calendar entries for this assignment.
1350       *
1351       * @param int $coursemoduleid - Required to pass this in because it might
1352       *                              not exist in the database yet.
1353       * @return bool
1354       */
1355      public function update_calendar($coursemoduleid) {
1356          global $DB, $CFG;
1357          require_once($CFG->dirroot.'/calendar/lib.php');
1358  
1359          // Special case for add_instance as the coursemodule has not been set yet.
1360          $instance = $this->get_instance();
1361  
1362          // Start with creating the event.
1363          $event = new stdClass();
1364          $event->modulename  = 'assign';
1365          $event->courseid = $instance->course;
1366          $event->groupid = 0;
1367          $event->userid  = 0;
1368          $event->instance  = $instance->id;
1369          $event->type = CALENDAR_EVENT_TYPE_ACTION;
1370  
1371          // Convert the links to pluginfile. It is a bit hacky but at this stage the files
1372          // might not have been saved in the module area yet.
1373          $intro = $instance->intro;
1374          if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
1375              $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
1376          }
1377  
1378          // We need to remove the links to files as the calendar is not ready
1379          // to support module events with file areas.
1380          $intro = strip_pluginfile_content($intro);
1381          if ($this->show_intro()) {
1382              $event->description = array(
1383                  'text' => $intro,
1384                  'format' => $instance->introformat
1385              );
1386          } else {
1387              $event->description = array(
1388                  'text' => '',
1389                  'format' => $instance->introformat
1390              );
1391          }
1392  
1393          $eventtype = ASSIGN_EVENT_TYPE_DUE;
1394          if ($instance->duedate) {
1395              $event->name = get_string('calendardue', 'assign', $instance->name);
1396              $event->eventtype = $eventtype;
1397              $event->timestart = $instance->duedate;
1398              $event->timesort = $instance->duedate;
1399              $select = "modulename = :modulename
1400                         AND instance = :instance
1401                         AND eventtype = :eventtype
1402                         AND groupid = 0
1403                         AND courseid <> 0";
1404              $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
1405              $event->id = $DB->get_field_select('event', 'id', $select, $params);
1406  
1407              // Now process the event.
1408              if ($event->id) {
1409                  $calendarevent = calendar_event::load($event->id);
1410                  $calendarevent->update($event, false);
1411              } else {
1412                  calendar_event::create($event, false);
1413              }
1414          } else {
1415              $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1416                  'eventtype' => $eventtype));
1417          }
1418  
1419          $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE;
1420          if ($instance->gradingduedate) {
1421              $event->name = get_string('calendargradingdue', 'assign', $instance->name);
1422              $event->eventtype = $eventtype;
1423              $event->timestart = $instance->gradingduedate;
1424              $event->timesort = $instance->gradingduedate;
1425              $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign',
1426                  'instance' => $instance->id, 'eventtype' => $event->eventtype));
1427  
1428              // Now process the event.
1429              if ($event->id) {
1430                  $calendarevent = calendar_event::load($event->id);
1431                  $calendarevent->update($event, false);
1432              } else {
1433                  calendar_event::create($event, false);
1434              }
1435          } else {
1436              $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1437                  'eventtype' => $eventtype));
1438          }
1439  
1440          return true;
1441      }
1442  
1443      /**
1444       * Update this instance in the database.
1445       *
1446       * @param stdClass $formdata - the data submitted from the form
1447       * @return bool false if an error occurs
1448       */
1449      public function update_instance($formdata) {
1450          global $DB;
1451          $adminconfig = $this->get_admin_config();
1452  
1453          $update = new stdClass();
1454          $update->id = $formdata->instance;
1455          $update->name = $formdata->name;
1456          $update->timemodified = time();
1457          $update->course = $formdata->course;
1458          $update->intro = $formdata->intro;
1459          $update->introformat = $formdata->introformat;
1460          $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
1461          $update->submissiondrafts = $formdata->submissiondrafts;
1462          $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
1463          $update->sendnotifications = $formdata->sendnotifications;
1464          $update->sendlatenotifications = $formdata->sendlatenotifications;
1465          $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
1466          if (isset($formdata->sendstudentnotifications)) {
1467              $update->sendstudentnotifications = $formdata->sendstudentnotifications;
1468          }
1469          $update->duedate = $formdata->duedate;
1470          $update->cutoffdate = $formdata->cutoffdate;
1471          $update->gradingduedate = $formdata->gradingduedate;
1472          $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
1473          $update->grade = $formdata->grade;
1474          if (!empty($formdata->completionunlocked)) {
1475              $update->completionsubmit = !empty($formdata->completionsubmit);
1476          }
1477          $update->teamsubmission = $formdata->teamsubmission;
1478          $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
1479          if (isset($formdata->teamsubmissiongroupingid)) {
1480              $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
1481          }
1482          if (isset($formdata->hidegrader)) {
1483              $update->hidegrader = $formdata->hidegrader;
1484          }
1485          $update->blindmarking = $formdata->blindmarking;
1486          $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
1487          if (!empty($formdata->attemptreopenmethod)) {
1488              $update->attemptreopenmethod = $formdata->attemptreopenmethod;
1489          }
1490          if (!empty($formdata->maxattempts)) {
1491              $update->maxattempts = $formdata->maxattempts;
1492          }
1493          if (isset($formdata->preventsubmissionnotingroup)) {
1494              $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
1495          }
1496          $update->markingworkflow = $formdata->markingworkflow;
1497          $update->markingallocation = $formdata->markingallocation;
1498          if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
1499              $update->markingallocation = 0;
1500          }
1501  
1502          $result = $DB->update_record('assign', $update);
1503          $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
1504  
1505          $this->save_intro_draft_files($formdata);
1506  
1507          // Load the assignment so the plugins have access to it.
1508  
1509          // Call save_settings hook for submission plugins.
1510          foreach ($this->submissionplugins as $plugin) {
1511              if (!$this->update_plugin_instance($plugin, $formdata)) {
1512                  print_error($plugin->get_error());
1513                  return false;
1514              }
1515          }
1516          foreach ($this->feedbackplugins as $plugin) {
1517              if (!$this->update_plugin_instance($plugin, $formdata)) {
1518                  print_error($plugin->get_error());
1519                  return false;
1520              }
1521          }
1522  
1523          $this->update_calendar($this->get_course_module()->id);
1524          $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null;
1525          \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance,
1526                  $completionexpected);
1527          $this->update_gradebook(false, $this->get_course_module()->id);
1528  
1529          $update = new stdClass();
1530          $update->id = $this->get_instance()->id;
1531          $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1532          $DB->update_record('assign', $update);
1533  
1534          return $result;
1535      }
1536  
1537      /**
1538       * Save the attachments in the draft areas.
1539       *
1540       * @param stdClass $formdata
1541       */
1542      protected function save_intro_draft_files($formdata) {
1543          if (isset($formdata->introattachments)) {
1544              file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1545                                         'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1546          }
1547      }
1548  
1549      /**
1550       * Add elements in grading plugin form.
1551       *
1552       * @param mixed $grade stdClass|null
1553       * @param MoodleQuickForm $mform
1554       * @param stdClass $data
1555       * @param int $userid - The userid we are grading
1556       * @return void
1557       */
1558      protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1559          foreach ($this->feedbackplugins as $plugin) {
1560              if ($plugin->is_enabled() && $plugin->is_visible()) {
1561                  $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1562              }
1563          }
1564      }
1565  
1566  
1567  
1568      /**
1569       * Add one plugins settings to edit plugin form.
1570       *
1571       * @param assign_plugin $plugin The plugin to add the settings from
1572       * @param MoodleQuickForm $mform The form to add the configuration settings to.
1573       *                               This form is modified directly (not returned).
1574       * @param array $pluginsenabled A list of form elements to be added to a group.
1575       *                              The new element is added to this array by this function.
1576       * @return void
1577       */
1578      protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1579          global $CFG;
1580          if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1581              $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1582              $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1583              $mform->setType($name, PARAM_BOOL);
1584              $plugin->get_settings($mform);
1585          } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1586              $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1587              $label = $plugin->get_name();
1588              $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1589              $helpicon = $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1590              $pluginsenabled[] = $mform->createElement('static', '', '', $helpicon);
1591  
1592              $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1593              if ($plugin->get_config('enabled') !== false) {
1594                  $default = $plugin->is_enabled();
1595              }
1596              $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1597  
1598              $plugin->get_settings($mform);
1599  
1600          }
1601      }
1602  
1603      /**
1604       * Add settings to edit plugin form.
1605       *
1606       * @param MoodleQuickForm $mform The form to add the configuration settings to.
1607       *                               This form is modified directly (not returned).
1608       * @return void
1609       */
1610      public function add_all_plugin_settings(MoodleQuickForm $mform) {
1611          $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1612  
1613          $submissionpluginsenabled = array();
1614          $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1615          foreach ($this->submissionplugins as $plugin) {
1616              $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1617          }
1618          $group->setElements($submissionpluginsenabled);
1619  
1620          $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1621          $feedbackpluginsenabled = array();
1622          $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1623          foreach ($this->feedbackplugins as $plugin) {
1624              $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1625          }
1626          $group->setElements($feedbackpluginsenabled);
1627          $mform->setExpanded('submissiontypes');
1628      }
1629  
1630      /**
1631       * Allow each plugin an opportunity to update the defaultvalues
1632       * passed in to the settings form (needed to set up draft areas for
1633       * editor and filemanager elements)
1634       *
1635       * @param array $defaultvalues
1636       */
1637      public function plugin_data_preprocessing(&$defaultvalues) {
1638          foreach ($this->submissionplugins as $plugin) {
1639              if ($plugin->is_visible()) {
1640                  $plugin->data_preprocessing($defaultvalues);
1641              }
1642          }
1643          foreach ($this->feedbackplugins as $plugin) {
1644              if ($plugin->is_visible()) {
1645                  $plugin->data_preprocessing($defaultvalues);
1646              }
1647          }
1648      }
1649  
1650      /**
1651       * Get the name of the current module.
1652       *
1653       * @return string the module name (Assignment)
1654       */
1655      protected function get_module_name() {
1656          if (isset(self::$modulename)) {
1657              return self::$modulename;
1658          }
1659          self::$modulename = get_string('modulename', 'assign');
1660          return self::$modulename;
1661      }
1662  
1663      /**
1664       * Get the plural name of the current module.
1665       *
1666       * @return string the module name plural (Assignments)
1667       */
1668      protected function get_module_name_plural() {
1669          if (isset(self::$modulenameplural)) {
1670              return self::$modulenameplural;
1671          }
1672          self::$modulenameplural = get_string('modulenameplural', 'assign');
1673          return self::$modulenameplural;
1674      }
1675  
1676      /**
1677       * Has this assignment been constructed from an instance?
1678       *
1679       * @return bool
1680       */
1681      public function has_instance() {
1682          return $this->instance || $this->get_course_module();
1683      }
1684  
1685      /**
1686       * Get the settings for the current instance of this assignment.
1687       *
1688       * @return stdClass The settings
1689       * @throws dml_exception
1690       */
1691      public function get_default_instance() {
1692          global $DB;
1693          if (!$this->instance && $this->get_course_module()) {
1694              $params = array('id' => $this->get_course_module()->instance);
1695              $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1696  
1697              $this->userinstances = [];
1698          }
1699          return $this->instance;
1700      }
1701  
1702      /**
1703       * Get the settings for the current instance of this assignment
1704       * @param int|null $userid the id of the user to load the assign instance for.
1705       * @return stdClass The settings
1706       */
1707      public function get_instance(int $userid = null) : stdClass {
1708          global $USER;
1709          $userid = $userid ?? $USER->id;
1710  
1711          $this->instance = $this->get_default_instance();
1712  
1713          // If we have the user instance already, just return it.
1714          if (isset($this->userinstances[$userid])) {
1715              return $this->userinstances[$userid];
1716          }
1717  
1718          // Calculate properties which vary per user.
1719          $this->userinstances[$userid] = $this->calculate_properties($this->instance, $userid);
1720          return $this->userinstances[$userid];
1721      }
1722  
1723      /**
1724       * Calculates and updates various properties based on the specified user.
1725       *
1726       * @param stdClass $record the raw assign record.
1727       * @param int $userid the id of the user to calculate the properties for.
1728       * @return stdClass a new record having calculated properties.
1729       */
1730      private function calculate_properties(\stdClass $record, int $userid) : \stdClass {
1731          $record = clone ($record);
1732  
1733          // Relative dates.
1734          if (!empty($record->duedate)) {
1735              $course = $this->get_course();
1736              $usercoursedates = course_get_course_dates_for_user_id($course, $userid);
1737              if ($usercoursedates['start']) {
1738                  $userprops = ['duedate' => $record->duedate + $usercoursedates['startoffset']];
1739                  $record = (object) array_merge((array) $record, (array) $userprops);
1740              }
1741          }
1742          return $record;
1743      }
1744  
1745      /**
1746       * Get the primary grade item for this assign instance.
1747       *
1748       * @return grade_item The grade_item record
1749       */
1750      public function get_grade_item() {
1751          if ($this->gradeitem) {
1752              return $this->gradeitem;
1753          }
1754          $instance = $this->get_instance();
1755          $params = array('itemtype' => 'mod',
1756                          'itemmodule' => 'assign',
1757                          'iteminstance' => $instance->id,
1758                          'courseid' => $instance->course,
1759                          'itemnumber' => 0);
1760          $this->gradeitem = grade_item::fetch($params);
1761          if (!$this->gradeitem) {
1762              throw new coding_exception('Improper use of the assignment class. ' .
1763                                         'Cannot load the grade item.');
1764          }
1765          return $this->gradeitem;
1766      }
1767  
1768      /**
1769       * Get the context of the current course.
1770       *
1771       * @return mixed context|null The course context
1772       */
1773      public function get_course_context() {
1774          if (!$this->context && !$this->course) {
1775              throw new coding_exception('Improper use of the assignment class. ' .
1776                                         'Cannot load the course context.');
1777          }
1778          if ($this->context) {
1779              return $this->context->get_course_context();
1780          } else {
1781              return context_course::instance($this->course->id);
1782          }
1783      }
1784  
1785  
1786      /**
1787       * Get the current course module.
1788       *
1789       * @return cm_info|null The course module or null if not known
1790       */
1791      public function get_course_module() {
1792          if ($this->coursemodule) {
1793              return $this->coursemodule;
1794          }
1795          if (!$this->context) {
1796              return null;
1797          }
1798  
1799          if ($this->context->contextlevel == CONTEXT_MODULE) {
1800              $modinfo = get_fast_modinfo($this->get_course());
1801              $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1802              return $this->coursemodule;
1803          }
1804          return null;
1805      }
1806  
1807      /**
1808       * Get context module.
1809       *
1810       * @return context
1811       */
1812      public function get_context() {
1813          return $this->context;
1814      }
1815  
1816      /**
1817       * Get the current course.
1818       *
1819       * @return mixed stdClass|null The course
1820       */
1821      public function get_course() {
1822          global $DB;
1823  
1824          if ($this->course && is_object($this->course)) {
1825              return $this->course;
1826          }
1827  
1828          if (!$this->context) {
1829              return null;
1830          }
1831          $params = array('id' => $this->get_course_context()->instanceid);
1832          $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1833  
1834          return $this->course;
1835      }
1836  
1837      /**
1838       * Count the number of intro attachments.
1839       *
1840       * @return int
1841       */
1842      protected function count_attachments() {
1843  
1844          $fs = get_file_storage();
1845          $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1846                          0, 'id', false);
1847  
1848          return count($files);
1849      }
1850  
1851      /**
1852       * Are there any intro attachments to display?
1853       *
1854       * @return boolean
1855       */
1856      protected function has_visible_attachments() {
1857          return ($this->count_attachments() > 0);
1858      }
1859  
1860      /**
1861       * Return a grade in user-friendly form, whether it's a scale or not.
1862       *
1863       * @param mixed $grade int|null
1864       * @param boolean $editing Are we allowing changes to this grade?
1865       * @param int $userid The user id the grade belongs to
1866       * @param int $modified Timestamp from when the grade was last modified
1867       * @return string User-friendly representation of grade
1868       */
1869      public function display_grade($grade, $editing, $userid=0, $modified=0) {
1870          global $DB;
1871  
1872          static $scalegrades = array();
1873  
1874          $o = '';
1875  
1876          if ($this->get_instance()->grade >= 0) {
1877              // Normal number.
1878              if ($editing && $this->get_instance()->grade > 0) {
1879                  if ($grade < 0) {
1880                      $displaygrade = '';
1881                  } else {
1882                      $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals());
1883                  }
1884                  $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
1885                         get_string('usergrade', 'assign') .
1886                         '</label>';
1887                  $o .= '<input type="text"
1888                                id="quickgrade_' . $userid . '"
1889                                name="quickgrade_' . $userid . '"
1890                                value="' .  $displaygrade . '"
1891                                size="6"
1892                                maxlength="10"
1893                                class="quickgrade"/>';
1894                  $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
1895                  return $o;
1896              } else {
1897                  if ($grade == -1 || $grade === null) {
1898                      $o .= '-';
1899                  } else {
1900                      $item = $this->get_grade_item();
1901                      $o .= grade_format_gradevalue($grade, $item);
1902                      if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
1903                          // If displaying the raw grade, also display the total value.
1904                          $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $item->get_decimals());
1905                      }
1906                  }
1907                  return $o;
1908              }
1909  
1910          } else {
1911              // Scale.
1912              if (empty($this->cache['scale'])) {
1913                  if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
1914                      $this->cache['scale'] = make_menu_from_list($scale->scale);
1915                  } else {
1916                      $o .= '-';
1917                      return $o;
1918                  }
1919              }
1920              if ($editing) {
1921                  $o .= '<label class="accesshide"
1922                                for="quickgrade_' . $userid . '">' .
1923                        get_string('usergrade', 'assign') .
1924                        '</label>';
1925                  $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
1926                  $o .= '<option value="-1">' . get_string('nograde') . '</option>';
1927                  foreach ($this->cache['scale'] as $optionid => $option) {
1928                      $selected = '';
1929                      if ($grade == $optionid) {
1930                          $selected = 'selected="selected"';
1931                      }
1932                      $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
1933                  }
1934                  $o .= '</select>';
1935                  return $o;
1936              } else {
1937                  $scaleid = (int)$grade;
1938                  if (isset($this->cache['scale'][$scaleid])) {
1939                      $o .= $this->cache['scale'][$scaleid];
1940                      return $o;
1941                  }
1942                  $o .= '-';
1943                  return $o;
1944              }
1945          }
1946      }
1947  
1948      /**
1949       * Get the submission status/grading status for all submissions in this assignment for the
1950       * given paticipants.
1951       *
1952       * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
1953       * If this is a group assignment, group info is also returned.
1954       *
1955       * @param array $participants an associative array where the key is the participant id and
1956       *                            the value is the participant record.
1957       * @return array an associative array where the key is the participant id and the value is
1958       *               the participant record.
1959       */
1960      private function get_submission_info_for_participants($participants) {
1961          global $DB;
1962  
1963          if (empty($participants)) {
1964              return $participants;
1965          }
1966  
1967          list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1968  
1969          $assignid = $this->get_instance()->id;
1970          $params['assignmentid1'] = $assignid;
1971          $params['assignmentid2'] = $assignid;
1972          $params['assignmentid3'] = $assignid;
1973  
1974          $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate';
1975          $from = ' FROM {user} u
1976                           LEFT JOIN {assign_submission} s
1977                                  ON u.id = s.userid
1978                                 AND s.assignment = :assignmentid1
1979                                 AND s.latest = 1
1980                           LEFT JOIN {assign_grades} g
1981                                  ON u.id = g.userid
1982                                 AND g.assignment = :assignmentid2
1983                                 AND g.attemptnumber = s.attemptnumber
1984                           LEFT JOIN {assign_user_flags} uf
1985                                  ON u.id = uf.userid
1986                                 AND uf.assignment = :assignmentid3
1987              ';
1988          $where = ' WHERE u.id ' . $insql;
1989  
1990          if (!empty($this->get_instance()->blindmarking)) {
1991              $from .= 'LEFT JOIN {assign_user_mapping} um
1992                               ON u.id = um.userid
1993                              AND um.assignment = :assignmentid4 ';
1994              $params['assignmentid4'] = $assignid;
1995              $fields .= ', um.id as recordid ';
1996          }
1997  
1998          $sql = "$fields $from $where";
1999  
2000          $records = $DB->get_records_sql($sql, $params);
2001  
2002          if ($this->get_instance()->teamsubmission) {
2003              // Get all groups.
2004              $allgroups = groups_get_all_groups($this->get_course()->id,
2005                                                 array_keys($participants),
2006                                                 $this->get_instance()->teamsubmissiongroupingid,
2007                                                 'DISTINCT g.id, g.name');
2008  
2009          }
2010          foreach ($participants as $userid => $participant) {
2011              $participants[$userid]->fullname = $this->fullname($participant);
2012              $participants[$userid]->submitted = false;
2013              $participants[$userid]->requiregrading = false;
2014              $participants[$userid]->grantedextension = false;
2015          }
2016  
2017          foreach ($records as $userid => $submissioninfo) {
2018              // These filters are 100% the same as the ones in the grading table SQL.
2019              $submitted = false;
2020              $requiregrading = false;
2021              $grantedextension = false;
2022  
2023              if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2024                  $submitted = true;
2025              }
2026  
2027              if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime ||
2028                      empty($submissioninfo->gtime) ||
2029                      $submissioninfo->grade === null)) {
2030                  $requiregrading = true;
2031              }
2032  
2033              if (!empty($submissioninfo->extensionduedate)) {
2034                  $grantedextension = true;
2035              }
2036  
2037              $participants[$userid]->submitted = $submitted;
2038              $participants[$userid]->requiregrading = $requiregrading;
2039              $participants[$userid]->grantedextension = $grantedextension;
2040              if ($this->get_instance()->teamsubmission) {
2041                  $group = $this->get_submission_group($userid);
2042                  if ($group) {
2043                      $participants[$userid]->groupid = $group->id;
2044                      $participants[$userid]->groupname = $group->name;
2045                  }
2046              }
2047          }
2048          return $participants;
2049      }
2050  
2051      /**
2052       * Get the submission status/grading status for all submissions in this assignment.
2053       * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
2054       * If this is a group assignment, group info is also returned.
2055       *
2056       * @param int $currentgroup
2057       * @param boolean $tablesort Apply current user table sorting preferences.
2058       * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension',
2059       *               'groupid', 'groupname'
2060       */
2061      public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) {
2062          $participants = $this->list_participants($currentgroup, false, $tablesort);
2063  
2064          if (empty($participants)) {
2065              return $participants;
2066          } else {
2067              return $this->get_submission_info_for_participants($participants);
2068          }
2069      }
2070  
2071      /**
2072       * Return a valid order by segment for list_participants that matches
2073       * the sorting of the current grading table. Not every field is supported,
2074       * we are only concerned with a list of users so we can't search on anything
2075       * that is not part of the user information (like grading statud or last modified stuff).
2076       *
2077       * @return string Order by clause for list_participants
2078       */
2079      private function get_grading_sort_sql() {
2080          $usersort = flexible_table::get_sort_for_table('mod_assign_grading');
2081          // TODO Does not support custom user profile fields (MDL-70456).
2082          $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_userpic();
2083          $userfields = $userfieldsapi->get_required_fields();
2084          $orderfields = explode(',', $usersort);
2085          $validlist = [];
2086  
2087          foreach ($orderfields as $orderfield) {
2088              $orderfield = trim($orderfield);
2089              foreach ($userfields as $field) {
2090                  $parts = explode(' ', $orderfield);
2091                  if ($parts[0] == $field) {
2092                      // Prepend the user table prefix and count this as a valid order field.
2093                      array_push($validlist, 'u.' . $orderfield);
2094                  }
2095              }
2096          }
2097          // Produce a final list.
2098          $result = implode(',', $validlist);
2099          if (empty($result)) {
2100              // Fall back ordering when none has been set.
2101              $result = 'u.lastname, u.firstname, u.id';
2102          }
2103  
2104          return $result;
2105      }
2106  
2107      /**
2108       * Returns array with sql code and parameters returning all ids of users who have submitted an assignment.
2109       *
2110       * @param int $group The group that the query is for.
2111       * @return array list($sql, $params)
2112       */
2113      protected function get_submitted_sql($group = 0) {
2114          // We need to guarentee unique table names.
2115          static $i = 0;
2116          $i++;
2117          $prefix = 'sa' . $i . '_';
2118          $params = [
2119              "{$prefix}assignment" => (int) $this->get_instance()->id,
2120              "{$prefix}status" => ASSIGN_SUBMISSION_STATUS_NEW,
2121          ];
2122          $capjoin = get_enrolled_with_capabilities_join($this->context, $prefix, '', $group, $this->show_only_active_users());
2123          $params += $capjoin->params;
2124          $sql = "SELECT {$prefix}s.userid
2125                    FROM {assign_submission} {$prefix}s
2126                    JOIN {user} {$prefix}u ON {$prefix}u.id = {$prefix}s.userid
2127                    $capjoin->joins
2128                   WHERE {$prefix}s.assignment = :{$prefix}assignment
2129                     AND {$prefix}s.status <> :{$prefix}status
2130                     AND $capjoin->wheres";
2131          return array($sql, $params);
2132      }
2133  
2134      /**
2135       * Load a list of users enrolled in the current course with the specified permission and group.
2136       * 0 for no group.
2137       * Apply any current sort filters from the grading table.
2138       *
2139       * @param int $currentgroup
2140       * @param bool $idsonly
2141       * @param bool $tablesort
2142       * @return array List of user records
2143       */
2144      public function list_participants($currentgroup, $idsonly, $tablesort = false) {
2145          global $DB, $USER;
2146  
2147          // Get the last known sort order for the grading table.
2148  
2149          if (empty($currentgroup)) {
2150              $currentgroup = 0;
2151          }
2152  
2153          $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
2154          if (!isset($this->participants[$key])) {
2155              list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup,
2156                      $this->show_only_active_users());
2157              list($ssql, $sparams) = $this->get_submitted_sql($currentgroup);
2158              $params += $sparams;
2159  
2160              $fields = 'u.*';
2161              $orderby = 'u.lastname, u.firstname, u.id';
2162  
2163              $additionaljoins = '';
2164              $additionalfilters = '';
2165              $instance = $this->get_instance();
2166              if (!empty($instance->blindmarking)) {
2167                  $additionaljoins .= " LEFT JOIN {assign_user_mapping} um
2168                                    ON u.id = um.userid
2169                                   AND um.assignment = :assignmentid1
2170                             LEFT JOIN {assign_submission} s
2171                                    ON u.id = s.userid
2172                                   AND s.assignment = :assignmentid2
2173                                   AND s.latest = 1
2174                          ";
2175                  $params['assignmentid1'] = (int) $instance->id;
2176                  $params['assignmentid2'] = (int) $instance->id;
2177                  $fields .= ', um.id as recordid ';
2178  
2179                  // Sort by submission time first, then by um.id to sort reliably by the blind marking id.
2180                  // Note, different DBs have different ordering of NULL values.
2181                  // Therefore we coalesce the current time into the timecreated field, and the max possible integer into
2182                  // the ID field.
2183                  if (empty($tablesort)) {
2184                      $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC";
2185                  }
2186              }
2187  
2188              if ($instance->markingworkflow &&
2189                      $instance->markingallocation &&
2190                      !has_capability('mod/assign:manageallocations', $this->get_context()) &&
2191                      has_capability('mod/assign:grade', $this->get_context())) {
2192  
2193                  $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf
2194                                       ON u.id = uf.userid
2195                                       AND uf.assignment = :assignmentid3';
2196  
2197                  $params['assignmentid3'] = (int) $instance->id;
2198  
2199                  $additionalfilters .= ' AND uf.allocatedmarker = :markerid';
2200                  $params['markerid'] = $USER->id;
2201              }
2202  
2203              $sql = "SELECT $fields
2204                        FROM {user} u
2205                        JOIN ($esql UNION $ssql) je ON je.id = u.id
2206                             $additionaljoins
2207                       WHERE u.deleted = 0
2208                             $additionalfilters
2209                    ORDER BY $orderby";
2210  
2211              $users = $DB->get_records_sql($sql, $params);
2212  
2213              $cm = $this->get_course_module();
2214              $info = new \core_availability\info_module($cm);
2215              $users = $info->filter_user_list($users);
2216  
2217              $this->participants[$key] = $users;
2218          }
2219  
2220          if ($tablesort) {
2221              // Resort the user list according to the grading table sort and filter settings.
2222              $sortedfiltereduserids = $this->get_grading_userid_list(true, '');
2223              $sortedfilteredusers = [];
2224              foreach ($sortedfiltereduserids as $nextid) {
2225                  $nextid = intval($nextid);
2226                  if (isset($this->participants[$key][$nextid])) {
2227                      $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid];
2228                  }
2229              }
2230              $this->participants[$key] = $sortedfilteredusers;
2231          }
2232  
2233          if ($idsonly) {
2234              $idslist = array();
2235              foreach ($this->participants[$key] as $id => $user) {
2236                  $idslist[$id] = new stdClass();
2237                  $idslist[$id]->id = $id;
2238              }
2239              return $idslist;
2240          }
2241          return $this->participants[$key];
2242      }
2243  
2244      /**
2245       * Load a user if they are enrolled in the current course. Populated with submission
2246       * status for this assignment.
2247       *
2248       * @param int $userid
2249       * @return null|stdClass user record
2250       */
2251      public function get_participant($userid) {
2252          global $DB, $USER;
2253  
2254          if ($userid == $USER->id) {
2255              $participant = clone ($USER);
2256          } else {
2257              $participant = $DB->get_record('user', array('id' => $userid));
2258          }
2259          if (!$participant) {
2260              return null;
2261          }
2262  
2263          if (!is_enrolled($this->context, $participant, '', $this->show_only_active_users())) {
2264              return null;
2265          }
2266  
2267          $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
2268  
2269          $submissioninfo = $result[$participant->id];
2270          if (!$submissioninfo->submitted && !has_capability('mod/assign:submit', $this->context, $userid)) {
2271              return null;
2272          }
2273  
2274          return $submissioninfo;
2275      }
2276  
2277      /**
2278       * Load a count of valid teams for this assignment.
2279       *
2280       * @param int $activitygroup Activity active group
2281       * @return int number of valid teams
2282       */
2283      public function count_teams($activitygroup = 0) {
2284  
2285          $count = 0;
2286  
2287          $participants = $this->list_participants($activitygroup, true);
2288  
2289          // If a team submission grouping id is provided all good as all returned groups
2290          // are the submission teams, but if no team submission grouping was specified
2291          // $groups will contain all participants groups.
2292          if ($this->get_instance()->teamsubmissiongroupingid) {
2293  
2294              // We restrict the users to the selected group ones.
2295              $groups = groups_get_all_groups($this->get_course()->id,
2296                                              array_keys($participants),
2297                                              $this->get_instance()->teamsubmissiongroupingid,
2298                                              'DISTINCT g.id, g.name');
2299  
2300              $count = count($groups);
2301  
2302              // When a specific group is selected we don't count the default group users.
2303              if ($activitygroup == 0) {
2304                  if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2305                      // See if there are any users in the default group.
2306                      $defaultusers = $this->get_submission_group_members(0, true);
2307                      if (count($defaultusers) > 0) {
2308                          $count += 1;
2309                      }
2310                  }
2311              } else if ($activitygroup != 0 && empty($groups)) {
2312                  // Set count to 1 if $groups returns empty.
2313                  // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid.
2314                  $count = 1;
2315              }
2316          } else {
2317              // It is faster to loop around participants if no grouping was specified.
2318              $groups = array();
2319              foreach ($participants as $participant) {
2320                  if ($group = $this->get_submission_group($participant->id)) {
2321                      $groups[$group->id] = true;
2322                  } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2323                      $groups[0] = true;
2324                  }
2325              }
2326  
2327              $count = count($groups);
2328          }
2329  
2330          return $count;
2331      }
2332  
2333      /**
2334       * Load a count of active users enrolled in the current course with the specified permission and group.
2335       * 0 for no group.
2336       *
2337       * @param int $currentgroup
2338       * @return int number of matching users
2339       */
2340      public function count_participants($currentgroup) {
2341          return count($this->list_participants($currentgroup, true));
2342      }
2343  
2344      /**
2345       * Load a count of active users submissions in the current module that require grading
2346       * This means the submission modification time is more recent than the
2347       * grading modification time and the status is SUBMITTED.
2348       *
2349       * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2350       * @return int number of matching submissions
2351       */
2352      public function count_submissions_need_grading($currentgroup = null) {
2353          global $DB;
2354  
2355          if ($this->get_instance()->teamsubmission) {
2356              // This does not make sense for group assignment because the submission is shared.
2357              return 0;
2358          }
2359  
2360          if ($currentgroup === null) {
2361              $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2362          }
2363          list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2364  
2365          $params['assignid'] = $this->get_instance()->id;
2366          $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2367          $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : '';
2368  
2369          $sql = 'SELECT COUNT(s.userid)
2370                     FROM {assign_submission} s
2371                     LEFT JOIN {assign_grades} g ON
2372                          s.assignment = g.assignment AND
2373                          s.userid = g.userid AND
2374                          g.attemptnumber = s.attemptnumber
2375                     JOIN(' . $esql . ') e ON e.id = s.userid
2376                     WHERE
2377                          s.latest = 1 AND
2378                          s.assignment = :assignid AND
2379                          s.timemodified IS NOT NULL AND
2380                          s.status = :submitted AND
2381                          (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL '
2382                              . $sqlscalegrade . ')';
2383  
2384          return $DB->count_records_sql($sql, $params);
2385      }
2386  
2387      /**
2388       * Load a count of grades.
2389       *
2390       * @return int number of grades
2391       */
2392      public function count_grades() {
2393          global $DB;
2394  
2395          if (!$this->has_instance()) {
2396              return 0;
2397          }
2398  
2399          $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2400          list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2401  
2402          $params['assignid'] = $this->get_instance()->id;
2403  
2404          $sql = 'SELECT COUNT(g.userid)
2405                     FROM {assign_grades} g
2406                     JOIN(' . $esql . ') e ON e.id = g.userid
2407                     WHERE g.assignment = :assignid';
2408  
2409          return $DB->count_records_sql($sql, $params);
2410      }
2411  
2412      /**
2413       * Load a count of submissions.
2414       *
2415       * @param bool $includenew When true, also counts the submissions with status 'new'.
2416       * @return int number of submissions
2417       */
2418      public function count_submissions($includenew = false) {
2419          global $DB;
2420  
2421          if (!$this->has_instance()) {
2422              return 0;
2423          }
2424  
2425          $params = array();
2426          $sqlnew = '';
2427  
2428          if (!$includenew) {
2429              $sqlnew = ' AND s.status <> :status ';
2430              $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
2431          }
2432  
2433          if ($this->get_instance()->teamsubmission) {
2434              // We cannot join on the enrolment tables for group submissions (no userid).
2435              $sql = 'SELECT COUNT(DISTINCT s.groupid)
2436                          FROM {assign_submission} s
2437                          WHERE
2438                              s.assignment = :assignid AND
2439                              s.timemodified IS NOT NULL AND
2440                              s.userid = :groupuserid' .
2441                              $sqlnew;
2442  
2443              $params['assignid'] = $this->get_instance()->id;
2444              $params['groupuserid'] = 0;
2445          } else {
2446              $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2447              list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2448  
2449              $params = array_merge($params, $enrolparams);
2450              $params['assignid'] = $this->get_instance()->id;
2451  
2452              $sql = 'SELECT COUNT(DISTINCT s.userid)
2453                         FROM {assign_submission} s
2454                         JOIN(' . $esql . ') e ON e.id = s.userid
2455                         WHERE
2456                              s.assignment = :assignid AND
2457                              s.timemodified IS NOT NULL ' .
2458                              $sqlnew;
2459  
2460          }
2461  
2462          return $DB->count_records_sql($sql, $params);
2463      }
2464  
2465      /**
2466       * Load a count of submissions with a specified status.
2467       *
2468       * @param string $status The submission status - should match one of the constants
2469       * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2470       * @return int number of matching submissions
2471       */
2472      public function count_submissions_with_status($status, $currentgroup = null) {
2473          global $DB;
2474  
2475          if ($currentgroup === null) {
2476              $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2477          }
2478          list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2479  
2480          $params['assignid'] = $this->get_instance()->id;
2481          $params['assignid2'] = $this->get_instance()->id;
2482          $params['submissionstatus'] = $status;
2483  
2484          if ($this->get_instance()->teamsubmission) {
2485  
2486              $groupsstr = '';
2487              if ($currentgroup != 0) {
2488                  // If there is an active group we should only display the current group users groups.
2489                  $participants = $this->list_participants($currentgroup, true);
2490                  $groups = groups_get_all_groups($this->get_course()->id,
2491                                                  array_keys($participants),
2492                                                  $this->get_instance()->teamsubmissiongroupingid,
2493                                                  'DISTINCT g.id, g.name');
2494                  if (empty($groups)) {
2495                      // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid.
2496                      // All submissions from students that do not belong to any of teamsubmissiongroupingid groups
2497                      // count towards groupid = 0. Setting to true as only '0' key matters.
2498                      $groups = [true];
2499                  }
2500                  list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
2501                  $groupsstr = 's.groupid ' . $groupssql . ' AND';
2502                  $params = $params + $groupsparams;
2503              }
2504              $sql = 'SELECT COUNT(s.groupid)
2505                          FROM {assign_submission} s
2506                          WHERE
2507                              s.latest = 1 AND
2508                              s.assignment = :assignid AND
2509                              s.timemodified IS NOT NULL AND
2510                              s.userid = :groupuserid AND '
2511                              . $groupsstr . '
2512                              s.status = :submissionstatus';
2513              $params['groupuserid'] = 0;
2514          } else {
2515              $sql = 'SELECT COUNT(s.userid)
2516                          FROM {assign_submission} s
2517                          JOIN(' . $esql . ') e ON e.id = s.userid
2518                          WHERE
2519                              s.latest = 1 AND
2520                              s.assignment = :assignid AND
2521                              s.timemodified IS NOT NULL AND
2522                              s.status = :submissionstatus';
2523  
2524          }
2525  
2526          return $DB->count_records_sql($sql, $params);
2527      }
2528  
2529      /**
2530       * Utility function to get the userid for every row in the grading table
2531       * so the order can be frozen while we iterate it.
2532       *
2533       * @param boolean $cached If true, the cached list from the session could be returned.
2534       * @param string $useridlistid String value used for caching the participant list.
2535       * @return array An array of userids
2536       */
2537      protected function get_grading_userid_list($cached = false, $useridlistid = '') {
2538          global $SESSION;
2539  
2540          if ($cached) {
2541              if (empty($useridlistid)) {
2542                  $useridlistid = $this->get_useridlist_key_id();
2543              }
2544              $useridlistkey = $this->get_useridlist_key($useridlistid);
2545              if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
2546                  $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, '');
2547              }
2548              return $SESSION->mod_assign_useridlist[$useridlistkey];
2549          }
2550          $filter = get_user_preferences('assign_filter', '');
2551          $table = new assign_grading_table($this, 0, $filter, 0, false);
2552  
2553          $useridlist = $table->get_column_data('userid');
2554  
2555          return $useridlist;
2556      }
2557  
2558      /**
2559       * Finds all assignment notifications that have yet to be mailed out, and mails them.
2560       *
2561       * Cron function to be run periodically according to the moodle cron.
2562       *
2563       * @return bool
2564       */
2565      public static function cron() {
2566          global $DB;
2567  
2568          // Only ever send a max of one days worth of updates.
2569          $yesterday = time() - (24 * 3600);
2570          $timenow   = time();
2571          $task = \core\task\manager::get_scheduled_task(mod_assign\task\cron_task::class);
2572          $lastruntime = $task->get_last_run_time();
2573  
2574          // Collect all submissions that require mailing.
2575          // Submissions are included if all are true:
2576          //   - The assignment is visible in the gradebook.
2577          //   - No previous notification has been sent.
2578          //   - The grader was a real user, not an automated process.
2579          //   - The grade was updated in the past 24 hours.
2580          //   - If marking workflow is enabled, the workflow state is at 'released'.
2581          $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities, a.hidegrader,
2582                         g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid
2583                   FROM {assign} a
2584                   JOIN {assign_grades} g ON g.assignment = a.id
2585              LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
2586                   JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
2587                   JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
2588                   JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
2589              LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
2590                   WHERE (a.markingworkflow = 0 OR (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
2591                         g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0 AND
2592                         g.timemodified >= :yesterday AND g.timemodified <= :today
2593                ORDER BY a.course, cm.id";
2594  
2595          $params = array(
2596              'yesterday' => $yesterday,
2597              'today' => $timenow,
2598              'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2599          );
2600          $submissions = $DB->get_records_sql($sql, $params);
2601  
2602          if (!empty($submissions)) {
2603  
2604              mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
2605  
2606              // Preload courses we are going to need those.
2607              $courseids = array();
2608              foreach ($submissions as $submission) {
2609                  $courseids[] = $submission->course;
2610              }
2611  
2612              // Filter out duplicates.
2613              $courseids = array_unique($courseids);
2614              $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
2615              list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
2616              $sql = 'SELECT c.*, ' . $ctxselect .
2617                        ' FROM {course} c
2618                   LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
2619                       WHERE c.id ' . $courseidsql;
2620  
2621              $params['contextlevel'] = CONTEXT_COURSE;
2622              $courses = $DB->get_records_sql($sql, $params);
2623  
2624              // Clean up... this could go on for a while.
2625              unset($courseids);
2626              unset($ctxselect);
2627              unset($courseidsql);
2628              unset($params);
2629  
2630              // Message students about new feedback.
2631              foreach ($submissions as $submission) {
2632  
2633                  mtrace("Processing assignment submission $submission->id ...");
2634  
2635                  // Do not cache user lookups - could be too many.
2636                  if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
2637                      mtrace('Could not find user ' . $submission->userid);
2638                      continue;
2639                  }
2640  
2641                  // Use a cache to prevent the same DB queries happening over and over.
2642                  if (!array_key_exists($submission->course, $courses)) {
2643                      mtrace('Could not find course ' . $submission->course);
2644                      continue;
2645                  }
2646                  $course = $courses[$submission->course];
2647                  if (isset($course->ctxid)) {
2648                      // Context has not yet been preloaded. Do so now.
2649                      context_helper::preload_from_record($course);
2650                  }
2651  
2652                  // Override the language and timezone of the "current" user, so that
2653                  // mail is customised for the receiver.
2654                  cron_setup_user($user, $course);
2655  
2656                  // Context lookups are already cached.
2657                  $coursecontext = context_course::instance($course->id);
2658                  if (!is_enrolled($coursecontext, $user->id)) {
2659                      $courseshortname = format_string($course->shortname,
2660                                                       true,
2661                                                       array('context' => $coursecontext));
2662                      mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
2663                      continue;
2664                  }
2665  
2666                  if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
2667                      mtrace('Could not find grader ' . $submission->grader);
2668                      continue;
2669                  }
2670  
2671                  $modinfo = get_fast_modinfo($course, $user->id);
2672                  $cm = $modinfo->get_cm($submission->cmid);
2673                  // Context lookups are already cached.
2674                  $contextmodule = context_module::instance($cm->id);
2675  
2676                  if (!$cm->uservisible) {
2677                      // Hold mail notification for assignments the user cannot access until later.
2678                      continue;
2679                  }
2680  
2681                  // Notify the student. Default to the non-anon version.
2682                  $messagetype = 'feedbackavailable';
2683                  // Message type needs 'anon' if "hidden grading" is enabled and the student
2684                  // doesn't have permission to see the grader.
2685                  if ($submission->hidegrader && !has_capability('mod/assign:showhiddengrader', $contextmodule, $user)) {
2686                      $messagetype = 'feedbackavailableanon';
2687                      // There's no point in having an "anonymous grader" if the notification email
2688                      // comes from them. Send the email from the noreply user instead.
2689                      $grader = core_user::get_noreply_user();
2690                  }
2691  
2692                  $eventtype = 'assign_notification';
2693                  $updatetime = $submission->lastmodified;
2694                  $modulename = get_string('modulename', 'assign');
2695  
2696                  $uniqueid = 0;
2697                  if ($submission->blindmarking && !$submission->revealidentities) {
2698                      if (empty($submission->recordid)) {
2699                          $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id);
2700                      } else {
2701                          $uniqueid = $submission->recordid;
2702                      }
2703                  }
2704                  $showusers = $submission->blindmarking && !$submission->revealidentities;
2705                  self::send_assignment_notification($grader,
2706                                                     $user,
2707                                                     $messagetype,
2708                                                     $eventtype,
2709                                                     $updatetime,
2710                                                     $cm,
2711                                                     $contextmodule,
2712                                                     $course,
2713                                                     $modulename,
2714                                                     $submission->name,
2715                                                     $showusers,
2716                                                     $uniqueid);
2717  
2718                  $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
2719                  if ($flags) {
2720                      $flags->mailed = 1;
2721                      $DB->update_record('assign_user_flags', $flags);
2722                  } else {
2723                      $flags = new stdClass();
2724                      $flags->userid = $user->id;
2725                      $flags->assignment = $submission->assignment;
2726                      $flags->mailed = 1;
2727                      $DB->insert_record('assign_user_flags', $flags);
2728                  }
2729  
2730                  mtrace('Done');
2731              }
2732              mtrace('Done processing ' . count($submissions) . ' assignment submissions');
2733  
2734              cron_setup_user();
2735  
2736              // Free up memory just to be sure.
2737              unset($courses);
2738          }
2739  
2740          // Update calendar events to provide a description.
2741          $sql = 'SELECT id
2742                      FROM {assign}
2743                      WHERE
2744                          allowsubmissionsfromdate >= :lastruntime AND
2745                          allowsubmissionsfromdate <= :timenow AND
2746                          alwaysshowdescription = 0';
2747          $params = array('lastruntime' => $lastruntime, 'timenow' => $timenow);
2748          $newlyavailable = $DB->get_records_sql($sql, $params);
2749          foreach ($newlyavailable as $record) {
2750              $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
2751              $context = context_module::instance($cm->id);
2752  
2753              $assignment = new assign($context, null, null);
2754              $assignment->update_calendar($cm->id);
2755          }
2756  
2757          return true;
2758      }
2759  
2760      /**
2761       * Mark in the database that this grade record should have an update notification sent by cron.
2762       *
2763       * @param stdClass $grade a grade record keyed on id
2764       * @param bool $mailedoverride when true, flag notification to be sent again.
2765       * @return bool true for success
2766       */
2767      public function notify_grade_modified($grade, $mailedoverride = false) {
2768          global $DB;
2769  
2770          $flags = $this->get_user_flags($grade->userid, true);
2771          if ($flags->mailed != 1 || $mailedoverride) {
2772              $flags->mailed = 0;
2773          }
2774  
2775          return $this->update_user_flags($flags);
2776      }
2777  
2778      /**
2779       * Update user flags for this user in this assignment.
2780       *
2781       * @param stdClass $flags a flags record keyed on id
2782       * @return bool true for success
2783       */
2784      public function update_user_flags($flags) {
2785          global $DB;
2786          if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
2787              return false;
2788          }
2789  
2790          $result = $DB->update_record('assign_user_flags', $flags);
2791          return $result;
2792      }
2793  
2794      /**
2795       * Update a grade in the grade table for the assignment and in the gradebook.
2796       *
2797       * @param stdClass $grade a grade record keyed on id
2798       * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
2799       * @return bool true for success
2800       */
2801      public function update_grade($grade, $reopenattempt = false) {
2802          global $DB;
2803  
2804          $grade->timemodified = time();
2805  
2806          if (!empty($grade->workflowstate)) {
2807              $validstates = $this->get_marking_workflow_states_for_current_user();
2808              if (!array_key_exists($grade->workflowstate, $validstates)) {
2809                  return false;
2810              }
2811          }
2812  
2813          if ($grade->grade && $grade->grade != -1) {
2814              if ($this->get_instance()->grade > 0) {
2815                  if (!is_numeric($grade->grade)) {
2816                      return false;
2817                  } else if ($grade->grade > $this->get_instance()->grade) {
2818                      return false;
2819                  } else if ($grade->grade < 0) {
2820                      return false;
2821                  }
2822              } else {
2823                  // This is a scale.
2824                  if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
2825                      $scaleoptions = make_menu_from_list($scale->scale);
2826                      if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
2827                          return false;
2828                      }
2829                  }
2830              }
2831          }
2832  
2833          if (empty($grade->attemptnumber)) {
2834              // Set it to the default.
2835              $grade->attemptnumber = 0;
2836          }
2837          $DB->update_record('assign_grades', $grade);
2838  
2839          $submission = null;
2840          if ($this->get_instance()->teamsubmission) {
2841              if (isset($this->mostrecentteamsubmission)) {
2842                  $submission = $this->mostrecentteamsubmission;
2843              } else {
2844                  $submission = $this->get_group_submission($grade->userid, 0, false);
2845              }
2846          } else {
2847              $submission = $this->get_user_submission($grade->userid, false);
2848          }
2849  
2850          // Only push to gradebook if the update is for the most recent attempt.
2851          if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
2852              return true;
2853          }
2854  
2855          if ($this->gradebook_item_update(null, $grade)) {
2856              \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
2857          }
2858  
2859          // If the conditions are met, allow another attempt.
2860          if ($submission) {
2861              $this->reopen_submission_if_required($grade->userid,
2862                      $submission,
2863                      $reopenattempt);
2864          }
2865  
2866          return true;
2867      }
2868  
2869      /**
2870       * View the grant extension date page.
2871       *
2872       * Uses url parameters 'userid'
2873       * or from parameter 'selectedusers'
2874       *
2875       * @param moodleform $mform - Used for validation of the submitted data
2876       * @return string
2877       */
2878      protected function view_grant_extension($mform) {
2879          global $CFG;
2880          require_once($CFG->dirroot . '/mod/assign/extensionform.php');
2881  
2882          $o = '';
2883  
2884          $data = new stdClass();
2885          $data->id = $this->get_course_module()->id;
2886  
2887          $formparams = array(
2888              'instance' => $this->get_instance(),
2889              'assign' => $this
2890          );
2891  
2892          $users = optional_param('userid', 0, PARAM_INT);
2893          if (!$users) {
2894              $users = required_param('selectedusers', PARAM_SEQUENCE);
2895          }
2896          $userlist = explode(',', $users);
2897  
2898          $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
2899          $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
2900          foreach ($userlist as $userid) {
2901              // To validate extension date with users overrides.
2902              $override = $this->override_exists($userid);
2903              foreach ($keys as $key) {
2904                  if ($override->{$key}) {
2905                      if ($maxoverride[$key] < $override->{$key}) {
2906                          $maxoverride[$key] = $override->{$key};
2907                      }
2908                  } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
2909                      $maxoverride[$key] = $this->get_instance()->{$key};
2910                  }
2911              }
2912          }
2913          foreach ($keys as $key) {
2914              if ($maxoverride[$key]) {
2915                  $this->get_instance()->{$key} = $maxoverride[$key];
2916              }
2917          }
2918  
2919          $formparams['userlist'] = $userlist;
2920  
2921          $data->selectedusers = $users;
2922          $data->userid = 0;
2923  
2924          if (empty($mform)) {
2925              $mform = new mod_assign_extension_form(null, $formparams);
2926          }
2927          $mform->set_data($data);
2928          $header = new assign_header($this->get_instance(),
2929                                      $this->get_context(),
2930                                      $this->show_intro(),
2931                                      $this->get_course_module()->id,
2932                                      get_string('grantextension', 'assign'));
2933          $o .= $this->get_renderer()->render($header);
2934          $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
2935          $o .= $this->view_footer();
2936          return $o;
2937      }
2938  
2939      /**
2940       * Get a list of the users in the same group as this user.
2941       *
2942       * @param int $groupid The id of the group whose members we want or 0 for the default group
2943       * @param bool $onlyids Whether to retrieve only the user id's
2944       * @param bool $excludesuspended Whether to exclude suspended users
2945       * @return array The users (possibly id's only)
2946       */
2947      public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
2948          $members = array();
2949          if ($groupid != 0) {
2950              $allusers = $this->list_participants($groupid, $onlyids);
2951              foreach ($allusers as $user) {
2952                  if ($this->get_submission_group($user->id)) {
2953                      $members[] = $user;
2954                  }
2955              }
2956          } else {
2957              $allusers = $this->list_participants(null, $onlyids);
2958              foreach ($allusers as $user) {
2959                  if ($this->get_submission_group($user->id) == null) {
2960                      $members[] = $user;
2961                  }
2962              }
2963          }
2964          // Exclude suspended users, if user can't see them.
2965          if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
2966              foreach ($members as $key => $member) {
2967                  if (!$this->is_active_user($member->id)) {
2968                      unset($members[$key]);
2969                  }
2970              }
2971          }
2972  
2973          return $members;
2974      }
2975  
2976      /**
2977       * Get a list of the users in the same group as this user that have not submitted the assignment.
2978       *
2979       * @param int $groupid The id of the group whose members we want or 0 for the default group
2980       * @param bool $onlyids Whether to retrieve only the user id's
2981       * @return array The users (possibly id's only)
2982       */
2983      public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
2984          $instance = $this->get_instance();
2985          if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
2986              return array();
2987          }
2988          $members = $this->get_submission_group_members($groupid, $onlyids);
2989  
2990          foreach ($members as $id => $member) {
2991              $submission = $this->get_user_submission($member->id, false);
2992              if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2993                  unset($members[$id]);
2994              } else {
2995                  if ($this->is_blind_marking()) {
2996                      $members[$id]->alias = get_string('hiddenuser', 'assign') .
2997                                             $this->get_uniqueid_for_user($id);
2998                  }
2999              }
3000          }
3001          return $members;
3002      }
3003  
3004      /**
3005       * Load the group submission object for a particular user, optionally creating it if required.
3006       *
3007       * @param int $userid The id of the user whose submission we want
3008       * @param int $groupid The id of the group for this user - may be 0 in which
3009       *                     case it is determined from the userid.
3010       * @param bool $create If set to true a new submission object will be created in the database
3011       *                     with the status set to "new".
3012       * @param int $attemptnumber - -1 means the latest attempt
3013       * @return stdClass The submission
3014       */
3015      public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
3016          global $DB;
3017  
3018          if ($groupid == 0) {
3019              $group = $this->get_submission_group($userid);
3020              if ($group) {
3021                  $groupid = $group->id;
3022              }
3023          }
3024  
3025          // Now get the group submission.
3026          $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
3027          if ($attemptnumber >= 0) {
3028              $params['attemptnumber'] = $attemptnumber;
3029          }
3030  
3031          // Only return the row with the highest attemptnumber.
3032          $submission = null;
3033          $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3034          if ($submissions) {
3035              $submission = reset($submissions);
3036          }
3037  
3038          if ($submission) {
3039              return $submission;
3040          }
3041          if ($create) {
3042              $submission = new stdClass();
3043              $submission->assignment = $this->get_instance()->id;
3044              $submission->userid = 0;
3045              $submission->groupid = $groupid;
3046              $submission->timecreated = time();
3047              $submission->timemodified = $submission->timecreated;
3048              if ($attemptnumber >= 0) {
3049                  $submission->attemptnumber = $attemptnumber;
3050              } else {
3051                  $submission->attemptnumber = 0;
3052              }
3053              // Work out if this is the latest submission.
3054              $submission->latest = 0;
3055              $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
3056              if ($attemptnumber == -1) {
3057                  // This is a new submission so it must be the latest.
3058                  $submission->latest = 1;
3059              } else {
3060                  // We need to work this out.
3061                  $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3062                  if ($result) {
3063                      $latestsubmission = reset($result);
3064                  }
3065                  if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
3066                      $submission->latest = 1;
3067                  }
3068              }
3069              $transaction = $DB->start_delegated_transaction();
3070              if ($submission->latest) {
3071                  // This is the case when we need to set latest to 0 for all the other attempts.
3072                  $DB->set_field('assign_submission', 'latest', 0, $params);
3073              }
3074              $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3075              $sid = $DB->insert_record('assign_submission', $submission);
3076              $transaction->allow_commit();
3077              return $DB->get_record('assign_submission', array('id' => $sid));
3078          }
3079          return false;
3080      }
3081  
3082      /**
3083       * View a summary listing of all assignments in the current course.
3084       *
3085       * @return string
3086       */
3087      private function view_course_index() {
3088          global $USER;
3089  
3090          $o = '';
3091  
3092          $course = $this->get_course();
3093          $strplural = get_string('modulenameplural', 'assign');
3094  
3095          if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
3096              $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
3097              $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
3098              return $o;
3099          }
3100  
3101          $strsectionname = '';
3102          $usesections = course_format_uses_sections($course->format);
3103          $modinfo = get_fast_modinfo($course);
3104  
3105          if ($usesections) {
3106              $strsectionname = get_string('sectionname', 'format_'.$course->format);
3107              $sections = $modinfo->get_section_info_all();
3108          }
3109          $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
3110  
3111          $timenow = time();
3112  
3113          $currentsection = '';
3114          foreach ($modinfo->instances['assign'] as $cm) {
3115              if (!$cm->uservisible) {
3116                  continue;
3117              }
3118  
3119              $timedue = $cms[$cm->id]->duedate;
3120  
3121              $sectionname = '';
3122              if ($usesections && $cm->sectionnum) {
3123                  $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
3124              }
3125  
3126              $submitted = '';
3127              $context = context_module::instance($cm->id);
3128  
3129              $assignment = new assign($context, $cm, $course);
3130  
3131              // Apply overrides.
3132              $assignment->update_effective_access($USER->id);
3133              $timedue = $assignment->get_instance()->duedate;
3134  
3135              if (has_capability('mod/assign:grade', $context)) {
3136                  $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
3137  
3138              } else if (has_capability('mod/assign:submit', $context)) {
3139                  if ($assignment->get_instance()->teamsubmission) {
3140                      $usersubmission = $assignment->get_group_submission($USER->id, 0, false);
3141                  } else {
3142                      $usersubmission = $assignment->get_user_submission($USER->id, false);
3143                  }
3144  
3145                  if (!empty($usersubmission->status)) {
3146                      $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
3147                  } else {
3148                      $submitted = get_string('submissionstatus_', 'assign');
3149                  }
3150              }
3151              $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
3152              if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
3153                      !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
3154                  $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
3155              } else {
3156                  $grade = '-';
3157              }
3158  
3159              $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(), $sectionname, $timedue, $submitted, $grade);
3160  
3161          }
3162  
3163          $o .= $this->get_renderer()->render($courseindexsummary);
3164          $o .= $this->view_footer();
3165  
3166          return $o;
3167      }
3168  
3169      /**
3170       * View a page rendered by a plugin.
3171       *
3172       * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
3173       *
3174       * @return string
3175       */
3176      protected function view_plugin_page() {
3177          global $USER;
3178  
3179          $o = '';
3180  
3181          $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
3182          $plugintype = required_param('plugin', PARAM_PLUGIN);
3183          $pluginaction = required_param('pluginaction', PARAM_ALPHA);
3184  
3185          $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
3186          if (!$plugin) {
3187              print_error('invalidformdata', '');
3188              return;
3189          }
3190  
3191          $o .= $plugin->view_page($pluginaction);
3192  
3193          return $o;
3194      }
3195  
3196  
3197      /**
3198       * This is used for team assignments to get the group for the specified user.
3199       * If the user is a member of multiple or no groups this will return false
3200       *
3201       * @param int $userid The id of the user whose submission we want
3202       * @return mixed The group or false
3203       */
3204      public function get_submission_group($userid) {
3205  
3206          if (isset($this->usersubmissiongroups[$userid])) {
3207              return $this->usersubmissiongroups[$userid];
3208          }
3209  
3210          $groups = $this->get_all_groups($userid);
3211          if (count($groups) != 1) {
3212              $return = false;
3213          } else {
3214              $return = array_pop($groups);
3215          }
3216  
3217          // Cache the user submission group.
3218          $this->usersubmissiongroups[$userid] = $return;
3219  
3220          return $return;
3221      }
3222  
3223      /**
3224       * Gets all groups the user is a member of.
3225       *
3226       * @param int $userid Teh id of the user who's groups we are checking
3227       * @return array The group objects
3228       */
3229      public function get_all_groups($userid) {
3230          if (isset($this->usergroups[$userid])) {
3231              return $this->usergroups[$userid];
3232          }
3233  
3234          $grouping = $this->get_instance()->teamsubmissiongroupingid;
3235          $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping);
3236  
3237          $this->usergroups[$userid] = $return;
3238  
3239          return $return;
3240      }
3241  
3242  
3243      /**
3244       * Display the submission that is used by a plugin.
3245       *
3246       * Uses url parameters 'sid', 'gid' and 'plugin'.
3247       *
3248       * @param string $pluginsubtype
3249       * @return string
3250       */
3251      protected function view_plugin_content($pluginsubtype) {
3252          $o = '';
3253  
3254          $submissionid = optional_param('sid', 0, PARAM_INT);
3255          $gradeid = optional_param('gid', 0, PARAM_INT);
3256          $plugintype = required_param('plugin', PARAM_PLUGIN);
3257          $item = null;
3258          if ($pluginsubtype == 'assignsubmission') {
3259              $plugin = $this->get_submission_plugin_by_type($plugintype);
3260              if ($submissionid <= 0) {
3261                  throw new coding_exception('Submission id should not be 0');
3262              }
3263              $item = $this->get_submission($submissionid);
3264  
3265              // Check permissions.
3266              if (empty($item->userid)) {
3267                  // Group submission.
3268                  $this->require_view_group_submission($item->groupid);
3269              } else {
3270                  $this->require_view_submission($item->userid);
3271              }
3272              $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3273                                                                $this->get_context(),
3274                                                                $this->show_intro(),
3275                                                                $this->get_course_module()->id,
3276                                                                $plugin->get_name()));
3277              $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
3278                                                                $item,
3279                                                                assign_submission_plugin_submission::FULL,
3280                                                                $this->get_course_module()->id,
3281                                                                $this->get_return_action(),
3282                                                                $this->get_return_params()));
3283  
3284              // Trigger event for viewing a submission.
3285              \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
3286  
3287          } else {
3288              $plugin = $this->get_feedback_plugin_by_type($plugintype);
3289              if ($gradeid <= 0) {
3290                  throw new coding_exception('Grade id should not be 0');
3291              }
3292              $item = $this->get_grade($gradeid);
3293              // Check permissions.
3294              $this->require_view_submission($item->userid);
3295              $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3296                                                                $this->get_context(),
3297                                                                $this->show_intro(),
3298                                                                $this->get_course_module()->id,
3299                                                                $plugin->get_name()));
3300              $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
3301                                                                $item,
3302                                                                assign_feedback_plugin_feedback::FULL,
3303                                                                $this->get_course_module()->id,
3304                                                                $this->get_return_action(),
3305                                                                $this->get_return_params()));
3306  
3307              // Trigger event for viewing feedback.
3308              \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
3309          }
3310  
3311          $o .= $this->view_return_links();
3312  
3313          $o .= $this->view_footer();
3314  
3315          return $o;
3316      }
3317  
3318      /**
3319       * Rewrite plugin file urls so they resolve correctly in an exported zip.
3320       *
3321       * @param string $text - The replacement text
3322       * @param stdClass $user - The user record
3323       * @param assign_plugin $plugin - The assignment plugin
3324       */
3325      public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
3326          // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance.
3327          // Rather, it should be determined by checking the group submission settings of the instance,
3328          // which is what download_submission() does when generating the file name prefixes.
3329          $groupname = '';
3330          if ($this->get_instance()->teamsubmission) {
3331              $submissiongroup = $this->get_submission_group($user->id);
3332              if ($submissiongroup) {
3333                  $groupname = $submissiongroup->name . '-';
3334              } else {
3335                  $groupname = get_string('defaultteam', 'assign') . '-';
3336              }
3337          }
3338  
3339          if ($this->is_blind_marking()) {
3340              $prefix = $groupname . get_string('participant', 'assign');
3341              $prefix = str_replace('_', ' ', $prefix);
3342              $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3343          } else {
3344              $prefix = $groupname . fullname($user);
3345              $prefix = str_replace('_', ' ', $prefix);
3346              $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3347          }
3348  
3349          // Only prefix files if downloadasfolders user preference is NOT set.
3350          if (!get_user_preferences('assign_downloadasfolders', 1)) {
3351              $subtype = $plugin->get_subtype();
3352              $type = $plugin->get_type();
3353              $prefix = $prefix . $subtype . '_' . $type . '_';
3354          } else {
3355              $prefix = "";
3356          }
3357          $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
3358  
3359          return $result;
3360      }
3361  
3362      /**
3363       * Render the content in editor that is often used by plugin.
3364       *
3365       * @param string $filearea
3366       * @param int $submissionid
3367       * @param string $plugintype
3368       * @param string $editor
3369       * @param string $component
3370       * @param bool $shortentext Whether to shorten the text content.
3371       * @return string
3372       */
3373      public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) {
3374          global $CFG;
3375  
3376          $result = '';
3377  
3378          $plugin = $this->get_submission_plugin_by_type($plugintype);
3379  
3380          $text = $plugin->get_editor_text($editor, $submissionid);
3381          if ($shortentext) {
3382              $text = shorten_text($text, 140);
3383          }
3384          $format = $plugin->get_editor_format($editor, $submissionid);
3385  
3386          $finaltext = file_rewrite_pluginfile_urls($text,
3387                                                    'pluginfile.php',
3388                                                    $this->get_context()->id,
3389                                                    $component,
3390                                                    $filearea,
3391                                                    $submissionid);
3392          $params = array('overflowdiv' => true, 'context' => $this->get_context());
3393          $result .= format_text($finaltext, $format, $params);
3394  
3395          if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
3396              require_once($CFG->libdir . '/portfoliolib.php');
3397  
3398              $button = new portfolio_add_button();
3399              $portfolioparams = array('cmid' => $this->get_course_module()->id,
3400                                       'sid' => $submissionid,
3401                                       'plugin' => $plugintype,
3402                                       'editor' => $editor,
3403                                       'area'=>$filearea);
3404              $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
3405              $fs = get_file_storage();
3406  
3407              if ($files = $fs->get_area_files($this->context->id,
3408                                               $component,
3409                                               $filearea,
3410                                               $submissionid,
3411                                               'timemodified',
3412                                               false)) {
3413                  $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3414              } else {
3415                  $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3416              }
3417              $result .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3418          }
3419          return $result;
3420      }
3421  
3422      /**
3423       * Display a continue page after grading.
3424       *
3425       * @param string $message - The message to display.
3426       * @return string
3427       */
3428      protected function view_savegrading_result($message) {
3429          $o = '';
3430          $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3431                                                        $this->get_context(),
3432                                                        $this->show_intro(),
3433                                                        $this->get_course_module()->id,
3434                                                        get_string('savegradingresult', 'assign')));
3435          $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
3436                                                     $message,
3437                                                     $this->get_course_module()->id);
3438          $o .= $this->get_renderer()->render($gradingresult);
3439          $o .= $this->view_footer();
3440          return $o;
3441      }
3442      /**
3443       * Display a continue page after quickgrading.
3444       *
3445       * @param string $message - The message to display.
3446       * @return string
3447       */
3448      protected function view_quickgrading_result($message) {
3449          $o = '';
3450          $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3451                                                        $this->get_context(),
3452                                                        $this->show_intro(),
3453                                                        $this->get_course_module()->id,
3454                                                        get_string('quickgradingresult', 'assign')));
3455          $gradingerror = in_array($message, $this->get_error_messages());
3456          $lastpage = optional_param('lastpage', null, PARAM_INT);
3457          $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
3458                                                     $message,
3459                                                     $this->get_course_module()->id,
3460                                                     $gradingerror,
3461                                                     $lastpage);
3462          $o .= $this->get_renderer()->render($gradingresult);
3463          $o .= $this->view_footer();
3464          return $o;
3465      }
3466  
3467      /**
3468       * Display the page footer.
3469       *
3470       * @return string
3471       */
3472      protected function view_footer() {
3473          // When viewing the footer during PHPUNIT tests a set_state error is thrown.
3474          if (!PHPUNIT_TEST) {
3475              return $this->get_renderer()->render_footer();
3476          }
3477  
3478          return '';
3479      }
3480  
3481      /**
3482       * Throw an error if the permissions to view this users' group submission are missing.
3483       *
3484       * @param int $groupid Group id.
3485       * @throws required_capability_exception
3486       */
3487      public function require_view_group_submission($groupid) {
3488          if (!$this->can_view_group_submission($groupid)) {
3489              throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3490          }
3491      }
3492  
3493      /**
3494       * Throw an error if the permissions to view this users submission are missing.
3495       *
3496       * @throws required_capability_exception
3497       * @return none
3498       */
3499      public function require_view_submission($userid) {
3500          if (!$this->can_view_submission($userid)) {
3501              throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3502          }
3503      }
3504  
3505      /**
3506       * Throw an error if the permissions to view grades in this assignment are missing.
3507       *
3508       * @throws required_capability_exception
3509       * @return none
3510       */
3511      public function require_view_grades() {
3512          if (!$this->can_view_grades()) {
3513              throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3514          }
3515      }
3516  
3517      /**
3518       * Does this user have view grade or grade permission for this assignment?
3519       *
3520       * @param mixed $groupid int|null when is set to a value, use this group instead calculating it
3521       * @return bool
3522       */
3523      public function can_view_grades($groupid = null) {
3524          // Permissions check.
3525          if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
3526              return false;
3527          }
3528          // Checks for the edge case when user belongs to no groups and groupmode is sep.
3529          if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) {
3530              if ($groupid === null) {
3531                  $groupid = groups_get_activity_allowed_groups($this->get_course_module());
3532              }
3533              $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context());
3534              $groupflag = $groupflag || !empty($groupid);
3535              return (bool)$groupflag;
3536          }
3537          return true;
3538      }
3539  
3540      /**
3541       * Does this user have grade permission for this assignment?
3542       *
3543       * @param int|stdClass $user The object or id of the user who will do the editing (default to current user).
3544       * @return bool
3545       */
3546      public function can_grade($user = null) {
3547          // Permissions check.
3548          if (!has_capability('mod/assign:grade', $this->context, $user)) {
3549              return false;
3550          }
3551  
3552          return true;
3553      }
3554  
3555      /**
3556       * Download a zip file of all assignment submissions.
3557       *
3558       * @param array $userids Array of user ids to download assignment submissions in a zip file
3559       * @return string - If an error occurs, this will contain the error page.
3560       */
3561      protected function download_submissions($userids = false) {
3562          global $CFG, $DB;
3563  
3564          // More efficient to load this here.
3565          require_once($CFG->libdir.'/filelib.php');
3566  
3567          // Increase the server timeout to handle the creation and sending of large zip files.
3568          core_php_time_limit::raise();
3569  
3570          $this->require_view_grades();
3571  
3572          // Load all users with submit.
3573          $students = get_enrolled_users($this->context, "mod/assign:submit", null, 'u.*', null, null, null,
3574                          $this->show_only_active_users());
3575  
3576          // Build a list of files to zip.
3577          $filesforzipping = array();
3578          $fs = get_file_storage();
3579  
3580          $groupmode = groups_get_activity_groupmode($this->get_course_module());
3581          // All users.
3582          $groupid = 0;
3583          $groupname = '';
3584          if ($groupmode) {
3585              $groupid = groups_get_activity_group($this->get_course_module(), true);
3586              if (!empty($groupid)) {
3587                  $groupname = groups_get_group_name($groupid) . '-';
3588              }
3589          }
3590  
3591          // Construct the zip file name.
3592          $filename = clean_filename($this->get_course()->shortname . '-' .
3593                                     $this->get_instance()->name . '-' .
3594                                     $groupname.$this->get_course_module()->id . '.zip');
3595  
3596          // Get all the files for each student.
3597          foreach ($students as $student) {
3598              $userid = $student->id;
3599              // Download all assigments submission or only selected users.
3600              if ($userids and !in_array($userid, $userids)) {
3601                  continue;
3602              }
3603  
3604              if ((groups_is_member($groupid, $userid) or !$groupmode or !$groupid)) {
3605                  // Get the plugins to add their own files to the zip.
3606  
3607                  $submissiongroup = false;
3608                  $groupname = '';
3609                  if ($this->get_instance()->teamsubmission) {
3610                      $submission = $this->get_group_submission($userid, 0, false);
3611                      $submissiongroup = $this->get_submission_group($userid);
3612                      if ($submissiongroup) {
3613                          $groupname = $submissiongroup->name . '-';
3614                      } else {
3615                          $groupname = get_string('defaultteam', 'assign') . '-';
3616                      }
3617                  } else {
3618                      $submission = $this->get_user_submission($userid, false);
3619                  }
3620  
3621                  if ($this->is_blind_marking()) {
3622                      $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign'));
3623                      $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
3624                  } else {
3625                      $fullname = fullname($student, has_capability('moodle/site:viewfullnames', $this->get_context()));
3626                      $prefix = str_replace('_', ' ', $groupname . $fullname);
3627                      $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
3628                  }
3629  
3630                  if ($submission) {
3631                      $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
3632                      foreach ($this->submissionplugins as $plugin) {
3633                          if ($plugin->is_enabled() && $plugin->is_visible()) {
3634                              if ($downloadasfolders) {
3635                                  // Create a folder for each user for each assignment plugin.
3636                                  // This is the default behavior for version of Moodle >= 3.1.
3637                                  $submission->exportfullpath = true;
3638                                  $pluginfiles = $plugin->get_files($submission, $student);
3639                                  foreach ($pluginfiles as $zipfilepath => $file) {
3640                                      $subtype = $plugin->get_subtype();
3641                                      $type = $plugin->get_type();
3642                                      $zipfilename = basename($zipfilepath);
3643                                      $prefixedfilename = clean_filename($prefix .
3644                                                                         '_' .
3645                                                                         $subtype .
3646                                                                         '_' .
3647                                                                         $type .
3648                                                                         '_');
3649                                      if ($type == 'file') {
3650                                          $pathfilename = $prefixedfilename . $file->get_filepath() . $zipfilename;
3651                                      } else if ($type == 'onlinetext') {
3652                                          $pathfilename = $prefixedfilename . '/' . $zipfilename;
3653                                      } else {
3654                                          $pathfilename = $prefixedfilename . '/' . $zipfilename;
3655                                      }
3656                                      $pathfilename = clean_param($pathfilename, PARAM_PATH);
3657                                      $filesforzipping[$pathfilename] = $file;
3658                                  }
3659                              } else {
3660                                  // Create a single folder for all users of all assignment plugins.
3661                                  // This was the default behavior for version of Moodle < 3.1.
3662                                  $submission->exportfullpath = false;
3663                                  $pluginfiles = $plugin->get_files($submission, $student);
3664                                  foreach ($pluginfiles as $zipfilename => $file) {
3665                                      $subtype = $plugin->get_subtype();
3666                                      $type = $plugin->get_type();
3667                                      $prefixedfilename = clean_filename($prefix .
3668                                                                         '_' .
3669                                                                         $subtype .
3670                                                                         '_' .
3671                                                                         $type .
3672                                                                         '_' .
3673                                                                         $zipfilename);
3674                                      $filesforzipping[$prefixedfilename] = $file;
3675                                  }
3676                              }
3677                          }
3678                      }
3679                  }
3680              }
3681          }
3682          $result = '';
3683          if (count($filesforzipping) == 0) {
3684              $header = new assign_header($this->get_instance(),
3685                                          $this->get_context(),
3686                                          '',
3687                                          $this->get_course_module()->id,
3688                                          get_string('downloadall', 'assign'));
3689              $result .= $this->get_renderer()->render($header);
3690              $result .= $this->get_renderer()->notification(get_string('nosubmission', 'assign'));
3691              $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id,
3692                                                                      'action'=>'grading'));
3693              $result .= $this->get_renderer()->continue_button($url);
3694              $result .= $this->view_footer();
3695  
3696              return $result;
3697          }
3698  
3699          // Log zip as downloaded.
3700          \mod_assign\event\all_submissions_downloaded::create_from_assign($this)->trigger();
3701  
3702          // Close the session.
3703          \core\session\manager::write_close();
3704  
3705          $zipwriter = \core_files\archive_writer::get_stream_writer($filename, \core_files\archive_writer::ZIP_WRITER);
3706  
3707          // Stream the files into the zip.
3708          foreach ($filesforzipping as $pathinzip => $file) {
3709              if ($file instanceof \stored_file) {
3710                  // Most of cases are \stored_file.
3711                  $zipwriter->add_file_from_stored_file($pathinzip, $file);
3712              } else if (is_array($file)) {
3713                  // Save $file as contents, from onlinetext subplugin.
3714                  $content = reset($file);
3715                  $zipwriter->add_file_from_string($pathinzip, $content);
3716              }
3717          }
3718  
3719          // Finish the archive.
3720          $zipwriter->finish();
3721          exit();
3722      }
3723  
3724      /**
3725       * Util function to add a message to the log.
3726       *
3727       * @deprecated since 2.7 - Use new events system instead.
3728       *             (see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins).
3729       *
3730       * @param string $action The current action
3731       * @param string $info A detailed description of the change. But no more than 255 characters.
3732       * @param string $url The url to the assign module instance.
3733       * @param bool $return If true, returns the arguments, else adds to log. The purpose of this is to
3734       *                     retrieve the arguments to use them with the new event system (Event 2).
3735       * @return void|array
3736       */
3737      public function add_to_log($action = '', $info = '', $url='', $return = false) {
3738          global $USER;
3739