Search moodle.org's
Developer Documentation

  • 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 37 and 311] [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              if ($submission->latest) {
    3070                  // This is the case when we need to set latest to 0 for all the other attempts.
    3071                  $DB->set_field('assign_submission', 'latest', 0, $params);
    3072              }
    3073              $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
    3074              $sid = $DB->insert_record('assign_submission', $submission);
    3075              return $DB->get_record('assign_submission', array('id' => $sid));
    3076          }
    3077          return false;
    3078      }
    3079  
    3080      /**
    3081       * View a summary listing of all assignments in the current course.
    3082       *
    3083       * @return string
    3084       */
    3085      private function view_course_index() {
    3086          global $USER;
    3087  
    3088          $o = '';
    3089  
    3090          $course = $this->get_course();
    3091          $strplural = get_string('modulenameplural', 'assign');
    3092  
    3093          if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
    3094              $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
    3095              $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
    3096              return $o;
    3097          }
    3098  
    3099          $strsectionname = '';
    3100          $usesections = course_format_uses_sections($course->format);
    3101          $modinfo = get_fast_modinfo($course);
    3102  
    3103          if ($usesections) {
    3104              $strsectionname = get_string('sectionname', 'format_'.$course->format);
    3105              $sections = $modinfo->get_section_info_all();
    3106          }
    3107          $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
    3108  
    3109          $timenow = time();
    3110  
    3111          $currentsection = '';
    3112          foreach ($modinfo->instances['assign'] as $cm) {
    3113              if (!$cm->uservisible) {
    3114                  continue;
    3115              }
    3116  
    3117              $timedue = $cms[$cm->id]->duedate;
    3118  
    3119              $sectionname = '';
    3120              if ($usesections && $cm->sectionnum) {
    3121                  $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
    3122              }
    3123  
    3124              $submitted = '';
    3125              $context = context_module::instance($cm->id);
    3126  
    3127              $assignment = new assign($context, $cm, $course);
    3128  
    3129              // Apply overrides.
    3130              $assignment->update_effective_access($USER->id);
    3131              $timedue = $assignment->get_instance()->duedate;
    3132  
    3133              if (has_capability('mod/assign:grade', $context)) {
    3134                  $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
    3135  
    3136              } else if (has_capability('mod/assign:submit', $context)) {
    3137                  if ($assignment->get_instance()->teamsubmission) {
    3138                      $usersubmission = $assignment->get_group_submission($USER->id, 0, false);
    3139                  } else {
    3140                      $usersubmission = $assignment->get_user_submission($USER->id, false);
    3141                  }
    3142  
    3143                  if (!empty($usersubmission->status)) {
    3144                      $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
    3145                  } else {
    3146                      $submitted = get_string('submissionstatus_', 'assign');
    3147                  }
    3148              }
    3149              $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
    3150              if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
    3151                      !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
    3152                  $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
    3153              } else {
    3154                  $grade = '-';
    3155              }
    3156  
    3157              $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(), $sectionname, $timedue, $submitted, $grade);
    3158  
    3159          }
    3160  
    3161          $o .= $this->get_renderer()->render($courseindexsummary);
    3162          $o .= $this->view_footer();
    3163  
    3164          return $o;
    3165      }
    3166  
    3167      /**
    3168       * View a page rendered by a plugin.
    3169       *
    3170       * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
    3171       *
    3172       * @return string
    3173       */
    3174      protected function view_plugin_page() {
    3175          global $USER;
    3176  
    3177          $o = '';
    3178  
    3179          $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
    3180          $plugintype = required_param('plugin', PARAM_PLUGIN);
    3181          $pluginaction = required_param('pluginaction', PARAM_ALPHA);
    3182  
    3183          $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
    3184          if (!$plugin) {
    3185              print_error('invalidformdata', '');
    3186              return;
    3187          }
    3188  
    3189          $o .= $plugin->view_page($pluginaction);
    3190  
    3191          return $o;
    3192      }
    3193  
    3194  
    3195      /**
    3196       * This is used for team assignments to get the group for the specified user.
    3197       * If the user is a member of multiple or no groups this will return false
    3198       *
    3199       * @param int $userid The id of the user whose submission we want
    3200       * @return mixed The group or false
    3201       */
    3202      public function get_submission_group($userid) {
    3203  
    3204          if (isset($this->usersubmissiongroups[$userid])) {
    3205              return $this->usersubmissiongroups[$userid];
    3206          }
    3207  
    3208          $groups = $this->get_all_groups($userid);
    3209          if (count($groups) != 1) {
    3210              $return = false;
    3211          } else {
    3212              $return = array_pop($groups);
    3213          }
    3214  
    3215          // Cache the user submission group.
    3216          $this->usersubmissiongroups[$userid] = $return;
    3217  
    3218          return $return;
    3219      }
    3220  
    3221      /**
    3222       * Gets all groups the user is a member of.
    3223       *
    3224       * @param int $userid Teh id of the user who's groups we are checking
    3225       * @return array The group objects
    3226       */
    3227      public function get_all_groups($userid) {
    3228          if (isset($this->usergroups[$userid])) {
    3229              return $this->usergroups[$userid];
    3230          }
    3231  
    3232          $grouping = $this->get_instance()->teamsubmissiongroupingid;
    3233          $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping);
    3234  
    3235          $this->usergroups[$userid] = $return;
    3236  
    3237          return $return;
    3238      }
    3239  
    3240  
    3241      /**
    3242       * Display the submission that is used by a plugin.
    3243       *
    3244       * Uses url parameters 'sid', 'gid' and 'plugin'.
    3245       *
    3246       * @param string $pluginsubtype
    3247       * @return string
    3248       */
    3249      protected function view_plugin_content($pluginsubtype) {
    3250          $o = '';
    3251  
    3252          $submissionid = optional_param('sid', 0, PARAM_INT);
    3253          $gradeid = optional_param('gid', 0, PARAM_INT);
    3254          $plugintype = required_param('plugin', PARAM_PLUGIN);
    3255          $item = null;
    3256          if ($pluginsubtype == 'assignsubmission') {
    3257              $plugin = $this->get_submission_plugin_by_type($plugintype);
    3258              if ($submissionid <= 0) {
    3259                  throw new coding_exception('Submission id should not be 0');
    3260              }
    3261              $item = $this->get_submission($submissionid);
    3262  
    3263              // Check permissions.
    3264              if (empty($item->userid)) {
    3265                  // Group submission.
    3266                  $this->require_view_group_submission($item->groupid);
    3267              } else {
    3268                  $this->require_view_submission($item->userid);
    3269              }
    3270              $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
    3271                                                                $this->get_context(),
    3272                                                                $this->show_intro(),
    3273                                                                $this->get_course_module()->id,
    3274                                                                $plugin->get_name()));
    3275              $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
    3276                                                                $item,
    3277                                                                assign_submission_plugin_submission::FULL,
    3278                                                                $this->get_course_module()->id,
    3279                                                                $this->get_return_action(),
    3280                                                                $this->get_return_params()));
    3281  
    3282              // Trigger event for viewing a submission.
    3283              \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
    3284  
    3285          } else {
    3286              $plugin = $this->get_feedback_plugin_by_type($plugintype);
    3287              if ($gradeid <= 0) {
    3288                  throw new coding_exception('Grade id should not be 0');
    3289              }
    3290              $item = $this->get_grade($gradeid);
    3291              // Check permissions.
    3292              $this->require_view_submission($item->userid);
    3293              $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
    3294                                                                $this->get_context(),
    3295                                                                $this->show_intro(),
    3296                                                                $this->get_course_module()->id,
    3297                                                                $plugin->get_name()));
    3298              $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
    3299                                                                $item,
    3300                                                                assign_feedback_plugin_feedback::FULL,
    3301                                                                $this->get_course_module()->id,
    3302                                                                $this->get_return_action(),
    3303                                                                $this->get_return_params()));
    3304  
    3305              // Trigger event for viewing feedback.
    3306              \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
    3307          }
    3308  
    3309          $o .= $this->view_return_links();
    3310  
    3311          $o .= $this->view_footer();
    3312  
    3313          return $o;
    3314      }
    3315  
    3316      /**
    3317       * Rewrite plugin file urls so they resolve correctly in an exported zip.
    3318       *
    3319       * @param string $text - The replacement text
    3320       * @param stdClass $user - The user record
    3321       * @param assign_plugin $plugin - The assignment plugin
    3322       */
    3323      public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
    3324          // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance.
    3325          // Rather, it should be determined by checking the group submission settings of the instance,
    3326          // which is what download_submission() does when generating the file name prefixes.
    3327          $groupname = '';
    3328          if ($this->get_instance()->teamsubmission) {
    3329              $submissiongroup = $this->get_submission_group($user->id);
    3330              if ($submissiongroup) {
    3331                  $groupname = $submissiongroup->name . '-';
    3332              } else {
    3333                  $groupname = get_string('defaultteam', 'assign') . '-';
    3334              }
    3335          }
    3336  
    3337          if ($this->is_blind_marking()) {
    3338              $prefix = $groupname . get_string('participant', 'assign');
    3339              $prefix = str_replace('_', ' ', $prefix);
    3340              $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
    3341          } else {
    3342              $prefix = $groupname . fullname($user);
    3343              $prefix = str_replace('_', ' ', $prefix);
    3344              $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
    3345          }
    3346  
    3347          // Only prefix files if downloadasfolders user preference is NOT set.
    3348          if (!get_user_preferences('assign_downloadasfolders', 1)) {
    3349              $subtype = $plugin->get_subtype();
    3350              $type = $plugin->get_type();
    3351              $prefix = $prefix . $subtype . '_' . $type . '_';
    3352          } else {
    3353              $prefix = "";
    3354          }
    3355          $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
    3356  
    3357          return $result;
    3358      }
    3359  
    3360      /**
    3361       * Render the content in editor that is often used by plugin.
    3362       *
    3363       * @param string $filearea
    3364       * @param int $submissionid
    3365       * @param string $plugintype
    3366       * @param string $editor
    3367       * @param string $component
    3368       * @param bool $shortentext Whether to shorten the text content.
    3369       * @return string
    3370       */
    3371      public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) {
    3372          global $CFG;
    3373  
    3374          $result = '';
    3375  
    3376          $plugin = $this->get_submission_plugin_by_type($plugintype);
    3377  
    3378          $text = $plugin->get_editor_text($editor, $submissionid);
    3379          if ($shortentext) {
    3380              $text = shorten_text($text, 140);
    3381          }
    3382          $format = $plugin->get_editor_format($editor, $submissionid);
    3383  
    3384          $finaltext = file_rewrite_pluginfile_urls($text,
    3385                                                    'pluginfile.php',
    3386                                                    $this->get_context()->id,
    3387                                                    $component,
    3388                                                    $filearea,
    3389                                                    $submissionid);
    3390          $params = array('overflowdiv' => true, 'context' => $this->get_context());
    3391          $result .= format_text($finaltext, $format, $params);
    3392  
    3393          if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
    3394              require_once($CFG->libdir . '/portfoliolib.php');
    3395  
    3396              $button = new portfolio_add_button();
    3397              $portfolioparams = array('cmid' => $this->get_course_module()->id,
    3398                                       'sid' => $submissionid,
    3399                                       'plugin' => $plugintype,
    3400                                       'editor' => $editor,
    3401                                       'area'=>$filearea);
    3402              $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
    3403              $fs = get_file_storage();
    3404  
    3405              if ($files = $fs->get_area_files($this->context->id,
    3406                                               $component,
    3407                                               $filearea,
    3408                                               $submissionid,
    3409                                               'timemodified',
    3410                                               false)) {
    3411                  $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
    3412              } else {
    3413                  $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
    3414              }
    3415              $result .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
    3416          }
    3417          return $result;
    3418      }
    3419  
    3420      /**
    3421       * Display a continue page after grading.
    3422       *
    3423       * @param string $message - The message to display.
    3424       * @return string
    3425       */
    3426      protected function view_savegrading_result($message) {
    3427          $o = '';
    3428          $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
    3429                                                        $this->get_context(),
    3430                                                        $this->show_intro(),
    3431                                                        $this->get_course_module()->id,
    3432                                                        get_string('savegradingresult', 'assign')));
    3433          $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
    3434                                                     $message,
    3435                                                     $this->get_course_module()->id);
    3436          $o .= $this->get_renderer()->render($gradingresult);
    3437          $o .= $this->view_footer();
    3438          return $o;
    3439      }
    3440      /**
    3441       * Display a continue page after quickgrading.
    3442       *
    3443       * @param string $message - The message to display.
    3444       * @return string
    3445       */
    3446      protected function view_quickgrading_result($message) {
    3447          $o = '';
    3448          $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
    3449                                                        $this->get_context(),
    3450                                                        $this->show_intro(),
    3451                                                        $this->get_course_module()->id,
    3452                                                        get_string('quickgradingresult', 'assign')));
    3453          $gradingerror = in_array($message, $this->get_error_messages());
    3454          $lastpage = optional_param('lastpage', null, PARAM_INT);
    3455          $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
    3456                                                     $message,
    3457                                                     $this->get_course_module()->id,
    3458                                                     $gradingerror,
    3459                                                     $lastpage);
    3460          $o .= $this->get_renderer()->render($gradingresult);
    3461          $o .= $this->view_footer();
    3462          return $o;
    3463      }
    3464  
    3465      /**
    3466       * Display the page footer.
    3467       *
    3468       * @return string
    3469       */
    3470      protected function view_footer() {
    3471          // When viewing the footer during PHPUNIT tests a set_state error is thrown.
    3472          if (!PHPUNIT_TEST) {
    3473              return $this->get_renderer()->render_footer();
    3474          }
    3475  
    3476          return '';
    3477      }
    3478  
    3479      /**
    3480       * Throw an error if the permissions to view this users' group submission are missing.
    3481       *
    3482       * @param int $groupid Group id.
    3483       * @throws required_capability_exception
    3484       */
    3485      public function require_view_group_submission($groupid) {
    3486          if (!$this->can_view_group_submission($groupid)) {
    3487              throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
    3488          }
    3489      }
    3490  
    3491      /**
    3492       * Throw an error if the permissions to view this users submission are missing.
    3493       *
    3494       * @throws required_capability_exception
    3495       * @return none
    3496       */
    3497      public function require_view_submission($userid) {
    3498          if (!$this->can_view_submission($userid)) {
    3499              throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
    3500          }
    3501      }
    3502  
    3503      /**
    3504       * Throw an error if the permissions to view grades in this assignment are missing.
    3505       *
    3506       * @throws required_capability_exception
    3507       * @return none
    3508       */
    3509      public function require_view_grades() {
    3510          if (!$this->can_view_grades()) {
    3511              throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
    3512          }
    3513      }
    3514  
    3515      /**
    3516       * Does this user have view grade or grade permission for this assignment?
    3517       *
    3518       * @param mixed $groupid int|null when is set to a value, use this group instead calculating it
    3519       * @return bool
    3520       */
    3521      public function can_view_grades($groupid = null) {
    3522          // Permissions check.
    3523          if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
    3524              return false;
    3525          }
    3526          // Checks for the edge case when user belongs to no groups and groupmode is sep.
    3527          if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) {
    3528              if ($groupid === null) {
    3529                  $groupid = groups_get_activity_allowed_groups($this->get_course_module());
    3530              }
    3531              $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context());
    3532              $groupflag = $groupflag || !empty($groupid);
    3533              return (bool)$groupflag;
    3534          }
    3535          return true;
    3536      }
    3537  
    3538      /**
    3539       * Does this user have grade permission for this assignment?
    3540       *
    3541       * @param int|stdClass $user The object or id of the user who will do the editing (default to current user).
    3542       * @return bool
    3543       */
    3544      public function can_grade($user = null) {
    3545          // Permissions check.
    3546          if (!has_capability('mod/assign:grade', $this->context, $user)) {
    3547              return false;
    3548          }
    3549  
    3550          return true;
    3551      }
    3552  
    3553      /**
    3554       * Download a zip file of all assignment submissions.
    3555       *
    3556       * @param array $userids Array of user ids to download assignment submissions in a zip file
    3557       * @return string - If an error occurs, this will contain the error page.
    3558       */
    3559      protected function download_submissions($userids = false) {
    3560          global $CFG, $DB;
    3561  
    3562          // More efficient to load this here.
    3563          require_once($CFG->libdir.'/filelib.php');
    3564  
    3565          // Increase the server timeout to handle the creation and sending of large zip files.
    3566          core_php_time_limit::raise();
    3567  
    3568          $this->require_view_grades();
    3569  
    3570          // Load all users with submit.
    3571          $students = get_enrolled_users($this->context, "mod/assign:submit", null, 'u.*', null, null, null,
    3572                          $this->show_only_active_users());
    3573  
    3574          // Build a list of files to zip.
    3575          $filesforzipping = array();
    3576          $fs = get_file_storage();
    3577  
    3578          $groupmode = groups_get_activity_groupmode($this->get_course_module());
    3579          // All users.
    3580          $groupid = 0;
    3581          $groupname = '';
    3582          if ($groupmode) {
    3583              $groupid = groups_get_activity_group($this->get_course_module(), true);
    3584              if (!empty($groupid)) {
    3585                  $groupname = groups_get_group_name($groupid) . '-';
    3586              }
    3587          }
    3588  
    3589          // Construct the zip file name.
    3590          $filename = clean_filename($this->get_course()->shortname . '-' .
    3591                                     $this->get_instance()->name . '-' .
    3592                                     $groupname.$this->get_course_module()->id . '.zip');
    3593  
    3594          // Get all the files for each student.
    3595          foreach ($students as $student) {
    3596              $userid = $student->id;
    3597              // Download all assigments submission or only selected users.
    3598              if ($userids and !in_array($userid, $userids)) {
    3599                  continue;
    3600              }
    3601  
    3602              if ((groups_is_member($groupid, $userid) or !$groupmode or !$groupid)) {
    3603                  // Get the plugins to add their own files to the zip.
    3604  
    3605                  $submissiongroup = false;
    3606                  $groupname = '';
    3607                  if ($this->get_instance()->teamsubmission) {
    3608                      $submission = $this->get_group_submission($userid, 0, false);
    3609                      $submissiongroup = $this->get_submission_group($userid);
    3610                      if ($submissiongroup) {
    3611                          $groupname = $submissiongroup->name . '-';
    3612                      } else {
    3613                          $groupname = get_string('defaultteam', 'assign') . '-';
    3614                      }
    3615                  } else {
    3616                      $submission = $this->get_user_submission($userid, false);
    3617                  }
    3618  
    3619                  if ($this->is_blind_marking()) {
    3620                      $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign'));
    3621                      $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
    3622                  } else {
    3623                      $fullname = fullname($student, has_capability('moodle/site:viewfullnames', $this->get_context()));
    3624                      $prefix = str_replace('_', ' ', $groupname . $fullname);
    3625                      $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
    3626                  }
    3627  
    3628                  if ($submission) {
    3629                      $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
    3630                      foreach ($this->submissionplugins as $plugin) {
    3631                          if ($plugin->is_enabled() && $plugin->is_visible()) {
    3632                              if ($downloadasfolders) {
    3633                                  // Create a folder for each user for each assignment plugin.
    3634                                  // This is the default behavior for version of Moodle >= 3.1.
    3635                                  $submission->exportfullpath = true;
    3636                                  $pluginfiles = $plugin->get_files($submission, $student);
    3637                                  foreach ($pluginfiles as $zipfilepath => $file) {
    3638                                      $subtype = $plugin->get_subtype();
    3639                                      $type = $plugin->get_type();
    3640                                      $zipfilename = basename($zipfilepath);
    3641                                      $prefixedfilename = clean_filename($prefix .
    3642                                                                         '_' .
    3643                                                                         $subtype .
    3644                                                                         '_' .
    3645                                                                         $type .
    3646                                                                         '_');
    3647                                      if ($type == 'file') {
    3648                                          $pathfilename = $prefixedfilename . $file->get_filepath() . $zipfilename;
    3649                                      } else if ($type == 'onlinetext') {
    3650                                          $pathfilename = $prefixedfilename . '/' . $zipfilename;
    3651                                      } else {
    3652                                          $pathfilename = $prefixedfilename . '/' . $zipfilename;
    3653                                      }
    3654                                      $pathfilename = clean_param($pathfilename, PARAM_PATH);
    3655                                      $filesforzipping[$pathfilename] = $file;
    3656                                  }
    3657                              } else {
    3658                                  // Create a single folder for all users of all assignment plugins.
    3659                                  // This was the default behavior for version of Moodle < 3.1.
    3660                                  $submission->exportfullpath = false;
    3661                                  $pluginfiles = $plugin->get_files($submission, $student);
    3662                                  foreach ($pluginfiles as $zipfilename => $file) {
    3663                                      $subtype = $plugin->get_subtype();
    3664                                      $type = $plugin->get_type();
    3665                                      $prefixedfilename = clean_filename($prefix .
    3666                                                                         '_' .
    3667                                                                         $subtype .
    3668                                                                         '_' .
    3669                                                                         $type .
    3670                                                                         '_' .
    3671                                                                         $zipfilename);
    3672                                      $filesforzipping[$prefixedfilename] = $file;
    3673                                  }
    3674                              }
    3675                          }
    3676                      }
    3677                  }
    3678              }
    3679          }
    3680          $result = '';
    3681          if (count($filesforzipping) == 0) {
    3682              $header = new assign_header($this->get_instance(),
    3683                                          $this->get_context(),
    3684                                          '',
    3685                                          $this->get_course_module()->id,
    3686                                          get_string('downloadall', 'assign'));
    3687              $result .= $this->get_renderer()->render($header);
    3688              $result .= $this->get_renderer()->notification(get_string('nosubmission', 'assign'));
    3689              $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id,
    3690                                                                      'action'=>'grading'));
    3691              $result .= $this->get_renderer()->continue_button($url);
    3692              $result .= $this->view_footer();
    3693  
    3694              return $result;
    3695          }
    3696  
    3697          // Log zip as downloaded.
    3698          \mod_assign\event\all_submissions_downloaded::create_from_assign($this)->trigger();
    3699  
    3700          // Close the session.
    3701          \core\session\manager::write_close();
    3702  
    3703          $zipwriter = \core_files\archive_writer::get_stream_writer($filename, \core_files\archive_writer::ZIP_WRITER);
    3704  
    3705          // Stream the files into the zip.
    3706          foreach ($filesforzipping as $pathinzip => $file) {
    3707              if ($file instanceof \stored_file) {
    3708                  // Most of cases are \stored_file.
    3709                  $zipwriter->add_file_from_stored_file($pathinzip, $file);
    3710              } else if (is_array($file)) {
    3711                  // Save $file as contents, from onlinetext subplugin.
    3712                  $content = reset($file);
    3713                  $zipwriter->add_file_from_string($pathinzip, $content);
    3714              }
    3715          }
    3716  
    3717          // Finish the archive.
    3718          $zipwriter->finish();
    3719          exit();
    3720      }
    3721  
    3722      /**
    3723       * Util function to add a message to the log.
    3724       *
    3725       * @deprecated since 2.7 - Use new events system instead.
    3726       *             (see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins).
    3727       *
    3728       * @param string $action The current action
    3729       * @param string $info A detailed description of the change. But no more than 255 characters.
    3730       * @param string $url The url to the assign module instance.
    3731       * @param bool $return If true, returns the arguments, else adds to log. The purpose of this is to
    3732       *                     retrieve the arguments to use them with the new event system (Event 2).
    3733       * @return void|array
    3734       */
    3735      public function add_to_log($action = '', $info = '', $url='', $return = false) {
    3736          global $USER;
    3737  
    3738          $fullurl = 'view.php?id=' . $this->get_course_module()->id;
    3739          if (