Search moodle.org's
Developer Documentation


  • Bug fixes for general core bugs in 2.8.x ended 9 November 2015 (12 months).
  • Bug fixes for security issues in 2.8.x ended 9 May 2016 (18 months).
  • minimum PHP 5.4.4 (always use latest PHP 5.4.x or 5.5.x on Windows - http://windows.php.net/download/), PHP 7 is NOT supported
  • Differences Between: [Versions 28 and 29] [Versions 28 and 30] [Versions 28 and 31] [Versions 28 and 32] [Versions 28 and 33] [Versions 28 and 34] [Versions 28 and 35] [Versions 28 and 36] [Versions 28 and 37]

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