PageRenderTime 67ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 3ms

/mod/assign/locallib.php

https://bitbucket.org/moodle/moodle
PHP | 9934 lines | 6563 code | 1241 blank | 2130 comment | 1424 complexity | 085690f4073a9a088ce977dc5f77266c MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0

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

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