PageRenderTime 79ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 3ms

/mod/assign/locallib.php

http://github.com/moodle/moodle
PHP | 9806 lines | 6471 code | 1222 blank | 2113 comment | 1410 complexity | 142664dd9affcbf3e2dbedfeac72cd8b MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  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. * This file contains the definition for the class assignment
  18. *
  19. * This class provides all the functionality for the new assign module.
  20. *
  21. * @package mod_assign
  22. * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. */
  25. defined('MOODLE_INTERNAL') || die();
  26. // Assignment submission statuses.
  27. define('ASSIGN_SUBMISSION_STATUS_NEW', 'new');
  28. define('ASSIGN_SUBMISSION_STATUS_REOPENED', 'reopened');
  29. define('ASSIGN_SUBMISSION_STATUS_DRAFT', 'draft');
  30. define('ASSIGN_SUBMISSION_STATUS_SUBMITTED', 'submitted');
  31. // Search filters for grading page.
  32. define('ASSIGN_FILTER_NONE', 'none');
  33. define('ASSIGN_FILTER_SUBMITTED', 'submitted');
  34. define('ASSIGN_FILTER_NOT_SUBMITTED', 'notsubmitted');
  35. define('ASSIGN_FILTER_SINGLE_USER', 'singleuser');
  36. define('ASSIGN_FILTER_REQUIRE_GRADING', 'requiregrading');
  37. define('ASSIGN_FILTER_GRANTED_EXTENSION', 'grantedextension');
  38. // Marker filter for grading page.
  39. define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
  40. // Reopen attempt methods.
  41. define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
  42. define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
  43. define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
  44. // Special value means allow unlimited attempts.
  45. define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
  46. // Special value means no grade has been set.
  47. define('ASSIGN_GRADE_NOT_SET', -1);
  48. // Grading states.
  49. define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
  50. define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
  51. // Marking workflow states.
  52. define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
  53. define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
  54. define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
  55. define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
  56. define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
  57. define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
  58. /** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
  59. define("ASSIGN_MAX_EVENT_LENGTH", "432000");
  60. // Name of file area for intro attachments.
  61. define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
  62. // Event types.
  63. define('ASSIGN_EVENT_TYPE_DUE', 'due');
  64. define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
  65. define('ASSIGN_EVENT_TYPE_OPEN', 'open');
  66. define('ASSIGN_EVENT_TYPE_CLOSE', 'close');
  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 . '/portfolio/caller.php');
  78. use \mod_assign\output\grading_app;
  79. /**
  80. * Standard base class for mod_assign (assignment types).
  81. *
  82. * @package mod_assign
  83. * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
  84. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  85. */
  86. class assign {
  87. /** @var stdClass the assignment record that contains the global settings for this assign instance */
  88. private $instance;
  89. /** @var array $var array an array containing per-user assignment records, each having calculated properties (e.g. dates) */
  90. private $userinstances = [];
  91. /** @var grade_item the grade_item record for this assign instance's primary grade item. */
  92. private $gradeitem;
  93. /** @var context the context of the course module for this assign instance
  94. * (or just the course if we are creating a new one)
  95. */
  96. private $context;
  97. /** @var stdClass the course this assign instance belongs to */
  98. private $course;
  99. /** @var stdClass the admin config for all assign instances */
  100. private $adminconfig;
  101. /** @var assign_renderer the custom renderer for this module */
  102. private $output;
  103. /** @var cm_info the course module for this assign instance */
  104. private $coursemodule;
  105. /** @var array cache for things like the coursemodule name or the scale menu -
  106. * only lives for a single request.
  107. */
  108. private $cache;
  109. /** @var array list of the installed submission plugins */
  110. private $submissionplugins;
  111. /** @var array list of the installed feedback plugins */
  112. private $feedbackplugins;
  113. /** @var string action to be used to return to this page
  114. * (without repeating any form submissions etc).
  115. */
  116. private $returnaction = 'view';
  117. /** @var array params to be used to return to this page */
  118. private $returnparams = array();
  119. /** @var string modulename prevents excessive calls to get_string */
  120. private static $modulename = null;
  121. /** @var string modulenameplural prevents excessive calls to get_string */
  122. private static $modulenameplural = null;
  123. /** @var array of marking workflow states for the current user */
  124. private $markingworkflowstates = null;
  125. /** @var bool whether to exclude users with inactive enrolment */
  126. private $showonlyactiveenrol = null;
  127. /** @var string A key used to identify userlists created by this object. */
  128. private $useridlistid = null;
  129. /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
  130. private $participants = array();
  131. /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
  132. private $usersubmissiongroups = array();
  133. /** @var array cached list of user groups. The cache key will be the user. */
  134. private $usergroups = array();
  135. /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
  136. private $sharedgroupmembers = array();
  137. /**
  138. * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
  139. * to update the gradebook.
  140. */
  141. private $mostrecentteamsubmission = null;
  142. /** @var array Array of error messages encountered during the execution of assignment related operations. */
  143. private $errors = array();
  144. /**
  145. * Constructor for the base assign class.
  146. *
  147. * Note: For $coursemodule you can supply a stdclass if you like, but it
  148. * will be more efficient to supply a cm_info object.
  149. *
  150. * @param mixed $coursemodulecontext context|null the course module context
  151. * (or the course context if the coursemodule has not been
  152. * created yet).
  153. * @param mixed $coursemodule the current course module if it was already loaded,
  154. * otherwise this class will load one from the context as required.
  155. * @param mixed $course the current course if it was already loaded,
  156. * otherwise this class will load one from the context as required.
  157. */
  158. public function __construct($coursemodulecontext, $coursemodule, $course) {
  159. global $SESSION;
  160. $this->context = $coursemodulecontext;
  161. $this->course = $course;
  162. // Ensure that $this->coursemodule is a cm_info object (or null).
  163. $this->coursemodule = cm_info::create($coursemodule);
  164. // Temporary cache only lives for a single request - used to reduce db lookups.
  165. $this->cache = array();
  166. $this->submissionplugins = $this->load_plugins('assignsubmission');
  167. $this->feedbackplugins = $this->load_plugins('assignfeedback');
  168. // Extra entropy is required for uniqid() to work on cygwin.
  169. $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
  170. if (!isset($SESSION->mod_assign_useridlist)) {
  171. $SESSION->mod_assign_useridlist = [];
  172. }
  173. }
  174. /**
  175. * Set the action and parameters that can be used to return to the current page.
  176. *
  177. * @param string $action The action for the current page
  178. * @param array $params An array of name value pairs which form the parameters
  179. * to return to the current page.
  180. * @return void
  181. */
  182. public function register_return_link($action, $params) {
  183. global $PAGE;
  184. $params['action'] = $action;
  185. $cm = $this->get_course_module();
  186. if ($cm) {
  187. $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
  188. } else {
  189. $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
  190. }
  191. $currenturl->params($params);
  192. $PAGE->set_url($currenturl);
  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. // Web services don't set a URL, we should avoid debugging when ussing the url object.
  202. if (!WS_SERVER) {
  203. $params = $PAGE->url->params();
  204. }
  205. if (!empty($params['action'])) {
  206. return $params['action'];
  207. }
  208. return '';
  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. * Return a list of parameters that can be used to get back to the current page.
  224. *
  225. * @return array params
  226. */
  227. public function get_return_params() {
  228. global $PAGE;
  229. $params = array();
  230. if (!WS_SERVER) {
  231. $params = $PAGE->url->params();
  232. }
  233. unset($params['id']);
  234. unset($params['action']);
  235. return $params;
  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. * Set the context.
  247. *
  248. * @param context $context The new context
  249. */
  250. public function set_context(context $context) {
  251. $this->context = $context;
  252. }
  253. /**
  254. * Set the course data.
  255. *
  256. * @param stdClass $course The course data
  257. */
  258. public function set_course(stdClass $course) {
  259. $this->course = $course;
  260. }
  261. /**
  262. * Set error message.
  263. *
  264. * @param string $message The error message
  265. */
  266. protected function set_error_message(string $message) {
  267. $this->errors[] = $message;
  268. }
  269. /**
  270. * Get error messages.
  271. *
  272. * @return array The array of error messages
  273. */
  274. protected function get_error_messages(): array {
  275. return $this->errors;
  276. }
  277. /**
  278. * Get list of feedback plugins installed.
  279. *
  280. * @return array
  281. */
  282. public function get_feedback_plugins() {
  283. return $this->feedbackplugins;
  284. }
  285. /**
  286. * Get list of submission plugins installed.
  287. *
  288. * @return array
  289. */
  290. public function get_submission_plugins() {
  291. return $this->submissionplugins;
  292. }
  293. /**
  294. * Is blind marking enabled and reveal identities not set yet?
  295. *
  296. * @return bool
  297. */
  298. public function is_blind_marking() {
  299. return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
  300. }
  301. /**
  302. * Is hidden grading enabled?
  303. *
  304. * This just checks the assignment settings. Remember to check
  305. * the user has the 'showhiddengrader' capability too
  306. *
  307. * @return bool
  308. */
  309. public function is_hidden_grader() {
  310. return $this->get_instance()->hidegrader;
  311. }
  312. /**
  313. * Does an assignment have submission(s) or grade(s) already?
  314. *
  315. * @return bool
  316. */
  317. public function has_submissions_or_grades() {
  318. $allgrades = $this->count_grades();
  319. $allsubmissions = $this->count_submissions();
  320. if (($allgrades == 0) && ($allsubmissions == 0)) {
  321. return false;
  322. }
  323. return true;
  324. }
  325. /**
  326. * Get a specific submission plugin by its type.
  327. *
  328. * @param string $subtype assignsubmission | assignfeedback
  329. * @param string $type
  330. * @return mixed assign_plugin|null
  331. */
  332. public function get_plugin_by_type($subtype, $type) {
  333. $shortsubtype = substr($subtype, strlen('assign'));
  334. $name = $shortsubtype . 'plugins';
  335. if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
  336. return null;
  337. }
  338. $pluginlist = $this->$name;
  339. foreach ($pluginlist as $plugin) {
  340. if ($plugin->get_type() == $type) {
  341. return $plugin;
  342. }
  343. }
  344. return null;
  345. }
  346. /**
  347. * Get a feedback plugin by type.
  348. *
  349. * @param string $type - The type of plugin e.g comments
  350. * @return mixed assign_feedback_plugin|null
  351. */
  352. public function get_feedback_plugin_by_type($type) {
  353. return $this->get_plugin_by_type('assignfeedback', $type);
  354. }
  355. /**
  356. * Get a submission plugin by type.
  357. *
  358. * @param string $type - The type of plugin e.g comments
  359. * @return mixed assign_submission_plugin|null
  360. */
  361. public function get_submission_plugin_by_type($type) {
  362. return $this->get_plugin_by_type('assignsubmission', $type);
  363. }
  364. /**
  365. * Load the plugins from the sub folders under subtype.
  366. *
  367. * @param string $subtype - either submission or feedback
  368. * @return array - The sorted list of plugins
  369. */
  370. public function load_plugins($subtype) {
  371. global $CFG;
  372. $result = array();
  373. $names = core_component::get_plugin_list($subtype);
  374. foreach ($names as $name => $path) {
  375. if (file_exists($path . '/locallib.php')) {
  376. require_once($path . '/locallib.php');
  377. $shortsubtype = substr($subtype, strlen('assign'));
  378. $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
  379. $plugin = new $pluginclass($this, $name);
  380. if ($plugin instanceof assign_plugin) {
  381. $idx = $plugin->get_sort_order();
  382. while (array_key_exists($idx, $result)) {
  383. $idx +=1;
  384. }
  385. $result[$idx] = $plugin;
  386. }
  387. }
  388. }
  389. ksort($result);
  390. return $result;
  391. }
  392. /**
  393. * Display the assignment, used by view.php
  394. *
  395. * The assignment is displayed differently depending on your role,
  396. * the settings for the assignment and the status of the assignment.
  397. *
  398. * @param string $action The current action if any.
  399. * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
  400. * @return string - The page output.
  401. */
  402. public function view($action='', $args = array()) {
  403. global $PAGE;
  404. $o = '';
  405. $mform = null;
  406. $notices = array();
  407. $nextpageparams = array();
  408. if (!empty($this->get_course_module()->id)) {
  409. $nextpageparams['id'] = $this->get_course_module()->id;
  410. }
  411. // Handle form submissions first.
  412. if ($action == 'savesubmission') {
  413. $action = 'editsubmission';
  414. if ($this->process_save_submission($mform, $notices)) {
  415. $action = 'redirect';
  416. if ($this->can_grade()) {
  417. $nextpageparams['action'] = 'grading';
  418. } else {
  419. $nextpageparams['action'] = 'view';
  420. }
  421. }
  422. } else if ($action == 'editprevioussubmission') {
  423. $action = 'editsubmission';
  424. if ($this->process_copy_previous_attempt($notices)) {
  425. $action = 'redirect';
  426. $nextpageparams['action'] = 'editsubmission';
  427. }
  428. } else if ($action == 'lock') {
  429. $this->process_lock_submission();
  430. $action = 'redirect';
  431. $nextpageparams['action'] = 'grading';
  432. } else if ($action == 'removesubmission') {
  433. $this->process_remove_submission();
  434. $action = 'redirect';
  435. if ($this->can_grade()) {
  436. $nextpageparams['action'] = 'grading';
  437. } else {
  438. $nextpageparams['action'] = 'view';
  439. }
  440. } else if ($action == 'addattempt') {
  441. $this->process_add_attempt(required_param('userid', PARAM_INT));
  442. $action = 'redirect';
  443. $nextpageparams['action'] = 'grading';
  444. } else if ($action == 'reverttodraft') {
  445. $this->process_revert_to_draft();
  446. $action = 'redirect';
  447. $nextpageparams['action'] = 'grading';
  448. } else if ($action == 'unlock') {
  449. $this->process_unlock_submission();
  450. $action = 'redirect';
  451. $nextpageparams['action'] = 'grading';
  452. } else if ($action == 'setbatchmarkingworkflowstate') {
  453. $this->process_set_batch_marking_workflow_state();
  454. $action = 'redirect';
  455. $nextpageparams['action'] = 'grading';
  456. } else if ($action == 'setbatchmarkingallocation') {
  457. $this->process_set_batch_marking_allocation();
  458. $action = 'redirect';
  459. $nextpageparams['action'] = 'grading';
  460. } else if ($action == 'confirmsubmit') {
  461. $action = 'submit';
  462. if ($this->process_submit_for_grading($mform, $notices)) {
  463. $action = 'redirect';
  464. $nextpageparams['action'] = 'view';
  465. } else if ($notices) {
  466. $action = 'viewsubmitforgradingerror';
  467. }
  468. } else if ($action == 'submitotherforgrading') {
  469. if ($this->process_submit_other_for_grading($mform, $notices)) {
  470. $action = 'redirect';
  471. $nextpageparams['action'] = 'grading';
  472. } else {
  473. $action = 'viewsubmitforgradingerror';
  474. }
  475. } else if ($action == 'gradingbatchoperation') {
  476. $action = $this->process_grading_batch_operation($mform);
  477. if ($action == 'grading') {
  478. $action = 'redirect';
  479. $nextpageparams['action'] = 'grading';
  480. }
  481. } else if ($action == 'submitgrade') {
  482. if (optional_param('saveandshownext', null, PARAM_RAW)) {
  483. // Save and show next.
  484. $action = 'grade';
  485. if ($this->process_save_grade($mform)) {
  486. $action = 'redirect';
  487. $nextpageparams['action'] = 'grade';
  488. $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
  489. $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
  490. }
  491. } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
  492. $action = 'redirect';
  493. $nextpageparams['action'] = 'grade';
  494. $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
  495. $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
  496. } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
  497. $action = 'redirect';
  498. $nextpageparams['action'] = 'grade';
  499. $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
  500. $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
  501. } else if (optional_param('savegrade', null, PARAM_RAW)) {
  502. // Save changes button.
  503. $action = 'grade';
  504. if ($this->process_save_grade($mform)) {
  505. $action = 'redirect';
  506. $nextpageparams['action'] = 'savegradingresult';
  507. }
  508. } else {
  509. // Cancel button.
  510. $action = 'redirect';
  511. $nextpageparams['action'] = 'grading';
  512. }
  513. } else if ($action == 'quickgrade') {
  514. $message = $this->process_save_quick_grades();
  515. $action = 'quickgradingresult';
  516. } else if ($action == 'saveoptions') {
  517. $this->process_save_grading_options();
  518. $action = 'redirect';
  519. $nextpageparams['action'] = 'grading';
  520. } else if ($action == 'saveextension') {
  521. $action = 'grantextension';
  522. if ($this->process_save_extension($mform)) {
  523. $action = 'redirect';
  524. $nextpageparams['action'] = 'grading';
  525. }
  526. } else if ($action == 'revealidentitiesconfirm') {
  527. $this->process_reveal_identities();
  528. $action = 'redirect';
  529. $nextpageparams['action'] = 'grading';
  530. }
  531. $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
  532. 'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
  533. $this->register_return_link($action, $returnparams);
  534. // Include any page action as part of the body tag CSS id.
  535. if (!empty($action)) {
  536. $PAGE->set_pagetype('mod-assign-' . $action);
  537. }
  538. // Now show the right view page.
  539. if ($action == 'redirect') {
  540. $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
  541. $messages = '';
  542. $messagetype = \core\output\notification::NOTIFY_INFO;
  543. $errors = $this->get_error_messages();
  544. if (!empty($errors)) {
  545. $messages = html_writer::alist($errors, ['class' => 'mb-1 mt-1']);
  546. $messagetype = \core\output\notification::NOTIFY_ERROR;
  547. }
  548. redirect($nextpageurl, $messages, null, $messagetype);
  549. return;
  550. } else if ($action == 'savegradingresult') {
  551. $message = get_string('gradingchangessaved', 'assign');
  552. $o .= $this->view_savegrading_result($message);
  553. } else if ($action == 'quickgradingresult') {
  554. $mform = null;
  555. $o .= $this->view_quickgrading_result($message);
  556. } else if ($action == 'gradingpanel') {
  557. $o .= $this->view_single_grading_panel($args);
  558. } else if ($action == 'grade') {
  559. $o .= $this->view_single_grade_page($mform);
  560. } else if ($action == 'viewpluginassignfeedback') {
  561. $o .= $this->view_plugin_content('assignfeedback');
  562. } else if ($action == 'viewpluginassignsubmission') {
  563. $o .= $this->view_plugin_content('assignsubmission');
  564. } else if ($action == 'editsubmission') {
  565. $o .= $this->view_edit_submission_page($mform, $notices);
  566. } else if ($action == 'grader') {
  567. $o .= $this->view_grader();
  568. } else if ($action == 'grading') {
  569. $o .= $this->view_grading_page();
  570. } else if ($action == 'downloadall') {
  571. $o .= $this->download_submissions();
  572. } else if ($action == 'submit') {
  573. $o .= $this->check_submit_for_grading($mform);
  574. } else if ($action == 'grantextension') {
  575. $o .= $this->view_grant_extension($mform);
  576. } else if ($action == 'revealidentities') {
  577. $o .= $this->view_reveal_identities_confirm($mform);
  578. } else if ($action == 'removesubmissionconfirm') {
  579. $o .= $this->view_remove_submission_confirm();
  580. } else if ($action == 'plugingradingbatchoperation') {
  581. $o .= $this->view_plugin_grading_batch_operation($mform);
  582. } else if ($action == 'viewpluginpage') {
  583. $o .= $this->view_plugin_page();
  584. } else if ($action == 'viewcourseindex') {
  585. $o .= $this->view_course_index();
  586. } else if ($action == 'viewbatchsetmarkingworkflowstate') {
  587. $o .= $this->view_batch_set_workflow_state($mform);
  588. } else if ($action == 'viewbatchmarkingallocation') {
  589. $o .= $this->view_batch_markingallocation($mform);
  590. } else if ($action == 'viewsubmitforgradingerror') {
  591. $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
  592. } else if ($action == 'fixrescalednullgrades') {
  593. $o .= $this->view_fix_rescaled_null_grades();
  594. } else {
  595. $o .= $this->view_submission_page();
  596. }
  597. return $o;
  598. }
  599. /**
  600. * Add this instance to the database.
  601. *
  602. * @param stdClass $formdata The data submitted from the form
  603. * @param bool $callplugins This is used to skip the plugin code
  604. * when upgrading an old assignment to a new one (the plugins get called manually)
  605. * @return mixed false if an error occurs or the int id of the new instance
  606. */
  607. public function add_instance(stdClass $formdata, $callplugins) {
  608. global $DB;
  609. $adminconfig = $this->get_admin_config();
  610. $err = '';
  611. // Add the database record.
  612. $update = new stdClass();
  613. $update->name = $formdata->name;
  614. $update->timemodified = time();
  615. $update->timecreated = time();
  616. $update->course = $formdata->course;
  617. $update->courseid = $formdata->course;
  618. $update->intro = $formdata->intro;
  619. $update->introformat = $formdata->introformat;
  620. $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
  621. $update->submissiondrafts = $formdata->submissiondrafts;
  622. $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
  623. $update->sendnotifications = $formdata->sendnotifications;
  624. $update->sendlatenotifications = $formdata->sendlatenotifications;
  625. $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
  626. if (isset($formdata->sendstudentnotifications)) {
  627. $update->sendstudentnotifications = $formdata->sendstudentnotifications;
  628. }
  629. $update->duedate = $formdata->duedate;
  630. $update->cutoffdate = $formdata->cutoffdate;
  631. $update->gradingduedate = $formdata->gradingduedate;
  632. $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
  633. $update->grade = $formdata->grade;
  634. $update->completionsubmit = !empty($formdata->completionsubmit);
  635. $update->teamsubmission = $formdata->teamsubmission;
  636. $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
  637. if (isset($formdata->teamsubmissiongroupingid)) {
  638. $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
  639. }
  640. $update->blindmarking = $formdata->blindmarking;
  641. if (isset($formdata->hidegrader)) {
  642. $update->hidegrader = $formdata->hidegrader;
  643. }
  644. $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
  645. if (!empty($formdata->attemptreopenmethod)) {
  646. $update->attemptreopenmethod = $formdata->attemptreopenmethod;
  647. }
  648. if (!empty($formdata->maxattempts)) {
  649. $update->maxattempts = $formdata->maxattempts;
  650. }
  651. if (isset($formdata->preventsubmissionnotingroup)) {
  652. $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
  653. }
  654. $update->markingworkflow = $formdata->markingworkflow;
  655. $update->markingallocation = $formdata->markingallocation;
  656. if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
  657. $update->markingallocation = 0;
  658. }
  659. $returnid = $DB->insert_record('assign', $update);
  660. $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
  661. // Cache the course record.
  662. $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
  663. $this->save_intro_draft_files($formdata);
  664. if ($callplugins) {
  665. // Call save_settings hook for submission plugins.
  666. foreach ($this->submissionplugins as $plugin) {
  667. if (!$this->update_plugin_instance($plugin, $formdata)) {
  668. print_error($plugin->get_error());
  669. return false;
  670. }
  671. }
  672. foreach ($this->feedbackplugins as $plugin) {
  673. if (!$this->update_plugin_instance($plugin, $formdata)) {
  674. print_error($plugin->get_error());
  675. return false;
  676. }
  677. }
  678. // In the case of upgrades the coursemodule has not been set,
  679. // so we need to wait before calling these two.
  680. $this->update_calendar($formdata->coursemodule);
  681. if (!empty($formdata->completionexpected)) {
  682. \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance,
  683. $formdata->completionexpected);
  684. }
  685. $this->update_gradebook(false, $formdata->coursemodule);
  686. }
  687. $update = new stdClass();
  688. $update->id = $this->get_instance()->id;
  689. $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
  690. $DB->update_record('assign', $update);
  691. return $returnid;
  692. }
  693. /**
  694. * Delete all grades from the gradebook for this assignment.
  695. *
  696. * @return bool
  697. */
  698. protected function delete_grades() {
  699. global $CFG;
  700. $result = grade_update('mod/assign',
  701. $this->get_course()->id,
  702. 'mod',
  703. 'assign',
  704. $this->get_instance()->id,
  705. 0,
  706. null,
  707. array('deleted'=>1));
  708. return $result == GRADE_UPDATE_OK;
  709. }
  710. /**
  711. * Delete this instance from the database.
  712. *
  713. * @return bool false if an error occurs
  714. */
  715. public function delete_instance() {
  716. global $DB;
  717. $result = true;
  718. foreach ($this->submissionplugins as $plugin) {
  719. if (!$plugin->delete_instance()) {
  720. print_error($plugin->get_error());
  721. $result = false;
  722. }
  723. }
  724. foreach ($this->feedbackplugins as $plugin) {
  725. if (!$plugin->delete_instance()) {
  726. print_error($plugin->get_error());
  727. $result = false;
  728. }
  729. }
  730. // Delete files associated with this assignment.
  731. $fs = get_file_storage();
  732. if (! $fs->delete_area_files($this->context->id) ) {
  733. $result = false;
  734. }
  735. $this->delete_all_overrides();
  736. // Delete_records will throw an exception if it fails - so no need for error checking here.
  737. $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
  738. $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
  739. $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
  740. $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
  741. $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
  742. // Delete items from the gradebook.
  743. if (! $this->delete_grades()) {
  744. $result = false;
  745. }
  746. // Delete the instance.
  747. // We must delete the module record after we delete the grade item.
  748. $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
  749. return $result;
  750. }
  751. /**
  752. * Deletes a assign override from the database and clears any corresponding calendar events
  753. *
  754. * @param int $overrideid The id of the override being deleted
  755. * @return bool true on success
  756. */
  757. public function delete_override($overrideid) {
  758. global $CFG, $DB;
  759. require_once($CFG->dirroot . '/calendar/lib.php');
  760. $cm = $this->get_course_module();
  761. if (empty($cm)) {
  762. $instance = $this->get_instance();
  763. $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course);
  764. }
  765. $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
  766. // Delete the events.
  767. $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
  768. if (isset($override->userid)) {
  769. $conds['userid'] = $override->userid;
  770. } else {
  771. $conds['groupid'] = $override->groupid;
  772. }
  773. $events = $DB->get_records('event', $conds);
  774. foreach ($events as $event) {
  775. $eventold = calendar_event::load($event);
  776. $eventold->delete();
  777. }
  778. $DB->delete_records('assign_overrides', array('id' => $overrideid));
  779. // Set the common parameters for one of the events we will be triggering.
  780. $params = array(
  781. 'objectid' => $override->id,
  782. 'context' => context_module::instance($cm->id),
  783. 'other' => array(
  784. 'assignid' => $override->assignid
  785. )
  786. );
  787. // Determine which override deleted event to fire.
  788. if (!empty($override->userid)) {
  789. $params['relateduserid'] = $override->userid;
  790. $event = \mod_assign\event\user_override_deleted::create($params);
  791. } else {
  792. $params['other']['groupid'] = $override->groupid;
  793. $event = \mod_assign\event\group_override_deleted::create($params);
  794. }
  795. // Trigger the override deleted event.
  796. $event->add_record_snapshot('assign_overrides', $override);
  797. $event->trigger();
  798. return true;
  799. }
  800. /**
  801. * Deletes all assign overrides from the database and clears any corresponding calendar events
  802. */
  803. public function delete_all_overrides() {
  804. global $DB;
  805. $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id');
  806. foreach ($overrides as $override) {
  807. $this->delete_override($override->id);
  808. }
  809. }
  810. /**
  811. * Updates the assign properties with override information for a user.
  812. *
  813. * Algorithm: For each assign setting, if there is a matching user-specific override,
  814. * then use that otherwise, if there are group-specific overrides, return the most
  815. * lenient combination of them. If neither applies, leave the assign setting unchanged.
  816. *
  817. * @param int $userid The userid.
  818. */
  819. public function update_effective_access($userid) {
  820. $override = $this->override_exists($userid);
  821. // Merge with assign defaults.
  822. $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
  823. foreach ($keys as $key) {
  824. if (isset($override->{$key})) {
  825. $this->get_instance($userid)->{$key} = $override->{$key};
  826. }
  827. }
  828. }
  829. /**
  830. * Returns whether an assign has any overrides.
  831. *
  832. * @return true if any, false if not
  833. */
  834. public function has_overrides() {
  835. global $DB;
  836. $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
  837. if ($override) {
  838. return true;
  839. }
  840. return false;
  841. }
  842. /**
  843. * Returns user override
  844. *
  845. * Algorithm: For each assign setting, if there is a matching user-specific override,
  846. * then use that otherwise, if there are group-specific overrides, use the one with the
  847. * lowest sort order. If neither applies, leave the assign setting unchanged.
  848. *
  849. * @param int $userid The userid.
  850. * @return stdClass The override
  851. */
  852. public function override_exists($userid) {
  853. global $DB;
  854. // Gets an assoc array containing the keys for defined user overrides only.
  855. $getuseroverride = function($userid) use ($DB) {
  856. $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
  857. return $useroverride ? get_object_vars($useroverride) : [];
  858. };
  859. // Gets an assoc array containing the keys for defined group overrides only.
  860. $getgroupoverride = function($userid) use ($DB) {
  861. $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
  862. if (empty($groupings[0])) {
  863. return [];
  864. }
  865. // Select all overrides that apply to the User's groups.
  866. list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
  867. $sql = "SELECT * FROM {assign_overrides}
  868. WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
  869. $params[] = $this->get_instance()->id;
  870. $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
  871. return $groupoverride ? get_object_vars($groupoverride) : [];
  872. };
  873. // Later arguments clobber earlier ones with array_merge. The two helper functions
  874. // return arrays containing keys for only the defined overrides. So we get the
  875. // desired behaviour as per the algorithm.
  876. return (object)array_merge(
  877. ['duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
  878. $getgroupoverride($userid),
  879. $getuseroverride($userid)
  880. );
  881. }
  882. /**
  883. * Check if the given calendar_event is either a user or group override
  884. * event.
  885. *
  886. * @return bool
  887. */
  888. public function is_override_calendar_event(\calendar_event $event) {
  889. global $DB;
  890. if (!isset($event->modulename)) {
  891. return false;
  892. }
  893. if ($event->modulename != 'assign') {
  894. return false;
  895. }
  896. if (!isset($event->instance)) {
  897. return false;
  898. }
  899. if (!isset($event->userid) && !isset($event->groupid)) {
  900. return false;
  901. }
  902. $overrideparams = [
  903. 'assignid' => $event->instance
  904. ];
  905. if (isset($event->groupid)) {
  906. $overrideparams['groupid'] = $event->groupid;
  907. } else if (isset($event->userid)) {
  908. $overrideparams['userid'] = $event->userid;
  909. }
  910. if ($DB->get_record('assign_overrides', $overrideparams)) {
  911. return true;
  912. } else {
  913. return false;
  914. }
  915. }
  916. /**
  917. * This function calculates the minimum and maximum cutoff values for the timestart of
  918. * the given event.
  919. *
  920. * It will return an array with two values, the first being the minimum cutoff value and
  921. * the second being the maximum cutoff value. Either or both values can be null, which
  922. * indicates there is no minimum or maximum, respectively.
  923. *
  924. * If a cutoff is required then the function must return an array containing the cutoff
  925. * timestamp and error string to display to the user if the cutoff value is violated.
  926. *
  927. * A minimum and maximum cutoff return value will look like:
  928. * [
  929. * [1505704373, 'The due date must be after the sbumission start date'],
  930. * [1506741172, 'The due date must be before the cutoff date']
  931. * ]
  932. *
  933. * If the event does not have a valid timestart range then [false, false] will
  934. * be returned.
  935. *
  936. * @param calendar_event $event The calendar event to get the time range for
  937. * @return array
  938. */
  939. function get_valid_calendar_event_timestart_range(\calendar_event $event) {
  940. $instance = $this->get_instance();
  941. $submissionsfromdate = $instance->allowsubmissionsfromdate;
  942. $cutoffdate = $instance->cutoffdate;
  943. $duedate = $instance->duedate;
  944. $gradingduedate = $instance->gradingduedate;
  945. $mindate = null;
  946. $maxdate = null;
  947. if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
  948. // This check is in here because due date events are currently
  949. // the only events that can be overridden, so we can save a DB
  950. // query if we don't bother checking other events.
  951. if ($this->is_override_calendar_event($event)) {
  952. // This is an override event so there is no valid timestart
  953. // range to set it to.
  954. return [false, false];
  955. }
  956. if ($submissionsfromdate) {
  957. $mindate = [
  958. $submissionsfromdate,
  959. get_string('duedatevalidation', 'assign'),
  960. ];
  961. }
  962. if ($cutoffdate) {
  963. $maxdate = [
  964. $cutoffdate,
  965. get_string('cutoffdatevalidation', 'assign'),
  966. ];
  967. }
  968. if ($gradingduedate) {
  969. // If we don't have a cutoff date or we've got a grading due date
  970. // that is earlier than the cutoff then we should use that as the
  971. // upper limit for the due date.
  972. if (!$cutoffdate || $gradingduedate < $cutoffdate) {
  973. $maxdate = [
  974. $gradingduedate,
  975. get_string('gradingdueduedatevalidation', 'assign'),
  976. ];
  977. }
  978. }
  979. } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
  980. if ($duedate) {
  981. $mindate = [
  982. $duedate,
  983. get_string('gradingdueduedatevalidation', 'assign'),
  984. ];
  985. } else if ($submissionsfromdate) {
  986. $mindate = [
  987. $submissionsfromdate,
  988. get_string('gradingduefromdatevalidation', 'assign'),
  989. ];
  990. }
  991. }
  992. return [$mindate, $maxdate];
  993. }
  994. /**
  995. * Actual implementation of the reset course functionality, delete all the
  996. * assignment submissions for course $data->courseid.
  997. *
  998. * @param stdClass $data the data submitted from the reset course.
  999. * @return array status array
  1000. */
  1001. public function reset_userdata($data) {
  1002. global $CFG, $DB;
  1003. $componentstr = get_string('modulenameplural', 'assign');
  1004. $status = array();
  1005. $fs = get_file_storage();
  1006. if (!empty($data->reset_assign_submissions)) {
  1007. // Delete files associated with this assignment.
  1008. foreach ($this->submissionplugins as $plugin) {
  1009. $fileareas = array();
  1010. $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
  1011. $fileareas = $plugin->get_file_areas();
  1012. foreach ($fileareas as $filearea => $notused) {
  1013. $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
  1014. }
  1015. if (!$plugin->delete_instance()) {
  1016. $status[] = array('component'=>$componentstr,
  1017. 'item'=>get_string('deleteallsubmissions', 'assign'),
  1018. 'error'=>$plugin->get_error());
  1019. }
  1020. }
  1021. foreach ($this->feedbackplugins as $plugin) {
  1022. $fileareas = array();
  1023. $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
  1024. $fileareas = $plugin->get_file_areas();
  1025. foreach ($fileareas as $filearea => $notused) {
  1026. $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
  1027. }
  1028. if (!$plugin->delete_instance()) {
  1029. $status[] = array('component'=>$componentstr,
  1030. 'item'=>get_string('deleteallsubmissions', 'assign'),
  1031. 'error'=>$plugin->get_error());
  1032. }
  1033. }
  1034. $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
  1035. list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
  1036. $DB->delete_records_select('assign_submission', "assignment $sql", $params);
  1037. $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
  1038. $status[] = array('component'=>$componentstr,
  1039. 'item'=>get_string('deleteallsubmissions', 'assign'),
  1040. 'error'=>false);
  1041. if (!empty($data->reset_gradebook_grades)) {
  1042. $DB->delete_records_select('assign_grades', "assignment $sql", $params);
  1043. // Remove all grades from gradebook.
  1044. require_once($CFG->dirroot.'/mod/assign/lib.php');
  1045. assign_reset_gradebook($data->courseid);
  1046. }
  1047. // Reset revealidentities for assign if blindmarking is enabled.
  1048. if ($this->get_instance()->blindmarking) {
  1049. $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
  1050. }
  1051. }
  1052. // Remove user overrides.
  1053. if (!empty($data->reset_assign_user_overrides)) {
  1054. $DB->delete_records_select('assign_overrides',
  1055. 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
  1056. $status[] = array(
  1057. 'component' => $componentstr,
  1058. 'item' => get_string('useroverridesdeleted', 'assign'),
  1059. 'error' => false);
  1060. }
  1061. // Remove group overrides.
  1062. if (!empty($data->reset_assign_group_overrides)) {
  1063. $DB->delete_records_select('assign_overrides',
  1064. 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
  1065. $status[] = array(
  1066. 'component' => $componentstr,
  1067. 'item' => get_string('groupoverridesdeleted', 'assign'),
  1068. 'error' => false);
  1069. }
  1070. // Updating dates - shift may be negative too.
  1071. if ($data->timeshift) {
  1072. $DB->execute("UPDATE {assign_overrides}
  1073. SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
  1074. WHERE assignid = ? AND allowsubmissionsfromdate <> 0",
  1075. array($data->timeshift, $this->get_instance()->id));
  1076. $DB->execute("UPDATE {assign_overrides}
  1077. SET duedate = duedate + ?
  1078. WHERE assignid = ? AND duedate <> 0",
  1079. array($data->timeshift, $this->get_instance()->id));
  1080. $DB->execute("UPDATE {assign_overrides}
  1081. SET cutoffdate = cutoffdate + ?
  1082. WHERE assignid =? AND cutoffdate <> 0",
  1083. array($data->timeshift, $this->get_instance()->id));
  1084. // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
  1085. // See MDL-9367.
  1086. shift_course_mod_dates('assign',
  1087. array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
  1088. $data->timeshift,
  1089. $data->courseid, $this->get_instance()->id);
  1090. $status[] = array('component'=>$componentstr,
  1091. 'item'=>get_string('datechanged'),
  1092. 'error'=>false);
  1093. }
  1094. return $status;
  1095. }
  1096. /**
  1097. * Update the settings for a single plugin.
  1098. *
  1099. * @param assign_plugin $plugin The plugin to update
  1100. * @param stdClass $formdata The form data
  1101. * @return bool false if an error occurs
  1102. */
  1103. protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
  1104. if ($plugin->is_visible()) {
  1105. $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
  1106. if (!empty($formdata->$enabledname)) {
  1107. $plugin->enable();
  1108. if (!$plugin->save_settings($formdata)) {
  1109. print_error($plugin->get_error());
  1110. return false;
  1111. }
  1112. } else {
  1113. $plugin->disable();
  1114. }
  1115. }
  1116. return true;
  1117. }
  1118. /**
  1119. * Update the gradebook information for this assignment.
  1120. *
  1121. * @param bool $reset If true, will reset all grades in the gradbook for this assignment
  1122. * @param int $coursemoduleid This is required because it might not exist in the database yet
  1123. * @return bool
  1124. */
  1125. public function update_gradebook($reset, $coursemoduleid) {
  1126. global $CFG;
  1127. require_once($CFG->dirroot.'/mod/assign/lib.php');
  1128. $assign = clone $this->get_instance();
  1129. $assign->cmidnumber = $coursemodule

Large files files are truncated, but you can click here to view the full file